Source Code: SmartFactory/ObjectFactory.php

  1: <?php
  2: /**
  3:  * This file contains the implementation of the class ObjectFactory
  4:  * for creation of the objects.
  5:  *
  6:  * @package Factory
  7:  *
  8:  * @author Oleg Schildt
  9:  */
 10: 
 11: namespace SmartFactory;
 12: 
 13: /**
 14:  * Class for creation of the objects.
 15:  *
 16:  * The class ObjectFactory is an auxiliary class that empowers the factory methods. It provides the methods
 17:  * for binding of class implementations to the interfaces and for creation of objects for the requested interface.
 18:  *
 19:  * @author Oleg Schildt
 20:  */
 21: class ObjectFactory
 22: {
 23:     /**
 24:      * Internal array for storing the singleton instances for re-using.
 25:      *
 26:      * @var array
 27:      *
 28:      * @author Oleg Schildt
 29:      */
 30:     static protected array $singletons = [];
 31:     
 32:     /**
 33:      * Internal array for storing the mapping between interfaces/classes and the bound classes.
 34:      *
 35:      * @var array
 36:      *
 37:      * @author Oleg Schildt
 38:      */
 39:     static protected array $itable = [];
 40:     
 41:     /**
 42:      * Binds a class to an interface or a parent class.
 43:      *
 44:      * The key point of this approach is the definition common interfaces
 45:      * and implementation of them in classes. When an object that supports an
 46:      * interface is requested, an instance of the corresponding bound class
 47:      * is created. When you want to change the class, you need just bind
 48:      * the new class to the interface.
 49:      *
 50:      * You can also bind a class to itself and request it by own name in factory
 51:      * method. And later, you can implement a derived class and change the binding
 52:      * without affecting the code in the business logic.
 53:      *
 54:      * @param object|string $interface_or_class
 55:      * Name of the class/interface as string or the class/interface.
 56:      *
 57:      * @param object|string $class
 58:      * The class which instance should be created if the object is requested.
 59:      *
 60:      * @param ?callable $init_function
 61:      * The optional initialization function. You can provide it to do some
 62:      * custom initialization. The signature of
 63:      * this function is:
 64:      *
 65:      * ```php
 66:      * function (object $instance) : void;
 67:      * ```
 68:      *
 69:      * - $instance - the created instance.
 70:      *
 71:      * Example:
 72:      *
 73:      * ```php
 74:      * ObjectFactory::bindClass(ILanguageManager::class, LanguageManager::class, function($instance) {
 75:      *   $instance->detectLanguage();
 76:      * });
 77:      *
 78:      * ObjectFactory::bindClass(IRecordsetManager::class, RecordsetManager::class, function($instance) {
 79:      *   $instance->setDBWorker(dbworker());
 80:      * });
 81:      * ```
 82:      *
 83:      * @return void
 84:      *
 85:      * @throws \Exception
 86:      * It might throw the following exceptions in the case of any errors:
 87:      *
 88:      * - if the interface or bound class is not specified.
 89:      * - if the interface or class does not exist.
 90:      * - if the bound class is empty.
 91:      * - if the bound class does not implement the corresponding interface.
 92:      * - if the bound class is not instantiable.
 93:      * - if the check of the classes and interfaces fails.
 94:      *
 95:      * @author Oleg Schildt
 96:      */
 97:     static public function bindClass(object|string $interface_or_class, object|string $class, ?callable $init_function = null): void
 98:     {
 99:         self::bindClassContext("default", $interface_or_class, $class, $init_function);
100:     } // bindClass
101: 
102:     /**
103:      * Binds a class to an interface or a parent class.
104:      *
105:      * The key point of this approach is the definition common interfaces
106:      * and implementation of them in classes. When an object that supports an
107:      * interface is requested, an instance of the corresponding bound class
108:      * is created. When you want to change the class, you need just bind
109:      * the new class to the interface.
110:      *
111:      * You can also bind a class to itself and request it by own name in factory
112:      * method. And later, you can implement a derived class and change the binding
113:      * without affecting the code in the business logic.
114:      *
115:      * @param string $context
116:      * The context of binding. You can introduce a different context, and bind another implementation.
117:      * Then, you can request another instance specifying the context explicitly.
118:      *
119:      * @param object|string $interface_or_class
120:      * Name of the class/interface as string or the class/interface.
121:      *
122:      * @param object|string $class
123:      * The class which instance should be created if the object is requested.
124:      *
125:      * @param ?callable $init_function
126:      * The optional initialization function. You can provide it to do some
127:      * custom initialization. The signature of
128:      * this function is:
129:      *
130:      * ```php
131:      * function (object $instance) : void;
132:      * ```
133:      *
134:      * - $instance - the created instance.
135:      *
136:      * Example:
137:      *
138:      * ```php
139:      * ObjectFactory::bindClassContext("console", ILanguageManager::class, LanguageManager::class, function($instance) {
140:      *   $instance->detectLanguage();
141:      * });
142:      *
143:      * ObjectFactory::bindClassContext("console", IRecordsetManager::class, RecordsetManager::class, function($instance) {
144:      *   $instance->setDBWorker(dbworker());
145:      * });
146:      * ```
147:      *
148:      * @return void
149:      *
150:      * @throws \Exception
151:      * It might throw the following exceptions in the case of any errors:
152:      *
153:      * - if the interface or bound class is not specified.
154:      * - if the interface or class does not exist.
155:      * - if the bound class is empty.
156:      * - if the bound class does not implement the corresponding interface.
157:      * - if the bound class is not instantiable.
158:      * - if the check of the classes and interfaces fails.
159:      *
160:      * @author Oleg Schildt
161:      */
162:     static public function bindClassContext(string $context, object|string $interface_or_class, object|string $class, ?callable $init_function = null): void
163:     {
164:         if (empty($class)) {
165:             throw new \Exception("Bound class is empty!");
166:         }
167: 
168:         if (empty($interface_or_class)) {
169:             throw new \Exception("Bound interface or class is empty!");
170:         }
171: 
172:         if (!interface_exists($interface_or_class) && !class_exists($interface_or_class)) {
173:             throw new \Exception(sprintf("The interface or class '%s' does not exist!", $interface_or_class));
174:         }
175: 
176:         if (!class_exists($class)) {
177:             throw new \Exception(sprintf("The class '%s' does not exist!", $class));
178:         }
179: 
180:         $ic = new \ReflectionClass($interface_or_class);
181:         $c = new \ReflectionClass($class);
182: 
183:         if (!$c->isInstantiable()) {
184:             throw new \Exception(sprintf("The class '%s' is not instantiable!", $c->getName()));
185:         }
186: 
187:         if ($c != $ic) {
188:             if (!$c->isSubclassOf($ic)) {
189:                 throw new \Exception(sprintf("The class '%s' does not implement the interface '%s'!", $c->getName(), $ic->getName()));
190:             }
191:         }
192: 
193:         $f = null;
194:         if ($init_function !== null) {
195:             if (!is_callable($init_function)) {
196:                 throw new \Exception(sprintf("'%s' is not a function!", $init_function));
197:             }
198: 
199:             $f = new \ReflectionFunction($init_function);
200:         }
201: 
202:         self::$itable[$context][$ic->getName()] = ["class" => $c, "init_function" => $f];
203:     } // bindClassContext
204: 
205:     /**
206:      * Сreates an object that support the interface $interface_or_class.
207:      *
208:      * @param object|string $interface_or_class
209:      * Name of the class/interface which instance should be created.
210:      *
211:      * @param bool $singleton
212:      * If the parameter is true, it ensures that only one instance of this object exists.
213:      * The singleton is a usual patter for the action objects like SessionManager, EventManager,
214:      * DBWorker etc. It makes no sense to produce many instances of such classes,
215:      * it wastes the computer resources and might cause errors.
216:      *
217:      * @param string $context
218:      * The context of binding. You can introduce a different context, and bind another implementation.
219:      * Then, you can request another instance specifying the context explicitly.
220:      *
221:      * @return object
222:      * Returns object of the class bound to the interface.
223:      *
224:      * @throws \Exception
225:      * It might throw the following exceptions in the case of any errors:
226:      *
227:      * - if the interface or class is not specified.
228:      * - if the interface or class does not exist.
229:      * - if the check of the classes and interfaces fails.
230:      *
231:      * @used_by \SmartFactory\instance()
232:      * @used_by \SmartFactory\singleton()
233:      *
234:      * @author Oleg Schildt
235:      */
236:     static public function getInstance(object|string $interface_or_class, bool $singleton, string $context = "default"): object
237:     {
238:         if (empty($interface_or_class)) {
239:             throw new \Exception("Class or interface is not specified!");
240:         }
241:         
242:         if (!interface_exists($interface_or_class) && !class_exists($interface_or_class)) {
243:             throw new \Exception(sprintf("The interface or class '%s' does not exist!", $interface_or_class));
244:         }
245:         
246:         $class = new \ReflectionClass($interface_or_class);
247:         
248:         $class_name = $class->getName();
249:         
250:         if (empty(self::$itable[$context][$class_name])) {
251:             throw new \Exception(sprintf("The interface or class '%s' has no bound class in the context '$context'!", $class_name));
252:         }
253:         
254:         // if not singleton, we create a new instance every time it is requested
255:         if (!$singleton) {
256:             $instance = self::$itable[$context][$class_name]["class"]->newInstance();
257:             
258:             if (!empty(self::$itable[$context][$class_name]["init_function"])) {
259:                 self::$itable[$context][$class_name]["init_function"]->invoke($instance);
260:             }
261:             
262:             return $instance;
263:         }
264:         
265:         // if singleton, we create an instance only if it does not exist yet
266:         if (empty(self::$singletons[$context][$class_name])) {
267:             self::$singletons[$context][$class_name] = self::$itable[$context][$class_name]["class"]->newInstance();
268:             
269:             if (!empty(self::$itable[$context][$class_name]["init_function"])) {
270:                 self::$itable[$context][$class_name]["init_function"]->invoke(self::$singletons[$context][$class_name]);
271:             }
272:         }
273:         
274:         return self::$singletons[$context][$class_name];
275:     } // getInstance
276: } // ObjectFactory