Skip to content

Commit

Permalink
Merge pull request #5336 from neos/feature/5335-command-hooks
Browse files Browse the repository at this point in the history
FEATURE: Command Hooks
  • Loading branch information
bwaidelich authored Nov 12, 2024
2 parents 5f230e3 + 6256da9 commit 83575f3
Show file tree
Hide file tree
Showing 10 changed files with 258 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\CommandHandler;

use Neos\ContentRepository\Core\ContentRepository;

/**
* Contract for a hook that is invoked just before any command is processed via {@see ContentRepository::handle()}
*
* A command hook can be used to replace/alter an incoming command before it is being passed to the corresponding {@see CommandHandlerInterface}.
* This can be used to change or enrich the payload of the command.
* A command hook can also be used to intercept commands based on their type or payload but this is not the intended use case because it can lead to a degraded user experience
*
* @api
*/
interface CommandHookInterface
{
/**
* @param CommandInterface $command The command that is about to be handled
* @return CommandInterface This hook must return a command instance. It can be the unaltered incoming $command or a new instance
*/
public function onBeforeHandle(CommandInterface $command): CommandInterface;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\CommandHandler;

/**
* Collection of {@see CommandHookInterface} instances, functioning as a delegating command hook implementation
*
* @implements \IteratorAggregate<CommandHookInterface>
* @api
*/
final readonly class CommandHooks implements CommandHookInterface, \IteratorAggregate, \Countable
{
/**
* @var array<CommandHookInterface>
*/
private array $commandHooks;

private function __construct(
CommandHookInterface ...$commandHooks
) {
$this->commandHooks = $commandHooks;
}

/**
* @param array<CommandHookInterface> $commandHooks
*/
public static function fromArray(array $commandHooks): self
{
return new self(...$commandHooks);
}

public static function none(): self
{
return new self();
}

public function getIterator(): \Traversable
{
yield from $this->commandHooks;
}

public function count(): int
{
return count($this->commandHooks);
}

public function onBeforeHandle(CommandInterface $command): CommandInterface
{
foreach ($this->commandHooks as $commandHook) {
$command = $commandHook->onBeforeHandle($command);
}
return $command;
}
}
7 changes: 5 additions & 2 deletions Neos.ContentRepository.Core/Classes/ContentRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Neos\ContentRepository\Core;

use Neos\ContentRepository\Core\CommandHandler\CommandBus;
use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface;
use Neos\ContentRepository\Core\CommandHandler\CommandInterface;
use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface;
use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph;
Expand Down Expand Up @@ -83,7 +84,8 @@ public function __construct(
private readonly ContentDimensionSourceInterface $contentDimensionSource,
private readonly UserIdProviderInterface $userIdProvider,
private readonly ClockInterface $clock,
private readonly ContentGraphReadModelInterface $contentGraphReadModel
private readonly ContentGraphReadModelInterface $contentGraphReadModel,
private readonly CommandHookInterface $commandHook,
) {
}

Expand All @@ -94,6 +96,7 @@ public function __construct(
*/
public function handle(CommandInterface $command): void
{
$command = $this->commandHook->onBeforeHandle($command);
// the commands only calculate which events they want to have published, but do not do the
// publishing themselves
$eventsToPublishOrGenerator = $this->commandBus->handle($command);
Expand Down Expand Up @@ -147,7 +150,7 @@ public function catchUpProjection(string $projectionClassName, CatchUpOptions $o
$projection = $this->projectionsAndCatchUpHooks->projections->get($projectionClassName);

$catchUpHookFactory = $this->projectionsAndCatchUpHooks->getCatchUpHookFactoryForProjection($projection);
$catchUpHook = $catchUpHookFactory?->build(new CatchUpHookFactoryDependencies(
$catchUpHook = $catchUpHookFactory?->build(CatchUpHookFactoryDependencies::create(
$this->id,
$projection->getState(),
$this->nodeTypeManager,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Factory;

use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface;

/**
* @api for implementers of custom {@see CommandHookInterface}s
*/
interface CommandHookFactoryInterface
{
public function build(
CommandHooksFactoryDependencies $commandHooksFactoryDependencies,
): CommandHookInterface;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Factory;

use Neos\ContentRepository\Core\CommandHandler\CommandHooks;

/**
* @internal
*/
final readonly class CommandHooksFactory
{
/**
* @var array<CommandHookFactoryInterface>
*/
private array $commandHookFactories;

public function __construct(
CommandHookFactoryInterface ...$commandHookFactories,
) {
$this->commandHookFactories = $commandHookFactories;
}

public function build(
CommandHooksFactoryDependencies $commandHooksFactoryDependencies,
): CommandHooks {
return CommandHooks::fromArray(array_map(
static fn (CommandHookFactoryInterface $factory) => $factory->build($commandHooksFactoryDependencies),
$this->commandHookFactories
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the Neos.ContentRepository package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\ContentRepository\Core\Factory;

use Neos\ContentRepository\Core\CommandHandler\CommandHookInterface;
use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface;
use Neos\ContentRepository\Core\DimensionSpace\InterDimensionalVariationGraph;
use Neos\ContentRepository\Core\NodeType\NodeTypeManager;
use Neos\ContentRepository\Core\Projection\ContentGraph\ContentGraphReadModelInterface;
use Neos\ContentRepository\Core\SharedModel\ContentRepository\ContentRepositoryId;

/**
* @api for implementers of custom {@see CommandHookInterface}s
*/
final readonly class CommandHooksFactoryDependencies
{
private function __construct(
public ContentRepositoryId $contentRepositoryId,
public ContentGraphReadModelInterface $contentGraphReadModel,
public NodeTypeManager $nodeTypeManager,
public ContentDimensionSourceInterface $contentDimensionSource,
public InterDimensionalVariationGraph $variationGraph
) {
}

/**
* @internal
*/
public static function create(
ContentRepositoryId $contentRepositoryId,
ContentGraphReadModelInterface $contentGraphReadModel,
NodeTypeManager $nodeTypeManager,
ContentDimensionSourceInterface $contentDimensionSource,
InterDimensionalVariationGraph $variationGraph
): self {
return new self(
$contentRepositoryId,
$contentGraphReadModel,
$nodeTypeManager,
$contentDimensionSource,
$variationGraph
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Neos\ContentRepository\Core\Factory;

use Neos\ContentRepository\Core\CommandHandler\CommandBus;
use Neos\ContentRepository\Core\CommandHandler\CommandHooks;
use Neos\ContentRepository\Core\CommandHandler\CommandSimulatorFactory;
use Neos\ContentRepository\Core\CommandHandler\CommandHandlingDependencies;
use Neos\ContentRepository\Core\ContentRepository;
Expand Down Expand Up @@ -55,6 +56,7 @@ public function __construct(
ProjectionsAndCatchUpHooksFactory $projectionsAndCatchUpHooksFactory,
private readonly UserIdProviderInterface $userIdProvider,
private readonly ClockInterface $clock,
private readonly CommandHooksFactory $commandHooksFactory,
) {
$contentDimensionZookeeper = new ContentDimensionZookeeper($contentDimensionSource);
$interDimensionalVariationGraph = new InterDimensionalVariationGraph(
Expand Down Expand Up @@ -133,7 +135,13 @@ public function getOrBuild(): ContentRepository
$this->projectionFactoryDependencies->eventNormalizer,
)
);

$commandHooks = $this->commandHooksFactory->build(CommandHooksFactoryDependencies::create(
$this->contentRepositoryId,
$this->projectionsAndCatchUpHooks->contentGraphProjection->getState(),
$this->projectionFactoryDependencies->nodeTypeManager,
$this->projectionFactoryDependencies->contentDimensionSource,
$this->projectionFactoryDependencies->interDimensionalVariationGraph,
));
$this->contentRepository = new ContentRepository(
$this->contentRepositoryId,
$publicCommandBus,
Expand All @@ -146,7 +154,8 @@ public function getOrBuild(): ContentRepository
$this->projectionFactoryDependencies->contentDimensionSource,
$this->userIdProvider,
$this->clock,
$contentGraphReadModel
$contentGraphReadModel,
$commandHooks,
);
$this->isBuilding = false;
return $this->contentRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,34 @@
* @param ContentRepositoryId $contentRepositoryId the content repository the catchup was registered in
* @param ProjectionStateInterface&T $projectionState the state of the projection the catchup was registered to (Its only safe to access this projections state)
*/
public function __construct(
private function __construct(
public ContentRepositoryId $contentRepositoryId,
public ProjectionStateInterface $projectionState,
public NodeTypeManager $nodeTypeManager,
public ContentDimensionSourceInterface $contentDimensionSource,
public InterDimensionalVariationGraph $variationGraph
) {
}

/**
* @template U of ProjectionStateInterface
* @param ProjectionStateInterface&U $projectionState
* @return CatchUpHookFactoryDependencies<U>
* @internal
*/
public static function create(
ContentRepositoryId $contentRepositoryId,
ProjectionStateInterface $projectionState,
NodeTypeManager $nodeTypeManager,
ContentDimensionSourceInterface $contentDimensionSource,
InterDimensionalVariationGraph $variationGraph
): self {
return new self(
$contentRepositoryId,
$projectionState,
$nodeTypeManager,
$contentDimensionSource,
$variationGraph
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use Neos\ContentRepository\Core\ContentRepository;
use Neos\ContentRepository\Core\Dimension\ContentDimensionSourceInterface;
use Neos\ContentRepository\Core\Factory\CommandHookFactoryInterface;
use Neos\ContentRepository\Core\Factory\CommandHooksFactory;
use Neos\ContentRepository\Core\Factory\ContentRepositoryFactory;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface;
use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceInterface;
Expand Down Expand Up @@ -176,7 +178,8 @@ private function buildFactory(ContentRepositoryId $contentRepositoryId): Content
$this->buildPropertySerializer($contentRepositoryId, $contentRepositorySettings),
$this->buildProjectionsFactory($contentRepositoryId, $contentRepositorySettings),
$this->buildUserIdProvider($contentRepositoryId, $contentRepositorySettings),
$clock
$clock,
$this->buildCommandHooksFactory($contentRepositoryId, $contentRepositorySettings),
);
} catch (\Exception $exception) {
throw InvalidConfigurationException::fromException($contentRepositoryId, $exception);
Expand Down Expand Up @@ -275,6 +278,28 @@ private function buildProjectionsFactory(ContentRepositoryId $contentRepositoryI
return $projectionsAndCatchUpHooksFactory;
}

/** @param array<string, mixed> $contentRepositorySettings */
private function buildCommandHooksFactory(ContentRepositoryId $contentRepositoryId, array $contentRepositorySettings): CommandHooksFactory
{
$commandHooksSettings = $contentRepositorySettings['commandHooks'] ?? [];
if (!is_array($commandHooksSettings)) {
throw InvalidConfigurationException::fromMessage('Content repository "%s" does not have the "commandHooks" configured properly. Expected array, got %s.', $contentRepositoryId->value, get_debug_type($commandHooksSettings));
}
$commandHookFactories = [];
foreach ((new PositionalArraySorter($commandHooksSettings))->toArray() as $name => $commandHookSettings) {
// Allow to unset/disable command hooks
if ($commandHookSettings === null) {
continue;
}
$commandHookFactory = $this->objectManager->get($commandHookSettings['factoryObjectName']);
if (!$commandHookFactory instanceof CommandHookFactoryInterface) {
throw InvalidConfigurationException::fromMessage('Factory object name for command hook "%s" (content repository "%s") is not an instance of %s but %s.', $name, $contentRepositoryId->value, CommandHookFactoryInterface::class, get_debug_type($commandHookFactory));
}
$commandHookFactories[] = $commandHookFactory;
}
return new CommandHooksFactory(...$commandHookFactories);
}

/**
* @param ProjectionFactoryInterface<ProjectionInterface<ProjectionStateInterface>> $projectionFactory
*/
Expand Down
6 changes: 6 additions & 0 deletions Neos.ContentRepositoryRegistry/Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,9 @@ Neos:
# factoryObjectName: My\Package\Projection\SomeProjectionFactory
# options: {}
# catchUpHooks: {}

# Command Hooks
#
# commandHooks:
# 'My.Package:SomeCommandHook': # just a name
# factoryObjectName: My\Package\CommandHook\SomeCommandHookFactory

0 comments on commit 83575f3

Please sign in to comment.