mark: PHP - OOP - Wie sollte ich am besten meinen Code strukturieren?

Beitrag lesen

Ich bin nun zur Tat geschritten und habe versucht eure Ratschläge in Code umzuwandeln. Ich hoffe ich überstrapaziere eure Geduld nicht, wenn ich hier meinen Code und meine Überlegungen poste und Euch um Feedback bitte. Ich habe erst wenige Zeilen Code geschrieben, doch ich glaube bereits da sieht man die grundlegenden Probleme, die ich mit der Strukturierung Objekt orientierter Programmierung habe.

Ich bitte auch Rolf gleich schon im Voraus um Entschuldigung, sollte ich seine gut gemeinten Ratschläge völlig in den Sand gesetzt haben :)

Also los! Hosen runter, auf gehts!

#INTRO:

Meine Entwicklungsumgebung

  • Editor: Visual Studio Code mit Plugins für PHP-Intellisense und XDebug
  • php 7.1.1 ich verwende den mitgelieferten Server php -S localhost:8000. Kein Apache o.ä.
  • composer habe ich zur Installation des language-Servers benötigt, so, dass ich Intellisense habe und auch für's autoloading der Klassen, was ich aber nicht hinbekommen habe.
  • npm als paketmanager für's Frontend

Anmerkung: Ich möchte keine vollwertige IDE verwenden, da mich diese mMn, ähnlich wie ein Framework von der Erlernung der Basis abhält und ich "Magie" vermeiden möchte. Auch auf die Erlernung einer neuen IDE möchte ich zunächst verzichten.

Meine Ordner- und Dateistruktur:

  • assets
  • config
    • config.json
  • data
    • anwendung_v1.0.xml
    • anwendung_v2.0.xml
    • beliebigerDateiname.xml
  • src
    • changelog
      • App.php
      • ChangelogReader.php
      • Config.php
      • Context.php
      • Language.php
      • SortData.php
      • TransformData.php
      • UrlParams.php
  • vendor
  • index.php
  • composer.json
  • composer.lock
  • package.json

Ordner "assets": Hier befinden sich css, js, und img. Ich gehe nicht weiter darauf ein, weil irrelevant.

Ordner "config": Hier befindet sich die Datei config.json, welche das Konfigurationsobjekt meiner Anwendung enthält.

Ordner: "data": Hier befinden sich die ganzen XML-Dokumente. Pro Datei ein Release mit "Subreleases". Schema folgt weiter unten.

Ordner "src\Changelog": hier befindet sich mein PHP-Quellcode. Auf die Klassen gehe ich weiter unten ein.

Die restlichen Dateien brauche ich glaube ich nicht beschreiben. Ich hoffe meine Ordnerstruktur ist weitestgehend Standard.

ANS EINGEMACHTE

Workflow

Mein Workflow ist im Moment sehr holprig. Ich teste die Ausgabe einer jeden Methode, indem ich die entsprechende Klasse in der index.php initialisiere und dann die Methode mit print_r ausgebe. Scheint mir recht Zeitaufwendig. Für Anregungen -und sei es auch nur ein Wink mit dem Zaunpfahl- wäre ich dankbar. Ich habe zwar einen Debugger, aber den nutze ich nicht wirklich, weil ich auch da erst mal die Methode aufrufen muss, damit ich sie debuggen kann. Da bin ich gleich schnell mit print_r.

Meine Überlegungen zum Code

Ich habe versucht mich an Rolfs Vorschläge zu halten. Manchmal ist das gelungen, oft haben sich dadurch erst neue Problemstellugen aufgetan, die ich nicht immer zufriedenstellend handeln konnte.

Die erste Hürde für mich war, wo ich denn eigentlich beginnen sollte. Ich hab mir gedacht das Lesen und Parsen der Konfigurationsdatei könnte ein guter Einstieg sein. Ich habe mich hier für das Singleton-Pattern entschieden, aus dem vielleicht naiven Grund, dass dieses Pattern verhindert, dass ich eine Klasse mehrfach initialisiere, so, dass nur ein Objekt exisitiert und, weil in den Beschreibungen stand, dass das Singleton-Pattern dafür geeignet ist Objekte global zur Verfügung zu stellen. Ich habe natürlich auch gelesen, dass das Singleton-Pattern mit Vorsicht zu genießen ist, ja sogar vollkommen abzulehnen, weil es nur schwer testbar (TDD) ist und man es durch Dependency-Injection ersetzen sollte. Dependency-Injection kann ich noch nicht und TDD wollte ich mir nicht als zusätzliche Hürde auferlegen. Deshalb hier mein Code. Er findet sich unter src/Changelog/Config.php. Der Datei- und Klassennamen ist etwas ungünstig. Ich werde das in ConfigReader umbenennen. Zunächst aber das Konfigurationsobjekt:


{
    "dataFolder": "data", 
    "iconTable": {
        "bugfix": "bug_report",
        "feature": "star"
    },
    "avaiableLang": [
        "de",
        "en",
        "fr",
        "it"
    ],
    "defaultLang": "de",
    "dateFormat": "d.m.Y",
    "translations": {
        "Application":{
            "de":"Anwendung",
            "it":"Applicazione",
            "fr":"Application"
        },
        "Language":{
            "de":"Sprache",
            "it":"Lingua",
            "fr":"Langue"
        },"All":{
            "de":"Alles",
            "it":"Tutto",
            "fr":"Tout"
        },
        "Jump to version":{
            "de":"Springe zu Version",
            "it":"Salta alla versione",
            "fr":"Aller à la version"
        },
        "Select application":{
            "de":"Anwendung auswählen",
            "it":"Scegliere applicazione",
            "fr":"Sélectionner application"
        },
        "Select language":{
            "de":"Sprache wählen",
            "it":"Scegli lingua",
            "fr":"Sélectionner Langue"
        }
    }
}


<?php
namespace Changelog;

class Config {

    public $data;
    private static $instance = null;
        
    private function __construct(){
        return $this->getConfigObject();
    }

    public static function getData() {     
        if(self::$instance == null) {
            self::$instance = new Config();
        }
            
        return self::$instance;
    }

    private function getConfigObject(){
        $configJson = file_get_contents($_SERVER['DOCUMENT_ROOT'] . "\config\config.json", false);
        $this->data = json_decode($configJson, true);
    }

    private function __clone() {
    }
     
    private function __wakeup() {
    }
}

Ich kenne MVC nur oberflächlich, aber ich weiß, dass da an erster Stelle ein Router steht, der die einkommende Request verarbeitet und damit dann einen Controller aufruft. Also habe ich gedacht, es wäre eine gute Idee mit der Umsetzung eben dieses Routers fortzufahren.

Nur habe ich kein Routing im klassischen sinne, sondern nur 3 optionale Parameter, die mittels GET übergeben werden (Sprache "l", Version "v" und Anwendungs Name "a"). Die Seite die aufgerufen wird ist immer die selbe, nur eben mit anderen Queries. Wenn kein Query vorhanden ist gebe ich einen leeren String zurück. Ich verwende aus oben genannten Gründen wieder das Singleton-Pattern. Die Datei ist src/Changelog/UrlParams.php

<?php
namespace Changelog;


class UrlParams {
    public $data;
    private static $instance = null;
        
    private function __construct(){
        return $this->getUrlParams();
    }

    public static function getData() {     
        if(self::$instance == null) {
            self::$instance = new UrlParams();
        }
            
        return self::$instance;
    }

    private function getGETParam($param){
        if( isset($_GET[$param]) ){
            return $_GET[$param];
        }else{
            return '';
        } 
    }

    private function getLanguage (){
        return $this->getGETParam('l');
    }

    private function getApplicationHash(){
        return $this->getGETParam('a');
    }

    private function getVersionHash(){
        return $this->getGETParam('v');
    }

    public function getRequestParams(){
        return $this->requestParams;
    }

    public function getUrlParams(){
        $this->data["language"] = $this->getLanguage();
        $this->data["apphash"] = $this->getApplicationHash();
        $this->data["versionHash"] = $this->getVersionHash();
    }

    private function __clone() {
    }
     
    private function __wakeup() {
    }
}

Dann habe ich mir den ChangelogReader vorgenommen. Dieser liest alle im Ordner "data" enthaltenen XML Dateien und kettet sie aneinander. Auch hier habe ich wieder das Singleton-Pattern verwendet. Da ich dieses Array ja global zur Verfügung stellen muss, damit ich es anschließend gemäß des Konfigurationsobjektes, und den GET-Parametern transformieren, filtern und sortieren kann.

Hier wurde ich zum ersten mal stutzig, ob das mit dem Singleton-Pattern wirklich so eine gute Idee ist. Ich hatte und habe dafür aber keine Argumente dagegen, außer, dass es sich nicht gut anfühlt dieses Pattern so häufig zu verwenden. Deshalb nachfolgend: Singleton die zum 3. Mal.


<?php
namespace Changelog;

use Changelog\Config;

class ChangelogReader {
    public $data = [];
    private static $instance = null;
        
    private function __construct(){
        return $this->getFileContent();
    }

    public static function getData() {   
        if(self::$instance == null) {
            self::$instance = new ChangelogReader();
        }
            
        return self::$instance;
    }

    private function getFileContent(){
        $fileList = $this->getFileList();
        
        foreach($fileList as $filename) {
            $xmlFile = file_get_contents($filename, FILE_TEXT);
            $xml = simplexml_load_string($xmlFile);

            array_push($this->data, $xml);
        }
    }

    private function getFileList() {
        $files = glob( Config::getData()->data['dataFolder'] . "/*xml");

        if ($files == false || empty($files)) {
            die("NO FILES FOUND");
        }

        return $files;
    }

    private function __clone() {
    }
     
    private function __wakeup() {
    } 
}  

So. Nun hatte ich alle Informationen beisammen, um gemäß Rolfs Ratschlägen das Context-Objekt zu erstellen. In der nachfolgenden Klasse bündle ich alle Informationen aus UrlParams, Config und ChangelogReader. Von einem ChangelogWriter sehe ich vorerst der Einfachheit halber ab. Und wieder grüßt das Singleton-Pattern:


<?php
namespace Changelog;

use Changelog\UrlParams;
use Changelog\Config;
use Changelog\ChangelogReader;

class Context {
    public $context = [];
    private static $instance = null;
        
    private function __construct(){
        return $this->getContext();
    }

    public static function getData() {    
        if(self::$instance == null) {
            self::$instance = new Context();
        }
            
        return self::$instance;
    }

    private function getContext(){
        $this->context["urlParams"] = UrlParams::getData()->data;
        $this->context["config"] = Config::getData()->data;
        $this->context["data"] = ChangelogReader::getData()->data;
    }

    private function __clone() {
    }
     
    private function __wakeup() {
    } 
} 

Die Idee war nun, das mit dieser Klasse erstellte Context-Objekt an eine Klasse mit dem Namen "App" zu übergeben darin befindet sich eine Methode Worker, die die gesammelten XML-Daten gemäß der GET-Parameter und den Konfigurationseinstellungen abarbeitet.

Hier habe ich nun, das Problem, dass ich nicht weiß, wie ich das Objektorientiert abbilden soll. Die Methode "Worker" würde ja dann nur statische Funktionen aufrufen und mein riesengroßes Context-Objekt als Parameter übergeben. Das riecht wieder nach Spaghetti-Code und ich habe da bislang noch keinen Ausweg gefunden. Erschwerdend kommt hinzu, dass ich nicht weiß, ob das, was ich bis jetzt gemacht habe überhaupt gut ist.

Deshalb bitte ich um Euer Feedback zu der allgemeinen Vorgehens- und Denkweise, meinem Workflow, zu Variablen- und Klassennamen, zur Strukturierung des Projektes und vielleicht ein Tipp, wie ich weitermachen kann.

Besten Dank.

mark

P.S.: In der index.php verwende ich folgende Codzeile, dass meine Klassen automatisch geladen werden. Mit dem vom Composer angebotenen autoloader habe ich das leider noch nicht hinbekommen.

<?php
namespace Changelog;

spl_autoload_register(function ($class) {
    include 'src\\' . $class . '.php';
});