Introduction
Object-oriented programming (OOP) is a programming paradigm based on the concept of classes and objects. OOP is very popular as a programming style because the features of OOP have numerous advantages. In OOP, complex things are modeled as reproducible, simple structures that can be used across programs. This reduces programming effort and makes it easier to avoid errors.
Since we at arocom love Drupal and Drupal is strongly based on OOP, this content management system offers us the perfect opportunity to explain the basics of object-oriented programming to you in more detail.
Namespaces
In large codebases, it can happen that two classes with the same name exist in different directories (e.g. different modules). This can lead to confusion when these classes are imported into another file. In addition, importing these classes with the require() command and relative file paths can be a pain. The same applies to the definition of the path to the inline class. This is why so-called namespaces exist.
Namespaces in a class
To give a class a namespace, implement the following code above the class declaration:
namespace NAMESPACE_FOR_YOUR_CLASS;
For example:
<?php
namespace my_module\Controller;
class Controller {}
Importing a class via namespace
To use a class with a namespace, enter the following:
use NAMESPACE_OF_THE_CLASS;
For example, if another class has the namespace Drupal\App, you can import this class as follows:
<?php
use Drupal\App;
class MyClass {
$app = new App();
}
Namespaces in Drupal
Namespaces can be found everywhere in Drupal. Simply open any class file from the Drupal core or any module and you will find "use" statements at the beginning of this file. It is very likely that you will find several such "use" statements.
Access modifiers (access modifiers)
Access modifiers can be assigned to any properties and methods within a class. They define from where these properties and methods can be accessed. There are three modifiers:
- Public
- Protected
- Private
Public
The property or method can be accessed from anywhere, even from outside the class:
class Greeting {
public $value = 1;
}
$greeting = new Greeting();
$greeting->value = 2;
print($greeting->value); // 2.
Protected
Access is only permitted in the class and in classes that are derived from this parent class:
class Greeting {
protected $value = 1;
}
class GermanGretting extends Greeting {
public function changeValue () {
$this->value = 2; // Works fine.
}
}
$germanGreeting = new GermanGreeting();
$germanGreeting->value = 4; // Error.
Private
Properties and methods are only accessible from within the class.
class Greeting {
private $value = 1;
}
class GermanGretting extends Greeting {
public function changeValue () {
$this->value = 2; // Error.
}
}
$germanGreeting = new GermanGreeting();
$germanGreeting->value = 4; // Error.
Access functions: Getters and setters
In OOP, it is common practice to declare all (or at least most) properties of a class as "private". Access to these from properties outside the class is then made possible by getter and setter methods. These are public methods that either return the value (getter) or change it (setter). For example:
class Greeting {
private $greeting = "Hello";
public function getGreeting(){
return $this->greeting;
}
public function setGreeting($greeting) {
$this->greeting = $greeting;
}
}
Magic Methods
Magic methods are predefined methods that are automatically called on an object under certain circumstances. Roughly speaking, all magic methods are automatically attached to an object and their functionality can be overridden by the programmer. The most commonly used is the constructor method, which is always called when an object is instantiated. A list of all magic methods can be found here.
Insert a magic method:
<?php
class GreetingClass {
public $greeting;
public function __construct($greeting) {
$this->greeting = $greeting;
}
}
$my_greeting = new GreetingClass('Hello');
echo $my_greeting->greeting; // Hello
In the example above, the GreetingClass has a property called "greeting", which is initially null. However, it is filled in the constructor method: the constructor method takes a greeting as an argument and sets the "greeting" property to the passed parameter. But how do we pass the greeting parameter to the constructor method? Quite simply by passing the parameter when instantiating an object from this class. Remember that the method is always called when we instantiate an object from the class. PHP takes care of the rest and passes the parameter to the constructor. So if you write $my_greeting = new GreetingClass('Hello'), the constructor is called as follows: __construct('Hello').
Magic methods in Drupal
The most commonly used magic method in Drupal is the constructor. In particular, it is used when using dependency injection (more on this below).
Static methods
Static methods are methods in classes that can be called without instantiating an object of this class. The keyword 'static' is useful for properties and methods that are shared by all instances of a particular class.
Create static methods:
For example:
class Greeting {
public function sayHello() {
print("Hello World!");
}
}
The sayHello() method is not dependent on properties of the class that could change with different instantiations of this class. It therefore makes sense to revise the method and declare it as static:
class Greeting {
public static function sayHello() {
print("Hello World!");
}
}
The keyword 'static' was added here when declaring the class.
Call static methods:
Outside the class, static methods can be accessed via two colons ('::'). In the example above, the sayHello() method can be called as follows:
Greeting::sayHello();
Within the class, static methods can be called using the keyword "self":
public function otherHello() {
self::sayHello();
}
Static methods in Drupal
Drupal makes extensive use of the concept of static methods and properties, for example in the service container. The service container is a way of calling a service by its name:
$coords = \Drupal::service('router.router_provider');
Inheritance & Extension (Inheritance & Extension)
In OOP, inheritance means the extension of an object (or a class), the so-called child, by another object (or a class), the so-called parent. All methods and properties of the parent class are also available on the child, with the exception of the constructor and descructors. The concept of inheritance can reduce boilerplate code and minimize the refactoring effort.
Inheritance in action
<?php
class ParentClass {
// Do Stuff
}
class ChildClass extends ParentClass {
// Do Stuff
}
The keyword "extends" ensures that the child class extends the parent class. So when an object of the child class is instantiated, it has all the properties and methods of the parent class plus the properties and methods defined within the child class
Calling the parent constructor
As mentioned above, the constructor method of the parent class is not automatically available in the child class. To call the parent constructor, it must be executed in the constructor of the child class.
class Class2 extends Class1 {
public function __construct() {
parent::construct()
}
}
Example
Here you can use blocks in Drupal as an example: If you want to create your own/custom block, the Block class must extend the BlockBase class. The BlockBase class ensures that the block is recognized by Drupal so that it can be used in the admin panel. It would be very inefficient if you had to write the functions yourself every time you created a new block. In addition, if you find an error, you would have to correct it in every block class.
Interfaces (interfaces)
In OOP, an interface is a description of all the methods that an object must have in order to be an X. In other words, an interface generally defines a set of the methods (or messages) that an instance of a class implementing that interface could respond to.
Creating an interface
interface NameOfInterface {
public function nameOfFunction () {
}
}
Implementing an interface
class NameOfClass implements NameOfInterface {
}
This class must now implement all methods of the interface. Otherwise an error is thrown.
Use cases of interfaces
Here you can use the concept of nodes and bundles in Drupal as an example: For example, you want to fetch an instance of a bundle from the database and then output the name of this instance. How do you know how to get this title? Is it called "Title" or "Name"? This is very error-prone. However, if the bundle implements the NodeInterface, you can be sure that it has the "label" method that provides you with the name.
Interfaces in Drupal
Interfaces are an integral part of Drupal. Their power and need becomes clear when you take a closer look at the nodes (and their classes). The NodeInterface class in the core defines which properties and methods a class/object must implement in order to be classified as a node.
$current_node = \Drupal::request()->attributes->get('node');
$title='';
if ($current_node instanceof NodeInterface) {
$title = $current_node->getTitle();
}
We first load the current node from Drupal's static request method. Next, we want to query the title. Since we know that every node implements the NodeInterface and therefore must implement the getTitle() method, we can safely call this method on $current_node as long as we are sure that it is actually a node.
Factory pattern
The factory pattern is not a standard OOP concept built into PHP or any other programming language. It is a design pattern. Design patterns in software development are reusable solutions to common problems. In other words, they are templates for how to solve a problem.
In PHP, the factory pattern is one of the most commonly used and simplest design patterns. In simple words, it is used to instantiate an object from another class.
For example:
<?php
class Automobile
{
private $vehicleMake;
private $vehicleModel;
public function __construct($make, $model)
{
$this->vehicleMake = $make;
$this->vehicleModel = $model;
}
public function getMakeAndModel()
{
return $this->vehicleMake . ' ' . $this->vehicleModel;
}
}
class AutomobileFactory
{
public static function create($make, $model)
{
return new Automobile($make, $model);
}
}
In the example above, the AutomobileFactory is used to create an instance of the Automobile class.
Factory pattern in Drupal
The core of Drupal uses a number of factory methods. You simply have to search for "factory" in the core path and you will get dozens of results. One that you've probably already used is the ContainerFactory, which is used when implementing dependency injection on plugins (more on this later). For example:
class NewStageBlock extends BlockBase implements ContainerFactoryPluginInterface {
protected $requestStack;
public function __construct(array $configuration, $plugin_id, $plugin_definition, RequestStack $request_stack) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->requestStack = $request_stack;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('request_stack'),
);
}
As you can see, the ContainerFactoryPluginInterface class is implemented here and has the create() method. This ensures that the dependencies are created correctly, e.g. for the creation of all necessary objects.
Dependency injection (dependency injection)
Dependency injection is a process in which an object provides the dependencies of another object. It is therefore a software design pattern that avoids the hard coding of dependencies. The more you read about it online, the more confusing and complex it seems. However, it is a fairly simple concept. Instead of instantiating objects from other classes (dependencies) in an object when needed, you pass the dependencies directly into the class.
Using dependency injection
Let's look at this example where we don't use dependency injection and create dependencies spontaneously:
class ParentClass {
public function getUser(){
$db = new DB();
$user = $db->getUser();
}
public function getPost(){
$db = new DB();
$post = $db->getPost();
}
}
As you can see, we instantiate a $db object in two different places here. This is neither nice nor maintainable and also difficult to test (e.g. unit tests). This can be reworked with dependency injection:
class ParentClass {
private $db;
// Pass DB as dependency
public function __construct(DB $db) {
$this->db = $db;
}
public function getUser(){
$user = $this->db->getUser();
}
public function getPost(){
$post = $this->db->getPost();
}
}
Here we do not instantiate a $db object twice. Instead, we pass it into the constructor, making the $db object a property of our parent class. This reduces duplicate code and increases maintainability if you want to change the code in the future.
Dependency injection in Drupal
In Drupal, dependency injection is used to inject services into an object. In particular, this eliminates the need to access the global service container every time a service is required. Depending on the type of object we are working on, different types of dependency injection are necessary. For example, when using dependency injection in controllers, dependencies are injected via the Drupal factory method create(). A good overview of the use of dependency injection in Drupal can be found here.
Traits
PHP only supports simple inheritance. This means that a class can only extend ONE other class. However, sometimes a class needs to inherit functionality from multiple classes. This is where traits come into play.
Traits are used to declare methods and properties that can be used in other classes without those classes having to extend the trait. In this way, the class can extend another class and still benefit from the functionality of the trait.
Creating a trait
Traits are defined as follows:
trait MyTrait {
public function hello(){
print("hello");
}
}
It should be noted that it was not defined with the keyword "class", but with the keyword "trait". It is also important that it is possible to assign access modifiers such as "public" to the trait.
Using a trait
Once a trait has been defined, the next step is to use it in a class:
class Greeting {
use MyTrait;
}
$greeting = new Greeting();
$greeting->hello();
The trait is imported into the class via the keyword 'use'. All instantiated objects from this class then have access to the properties and methods of the trait.
Traits in Drupal
A very popular trait in Drupal is the StringTranslationTrait from Core. You've probably used it yourself whenever you've had to work with translatable strings in a class ;).