Dienstag, 31. Juli 2012

Bei Symfony 2.1 fehlen Doctrine-Settings

Ich wollte es kaum glauben, aber als ich nach meinem letzten Datenbankumbau im MySQL-Workbench nun mittels Symfony 2.1 CLI ein Datenbank reverse-Engeneering durchführen wollte flog mir Doctrine um die Ohren.

Hier der eigentlich recht simple Aufbau der Problemstellung, zwei Tabellen die jeweils mit zwei M:N Verknüpfungen verwurstet sind.



So, nun Symfony 2.1 mitteilen dass wir gerne daraus Entity-Klassen erstellt haben möchten:

php app/console doctrine:mapping:convert xml ./src/SkyDiablo/StoreBundle/Resources/config/doctrine/metadata/orm --from-database --force

Alles in allem nix besonderes bisher... nur was passiert jetzt. Doctrine wird nun aufgefordert aus der hinterlegten DB, Entitys zu erstellen, was es mit folgender Meldung und mit vollster überzeugung verweigert:

[Doctrine\ORM\Mapping\MappingException]
Property "radUser" in "RadAttribute" was already declared, but it must be declared only once

Was ist passiert? Bei der Erstellung des "RadAttribute"-Entity wollte Doctrine nun für jede M:N Verknüpfung ein Feld anlegen und da die Verknüpfungen auf "RadUser" zeigen, liegt es nahe auch das Feld so zu bennen. Leider haben wir hier zwei Verknüpfungen auf die gleiche Tabelle, was dazu führen würde, dass wir zwei mal das gleiche Feld hätten. Das erkennt Doctrine und streikt an der stelle. Leider bringt uns das kein Meter weiter, keinerlei Übergabeparameter oder Configs im "Doctrine-Bundle" ermöglichen uns hier abhilfe zu schaffen.
Da ich aber öffters über diesen Weg meine Entitys erstelle und ich somit darauf angeweisen bin ging für mich das große Suchen los. Ich habe etliche Zeilen Source debuggt und mich um den Verstand gegoogelt, nichts brauchbares gefunden. Doch da, nach langem hin und her konnte ich ein Schlupfloch finden... Doch erst nochmal etwas Hintergrundinformationen. Die eigentliche Klasse welche mir den Kopf zerbrochen hat, ist diese hier:

Doctrine\ORM\Mapping\Driver\DatabaseDriver

 In ihr werden über die function "loadMetadataForClass" die Feldnamen definiert. Dort habe ich nun gesucht wie ich meinem Problem abhilfe schaffen kann und konnte recht schnell feststellen dass die Feldnamen über die function "getFieldNameForColumn" generiert werden. Und hier gibt uns Doctrine nun auch endliche die Chance selbst Feldnamen zu vergeben! Überglücklich über meinem Fund war auch schon das nächste Problem da, wie sag ich nun Doctrine dass ich an dieser stelle gerne alternative Feldnamen verwenden möchte. Wie bereits gesagt, von seiten Symfony bzw dem Doctrine-Bundle habe ich keine Möglichkeit gefunden.
Nach weiterem langen Gesuche bin ich auf folgendes gestoßen, die Klasse "DatabaseDriver" wird bei Doctrine als eine sogenannte "Metadata-Driver-Implementierung" genutzt und über die Klasse "Doctrine\ORM\Configuration" verwaltet, also vorgehalten. Im Doctrine-Bundle wird das ganze in der Klasse "Doctrine\Bundle\DoctrineBundle\Command\ImportMappingDoctrineCommand" in der Function "execute" so zusammengesteckt:

$em = $this->getEntityManager($input->getOption('em'));

$databaseDriver = new DatabaseDriver($em->getConnection()->getSchemaManager());
$em->getConfiguration()->setMetadataDriverImpl($databaseDriver);

Und weiter keinerlei Möglichkeit auf den "$databaseDriver" zuzugreifen um eventuell alternative Feldnamen zu hinterlegen. Nun kommt der Clue an der geschichte, die Klasse "Doctrine\ORM\Configuration" wird im Doctrine-Bundle über eine Parameter in der "orm.xml" definiert:

<parameters>
        <parameter key="doctrine.orm.configuration.class">Doctrine\ORM\Configuration</parameter>

Hier  habe ich nun angesetzt und diesen Parameter überschrieben indem ich ein neuen gleichnamigen Parameter angelegt habe und somit diese Definition überschrieben habe und dann meine eigene Configuration-Class implementierung angeben habe. In jener habe ich nun die Function "setMetadataDriverImpl" überschrieben und schon hatte ich endlich zugriff auf die gewünschte Klasse und konnte nun so Doctrine mitteilen, dass ich gerne alternative Feldnamen nutzen möchte. Der einfachhalber habe ich alles hard-codiert hinterlegt, könnte man aber natürlich noch über eigene Konfigurations-möglichkeiten im eigenen Bundle durchaus erweitern. In der Praxis sieht das nun so aus:

namespace SkyDiablo\Doctrine\ORM\ConfigurationExtenderBundle\Controller;

use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;

class Configuration extends \Doctrine\ORM\Configuration {

    public function setMetadataDriverImpl(MappingDriver $driverImpl) {
        parent::setMetadataDriverImpl($driverImpl);

        if ($driverImpl instanceof \Doctrine\ORM\Mapping\Driver\DatabaseDriver) {
            /* @var $driverImpl \Doctrine\ORM\Mapping\Driver\DatabaseDriver */
            $driverImpl->setFieldNameForColumn("rad_user_has_rad_check_attribute", "rad_user_id", "radUserCheck");
            $driverImpl->setFieldNameForColumn("rad_user_has_rad_reply_attribute", "rad_user_id", "radUserReply");
            $driverImpl->setFieldNameForColumn("rad_user_has_rad_check_attribute", "rad_attribute_id", "radAttributeCheck");
            $driverImpl->setFieldNameForColumn("rad_user_has_rad_reply_attribute", "rad_attribute_id", "radAttributeReply");

        }
    }

}

Nun werden die Entitys problemlos erstellt und bekommen jeweils eigenständige Feldnamen! Sollte ich mit meinem Aufwand hier voll daneben liegen und es gibt doch schon eine Lösung mit Boardmitteln, bin ich über jeden Kommentar dankbar!

EDIT: 13.08.2012

Ein alternativer weg, ist über die generelle Namens-Convention von Doctrine zu arbeiten. Das geht zwar etwas hier am Ziel vorbei, lässt aber dennoch das geleiche ergebnis mit sich bringen. Eine entsprechende Doku ist hier zu finden:

http://developmentwithart.com/2012/07/04/solving-the-sql-naming-dilemma-by-using-doctrine2-naming-strategy/

EDIT: 18.06.2013

Habe eben beim Durchstöbern meiner Posts das hier noch gefunden, könnte eventuell auch nochmal abhilfe schaffen. Hatte aber bisher noch keine Zeit mich damit zu beschäftigen:

https://github.com/doctrine/doctrine2/pull/241


Greez,
  hoessi...