Problem/Motivation

Drupal\Core\Entity\TypedData\EntityDataDefinition::create() does not use the definition derived by Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver.

This causes EntityDataDefinition::getLabel() to always return NULL.

This blocks #2936714: Entity type definitions cannot be set as 'internal'.

Proposed resolution

Get the data definition from the typed data manager and use it to instantiate the EntityDataDefinition.

User interface changes

None.

API changes

None.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

gabesullice created an issue. See original summary.

gabesullice’s picture

The last submitted patch, 2: 2936725-2.tests_only.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 2: 2936725-2.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

gabesullice’s picture

This adds test coverage for bundle-defined labels and fixes the broken Views test.

gabesullice’s picture

Fix whitespace issues.

The last submitted patch, 5: interdiff-2936725-2-5.patch, failed testing. View results

The last submitted patch, 5: 2936725-5.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 6: 2936725-6.patch, failed testing. View results

gabesullice’s picture

Status: Needs work » Needs review
FileSize
570 bytes
5.11 KB

Fixes the failing test.

Wim Leers’s picture

Status: Needs review » Needs work

Can we get a failing test-only patch?

  1. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -18,13 +18,25 @@ class EntityDataDefinition extends ComplexDataDefinitionBase implements EntityDa
    -  public static function create($entity_type_id = NULL) {
    ...
    +  public static function create($entity_type_id = NULL, $bundle = NULL) {
    

    Isn't this a violation of the interface?

    My guess is that it's not, because \Drupal\Core\Field\TypedData\FieldItemDataDefinition::create() goes much further still: rather than receiving a string as its parent method \Drupal\Core\TypedData\DataDefinition::create() dictates, it receives an object, which can contain arbitrary complexity!

    Oh, and in fact, \Drupal\Core\Entity\TypedData\EntityDataDefinition::createFromDataType() already supports this! So ::createFromDataType() supports entity type + bundle, it's just ::create() that doesn't.

  2. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -18,13 +18,25 @@ class EntityDataDefinition extends ComplexDataDefinitionBase implements EntityDa
    +      // If the definition doesn't exist, just create from defaults.
    +      $values = \Drupal::typedDataManager()->getDefinition($data_type, FALSE);
    

    This is where the updated code does use \Drupal\Core\Entity\Plugin\DataType\Deriver\EntityDeriver::getDerivativeDefinitions(), correct?

  3. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -35,13 +47,15 @@ public static function createFromDataType($data_type) {
    +    $entity_type_id = isset($parts[1]) ? $parts[1] : NULL;
    +    $bundle = isset($parts[2]) ? $parts[2] : NULL;
    +    $definition = static::create($entity_type_id, $bundle);
         // Set the passed entity type and bundle.
    -    if (isset($parts[1])) {
    -      $definition->setEntityTypeId($parts[1]);
    +    if ($entity_type_id) {
    +      $definition->setEntityTypeId($entity_type_id);
         }
    -    if (isset($parts[2])) {
    -      $definition->setBundles([$parts[2]]);
    +    if ($bundle) {
    +      $definition->setBundles([$bundle]);
    

    AFAICT these changes are unnecessary?

gabesullice’s picture

Can we get a failing test-only patch?

Absolutely. Attached the failing and passing patched (no changes since #10)


1. Correct! And because it has $bundle = NULL, it can't break existing implementations that don't have the argument. It will just fall right back to the way it worked before.

2. Yep.

3. Almost...

+    $entity_type_id = isset($parts[1]) ? $parts[1] : NULL;
+    $bundle = isset($parts[2]) ? $parts[2] : NULL;
+    $definition = static::create($entity_type_id, $bundle);

Thats needed to avoid undefined indices while also "falling back" to the NULL behavior of create. It's entirely possible for either of those $parts to be missing.

-    if (isset($parts[1])) {
-      $definition->setEntityTypeId($parts[1]);
+    if ($entity_type_id) {
+      $definition->setEntityTypeId($entity_type_id);
     }
-    if (isset($parts[2])) {
-      $definition->setBundles([$parts[2]]);
+    if ($bundle) {
+      $definition->setBundles([$bundle]);

Since we already did the isset check, isn't it clearer to use the named variables?

Wim Leers’s picture

Status: Needs work » Needs review

#12.3: right, I see it now!

The last submitted patch, 12: 2936725-12.tests_only.patch, failed testing. View results

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -18,13 +18,25 @@ class EntityDataDefinition extends ComplexDataDefinitionBase implements EntityDa
    +      // If a bundle was given, use the bundle-specific definition.
    +      $data_type = isset($bundle) ?
    +        sprintf('entity:%s:%s', $entity_type_id, $bundle) :
    +        sprintf('entity:%s', $entity_type_id);
    

    This might be slightly shorter than the following:

    <?php
    $data_type = 'entity:' . $entity_type_id;
    if ($bundle) {
    $data_type .= ':' . $bundle;
    }

    But I personally find that more readable and I don't like multi-line if/?: statements like that. But not feeling strongly about that.

    Also doesn't really need isset() on $entity_type_id/$bundle, it can't be not defined.

  2. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -35,13 +47,15 @@ public static function createFromDataType($data_type) {
    -    if (isset($parts[1])) {
    -      $definition->setEntityTypeId($parts[1]);
    +    if ($entity_type_id) {
    +      $definition->setEntityTypeId($entity_type_id);
         }
    -    if (isset($parts[2])) {
    -      $definition->setBundles([$parts[2]]);
    +    if ($bundle) {
    +      $definition->setBundles([$bundle]);
    

    entity type is already set in the create() call and we could/should also set bundles there, then this can all go away. Maybe it's already set through $values?

  3. +++ b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php
    @@ -93,8 +93,10 @@ protected function setUp() {
           ->method('getDefinition')
    -      ->with($this->equalTo('field_item:string_long'))
    -      ->willReturn(['class' => '\Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem']);
    +      ->will($this->returnValueMap([
    +        'entity:user' => $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'),
    +        'field_item:string_long' => ['class' => '\Drupal\Core\Field\Plugin\Field\FieldType\StringLongItem'],
    +      ]));
    

    Why does one call return an object and the other an array? doesn't the mock object just get ignored due to the is_array($values) call?

  4. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php
    @@ -82,10 +89,15 @@ public function testFields() {
     
    +    // Entity definitions should inherit their labels from the entity type.
    +    $this->assertEqual($entity_definition->getLabel(), \Drupal::entityTypeManager()->getDefinition('node')->getLabel());
    +    $this->assertEqual($bundle_definition->getLabel(), \Drupal::service('entity_type.bundle.info')->getBundleInfo('node')['article']['label']);
    +
    

    lets use the non-deprecated assertEquals() which expects the expected value first.

    Might also be easier to just hardcode the expected label, makes tests easier to read IMHO.

gabesullice’s picture

Status: Needs review » Needs work

Just setting this back NW so I don't keep forgetting to incorporate @berdir's suggestions.

gabesullice’s picture

All fixed. @Berdir, thanks for all the great suggestions. This is much more readable.

The last submitted patch, 18: 2936725-18.tests_only.patch, failed testing. View results

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -18,13 +18,32 @@ class EntityDataDefinition extends ComplexDataDefinitionBase implements EntityDa
    +      // If the definition doesn't exist, just create from defaults.
    ...
    +      // Set the passed entity type.
    ...
    +      // Set the passed bundle, if it was given.
    

    These two comments seems to be a bit pointless, given they describe the code exactly, or is it the code describing the comment ... :) For the first case we could describer rather, why the definition might not exist. In the other cases we could describe that for those cases it doesn't exist, we need to set the entity type explicitly.

  2. +++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
    @@ -35,15 +54,10 @@ public static function createFromDataType($data_type) {
    -    $definition = static::create();
    -    // Set the passed entity type and bundle.
    -    if (isset($parts[1])) {
    -      $definition->setEntityTypeId($parts[1]);
    -    }
    -    if (isset($parts[2])) {
    -      $definition->setBundles([$parts[2]]);
    -    }
    -    return $definition;
    +    return static::create(
    +      isset($parts[1]) ? $parts[1] : NULL,
    +      isset($parts[2]) ? $parts[2] : NULL
    +    );
    

    Nice!

  3. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php
    @@ -31,10 +32,16 @@ class EntityTypedDataDefinitionTest extends KernelTestBase {
    -  public static $modules = ['filter', 'text', 'node', 'user'];
    +  public static $modules = ['system', 'filter', 'text', 'node', 'user'];
    ...
    +    NodeType::create([
    +      'type' => 'article',
    +      'name' => 'Article',
    +    ])->save();
    
    @@ -82,10 +89,15 @@ public function testFields() {
     
    +    // Entity definitions should inherit their labels from the entity type.
    +    $this->assertEquals('Content', $entity_definition->getLabel());
    +    $this->assertEquals('Article', $bundle_definition->getLabel());
    

    IMHO it would be nice trying to avoid adding "node" specific test code. What about using \Drupal\entity_test\Entity\EntityTestWithBundle|\Drupal\entity_test\Entity\EntityTestBundle instead?

gabesullice’s picture

Thanks for the review @dawehner!

1. I've tried to make these comments a little more informative.
2. Thanks :)
3. I've created a follow-up to avoid expanding the scope of this issue to include rewriting the existing tests for this class here: #2938920: Convert EntityTypeDataTest to use EntityTestWithBundle

gabesullice’s picture

Aaaand... here's the patch!

gabesullice’s picture

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
@@ -28,7 +28,10 @@
 
-      // Load definitions created by the EntityDeriver.
+      // It's possible that the given entity type ID or bundle wasn't discovered
+      // by the TypedData plugin manager and/or weren't created by the
+      // EntityDeriver. In that case, this is a new definition and we'll just
+      // create the definition from defaults by using an empty array.
       $values = \Drupal::typedDataManager()->getDefinition($data_type, FALSE);
       $definition = new static(is_array($values) ? $values : []);

This is really nice!

larowlan’s picture

+++ b/core/lib/Drupal/Core/Entity/TypedData/EntityDataDefinition.php
@@ -35,15 +57,10 @@ public static function createFromDataType($data_type) {
+      isset($parts[1]) ? $parts[1] : NULL,
+      isset($parts[2]) ? $parts[2] : NULL

if only we could use php7's null coalesce operator and/or splat operator!

Adding review credits

  • larowlan committed b2b7f46 on 8.6.x
    Issue #2936725 by gabesullice, Wim Leers, dawehner, Berdir:...

  • larowlan committed 8525135 on 8.5.x
    Issue #2936725 by gabesullice, Wim Leers, dawehner, Berdir:...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed b2b7f46 and pushed to 8.6.x.

Cherry-picked as 8525135 and pushed to 8.5.x

Thanks folks

Wim Leers’s picture

Woot, this means #2936725: EntityDataDefinition::create() does not respect derived entity definitions is unblocked! This also means there's one less blocker for #2932035: ResourceTypes should be internal when EntityType::isInternal is TRUE, which is a blocker for JSON API to get into 8.6.x.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Wim Leers’s picture

Wim Leers’s picture