Overview

Namespaces

  • Charcoal
    • Factory

Classes

  • Charcoal\Factory\AbstractFactory
  • Charcoal\Factory\GenericFactory
  • Charcoal\Factory\GenericResolver
  • Charcoal\Factory\MapFactory
  • Charcoal\Factory\ResolverFactory

Interfaces

  • Charcoal\Factory\FactoryInterface
  • Overview
  • Namespace
  • Class
  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: 
API documentation generated by ApiGen