1

I have been working on transitioning from procedural -> oop for a few months now. I decided to avoid jumping right into a framework. I didn't want to rely on the behind the scenes magic, so during my spare time i decided to convert an old project and build a small framework to run it. For tutorials i have mainly turned to googling laravel/laracast tutorials since it is the easiest to find information on. I would say i am at a point where my 'framework' is a smaller simplified ( by this i mean not as feature packed/complex ) version of laravel. I haven't wrapped my head around all of the things symfony/laravel offer but things are starting to click as i progress. At the moment i believe i will be working with laravel once i finish converting my old project.

Ok now. One of the things i continuously have doubts about is dependency injection. All/most injectors that i have found inject via construct using type hinting. I started out with a static registry and slowly iterated until i had the following class

<?php

/**
 *------------------------------------------------------------------------------
 *
 *  Framework Container - Class Builder + Injector + Container
 *
 *  The Container Is Responsible For Dependency Injection Via Class Constructors
 *  Through Abstract 'Alias' Keys Defined Within The Class. It Is Also Used To
 *  Hold Various Data Including Closures, Or Cached Object Instances.
 *
 */
namespace eSportspCMS;
use \eSportspCMS\Helpers\Support\Base;
use \eSportspCMS\Contracts\Container as ContainerContract;

class Container extends Base implements ContainerContract {


    /**
     *  Alias Container
     *  Contains Alias Keys Defining Classes To Instantiate     See $this->make()
     */
    private $aliases    = [];

    /**
     *  Binding Container
     *  Contains Custom Closure Bindings                        See $this->make()
     */
    private $bindings   = [];


    /**
     *  Cache Container
     *  Contains Previously Instantiated Classes                See $this->make()
     */
    private $cache      = [];

    /**
     *  Closure Container
     *  Contains Custom Closures To Avoid Hardcoding Within Class
     */
    private $closures   = [];


    /**
     *  Class Dependency Key
     *  Public Var Within Classes Defining The Dependency List
     */
    private $varkey     = 'dependencies';


    /**
     *  Define Class Dependency Key
     *
     *  @param string $varkey           Class Dependency Key
     */
    public function setvarkey($varkey)  {
        $this->varkey = $varkey;
    }


    /**
     *  Set Data Within Containers
     *
     *  @param string $key              Key To Use When Setting Container Var
     *  @param mixed  $value            Value To Use When Setting Container Var
     */
    public function alias($key, $value) {   $this->aliases[$key]    = $value;   }
    public function bind ($key, $value) {   $this->bindings[$key]   = $value;   }
    public function cache($key, $value) {   $this->cache[$key]      = $value;   }


    /**
     *  Add New Closure Within Container
     *
     *  @param string   $key            Closure Key
     *  @param callable $value          Callable Function
     */
    public function closure($key, callable $value) {
        if (method_exists($this, $key)) {
            throw new \Exception($key . ' Already Exists As A Method. Which
            Means This Closure Would Not Be Accessible If Set!');
        }
        $this->closures[$key] = $value;
    }


    /**
     *  Access Closure If Method Being Called Does Not Exist
     *
     *  @param  string $key             Closure Key
     *  @param  array  $params          Params To Pass To Closure
     *  @return mixed                   Closure Response If Exists
     */
    public function __call($key, $params = []) {
        if (isset($this->closures[$key]) && is_callable($this->closures[$key])) {
            return call_user_func_array($this->closures[$key], $params);
        }
        return false;
    }


    /**
     *  Create New Class Instance
     *
     *  Forces $this->make() To Instantiate A New Class Instead Of Returning
     *  A Cached Instance.
     *
     *  @see                            $this->make() Comments
     */
    public function makeNew($class, $params = []) {
        return $this->make($class, $params, false, true);
    }


    /**
     *  Pull Class From Cache Based On Key, Call Binding Or Instantiate Class
     *  Resolve Dependencies And Pass Via Construct.
     *
     *  @param  string $key             Alias Key | Binding Key | Class To Make
     *  @param  array  $params          Additional Params Passed Via Constructor
     *  @param  bool   $cache           Determines If Class Can Be Cached
     *  @param  bool   $new             Forces New Instance Of Object
     *  @return object                  Instantiated Or Cached Object
     */
    public function make($key, $params = [], $cache = true, $new = false) {

        /**
         *  Params Indicate Cached Instance Can Be Used
         */
        if (!$new && isset($this->cache[$key])) {
            return $this->cache[$key];
        }


        /**
         *  If Binding Is Defined And Key Matches Return It Instead Of Building
         *  The Class Directly. Replace Params With App
         */
        if (isset($this->bindings[$key]) && is_callable($this->bindings[$key])) {
            $instance   = call_user_func_array($this->bindings[$key], $this->make('app'));
            $cache      ? $this->cache($key, $instance) : '';
            return $instance;
        }


        /**
         *  Cache And Binding Statement Failed! Attempt To Build Class.
         *
         *  If      Class Exists Instantiate, Resolve/Pass Dependencies,
         *          Cache ( If Allowed ), And Return.
         *
         *  Else    Throw Exception!
         */
        $classname = isset($this->aliases[$key]) ? $this->aliases[$key] : $key;
        if (class_exists($classname)) {
            $instance   = new $classname($this->resolveDependencies($classname, $params));
            $cache      ? $this->cache($key, $instance) : '';
            return $instance;
        }


        // All Statements Failed! Class Couldn't Be Created
        throw new \Exception('Container Could Not Create Class: ' . $classname);
    }


    /**
     *  Resolve/Build Class Dependencies
     *
     *  Dependencies Cascade To Simplify/Unify Dependency Setting Within Grouped
     *  Classes. ( Classes Like Controllers Which Would Extend Base Controller )
     *
     *  @param  string $classname       Class Being Instantiated
     *  @param  array  $params          Additional Params Being Passed
     *  @return array                   Assoc Array With Class Dependencies
     */
    private function resolveDependencies($classname, $params = []) {

        // Define Class Tree
        $classes    = array_reverse((array) class_parents($classname));
        $classes[]  = $classname;

        // Retrieve Class Dependencies From Tree ( Alias Keys ) & Build
        $dependencies = $this->dependencies($classes);
        foreach ((array) $dependencies as $dependency) {
            $dependencies[$dependency] = $this->make($dependency);
        }

        // Return Merged Dependencies
        return array_merge($dependencies, $params);
    }


    /**
     *  Retrieve Class Dependency List ( Alias Keys )
     *
     *  @param  array $classes          Array Containing Classes
     *  @return array                   Class Dependencies ( Alias Keys )
     */
    private function dependencies($classes = []) {
        if (!$classes) {    return;    }

        $aliases = [];
        foreach ((array) $classes as $c) {
            $vars = get_class_vars($c);

            if (isset($vars[$this->varkey])) {
                $aliases = array_merge($aliases, $vars[$this->varkey]);
            }
        }
        return array_unique($aliases);
    }
}

How I Use It The framework application class will extend the container. During application bootstrap the alias keys for dependencies will be loaded ( i use the same configuration setup of laravel - loading config from returned arrays within config directory ) When a class is instantiated using the container it will search for the dependency list 'varkey', it will iterate and merge parent class dependencies ( for grouped dependencies like models - models extend base model ) it will then build the dependency tree, instantiate the class and cache if allowed. The dependencies are passed to the class via construct as an assoc array ['alias' => object]. Classes extending the '\helpers\support\base' class have the dependencies set for them. The base construct class iterates through the array setting the dependency within the class using the alias key as the class key.

Concerns When looking at other injectors i see typehinting being used and i am not sure if that is due to a flaw with this type of dependency injection that i am not seeing yet. The way i see it the alias keys are the 'interfaces' on top of that the dependencies that are used implement contracts so when i work on changing the class the contract still defines the class requirements.

Questions Is there a reason to: * stray away from using this injector? Why? ( I am not looking for "Dont reinvent the wheel" answers i am trying to further my knowledge and understand what i am doing ) * implement service providers "laravel tutorials" when using this? Currently i manipulate dependencies within construct ( if i need specific data from a dependency i use getters/setters to retrieve the data and set within the class )

Do i need to create a contract/interface for EVERY class that i create including controllers, or are interfaces used on classes that are dependencies only? ( I have found lots of mixed feedback so i am torn atm thought i would throw this in )

Class Example The following class is my 404 error controller. It is one of the dependencies used in other controllers since i have pages dependent on db data ( users ) i display 404 errors if the user does not exist.

<?php

/**
 *------------------------------------------------------------------------------
 *
 *  Error Controller
 *
 */
namespace Application\Controllers;
use \eSportspCMS\Helpers\Support\Base;

class Error extends Base {
    public $dependencies = ['http', 'log', 'metadata', 'view'];


    /**
     *  404 Error Page Not Found
     */
    public function display404($msg = '', $params = []) {

        // If Msg Present Log Error
        !$msg ?: $this->log->error($msg, $params);

        // Define 404 HTTP Header
        $this->http->set('header', 'HTTP/1.1 404 Not Found');

        // Set 404 Sitetitle
        $this->metadata->sitetitle('error', 'display404');


        // Return View File + View Data
        return $this->view->display('www/custompages', $this->custompage->info(1));
    }
}

Thank you in advance for all feedback.

ICJ
  • 83
  • 6

0 Answers0