2019-08-14 23:50:53 +00:00
|
|
|
<?php
|
|
|
|
namespace App\Doctrine\Event;
|
|
|
|
|
|
|
|
use App\Annotations\AuditLog\Auditable;
|
|
|
|
use App\Annotations\AuditLog\AuditIdentifier;
|
|
|
|
use App\Annotations\AuditLog\AuditIgnore;
|
|
|
|
use App\Entity;
|
|
|
|
use Doctrine\Common\Annotations\Reader;
|
|
|
|
use Doctrine\Common\EventSubscriber;
|
2019-08-15 19:01:00 +00:00
|
|
|
use Doctrine\ORM\EntityManager;
|
2019-08-14 23:50:53 +00:00
|
|
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
|
|
|
use Doctrine\ORM\Events;
|
|
|
|
use Doctrine\ORM\Mapping\ClassMetadataInfo;
|
|
|
|
use Doctrine\ORM\PersistentCollection;
|
2019-08-15 19:01:00 +00:00
|
|
|
use Doctrine\ORM\Proxy\Proxy;
|
2019-09-04 18:00:51 +00:00
|
|
|
use ReflectionClass;
|
|
|
|
use ReflectionObject;
|
2019-08-14 23:50:53 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A hook into Doctrine's event listener to write changes to "Auditable"
|
|
|
|
* entities to the audit log.
|
|
|
|
*
|
|
|
|
* Portions inspired by DataDog's audit bundle for Doctrine:
|
|
|
|
* https://github.com/DATA-DOG/DataDogAuditBundle/blob/master/src/DataDog/AuditBundle/EventSubscriber/AuditSubscriber.php
|
|
|
|
*/
|
|
|
|
class AuditLog implements EventSubscriber
|
|
|
|
{
|
|
|
|
/** @var Reader */
|
|
|
|
protected $reader;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Reader $reader
|
|
|
|
*/
|
|
|
|
public function __construct(Reader $reader)
|
|
|
|
{
|
|
|
|
$this->reader = $reader;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSubscribedEvents()
|
|
|
|
{
|
|
|
|
return [
|
|
|
|
Events::onFlush,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onFlush(OnFlushEventArgs $args)
|
|
|
|
{
|
|
|
|
$newAuditLogs = [];
|
|
|
|
|
|
|
|
$em = $args->getEntityManager();
|
|
|
|
$uow = $em->getUnitOfWork();
|
|
|
|
|
|
|
|
$collections = [
|
|
|
|
Entity\AuditLog::OPER_INSERT => $uow->getScheduledEntityInsertions(),
|
|
|
|
Entity\AuditLog::OPER_UPDATE => $uow->getScheduledEntityUpdates(),
|
|
|
|
Entity\AuditLog::OPER_DELETE => $uow->getScheduledEntityDeletions(),
|
|
|
|
];
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
foreach ($collections as $changeType => $collection) {
|
2019-08-14 23:50:53 +00:00
|
|
|
foreach ($collection as $entity) {
|
|
|
|
// Check that the entity being managed is "Auditable".
|
2019-09-04 18:00:51 +00:00
|
|
|
$reflectionClass = new ReflectionObject($entity);
|
2019-08-14 23:50:53 +00:00
|
|
|
|
|
|
|
$auditable = $this->reader->getClassAnnotation($reflectionClass, Auditable::class);
|
|
|
|
if (null === $auditable) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the changes made to the entity.
|
2019-08-15 19:01:00 +00:00
|
|
|
$changesRaw = $uow->getEntityChangeSet($entity);
|
2019-08-14 23:50:53 +00:00
|
|
|
|
|
|
|
// Look for the @AuditIgnore annotation on properties.
|
2019-08-15 19:01:00 +00:00
|
|
|
$changes = [];
|
|
|
|
|
|
|
|
foreach ($changesRaw as $changeField => [$fieldPrev, $fieldNow]) {
|
|
|
|
// With new entity creation, fields left NULL are still included.
|
|
|
|
if ($fieldPrev === $fieldNow) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure the property isn't ignored.
|
|
|
|
$property = $reflectionClass->getProperty($changeField);
|
2019-08-14 23:50:53 +00:00
|
|
|
$annotation = $this->reader->getPropertyAnnotation($property, AuditIgnore::class);
|
|
|
|
|
|
|
|
if (null !== $annotation) {
|
2019-08-15 19:01:00 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if either field value is an object.
|
|
|
|
if ($this->isEntity($em, $fieldPrev)) {
|
2019-09-04 18:00:51 +00:00
|
|
|
$fieldPrev = $this->getIdentifier(new ReflectionObject($fieldPrev), $fieldPrev);
|
2019-08-15 19:01:00 +00:00
|
|
|
}
|
|
|
|
if ($this->isEntity($em, $fieldNow)) {
|
2019-09-04 18:00:51 +00:00
|
|
|
$fieldNow = $this->getIdentifier(new ReflectionObject($fieldNow), $fieldNow);
|
2019-08-14 23:50:53 +00:00
|
|
|
}
|
2019-08-15 19:01:00 +00:00
|
|
|
|
|
|
|
$changes[$changeField] = [$fieldPrev, $fieldNow];
|
2019-08-14 23:50:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (Entity\AuditLog::OPER_UPDATE === $changeType && empty($changes)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the identifier method or property.
|
|
|
|
$identifier = $this->getIdentifier($reflectionClass, $entity);
|
|
|
|
if (null === $identifier) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$newAuditLogs[] = new Entity\AuditLog(
|
|
|
|
$changeType,
|
|
|
|
get_class($entity),
|
|
|
|
$identifier,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
$changes
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle changes to collections.
|
|
|
|
$associated = [];
|
|
|
|
$disassociated = [];
|
|
|
|
|
|
|
|
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
|
|
|
|
/** @var PersistentCollection $collection */
|
|
|
|
$owner = $collection->getOwner();
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
$reflectionClass = new ReflectionObject($owner);
|
2019-08-14 23:50:53 +00:00
|
|
|
$isAuditable = $this->reader->getClassAnnotation($reflectionClass, Auditable::class);
|
|
|
|
if (null === $isAuditable) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore inverse side or one to many relations
|
|
|
|
$mapping = $collection->getMapping();
|
|
|
|
if (!$mapping['isOwningSide'] || $mapping['type'] !== ClassMetadataInfo::MANY_TO_MANY) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$ownerIdentifier = $this->getIdentifier($reflectionClass, $owner);
|
|
|
|
|
|
|
|
foreach ($collection->getInsertDiff() as $entity) {
|
2019-09-04 18:00:51 +00:00
|
|
|
$targetReflectionClass = new ReflectionObject($entity);
|
2019-08-14 23:50:53 +00:00
|
|
|
$targetIsAuditable = $this->reader->getClassAnnotation($targetReflectionClass, Auditable::class);
|
|
|
|
if (null === $targetIsAuditable) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$entityIdentifier = $this->getIdentifier($targetReflectionClass, $entity);
|
|
|
|
$associated[] = [$owner, $ownerIdentifier, $entity, $entityIdentifier];
|
|
|
|
}
|
|
|
|
foreach ($collection->getDeleteDiff() as $entity) {
|
2019-09-04 18:00:51 +00:00
|
|
|
$targetReflectionClass = new ReflectionObject($entity);
|
2019-08-14 23:50:53 +00:00
|
|
|
$targetIsAuditable = $this->reader->getClassAnnotation($targetReflectionClass, Auditable::class);
|
|
|
|
if (null === $targetIsAuditable) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$entityIdentifier = $this->getIdentifier($targetReflectionClass, $entity);
|
|
|
|
$disassociated[] = [$owner, $ownerIdentifier, $entity, $entityIdentifier];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
|
|
|
|
/** @var PersistentCollection $collection */
|
|
|
|
$owner = $collection->getOwner();
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
$reflectionClass = new ReflectionObject($owner);
|
2019-08-14 23:50:53 +00:00
|
|
|
$isAuditable = $this->reader->getClassAnnotation($reflectionClass, Auditable::class);
|
|
|
|
if (null === $isAuditable) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore inverse side or one to many relations
|
|
|
|
$mapping = $collection->getMapping();
|
|
|
|
if (!$mapping['isOwningSide'] || $mapping['type'] !== ClassMetadataInfo::MANY_TO_MANY) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$ownerIdentifier = $this->getIdentifier($reflectionClass, $owner);
|
|
|
|
|
|
|
|
foreach ($collection->toArray() as $entity) {
|
2019-09-04 18:00:51 +00:00
|
|
|
$targetReflectionClass = new ReflectionObject($entity);
|
2019-08-14 23:50:53 +00:00
|
|
|
$targetIsAuditable = $this->reader->getClassAnnotation($targetReflectionClass, Auditable::class);
|
|
|
|
if (null === $targetIsAuditable) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$entityIdentifier = $this->getIdentifier($targetReflectionClass, $entity);
|
|
|
|
$disassociated[] = [$owner, $ownerIdentifier, $entity, $entityIdentifier];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
foreach ($associated as [$owner, $ownerIdentifier, $entity, $entityIdentifier]) {
|
2019-08-14 23:50:53 +00:00
|
|
|
$newAuditLogs[] = new Entity\AuditLog(
|
|
|
|
Entity\AuditLog::OPER_INSERT,
|
|
|
|
get_class($owner),
|
|
|
|
$ownerIdentifier,
|
|
|
|
get_class($entity),
|
|
|
|
$entityIdentifier,
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
foreach ($disassociated as [$owner, $ownerIdentifier, $entity, $entityIdentifier]) {
|
2019-08-14 23:50:53 +00:00
|
|
|
$newAuditLogs[] = new Entity\AuditLog(
|
|
|
|
Entity\AuditLog::OPER_DELETE,
|
|
|
|
get_class($owner),
|
|
|
|
$ownerIdentifier,
|
|
|
|
get_class($entity),
|
|
|
|
$entityIdentifier,
|
|
|
|
[]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$auditLogMetadata = $em->getClassMetadata(Entity\AuditLog::class);
|
2019-09-04 18:00:51 +00:00
|
|
|
foreach ($newAuditLogs as $auditLog) {
|
2019-08-14 23:50:53 +00:00
|
|
|
$uow->persist($auditLog);
|
|
|
|
$uow->computeChangeSet($auditLogMetadata, $auditLog);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-15 19:01:00 +00:00
|
|
|
/**
|
|
|
|
* @param EntityManager $em
|
|
|
|
* @param object|string $class
|
2019-09-20 16:44:38 +00:00
|
|
|
*
|
2019-08-15 19:01:00 +00:00
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
protected function isEntity(EntityManager $em, $class): bool
|
|
|
|
{
|
|
|
|
if (is_object($class)) {
|
|
|
|
$class = ($class instanceof Proxy)
|
|
|
|
? get_parent_class($class)
|
|
|
|
: get_class($class);
|
2019-09-04 18:00:51 +00:00
|
|
|
} else {
|
|
|
|
if (!is_string($class)) {
|
|
|
|
return false;
|
|
|
|
}
|
2019-08-15 19:01:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!class_exists($class)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !$em->getMetadataFactory()->isTransient($class);
|
|
|
|
}
|
|
|
|
|
2019-08-14 23:50:53 +00:00
|
|
|
/**
|
|
|
|
* Get the identifier string for an entity, if it's set or fetchable.
|
|
|
|
*
|
2019-09-04 18:00:51 +00:00
|
|
|
* @param ReflectionClass $reflectionClass
|
2019-08-14 23:50:53 +00:00
|
|
|
* @param object $entity
|
2019-09-20 16:44:38 +00:00
|
|
|
*
|
2019-08-14 23:50:53 +00:00
|
|
|
* @return string|null
|
|
|
|
*/
|
2019-09-04 18:00:51 +00:00
|
|
|
protected function getIdentifier(ReflectionClass $reflectionClass, $entity): ?string
|
2019-08-14 23:50:53 +00:00
|
|
|
{
|
2019-09-04 18:00:51 +00:00
|
|
|
foreach ($reflectionClass->getMethods() as $reflectionMethod) {
|
2019-08-14 23:50:53 +00:00
|
|
|
$isIdentifier = $this->reader->getMethodAnnotation($reflectionMethod, AuditIdentifier::class);
|
|
|
|
|
|
|
|
if (null !== $isIdentifier) {
|
|
|
|
return (string)$reflectionMethod->invoke($entity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-04 18:00:51 +00:00
|
|
|
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
|
2019-08-14 23:50:53 +00:00
|
|
|
$isIdentifier = $this->reader->getPropertyAnnotation($reflectionProperty, AuditIdentifier::class);
|
|
|
|
|
|
|
|
if (null !== $isIdentifier) {
|
|
|
|
return $reflectionProperty->getValue($entity);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (method_exists($entity, 'getName')) {
|
|
|
|
return $entity->getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|