Entity API 8.0
Wolfgang Ziegler
fago // @the_real_fago
- Maintainer of the Entity API module
- co-Maintainer of the Entity & Form core components
- Maintainer of various contrib modules
- Rules, Field Collection, Profile2, ...
- CEO of drunomics
Outline
- A little bit of history...
- Working with entities & fields
- Provide a new entity type
- Entity Validation API
- Querying
- Configuration Entities, Typed Data
- Comparison to Drupal 7
Drupal 8 Field API - fields reborn
by Kristof De Jaeger // swentel
today 13:00, same room
Code-Driven Content Modelling
with Drupal 8
by Tobias Stöckler // tstoeckler
Friday, 10:00 in room Ursa Minor
Entities in Drupal 7
-
Side-effect of Field API being decoupled from nodes
- Introduced very late in the cycle
-
Vastly unfinished
Photo source: Metro Centric, flickr.com/people/16782093@N03
Entity module to the rescue!
Drupal 8: Let's complete it!
Put Entity module in core?
Ok, so we need...
Class based entity objects
- EnitityInterface
- Full CRUD storage controllers
But thats' not enough
We also need...
- Unification of fields and properties
- Metadata about fields / properties
- Validation API
- Re-usable components built on top
Entities are content?
Entities
=
Content
+
Configuration
ContentEntityInterface extends EntityInterface ConfigEntityInterface extends EntityInterface
Working with entities
Photo source: Bob Jagendorf, flickr.com/photos/bobjagendorf/
$manager = \Drupal::entityManager();
$entity = $manager ->getStorageController('comment') ->load($id);$entity->getEntityTypeId();
$entity->label();
$entity->id();
Entity types
$entity_type = $entity_manager
->getDefinition('node');
$entity_type->id() == 'node'
$entity_type
->getClass()
$entity_type
->hasKey('label')
$entity_type
->isSubclassOf('ContentEntityInterface')
echo $entity->subject->value;
$tag = $entity ->field_tags[2] ->entity ->name->value;
$entity->hasField($field_name);
$entity = $field_item->getEntity();
$entity->title->value = 'new Title';
$entity->save();
Use methods!
if ($node->isPromoted()) {
$title = $node->getTitle();
}
elseif ($node->isPublished()) {
$node->setTitle(
$node->getAuthor()
->getUsername()
);
}
Translation
echo $entity ->getTranslation('de') ->title->value;
$translation = $entity->getTranslation('de');
$translation->language() == 'de';$translation->title->value = 'German title';
$translation = $manager ->getTranslationFromContext($entity);
echo $translation->label();
$entity = $translation->getUntranslated();
Iterating
foreach ($entity as $field_name => $items) {
$items instanceof FieldItemListInterface
foreach ($items as $item) {
$item instanceof FieldItemInterface
echo $item->target_id;
echo $item->entity->label();
}
}
Everything
on a content entity
is a field!
Different flavours of fields
Fields
=
Base fields
(Node title, Node id, User name, User roles, ...)
+
Bundle fields
(Node body, Node tags, User tags, ... - all configurable fields)
Configurable fields
- are fields provided by field.module
- managed via the Configuration system
- appear in the UI by default
- implement the same interfaces as others
Module provided fields
hook_entity_base_field_info()
hook_entity_base_field_info_alter()
hook_entity_bundle_field_info()hook_entity_bundle_field_info_alter()
$module = $field_definition->getProvider();
Field storage
- handled by the Entity Storage
(even for configurable fields)
- custom (modules take over)
- none (Computed fields)
Computed fields
allow you to
compute field item properites
when they are accessed.
echo $node->body->processed;
echo $node->uid->entity->label();
Getting field definitions
$field_definitions =
$em->getBaseFieldDefinitions($entity_type_id);$field_definitions =
$em->getFieldDefinitions($etype_id, $bundle);
Config entities & Fields?
is_string($node->title) == FALSE
is_string($view->name) == TRUE
Providing a new
content entity type
Entity class
/**
* Defines the comment entity class.
*
* @ContentEntityType(
* id = "comment",
* label = @Translation("Comment"),
* bundle_label = @Translation("Content type"),
* controllers = {
* "storage" = "Drupal\comment\CommentStorageController",
* "access" = "Drupal\comment\CommentAccessController",
* "view_builder" = "Drupal\comment\CommentViewBuilder",
* "form" = {
* "default" = "Drupal\comment\CommentFormController",
* "delete" = "Drupal\comment\Form\DeleteForm"
* },
* "translation" = "Drupal\comment\CommentTranslationController"
* },
* base_table = "comment",
* uri_callback = "comment_uri",
* fieldable = TRUE,
* translatable = TRUE,
* entity_keys = {
* "id" = "cid",
* "bundle" = "field_id",
* "label" = "subject",
* "uuid" = "uuid"
* },
* links = {
* "canonical" = "comment.permalink",
* "delete-form" = "comment.confirm_delete",
* "edit-form" = "comment.edit_page",
* "admin-form" = "comment.bundle"
* }
* )
*/
class Comment extends ContentEntityBase implements CommentInterface {
Define your fields!
class Comment extends ContentEntityBase implements CommentInterface {
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['cid'] = FieldDefinition::create('integer')
->setLabel(t('Comment ID'))
->setDescription(t('The comment ID.'))
->setReadOnly(TRUE);
$fields['uuid'] = FieldDefinition::create('uuid')
->setLabel(t('UUID'))
->setDescription(t('The comment UUID.'))
->setReadOnly(TRUE);
$fields['pid'] = FieldDefinition::create('entity_reference')
->setLabel(t('Parent ID'))
->setSetting('target_type', 'comment')
return $fields;
}}
Widgets & Formatters!
$fields['title'] = FieldDefinition::create('string')
->setLabel(t('Title'))
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setSettings(array(
'default_value' => '',
'max_length' => 255,
))
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
))
->setDisplayOptions('form', array(
'type' => 'string',
'weight' => -5,
))
->setDisplayConfigurable('form', TRUE);
Storage
class CommentStorageController extends FieldableDatabaseStorageController implements CommentStorageControllerInterface {
/**
* {@inheritdoc}
*/
public function getMaxThread(EntityInterface $comment) {
$query = $this->database->select('comment', 'c')
->condition('entity_id', $comment->entity_id->value)
->condition('field_id', $comment->field_id->value)
->condition('entity_type', $comment->entity_type->value);
$query->addExpression('MAX(thread)', 'thread');
return $query->execute()
->fetchField();
}
}
$manager->getStorageController('comment')->save($comment);
Storage in-dependent logic?
class Comment extends ContentEntityBase implements CommentInterface {
/**
* {@inheritdoc}
*/
public function postSave($storage, $update = TRUE) {
parent::postSave($storage, $update);
$this->releaseThreadLock();
//...
}
}
Field storage
I
V
Entity storage
Fieldable entities
I
V
Extendable entities
ViewBuilder
$manager->getViewBuilder('comment')->view($comment);
Forms
\Drupal::service('entity.form_builder')
->getForm($comment);
\Drupal::service('entity.form_builder')
->getForm($comment, 'delete');
Access control
/**
* Access controller for the comment entity.
*
* @see \Drupal\comment\Entity\Comment.
*/
class CommentAccessController extends EntityAccessController {
// ...
}
$manager->getAccessController('comment')
->access($comment, 'view');
Entity lists
$entityManager
->getListController('contact_category')
->render();
There is a pattern!
Controllers
provide re-usable functionality
based upon
a well-defined interface
Controllers
allow you to easily
customize certain aspects of
your entity type
Controllers
can be provided by contribto allow for easy
per-entity type customization
Controller?
MVC?
#1976158
Controllers
II
V
Handlers
Fields
allow you to provide
re-usable behaviour along with storage
UUID Field
(no UI)
Language field
Path field
-function path_entity_insert(EntityInterface $entity) {
- if ($entity instanceof ContentEntityInterface && $entity->hasField('path')) {
- ....
+ class PathItem extends FieldItemBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function insert() {
+ ....
and more...
"created" field
- "changed" field
- ...
Entity Validation API
- decoupled from form validation -> REST
- makes use of Symfony Validator component
- based upon Constraint plugins
Define constraints
$fields['mail'] = FieldDefinition::create('email')
->setLabel(t('Email'))
->setDescription(t('The email of this user.'))
->setSetting('default_value', '')
->setPropertyConstraints(
'value',
array('UserMailUnique' => array()
));
Defaults based on field types
Validate!
$violations = $entity->validate();
// PASSED!!!
$this->assertEqual($violations->count(), 0);
// Try setting a too long string as UUID.
$test_entity->uuid->value = $this->randomString(129);
$violations = $test_entity->validate();
$this->assertTrue($violations->count() > 0);
// FAILED!!!
$violation = $violations[0];
echo $violation->getMessage();
$violation->getRoot() === $entity';
$violation->getPropertyPath() == 'field_test_text.0.format'
$violation->getInvalidValue();
Constraint plugins
/**
* Range constraint.
*
* Overrides the symfony constraint to use Drupal-style replacement patterns.
*
* @Plugin(
* id = "Range",
* label = @Translation("Range", context = "Validation"),
* type = { "integer", "float" }
* )
*/
class RangeConstraint extends Range {
public $minMessage = 'This value should be %limit or more.';
public $maxMessage = 'This value should be %limit or less.';
/**
* Overrides Range::validatedBy().
*/
public function validatedBy() {
return '\Symfony\Component\Validator\Constraints\RangeValidator';
}
}
Widgets map
violations
to form elements!
Entity query
// Make sure there are no active blocks for these feeds.
$ids = \Drupal::entityQuery('block')
->condition('plugin', 'aggregator_feed_block')
->condition('settings.feed', array_keys($entities))
->execute();
if ($ids) {
...
- propertyCondition(), fieldCondition() ?
-> Just condition()!
- Uses an entity query service as defined by the storage
- Works independently of the storage
Do not directly
query your database
outside of
your storage controller
(or entity query service)
You do not care about MongoDB?
Multi-lingual
Schema changes
Language,
Relationship &
Aggregation Support
Configuration entities
/**
* Defines the Node type configuration entity.
*
* @ConfigEntityType(
* id = "node_type",
* label = @Translation("Content type"),
* controllers = {
* "access" = "Drupal\node\NodeTypeAccessController",
* "form" = {
* "add" = "Drupal\node\NodeTypeFormController",
* "edit" = "Drupal\node\NodeTypeFormController",
* "delete" = "Drupal\node\Form\NodeTypeDeleteConfirm"
* },
* "list_builder" = "Drupal\node\NodeTypeListBuilder",
* },
* admin_permission = "administer content types",
* config_prefix = "type",
* bundle_of = "node",
* entity_keys = {
* "id" = "type",
* "label" = "name"
* },
* links = {
* "add-form" = "node.add",
* "edit-form" = "node.type_edit",
* "delete-form" = "node.type_delete_confirm"
* }
* )
*/
class NodeType extends ConfigEntityBase implements NodeTypeInterface {
echo $type->name;
class NodeType extends ConfigEntityBase implements NodeTypeInterface {
public $type;
public $uuid;
public $name;
public $has_title = TRUE;
public $title_label = 'Title';
//.....
}
$node_type->toArray()
Typed data API
- It's about metadata and being able to leverage it!
- Describe your data using typed data definitions
- Based on an extendable list of data types
- string, integer, float, entity, node, field_item:image
-
Helps REST, Rules, CTools, Search api, Tokens, ...
- Primitives
- ComplexData
- List
Comparison to D7
entity module
Entity wrapper
I
V
Entity
(mostly)
EntityAPIController
=~
EntityStorageController
+
EntityViewBuilder
Entity property info
I
V
Field definitions
EntityAPIControllerExportable
~
ConfigStorageController
So far missing...
- EntityUIController
- we've got forms though
- EntityViewsController
- starting...
We are almost there...
Angelo DeSantis, flickr.com/photos/angeloangelo
Current work areas
- Entity cache support (Render caching is done)
- Unified repository for gettting field definitions
-> FieldStorageDefinitionInterface
- Automatic database schema generation
-> Translatable storage for all content entities!
Modules,
start re-using
entity field storage
for your module data!
(no UI exposure required)
Get involved!
[META] Complete the Entity Field API
http://drupal.org/node/2095603
Weekly IRC meetings:
#drupal-entity
Thursday, 18:00 CET