AbstractFactory   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 484
Duplicated Lines 4.34 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 61
lcom 1
cbo 1
dl 21
loc 484
rs 3.52
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 29 8
B create() 0 61 8
A get() 0 12 4
B setBaseClass() 10 26 7
A baseClass() 0 4 1
A setDefaultClass() 11 24 5
A defaultClass() 0 4 1
A setArguments() 0 5 1
A arguments() 0 4 1
A setCallback() 0 5 1
A callback() 0 4 1
A resolve() 0 21 4
A isResolvable() 0 25 5
A createClass() 0 19 4
A resolver() 0 4 1
A map() 0 4 1
A addClassToMap() 0 11 2
A setResolver() 0 5 1
A setMap() 0 9 2
A runCallbacks() 0 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AbstractFactory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractFactory, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Charcoal\Factory;
4
5
use Exception;
6
use InvalidArgumentException;
7
use ReflectionClass;
8
9
/**
10
 * Full implementation, as Abstract class, of the FactoryInterface.
11
 *
12
 * ## Class dependencies:
13
 *
14
 * | Name               | Type       | Description                            |
15
 * | ------------------ | ---------- | -------------------------------------- |
16
 * | `base_class`       | _string_   | Optional. A base class (or interface) to ensure a type of object.
17
 * | `default_class`    | _string_   | Optional. A default class, as fallback when the requested object is not resolvable.
18
 * | `arguments`        | _array_    | Optional. Constructor arguments that will be passed along to created instances.
19
 * | `callback`         | _Callable_ | Optional. A callback function that will be called upon object creation.
20
 * | `resolver`         | _Callable_ | Optional. A class resolver. If none is provided, a default will be used.
21
 * | `resolver_options` | _array_    | Optional. Resolver options (prefix, suffix, capitals and replacements). This is ignored / unused if `resolver` is provided.
22
 *
23
 */
24
abstract class AbstractFactory implements FactoryInterface
25
{
26
    /**
27
     * @var array $resolved
28
     */
29
    protected $resolved = [];
30
31
    /**
32
     * If a base class is set, then it must be ensured that the
33
     * @var string $baseClass
34
     */
35
    private $baseClass = '';
36
    /**
37
     *
38
     * @var string $defaultClass
39
     */
40
    private $defaultClass = '';
41
42
    /**
43
     * @var array $arguments
44
     */
45
    private $arguments;
46
47
    /**
48
     * @var callable $callback
49
     */
50
    private $callback;
51
52
    /**
53
     * Keeps loaded instances in memory, in `[$type => $instance]` format.
54
     * Used with the `get()` method only.
55
     * @var array $instances
56
     */
57
    private $instances = [];
58
59
    /**
60
     * @var callable $resolver
61
     */
62
    private $resolver;
63
64
    /**
65
     * The class map array holds available types, in `[$type => $className]` format.
66
     * @var string[] $map
67
     */
68
    private $map = [];
69
70
    /**
71
     * @param array $data Constructor dependencies.
72
     */
73
    public function __construct(array $data = null)
74
    {
75
        if (isset($data['base_class'])) {
76
            $this->setBaseClass($data['base_class']);
77
        }
78
79
        if (isset($data['default_class'])) {
80
            $this->setDefaultClass($data['default_class']);
81
        }
82
83
        if (isset($data['arguments'])) {
84
            $this->setArguments($data['arguments']);
85
        }
86
87
        if (isset($data['callback'])) {
88
            $this->setCallback($data['callback']);
89
        }
90
91
        if (!isset($data['resolver'])) {
92
            $opts = isset($data['resolver_options']) ? $data['resolver_options'] : null;
93
            $data['resolver'] = new GenericResolver($opts);
94
        }
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
114
     *     (will override the arguments set on the class from constructor).
115
     * @param  callable $cb   Optional. Object callback, called at creation.
116
     *     Will run in addition to the default callback, if any.
117
     * @throws Exception If the base class is set and  the resulting instance is not of the base class.
118
     * @throws InvalidArgumentException If type argument is not a string or is not an available type.
119
     * @return mixed The instance / object
120
     */
121
    final public function create($type, array $args = null, callable $cb = null)
122
    {
123
        if (!is_string($type)) {
124
            throw new InvalidArgumentException(
125
                sprintf(
126
                    '%s: Type must be a string.',
127
                    get_called_class()
128
                )
129
            );
130
        }
131
132
        if (!isset($args)) {
133
            $args = $this->arguments();
134
        }
135
136
        $pool = get_called_class();
137
        if (isset($this->resolved[$pool][$type])) {
138
            $className = $this->resolved[$pool][$type];
139
        } else {
140
            if ($this->isResolvable($type) === false) {
141
                $defaultClass = $this->defaultClass();
142
                if ($defaultClass !== '') {
143
                    $obj = $this->createClass($defaultClass, $args);
144
                    $this->runCallbacks($obj, $cb);
145
                    return $obj;
146
                } else {
147
                    throw new InvalidArgumentException(
148
                        sprintf(
149
                            '%1$s: Type "%2$s" is not a valid type. (Using default class "%3$s")',
150
                            get_called_class(),
151
                            $type,
152
                            $defaultClass
153
                        )
154
                    );
155
                }
156
            }
157
158
            // Create the object from the type's class name.
159
            $className = $this->resolve($type);
160
            $this->resolved[$pool][$type] = $className;
161
        }
162
163
        $obj = $this->createClass($className, $args);
164
165
        // Ensure base class is respected, if set.
166
        $baseClass = $this->baseClass();
167
        if ($baseClass !== '' && !($obj instanceof $baseClass)) {
168
            throw new Exception(
169
                sprintf(
170
                    '%1$s: Class "%2$s" must be an instance of "%3$s"',
171
                    get_called_class(),
172
                    $className,
173
                    $baseClass
174
                )
175
            );
176
        }
177
178
        $this->runCallbacks($obj, $cb);
179
180
        return $obj;
181
    }
182
183
    /**
184
     * Get (load or create) an instance of a class, by type.
185
     *
186
     * Unlike `create()` (which always call a `new` instance),
187
     * this function first tries to load / reuse
188
     * an already created object of this type, from memory.
189
     *
190
     * @param string $type The type (class ident).
191
     * @param array  $args The constructor arguments (optional).
192
     * @throws InvalidArgumentException If type argument is not a string.
193
     * @return mixed The instance / object
194
     */
195
    final public function get($type, array $args = null)
196
    {
197
        if (!is_string($type)) {
198
            throw new InvalidArgumentException(
199
                'Type must be a string.'
200
            );
201
        }
202
        if (!isset($this->instances[$type]) || $this->instances[$type] === null) {
203
            $this->instances[$type] = $this->create($type, $args);
204
        }
205
        return $this->instances[$type];
206
    }
207
208
209
    /**
210
     * If a base class is set, then it must be ensured that the created objects
211
     * are `instanceof` this base class.
212
     *
213
     * @param string $type The FQN of the class, or "type" of object, to set as base class.
214
     * @throws InvalidArgumentException If the class is not a string or is not an existing class / interface.
215
     * @return self
216
     */
217
    public function setBaseClass($type)
218
    {
219
        if (!is_string($type) || empty($type)) {
220
            throw new InvalidArgumentException(
221
                'Class name or type must be a non-empty string.'
222
            );
223
        }
224
225
        $exists = (class_exists($type) || interface_exists($type));
226
        if ($exists) {
227
            $className = $type;
228 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...
229
            $className = $this->resolve($type);
230
231
            $exists = (class_exists($className) || interface_exists($className));
232
            if (!$exists) {
233
                throw new InvalidArgumentException(
234
                    sprintf('Can not set "%s" as base class: Invalid class or interface name.', $className)
235
                );
236
            }
237
        }
238
239
        $this->baseClass = $className;
240
241
        return $this;
242
    }
243
244
    /**
245
     * @return string The FQN of the base class
246
     */
247
    public function baseClass()
248
    {
249
        return $this->baseClass;
250
    }
251
252
    /**
253
     * If a default class is set, then calling `get()` or `create()` an invalid type
254
     * should return an object of this class instead of throwing an error.
255
     *
256
     * @param string $type The FQN of the class, or "type" of object, to set as default class.
257
     * @throws InvalidArgumentException If the class name is not a string or not a valid class.
258
     * @return self
259
     */
260
    public function setDefaultClass($type)
261
    {
262
        if (!is_string($type) || empty($type)) {
263
            throw new InvalidArgumentException(
264
                'Class name or type must be a non-empty string.'
265
            );
266
        }
267
268 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...
269
            $className = $type;
270
        } else {
271
            $className = $this->resolve($type);
272
273
            if (!class_exists($className)) {
274
                throw new InvalidArgumentException(
275
                    sprintf('Can not set "%s" as defaut class: Invalid class name.', $className)
276
                );
277
            }
278
        }
279
280
        $this->defaultClass = $className;
281
282
        return $this;
283
    }
284
285
    /**
286
     * @return string The FQN of the default class
287
     */
288
    public function defaultClass()
289
    {
290
        return $this->defaultClass;
291
    }
292
293
    /**
294
     * @param array $arguments The constructor arguments to be passed to the created object's initialization.
295
     * @return self
296
     */
297
    public function setArguments(array $arguments)
298
    {
299
        $this->arguments = $arguments;
300
        return $this;
301
    }
302
303
    /**
304
     * @return array
305
     */
306
    public function arguments()
307
    {
308
        return $this->arguments;
309
    }
310
311
    /**
312
     * @param callable $callback The object callback.
313
     * @return self
314
     */
315
    public function setCallback(callable $callback)
316
    {
317
        $this->callback = $callback;
318
        return $this;
319
    }
320
321
    /**
322
     * @return callable|null
323
     */
324
    public function callback()
325
    {
326
        return $this->callback;
327
    }
328
329
    /**
330
     * The Generic factory resolves the class name from an exact FQN.
331
     *
332
     * @param string $type The "type" of object to resolve (the object ident).
333
     * @throws InvalidArgumentException If the type parameter is not a string.
334
     * @return string The resolved class name (FQN).
335
     */
336
    public function resolve($type)
337
    {
338
        if (!is_string($type)) {
339
            throw new InvalidArgumentException(
340
                'Can not resolve class ident: type must be a string'
341
            );
342
        }
343
344
        $map = $this->map();
345
        if (isset($map[$type])) {
346
            $type = $map[$type];
347
        }
348
349
        if (class_exists($type)) {
350
            return $type;
351
        }
352
353
        $resolver = $this->resolver();
354
        $resolved = $resolver($type);
355
        return $resolved;
356
    }
357
358
    /**
359
     * Whether a `type` is resolvable. The Generic Factory simply checks if the _FQN_ `type` class exists.
360
     *
361
     * @param string $type The "type" of object to resolve (the object ident).
362
     * @throws InvalidArgumentException If the type parameter is not a string.
363
     * @return boolean
364
     */
365
    public function isResolvable($type)
366
    {
367
        if (!is_string($type)) {
368
            throw new InvalidArgumentException(
369
                'Can not check resolvable: type must be a string'
370
            );
371
        }
372
373
        $map = $this->map();
374
        if (isset($map[$type])) {
375
            $type = $map[$type];
376
        }
377
378
        if (class_exists($type)) {
379
            return true;
380
        }
381
382
        $resolver = $this->resolver();
383
        $resolved = $resolver($type);
384
        if (class_exists($resolved)) {
385
            return true;
386
        }
387
388
        return false;
389
    }
390
391
392
    /**
393
     * Create a class instance with given arguments.
394
     *
395
     * How the constructor arguments are passed depends on its type:
396
     *
397
     * - if null, no arguments are passed at all.
398
     * - if it's not an array, it's passed as a single argument.
399
     * - if it's an associative array, it's passed as a sing argument.
400
     * - if it's a sequential (numeric keys) array, it's
401
     *
402
     * @param string $className The FQN of the class to instanciate.
403
     * @param mixed  $args      The constructor arguments.
404
     * @return mixed The created object.
405
     */
406
    protected function createClass($className, $args)
407
    {
408
        if ($args === null) {
409
            return new $className;
410
        }
411
        if (!is_array($args)) {
412
            return new $className($args);
413
        }
414
        if (count(array_filter(array_keys($args), 'is_string')) > 0) {
415
            return new $className($args);
416
        } else {
417
            /**
418
             * @todo Use argument unpacking (`return new $className(...$args);`)
419
             *     when minimum PHP requirement is bumped to 5.6.
420
             */
421
            $reflection = new ReflectionClass($className);
422
            return $reflection->newInstanceArgs($args);
423
        }
424
    }
425
426
    /**
427
     * @return callable
428
     */
429
    protected function resolver()
430
    {
431
        return $this->resolver;
432
    }
433
434
    /**
435
     * Get the map of all types in `[$type => $class]` format.
436
     *
437
     * @return string[]
438
     */
439
    protected function map()
440
    {
441
        return $this->map;
442
    }
443
444
    /**
445
     * Add a class name to the available types _map_.
446
     *
447
     * @param string $type      The type (class ident).
448
     * @param string $className The FQN of the class.
449
     * @throws InvalidArgumentException If the $type parameter is not a striing or the $className class does not exist.
450
     * @return self
451
     */
452
    protected function addClassToMap($type, $className)
453
    {
454
        if (!is_string($type)) {
455
            throw new InvalidArgumentException(
456
                'Type (class key) must be a string'
457
            );
458
        }
459
460
        $this->map[$type] = $className;
461
        return $this;
462
    }
463
464
    /**
465
     * @param callable $resolver The class resolver instance to use.
466
     * @return self
467
     */
468
    private function setResolver(callable $resolver)
469
    {
470
        $this->resolver = $resolver;
471
        return $this;
472
    }
473
474
    /**
475
     * Add multiple types, in a an array of `type` => `className`.
476
     *
477
     * @param string[] $map The map (key=>classname) to use.
478
     * @return self
479
     */
480
    private function setMap(array $map)
481
    {
482
        // Resets (overwrites) map.
483
        $this->map = [];
484
        foreach ($map as $type => $className) {
485
            $this->addClassToMap($type, $className);
486
        }
487
        return $this;
488
    }
489
490
    /**
491
     * Run the callback(s) on the object, if applicable.
492
     *
493
     * @param mixed    $obj            The object to pass to callback(s).
494
     * @param callable $customCallback An optional additional custom callback.
495
     * @return void
496
     */
497
    private function runCallbacks(&$obj, callable $customCallback = null)
498
    {
499
        $factoryCallback = $this->callback();
500
        if (isset($factoryCallback)) {
501
            $factoryCallback($obj);
502
        }
503
        if (isset($customCallback)) {
504
            $customCallback($obj);
505
        }
506
    }
507
}
508