borisbaer: PHP-Klassen in Framework global verfügbar machen

Hallo,

ich habe Probleme damit, in meinem PHP-Framework Klassen global verfügbar zu machen.

Ich möchte die Klasse nicht im View instanziieren müssen, d.h. anstatt

<?php $session = new \Core\Session;

if ( $session -> isGuest() ): ?>

möchte ich einfach nur

<?php if ( $session -> isGuest() ): ?>

schreiben können.

Nun rendern meine Controller die Views und ich könnte die Klasse als Variable $session in in den Parametern übergeben, z.B.

return View::render( 'user', [ 'session' => new Session ] );

Aber wenn ich die Klasse auch im Layout verwenden oder darauf verzichten möchte, sie jedes Mal beim Rendern zu übergeben, dann müsste ich einen Weg finden, sie überall auf jeder Seite verfügbar zu machen. Ich hatte versucht, sie in der index.php zu definieren, doch ohne Erfolg.

Sollte ich es vielleicht mit der Router-Klasse versuchen? Mir scheint das die einzige Möglichkeit.

Grüße
Boris

akzeptierte Antworten

  1. Lieber borisbaer,

    Ich möchte die Klasse nicht im View instanziieren müssen,

    warum eigentlich nicht?

    Naja, Du könntest mWn eine Variable global verfügbar machen:

    $sess = new \Core\Session;
    
    class MyClass {
      public function myMethod () {
        global $sess;
        if ($sess->isGuest()) {
          //
        }
      }
    }
    

    Liebe Grüße

    Felix Riesterer

    1. Hallo Felix,

      Ich möchte die Klasse nicht im View instanziieren müssen,

      warum eigentlich nicht?

      eine Klasse, die ohnehin global verfügbar gemacht werden soll, immer wieder neu zu instanziieren scheint mir unnötig. Aber darum geht es mir eigentlich gar nicht. Ich muss auch im Layout auf eine bestimmte Klasse zugreifen und das Layout wird nicht durch einen Controller gerendert, sondern durch die View-Klasse. Ich könnte hier die entsprechende Variable übergeben, doch versuche ich, gerade das zu vermeiden und nach Möglichkeit die index.php dafür zu bemühen.

      Naja, Du könntest mWn eine Variable global verfügbar machen:

      $sess = new \Core\Session;
      
      class MyClass {
        public function myMethod () {
          global $sess;
          if ($sess->isGuest()) {
            //
          }
        }
      }
      

      Wie unten beschrieben, habe ich das Problem, dass die Variablen meiner index.php nicht mehr auf den gerenderten Seiten abgerufen werden können. Ich könnte natürlich direkt in die Layout-Datei schreiben, z.B. <?php $session = new \Core\Session; ?>. Allerdings gefällt es mir nicht, Klassen direkt in Templates zu laden.

      Grüße
      Boris

  2. Hallo borisbaer,

    Sehe ich das richtig, dass du nur ein Objekt der Session Klasse hast? In dem Fall solltest du dich mit dem Singleton Pattern befassen.

    Du würdest das Session-Objekt dann in einer statischen Variable der Session-Klasse speichern und einmal in der index.php initialisieren.

    Zugriff dann beispielsweise mit

    \Core\Session::$instance

    Das Namespace-Präfix kannst du mit use loswerden.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      Sehe ich das richtig, dass du nur ein Objekt der Session Klasse hast? In dem Fall solltest du dich mit dem Singleton Pattern befassen.

      so ist es. Das Singleton Pattern sieht sehr vielversprechend aus. Vielen Dank für den Hinweis! 🙂

      Du würdest das Session-Objekt dann in einer statischen Variable der Session-Klasse speichern und einmal in der index.php initialisieren.

      Seltsamerweise habe ich keinen Zugriff auf Variablen in der index.php. Auf Funktionen, die ich in der index.php erstelle, hingegen schon. Ich kann mir das nicht erklären.

      Bei mir wird die App in der index.php folgendermaßen gestartet:

      ( new App(
      	[ 'httpMethod' => $_SERVER['REQUEST_METHOD'], 'uri' => $uri ],
      	new Config( $_SERVER ),
      	$router,
      ) ) -> run();
      

      Die run-Funktion wiederum startet den dispatcher der Router-Klasse:

      public function run(): void
      {
      	echo $this -> router -> dispatch( $this -> request['httpMethod'], $this -> request['uri'] );
      }
      

      Grüße
      Boris

      1. Hallo,

        Singleton-Pattern wird dir sehr kurzfristig bei deinem Problem helfen, langfristig aber viele neue Probleme verursachen. Insbesondere die (Unit-)Testbarkeit von Code leidet unter Singleton-Patterns sehr (da keine "Seams" zum injecten von Testdouble/-Mocks/... vorhanden sind), und auch architekturell ist es unschön (da eine Dependency deines Codes unsichtbar wird). Daher gilt das Singleton-Pattern generell eher zu den Anti-Pattern, von denen ich abrate (und das ist eine sehr höfliche Formulierung!).

        Beschäftige dich mit Dependency Injection. Wenn deine Klasse/Code eine Abhängigkeit zu einem bestimmten Objekt hat ist es nicht dessen Aufgabe, dieses Objekt zu erstellen oder zu holen. Stattdessen sollte es dem Code übergeben werden.

        Folgende Videos waren für mich ein Eye-Opener um testbaren Code zu entwickeln:

        Seitdem habe ich eine gesunde und tiefe Aversion gegen Singletons und statische Methoden entwickelt.

        Viele Grüße Matti

        1. Hallo Matti,

          du hast natürlich grundsätzlich recht. Ein Singleton, der tatsächlich stateful ist und die Wiederholbarkeit von Tests sabotiert, ist Käse.

          Deswegen arbeite ich in solchen Situationen auch gerne mit einem Context-Objekt, wo alle "globalen" Dinge drin stecken und der an alle Objekte durchgereicht wird, die im Verlauf der Requestverarbeitung entstehen.

          Für Tests baut man dann einen Mock-Context auf.

          Aber - wer mit DI arbeitet, hat auch einen DI-Container. Eine korrekte Implementierung eines Singleton in einer DI-Welt setzt voraus, dass dieser Singleton ebenfalls im Container gehostet wird, und nicht in einer statischen Variablen einer Klasse. Damit das funktioniert und testbar bleibt, muss man dann natürlich den DI Container überallhin durchschleifen und darf ihn nicht als globalen State missbrauchen. Aber das lässt sich bei einem ordentlichen DI Framework ja deklarativ erledigen.

          Keine Ahnung, was PHP da im Angebot hat, bislang habe ich keine professionellen PHP Projekte gemacht. Laravel scheint da ja ein magisches Teil zu enthalten, muss ich mir vielleicht mal anschauen…

          Rolf

          --
          sumpsi - posui - obstruxi
          1. Hallo,

            entschuldige die späte Antwort, ich schaue nicht jeden Tag hier rein.

            Singleton

            In den erwähnten Videos wird der wichtige Unterschied zwischen Singleton (dem Pattern) und einem singleton (ein Objekt, welches nur einmal existieren darf) gemacht. Letzteres ist je nach Anwendungsfall durchaus sinnvoll. Als Beispiel sei ein Cache genannt (wenn mehrere Caches für dieselbe Sache existieren ist der Cache schnell nutzlos). Das (Anti-)Pattern hingegen ist unnötig.

            Laravel

            Damit habe ich keine Erfahrungen. Die Schnippsel, die ich gesehen habe, fande ich eher meh (statische Funktionen, viel Magie). Ich bin (war, habe seit knapp 9 Jahren kein PHP-Projekt mehr gemacht) großer Freund von Symfony und empfehle daher das. Und da ist auch vglw. wenig Magie drinnen.

            Deswegen arbeite ich in solchen Situationen auch gerne mit einem Context-Objekt, wo alle "globalen" Dinge drin stecken und der an alle Objekte durchgereicht wird, die im Verlauf der Requestverarbeitung entstehen.

            So ein Kontext-Objekt macht die Abhängigkeit zwar klar, aber macht die Testbarkeit nicht wirklich einfacher als wenn man viele globale Variablen hat. Ohne den Code zu kennen weiß man immer noch nicht, welche Teile des Kontext-Objekts verwendet werden, und deswegen muss man im Zweifel alle irgendwie belegen. Das macht den Test unnötig schwierig.

            Damit das funktioniert und testbar bleibt, muss man dann natürlich den DI Container überallhin durchschleifen und darf ihn nicht als globalen State missbrauchen.

            Diesen Satz würde ich gerne hinterfragen, da er so klingt als ob hier DI falsch verwendet wird (aber vielleicht lese ich auch etwas anderes hinein als gemeint ist). In einer ordentlichen DI-Welt sollte man nie einen DI-Container ("Service Locator") irgendwo injecten oder damit arbeiten, da der Grundsatz "don't look for things" gilt. Wenn eine "Bean" (ein instanziiertes Objekt mit seinen Abhängigkeiten) irgendwo gebraucht wird dann injected man dieses. Aber nie den DI-Container/Service-Locator irgendwo injecten und dann die Bean abfragen.

            Sprich: dein Business-Code sollte sowohl frei von DI-Container/Service-Locator sein als auch (mit Ausnahme von Factories und von simplen Value-Objekten) von Objekt-Instanziierungen (new bzw. ein Konstruktor ist im Grunde nichts anderes als eine statische Methode und verhindert die Austauschbarkeit der Klasse.

            VG Matti

            1. Hallo Matti,

              die reine Lehre ist mir schon klar, aber ich kämpfe oft genug damit, sie auch durchzuhalten.

              Es kommt ja durchaus vor, dass irgendwelche Komponenten weitere Subkomponenten erzeugen müssen, und wenn ich alles von "ganz oben" hinein injizieren will, habe ich am Ende eine unüberschaubare Parameterleiste.

              In einer einfache Funktionsklasse, die wenig Abhängigkeiten hat, lässt sich das leichter erledigen. Aber in einer Web-Anwendung habe ich typischerweise den Zugriff auf die Session, auf die Request-Daten und auf die Datenbank. In einer Controller-Klasse, die ich testen können möchte, will ich aber nicht $_GET, $_POST oder $_SESSION stehen haben. Natürlich, für Tests kann ich die Fake-bestücken, aber das ist PHP typisch. In C#, wo ich öfter unterwegs bin, gibt's dafür Objekte und die kann ich nicht mal eben so wegmocken. Also muss ich sie kapseln, und entweder injiziere ich jede einzelne Kapsel oder ich erstelle ein Kontextobjekt, wo alles beieinander ist. Damit habe ich einen globalen Scope, ja, aber er ist für Tests isolierbar und austauschbar.

              In einer Web-Anwendung habe ich auch oft genug nur eine index.php, die nichts weiter tut als das Objektframework für die Requestverarbeitung zu errichten, und den Request dann dem Router zu übergeben. Der ermittelt einen Controller und delegiert den Job dorthin. Der Controller braucht dann Zugriff auf die DB-Repositories und muss passende Views erzeugen - die kann und will ich nicht aus dem Router injizieren. Ich brauche also eine View-Factory, DIE kann ich injizieren und für Tests einen Ersatz [1] unterschieben. Letztlich sind das alles Dinge, die in den Runtime-Kontext passen. Controller in Web-Anwendungen haben ein nichttriviales Umfeld, also ist auch der Test nichttrivial. Oder ich bin zu blöd dafür. Würde ich nie ausschließen…

              Rolf

              --
              sumpsi - posui - obstruxi

              1. Mein Lieblingstool für Unittests in C# heißt NSubstitute, und zwar wegen dieses Handbuchsatzes: We could ask for a stub, mock, fake, spy, test double etc., but why bother when we just want to substitute an instance we have some control over? ↩︎

              1. Hallo,

                Es kommt ja durchaus vor, dass irgendwelche Komponenten weitere Subkomponenten erzeugen müssen, und wenn ich alles von "ganz oben" hinein injizieren will, habe ich am Ende eine unüberschaubare Parameterleiste.

                Eine Komponente sollte weder wissen, wo sie verwendet wird, noch welche Abhängigkeiten ihre eigenen Abhängigkeiten haben. Das macht die Parameterliste sehr klein.

                Die Komplexität des Abhängigkeitsbaums besteht ja nur beim Aufbau, was aber meistens anderswo als in der Klasse selber geschieht. Wenn man natürlich keine Hilfe verwendet (irgendeine Library, die die Instanziierungslogik generiert aus Annotationen/Config/...) ist das unter Umständen sehr langwierig, da stimme ich zu. Aber nichtsdestotrotz sollte eine einzelne Abhängigkeitsliste einer Klasse kurz sein - und wenn sie zu lang werden sollte sollte man sich auch fragen, ob man diese Klasse teilen kann.

                Der Controller braucht dann Zugriff auf die DB-Repositories und muss passende Views erzeugen - die kann und will ich nicht aus dem Router injizieren. Ich brauche also eine View-Factory, DIE kann ich injizieren und für Tests einen Ersatz [1] unterschieben.

                Ich fange mal mit dem zwischengeschobenen "die kann und will ich nicht aus dem Router injizieren" an: das ist vollkommen korrekt. Der Router sollte gar nichts darüber wissen, was der Controller denn so zum Arbeiten braucht. Vielleicht liefert der Controller ja etwas statisches zurück und braucht weder DB noch eine View? Oder er liefert JSON/XML/... und braucht keine View? Daher ist dieses Zitat von dir vollkommen korrekt.

                Bei dem folgenden Satz bin ich mir unsicher. Worein injectest du eine View-Factory? In den Router oder in den Controller?

                In meiner Welt: der Router darf nichts darüber wissen, was der Controller denn so tut. Daher sollte die View-Factory weder im Router injected werden noch das Controller-Objekt dort instantiiert.

                BTW - $_SESSION, $_GET, $_POST ist tatsächlich ein gutes Beispiel für ein singleton Objekt ($request), welches man herumreichen kann. Ich referenziere hiermal eine Symfony Kernkomponente: Symfony: HTTP-Foundation. Muss man in PHP also auch nicht neu erfinden.

                Deine index.php (auch Front-Controller genannt) hat dann etwa folgende Aufgaben:

                • DI-Container erstellen (inkl. des Request/Context-Objekts)
                • Router entnehmen und starten.

                Der Router hat Referenzen zu allen Controllern, die er benötigt - bereits fertig instantiiert, er muss also nicht das Controller-Objekt zusammenbauen. uswusf

                Ich glaube nicht, dass wir sonderlich weit voneinander entfernt sind in unserer Ansicht. Ich vermute allerdings, dass du in C# "sauberer" entwickelst als in PHP, und ich will nur das Bewusstsein dafür schaffen, dass auch in PHP sauberer Code funktioniert (und empfehle dazu Symfony als Ausgangsbasis - viele Komponenten wie HTTP-Foundation oder auch die DependencyInjection-Komponente kann man auch standalone ohne das komplette Framework nutzen).

                Viele Grüße Matti

                1. Hallo Matti,

                  Ich glaube nicht, dass wir sonderlich weit voneinander entfernt sind in unserer Ansicht.

                  Nö, sind wir auch nicht. Wir fachsimpeln halt rum 😉

                  Der Router hat Referenzen zu allen Controllern, die er benötigt - bereits fertig instantiiert, er muss also nicht das Controller-Objekt zusammenbauen. uswusf

                  Das finde ich jetzt fragwürdig - wenn es siebenunddrölfzig Controller gibt, dann instanziiere ich die doch nicht auf Halde. Vor dem Routing weiß ich auch gar nicht, welchen ich brauche. Der Router braucht genau einen. Welchen, findet er an Hand der Route heraus. Und dann muss der Router den DI Container kennen, um ihn nach diesem Controller zu fragen - worauf der DI Container den Controller instanziiert und bereitstellt.

                  Es sei denn, ich trenne Routing und Execution auf, d.h. der Router ermittelt nur den Namen des benötigten Controllers und der benötigten Action-Methode und überlässt die Controllerbeschaffung und die Ausführung der Methode wieder der index.php. Das sind Dinge, die mir .net transparent abnimmt, deswegen hab ich das in PHP bisher vielleicht falsch gemacht.

                  Rolf

                  --
                  sumpsi - posui - obstruxi
                  1. Hallo,

                    da kann man sicherlich nachschärfen, wenn es Laufzeitprobleme gibt, z.B. den entsprechenden Ast am Dependency-Tree lazy instantiieren und nur einen Proxy injecten oder sowas.

                    Andererseits soll Objektinstantiierung ja auch billig sein (d.h. im Konstruktur nur Zuweisungen, keine eigentliche Tätigkeit), und da wird das vermutlich gar nicht so sehr auffallen.

                    VG Matti