Entity Metadata Wrappers & EntityFieldQuery /
@adamwhite
Introducing site builders to Entity Metadata Wrappers & EntityFieldQuery (before D8 makes them all irrelevant)
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.
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.
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.
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...
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"
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.
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.
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.
$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.
EntityFieldQuery returns the IDs of the found entities, keyed by entity type.
['node']
[123]
[125]
[126]
['user']
[34]
[36]
$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.
$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.
$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.
$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.
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
.
$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();
}
$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);
}
}
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.
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...
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.
$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.
$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.
// 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.
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.
...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();
$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();
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;
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
}
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());
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.
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();
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!
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();
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();
Just as with Entity Metadata Wrapper, you can chain referenced entities:
$album->field_band->entity->get('field_lead_singer')->value;
$node->getOwner()->getUsername();
Thanks!