Custom entity operation links 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().



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



(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?
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-&gt;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-&gt;getBundleOf()) &amp;&amp; \Drupal::entityManager()-&gt;getDefinition($bundle_of)-&gt;get('field_ui_base_route')) {
    $account = \Drupal::currentUser();
    if ($account-&gt;hasPermission('administer ' . $bundle_of . ' fields')) {
      $operations['manage-fields'] = array(
        'title' =&gt; t('Manage fields'),
        'weight' =&gt; 15,
        'url' =&gt; Url::fromRoute("entity.{$bundle_of}.field_ui_fields", array(
          $entity-&gt;getEntityTypeId() =&gt; $entity-&gt;id(),
        )),
      );
    }
    if ($account-&gt;hasPermission('administer ' . $bundle_of . ' form display')) {
      $operations['manage-form-display'] = array(
        'title' =&gt; t('Manage form display'),
        'weight' =&gt; 20,
        'url' =&gt; Url::fromRoute("entity.entity_form_display.{$bundle_of}.default", array(
          $entity-&gt;getEntityTypeId() =&gt; $entity-&gt;id(),
        )),
      );
    }
    if ($account-&gt;hasPermission('administer ' . $bundle_of . ' display')) {
      $operations['manage-display'] = array(
        'title' =&gt; t('Manage display'),
        'weight' =&gt; 25,
        'url' =&gt; Url::fromRoute("entity.entity_view_display.{$bundle_of}.default", array(
          $entity-&gt;getEntityTypeId() =&gt; $entity-&gt;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-&gt;moduleHandler-&gt;moduleExists('field_ui') &amp;&amp; user_access('administer node fields')) {
    $operations['manage-fields'] = array(
      'title' =&gt; t('Manage fields'),
      'href' =&gt; $uri['path'] . '/fields',
      'options' =&gt; $uri['options'],
      'weight' =&gt; 0,
    );
  }
  if ($this-&gt;moduleHandler-&gt;moduleExists('field_ui') &amp;&amp; user_access('administer node form display')) {
    $operations['manage-form-display'] = array(
      'title' =&gt; t('Manage form display'),
      'href' =&gt; $uri['path'] . '/form-display',
      'options' =&gt; $uri['options'],
      'weight' =&gt; 5,
    );
  }
  if ($this-&gt;moduleHandler-&gt;moduleExists('field_ui') &amp;&amp; user_access('administer node display')) {
    $operations['manage-display'] = array(
      'title' =&gt; t('Manage display'),
      'href' =&gt; $uri['path'] . '/display',
      'options' =&gt; $uri['options'],
      'weight' =&gt; 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-&gt;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-&gt;entityInfo['entity_keys']['status'])) {
    if (!$entity-&gt;status()) {
      $operations['enable'] = array(
        'title' =&gt; t('Enable'),
        'href' =&gt; $uri['path'] . '/enable',
        'options' =&gt; $uri['options'],
        'weight' =&gt; -10,
      );
    }
    else {
      $operations['disable'] = array(
        'title' =&gt; t('Disable'),
        'href' =&gt; $uri['path'] . '/disable',
        'options' =&gt; $uri['options'],
        'weight' =&gt; 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.