Problem

Entity operation links are defined by entity types' list controllers and can be altered using hook_entity_operation_alter(). However, Drupal core provides no way for modules to declare operation links for entities that are not their own. Because of this, Field UI abuses the alter hook for definitions, which means its operation links can never be altered by modules with a lower weight.

Proposal

#1839516: Introduce entity operation providers aspired to add entity controllers to solve this problem, but the change was deemed too large to be committed to Drupal 8 at this point in the development cycle.

This issue introduces hook_entity_operation() which is intended to define entity operations links before they are all altered by the existing hook_entity_operation_alter().

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Xano’s picture

Status: Active » Needs review
FileSize
6.08 KB
plach’s picture

Well, we definitely need something like this at least.

+++ b/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
@@ -34,6 +35,27 @@ class EntityListControllerTest extends UnitTestCase {
+   * @var \Drupal\user\RoleStorageControllerInterface|\PHPUnit_Framework_MockObject_MockObject
...
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
...
+   * @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject

These look weird :)

Xano’s picture

What's weird about it?

Status: Needs review » Needs work

The last submitted patch, 1: drupal_2165725_1.patch, failed testing.

dawehner’s picture

We do that kind of style all over the place.

plach’s picture

Yup, sorry, I had a very quick look and didn't notice it was a test.

Xano’s picture

Status: Needs work » Needs review
FileSize
803 bytes
6.09 KB

Status: Needs review » Needs work

The last submitted patch, 7: drupal_2165725_6.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
613 bytes
6.1 KB
joachim’s picture

+ *
+ * @return array
+ *   Operations array as returned by
+ *   \Drupal\Core\Entity\EntityListControllerInterface::getOperations().

It's usually normal for the hook docs to have the specification for the array structure, and the retrieval function to reference the hook docs.

Status: Needs review » Needs work

The last submitted patch, 9: drupal_2165725_9.patch, failed testing.

The last submitted patch, 9: drupal_2165725_9.patch, failed testing.

Xano’s picture

The problem is actually a bit bigger than I initially thought. EntityListControllerInterface only specifies getOperations(), but not buildOperations(). As we depend on interfaces, we have to use getOperations(), which at this moment does not cause the hooks to be invoked.

Xano’s picture

Status: Needs work » Needs review
FileSize
7.54 KB

This is very likely to break a bunch of things in core, but it does illustrate the problem mentioned in #13. I tried to solve this by moving all hook invocations to the implementation of EntityListControllerInterface::getOperations(), and the definitions of default operations to EntityListController::getDefaultOperations(). If this is okay, we will have to modify all existing list controllers slightly, as most overrides of getOperations() will then become overrides of getDefaultOperations().

damiankloip’s picture

Yeah, this certainly looks like a more sensible change to me. Why the hell do we have the alter hook in a method that is not a part of the interface? That doesn't really make alot of sense...

+1 to that change in principle.

Xano’s picture

This issue is necessary for #2165989: Add a Views field handler for multiple entity operations not to cause WTFs.

Xano’s picture

Any other feedback before I start converting current getOperations() implementations to getDefaultOperations()?

Xano’s picture

14: drupal_2165725_14.patch queued for re-testing.

Xano’s picture

To do:
- use a route name instead of a URL path in the hook_entity_operations() documentation.
- Move the sort to getOperations().

Xano’s picture

FileSize
11.16 KB
4.49 KB

I

  • Used a route name instead of a URL path in the hook_entity_operations() documentation.
  • Moved the sort to getOperations().
  • Renamed all implementations of getOperations() to getDefaultOperations() to make sure controllers' default operations can be altered through hook_entity_operation_alter().
joachim’s picture

Does getOperations() always get all of them?

What if you only wanted to show some of them?

Xano’s picture

That's currently impossible (except through hook_entity_operation_alter() and some black magic) and beyond the scope of this issue.

Xano’s picture

20: drupal_2165725_20.patch queued for re-testing.

damiankloip’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/EntityListController.php
    @@ -112,6 +112,24 @@ protected function getLabel(EntityInterface $entity) {
    +   *   self::getOperations().
    

    I guess this should not use $this yet? :)

  2. +++ b/core/lib/Drupal/Core/Entity/EntityListController.php
    @@ -176,13 +194,9 @@ public function buildRow(EntityInterface $entity) {
    -    $this->moduleHandler->alter('entity_operation', $operations, $entity);
    

    Thank you! This living here was a mistake, for sure.

  3. +++ b/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
    @@ -49,10 +71,32 @@ protected function setUp() {
    +    $list->getOperations($this->role);
    

    Shouldn't we assert the actual return value from getOperations() too?

  4. +++ b/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
    @@ -49,10 +71,32 @@ protected function setUp() {
    +    $this->entityListController = new TestEntityListController($this->entityType, $this->roleStorage, $this->moduleHandler);
    ...
    +    $list = new EntityListController($this->entityType, $this->roleStorage, $this->moduleHandler);
    

    What's going on here? Are we testing with TestEntityListController or EntityListController? :)

  5. +++ b/core/lib/Drupal/Core/Entity/EntityListController.php
    @@ -112,6 +112,24 @@ protected function getLabel(EntityInterface $entity) {
    +    $operations = array_merge($this->moduleHandler->invokeAll('entity_operation', array($entity)), $this->getDefaultOperations($entity));
    

    This means defaultOperations will always win over the hook declared operations if there is a key clash. Not saying it's wrong, but is that the desired behaviour?

Xano’s picture

FileSize
2.51 KB
12.11 KB

I guess this should not use $this yet? :)

It refers to the method on the same class and not necessarily to the one on the class the method is called on.

Shouldn't we assert the actual return value from getOperations() too?

Done.

What's going on here? Are we testing with TestEntityListController or EntityListController? :)

TestEntityListController is for the other test methods. This one doesn't need it.

This means defaultOperations will always win over the hook declared operations if there is a key clash. Not saying it's wrong, but is that the desired behaviour?

I did this on purpose. I don't want people to abuse the info hook as an alter hook just because it happens to work for now.

Xano’s picture

FileSize
12.11 KB

Re-roll.

Status: Needs review » Needs work

The last submitted patch, 26: drupal_2165725_26.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
2.43 KB
12.62 KB

Status: Needs review » Needs work

The last submitted patch, 28: drupal_2165725_28.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
9.51 KB
23.31 KB

There turned about to be a number of unconverted list controllers (thanks @herom!)

herom’s picture

Status: Needs review » Reviewed & tested by the community

I don't see anymore issues with this.

Xano’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
23.36 KB

Re-roll........

Xano’s picture

FileSize
23.36 KB

Re-roll because of field_ui.module.

Xano’s picture

FileSize
23.61 KB

Re-roll.

Status: Needs review » Needs work

The last submitted patch, 34: drupal_2165725_34.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
23.83 KB
803 bytes

Status: Needs review » Needs work

The last submitted patch, 36: drupal_2165725_36.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
Xano’s picture

Status: Needs review » Reviewed & tested by the community

RTBC per #31.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

drupal_2165725_36.patch no longer applies.

error: patch failed: core/modules/system/entity.api.php:699
error: core/modules/system/entity.api.php: patch does not apply

Xano’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
23.84 KB

Re-roll and RTBC, as everything applied, except for a tiny bit of documentation.

Xano’s picture

Issue tags: -Needs reroll
xjm’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record

I think this needs probably a change record, and draft change records are now required before commit. Please also search for existing change records that might need updates and link them here (or confirm that there are none). Thanks!

Xano’s picture

There is Entity list operations can now be altered. Should I just edit that one (especially because this patch changes the existing alters in core to definitions), or do we indeed want a dedicated change notice as well?

Xano’s picture

Status: Needs work » Reviewed & tested by the community
xjm’s picture

Looking at the two change records... the new draft at https://drupal.org/node/2198231 looks good, but https://drupal.org/node/2019575 is then a little confusing.

Can we merge the two? I.e., add everything from https://drupal.org/node/2019575 to the new draft at https://drupal.org/node/2198231, and then once this is committed, we can make the new change record live and unpublish the old one. Make sure to add a reference to #2004408: Allow modules to alter the result of EntityListController::getOperations on the new draft as well, I guess. Thanks!

xjm’s picture

Also, maybe the bottom section of https://drupal.org/node/1799642 needs an update?

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 41: drupal_2165725_41.patch, failed testing.

Xano’s picture

Xano’s picture

Status: Needs work » Needs review

41: drupal_2165725_41.patch queued for re-testing.

Xano’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs change record

RTBC, under the condition that the tests pass.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 41: drupal_2165725_41.patch, failed testing.

jibran’s picture

Status: Needs work » Needs review

41: drupal_2165725_41.patch queued for re-testing.

Xano’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, as the test failures were caused by a bug in HEAD.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 41: drupal_2165725_41.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
23.97 KB
Xano’s picture

Status: Needs review » Reviewed & tested by the community

RTBC, under condition that the tests pass.

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs work

The only thing that's worth marking this needs work for is that config_translation_entity_operation_alter() should be re-purposed to config_translation_entity_operation_info().

Also, as I'm writing this I realize the following: Why is this not called hook_entity_operation_info()? I think that would be more consistent.

I also don't really know why #1839516: Introduce entity operation providers was abandoned. I liked that issue a lot more ... :-/

The rest are nitpicks:

  1. +++ b/core/lib/Drupal/Core/Entity/EntityListController.php
    @@ -93,6 +93,24 @@ protected function getLabel(EntityInterface $entity) {
    +    $operations = array_merge($this->moduleHandler()->invokeAll('entity_operation', array($entity)), $this->getDefaultOperations($entity));
    

    Super minor but since we are using keyed arrays, I think something like

    $operations = $this->getDefaultOperations();
    $operations += $this->moduleHandler()...
    

    would be clearer.

  2. +++ b/core/modules/system/entity.api.php
    @@ -756,6 +756,27 @@ function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit
    + *   Operations array as returned by
    

    Maybe "An array of operations as ..."

  3. +++ b/core/modules/system/entity.api.php
    @@ -765,10 +786,13 @@ function hook_entity_bundle_field_info_alter(&$fields, \Drupal\Core\Entity\Entit
    +  // Alter the title and weight.
       $operations['translate'] = array(
    -    'title' => t('Translate'),
    -    'weight' => 50,
    -  ) + $entity->urlInfo('my-custom-link-template');
    +    'title' => t('Translate @entity_type', array(
    +      '@entity_type' => $entity->getEntityTypeId(),
    +    )),
    +    'weight' => 99,
    +  );
     }
    

    This is not altering but overriding the entire 'translate' operation.

  4. +++ b/core/tests/Drupal/Tests/Core/Entity/EntityListControllerTest.php
    @@ -49,10 +86,55 @@ protected function setUp() {
    +    \Drupal::setContainer($this->container);
    

    I know that that is not your fault but it is nothing but insane that we have to do this in a "unit" test.

Xano’s picture

Status: Needs work » Needs review
FileSize
23.68 KB

Re-roll because of #2120871: Rename EntityListController to EntityListBuilder, and addressed 1, 2, and 3 from #58.

Also, as I'm writing this I realize the following: Why is this not called hook_entity_operation_info()? I think that would be more consistent.

I disagree. The hook does not expose static data, but in fact acts upon the entity that is passed on.

Status: Needs review » Needs work

The last submitted patch, 59: drupal_2165725_59.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
26.92 KB
3.57 KB

There was one direct invocation of hook_entity_operation_alter() left, which I just changed to calling the respective list builder instead.

tstoeckler’s picture

This looks great. Sadly I have one more problem with this, see 2.

  1. +++ b/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php
    @@ -23,6 +23,7 @@
    + *     "list_builder" = "\Drupal\Core\Config\Entity\ConfigEntityListBuilder",
    
    +++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldOverview.php
    @@ -164,11 +154,9 @@ public function buildForm(array $form, array &$form_state, $entity_type_id = NUL
    +        '#links' => $this->entityManager->getListBuilder('field_instance_config')->getOperations($instance),
    

    So we're introducing a list builder here, but only using it for the operations? Hmm... It's definitely better than calling the hook manually, so yeah. And that's a step in the right direction, because field instances really should be using a *proper* list builder.

  2. +++ b/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php
    @@ -49,10 +86,55 @@ protected function setUp() {
    +    $operations = array(
    +      $this->randomName() => array(
    +        'title' => $this->randomName(),
    +      ),
    +    );
    ...
    +    $this->assertInternalType('array', $operations);
    +    $this->assertArrayHasKey('edit', $operations);
    +    $this->assertInternalType('array', $operations['edit']);
    +    $this->assertArrayHasKey('title', $operations['edit']);
    +    $this->assertArrayHasKey('delete', $operations);
    +    $this->assertInternalType('array', $operations['delete']);
    +    $this->assertArrayHasKey('title', $operations['delete']);
    

    I might be missing something but this is not actually testing that the operation that got added is there?

Xano’s picture

FileSize
27.16 KB
1021 bytes

So we're introducing a list builder here, but only using it for the operations? Hmm... It's definitely better than calling the hook manually, so yeah. And that's a step in the right direction, because field instances really should be using a *proper* list builder.

Yes, yes, and yes. It's also one of the consequences of not having list controllers or operation plugins.

I might be missing something but this is not actually testing that the operation that got added is there?

You were missing a fix! Gladly I had a spare one and added it to the patch.

Sadly I have one more problem

When I feel sad, I stop being sad and be awesome instead. True Story.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +drupaldevdays

Awesome, looks great!

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.x, thanks!

  • Commit 2cf8710 on 8.x by catch:
    Issue #2165725 by Xano: Introduce hook_entity_operation().
    

  • Commit 24526ae on 8.x by catch:
    Revert "Issue #2165725 by Xano: Introduce hook_entity_operation()."...
catch’s picture

Status: Fixed » Needs work
Xano’s picture

Status: Needs work » Needs review
FileSize
31.25 KB
5.05 KB

The problem was that field instance configuration entities did not have a list builder that provided operation links. The attached patch adds this.

Xano’s picture

FileSize
31.18 KB

Re-roll.

Xano’s picture

FileSize
31.96 KB
1.06 KB

Now with test coverage for field instance operations.

fago’s picture

Status: Needs review » Reviewed & tested by the community
  1. +++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldInstanceListBuilder.php
    @@ -0,0 +1,90 @@
    +
    

    Unnecessary new line.

  2. +++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldInstanceListBuilder.php
    @@ -0,0 +1,90 @@
    +    throw new \Exception('This class is only used for operations and not for building lists.');
    

    This seems a bit weird, but is a pre-existing condition. Having at least an exception now helps.

Thus, patch looks good and come with test coverage for the previous problem. RTBC again then.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 71: drupal_2165725_71.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
32.01 KB

Re-roll, and removed the redundant newline.

Status: Needs review » Needs work

The last submitted patch, 74: drupal_2165725_74.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review

74: drupal_2165725_74.patch queued for re-testing.

Status: Needs review » Needs work

The last submitted patch, 74: drupal_2165725_74.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
32.43 KB
7.79 KB

Fixed the failure, renamed FieldInstanceListBuilder to FieldInstanceConfigListBuilder and added an explanation for the exception to FieldInstanceConfigListBuilder::render().

Status: Needs review » Needs work

The last submitted patch, 78: drupal_2165725_78.patch, failed testing.

tstoeckler’s picture

+++ b/core/tests/Drupal/Tests/Core/Entity/EntityListBuilderTest.php
--- /dev/null
+++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldInstanceConfigListBuilder.php

+++ b/core/modules/field_ui/lib/Drupal/field_ui/FieldInstanceConfigListBuilder.php
@@ -0,0 +1,92 @@
+/**
...
+ * Contains \Drupal\field_ui\FieldInstanceListConfigBuilder.
...
+class FieldInstanceListConfigBuilder extends ConfigEntityListBuilder {

I don't think this works, because the filename doesn't match the class name.

Regardless we should add some tests for this. Since this broke HEAD already that means we are lacking in this area.

Xano’s picture

Status: Needs work » Needs review
FileSize
32.57 KB
785 bytes

Regardless we should add some tests for this. Since this broke HEAD already that means we are lacking in this area.

This *has* tests now (see #71), which is partly why the patch from #78 broke.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Oops, sorry, I missed that. Looks great!

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 81: drupal_2165725_81.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
32.57 KB
730 bytes
tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 84: drupal_2165725_84.patch, failed testing.

Xano’s picture

Status: Needs work » Needs review
FileSize
32.57 KB

Re-roll. The reject was

diff a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyListBuilder.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyListBuilder.php	(rejected hunks)
@@ -32,8 +32,8 @@ public function getFormId() {
   /**
    * {@inheritdoc}
    */
-  public function getOperations(EntityInterface $entity) {
-    $operations = parent::getOperations($entity);
+  public function getDefaultOperations(EntityInterface $entity) {
+    $operations = parent::getDefaultOperations($entity);
 
     if (isset($operations['edit'])) {
       $operations['edit']['title'] = t('edit vocabulary');
Xano’s picture

Status: Needs review » Reviewed & tested by the community

RTBC if the tests pass.

  • Commit b504423 on 8.x by webchick:
    Issue #2165725 by Xano: Introduce hook_entity_operation().
    
webchick’s picture

Category: Feature request » Task
Status: Reviewed & tested by the community » Fixed

This unblocks some other refactoring as well as some contrib stuff, so re-categorizing as a task vs. a feature request, since we're not committing those.

I didn't look at the patch at all, but since catch had already committed it earlier and it was rolled back due to a regression, now fixed + test covered, this seems good to go.

Committed and pushed to 8.x. Thanks!

tim.plunkett’s picture

Status: Fixed » Closed (fixed)

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