Symfony2: Verlass den Weg und brenne in der Hölle

| 8 Kommentare

Wie der Titel vielleicht erahnen lässt, schreibe ich diesen Artikel nicht ganz frustfrei. Hinter mir liegen drei Tage gegoogle und viele schlaue Kommentare selbsternannter Experten in diversen Foren. Dennoch ist die aktuelle Lösung gerade mal ein recht aufwändiger Workaround.

Was hatte ich nur vor? Nun, ursprünglich hatte ich zwei Objekttypen, nennen wir sie einfach Category und Product, die in einer ManyToMany-Beziehung stehen. Wunderbar! Ein paar Annotations an die Entity-Klassen, Methoden und CRUD-Controller generieren und alles läuft binnen weniger Minuten rund.

Nun kam aber eine weitere Anforderung hinzu: Die Position der Produkte soll innerhalb der Kategorie konfigurierbar sein. Eigentlich eine einfache Aufgabe und in Minuten erledigt, dachte ich…

Bekanntlich gehören zu einer ManyToMany-Beziehung drei Tabellen in der Datenbank. Zwei gehören zu den Objekten (Category und Product) und die dritte speichert die Beziehung der beiden. Letztere verwaltet Symfony/Doctrine als JoinTable selbständig.

categories
==========
id (PK)
name

products
========
id (PK)
name

category_products
=================
category_id (PK)
product_id (PK)

Die Position gehört zu der Beziehung zwischen den beiden Objekten und so auch in die JoinTable. Es wird also ein weiteres Feld in der Tabelle benötigt. Im einfachsten Fall würde man dieses der Tabelle hinzufügen, ein JOIN über alle drei Tabellen machen und nach der Position in der JoinTable sortieren.

In dem Fall, so scheinen sich alle einig zu sein, ist es keine ManyToMany-Beziehung mehr und wird zu zwei ManyToOne/OneToMany-Beziehungen. Statt der automatischen JoinTable kommt nun eine Entity (CategoryProduct) in die Mitte mit Beziehungen zu den beiden anderen Objekten und einem Feld “position”. Ansich nicht schlecht, dann hat man gleich etwas “greifbares”, wenn die Positionen verändert werden sollen.

Das war dann aber auch der einzige positive Effekt, denn die ganze Magie die einem bislang alles abgenommen hat scheint nun verschwunden zu sein.

Die Auswahlboxen in den Formularen, um z.B. ein Produkt einer Gruppe zuzuweisen funktionieren nun nicht mehr, da die Objekte sich nicht mehr direkt kennen und nur noch mit der CategoryProduct-Entity verbunden sind. Das hilft aber leider nicht, da dann nur noch bestehende Beziehungen zu Auswahl stehen.

Etwas Hoffnung versprach der Feldtyp “entity” in der Formularkonfiguration. Damit und mit etwas getrickse mit den Getter- und Settermethoden lässt sich das gewünschte Verhalten fast wiederherstellen, denn diese müssen nun ggf. ein CategoryProduct für die Beziehung erzeugen.

Wenn wir nun ein Feld “products” im Kategorie Formular verwenden, muss es dazu auch Getter- und Setter in der Entity-Klasse geben. Die Kategory-Entity hat eine Collection von CategoryProduct-Objekten, die dann entsprechend gelesen und beschrieben werden. Leider werden aus der Collection gelöschte Objekte nicht automatisch in der Datenbank gelöscht und das unabhängig von der “cascade” Konfiguration.

So kommt nun noch ein Doctrine-EventListener dazu, um das ganze bei jedem Update wieder aufzuräumen. Leider reagiert der aber nur, wenn sich aus der Sicht von Doctrine etwas verändert hat. Ändert man den Namen der Kategorie oder fügt ein Produkt hinzu, dann funktioniert es. Löscht man hingegen nur ein Produkt, dann passiert nichts. Auch hier verschiedene Optionen ohne Erfolg probiert und der Listener muss aus dem Controller manuell getriggert werden. Nicht wirklich schön, da es Müll hinterlässt, wenn die Entity an anderer Stelle gespeichert und der Listener dabei vergessen wird.

Plan B gab es natürlich auch noch: Die beiden Objekte behalten ihre ManyToMany-Beziehung damit die Formulare gewohnt bequem funktionieren, die JoinTable wird trotzdem eine eigene Entity und in der Konfiguration für JoinTable und EntityTable wird einfach der selbe Name verwendet. Dann nur noch die jeweiligen Resourcen-Klassen pimpen und schon geht alles seinen Gang.

Leider nicht. In dem Fall kommt sich die Magie von Doctrine selber in den Weg und behauptet beim Schemaupdate, die Tabelle sei bereits vorhanden.

Fazit: Möglich dass ich etwas Entscheidenes übersehen habe und es einen ganz einfachen Weg gibt, um diese Aufgabe zu lösen. Bei meiner Suche habe ich sehr viele ähnliche Fragen und halbherzige Lösungsvorschläge dazu gefunden, aber keine funktionierende Lösung. Die Anforderung zusätzliche Daten zu einer Beziehung zu verwalten kommt doch häufiger vor. Daher ist es sehr schade, dass es doch scheinbar so kompliziert ist damit ans Ziel zu kommen.

Für mein Projekt werde ich erstmal mit dem Workaround leben und nicht noch mehr Zeit damit vertrödeln. Wie immer freue ich mich über Kommentare von euch, z.B. falls ihr schon in einer ähnlichen Situation wart und eine gute Lösung gefunden habt. ;-)

 

 

 

 

 

8 Kommentare

  1. Will niemand hören und wahrscheinlich holen alle gleich den Knüppel aus dem Sack…

    Ich hatte vor einiger Zeit in einer Anwendung die gleiche Anforderung. Mit Rails war es innerhalb von kurzer Zeit gelöst – da funktioniert nämlich Deine ursprüngliche Intention wie erwartet.

    Viele Grüsse,

    Holgersen

  2. Es ist ja ansich auch eine einfache Sache. Wie gesagt, vielleicht habe ich es auch einfach nicht richtig gemacht.

  3. Deine erste Idee funktioniert mit Symfony 2.1.

    Als erstes musst du in der @ManyToOne Beziehung das Attribute “orphanRemoval” auf true setzten. Dann in deinem CategoryType den “product” Feld die Option “by_reference” auf false setzen. Dann noch deiner Produkt Klasse addCategory und removeCategory hinzufügen und in der Category Klasse die Methoden addProduct und removeProduct und nun funktioniert alles wie erwartet.

  4. Das ist ein echt interessantes Grundproblem, das eigentlich jeden Entwickler betrifft! Irgendwie hat ja jeder Bammel davor ein Framework zu benutzen, weil man vor genau dieser Situation Angst hat. Irgendwann kommt ja meist der Punkt, wo man merkt das das Framework für Fall X nicht ausgelegt ist. Und dann wirds echt böse, weil das Framework, das ja eigentlich die Arbeit erleichtern soll, die Arbeit extrem erschwert. Das hab ich krasserweise schon sehr oft erlebt! Und dann fummelt man im Core rum und wenn’s dann mit viel Gefitzel geht, dann kommt das nächste Framework-Update und alles ist überschrieben oder artet im Mega-Chaos aus. Das erste Zend-Framework hat mich fast meinen damaligen Job gekostet, weil es tagelang brauchte um simpelste CSS/HTML-Änderungen vernünftig einzubauen, und mein Arbeitgeber dachte ich verarsche ihn ;) !

    Um mal den Kreis zu schließen: Die wenigsten Problem hab ich bisher in Verbindung mit rudimentären Microframeworks gehört … Kann aber auch nur meine Wahrnehmung sein.

  5. @Markus: Danke für deinen Tipp. Löst das Problem zwar nicht komplett, aber der Listener kann damit wieder weg. :-)

    @Chris: Ganz so hart ist es zum Glück nicht. Ich musste dafür nicht am Core schrauben, sondern “nur etwas” tricksen. Symfony2 ist schon ein sehr gutes Framework, man muss nur wissen wie es geht. Die BWLer haben leider dafür oft kein Verständnis, wenn es doch mal etwas länger dauert.

  6. Moin,

    das was du suchst, ist ein FormType mit Colletions. Das dürfte dein Problem lösen ;-)
    Ist zwar wie ich finde nicht Optimal aber es Funktioniert gut (auch über die GUI)

    Hier ist mal ein Link zur Doku: http://symfony.com/doc/2.2/cookbook/form/form_collections.html

  7. So habe ich das letztendlich auch gemacht. Ist nicht ganz so bequem, aber es tut was es soll. :-)

  8. zum thema, dass doctrine der meinung ist, es gäbe schon die eine oder andere tabelle, habe ich mich mal knie tief in den doctrine 2 slug vertieft… das könnte für dich recht interessant sein:

    http://hoessi2web.blogspot.de/2012/07/bei-symfony-21-fehlen-doctrine-settings.html

    greez,
    hoessi

Hinterlasse eine Antwort

Pflichtfelder sind mit * markiert.


Schlagwörter: A/B-Test, AbstractType, Adapter, AddOn, Administration, Ajax, Alühn, Alühn2, Amazon, Animation, Annotations, Anonyme Klasse, Ant, Apache, API, Array, ArrayAccess, Attachment, Auftrag, Ausbildung, Auswertung, Authentifizierung, AutoLoader, AWS, Backup, Bedienung, Bedingung, Benchmark, Berechtigung, Berlin, Bildbearbeitung, Bildschirmfoto, Blog, Blogroll, BOM, Bootstrap, Bot, Browser, Bugtracker, Byte Order Mark, Bücher, Cache, CakePHP, Call-Center, Callback, CamelCase, Canvas, Captcha, CDN, Cheatsheet, CLI, Clickout, Closure, Cloud, CodeSniffer, Collection, Community, Comparator, Config, Contest, Controller, Converter, CouchDB, Countable, Cronjob, CRUD, CSS, CSV, CustomLibrary, Custom_Model, Daemon, Data Mapper, Datei, Datenbank, Datensicherung, Datenstruktur, Datentypen, Dating, Datum, Debug, Decorator, Dekorierer, Design, Design Patterns, Doctrine, Dokumentation, Dump, Duplikat, each, EC2, Eclipse, Email, Entwicklung, Entwurfsmuster, Enum, Erweiterung, Event, Eventhandling, Exception-Handling, Extension, Facebook, Factory, Fallback, Fehler, Fehlermeldung, Filter, Firefox, Flash, flexigrid, Foreach, Formatierung, Formular, Framework, FTP, Funktion, Futon, ga:pi(), Getter, Gnome, Google Analytics, Hash, Hash-Bang, Header, htaccess, HTML5, htpasswd, HTTP, HTTPS, IDE, If, Implementierung, InnoDB, Interceptor, Interface, Internet Explorer, isset, Iterator, Java, JavaScript, Job, jQuery, Kommentar, Konfiguration, Konsole, Kontrollstruktur, kopieren, kostenlos, Kundenbetreuung, Late Static Binding, Layout, Links, Linux, Listeners, Lizenz, Logging, Löschen, Magento, Magic Methods, Manual, ManyToMany, Marketing, Methode, Model, Monolog, MVC, MySQL, NetBeans, Network, Nirvanix, Objekt, Observable, Observer, OneToMany, Online Tool, OOP, Open Source, Operator, OR-Mapper, Order, ORM, O’Reilly, Parameter, Partnersuche, Passwort, Performance, PHP, php.ini, PHP hates me, phpMyAdmin, PHPUnit, Plugin, Point and Click, Popup, Praktikum, Proxy, Prüfsumme, Prüfung, QR-Code, Qualitätssicherung, Query, Queue, Redesign, Refactoring, Reflection, Repository, Request, Response, Responsive Design, Rest-API, Rockstar, Rollback, Routing, S3, Samba, Scheifen, Schleife, Schutz, Screenshot, Secure Shell, Selbstreferenz, Server, Setter, setTimeout, Shop, Sicherheit, Sicherung, Sichtbarkeit, Singleton Pattern, Skin, SOAP, Social Network, Software, Sortierung, Sourcecode, Spam, Speicherproblem, Spickzettel, SPL, Splittest, SSH, SSL, Stammtisch, Statement, static, Statistik, Status, Stellvertreter, Strategy Pattern, Stream, String, Stuttgart, Stylesheet, Subversion, Sun VirtualBox, Support, SVN, Switch, Symfony, Symfony2, Symfony Live, Tag, Template, Template Method, Ternär Operator, Testing, Theme, Thumbnail, Tool, Tour, Tracking, Twig, Twitter, Type-Cast, Ubuntu, Umwandlung, Underscore, unset, Update, Upload, Url, User Story, Validierung, Vererbung, Versionskontrolle, Versionsnummer, Verzweigung, Video, Videospiel, Virtualisierung, Visitor Pattern, Vorschaubild, walk, Warteschlange, Webserver, Webservice, Weiterleitung, Werkzeug, Windows, WindowsAzure, WordPress, Wrapper, Writer, XML, Youtube, Zeitschleife, Zeitsteuerung, Zend Framework, Zend_Application, Zend_Cloud, Zend_CodeGenerator, Zend_Http_Client, Zend_Reflection, Zend_Service, ZPress, Zugangskontrolle, Zugriffsmethode