如何将参数传递给 Symfony4 中的自定义 Doctrine 类型

How to pass parameters to a custom Doctrine type in Symfony4

我开发了一种新的 Doctrine 类型来加密字符串。

<?php
namespace App\Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\StringType;
use App\Security\Encoder\OpenSslEncoder;

class EncryptedStringType extends StringType {

    const MTYPE = 'encrypted_string';

    private $cypherMethod;
    private $iv;
    private $privateKey;

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        $openSslEncoder = new OpenSslEncoder($this->cypherMethod, $this->iv, $this->privateKey);

        return $openSslEncoder->decrypt($value);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        $openSslEncoder = new OpenSslEncoder($this->cypherMethod, $this->iv, $this->privateKey);

        return $openSslEncoder->encrypt($value);
    }

    public function getName()
    {
        return self::MTYPE;
    }

    /**
     * @param mixed $cypherMethod
     */
    public function setCypherMethod($cypherMethod)
    {
        $this->cypherMethod = $cypherMethod;
    }

    /**
     * @param mixed $iv
     */
    public function setIv($iv)
    {
        $this->iv = $iv;
    }

    /**
     * @param mixed $privateKey
     */
    public function setPrivateKey($privateKey)
    {
        $this->privateKey = $privateKey;
    }
} 

在旧的 Symfony3 应用程序中,我通过以下方式注册了新类型:

<?php

namespace AppBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Doctrine\DBAL\Types\Type;

class AppBundle extends Bundle
{
    public function __construct()
    {
        Type::addType('encrypted_string', 'AppBundle\Doctrine\DBAL\Types\EncryptedStringType');
    }

    public function boot()
    {
        $encryptedString = Type::getType('encrypted_string');
        $encryptedString->setCypherMethod($this->container->getParameter('open_ssl_cypher_method'));
        $encryptedString->setIv($this->container->getParameter('open_ssl_iv'));
        $encryptedString->setPrivateKey($this->container->getParameter('open_ssl_private_key'));
    }
}

如何在新的 Symfony4 应用程序中执行相同的操作?我知道我可以在 doctrine.yaml 配置文件中注册一个新类型。但是我需要设置密码参数...如何在新版本中设置对象参数?

非常感谢。

以同样的方式,Symfony 4 Kernel class has a boot() method 具有相似的目的,因此您可以肯定地将代码移到那里:

// src/Kernel.php

class Kernel extends BaseKernel
{   
    // ...

    public function boot()
    {
        parent::boot();

        // move here.
    }

// ...

我不喜欢引导答案,所以我深入研究了条令代码,看看是否可以找到与 symfony 依赖注入系统更集成的解决方案。

这是我带来的:

config/services.yaml

我重写了连接工厂 class 和一个接口来标记我需要使用注入的服务。

parameters:
         doctrine.dbal.connection_factory.class: App\Doctrine\Bundle\ConnectionFactory

services:
    _instanceof:
        App\Doctrine\DBAL\Types\ServiceTypeInterface:
            tags: [ app.doctrine.dbal.service_type ]

config/packages

标准的自定义类型声明,没什么特别的。

doctrine:
  dbal:
    url: '%env(resolve:DATABASE_URL)%' 
    types:
      my_type:  App\Doctrine\DBAL\Types\MyTypeNeedingDependencyInjection

  orm:
    auto_generate_proxy_classes: true
    naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
    auto_mapping: true
    mappings:
      App:
        is_bundle: false
        type: annotation
        dir: '%kernel.project_dir%/src/Entity'
        prefix: 'App\Entity'
        alias: App

src/Kernel.php

编译器将类型注册到连接工厂。您也可以使用单独的编译器传递。

<?php

namespace App;

// Standard Kernel use plus those
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;

    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition(Breadcrumb::class);
        $tag = 'app.breadcrumb.title_provider';

        foreach ($this->findAndSortTaggedServices($tag, $container) as $ref) {
            $definition->addMethodCall('addTitleProvider', [$ref]);
        }

        $definition = $container->getDefinition('doctrine.dbal.connection_factory');

        foreach ($container->findTaggedServiceIds('app.doctrine.dbal.service_type') as $id => $_) {
            $definition->addMethodCall('registerServiceType', [new Reference($id)]);
        }
    }

    // Standard Kernel code goes there
}

我检查是否有对应类型 class 的服务,如果有,我通过类型注册表使用该服务,而不是注册 class 名称。

src/Doctrine/Bundle/ConnectionFactory.php

<?php

namespace App\Doctrine\Bundle;

use App\Doctrine\DBAL\Types\ServiceTypeInterface;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory as BaseFactory;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Types\Type;

class ConnectionFactory extends BaseFactory
{
    protected $serviceTypes;

    public function registerServiceType(ServiceTypeInterface $serviceType)
    {
        $this->serviceTypes[get_class($serviceType)] = $serviceType;
    }
    
    public function createConnection(
        array $params,
        Configuration $config = null,
        EventManager $eventManager = null,
        array $mappingTypes = []
    )
    {
        $reflect = new \ReflectionProperty(BaseFactory::class, 'initialized');
        $reflect->setAccessible(true);

        if (!$reflect->getValue($this)) {
            $typesReflect = new \ReflectionProperty(BaseFactory::class, 'typesConfig');
            $typesReflect->setAccessible(true);
            
            foreach ($typesReflect->getValue($this) as $typeName => $typeConfig) {
                if (is_a($typeConfig['class'], ServiceTypeInterface::class, true)) {
                    $registry = Type::getTypeRegistry();
                    
                    if ($registry->has($typeName)) {
                        $registry->override($typeName, $this->serviceTypes[$typeConfig['class']]);
                    }
                    else {
                        $registry->register($typeName, $this->serviceTypes[$typeConfig['class']]);
                    }
                }
                elseif (Type::hasType($typeName)) {
                    Type::overrideType($typeName, $typeConfig['class']);
                }
                else {
                    Type::addType($typeName, $typeConfig['class']);
                }
            }
            
            $reflect->setValue($this, true);
        }            

        return parent::createConnection($params, $config, $eventManager, $mappingTypes);
    }
}

src/App/Doctrine/DBAL/Types/ServiceTypeInterface.php

<?php

namespace App\Doctrine\DBAL\Types;

interface ServiceTypeInterface
{
}

src/Doctrine/DBAL/Types/MyTypeNeedingDependencyInjection.php

<?php

namespace App\Doctrine\DBAL\Types;

use App\Service\MyService;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;

class MyTypeNeedingDependencyInjection extends Type implements ServiceTypeInterface
{
    
    protected $myService;

    /**
     * We have to use setter injection since parent Type class make the constructor final
     * @required
     */
    public function setService(MyService $myService)
    {
        $this->myService = $myService;
    }

    public function getSQLDeclaration(array $column, AbstractPlatform $platform)
    {
        return $platform->getVarcharTypeDeclarationSQL($column);
    }

    public function getDefaultLength(AbstractPlatform $platform)
    {
        return 16;
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $this->myService->toPHP($value);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        return $this->myService->fromPHP($value);
    }

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

评论

这项工作非常好(symfony 5.1,学说 2.7),至少对于我需要的用法而言,我可以用最少的努力添加更多类型(只需要实现 ServiceTypeInterface 并使用 setter 注入) ,但请注意,这使用内部学说功能,通过反射和使用注释为内部的功能,因此没有向前兼容版本。