Completed
Push — master ( 39810e...374aee )
by André
146:09 queued 124:51
created

ConfigResolver::getParameter()   C

Complexity

Conditions 11
Paths 44

Size

Total Lines 52

Duplication

Lines 6
Ratio 11.54 %

Importance

Changes 0
Metric Value
cc 11
nc 44
nop 3
dl 6
loc 52
rs 6.9006
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 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) {
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 = 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) {
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 $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