Eric

Working with entities in Drupal 7

Eric Pinxteren Drupal

Developers love object-oriented code. But how can this be achieved with Drupal 7 entities? By default Drupal uses a single class for all entities of a given type. For example, all node objects are standard classes (\stdClass) and all entity objects have the \Entity type. I personally like to have an entity model that only exposes the functionality that is applicable for the logic of your domain. Something like:

class Article {
  private $id;
  private $title;

  public function getTitle() {
    return $this->title;
  }

  public function publish() {
    // publish logic.
  }
}

Wouldn’t it be nice to create your own classes for entities?

What are entities?

 

First some refreshment about entities. A node or a term is an entity type inside Drupal that represents the base properties for that entity. These properties are mapped to a database schema:

blog

A bundle is a subtype of an entity. For instance a node can have bundles like an article, a slideshow or a blog. In Drupal you can attach fields to each individual bundle. So we have:

Blog

In-depth documentation about entities can be found on drupal.org https://www.drupal.org/node/1261744.

Access properties and fields

 

It’s always difficult to access field and properties of an entity object. This is because Drupal uses the same class for all your entities. This brings many disadvantage like no IDE autocompletion and you have no clue what kind of entity type you’re dealing with. You can't do contract checking just by looking at the object type, so you need to verify the entity type and bundle name everywhere in your code.

function hook_entity_update($entity, $type) {

   if ($type === 'node' && $entity->type === 'article') {
       // Yes it's a node, of the bundle type 'article'!
   }

   if ($type === 'taxonomy_term’ && 
       $entity->vocabulary_machine_name === 'tags') {
      // Yes it's a term, of the vocabulary bundle 'tags'!
   }
}

Of course most of the time you know what kind of entity type (node, term etc) you’re dealing with. But accessing the fields of a Drupal object is very cumbersome without any help.

$node = node_load(1);
$tag = $node->field_tags[LANGUAGE_NONE][0]['value'];

Drupal can make this a little bit easier by wrapping entity objects in a so-called 'metadata wrapper'. So let's wrap it up:

$node = node_load(1);
$wrapped_node = entity_metadata_wrapper('node', $node);
$tag = $wrapped_node->field_tags->value()[0];

The ‘metadata wrapper’ gives you magic methods to set and get each property or field an entity has. Using a wrapper is a bit more convenient than accessing the raw values in an entity object but it’s still ugly to read and you need to know many things like:

  • What kind of entity types there are. (taxonomy_term or node etc)
  • The names of the different fields.
  • How the fields are structured internally. (Like the name of a term)

These facts are all over your code base.

The solution for ‘all’ your entity problems.

 

Drupal supports defining your own classes for entities. But there is no support for separate classes for each individual bundle. The problem is that the different bundles do not expose the similar code interface. So we have created the module classable that supports custom classes for bundles.

You just need to enable the classable module and then register your custom class in the entity_info_alter hook:

function hook_entity_info_alter(&$entity_info) {
    // Set a base class on all entities of this type.
    $entity_info['<entity type>']['entity class'] = Node::class;

    // Set class on entity bundle.
    $entity_info['<entity type>']['bundles']['<bundle name>']['entity class'] =         
    Node::class;
}

The classable also provides simplified base classes for nodes and terms.

Let’s create some custom classes!

 

In the examples above we have the tag and article entities. Now lets create specific classes for these entities.

class Tag extends Drupal\classable\Entity\Term {

}

class Article extends Drupal\classable\Entity\Node {
    /**
     * Constructor.
     */
    public function __construct() {
        parent::__construct('article');
    }

    /**
     * Returns the tags.
     *
     * @return Tag[]
     *   The tags.
     */
    public function getTags() {
        return $this->wrapper()->tags->value();
    }
}

We still use the Drupal 'metadata wrapper’ inside the class to access fields and properties. But the benefit is that you only need to use this interface inside your entity classes.

To let Drupal know about our new entity classes we must Implement the hookentityinfo_alter hook:

function hook_entity_info_alter(&$entity_info) {

     $entity_info['node']['bundles']['article']['entity class'] = Article::class;

     $entity_info['taxonomy_term']['bundles']['tags']['entity class'] = Tag::class;

}

Now we can use the classes with the existing drupal API:

// Saving entities.

$article = new Article();
$article->setTitle('2');
node_save($article);

// Loading entities.

$article = node_load(1);
$tags = $article->getTags();
print $tags[0]->getName();

// Use it in hooks.

function hook_entity_update($entity, $type) {

    // With Classable
    if ($entity instanceof Article) {
        // It is a Article!
    }
    if ($entity instanceof Tag) {
        // it is a Tag!
    }

    // See how much easier that was with Classable
    if ($type === 'node' && $entity->type === 'article') {
        // It is a Article!
    }

    if ($type === 'taxonomy_term' && 
        $entity->vocabulary_machine_name === 'tags') {
        // it is a Tag!
    }

}

Conclusion

 

We can now use our own classes instead of using \stdClass. There are some remarks however:

  • Some entities like nodes or terms still extend from \stdClass, so most of the properties are public.
  • Other entities must extend from the \Entity class. This can be dangerous because all kind of database operations are exposed.
  • It's not limited to nodes or terms, you can create a class for all kinds of entity types.
  • classable is still a sandbox project. But the module is already used in multiple production websites.
  • Not yet supported for Drupal 8, I’m going to look if i can port this functionality.

I hope you find this module useful in your own projects. Let me know what you think about it!