custom/plugins/SwagPublisher/src/VersionControlSystem/Internal/ActivityLogSubscriber.php line 82

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /*
  3.  * (c) shopware AG <info@shopware.com>
  4.  * For the full copyright and license information, please view the LICENSE
  5.  * file that was distributed with this source code.
  6.  */
  7. namespace SwagPublisher\VersionControlSystem\Internal;
  8. use Shopware\Core\Content\Cms\Aggregate\CmsBlock\CmsBlockDefinition;
  9. use Shopware\Core\Content\Cms\Aggregate\CmsPageTranslation\CmsPageTranslationDefinition;
  10. use Shopware\Core\Content\Cms\Aggregate\CmsSection\CmsSectionDefinition;
  11. use Shopware\Core\Content\Cms\Aggregate\CmsSlot\CmsSlotDefinition;
  12. use Shopware\Core\Content\Cms\Aggregate\CmsSlotTranslation\CmsSlotTranslationDefinition;
  13. use Shopware\Core\Content\Cms\CmsPageDefinition;
  14. use Shopware\Core\Content\Cms\CmsPageEvents;
  15. use Shopware\Core\Defaults;
  16. use Shopware\Core\Framework\Api\Context\AdminApiSource;
  17. use Shopware\Core\Framework\Context;
  18. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  19. use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityDeletedEvent;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  26. use Shopware\Core\Framework\Struct\ArrayEntity;
  27. use SwagPublisher\Common\PreDeletedEvent;
  28. use SwagPublisher\Common\UpdateChangeContextExtension;
  29. use SwagPublisher\VersionControlSystem\Exception\NoDraftFound;
  30. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  31. class ActivityLogSubscriber implements EventSubscriberInterface
  32. {
  33.     private EntityRepositoryInterface $pageRepository;
  34.     private VersionControlCmsGateway $versionControlCmsGateway;
  35.     /**
  36.      * @var string[]
  37.      */
  38.     private array $pageIdMap = [];
  39.     public function __construct(
  40.         VersionControlCmsGateway $versionControlCmsGateway,
  41.         EntityRepositoryInterface $pageRepository
  42.     ) {
  43.         $this->versionControlCmsGateway $versionControlCmsGateway;
  44.         $this->pageRepository $pageRepository;
  45.     }
  46.     public static function getSubscribedEvents(): array
  47.     {
  48.         return [
  49.             // update & insert
  50.             CmsPageEvents::PAGE_WRITTEN_EVENT => 'logActivityOnCmsWriteEvent',
  51.             CmsSectionDefinition::ENTITY_NAME '.written' => 'logActivityOnCmsWriteEvent',
  52.             CmsPageEvents::SLOT_WRITTEN_EVENT => 'logActivityOnCmsWriteEvent',
  53.             CmsPageEvents::BLOCK_WRITTEN_EVENT => 'logActivityOnCmsWriteEvent',
  54.             CmsSlotTranslationDefinition::ENTITY_NAME '.written' => 'logActivityOnCmsWriteEvent',
  55.             CmsPageTranslationDefinition::ENTITY_NAME '.written' => 'logActivityOnCmsWriteEvent',
  56.             // delete handling
  57.             CmsSlotDefinition::ENTITY_NAME '.pre-delete' => 'rememberActivityOnCmsDeleteEvent',
  58.             CmsSlotDefinition::ENTITY_NAME '.deleted' => 'logActivityOnCmsDeleteEvent',
  59.             CmsBlockDefinition::ENTITY_NAME '.pre-delete' => 'rememberActivityOnCmsDeleteEvent',
  60.             CmsBlockDefinition::ENTITY_NAME '.deleted' => 'logActivityOnCmsDeleteEvent',
  61.             CmsSectionDefinition::ENTITY_NAME '.pre-delete' => 'rememberActivityOnCmsDeleteEvent',
  62.             CmsSectionDefinition::ENTITY_NAME '.deleted' => 'logActivityOnCmsDeleteEvent',
  63.         ];
  64.     }
  65.     public function rememberActivityOnCmsDeleteEvent(PreDeletedEvent $event): void
  66.     {
  67.         $context $event->getContext();
  68.         $writeResults $event->getWriteResults();
  69.         $this->storePageIds($writeResults$context);
  70.     }
  71.     public function logActivityOnCmsDeleteEvent(EntityDeletedEvent $event): void
  72.     {
  73.         $context $event->getContext();
  74.         $writeResults $event->getWriteResults();
  75.         $filteredWriteResults $this->filterOnlyRememberedWriteResults($writeResults);
  76.         $this->writeLogActivity($filteredWriteResults$context);
  77.     }
  78.     public function logActivityOnCmsWriteEvent(EntityWrittenEvent $event): void
  79.     {
  80.         $context $event->getContext();
  81.         $writeResults $event->getWriteResults();
  82.         $this->storePageIds($writeResults$context);
  83.         $this->writeLogActivity($writeResults$context);
  84.     }
  85.     public function containsChangedData(EntityWriteResult $writeResultContext $context): bool
  86.     {
  87.         if ($writeResult->getOperation() !== EntityWriteResult::OPERATION_UPDATE) {
  88.             return true;
  89.         }
  90.         return UpdateChangeContextExtension::extract($context)->hasChanges($writeResult);
  91.     }
  92.     /**
  93.      * @param EntityWriteResult[] $writeResults
  94.      */
  95.     private function storePageIds(array $writeResultsContext $context): void
  96.     {
  97.         foreach ($writeResults as $writeResult) {
  98.             $affectedEntity WriteResultExtractor::extractAffectedEntity($writeResult);
  99.             $key $this->getIdMapKey($affectedEntity);
  100.             if (!($cmsPageId $this->fetchCmsPageIdByAffectedEntity($affectedEntity$context))) {
  101.                 continue;
  102.             }
  103.             $this->pageIdMap[$key] = $cmsPageId;
  104.         }
  105.     }
  106.     /**
  107.      * @param EntityWriteResult[] $writeResults
  108.      */
  109.     private function writeLogActivity(array $writeResultsContext $context): void
  110.     {
  111.         $source $context->getSource();
  112.         if (!$source instanceof AdminApiSource) {
  113.             return;
  114.         }
  115.         try {
  116.             $draftVersion $this->determineDraftVersion($context);
  117.         } catch (NoDraftFound $exception) {
  118.             return;
  119.         }
  120.         $pageIdToDetailMap $this->extractDetails($writeResults$context);
  121.         $pageIdToActivityMap $this->loadActivities($pageIdToDetailMap$draftVersion$context);
  122.         $this->writeActivityDetails($pageIdToDetailMap$pageIdToActivityMap$source$draftVersion$context);
  123.         $this->pageIdMap = [];
  124.     }
  125.     private function createNewActivity(string $pageId, ?string $draftVersion, array $detailsContext $context): void
  126.     {
  127.         $context->scope(Context::SYSTEM_SCOPE, function (Context $systemContext) use ($details$pageId$draftVersion): void {
  128.             $source $systemContext->getSource();
  129.             if (!$source instanceof AdminApiSource) {
  130.                 return;
  131.             }
  132.             $userId $source->getUserId();
  133.             $this->versionControlCmsGateway->createActivities([[
  134.                 'draftVersion' => $draftVersion,
  135.                 'details' => $details,
  136.                 'pageId' => $pageId,
  137.                 'userId' => $userId,
  138.                 'name' => $this->fetchDraftName($pageId$draftVersion$systemContext),
  139.             ]], $systemContext->createWithVersionId(Defaults::LIVE_VERSION));
  140.         });
  141.     }
  142.     private function updateExistingActivity(ArrayEntity $activity, array $detailsContext $context): void
  143.     {
  144.         $context->scope(Context::SYSTEM_SCOPE, function (Context $systemContext) use ($activity$details): void {
  145.             $this->versionControlCmsGateway->updateActivities([[
  146.                 'id' => $activity->getId(),
  147.                 'details' => $this->mergeActivityDetails($details$activity['details']),
  148.             ]], $systemContext->createWithVersionId(Defaults::LIVE_VERSION));
  149.         });
  150.     }
  151.     private function extractDetailsFromWriteResults(EntityWriteResult $writeResultContext $context): ?array
  152.     {
  153.         $payload $writeResult->getPayload();
  154.         if ($this->isAllowedToSkip($writeResult$context)) {
  155.             return null;
  156.         }
  157.         return [
  158.             'id' => $writeResult->getPrimaryKey(),
  159.             'name' => $payload['name'] ?? null,
  160.             'operation' => $writeResult->getOperation(),
  161.             'entityName' => $writeResult->getEntityName(),
  162.             'timestamp' => (new \DateTime())->format(\DateTime::ATOM),
  163.         ];
  164.     }
  165.     private function isAllowedToSkip(EntityWriteResult $writeResultContext $context): bool
  166.     {
  167.         return $this->containsTranslationInsertion($writeResult) || !$this->containsChangedData($writeResult$context);
  168.     }
  169.     private function fetchCmsPageIdByAffectedEntity(AffectedEntity $affectedEntityContext $context): ?string
  170.     {
  171.         $entityName $affectedEntity->getName();
  172.         $id $affectedEntity->getId();
  173.         switch ($entityName) {
  174.             case CmsPageDefinition::ENTITY_NAME:
  175.                 $criteria CriteriaFactory::withIds($id);
  176.                 break;
  177.             case CmsBlockDefinition::ENTITY_NAME:
  178.                 $criteria CriteriaFactory::forPageByBlockId($id);
  179.                 break;
  180.             case CmsSlotDefinition::ENTITY_NAME:
  181.                 $criteria CriteriaFactory::forPageBySlotId($id);
  182.                 break;
  183.             case CmsSectionDefinition::ENTITY_NAME:
  184.                 $criteria CriteriaFactory::forPageBySectionId($id);
  185.                 break;
  186.             default:
  187.                 return null;
  188.         }
  189.         return $this->pageRepository
  190.             ->search($criteria$context)
  191.             ->first()
  192.             ->getId();
  193.     }
  194.     private function mergeActivityDetails(array $newDetails, ?array $activityDetails): array
  195.     {
  196.         if (!$activityDetails) {
  197.             return $newDetails;
  198.         }
  199.         return \array_merge(\array_reverse($newDetails), $activityDetails);
  200.     }
  201.     private function fetchDraftActivity(string $pageId, ?string $draftVersionContext $context): ?ArrayEntity
  202.     {
  203.         $criteria = new Criteria();
  204.         $criteria->setLimit(1);
  205.         $criteria->addFilter(
  206.             new MultiFilter(MultiFilter::CONNECTION_AND, [
  207.                 new EqualsFilter('pageId'$pageId),
  208.                 new EqualsFilter('draftVersion'$draftVersion),
  209.             ])
  210.         );
  211.         $criteria->addSorting(new FieldSorting('createdAt'FieldSorting::DESCENDING));
  212.         return $this->versionControlCmsGateway->searchActivities($criteria$context->createWithVersionId(Defaults::LIVE_VERSION))
  213.             ->first();
  214.     }
  215.     private function containsTranslationInsertion(EntityWriteResult $writeResult): bool
  216.     {
  217.         return $writeResult->getOperation() === EntityWriteResult::OPERATION_INSERT
  218.             && WriteResultExtractor::isTranslation($writeResult);
  219.     }
  220.     private function determineDraftVersion(Context $context): ?string
  221.     {
  222.         if ($context->getVersionId() === Defaults::LIVE_VERSION) {
  223.             return null;
  224.         }
  225.         $draftVersion $context->getVersionId();
  226.         $drafts $this->versionControlCmsGateway
  227.             ->searchDrafts(
  228.                 CriteriaFactory::forDraftWithVersion($draftVersion),
  229.                 $context->createWithVersionId(Defaults::LIVE_VERSION)
  230.             );
  231.         if (!$drafts->count()) {
  232.             throw new NoDraftFound();
  233.         }
  234.         return $draftVersion;
  235.     }
  236.     /**
  237.      * @param EntityWriteResult[] $writeResults
  238.      */
  239.     private function extractDetails(array $writeResultsContext $context): array
  240.     {
  241.         $pageIdToDetailMap = [];
  242.         foreach ($writeResults as $writeResult) {
  243.             $affectedEntity WriteResultExtractor::extractAffectedEntity($writeResult);
  244.             $cmsPageId $this->pageIdMap[$this->getIdMapKey($affectedEntity)];
  245.             $details $this->extractDetailsFromWriteResults($writeResult$context);
  246.             if (!$details) {
  247.                 continue;
  248.             }
  249.             $this->updateDetailsByAffectedEntity($details$affectedEntity);
  250.             if (!isset($pageIdToDetailMap[$cmsPageId])) {
  251.                 $pageIdToDetailMap[$cmsPageId] = [];
  252.             }
  253.             $pageIdToDetailMap[$cmsPageId][] = $details;
  254.         }
  255.         return $pageIdToDetailMap;
  256.     }
  257.     private function updateDetailsByAffectedEntity(array &$detailsAffectedEntity $affectedEntity): void
  258.     {
  259.         if ($details['id'] === $affectedEntity->getId()) {
  260.             return;
  261.         }
  262.         $details['id'] = $affectedEntity->getId();
  263.         $details['entityName'] = $affectedEntity->getName();
  264.     }
  265.     private function loadActivities(array $pageIdToDetailMap, ?string $draftVersionContext $context): array
  266.     {
  267.         $pageIdToActivityMap = [];
  268.         foreach ($pageIdToDetailMap as $cmsPageId => $null) {
  269.             $pageIdToActivityMap[$cmsPageId] = $this
  270.                 ->fetchDraftActivity($cmsPageId$draftVersion$context);
  271.         }
  272.         return $pageIdToActivityMap;
  273.     }
  274.     private function writeActivityDetails(
  275.         array $pageIdToDetailMap,
  276.         array $pageIdToActivityMap,
  277.         AdminApiSource $source,
  278.         ?string $draftVersion,
  279.         Context $context
  280.     ): void {
  281.         foreach ($pageIdToDetailMap as $cmsPageId => $details) {
  282.             $activity $pageIdToActivityMap[$cmsPageId];
  283.             if (!$activity || $activity['userId'] !== $source->getUserId()) {
  284.                 $this->createNewActivity($cmsPageId$draftVersion$details$context);
  285.                 continue;
  286.             }
  287.             $this->updateExistingActivity($activity$details$context);
  288.         }
  289.     }
  290.     private function getIdMapKey(AffectedEntity $affectedEntity): string
  291.     {
  292.         return $affectedEntity->getName() . $affectedEntity->getId();
  293.     }
  294.     /**
  295.      * @param EntityWriteResult[] $writeResults
  296.      */
  297.     private function filterOnlyRememberedWriteResults(array $writeResults): array
  298.     {
  299.         $filteredWriteResults = [];
  300.         foreach ($writeResults as $writeResult) {
  301.             $affectedEntity WriteResultExtractor::extractAffectedEntity($writeResult);
  302.             $key $this->getIdMapKey($affectedEntity);
  303.             if (!isset($this->pageIdMap[$key])) {
  304.                 continue;
  305.             }
  306.             $filteredWriteResults[] = $writeResult;
  307.         }
  308.         return $filteredWriteResults;
  309.     }
  310.     private function fetchDraftName(string $pageId, ?string $draftVersionContext $context): string
  311.     {
  312.         if (!$draftVersion) {
  313.             return $this->fetchOriginalPageName($pageId$context);
  314.         }
  315.         $criteria CriteriaFactory::forActivityWithPageAndVersion($pageId$draftVersion);
  316.         $activity $this->versionControlCmsGateway
  317.             ->searchActivities($criteria$context)
  318.             ->first();
  319.         if (!$activity) {
  320.             return $this->fetchOriginalPageName($pageId$context);
  321.         }
  322.         return $activity->get('name');
  323.     }
  324.     private function fetchOriginalPageName(string $pageIdContext $context): string
  325.     {
  326.         return $this->versionControlCmsGateway
  327.             ->fetchInheritedDraftData($pageId$context)['name'];
  328.     }
  329. }