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());
}
这是我们在第一行 添加 Filter
到 EntityManager
配置的地方。这允许我们使用它。需要的是你给 Filter
一个名字,然后用第二个参数告诉 EntityManager
哪个 class 属于这个名字。
接下来,我们enable()
Filter
的名字。 Daredevel 的教程向我展示了 enable()
函数实际上 returns 过滤器刚刚启用。
在 $filter
变量中使用 returned Filter
,我们现在可以使用该实例来设置参数,这是我们在最后一条语句中所做的。
这与我在我的问题中尝试的有何不同?好吧,在我的问题中,我试图以相反的方式来做。我尝试通过配置和 Listener
启用 Filter
。但是,使用后一种方法,我尝试在变量中创建和存储一个 Filter
实例,设置所需的参数,然后在 EntityManager
中启用它,这恰恰是错误的方法,因为 [=40] =] 创建一个新实例(因此没有变量)。
因此,我将此内容留给可能偶然发现相同问题的任何人。
我想出来了,答案在下面 - 把问题和所有过程的东西留在这里,以防它可能会帮助别人在未来找到同样的东西,虽然大多数答案使它变得多余。
我想做的是,根据用户关联的公司 (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());
}
这是我们在第一行 添加 Filter
到 EntityManager
配置的地方。这允许我们使用它。需要的是你给 Filter
一个名字,然后用第二个参数告诉 EntityManager
哪个 class 属于这个名字。
接下来,我们enable()
Filter
的名字。 Daredevel 的教程向我展示了 enable()
函数实际上 returns 过滤器刚刚启用。
在 $filter
变量中使用 returned Filter
,我们现在可以使用该实例来设置参数,这是我们在最后一条语句中所做的。
这与我在我的问题中尝试的有何不同?好吧,在我的问题中,我试图以相反的方式来做。我尝试通过配置和 Listener
启用 Filter
。但是,使用后一种方法,我尝试在变量中创建和存储一个 Filter
实例,设置所需的参数,然后在 EntityManager
中启用它,这恰恰是错误的方法,因为 [=40] =] 创建一个新实例(因此没有变量)。
因此,我将此内容留给可能偶然发现相同问题的任何人。