Welcome Guest, Not a member yet? Register   Sign In
Dependency Injection for CodeIgniter Models
#1

I'm looking for some ideas / good practices on injecting classes (possibly other CI models) and properties into CodeIgniter models

My goal is to create models that are "testable" (although I won't necessarily start testing every model, I feel "testable" models is a good practice to adhere to).

For clarity, I'm referring to models that contain business logic. And I'm using CI3.

At the moment I am trying out two different approaches (from a controller):


Code:
$this->load->model('some_model');
$this->some_model->inject(new someClass, $some_property); // this is acting like a constructor
$this->some_model->doSomething(); // this checks if inject() has been called

And

Code:
$this->load->model('some_model');
$this->some_model->doSomething(new someClass, $some_property);


In general, my design preferences are:
  • Constructor injection over setter injection
  • Avoid a DI Container
  • Keep the business logic in CI models (I read Practical CodeIgniter 3, which suggests using libraries for logic, but it's not an option for me at the moment)
  • Limit the number of public methods in a model
  • Limit the number of classes or other models a model is dependant on or calls
Questions:

Are there other ways to inject into CI Models? How do others handle it?

I've Googled around and can't seem to find too much.

Thanks so much!
Reply
#2

Code:
$foo = new Foo_model($dependency);
Reply
#3

(07-07-2016, 05:03 AM)Narf Wrote:
Code:
$foo = new Foo_model($dependency);

Thanks for your help. If I have application/models/Foo_model and then instantiate as you suggest I get:


Code:
Class 'Foo_model' not found

If I load the model first then I get:

Code:
Missing argument 1 for Foo_model::__construct();

Because I have this in Foo_model:

Code:
   function __construct($dependency)
   {
       $this->dependency = $dependency;
   }

My understanding is that you can't inject into the __construct of a CI Model unlike with a Library. Is that right?
Reply
#4

You can't use $this->load->model() to inject a dependency into a constructor. However, if you have an autoloader which can find your model or you manually load the model, you can inject the dependency when you instantiate the model.

If you wanted to use $this->load->model() and constructor injection, the constructor's argument would have to be optional, which means you would have to determine how you want to handle the case in which the dependency is not injected. Since the CI loader instantiates the model, it's not really the best way to handle loading a model if you intend to instantiate it yourself to inject a dependency. However, if the extra instantiation doesn't pose a problem in your application, you'll probably want to make sure you replace the instance on the CI singleton with your new instance, e.g.

PHP Code:
$this->foo_model = new Foo_model($dependency);

// or, in a library or other place where $this may not refer to the CI instance:
get_instance()->foo_model = new Foo_model($dependency); 

Another alternative, which I would definitely NOT recommend, would be to extend the loader. One of the biggest problems I see with this is that I don't see a clear way to do it without duplicating a large amount of the code in the model() method, which leaves you with a lot of potential for maintenance issues down the line. Further, you'd probably want to create a new method, rather than overriding the existing model() method and adding a 4th parameter, otherwise you'd be forced to add the default values of the 2nd and 3rd parameters to your method calls when you are injecting dependencies.
Reply
#5

(07-07-2016, 06:59 AM)mwhitney Wrote: You can't use $this->load->model() to inject a dependency into a constructor. However, if you have an autoloader which can find your model or you manually load the model, you can inject the dependency when you instantiate the model.

If you wanted to use $this->load->model() and constructor injection, the constructor's argument would have to be optional, which means you would have to determine how you want to handle the case in which the dependency is not injected. Since the CI loader instantiates the model, it's not really the best way to handle loading a model if you intend to instantiate it yourself to inject a dependency. However, if the extra instantiation doesn't pose a problem in your application, you'll probably want to make sure you replace the instance on the CI singleton with your new instance, e.g.

PHP Code:
$this->foo_model = new Foo_model($dependency);

// or, in a library or other place where $this may not refer to the CI instance:
get_instance()->foo_model = new Foo_model($dependency); 

Another alternative, which I would definitely NOT recommend, would be to extend the loader. One of the biggest problems I see with this is that I don't see a clear way to do it without duplicating a large amount of the code in the model() method, which leaves you with a lot of potential for maintenance issues down the line. Further, you'd probably want to create a new method, rather than overriding the existing model() method and adding a 4th parameter, otherwise you'd be forced to add the default values of the 2nd and 3rd parameters to your method calls when you are injecting dependencies.

Thanks @mwhitney for your suggestions. I'll give it some thought. I'm not keen on the optional arguments, as to me that defeats the purpose.
Reply
#6

The way i do this is by creating my own override of the model() method MY_Loader.php and copying in the model loading method from the core. Before the return on that method i add the following code:

PHP Code:
$this->_ci_models[] = $name;
 
       $CI->$name = new $model();

 
       // now check for any dependencies in the model
 
       if(method_exists($CI->$name,'loadDependencies')){

 
           // get the dependencies
 
           $dependencies $CI->$name->loadDependencies();

 
           // if it's an array
 
           if(is_array($dependencies) && !empty($dependencies)){

 
               //  loop
 
               foreach($dependencies as $dependency){


 
                   // load it in
 
                   foreach ($this->_ci_model_paths as $mod_path) {


 
                       if (!file_exists($mod_path 'models/' $dependency '.php')) {
 
                           continue;
 
                       }

 
                       require_once($mod_path 'models/' $dependency '.php');
 
                   }

 
               }
 
           }
 
       }


 
       return $this

Once that is in, put the MY_Loader.php in the application/core folder and then inside your model you can add an optional method called loadDependencies() which will be called when it's available:

PHP Code:
use Clubs\Club;

/**
 * Class Clubs_m
 */
class Clubs_m extends CI_Model
{


 
   /**
     * @return array
     */
 
   public function loadDependencies()
 
   {

 
       return [
 
           'v3/Clubs/Club'
 
       ];
 
   }



In this example i am loading a dependency of /application/models/v3/Clubs/Club.php which has it's own namespace of "Clubs" and loads in the constructor it's own model:

PHP Code:
<?php

namespace Clubs;


/**
 * Class Club
 * @package Clubs
 */
class Club extends \CI_Model
{


 
   /**
     * Club constructor.
     */
 
   public function __construct()
 
   {

 
       // make sure parents model is loaded
 
       $this->load->model('v3/Clubs_m');
 
   }

 
   /**
     * @return bool
     */
 
   public function usesSSL(){

 
       return ((int)$this->website_uses_ssl == true false);
 
   }
}
?>

So now in the main model when i do a database query i can return the result with this object:

PHP Code:
/**
     * @param bool $isActive
     * @return mixed
     */
 
   public function fetchAllClubs($isActive=true){

 
       $this->mainDB->select('*')->from(static::Table());
 
       $this->mainDB->where('website_is_club',1);


 
       return $this->mainDB->where('website_active',($isActive 0))
 
           ->get()
 
           ->result('Clubs\Club');

 
   

Ok the "use" statement is a bit ambiguous in this case...you can ignore it.

Hope that helps.
Reply




Theme © iAndrew 2016 - Forum software by © MyBB