Teaser - PHP-Framework: Grundlagen

PHP-Framework: Grundlagen

Willkommen zurück! Bevor wir uns ins Eingemachte stürzen und mit den komplexeren Themen beginnen, möchte ich in diesem Beitrag kurz auf die grundlegenden Funktionen eingehen, auf denen dann auch weitere Komponenten basieren. Solltest du direkt auf diesem Beitrag gelandet sein, empfehle ich zuerst die vorhergehenden Beiträge dieser Serie zu lesen. Alle Beiträge bauen aufeinander auf.

Enums

PHP hat zwar native Aufzählungen über die Standard-PHP-Library (kurz SPL). Ich habe mich für mein Framework trotzdem dafür entschieden, eine eigene Implementierung zu verwenden. Die SPL Implementierung hat zwar den Vorteil, das man die Werte als Typ für Funktionsparameter verwenden kann, jedoch muss man für die Verwendung immer Instanzen von Klassen erstellen.

Meine eigene Implementierung setzt stattdessen auf statische Konstanten in einer abstrakten Klasse. Diese erbt wiederum von einer Basis Enum Klasse, die zwei hilfreiche Funktionen bereitstellt. Mit der ersten könnt ihr euch alle definierten Werte zurückgeben lassen, während die zweite dafür zuständig ist, zu überprüfen, ob ein gegebener Wert für die Aufzählung gültig ist.

Hier ein Beispiel für eine Aufzählung, die alle verfügbaren Konfigurationsdateien enthält:

<?php
namespace NextDirection\Framework\Config;

use NextDirection\Framework\Common\EnumBase;

abstract class Types extends EnumBase {

    /**
     * Application configuration
     *
     * @var string
     */
    public const APP = __DIR__ . '/../../config/app.php';

    /**
     * DI Mapping
     *
     * @var string
     */
    public const DI = __DIR__ . '/../../config/di.php';

    /**
     * File cache config
     *
     * @var string
     */
    public const FILE_CACHE = __DIR__ . '/../../config/fileCache.php';
}

Um einen Aufzählungswert zu verwenden, könnt ihr folgende Logik verwenden:

$appConfig = \NextDirection\Framework\Config\Types::APP;

Um zu prüfen, ob ein an eine Funktion übergebener Wert gültig ist, dient der folgende Aufruf:

$isValid = \NextDirection\Framework\Config\Types::isValid($appConfig);

Im Laufe dieser Serie, werde ich immer wieder Beispiele für Enums zeigen. Dabei werde ich dann der Einfachheit halber aber nur noch die Klasse mit den Konstanten ohne jeglichen Overhead zeigen. Es werden also alle Kommentare und Namespaces ebenso wie use Anweisungen fehlen.

Config Reader

Im vorherigen Abschnitt habe ich bereits etwas vorgegriffen und euch die Pfade zu möglichen Konfigurationsdateien, in Form eines Enums, gezeigt. Dieser Abschnitt befasst sich nun mit einer kleinen Hilfsklasse, um diese Konfigurationen einzulesen und auf einzelne Werte daraus zuzugreifen.

Hier zuerst die relativ unspektakuläre Konfigurationsdatei:

<?php
// config/app.php

return [
    'controllerDirectory' => __DIR__ . '/../app/Controller',
    'modelDirectory'      => __DIR__ . '/../app/Model',
    'commandDirectory'    => __DIR__ . '/../app/Command',
    'prettyExceptions'    => false
];

Und hier der Code, um einen Wert dieser Konfiguration einzulesen:

$appConfig = new Reader(Types::APP);
$controllerDirectory = $appConfig->get('controllerDirectory');

Aus Gründen der Übersichtlichkeit, verzichte ich bei diesen Beispielen auf volle Namespaces. Bei Types handelt es sich um das im vorherigen Abschnitt beschriebene Enum. Der Reader liest die Konfiguration aus dem gegebenen Pfad und erlaubt den Zugriff per get(). Im Beispiel wird ein spezieller Wert abgefragt. Wenn ihr die Methode ohne einen Parameter aufruft, bekommt ihr alle Werte als Array übergeben.

Umgebungsvariablen

Dieser Abschnitt hängt ein bisschen mit dem vorhergehenden zusammen. Neben Konfigurationen können über eine entsprechende Hilfsklasse auch Umgebungsvariablen eingelesen werden. Ich habe mich hier bei der Implementierung an Symfony orientiert.

Im Hauptverzeichnis liegt eine Datei namens .env.dist. Diese enthält alle möglichen Umgebungsvariablen, die innerhalb des Frameworks benötigt werden. Um die Umgebungsvariablen zu aktivieren, müsst ihr diese Datei kopieren und .env nennen. Diese Datei ist über die .gitconfig vor versehentlichen Commits geschützt. Ihr könnt diese Datei natürlich beliebig erweitern, wenn ihr für die Anwendungslogik eigene Variablen benötigt. Stellt auf jeden Fall sicher, dass keine sensiblen Informationen in der Datei .env.dist stehen, die versehentlich in euer Repository gelangen könnten.

Der Zugriff auf die Variablen erfolgt vergleichbar zu Konfigurationswerten:

$env = new Environment();
$jwtSecret = $env->get('JWT_SECRET');

Natürlich könnt ihr die Umgebungsvariablen auch direkt per Dependency Injection in einen Controller holen. Mehr dazu in einem der nächsten Beiträge dieser Serie.

Zwei Punkte, die ich hierzu kurz erwähnen möchte:

  • Konventionen sehen vor, Umgebungsvariablen mit Großbuchstaben und Unterstrichen zu schreiben
  • Im Gegensatz zu Konfigurationen, ist es hier nicht möglich die Funktion get() ohne Parameter aufzurufen, um alle Werte zu erhalten

Cache

Je nach Komplexität einer Anwendung, kann es einen großen Unterschied machen, ob die Ergebnisse komplexer Algorithmen zwischengespeichert werden oder nicht. Für mein Framework habe ich daher eine PSR-6 konforme Implementierung eines Dateisystem-Caches integriert.

Da dieser Beitrag lediglich eine kurze Einführung in die Grundlagen darstellen soll, werde ich nicht zu sehr auf Details der Umsetzung eingehen. Im Wesentlichen handelt es sich um eine Implementierung, die den Standard PSR-6 umsetzt.

Hier ein paar Besonderheiten, die zu beachten sind:

  • Die Schlüssel mit denen Werte im Cache gespeichert werden sollen, dürfen folgende Zeichen nicht enthalten: { } ( ) / \ @ :
  • Der Wert null kann nicht gespeichert werden, da er zur Prüfung eines Treffers verwendet wird
  • Jeder Wert wird innerhalb einer Datei gespeichert
  • Die Gültigkeitsdauer eines Wertes wird über die Änderungszeit der jeweiligen Datei geregelt
  • Der Cache kann über die Datei config/fileCache.php konfiguriert werden (Pfad + Standard Gültigkeit)

Request

Wer schon mal eine größere PHP Anwendung entwickelt hat, kann sicher meine Erfahrungen bezüglich der Abarbeitung eines Requests teilen. Man hat mit einer Vielzahl verschiedener Probleme zu kämpfen, für die man auf unterschiedlichste globale Variablen oder Funktionen zurückgreifen muss. Vor allem bei der Entwicklung einer API treten u.a. folgende Situationen ein:

  • Verarbeitung von Anfrage-Headern, um Content-Type oder unterstütze Sprachen auszulesen
  • Auslesen von GET oder POST Variablen
  • Zugriff auf Cookies oder Dateien

Für alle diese Fälle, gibt es innerhalb des Frameworks eine Abstraktion, die einheitliche Aufrufe zum Abfragen benötigter Informationen anbietet. Das beste daran ist, dass ihr über Dependency Injection diese Abstraktion direkt in euren Controller geliefert bekommt. Mehr dazu gibt es, wie bereits erwähnt, in einem der Folgebeiträge.

Folgende Informationen können aus dem Request ausgelesen werden:

  • Die aufgerufene URL
  • Die verwendete HTTP Methode
  • POST, GET, COOKIE und FILES Variablen
  • Das verwendete HTTP Protokoll und ob es sich um eine sichere Anfrage handelt (HTTPS)
  • Die Anfrage-Header
  • Den unformatierten Inhalt der Anfrage (Raw Body)

Für die Verwendung der Header, gibt es einen Punkt zu beachten. Die Browserheader werden vom Webserver in der Variable $_SERVER in der Form HTTP_ACCEPT_ENCODING geliefert. Für die Verwendung in der Request Klasse, wird das Prefix HTTP_ entfernt, und die Schreibweise zu Accept-Encoding geändert. Die verbleibenden Unterstriche werden dabei durch Bindestriche ersetzt und der Anfangsbuchstabe eines jeden Wortes wird groß geschrieben, der Rest des Wortes klein.

Die Zugriffe erfolgen sehr ähnlich zu den bereits beschriebenen Konfigurationen oder Umgebungsvariablen:

$request = Request::createInstance();
$request->getPost();
$request->getCookie('Authorization');
$request->isSecure();
$request->getHeader('Content-Type');

Bei der Instanzierung des Request Objekts gibt es einen Unterschied zu bisherigen Beispielen. Da es sich beim Request um ein Singleton handelt, darf die Erzeugung nicht über den Konstruktor laufen. Es wäre sonst nicht sichergestellt, dass es nur eine Instanz pro Request gibt. Da sich die darin gespeicherten Informationen während der Abarbeitung nicht ändern, sorgt das für entsprechende Geschwindigkeit, da nicht bei jeder Erstellung alle Bestandteile neu eingelesen und transformiert werden müssen.

Für die Zugriffe auf GET, POST, COOKIE und Header Informationen, gilt ähnlich zu Konfigurationen, dass die jeweiligen Funktionen mit und ohne Parameter aufgerufen werden können. Ist ein Parameter angegeben, wird der entsprechende Wert zurückgegeben. Fehlt der Parameter, werden alle Werte als Array geliefert.

Response

Am Ende einer jeden Anfrage steht dann schließlich noch die Herausforderung, eine Antwort an den Browser zu senden. Auch hier gilt es, immer wieder die gleichen Abläufe abzubilden:

  • Setzen benötigter Header, wie z.B. Content-Type
  • Senden des Statuscodes und der Statusnachricht, z.B. 200 OK
  • Senden des eigentlichen Inhalts

Zum Glück, gibt es auch dafür eine Abstraktionsklasse, die ihr per Dependency Injection anfordern könnt.

Im einfachsten Fall, sieht die Verwendung folgendermaßen aus:

$response = new Response();
$repsonse->send();

Ihr erzeugt eine Instanz der Klasse und ruft die Methode send() auf. Ohne weitere Einstellungen, wird in diesem Fall eine leere Antwort an den Browser geschickt. Der Statuscode ist dabei 200 und die Statusnachricht ist OK. Der Content-Type dieser Antwort wird auf text/plain gestellt und die Kodierung ist utf-8.

Alle diese Werte, könnt ihr bei Bedarf über entsprechende Methoden anpassen.

Für die Statuscodes gibt es ein vordefiniertes Enum. Wenn ihr den entsprechenden Code für den Repsonse setzt, wird automatisch auch gleich die Statusnachricht entsprechend dem HTTP Standard gesetzt.

$response->setCode(ResponseCodes::HTTP_CREATED); // 201 Created

Es gibt auch Hilfsmethoden, die das Setzen bestimmter Informationen für häufig benötigte Fälle vereinfachen. Ein Beispiel für eine solche Methode ist das Setzen von JSON Inhalt:

/**
 * @param array $data
 *
 * @return Response
 */
public function setJson(array $data): Response {
    $this->headers['Content-Type'] = 'application/json';
    $this->body = json_encode($data);

    return $this;
}

Dieser Funktion könnt ihr ein Array mit Informationen übergeben, die automatisch konvertiert werden. Außerdem wird der Content-Type der Antwort gleich entsprechend gesetzt.

Zusammenfassung

Ihr habt in diesem Beitrag schon einiges erfahren. Viele, der hier beschriebenen Grundlagen, werdet ihr in den nächsten Beiträgen wiederfinden. Grundlagen sind zwar oft nicht so spannend, aber sie waren für mich während der Entwicklung auf jeden Fall sehr interessant und lehrreich. Was in der Verwendung oft einfach aussieht, muss doch gründlich vorbereitet werden. Es ist auch nicht ausgeschlossen, dass sich bestimmte Umsetzungen während der folgenden Entwicklungen noch verändern werden.

Jetzt will ich euch aber nicht länger aufhalten. Bis zum nächsten Mal.