Entity queries
An entity query finds nodes by conditions: type, status, a field value. It returns only the numbers of the entities it found, and the storage then loads the entities themselves. With its help the /blog page finally shows real articles.
A simple query
The query is created by the storage from the previous lesson:
// web/modules/custom/blog/src/Controller/BlogController.php
$storage = $this->entityTypeManager()->getStorage('node');
$ids = $storage->getQuery()
->accessCheck(TRUE)
->condition('type', 'article')
->execute();
$nodes = $storage->loadMultiple($ids);condition('type', 'article') keeps only articles, execute() runs the query and returns an array of numbers. The accessCheck() call is required: TRUE means "only what the current user is allowed to see".
Conditions, sorting, limiting
// web/modules/custom/blog/src/Controller/BlogController.php
$ids = $storage->getQuery()
->accessCheck(TRUE)
->condition('type', 'article')
->condition('status', 1)
->condition('created', strtotime('-30 days'), '>=')
->sort('created', 'DESC')
->range(0, 10)
->execute();The query returns at most ten published articles from the last 30 days, newest first. The third argument of condition() is the comparison operator; without it equality is checked. range(0, 10) is the offset and the count.
| Entry | Meaning |
|---|---|
condition('status', 1) | Equals |
condition('created', $time, '>=') | Comparison: >, >=, <, <=, <> |
condition('title', '%drupal%', 'LIKE') | Contains a substring |
condition('type', ['article', 'page'], 'IN') | One of the values |
condition('field_tags.entity.name', 'PHP') | By a field of a referenced entity |
exists('field_image') | The field is filled in |
count() | Return the count instead of the numbers |
The list of articles on the page
Let us put it all together in the list() method:
// web/modules/custom/blog/src/Controller/BlogController.php
public function list(): array
{
$storage = $this->entityTypeManager()->getStorage('node');
$ids = $storage->getQuery()
->accessCheck(TRUE)
->condition('type', 'article')
->condition('status', 1)
->sort('created', 'DESC')
->range(0, $this->config('blog.settings')->get('items_per_page'))
->execute();
$nodes = $storage->loadMultiple($ids);
return $this->entityTypeManager()
->getViewBuilder('node')
->viewMultiple($nodes, 'teaser');
}The view builder getViewBuilder('node') turns nodes into a render array. The second argument is the view mode: teaser is the short view, full the complete one. The /blog page shows the latest articles the way the Manage display tab of the content type is set up, using the node--article.html.twig template from the theme.
Creating
// anywhere, for example in a Drush command
$node = \Drupal::entityTypeManager()->getStorage('node')->create([
'type' => 'article',
'title' => 'New article',
'body' => ['value' => 'The text of the article.', 'format' => 'basic_html'],
'uid' => 1,
'status' => 1,
]);
$node->save();create() only creates the object; save() writes it to the database. type and title are required; uid is the author number, status says whether the node is published. After saving, $node->id() returns the assigned number.
Deleting
// anywhere, for example in a Drush command
$node->delete();
$storage->delete($storage->loadMultiple($ids));The first line deletes one node, the second several at once. Deletion cannot be undone.
Common mistake
A query without accessCheck() ends with an exception: Drupal requires an explicit decision on whether to respect access. accessCheck(TRUE) is needed for everything shown to a visitor. FALSE is acceptable only in internal tasks — cron, Drush commands, updates — where there is no user. Mixing them up makes it easy to show unpublished nodes to an anonymous visitor.