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


Field instance config  ->  Field definition
Field config  ->  Field storage definition


$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

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\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!

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

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 contrib
to allow for easy
per-entity type customization

Controller?
Handler?


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';
}
}

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)



Rules needs your help!

visit d8rules.org


Questions?