Create, load, and query entities
Almost everything in Drupal is an entity: nodes, users, taxonomy terms, media, files, your own types. This page covers the daily verbs: create an entity in code, load it, read and change its fields, delete it, and find entities by their field values with entity queries.
Code that manipulates content through the Entity API so that hooks fire, caches invalidate, and revisions and translations behave, regardless of entity type. If you are defining a new kind of data rather than working with existing kinds, start at Define a custom entity type. And if you find yourself writing SQL against node__* tables, stop: the storage layout is an implementation detail, and writing to it directly bypasses every guarantee this page relies on.
Prerequisites
Section titled “Prerequisites”- Entities, bundles, and fields for the data model these operations run against.
- A class that can receive services, since everything below hangs off the
entity type managerservice; see Services and dependency injection.
-
Get the storage handler for the entity type. All CRUD goes through storage handlers obtained from
\Drupal\Core\Entity\EntityTypeManagerInterface:// Fragment: $this->entityTypeManager is injected.$storage = $this->entityTypeManager->getStorage('node');The string is the entity type ID:
node,user,taxonomy_term,media, or your own. Everything below is identical across types; only the field names differ per type and bundle. -
Create, load, update, and delete. The four operations in one injected service class:
web/modules/custom/docs_snippets/src/ArticleManager.php <?phpdeclare(strict_types=1);namespace Drupal\docs_snippets;use Drupal\Core\Entity\EntityTypeManagerInterface;use Drupal\node\NodeInterface;/*** Creates, reads, updates and deletes article nodes.** All content entities share this API: swap 'node' for 'taxonomy_term',* 'media' or your own entity type and the same patterns apply.*/class ArticleManager {/*** Constructs the manager.** @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager* The entity type manager.*/public function __construct(protected readonly EntityTypeManagerInterface $entityTypeManager,) {}/*** Creates and saves a new article.** @param string $title* The article title.* @param string $body* The body text.* @param int $uid* The author's user ID.** @return \Drupal\node\NodeInterface* The saved node.*/public function createArticle(string $title, string $body, int $uid): NodeInterface {/** @var \Drupal\node\NodeInterface $node */$node = $this->entityTypeManager->getStorage('node')->create(['type' => 'article','title' => $title,'body' => ['value' => $body,'format' => 'basic_html',],'uid' => $uid,]);// create() only builds the object in memory; save() writes it.$node->save();return $node;}/*** Loads an article by ID.** @param int $nid* The node ID.** @return \Drupal\node\NodeInterface|null* The node, or NULL if it does not exist or is not an article.*/public function loadArticle(int $nid): ?NodeInterface {$node = $this->entityTypeManager->getStorage('node')->load($nid);if ($node instanceof NodeInterface && $node->bundle() === 'article') {return $node;}return NULL;}/*** Retitles an article.** @param int $nid* The node ID.* @param string $title* The new title.*/public function retitleArticle(int $nid, string $title): void {$node = $this->loadArticle($nid);if ($node) {$node->setTitle($title);$node->save();}}/*** Deletes articles in bulk.** @param int[] $nids* The node IDs to delete.*/public function deleteArticles(array $nids): void {$storage = $this->entityTypeManager->getStorage('node');$storage->delete($storage->loadMultiple($nids));}}Two behaviors to internalize:
create()only builds an object in memory, and nothing exists untilsave(); and for bundled entity types, the bundle key (typefor nodes) is required at creation and immutable afterward.load()returns the entity orNULL, so guard before use. -
Read and write field values. Every field is a
\Drupal\Core\Field\FieldItemListInterface(a list, because most fields can hold multiple values), and each item has one or more properties:// Fragment: $node is a loaded node entity.$title = $node->getTitle();$body_text = $node->get('body')->value;$summary = $node->get('body')->summary;$first_tag_id = $node->get('field_tags')->target_id;->value(or any property name) on the list reads that property from the first item; reference fields use->target_idfor the ID and->entityfor the loaded target. For multi-value reference fields,referencedEntities()loads them all. Writing mirrors reading, and nothing persists untilsave():// Fragment: $node is a loaded node entity.$node->setTitle('Updated title');$node->set('field_tags', [12, 34]);$node->save(); -
Find entities with entity queries. A query returns entity IDs that match field conditions; you then
loadMultiple()the ones you need:web/modules/custom/docs_snippets/src/ArticleRepository.php <?phpdeclare(strict_types=1);namespace Drupal\docs_snippets;use Drupal\Core\Entity\EntityTypeManagerInterface;use Drupal\node\NodeInterface;/*** Finds articles with entity queries.** Entity queries search entities by field values and return IDs; they are* storage-agnostic and respect the entity access system when asked to.*/class ArticleRepository {/*** Constructs the repository.** @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager* The entity type manager.*/public function __construct(protected readonly EntityTypeManagerInterface $entityTypeManager,) {}/*** Loads the newest published articles by one author.** @param int $uid* The author's user ID.* @param int $limit* How many articles to return at most.** @return \Drupal\node\NodeInterface[]* The matching nodes, newest first.*/public function findRecentByAuthor(int $uid, int $limit = 10): array {$storage = $this->entityTypeManager->getStorage('node');$ids = $storage->getQuery()->condition('type', 'article')->condition('status', NodeInterface::PUBLISHED)->condition('uid', $uid)->sort('created', 'DESC')->range(0, $limit)// Every entity query must choose: filter out entities the current// user may not view (TRUE) or return everything (FALSE). Use FALSE// only for trusted, non-display purposes.->accessCheck(TRUE)->execute();/** @var \Drupal\node\NodeInterface[] $nodes */$nodes = $storage->loadMultiple($ids);return $nodes;}/*** Counts published articles tagged with a term.** @param int $tid* The taxonomy term ID.** @return int* The number of matching articles.*/public function countByTag(int $tid): int {return (int) $this->entityTypeManager->getStorage('node')->getQuery()->condition('type', 'article')->condition('status', NodeInterface::PUBLISHED)->condition('field_tags.target_id', $tid)->accessCheck(TRUE)->count()->execute();}}Every entity query in Drupal 11 must call
accessCheck();\Drupal\Core\Entity\Query\QueryInterfacerequires the choice to be explicit, and a query that omits it throwsQueryExceptionat execute time.accessCheck(TRUE)filters out entities the current user may not view and is right for anything user-facing.accessCheck(FALSE)returns everything and belongs in trusted, non-display contexts: cron, migrations, administrative counts. Note the dotted syntax for conditions on reference fields (field_tags.target_id). -
React to CRUD from other modules. Storage fires hooks around every operation, in pairs: a type-specific hook (
hook_ENTITY_TYPE_presave(), for nodesnode_presave) then the generic one (hook_entity_presave()). The full sequence per operation is inthe entity CRUD topic. Implement them in your#[Hook]class:web/modules/custom/event_rsvp/src/Hook/EventRsvpHooks.php <?phpdeclare(strict_types=1);namespace Drupal\event_rsvp\Hook;use Drupal\Core\Hook\Attribute\Hook;use Drupal\event_rsvp\RsvpStorage;use Drupal\node\NodeInterface;/*** Hook implementations for the Event RSVP module.** Since Drupal 11.1, hooks live in autowired classes with the #[Hook]* attribute instead of procedural functions in the .module file.*/class EventRsvpHooks {/*** Constructs the hook implementation class.** @param \Drupal\event_rsvp\RsvpStorage $rsvpStorage* The RSVP storage service.*/public function __construct(protected readonly RsvpStorage $rsvpStorage,) {}/*** Implements hook_ENTITY_TYPE_delete() for node entities.*/#[Hook('node_delete')]public function nodeDelete(NodeInterface $node): void {// Don't leave orphaned RSVP records behind when an event is deleted.$this->rsvpStorage->deleteForNode((int) $node->id());}}presaveis the place to compute derived values before they hit storage;insert/update/deleterun after the database write, for reacting to what happened.
Verify
Section titled “Verify”Run each piece interactively against your site:
ddev drush php:eval " \$node = \Drupal::entityTypeManager()->getStorage('node') ->create(['type' => 'article', 'title' => 'From code']); \$node->save(); print \$node->id();"Then confirm the article appears under Administer → Content/admin/content, and that an entity query finds it: swap the body for print count(\Drupal::entityQuery('node')->condition('type', 'article')->accessCheck(FALSE)->execute());.
Variations
Section titled “Variations”Load by properties instead of querying
Section titled “Load by properties instead of querying”For simple exact-match lookups, storage handlers offer a shortcut that skips the query builder: $storage->loadByProperties(['type' => 'article', 'status' => 1]) returns loaded entities directly. Note that it performs no access checking, so treat its results like an accessCheck(FALSE) query.
Count without loading
Section titled “Count without loading”End a query with ->count()->execute() to get a number instead of IDs, as countByTag() above does. Never load entities only to count() them.
Update many entities
Section titled “Update many entities”Load in batches and save in a loop; there is no bulk update API that fires hooks correctly. For large sets, do the work in a queue or batch so requests stay fast; see Background processing.
Work with revisions and translations
Section titled “Work with revisions and translations”Revisionable types version their data: $storage->loadRevision($vid) fetches a specific revision, and queries can target all revisions with allRevisions() or the newest with latestRevision(). Translated values live on translation objects ($node->getTranslation('fi')), not on extra entities. The model behind both is described in the entity CRUD topic.
Related
Section titled “Related”- Entities, bundles, and fields for the concepts behind storage, bundles, and fields.
- Define a custom entity type when none of the built-in types fit your data.
- Convert procedural hooks to classes for the hook classes used in step 5.
the Entity API topicandthe entity CRUD topicon api.drupal.org.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.