I'm currently working on a Drupal 8 module including a custom entity type, which also should have bundle support. As I'm still not 100% familiar with D8 and there's still much D8 documentation lacking at the moment, also not many well-written tutorials to certain topics available, I've naturally stepped into several pitfalls. But I've finally made it and I want to share my experience with you.

Prerequisites

Tools

First of all, using Drupal Console is very recommended - no wait, I'd say it's mandatory. Drupal Console is a CLI tool that is able to generate boilerplate code for modules, entities, controllers, and many more. No matter, if you're already very experienced in D8 or not, in both cases it saves you a lot of time by freeing from doing the initial copy & paste work to create your module/entity skeleton code. If you're a D8 newbie, it's even more important for you because it teaches you, how to setup routes, permissions, etc. This tutorial builds up on the code generated by Drupal Console.

While not necessary, I also recommend the usage of DrupalVM for developing your Drupal 8 site. It helps you to spin up a local Drupal dev environment very quickly and easily. It's highly configurable and you get a virtual machine packed with everyting you need to run and develop a Drupal website. That also includes Drush and Drupal Console. You don't have to mess around with configuring XAMPP/WAMP/MAMP to your needs, you just can start working on your project :-)

Reading

I've found some blog posts that really helped me a lot on my mission. And I fully recommend to read them before/during studying my post.

If you're new to Drupal Console, you should start with Kwok Wai's post on how to generate modules and entity types with Drupal Console.

Next, there are the great and detailled "Learning Drupal 8" posts from Rich Lott. Day 2 is also about creating entity types with bundles. But I still had some problems during development - maybe I've only missed out some points because I came across his post, when I was already busy working on my module. Finally, after your code basically works and you are able to create your custom entities with bundles, you should read on Day 3 and you'll learn, how you can actually define specific bundles and include them in your module.

In Drupal 8, basically everything's an entity, even the bundle definitions are. So it's important to know the difference of content entities and the new config entities. In Drupal 7, we only have content entities. Now, the concept of config entities was introduced as a base for the new configuration management, that is fully im- and exportable. Node types and any other entity bundles are config entities. If you want to know more about that, Lee Rowlands "Understanding Drupal 8s config entities' is a great starting point.

And there's also no info hook more to define your entity type. This is done by annotations now. Have a look at the Drupal documentation to see an example of an entity annotation.

And as always in the past, having a look at Drupal core's code is always a good option, when you need an example, how certain thing should be done. In this case, looking at the node module, especially on the entity and controller classes, helped me a lot.

Creating the boilerplate code

As I've already mentioned, we'll use Drupal Console to generate the base for our module:

  1. Generate the module
  2. Generate the config entity
  3. Generate the content entity
  4. Generate the controller with an "add" function/route

You then have a base module, containing all the stuff to create entities and bundles, add fields, edit/delete them,... - but your bundle definition (config entity) is not yet connected with your entity model. (This was written on 2015-09-11 - it is likely that this will be possible in future versions of Drupal Console).

Let's assume for the rest of this tutorial, that you've called your content entity "foo" and the config entity "foo_type" (class names "Foo" and "FooType").

Add bundle capability to your entity

First you go to your FooType class and change/add these things:

  • add the following line to the annotation: bundle_of = "foo",
  • let the class extend ConfigEntityBundleBase instead of ConfigEntityBase (and do not forget to also change the namespace "use" declaration on the top of the file)

Then go over to the Foo class and add these things (assuming you want to store the bundle as "type" column in the entity's database table):

  • add the following line to the annoation: bundle_entity_type = "foo_type",
  • also add a "bundle_label" to the annotation.
  • and also add the following line to the entity_keys in the annotation: "bundle" = "type",

Further, add this function to Foo class:


  public function getType() {
    return $this->bundle();
  }

Finally, add this declaration inside the "baseFieldDefinitions" function of Foo class to add the database column:


    $fields['type'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Type'))
      ->setDescription(t('The foo type.'))
      ->setSetting('target_type', 'foo_type')
      ->setReadOnly(TRUE);

Note, that 'entity_reference' is used as field definition here, as we're referencing a config entity.

Adjust the entity creation page

From the perspective of the data model, we're finished here, as we've successfully connected our config entity with our content entity and made it bundle capable. However, you'll bump your head against the wall, if you'd now try to a new Foo entity using the "entity.foo.add_form" route (defined by Drupal Console in foo.routing.yml file) because the only thing you will see, is a HTTP 500 error, caused by an exception, that you can find in watchdog's log. This is because the bundle property of the (empty and unsaved) foo entity, that should be attached to the form, is not set yet. And this causes an exception.

Well, we could now set a default value in the preCreate function of our Foo class and add an "type" select list in our creation form. But this would not fit to the "Drupal way". Think of nodes: the "node/add" page is just a listing of all available node types linked to their creation page, like node/add/page or node/add/article. So inspired by node module, we also want to add such an general overview page and change our creation page to accept the bundle, we want to use, via URL argument.

First, open the "foo.routing.yml" file and look for the route "entity.foo.add_form". We'll modifiy this one to add the bundle parameter to its route. But before, there's another thing to do first. At the beginning I've also instructed you to create a controller via Drupal Console and add an "add" route. You should find this route at the bottom of the routing file. Copy the line starting with "_controller" from this route into clipboard and delete this route afterwards (we don't need it, as we change the add_form route instead). Next, go back to the "entity.foo.add_form" route and replace the line starting with "_entity_form" with the line in your clipboard. Also, change the path of this route from "/admin/foo/add" to "/admin/foo/add/{foo_type}". Optionally, we can also add a title callback. To do so replace the "_title" line with:


_title_callback: '\Drupal\foo\Controller\FooController::addPageTitle'

Also, we can also add the route for our overview page:


entity.foo.add_page:
  path: '/admin/foo/add'
  defaults:
    _controller: '\Drupal\foo\Controller\FooController::addPage'
    _title: 'Add foo'
  requirements:
    _permission: 'add foo entities'

Controller class

I've oriented very much on the NodeController class, which is a perfect prototype for the functions we have to create. DrupalConsole should already have inserted a function with the name "add", which only delivers a dummy response. Based on NodeController's add function, we rewrite it to fetch the URL argument and return the entity creation form:


  public function add(FooTypeInterface $foo_type) {
    $foo = $this->entityManager()->getStorage('foo')->create(array(
      'type' => $foo_type->id(),
    ));

    $form = $this->entityFormBuilder()->getForm($foo);

    return $form;
  }

If you've added the title callback to your route, you have to implement the callback, e.g. like this:


  public function addPageTitle(FooTypeInterface $foo_type) {
    return $this->t('Create @name', array('@name' => $foo_type->label()));
  }

Finally, we have to implement the page callback for the overview page, again strongly based on NodeController's pendant.


  public function addPage() {
    $content = array();

    // Only use foo types the user has access to.
    foreach ($this->entityManager()->getStorage('foo_type')->loadMultiple() as $type) {
      //TODO if ($this->entityManager()->getAccessControlHandler('foo')->createAccess($type->id())) {
        $content[$type->id()] = $type;
      //}
    }

    // Bypass the foo/add listing if only one foo type is available.
    if (count($content) == 1) {
      $type = array_shift($content);
      return $this->redirect('entity.foo.add_form', array('foo_type' => $type->id()));
    }

    return array(
      '#theme' => 'foo_add_list',
      '#content' => $content,
    );
  }

I've commented out the access check part here, as this can be added and tested later on. First, we are happy to build the page without the access checks. Similar to NodeController, we redirect to the creation page, if only one bundle is available. Note, that otherwise the list of the available bundles is generated by a theme function, we have called "foo_add_list" - again, this is copied from node module, where you can find a "node_add_list" theme function. I won't cover this now. Just have a look at the node module, or as a workaround you could stick together the markup within the page callback and return directly a "#markup" instead of the "'#theme" key - this works the same way as in Drupal 7.

Ready!

If you've followed all the steps, your bundle-capable custom entities should work, including all the administration pages, you'll basically need :-)

Kommentare6

Klartext

  • Keine HTML-Tags erlaubt.
  • Zeilenumbrüche und Absätze werden automatisch erzeugt.
  • Website- und E-Mail-Adressen werden automatisch in Links umgewandelt.
Der Inhalt dieses Feldes wird nicht öffentlich zugänglich angezeigt.
Nice article and thanks for
Nice article and thanks for mentioning the DrupalConsole, after reading this post I think we can implement this improvements on the project.
I wish you had expanded on
I wish you had expanded on the "Creating the boilerplate code" portion of your tutorial. The answers are mostly in the reading suggestions in your introduction, but knowing what you did step-by-step would have helped. Even if updates to Drupal Console had changed those steps, I could at least investigate the change records to see what I should be doing instead.

Lucas

vor 8 years 9 months

Here is some related work in
Here is some related work in this space that is trying to improve things: https://github.com/Jaesin/content_entity_base/issues/6 https://github.com/hechoendrupal/DrupalConsole/pull/1261

Narsing

vor 8 years 7 months

Nice Article, need a small
Nice Article, need a small information adding the revisions for these custom entities.
haven't tried out so far...

...but I would recommend to have a look at some core entities, e.g. \Drupal\node\Entity\Node class and search for "revision" in code. You'll need at least some class annotations, as well as setting your fields setRevisionable(TRUE).