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