1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* File containing the ConfigResolver class. |
5
|
|
|
* |
6
|
|
|
* @copyright Copyright (C) eZ Systems AS. All rights reserved. |
7
|
|
|
* @license For full copyright and license information view LICENSE file distributed with this source code. |
8
|
|
|
*/ |
9
|
|
|
namespace eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration; |
10
|
|
|
|
11
|
|
|
use eZ\Publish\Core\MVC\Symfony\Configuration\VersatileScopeInterface; |
12
|
|
|
use eZ\Publish\Core\MVC\Symfony\SiteAccess; |
13
|
|
|
use eZ\Publish\Core\MVC\Symfony\SiteAccess\SiteAccessAware; |
14
|
|
|
use eZ\Publish\Core\MVC\Exception\ParameterNotFoundException; |
15
|
|
|
use Symfony\Component\DependencyInjection\ContainerAwareInterface; |
16
|
|
|
use Symfony\Component\DependencyInjection\ContainerAwareTrait; |
17
|
|
|
use Symfony\Component\DependencyInjection\ContainerBuilder; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* This class will help you get settings for a specific scope. |
21
|
|
|
* This is useful to get a setting for a specific siteaccess for example. |
22
|
|
|
* |
23
|
|
|
* It will check the different scopes available for a given namespace to find the appropriate parameter. |
24
|
|
|
* To work, the dynamic setting must comply internally to the following name format : "<namespace>.<scope>.parameter.name". |
25
|
|
|
* |
26
|
|
|
* - <namespace> is the namespace for your dynamic setting. Defaults to "ezsettings", but can be anything. |
27
|
|
|
* - <scope> is basically the siteaccess name you want your parameter value to apply to. |
28
|
|
|
* Can also be "global" for a global override. |
29
|
|
|
* Another scope is used internally: "default". This is the generic fallback. |
30
|
|
|
* |
31
|
|
|
* The resolve scope order is the following: |
32
|
|
|
* 1. "global" |
33
|
|
|
* 2. SiteAccess name |
34
|
|
|
* 3. "default" |
35
|
|
|
*/ |
36
|
|
|
class ConfigResolver implements VersatileScopeInterface, SiteAccessAware, ContainerAwareInterface |
37
|
|
|
{ |
38
|
|
|
use ContainerAwareTrait; |
39
|
|
|
|
40
|
|
|
const SCOPE_GLOBAL = 'global'; |
41
|
|
|
const SCOPE_DEFAULT = 'default'; |
42
|
|
|
|
43
|
|
|
const UNDEFINED_STRATEGY_EXCEPTION = 1; |
44
|
|
|
const UNDEFINED_STRATEGY_NULL = 2; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var \eZ\Publish\Core\MVC\Symfony\SiteAccess |
48
|
|
|
*/ |
49
|
|
|
protected $siteAccess; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var array Siteaccess groups, indexed by siteaccess name |
53
|
|
|
*/ |
54
|
|
|
protected $groupsBySiteAccess; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var string |
58
|
|
|
*/ |
59
|
|
|
protected $defaultNamespace; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
protected $defaultScope; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var int |
68
|
|
|
*/ |
69
|
|
|
protected $undefinedStrategy; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var string[] List of param => [services] loaded while siteAccess->matchingType was 'uninitialized' |
73
|
|
|
*/ |
74
|
|
|
private $tooEarlyLoadedList = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @param array $groupsBySiteAccess SiteAccess groups, indexed by siteaccess. |
78
|
|
|
* @param string $defaultNamespace The default namespace |
79
|
|
|
* @param int $undefinedStrategy Strategy to use when encountering undefined parameters. |
80
|
|
|
* Must be one of |
81
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_EXCEPTION (throw an exception) |
82
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_NULL (return null) |
83
|
|
|
*/ |
84
|
|
|
public function __construct( |
85
|
|
|
array $groupsBySiteAccess, |
86
|
|
|
$defaultNamespace, |
87
|
|
|
$undefinedStrategy = self::UNDEFINED_STRATEGY_EXCEPTION |
88
|
|
|
) { |
89
|
|
|
$this->groupsBySiteAccess = $groupsBySiteAccess; |
90
|
|
|
$this->defaultNamespace = $defaultNamespace; |
91
|
|
|
$this->undefinedStrategy = $undefinedStrategy; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
public function setSiteAccess(SiteAccess $siteAccess = null) |
95
|
|
|
{ |
96
|
|
|
$this->siteAccess = $siteAccess; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* Sets the strategy to use if an undefined parameter is being asked. |
101
|
|
|
* Can be one of: |
102
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_EXCEPTION (throw an exception) |
103
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_NULL (return null). |
104
|
|
|
* |
105
|
|
|
* Defaults to ConfigResolver::UNDEFINED_STRATEGY_EXCEPTION. |
106
|
|
|
* |
107
|
|
|
* @param int $undefinedStrategy |
108
|
|
|
*/ |
109
|
|
|
public function setUndefinedStrategy($undefinedStrategy) |
110
|
|
|
{ |
111
|
|
|
$this->undefinedStrategy = $undefinedStrategy; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @return int |
116
|
|
|
*/ |
117
|
|
|
public function getUndefinedStrategy() |
118
|
|
|
{ |
119
|
|
|
return $this->undefinedStrategy; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Checks if $paramName exists in $namespace. |
124
|
|
|
* |
125
|
|
|
* @param string $paramName |
126
|
|
|
* @param string $namespace If null, the default namespace should be used. |
127
|
|
|
* @param string $scope The scope you need $paramName value for. It's typically the siteaccess name. |
128
|
|
|
* If null, the current siteaccess name will be used. |
129
|
|
|
* |
130
|
|
|
* @return bool |
131
|
|
|
*/ |
132
|
|
|
public function hasParameter($paramName, $namespace = null, $scope = null) |
133
|
|
|
{ |
134
|
|
|
$namespace = $namespace ?: $this->defaultNamespace; |
135
|
|
|
$scope = $scope ?: $this->getDefaultScope(); |
136
|
|
|
|
137
|
|
|
$defaultScopeParamName = "$namespace." . self::SCOPE_DEFAULT . ".$paramName"; |
138
|
|
|
$globalScopeParamName = "$namespace." . self::SCOPE_GLOBAL . ".$paramName"; |
139
|
|
|
$relativeScopeParamName = "$namespace.$scope.$paramName"; |
140
|
|
|
|
141
|
|
|
// Relative scope, siteaccess group wise |
142
|
|
|
$groupScopeHasParam = false; |
143
|
|
|
if (isset($this->groupsBySiteAccess[$scope])) { |
144
|
|
View Code Duplication |
foreach ($this->groupsBySiteAccess[$scope] as $groupName) { |
|
|
|
|
145
|
|
|
$groupScopeParamName = "$namespace.$groupName.$paramName"; |
146
|
|
|
if ($this->container->hasParameter($groupScopeParamName)) { |
147
|
|
|
$groupScopeHasParam = true; |
148
|
|
|
break; |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
return |
154
|
|
|
$this->container->hasParameter($defaultScopeParamName) |
155
|
|
|
|| $groupScopeHasParam |
156
|
|
|
|| $this->container->hasParameter($relativeScopeParamName) |
157
|
|
|
|| $this->container->hasParameter($globalScopeParamName); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Returns value for $paramName, in $namespace. |
162
|
|
|
* |
163
|
|
|
* @param string $paramName The parameter name, without $prefix and the current scope (i.e. siteaccess name). |
164
|
|
|
* @param string $namespace Namespace for the parameter name. If null, the default namespace will be used. |
165
|
|
|
* @param string $scope The scope you need $paramName value for. It's typically the siteaccess name. |
166
|
|
|
* If null, the current siteaccess name will be used. |
167
|
|
|
* |
168
|
|
|
* @throws \eZ\Publish\Core\MVC\Exception\ParameterNotFoundException |
169
|
|
|
* |
170
|
|
|
* @return mixed |
171
|
|
|
*/ |
172
|
|
|
public function getParameter($paramName, $namespace = null, $scope = null) |
173
|
|
|
{ |
174
|
|
|
$this->logTooEarlyLoadedListIfNeeded($paramName); |
175
|
|
|
|
176
|
|
|
$namespace = $namespace ?: $this->defaultNamespace; |
177
|
|
|
$scope = $scope ?: $this->getDefaultScope(); |
178
|
|
|
$triedScopes = array(); |
179
|
|
|
|
180
|
|
|
// Global scope |
181
|
|
|
$globalScopeParamName = "$namespace." . self::SCOPE_GLOBAL . ".$paramName"; |
182
|
|
|
if ($this->container->hasParameter($globalScopeParamName)) { |
183
|
|
|
return $this->container->getParameter($globalScopeParamName); |
184
|
|
|
} |
185
|
|
|
$triedScopes[] = self::SCOPE_GLOBAL; |
186
|
|
|
unset($globalScopeParamName); |
187
|
|
|
|
188
|
|
|
// Relative scope, siteaccess wise |
189
|
|
|
$relativeScopeParamName = "$namespace.$scope.$paramName"; |
190
|
|
|
if ($this->container->hasParameter($relativeScopeParamName)) { |
191
|
|
|
return $this->container->getParameter($relativeScopeParamName); |
192
|
|
|
} |
193
|
|
|
$triedScopes[] = $scope; |
194
|
|
|
unset($relativeScopeParamName); |
195
|
|
|
|
196
|
|
|
// Relative scope, siteaccess group wise |
197
|
|
|
if (isset($this->groupsBySiteAccess[$scope])) { |
198
|
|
View Code Duplication |
foreach ($this->groupsBySiteAccess[$scope] as $groupName) { |
|
|
|
|
199
|
|
|
$relativeScopeParamName = "$namespace.$groupName.$paramName"; |
200
|
|
|
if ($this->container->hasParameter($relativeScopeParamName)) { |
201
|
|
|
return $this->container->getParameter($relativeScopeParamName); |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// Default scope |
207
|
|
|
$defaultScopeParamName = "$namespace." . self::SCOPE_DEFAULT . ".$paramName"; |
208
|
|
|
if ($this->container->hasParameter($defaultScopeParamName)) { |
209
|
|
|
return $this->container->getParameter($defaultScopeParamName); |
210
|
|
|
} |
211
|
|
|
$triedScopes[] = $this->defaultNamespace; |
212
|
|
|
unset($defaultScopeParamName); |
213
|
|
|
|
214
|
|
|
// Undefined parameter |
215
|
|
|
switch ($this->undefinedStrategy) { |
216
|
|
|
case self::UNDEFINED_STRATEGY_NULL: |
217
|
|
|
return null; |
218
|
|
|
|
219
|
|
|
case self::UNDEFINED_STRATEGY_EXCEPTION: |
220
|
|
|
default: |
221
|
|
|
throw new ParameterNotFoundException($paramName, $namespace, $triedScopes); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Changes the default namespace to look parameter into. |
227
|
|
|
* |
228
|
|
|
* @param string $defaultNamespace |
229
|
|
|
*/ |
230
|
|
|
public function setDefaultNamespace($defaultNamespace) |
231
|
|
|
{ |
232
|
|
|
$this->defaultNamespace = $defaultNamespace; |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** |
236
|
|
|
* @return string |
237
|
|
|
*/ |
238
|
|
|
public function getDefaultNamespace() |
239
|
|
|
{ |
240
|
|
|
return $this->defaultNamespace; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
public function getDefaultScope() |
244
|
|
|
{ |
245
|
|
|
return $this->defaultScope ?: $this->siteAccess->name; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* @param string $scope The default "scope" aka siteaccess name, as opposed to the self::SCOPE_DEFAULT. |
250
|
|
|
*/ |
251
|
|
|
public function setDefaultScope($scope) |
252
|
|
|
{ |
253
|
|
|
$this->defaultScope = $scope; |
254
|
|
|
|
255
|
|
|
// On scope change check if siteaccess has been updated so we can log warnings if there are any |
256
|
|
|
if ($this->siteAccess->matchingType !== 'uninitialized') { |
257
|
|
|
$this->warnAboutTooEarlyLoadedParams(); |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
private function warnAboutTooEarlyLoadedParams() |
262
|
|
|
{ |
263
|
|
|
if (empty($this->tooEarlyLoadedList)) { |
264
|
|
|
return; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
$logger = $this->container->get('logger'); |
268
|
|
|
foreach ($this->tooEarlyLoadedList as $param => $services) { |
269
|
|
|
$logger->warning(sprintf( |
270
|
|
|
'ConfigResolver was used to load parameter "%s" before SiteAccess was loaded by services: %s. This can cause issues. ' |
271
|
|
|
. 'Try to use ConfigResolver lazily, ' |
272
|
|
|
. (PHP_SAPI === 'cli' ? 'make commands that rely on them lazy, ' : '') |
273
|
|
|
. 'or try to mark the service as lazy.', |
274
|
|
|
$param, |
275
|
|
|
'"' . implode($services, '", "') . '"' |
276
|
|
|
)); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
$this->tooEarlyLoadedList = []; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* If in run-time debug mode, before SiteAccess is initialized, log getParameter() usages when considered "unsafe". |
284
|
|
|
* |
285
|
|
|
* @return string |
286
|
|
|
*/ |
287
|
|
|
private function logTooEarlyLoadedListIfNeeded($paramName) |
288
|
|
|
{ |
289
|
|
|
if ($this->container instanceof ContainerBuilder) { |
290
|
|
|
return; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
if ($this->siteAccess->matchingType !== 'uninitialized') { |
294
|
|
|
return; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
// So we are in a state where we need to warn about unsafe use of config resolver parameters... |
298
|
|
|
// .. it's a bit costly to do so, so we only do it in debug mode |
299
|
|
|
$container = $this->container; |
300
|
|
|
if (!$container->getParameter('kernel.debug')) { |
301
|
|
|
return; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$serviceName = '??'; |
305
|
|
|
$resettableServiceIds = $container->getParameter('ezpublish.config_resolver.resettable_services'); |
306
|
|
|
$updatableServices = $container->getParameter('ezpublish.config_resolver.updateable_services'); |
307
|
|
|
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10) as $t) { |
308
|
|
|
if (!isset($t['function']) || $t['function'] === 'getParameter' || $t['function'] === __FUNCTION__) { |
309
|
|
|
continue; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
// Extract service name from first service matching getXXService pattern |
313
|
|
|
if (\strpos($t['function'], 'get') === 0 && \strpos($t['function'], 'Service') === \strlen($t['function']) -7) { |
314
|
|
|
$serviceName = \strtolower(\preg_replace('/\B([A-Z])/', '_$1', \str_replace('_', '.', \substr($t['function'], 3, -7)))); |
315
|
|
|
|
316
|
|
|
// This (->setter('$dynamic_param$')) is safe as the system is able to update it on scope changes |
317
|
|
|
if (isset($updatableServices[$serviceName])) { |
318
|
|
|
return; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
// !! The remaining cases are not safe, typically: |
322
|
|
|
// - ctor('$dynamic_param$') => this should be avoided, use setter or use config resolver instead |
323
|
|
|
// - config resolver use in service factory => the service (or decorator, if any) should be marked lazy |
324
|
|
|
|
325
|
|
|
// Exception are up-datable cases where we where unable to reverse engineer service name |
326
|
|
|
// Which is the case for any class name based services, as namespace is omitted from compiled fn |
327
|
|
|
if (!in_array($serviceName, $resettableServiceIds, true) && !$container->has($serviceName)) { |
328
|
|
|
// So in this case we are not sure if this is a warning or not, but we stay safe and warn about it |
329
|
|
|
// TODO: Might be possible to introspect the compiled container to extract class/service name |
330
|
|
|
$serviceName = '->' . $t['function'] . '()'; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
break; |
334
|
|
|
} |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
$this->tooEarlyLoadedList[$paramName][] = $serviceName; |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.