postgresqllaraveldoctrine-ormdbal

Register custom Doctrine DBAL types in Laravel


Just recently I on-boarded a Laravel 5 project and faced the problem of extending Doctrine DBAL implementation with custom types supported by Postgres. I have to plug in nicely https://github.com/opensoft/doctrine-postgres-types and https://github.com/boldtrn/JsonbBundle into the application configuration. Coming from Symfony2 I was expecting that there will be a smooth way of adding it to Doctrine's configuration (as in http://symfony.com/doc/current/cookbook/doctrine/dbal.html#registering-custom-mapping-types-in-the-schematool). Sadly it doesn't seem to be the case.

I saw the suggestion mentioned in 2013 by betawax in https://github.com/laravel/framework/issues/1675, also replied by tylorotwell that there are no plans for such kind of support. I don't feel convinced this is the right way to do it.

Did anyone overcome this obstacle recently?


Solution

  • After some further research I've created my own package with support of some additional functions and datatypes for PostgreSql 9.4 on Laravel and Symfony. Below is a working solution for my initial question (see original instructions here):

    The steps below are based on Laravel 5 integration with FoxxMD's fork of mitchellvanw/laravel-doctrine.

    1) Register the functions and datatype mappings:

    # Usually part of config/doctrine.php
    <?php
    
    return [
        'entity_managers' => [
            'name_of_your_entity_manager' => [
                'dql' => [
                    'string_functions' => [
                        // Array data types related functions
                        'ALL' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\All',
                        'ANY' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Any',
                        'ARRAY' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Arr',
                        'ARRAY_APPEND' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayAppend',
                        'ARE_OVERLAPING_EACH_OTHER' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayAreOverlapingEachOther',
                        'ARRAY_CARDINALITY' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayCardinality',
                        'ARRAY_CAT' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayCat',
                        'ARRAY_PREPEND' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayPrepend',
                        'ARRAY_REMOVE' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayRemove',
                        'ARRAY_REPLACE' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayReplace',
                        'ARRAY_TO_STRING' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToString',
                        'STRING_TO_ARRAY' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StringToArray',
    
                        // Functions and operators used by both array and json(-b) data types
                        'CONTAINS' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains',
                        'IS_CONTAINED_BY' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy',
    
                        // Json(-b) data type related functions and operators
                        'JSON_GET_FIELD' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField',
                        'JSON_GET_FIELD_AS_TEXT' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText',
                        'JSON_GET_OBJECT' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject',
                        'JSON_GET_OBJECT_AS_TEXT' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText',
                        'JSONB_ARRAY_ELEMENTS' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayElements',
                        'JSONB_ARRAY_ELEMENTS_TEXT' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayElementsText',
                        'JSONB_ARRAY_LENGTH' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayLength',
                        'JSONB_EACH' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbEach',
                        'JSONB_EACH_TEXT' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbEachText',
                        'JSONB_EXISTS' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbObjectKeys',
                        'JSONB_OBJECT_KEYS' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbExists',
    
                        // Basic text search related functions and operators
                        'TO_TSQUERY' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsquery',
                        'TO_TSVECTOR' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsvector',
                        'TSMATCH' => 'MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tsmatch',
                    ],
                ],
                'mapping_types' => [
                    'jsonb' => 'jsonb',
                    '_jsonb' => 'jsonb[]',
                    'jsonb[]' => 'jsonb[]',
                ],
    

    2) Add EventSubscriber for Doctrine

    <?php
    
    namespace Acme\Handlers\Events;
    
    use Doctrine\Common\EventSubscriber as Subscriber;
    use Doctrine\DBAL\DBALException;
    use Doctrine\DBAL\Event\ConnectionEventArgs;
    use Doctrine\DBAL\Events;
    use Doctrine\DBAL\Types\Type;
    
    class DoctrineEventSubscriber implements Subscriber
    {
        /**
         * @return array
         */
        public function getSubscribedEvents()
        {
            return [
                Events::postConnect,
            ];
        }
    
        /**
         * @param ConnectionEventArgs $args
         * @throws DBALException
         */
        public function postConnect(ConnectionEventArgs $args)
        {
            Type::addType('jsonb', "\MartinGeorgiev\Doctrine\DBAL\Types\Jsonb");
            Type::addType('jsonb[]', "\MartinGeorgiev\Doctrine\DBAL\Types\JsonbArray");
        }
    }
    

    3) Add the EventSubscriber for Doctrine to a ServiceProvider

    <?php
    
    namespace Acme\Providers;
    
    use Config;
    use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
    use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
    use Doctrine\Common\Persistence\ManagerRegistry as DoctrineManagerRegistry;
    use Acme\Handlers\Events\DoctrineEventSubscriber;
    
    /**
     * Class EventServiceProvider
     * @package Quantum\Providers
     */
    class EventServiceProvider extends ServiceProvider
    {
        /**
         * Register Doctrine Events as well.
         */
        public function register()
        {
            $this->registerDoctrineEvents();
            $this->registerDoctrineTypeMapping();
        }
    
        /**
         * Register any other events for your application.
         * @param DispatcherContract $events
         * @return void
         */
        public function boot(DispatcherContract $events)
        {
            parent::boot($events);
        }
    
        /**
         * Register Doctrine events.
         */
        private function registerDoctrineEvents()
        {
            $eventManager = $this->registry()->getConnection()->getEventManager();
            $eventManager->addEventSubscriber(new DoctrineEventSubscriber);
        }
    
        /**
         * Register any custom Doctrine type mappings
         */
        private function registerDoctrineTypeMapping()
        {
            $databasePlatform = $this->registry()->getConnection()->getDatabasePlatform();
            $entityManagers = Config::get('doctrine.entity_managers');
            foreach ($entityManagers as $entityManager) {
                if (array_key_exists('mapping_types', $entityManager)) {
                    foreach ($entityManager['mapping_types'] as $dbType => $doctrineName) {
                        $databasePlatform->registerDoctrineTypeMapping($dbType, $doctrineName);
                    }
                }
            }
        }
    
        /**
         * Get the entity manager registry
         * @return DoctrineManagerRegistry
         */
        function registry()
        {
            return app(DoctrineManagerRegistry::class);
        }
    }