Warum denn nun noch ein Framework?

Ich entwickele Webanwendungen nun schon seit vielen Jahren, meistens habe ich kein Framework benutzt sondern höchstens einzelne Open-Source-Klassen. Der Sinn, weshalb man sich noch zusätzlich in ein Framework einarbeiten soll, wenn PHP einem doch schon alles an die Hand gibt, war mir nicht klar. Irgendwann machte ich Bekanntschaft mit Design Patterns. Anfangs konnte ich mir nicht vorstellen, wie vorgefertigte Lösungen auf meine speziellen Probleme passen könnten. Doch je mehr ich darüber las, desto mehr wurde ich ein Fan von Design Patterns, weil sie einen vor so vielen Fallstricken bewahren, an die man möglicherweise bei eigenen Implementierungen nicht gedacht hätte. Das führte dazu, dass ich mich immer mehr mit guter Software-Architektur und -qualität auseinander setzte.

Nach und nach wendete ich verschiedene Patterns für Teilprobleme in meinen Anwendungen an, doch nach einiger Zeit stellte ich fest, dass es ziemlich kompliziert ist, eine Anwendung, die auf einer schlechten Architektur basiert, so umzustrukturieren, dass sie wartbarer und flexibler wird. Das war der Zeitpunkt, an dem mir mein alter Code auf die Füße fiel.

So informierte ich mich weiter über die Möglichkeiten, die moderne Softwarearchitektur bietet:

  • von SOLID-Code liest man mittlerweile an jeder Ecke
  • Die Trennung von Model, View und Controller wird einem in jedem OOP-Buch näher gebracht
  • Wie tragen Design Patterns und Anti Patterns zu gutem bzw. schlechten Software Design bei?

All diese Gedanken mündeten letztlich darin, dass ich mir angeschaut habe, was bekannte PHP Frameworks wie Symfony, Zend Framework 2, CakePHP und weitere unter sauberer Softwarearchitektur verstehen. Um es vorweg zu sagen, all diese Framework sind gute Produkte! Leider musste ich aber feststellen, dass das theoretische Wissen von guter Architektur, das ich in vielen Blogs und Büchern gelesen hatte, nicht in allen Punkten Anwendung fand:

  • Controller und Model sind in fast allen modernen PHP-Frameworks vermischt (man greift in den Controllern auf die Datenbank zu), was das Unit-Testing enorm erschwert
  • Einige Optionen zur Programmsteuerung werden mit mehr oder weniger “magischen” Strings übergeben. Das fällt vor allem beim Routing auf, bei dem fast alle Frameworks den aufzurufenden Controller per String definieren – der Tod für jede IDE
  • viele Frameworks schreiben dem Nutzer vor, welche Template-Engine er nutzen muss. Das erschwert die Interoperabilität enorm (z.B. wenn man eine alte Anwendung mit vorhandenen Templates hat, die das neue Framework nicht unterstützt)

In Kenntnis dieser Schwächen und auch um mich selbst im Erstellen von einer sauberen OOP-Architektur zu üben, wage ich nun mit dem Fundament Framework einen eigenen Versuch, ein Framework zu erstellen. Die Kritik wird wahrscheinlich groß sein, aber genau das ist mein Ziel, denn nur so kann am Ende eventuell ein Framework entstehen, das oben genannte Schwachpunkte eleganter löst.

Jan hat 152 Beiträge geschrieben

13 Kommentare zu “Warum denn nun noch ein Framework?

  1. Nikolaj Giebelhaus sagt:

    Hallo,

    also ich muss sagen, dass mich deine Kritikpunkte stützig machen.

    – Weder Symfony, noch Zend Framework greifen direkt im Controller auf die Datenbank zu. Richtig ist, dass Controller Models nutzen, was auch legitim ist. Irgendwie muss der Controller dafür sorgen, dass die View mit Daten angereichert werden kann. Dank Dependency Injection in Symfony2 und Zend Framework 2 lässt sich die Model-Schicht ziemlich leicht Mocken. Ich sehe nicht, dass das Unit-Testing enorm erschwert wird. Wenn man in Symfony noch Param Converter nutzt, lassen sich Models auch ohne Umweg über DI mocken. Außerdem habe ich die Erfahrung gemacht, dass Controller meistens funktional getestet werden, während Models, Helper usw UnitTests unterzogen werden.

    – warum soll den “Tod für jede IDE” bedeuten, wenn man Controller per String definiert?

    – Weder Zend noch Symfony schreiben eine Template-Engine vor. Zend Framework liefert gar keine eigene Engine mit, Symfony2 liefert zwar eine mit (Twig), zwingt aber niemanden dazu, diese zu nutzen.

  2. Jan sagt:

    Ich will es mal an einem Beispiel aus dem Symfony-Book beschreiben:

    class MyController {
        public function showAction($id)
        {
            $product = $this->getDoctrine()
                            ->getRepository('AppBundle:Product')
                            ->find($id);
     
             // ... do something, like pass the $product object into a template
        }
    }

    Allein durch diese Anweisung verknüpfst Du Deine Controller mit Doctrine. Ein späterer Austausch des ORM wird schwierig. Oder anders gesagt: Wenn sich Dein Zugriff auf das ORM in irgendeiner Form ändert, müsste immer auch der Controller mit angepasst werden.
    Eleganter wäre meiner Meinung nach ein separates Controller-Model zu haben, das der Controller immer dann fragen kann, wenn er Daten benötigt. In diesem Beispiel würde das so aussehen:

    class MyController {
        protected $MyControllerModel;
     
        /**
        * @param ControllerModel $controllerModel Interface, das alle Methoden enthält, die ein 
                                                  MyController-Objekt zum Arbeiten benötigt
        */
        public function __construct(ControllerModel $controllerModel) {
            $this->controllerModel = $controllerModel;
        }
     
        public function showAction($id)
        {
            $product = $this->getControllerModel()
                            ->getProductById($id);
     
             // ... do something, like pass the $product object into a template
        }
    }
     
    class MyDoctrineControllerModel implements MyControllerModel {
        public function getProductById($id) {
            return $this->getDoctrine()
                        ->getRepository('AppBundle:Product')
                        ->find($id);
        }
    }

    Nun könntest Du auch ganz einfach das ORM wechseln, indem Du einfach eine Klasse MyPropelControllerModel schreibst. Am Controller würde sich dann nichts ändern. Ich bin mir nicht sicher, ob diese Umsetzung nicht sogar mit Symfony möglich wäre. Könnte man beim Routing einen Konstruktorparameter für den Controller mitgeben?

    Aber was meinst Du damit, dass “Controller meistens funktional getestet werden”. Eigentlich sollte doch jede Klasse einem Unit-Test unterzogen werden können.

    – warum soll den “Tod für jede IDE” bedeuten, wenn man Controller per String definiert?

    PhpStorm z.B. hat eine Funktion, mit der man alle Aufrufe einer Klasse anzeigen kann. Das ist z.B. hilfreich, wenn man eine Klasse bei einem Refactoring so umstrukturiert hat, das sie inkompatibel zu ihrem bisherigen Aufruf ist und gucken will, an welchen Stellen noch der alte Aufruf benutzt wird. Wenn dann die Controller-Klassen nur als String hinterlegt sind, kann sie die IDE nicht finden und man denkt fälschlicherweise, dass der alte Aufruf nun nicht mehr benutzt wird und das Refactoring erfolgreich abgeschlossen ist.

    Weder Zend noch Symfony schreiben eine Template-Engine vor.

    Kann man in Symfony eine eigene Template-Engine benutzen? Ich habe bisher nur Beispiele mit Twig oder reinem PHP gesehen.

  3. Mischosch sagt:

    deine aufgeführten beispiele sind eben keine guten beispiele für gute Architektur. (Aber Beispiele, die zeigen sollen, wie man anfangen kann – in der ZF Welt wurde sehr kritisiert, dass solche Beispiele in der doku existieren). Eon Service Locator/DI Aufruf im Controller hat dort nichs verloren, Ahängiggkeiten bitte von aussen in den Controller geben. Die Logik bitte in möglichst Framework unahängige (Service) Klassen packen.

    Bzgl view – Zend\View extenden – aber es gibt bestimmt für jede Template ENginge bereits ein Module, dass das bietet.

  4. Jan sagt:

    @Mischosch: Meinst Du die Beispiele in meinem Kommentar oder im Beitrag? In meinem Kommentar gibt es keinen Service Locator oder Dependency Injection Container, weil man durch deren Einsatz seine Klassen eben mit dem DIC koppeln würde. Man gibt dem Controller einfach ein eigenes “Controller-Model” (wird im Konstruktor von außen übergeben, genau wie Du es vorschlägst) an die Hand, sodass es nicht selbst die Datenbank- bzw. ORM-Klassen fragen muss. Dieses Controller-Model stellt die Verbindung zwischen Controller und den Services dar. Oder anders gesagt: Es abstrahiert den Zugriff auf die Service-Klassen, sodass man ohne den Controller verändern zu müssen die Datenbank-Abfragen (bzw. den Zugriff auf die Query-Klassen) ändern kann.
    Welche Nachteile siehst Du an diesem Entwurf?

  5. Nikolaj Giebelhaus sagt:

    Was Verwendung von Models im Controller angeht: Symfony zwingt Entwickler ja nicht dazu, Doctrine-Models (oder Propel-Models oder XY-Models) direkt im Controller zu nutzen. Es steht dir Frei, deine eigene drum herum zu bauen und diese zu verwenden. Wie mein Vorredner schon angemerkt hat, zeigen die Beispiele in den Dokus der Framework nicht den einzig möglichen Weg, sondern einen, mit dem auch ein Neuling direkt was anfangen kann.

  6. Nikolaj Giebelhaus sagt:

    In meinem Kommentar gibt es keinen Service Locator oder Dependency Injection Container, weil man durch deren Einsatz seine Klassen eben mit dem DIC koppeln würde.

    So wie du das machst, verkoppelst du deine Klassen aber miteinander. Eine Frage: an welcher Stelle hast du vor, dein ControllerModel an den Controller zu übergeben? Wo entscheidest du, welcher Controller welches Model braucht? Was machst du, wenn du en einem Controller mehrere Models brauchst? Dein ControllerModel hängt seinerseits nun von Doctrine ab. Wie handelst du diese Abhängigkeit (und weitere, tiefer verwurzelte)?

    Mein Liebling-Konzept für das übergeben von Abhängigkeiten an Controller-Methoden sind die “Param Converter”. Man definierst z.B. eine route /products/{product}

    und im Controller eine Methode wie z.B.

    public function showAction(Product $product) {
        // Hier der Code
    }

    Anhand der TypeHints erkennt Symfony, dass du eigentlich ein Objekt der Klasse “Product” brauchst, lädt es und übergibt es an die Methode. Damit vermeidet man die direkte Verwendung von DI im Controller ohne mit eigenen Settern/Gettern oder Konstruktor-Parametern rumhantieren zu müssen. Der Haken: standardmäßig geht es mit Doctrine-Entities und einer Handvoll anderer Klassen. Aber auch hier gilt: du kannst eigenen ParamConverter schreiben, welcher mit deinen eigenen Models umgehen kann.

  7. Mischosch sagt:

    ich meinte die beispiele im kommentar. Ob du die Abhängigkeiten per Hand in einen Controller schiebst, oder das dann doch lieber nen Service Locator/Dic benutzt, ist dir überlassen.

    Ich persönlich finde das in ZF2 und eigenen Factories eigentlich sehr gut gelöst. Der Blogeintrag beschreibt das auch noch einmal sehr gut: http://www.masterzendframework.com/tutorial/howto-constructor-injection-in-zf2

    Dein Ansatz klingt schon vernünftig, kommt immer auf den Fall/Bedarf an, ob das nun ControllerModel heisst oder Service, ist nach hinten raus relativ egal.

    WIe gesagt, ich würde mir möglichst Mühe geben, Service Klassen hinter dem Controller Framework unabhänig zu halten.

    Ach ja: und es braucht definitiv kein neues Fraamework mehr: du kannst aber gerne eines schreiben, wenn du dadurch lernen willst. Aber erwarte nicht, dass es von anderen groß wahrgenommen werden muss. (Die “Lücken”/Fehler, die du da siehst, kann ich persönlich nicht bei Symfony oder ZF sehen.)

  8. Jan sagt:

    @Mischosch: Genau deshalb entwickle ich ja ein Framework – um nachzuvollziehen, wie andere Frameworks es gemacht haben und ob es evtl. Möglichkeiten gibt, wie man vorhandene Frameworks effektiver einsetzen kann. Denn hätte ich wie z.B. in den Symfony-Dokus beschrieben in den Controllern direkt auf die Model-Klassen zugegriffen, bestünde wie oben beschrieben bereits ein Problem beim Testen. Und wenn am Ende herauskommt, dass Symfony oder Zend das ja alles ähnlich oder besser umsetzt, als ich es mir ausgedacht habe, na dann um so besser! Ich möchte niemanden konvertieren 😉

    @Nikolaj:

    So wie du das machst, verkoppelst du deine Klassen aber miteinander. Eine Frage: an welcher Stelle hast du vor, dein ControllerModel an den Controller zu übergeben? Wo entscheidest du, welcher Controller welches Model braucht?

    Zu einem Controller gibt es genau ein Controller-Model-Interface, das auch nur zu diesem Controller gehört. Das Objekt, das dieses Controller-Model-Interface implementiert, wird im Konstruktor an den Controller übergeben. Und damit kopple ich nichts aneinander, da ich ja ein Interface für das Controller-Model nutze. Natürlich wäre das Controller-Model dann an das jeweilige ORM gekoppelt – aber das ist ja genau deren Sinn, sodass ich die Controller-Models einfach austauschen kann, wenn ich das ORM wechseln möchte.

    Mein Liebling-Konzept für das übergeben von Abhängigkeiten an Controller-Methoden sind die “Param Converter”. Man definierst z.B. eine route /products/{product}

    und im Controller eine Methode […]
    Anhand der TypeHints erkennt Symfony, dass du eigentlich ein Objekt der Klasse “Product” brauchst, lädt es und übergibt es an die Methode.

    Genau das ist mir zu viel “Magic”. Man weiß überhaupt nicht mehr, was im Hintergrund passiert. Ist wahrscheinlich Geschmackssache, ob einem das gefällt.

  9. Nikolaj Giebelhaus sagt:

    Das Objekt, das dieses Controller-Model-Interface implementiert, wird im Konstruktor an den Controller übergeben. Und damit kopple ich nichts aneinander, da ich ja ein Interface für das Controller-Model nutze.

    Ich möchte gerne meine Frage wiederholen: wo und wann passiert die Übergabe des ControllerModels an den Controller? Ich weiß, im Konstruktor, aber wo wird dieser aufgerufen? Was passiert, wenn ein Controller mehrere unterschiedlichen Models benötigt? Wie löst du die Abhängigkeiten der Models auf (nicht zu vergessen, Abhängigkeit könne ihrerseits Abhängigkeit haben, die wiederum Abhängigkeit haben usw.) Oder ist in deinem Framework auch eine DI-Komponente vorgesehen?

    Natürlich wäre das Controller-Model dann an das jeweilige ORM gekoppelt – aber das ist ja genau deren Sinn, sodass ich die Controller-Models einfach austauschen kann, wenn ich das ORM wechseln möchte.

    Dieses Ziel erreichst du in deinem Beispiel aber nicht.MyDoctrineControllerModel kapselt zwar die Logik um ein Produkt zu finden, stimmt. Aber was liefert die getProductById zurück? Richtig, eine Doctrine-Entity! Wenn du diese im Controller verwendest und dann den ORM austauschen willst, dann reicht es unter Umständen nicht aus, dein ControllerModel zu ändern. Schließlich haben andere ORM auch andere Entity-Objekte, mit anderen Methoden usw. Also wird es evtl. doch noch notwendig werden, Controller anzupassen.

    Genau das ist mir zu viel “Magic”. Man weiß überhaupt nicht mehr, was im Hintergrund passiert. Ist wahrscheinlich Geschmackssache, ob einem das gefällt.

    Du hast recht, es ist Geschmackssache. Aber als “Magic” kann man das ganze nur bezeichnen, wenn man es “blind”. verwendet. Wenn man sich damit auseinandergesetzt und das Prinzip verstanden hat, weiß man auch, was im Hintergrund passiert.

  10. Jan sagt:

    wo und wann passiert die Übergabe des ControllerModels an den Controller? Ich weiß, im Konstruktor, aber wo wird dieser aufgerufen?

    Das würde beim Anlegen der Route passieren, aber das führt schon fast zu weit an dieser Stelle. Ich werde ja die einzelnen Komponenten dann in einzelnen Beiträgen vorstellen und dann können wir ja einfach nochmal darüber “reden”, was man anders lösen könnte / sollte.

    Was passiert, wenn ein Controller mehrere unterschiedlichen Models benötigt?

    Das wäre nicht möglich, da zu einem Controller genau ein ControllerModel gehört. Dieses beinhaltet alle Funktionen, die der Controller abrufen kann, um auf externe Daten zuzugreifen (Datenbank, HTTP-Request usw.).

    Aber was liefert die getProductById zurück? Richtig, eine Doctrine-Entity

    Da hast Du recht. Hier müsste dann ein abstraktes Product-Model erschaffen werden, in das sowohl die Doctrine-Entity als auch das Objekt eines anderen ORM umgewandelt werden kann. Da liegt tatsächlich noch ein Schwachpunkt, über den ich nochmal nachdenken muss.
    Ich habe meine Anwendungen in der Vergangenheit einfach zu sehr an das eingesetzte ORM gekoppelt, sodass ich kaum noch richtig Unit-Tests schreiben konnte. Das würde ich in diesem Versuch gern ändern. Aber dieses Problem scheinst Du ja auch zu haben, oder? Siehe

    Außerdem habe ich die Erfahrung gemacht, dass Controller meistens funktional getestet werden, während Models, Helper usw UnitTests unterzogen werden.

    Oder wie meinst Du das “funktional”?

  11. Patrick Bauer sagt:

    Das wäre nicht möglich, da zu einem Controller genau ein ControllerModel gehört.

    Empfindest du das nicht als sehr einschränkend? Ich denke nicht, dass solche Vorgaben etwas in einem Framework zu suchen haben, welches besonderes auf sauberen Software-Prinzipien aufbaut.

  12. Jan sagt:

    Nein, ich empfinde das nicht als einschränkend, dass ein Controller genau eine zugehörige Klasse hat, in der sämtliche Methoden enthalten sind, die der Controller für den Zugriff auf Daten benötigt. Viel mehr wird dadurch die Trennung der Logik (Controller) von den Datenzugriffen (Controller-Model bzw. Service) und von der Datenhaltung (ORM-Klassen) gewährleistet.

Eine Antwort schreiben

Ihre E-Mail-Adresse wird nicht veröffentlicht. Benötigte Felder sind markiert mit *

You may use these HTML tags and attributes: <a href=""> <blockquote cite=""> <pre lang=""> <b> <strong> <i> <em>