This is the sixth post in a series about coding standards. In this post we’ll talk about how to adhere to standards when writing object-oriented code in Drupal.
Other posts in this series:
- Drupal Code Standards: What Are They?
- Drupal Code Standards: How Do We Implement Them?
- Drupal Code Standards: Formatting
- Drupal Code Standards: Documentation
- Drupal Code Standards: The t() function
- Drupal Code Standards: Object Oriented Coding & Drupal 8
- Drupal Code Standards: Twig in Drupal 8
What is Object Oriented Programming?
Object-oriented programming, or OOP, is a way of programming that is based on the concept of objects, which represent data in a program. Objects have properties, which hold data, and methods, which execute functions. After an object is created, or instantiated, it can be used over and over again. OOP allows for a lot of reuse and convenience that procedural programming does not. If you’re not yet familiar with Drupal 8 and OOP, you may want to brush up first, because we’re going over the best practices and formatting here, and not the concepts. The OOP Examples project may be helpful.
Best Practices
Drupal has a set of coding standards just for object-oriented code. As with other standards, most of these are based on common PHP coding conventions, and decided on by the Drupal community. This post will cover most of the object-oriented coding conventions you’ll run into. All of these examples are from Drupal 8. While you can certainly use object-oriented code in Drupal 7, and many people have, it’s now mandatory, so it’s best to get used to it.
Declaring Classes
There should only be one class, interface, or trait per file. Name the file after the class or interface. Here’s an example from the ctools contrib module:
EntityFormWizardBase.php
<?php
/**
* @file
* Contains \Drupal\ctools\Wizard\EntityFormWizardBase.
*/
namespace Drupal\ctools\Wizard;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\ctools\Event\WizardEvent;
use Drupal\user\SharedTempStoreFactory;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The base class for all entity form wizards.
*/
abstract class EntityFormWizardBase extends FormWizardBase implements EntityFormWizardInterface {
The file is called EntityFormWizardBase.php
.
The class is called EntityFormWizardBase
.
This is straightforward, and probably something you’ve already been doing if you’ve been creating any class files.
Class naming is important for autoloading. Autoloading allows for classes to be loaded on demand, instead of a long list of require
statements. From Drupal.org:
In Drupal 8, classes will be autoloaded based on the PSR-4 namespacing convention.
In core, the PSR-4 'tree' starts under
core/lib/
.In modules, including contrib, custom and those in core, the PSR-4 'tree' starts under
modulename/src
.Defining a class in your module's .module file is only possible if the class does not have a superclass which might not be available when the .module file is loaded. It's best practice to move such classes into a PSR-4 source directory.
Most of what matters here is how you name and arrange your files and directories - the rest is happening behind the scenes. From the PSR-4 Autoloader documentation (which is quite brief and worth looking over):
This PSR describes a specification for autoloading classes from file paths... This PSR also describes where to place files that will be autoloaded according to the specification.
So all that’s going on here is that PSR-4 is telling you how to create your file paths so that classes can be autoloaded.
A note on the file docblock
The current Drupal standards state:
The @file doc block MUST be present for all PHP files, with one exception: files that contain a namespaced class/interface/trait, whose file name is the class name with a .php extension, and whose file path is closely related to the namespace (under PSR-4 or a similar standard), SHOULD NOT have a @file documentation block.
Looking through the coding standards issue queue, it appears that this was adopted after most Drupal 8 code was written, which is why you are still seeing @file
blocks in php files that don't require them. I decided to leave them in the snippets I am quoting here, but be aware that this is a new standard that you will probably see being adopted in module code.
Namespaces
If you’re not familiar with namespaces, you can start with this blog post or documentation on php.net. Namespaces are a way of organizing codebases. Drupal’s coding standards regarding namespaces are detailed, and we’ll go over the most important points here.
First, let’s look at an example of a namespace from the Metatag contrib module.
<?php
/**
* @file
* Contains Drupal\metatag\Command\GenerateGroupCommand.
*/
namespace Drupal\metatag\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Drupal\Console\Command\GeneratorCommand;
use Drupal\Console\Command\Shared\ContainerAwareCommandTrait;
use Drupal\Console\Command\Shared\ModuleTrait;
use Drupal\Console\Command\Shared\FormTrait;
use Drupal\Console\Command\Shared\ConfirmationTrait;
use Drupal\Console\Style\DrupalStyle;
use Drupal\metatag\Generator\MetatagGroupGenerator;
/**
* Class GenerateGroupCommand.
*
* Generate a Metatag group plugin.
*
* @package Drupal\metatag
*/
class GenerateGroupCommand extends GeneratorCommand {
use ContainerAwareCommandTrait;
use ModuleTrait;
use FormTrait;
use ConfirmationTrait;
The file doc block tells us that this file contains a class called Drupal\metatag\Command\GenerateGroupCommand
, and then we see the namespace declaration for Drupal\metatag\Command
. Now look at the directory structure and that’s what we’ll see:
-metatag
-config
-metatag_google_plus
-metatag_open_graph
-metatag_twitter_cards
-matatag_verification
-src
-Annotation
-Command
GenerateGroupCommand.php
GenerateTagCommand.php
If you remember we learned just above that the PSR-4 directory tree starts under src/
, which is why it’s not included in the namespace itself.
When creating a Drupal module, you should follow this directory structure - <modulename>/src/<namespace>
.
You can also see a list of classes to be used in this file. Any class or interface with a backslash in it must be declared like this at the top of the file. These are called "fully-qualified namespaces" and they now can be referred to by just the last part of the namespace - the fully-qualified namespace may no longer be used inside the code. Take a look at the above code, in the class GenerateGroupCommand
- the use
statements there refer to the same namespaces used at the top of the file, but here we don’t use
the entire name (no backslash).
If you have two classes with the same name, that’s a collision, and we fix it by aliasing the namespace. Use the next higher portion of the namespace to create the alias.
Here’s an example from Drupal core:
<?php
namespace Drupal\Component\Bridge;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Zend\Feed\Reader\ExtensionManagerInterface as ReaderManagerInterface;
use Zend\Feed\Writer\ExtensionManagerInterface as WriterManagerInterface;
Here there are two ExtensionManagerInterface
classes being called, so they are aliased to ReaderManagerInterface
and WriterManagerInterface
.
There should only be one class per use statement.
An exception to use
statements is if you are using a global class - in that case, you don’t need to use
anything.
Here’s an example from the Devel contrib module:
/**
* Formats a time.
*
* @param integer|float $time A raw time
*
* @return float The formatted time
*
* @throws \InvalidArgumentException When the raw time is not valid
*/
private function formatTime($time) {
if (!is_numeric($time)) {
throw new \InvalidArgumentException('The time must be a numerical value');
}
return round($time, 1);
}
The \InvalidArgumentException
is not declared anywhere in the file, because it’s a global class - it’s a part of Drupal core. In fact, a part of Symfony, on which Drupal 8 is built - Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
Indenting and Whitespace
For all of the formatting basics, read our previous post on formatting. That hasn’t changed, but there are some specific OO conventions.
There should be an empty line between the start of a class or interface definition and a property or method definition.
Here’s an example from the Token contrib module:
<?php
/**
* @file
* Contains \Drupal\token\TokenEntityMapperInterface.
*/
namespace Drupal\token;
interface TokenEntityMapperInterface {
/**
* Return an array of entity type to token type mappings.
*
* @return array
* An array of mappings with entity type mapping to token type.
*/
public function getEntityTypeMappings();
This code declares an interface, TokenEntityMapperInterface
, and leaves a blank line before the function declaration.
There should be an empty line between a property definition and a method definition. Here’s another example from the Token contrib module:
/**
* @var array
*/
protected $entityMappings;
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
This code declares a property, $entityMappings
, and leaves a blank line before the function definition.
There should also be a blank line between the end of a method definition and the end of a class definition - so a blank line between the ending curly braces.
Again from the Token module, here we can see that there is a blank line after the last function:
/**
* Resets metadata describing supported tokens.
*/
public function resetInfo() {
$this->tokenInfo = NULL;
$this->cacheTagsInvalidator->invalidateTags([static::TOKEN_INFO_CACHE_TAG]);
}
}
Naming Conventions
You can find a list of detailed naming conventions on Drupal.org, but we’ll go over some basics.
- When declaring a class or interface, use UpperCamel.
- When declaring a method or class property, use lowerCamel.
- Class names shouldn’t include "drupal" or “class.”
- Interfaces should end with "Interface."
- Test classes should end with "Test."
Here’s a good example from the Google Analytics contrib module:
/**
* @file
* Contains \Drupal\google_analytics\Tests\GoogleAnalyticsBasicTest.
*/
namespace Drupal\google_analytics\Tests;
use Drupal\Core\Session\AccountInterface;
use Drupal\simpletest\WebTestBase;
/**
* Test basic functionality of Google Analytics module.
*
* @group Google Analytics
*/
class GoogleAnalyticsBasicTest extends WebTestBase {
In the above code, there are several properly named classes and interfaces, including a test class.
Interfaces
For flexibility reasons, it is strongly encouraged that you create interface definitions and implement them in separate classes. From Drupal.org:
The use of a separate interface definition from an implementing class is strongly encouraged because it allows more flexibility in extending code later. A separate interface definition also neatly centralizes documentation making it easier to read. All interfaces should be fully documented according to established documentation standards.
If there is even a remote possibility of a class being swapped out for another implementation at some point in the future, split the method definitions off into a formal Interface. A class that is intended to be extended must always provide an Interface that other classes can implement rather than forcing them to extend the base class.
Here’s an example of an interface and a class implementation from the ctools contrib module:
<?php
/**
* @file
* Contains \Drupal\ctools\ConstraintConditionInterface.
*/
namespace Drupal\ctools;
interface ConstraintConditionInterface {
/**
* Applies relevant constraints for this condition to the injected contexts.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*
* @return NULL
*/
public function applyConstraints(array $contexts = array());
/**
* Removes constraints for this condition from the injected contexts.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*
* @return NULL
*/
public function removeConstraints(array $contexts = array());
}
In this code, we can see a simple interface declared, ConstraintConditionInterface
, with two functions, applyConstraints
and removeConstraints
.
<?php
/**
* @file
* Contains \Drupal\ctools\Plugin\Condition\NodeType.
*/
namespace Drupal\ctools\Plugin\Condition;
use Drupal\node\Plugin\Condition\NodeType as CoreNodeType;
use Drupal\ctools\ConstraintConditionInterface;
class NodeType extends CoreNodeType implements ConstraintConditionInterface {
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*/
public function applyConstraints(array $contexts = array()) {
// Nullify any bundle constraints on contexts we care about.
$this->removeConstraints($contexts);
// If a single bundle is configured, we can set a proper constraint.
if (count($this->configuration['bundles']) == 1) {
$bundle = array_values($this->configuration['bundles']);
foreach ($this->getContextMapping() as $definition_id => $context_id) {
$contexts[$context_id]->getContextDefinition()->addConstraint('Bundle', ['value' => $bundle[0]]);
}
}
}
/**
* {@inheritdoc}
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
*/
public function removeConstraints(array $contexts = array()) {
// Reset the bundle constraint for any context we've mapped.
foreach ($this->getContextMapping() as $definition_id => $context_id) {
$constraints = $contexts[$context_id]->getContextDefinition()->getConstraints();
unset($constraints['Bundle']);
$contexts[$context_id]->getContextDefinition()->setConstraints($constraints);
}
}
}
In this code, we have a class, NodeType
, which implements ConstraintConditionInterface
. It also provides an implementation for both of the classes in the interface - applyConstraints
and removeConstraints
both get fleshed out here. (Another note here, the implemented functions use the {@inheritdoc}
notation, because the parameters have not changed, so documentation can be referred to the interface.) You can see how the interface could be implemented differently in a different class, but still adhere to the original interface.
If you’re familiar with the OOP concept of polymorphism, it applies here. Polymorphism allows for classes to implement different functionality while sharing a common interface. If you’re not familiar with this, the OOP examples project is a great place to start.
From the 8.x version of the project, example 8, here is a very simple interface that returns a color:
/**
* Color interface.
*/
interface ColorInterface {
/**
* Returns color of the object.
*/
public function getColor();
}
Here’s a class that implements the color interface:
/**
* Fruit class.
*/
class Fruit implements ColorInterface {
/**
* Returns color of the object.
*/
public function getColor() {
return t('green');
}
}
And another completely different class, which also implements the color interface:
/**
* Vehicle class.
*/
class Vehicle implements ColorInterface {
/**
* The vehicle color.
*
* @var string
*
* Default color translation t() is set up in class constructor because
* expression is not allowed as field default value like:
* public $color = t('red');
*/
public $color;
/**
* Class constructor.
*/
public function __construct() {
$this->color = t('red');
}
/**
* Returns class type description.
*/
public function getClassTypeDescription() {
$s = t('a generic vehicle');
return $s;
}
/**
* Returns class description.
*/
public function getDescription() {
$s = t('This is') . ' ';
$s .= $this->getClassTypeDescription();
$s .= ' ' . t('of color') . ' ';
$s .= $this->color;
$s .= '.';
return $s;
}
/**
* Implements ColorInterface.
*/
public function getColor() {
return $this->color;
}
}
These are just a couple parts of the examples, but it helps to show how flexible interfaces are and why we use them.
Visibility
All methods and properties of classes must have their visibility declared. They can be public, protected, or private. Public properties are strongly discouraged. Here’s an example from the Metatag contrib module:
/**
* Token handling service. Uses core token service or contributed Token.
*/
class MetatagToken {
/**
* Token service.
*
* @var \Drupal\Core\Utility\Token
*/
protected $token;
/**
* Constructs a new MetatagToken object.
*
* @param \Drupal\Core\Utility\Token $token
* Token service.
*/
public function __construct(Token $token) {
$this->token = $token;
}
This code declares a protected property, $token
, and a public function, __construct
.
Type Hinting
Type-hinting is optional, but recommended as it can be a great debugging tool. If an object of the incorrect type is passed, an error will be thrown. If a method’s parameters expects a certain interface, specify it. Do not specify a class as a type, only an interface. This ensures that you are checking for a type, but keeps your code fluid by not adhering rigidly to a class. This is another instance where polymorphism comes into play. We can keep our code reusable by checking only for the interface and not the class, allowing the classes to differ.
Here’s an example from the Pathauto contrib module:
/**
* Extends the default PathItem implementation to generate aliases.
*/
class PathautoItem extends PathItem {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = parent::propertyDefinitions($field_definition);
$properties['pathauto'] = DataDefinition::create('integer')
->setLabel(t('Pathauto state'))
->setDescription(t('Whether an automated alias should be created or not.'))
->setComputed(TRUE)
->setClass('\Drupal\pathauto\PathautoState');
return $properties;
}
In this code, we can see that the propertyDefinitions
function has the parameter $field_definition
with the type hint FieldStorageDefinitionInterface
.
Chaining
Chaining allows you to immediately call a function on a returned object. You’ve probably most often seen or used this with database objects. Here’s an example from the Devel Node Access contrib module:
// How many nodes are not represented in the node_access table?
$num = db_query('SELECT COUNT(n.nid) AS num_nodes FROM {node} n LEFT JOIN {node_access} na ON n.nid = na.nid WHERE na.nid IS NULL')->fetchField();
Without chaining, you’d have set the results of that query into an object like $result
, and then you could use the fetchField()
function in another statement.
Additionally, to allow for chaining whenever possible, methods that don’t return a specific value should return $this
. Especially in methods that set a state or property on an object, returning the object itself is more useful than returning a boolean or NULL.
Here are some chaining examples from the Google Analytics contrib module, where configuration is set and saved:
$this->config('google_analytics.settings')->set('account', $ua_code)->save();
// Show tracking on "every page except the listed pages".
$this->config('google_analytics.settings')->set('visibility.request_path_mode', 0)->save();
// Disable tracking on "admin*" pages only.
$this->config('google_analytics.settings')->set('visibility.request_path_pages', "/admin\n/admin/*")->save();
// Enable tracking only for authenticated users only.
$this->config('google_analytics.settings')->set('visibility.user_role_roles', [AccountInterface::AUTHENTICATED_ROLE => AccountInterface::AUTHENTICATED_ROLE])->save();
Constructors & Instantiation
Drupal’s coding standards discourage directly creating classes. Instead, it is ideal to create a function to instantiate the object and return it. Two reasons are given for this:
-
The function can be written to be re-used to return different objects with the same interface as needed. As we discussed above when reviewing interfaces, this relies on polymorphism - the idea that we have classes that implement different functionality while sharing a common interface. If the function is written to return objects with the same interface and not limited to a class, it can be re-used more widely in our code.
-
You cannot chain constructors in PHP, but you can chain the returned object from a function, and chaining is very useful, as you’ve already read above.
Here’s an example from the Metatag contrib module:
/**
* {@inheritdoc}
*/
protected function createGenerator() {
return new MetatagTagGenerator();
}
This is a function that returns a new instance of a MetatagTagGenerator()
. The createGenerator()
function is declared in an interface that is implemented by this class.
Wrapping Up
This is a lot of information - and if you’re not especially familiar with Drupal 8 or OOP yet, it may not make a lot of sense - but it will! It can take a lot of time and practice to change the way you program, and the way you think about a system like Drupal and how it works, but keep your chin up and your code clean, and you’ll get there! If you need more resources, this Object Oriented Programming 101 post and this Introduction to Drupal 8 Object-Oriented Concepts have been helpful to our team.
As always, reach out to us! Have some questions/comments/concerns? Experience you’d like to share? Talk to us on Twitter: @ChromaticHQ!
Keep an eye out for our next post in our Drupal Code Standards series, on Twig in Drupal 8!