Completed
Pull Request — develop (#61)
by Kevin
09:23
created

Builder::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 5
crap 1
1
<?php
2
3
namespace Magium\Configuration\Config;
4
5
use Interop\Container\ContainerInterface;
6
use Magium\Configuration\Config\Repository\ConfigInterface;
7
use Magium\Configuration\Config\Repository\ConfigurationRepository;
8
use Magium\Configuration\Config\Storage\CallbackInterface;
9
use Magium\Configuration\Config\Storage\StorageInterface as ConfigurationStorageInterface;
10
use Magium\Configuration\Container\GenericContainer;
11
use Magium\Configuration\File\AdapterInterface;
12
use Magium\Configuration\File\Configuration\ConfigurationFileRepository;
13
use Magium\Configuration\File\InvalidFileException;
14
use Magium\Configuration\InvalidConfigurationException;
15
use Zend\Cache\Storage\StorageInterface;
16
17
class Builder implements BuilderInterface
18
{
19
20
    protected $repository;
21
    protected $cache;
22
    protected $container;
23
    protected $hashAlgo;
24
    protected $storage;
25
26 21
    public function __construct(
27
        StorageInterface $cache,
28
        ConfigurationStorageInterface $storage,
29
        ConfigurationFileRepository $repository,
30
        ContainerInterface $container = null,
31
        $hashAlgo = 'sha1'
32
    )
33
    {
34 21
        $this->cache = $cache;
35 21
        $this->storage = $storage;
36 21
        $this->hashAlgo = $hashAlgo;
37 21
        $this->repository = $repository;
38 21
        $this->container = $container;
39 21
    }
40
41 3
    public function getConfigurationRepository()
42
    {
43 3
        return $this->repository;
44
    }
45
46 1
    public function getStorage()
47
    {
48 1
        return $this->storage;
49
    }
50
51 6
    public function setValue($path, $value, $requestedContext = ConfigurationRepository::CONTEXT_DEFAULT)
52
    {
53 6
        $parts = explode('/', $path);
54 6
        if (count($parts) != 3) {
55 3
            throw new InvalidArgumentException('Path must be in the structure of section/group/element');
56
        }
57
58 3
        $merged = $this->getMergedStructure();
59 3
        $merged->registerXPathNamespace('s', 'http://www.magiumlib.com/Configuration');
60
61 3
        $xpath = sprintf('//s:section[@identifier="%s"]/s:group[@identifier="%s"]/s:element[@identifier="%s"]/s:permittedValues/s:value',
62 3
            $parts[0],
63 3
            $parts[1],
64 3
            $parts[2]
65
        );
66
67 3
        $elements = $merged->xpath($xpath);
68 3
        if ($elements) {
69 3
            $check = [];
70 3
            foreach ($elements as $element) {
71 3
                $check[] = (string)$element;
72
            }
73 3
            if (!in_array($value, $check)) {
74 1
                throw new InvalidArgumentException('The value must be one of: ' . implode(', ', $check));
75
            }
76
        }
77
78 2
        return $this->getStorage()->setValue($path, $value, $requestedContext);
79
    }
80
81 8
    public function getContainer()
82
    {
83 8
        if (!$this->container instanceof ContainerInterface) {
84 8
            $this->container = new GenericContainer();
85 8
            $this->container->set($this);
86 8
            $this->container->set($this->cache);
87 8
            $this->container->set($this->repository);
88 8
            $this->container->set($this->storage);
89
        }
90 8
        return $this->container;
91
    }
92
93
    /**
94
     * Retrieves a list of files that have been registered
95
     *
96
     * @return ConfigurationFileRepository
97
     */
98
99 5
    public function getRegisteredConfigurationFiles()
100
    {
101 5
        return $this->repository;
102
    }
103
104
    /**
105
     * @param ConfigurationRepository|null $config
106
     * @return ConfigurationRepository
107
     * @throws InvalidConfigurationLocationException
108
     * @throws InvalidFileException
109
     * @throws
110
     */
111
112 6
    public function build($context = ConfigurationRepository::CONTEXT_DEFAULT, ConfigInterface $config = null)
113
    {
114
115 6
        if (!$config instanceof ConfigurationRepository) {
116 6
            $config = new ConfigurationRepository('<config />');
117
        }
118
119 6
        $structure = $this->getMergedStructure();
120
121 4
        if (!$structure instanceof MergedStructure) {
122 1
            throw new InvalidConfigurationException('No configuration files provided');
123
        }
124
125 3
        $this->getContainer()->set($config);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Interop\Container\ContainerInterface as the method set() does only exist in the following implementations of said interface: Magium\Configuration\Container\GenericContainer, Zend\Di\ServiceLocator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
126 3
        $this->getContainer()->set($structure);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Interop\Container\ContainerInterface as the method set() does only exist in the following implementations of said interface: Magium\Configuration\Container\GenericContainer, Zend\Di\ServiceLocator.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
127
128 3
        $this->buildConfigurationObject($structure, $config, $context);
129
130 3
        return $config;
131
    }
132
133
    /**
134
     * @return \SimpleXMLElement
135
     * @throws InvalidFileException
136
     * @throws MissingConfigurationException
137
     */
138
139 7
    public function getMergedStructure()
140
    {
141 7
        $files = $this->getRegisteredConfigurationFiles();
142 7
        if (!count($files)) {
143 1
            throw new MissingConfigurationException('No configuration files have been provided.  Please add via registerConfigurationFile()');
144
        }
145
146 6
        $structure = null;
147 6
        foreach ($files as $file) {
148 5
            if (!$file instanceof AdapterInterface) {
149 1
                throw new InvalidFileException('Configuration file object must implement ' . AdapterInterface::class);
150
            }
151
152 4
            $simpleXml = $file->toXml();
153 4
            if (!$structure instanceof MergedStructure) {
154 4
                $structure = $simpleXml;
155
            } else {
156 4
                $this->mergeStructure($structure, $simpleXml);
157
            }
158
        }
159 4
        return $structure;
160
    }
161
162 6
    protected function executeCallback($callbackString, $value)
163
    {
164 6
        if (function_exists($callbackString)) {
165 1
            $execute = $callbackString;
166
        } else {
167 5
            $container = $this->getContainer();
168
            try {
169 5
                $callbackObject = $this->getContainer()->get($callbackString);
170 3
                if (!$callbackObject instanceof CallbackInterface) {
171 1
                    throw new UncallableCallbackException('Callback must implement ' . CallbackInterface::class);
172
                }
173
                $execute = [
174 2
                    $callbackObject,
175 2
                    'filter'
176
                ];
177
178 2
                if (!is_callable($execute)) {
179 2
                    throw new UncallableCallbackException('Unable to execute callback: ' . $callbackString);
180
                }
181 3
            } catch (\Exception $e) {
182
                /*
183
                 * This is slightly a hack but the purpose is to throw the insufficient exception if it's an actual
184
                 * problem with the generic container we provide.
185
                 */
186 3
                if ($container instanceof GenericContainer && !$e instanceof UncallableCallbackException) {
187 2
                    throw new InsufficientContainerException(
188
                        'You are using functionality that requires either a fully functional DI Container or Service Locator.  '
189 2
                        . 'The container, or container adapter, must implement Interop\Container\ContainerInterface.'
190
                    );
191
                }
192 1
                throw $e;
193
            }
194
        }
195
196 3
        return call_user_func($execute, $value);
197
    }
198
199
200
    /**
201
     * @param \SimpleXMLElement $structure The object representing the merged configuration structure
202
     * @param \SimpleXmlElement $config An empty config object to be populated
203
     * @param string $context The name of the context
204
     * @return ConfigurationRepository The resulting configuration object
205
     */
206
207 14
    public function buildConfigurationObject(
208
        MergedStructure $structure,
209
        ConfigInterface $config,
210
        $context = ConfigurationRepository::CONTEXT_DEFAULT
211
    )
212
    {
213 14
        $structure->registerXPathNamespace('s', 'http://www.magiumlib.com/Configuration');
214 14
        $elements = $structure->xpath('/*/s:section/s:group/s:element');
215 14
        foreach ($elements as $element) {
216 14
            if ($element instanceof MergedStructure) {
217 14
                $elementId = $element['identifier'];
218 14
                $group = $element->xpath('..')[0];
219 14
                $groupId = $group['identifier'];
220 14
                $section = $group->xpath('..')[0];
221 14
                $sectionId = $section['identifier'];
222 14
                $configPath = sprintf('%s/%s/%s', $sectionId, $groupId, $elementId);
223 14
                $value = $this->storage->getValue($configPath, $context);
224 14
                if (!$value) {
225 7
                    $xpath = sprintf('/*/s:section[@identifier="%s"]/s:group[@identifier="%s"]/s:element[@identifier="%s"]/s:value',
226
                        $sectionId,
227
                        $groupId,
228
                        $elementId
229
                    );
230 7
                    $result = $structure->xpath($xpath);
231 7
                    if (!empty($result)) {
232 7
                        $value = trim((string)$result[0]);
233
                    }
234
                }
235 14
                if (isset($element['callbackFromStorage'])) {
236 6
                    $callbackString = (string)$element['callbackFromStorage'];
237
238 6
                    $value = $this->executeCallback($callbackString, $value);
239
                }
240
241 11
                if ($value) {
242 11
                    $config->$sectionId->$groupId->$elementId = $value;
243
                }
244
            }
245
        }
246 11
    }
247
248 4 View Code Duplication
    public function mergeStructure(MergedStructure $base, \SimpleXMLElement $new)
249
    {
250 4
        $base->registerXPathNamespace('s', 'http://www.magiumlib.com/Configuration');
251 4
        foreach ($new as $item) {
252 4
            if ($item instanceof \SimpleXMLElement) {
253 4
                $xpath = sprintf('/*/s:section[@identifier="%s"]', $item['identifier']);
254 4
                $sectionExists = $base->xpath($xpath);
255
256 4
                if (!empty($sectionExists) && $sectionExists[0] instanceof \SimpleXMLElement) {
257 3
                    $section = $sectionExists[0];
258
                } else {
259 1
                    $section = $base->addChild('section');
260
                }
261
262 4
                foreach ($item->attributes() as $name => $value) {
263 4
                    $section[$name] = (string)$value;
264
                }
265 4
                if ($item->group) {
266 4
                    $this->mergeGroup($section, $item->group);
267
                }
268
            }
269
        }
270 4
    }
271
272 4 View Code Duplication
    protected function mergeGroup(\SimpleXMLElement $section, \SimpleXMLElement $newGroups)
273
    {
274 4
        $section->registerXPathNamespace('s', 'http://www.magiumlib.com/Configuration');
275 4
        foreach ($newGroups as $newGroup) {
276 4
            if ($newGroup instanceof \SimpleXMLElement) {
277 4
                $xpath = sprintf('./s:group[@identifier="%s"]', $newGroup['identifier']);
278 4
                $groupExists = $section->xpath($xpath);
279
280 4
                if (!empty($groupExists) && $groupExists[0] instanceof \SimpleXMLElement) {
281 3
                    $group = $groupExists[0];
282
                } else {
283 2
                    $group = $section->addChild('group');
284
                }
285 4
                foreach ($newGroup->attributes() as $name => $value) {
286 4
                    $group[$name] = (string)$value;
287
                }
288 4
                $this->mergeElements($group, $newGroup->element);
289
            }
290
        }
291 4
    }
292
293 4
    protected function mergeElements(\SimpleXMLElement $group, \SimpleXMLElement $newElements)
294
    {
295 4
        $group->registerXPathNamespace('s', 'http://www.magiumlib.com/Configuration');
296 4
        foreach ($newElements as $newElement) {
297 4
            if ($newElement instanceof \SimpleXMLElement) {
298 4
                $xpath = sprintf('./s:element[@identifier="%s"]', $newElement['identifier']);
299 4
                $elementExists = $group->xpath($xpath);
300
301 4
                if (!empty($elementExists) && $elementExists[0] instanceof \SimpleXMLElement) {
302 1
                    $element = $elementExists[0];
303
                } else {
304 4
                    $element = $group->addChild('element');
305
                }
306 4
                foreach ($newElement->attributes() as $name => $value) {
307 4
                    $element[$name] = (string)$value;
308
                }
309 4
                foreach ($newElement->children() as $key => $item) {
310 4
                    $element->$key = (string)$item;
311
                }
312
            }
313
        }
314 4
    }
315
316
}
317