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 { |
|
|
|
|
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)) { |
|
|
|
|
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
|
|
|
|
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.