Decoupling Your Backend Code from Drupal (and Improving Your Life) with Wrappers Delight

If you've ever written a lot of custom code for a Drupal site, then you know it can be a tedious and error-prone experience. Your IDE doesn't know how Drupal's data structures are organized, and it doesn't have a way to extract information about configured fields to do any autocomplete or check data types. This leads to some frustrations:

  • You spend a lot of time typing out by hand all the keys in every array of doom you come across. It's tedious, verbose, and tiring.
  • Your code can contains errors your IDE won't alert you to. Simple typos can go unnoticed since the IDE has no idea how the objects and arrays are structured.
  • Your code is tightly coupled to specific field names, configured in the database. You must remember these, because your IDE can't autocomplete them.
  • Your code is tightly coupled to specific field types. (If you start off with a text field and then decide to switch to an email field, for example, you will find the value is now stored in a different key of the data array. You need to update all your custom code related to that field.)
  • It can be easy to create cross-site-scripting vulnerabilities in your code. You need to keep in mind all the field data that needs to be sanitized for output. It only takes one forgotten spot to open your site to attacks.

Wrappers Delight (https://www.drupal.org/project/wrappers_delight) is a development tool I've created to help address these issues, and make my life easier. Here's what it does:

  • Provides wrapper classes for common entity types, with getters and setters for the entities' base properties. (These classes are wrappers/decorators around EntityMetadataWrapper.)
  • Adds a Drush command that generates wrapper classes for the specific entity bundles on your site, taking care of the boilerplate getter and setter code for all the fields you have configured on the bundles.
  • Returns sanitized values by default for the generated getters for text fields. (raw values can be returned with an optional parameter)
  • Allows the wrapper classes to be customized, so that you can decouple your custom code from specific Drupal field implementation.

With Wrappers Delight, your custom code can be written to interface with wrapper classes you control instead of with Drupal objects directly. So, in the example of changing a text type field to an email type field, only the corresponding wrapper class needs to be updated. All your other code could work as it was written.

But wait, there's more!

Wrappers Delight also provides bundle-specific wrapper classes for EntityFieldQuery, which allow you to build queries (with field-level autocomplete) in your IDE, again decoupled from specific internal Drupal field names and formats. Whatever your decoupled CRUD needs may be, Wrappers Delight has you covered!

Getting Started with Wrappers Delight

To generate wrapper classes for all the content types on your site:

  1. Install and enable the Wrapper Delight module.
  2. Install Drush, if you don't already have it.
  3. At the command line, in your Drupal directory, run drush wrap node.
  4. This will generate a new module called "wrappers_custom" that contains wrapper classes for all your content types.
  5. Enable the wrappers_custom module, and you can start writing code with these wrapper classes.
  6. This process works for other entity types, as well: Users, Commerce Products, OG Memberships, Messages, etc. Just follow the Drush command pattern: drush wrap ENTITY_TYPE. For contributed entity types, you may need to enable a submodule like Wrappers Delight: Commerce to get all the base entity properties.

Using the Wrapper Classes

The wrapper classes generated by Wrappers Delight have getters and setters for the fields you define on each bundle, and they inherit getters and settings for the entity's base properties. The class names follow the pattern BundlenameEntitytypeWrapper. So, to use the wrapper class for the standard article node type, you would do something like this:

$article = new ArticleNodeWrapper($node);
$body_value = $article->getBody();
$image = $article->getImage();

Wrapper classes also support passing an ID to the constructor instead of an entity object:

$article = new ArticleNodeWrapper($nid);

In addition to getters that return standard data arrays, Wrappers Delight creates custom utility getters for certain field types. For example, for image fields, these will all work out of the box:

$article = new ArticleNodeWrapper($node);
$image_array = $article->getImage();
$image_url = $article->getImageUrl();
$image_style_url = $article->getImageUrl('medium');
$absolute_url = $article->getImageUrl('medium', TRUE);

// Get a full <img> tag (it's calling theme_image_style
// under the hood)
$image_html = $article->getImageHtml('medium');

Creating New Entities and Using the Setter Methods

If you want to create a new entity, wrapper classes include a static create() method, which can be used like this:

$values = array(
'title' => 'My Article',
'status' => 1,
'promote' => 1,
);
$article = ArticleNodeWrapper::create($values);
$article->save();

You can also chain the setters together like this:

$article = ArticleNodeWrapper::create();
$article->setTitle('My Article')
->setPublished(TRUE)
->setPromoted(TRUE)
->save();

Customizing Wrapper Classes

Once you generate a wrapper class for an entity bundle, you are encouraged to customize it to your specific needs. Add your own methods, edit the getters and setters to have more parameters or different return types. The Drush command can be run multiple times as new fields are added to your bundles, and your customizations to the existing methods will not be overwritten. Take note that Wrappers Delight never deletes any methods, so if you delete a field, you should clean up the corresponding methods (or rewrite them to get the data from other fields) manually.

Drush Command Options

The Drush command supports the following options:

  • --bundles: specify the bundles to export (defaults to all bundles for a given entity type)
  • --module: specify the module name to create (defaults to wrappers_custom)
  • --destination: specify the destination directory of the module (defaults to sites/all/modules/contrib or sites/all/modules)

Packaging Wrapper Classes with Feature Modules or Other Bundle-Supplying Modules

With the options listed above, you can export individual wrapper classes to existing modules by running a command like the following:

drush wrap node --bundles=blog --module=blog_feature

That will put the one single wrapper class for blog in the blog_feature module. Wrappers Delight will be smart enough to find this class automatically on subsequent runs if you have enabled the blog_feature module. This means that once you do some individual exports, you could later run something like this:

drush wrap node

and existing classes will be updated in place and any new classes would end up in the wrappers_custom module.

Did You Say Something About Queries?

Yes! Wrappers Delight includes a submodule called Wrapper Delight Query that provides bundle-specific wrapper classes around EntityFieldQuery. Once you generate the query wrapper classes (by running drush wrap ENTITY_TYPE), you can use the find() method of the new classes to execute queries:

$results = ArticleNodeWrapperQuery::find()
->byAuthor($uid)
->bySomeCustomField($value1)
->byAnotherCustomField($value2)
->orderByCreatedTime('DESC')
->range(0, 10)
->execute();

The results array will contain objects of the corresponding wrapper type, which in this example is ArticleNodeWrapper. That means you can immediately access all the field methods, with autocomplete, in your IDE:

foreach ($results as $article) {
$output .= $article->getTitle();
$output .= $article->getImageHtml('medium');
}

You can also run queries across all bundles of a given entity type by using the base wrapper query class:

$results = WdNodeWrapperQuery::find()
->byAuthor($uid)
->byTitle('%Awesome%', 'LIKE')
->execute();

Note that results from a query like this will be of type WdNodeWrapper, so you'll need to check the actual bundle type and re-wrap the object with the corresponding bundle wrapper in order to use the bundle-level field getters and setters.

Wrapping Up

So, that's Wrappers Delight. I hope you'll give it a try and see if it makes your Drupal coding experience more pleasant. Personally, I've used on four new projects since creating it this summer, and it's been amazing. I'm kicking myself for not doing this earlier. My code is easier to read, WAY easier to type, and more adaptable to changes in the underlying architecture of the project.

If you want to help me expand the project, here are some things I could use help with:

  • Additional base entity classes for common core and contrib entities like comments, taxonomy terms, and Workflow states.
  • Additional custom getter/setter templates for certain field types where utility functions would be useful, such as Date fields.
  • Feedback from different use cases. Try it out and let me know what could make it work better for your projects.

Post in the issue queue (https://www.drupal.org/project/issues/wrappers_delight) if you have questions or want to lend a hand.

Wayne Eaker
December 3, 2014