Passed
Pull Request — develop (#52)
by Kevin
06:37
created

Builder::build()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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

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...
125 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.

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