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