Completed
Push — master ( 678c0d...3e10a5 )
by Mathieu
02:10
created

AbstractFactory::setMap()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
namespace Charcoal\Factory;
4
5
// Dependencies from `PHP`
6
use \Exception;
7
use \InvalidArgumentException;
8
9
// Local namespace dependencies
10
use \Charcoal\Factory\FactoryInterface;
11
use \Charcoal\Factory\GenericResolver;
12
13
/**
14
 * Full implementation, as Abstract class, of the FactoryInterface.
15
 *
16
 * ## Class dependencies:
17
 *
18
 * | Name               | Type       | Description                            |
19
 * | ------------------ | ---------- | -------------------------------------- |
20
 * | `base_class`       | _string_   | Optional. A base class (or interface) to ensure a type of object.
21
 * | `default_class`    | _string_   | Optional. A default class, as fallback when the requested object is not resolvable.
22
 * | `arguments`        | _array_    | Optional. Constructor arguments that will be passed along to created instances.
23
 * | `callback`         | _Callable_ | Optional. A callback function that will be called upon object creation.
24
 * | `resolver`         | _Callable_ | Optional. A class resolver. If none is provided, a default will be used.
25
 * | `resolver_options` | _array_    | Optional. Resolver options (prefix, suffix, capitals and replacements). This is ignored / unused if `resolver` is provided.
26
 *
27
 */
28
abstract class AbstractFactory implements FactoryInterface
29
{
30
    /**
31
     * @var array $resolved
32
     */
33
    static protected $resolved = [];
34
35
    /**
36
     * If a base class is set, then it must be ensured that the
37
     * @var string $baseClass
38
     */
39
    private $baseClass = '';
40
    /**
41
     *
42
     * @var string $defaultClass
43
     */
44
    private $defaultClass = '';
45
46
    /**
47
     * @var array $arguments
48
     */
49
    private $arguments;
50
51
    /**
52
     * @var callable $callback
53
     */
54
    private $callback;
55
56
    /**
57
     * Keeps loaded instances in memory, in `[$type => $instance]` format.
58
     * Used with the `get()` method only.
59
     * @var array $instances
60
     */
61
    private $instances = [];
62
63
    /**
64
     * @var Callable $resolver
65
     */
66
    private $resolver;
67
68
    /**
69
     * The class map array holds available types, in `[$type => $className]` format.
70
     * @var string[] $map
71
     */
72
    private $map = [];
73
74
    /**
75
     * @param array $data Constructor dependencies.
76
     */
77
    public function __construct(array $data = null)
78
    {
79
        if (isset($data['base_class'])) {
80
            $this->setBaseClass($data['base_class']);
81
        }
82
        if (isset($data['default_class'])) {
83
            $this->setDefaultClass($data['default_class']);
84
        }
85
        if (isset($data['arguments'])) {
86
            $this->setArguments($data['arguments']);
87
        }
88
        if (isset($data['callback'])) {
89
            $this->setCallback($data['callback']);
90
        }
91
92
        if (!isset($data['resolver'])) {
93
            $opts = isset($data['resolver_options']) ? $data['resolver_options'] : null;
94
            $data['resolver'] = new GenericResolver($opts);
95
        }
96
        $this->setResolver($data['resolver']);
97
98
        if (isset($data['map'])) {
99
            $this->setMap($data['map']);
100
        }
101
    }
102
103
    /**
104
     * Create a new instance of a class, by type.
105
     *
106
     * Unlike `get()`, this method *always* return a new instance of the requested class.
107
     *
108
     * ## Object callback
109
     * It is possible to pass a callback method that will be executed upon object instanciation.
110
     * The callable should have a signature: `function($obj);` where $obj is the newly created object.
111
     *
112
     *
113
     * @param string   $type The type (class ident).
114
     * @param array    $args Optional. Constructor arguments (will override the arguments set on the class from constructor).
115
     * @param callable $cb   Optional. Object callback, called at creation. Leave blank to use `$callback` member.
116
     * @throws Exception If the base class is set and  the resulting instance is not of the base class.
117
     * @throws InvalidArgumentException If type argument is not a string or is not an available type.
118
     * @return mixed The instance / object
119
     */
120
    final public function create($type, array $args = null, callable $cb = null)
121
    {
122
        if (!is_string($type)) {
123
            throw new InvalidArgumentException(
124
                sprintf(
125
                    '%s: Type must be a string.',
126
                    get_called_class()
127
                )
128
            );
129
        }
130
131
        if (!isset($args)) {
132
            $args = $this->arguments();
133
        }
134
135
        if (!isset($cb)) {
136
            $cb = $this->callback();
137
        }
138
139
        $pool = get_called_class();
140
        if (isset(self::$resolved[$pool][$type])) {
141
            $classname = self::$resolved[$pool][$type];
142
        } else {
143
            if ($this->isResolvable($type) === false) {
144
                $defaultClass = $this->defaultClass();
145
                if ($defaultClass !== '') {
146
                    $obj = $this->createClass($defaultClass, $args);
0 ignored issues
show
Unused Code introduced by
$obj is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
147
                    $obj = new $defaultClass($args);
148
                    if (isset($cb)) {
149
                        $cb($obj);
150
                    }
151
                    return $obj;
152
                } else {
153
                    throw new InvalidArgumentException(
154
                        sprintf(
155
                            '%1$s: Type "%2$s" is not a valid type. (Using default class "%3$s")',
156
                            get_called_class(),
157
                            $type,
158
                            $defaultClass
159
                        )
160
                    );
161
                }
162
            }
163
164
            // Create the object from the type's class name.
165
            $classname = $this->resolve($type);
166
            self::$resolved[$pool][$type] = $classname;
167
        }
168
169
        $obj = $this->createClass($classname, $args);
170
171
        // Ensure base class is respected, if set.
172
        $baseClass = $this->baseClass();
173
        if ($baseClass !== '' && !($obj instanceof $baseClass)) {
174
            throw new Exception(
175
                sprintf(
176
                    '%1$s: Object is not a valid "%2$s" class',
177
                    get_called_class(),
178
                    $baseClass
179
                )
180
            );
181
        }
182
183
        if (isset($cb)) {
184
            $cb($obj);
185
        }
186
187
        return $obj;
188
    }
189
190
    /**
191
     * Create a class instance with given arguments.
192
     *
193
     * How the constructor arguments are passed depends on its type:
194
     *
195
     * - if null, no arguments are passed at all.
196
     * - if it's not an array, it's passed as a single argument.
197
     * - if it's an associative array, it's passed as a sing argument.
198
     * - if it's a sequential (numeric keys) array, it's
199
     *
200
     * @param string $classname The FQN of the class to instanciate.
201
     * @param mixed  $args      The constructor arguments.
202
     * @return mixed The created object.
203
     */
204
    protected function createClass($classname, $args)
205
    {
206
        if ($args === null) {
207
            return new $classname;
208
        }
209
        if (!is_array($args)) {
210
            return new $classname($args);
211
        }
212
        if (count(array_filter(array_keys($args), 'is_string')) > 0) {
213
            return new $classname($args);
214
        } else {
215
            // Use argument unpacking (`return new $classname(...$args);`) when minimum PHP requirement is bumped to 5.6.
216
            $reflection = new \ReflectionClass($classname);
217
            return $reflection->newInstanceArgs($args);
218
        }
219
    }
220
221
    /**
222
     * Get (load or create) an instance of a class, by type.
223
     *
224
     * Unlike `create()` (which always call a `new` instance), this function first tries to load / reuse
225
     * an already created object of this type, from memory.
226
     *
227
     * @param string $type The type (class ident).
228
     * @param array  $args The constructor arguments (optional).
229
     * @throws InvalidArgumentException If type argument is not a string.
230
     * @return mixed The instance / object
231
     */
232
    final public function get($type, array $args = null)
233
    {
234
        if (!is_string($type)) {
235
            throw new InvalidArgumentException(
236
                'Type must be a string.'
237
            );
238
        }
239
        if (!isset($this->instances[$type]) || $this->instances[$type] === null) {
240
            $this->instances[$type] = $this->create($type, $args);
241
        }
242
        return $this->instances[$type];
243
    }
244
245
    /**
246
     * @param callable $resolver The class resolver instance to use.
247
     * @return FactoryInterface Chainable
248
     */
249
    private function setResolver(callable $resolver)
250
    {
251
        $this->resolver = $resolver;
252
        return $this;
253
    }
254
255
    /**
256
     * @return callable
257
     */
258
    protected function resolver()
259
    {
260
        return $this->resolver;
261
    }
262
263
    /**
264
     * Add multiple types, in a an array of `type` => `className`.
265
     *
266
     * @param string[] $map The map (key=>classname) to use.
267
     * @return FactoryInterface Chainable
268
     */
269
    private function setMap(array $map)
270
    {
271
        // Resets (overwrites) map.
272
        $this->map = [];
273
        foreach ($map as $type => $className) {
274
            $this->addClassToMap($type, $className);
275
        }
276
        return $this;
277
    }
278
279
    /**
280
     * Get the map of all types in `[$type => $class]` format.
281
     *
282
     * @return string[]
283
     */
284
    protected function map()
285
    {
286
        return $this->map;
287
    }
288
289
    /**
290
     * Add a class name to the available types _map_.
291
     *
292
     * @param string $type      The type (class ident).
293
     * @param string $className The FQN of the class.
294
     * @throws InvalidArgumentException If the $type parameter is not a striing or the $className class does not exist.
295
     * @return FactoryInterface Chainable
296
     */
297
    protected function addClassToMap($type, $className)
298
    {
299
        if (!is_string($type)) {
300
            throw new InvalidArgumentException(
301
                'Type (class key) must be a string'
302
            );
303
        }
304
305
        $this->map[$type] = $className;
306
        return $this;
307
    }
308
309
    /**
310
     * If a base class is set, then it must be ensured that the created objects
311
     * are `instanceof` this base class.
312
     *
313
     * @param string $type The FQN of the class, or "type" of object, to set as base class.
314
     * @throws InvalidArgumentException If the class is not a string or is not an existing class / interface.
315
     * @return FactoryInterface Chainable
316
     */
317
    public function setBaseClass($type)
318
    {
319
        if (!is_string($type) || empty($type)) {
320
            throw new InvalidArgumentException(
321
                'Class name or type must be a non-empty string.'
322
            );
323
        }
324
325
        $exists = (class_exists($type) || interface_exists($type));
326
        if ($exists) {
327
            $classname = $type;
328 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
329
            $classname = $this->resolve($type);
330
331
            $exists = (class_exists($classname) || interface_exists($classname));
332
            if (!$exists) {
333
                throw new InvalidArgumentException(
334
                    sprintf('Can not set "%s" as base class: Invalid class or interface name.', $classname)
335
                );
336
            }
337
        }
338
339
        $this->baseClass = $classname;
340
341
        return $this;
342
    }
343
344
    /**
345
     * @return string The FQN of the base class
346
     */
347
    public function baseClass()
348
    {
349
        return $this->baseClass;
350
    }
351
352
    /**
353
     * If a default class is set, then calling `get()` or `create()` an invalid type
354
     * should return an object of this class instead of throwing an error.
355
     *
356
     * @param string $type The FQN of the class, or "type" of object, to set as default class.
357
     * @throws InvalidArgumentException If the class name is not a string or not a valid class.
358
     * @return FactoryInterface Chainable
359
     */
360
    public function setDefaultClass($type)
361
    {
362
        if (!is_string($type) || empty($type)) {
363
            throw new InvalidArgumentException(
364
                'Class name or type must be a non-empty string.'
365
            );
366
        }
367
368 View Code Duplication
        if (class_exists($type)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
369
            $classname = $type;
370
        } else {
371
            $classname = $this->resolve($type);
372
373
            if (!class_exists($classname)) {
374
                throw new InvalidArgumentException(
375
                    sprintf('Can not set "%s" as defaut class: Invalid class name.', $classname)
376
                );
377
            }
378
        }
379
380
        $this->defaultClass = $classname;
381
382
        return $this;
383
    }
384
385
    /**
386
     * @return string The FQN of the default class
387
     */
388
    public function defaultClass()
389
    {
390
        return $this->defaultClass;
391
    }
392
393
    /**
394
     * @param array $arguments The constructor arguments to be passed to the created object's initialization.
395
     * @return FactoryInterface Chainable
396
     */
397
    public function setArguments(array $arguments)
398
    {
399
        $this->arguments = $arguments;
400
        return $this;
401
    }
402
403
    /**
404
     * @return array
405
     */
406
    public function arguments()
407
    {
408
        return $this->arguments;
409
    }
410
411
    /**
412
     * @param callable $callback The object callback.
413
     * @return FactoryInterface Chainable
414
     */
415
    public function setCallback(callable $callback)
416
    {
417
        $this->callback = $callback;
418
        return $this;
419
    }
420
421
    /**
422
     * @return callable|null
423
     */
424
    public function callback()
425
    {
426
        return $this->callback;
427
    }
428
429
 /**
430
  * The Generic factory resolves the class name from an exact FQN.
431
  *
432
  * @param string $type The "type" of object to resolve (the object ident).
433
  * @throws InvalidArgumentException If the type parameter is not a string.
434
  * @return string The resolved class name (FQN).
435
  */
436
    public function resolve($type)
437
    {
438
        if (!is_string($type)) {
439
            throw new InvalidArgumentException(
440
                'Can not resolve class ident: type must be a string'
441
            );
442
        }
443
444
        $map = $this->map();
445
        if (isset($map[$type])) {
446
            $type = $map[$type];
447
        }
448
449
        if (class_exists($type)) {
450
            return $type;
451
        }
452
453
        $resolver = $this->resolver();
454
        $resolved = $resolver($type);
455
        return $resolved;
456
    }
457
458
    /**
459
     * Wether a `type` is resolvable. The Generic Factory simply checks if the _FQN_ `type` class exists.
460
     *
461
     * @param string $type The "type" of object to resolve (the object ident).
462
     * @throws InvalidArgumentException If the type parameter is not a string.
463
     * @return boolean
464
     */
465
    public function isResolvable($type)
466
    {
467
        if (!is_string($type)) {
468
            throw new InvalidArgumentException(
469
                'Can not check resolvable: type must be a string'
470
            );
471
        }
472
473
        $map = $this->map();
474
        if (isset($map[$type])) {
475
            $type = $map[$type];
476
        }
477
478
        if (class_exists($type)) {
479
            return true;
480
        }
481
482
        $resolver = $this->resolver();
483
        $resolved = $resolver($type);
484
        if (class_exists($resolved)) {
485
            return true;
486
        }
487
488
        return false;
489
    }
490
}
491