I recently made the switch from procedural to OOP style of coding. I decided I wanted to learn how everything works before deciding to jump into a framework, follow tutorials, and becoming dependent on the tutorials/help from others. This is my take on dependency injection so far, I didn't see the appeal in a page that lists all dependencies for every class, I much rather manage and maintain dependencies within the class itself so I don't have to open several files to fix/change/add dependencies. However, I am not entirely sure if this is the correct approach or if this violates any development standards or if this is considered an anti-pattern etc.
<?php
/**
*-----------------------------------------------------------------
*
* FACTORY
*
* Factory Works By Looking for a Public 'dependencies' Var Within the
* Class Being Instantiated. To Avoid Errors & Instantiation
* of the Same Class Over & Over the Factory Looks for a Cached Version
* of the Class to Inject. If a New Instance or Controller is Requested
* a New Instance is Instantiated Instead of Being Pulled From Cache.
* If One is Not Found the Dependency is Instantiated.
*
* The $dependencies Var Within the Class is Sequential Array Containing
* the Factory Keys ID
*
* Example:
* class Example {
* public $dependencies = ['db'];
* }
*
* 'db' is the ID Used Within the Factory. If The Factory ID Was
* Defined & Class Exists the Class Will Be Injected. The Factory
* Is Used to Define the Dependency Being Requested as Well as the
* varname to Use Within the Class Requesting the Dependency.
*
* If the Key is Missing the Factory Will Check if the Class Exists, If
* it Does Not Exist Exception is Thrown. An Emergency Shutdown Page Will
* Be Displayed to the Users if the ENVIRONMENT Constant is Set to Production.
* If ENVIRONMENT is Set to Development an Error Will Be Displayed on the
* Screen Displaying the Class Name.
*
* The Dependencies Are Passed Via Construct as an Assoc Array. Some Classes
* Extend the Base Helper Class for its Construct that Iterates Through and
* Sets the Dependencies For You.
*
* Example Without Extending Base:
* class Example2 {
* public $dependencies = ['db'];
*
* public function __construct($D) {
* $this->db = $D['db'];
* }
* }
*
* Base Contains Simple Foreach Loop Setting Key as Class Var & Value as Value
*
* Example of Base Construct:
* Class Base {
* public funciton __construct($D) {
* foreach ((array) $D as $key => $value) {
* $this->$key = $value
* }
* }
* }
*
*/
namespace Core;
class Factory {
public $keys = []; // Assoc Array to Define Proper Class To Instantiate
private $cache = []; // Cacheable Data
/**
* Define Factory Keys on Instantiation
*/
public function __construct() {
// Define Factory Keys
$factoryKeys = require CONFIG . 'factory/keys.php';
// Iterate Through Factory Keys & Define Final Factory Key
foreach ($factoryKeys as $namespace => $keys) {
foreach ($keys as $id => $class) {
$this->keys[$id] = $namespace . $class;
}
}
}
/**
* Set Vars Within Cached Container
*
* Define Specific Container Vars To Pass As Dependencies
*
* @param string $key Key to Use When Setting Container Var
* @param mixed $value Value to Use When Setting Container Var Value
*/
public function set($key, $value) {
$this->cache[$key] = $value;
}
/**
* Instantiate Controller
*
* Bridge/Helper Method to Simplify Controller Instantiation
*
* @see $this->make() Comments
*/
public function controller($class, $params = []) {
return $this->make('Controller\\' . $class, $params, false, true);
}
/**
* Create New Instance of Object
*
* Bridge/Helper Method to Simplify Forcing New Class Instance
*
* @see $this->make() Comments
*/
public function newInstance($class, $params = [], $cacheable = false) {
return $this->make($class, $params, $cacheable, false, true);
}
/**
* Instantiate Class
*
* Run Through Various Class Rules Getting Class, & Var Dependencies
* Instantiate Class if Not Already Cached
* If Class is Not Found Throw Exception
*
* @param string $key Factory Key ID to Create
* @param array $params Additional Params That Can Be Passed Via Constructor
* @param bool $cacheable Determines if Class be Cached Within Factory Cache Container
* @param bool $controller Bypass Key Search & Cache Check to Instantiate Controller
* @param bool $newInstance Force New Instance of Object
* @return object Instantiated or Cached Object
*/
public function make($key, $params = [], $cacheable = true, $controller = false, $newInstance = false) {
// Define Classname
$classname = $controller || !isset($this->keys[$key]) ? $key : $this->keys[$key];
// If Class Exists Continue
if (class_exists($classname)) {
// Instantiate Class
if ($controller || $newInstance || !isset($this->cache[$key])) {
// Define Dependencies & Instantiate Class
$params = $this->dependencies($classname, $params);
$instance = new $classname($params);
// Cache Class Instance If Allowed
if ($cacheable) {
$this->cache[$key] = $instance;
}
}
// Params Indicate Cached Instance Can Be Used
else {
$instance = $this->cache[$key];
}
// Return Instance
return $instance;
}
// Classname Not Found Throw Exception
else {
throw new \Exception('The Factory Could Not Find Key ' . $key);
}
}
/**
* Return Class Dependencies
*
* Using Class Name Grab Class Dependencies, Iterate Through Dependencies
* And Return Class Dependencies
*
* @param string $classname Class Name Being Instantiated by $this->make();
* @param array $params Additional Params Passed Via $this->make();
* @return array Assoc Array Containing All Dependencies
*/
private function dependencies($classname, $params = []) {
// Define Default
$keys = [];
$dependencies = [];
// Define Class Tree
$classes = array_reverse((array) class_parents($classname));
$classes[] = $classname;
// Iterate Through Setting Dependency Keys
foreach ($classes as $c) {
$classvars = get_class_vars($c);
if (isset($classvars['dependencies'])) {
$keys = array_merge($keys, $classvars['dependencies']);
}
}
// Set Var Dependencies
if (isset($keys['vars'])) {
foreach ($keys['vars'] as $v) {
$dependencies['vars'][$v] = isset($this->cache['vars'][$v]) ? $this->cache['vars'][$v] : [];
}
unset($keys['vars']);
}
// Set Class Dependencies
foreach ($keys as $key) {
$dependencies[$key] = $this->make($key);
}
// Return Merged Dependencies
return array_merge($dependencies, $params);
}
private function __clone() {}
private function __wakeup() {}
}
Below is an example of my factory keys. I did not bother listing them all since it is just needed for example.
<?php
/**
*-----------------------------------------------------------------
*
* FACTORY KEYS
*
* FORMAT: 'namespace' => [
* 'key' => 'class'
* ]
*
* Namespace Refers to the Complete Namespace Used to
* Find the Class Via Autoloader
*
* Key Is Used When Searching for Classes Via Factory
*
* Class The Name of the Class ( Must Match Filename as Well )
*
* Controllers are Instantiated Via Router & Do Not Need to Be Defined
* Below. The Error Controller Handles 404 Pages and is Defined So it
* Can Be Included as a Dependency In Classes Where There is a
* Possibility of Displaying a 404 Page.
*
*/
return [
'\Controller\\' => ['error' => 'Error'],
'\Application\Helper\\' => [
'ad' => 'Ads',
],
'\Model\\' => [
],
'\Service\\'=> [
],
'\Core\\' => [
'factory' => 'Factory',
],
'\Helper\\' => [
'alert' => 'Alerts',
'db' => 'Database',
]
];
Example (quick rundown)
The root index
file of my application instantiates the factory normally $factory = new Core\Factory
the factory keys are loaded via construct and we can get started. After the factory is called I then call the router $route = $factory->make('router')->mapRoute();
The router will look at the URI and determine the correct controller to call. The controller/method/params are returned and the controller is instantiated. $factory->controller('$route['controller']')->$route['method']($route['params']);
The factory takes a look at the dependencies var within the class and if they are not already cached it will instantiate the dependencies based on the factory keys.
An example controller would look like this:
/**
*-----------------------------------------------------------------
*
* HOMEPAGE
*
*/
namespace Controller\www;
use \Helper\Controller;
class Index extends Controller {
public function index() {
/**
* Define Site Title & Display Page
*/
$this->view->sitetitle('index');
$this->view->display('www/index');
}
}
Controllers extend the base Controller
Class so default dependencies are passed/merged via the factory, and the base controller
extends the base
class is extended for var assignment - See Factory Comments. Going this route allows me to instantiate only the classes needed based on the dependencies set within the controller itself. If I run into a situation where I need to change my naming convention all I have to do is alter the factory key values to set the correct class and we are ready to go. If I need to cleanup dependencies I can do so within each class instead of scrolling through a large list of interface files, etc.
My understanding of a service locator is a class that instantiates the classes that "could" be needed and passes them around. With this the dependencies are found, instantiated if they haven't been already, and passed within construct for assignment.
Also, the only dependencies which are really cached (at the moment at least) are helper classes like input sanitation, form builder, session alert setter, etc.