我们有一个服务,其目的是将用户操作记录到数据库中。底层实体ActionLog与我们的用户实体具有多对一关系。这两个实体都绑定到同一个DBAL连接(和ORM EntityManager)。
问题是:当保留和刷新新的ActionLog实体时会引发异常,说我们应该级联持久化对象集为#user属性,因为被认为是新的:
通过关系"Doctrine\Model\ActionLog#user"找到一个新实体,该实体未配置为对实体 John Doe 进行级联持久操作。要解决此问题:在此未知实体上显式调用 EntityManager#persist(),或者配置级联在映射中保留此关联,例如 @ManyToOne(..,cascade={"persist"})。
这很烦人,因为 User 实例实际上直接来自数据库,因此根本不是什么新鲜事!我们希望此 User 对象已经由entityManager"管理",并通过标识映射引用(换句话说,该对象不是"分离"的)。
那么,为什么 Doctrine 会将用户实体实例(经过身份验证的用户)视为分离/新实例?
使用 Symfony 4.0.6 ; doctrine/orm v2.6.1, doctrine/dbal 2.6.3, doctrine/doctrine-bundle 1.8.1
操作日志模型映射提取
DoctrineModelActionLog:
type: entity
table: action_log
repositoryClass: DoctrineRepositoryActionLogRepository
manyToOne:
user:
targetEntity: DoctrineModelUser
id: # …
fields: # …
日志服务声明
log_manager:
class: ServiceLogLogManager
public: true
arguments:
- "@?security.token_storage"
calls:
# setter required instead of the dependency injection
# to prevent circular dependency.
- ['setEntityManager', ["@doctrine.orm.entity_manager"]]
日志服务实现 - 创建新的操作日志记录
<?php
namespace ServiceLog;
use DoctrineCommonPersistenceObjectManager;
use SymfonyComponentSecurityCoreAuthenticationTokenStorageTokenStorage;
use SymfonyComponentSecurityCoreUserUserInterface;
use DoctrineModelUser;
use DoctrineModelActionLog;
class LogManager
{
/**
* @var ObjectManager
*/
protected $om;
/**
* @var TokenStorage
*/
protected $tokenStorage;
/**
* @var User
*/
protected $user;
/**
* @var bool
*/
protected $disabled = false;
public function __construct(TokenStorage $tokenStorage = null)
{
$this->tokenStorage = $tokenStorage;
}
public function setEntityManager(ObjectManager $om)
{
$this->om = $om;
}
public function log(string $namespace, string $action, string $message = null, array $changeSet = null)
{
$log = new ActionLog;
$log
->setNamespace($namespace)
->setAction($action)
->setMessage($message)
->setChangeset($changeSet)
;
if ($this->isDisabled()) {
return;
}
if (!$log->getUser()) {
$user = $this->getUser();
$log->setUsername(
$user instanceof UserInterface
? $user->getUsername()
: ''
);
$user instanceof User && $log->setUser($user);
}
$this->om->persist($log);
$this->om->flush();
}
public function setUser(User $user): self
{
$this->user = $user;
return $this;
}
public function getUser(): ?UserInterface
{
if (!$this->user) {
if ($token = $this->tokenStorage->getToken()) {
$this->user = $token->getUser();
}
}
return is_string($this->user) ? null : $this->user;
}
public function disable(bool $disabled = true): self
{
$this->disabled = $disabled;
return $this;
}
public function isDisabled(): bool
{
return $this->disabled;
}
}
用户实体转储。如您所见,信息来自数据库。
User {#417 ▼
#name: "John Doe"
#email: "john_doe@example.com"
#password: "ec40577ad8057ee34ce0bb9414673bf3"
#createdAt: DateTime @1523344938 {#427 ▶}
#enabled: true
#lastLogin: null
#id: 1
}
# Associated database row
'1', 'John Doe', 'john_doe@example.com', 'ec40577ad8057ee34ce0bb9414673bf3', '2018-04-10 07:22:18', '1', '1', null
断言是正确的,从ORM的角度来看,传递给ActionLog::setUser
方法的用户实例不是已知的引用。
发生的情况:对象来自身份验证过程,该过程在每个请求(建议的yceruto)上从会话存储中反序列化用户数据,并创建一个用户实例。
我的自定义用户提供程序应该通过 ORM 刷新用户对象,但它没有,因此"新引用">仍然存在。我不知道为什么虽然我的用户提供程序实现假设它应该:
/**
* @var ObjectManager
*/
protected $em;
public function __construct(ObjectManager $em)
{
$this->em = $em;
}
public function loadUserByUsername($username)
{
$user = $this->em->getRepository(User::class)->loadUserByUsername($username);
if ($user && $user->isAdmin()) {
return $user;
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return UserInterface::class === $class;
}
也就是说,我设法(暂时)在DoctrineORMEntityManager::getReference
方法的帮助下使用 ORM 代理机制解决了这个问题,这可以完成,因为会话中的重建对象包含用户 ID(主键)。
此修复包括替换Log_manager服务中的以下指令:
$this->user = $token->getUser();
# ↓ BECOMES ↓
$this->user = $this->om->getReference(User::class, $token->getUser()->getId());
对此有任何想法吗?滥用?Github问题?不管是什么原因,非常欢迎评论。