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
|
|
|
/** @var \eZ\Publish\Core\MVC\Symfony\SiteAccess */ |
47
|
|
|
protected $siteAccess; |
48
|
|
|
|
49
|
|
|
/** @var array Siteaccess groups, indexed by siteaccess name */ |
50
|
|
|
protected $groupsBySiteAccess; |
51
|
|
|
|
52
|
|
|
/** @var string */ |
53
|
|
|
protected $defaultNamespace; |
54
|
|
|
|
55
|
|
|
/** @var string */ |
56
|
|
|
protected $defaultScope; |
57
|
|
|
|
58
|
|
|
/** @var int */ |
59
|
|
|
protected $undefinedStrategy; |
60
|
|
|
|
61
|
|
|
/** @var array[] List of blame => [params] loaded while siteAccess->matchingType was 'uninitialized' */ |
62
|
|
|
private $tooEarlyLoadedList = []; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @param array $groupsBySiteAccess SiteAccess groups, indexed by siteaccess. |
66
|
|
|
* @param string $defaultNamespace The default namespace |
67
|
|
|
* @param int $undefinedStrategy Strategy to use when encountering undefined parameters. |
68
|
|
|
* Must be one of |
69
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_EXCEPTION (throw an exception) |
70
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_NULL (return null) |
71
|
|
|
*/ |
72
|
|
|
public function __construct( |
73
|
|
|
array $groupsBySiteAccess, |
74
|
|
|
$defaultNamespace, |
75
|
|
|
$undefinedStrategy = self::UNDEFINED_STRATEGY_EXCEPTION |
76
|
|
|
) { |
77
|
|
|
$this->groupsBySiteAccess = $groupsBySiteAccess; |
78
|
|
|
$this->defaultNamespace = $defaultNamespace; |
79
|
|
|
$this->undefinedStrategy = $undefinedStrategy; |
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
public function setSiteAccess(SiteAccess $siteAccess = null) |
83
|
|
|
{ |
84
|
|
|
$this->siteAccess = $siteAccess; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Sets the strategy to use if an undefined parameter is being asked. |
89
|
|
|
* Can be one of: |
90
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_EXCEPTION (throw an exception) |
91
|
|
|
* - ConfigResolver::UNDEFINED_STRATEGY_NULL (return null). |
92
|
|
|
* |
93
|
|
|
* Defaults to ConfigResolver::UNDEFINED_STRATEGY_EXCEPTION. |
94
|
|
|
* |
95
|
|
|
* @param int $undefinedStrategy |
96
|
|
|
*/ |
97
|
|
|
public function setUndefinedStrategy($undefinedStrategy) |
98
|
|
|
{ |
99
|
|
|
$this->undefinedStrategy = $undefinedStrategy; |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* @return int |
104
|
|
|
*/ |
105
|
|
|
public function getUndefinedStrategy() |
106
|
|
|
{ |
107
|
|
|
return $this->undefinedStrategy; |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Checks if $paramName exists in $namespace. |
112
|
|
|
* |
113
|
|
|
* @param string $paramName |
114
|
|
|
* @param string $namespace If null, the default namespace should be used. |
115
|
|
|
* @param string $scope The scope you need $paramName value for. It's typically the siteaccess name. |
116
|
|
|
* If null, the current siteaccess name will be used. |
117
|
|
|
* |
118
|
|
|
* @return bool |
119
|
|
|
*/ |
120
|
|
|
public function hasParameter($paramName, $namespace = null, $scope = null) |
121
|
|
|
{ |
122
|
|
|
$namespace = $namespace ?: $this->defaultNamespace; |
123
|
|
|
$scope = $scope ?: $this->getDefaultScope(); |
124
|
|
|
|
125
|
|
|
$defaultScopeParamName = "$namespace." . self::SCOPE_DEFAULT . ".$paramName"; |
126
|
|
|
$globalScopeParamName = "$namespace." . self::SCOPE_GLOBAL . ".$paramName"; |
127
|
|
|
$relativeScopeParamName = "$namespace.$scope.$paramName"; |
128
|
|
|
|
129
|
|
|
// Relative scope, siteaccess group wise |
130
|
|
|
$groupScopeHasParam = false; |
131
|
|
|
if (isset($this->groupsBySiteAccess[$scope])) { |
132
|
|
View Code Duplication |
foreach ($this->groupsBySiteAccess[$scope] as $groupName) { |
|
|
|
|
133
|
|
|
$groupScopeParamName = "$namespace.$groupName.$paramName"; |
134
|
|
|
if ($this->container->hasParameter($groupScopeParamName)) { |
135
|
|
|
$groupScopeHasParam = true; |
136
|
|
|
break; |
137
|
|
|
} |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return |
142
|
|
|
$this->container->hasParameter($defaultScopeParamName) |
143
|
|
|
|| $groupScopeHasParam |
144
|
|
|
|| $this->container->hasParameter($relativeScopeParamName) |
145
|
|
|
|| $this->container->hasParameter($globalScopeParamName); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Returns value for $paramName, in $namespace. |
150
|
|
|
* |
151
|
|
|
* @param string $paramName The parameter name, without $prefix and the current scope (i.e. siteaccess name). |
152
|
|
|
* @param string $namespace Namespace for the parameter name. If null, the default namespace will be used. |
153
|
|
|
* @param string $scope The scope you need $paramName value for. It's typically the siteaccess name. |
154
|
|
|
* If null, the current siteaccess name will be used. |
155
|
|
|
* |
156
|
|
|
* @throws \eZ\Publish\Core\MVC\Exception\ParameterNotFoundException |
157
|
|
|
* |
158
|
|
|
* @return mixed |
159
|
|
|
*/ |
160
|
|
|
public function getParameter($paramName, $namespace = null, $scope = null) |
161
|
|
|
{ |
162
|
|
|
$this->logTooEarlyLoadedListIfNeeded($paramName); |
163
|
|
|
|
164
|
|
|
$namespace = $namespace ?: $this->defaultNamespace; |
165
|
|
|
$scope = $scope ?: $this->getDefaultScope(); |
166
|
|
|
$triedScopes = []; |
167
|
|
|
|
168
|
|
|
// Global scope |
169
|
|
|
$globalScopeParamName = "$namespace." . self::SCOPE_GLOBAL . ".$paramName"; |
170
|
|
|
if ($this->container->hasParameter($globalScopeParamName)) { |
171
|
|
|
return $this->container->getParameter($globalScopeParamName); |
172
|
|
|
} |
173
|
|
|
$triedScopes[] = self::SCOPE_GLOBAL; |
174
|
|
|
unset($globalScopeParamName); |
175
|
|
|
|
176
|
|
|
// Relative scope, siteaccess wise |
177
|
|
|
$relativeScopeParamName = "$namespace.$scope.$paramName"; |
178
|
|
|
if ($this->container->hasParameter($relativeScopeParamName)) { |
179
|
|
|
return $this->container->getParameter($relativeScopeParamName); |
180
|
|
|
} |
181
|
|
|
$triedScopes[] = $scope; |
182
|
|
|
unset($relativeScopeParamName); |
183
|
|
|
|
184
|
|
|
// Relative scope, siteaccess group wise |
185
|
|
|
if (isset($this->groupsBySiteAccess[$scope])) { |
186
|
|
View Code Duplication |
foreach ($this->groupsBySiteAccess[$scope] as $groupName) { |
|
|
|
|
187
|
|
|
$relativeScopeParamName = "$namespace.$groupName.$paramName"; |
188
|
|
|
if ($this->container->hasParameter($relativeScopeParamName)) { |
189
|
|
|
return $this->container->getParameter($relativeScopeParamName); |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
// Default scope |
195
|
|
|
$defaultScopeParamName = "$namespace." . self::SCOPE_DEFAULT . ".$paramName"; |
196
|
|
|
if ($this->container->hasParameter($defaultScopeParamName)) { |
197
|
|
|
return $this->container->getParameter($defaultScopeParamName); |
198
|
|
|
} |
199
|
|
|
$triedScopes[] = $this->defaultNamespace; |
200
|
|
|
unset($defaultScopeParamName); |
201
|
|
|
|
202
|
|
|
// Undefined parameter |
203
|
|
|
switch ($this->undefinedStrategy) { |
204
|
|
|
case self::UNDEFINED_STRATEGY_NULL: |
205
|
|
|
return null; |
206
|
|
|
|
207
|
|
|
case self::UNDEFINED_STRATEGY_EXCEPTION: |
208
|
|
|
default: |
209
|
|
|
throw new ParameterNotFoundException($paramName, $namespace, $triedScopes); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Changes the default namespace to look parameter into. |
215
|
|
|
* |
216
|
|
|
* @param string $defaultNamespace |
217
|
|
|
*/ |
218
|
|
|
public function setDefaultNamespace($defaultNamespace) |
219
|
|
|
{ |
220
|
|
|
$this->defaultNamespace = $defaultNamespace; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* @return string |
225
|
|
|
*/ |
226
|
|
|
public function getDefaultNamespace() |
227
|
|
|
{ |
228
|
|
|
return $this->defaultNamespace; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
public function getDefaultScope() |
232
|
|
|
{ |
233
|
|
|
return $this->defaultScope ?: $this->siteAccess->name; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @param string $scope The default "scope" aka siteaccess name, as opposed to the self::SCOPE_DEFAULT. |
238
|
|
|
*/ |
239
|
|
|
public function setDefaultScope($scope) |
240
|
|
|
{ |
241
|
|
|
$this->defaultScope = $scope; |
242
|
|
|
|
243
|
|
|
// On scope change check if siteaccess has been updated so we can log warnings if there are any |
244
|
|
|
if ($this->siteAccess->matchingType !== 'uninitialized') { |
245
|
|
|
$this->warnAboutTooEarlyLoadedParams(); |
246
|
|
|
} |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
private function warnAboutTooEarlyLoadedParams() |
250
|
|
|
{ |
251
|
|
|
if (empty($this->tooEarlyLoadedList)) { |
252
|
|
|
return; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
$logger = $this->container->get('logger'); |
256
|
|
|
foreach ($this->tooEarlyLoadedList as $blame => $params) { |
257
|
|
|
$logger->warning(sprintf( |
258
|
|
|
'ConfigResolver was used by "%s" before SiteAccess was initialized, loading parameter(s) ' |
259
|
|
|
. '%s. As this can cause very hard to debug issues, ' |
260
|
|
|
. 'try to use ConfigResolver lazily, ' |
261
|
|
|
. (PHP_SAPI === 'cli' ? 'make the affected commands lazy, ' : '') |
262
|
|
|
. 'make the service lazy or see if you can inject another lazy service.', |
263
|
|
|
$blame, |
264
|
|
|
'"$' . implode('$", "$', array_unique($params)) . '$"' |
265
|
|
|
)); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
$this->tooEarlyLoadedList = []; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* If in run-time debug mode, before SiteAccess is initialized, log getParameter() usages when considered "unsafe". |
273
|
|
|
* |
274
|
|
|
* @return string |
275
|
|
|
*/ |
276
|
|
|
private function logTooEarlyLoadedListIfNeeded($paramName) |
277
|
|
|
{ |
278
|
|
|
if ($this->container instanceof ContainerBuilder) { |
279
|
|
|
return; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
if ($this->siteAccess->matchingType !== 'uninitialized') { |
283
|
|
|
return; |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
// So we are in a state where we need to warn about unsafe use of config resolver parameters... |
287
|
|
|
// .. it's a bit costly to do so, so we only do it in debug mode |
288
|
|
|
$container = $this->container; |
289
|
|
|
if (!$container->getParameter('kernel.debug')) { |
290
|
|
|
return; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
$serviceName = '??'; |
294
|
|
|
$firstService = '??'; |
295
|
|
|
$commandName = null; |
296
|
|
|
$resettableServiceIds = $container->getParameter('ezpublish.config_resolver.resettable_services'); |
297
|
|
|
$updatableServices = $container->getParameter('ezpublish.config_resolver.updateable_services'); |
298
|
|
|
|
299
|
|
|
// Lookup trace to find last service being loaded as possible blame for eager loading |
300
|
|
|
// Abort if one of the earlier services is detected to be "safe", aka updatable |
301
|
|
|
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 35) as $t) { |
302
|
|
|
if (!isset($t['function']) || $t['function'] === 'getParameter' || $t['function'] === __FUNCTION__) { |
303
|
|
|
continue; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
// Extract service name from first service matching getXXService pattern |
307
|
|
|
if (\strpos($t['function'], 'get') === 0 && \strrpos($t['function'], 'Service') === \strlen($t['function']) - 7) { |
308
|
|
|
$potentialClassName = \substr($t['function'], 3, -7); |
309
|
|
|
$serviceName = \strtolower(\preg_replace('/\B([A-Z])/', '_$1', \str_replace('_', '.', $potentialClassName))); |
310
|
|
|
|
311
|
|
|
// This (->setter('$dynamic_param$')) is safe as the system is able to update it on scope changes, abort |
312
|
|
|
if (isset($updatableServices[$serviceName])) { |
313
|
|
|
return; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
// !! The remaining cases are most likely "not safe", typically: |
317
|
|
|
// - ctor('$dynamic_param$') => this should be avoided, use setter or use config resolver instead |
318
|
|
|
// - config resolver use in service factory => the service (or decorator, if any) should be marked lazy |
319
|
|
|
|
320
|
|
|
// Possible exception: Class name based services, can't be resolved as namespace is omitted from |
321
|
|
|
// compiled function. In this case we won't know if it was updateable and "safe", so we warn to be sure |
322
|
|
|
if (!in_array($serviceName, $resettableServiceIds, true) && !$container->has($serviceName)) { |
323
|
|
|
$serviceName = $potentialClassName; |
324
|
|
|
} else { |
325
|
|
|
$serviceName = '@' . $serviceName; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
// Keep track of the first service loaded |
329
|
|
|
if ($firstService === '??') { |
330
|
|
|
$firstService = $serviceName; |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
// Detect if we found the command loading the service, if we track that as lasts service |
334
|
|
|
if (PHP_SAPI === 'cli' && isset($t['file']) && \stripos($t['file'], 'CommandService.php') !== false) { |
335
|
|
|
$path = explode(DIRECTORY_SEPARATOR, $t['file']); |
336
|
|
|
$commandName = \substr($path[count($path) - 1], 3, -11); |
337
|
|
|
break; |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
// Skip service name if same as first service |
343
|
|
|
if ($serviceName === $firstService) { |
344
|
|
|
$serviceName = ''; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
// Add command name if present as the trigger |
348
|
|
|
if ($commandName) { |
|
|
|
|
349
|
|
|
$blame = "$commandName($serviceName) -> $firstService"; |
350
|
|
|
} else { |
351
|
|
|
$blame = ($serviceName ? $serviceName . ' -> ' : '') . $firstService; |
352
|
|
|
} |
353
|
|
|
$this->tooEarlyLoadedList[$blame][] = $paramName; |
354
|
|
|
} |
355
|
|
|
} |
356
|
|
|
|
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.