Completed
Push — EZP-25721_more_info ( abccaf )
by André
27:50
created

ConfigResolver::logTooEarlyLoadedListIfNeeded()   D

Complexity

Conditions 20
Paths 70

Size

Total Lines 78

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 20
nc 70
nop 1
dl 0
loc 78
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
    /** @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) {
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...
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) {
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...
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
        $firstService = '??';
294
        $commandName = null;
295
        $resettableServiceIds = $container->getParameter('ezpublish.config_resolver.resettable_services');
296
        $updatableServices = $container->getParameter('ezpublish.config_resolver.updateable_services');
297
298
        // Lookup trace to find last service being loaded as possible blame for eager loading
299
        // Abort if one of the earlier services is detected to be "safe", aka updatable
300
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 35) as $t) {
301
            if (!isset($t['function']) || $t['function'] === 'getParameter' || $t['function'] === __FUNCTION__) {
302
                continue;
303
            }
304
305
            // Extract service name from first service matching getXXService pattern
306
            if (\strpos($t['function'], 'get') === 0 && \strrpos($t['function'], 'Service') === \strlen($t['function']) - 7) {
307
                $potentialClassName = \substr($t['function'], 3, -7);
308
                $serviceName = \strtolower(\preg_replace('/\B([A-Z])/', '_$1', \str_replace('_', '.', $potentialClassName)));
309
310
                // This (->setter('$dynamic_param$')) is safe as the system is able to update it on scope changes, abort
311
                if (isset($updatableServices[$serviceName])) {
312
                    return;
313
                }
314
315
                // !! The remaining cases are most likely "not safe", typically:
316
                // - ctor('$dynamic_param$') => this should be avoided, use setter or use config resolver instead
317
                // - config resolver use in service factory => the service (or decorator, if any) should be marked lazy
318
319
                // Possible exception: Class name based services, can't be resolved as namespace is omitted from
320
                // compiled function. In this case we won't know if it was updateable and "safe", so we warn to be sure
321
                if (!in_array($serviceName, $resettableServiceIds, true) && !$container->has($serviceName)) {
322
                    $serviceName = $potentialClassName;
323
                } else {
324
                    $serviceName = '@' . $serviceName;
325
                }
326
327
                // Keep track of the first service loaded
328
                if ($firstService === '??') {
329
                    $firstService = $serviceName;
330
                }
331
332
                // Detect if we found the command loading the service, if we track that as lasts service
333
                if (PHP_SAPI === 'cli' && isset($t['file']) && \stripos($t['file'], 'CommandService.php') !== false) {
334
                    $path = explode(DIRECTORY_SEPARATOR, $t['file']);
335
                    $commandName = \substr($path[count($path) - 1], 3, -11);
336
                    break;
337
                }
338
            }
339
        }
340
341
        // Skip service name if same as first service
342
        if ($serviceName === $firstService) {
0 ignored issues
show
Bug introduced by
The variable $serviceName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
343
            $serviceName = '';
344
        }
345
346
        // Add command name if present as the trigger
347
        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...
348
            $blame = "$commandName($serviceName) -> $firstService";
349
        } else {
350
            $blame = ($serviceName ? $serviceName . ' -> ' : '') . $firstService;
351
        }
352
        $this->tooEarlyLoadedList[$blame][] = $paramName;
353
    }
354
}
355