Drupal 8 API: objected-oriented programming conventions

Pre-requisites

Naming conventions for both manual and automatic discovery

Even before Drupal adopted object-oriented (OO) principles as heavily as 8 has done, it's always used naming conventions in part as a way of working out where a particular thing should be (the key example is hook function naming.) And Drupal has always had great coding standards (which incidentally cover PHP classes). But we must go one step further than informal standards, to ensure that classes are always findable by some automatic system like Drupal itself.

PHP has a principle of autoloading, whereby if an OO component (e.g. a class) is requested, but is not yet known to the current PHP process, it will ask an autoloading registry where it might look for a file containing the component. Separately, there is a PSR-4 standard for class, trait and interface naming which, by mandating a combination of namespace-plus-classname, and subdirectory-plus-filename, PSR-4 ensures that any class referenced in any code is automatically discoverable and loadable.

Drupal 8 adopts and recommends PSR-4: that means that, if you name(space) your classes correctly, and put them in the right files (and subfolders), then they will be discoverable to Drupal.

OO naming conventions specific to Drupal

PSR-4 is adaptable to lots of different folder structures, but in Drupal it's set up so that:

  1. If you have a module called modulename
  2. And you want to store a class in a file deep in some subdirectory of its src/ subfolder e.g. src/FolderOne/FolderTwo/FileName.php
  3. Then your class must begin with the PHP namespace Drupal\modulename\FolderOne\FolderTwo
  4. And the class name must be FileName.

As long as your class name(space) and its file location are consistent in this way, PSR-4 means Drupal can always find the source code for your class. (The same obviously goes for PHP traits, interfaces etc.)

A couple of key consequences of these requirements are:

  • All object-oriented code goes in the src/ subfolder: no exceptions, as otherwise it can't be found at all by autoloading.
  • You should only ever have one OO class etc. per file, because the class name must match the file name.

Below, we'll look at a counterexample for the second point, to illustrate why it's important.

Creating and then finding a new class

In the folder for the d8api module you created previously, create a src/ subfolder. Within that, create a further src/Convention/ subfolder.

Then, in the latter, add the following file and call it Test.php:

<?php
 
namespace Drupal\d8api\Convention;
 
/**
 * Test class.
 */
class Test {
}
 
/**
 * This class should not be here & it violates PSR-4, but
 * we're going to use it in testing to show what the problem is.
 */
class ContraryToPsr4 {
} 

So this file is src/Convention/Test.php within your d8api/ module folder.

What you should see

In order to test this code, you should install Drush. We don't cover that process here.

With Drush installed, open a terminal and change the directory so you're inside the codebase for your Drupal site. You can then type the following:

drush php-eval 'print get_class(new \Drupal\d8api\Convention\Test) . "\n";'

The response will be:

Drupal\d8api\Convention\Test

Drupal has successfully (a) found your class file, based on the PSR-4-compatible naming conventions (b) loaded the file (c) and created an object instance of the class. The naming convention has worked.

If you edit the class name, or the namespace, without renaming files and folders to match, you'll find that Drush will rapidly be unable to find the class on demand; for example, even if you edit the namespace in Test.php, then if it's still in the src/Convention/ subfolder:

drush php-eval 'print get_class(new \Drupal\d8api\SomethingElse\Test) . "\n";'
# Output:
PHP Fatal error:  Class 'Drupal\d8api\SomethingElse\Test' not found

It won't work! Class name must match filename; class namespace must relate to subfolder structure.

Speaking of which, what about the other class we added to the file—ContraryToPsr4—in contravention of the PSR-4 conventions? Obviously, it can't be found:

drush php-eval 'print get_class(new \Drupal\d8api\Convention\ContraryToPsr4) . "\n";'
# Output:
PHP Fatal error:  Class 'Drupal\d8api\Convention\ContraryToPsr4' not found

However, it's worth pointing out a quirk with autoloading. If you autoload the Test class first, then it just so happens that the ContraryToPsr4 class is loaded along with it. So if you then try to use the latter, doing so doesn't have to trigger autoloading/discovery, so it will appear to work:

drush php-eval 'print get_class(new \Drupal\d8api\Convention\Test) . "\n"; print get_class(new \Drupal\d8api\Convention\ContraryToPsr4) . "\n";'
# Output:
Drupal\d8api\Convention\Test
Drupal\d8api\Convention\ContraryToPsr4

Once you've seen that happen, you're tempted to rely on it for loading code: especially when you yourself will only ever use the two classes (or a class and an interface) together. But it means that your classes are no longer usable in isolation, making your code brittle. This is inevitably dangerous as far as the rest of the Drupal framework is concerned; for example, if you reverse the order of autoloading:

drush php-eval 'print get_class(new \Drupal\d8api\Convention\ContraryToPsr4) . "\n"; print get_class(new \Drupal\d8api\Convention\Test) . "\n";'
# Output:
PHP Fatal error:  Class 'Drupal\d8api\Convention\ContraryToPsr4' not found

then it will break as expected: the autoloader tries to discover the first class it needs, and fails early. So always limit any given file to a single class or similar OO component, because you never know how other people's code might need to use your own.

If you can reproduce these results, then congratulations! you've successfully leveraged Drupal's OO programming conventions.

Further reading etc.

Add new comment