From 23ca15481f1e91eab9ebdbd6c0234fd17c0e372e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Sat, 3 Jun 2023 12:25:32 +0200 Subject: [PATCH 01/43] WIP: TASK: Separate template creation from apply --- Classes/Domain/CaughtExceptions.php | 31 ++ Classes/{Service => Domain}/EelException.php | 2 +- Classes/Domain/RootTemplate.php | 51 ++++ Classes/Domain/Template.php | 72 +++++ Classes/Domain/TemplateBuilder.php | 208 +++++++++++++ Classes/Domain/TemplateFactory.php | 71 +++++ Classes/Domain/Templates.php | 46 +++ .../ContentRepositoryTemplateHandler.php | 54 ++++ .../TemplateNodeCreationHandler.php | 36 +-- ...latingDocumentTitleNodeCreationHandler.php | 3 + Classes/Service/EelEvaluationService.php | 52 ---- Classes/Template.php | 277 ------------------ Configuration/Settings.yaml | 1 - 13 files changed, 548 insertions(+), 356 deletions(-) create mode 100644 Classes/Domain/CaughtExceptions.php rename Classes/{Service => Domain}/EelException.php (54%) create mode 100644 Classes/Domain/RootTemplate.php create mode 100644 Classes/Domain/Template.php create mode 100644 Classes/Domain/TemplateBuilder.php create mode 100644 Classes/Domain/TemplateFactory.php create mode 100644 Classes/Domain/Templates.php create mode 100644 Classes/Infrastructure/ContentRepositoryTemplateHandler.php delete mode 100644 Classes/Service/EelEvaluationService.php delete mode 100644 Classes/Template.php diff --git a/Classes/Domain/CaughtExceptions.php b/Classes/Domain/CaughtExceptions.php new file mode 100644 index 0000000..7bfd576 --- /dev/null +++ b/Classes/Domain/CaughtExceptions.php @@ -0,0 +1,31 @@ + */ + private array $exceptions = []; + + private function __construct() + { + } + + public static function create(): self + { + return new self(); + } + + public function add(\Exception $exception): void + { + $this->exceptions[] = $exception; + } + + public function getIterator() + { + yield from $this->exceptions; + } +} diff --git a/Classes/Service/EelException.php b/Classes/Domain/EelException.php similarity index 54% rename from Classes/Service/EelException.php rename to Classes/Domain/EelException.php index 5c36816..9a66ed7 100644 --- a/Classes/Service/EelException.php +++ b/Classes/Domain/EelException.php @@ -1,6 +1,6 @@ + */ + private array $properties; + + private Templates $childNodes; + + /** + * @param array $properties + */ + public function __construct(array $properties, Templates $childNodes) + { + $this->properties = $properties; + $this->childNodes = $childNodes; + } + + /** + * @return array + */ + public function getProperties(): array + { + return $this->properties; + } + + public function getChildNodes(): Templates + { + return $this->childNodes; + } + + public function jsonSerialize() + { + return [ + 'properties' => $this->properties, + 'childNodes' => $this->childNodes + ]; + } +} diff --git a/Classes/Domain/Template.php b/Classes/Domain/Template.php new file mode 100644 index 0000000..9166ce5 --- /dev/null +++ b/Classes/Domain/Template.php @@ -0,0 +1,72 @@ + + */ + private array $properties; + + private Templates $childNodes; + + /** + * @param array $properties + */ + public function __construct(?NodeTypeName $type, ?NodeName $name, array $properties, Templates $childNodes) + { + $this->type = $type; + $this->name = $name; + $this->properties = $properties; + $this->childNodes = $childNodes; + } + + public static function empty(): self + { + return new self(null, null, [], new Templates()); + } + + public function getType(): ?NodeTypeName + { + return $this->type; + } + + public function getName(): ?NodeName + { + return $this->name; + } + + /** + * @return array + */ + public function getProperties(): array + { + return $this->properties; + } + + public function getChildNodes(): Templates + { + return $this->childNodes; + } + + public function jsonSerialize() + { + return [ + 'type' => $this->type, + 'name' => $this->name, + 'properties' => $this->properties, + 'childNodes' => $this->childNodes + ]; + } +} diff --git a/Classes/Domain/TemplateBuilder.php b/Classes/Domain/TemplateBuilder.php new file mode 100644 index 0000000..52acc57 --- /dev/null +++ b/Classes/Domain/TemplateBuilder.php @@ -0,0 +1,208 @@ +configuration = $configuration; + $this->evaluationContext = $evaluationContext; + $this->configurationValueProcessor = $configurationValueProcessor; + $this->caughtExceptions = $caughtExceptions; + } + + /** + * Creates a template tree based on the given configuration. + */ + public static function createTemplate( + array $configuration, + array $evaluationContext, + \Closure $configurationValueProcessor, + CaughtExceptions $caughtExceptions + ): RootTemplate { + $builder = new self($configuration, $evaluationContext, $configurationValueProcessor, $caughtExceptions); + $builder->validateRootLevelTemplateConfigurationKeys(); + $templates = self::createTemplatesFromBuilder($builder); + /** @var Template[] $templateList */ + $templateList = iterator_to_array($templates, false); + assert(\count($templateList) === 1); + return new RootTemplate( + $templateList[0]->getProperties(), + $templateList[0]->getChildNodes(), + ); + } + + private static function createTemplatesFromBuilder(self $builder): Templates + { + $builder = $builder->mergeContextAndWithContextConfiguration(); + + if (!$builder->processConfiguration('when', true)) { + return Templates::empty(); + } + + if (!$builder->hasConfiguration('withItems')) { + return new Templates($builder->toTemplate()); + } + + $items = $builder->processConfiguration('withItems', []); + if (!is_iterable($items)) { + throw new \RuntimeException(sprintf('With items is not iterable. Configuration %s evaluated to type %s', json_encode($builder->configuration['withItems']), gettype($items))); + } + + $templates = Templates::empty(); + foreach ($items as $itemKey => $itemValue) { + $evaluationContextWithItem = $builder->evaluationContext; + $evaluationContextWithItem['item'] = $itemValue; + $evaluationContextWithItem['key'] = $itemKey; + + $itemBuilder = new self( + $builder->configuration, + $evaluationContextWithItem, + $builder->configurationValueProcessor, + $builder->caughtExceptions + ); + + $templates = $templates->withAdded($itemBuilder->toTemplate()); + } + return $templates; + } + + private function toTemplate(): Template + { + $type = $this->processConfiguration('type', null); + $name = $this->processConfiguration('name', null); + return new Template( + $type ? NodeTypeName::fromString($type) : null, + $name ? NodeName::fromString($name) : null, + $this->processProperties(), + $this->expandChildNodes() + ); + } + + private function processProperties(): array + { + $processedProperties = []; + foreach ($this->configuration['properties'] ?? [] as $propertyName => $value) { + if (!is_scalar($value)) { + throw new \InvalidArgumentException(sprintf('Template configuration properties can only hold int|float|string|bool. Property "%s" has type "%s"', $propertyName, gettype($value)), 1685725310730); + } + $processedProperties[$propertyName] = ($this->configurationValueProcessor)($value, $this->evaluationContext); + } + return $processedProperties; + } + + private function expandChildNodes(): Templates + { + if (!isset($this->configuration['childNodes'])) { + return Templates::empty(); + } + $templates = Templates::empty(); + foreach ($this->configuration['childNodes'] as $childNodeConfiguration) { + $builder = new self( + $childNodeConfiguration, + $this->evaluationContext, + $this->configurationValueProcessor, + $this->caughtExceptions + ); + $builder->validateNestedLevelTemplateConfigurationKeys(); + $templates = $templates->merge(self::createTemplatesFromBuilder($builder)); + } + return $templates; + } + + /** @return mixed */ + private function processConfiguration(string $configurationKey, $fallback) + { + if (!$this->hasConfiguration($configurationKey)) { + return $fallback; + } + return ($this->configurationValueProcessor)($this->configuration[$configurationKey], $this->evaluationContext); + } + + private function hasConfiguration(string $configurationKey): bool + { + return array_key_exists($configurationKey, $this->configuration); + } + + /** + * Merge `withContext` onto the current $evaluationContext, evaluating EEL if necessary and return a new Builder + * + * The option `withContext` takes an array of items whose value can be any yaml/php type + * and might also contain eel expressions + * + * ```yaml + * withContext: + * someText: '

foo

' + * processedData: "${String.trim(data.bla)}" + * booleanType: true + * arrayType: ["value"] + * ``` + * + * scopes and order of evaluation: + * + * - inside `withContext` the "upper" context may be accessed in eel expressions, + * but sibling context values are not available + * + * - `withContext` is evaluated before `when` and `withItems` so you can access computed values, + * that means the context `item` from `withItems` will not be available yet + * + */ + private function mergeContextAndWithContextConfiguration(): self + { + if (($this->configuration['withContext'] ?? []) === []) { + return $this; + } + $withContext = []; + foreach ($this->configuration['withContext'] as $key => $value) { + $withContext[$key] = ($this->configurationValueProcessor)($value, $this->evaluationContext); + } + return new self( + $this->configuration, + array_merge($this->evaluationContext, $withContext), + $this->configurationValueProcessor, + $this->caughtExceptions + ); + } + + private function validateNestedLevelTemplateConfigurationKeys(): void + { + foreach (array_keys($this->configuration) as $key) { + if (!in_array($key, ['type', 'name', 'properties', 'childNodes', 'when', 'withItems', 'withContext'], true)) { + throw new \InvalidArgumentException(sprintf('Template configuration has illegal key "%s', $key)); + } + } + } + + private function validateRootLevelTemplateConfigurationKeys(): void + { + foreach (array_keys($this->configuration) as $key) { + if (!in_array($key, ['properties', 'childNodes', 'when', 'withContext'], true)) { + throw new \InvalidArgumentException(sprintf('Root template configuration has illegal key "%s', $key)); + } + } + } +} diff --git a/Classes/Domain/TemplateFactory.php b/Classes/Domain/TemplateFactory.php new file mode 100644 index 0000000..3c5d34e --- /dev/null +++ b/Classes/Domain/TemplateFactory.php @@ -0,0 +1,71 @@ + $this->preprocessConfigurationValue($value, $evaluationContext), + $caughtEvaluationExceptions + ); + } + + /** @return mixed */ + private function preprocessConfigurationValue($rawConfigurationValue, array $evaluationContext) + { + if (!is_string($rawConfigurationValue)) { + return $rawConfigurationValue; + } + if (strpos($rawConfigurationValue, '${') !== 0) { + return $rawConfigurationValue; + } + return $this->evaluateEelExpression($rawConfigurationValue, $evaluationContext); + } + + /** + * Evaluate an Eel expression. + * + * @param $contextVariables array additional context for eel expressions + * @return mixed The result of the evaluated Eel expression + * @throws EelException + */ + private function evaluateEelExpression(string $expression, array $contextVariables) + { + if ($this->defaultContextVariables === null) { + $this->defaultContextVariables = EelUtility::getDefaultContextVariables($this->defaultContextConfiguration); + } + $contextVariables = array_merge($this->defaultContextVariables, $contextVariables); + try { + return EelUtility::evaluateEelExpression($expression, $this->eelEvaluator, $contextVariables); + } catch (ParserException $parserException) { + throw new EelException('EEL Expression in NodeType template could not be parsed.', 1684788574212, $parserException); + } catch (\Exception $exception) { + throw new EelException(sprintf('EEL Expression "%s" in NodeType template caused an error.', $expression), 1684761760723, $exception); + } + } +} diff --git a/Classes/Domain/Templates.php b/Classes/Domain/Templates.php new file mode 100644 index 0000000..0897b8d --- /dev/null +++ b/Classes/Domain/Templates.php @@ -0,0 +1,46 @@ + */ + private array $items; + + public function __construct( + Template ...$items + ) { + $this->items = $items; + } + + public static function empty(): self + { + return new self(); + } + + /** + * @return \Traversable|Template[] + */ + public function getIterator(): \Traversable + { + yield from $this->items; + } + + public function withAdded(Template $template): self + { + return new self(...$this->items, ...[$template]); + } + + public function merge(Templates $other): self + { + return new self(...$this->items, ...$other->items); + } + + public function jsonSerialize() + { + return $this->items; + } +} diff --git a/Classes/Infrastructure/ContentRepositoryTemplateHandler.php b/Classes/Infrastructure/ContentRepositoryTemplateHandler.php new file mode 100644 index 0000000..a1f14fe --- /dev/null +++ b/Classes/Infrastructure/ContentRepositoryTemplateHandler.php @@ -0,0 +1,54 @@ +getProperties() as $key => $value) { + $node->setProperty($key, $value); + } + $this->applyTemplateRecursively($template->getChildNodes(), $node); + } + + private function applyTemplateRecursively(Templates $templates, NodeInterface $parentNode): void + { + foreach ($templates as $template) { + if ($template->getName() && $parentNode->getNodeType()->hasAutoCreatedChildNode($template->getName())) { + $node = $parentNode->getNode($template->getName()->__toString()); + foreach ($template->getProperties() as $key => $value) { + $node->setProperty($key, $value); + } + } else { + $node = $this->nodeOperations->create( + $parentNode, + [ + 'nodeType' => $template->getType()->getValue(), + 'nodeName' => $template->getName() ? $template->getName()->__toString() : null + ], + 'into' + ); + foreach ($template->getProperties() as $key => $value) { + $node->setProperty($key, $value); + } + } + $this->applyTemplateRecursively($template->getChildNodes(), $node); + } + } +} diff --git a/Classes/NodeCreationHandler/TemplateNodeCreationHandler.php b/Classes/NodeCreationHandler/TemplateNodeCreationHandler.php index 584ccfb..44b637e 100644 --- a/Classes/NodeCreationHandler/TemplateNodeCreationHandler.php +++ b/Classes/NodeCreationHandler/TemplateNodeCreationHandler.php @@ -2,12 +2,13 @@ namespace Flowpack\NodeTemplates\NodeCreationHandler; -use Flowpack\NodeTemplates\Service\EelException; -use Flowpack\NodeTemplates\Template; +use Flowpack\NodeTemplates\Domain\CaughtExceptions; +use Flowpack\NodeTemplates\Domain\EelException; +use Flowpack\NodeTemplates\Domain\TemplateFactory; +use Flowpack\NodeTemplates\Infrastructure\ContentRepositoryTemplateHandler; use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Exception\NodeConstraintException; use Neos\Flow\Annotations as Flow; -use Neos\Flow\Property\PropertyMapper; use Neos\Neos\Ui\Domain\Model\Feedback\Messages\Error; use Neos\Neos\Ui\Domain\Model\FeedbackCollection; use Neos\Neos\Ui\NodeCreationHandler\NodeCreationHandlerInterface; @@ -15,16 +16,16 @@ class TemplateNodeCreationHandler implements NodeCreationHandlerInterface { /** - * @var PropertyMapper + * @var TemplateFactory * @Flow\Inject */ - protected $propertyMapper; + protected $templateFactory; /** - * @var integer - * @Flow\InjectConfiguration(path="nodeCreationDepth") + * @var ContentRepositoryTemplateHandler + * @Flow\Inject */ - protected $nodeCreationDepth; + protected $templateContentRepositoryApplier; /** * @var FeedbackCollection @@ -46,29 +47,14 @@ public function handle(NodeInterface $node, array $data): void return; } - $propertyMappingConfiguration = $this->propertyMapper->buildPropertyMappingConfiguration(); - - $subPropertyMappingConfiguration = $propertyMappingConfiguration; - for ($i = 0; $i < $this->nodeCreationDepth; $i++) { - $subPropertyMappingConfiguration = $subPropertyMappingConfiguration - ->forProperty('childNodes.*') - ->allowAllProperties(); - } - - /** @var Template $template */ - $template = $this->propertyMapper->convert( - $templateConfiguration, - Template::class, - $propertyMappingConfiguration - ); - $context = [ 'data' => $data, 'triggeringNode' => $node, ]; try { - $template->apply($node, $context); + $template = $this->templateFactory->createFromTemplateConfiguration($templateConfiguration, $context, CaughtExceptions::create()); + $this->templateContentRepositoryApplier->apply($template, $node); } catch (\Exception $exception) { $this->handleExceptions($node, $exception); } diff --git a/Classes/NodeCreationHandler/TemplatingDocumentTitleNodeCreationHandler.php b/Classes/NodeCreationHandler/TemplatingDocumentTitleNodeCreationHandler.php index ef4e5c2..8b84b14 100644 --- a/Classes/NodeCreationHandler/TemplatingDocumentTitleNodeCreationHandler.php +++ b/Classes/NodeCreationHandler/TemplatingDocumentTitleNodeCreationHandler.php @@ -30,6 +30,9 @@ class TemplatingDocumentTitleNodeCreationHandler implements NodeCreationHandlerI */ public function handle(NodeInterface $node, array $data): void { + // TODO; + return; + $title = null; $uriPathSegment = null; diff --git a/Classes/Service/EelEvaluationService.php b/Classes/Service/EelEvaluationService.php deleted file mode 100644 index d99404f..0000000 --- a/Classes/Service/EelEvaluationService.php +++ /dev/null @@ -1,52 +0,0 @@ -defaultContextVariables === null) { - $this->defaultContextVariables = EelUtility::getDefaultContextVariables($this->defaultContext); - } - $contextVariables = array_merge($this->defaultContextVariables, $contextVariables); - try { - return EelUtility::evaluateEelExpression($expression, $this->eelEvaluator, $contextVariables); - } catch (ParserException $parserException) { - throw new EelException('EEL Expression in NodeType template could not be parsed.', 1684788574212, $parserException); - } catch (\Exception $exception) { - throw new EelException(sprintf('EEL Expression "%s" in NodeType template caused an error.', $expression), 1684761760723, $exception); - } - } -} diff --git a/Classes/Template.php b/Classes/Template.php deleted file mode 100644 index 44c951c..0000000 --- a/Classes/Template.php +++ /dev/null @@ -1,277 +0,0 @@ - - */ - protected $childNodes; - - /** - * Options can be used to configure third party processing - * - * @var array - */ - protected $options; - - /** - * @var string - */ - protected $when; - - /** - * @var string - */ - protected $withItems; - - /** - * @var array - */ - protected $withContext; - - /** - * @var EelEvaluationService - * @Flow\Inject - */ - protected $eelEvaluationService; - - /** - * @var NodeOperations - * @Flow\Inject - */ - protected $nodeOperations; - - /** - * @var PersistenceManager - * @Flow\Inject - */ - protected $persistenceManager; - - /** - * Template constructor - * - * @param string $type - * @param string $name - * @param array $properties - * @param array