Passed
Push — master ( 02eddd...2aa0cc )
by mcfog
02:49 queued 18s
created

Configurator::convertToRecipe()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 7

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 15
ccs 8
cts 8
cp 1
rs 8.8333
c 0
b 0
f 0
cc 7
nc 4
nop 1
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Lit\Air;
6
7
use Lit\Air\Psr\Container;
8
use Lit\Air\Psr\ContainerException;
9
use Lit\Air\Recipe\BuilderRecipe;
10
use Lit\Air\Recipe\Decorator\AbstractRecipeDecorator;
11
use Lit\Air\Recipe\Decorator\CallbackDecorator;
12
use Lit\Air\Recipe\Decorator\SingletonDecorator;
13
use Lit\Air\Recipe\FixedValueRecipe;
14
use Lit\Air\Recipe\RecipeInterface;
15
16
/**
17
 * Configurator helps to build an array configuration, and writes array configuration into a container.
18
 * http://litphp.github.io/docs/air-config
19
 */
20
class Configurator
21
{
22
    protected static $decorators = [
23
        'callback' => CallbackDecorator::class,
24
        'singleton' => SingletonDecorator::class,
25
    ];
26
27
    /**
28
     * Write a configuration array into a container
29
     *
30
     * @param Container $container The container.
31
     * @param array     $config    The configuration array.
32
     * @param boolean   $force     Whether overwrite existing values.
33
     * @return void
34
     */
35 1
    public static function config(Container $container, array $config, bool $force = true): void
36
    {
37 1
        foreach ($config as $key => $value) {
38 1
            if (!$force && $container->has($key)) {
39
                continue;
40
            }
41 1
            self::write($container, $key, $value);
42
        }
43 1
    }
44
45
    /**
46
     * Convert a mixed value into a recipe.
47
     *
48
     * @param mixed $value The value.
49
     * @return RecipeInterface
50
     */
51 7
    public static function convertToRecipe($value): RecipeInterface
52
    {
53 7
        if (is_object($value) && $value instanceof RecipeInterface) {
54 2
            return $value;
55
        }
56
57 6
        if (is_callable($value)) {
58 1
            return (new BuilderRecipe($value))->singleton();
59
        }
60
61 5
        if (is_array($value) && array_key_exists(0, $value) && isset($value['$'])) {
62 1
            return self::makeRecipe($value);
63
        }
64
65 4
        return Container::value($value);
66
    }
67
68
    /**
69
     * Configuration indicating a singleton
70
     *
71
     * @param string $classname The class name.
72
     * @param array  $extra     Extra parameters.
73
     * @return array
74
     */
75 1
    public static function singleton(string $classname, array $extra = []): array
76
    {
77 1
        return self::decorateSingleton(self::instance($classname, $extra));
78
    }
79
80
    /**
81
     * Decorate a configuration, makes it a singleton (\Lit\Air\Recipe\Decorator\SingletonDecorator)
82
     *
83
     * @param array $config The configuration.
84
     * @return array
85
     */
86 1
    public static function decorateSingleton(array $config): array
87
    {
88 1
        $config['decorator'] = $config['decorator'] ?? [];
89 1
        $config['decorator']['singleton'] = true;
90
91 1
        return $config;
92
    }
93
94
    /**
95
     * Decorate a configuration with provided callback
96
     *
97
     * @param array    $config   The configuration.
98
     * @param callable $callback The callback.
99
     * @return array
100
     */
101 1
    public static function decorateCallback(array $config, callable $callback): array
102
    {
103 1
        $config['decorator'] = $config['decorator'] ?? [];
104 1
        $config['decorator']['callback'] = $callback;
105
106 1
        return $config;
107
    }
108
109
    /**
110
     * Provide extra parameter for autowired entry. The key should be a valid class name.
111
     *
112
     * @param array $extra Extra parameters.
113
     * @return array
114
     */
115 1
    public static function provideParameter(array $extra = []): array
116
    {
117
        return [
118 1
            '$' => 'autowire',
119
            null,
120 1
            $extra,
121
        ];
122
    }
123
124
    /**
125
     * Configuration indicating an autowired entry.
126
     *
127
     * @param string|null $classname The class name. Can be ignored but better use `provideParameter` in that case.
128
     * @param array       $extra     Extra parameters.
129
     * @return array
130
     */
131 1
    public static function produce(?string $classname, array $extra = []): array
132
    {
133
        return [
134 1
            '$' => 'autowire',
135 1
            $classname,
136 1
            $extra,
137
        ];
138
    }
139
140
    /**
141
     * Configuration indicating an instance created by factory.
142
     *
143
     * @param string $classname The class name.
144
     * @param array  $extra     Extra parameters.
145
     * @return array
146
     */
147 1
    public static function instance(string $classname, array $extra = []): array
148
    {
149
        return [
150 1
            '$' => 'instance',
151 1
            $classname,
152 1
            $extra,
153
        ];
154
    }
155
156
    /**
157
     * Configuration indicating an alias
158
     *
159
     * @param string ...$key Multiple keys will be auto joined.
160
     * @return array
161
     */
162 1
    public static function alias(string ...$key): array
163
    {
164
        return [
165 1
            '$' => 'alias',
166 1
            self::join(...$key),
167
        ];
168
    }
169
170
    /**
171
     * Configuration wrapping a builder method
172
     *
173
     * @param callable $builder The builder method.
174
     * @param array    $extra   Extra parameters.
175
     * @return array
176
     */
177 1
    public static function builder(callable $builder, array $extra = []): array
178
    {
179
        return [
180 1
            '$' => 'builder',
181 1
            $builder,
182 1
            $extra,
183
        ];
184
    }
185
186
    /**
187
     * Configuration wraps an arbitary value. For arrays it's recommended to always wrap with this.
188
     *
189
     * @param mixed $value The value.
190
     * @return array
191
     */
192 2
    public static function value($value): array
193
    {
194
        return [
195 2
            '$' => 'value',
196 2
            $value,
197
        ];
198
    }
199
200 1
    protected static function write(Container $container, $key, $value)
201
    {
202 1
        if (is_scalar($value) || is_resource($value)) {
203 1
            $container->set($key, $value);
204 1
            return;
205
        }
206
207
        if (
208 1
            substr($key, -2) === '::'
209 1
            && class_exists(substr($key, 0, -2))
210
        ) {
211 1
            $container->set($key, self::convertArray($value));
212 1
            return;
213
        }
214
215 1
        $recipe = self::convertToRecipe($value);
216
217 1
        if ($recipe instanceof FixedValueRecipe) {
218 1
            $container->set($key, $recipe->getValue());
219
        } else {
220 1
            $container->flush($key);
221 1
            $container->define($key, $recipe);
222
        }
223 1
    }
224
225 1
    protected static function convertArray(array $value): array
226
    {
227 1
        $result = [];
228 1
        foreach ($value as $k => $v) {
229 1
            $result[$k] = self::convertToRecipe($v);
230 1
            if ($result[$k] instanceof FixedValueRecipe) {
231 1
                $result[$k] = $result[$k]->getValue();
232
            }
233
        }
234
235 1
        return $result;
236
    }
237
238 1
    protected static function makeRecipe(array $value): RecipeInterface
239
    {
240 1
        $type = $value['$'];
241 1
        unset($value['$']);
242
243
        if (
244 1
            array_key_exists($type, [
245 1
            'alias' => 1,
246
            'autowire' => 1,
247
            'instance' => 1,
248
            'builder' => 1,
249
            'value' => 1,
250
            ])
251
        ) {
252 1
            $valueDecorator = $value['decorator'] ?? null;
253 1
            unset($value['decorator']);
254
255 1
            $builder = [Container::class, $type];
256 1
            assert(is_callable($builder));
257
            /**
258
             * @var RecipeInterface $recipe
259
             */
260 1
            $recipe = call_user_func_array($builder, $value);
261
262 1
            if ($valueDecorator) {
263 1
                $recipe = self::wrapRecipeWithDecorators($valueDecorator, $recipe);
264
            }
265
266 1
            return $recipe;
267
        }
268
269
        throw new ContainerException("cannot understand given recipe");
270
    }
271
272
    /**
273
     * Apply decorators to a recipe and return the decorated recipe
274
     *
275
     * @param array           $decorators Assoc array of decorator names => options.
276
     * @param RecipeInterface $recipe     The decorated recipe instance.
277
     * @return RecipeInterface
278
     */
279 1
    public static function wrapRecipeWithDecorators(array $decorators, RecipeInterface $recipe): RecipeInterface
280
    {
281 1
        foreach ($decorators as $name => $option) {
282 1
            if (isset(self::$decorators[$name])) {
283
                $decorateFn = [self::$decorators[$name], 'decorate'];
284
                assert(is_callable($decorateFn));
285
                $recipe = call_user_func($decorateFn, $recipe);
286 1
            } elseif (is_subclass_of($name, AbstractRecipeDecorator::class)) {
287 1
                $decorateFn = [$name, 'decorate'];
288 1
                assert(is_callable($decorateFn));
289 1
                $recipe = call_user_func($decorateFn, $recipe);
290
            } else {
291
                throw new ContainerException("cannot understand recipe decorator [$name]");
292
            }
293
294 1
            assert($recipe instanceof AbstractRecipeDecorator);
295 1
            if (!empty($option)) {
296 1
                $recipe->setOption($option);
297
            }
298
        }
299
300 1
        return $recipe;
301
    }
302
303
    /**
304
     * Join multiple strings with air conventional separator `::`
305
     *
306
     * @param string ...$args Parts of the key to be joined.
307
     * @return string
308
     */
309 1
    public static function join(string ...$args): string
310
    {
311 1
        return implode('::', $args);
312
    }
313
}
314