4

I've built a couple of custom entities in Drupal 8

  • Vendor, defined as such:

    /**
     * Defines the vendor entity.
     * 
     * @EntityType(
     *   id = "vendor",
     *   label = @Translation("Vendor"),
     *   module = "vendor",
     *   controllers = {
     *     "storage" = "Drupal\vendor\VendorStorageController",
     *     "render" = "Drupal\Core\Entity\EntityRenderController",
     *     "form" = {
     *       "default" = "Drupal\vendor\VendorFormController",
     *       "delete" = "Drupal\vendor\Form\VendorDeleteForm"
     *     }
     *   },
     *   base_table = "vendor",
     *   uri_callback = "vendor_uri",
     *   translatable = FALSE,
     *   fieldable = TRUE,
     *   entity_keys = {
     *     "id" = "vendor_id",
     *     "uuid" = "uuid",
     *     "label" = "name",
     *     "bundle" = "type"
     *   },
     *   bundle_keys = {
     *     "type" = "type"
     *   },
     *   route_base_path = "admin/vendors/types/manage/{bundle}"
     * )
     */
    
  • and VendorType:

    /**
     * Defines the vendor type entity.
     * 
     * @EntityType(
     *   id = "vendor_type",
     *   label = @Translation("Vendor Type"),
     *   module = "vendor",
     *   controllers = {
     *     "storage" = "Drupal\vendor\VendorTypeStorageController",
     *     "access" = "Drupal\vendor\VendorTypeAccessController",
     *     "list" = "Drupal\Core\Config\Entity\ConfigEntityListController",
     *     "form" = {
     *       "default" = "Drupal\vendor\VendorTypeFormController",
     *       "delete" = "Drupal\vendor\Form\VendorTypeDeleteForm"
     *     }
     *   },
     *   config_prefix = "vendor.type",
     *   entity_keys = {
     *     "id" = "id",
     *     "uuid" = "uuid",
     *     "label" = "label"
     *   }
     * )
     */
    

VendorType is a config entity and serves as the bundle for Vendor, in exactly the same way that the core Node/NodeType entity types do. I have all the boilerplate code in place mimicking the Node/NodeType relationship.

Everything's up and running, the entities work great in every respect, but I can't figure out a small detail. On the "Content types" admin page, the action links in the right hand column of each row have the "Manage Fields/Manage Display/etc" links, plus the usual "Edit/Delete" as inherited from EntityFormController::actions().

enter image description here

On my VendorType overview form, I only see the Edit and Delete links, not the field-specific ones.

enter image description here

(ignore the missing delete buttons, those particular types can't be deleted; the delete link shows up for manually added types)

I've tried to track it down but it seems like the node module is getting these links added for "free" as it were, without implementing anything explicitly to add them.

What do I need to do to take advantage of this little nicety and get the field-specific links added automatically for my own entity types?

  • 1
    +1 for asking something which I don't even think to do experiment & find the answer :) – RajeevK Aug 10 '13 at 17:33
  • 1
    Hint: Those are called entity operations. Contextual something usually refers to the links added by the contextual_links.module. – Berdir Aug 11 '13 at 9:20
8

Since the patch in Use hook_entity_operation_alter() for manage fields and manage display links has been deployed, there is now a easier way to alter the operations visible in a list of entities. Also, the Field UI module automatically adds some operations.
Differently from what the patch in that issue shows, the hook to implement is hook_entity_operation(), which is implemented as follows, from the Field UI module.

function field_ui_entity_operation(EntityInterface $entity) {
  $operations = array();
  $info = $entity->getEntityType();
  // Add manage fields and display links if this entity type is the bundle
  // of another and that type has field UI enabled.
  if (($bundle_of = $info->getBundleOf()) && \Drupal::entityManager()->getDefinition($bundle_of)->get('field_ui_base_route')) {
    $account = \Drupal::currentUser();
    if ($account->hasPermission('administer ' . $bundle_of . ' fields')) {
      $operations['manage-fields'] = array(
        'title' => t('Manage fields'),
        'weight' => 15,
        'url' => Url::fromRoute("entity.{$bundle_of}.field_ui_fields", array(
          $entity->getEntityTypeId() => $entity->id(),
        )),
      );
    }
    if ($account->hasPermission('administer ' . $bundle_of . ' form display')) {
      $operations['manage-form-display'] = array(
        'title' => t('Manage form display'),
        'weight' => 20,
        'url' => Url::fromRoute("entity.entity_form_display.{$bundle_of}.default", array(
          $entity->getEntityTypeId() => $entity->id(),
        )),
      );
    }
    if ($account->hasPermission('administer ' . $bundle_of . ' display')) {
      $operations['manage-display'] = array(
        'title' => t('Manage display'),
        'weight' => 25,
        'url' => Url::fromRoute("entity.entity_view_display.{$bundle_of}.default", array(
          $entity->getEntityTypeId() => $entity->id(),
        )),
      );
    }
  }

  return $operations;
}

Before that patch was deployed, you had to implement a list controller and use its getOperations() method. For example, for nodes that is done in NodeTypeListController, whose NodeTypeListController::getOperations() method contains the following code.

  if ($this->moduleHandler->moduleExists('field_ui') && user_access('administer node fields')) {
    $operations['manage-fields'] = array(
      'title' => t('Manage fields'),
      'href' => $uri['path'] . '/fields',
      'options' => $uri['options'],
      'weight' => 0,
    );
  }
  if ($this->moduleHandler->moduleExists('field_ui') && user_access('administer node form display')) {
    $operations['manage-form-display'] = array(
      'title' => t('Manage form display'),
      'href' => $uri['path'] . '/form-display',
      'options' => $uri['options'],
      'weight' => 5,
    );
  }
  if ($this->moduleHandler->moduleExists('field_ui') && user_access('administer node display')) {
    $operations['manage-display'] = array(
      'title' => t('Manage display'),
      'href' => $uri['path'] . '/display',
      'options' => $uri['options'],
      'weight' => 10,
    );
  }

The description for the class simply says "Provides a listing of node types." Looking at the plugin annotation for the NodeType class, you could notice a reference to NodeTypeListController.

@EntityType(
  id = "node_type",
  label = @Translation("Content type"),
  module = "node",
  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",
  },
  config_prefix = "node.type",
  entity_keys = {
    "id" = "type",
    "label" = "name",
    "uuid" = "uuid"
  }
)

The class defined as list controller for your Vendor Type entity uses the following code, for ConfigEntityListController::getOperations().

public function getOperations(EntityInterface $entity) {
  $operations = parent::getOperations($entity);
  $uri = $entity->uri();

  // Ensure the edit operation exists since it is access controlled.
  if (isset($operations['edit'])) {
    // For configuration entities edit path is the MENU_DEFAULT_LOCAL_TASK and
    // therefore should be accessed by the short route.
    $operations['edit']['href'] = $uri['path'];
  }

  if (isset($this->entityInfo['entity_keys']['status'])) {
    if (!$entity->status()) {
      $operations['enable'] = array(
        'title' => t('Enable'),
        'href' => $uri['path'] . '/enable',
        'options' => $uri['options'],
        'weight' => -10,
      );
    }
    else {
      $operations['disable'] = array(
        'title' => t('Disable'),
        'href' => $uri['path'] . '/disable',
        'options' => $uri['options'],
        'weight' => 20,
      );
    }
  }

  return $operations;
}

To get the same links, you should define a custom class that extends ConfigEntityListController and implements EntityControllerInterface, adding in the getOperations() method the necessary code.

  • Arrrgh how did I miss that?! Thanks :) – Clive Aug 10 '13 at 20:11
  • It's not that easy to find out: I searched the code using the "Manage fields" and "Manage display" strings, and found the NodeTypeListController; from its name I guessed it was the controller for the list of content types, and I searched for references to that class name. :) – kiamlaluno Aug 10 '13 at 20:18
  • 2
    There's an RTBC issue that adds manage fields links automatically if you use a list controller, you just need to specify that your vendor type entity provides bundles for vendor: drupal.org/node/2021063. The answer is still correct, because you do need a list controller, but maybe you want to update it when that issue is committed? – Berdir Aug 11 '13 at 9:19
  • Thanks @Berdir, it felt like there should be something automagic here! Nice that you've used bundle_of to match d7 entity API, makes transition a bit easier for those of us who are a bit slower trying out d8 – Clive Aug 11 '13 at 10:58
  • 2
    i didn't know that bundle_of existed just couldn't think of something better. – Berdir Aug 11 '13 at 12:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.