Configurator::singleton()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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