Extending EntityDrupalWrapper
We'd all like to be writing better OO code with Drupal 7, wouldn't we?
It's always been a best practice of ours to use the entity_metadata_wrapper() function when programming the additional logic that inevitably comes when constructing complex content types. But a pain point of this is that the code usually ends up in one hook_ implementation or another and any shared code tends to get arranged into functions in the .module file.
In an ideal world, Drupal would have provided us with entity classes that we could extend at the bundle level to allow us to add feature-specific functionality. I had a bit of a eureka moment the other day, when I realised that we’re already quite close to this, and that it's actually quite trivial to extend the EntityDrupalWrapper class provided by entity_metadata_wrapper() with functionality specific to each entity type.
Indeed, as I think about it, I'm wondering if this should be a new best practice?
An overly simple example
Let’s imagine that we have a content type that is tied up with Organic Groups and that we use it as a way of grouping members into different organisations on our site. We’ll call it “company”.
Let’s also imagine that we would like to send an email to each of the members of a company whenever one of them posts a comment.
For the convenience of this example, let's forget that Rules exist for a moment!
Firstly, a quick note about autoloading
Drupal 8 takes advantage of the wonderful PSR-4 autoloading standard so that we no longer have to define includes in our module's .info file. The brilliant X Autoload module provides just the same functionality for Drupal 7. Since we’re going to be writing a lot more classes from now on, I’d recommend using it, especially as what we’re doing supports PHP 5.3 namespaces. My example below assumes that you're using it.
The old way
So you’d probably start with a hook_comment_insert, and in there you’d probably grab the user, load the company that they belong to and start looping.
The problem with this approach is that everything is inside of the hook implementation and if you take it out of the hook implementation, you’ll just be littering the code base with API-like functions. Your modules are soon going to get messy. Additionally, if you need to do something else non-related in hook_comment_insert() things are going to get even more messy.
The new way(?)
What if in your hook_comment_insert, instead you just called a function on an entity wrapper?
$author_wrapper = new UserWrapper($comment->uid); $author_wrapper->emailColleagues([...]);
Then, in my_module/src/EntityWrapper/User/UserWrapper.php (Drupal\my_module\EntityWrapper\User\UserWrapper.php) you’d make your class:
<?php /** * @file * Firm Profile wrapper class. */ namespace Drupal\my_module\EntityWrapper\User; use \EntityDrupalWrapper; use Drupal\my_module\EntityWrapper\Node\CompanyWrapper; /** * Wraps nodes of type firm_profile with additional functionality. */ class UserWrapper extends EntityDrupalWrapper { /** * Wrap a user object. * * @param int|object $data * A uid or user object. */ public function __construct($data) { parent::__construct('user', $data); } /** * Send an email to all colleagues of this user. * * @param mixed $some_args * Whatever is needed here. */ public function emailColleagues($some_args) { foreach ($this->getColleagues() as $colleague) { $colleague->email($some_args); } } /** * Send an email to this user. * * @param mixed $some_args * Whatever is needed here. */ public function email($some_args) { // Call something like drupal_mail() here; } /** * Get a list of colleagues in the same company as this user. * * @return UserWrapper[] * An array UserWrappers. */ public function getColleagues() { $colleagues = array(); foreach ($this->getCompany()->getEmployees() as $employee) { if ($employee->getIdentifier() !== $this->getIdentifier()) { $colleagues[] = $employee; } } return $colleagues; } /** * Get the company for this user. * * Note that we can cleanly wrap relationships between entities. * * @return CompanyWrapper * A company wrapper object. Check it with ->value() if you * need to make sure it has data. */ public function getCompany() { return new CompanyWrapper($this->field_company->raw()); } }
How simple is that?! Look at all those reusable functions that we've placed directly against the relevant class. You’ll call getCompany() all the time and if you add functionality to the CompanyWrapper class then that will be ready to use as well.
I’m quite convinced that my code is going to start looking very different from now on!