Entity API 8.0



by Wolfgang Ziegler // fago // @the_real_fago


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


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

Photo source: David Francis, flickr.com/photos/therealdavidfrancis

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!

Photo source: Alex Grande, flickr.com/photos/alexgrande

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 contrib
to 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

for entity types

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();
// FAILED!!! $this->assertTrue($violations->count() > 0);

$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


http://entity.worldempire.ch/


 Weekly IRC meetings:


#drupal-entity
Thursday, 18:00 CET



Questions?