Completed
Push — EZP-25721 ( 7a74c3 )
by André
20:05
created

ConfigResolver::setSiteAccess()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
        if (!$this->container instanceof ContainerBuilder && $this->siteAccess->matchingType === 'uninitialized') {
175
            $this->tooEarlyLoadedList[$paramName][] = $this->extractServiceName();
176
        }
177
178
        $namespace = $namespace ?: $this->defaultNamespace;
179
        $scope = $scope ?: $this->getDefaultScope();
180
        $triedScopes = array();
181
182
        // Global scope
183
        $globalScopeParamName = "$namespace." . self::SCOPE_GLOBAL . ".$paramName";
184
        if ($this->container->hasParameter($globalScopeParamName)) {
185
            return $this->container->getParameter($globalScopeParamName);
186
        }
187
        $triedScopes[] = self::SCOPE_GLOBAL;
188
        unset($globalScopeParamName);
189
190
        // Relative scope, siteaccess wise
191
        $relativeScopeParamName = "$namespace.$scope.$paramName";
192
        if ($this->container->hasParameter($relativeScopeParamName)) {
193
            return $this->container->getParameter($relativeScopeParamName);
194
        }
195
        $triedScopes[] = $scope;
196
        unset($relativeScopeParamName);
197
198
        // Relative scope, siteaccess group wise
199
        if (isset($this->groupsBySiteAccess[$scope])) {
200 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...
201
                $relativeScopeParamName = "$namespace.$groupName.$paramName";
202
                if ($this->container->hasParameter($relativeScopeParamName)) {
203
                    return $this->container->getParameter($relativeScopeParamName);
204
                }
205
            }
206
        }
207
208
        // Default scope
209
        $defaultScopeParamName = "$namespace." . self::SCOPE_DEFAULT . ".$paramName";
210
        if ($this->container->hasParameter($defaultScopeParamName)) {
211
            return $this->container->getParameter($defaultScopeParamName);
212
        }
213
        $triedScopes[] = $this->defaultNamespace;
214
        unset($defaultScopeParamName);
215
216
        // Undefined parameter
217
        switch ($this->undefinedStrategy) {
218
            case self::UNDEFINED_STRATEGY_NULL:
219
                return null;
220
221
            case self::UNDEFINED_STRATEGY_EXCEPTION:
222
            default:
223
                throw new ParameterNotFoundException($paramName, $namespace, $triedScopes);
224
        }
225
    }
226
227
    /**
228
     * Try to extract service name that asked for a parameter using debug_backtrace().
229
     *
230
     * @return string
231
     */
232
    private function extractServiceName()
233
    {
234
        foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10) as $t) {
235
            if (!isset($t['function']) || $t['function'] === 'getParameter' || $t['function'] === __FUNCTION__) {
236
                continue;
237
            }
238
239
            // Extract service name from first service matching getXXService pattern
240
            // We can only reverse engineer traditional service name, namspace is stripped from class name based services
241
            if (\strpos($t['function'], 'get') === 0 && \strpos($t['function'], 'Service') === \strlen($t['function']) -7) {
242
                $serviceName = \strtolower(\preg_replace('/\B([A-Z])/', '_$1', \str_replace('_', '.', \substr($t['function'], 3, -7))));
243
                if ($this->container->has($serviceName)) {
244
                    return $serviceName;
245
                }
246
247
                return '->' . $t['function'] . '()';
248
            }
249
        }
250
251
        return '??';
252
    }
253
254
    /**
255
     * Changes the default namespace to look parameter into.
256
     *
257
     * @param string $defaultNamespace
258
     */
259
    public function setDefaultNamespace($defaultNamespace)
260
    {
261
        $this->defaultNamespace = $defaultNamespace;
262
    }
263
264
    /**
265
     * @return string
266
     */
267
    public function getDefaultNamespace()
268
    {
269
        return $this->defaultNamespace;
270
    }
271
272
    public function getDefaultScope()
273
    {
274
        return $this->defaultScope ?: $this->siteAccess->name;
275
    }
276
277
    public function setDefaultScope($scope)
278
    {
279
        $this->defaultScope = $scope;
280
281
        // On scope change check if siteaccess has been updated so we can log warnings if there are any
282
        if ($this->siteAccess->matchingType !== 'uninitialized') {
283
            $this->logTooEarlyLoadedParams();
284
        }
285
    }
286
287
    private function logTooEarlyLoadedParams()
288
    {
289
        if (empty($this->tooEarlyLoadedList)) {
290
            return;
291
        }
292
293
        $logger = $this->container->get('logger');
294
        foreach ($this->tooEarlyLoadedList as $param => $services) {
295
            // Ideally we we would want to skip warnings for services that use dynamic settings on setters as that means
296
            // paramter will get update on scope change, but we don't have a way to detect that here.
297
            $logger->warning(sprintf(
298
                'ConfigResolver was used to load parameter "%s" before SiteAccess was loaded by the following services: %s. This should be avoided; '
299
                . 'first try to use ConfigResolver lazily in these services instead of "$dynamic_paramter$" injection, '
300
                . (PHP_SAPI == 'cli' ? 'if not possible make sure your commands that rely on them are lazy loaded, ' : '')
301
                . 'if nothing else helps try to mark service as lazy.',
302
                $param,
303
                '"' . implode($services, '", "') . '"'
304
            ));
305
        }
306
307
        $this->tooEarlyLoadedList = [];
308
    }
309
}
310