Direkt zum Inhalt
Niclas Timm
Objektorientierte Programmierung in Drupal

Einleitung

Die objektorientierte Programmierung (OOP) ist ein Programmierparadigma, welches auf dem Konzept von Klassen und Objekten beruht. OOP ist als Programmierstil sehr beliebt, da die Merkmale von OOP zahlreiche Vorteile mit sich bringen. In der OOP werden komplexe Dinge als reproduzierbare, einfache Strukturen modelliert, die  programmübergreifend verwendet werden können. Dadurch wird der Programmieraufwand reduziert und Fehler können besser vermieden werden.

Da wir bei arocom Drupal lieben und Drupal stark auf OOP aufbaut, bietet dieses Content Management System für uns die perfekte Möglichkeit, Ihnen die Grundlagen der objektorientierten Programmierung näher zu erläutern.

Namespaces

In großen Codebasen kann es vorkommen, dass zwei Klassen mit demselben Namen in verschiedenen Verzeichnissen (z. B. verschiedenen Modulen) vorhanden sind. Dies kann zu Verwirrung führen, wenn diese Klassen in eine andere Datei importiert werden. Außerdem kann das Importieren dieser Klassen mit dem  require() -Befehl und relativen Dateipfaden fürchterlich sein. Das Gleiche gilt für die Definition des Pfades zur inline Klasse. Deshalb existieren sogenannte Namespaces (Namensräume).

Namespaces in einer Klasse

Um einer Klasse einen Namespace zu geben, implementieren Sie oberhalb der Klassendeklaration den folgenden Code:

namespace NAMESPACE_FOR_YOUR_CLASS;

Zum Beispiel:

<?php
namespace my_module\Controller;

class Controller {}

Eine Klasse per Namespace importieren

Um eine Klasse mit einem Namespace zu verwenden, gibt man folgendes ein:

use NAMESPACE_OF_THE_CLASS;

Wenn zum Beispiel eine andere Klasse den Namespace Drupal\App hat, kann man diese Klasse wie folgt importieren:

<?php

use Drupal\App;

class MyClass {
  $app = new App();
}

Namespaces in Drupal

Namensräume sind überall in Drupal zu finden. Man öffnet dazu einfach eine beliebige Klassendatei aus dem Drupal core oder einem beliebigen Modul und findet hier "use"-Statements am Anfang dieser Datei. Es ist sehr wahrscheinlich, dass man mehrere solcher “use”-Statements findet.

Access modifiers (Zugriffsmodifikatoren)

Access modifiers können beliebigen Eigenschaften und Methoden innerhalb einer Klasse zugewiesen werden. Sie definieren, von wo aus auf diese Eigenschaften und Methoden zugegriffen werden kann. Es gibt drei Modifikatoren:

  • Public
  • Protected
  • Private

Public

Auf die Eigenschaft oder Methode kann von überall, auch von außerhalb der Klasse, zugegriffen werden:

class Greeting {
  public $value = 1;
}

$greeting = new Greeting();
$greeting->value = 2;
print($greeting->value); // 2.

Protected

Der Zugriff ist nur in der Klasse und in Klassen, die von dieser Elternklasse abgeleitet sind, erlaubt:

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

Eigenschaften und Methoden sind nur von innerhalb der Klasse zugänglich.

class Greeting {
  private $value = 1;
}

class GermanGretting extends Greeting {
 public function changeValue () {
   $this->value = 2; // Error.
 }
}

$germanGreeting = new GermanGreeting();
$germanGreeting->value = 4; // Error.

Zugriffsfunktionen: Getters und Setters

In der OOP ist es gängige Praxis, alle (oder zumindest die meisten) Eigenschaften einer Klasse als “private” zu deklarieren. Der Zugriff auf diese  von Eigenschaften außerhalb der Klasse wird dann durch Getter- und Setter-Methoden ermöglicht. Diese sind öffentliche Methoden, die entweder den Wert zurückgeben (Getter) oder ihn verändern (Setter). Zum Beispiel:

class Greeting {
 private $greeting = "Hello";

 public function getGreeting(){
   return $this->greeting;
 }

 public function setGreeting($greeting) {
   $this->greeting = $greeting;
 }
}

Magic Methods (Magische Methoden)

Magische Methoden sind vordefinierte Methoden, die unter bestimmten Umständen automatisch auf ein Objekt aufgerufen werden. Grob gesagt sind alle magischen Methoden automatisch an ein Objekt angehängt und ihre Funktionalität kann vom Programmierer überschrieben werden. Die am häufigsten verwendete ist die constructor-Methode, die immer dann aufgerufen wird, wenn ein Objekt instanziiert wird. Eine Liste aller magischen Methoden kann hier gefunden werden.

Eine magische Methode einfügen:

<?php

class GreetingClass {
  
  public $greeting;  

  public function __construct($greeting) {
    $this->greeting = $greeting;
  }
}

$my_greeting = new GreetingClass(‘Hello’);

echo $my_greeting->greeting; // Hello

Im obigen Beispiel hat die GreetingClass eine Eigenschaft namens “greeting”, die zunächst null ist. Sie wird jedoch in der constructor-Methode befüllt: die constructor-Methode nimmt als Argument eine Begrüßung und setzt die Eigenschaft "greeting" auf den übergebenen Parameter. Aber wie übergeben wir den Parameter greeting an die constructor-Methode? Ganz einfach, indem wir beim Instanziieren eines Objekts aus dieser Klasse den Parameter übergeben. Man sollte daran denken, dass die Methode immer dann aufgerufen wird, wenn wir ein Objekt aus der Klasse instanziieren. PHP kümmert sich um den Rest und übergibt den Parameter an den constructor. Wenn man also  $my_greeting = new GreetingClass('Hello') schreibt, wird der constructor wie folgt aufgerufen: __construct('Hello').

Magische Methoden in Drupal

Die am häufigsten verwendete magische Methode in Drupal ist der constructor. Insbesondere wird er bei der Verwendung von Dependency Injection (Abhängigkeitsinjektion) eingesetzt (mehr dazu weiter unten).

Static methods (Statische Methoden)

Statische Methoden sind Methoden in Klassen, die aufgerufen werden können, ohne ein Objekt dieser Klasse zu instanziieren. Das Stichwort ‘static’ ist nützlich für Eigenschaften und Methoden, die von allen Instanzen einer bestimmten Klasse gemeinsam genutzt werden.

Statische Methoden kreieren:

Zum Beispiel:

class Greeting {
 public function sayHello() {
   print("Hello World!");
 }
}

Die Methode sayHello() ist nicht von Eigenschaften der Klasse abhängig, die sich bei verschiedenen Instanziierungen dieser Klasse ändern könnten. Daher ist es sinnvoll, die Methode zu überarbeiten und sie als statisch zu deklarieren:

class Greeting {
 public static function sayHello() {
   print("Hello World!");
 }
}

Hier wurde beim Deklarieren der Klasse das Stichwort ‘static’ hinzugefügt.

Statische Methoden aufrufen:

Außerhalb der Klasse kann auf statische Methoden über zwei Doppelpunkte ('::') zugegriffen werden. Im obigen Beispiel kann die Methode sayHello() wie folgt aufgerufen werden:

Greeting::sayHello();

Innerhalb der Klasse können statische Methoden über das Stichwort "self" aufgerufen werden:

public function otherHello() {
 self::sayHello();
}

Statische Methoden in Drupal

Drupal nutzt das Konzept der statischen Methoden und Eigenschaften intensiv, zum Beispiel im Service-Container. Der Service-Container ist eine Möglichkeit, einen Service über seinen Namen aufzurufen:

$coords = \Drupal::service('router.router_provider');

Inheritance & Extension (Vererbung & Erweiterung)

In der OOP bedeutet Vererbung die Erweiterung eines Objekts (oder einer Klasse), das sogenannte Kind (child), um ein anderes Objekt (oder eine Klasse), der so genannte Elternteil (parent). Dabei sind alle Methoden und Eigenschaften der Elternklasse auch auf dem Kind verfügbar?, mit Ausnahme des Constructors und Descructors. Durch das Konzept der Vererbung kann Boilerplate-Code reduziert und der Refactoring-Aufwand minimiert werden.

Vererbung in Aktion

<?php

class ParentClass {
  // Do Stuff
}

class ChildClass extends ParentClass {
  // Do Stuff
}

Das Stichwort "extends" sorgt dafür, dass die Kindklasse die Elternklasse erweitert. Wenn also ein Objekt der Kindklasse instanziiert wird, hat es alle Eigenschaften und Methoden der Elternklasse plus die Eigenschaften und Methoden, die innerhalb der Kindklasse definiert sind

Den Eltern-Constructor aufrufen

Wie oben erwähnt, ist die Constructor-Methode der Elternklasse nicht automatisch in der Kindklasse verfügbar. Um den Eltern-Constructor aufzurufen, muss er im Konstruktor der Kindklasse ausgeführt werden.

class Class2 extends Class1 {
  public function __construct() {
		parent::construct()
  }
}

Beispiel

Hier kann man Blöcke in Drupal als Beispiel heranziehen: Wenn man einen eigenen/custom Block erstellen möchte, muss die Block-Klasse die BlockBase-Klasse erweitern. Die BlockBase-Klasse sorgt dafür, dass der Block von Drupal erkannt wird, damit man ihn im Admin-Panel nutzen kann. Es wäre sehr ineffizient, wenn man die Funktionen jedes Mal selbst schreiben müsste, wenn man einen neuen Block erstellt. Außerdem müsste man, wenn man einen Fehler findet, diesen in jeder Block-Klasse korrigieren. 

Interfaces (Schnittstellen)

In OOP, ist ein Interface eine Beschreibung aller Methoden, die ein Objekt besitzen muss, um ein X zu sein. Mit anderen Worten: Eine Schnittstelle definiert im Allgemeinen eine Reihe der Methoden (oder Nachrichten), auf die eine Instanz einer Klasse, die dieses Interface implementiert, reagieren könnte.

Ein Interface kreieren

interface NameOfInterface {
  public function nameOfFunction () {
  }
}

Ein Interface implementieren

class NameOfClass implements NameOfInterface {
}

Diese Klasse muss nun alle Methoden des Interfaces implementieren. Andernfalls wird ein Fehler geworfen.

Anwendungsfälle von Interfaces

Hier kann man das Konzept von Nodes und Bundles in Drupal als Beispiel heranziehen: Man möchte zum Beispiel eine Instanz eines Bundles aus der Datenbank holen und dann den Namen dieser Instanz ausgeben. Woher weiß man nun wie man diesen Titel bekommt? Heißt er "Titel" oder "Name"? Dies ist sehr fehleranfällig. Wenn das Bundle jedoch das NodeInterface implementiert, kann man sicher sein, dass es die Methode "label" hat, die einem den Namen liefert.

Interfaces in Drupal

Interfaces sind ein integraler Bestandteil von Drupal. Ihre Macht und ihr Bedarf wird deutlich, wenn man sich die Nodes (und ihre Klassen) näher ansieht. Die Klasse NodeInterface im Core definiert, welche Eigenschaften und Methoden eine Klasse/ein Objekt implementieren muss, um als Node klassifiziert zu werden.

$current_node = \Drupal::request()->attributes->get('node');
$title=’’;

if ($current_node instanceof NodeInterface) {
 $title = $current_node->getTitle();
}

Wir laden zunächst die aktuelle Node aus der statischen Request-Methode von Drupal. Als nächstes wollen wir den Titel abfragen. Da wir wissen, dass jede Node das NodeInterface implementiert und somit die Methode getTitle() implementieren muss, können wir diese Methode sicher auf $current_node aufrufen, solange sichergestellt ist, dass es sich tatsächlich um eine Node handelt.

Factory Pattern

Das Factory Pattern ist kein Standard-OOP-Konzept, das in PHP oder einer anderen Programmiersprache eingebaut ist. Es ist ein Entwurfsmuster. Entwurfsmuster in der Softwareentwicklung sind wiederverwendbare Lösungen für häufige Probleme. Mit anderen Worten, sie sind Vorlagen dafür, wie ein Problem zu lösen ist. 

In PHP ist das Factory Pattern eines der am häufigsten verwendeten und einfachsten Entwurfsmuster. In einfachen Worten: Es wird verwendet, um ein Objekt aus einer anderen Klasse zu instanziieren.

Zum Beispiel:

<?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);
    }
}

Im obigen Beispiel wird die AutomobileFactory verwendet, um eine Instanz der Klasse Automobile zu erzeugen.

Factory pattern in Drupal

Der Core von Drupal verwendet eine Reihe von factory method . Man muss einfach nach "Factory" im Core-Pfad suchen und wird Dutzende von Ergebnissen erhalten. Eine, die man wahrscheinlich schon benutzt hat, ist die ContainerFactory, die bei der Implementierung von Dependency Injection auf Plugins verwendet wird (mehr dazu später). Zum Beispiel:

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'),
   );
 }

Wie man sehen kann, wird hier die Klasse ContainerFactoryPluginInterface implementiert und hat die create()-Methode. Diese sorgen für das korrekte Erzeugen der Abhängigkeiten, z.B. für das Erzeugen aller notwendigen Objekte.

Dependency Injection (Abhängigkeitsinjektion)

Dependency Injection ist ein Verfahren, bei dem ein Objekt die Abhängigkeiten eines anderen Objekts liefert. Es handelt sich also um ein Software-Entwurfsmuster, das die harte Kodierung von Abhängigkeiten vermeidet. Je mehr man online darüber liest, desto verwirrender und komplexer erscheint das Thema. Es ist jedoch ein ziemlich einfaches Konzept. Anstatt in einem Objekt bei Bedarf Objekte aus anderen Klassen (Abhängigkeiten) zu instanziieren, übergibt man die Abhängigkeiten direkt in die Klasse.

Verwendung von Dependency Injection

Betrachten wir dieses Beispiel, bei dem wir keine Dependency Injection verwenden und Abhängigkeiten spontan erzeugen:

class ParentClass {
 public function getUser(){
   $db = new DB();
   $user = $db->getUser();
 }

 public function getPost(){
   $db = new DB();
   $post = $db->getPost();
 }
}

Wie man sehen kann, instanziieren wir hier ein $db-Objekt an zwei verschiedenen Stellen. Das ist weder schön noch wartbar und auch schwer zu testen (z.B. Unit-Tests). Dies kann mit Dependency Injection überarbeitet werden:

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();
 }
}

Hier instanziieren wir ein $db-Objekt nicht zweimal. Stattdessen übergeben wir es in den Constructor und machen so das $db-Objekt zu einer Eigenschaft unserer Eltern-Klasse. Das reduziert doppelten Code und erhöht die Wartbarkeit, wenn man den Code in Zukunft ändern will.

Dependency injection in Drupal

In Drupal wird die Dependency Injection verwendet, um Services in ein Objekt zu injizieren. Dadurch entfällt insbesondere die Notwendigkeit, jedes Mal, wenn ein Service benötigt wird, auf den globalen Service-Container zurückzugreifen. Abhängig von der Art des Objekts, an dem wir arbeiten, sind verschiedene Arten der Injektion von Abhängigkeiten notwendig. Zum Beispiel werden bei der Verwendung von Dependency Injection in Controllern Abhängigkeiten über die Drupal-Factory-Methode create() injiziert. Eine gute Übersicht über die Verwendung von Dependency Injection in Drupal kann be hier gefunden werden.

Traits

PHP unterstützt nur einfache Vererbung. Das heißt, eine Klasse kann nur EINE andere Klasse erweitern. Manchmal muss eine Klasse jedoch Funktionalität von mehreren Klassen erben. An dieser Stelle kommen Traits ins Spiel. 

Traits werden verwendet, um Methoden und Eigenschaften zu deklarieren, die in anderen Klassen verwendet werden können, ohne dass diese Klassen den Trait erweitern müssen. So kann die Klasse eine andere Klasse erweitern und trotzdem von der Funktionalität des Traits profitieren.

Einen Trait kreieren

Traits werden folgendermaßen definiert:

trait MyTrait {
 public function hello(){
   print("hello");
 }
}

Es sollte beachtet werden, dass es nicht mit dem Stichwort "class", sondern mit dem Stichwort "trait" definiert wurde. Wichtig ist auch, dass es möglich ist, dem Trait Access modifiers wie "public" zuzuweisen.

Verwendung eines Traits

Nachdem ein Trait definiert wurde, ist der nächste Schritt, ihn in einer Klasse zu verwenden:

class Greeting {
 use MyTrait;
}

$greeting = new Greeting();
$greeting->hello();

Der Trait wird über das Stichwort 'use' in die Klasse importiert. Dann haben alle instanziierten Objekte aus dieser Klasse Zugriff auf die Eigenschaften und Methoden des Traits.

Traits in Drupal

Ein sehr beliebtes Trait in Drupal ist das StringTranslationTrait aus Core. Wahrscheinlich haben Sie es selbst schon benutzt, wann immer Sie in einer Klasse mit übersetzbaren Strings arbeiten mussten ;).