phpsymfonydoctrine-orm

Symfony2 entity metadata caching?


Finding a solution for reusing entity annotations between different database platforms I reach this:
I have a solution for rename tableName on entity classes metadata listening loadClassMetada event:

My services.xml

<service id="framework.loadclassmetadata.listener" class="%framework.loadclassmetadata.listener.class%">
        <tag name="doctrine.event_listener" event="loadClassMetadata" method="loadClassMetadata"/>
        <call method="setContainer"><argument type="service" id="service_container" /></call>
    </service>

My Listener class:

<?php

namespace Fluency\Bundle\FrameworkBundle\EventListener;

use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Symfony\Component\DependencyInjection\ContainerAware;

/**
 * Class LoadClassMetadataListener
 *
 * @package Fluency\Bundle\FrameworkBundle\EventListener
 */
class LoadClassMetadataListener extends ContainerAware
{
    /**
     * @param LoadClassMetadataEventArgs $args
     */
    public function loadClassMetadata(LoadClassMetadataEventArgs $args)
    {
        $connection = $this->container->get('database_connection');
        $classMetadata = $args->getClassMetadata();

        if(!$connection->getDatabasePlatform()->supportsSchemas())
        {
            $tableName = $classMetadata->table['name'];
            $classMetadata->table['name'] = str_replace('.', '_', $tableName);

            foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
                if ($mapping['type'] == ClassMetadataInfo::MANY_TO_MANY) {
                    if(isset($classMetadata->associationMappings[$fieldName]['joinTable']['name']))
                    {
                        $mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
                        $classMetadata->associationMappings[$fieldName]['joinTable']['name'] = str_replace('.', '_',
                            $mappedTableName);
                    }
                }
            }
        }
    }
}

And a solution for create schemas on database platforms like postgresql:

<?php

namespace Fluency\Bundle\FrameworkBundle\EventListener;

use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Event\SchemaCreateTableEventArgs;
use Symfony\Component\DependencyInjection\ContainerAware;

/**
 * Class SchemaCreateTableListener
 *
 * @package Fluency\Bundle\FrameworkBundle\EventListener
 */
class SchemaCreateTableListener extends ContainerAware
{
    /**
     * @var array
     */
    private $_createdSchemas = array();

    /**
     * @param SchemaCreateTableEventArgs $args
     */
    public function onSchemaCreateTable(SchemaCreateTableEventArgs $args)
    {
        $connection = $this->container->get('database_connection');

        if ($args->getPlatform()->supportsSchemas())
        {

            $tableName = $args->getTable()->getName();

            $separetedTableName = explode('.', $tableName);

            if (count($separetedTableName) == 2)
            {
                $schemaName = $separetedTableName[0];

                if (!in_array($schemaName, $this->_createdSchemas))
                {
                    try
                    {
                        $connection->exec(sprintf("CREATE SCHEMA %s", $schemaName));
                    }
                    catch (DBALException $e)
                    {

                    }

                    $this->_createdSchemas[] = $schemaName;
                }
            }
        }
    }
}

All works fine but I think that it's not optimal cause on every time I need class metadata the eventlistener is called by the main entity and each related entity. I though a dirty solution with a command changing annotations by the hard way, reading Entities and replacing tablenames using regexp. (file_get_contents, regexp replacement and file_put_contents)... but don't like me.

My question (finally) is if its possible, when symfony2-doctrine2 generates annotations cache and or proxy classes, set the correct tableName? then my entities code remains without change and loadClassMetadata method logic it's called only when cache it's generated.


Solution

  • Finally I have things riding on wheels, thanks to Fluency Dev Team for give me a north. The guys found a solution touching Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector to fix creation of schemas. There is a beautiful TODO from Doctrine Team. Now the cache problem was solved trough harcode hacking on Doctrine\Common\Annotations\FileCacheReader. I prefer keep my hands off from Sensio and Doctrine code and there is my final solution until Doctrine make required fixes, cause i 'can't work on GitHub due to Internet limitations:

    services.xml

    <?xml version="1.0" ?>
    
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <parameters>
            <parameter key="fluency.framework.loadclassmetadata.listener.class">Fluency\Bundle\FrameworkBundle\EventListener\LoadClassMetadataListener</parameter>
            <parameter key="fluency.framework.schemacreatetable.listener.class">Fluency\Bundle\FrameworkBundle\EventListener\SchemaCreateTableListener</parameter>
        </parameters>
    
        <services>
            <service id="fluency.framework.loadclassmetadata.listener" class="%fluency.framework.loadclassmetadata.listener.class%">
                <tag name="doctrine.event_listener" event="loadClassMetadata" method="loadClassMetadata"/>
                <call method="setContainer"><argument type="service" id="service_container" /></call>
            </service>
            <service id="fluency.framework.schemacreatetable.listener" class="%fluency.framework.schemacreatetable.listener.class%">
                <tag name="doctrine.event_listener" event="onSchemaCreateTable" method="onSchemaCreateTable"/>
                <call method="setContainer"><argument type="service" id="service_container" /></call>
            </service>
        </services>
    </container>
    

    LoadClassMetadataListener.php

    <?php
    
    namespace Fluency\Bundle\FrameworkBundle\EventListener;
    
    use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
    use Doctrine\ORM\Mapping\ClassMetadataInfo;
    use Symfony\Component\DependencyInjection\ContainerAware;
    
    /**
     * Class LoadClassMetadataListener
     *
     * @package Fluency\Bundle\FrameworkBundle\EventListener
     */
    class LoadClassMetadataListener extends ContainerAware
    {
        /**
         * @param LoadClassMetadataEventArgs $args
         */
        public function loadClassMetadata(LoadClassMetadataEventArgs $args)
        {
            if (!$this->container->get('database_connection')->getDatabasePlatform()->supportsSchemas())
            {
                $classMetadata = $args->getClassMetadata();
                $tableName = $classMetadata->table['name'];
                if (strpos($tableName, '.'))
                {
                    $reflectionClass = $classMetadata->getReflectionClass();
    
                    $hashedName = sha1($reflectionClass->name);
                    $cacheFileName = strtr($hashedName, '\\', '-') . '.cache.php';
                    $this->refreshAnnotationsCache($cacheFileName);
    
                    $classMetadata->table['name'] = str_replace('.', '_', $tableName);
    
                    foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping)
                    {
                        if ($mapping['type'] == ClassMetadataInfo::MANY_TO_MANY)
                        {
                            if (isset($classMetadata->associationMappings[$fieldName]['joinTable']['name']))
                            {
                                $mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
                                if (strpos($mappedTableName, '.'))
                                {
                                    $classMetadata->associationMappings[$fieldName]['joinTable']['name'] = str_replace('.', '_',
                                        $mappedTableName);
    
                                    $cacheFileName = strtr($hashedName, '\\', '-') . '$' . $fieldName . '.cache.php';
                                    $this->refreshAnnotationsCache($cacheFileName);
                                }
                            }
                        }
                    }
                }
            }
        }
    
        /**
         * @param $cacheFileName
         */
        private function refreshAnnotationsCache($cacheFileName)
        {
            $cachePath = $this->container->getParameter('kernel.root_dir') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR .
                $this->container->getParameter('kernel.environment') .
                DIRECTORY_SEPARATOR . 'annotations';
    
            $cacheFilePath = $cachePath . DIRECTORY_SEPARATOR . $cacheFileName;
    
            $tableAnnotation = 'Doctrine\\ORM\\Mapping\\Table';
            $joinTableAnnotation = 'Doctrine\\ORM\\Mapping\\JoinTable';
    
            $data = include $cacheFilePath;
    
            $newData = array();
            foreach ($data AS $annotationClass)
            {
                if (get_class($annotationClass) == $tableAnnotation OR get_class($annotationClass) == $joinTableAnnotation)
                {
                    $annotationClass->name = str_replace('.', '_', $annotationClass->name);
                }
                $newData[] = $annotationClass;
            }
    
            file_put_contents($cacheFilePath, '<?php return unserialize(' . var_export(serialize($newData), true) . ');');
    
        }
    }
    

    With both listeners (remember SchemaCreateTableListener) we can switch between databases platforms without problems with table names. Thank's to Peter Bailey but these pieces of code it's was I talking about it.