Drupal Entity Api
Published by gkastanis in drupal-workflow
What this skill does
Field type selection, entity CRUD operations, view modes, and content modeling patterns for Drupal 10/11. Use when designing content types, selecting field types, or working with the Entity API.
Add Drupal Entity Api to your agent
Review the source and files first. When you are ready, copy the prompt instruction or use the CLI command supported by your environment.
Install with a prompt
Paste this into a compatible coding agent:
add this skill "drupal-entity-api" from https://github.com/gkastanis/drupal-workflowInstall with the CLI
Run this command in a controlled environment after reviewing the repository:
npx skills add https://github.com/gkastanis/drupal-workflow --skill drupal-entity-apiSkill instructions
Drupal Entity API
Field Type Selection Matrix
| Content Need | Field Type | Widget | Notes |
|---|---|---|---|
| Event date/time | datetime | datetime_default | Single datetime |
| Date range | daterange | daterange_default | Start/end dates |
| Location (simple) | string | string_textfield | Text-based |
| Location (structured) | address (contrib) | address_default | Full address |
| Short text | string | string_textfield | Max 255 chars |
| Long text | text_long | text_textarea | Unlimited plain text |
| Rich content | text_with_summary | text_textarea_with_summary | Formatted with summary |
email | email_default | Validated email | |
| Phone | telephone | telephone_default | Phone number |
| Website | link | link_default | URL with title |
| Document | file | file_generic | Any file type |
| Image | image | image_image | With alt text |
| Content reference | entity_reference (node) | entity_reference_autocomplete | Links to nodes |
| Term reference | entity_reference (taxonomy_term) | entity_reference_autocomplete | Categories |
| Yes/No flag | boolean | boolean_checkbox | True/false |
| Whole number | integer | number | Integer values |
| Decimal number | decimal | number | Precise decimals (financial) |
| Float number | float | number | Approximate decimals |
| Flexible components | entity_reference_revisions (paragraphs) | paragraphs | Contrib: requires entity_reference_revisions storage |
Entity Type Selection
Always choose the right entity type for your data. Use ContentEntityBase for fieldable content, ConfigEntityBase for exportable config. Never create a custom entity when a node type would suffice.
- Nodes (
ContentEntityType): Published content (articles, pages, events). - Custom entities (
ContentEntityBase): Non-published data (rarely needed — prefer nodes). - Taxonomy terms: Categorization and metadata.
- Paragraphs: Flexible content components.
- Media: Files, images, videos.
- Users: User profiles (extend, don't replace).
Custom Entity Definition
Always implement baseFieldDefinitions() to declare your entity's base fields. Always add an AccessControlHandler class for custom entities.
// In your entity class:
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
$fields = parent::baseFieldDefinitions($entity_type);
// Add custom base fields here.
return $fields;
}
Entity CRUD Operations
Use dependency injection — inject EntityTypeManagerInterface via constructor, never use \Drupal:: static calls in classes.
// In a service or controller with injected $entityTypeManager:
// Load single entity.
$node = $this->entityTypeManager->getStorage('node')->load($nid);
// Load multiple entities (avoids N+1).
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
// Create entity.
$node = $this->entityTypeManager->getStorage('node')->create([
'type' => 'article',
'title' => 'My Article',
'field_tags' => [['target_id' => $tid]],
]);
$node->save();
// Query entities (inject EntityTypeManagerInterface, not entityQuery directly).
$nids = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', 'article')
->condition('status', 1)
->accessCheck(TRUE)
->range(0, 10)
->execute();
Entity Access Control Handler
Custom entities need an AccessControlHandler. Without one, all access defaults to denied.
declare(strict_types=1);
namespace Drupal\my_module\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
final class MyEntityAccessControlHandler extends EntityAccessControlHandler {
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResult {
return match ($operation) {
'view' => AccessResult::allowedIfHasPermission($account, 'view my_entity'),
'update' => AccessResult::allowedIfHasPermission($account, 'edit my_entity'),
'delete' => AccessResult::allowedIfHasPermission($account, 'delete my_entity'),
default => AccessResult::neutral(),
};
}
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL): AccessResult {
return AccessResult::allowedIfHasPermission($account, 'create my_entity');
}
}
Register in the entity annotation: handlers = {"access" = "Drupal\my_module\Access\MyEntityAccessControlHandler"}
Field Storage Reusability
Share field storage across content types when the same concept applies:
# Good: shared storage
field_location (on event, venue, office)
field_contact_email (on person, organization, department)
# Bad: duplicated storage
field_event_location, field_venue_location, field_office_location
View Modes
full: Full content display.teaser: Summary/listing display.search_result: Search listing.- Custom view modes for specific contexts (e.g.,
card,sidebar).
Content Modeling Checklist
- Identify fields that can share storage across bundles.
- Select appropriate field types (not just "string" for everything).
- Plan widget types for optimal editor experience.
- Consider cardinality (single vs multi-value).
- Plan required fields vs optional.
- Document field purposes and usage.
- Consider field display in different view modes.
Files included
- SKILL.md

