ZF2 + Doctrine 2 - 使用工厂创建 Doctrine SQLFilter - 如何?

ZF2 + Doctrine 2 - Use a Factory to create a Doctrine SQLFilter - How?

我想出来了,答案在下面 - 把问题和所有过程的东西留在这里,以防它可能会帮助别人在未来找到同样的东西,虽然大多数答案使它变得多余。


我想做的是,根据用户关联的公司 (User#company) 过滤数据。

对于这个场景我有几个实体:

场景是数据(在本例中为 Address 实体对象)由 User 实体创建。每个 User 都有一个它所属的 Company 实体。因此,每个 Address 都有一个 Address#createdByCompany 属性.

现在我正在尝试创建 SQLFilter 扩展,如 Doctrine docs - "Working with Filters" 所述。

我创建了以下 class:

class CreatedByCompanyFilter extends SQLFilter
{
    /**
     * @var Company
     */
    protected $company;

    /**
     * @param ClassMetadata $targetEntity
     * @param string $targetTableAlias
     * @return string
     * @throws TraitNotImplementException
     */
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
    {
        // Check if Entity implements CreatedByCompanyAwareInterface, if not, return empty string
        if (!$targetEntity->getReflectionClass()->implementsInterface(CreatedByCompanyInterface::class)) {

            return '';
        }

        if (!array_key_exists(CreatedByCompanyTrait::class, $targetEntity->getReflectionClass()->getTraits())) {

            throw new TraitNotImplementException(
                ($targetEntity->getReflectionClass()->getName()) . ' requires "' . CreatedByCompanyTrait::class . '" to be implemented.'
            );
        }

        return $targetTableAlias . '.created_by_company = ' . $this->getCompany()->getId();
    }

    /**
     * @return Company
     */
    public function getCompany(): Company // Using PHP 7.1 -> explicit return types
    {
        return $this->company;
    }

    /**
     * @param Company $company
     * @return CreatedByCompanyFilter
     */
    public function setCompany(Company $company): CreatedByCompanyFilter
    {
        $this->company = $company;
        return $this;
    }
}

要使用此过滤器,它已在配置和设置中注册以加载到模块 Bootstrap (onBootstrap) 中。到目前为止一切顺利,上面的被使用了。

但是,上面的不是通过工厂模式使用的。此外,您可能会注意到 addFilterConstraint(...) 使用 $this->getCompany()->getId(),但 $this->setCompany() 并未在任何地方调用,而是在 null 值上创建函数调用。

我如何使用工厂来创建这个 class,使用 ZF2 ServiceManager 的正常路由或通过 Doctrine 本身的注册?


我已经尝试过的

除了 Google 在过去的几个小时里一直在努力寻找解决方案之外,我还尝试了以下方法

1 - ZF2 工厂

使用默认的 ZF2 方法在配置中注册工厂,不起作用:

'service_manager' => [
    'factories' => [
        CreatedByCompanyFilter::class => CreatedByCompanyFilterFactory::class,
    ],
],

工厂永远不会被调用。这可能与执行顺序有关。我正在考虑在 ServiceManager 完全启动和 运行ning 之前设置 Doctrine SQLFilters,以防万一我正在尝试做的场景:基于一些基于角色的东西(或 "company-stuff" 在这种情况下)。

2 - Ocramius's ZF2 Delegator Factories

在进行此工作时,我找到了 Ocramius 的委托人工厂。非常有趣的东西,绝对值得一读并且效果很好。但是,不适用于我的情况。我按照他的指导创建了一个 CreatedByCompanyFilterDelegatorFactory。这是我在配置中注册的,但没有结果,实际的工厂从未被调用。

(抱歉,代码已经删除)


我正在尝试的工厂 运行 更新为 *ListenerFactory,参见 'Currently trying' 下面

class CreatedByCompanyListenerFactory implements FactoryInterface
{
    // NOTE: REMOVED DOCBLOCKS/COMMENTS FOR LESS CODE
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $authService = $serviceLocator->get('zfcuser_auth_service');
        $user = $authService->getIdentity();

        $listener = new CreatedByCompanyListener();
        $listener->setCompany($user->getCompany());

        return $listener;
    }
}

正在尝试

我想我可以尝试从 Gedmo 扩展和 Doctrine 事件剧本中取出一页,并使用将监听器挂接到 loadClassMetadata 事件的格式。

例如,Gedmo 的 SoftDeleteable 在 Doctrine 的配置中具有以下配置以使其工作:

'eventmanager' => [
    'orm_default' => [
        'subscribers' => [
            SoftDeleteableListener::class,
        ],
    ],
],
'configuration' => [
    'orm_default' => [
        'filters' => [
            'soft-deleteable' => SoftDeleteableFilter::class,
        ],
    ],
],

所以我想,该死,让我们试试看,然后设置以下内容:

'eventmanager' => [
    'orm_default' => [
        'subscribers' => [
            CreatedByCompanyListener::class,
        ],
    ],
],
'configuration' => [
    'orm_default' => [
        'filters' => [
            'created-by-company' => CreatedByCompanyFilter::class,
        ],
    ],
],
'service_manager' => [
    'factories' => [
        CreatedByCompanyListener::class => CreatedByCompanyListenerFactory::class,
    ],
],

目的是使用 *ListenerFactory 将经过身份验证的 User 实体放入 *Listener*Listener 又会将与 User 关联的 Company 传递到 EventArgs 中,再传递给 CreatedByCompanyFilter。从理论上讲,该对象应该可用。

CreatedByCompanyLister 目前如下:

class CreatedByCompanyListener implements EventSubscriber
{
    //NOTE: REMOVED DOCBLOCKS/HINTS FOR LESS CODE
    protected $company;

    public function getSubscribedEvents()
    {
        return [
            Events::loadClassMetadata,
        ];
    }

    public function loadClassMetadata(EventArgs $eventArgs)
    {
        $test = $eventArgs; // Debug line, not sure on what to do if this works yet ;) 
    }

    // $company getter/setter
}

但是,我卡在 *ListenerFactory 中使用的 Zend\Authentication\AuthenticationService 上,它在尝试使用 $user = $authService->getIdentity(); 获取 User 的身份时抛出异常].

这个函数在内部继续到 ZfcUser\Authentication\Storage\Db class,第 70 行,($identity = $this->getMapper()->findById($identity);),它在那里崩溃,抛出 getMapper():

Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for doctrine.entitymanager.orm_default

跳过(PhpStorm 中的 xdebug)调用以某种方式让它继续一点(有点 wtf...),但随后抛出:

An exception was raised while creating "zfcuser_user_mapper"; no instance returned

“*Exception*previous”表示:

Circular dependency for LazyServiceLoader was found for instance Doctrine\ORM\EntityManager

我独立地知道所有这些错误的含义,但我以前从未见过所有这些错误都被同一个函数调用 (->getMapper()) 抛出。有任何想法吗?

当然,我当时想得太复杂了。我现在 运行 使用以下 classes:

  • CreatedByCompanyFilter
  • CreatedByCompanyListener
  • CreatedByCompanyListenerFactory

我唯一需要的配置如下:

'listeners' => [
    CreatedByCompanyListener::class,
],
'service_manager' => [
    'factories' => [
        CreatedByCompanyListener::class => CreatedByCompanyListenerFactory::class,
    ],
],

注意: 没有在 Doctrine 配置中注册过滤器。所以如果你想做同样的事情,请不要做下面的事情!

'doctrine' => [
    'eventmanager' => [
        'orm_default' => [
            'subscribers' => [
                CreatedByCompanyListener::class,
            ],
        ],
    ],
    'configuration' => [
        'orm_default' => [
            'filters' => [
                'created-by-company' => CreatedByCompanyFilter::class,
            ],
        ],
    ],
],

重复:在这种情况下不需要以上内容 - 虽然看到我一直在做什么,但我可以看到我(和其他人)如何假设它可能是。


所以,只有 3 个 class,其中一个已注册 Listener,在 ServiceManager 配置中注册了 ListenerFactory

逻辑最终是Listener需要在Doctrine EntityManager中启用Filter,之后可以在[=115]中设置所需的参数=] 值(过滤器)。不确定 "Daredevel" 是否有 Whosebug 帐户,但是 thanks for this article! 我注意到其中启用了过滤器,然后设置了它的参数。

因此,Listener 启用 Filter 并设置其参数。

CreatedByCompanyListener如下:

class CreatedByCompanyListener implements ListenerAggregateInterface
{
    // NOTE: Removed code comments & docblocks for less code. Added inline for clarity.

    protected $company;         // Type Company entity
    protected $filter;          // Type CreatedByCompanyFilter
    protected $entityManager;   // Type EntityManager|ObjectManager
    protected $listeners = [];  // Type array

    public function attach(EventManagerInterface $events)
    {
        $this->listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, [$this, 'onBootstrap'], -5000);
    }

    public function detach(EventManagerInterface $events)
    {
        foreach ($this->listeners as $index => $listener) {
            if ($events->detach($listener)) {
                unset($this->listeners[$index]);
            }
        }
    }

    public function onBootstrap(MvcEvent $event)
    {
        $this->getEntityManager()->getConfiguration()->addFilter('created-by-company', get_class($this->getFilter()));
        $filter = $this->getEntityManager()->getFilters()->enable('created-by-company');

        $filter->setParameter('company', $this->getCompany()->getId());
    }

    // Getters & Setters for $company, $filter, $entityManager
}

CreatedByCompanyListenerFactory如下:

class CreatedByCompanyListenerFactory 实现 FactoryInterface { public 函数 createService(ServiceLocatorInterface $serviceLocator) { $authService = $serviceLocator->get('zfcuser_auth_service'); $entityManager = $serviceLocator->get(EntityManager::class); $listener = new CreatedByCompanyListener();

    $user = $authService->getIdentity();
    if ($user instanceof User) {
        $company = $user->getCompany();
    } else {
        // Check that the database has been created (more than 0 tables)
        if (count($entityManager->getConnection()->getSchemaManager()->listTables()) > 0) {
            $companyRepo = $entityManager->getRepository(Company::class);
            $company = $companyRepo->findOneBy(['name' => 'Guest']);
        } else {
            // Set temporary company for guest user
            $company = $this->tmpGuestCompany(); // Creates "new" Company, sets name, excludes in $entityManager from persisting/flushing it
        }
    }

    $listener->setCompany($company);
    $listener->setFilter(new CreatedByCompanyFilter($entityManager));
    $listener->setEntityManager($entityManager);

    return $listener;
}

}

最后,CreatedByCompanyFilter需要实际做一些事情,所以如下:

class CreatedByCompanyFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
    {
        // Check if Entity implements CreatedByCompanyAwareInterface, if not, return empty string
        if (!$targetEntity->getReflectionClass()->implementsInterface(CreatedByCompanyInterface::class)) {

            return '';
        }

        if (!array_key_exists(CreatedByCompanyTrait::class, $targetEntity->getReflectionClass()->getTraits())) {

            throw new TraitNotImplementedException(
                $targetEntity->getReflectionClass()->getName() . ' requires "' . CreatedByCompanyTrait::class . '" to be implemented.'
            );
        }

        $column = 'created_by_company';
        return "{$targetTableAlias}.{$column} = {$this->getParameter('company')}";
    }
}

为什么这样有效

以上代码中最重要的部分如下,基于之前链接的 Daredevel 教程:

public function onBootstrap(MvcEvent $event)
{
    $this->getEntityManager()->getConfiguration()->addFilter('created-by-company', get_class($this->getFilter()));
    $filter = $this->getEntityManager()->getFilters()->enable('created-by-company');

    $filter->setParameter('company', $this->getCompany()->getId());
}

这是我们在第一行 添加 FilterEntityManager 配置的地方。这允许我们使用它。需要的是你给 Filter 一个名字,然后用第二个参数告诉 EntityManager 哪个 class 属于这个名字。

接下来,我们enable() Filter 的名字。 Daredevel 的教程向我展示了 enable() 函数实际上 returns 过滤器刚刚启用。

$filter 变量中使用 returned Filter,我们现在可以使用该实例来设置参数,这是我们在最后一条语句中所做的。


这与我在我的问题中尝试的有何不同?好吧,在我的问题中,我试图以相反的方式来做。我尝试通过配置和 Listener 启用 Filter。但是,使用后一种方法,我尝试在变量中创建和存储一个 Filter 实例,设置所需的参数,然后在 EntityManager 中启用它,这恰恰是错误的方法,因为 [=40] =] 创建一个新实例(因此没有变量)。

因此,我将此内容留给可能偶然发现相同问题的任何人。