Drupal
min read
January 8, 2024

Exploring the Power of PHP Attributes in Drupal Development

Exploring the Power of PHP Attributes in Drupal Development
Table of contents

Introduction

PHP gets better over time, and one recent improvement is PHP attributes, which came with PHP 8. These attributes help organize things like classes, methods, and properties by adding extra information. This makes it easier for tools and frameworks to understand and work with the code. In this post, we'll look into PHP attributes, comparing them to Drupal annotations, talking about their advantages, and showing examples. Plus, we'll show you how easily attributes fit into Drupal.

Drupal Plugin Discovery mechanism:

In Drupal, the Plugin Discovery thing finds and adds plugins as needed. Plugins are like building blocks that help add new features to Drupal. The Plugin API is like a helpful tool that makes it easy to include these plugins in different parts of the system, like blocks and fields.

Annotations: Drupal plugins often use annotations for discovery. Annotations are special comments in PHP code that provide metadata about the class.

YAML Files: Some plugins are discovered through YAML files, where plugin definitions are specified in a structured format.

Annotations in Drupal

Drupal's plugin system, a robust and flexible component, heavily relies on annotations for various purposes. These annotations, often expressed as PHPDoc comments, convey metadata information to the system.  Here's an example of an annotation:




* @ContentEntityType(
*   id = "comment",
*   label = @Translation("Comment"),
*   ...
*   base_table = "comment"
* )


Let's compare examples of annotations and attributes for a simple class definition:

Example of Annotations:




/**
 * @MyAnnotationClass
 */
class MyClass {
  /**
   * @MyAnnotationProperty
   */
  private $myProperty;

  /**
   * @MyAnnotationMethod
   */
  public function myMethod() {
    // method implementation
  }
}



Example of Attributes:




#[MyAttributeClass]
class MyClass {
  #[MyAttributeProperty]
  private $myProperty;

  #[MyAttributeMethod]
  public function myMethod() {
    // method implementation
  }
}




In the attributes example, we see a more concise and integrated syntax for expressing metadata.

Why Use Attributes Instead of Annotations?

Several reasons make PHP attributes a compelling choice over annotations, even in Drupal development:

  • Drupal currently utilizes the "doctrine/annotations" library for parsing annotations (https://github.com/doctrine/annotations). Attributes, which are a native feature of the PHP language, are employed, eliminating the necessity for parsing docblocks. This approach leads to code that is cleaner and more easily readable. 
  • The great thing about being able to create instances of objects in attributes is that we're not using attributes for this. We're using exactly the same objects as you would in regular PHP code. So for translatable strings that's \Drupal\Core\StringTranslation\TranslatableMarkup(). That means the way these strings are translated is exactly the same as regular PHP code. We've got rid a whole abstraction layer (i.e. \Drupal\Core\Annotation\Translation) and that's fantastic.
  • The attributes language feature was added to php to do the kind of things we (and doctrine ORM) are using doctrine annotations for. It is therefore reasonable that doctrine annotations will be deprecated in the future.
  • With Attributes finally we might be able to get IDE autocompletion and some amount of control and discoverability over the entity type annotations. Since attributes are part of the language, IDEs can provide better autocompletion and code navigation support. Developers can explore and use attributes more efficiently within their integrated development environment.

Example of Using Attributes in Drupal

Drupal is using something new called PHP attributes, and modules are beginning to use this cool feature.

Let us understand the process of creating a custom plugin type using Attribute and Annotation discovery mechanisms through the following example:

Plugins have several parts, in the example below we will cover only those aspects which are relevant for understanding the plugins discovery mechanism :


1. Plugin Definition

The first step is to define the definition for your plugin.

Annotation Definition:

This is a simple class that extends Drupal\Component\Annotation\Plugin to define the variables 

you want listed in the annotation comments of the plugin. This file goes in [your_module]/src/Annotation, and file name and class name match. for example: Importer.php



<?php

/**
 * Defines importer annotation object.
 *
 * @Annotation
 */
final class Importer extends Plugin {
  /**
   * The plugin ID.
   */
  public readonly string $id;

  /**
   * The human-readable name of the plugin.
   *
   * @ingroup plugin_translatable
   */
  public readonly string $label;

  /**
   * The description of the plugin.
   *
   * @ingroup plugin_translatable
   */
  public readonly string $description;

}
?>


Attribute Definition:

This extends Drupal\Component\Plugin\Attribute\Plugin class. The Attribute definition will go into the folder [your_module]/src/Attribute and 

as always the file name and class name match. for example: Importer.php.

Similar to annotation the variables defined here are used while using Attribute discovery for your plugin type.



<?php
#[\Attribute(\Attribute::TARGET_CLASS)]
class Importer extends Plugin {

  /**
   * Constructs an Importer attribute.
   *
   * @param string $id
   *   The plugin ID.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label
   *   The title of the importer.
   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $description
   *   A description of the importer.
   */
  public function __construct(
    public readonly string $id,
    public readonly ?TranslatableMarkup $label = NULL,
    public readonly ?TranslatableMarkup $description = NULL,
  ) {}
}
?>


2. Plugin manager

Responsible for discovering and loading plugins, the most important part in any plugin type is the manager.This file resides inside the "[example_module]/src" folder mostly with a name ending with Manager.php.

We have the file ImporterPluginManager.php



<?php
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\example_plugin\Attribute\Importer;

/**
 * Importer plugin manager.
 */
final class ImporterPluginManager extends DefaultPluginManager {

  /**
   * Constructs the object.
   */
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct('Plugin/Importer', $namespaces, $module_handler, ImporterInterface::class, Importer::class, 'Drupal\example_plugin\Annotation\Importer');
    $this->alterInfo('importer_info');
    $this->setCacheBackend($cache_backend, 'importer_plugins');
  }
}
?>



This single file is responsible for discovering our "Importer" plugins of which are either using Annotation or Attribute based discovery mechanism.

Here the important line is



parent::__construct('Plugin/Importer', $namespaces, $module_handler, 
ImporterInterface::class, Importer::class, 
'Drupal\example_plugin\Annotation\Importer');


because we are supporting both (Annotation and Attribute discovery)Importer::class is used to discover plugins using "Attribute" discovery Drupal\example_plugin\Attribute\Importer;whereas 'Drupal\example_plugin\Annotation\Importer' is used for the plugins using traditional Annotation discovery

To remove support for either of the discovery mechanisms for your custom plugin type you can remove one of the parameters:Importer::class or 'Drupal\example_plugin\Annotation\Importer'

3. Implementation of plugin of type plugin_name (in this example plugin_name is “Importer”)

Now assume we have created all the required files for defining a custom plugin type let us see examples of the same plugin typewith one using Annotation and other one using Attribute discover

within the "[example_module]/src/Plugin" folder we can have one plugin for example JSONImporter.php using Annotation discover



<?php
/**
 * Plugin implementation of the importer.
 *
 * @Importer(
 *   id = "json_importer",
 *   label = @Translation("JSON importer"),
 *   description = @Translation("Import content from JSON file.")
 * )
 */
final class JSONImporter extends ImporterPluginBase {

}
?>


While CSVImporter.php using Attribute discover mechanism



<?php
namespace Drupal\example_plugin\Plugin\Importer;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\example_plugin\Attribute\Importer;
use Drupal\example_plugin\ImporterPluginBase;

/**
 * Imports a CSV file.
 */
#[Importer(
  id: 'csv_importer',
  label: new TranslatableMarkup('CSV Importer'),
  description: new TranslatableMarkup('Import content from CSV file.'),
)]
class CSVImporter extends ImporterPluginBase {}
?>

Complete code of the above example could be found on https://github.com/prashantdsala/example_plugin 

Drupal Change record:

Plugin types should use PHP attributes instead of annotations

Plugin implementations should use PHP attributes instead of annotations

Plugin types are still being converted to Attribute Discovery. At the time of writing this blog post, only two of the plugin types support Attribute discovery which as mentioned below:

Annotation class

Attribute class

Available since

\Drupal\Core\Annotation\Action

\Drupal\Core\Action\Attribute\Action

10.2.0

\Drupal\Core\Block\Annotation\Block

\Drupal\Core\Block\Attribute\Block

10.2.0

Conclusion

PHP attributes are like new tools that make the PHP language better. They help make the code cleaner, easier to read. In Drupal, using these new tools instead of the old way brings a bunch of benefits. It makes the code work better with the system, fits well with code editors, and makes the code more reliable. As PHP gets updated, using these tools in Drupal is a good idea. It helps keep the code up-to-date and in line with the newest features of the language.

Written by
Editor
No art workers.