Learning Drupal 8: Day 2

Day 2 of my mission to learn Drupal 8.

Entities are used for everything (content and config, nodes, taxonomy terms, users,...) in Drupal 8

I want to build a social media aggregation tool that sucks content from a number of sources (twitter feeds, facebook, ...) and then lets views display these. I want an entity for different streams, and then I want an entity for each status posting, probably with a bundle per stream so I can store source-specific fields if necessary. I want each posting to have a published status so that I can curate the aggregated data.

I'm going to call it Bloom. I'm going to call the sources Stems and the statuses Petals.

Create my first entity

There's a useful console tool for Drupal 8 that can create boilerplate entity code for you. Wow that's useful because there's a lot to do.

rich@some-vm:~/web/docroot% console.phar  generate:entity:content --module bloom
Enter the entity class name [DefaultEntity]: BloomStem
Enter the entity name [bloom_stem]:
Add this to your hook_theme:
  $theme['bloom_stem'] = array(
    'render element' => 'elements',
    'file' => 'bloom_stem.page.inc',
    'template' => 'bloom_stem',
  );

 Generated or updated files

Site path: /var/www/d8/docroot
1 - modules/bloom/bloom.routing.yml
2 - modules/bloom/bloom.permissions.yml
3 - modules/bloom/bloom.links.menu.yml
4 - modules/bloom/bloom.links.task.yml
5 - modules/bloom/bloom.links.action.yml
6 - modules/bloom/src/BloomStemInterface.php
7 - modules/bloom/src/BloomStemAccessControlHandler.php
8 - modules/bloom/src/Entity/BloomStem.php
9 - modules/bloom/src/Entity/BloomStemViewsData.php
10 - modules/bloom/src/Entity/Controller/BloomStemListController.php
11 - modules/bloom/src/Entity/Form/BloomStemSettingsForm.php
12 - modules/bloom/src/Entity/Form/BloomStemForm.php
13 - modules/bloom/src/Entity/Form/BloomStemDeleteForm.php
14 - modules/bloom/bloom_stem.page.inc
15 - modules/bloom/templates/bloom_stem.html.twig

rich@some-vm:~/web/docroot% drush updatedb

This created an entity and all I need to be able to view them, list them, create/delete/edit them. I wanted to add some properties onto the entity; simple fields that will be part of the base table. I want to add a text field saying which source this was from, e.g. twitter.

<?php
// ...
class BloomStem extends ContentEntityBase implements BloomStemInterface {
  //... 
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    //... 
    $fields['stem_provider'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Source Type'))
      ->setDescription(t('The provider for this stem.'))
      ->setSettings(array(
        'default_value' => 'twitter',
        'max_length' => 30,
        'text_processing' => 0,
      ))
      ->setDisplayOptions('view', array(
        'label' => 'hidden',
        'type' => 'string',
        'weight' => -4,
      ))
      ->setDisplayOptions('form', array(
        'type' => 'string_textfield',
        'weight' => -4,
      ))
      ->setDisplayConfigurable('form', TRUE)
      ->setDisplayConfigurable('view', TRUE);

    //... 

Adding this code and running drush updatedb added the field! Nice. However, the default_value is ignored; it's not set in the entity create form, nor in the database table schema. As it turns out this is old code. I submitted a patch to the examples module to fix the examples code, and another patch to the drupal console project. The correct code is not to use setSettings() but do a separate call to setDefaultValue().

I tried to implement bundles...and crashed.

I wanted to use bundles with my "bloom stem" entities. That way I can have twitter account 'stems' that have fields required for that, and facebook account 'stems' that have likely different fields for those.

Bundles are themselves entities, config entities, and the Drupal console tool has a command to generate config entities, too. However, it does not (yet?) have the ability to create an entity with a bundle. And as of 24 June 2015, the drupal.org documentation page on entity bundles is a stub. At first it looked like I only needed to tweak a few AnnotationPI (ha ha) settings: bundle_label, bundle_entity_type, field_ui_base_route on the content entity and bundle_of on the config (type) entity. But I didn't get very far and then the site dies with an uncaught exception while trying to add a new entity so I obviously didn't get it right and I needed to step back to understand what's supposed to happen.

Thinking about 'node'

Coming from Drupal 7, and understanding how nodes work, I understand that entities with bundles ("content types" for node entities):

  • There's a list of entity bundles, i.e. for node, a list of content types. D7 and D8 path admin/structure/types

  • Each bundle has various paths/routes: (examples shown for node)

    • Edit admin/structure/types/{node_type}

      • Manage Fields

      • Manage form display (d8 only)

      • Manage display

    • Add

    • View (entity bundles do not typically have a View route.)

    • Delete

  • Each Entity may provide its own entity listing pages. e.g. for node this is admin/content

  • Each entity, then has:

    • Edit, i.e. node/{node}/edit

    • Add, i.e. node/add and node/add/{node_type}

    • Delete node/{node}/delete

    • View (there may be multiple routes here, e.g. preview/full/revisions...)

So that's a minimum of 9 routes, excluding those added by the Field API (under the bundle's Edit route).

Special routing for entities

Entities have some special routing/route names that reuses core components rather than their own controllers. In D8, node declares the following special entity routes: in bold are the ones mentioned above.

  • entity.node.preview
  • entity.node.version_history
  • entity.node.revision
  • entity.node_type.collection provides the list of content types.
  • entity.node_type.edit_form provides add/edit forms for content type settings; field API attaches here
  • entity.node_type.delete_form provides the delete confirmation form for the content type.

And these dynamically defined routes in NodeRouteProvider (referenced in route_provider annotation in node entity class)

  • entity.node.canonical provides main view path node/{node}
  • entity.node.delete_form provides node delete confirmation form
  • entity.node.edit_form provides node edit form.

As well as these:

  • node.add provides the add an entity form.
  • node.add_page
  • node.configure_rebuild_confirm
  • node.multiple_delete_confirm
  • node.revision_delete_confirm
  • node.revision_revert_confirm
  • node.type_add provides a form to add a new content type.

I cannot see where the route that defines admin/content is declared. It may be a View in D8?

Success!

First I had to change the add form to accept a bundle argument in bloom.routing.yml :

entity.bloom_stem.add_form:
  path: '/admin/bloom_stem/{bloom_stem_type}/add'
  defaults:
    _title: 'Add BloomStem'
    _controller: '\Drupal\bloom\Controller\BloomStemController::addForm'
  requirements:
    _entity_create_access: 'bloom_stem'

Then I had to remove the Add Bloom Stem local task link from bloom.links.action.yml because this was trying to generate a URL for entity.bloom_stem.add_form but this route now requires a bloom_stem_type entity, so it crashed when displaying the list. So I commented out:

#entity.bloom_stem.add_form:
#  route_name: entity.bloom_stem.add_form
#  title: 'Add BloomStem'
#  appears_on:
#    - entity.bloom_stem.collection
#    - entity.bloom_stem.canonical

For now I need to make my own URLs to add a 'stem' of a particular type (bundle), e.g. /admin/bloom_stem/twitter/add assuming that I have created a Bloom Stem Type (bundle) with machine name twitter.

I now have

  1. A page listing bundles - Bloom Stem Types - at /admin/config/system/bloom_stem_type From here I can add a new bundle; I can edit fields + displays for each type.

  2. A page listing Bloom Stem Entities at /admin/bloom_stem From here I can View/Edit/Delete these (but not Add)

Next challenge: make "Add" buttons/links for the bundles page

To me the obvious place for these is as an "operation" action on the bundle listing page; adding an entity of a particular bundle (type) should go along side editing the fields, display and other settings for that bundle.

I noticed that the annotations for the bundle config entity included a links section. Surely this was where to add my "Add Enitity link in", like so:

* @ConfigEntityType(
*   id = "bloom_stem_type",
*   label = @Translation("BloomStemType"),
*   bundle_of = "bloom_stem",
*   ...
*   links = {
*     "edit-form" = "entity.bloom_stem_type.edit_form",
*     "delete-form" = "entity.bloom_stem_type.delete_form",
*     "collection" = "entity.bloom_stem_type.collection",
*     "add-entity" = "entity.bloom_stem.add_form"
*   }
* )
*/

Then I would implement getDefaultOperations() on a custom ConfigEntityListBuilder class for my bundle entity:

class BloomStemTypeListBuilder extends ConfigEntityListBuilder {
  ...
  /**
   * Gets this list's default operations.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity the operations are for.
   *
   * @return array
   *   The array structure is identical to the return value of
   *   self::getOperations().
   */
  public function getDefaultOperations(EntityInterface $entity) {
    $operations = parent::getDefaultOperations($entity);

    $operations['add'] = array(
      'title' => $this->t('Add'),
      'weight' => 10,
      'url' => $entity->urlInfo('add-entity'),
    );

    return $operations;
  }

and override urlInfo() to provide the bloom_stem_type entity required to generate the route. However this did not work.

It turns out that the route name given as the value to the annotation links keys are apparently ignored(!) because the code that generates the link makes its own route name up by mangling the key and prepending entity.{entity_name}. The working solution was:

class BloomStemTypeListBuilder extends ConfigEntityListBuilder {
  // ...

  /**
   * Gets this list's default operations.
   *
   * This method calls the parent method, then adds in an operation
   * to create an entity of this type.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity the operations are for.
   *
   * @return array
   *   The array structure is identical to the return value of
   *   self::getOperations().
   */
  public function getDefaultOperations(EntityInterface $entity) {
    $operations = parent::getDefaultOperations($entity);

    $url = \Drupal\Core\Url::fromRoute('entity.bloom_stem.add_form', ['bloom_stem_type' => $entity->id()]);
    $operations['add'] = array(
      'title' => $this->t('Add'),
      'weight' => 10,
      'url' =>  $url,
    );

    return $operations;
  }
}

Random notes

Accessing values has changed from $entity->field to $entity->get('field') which gives you a FieldItemListInterface for the field, so to get the value of a field with a single item, it's one of these: (the following is copied from How Entity API implements TypedData API)

<?php
// The most verbose way.
$string = $entity->get('image')->offsetGet(0)->get('alt')->getValue();

// With magic added by the Entity API.
$string = $entity->image[0]->alt;

// With more magic added by Entity API, to fetch the first item
// in the list by default.
$string = $entity->image->alt;

Other learning

 

Tags: