Issues (6)

src/ConfigurationTrait.php (2 issues)

1
<?php
2
/**
3
 * Sandro Keil (https://sandro-keil.de)
4
 *
5
 * @link      http://github.com/sandrokeil/interop-config for the canonical source repository
6
 * @copyright Copyright (c) 2015-2020 Sandro Keil
7
 * @license   http://github.com/sandrokeil/interop-config/blob/master/LICENSE.md New BSD License
8
 */
9
10
declare(strict_types = 1);
11
12
namespace Interop\Config;
13
14
use ArrayAccess;
15
use Interop\Config\Exception;
16
use Iterator;
17
18
/**
19
 * ConfigurationTrait which retrieves options from configuration, see interface \Interop\Config\RequiresConfig
20
 *
21
 * This trait is a implementation of \Interop\Config\RequiresConfig. Retrieves options from a configuration and optional
22
 * to perform a mandatory option check. Default options are merged and overridden of the provided options.
23
 */
24
trait ConfigurationTrait
25
{
26
    /**
27
     * @inheritdoc \Interop\Config\RequiresConfig::dimensions
28
     */
29
    abstract public function dimensions(): iterable;
30
31
    /**
32
     * Checks if options are available depending on implemented interfaces and checks that the retrieved options from
33
     * the dimensions path are an array or have implemented \ArrayAccess. The RequiresConfigId interface is supported.
34
     *
35
     * `canRetrieveOptions()` returning true does not mean that `options($config)` will not throw an exception.
36
     * It does however mean that `options()` will not throw an `OptionNotFoundException`. Mandatory options are
37
     * not checked.
38
     *
39
     * @param array|ArrayAccess $config Configuration
40
     * @param string|null $configId Config name, must be provided if factory uses RequiresConfigId interface
41
     * @return bool True if options depending on dimensions are available, otherwise false
42
     */
43 65
    public function canRetrieveOptions($config, string $configId = null): bool
44
    {
45 65
        $dimensions = $this->dimensions();
46 65
        $dimensions = $dimensions instanceof Iterator ? iterator_to_array($dimensions) : $dimensions;
47
48 65
        if ($this instanceof RequiresConfigId) {
49 46
            $dimensions[] = $configId;
50
        }
51
52 65
        foreach ($dimensions as $dimension) {
53 62
            if (((array)$config !== $config && !$config instanceof ArrayAccess)
54 62
                || (!isset($config[$dimension]) && $this instanceof RequiresMandatoryOptions)
55 62
                || (!isset($config[$dimension]) && !$this instanceof ProvidesDefaultOptions)
56
            ) {
57 14
                return false;
58
            }
59 61
            if ($this instanceof ProvidesDefaultOptions && !isset($config[$dimension])) {
60 1
                return true;
61
            }
62
63 60
            $config = $config[$dimension];
64
        }
65 55
        return (array)$config === $config || $config instanceof ArrayAccess;
66
    }
67
68
    /**
69
     * Returns options based on dimensions() like [vendor][package] and can perform mandatory option checks if
70
     * class implements RequiresMandatoryOptions. If the ProvidesDefaultOptions interface is implemented, these options
71
     * must be overridden by the provided config. If you want to allow configurations for more then one instance use
72
     * RequiresConfigId interface.
73
     *
74
     * The RequiresConfigId interface is supported.
75
     *
76
     * @param array|ArrayAccess $config Configuration
77
     * @param string $configId Config name, must be provided if factory uses RequiresConfigId interface
78
     * @return array|ArrayAccess
79
     * @throws Exception\InvalidArgumentException If the $configId parameter is provided but factory does not support it
80
     * @throws Exception\UnexpectedValueException If the $config parameter has the wrong type
81
     * @throws Exception\OptionNotFoundException If no options are available
82
     * @throws Exception\MandatoryOptionNotFoundException If a mandatory option is missing
83
     */
84 61
    public function options($config, string $configId = null)
85
    {
86 61
        $dimensions = $this->dimensions();
87 61
        $dimensions = $dimensions instanceof Iterator ? iterator_to_array($dimensions) : $dimensions;
88
89 61
        if ($this instanceof RequiresConfigId) {
90 41
            $dimensions[] = $configId;
91 20
        } elseif ($configId !== null) {
92 1
            throw new Exception\InvalidArgumentException(
93 1
                sprintf('The factory "%s" does not support multiple instances.', __CLASS__)
94
            );
95
        }
96
97
        // get configuration for provided dimensions
98 60
        foreach ($dimensions as $dimension) {
99 57
            if ((array)$config !== $config && !$config instanceof ArrayAccess) {
100 1
                throw Exception\UnexpectedValueException::invalidOptions($dimensions, $dimension);
101
            }
102
103 57
            if (!isset($config[$dimension])) {
104 10
                if (!$this instanceof RequiresMandatoryOptions && $this instanceof ProvidesDefaultOptions) {
105 2
                    break;
106
                }
107 8
                throw Exception\OptionNotFoundException::missingOptions($this, $dimension, $configId);
0 ignored issues
show
It seems like $this can also be of type Interop\Config\ConfigurationTrait; however, parameter $factory of Interop\Config\Exception...ption::missingOptions() does only seem to accept Interop\Config\RequiresConfig, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

107
                throw Exception\OptionNotFoundException::missingOptions(/** @scrutinizer ignore-type */ $this, $dimension, $configId);
Loading history...
108
            }
109 54
            $config = $config[$dimension];
110
        }
111
112 51
        if ((array)$config !== $config && !$config instanceof ArrayAccess) {
113 1
            throw Exception\UnexpectedValueException::invalidOptions($this->dimensions());
114
        }
115
116 50
        if ($this instanceof RequiresMandatoryOptions) {
117 36
            $this->checkMandatoryOptions($this->mandatoryOptions(), $config);
118
        }
119
120 44
        if ($this instanceof ProvidesDefaultOptions) {
121 20
            $options = $this->defaultOptions();
122
123 20
            $config = array_replace_recursive(
124 20
                $options instanceof Iterator ? iterator_to_array($options) : (array)$options,
125 20
                (array)$config
126
            );
127
        }
128 44
        return $config;
129
    }
130
131
    /**
132
     * Checks if options can be retrieved from config and if not, default options (ProvidesDefaultOptions interface) or
133
     * an empty array will be returned.
134
     *
135
     * @param array|ArrayAccess $config Configuration
136
     * @param string $configId Config name, must be provided if factory uses RequiresConfigId interface
137
     * @return array|ArrayAccess options Default options or an empty array
138
     * @throws Exception\MandatoryOptionNotFoundException If a mandatory option is missing
139
     */
140 3
    public function optionsWithFallback($config, string $configId = null)
141
    {
142 3
        $options = [];
143
144 3
        if ($this->canRetrieveOptions($config, $configId)) {
145 3
            $options = $this->options($config, $configId);
146 3
        } elseif ($this instanceof ProvidesDefaultOptions) {
147 3
            $options = $this->defaultOptions();
148
        }
149 3
        return $options;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $options also could return the type iterable which is incompatible with the documented return type ArrayAccess|array.
Loading history...
150
    }
151
152
    /**
153
     * Checks if a mandatory param is missing, supports recursion
154
     *
155
     * @param iterable $mandatoryOptions
156
     * @param array|ArrayAccess $config
157
     * @throws Exception\MandatoryOptionNotFoundException
158
     */
159 19
    private function checkMandatoryOptions(iterable $mandatoryOptions, $config): void
160
    {
161 19
        foreach ($mandatoryOptions as $key => $mandatoryOption) {
162 19
            $useRecursion = !is_scalar($mandatoryOption);
163
164 19
            if (!$useRecursion && isset($config[$mandatoryOption])) {
165 18
                continue;
166
            }
167
168 13
            if ($useRecursion && isset($config[$key])) {
169 9
                $this->checkMandatoryOptions($mandatoryOption, $config[$key]);
170 6
                return;
171
            }
172
173 7
            throw Exception\MandatoryOptionNotFoundException::missingOption(
174 7
                $this->dimensions(),
175 7
                $useRecursion ? $key : $mandatoryOption
176
            );
177
        }
178 12
    }
179
}
180