We are building applications with Laravel but many of us do not know how Laravel Initiate Controller and Method with Dependency and Parameters.
We declare route like:
Route::get('hello', 'TestController@index')
AND Our Controller Like:
namespace App\Http\Controllers\Test;
class TestController extends Controller
{
private $requests;
public function __construct( Request $request)
{
$this->requests = $request;
}
/**
* Show the application dashboard.
*/
public function index(TestModel $testModel)
{
return $testModel->get();
}
}
OR Route Like:
Route::get('hello', 'TestController')
AND Controller like:
namespace App\Http\Controllers\Test;
class TestController extends Controller
{
private $requests;
public function __construct( Request $request)
{
$this->requests = $request;
}
/**
* Show the application dashboard.
*/
public function __invoke(TestModel $testModel)
{
return $testModel->get();
}
}
Now Question:
1.1 How Laravel detect construct's Dependency and inject ?
1.2 And How Laravel initiate class with construct's Dependencies and call the 'index' Method with Its Dependencies and Parameters?
2.1 We do not pass method name but How Laravel Detect __invoke method as default method?
Let's build a dependency injection container to understand previous Questions.
Why we should know?
- As Laravel Developer, We need to know How Laravel Work.
- It will help us to build a new Framework in our own approach.
- We can use this container for our large project or Module.
- Finally, we will start learning PHP Reflection (The Awesome)
Let's Start
Our Steps
- Create a Class
- Create a method (make) for initiating a class and injecting the constructor's dependencies and parameters
- create a method (call) for extracting class & method and injecting dependencies and parameters of the method
1. Let's Create a Class
class DependencyInjectionContainer
{
/**
* The container's instance.
*
* @var static
*/
protected static $instance;
/**
* the class name with namespace
*
* @var string
*/
protected $callbackClass;
/**
* the method name of provided class
*
* @var string
*/
protected $callbackMethod;
/**
* method separator of a class. when pass class and method as string
*/
protected $methodSeparator = '@';
/**
* namespace for class. when pass class and method as string
*
* @var string
*/
protected $namespace = "App\\controller\\";
/**
* get Singleton instance of the class
*
* @return static
*/
public static function instance()
{
}
/**
* @param $callable -- string class and method name with separator @
* @param array $parameters
*/
public function call($callable, $parameters = [])
{
}
/**
* separate class and method name
* @param $callback
*/
private function resolveCallback($callback)
{
}
/**
* instantiate class with dependency and return class instance
* @param $class - class name
* @param $parameters (optional) -- parameters as array . If constructor need any parameter
*/
public function make($class, $parameters = [])
{
}
}
Create a method (make) for initiating a class and injecting the constructor's dependencies and parameters
We are going to use PHP PHP Reflection. In this section we use PHP ReflactionClass object.There are so many methods exist in PHP ReflactionClass.
In this section, we need to make a class instance.
We know that if we make-instance of any class, we must pass/inject all dependencies & parameters.
With PHP ReflactionClass we need to detect all dependencies.
public function make($class, $parameters = [])
{
$classReflection = new ReflectionClass($class);
$constructorParams = $classReflection->getConstructor()->getParameters();
}
In this above code, we collect all parameters for the constructor to $constructorParams variable.
Then we need to loop with $constructorParams and detect parameter and dependency.
public function make($class, $parameters = [])
{
$classReflection = new ReflectionClass($class);
$constructorParams = $classReflection->getConstructor()->getParameters();
$dependencies = [];
/*
* loop with constructor parameters or dependency
*/
foreach ($constructorParams as $constructorParam) {
$type = $constructorParam->getType();
if ($type && $type instanceof ReflectionNamedType) {
echo "It is a class and we need to instantiate the class and pass to constructor"
} else {
echo "This is a normal parameter and We need to find parameter value from $parameters . If not found value then needs to check if this parameter is optional or not. If not optional then through error"
}
}
}
Point:
- if Parameter is a class, we need to instantiate the class and pass it to the constructor.
- if Parameter is not a class, We need to find parameter value from $parameters . If not found value then needs to check if this parameter optional or not. If not optional then through error.
Lets Finalize the make method:
public function make($class, $parameters = [])
{
$classReflection = new ReflectionClass($class);
$constructorParams = $classReflection->getConstructor()->getParameters();
$dependencies = [];
/*
* loop with constructor parameters or dependency
*/
foreach ($constructorParams as $constructorParam) {
$type = $constructorParam->getType();
if ($type && $type instanceof ReflectionNamedType) {
// make instance of this class :
$paramInstance = $constructorParam->getClass()->newInstance()
// push to $dependencies array
array_push($dependencies, $paramInstance);
} else {
$name = $constructorParam->getName(); // get the name of param
// check this param value exist in $parameters
if (array_key_exists($name, $parameters)) { // if exist
// push value to $dependencies sequencially
array_push($dependencies, $parameters[$name]);
} else { // if not exist
if (!$constructorParam->isOptional()) { // check if not optional
throw new Exception("Can not resolve parameters");
}
}
}
}
// finally pass dependancy and param to class instance
return $classReflection->newInstance(...$dependencies);
}
I explain everything in code.
OK.... we have completed a Dependency Injection container for class
Let's Create a Singleton Instance
public static function instance()
{
if (is_null(static::$instance)) {
static::$instance = new static;
}
return static::$instance;
}
Basic Usage
$container = DependencyInjectionContainer::instance();
Define a class
class MyClass
{
private $dependency;
public function __construct(AnotherClass $dependency)
{
$this->dependency = $dependency;
}
}
Instead of using new MyClass
, use the Container's make()
method:
$instance = $container->make(MyClass::class);
The container will automatically instantiate the dependencies, so this is functionally equivalent to:
$instance = new MyClass(new AnotherClass());
Practical Example
Here's a more practical example based on the PHP-DI docs - separating the mailer functionality from the user registration:
class Mailer
{
public function mail($recipient, $content)
{
// Send an email to the recipient
// ...
}
}
class UserManager
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
}
public function register($email, $password)
{
// Create the user account
}
}
$container = DependencyInjectionContainer::instance();
$userManager = $container->make(UserManager::class);
$userManager->register('dave@davejamesmiller.com', 'MySuperSecurePassword!');
3. create a method (call) for extracting class & method and injecting dependencies and parameters of the method
In this section, we are going to understand how Laravel parses the controller and method and initiates with dependency/parameters.
Steps
- Separate controller and method
- Detect method Dependency/parameters and call method
- Instantiate class with our
make()
function.
1. Separate controller and method
Our class has a property $methodSeparator as a separator for separating class name and method name.
private function resolveCallback($callable)
{
//separate class and method
$segments = explode($this->methodSeparator, $callable);
// set class name with namespace
$this->callbackClass = $this->namespace.$segments[0];
// set method name . if method name not provided then default method __invoke
$this->callbackMethod = isset($segments[1]) ? $segments[1] : '__invoke';
}
2. Detect method Dependency/parameters and call method
public function call($callable, $parameters = [])
{
// set class name with namespace and method name
$this->resolveCallback($callable);
// initiate ReflectionMethod with class and method
$methodReflection = new ReflectionMethod($this->callbackClass, $this->callbackMethod);
// get all dependencies/parameters
$methodParams = $methodReflection->getParameters();
$dependencies = [];
// loop with dependencies/parameters
foreach ($methodParams as $param) {
$type = $param->getType(); // check type
if ($type && $type instanceof ReflectionNamedType) { /// if parameter is a class
$name = $param->getClass()->newInstance(); // create insrance
array_push($dependencies, $name); // push to $dependencies array
} else { /// Normal parameter
$name = $param->getName();
if (array_key_exists($name, $parameters)) { // check exist in $parameters
array_push($dependencies, $parameters[$name]); // push to $dependencies array
} else { // if not exist
if (!$param->isOptional()) { // check if not optional
throw new Exception("Can not resolve parameters");
}
}
}
}
// make class instance
$initClass = $this->make($this->callbackClass, $parameters);
// call method with $dependencies/parameters
return $methodReflection->invoke($initClass, ...$dependencies);
}
Now you have a question how Laravel collects parameter for calling a function.
When we declare a route with dynamic parameter like:
Route::get('hello/{name}', 'TestController@index')
Laravel Routing system collects all parameter and pass to call function.
$parameters = ["name" => "AL EMRAN" ];
$container->cal("TestController@index", $parameters)
Usage
$container->call('TestController@index');
$container->call('TestController@show', ['id' => 4]);
$container->call('TestController');
$container->call('TestController', ['id' => 4]);
OR
example
class TestController
{
protected $company;
public function __construct(Company $company)
{
$this->company = $company ;
}
/**
* @param Request $request
*/
public function index(Request $request){
$company = $company->get();
return view('admin.company', compact('company'));
}
}
We can use :
$instance = DependencyInjectionContainer::instance();
$instance->namespace = "App\\Http\Controllers\\Admin\\"; // change namespace
$class = $instance->make(CompanyController::class); // make class instance
$instance->call(["CompanyController", 'index']); // call method
$instance->call([$class, 'index']); // or--call method
Install package
Package Github link here
composer require emrancu/dependency-injection-container
Declaimer
Laravel Dependency Injection Container is so vast and also its routing system. I just create this mini-class only for understanding.
This is my first article, so please comment your valuable opinion or mail me at "emrancu1@gmail.com"
You can Follow me on Twitter
Our Products:
Top comments (0)