I Wish I Knew This Three Years Ago

Introducing site builders to Entity Metadata Wrappers & EntityFieldQuery (before D8 makes them all irrelevant)

DrupalCamp Montréal 2015

Adam White

@adamwhite

Upper Rapids

http://www.upperrapids.ca

Hello from Niagara!

We're Niagara, Canada's only dedicated Drupal development shop.

We're a small agency with a huge output, building sites without taking a breath since the last days of Drupal 6.

Me & Drupal

I was a Java developer left holding the keys to an proprietary in-house CMS I didn't have a hand in building. Switching gears to Drupal allowed us to take part in a community and accelerated what we could take on.

I got to build tons of sites without a lot of time for introspection.

"Hands up if you're a site builder..."

Nothing's stopping me!

Drupal, as a framework, can best be though of as a set of guidelines for being awesome. But with all of PHP at your fingertips and multiple ways of creating the same result nothing stops you from making poor choices.

For those of us in tiny teams there's often nobody calling us out on writing completely bananas code.

Case Study

A few years back we helped a client rush out a prototype of a quiz-based e-learning app for professional sports referees. The client originally built it as an iPad app but his target audience wanted something on the web.

What we built worked great, but...

Case Study

Scores were saved directly to the MySQL database table using...

db_insert($table, array $options = array())

While our individual questions were nodes, we retrieved sets of questions using Views

views_get_view_result($name, $display_id = NULL)

...which is sort of weird and had caching issues, but seemed logical given that in my site builder mindset "views = lists of stuff"

Prototype to Product

Every weird choice we made didn't seem to have a negative repercussion... so we made them.

Years later the client returned having white-labelled his product and sold a number of them internationally. Suddenly we needed to take that old weird egg of a webapp and built it into a maintainable multilingual product we could build and maintain from an installation profile.

So let's do this right...

Re-building our quiz

While the in-game UI of the quiz will still be done in JavaScript we want to overhaul the guts of this system so that it would be flexible, maintainable, and plays as nicely with Drupal's conventions.

Before we start...

Everyone cool with Entities?

Querying Entities

Drupal 7 introduced EntityFieldQuery, a PHP class used for quickly building queries of entities and fields while abstracting any actual SQL.

We're going to use this in the function that generates our quiz questions instead of straight queries or Views.

Why Avoid Views?

  • Interacting with Views programmatically can be awkward
  • Increased readability of our code as no logic was hidden away in the view
  • It kept this core mechanic of our system out of the user space entirely
  • One less big module needed
  • We could handle our own caching

EntityFieldQuery


$query = new EntityFieldQuery();
$query
	->entityCondition('entity_type', 'node')
	->entityCondition('bundle', 'true_false_question')
	->propertyCondition('status',1)
	->propertyOrderBy('created', 'DESC')
	->range(0,10);

Notice the lack of semicolons between method calls. Each EntityFieldQuery (EFQ) method returns the same object the method was called on, so we can chain calls.

Returns

EntityFieldQuery returns the IDs of the found entities, keyed by entity type.


['node']
	[123]
	[125]
	[126]
['user']
	[34]
	[36]

entityCondition


$query = new EntityFieldQuery();
$query
	->entityCondition('entity_type', 'node')
	->entityCondition('bundle', 'true_false_question')
	->propertyCondition('status',1)
	->propertyOrderBy('created', 'DESC')
	->range(0,10);

The entityCondition method narrows down which type of entities you're querying, by both the type (nodes, users, taxonomy terms) and the bundle (content types for nodes). You can include more than one type of entity.

entityCondition


$query = new EntityFieldQuery();
$query
	->entityCondition('entity_type', 'node')

	->entityCondition('bundle',
		array('true_false_question','multiple_choice_question'),
		'IN')

	->propertyCondition('status',1)
	->propertyOrderBy('created', 'DESC')
	->range(0,10);

For multiple conditions an array is used as the second parameter and an operator is needed.

propertyCondition


$query = new EntityFieldQuery();
$query
	->entityCondition('entity_type', 'node')
	->entityCondition('bundle', 'true_false_question')

	->propertyCondition('status',1)

	->propertyOrderBy('created', 'DESC')
	->range(0,10);

Property conditions allow you to filter your results based on any column in the base table for the entity type. For nodes, this could be the published status, author, creation time, etc.

fieldCondition


	$query = new EntityFieldQuery();
	$query
	->entityCondition('entity_type', 'node')
	->entityCondition('bundle', 'true_false_question')
	->propertyCondition('status',1)
	->propertyOrderBy('created', 'DESC')

	->fieldCondition('field_topic', 'tid', $topic)

	->range(0,10);

Fields can also be used to filter your results, the method takes the Field Name (or array), a column name, and value to test against at a minimum. Optionally you can include an operator, delta group or language group.

Don't Mix Conditions

One limitation of EFQ is that it's not possible to query across multiple entity types. So you can't in a single query find "published nodes that were authored by users who were created in the last hour."

...that would be querying both node->status and user->created. However you can extend the query using hook_query_alter or hook_query_TAG_alter.

hook_query_TAG_alter


$query = new EntityFieldQuery();
$query
	->entityCondition('entity_type', 'node')
	->entityCondition('bundle', 'true_false_question')
	->propertyCondition('status',1)
	->addTag('random');

The addTag method allows you to hook into the query object which results from the EntityFieldQuery.


function MYMODULE_query_random_alter($query) {
	$query->orderRandom();
}

hook_query_TAG_alter


$query = new EntityFieldQuery();
$query
	->entityCondition('entity_type', 'node')
	->entityCondition('bundle', 'true_false_question')
	->propertyCondition('status',1)
	->addTag('debug');

Tip: Use this for debugging too!


function MYMODULE_query_alter($query) {
	if ($query->hasTag('debug') && module_exists('devel')) {
		dpq($query);
	}
}

Fixing our quiz

With EntityFieldQuery we're now retrieving our quiz questions in far less esoteric way.

What we want to do next is get away from storing our scores as data inserted with raw SQL. We want to make those into fielded Entities so we can leverage Drupal more.

Next, we need a module

Up until now we've been talking about stuff that's been part of the core entity API.

Entities were introduced late in the D7 development cycle, so it's an incomplete system. The Entity module fills out the gaps: CRUD, exportability, revisions, and...

Entity Metadata Wrapper

A wrapper class which makes dealing with entities and accessing property and field data much easier.

Your code, when using Metadata Wrappers, won't make your future self hate your current self. Let alone other people.

Arrays['Blegh']


$value = $node->field_maximum_points[LANGUAGE_NONE][0]['value'];

This is scary but your code is full of them (well, mine was). Not only are you hard-coding the language into the field lookup, you're also going to throw warnings when there's no data in that field.

Entity Metadata Wrapper


$wrapper = entity_metadata_wrapper('node', $node);
$value = $wrapper->field_maximum_points->value();

This, on the other hand, is lovely. I don't need to know if that field exists, I'm also no longer hard-coding the language. I can even load this value with just the NID for $node instead of a fully populated object.

Multilingual


// whole wrapper
$w = entity_metadata_wrapper('node', $node, array('langcode' => 'en'));

// individual field
$w->language('fr')->body->summary->value();

If you don't set it, the wrapper will use the default language, but you can set the language when you instantiate the class. Alternatively you can select language on a per-field or per-property basis.

Lazy Loading & Chaining

Think about having to do this...


$node = node_load($nid);
$author = user_load($node->uid);
$email = $author->mail;

Doing this the old way, I've had to load two entire entities from the database to get to that email address.

Lazy Loading & Chaining

...but with an Entity Metadata Wrapper:


$wrapper = entity_metadata_wrapper('node',$nid);
$email = $wrapper->author->mail->value();

With this method, the wrapper only needs to load as much as it needs to get to that email. This saves in database overhead and reads much cleaner.


$wrapper->author->mail = 'adam@upperrapids.ca';
$wrapper->save();

Populating an entity


$entity = entity_create('attempt', array('type' => 'attempt'));

$wrapper = entity_metadata_wrapper('attempt', $entity);

$wrapper->field_course->set($nid);
$wrapper->field_participant->set($user->uid);
$wrapper->field_finished->set($finished);
$wrapper->field_started->set($started);
$wrapper->field_correct->set($correct);
$wrapper->field_incorrect->set($incorrect);

$wrapper->save();

$createdId = $wrapper->getIdentifier();

Lists of items


foreach($wrapper->field_terms->getIterator() as $index => $term_wrapper) {
	$termLabels[] = $term_wrapper->label->value();
}

// or ...
foreach ($wrapper->field_terms->value() as $index => $term_wrapper) {
	$termLabels[] = $term_wrapper->label->value();
}

You can set taxonomy list values in numerous ways:


$tids = array(12,34,55);
$wrapper->field_category_terms->set($tids);

// or...
$wrapper->field_category_terms[] = 77;

Exceptions

Entity Metadata Wrapper throws an EntityMetadataWrapperException if you make it angry, so it's good practice to use try...catch blocks when using it:


try {
	$wrapper = entity_metadata_wrapper('node', $nid);
	$courseCode = $wrapper->field_related_course->field_code->value();
}
catch (EntityMetadataWrapperException $exc) {
	// panic and go hide in the woods
}

Inspecting wrappers

Entity Metadata Wrapper has a handy function called getPropertyInfo which will tell you which properties are available from a wrapper.


$wrapper = enitity_metadata_wrapper('user',$uid);
dpm($wrapper->getPropertyInfo());

What about Drupal 8?

While entities were introduced to D7 late in the development cycle, in D8 they're what most of the CMS is built upon. We're now properly object oriented and won't need the Metadata Wrapper to act as a pseudo-object to as a kludge. The Entity module as it is today will likely not be needed.

D8: Entities

Entities in D8 are now typed classes with methods, some are generic:


	$entity->id():

Others have entity-type specific methods defined in their interfaces:


	$node->getTitle();

D8: Entity Takeover

As in D7, content (nodes, taxonomy terms, users, etc) are entities, but D8 also represents many configuration concepts as entities as well (actions, menus, image styles, etc).

These configuration entities all extend the Entity class but they're non-fieldable.

That means you get even more transferable benefits from learning the Entity API!

D8: No Wrapper!

The sensible way that you'd expect to access information in an object oriented framework is what we're shooting for in D8, and it should make custom code appear far less esoterically Drupally.


$node = Drupal\node\Entity\Node::load($id);
$node->field_greatest_band_ever->value = "The Clash";
$node->save();

// or...

$nodeStorage = \Drupal::entityManager()->getStorage('node');
$node = $nodeStorage->load($id);
$node->field_greatest_band_ever->value = "The Clash";
$node->save();

D8: No more EntityFieldQuery

Replacing it is an entity.query service that will instantiate a query object for a given entity type.


$query = \Drupal::entityQuery('node')
	->condition('status', 1)
	->condition('changed', REQUEST_TIME, '<')
	->condition('title', 'Foo', 'CONTAINS');
	->condition('field_other_cool_articles.entity.title', 'Bar');

$nids = $query->execute();

D8: Reference Chaining

Just as with Entity Metadata Wrapper, you can chain referenced entities:


$album->field_band->entity->get('field_lead_singer')->value;

$node->getOwner()->getUsername();

Thanks!

Adam White

@adamwhite

Upper Rapids