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 Head of Development of drunomics
Outline
- A little bit of history...
- Working with entities
- Provide a new entity type
- Entity Validation API
- Querying
- Configuration Entities, Typed Data
- Comparison to Drupal 7
Drupal 8 Field API - fields reborn
by Yves Chedemois
Friday, 15:00 in room Zensations
Multilingual content in D8: a highly evolved permutated API
by Francesco Placella
Saturday, 14:00 in room vi knallgrau
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
$manager = \Drupal::entityManager();
$entity = $manager ->getStorageController('comment') ->load($id);$entity->entityType();
$entity->label();
$entity->id();
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()) {
echo $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 => $field_items) {
foreach ($field_items as $item) {
echo $item->value;
}
}
It's all fields!!!
Entity fields
=
Configurable fields
+
Base fields
+
Custom fields
Config entities?
is_string($node->title) == FALSE
is_string($view->name) == TRUE
Providing a new entity type
Entity class
/**
* Defines the comment entity class.
*
* @EntityType(
* 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,
* render_cache = FALSE,
* route_base_path = "admin/structure/comments/manage/{bundle}",
* entity_keys = {
* "id" = "cid",
* "bundle" = "field_id",
* "label" = "subject",
* "uuid" = "uuid"
* }
* )
*/
class Comment extends ContentEntityBase implements CommentInterface {
Define your fields!
class Comment extends ContentEntityBase implements CommentInterface {
public static function baseFieldDefinitions($entity_type) {
$fields['cid'] = FieldDefinition::create('integer') ->setLabel(t('Comment ID')) ->setReadOnly(TRUE); $fields['uuid'] = FieldDefinition::create('uuid') ->setLabel(t('UUID')) ->setReadOnly(TRUE); $fields['pid'] = FieldDefinition::create('entity_reference') ->setLabel(t('Parent ID')) ->setDescription(t('The parent comment ID if this is a reply to a comment.')) ->setFieldSetting('target_type', 'comment'); return $fields;}
}
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(EntityStorageControllerInterface $storage_controller, $update = TRUE) {
parent::postSave($storage_controller, $update);
$this->releaseThreadLock();
// Update the {comment_entity_statistics} table prior to executing the hook.
$storage_controller->updateEntityStatistics($this);
if ($this->status->value == COMMENT_PUBLISHED) {
module_invoke_all('comment_publish', $this);
}
}
}
Field storage
I
V
Entity storage
ViewBuilder
/**
* Render controller for comments.
*/
class CommentViewBuilder extends EntityViewBuilder implements EntityViewBuilderInterface, EntityControllerInterface {
public function buildContent(array $entities, array $displays, $view_mode, $langcode = NULL) {
// Takes care of fields, hooks, etc.
return parent::buildContent($entities, $displays, $view_mode, $langcode);
}
}
$manager->getViewBuilder('comment')->view($comment);
Forms
/**
* Base for controller for comment forms.
*/
class CommentFormController extends ContentEntityFormController {
public function form(array $form, array &$form_state) {
// Used for conditional validation of author fields.
$form['is_anonymous'] = array(
'#type' => 'value',
'#value' => ($comment->id() ? !$comment->uid->target_id : $this->currentUser->isAnonymous()),
);
return parent::form($form, $form_state, $comment);
}
}
$manager->getForm($comment);
Delete form
/**
* Provides the comment delete confirmation form.
*/
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));
}
//.....
}
$manager->getForm($comment, 'delete');
Access
/**
* Access controller for the comment entity.
*
* @see \Drupal\comment\Entity\Comment.
*/
class CommentAccessController extends EntityAccessController {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
switch ($operation) {
case 'view':
return user_access('access comments', $account);
break;
}
}
$manager->getAccessController('comment')->access($comment, 'view');
Translation
/**
* Defines the translation controller class for comments.
*/
class CommentTranslationController extends ContentTranslationController {
/**
* Overrides ContentTranslationController::entityFormTitle().
*/
protected function entityFormTitle(EntityInterface $entity) {
return t('Edit comment @subject', array('@subject' => $entity->label()));
}
}
Entity lists
class CategoryListController extends ConfigEntityListController {
public function buildHeader() {
$header['category'] = t('Category');
$header['recipients'] = t('Recipients');
$header['selected'] = t('Selected');
return $header + parent::buildHeader();
}
public function buildRow(EntityInterface $entity) {
// Add to row.....
return $row + parent::buildRow($entity);
}
}
$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
to allow for easy
per-entity type customization
Controller? MVC?
Fields
allow you to provide
re-usable behaviour along with storage
UUID Field
Language field
Path field
https://drupal.org/node/1980822
-function path_entity_insert(EntityInterface $entity) {
- if ($entity instanceof ContentEntityInterface && $entity->hasField('path')) {
- ....
+ class PathItem extends FieldItemBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function insert() {
+ ....
More to come...
Computed fields
allow you to
compute field item properites
when they are accessed.
echo $node->body->processed;
echo $node->uid->entity->label();
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('E-mail'))
->setFieldSetting('default_value', '')
->setPropertyConstraints('value', array(
'UserMailUnique' => array()
));
Type based defaults
/**
* Defines the 'uuid' entity field type.
*
* The field uses a newly generated UUID as default value.
*
* @FieldType(
* id = "uuid",
* label = @Translation("UUID"),
* description = @Translation("An entity field containing a UUID."),
* configurable = FALSE,
* constraints = {
* "ComplexData" = {
* "value" = {"Length" = {"max" = 128}}
* }
* }
* )
*/
class UuidItem extends StringItem {
}
Validate!
$violations = $entity->validate();
$this->assertEqual($violations->count(), 0, 'Validation passes.');
// Try setting a too long string as UUID.
$test_entity->uuid->value = $this->randomString(129);
$violations = $test_entity->validate();
$this->assertEqual($violations->count(), 1, 'Validation 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() ?
- 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)
I do not care about MongoDB!
Multi-lingual
Schema changes
Query languages
$query = Drupal::entityQuery('entity_test');
$default_langcode_group = $query->andConditionGroup()
->condition('user_id', $user_id, '=', $default_langcode)
->condition('name', $name, '=', $default_langcode);
$langcode_group = $query->andConditionGroup()
->condition('name', $name, '=', $langcode)
->condition("$this->field_name.value", $field_value, '=', $langcode);
$result = $query
->condition('langcode', $default_langcode)
->condition($default_langcode_group)
->condition($langcode_group)
->sort('name', 'ASC', $default_langcode)
->execute();
Aggregation Support
$query = Drupal::entityQueryAggregate('node');
$result = $query
->groupBy('type')
->aggregate('nid', 'COUNT')
->execute();
returnsarray(
0 => array(
'type' => 'article',
'nid_count' => '1',
),
)
Relationships
$results = \Drupal::entityQuery('entity_test')
->condition("user_id.entity.name", $this->accounts[0]->getUsername(), '<>')
->execute();
Configuration entities
/**
* Defines the Node type configuration entity.
*
* @EntityType(
* id = "node_type",
* label = @Translation("Content type"),
* controllers = {
* "storage" = "Drupal\Core\Config\Entity\ConfigStorageController",
* "access" = "Drupal\node\NodeTypeAccessController",
* "form" = {
* "add" = "Drupal\node\NodeTypeFormController",
* "edit" = "Drupal\node\NodeTypeFormController",
* "delete" = "Drupal\node\Form\NodeTypeDeleteConfirm"
* },
* "list" = "Drupal\node\NodeTypeListController",
* },
* admin_permission = "administer content types",
* config_prefix = "node.type",
* bundle_of = "node",
* entity_keys = {
* "id" = "type",
* "label" = "name",
* "uuid" = "uuid"
* },
* links = {
* "edit-form" = "node.type_edit"
* }
* )
*/
class NodeType extends ConfigEntityBase implements NodeTypeInterface {
echo $type->name;
class NodeType extends ConfigEntityBase implements NodeTypeInterface {
public $type;
public $uuid;
public $name;
public $description;
public $help;
public $has_title = TRUE;
public $title_label = 'Title';
//.....
}
$node_type->getExportProperties()
Typed data API
- It's about metadata and being able to leverage it!
- Describe your data using data definitions
- Based on an extensible list of data types
- string, integer, float, entity, node, field_item:image
- Primitives
- ComplexData
- List
Comparison to D7
entity module
Entity wrapper
I
V
Entity
EntityAPIController
=~
EntityStorageController
+
EntityViewBuilder
So far missing...
- EntityUIController
- EntityViewsController
Entity property info
I
V
Field definitions,
Data definitions
EntityAPIControllerExportable
~
ConfigStorageController
We are almost there...
Angelo DeSantis, flickr.com/photos/angeloangelo
Get involved!
[META] Complete the Entity Field API
http://drupal.org/node/2095603
Join the sprints!
Questions?
Drupal 8 Entity API
By Wolfgang Ziegler