Completed
Push — master ( 25ddb4...e271ef )
by Mathieu
03:37
created

AbstractFactory::runCallbacks()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 4
nop 2
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
     * @param string   $type The type (class ident).
113
     * @param array    $args Optional. Constructor arguments (will override the arguments set on the class from constructor).
114
     * @param callable $cb   Optional. Object callback, called at creation. Will run in addition to the default callback, if any.
115
     * @throws Exception If the base class is set and  the resulting instance is not of the base class.
116
     * @throws InvalidArgumentException If type argument is not a string or is not an available type.
117
     * @return mixed The instance / object
118
     */
119
    final public function create($type, array $args = null, callable $cb = null)
120
    {
121
        if (!is_string($type)) {
122
            throw new InvalidArgumentException(
123
                sprintf(
124
                    '%s: Type must be a string.',
125
                    get_called_class()
126
                )
127
            );
128
        }
129
130
        if (!isset($args)) {
131
            $args = $this->arguments();
132
        }
133
134
        $pool = get_called_class();
135
        if (isset(self::$resolved[$pool][$type])) {
136
            $classname = self::$resolved[$pool][$type];
137
        } else {
138
            if ($this->isResolvable($type) === false) {
139
                $defaultClass = $this->defaultClass();
140
                if ($defaultClass !== '') {
141
                    $obj = $this->createClass($defaultClass, $args);
142
                    $this->runCallbacks($obj, $cb);
143
                    return $obj;
144
                } else {
145
                    throw new InvalidArgumentException(
146
                        sprintf(
147
                            '%1$s: Type "%2$s" is not a valid type. (Using default class "%3$s")',
148
                            get_called_class(),
149
                            $type,
150
                            $defaultClass
151
                        )
152
                    );
153
                }
154
            }
155
156
            // Create the object from the type's class name.
157
            $classname = $this->resolve($type);
158
            self::$resolved[$pool][$type] = $classname;
159
        }
160
161
        $obj = $this->createClass($classname, $args);
162
163
        // Ensure base class is respected, if set.
164
        $baseClass = $this->baseClass();
165
        if ($baseClass !== '' && !($obj instanceof $baseClass)) {
166
            throw new Exception(
167
                sprintf(
168
                    '%1$s: Object is not a valid "%2$s" class',
169
                    get_called_class(),
170
                    $baseClass
171
                )
172
            );
173
        }
174
175
        $this->runCallbacks($obj, $cb);
176
177
        return $obj;
178
    }
179
180
    /**
181
     * Run the callback(s) on the object, if applicable.
182
     *
183
     * @param mixed    $obj            The object to pass to callback(s).
184
     * @param callable $customCallback An optional additional custom callback.
185
     * @return void
186
     */
187
    private function runCallbacks(&$obj, callable $customCallback = null)
188
    {
189
        $factoryCallback = $this->callback();
190
        if (isset($factoryCallback)) {
191
            $factoryCallback($obj);
192
        }
193
        if (isset($customCallback)) {
194
            $customCallback($obj);
195
        }
196
    }
197
198
    /**
199
     * Create a class instance with given arguments.
200
     *
201
     * How the constructor arguments are passed depends on its type:
202
     *
203
     * - if null, no arguments are passed at all.
204
     * - if it's not an array, it's passed as a single argument.
205
     * - if it's an associative array, it's passed as a sing argument.
206
     * - if it's a sequential (numeric keys) array, it's
207
     *
208
     * @param string $classname The FQN of the class to instanciate.
209
     * @param mixed  $args      The constructor arguments.
210
     * @return mixed The created object.
211
     */
212
    protected function createClass($classname, $args)
213
    {
214
        if ($args === null) {
215
            return new $classname;
216
        }
217
        if (!is_array($args)) {
218
            return new $classname($args);
219
        }
220
        if (count(array_filter(array_keys($args), 'is_string')) > 0) {
221
            return new $classname($args);
222
        } else {
223
            // Use argument unpacking (`return new $classname(...$args);`) when minimum PHP requirement is bumped to 5.6.
224
            $reflection = new \ReflectionClass($classname);
225
            return $reflection->newInstanceArgs($args);
226
        }
227
    }
228
229
    /**
230
     * Get (load or create) an instance of a class, by type.
231
     *
232
     * Unlike `create()` (which always call a `new` instance), this function first tries to load / reuse
233
     * an already created object of this type, from memory.
234
     *
235
     * @param string $type The type (class ident).
236
     * @param array  $args The constructor arguments (optional).
237
     * @throws InvalidArgumentException If type argument is not a string.
238
     * @return mixed The instance / object
239
     */
240
    final public function get($type, array $args = null)
241
    {
242
        if (!is_string($type)) {
243
            throw new InvalidArgumentException(
244
                'Type must be a string.'
245
            );
246
        }
247
        if (!isset($this->instances[$type]) || $this->instances[$type] === null) {
248
            $this->instances[$type] = $this->create($type, $args);
249
        }
250
        return $this->instances[$type];
251
    }
252
253
    /**
254
     * @param callable $resolver The class resolver instance to use.
255
     * @return FactoryInterface Chainable
256
     */
257
    private function setResolver(callable $resolver)
258
    {
259
        $this->resolver = $resolver;
260
        return $this;
261
    }
262
263
    /**
264
     * @return callable
265
     */
266
    protected function resolver()
267
    {
268
        return $this->resolver;
269
    }
270
271
    /**
272
     * Add multiple types, in a an array of `type` => `className`.
273
     *
274
     * @param string[] $map The map (key=>classname) to use.
275
     * @return FactoryInterface Chainable
276
     */
277
    private function setMap(array $map)
278
    {
279
        // Resets (overwrites) map.
280
        $this->map = [];
281
        foreach ($map as $type => $className) {
282
            $this->addClassToMap($type, $className);
283
        }
284
        return $this;
285
    }
286
287
    /**
288
     * Get the map of all types in `[$type => $class]` format.
289
     *
290
     * @return string[]
291
     */
292
    protected function map()
293
    {
294
        return $this->map;
295
    }
296
297
    /**
298
     * Add a class name to the available types _map_.
299
     *
300
     * @param string $type      The type (class ident).
301
     * @param string $className The FQN of the class.
302
     * @throws InvalidArgumentException If the $type parameter is not a striing or the $className class does not exist.
303
     * @return FactoryInterface Chainable
304
     */
305
    protected function addClassToMap($type, $className)
306
    {
307
        if (!is_string($type)) {
308
            throw new InvalidArgumentException(
309
                'Type (class key) must be a string'
310
            );
311
        }
312
313
        $this->map[$type] = $className;
314
        return $this;
315
    }
316
317
    /**
318
     * If a base class is set, then it must be ensured that the created objects
319
     * are `instanceof` this base class.
320
     *
321
     * @param string $type The FQN of the class, or "type" of object, to set as base class.
322
     * @throws InvalidArgumentException If the class is not a string or is not an existing class / interface.
323
     * @return FactoryInterface Chainable
324
     */
325
    public function setBaseClass($type)
326
    {
327
        if (!is_string($type) || empty($type)) {
328
            throw new InvalidArgumentException(
329
                'Class name or type must be a non-empty string.'
330
            );
331
        }
332
333
        $exists = (class_exists($type) || interface_exists($type));
334
        if ($exists) {
335
            $classname = $type;
336 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...
337
            $classname = $this->resolve($type);
338
339
            $exists = (class_exists($classname) || interface_exists($classname));
340
            if (!$exists) {
341
                throw new InvalidArgumentException(
342
                    sprintf('Can not set "%s" as base class: Invalid class or interface name.', $classname)
343
                );
344
            }
345
        }
346
347
        $this->baseClass = $classname;
348
349
        return $this;
350
    }
351
352
    /**
353
     * @return string The FQN of the base class
354
     */
355
    public function baseClass()
356
    {
357
        return $this->baseClass;
358
    }
359
360
    /**
361
     * If a default class is set, then calling `get()` or `create()` an invalid type
362
     * should return an object of this class instead of throwing an error.
363
     *
364
     * @param string $type The FQN of the class, or "type" of object, to set as default class.
365
     * @throws InvalidArgumentException If the class name is not a string or not a valid class.
366
     * @return FactoryInterface Chainable
367
     */
368
    public function setDefaultClass($type)
369
    {
370
        if (!is_string($type) || empty($type)) {
371
            throw new InvalidArgumentException(
372
                'Class name or type must be a non-empty string.'
373
            );
374
        }
375
376 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...
377
            $classname = $type;
378
        } else {
379
            $classname = $this->resolve($type);
380
381
            if (!class_exists($classname)) {
382
                throw new InvalidArgumentException(
383
                    sprintf('Can not set "%s" as defaut class: Invalid class name.', $classname)
384
                );
385
            }
386
        }
387
388
        $this->defaultClass = $classname;
389
390
        return $this;
391
    }
392
393
    /**
394
     * @return string The FQN of the default class
395
     */
396
    public function defaultClass()
397
    {
398
        return $this->defaultClass;
399
    }
400
401
    /**
402
     * @param array $arguments The constructor arguments to be passed to the created object's initialization.
403
     * @return FactoryInterface Chainable
404
     */
405
    public function setArguments(array $arguments)
406
    {
407
        $this->arguments = $arguments;
408
        return $this;
409
    }
410
411
    /**
412
     * @return array
413
     */
414
    public function arguments()
415
    {
416
        return $this->arguments;
417
    }
418
419
    /**
420
     * @param callable $callback The object callback.
421
     * @return FactoryInterface Chainable
422
     */
423
    public function setCallback(callable $callback)
424
    {
425
        $this->callback = $callback;
426
        return $this;
427
    }
428
429
    /**
430
     * @return callable|null
431
     */
432
    public function callback()
433
    {
434
        return $this->callback;
435
    }
436
437
 /**
438
  * The Generic factory resolves the class name from an exact FQN.
439
  *
440
  * @param string $type The "type" of object to resolve (the object ident).
441
  * @throws InvalidArgumentException If the type parameter is not a string.
442
  * @return string The resolved class name (FQN).
443
  */
444
    public function resolve($type)
445
    {
446
        if (!is_string($type)) {
447
            throw new InvalidArgumentException(
448
                'Can not resolve class ident: type must be a string'
449
            );
450
        }
451
452
        $map = $this->map();
453
        if (isset($map[$type])) {
454
            $type = $map[$type];
455
        }
456
457
        if (class_exists($type)) {
458
            return $type;
459
        }
460
461
        $resolver = $this->resolver();
462
        $resolved = $resolver($type);
463
        return $resolved;
464
    }
465
466
    /**
467
     * Wether a `type` is resolvable. The Generic Factory simply checks if the _FQN_ `type` class exists.
468
     *
469
     * @param string $type The "type" of object to resolve (the object ident).
470
     * @throws InvalidArgumentException If the type parameter is not a string.
471
     * @return boolean
472
     */
473
    public function isResolvable($type)
474
    {
475
        if (!is_string($type)) {
476
            throw new InvalidArgumentException(
477
                'Can not check resolvable: type must be a string'
478
            );
479
        }
480
481
        $map = $this->map();
482
        if (isset($map[$type])) {
483
            $type = $map[$type];
484
        }
485
486
        if (class_exists($type)) {
487
            return true;
488
        }
489
490
        $resolver = $this->resolver();
491
        $resolved = $resolver($type);
492
        if (class_exists($resolved)) {
493
            return true;
494
        }
495
496
        return false;
497
    }
498
}
499