ikucheriavenko
3/8/2019 - 6:58 PM

ChangeObservable stack

<?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
        );
    }
}