<?php /** @noinspection ALL */
namespace AppBundle\Annotations\Reader;
use AppBundle\Annotations\ChangeObservable;
use Doctrine\Common\Annotations\Reader;
/**
* Class ChangeObservableReader
*/
class ChangeObservableReader
{
/**
* @var Reader
*/
private $reader;
/**
* @var array
*/
private $observableMap = [];
/**
* @var StrategyReader
*/
private $strategyReader;
/**
* ChangeObservableReader constructor.
*
* @param Reader $reader
* @param StrategyReader $strategyReader
*/
public function __construct(Reader $reader, StrategyReader $strategyReader)
{
$this->reader = $reader;
$this->strategyReader = $strategyReader;
}
/**
* @param Reader $reader
* @param StrategyReader $strategyReader
*
* @return ChangeObservableReader
*/
public static function createReader(Reader $reader, StrategyReader $strategyReader): self
{
return new self($reader, $strategyReader);
}
/**
* @param $obj
*
* @return bool
*/
public function isObservable($obj): bool
{
$className = \is_object($obj) ? \get_class($obj) : $obj;
return (bool) $this->processClass($className);
}
/**
* @return array
*/
public function getAllObservableClasses(): array
{
return array_keys($this->observableMap);
}
/**
* @param string $className
*
* @return null|ChangeObservable
*/
public function processClass(string $className): ?ChangeObservable
{
if (isset($this->observableMap[$className])) {
return $this->observableMap[$className];
}
try {
/** @var ChangeObservable $observable */
$observable = $this->reader->getClassAnnotation(new \ReflectionClass($className), ChangeObservable::class);
} catch (\ReflectionException $e) {
return null;
}
if ($observable) {
$this->observableMap[$className] = $observable;
}
return $observable;
}
/**
* @param string $className
* @return null|string
*/
public function getDomainRoot(string $className): ?string
{
/** @var ChangeObservable $changeObservable */
$changeObservable = $this->observableMap[$className] ?? null;
return $changeObservable ? $changeObservable->domainRoot : null;
}
/**
* @param object $obj
*
* @return array
*/
public function getDomainRootObjects(object $obj): array
{
$observable = $this->processClass(\get_class($obj));
if (null === $observable) {
return null;
}
$found = $this->strategyReader->read($observable->strategy, $obj);
return \is_object($found) ? [$found] : $found;
}
/**
* @param string $className
* @param ChangeObservable $observable
*/
public function addObservable(string $className, ChangeObservable $observable): void
{
$this->observableMap[$className] = $observable;
}
/**
* @param string $className
* @param object $strategy
*/
public function addStrategy(string $className, object $strategy): void
{
$observable = $this->observableMap[$className];
$observable->strategy = $strategy;
$this->observableMap[$className] = $observable;
}
/**
* @return array
*/
public function getObservableMap(): array
{
return $this->observableMap;
}
}
<?php
namespace AppBundle\Annotations\Reader;
use AppBundle\Annotations\ExpressionStrategy;
use AppBundle\Annotations\StrategyInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
/**
* Class StrategyReader
*/
class StrategyReader
{
/**
* @param StrategyInterface $strategy
* @param mixed ...$args
*
* @return mixed
*/
public function read(StrategyInterface $strategy, ...$args)
{
if (!method_exists($this, $this->getStrategyMethod($strategy))) {
return null;
}
return $this->{$this->getStrategyMethod($strategy)}($strategy, $args[0]);
}
/**
* @param ExpressionStrategy $expressionStrategy
* @param object $obj
*
* @return mixed
*/
private function readExpression(ExpressionStrategy $expressionStrategy, object $obj)
{
return (new ExpressionLanguage())->evaluate(
$expressionStrategy->expression,
[
'object' => $obj,
]
);
}
/**
* @param StrategyInterface $strategy
*
* @return string
*/
private function getStrategyMethod(StrategyInterface $strategy): string
{
return sprintf('read%s', ucwords($strategy->getName()));
}
}
<?php
namespace AppBundle\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* @Annotation
* @Annotation\Target("CLASS")
*/
class ChangeObservable
{
/**
* @var string
*/
public $domainRoot;
/**
* MUST implements StrategyInterface
*
* @var object
*/
public $strategy;
}
<?php
namespace AppBundle\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* @Annotation
* @Annotation\Target({"PROPERTY", "ANNOTATION"})
*/
class ExpressionStrategy implements StrategyInterface
{
/**
* @var string
*/
public $expression;
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'expression';
}
}
<?php /** @noinspection ALL */
namespace AppBundle\DependencyInjection\Compiler;
use AppBundle\Annotations\ChangeObservable;
use AppBundle\Annotations\Reader\ChangeObservableReader;
use AppBundle\Annotations\Reader\StrategyReader;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
/**
* Class ChangeObservationCompilerPass
*/
class ChangeObservationCompilerPass implements CompilerPassInterface
{
private const CHANGE_OBSERVABLE_TAG = 'app.change_observable_model';
/**
* @var ChangeObservableReader
*/
private $observableReader;
/**
* {@inheritdoc}
*
* @throws \Exception
*/
public function process(ContainerBuilder $container): void
{
$this->observableReader = ChangeObservableReader::createReader(
$container->get(Reader::class),
$container->get(StrategyReader::class)
);
foreach ($container->findTaggedServiceIds(self::CHANGE_OBSERVABLE_TAG) as $id => $tag) {
$this->observableReader->processClass($container->getDefinition($id)->getClass());
}
$observableReaderService = $container->getDefinition(ChangeObservableReader::class);
foreach ($this->observableReader->getObservableMap() as $className => $observableAnnotation) {
$observableDef = new Definition(ChangeObservable::class);
$observableDef->setProperty('domainRoot', $observableAnnotation->domainRoot);
$reflectionClass = new \ReflectionClass($observableAnnotation->strategy);
$strategyDef = new Definition($reflectionClass->getName());
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
$strategyDef->setProperty(
$reflectionProperty->getName(),
$observableAnnotation->strategy->{$reflectionProperty->getName()}
);
}
$observableReaderService->addMethodCall('addObservable', [$className, $observableDef]);
$observableReaderService->addMethodCall('addStrategy', [$className, $strategyDef]);
}
}
}
<?php
namespace AppBundle\EventListener\DomainProcessDispatcher;
use AppBundle\Annotations\Reader\ChangeObservableReader;
use AppBundle\DTO\ObjectChangeset;
use AppBundle\Event\EntityChangedEvent;
use AppBundle\EventDispatcher\Deferred\DeferredEvent;
use AppBundle\EventDispatcher\EventTypeGenerator;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Class EntityChangedSubscriber
*/
class EntityChangedSubscriber implements EventSubscriber
{
/**
* @var ChangeObservableReader
*/
private $observableReader;
/**
* @var EventTypeGenerator
*/
private $eventTypeGenerator;
/**
* @var EventDispatcherInterface
*/
private $dispatcher;
/**
* EntityChangedSubscriber constructor.
*
* @param ChangeObservableReader $observableReader
* @param EventTypeGenerator $eventTypeGenerator
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
ChangeObservableReader $observableReader,
EventTypeGenerator $eventTypeGenerator,
EventDispatcherInterface $dispatcher
) {
$this->observableReader = $observableReader;
$this->eventTypeGenerator = $eventTypeGenerator;
$this->dispatcher = $dispatcher;
}
/**
* @return array
*/
public function getSubscribedEvents(): array
{
return [
Events::onFlush,
];
}
/**
* @param OnFlushEventArgs $args
*
* @throws \Exception
*/
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityDeletions() as $entityDelete) {
if (false === $this->observableReader->isObservable($entityDelete)) {
continue;
}
$type = $this->eventTypeGenerator->getEntityChangedType($entityDelete, EntityChangedEvent::DELETE_ACTION);
$this->dispatcher->dispatch($type, new DeferredEvent(new EntityChangedEvent($type, $entityDelete)));
}
foreach ($uow->getScheduledEntityInsertions() as $entityInsert) {
if (false === $this->observableReader->isObservable($entityInsert)) {
continue;
}
$type = $this->eventTypeGenerator->getEntityChangedType($entityInsert, EntityChangedEvent::CREATE_ACTION);
$this->dispatcher->dispatch($type, new DeferredEvent(new EntityChangedEvent($type, $entityInsert)));
}
foreach ($uow->getScheduledCollectionUpdates() as $entityUpdate) {
if (false === $this->observableReader->isObservable($entityUpdate)) {
continue;
}
$type = $this->eventTypeGenerator->getEntityChangedType($entityUpdate, EntityChangedEvent::UPDATE_ACTION);
$this->dispatcher->dispatch(
$type,
new DeferredEvent(
new EntityChangedEvent(
$type,
$entityUpdate,
new ObjectChangeset($uow->getEntityChangeSet($entityUpdate))
)
)
);
}
}
}
<?php
namespace AppBundle\EventDispatcher;
use AppBundle\Event\EntityChangedEvent;
use Doctrine\Common\Inflector\Inflector;
/**
* Class EventTypeGenerator
*/
class EventTypeGenerator
{
/**
* @param object|string $entity
* @param string $action
*
* @return string
*/
public function getEntityChangedType($entity, string $action): string
{
$className = \is_object($entity) ? \get_class($entity) : $entity;
return sprintf(
'%s.%s.%s',
EntityChangedEvent::APP_ENTITY_CHANGED_PREFIX,
\str_replace('\\', '_', Inflector::tableize($className)),
$action
);
}
}