Skip to content

Commit

Permalink
Merge pull request #4425 from neos/feature/customDefaultContextVariab…
Browse files Browse the repository at this point in the history
…lesFusionRuntime

!!! FEATURE:  Global variables for the Fusion runtime `FusionGlobals`
  • Loading branch information
mhsdesign authored Sep 10, 2023
2 parents 0c16899 + 413ee22 commit 1323218
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 111 deletions.
63 changes: 63 additions & 0 deletions Neos.Fusion/Classes/Core/FusionGlobals.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

namespace Neos\Fusion\Core;

use Neos\Utility\Arrays;

/**
* Fusion allows to add variable to the context either via
* \@context.foo = "bar" or by leveraging the php api {@see Runtime::pushContext()}.
*
* Those approaches are highly dynamic and don't guarantee the existence of variables,
* as they have to be explicitly preserved in uncached \@cache segments,
* or might accidentally be popped from the stack.
*
* The Fusion runtime is instantiated with a set of global variables which contain the EEL helper definitions
* or functions like FlowQuery. Also, variables like "request" are made available via it.
*
* The "${request}" special case: To make the request available in uncached segments, it would need to be serialized,
* but we don't allow this currently and despite that, it would be absurd to cache a random request.
* This is avoided by always exposing the current action request via the global variable.
*
* Overriding Fusion globals is disallowed via \@context and {@see Runtime::pushContext()}.
*/
final readonly class FusionGlobals
{
private function __construct(
public array $value
) {
}

public static function empty(): self
{
return new self([]);
}

public static function fromArray(array $variables): self
{
return new self($variables);
}

/**
* You can access the current request like via this getter:
* `$runtime->fusionGlobals->get('request')`
*/
public function get(string $name): mixed
{
return $this->value[$name] ?? null;
}

public function has(string $name): bool
{
return array_key_exists($name, $this->value);
}

public function merge(FusionGlobals $other): self
{
return new self(
Arrays::arrayMergeRecursiveOverrule($this->value, $other->value)
);
}
}
127 changes: 77 additions & 50 deletions Neos.Fusion/Classes/Core/Runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@
* source code.
*/

use Neos\Eel\Utility as EelUtility;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\Exception\InvalidConfigurationException;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\ActionResponse;
use Neos\Flow\Mvc\Controller\Arguments;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Flow\Mvc\Exception\StopActionException;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;
use Neos\Utility\Arrays;
use Neos\Utility\ObjectAccess;
use Neos\Utility\PositionalArraySorter;
use Neos\Flow\Security\Exception as SecurityException;
use Neos\Fusion\Core\Cache\RuntimeContentCache;
use Neos\Fusion\Core\ExceptionHandlers\AbstractRenderingExceptionHandler;
use Neos\Fusion\Exception as Exceptions;
use Neos\Fusion\Exception;
use Neos\Flow\Security\Exception as SecurityException;
use Neos\Fusion\Exception as Exceptions;
use Neos\Fusion\Exception\RuntimeException;
use Neos\Fusion\FusionObjects\AbstractArrayFusionObject;
use Neos\Fusion\FusionObjects\AbstractFusionObject;
use Neos\Eel\Utility as EelUtility;
use Neos\Utility\Arrays;
use Neos\Utility\ObjectAccess;
use Neos\Utility\PositionalArraySorter;

/**
* Fusion Runtime
Expand Down Expand Up @@ -94,21 +98,19 @@ class Runtime
protected $currentApplyValues = [];

/**
* Default context with helper definitions
*
* @var array
* Fusion global variables like EEL helper definitions {@see FusionGlobals}
*/
protected $defaultContextVariables;
public readonly FusionGlobals $fusionGlobals;

/**
* @var array
*/
protected $runtimeConfiguration;

/**
* @var ControllerContext
* @deprecated
*/
protected $controllerContext;
protected ControllerContext $controllerContext;

/**
* @var array
Expand All @@ -130,15 +132,56 @@ class Runtime
*/
protected $lastEvaluationStatus;

public function __construct(FusionConfiguration|array $fusionConfiguration, ControllerContext $controllerContext)
{
/**
* @internal use {@see RuntimeFactory} for instantiating.
*/
public function __construct(
FusionConfiguration $fusionConfiguration,
FusionGlobals $fusionGlobals
) {
$this->runtimeConfiguration = new RuntimeConfiguration(
$fusionConfiguration instanceof FusionConfiguration
? $fusionConfiguration->toArray()
: $fusionConfiguration
$fusionConfiguration->toArray()
);
$this->controllerContext = $controllerContext;
$this->runtimeContentCache = new RuntimeContentCache($this);
$this->fusionGlobals = $fusionGlobals;
}

/**
* @deprecated {@see self::getControllerContext()}
* @internal
*/
public function setControllerContext(ControllerContext $controllerContext): void
{
$this->controllerContext = $controllerContext;
}

/**
* Returns the context which has been passed by the currently active MVC Controller
*
* DEPRECATED CONCEPT. We only implement this as backwards-compatible layer.
*
* @deprecated use `Runtime::fusionGlobals->get('request')` instead to get the request. {@see FusionGlobals::get()}
* @internal
*/
public function getControllerContext(): ControllerContext
{
if (isset($this->controllerContext)) {
return $this->controllerContext;
}

if (!($request = $this->fusionGlobals->get('request')) instanceof ActionRequest) {
throw new \RuntimeException(sprintf('Expected Fusion variable "request" to be of type ActionRequest, got value of type "%s".', get_debug_type($request)), 1693558026485);
}

$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($request);

return $this->controllerContext = new ControllerContext(
$request,
new ActionResponse(),
new Arguments([]),
$uriBuilder
);
}

/**
Expand Down Expand Up @@ -179,8 +222,11 @@ public function addCacheTag($key, $value)
/**
* Completely replace the context array with the new $contextArray.
*
* Purely internal method, should not be called outside of Neos.Fusion.
* Warning unlike in Fusion's \@context or {@see Runtime::pushContext()},
* no checks are imposed to prevent overriding Fusion globals like "request".
* Relying on this behaviour is highly discouraged but leveraged by Neos.Fusion.Form {@see FusionGlobals}.
*
* @internal purely internal method, should not be called outside Neos.Fusion.
* @param array $contextArray
* @return void
*/
Expand All @@ -191,14 +237,18 @@ public function pushContextArray(array $contextArray)
}

/**
* Push a new context object to the rendering stack
* Push a new context object to the rendering stack.
* It is disallowed to replace global variables {@see FusionGlobals}.
*
* @param string $key the key inside the context
* @param mixed $context
* @return void
*/
public function pushContext($key, $context)
{
if ($this->fusionGlobals->has($key)) {
throw new RuntimeException(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $key), 1694284229044);
}
$newContext = $this->currentContext;
$newContext[$key] = $context;
$this->contextStack[] = $newContext;
Expand All @@ -218,7 +268,9 @@ public function popContext()
}

/**
* Get the current context array
* Get the current context array.
* This PHP context api unlike Fusion, doesn't include the Fusion globals {@see FusionGlobals}.
* The globals can be accessed via {@see Runtime::$fusionGlobals}.
*
* @return array the array of current context objects
*/
Expand Down Expand Up @@ -563,6 +615,9 @@ protected function prepareContextForFusionObject(AbstractFusionObject $fusionObj
if (isset($fusionConfiguration['__meta']['context'])) {
$newContextArray ??= $this->currentContext;
foreach ($fusionConfiguration['__meta']['context'] as $contextKey => $contextValue) {
if ($this->fusionGlobals->has($contextKey)) {
throw new RuntimeException(sprintf('Overriding Fusion global variable "%s" via @context is not allowed.', $contextKey), 1694247627130);
}
$newContextArray[$contextKey] = $this->evaluate($fusionPath . '/__meta/context/' . $contextKey, $fusionObject, self::BEHAVIOR_EXCEPTION);
}
}
Expand Down Expand Up @@ -684,7 +739,7 @@ protected function evaluateEelExpression($expression, AbstractFusionObject $cont
$expression = '${' . $expression . '}';
}

$contextVariables = array_merge($this->getDefaultContextVariables(), $this->currentContext);
$contextVariables = array_merge($this->fusionGlobals->value, $this->currentContext);

if (isset($contextVariables['this'])) {
throw new Exception('Context variable "this" not allowed, as it is already reserved for a pointer to the current Fusion object.', 1344325044);
Expand Down Expand Up @@ -831,34 +886,6 @@ protected function evaluateIfCondition($configurationWithEventualIf, $configurat
return true;
}

/**
* Returns the context which has been passed by the currently active MVC Controller
*
* @return ControllerContext
*/
public function getControllerContext()
{
return $this->controllerContext;
}

/**
* Get variables from configuration that should be set in the context by default.
* For example Eel helpers are made available by this.
*
* @return array Array with default context variable objects.
*/
protected function getDefaultContextVariables()
{
if ($this->defaultContextVariables === null) {
$this->defaultContextVariables = [];
if (isset($this->settings['defaultContext']) && is_array($this->settings['defaultContext'])) {
$this->defaultContextVariables = EelUtility::getDefaultContextVariables($this->settings['defaultContext']);
}
$this->defaultContextVariables['request'] = $this->controllerContext->getRequest();
}
return $this->defaultContextVariables;
}

/**
* Checks and throws an exception for an unrenderable path.
*
Expand Down
36 changes: 28 additions & 8 deletions Neos.Fusion/Classes/Core/RuntimeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

use GuzzleHttp\Psr7\ServerRequest;
use Neos\Eel\Utility as EelUtility;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\ActionResponse;
Expand All @@ -31,6 +32,11 @@ class RuntimeFactory
*/
protected $fusionParser;

/**
* @Flow\InjectConfiguration(path="defaultContext", package="Neos.Fusion")
*/
protected ?array $defaultContextConfiguration;

/**
* @deprecated with Neos 8.3 might be removed with Neos 9.0 use {@link createFromConfiguration} instead.
*/
Expand All @@ -39,24 +45,38 @@ public function create(array $fusionConfiguration, ControllerContext $controller
if ($controllerContext === null) {
$controllerContext = self::createControllerContextFromEnvironment();
}
return new Runtime(
$defaultContextVariables = EelUtility::getDefaultContextVariables(
$this->defaultContextConfiguration ?? []
);
$runtime = new Runtime(
FusionConfiguration::fromArray($fusionConfiguration),
$controllerContext
FusionGlobals::fromArray(
['request' => $controllerContext->getRequest(), ...$defaultContextVariables]
)
);
$runtime->setControllerContext($controllerContext);
return $runtime;
}

public function createFromConfiguration(FusionConfiguration $fusionConfiguration, ControllerContext $controllerContext): Runtime
{
return new Runtime($fusionConfiguration, $controllerContext);
public function createFromConfiguration(
FusionConfiguration $fusionConfiguration,
FusionGlobals $fusionGlobals
): Runtime {
$fusionGlobalHelpers = FusionGlobals::fromArray(
EelUtility::getDefaultContextVariables(
$this->defaultContextConfiguration ?? []
)
);
return new Runtime($fusionConfiguration, $fusionGlobalHelpers->merge($fusionGlobals));
}

public function createFromSourceCode(
FusionSourceCodeCollection $sourceCode,
ControllerContext $controllerContext
FusionGlobals $fusionGlobals
): Runtime {
return new Runtime(
return $this->createFromConfiguration(
$this->fusionParser->parseFromSource($sourceCode),
$controllerContext
$fusionGlobals
);
}

Expand Down
Loading

0 comments on commit 1323218

Please sign in to comment.