Wolfgang Ziegler
fago // @the_real_fago
- based in Vienna, Austria
- 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
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!
So we've got
-
Class based entity objects
- EnitityInterface
- Full CRUD storage controllers
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 ->getStorage('comment') ->load($id);
$entity = Comment::load($id);$user = User::create(array('name' => 'me'));
$entity->getEntityTypeId();
$entity->label();
$entity->id();
echo $entity->subject->value;
$term_id = $entity ->field_tags[2] ->target_id;
$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()->id == 'de';$translation->title->value = 'German title';
$translation = $manager ->getTranslationFromContext($entity);
echo $translation->label();
$entity = $translation->getUntranslated();
Entity types
$entity_type = $entity_manager
->getDefinition('node');
$entity_type->id() == 'node'
$entity_type
->getClass()
$entity_type
->hasKey('label')
$entity_type
->isSubclassOf('ContentEntityInterface')
Field definitions // Field info
$field_definition = $entity-> getFieldDefinition($field_name); $field_definition->getName(); $field_definition->getPropertyDefinitions();
$entity_manager ->getFieldDefinitions('node', 'article');
Everything
on a content entity
is a field!
Iterating
foreach ($entity as $field_name => $items) {
$items instanceof FieldItemListInterface;
foreach ($items as $item) {
$item instanceof FieldItemInterface;
echo $item->target_id;
}
}
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
Field storage definitions
$entity_manager-> getFieldStorageDefinitions('node');
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();
hook_entity_field_storage_info();
hook_entity_field_storage_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 properties
when they are accessed.
echo $node->body->processed;
echo $node->uid->entity->label();
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\CommentStorage",
* "access" = "Drupal\comment\CommentAccessController",
* "view_builder" = "Drupal\comment\CommentViewBuilder",
* "form" = {
* "default" = "Drupal\comment\CommentForm",
* "delete" = "Drupal\comment\Form\DeleteForm"
* },
* "translation" = "Drupal\comment\CommentTranslationHandler"
* },
* 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) ->setSetting('unsigned', 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'))// ...
->setDisplayOptions('view', array(
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
))
->setDisplayOptions('form', array(
'type' => 'string',
'weight' => -5,
))
->setDisplayConfigurable('form', TRUE);
Storage
- Specify ContentEntityDatabaseStorage
- Implement hook_schema()
Storage
- Specify ContentEntityDatabaseStorage
-
Implement hook_schema()
FieldDefinition::create('created')
->setLabel(t('Created')) ->setRevisionable(TRUE) ->setTranslatable(TRUE);
Storage
class CommentStorage extends ContentEntityDatabaseStorage implements CommentStorageInterface {
/**
* {@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->getStorage('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();
//...
}
}
ViewBuilder
$manager->getViewBuilder('comment')->view($comment);
Forms
class DeleteForm extends ContentEntityConfirmFormBase { /** * {@inheritdoc} */ public function getQuestion() { return $this->t('Are you sure you want to delete the comment %title?', array('%title' => $this->entity->subject->value)); }
} \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
->getListBuilder('contact_category')
->render();
There is a pattern!
Entity controllers
provide re-usable functionality
based upon
a well-defined interface
Entity controllers
allow you to easily
customize certain aspects of
your entity type
Entity controllers
can be provided by contribto allow for easy
per-entity type customization
Controller?
Handler?
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';
}
}
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
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\NodeTypeForm",
* // ..
* },
* "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 ConfigEntityBundleBase 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();
Config schema
node.type.*:
type: mapping
label: 'Content type'
mapping:
type:
type: string
label: 'Machine-readable name'
uuid:
type: string
label: 'UUID'
name:
type: label
label: 'Name'
description:
type: text
label: 'Description'
help:
type: text
label: 'Explanation or submission guidelines'
has_title:
type: boolean
label: 'Has title'
title_label:
type: label
label: 'Title field label'
settings:
type: mapping
label: 'Settings'
mapping:
node:
type: node.settings.node
status:
type: boolean
label: 'Enabled status of the configuration entity'
langcode:
type: string
label: 'Default language'
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, ...
We are almost there...
Angelo DeSantis, flickr.com/photos/angeloangelo
Current work areas
- Getting ready for the beta!
- Handle entity schema changes
- Ease re-using entity field storage for modules
- Entity cache support (Render caching is done)
Get involved!
[META] Complete the Entity Field API
http://drupal.org/node/2095603
Weekly IRC meetings:
#drupal-entity
Thursday, 18:00 CEST (UTC+2)