Natürlich wird Symfony 2 super. Die Dokumentation ist wie gewohnt zum jetzigen, frühen Zeitpunkt genial, die Architektur durchdacht, die Entwickler-Community steckt sowieso alles in die Tasche, man sieht einfach: Da steckt eine Menge Arbeit, Hirnschmalz und Erfahrung hinter. Aber genug geschleimt ;)
Mein Lieblingsthema ist ja zur Zeit der Dependency Injection Container. Und irgendwie stinkt mir die ganze Container-Konfiguration noch gewaltig. Meine kläglichen Versuche, selbst mal so was ähnliches wie eine brauchbare Autowiring-Implementierung herunterzubrechen, waren natürlich auch bzw. erst recht nicht der große Wurf – was vor allem daran lag, dass ich den Service Container und damit den ganzen Sinn und Zweck des Ganzen einfach mal wegrationalisiert hatte – Loose Coupling sieht natürlich anders aus, das sei den Kritikern zugestanden. Ich verteidige mich mal dadurch, dass ich eigentlich nur mal mit Mixins rumspielen wollte – da hab’ ich wohl die eine oder andere Begrifflichkeit durcheinander geworfen.
Aber um mal zu des Pudels Kern zu kommen: Ist es wirklich so geil, mit kilometerlangen XML-Dateien, ‘ner Menge Initialisierungscode und ohne mit der heißgeliebten Code-Completion in der IDE meiner Wahl ein Paradigma zu kaufen, das im speziellen Anwendungskontext – nicht im Framework-Kontext – eher selten Anwendung findet?
Es ist doch so: PHP bietet zwar mittlerweile eine vernünftige Reflection API, aber wenig (keine) Möglichkeiten, Code zur Compile- oder Runtime effizient zu injizieren (vorbei bspw. an der Sichtbarkeit, was für Property Injection aus meiner Sicht erforderlich wäre). Mit Handständen geht das mittels der Reflection API, noch fieser mit bspw. Runkit.
Somit ist – (natürlich nur aus meiner Sicht, was im Übrigen für den gesamten “Complaint” gilt – dabei wäre ich wirklich froh, berichtigt zu werden) jeder Versuch, die Konfiguration eines wie auch immer gearteten, auf PHP sitzenden, DIC mit Syntactic Sugar anzureichern, vergeblich. Und Syntactic Sugar ist dringend vonnöten, um das Zeug in der Praxis RAD-tauglich zu machen. Nehme ich PHP den RAD-Faktor, was bleibt denn dann noch übrig?
Symfony 2 behilft sich damit, einen DI-Container zu definieren und ihn als PHP-Code wegzucachen. Dieser hat dann Accessoren für alle definierten Services, Constructor-Injection ist durch Referenzierung in den Konfigurationsdateien möglich. Die Doku spricht zusätzlich von Setter- und Property-Injection, dazu konnte ich aber irgendwie keine dokumentierten Beispiele finden. Es gibt aber immerhin die Möglichkeit, Methoden in der Service-Klasse als Pre- oder Post-Initialization Interceptoren auszuführen.
Letztlich tut der Container, was er soll: Abhängigkeiten in beliebige Instanzen zu injizieren. Bei Bedarf auch gegen ein Interface, aber das ist letztlich aufgrund des Sprachentwurfs nur optional (Was impliziert, das man auf Kosten riesiger Flexibilität – die man nicht braucht – schnell mal riskiert, durch einen falsch konfigurierten DI-Container seine Anwendung kaputt zu machen).
Aber der Aufwand steht meiner Meinung nach noch in keinem Verhältnis zum Nutzen. Am Beispiel einer Controller Extension, die als Service definiert wird, möchte ich das mal demonstrieren. Die offizielle Symfony 2-Dokumentation sagt, dass
To keep things simple, Symfony2 by defaults does not require that controllers be defined as services. Furthermore Symfony2 injects the entire service container into your controller.
Das ist immerhin schon einmal eine Information. Das Standardverhalten ist also, dass ich automagisch eine Controllerklasse habe, die den Container kennt (Wie? Magisch? Ich dachte, Symfony 2 hat die Magie zugunsten transparentem Code und ständiger Kontrolle über jeden Aspekt des Core-Frameworks aufgegeben?). Dadurch, dass er ein ContainerAware Interface implementiert, kennt er auch einen Shortcut auf
$this->container->get('serviceid')
, nämlich
$this->get('serviceid');
. Um Bspw. ein Doctrine-Entity zu persistieren, reichen “ein paar codezeilen”:
$em = $this->get('doctrine.orm.entity_manager'); $em->???($myEntity);
Wie hieß die Methode da am Entity Manager jetzt nochmal? Das ist das erste Problem: Dadurch, dass alle Services anonym im Service Container herumdümpeln bzw. auf ihre Instanziierung warten, habe ich in meiner IDE keinerlei Code-Completion mehr. Um das wieder zu erreichen, muss ich entweder
– einen Accessor schreiben (getEntityManager()) und diesen mittels PHP-Doc type-hinten (annotieren) oder
– mindestens einen magischen Docblock davorsetzen, der genau *meiner* IDE die Möglichkeit gibt, den Variablentyp zu erraten (aber ggf. proprietär ist, d.h. in anderen IDEs nicht unbedingt funktionieren muss). Für die Netbeans IDE gibt es dafür bspw. das “vdoc“-Feature.
Das ganze setzt natürlich voraus, dass ich *weiß*, welches Interface der Service doctrine.orm.entity_manager implementiert bzw wie die konkrete Klasse (samt Namespace) desselben heißt. Und vorher muss ich auch noch wissen, wie überhaupt der *Servicename* des Entity Managers lautet.
Bin ich also komplett Ahnungslos, rufe ich erstmal ein Terminal auf, tippe sowas wie
$ app/console container:debug --show-private
ein und suche mir anschließend in der kilometerlangen Liste genau die Services zusammen, die ich brauche.
Dafür brauche ich als Framework-Newbie schonmal richtig tiefgehende Kenntnisse darüber, was ein DIC überhaupt ist, tut und zur Konfiguration erfordert.
Um also das, was “früher” ein
new Cart($color, $maxSpeed)->save()
war, zu erreichen, muss ich nun erstmal ganz schön viel Sport treiben (dieses Beispiel nur exemplarisch, es steht außer Frage, dass die Doctrine 2-Architektur wirklich klasse ist. Das Manko aus meiner Sicht ist die Integration in Sf2, nicht der ORM-Layer an sich.)
Möchte ich den Accessor der ContainerAware-Implementierung am Controller vermeiden und die Services “direkt” injizieren, muss ich ihn selbst als Service am DIC registrieren. Wie das funktioniert, würde den Rahmen sprengen, nur so viel: Der Aufwand steht aus meiner Sicht wieder in keinem Verhältnis zum Ergebnis. Allerdings habe ich damit den Vorteil, dass ich bspw. keine abstrakte Controllerklasse mehr zu erweitern habe – der Controller kann ein simples Popo sein. Dass es aber nicht zu simpel sein darf, zeigt bspw., das ich trotz alledem mindestens einen Constructor definieren muss, der eben alle benötigten Services aufnimmt und an Instanzvariablen bindet. Und das können einige sein – fängt an beim Templating, geht übers Formframework, ggf. den Entity-Manager, einen Mailer und was einem sonst noch so einfällt. Dabei wollten wir doch ab jetzt nur noch leichtgewichtige Controller haben?
Meiner Meinung nach liegt die Lösung der Malaise in zwei Alternativen: Man baut so lange Abstracts, die den ganzen Kram als Shortcuts bereitstellen (dann hätten wir bspw. wieder eine komplexe, abstrakte Kontrollerklasse, die mindestens alles kennt, was in Symfony 1 noch dem ApplicationContext gehörte) – womit wir unser Loose Coupling Pardigma wieder in den Schrank zu den übrigen Ideen stellen können, die im Nachgang irgendwie doch nicht funktionierten. Oder legt Autowiring drüber.
Flow 3 baut bspw. an einem DIC + Autowiring, der alle verwalteten Services durch eine Kombination der Analyse von Annotationen (also PHP-Doccomments) und Signaturen (Typehints) direkt an die erzeugten Instanzen bindet. Die Konfiguration scheint damit sehr viel leichtgewichtiger.
Grails (was so weit ich weiß auf der Spring IOC-Komponente sitzt) geht noch einen Schritt weiter und mappt Instanzvariablen durch reine Naming-Conventions: Wenn also eine Instanzvariable auf “Service” endet und es zur Eigenschaft “mailerService” einen passenden Service “mailer” gibt, wird dieser automagisch injiziert. Kein zusätzlicher Code noch Annotations vonnöten.
Ich wüsste aber nicht, wie man solche Features wirklich performanceneutral, effizient und stabil auf einer Plattform wie PHP aufsetzen könnte – dafür ist die Sprache einfach (noch) nicht weit genug.
Abschließend bleibt ein übler Nachgeschmack – und der Eindruck, dass man sich hier fatal an den XML-“Konfigurationswahnsinn” der entsprechenden Java-“Vorbilder” annähern möchte. Wo doch gerade in diesem Lager intensiv daran gearbeitet wird, genau davon abzurücken und Konfigurationen eben durch Konventionen zu ersetzen.
Was denkt ihr? Kann das ganze doch durch Routine punkten, sodass es durch ein wenig konkrete Projekterfahrung doch ganz leicht von der Hand geht? Wird der DIC im Tagesgeschäft schon intensiv benutzt, oder werden nur die Standardfunktionen des Frameworks verwendet und das Zeug “unter der Haube” doch lieber den Framework- und Bundle-Entwicklern überlassen? Ich bin wieder sehr gespannt auf Meinungen und Kritik…
Also erstmal wieder spreche ich der Meinung, dass das Anpassen von Dependencies nur theoretischen Nutzen hat. Mal abgesehen von Unittesting habe ich das in der Praxis schon reichlich genutzt.
Ansonsten drehst du dich ja da ein bisschen im Kreis. Zum einen willst Du lieber Magic, wenn du dann ganz einfach solche Magic nachruesten kannst (aka deine persoenliche Basis Controller Klasse) dann wird das wieder als Schwaeche gewertet.
Das ganze Konzept von Symfony2 ist das man alles sauber machen kann. Das ist die Grundvorraussetzung, dass man sich nicht in eine Sackgasse begibt. Es gibt immer einen sauberen Weg.
Jedoch das heisst ja nicht das man nicht mehr Magic rein bauen kann oder darf. Siehe z.B. FrameworkExtraBundle. Nur das gute ist wenn man damit an die Grenzen stoesst, dann kann man das halt auch easy wieder weglassen und durch andere oder gar keine Magic ersetzen.
Und ja .. derzeit verbringen wir viel Zeit damit die Out of the Box Experience so einfach wie sinnvoll zu machen. Am besten schaltest Du Dich aktiv in die Diskussionen ein:
https://github.com/symfony/symfony/issues?labels=Service+Container
Huhu Lukas, danke für Deine direkte Anwort (und sorry, dass ich erst jetz dazu komme, das zu kommentieren). Ja, recht haste – noch dazu ist es bestimmt auch ganz schön frustrierend, so viel Arbeit und Hirnschmalz in eine saubere, erweiterbare Kernarchitektur zu investieren, die auch noch performant ist – und die Leute mäkeln nur herum und beklagen sich über steigende Komplexität und Konfigurationsaufwand.
Ich spreche jetzt mal nur aus der Perspektive eines Anwenders, nicht eines Framework-Coreentwicklers (ist die einzige, die ich habe und halbwegs kompetent vertreten kann):
Ich sehe die “Magie” tatsächlich allenfalls als “Add-In”, keinesfalls als Ersatz für den bestehenden Mechanismus des Dependency-Managements.
Dennoch muss die Magie aus meiner Sicht einfach und generalisiert genug sein.
Das FrameworkExtraBundle erfüllt mit einer Menge Code “unter der Haube” einige wenige, spezielle Tasks – und das durch Annotationen, die zwar eventuell aus einer langen Zeile Code eine kurze machen – das “eigentliche” Problem, das mir unter den Nägeln brennt, nämlich ein vereinfachtes Projektmanagement in der IDE durch funktionierende Code-Completion bereitzustellen – nicht löst.
Wer vornehmlich in Textmate entwickelt, ist von diesem Problem nicht berührt – zugegeben.
Autowiring ermöglicht es, wie beschrieben durch Konventionen (Variablennamen) oder “Compilerhints” (Annotationen) durch den DIC verwaltete Objektinstanzen konkret und transparent mir Referenzen “zu befüllen” – mit dem Nachteil, dass man sich dann eben an den DIC koppelt.
Das Feature kann demnach auch nur ein domänenspezifisches sein. Für Controllerklassen macht es aus meiner Sicht wirklich Sinn, denn die Dinger sind nichts anderes als unliebsame “Stubs” ohne wirkliche Businesslogik, die einem bestimmten, immer gleichbleibenden Zweck dienen.
Ein Controller hat IMMER eine Abhängigkeit zum Routing (wenn ich nicht gerade was View-zentrisches benutze wie JSF oder so), zur View ebenso – warum also nicht auch generell zum DIC?
Ich weiß nur, dass sich das Konzept des Autowiring (das auch hinter dem von Dir genannten Link vorgeschlagen wird) in anderen Tools definitiv bewährt hat, doch wie gesagt – nur im eng abgesteckten Rahmen. Es macht definitiv keinen Sinn, einen komplexen Objektgraphen voller Business-Logic ausschließlich durch einen DIC + Autowiring zu verwalten.
Leider hat das ganze auch einen zweiten, großen Nachteil: Da PHP dynamisch typisiert ist und auch keine Type-Hints an Instanzvariablen erlaubt, hat ein wie auch immer gearteter Autowiring-Mechanismus arge Probleme, die benötigte Dependency zu identifizieren. Man käme also eh nicht umhin, eine autowired Property gleich zweimal zu annotieren: Einmal eine Annotation für den DIC (mit dem eindeutigen Identifizierer der Dependency), und einmal mit @var für die IDE. Darüber hinaus: Keine Interface-Injection durch Property-Wiring, das wär’ dann höchstens durch annotierte Setter möglich.
Trotzdem: Ich möchte gerne Code-Completion “out of the box”, das ist mit eines der wichtigsten Features, um wirklich effizient zu arbeiten – aus meiner Sicht.
Abschließend noch eine persönliche Frage, wenn Du erlaubst: Ist Deine Muttersprache Deutsch oder Englisch? Dein Name klingt so “Denglisch” ;)
Danke nochmal für Deinen Beitrag, ich schau mir das auch nochmal genau an und versuche vor allem, die Cons zu kapieren – ist gar nicht so einfach, das wird schon sehr schnell ganz schön akademisch …
Meine Mutter kommt aus Ostdeutschland, mein Vater aus dem Iran und mein Adoptivvater kommt aus USA.
Gebohren hiess ich Kahwe Haddad, in der Grundschule wurde ich dann offiziell adoptiert, habe dabei den Nachnamen meines Vaters angenommen und habe die Gelegenheit genutzt um meinen Vornamen nach Lukas dem Lokomotivfuehrer umzubennen :)
Meine Muttersprache ist Deutsch, aber mein Englisch ist auch recht fluessig :)
Jedenfalls bei weitem flüssiger als meins :D Danke nochmal für den Input, ich habe mich jetzt trotzdem mal getraut in die Diskussion hinein zu springen … Das ist hochinteressant!
Schönes Wochenende bis dahin :)
Endlich mal ein vernünftiger Symfony 2 artikel, wenn auch schon älter, der sich nicht mit bauchpinseln für den Potricier beschäftigt sondern die sache mal Praktisch beleuchtet.