Clicks and logic: Bundle classes
We will see how to better organize your code by using Bundle Clases, which allows us to put some logic in custom classes for bundles we create by the Drupal GUI.
One of the first thing one does with Drupal is to create Nodes. You want to create a website, get Drupal, create the node budles, and set the fields for text, image, links, and whatever else you may want. Nodes come with so called bundles, which may be thought as types or categories. The article bundle may have different fields then, say, a portrait bundle: one will contain mostly text, the other, perhaps, images, links.
You can create node bundles and add fields very quickly by yourself, just by doing some clicking – and then change and update them at will, as well as define the various view modes. This makes creating basic data structures easy in Drupal, and whose structures and they are neatly exportable to .yml configuration code.
This raises a problem of how to write code related to the logic of the data represented by the bundle types. Imagine that you have a field that connects one bundle to another. Perhaps you want something to happen when the field is set to a given value – or your system to behave in a certain way, depending on the value of the field in question, or restrict access to a node based on some field’s value.
One solution is to create what’s called Custom entities. In fact, Nodes, Users, and many other aspects of drupal are just entities defined in PHP code. You can code custom Entity Classes in PHP and base your system around that. That means, however, that you start you building your site by coding, not by clicking – which in some cases may not be fitting with our approach (although it may very well be in some others!)
A recent advancement in Drupal is the ability to create classes for the bundles that you define via the Graphical User Interface. So you can go and click whatever you wish, and then have a regular PHP class where you may want to put your related logic – getters and setters for fields, access methods, whatever you wish.
How to do that, then? We shall start by having a Drupal 10 site set up with the glorious DDEV system https://ddev.readthedocs.io/en/latest/users/install/ddev-installation/#__tabbed_1_2 – you have then a solid base for containerized development of your work. The example application is available in Ratioweb’s GitHub repository https://github.com/RatioWeb/EntityStoreBlog/tree/main. Ddev comes with drush built-in, and drush has now commands for creating the bundle classes.
Basic Site Configuration
Lets consider a site called “Entity Store”. The site deals with Stores, Customers and Partners. Those will be our 3 node bundles. you can clone the repository, start ddev, and import the initial database.
git clone git@github.com:RatioWeb/EntityStoreBlog.git cd entityStoresBlog git checkout init ddev start ddev composer install
At the end of the `web/sites/default/settings.php` we add a line
$settings['config_sync_directory'] = $app_root .'/../config';
We use the branch `init` where the basic config is set up. This will give you and barebones site with default drupal config at https://entitystoresblog.ddev.site/. We then import the basic db, placed in the root of the git dir (a step above what nginx serves).
ddev import-db --file=init_entities.sql.gz dev drush cim -y
The last command should give no important updates, but some minor ones may be present due to version changes etc.
You can now login to the page by issuing
ddev drush uli
The system has now a basic drupal with also entity and devel_entity_updates downloaded by composer and installed
composer require drupal/entity composer require drupal/devel_entity_updates drush en -y entity devel_entity_updates drush cex -y
But that’s already done at this point of git, db and config.
The database we import has three custom bundles set up: EntityStore Customer, EntityStore Partner, EntityStore Store. They have a typical Drupal `body` field, but we won’t use it for now.
What’s more at this stage we already have 3 custom modules: entitystore_customer, entitystore_partner, entitystore_store, located at web/modules/custom/. The modules are just placeholders, there’s an .info.yml, a .module, an .install and a README.md file – but that’s all. It’s in those modules that we’re going to put the custom classes for the relevant bundles.
By the way, if you wish create modules manually, you can use a drush command `ddev drush generate module` and answer a few questions (your input is marked by ➤):
ddev drush generate module Welcome to module generator! –––––––––––––––––––––––––––––– Module name: ➤ EntityStore Store Module machine name [entitystore_store]: ➤ Module description: ➤ Functionality related to the stores. Package [Custom]: ➤ EntityStore Dependencies (comma separated): ➤ Would you like to create module file? [No]: ➤ yes Would you like to create install file? [No]: ➤ yes Would you like to create README.md file? [No]: ➤ yes The following directories and files have been created or updated: ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– • /var/www/html/web/modules/entitystore_store/entitystore_store.info.yml • /var/www/html/web/modules/entitystore_store/entitystore_store.install • /var/www/html/web/modules/entitystore_store/entitystore_store.module • /var/www/html/web/modules/entitystore_store/README.md
and then you enable them:
ddev drush en -y entitystore_customer,entitystore_partner,entitystore_store
And you do get a basic structure of a module. Drush is built-n in ddev so you’re good to go, but at this point we have set up everything for you.
Setting up bundle clases
Ok, so we thus have bundles. When we work in code, instances of the classes will be objects of the very same class Node(web/core/modules/node/src/Entity/Node.php), a core drupal class that we should never modify. What we would like to get is to get custom classes for each bundle in our custom modules, so we can enter custom code each for them (and some that would be common to just those three).
We issue another drush command:
ddev drush generate entity:bundle Welcome to bundle-class generator! –––––––––––––––––––––––––––––––––––– Module machine name: ➤ entitystore_customer Entity type: [ 1] Content block [ 2] Comment [ 3] Contact message [ 4] File [ 5] Custom menu link [ 6] Content [ 7] URL alias [ 8] Shortcut link [ 9] Taxonomy term [10] User ➤ 6 Bundles, comma separated: [1] Article [2] EntityStore Customer [3] EntityStore Partner [4] EntityStore Store ➤ 3 Class for "EntityStore Partner" bundle [EntitystorePartner]: ➤ EntityStorePartner Use a base class? [No]: ➤ No The following directories and files have been created or updated: ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––– • /var/www/html/web/modules/custom/entitystore_customer/entitystore_customer.module • /var/www/html/web/modules/custom/entitystore_customer/src/Entity/Node/EntityStorePartner.php
We repeat this three times providing the relevant module machine name, content type (content means node) and the relevant bundle. We thus get three new classes:
web/modules/custom/entitystore_customer/src/Entity/Node/EntityStoreCustomer.php web/modules/custom/entitystore_partner/src/Entity/Node/EntityStorePartner.php web/modules/custom/entitystore_store/src/Entity/Node/EntityStoreStore.php
The example content of the class is:
<?php declare(strict_types = 1); namespace Drupal\entitystore_customer\Entity\Node; use Drupal\node\Entity\Node; /** * A bundle class for node entities. */ final class EntityStoreCustomer extends Node { }
This is where we can put our custom code. Drush also creates a procedural hook in the .module file so Drupal knows where to look for classes for those bundles:
function entitystore_customer_entity_bundle_info_alter(array &$bundles): void { if (isset($bundles['node']['entitystore_customer'])) { // phpcs:ignore Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing $bundles['node']['entitystore_customer']['class'] = EntityStoreCustomer::class; } }
Come to think of it, you can forward to branch ‘feature/102-enitity-bundle-classes’ and see the whole thing nicely done for you – just clear the cache and you will have the classes ready.
On that branch you will also get a controller (created by `drush generate controller`) `web/modules/custom/entitystore_store/src/Controller/EntityStoreTestController.php` which is a kinda test implementation. For now it creates the entities and checks whether they have all our custom classes:
public function __invoke(): array { $partner = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'type' => 'entitystore_partner', ] ); $customer = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'type' => 'entitystore_customer', ] ); $store = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'type' => 'entitystore_store', ] ); $is_custom_bundle_classes = is_a($partner, EntityStorePartner::class) && is_a($customer, EntityStoreCustomer::class) && is_a($store, EntityStoreStore::class); $result = $is_custom_bundle_classes ? $this->t('Test Passed') : $this->t('Test Failed'); $build['content'] = [ '#type' => 'item', '#markup' => $result, ]; return $build; } }
Example: Duplicate Save Protection
Drupal Entity API provides a tool for creating a safe duplicate of entities by means of the method EntityInterface::createDuplicate(), which clones the entity by PHP clone() makes sure that the new entity is set as new etc, has its own UUID etc. Now we may want to make sure that for our custom entity duplicates can’t be saved. Let us add the following code to the EntityStorePartner class:
final class EntityStorePartner extends Node { protected bool $isDuplicate = FALSE; /** * {@inheritDoc} */ public function createDuplicate() { $duplicate = parent::createDuplicate(); $duplicate->setUnpublished(); $duplicate->setDuplicate(TRUE); return $duplicate; } /** * {@inheritDoc} */ public function save() { if (!$this->getIsDuplicate()) { return parent::save(); } else { return FALSE; } } /** * Checks whether the entity is a duplicate. */ public function getIsDuplicate():bool { return $this->isDuplicate; } /** * Flags as duplicate. * * @param bool $is_duplicate * * @return void */ protected function setDuplicate(bool $is_duplicate = TRUE) { $this->isDuplicate = $is_duplicate; } }
We have a instance variable flag isDuplicate which is set during the overridden ::createDuplicate() method. We also override the ::save() method, so we can check if the flag is set and if so, we do not proceed with saving, returning FALSE instead. So now whenever a Partner node is duplicated – be it in your custom module or not – it wont be saved (unless you run $partner-> setDuplicate(FALSE) before saving).
We expand then our test Controller:
/** * Builds the response. */ public function __invoke(): array { $partner = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'title' => 'A Partner example', 'type' => 'entitystore_partner', ] ); $customer = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'title' => 'A customer example', 'type' => 'entitystore_customer', ] ); $store = $this->entityTypeManager->getStorage('node')->create( [ 'status' => 1, 'title' => 'A Store example', 'type' => 'entitystore_store', ] ); $is_custom_bundle_classes = is_a($partner, EntityStorePartner::class) && is_a($customer, EntityStoreCustomer::class) && is_a($store, EntityStoreStore::class); $messages[] = $is_custom_bundle_classes ? $this->t('Success: Custom bundle Classes creation test OK</br>') : $this->t('Fail: Custom bundle Classes creation test FAILURE</br>'); $partner_duplicate = $partner->createDuplicate(); $partner_save_result = $partner->save(); $partner_duplicate_save_result = $partner_duplicate->save(); $messages[] = $partner_save_result && !$partner_duplicate_save_result ? $this->t('Success: Duplicate save protection OK</br>') : $this->t('Failure: Duplicate save protection FAILURE </br>'); $result = ''; foreach ($messages as $message) { $result .= $message; } $build['content'] = [ '#type' => 'item', '#markup' => $result, ]; return $build; }
}
```
The whole thing is available in git branch feature/104-duplicate-save-protection (just remember to import the config and clear the cache)
Conclusion
This is just an example of what Bundle Classes can do for you. Just having one’s own private variables with setters and getters may be actually quite helpful. Stay tuned for a follow-up to this piece.
ambitious projects and people