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

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

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!

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?

Fields

allow you to provide

re-usable behaviour along with storage

for entity types

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();
returns
array(
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


http://entity.worldempire.ch/


Join the sprints!
http://2013.drupalcamp.at/sprints


Questions?