1
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\begingroup\$ Could you provide a code example of how you would use this code, to help work out what its meant to do? This looks like a somewhat convoluted Service Locator, but i dont want to write about that if its not what you are aiming for \$\endgroup\$
    – Steve
    Jun 19, 2015 at 11:13
  • \$\begingroup\$ @steve Edited to include a brief example. I am attempting to learn the best routes to building a php application/cms while avoiding anti-patterns etc. \$\endgroup\$
    – ICJ
    Jun 19, 2015 at 15:06

1 Answer 1

1
\$\begingroup\$

I'm not sure I totally understand your idea of dep injector nor the code (I don't do php actually) but maybe I can give you a bit insight into what DI is and what it is for. So basically in OOP objects encapsulate and execute some behavior right? Usually this behavior consist of making method calls on other objects - so this objects can be considered its dependecies. So how come object A gets its dependencies (let's call them D1, D2, etc...)? Well there are 2 ways - either it creates (instantiates) then itself, or they are given (injected) into it externally. The discussion on why injection is better is a long one, but the bottom line is that when you start using interfaces for dependencies (not sure how it works in PHP) you can very easily decouple your architecture (idea on how objects interwork) from actuall implementation - for example IStorage interface implementation can be either SqlStorage or FileStorage or MemoryStorage. That's the beauty and power of DI. So now you can ask - do I actually need an automated DI injector? I suppose in most cases you actually don't! It's very common mistake of people that start using DI, that they believe they need for example ninject, castle (.NET world) to make DI work. It's not really true, most of the times in small to medium size apps, you can just create all objects at your application startup and inject dependencies by yourself. It's much cleaner and understandable code. So when do we actually need DI injectors. Mostly the case is when we work with other frameworks that support connecting DI injectors to them because they work . Example is ASP.NET or WCF services - they create object basing on their name / location whatever, so they need external source of dependencies, thus DI injector. hope that helps a bit.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.