Drupal API — Render Arrays, URL, Database and Entity API
In this tutorial, you'll learn the essential Drupal APIs for module development: render arrays for building HTML output, the URL class for path generation, the database abstraction layer for queries, the entity API for loading and saving content, and the service container for dependency injection.
What You'll Learn
- Render arrays: #type, #markup, #theme, #attached, #cache
- Common render elements: html_tag, link, table, item_list, dropbutton, container, status_messages
- The URL class: Url::fromRoute, fromUri, fromUserInput, toString, toRenderArray
- Database abstraction: db_select, db_insert, db_update, db_delete, db_query
- Dynamic queries: select, fields, condition, range, orderBy, execute
- Entity API: entityTypeManager, load, loadMultiple, create, save, delete
- Entity queries: entityQuery, condition, sort, range, pager, execute
- Service container: \Drupal::service(), ::state(), ::config(), ::currentUser()
- State API for temporary storage
- Config API for configuration storage
- Private and shared tempstore
Why It Matters
Drupal's APIs abstract away the complexity of database operations, URL generation, and HTML rendering. Without them, you would write raw SQL, hardcode URLs, and mix HTML in PHP strings. The APIs ensure security (SQL Injection prevention, XSS filtering), consistency (cache tags, bubbleable metadata), and portability (upgrading Drupal does not break your code). Mastering these APIs is essential for writing production-quality Drupal modules.
Real-World Use
A developer building a custom dashboard needs to display a table of recent content with links to edit each item. Using the Entity API, they load the nodes. Using the render array system, they build a table with proper cache tags. Using the URL class, they generate edit links without hardcoding paths. The result is a page that is cacheable, secure, and survives Drupal core updates. The database abstraction layer ensures the module works on MySQL, PostgreSQL, and SQLite without changes.
Learning Path
flowchart LR A[Hooks] --> B[Drupal API] B --> C[Render Arrays] C --> D[URL Class] D --> E[Database API] E --> F[Entity API] F --> G[Service Container] G --> H[Drush and Config Management]
Render Arrays
Render arrays are the standard way to build HTML output in Drupal. They are nested arrays that describe what to render, not raw HTML.
Basic Markup
<?php
// Simple text output
$build = [
'#markup' => '<p>Hello, world!</p>',
];
// Multiple items
$build = [
'greeting' => [
'#markup' => '<h1>Welcome</h1>',
],
'content' => [
'#markup' => '<p>This is the dashboard page.</p>',
],
'#prefix' => '<div class="dashboard">',
'#suffix' => '</div>',
];
Common Render Elements
<?php
// Link
$build['link'] = [
'#type' => 'link',
'#title' => 'Edit article',
'#url' => Url::fromRoute('node.edit', ['node' => 123]),
];
// Item list (ul/ol)
$build['list'] = [
'#type' => 'item_list',
'#items' => ['Task 1', 'Task 2', 'Task 3'],
'#list_type' => 'ul',
];
// Table
$build['table'] = [
'#type' => 'table',
'#header' => ['Title', 'Status', 'Author'],
'#rows' => [
['My Article', 'Published', 'Admin'],
['Another Post', 'Draft', 'Editor'],
],
'#empty' => t('No content found.'),
];
// Dropbutton
$build['actions'] = [
'#type' => 'dropbutton',
'#links' => [
'edit' => [
'title' => 'Edit',
'url' => Url::fromRoute('node.edit', ['node' => 123]),
],
'delete' => [
'title' => 'Delete',
'url' => Url::fromRoute('node.delete', ['node' => 123]),
],
],
];
// Container (wrapper div)
$build['wrapper'] = [
'#type' => 'container',
'#attributes' => ['class' => ['highlighted']],
'child' => [
'#markup' => '<p>Inside a container.</p>',
],
];
// Status messages
\Drupal::messenger()->addMessage('Content saved successfully.');
\Drupal::messenger()->addWarning('Check your configuration.');
\Drupal::messenger()->addError('An error occurred.');
Attaching Assets
<?php
$build['page'] = [
'#markup' => '<div id="dashboard"></div>',
'#attached' => [
'library' => [
'my_module/dashboard',
],
'drupalSettings' => [
'myModule' => [
'endpoint' => '/api/dashboard',
],
],
],
];
Cache Metadata
<?php
$build['content'] = [
'#theme' => 'node',
'#node' => $node,
'#cache' => [
'tags' => ['node:' . $node->id()],
'contexts' => ['user'],
'max-age' => 3600,
],
];
URL Class
Use the Drupal URL class to generate paths safely:
<?php
use Drupal\Core\Url;
// From a route name (preferred)
$url = Url::fromRoute('entity.node.canonical', ['node' => 123]);
$link = $url->toString(); // /node/123
// From an external URI
$url = Url::fromUri('https://example.com');
$link = $url->toString();
// From an internal URI
$url = Url::fromUri('internal:/node/123');
$link = $url->toString();
// From user input (sanitized)
$url = Url::fromUserInput('/node/123');
$link = $url->toString();
// With options
$url = Url::fromRoute('entity.node.canonical', ['node' => 123], [
'absolute' => true,
'fragment' => 'comments',
'query' => ['page' => 2],
]);
$link = $url->toString(); // https://example.com/node/123?page=2#comments
// Render as a link
$build = [
'#type' => 'link',
'#title' => 'Read more',
'#url' => Url::fromRoute('entity.node.canonical', ['node' => 123]),
];
// Access checking
$url = Url::fromRoute('entity.node.edit_form', ['node' => 123]);
if ($url->access()) {
// Current user can access this route
}
Database Abstraction Layer
Drupal uses a database abstraction layer that works with MySQL, PostgreSQL, SQLite, and others:
<?php
use Drupal\Core\Database\Database;
// Get the database connection
$connection = Database::getConnection();
// Or: $connection = \Drupal::database();
Select Queries
<?php
// Simple select
$query = \Drupal::database()->select('node_field_data', 'n');
$query->fields('n', ['nid', 'title', 'created']);
$query->condition('n.status', 1);
$query->range(0, 10);
$query->orderBy('n.created', 'DESC');
$result = $query->execute()->fetchAll();
foreach ($result as $row) {
// $row->nid, $row->title, $row->created
}
// Select with join
$query = \Drupal::database()->select('node_field_data', 'n');
$query->fields('n', ['nid', 'title']);
$query->join('users_field_data', 'u', 'n.uid = u.uid');
$query->addField('u', 'name', 'author');
$query->condition('n.type', 'article');
$query->condition('n.status', 1);
$result = $query->execute()->fetchAll();
// Count query
$count_query = \Drupal::database()->select('node_field_data', 'n');
$count_query->addExpression('COUNT(*)', 'count');
$count_query->condition('n.status', 1);
$total = $count_query->execute()->fetchField();
Insert Queries
<?php
// Insert a record
\Drupal::database()->insert('my_table')
->fields([
'title' => 'New Task',
'status' => 'pending',
'user_id' => 1,
'created' => \Drupal::time()->getRequestTime(),
])
->execute();
// Insert multiple records
$records = [
['title' => 'Task 1', 'status' => 'pending', 'user_id' => 1],
['title' => 'Task 2', 'status' => 'done', 'user_id' => 1],
];
$query = \Drupal::database()->insert('my_table')
->fields(['title', 'status', 'user_id']);
foreach ($records as $record) {
$query->values($record);
}
$query->execute();
Update and Delete Queries
<?php
// Update records
\Drupal::database()->update('my_table')
->fields(['status' => 'completed'])
->condition('id', 123)
->execute();
// Delete records
\Drupal::database()->delete('my_table')
->condition('status', 'archived')
->condition('created', strtotime('-1 year'), '<')
->execute();
Entity API
The Entity API is the recommended way to work with content, users, taxonomy, and other entities:
<?php
use Drupal\node\Entity\Node;
use Drupal\user\Entity\User;
// Loading a single node
$node = Node::load(123);
$title = $node->getTitle();
$created = $node->getCreatedTime();
$author = $node->getOwner()->getDisplayName();
// Loading multiple nodes
$nodes = Node::loadMultiple([123, 124, 125]);
// Using entityTypeManager
$storage = \Drupal::entityTypeManager()->getStorage('node');
$node = $storage->load(123);
$nodes = $storage->loadMultiple([123, 124]);
$nodes = $storage->loadByProperties(['type' => 'article', 'status' => 1]);
// Creating a new node
$node = Node::create([
'type' => 'article',
'title' => 'My New Article',
'body' => 'This is the article body.',
'status' => 1,
'uid' => 1,
]);
$node->save();
// Updating a node
$node = Node::load(123);
$node->setTitle('Updated Title');
$node->set('field_custom', 'New value');
$node->save();
// Deleting a node
$node = Node::load(123);
$node->delete();
Entity Queries
For complex queries, use entity queries instead of direct database queries:
<?php
use Drupal\Core\Entity\EntityInterface;
// Basic entity query
$query = \Drupal::entityQuery('node')
->condition('type', 'article')
->condition('status', 1)
->condition('title', 'security', 'CONTAINS')
->sort('created', 'DESC')
->range(0, 10)
->accessCheck(TRUE);
$nids = $query->execute();
// Load the results
$nodes = Node::loadMultiple($nids);
// Query with fields
$query = \Drupal::entityQuery('node')
->condition('type', 'article')
->condition('field_category', 5)
->condition('field_tags', [1, 2, 3], 'IN')
->sort('title', 'ASC')
->pager(20);
$nids = $query->execute();
// Count query
$count_query = \Drupal::entityQuery('node')
->condition('type', 'article')
->condition('status', 1)
->accessCheck(FALSE);
$total = $count_query->count()->execute();
// Entity query for users
$uids = \Drupal::entityQuery('user')
->condition('status', 1)
->condition('roles', 'content_editor')
->sort('created', 'DESC')
->execute();
Service Container
Drupal uses dependency injection through the service container:
<?php
// Accessing services statically (for quick scripts)
$service = \Drupal::service('my_module.api_client');
$config = \Drupal::config('system.site');
$state = \Drupal::state();
$current_user = \Drupal::currentUser();
$entity_type_manager = \Drupal::entityTypeManager();
$messenger = \Drupal::messenger();
$logger = \Drupal::logger('my_module');
// Proper dependency injection in a controller
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
class DashboardController extends ControllerBase {
protected $apiClient;
public function __construct($api_client) {
$this->apiClient = $api_client;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('my_module.api_client')
);
}
public function page() {
$data = $this->apiClient->fetchData();
return [
'#theme' => 'dashboard',
'#data' => $data,
];
}
}
State API
The State API stores temporary, non-configuration data:
<?php
$state = \Drupal::state();
// Store a value
$state->set('my_module.last_cron_run', time());
$state->set('my_module.api_threshold', 1000);
// Retrieve a value
$last_run = $state->get('my_module.last_cron_run');
$threshold = $state->get('my_module.api_threshold', 500);
// Check if a key exists
if ($state->get('my_module.setup_completed')) {
// Setup has been done
}
// Delete a key
$state->delete('my_module.temp_data');
// Get multiple keys at once
$values = $state->getMultiple([
'my_module.last_cron_run',
'my_module.api_threshold',
]);
Config API
The Config API stores configuration data:
<?php
// Reading configuration
$config = \Drupal::config('system.site');
$site_name = $config->get('name');
$site_slogan = $config->get('slogan');
// Reading module configuration
$config = \Drupal::config('my_module.settings');
$api_endpoint = $config->get('api_endpoint');
$sync_interval = $config->get('sync_interval');
// Writing configuration (use getEditable)
\Drupal::configFactory()
->getEditable('my_module.settings')
->set('api_endpoint', 'https://new-api.example.com')
->set('sync_interval', 120)
->save();
Tempstore
Tempstore provides temporary storage for user sessions:
<?php
// Private tempstore (per user)
$tempstore = \Drupal::service('tempstore.private')
->get('my_module');
$tempstore->set('wizard_step', 3);
$step = $tempstore->get('wizard_step');
$tempstore->delete('wizard_step');
// Shared tempstore (across users)
$shared_tempstore = \Drupal::service('tempstore.shared')
->get('my_module');
$shared_tempstore->set('current_batch', 42);
Common Mistakes
Using raw SQL instead of entity queries: Entity queries are portable, cacheable, and respect access control. Raw SQL bypasses all of this. Use entity queries for entity data and the database abstraction layer only for custom non-entity tables.
Returning HTML strings instead of render arrays: Render arrays allow Drupal to apply cache metadata, bubble contexts, and attach assets. Returning
<p>Hello</p>directly breaks Caching and asset management.Hardcoding URLs: Use
Url::fromRoute()instead of hardcoding/node/123. Routes can change between versions, but route names stay the same.Forgetting accessCheck() on entity queries: Drupal 10+ requires explicit
accessCheck(TRUE)oraccessCheck(FALSE). Omitting it throws an exception.Not using dependency injection: Static calls to
\Drupal::service()work but make testing impossible. Inject dependencies via the constructor for testable, maintainable code.
Practice Questions
- How would you build a render array for a table of the last 10 published articles with columns for title, author, and date?
- What is the difference between
\Drupal::state()->set()and\Drupal::configFactory()->getEditable()->set()? - Why must you use
accessCheck()on entity queries, and what happens if you omit it? - Challenge: Build a custom dashboard page that: renders a table of the 5 most recent nodes (any type) using render arrays, generates proper edit links using
Url::fromRoute(), includes a counter of total published nodes using an entity query, attaches a custom CSS library, sets proper cache tags for each node displayed, and uses dependency injection for the entity type manager.
FAQ
Mini Project
Goal: Build a custom module that uses multiple Drupal APIs.
- Create a module called
content_dashboardwith a controller at/dashboard - Build a render array with an HTML title, a table of recent content, and a dropbutton with actions
- Use the Entity API to load the 10 most recently updated nodes
- Use the URL class to generate edit and delete links for each node
- Use the Config API to read the site name and display it in the page header
- Use the State API to track how many times the dashboard has been viewed
- Attach a custom CSS library to style the dashboard
- Set cache tags on the render array so it invalidates when any node changes
- Inject dependencies via create() instead of using static calls
What's Next
Now that you master Drupal's APIs, proceed to Drush commands to manage Drupal from the command line. Then explore configuration management to learn how to deploy configuration across environments.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro