Skip to content

Commit 642a11c

Browse files
committed
TASK: Cache expressions as strings
This might seem counter-intuitive at first but serves two purposes: 1) Incremental change to the expressions is possible now, before a change in policy protected classes would usually lead to an exception and required you to flush the full cache, this prevents the issue and therefore makes developing much smoother. 2) Caches can be moved to redis or any other backend as it's only strings now. The second part also makes a further separation of EEL and Flow possible in the future, eventually allowing EEL to be come a package that could be used independently from Flow.
1 parent d56d250 commit 642a11c

File tree

6 files changed

+50
-86
lines changed

6 files changed

+50
-86
lines changed

Neos.Eel/Classes/CompilingEvaluator.php

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Neos\Flow\Annotations as Flow;
1515
use Neos\Cache\Frontend\PhpFrontend;
16+
use Opis\Closure\SerializableClosure;
1617

1718
/**
1819
* An evaluator that compiles expressions down to PHP code
@@ -25,50 +26,18 @@
2526
class CompilingEvaluator implements EelEvaluatorInterface
2627
{
2728
/**
28-
* @var array
29+
* Runtime cache of execution ready closures.
30+
*
31+
* @var \closure[]
2932
*/
30-
protected $newExpressions = [];
33+
protected $evaluatedExpressions = [];
3134

3235
/**
3336
* @Flow\Inject(lazy=false)
34-
* @var PhpFrontend
37+
* @var StringFrontend
3538
*/
3639
protected $expressionCache;
3740

38-
/**
39-
* Initialize the Evaluator
40-
*/
41-
public function initializeObject()
42-
{
43-
$this->expressionCache->requireOnce('cachedExpressionClosures');
44-
}
45-
46-
/**
47-
* Shutdown the Evaluator
48-
*/
49-
public function shutdownObject()
50-
{
51-
if (count($this->newExpressions) > 0) {
52-
$changesToPersist = false;
53-
$codeToBeCached = $this->expressionCache->get('cachedExpressionClosures');
54-
/**
55-
* At this point a race condition could happen, that we try to prevent with an additional check.
56-
* So we compare the evaluated expressions during this request with the methods the cache has at
57-
* this point and only add methods that are not present. Only if we added anything we write the cache.
58-
*/
59-
foreach ($this->newExpressions as $functionName => $newExpression) {
60-
if (strpos($codeToBeCached, $functionName) === false) {
61-
$codeToBeCached .= 'if (!function_exists(\'' . $functionName . '\')) { ' . $newExpression . ' }' . chr(10);
62-
$changesToPersist = true;
63-
}
64-
}
65-
66-
if ($changesToPersist) {
67-
$this->expressionCache->set('cachedExpressionClosures', $codeToBeCached);
68-
}
69-
}
70-
}
71-
7241
/**
7342
* Evaluate an expression under a given context
7443
*
@@ -81,20 +50,34 @@ public function evaluate($expression, Context $context)
8150
$expression = trim($expression);
8251
$identifier = md5($expression);
8352
$functionName = 'expression_' . $identifier;
53+
if (isset($this->evaluatedExpressions[$functionName])) {
54+
return $this->evaluateAndUnwrap($this->evaluatedExpressions[$functionName], $context);
55+
}
8456

85-
if (!function_exists($functionName)) {
86-
$code = $this->generateEvaluatorCode($expression);
87-
$functionDeclaration = 'function ' . $functionName . '($context){return ' . $code . ';}';
88-
$this->newExpressions[$functionName] = $functionDeclaration;
89-
eval($functionDeclaration);
57+
$functionDeclaration = $this->expressionCache->get($functionName);
58+
if (!$functionDeclaration) {
59+
$functionDeclaration = $this->generateEvaluatorCode($expression);
60+
$this->expressionCache->set($functionName, $functionDeclaration);
9061
}
9162

92-
$result = $functionName($context);
63+
$expressionFunction = eval($functionDeclaration);
64+
$this->evaluatedExpressions[$functionName] = $expressionFunction;
65+
return $this->evaluateAndUnwrap($expressionFunction, $context);
66+
}
67+
68+
/**
69+
* @param \closure $expressionFunction
70+
* @param Context $context
71+
* @return mixed
72+
*/
73+
protected function evaluateAndUnwrap(\closure $expressionFunction, Context $context)
74+
{
75+
$result = $expressionFunction($context);
9376
if ($result instanceof Context) {
9477
return $result->unwrap();
95-
} else {
96-
return $result;
9778
}
79+
80+
return $result;
9881
}
9982

10083
/**
@@ -118,6 +101,7 @@ protected function generateEvaluatorCode($expression)
118101
} elseif (!array_key_exists('code', $result)) {
119102
throw new ParserException(sprintf('Parser error, no code in result %s ', json_encode($result)), 1334491498);
120103
}
121-
return $result['code'];
104+
105+
return 'return function ($context) {return ' . $result['code'] . ';};';
122106
}
123107
}

Neos.Eel/Configuration/Caches.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Eel_Expression_Code:
2-
frontend: Neos\Cache\Frontend\PhpFrontend
2+
frontend: Neos\Cache\Frontend\StringFrontend
33
backend: Neos\Cache\Backend\SimpleFileBackend

Neos.Flow/Classes/Aop/Pointcut/PointcutFilterComposite.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,14 @@ public function getRuntimeEvaluationsClosureCode()
173173
$conditionCode = $this->buildRuntimeEvaluationsConditionCode('', $this->getRuntimeEvaluationsDefinition(), $useGlobalObjects);
174174

175175
if ($conditionCode !== '') {
176-
$code = "\n\t\t\t\t\t\tfunction(\\Neos\\Flow\\Aop\\JoinPointInterface \$joinPoint, \$objectManager) {\n" .
177-
"\t\t\t\t\t\t\t\$currentObject = \$joinPoint->getProxy();\n";
176+
$code = "function(\\Neos\\Flow\\Aop\\JoinPointInterface \$joinPoint, \$objectManager) {\n" .
177+
" \$currentObject = \$joinPoint->getProxy();\n";
178178
if ($useGlobalObjects) {
179-
$code .= "\t\t\t\t\t\t\t\$globalObjectNames = \$objectManager->getSettingsByPath(array('Neos', 'Flow', 'aop', 'globalObjects'));\n";
180-
$code .= "\t\t\t\t\t\t\t\$globalObjects = array_map(function(\$objectName) use (\$objectManager) { return \$objectManager->get(\$objectName); }, \$globalObjectNames);\n";
179+
$code .= " \$globalObjectNames = \$objectManager->getSettingsByPath(array('Neos', 'Flow', 'aop', 'globalObjects'));\n";
180+
$code .= " \$globalObjects = array_map(function(\$objectName) use (\$objectManager) { return \$objectManager->get(\$objectName); }, \$globalObjectNames);\n";
181181
}
182-
$code .= "\t\t\t\t\t\t\treturn " . $conditionCode . ';' .
183-
"\n\t\t\t\t\t\t}";
182+
$code .= " return " . $conditionCode . ';' .
183+
"\n}";
184184
return $code;
185185
} else {
186186
return 'NULL';

Neos.Flow/Classes/Aop/Pointcut/RuntimeExpressionEvaluator.php

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
class RuntimeExpressionEvaluator
2929
{
3030
/**
31-
* @var PhpFrontend
31+
* @var StringFrontend
3232
*/
3333
protected $runtimeExpressionsCache;
3434

@@ -44,13 +44,6 @@ class RuntimeExpressionEvaluator
4444
*/
4545
protected $runtimeExpressions = [];
4646

47-
/**
48-
* Newly added expressions.
49-
*
50-
* @var array
51-
*/
52-
protected $newExpressions = [];
53-
5447
/**
5548
* This object is created very early and is part of the blacklisted "Neos\Flow\Aop" namespace so we can't rely on AOP for the property injection.
5649
*
@@ -64,30 +57,9 @@ public function injectObjectManager(ObjectManagerInterface $objectManager)
6457
/** @var CacheManager $cacheManager */
6558
$cacheManager = $this->objectManager->get(CacheManager::class);
6659
$this->runtimeExpressionsCache = $cacheManager->getCache('Flow_Aop_RuntimeExpressions');
67-
$this->runtimeExpressions = $this->runtimeExpressionsCache->requireOnce('Flow_Aop_RuntimeExpressions');
6860
}
6961
}
7062

71-
/**
72-
* Shutdown the Evaluator and save created expressions overwriting any existing expressions
73-
*
74-
* @return void
75-
*/
76-
public function saveRuntimeExpressions()
77-
{
78-
if ($this->newExpressions === []) {
79-
return;
80-
}
81-
82-
$codeToBeCached = 'return array (' . chr(10);
83-
84-
foreach ($this->newExpressions as $name => $function) {
85-
$codeToBeCached .= "'" . $name . "' => " . $function . ',' . chr(10);
86-
}
87-
$codeToBeCached .= ');';
88-
$this->runtimeExpressionsCache->set('Flow_Aop_RuntimeExpressions', $codeToBeCached);
89-
}
90-
9163
/**
9264
* Evaluate an expression with the given JoinPoint
9365
*
@@ -99,11 +71,17 @@ public function saveRuntimeExpressions()
9971
public function evaluate($privilegeIdentifier, JoinPointInterface $joinPoint)
10072
{
10173
$functionName = $this->generateExpressionFunctionName($privilegeIdentifier);
74+
if (isset($this->runtimeExpressions[$functionName])) {
75+
return $this->runtimeExpressions[$functionName]->__invoke($joinPoint, $this->objectManager);
76+
}
10277

103-
if (!isset($this->runtimeExpressions[$functionName]) || !$this->runtimeExpressions[$functionName] instanceof \Closure) {
78+
$expression = $this->runtimeExpressionsCache->get($functionName);
79+
80+
if (!$expression) {
10481
throw new Exception('Runtime expression "' . $functionName . '" does not exist. Flushing the code caches may help to solve this.', 1428694144);
10582
}
10683

84+
$this->runtimeExpressions[$functionName] = eval($expression);
10785
return $this->runtimeExpressions[$functionName]->__invoke($joinPoint, $this->objectManager);
10886
}
10987

@@ -116,7 +94,10 @@ public function evaluate($privilegeIdentifier, JoinPointInterface $joinPoint)
11694
*/
11795
public function addExpression($privilegeIdentifier, $expression)
11896
{
119-
$this->newExpressions[$this->generateExpressionFunctionName($privilegeIdentifier)] = $expression;
97+
$functionName = $this->generateExpressionFunctionName($privilegeIdentifier);
98+
$wrappedExpression = 'return ' . $expression . ';';
99+
$this->runtimeExpressionsCache->set($functionName, $wrappedExpression);
100+
$this->runtimeExpressions[$functionName] = eval($wrappedExpression);
120101
}
121102

122103
/**

Neos.Flow/Classes/Package.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ public function boot(Core\Bootstrap $bootstrap)
9595
$dispatcher->connect(Core\Bootstrap::class, 'bootstrapShuttingDown', Reflection\ReflectionService::class, 'saveToCache');
9696

9797
$dispatcher->connect(Command\CoreCommandController::class, 'finishedCompilationRun', Security\Authorization\Privilege\Method\MethodPrivilegePointcutFilter::class, 'savePolicyCache');
98-
$dispatcher->connect(Command\CoreCommandController::class, 'finishedCompilationRun', Aop\Pointcut\RuntimeExpressionEvaluator::class, 'saveRuntimeExpressions');
9998

10099
$dispatcher->connect(Security\Authentication\AuthenticationProviderManager::class, 'authenticatedToken', function () use ($bootstrap) {
101100
$session = $bootstrap->getObjectManager()->get(Session\SessionInterface::class);

Neos.Flow/Configuration/Caches.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Flow_Session_Storage:
128128

129129
# Aop filter expressions that need to be evaluated at runtime.
130130
Flow_Aop_RuntimeExpressions:
131-
frontend: Neos\Cache\Frontend\PhpFrontend
131+
frontend: Neos\Cache\Frontend\StringFrontend
132132
backend: Neos\Cache\Backend\SimpleFileBackend
133133

134134
# Caches the map of possible PropertyMappers for source and target types

0 commit comments

Comments
 (0)