Completed
Push — 6.7 ( 44756b...c65f08 )
by André
42:04 queued 21:21
created

ConfigResolver::logTooEarlyLoadedListIfNeeded()   D

Complexity

Conditions 20
Paths 70

Size

Total Lines 79

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
nc 70
nop 1
dl 0
loc 79
rs 4.1666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 array[] List of blame => [params] 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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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 = [];
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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 $blame => $params) {
269
            $logger->warning(sprintf(
270
                'ConfigResolver was used by "%s" before SiteAccess was initialized, loading parameter(s) '
271
                . '%s. As this can cause very hard to debug issues, '
272
                . 'try to use ConfigResolver lazily, '
273
                // Symfony 3.4+: . (PHP_SAPI === 'cli' ? 'make the affected commands lazy, ' : '')
274
                . 'make the service lazy or see if you can inject another lazy service.',
275
                $blame,
276
                '"$' . implode('$", "$', array_unique($params)) . '$"'
277
            ));
278
        }
279
280
        $this->tooEarlyLoadedList = [];
281
    }
282
283
    /**
284
     * If in run-time debug mode, before SiteAccess is initialized, log getParameter() usages when considered "unsafe".
285
     *
286
     * @return string
287
     */
288
    private function logTooEarlyLoadedListIfNeeded($paramName)
289
    {
290
        if ($this->container instanceof ContainerBuilder) {
291
            return;
292
        }
293
294
        if ($this->siteAccess->matchingType !== 'uninitialized') {
295
            return;
296
        }
297
298
        // So we are in a state where we need to warn about unsafe use of config resolver parameters...
299
        // .. it's a bit costly to do so, so we only do it in debug mode
300
        $container = $this->container;
301
        if (!$container->getParameter('kernel.debug')) {
302
            return;
303
        }
304
305
        $serviceName = '??';
306
        $firstService = '??';
307
        $commandName = null;
308
        $resettableServiceIds = $container->getParameter('ezpublish.config_resolver.resettable_services');
309
        $updatableServices = $container->getParameter('ezpublish.config_resolver.updateable_services');
310
311
        // Lookup trace to find last service being loaded as possible blame for eager loading
312
        // Abort if one of the earlier services is detected to be "safe", aka updatable
313
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 35) as $t) {
314
            if (!isset($t['function']) || $t['function'] === 'getParameter' || $t['function'] === __FUNCTION__) {
315
                continue;
316
            }
317
318
            // Extract service name from first service matching getXXService pattern
319
            if (\strpos($t['function'], 'get') === 0 && \strrpos($t['function'], 'Service') === \strlen($t['function']) - 7) {
320
                $potentialClassName = \substr($t['function'], 3, -7);
321
                $serviceName = \strtolower(\preg_replace('/\B([A-Z])/', '_$1', \str_replace('_', '.', $potentialClassName)));
322
323
                // This (->setter('$dynamic_param$')) is safe as the system is able to update it on scope changes, abort
324
                if (isset($updatableServices[$serviceName])) {
325
                    return;
326
                }
327
328
                // !! The remaining cases are most likely "not safe", typically:
329
                // - ctor('$dynamic_param$') => this should be avoided, use setter or use config resolver instead
330
                // - config resolver use in service factory => the service (or decorator, if any) should be marked lazy
331
332
                // Possible exception: Class name based services, can't be resolved as namespace is omitted from
333
                // compiled function. In this case we won't know if it was updateable and "safe", so we warn to be sure
334
                if (!in_array($serviceName, $resettableServiceIds, true) && !$container->has($serviceName)) {
335
                    $serviceName = $potentialClassName;
336
                } else {
337
                    $serviceName = '@' . $serviceName;
338
                }
339
340
                // Keep track of the first service loaded
341
                if ($firstService === '??') {
342
                    $firstService = $serviceName;
343
                }
344
345
                // Detect if we found the command loading the service, if we track that as lasts service
346
                if (PHP_SAPI === 'cli' && isset($t['file']) && \stripos($t['file'], 'CommandService.php') !== false) {
347
                    $path = explode(DIRECTORY_SEPARATOR, $t['file']);
348
                    $commandName = \substr($path[count($path) - 1], 3, -11);
349
                    break;
350
                }
351
            }
352
        }
353
354
        // Skip service name if same as first service
355
        if ($serviceName === $firstService) {
356
            $serviceName = '';
357
        }
358
359
        // Add command name if present as the trigger
360
        if ($commandName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $commandName of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
361
            $blame = "$commandName($serviceName) -> $firstService";
362
        } else {
363
            $blame = ($serviceName ? $serviceName . ' -> ' : '') . $firstService;
364
        }
365
        $this->tooEarlyLoadedList[$blame][] = $paramName;
366
    }
367
}
368