Series Parts:
- Part 1: Overview of Content and Config Entities in Drupal 8
- Part 2: Most Simple Entity with Bundles
- Part 3: Simple Content Entity with Bundles
- Part 4: Practical Content Entity with Bundles
Note: In this example the Content Entity has been renamed to “Practical” and the Config Entity has been renamed to “Practical Type”. This is because each example is actually its own module in the Github repo.
The Plan
- Per-bundle permissions and access control.
- Class Interfaces to follow best practices.
- Additional base fields for the Content Entity, making it more like Nodes.
- Name – serves as the entity’s “label”.
- Uid – serves as the “Owner” of the entity.
- Created – date entity was created.
- Changed – date the entity was last updated.
- Description field for the Config Entity.
- Better ListBuilders
The Code
Permissions
A minor update to the permissions yaml file allows us to specify a PHP callable that will return an array of dynamically generated permissions.
Permissions Generator
Next we need to create the callback we just told the permissions yaml file about, and we’ll do so with a new class. Note, this is the only class in all of the examples that doesn’t extend some core Drupal class. What goes in here is all up to you!
There isn’t much exceptional going on here.
- The entry point method referred to in the permissions yaml file simply loads all
PracticalTypeEntity
s, loops through them and executes thebuildPermissions()
method on each one. - The
buildPermissions()
method returns an array of dynamically defined permissions for each entity. - And I’m using the
StringTranslationTrait
to provide my class with thet()
method.
Access Control
The next part in implementing our custom permissions is to create a new “Access Control Handler”. This is done by extending the core EntityAccessControlHandler
class and overriding the checkAccess()
and checkCreateAccess()
methods.
As long as the logic and the permission pattern in checkAccess()
are correct, this is good to go. “But wait!”, you say, “Where did $entity->getOwnerId(); come from?”. Oh yeah, that’s a good question. For this permission/access control plan to work out, we need to update our Content Entity.
… Is it just me, or is this post getting really long? Oh well, no stopping now!
Content Entity
Time to make some significant improvements to our Content Entity. Take a look at this new version, and let’s see what all has changed.
New Entity Keys – uid
, label
, created
, and changed
. Take note of the label
as it is the only entity key we have aliased. Its alias is “name”, which means the base field “name” will be mapped to the Entity’s label
property, and the column created in the database for the label
property will be “name”.
Access Handler – Points to our new PracticalEntityAccessControlHandler
class.
Base Field Definitions – Now that we are using some entity_keys
that are not automagically converted into base fields, we need to manually define fields for the new ones. Summary of new base fields:
uid
– entity_reference field that targets user entities.name
– Simple string field. This is the field that will be aliased as the entity’s label.created
– This field will track the date/time for when the entity was created.changed
– This field will track the date/time for when the entity was last updated.
Getters & Setters – Simple functions that set, or return the values of some of our new fields. Note how there are no gettings & setters for the changed
field. The changed
getters & setters are provided by the EntityChangedTrait
.
preCreate()
method – Overrides the preCreate() method for the parent class. We’re using it to pre-populate the uid
field with the current user’s uid.
implements PracticalEntityInterface – Attempting to follow best practices, this entity is now programmed to an interface.
Content Entity Interface
The new PracticalEntityInterface
simply describes methods the PracticalEntity
must define.
In an ideal world, all of our classes are programmed to an interface. But for now, I’ve focused only on the two Entity classes.
Content Entity List Builder
Now that we have much more relevant information concerning each content entity we could display, let’s update the List Builder to show that information.
The important changes here are that we’re now showing our new fields on the Entity’s collection list. For us to show the created
and changed
fields as formatted dates instead of timestamps, we need to ask Drupal’s dependency injection service to provide us with the date formatter.
To accept dependency injected services, we need to create the static method createInstance()
and within it return a static instance of the object, passing in services from the dependency container as parameters. In the class’s constructor, we expect the new services to be passed in, and assign them to properties on the object.
Long story short, we can now use the date.formatter
and renderer
services within our object as the dateFormatter
and renderer
properties respectively.
Currently this class is only using the date.formatter
service to output the created
and changed
timestamps as dates. And though this example doesn’t use the renderer
, it is a common dependency we might want in the future.
Config Entity
The Config Entity (which handles our Bundles) has to be updated much less in order to provide a new description
field.
"description"
– added to the config_export
array, so that it is exported along with a Bundle’s configuration.
Getters & Setters – New methods for setting and returning the value of our description
property.
Protected Properties – Local class properties for storing the values of our entity.
implements PracticalTypeEntityInterface – Best practices!
Config Entity Interface
This interface doesn’t do much at the moment, but it’s worth noting that it extends the core provided interfaces for the Config Entity and Entity Description.
Config Entity Form
Little has changed with the Config Entity Form, but we need to create a new description
field so that the administrator can easily provide the Bundle’s description value.
Config Entity List Builder
The only thing that has changed in the Config’s List Builder is that we are now showing the value of the new description
field.
Results
There we have it. A fairly practical Custom Entity with Bundles. Complete with good administration experience, and some of the fields we all expect having worked with Nodes so much.
Future improvements: Some additional features you might like your custom entity to have are Revisions and Translations. But for now, this will do quite nicely.
Discussion
Really helpful, thanks!
Hello,
Thank you very much for this article which I followed successfully!
I wonder however: what are the benefits of ListBuilder over a custom view?
Thanks!
Hi Ludo,
I think a custom view would be the better option if you can package it up with the entity module, I just haven’t done that yet myself and I’m not sure what all it would take. If you know how to do that, or find a good resource that explains it, I’d love to know about it.
Thanks!
Hi Jonathan,
This how I achived replacing the ListBuilder by a view in my module:
1. Create a view
2. Export it in a yml file and remove first line (uuid)
3. Copy this file to config/install folder of your module
4. Remove list_builder attribute from your @ContentEntityType class
5. Change the route_name of the list to the route_name of the view in yourmodule.links.menu.yml
6. Change the route_name of the Add action to the route_name of the view in appears_on in yourmodule.links.menu.yml
7. Remove the ListBuilder class
Following resources helped me to get this result:
https://www.computerminds.co.uk/drupal-code/drupal-8-views-how-formulate-route-name
https://www.drupal.org/docs/8/api/entity-api/introduction-to-entity-api-in-drupal-8
http://subhojit777.in/create-views-programatically-drupal8/
Oh wow, that seems really straight forward. I’ll be updating my modules to this technique.
Thanks!
This is much better than the official documentation. Thank a lot. :)
Hi Jonathan,
it was really helpful, thanks :)
Do you have any idea how to change the entity “add-page” site? I add an image field to config entity and i would like to see it on this page.
Excellent series of posts, covering what is for many first-timers a difficult concept.
Don\’t be tempted to get Drupal Console generate:entity:content to do all the legwork. First of all, there is an incompatibility between DC 1.8.0 and D8.7.0_beta1, so I hit a few database update errors which were very difficult to reverse out of.
And secondly, you will learn so much more by taking time to read these posts in-depth.
thx much. :-)
Thanks for this post, very helpful.
I found a consistency problem in respect to what core\’s node module does with content permissions though. As can be seen in node.api.php\’s hook_node_access(), it first checks if the user has the \”edit any $type content\” permission and, if that\’s the case, it grants permission without taking into account who is the owner. This also looks to me the proper behaviour when saying \”Edit any…\”.
In your code\’s case, and if I\’m not mistaken, an user won\’t be able to edit/delete their own content entities unless they specifically have the \”edit/delete own…\” permission, even if they have the \”edit/delete any…\” one. I think it should either work like core\’s node or change the permission name to \”Edit/Delete others\’ $bundle_of\”, instead of any.
The same thing happens with the \”view\” permissions, but in this case node also takes into account the published status and other factors. So an user with the \”View any…\” permission won\’t be able to view their own entities unless they also have the \”View own…\” permission.
I fixed it by adding a hasPermission call to the $is_owner conditional:
if ($is_owner && $account->hasPermission(\”edit own $entity_type_id $bundle\”)) {
That same call happens inside AccessResult::allowedIfHasPermission but it\’s cached, so no extra queries.
And on a side note, there\’s also the `Drupal\\user\\EntityOwnerTrait` that handles the Owner stuff for you.
The best article about the custom entity creation I ever saw. Thanks.
Thank you very much for this article! I think a nice “extra” would be to allow the custom entity to use the menu UI so that the entity creation form allows you to add a menu entry as well. Would that be possible? And if yes, where should I start looking in order to create my own implementation for this functionality?