Статьи

Внедрение сервиса в доктрине типа DBAL

Когда вы думаете о типе DBAL Doctrine 2, вы думаете об элементарном, но как вы можете программно работать над этим типом, не определяя событие?

Тип DBAL не разрешает доступ к служебному контейнеру Symfony 2, вы должны использовать хак. Но перед этим позвольте мне объяснить классическим способом (используя события), почему вы должны использовать этот хак и почему вы не должны.

Классический способ определен в Поваренной книге Symfony 2:  Как зарегистрировать прослушиватели событий и подписчиков

События Doctrine 2 в отличие от событий Symfony 2 не определены разработчиком, разработчик может только прикреплять к ним прослушиватели. Зачем? Поскольку Doctrine 2 не является основой, которую вы можете использовать для всего, постоянство — это его единственная работа.

Когда вы должны использовать этот хак?  Если ваш хранимый объект не является представлением PHP-объекта в формате 1: 1, его разработка может быть запоминающейся или действительно быстрой.

Я использую этот хак для browscaps: с помощью  BrowscapBundle  я могу преобразовать строку пользовательского агента в  stdClass объект (например,   функцию get_browser ).

Наш объект

<?php
// src/Acme/DemoBundle/Entity/Agent.php
namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Table
* @ORM\Entity
*/
class Agent
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(name="header", type="string")
*/
private $header;
}

Как видите, нет ничего особенного: это просто архив, используемый для хранения  User-Agent строк.
С классическим способом я должен написать внешний прослушиватель событий (чтобы разрешить автоматическое восстановление сущностей).

services:
  acme.demo_bundle.event_listener.agent_listener
    class: "Acme\DemoBundle\EventListener\AgentListener"
    arguments:
      - "@service_container"
    tags:
      - { name: doctrine.event_listener, event: prePersist }
      - { name: doctrine.event_listener, event: postPersist }
      - { name: doctrine.event_listener, event: preUpdate }
      - { name: doctrine.event_listener, event: postUpdate }
      - { name: doctrine.event_listener, event: postLoad }

<?php
// src/Acme/DemoBundle/EventListener/AgentListener.php

namespace Acme\DemoBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Acme\DemoBundle\Entity\Agent;

class AgentListener
{
private $container;

public function __construct($container)
{
$this->container = $container;
}

public function prePersist(LifecycleEventArgs $args)
{
$this->doObjectToString($args);
}

public function postPersist(LifecycleEventArgs $args)
{
$this->doStringToObject($args);
}

public function preUpdate(LifecycleEventArgs $args)
{
$this->doObjectToString($args);
}

public function postUpdate(LifecycleEventArgs $args)
{
$this->doStringToObject($args);
}

public function postLoad(LifecycleEventArgs $args)
{
$this->doStringToObject($args);
}

private function doStringToObject($args)
{
$entity = $args->getEntity();

if ($entity instanceof Agent && !is_object($entity->getHeader())) {
$browscap = $this->container->get('browscap');
$browser = $browscap->getBrowser($entity->getHeader());
$entity->setHeader($browser);
}
}

private function doObjectToString($args)
{
$entity = $args->getEntity();

if ($entity instanceof Agent && is_object($entity->getHeader())) {
$user_agent = $entity->getHeader()->browser_name;
$entity->setHeader($user_agent);
}
}
}

С этим кодом каждый раз, когда вы сохраняете, обновляете или извлекаете сущность агента из / в связанную систему хранения, он будет преобразован из строки в объект.

Проблема в том, что эти обратные вызовы будут вызываться каждый раз, и для вашего приложения не рекомендуются многочисленные события.

Но с этим хаком я могу написать:

services:
  acme.demo_bundle.event_listener.container_listener:
    arguments:
      - "@service_container"
    class: "Acme\DemoBundle\EventListener\ContainerListener"
    tags:
      - { name: doctrine.event_listener, event: getContainer }

Doctrine ignores this event but it exists and results attached!

<?php
// src/Acme/DemoBundle/EventListener/ContainerListener.php

namespace Test\StorageBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ContainerListener
{
private $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

public function getContainer()
{
return $this->container;
}
}

This listener seems useless, but it’s the only way for this hack because Doctrine 2 DBAL Type doesn’t allow direct access to the service container but allows access to events listeners.

<?php
// src/Acme/DemoBundle/Types/BrowscapType.php

namespace Acme\DemoBundle\Types;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use phpbrowscap\Browscap;
use stdClass;

class BrowscapType extends Type
{
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration);
}

public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (is_null($value)) {
return null;
}

$listeners = $platform->getEventManager()->getListeners('getContainer');

$listener = array_shift($listeners);
$container = $listener->getContainer();

return $container->get('browscap')->getBrowser($value);
}

public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof Browscap) {
return $value->getBrowser()->browser_name;
} elseif ($value instanceof stdClass) {
return $value->browser_name;
}

return $value;
}

public function getName()
{
return 'browscap';
}

public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}

I use this hack to define only the events related to application flow (less events is better).

Now that you know when you can use this, you must read why you shouldn’t use it.
Let me explain the reason with one simple example: imagine that one day PHP will allow external hooks in native classes constructor, how can you work without knowing what you’re doing while initializing a new stdClass? The same reason here: everytime you extract a value from the database you want extract it fast (hopefully you’ll extract more than one records), but how can you be sure that extraction is fast if every attribute of a single record depends on external libraries and logics?

Quoting Ocramius, member of the Doctrine 2 development team:

DBAL types are not designed for Dependency Injection.

We explicitly avoided using DI for DBAL types because they have to stay simple.

We’ve been asked many many times to change this behaviour, but doctrine believes that complex data manipulation should NOT happen within the very core of the persistence layer itself. That should be handled in your service layer.