Вы находитесь на странице: 1из 22

1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

Tom Butler's programming blog
Home
About me
Contact

Code
Dice ­ PHP Dependency Injection Container

Transphporm ­ Fixing PHP Templating

Maphper ­ PHP Data Mapper ORM

NoF5 ­ Never press reload while developing again

CSS3 in Internet Explorer

Articles
Model­View­Confusion series

1. The View gets its own data from the Model
2. MVC Models are not Domain Models

MVC In PHP series

1. Hello World
2. Real world example (part 1)
3. Deploying MVC on the web
4. Create a router using Dependency Injection

MVVM

MVVM, MVC it's all just roman numerals to me

Dependencies in code
Constructor Injection vs Setter Injection
The "courier" anti­pattern.
Are Static Methods/Variables bad practice?

Best Practices

The $this variable isn't as Object­Oriented as you think it is
https://r.je/mvc­php­router­dependency­injection.html 1/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

Finding creative ways to break encapsulation isn't clever
PHP: Annotations are an abomination
Empty interfaces are bad practice
Stop the sprinf() abuse

Programming Philosophy
Is programming an art form?
The importance of getting terminology correct
SoCcam's Razor: Applying the Single Responsibility Principle using a practical approach

Test Driven Development

To assert or not assert, that is the question.

Other
PHP Autoloaders should not be case sensitive
PHP: PSR­0: Pretty Shortsighted, Really
Find every combination of an array
Why I don't have a comments section
Split/explode a string in PHP with an escape character

Using a Dependency Injection Container to simplify Routing in an
MVC framework
18 March 2013

Introduction
Today, I'm going to show how combining several different OOP techniques and design patterns can
create a very powerful yet simple application entry point for an MVC framework. From the ground up,
I'll walk you through building a Convention­Over­Configuration router for a MVC framework. By the
end you'll have something smaller, far more robust and powerful than is available in any of the popular
frameworks!

I'll be using my own Dependency Injection Container: Dice (available here) to create a smart
router/dispatcher for a simple MVC Framework. Any Dependency Injection Container can be used for
this job, but Dice takes away 99% of the configuration so makes everything far simpler.

Why use a Dependency Injection Container for this task?
In MVC In PHP part 2: MVC On the web. I discussed the problem of routing that the web architecture
presents. I chose not to get too technical in that article as I wanted everything to be self­contained.
However, this is a more advanced version of that and while it addresses the same concepts and the same
issues, this is far more complex and designed to be used in the real­world. It's not just for demonstration.

https://r.je/mvc­php­router­dependency­injection.html 2/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

As such, and sticking with the theme of Separation of Concerns that MVC strives for, Using a different
tool for the creation of objects within the system creates a far more robust and flexible application. I
won't go into the merits of dependency injection here as there are lot of articles around which discuss
that. However, I hope the code examples here will speak for themselves even if you're not familiar with
Dependency Injection.

Routing in MVC web frameworks
To recap, the biggest issue faced when deploying MVC on the web is:

How does the centralised entry point (front controller) know which Model, View and Controller to
initiate?

By delegating that job to a Dependency Injection Container it's easy to avoid the trivial binding logic of
"This controller needs this model, this view needs this model".

Firstly, how the router will interact with the rest of the system must be defined. Basic functionality
dictates that it will take a route such as '/users/list' and return the names (or instances) of the model, view
and controller that it's going to use.

Here is a skeleton router API with the basic logic:

class Router { 

    public function find($route) { 

        //Convert $route into a Route object as defined below 

        return new Route('...', '...', '...'); 

    } 

}         

class Route { 

    private $view; 

    private $controller; 

    private $model; 

    public function __construct(View $view, $model = null, $controller = null) { 

        $this‐>view = $view; 

https://r.je/mvc­php­router­dependency­injection.html 3/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

        $this‐>controller = $controller; 

    } 

    public function getView() { 

        return $this‐>view; 

    } 

    public function getController() { 

        return $this‐>controller; 

    } 

    public function getModel() { 

        return $this‐>model; 

    } 

To follow the basic rules of encapsulation, the three properties are private. This essentially makes them
read­only and once a route has been created it is immutable. This is good because it stops unknown
external code changing the route during the application's execution.

However, this can be simplified slightly. If there is a Model in use, the View and/or Controller will ask
for it in its constructor:

class MyView implements View { 

    public function __construct(MyModel $model) { 

    } 

https://r.je/mvc­php­router­dependency­injection.html 4/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

class MyController { 

    public function __construct(MyModel $model) { 

    } 

In MVC the View and Controller encapsulate the model. The top level of the application that knows
about the route doesn't need to know of the Model's existence, and as such it should be removed from the
Route object entirely:

class Router { 

    public function find($route) { 

        //Convert $route into a Route object as defined below 

        return new Route('...', '...'); 

    } 

class Route { 

    private $view; 

    private $controller; 

    public function __construct(View $view, $controller = null) { 

        $this‐>view = $view; 

        $this‐>controller = $controller; 

    } 

    public function getView() { 

        return $this‐>view; 
https://r.je/mvc­php­router­dependency­injection.html 5/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

    } 

    public function getController() { 

        return $this‐>controller; 

    } 

Later on a dispatcher will be required to call the correct controller action and this will need to know
which controller it's dealing with. And the top level of the application needs to know which view to
render. Nothing apart from the View and Controller need the model so we can minimise the data needed
in the Route object.

A convention based router
Back to the router itself, let's say the first two parts of the URL to generate the route. For example
/user/edit/12 becomes "\User\Edit\Controller" and "\User\Edit\View" by default. The default action is
then called by the dispatcher with a parameter of "12". A Convention­over­configuration approach can be
used so that this can be overridden where needed.

This could be defined manually in the router:

class Router { 

    /* 

    * @param string 

    * @description: Convert $route into a Route object as defined below 

    * @return \Route 

    */ 

    public function find($route) { 

        //Split the route on / so the first two parts can be extracted 

        $routeParts = explode('/', $route); 

     
https://r.je/mvc­php­router­dependency­injection.html 6/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

        //A name based on the first two parts such as "\User\Edit" or "\User\List" 

        $name = '\\' . array_shift($route) . '\\' . array_shift($route); 

        $viewName = $name . '\\View'; 

        //Does the class e.g. "\User\List\View" exist? 

        if (class_exists($viewName))  { 

            $view = new $viewName; 

        } 

        else { 

            //Exit, this should display an error or throw an exepction but for simpliticy 

            //do not have a view will be disabled 

            return false; 

        } 

        //E.g. "\User\Edit\Controller" 

        $controllerName = $name . '\\Controller'; 

        if (class_exists($controllerName)) { 

            $controller = new $controllerName; 

        } 

        else { 

            $controller = null;         

        } 

        //Finally, return the matched route 

        return new Route($view, $controller); 

    } 
https://r.je/mvc­php­router­dependency­injection.html 7/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

    } 

The obvious problem here is that it won't work. The logic is sound but the controller and view can't be
initialised in this way because they'll never get passed the model. This is where Dice, the Dependency
Injection Container, comes in. It can automatically resolve dependencies:

Rather than having Dice construct the Model, View and Controller, Dice will instead, dynamically
construct the Route object with the correct parameters.

This means that the Router is never aware of any dependencies that controllers, models or views may
have and resolving those dependencies is all down to Dice. This ensures a proper separation of concerns
that abide by the single responsibility principle. The router is concerned with routing and the
Dependency Injection Container is concerned only with managing dependencies.

class Router { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        //Dice, the dependency injection container 

        $this‐>dice = $dice; 

    } 

    public function find($route) { 

        //Convert $route into a Route object as defined below 

         

         

        //A name based on the first two parts such as "\User\Edit" or "\User\List" 

        $name = '\\' . array_shift($route) . '\\' . array_shift($route); 

https://r.je/mvc­php­router­dependency­injection.html 8/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

        $viewName = $name . '\\View'; 

         

        if (!class_exists($viewName)) return false; 

         

        $controllerName = $name . '\\Controller'; 

         

        //Auto‐generate a rule for a route if it's not already been generated 

        if ($this‐>dice‐>getRule('$autoRoute_' . $className) == $this‐>dice‐>getRule(

            $rule = new \Dice\Rule; 

            

            //Dice will be creating a Route object and pass it a specific view and control

            $rule‐>instanceOf = 'Route'; 

            

            //The first parameter will be a view 

            $rule‐>constructParams[] = new \Dice\Instance($viewName); 

            //Only pass the controller to the route object if the controller exists 

            $rule‐>constructParams[] = (class_exists($controllerName)) ? new \Dice\Instanc

            //The model (or ViewModel) will need to be shared between the controller and v

            $modelName = $className . '\\ViewModel'; 

            //The model doesn't need to exist, in its purest form, an MVC triad could just

            //This tells Dice that the same instance of the model should be passed to both

            if (class_exists($modelName))  $rule‐>shareInstances[] = $modelName; 

            

https://r.je/mvc­php­router­dependency­injection.html 9/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

            

            //Add the rule to the DIC 

            $this‐>dice‐>addRule('$autoRoute_' . $className, $rule); 

        } 

         

         

        //Have Dice construct the Route object with the correct controller and view set.

        //Dice will automatically pass the model into the View nad Controller if they ask 

        return $this‐>dice‐>create('$autoRoute_' . $className); 

    } 

     

This works by having Dice create the Route object with a specific View and Controller that are using a
convention based name

Dice will create an instance of the Route object passing it the required View and Controller. If the
controller or view ask for the model in their constructor, they will be automatically passed that too.

The best part about this is that thanks to Dice, the controller (e.g. \User\Edit\Controller) can ask for
additional dependencies and they will be automatically resolved by Dice. Amending the controller to ask
for a session object and a request object will just work without any further configuration! And not need
to alter or reconfigure the router in any way.

namespace User\Edit; 

class Controller { 

    public function __construct(UserModel $user, Session $session, Request $request) {

    } 

https://r.je/mvc­php­router­dependency­injection.html 10/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

Nice! This is now a very simple Convention­based router. That is, it will always use the naming
convention to work out the route. However, it's not very flexible. It ties you to a very specific naming
convention for your classes. It must initiate $route1$route2View and $route1$route2 controller. This is
far from ideal as it's impossible to reuse a class. For instance You may wish to use a generic "\List\View"
rather than a specific "\User\List\View"

A configuration based router
Sometimes you want to be able to manually define which controller and view to initialise. This is
important for reusability, if you have to use the convention approach you must needlessly use inheritance
if you want to reuse a controller or view on two different routes

By defining the routes using Dice's Named Instances feature, it's possible to have complete control over
an instance:

$rule = new \Dice\Rule; 

//It will be an instance of the Route object 

$rule‐>instanceOf = 'Route'; 

//Define the View and Controller which will be used. These don't rely on a naming conventi

//Here a generic ListView is used with a user‐specific UserListController 

$rule‐>constructParams = [new DiceInstance('ListView'), new DiceInstance('UserListControll

//And add a rule containing the route 

$dice‐>addRule('$route_user/edit', $rule); 

This means that when Dice is told to create a component called "$route_user/edit" it creates the an
instance of the Route class by passing it an instance of ListView and an instance of UserListController in
its constructor.

And now change the router to allow Dice to piece together all the relevant components:

https://r.je/mvc­php­router­dependency­injection.html 11/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

class Router { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        $this‐>dice = $dice; 

    } 

    public function find($route) { 

        //Convert $route into a Route object as defined below 

        $routeParts = explode('/', $route); 

        $name = $routeParts[0] . '/' . $routeparts[1]; 

        return $this‐>dice‐>create('$route_' . $name); 

    } 

And Dice has done all the work! Of course, the fallback to the default view has been lost. That should be
added back in:

$rule = new DiceRule; 

//It will be an instance of the Route object 

$rule‐>instanceOf = 'Route'; 

$rule‐>constructParams = array(new DiceInstance('DefaultView')); 

//Add a named instance for the fallback view: 

$dice‐>addRule('$route_default', $rule); 

https://r.je/mvc­php­router­dependency­injection.html 12/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

And then in the router:

class Router { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        $this‐>dice = $dice; 

    } 

    public function find($route) { 

        //Convert $route into a Route object as defined below 

        $routeParts = explode('/', $route); 

        $name = '$route_' . $routeParts[0] . '/' . $routeparts[1]; 

        //If there is no special rule set up for this $name, revert to the default 

        if ($this‐>dice‐>getRule($name) === $this‐>dice‐>getRule('*')) { 

            return $this‐>dice‐>create('$route_default'); 

        } 

        else return $this‐>dice‐>create($name); 

    } 

This will now fall back to whatever has been defined as the default route. The problem with this
approach is that every single route still needs to be defined manually. This is a Configuration based (or
static) router where each route is manually defined. The downside is that every single possible route in
the system must be defined as a rule. This can very quickly grow to a very long list in large application

https://r.je/mvc­php­router­dependency­injection.html 13/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

and become a pain in larger applications. Doing something as simple as adding a page means
reconfiguring the router.

Combining the two to create a Convention­Over­Configuration
router
By combining the two, it's easy to create a Convention­over­configuration router that has all the
flexibility of the Configuration­based and along with the convenience of the Convention­based router!
And what's more, by using Dice you don't need to worry about locating any dependencies the View or
Controller may have.

class Router { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        $this‐>dice = $dice; 

    } 

    public function find($route) { 

        //Convert $route into a Route object as defined below 

        $routeParts = explode('/', $route); 

         

        $configName = '$route_' . $routeParts[0] . '/' . $routeparts[1]; 

         

        //Check if there is a manual configuration for this route 

        if ($this‐>dice‐>getRule($configName) === $this‐>dice‐>getRule('*')) { 

            $className = '\\' . array_shift($route) . '\\' . array_shift($route); 

            $viewName = $className . '\\View'; 

            
https://r.je/mvc­php­router­dependency­injection.html 14/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

            //If the view doesn't exist, the convention rule can't continue 

            if (!class_exists($viewName)) return false; 

            

            $controllerName = $className . '\\Controller'; 

            

            //Auto‐generate a rule for a route if it's not already been generated 

            if ($this‐>dice‐>getRule('$autoRoute_' . $className) == $this‐>dice‐>getRule

                $rule = new \Dice\Rule; 

                 

                //Dice will be creating a Route object and pass it a specific view and con

                $rule‐>instanceOf = 'Route'; 

                 

                //The first parameter will be a view 

                $rule‐>constructParams[] = new \Dice\Instance($viewName); 

     

                //Only pass the controller to the route object if the controller exists

                $rule‐>constructParams[] = (class_exists($controllerName)) ? new \Dice

     

                //The model (or ViewModel) will need to be shared between the controller a

                $modelName = $className . '\\ViewModel'; 

     

                //The model doesn't need to exist, in its purest form, an MVC triad could 

                if (class_exists($modelName))  $rule‐>shareInstances[] = $modelName; 

                                 

                //Add the rule to the DIC 

                $this‐>dice‐>addRule('$autoRoute_' . $className, $rule); 

            } 
https://r.je/mvc­php­router­dependency­injection.html 15/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

            } 

            

            //Create the route object 

            return $this‐>dice‐>create('$autoRoute_' . $className); 

        } 

        //There is a manual route defined, use that 

        else return $this‐>dice‐>create($configName); 

    } 

This is excellent! Here is a very simple yet very powerful Convention­Over­Configuration router. It's
simple and follows the Single Responsibility Principle as well as keeping the idea of separation of
concerns by not being concerned with the creation of the objects and delegating it to Dice.

Room for improvement
Of course, there's always room for improvement. How about making the router entirely extensible?
Should the router itself really be concerned with looking at class names or the implementation of Dice? It
can be simplified and made extensible:

namespace Router; 

interface Rule { 

    public function find(array $route); 

class Router { 

    private $rules = array(); 
https://r.je/mvc­php­router­dependency­injection.html 16/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

     

    public function addRule(Rule $rule) { 

        $this‐>rules[] = $rule; 

    } 

     

    public function getRoute(array $route) { 

        foreach ($this‐>rules as $rule) { 

            if ($found = $rule‐>find($route)) return $found;     

        } 

         

        throw new Exception('No matching route found'); 

    } 

Here is a very basic router that requires some third party rules are provided for it to work. The first thing
you'll notice is that getRoute() now takes an array. This is because assuming that all routes are strings
broken up by a '/' is a bad thing and will limit the scope of the router. Instead, the part of the code that
actually accepts the route as a string can format it into an array for the router. The router doesn't care
where the route comes from or whether the URL was broken on a '/' or a '\' or even a ':' or "@". In fact it
may not have been a URL at all.

This router works on rules. You pass it rules and it processes them in order. At the moment the router
will never match a single route. To add back the functionality that existed previously it needs to be
moved to rules. Firstly the Configuration­based rule:

namespace Router\Rule; 

class Configuration implements \Router\Rule { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        $this‐>dice = $dice; 
https://r.je/mvc­php­router­dependency­injection.html 17/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

    } 

    public function find(array $route) { 

        $name = '$route_' . $route[0] . '/' . $route[1]; 

        //If there is no special rule set up for this $name, revert to the default 

        if ($this‐>dice‐>getRule($name) == $this‐>dice‐>getRule('*')) { 

            return false; 

        } 

        else return $this‐>dice‐>create($name); 

    } 

and one for the Convention based approach:

namespace Router\Rule; 

class Convention implements \Router\Rule { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        $this‐>dice = $dice; 

    } 

    public function find(array $route) { 

        //The name of the class 

https://r.je/mvc­php­router­dependency­injection.html 18/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

        $className = '\\' . array_shift($route) . '\\' . array_shift($route); 

        $viewName = $className . '\\View'; 

         

        //If the view doesn't exist, the convention rule can't continue 

        if (!class_exists($viewName)) return false; 

         

        $controllerName = $className . '\\Controller'; 

         

        //Auto‐generate a rule for a route if it's not already been generated 

        if ($this‐>dice‐>getRule('$autoRoute_' . $className) == $this‐>dice‐>getRule(

            $rule = new \Dice\Rule; 

            

            //Dice will be creating a Route object and pass it a specific view and control

            $rule‐>instanceOf = 'Route'; 

            

            //The first parameter will be a view 

            $rule‐>constructParams[] = new \Dice\Instance($viewName); 

            //Only pass the controller to the route object if the controller exists 

            $rule‐>constructParams[] = (class_exists($controllerName)) ? new \Dice\Instanc

            //The model (or ViewModel) will need to be shared between the controller and v

            $modelName = $className . '\\ViewModel'; 

            //The model doesn't need to exist, in its purest form, an MVC triad could just

            if (class_exists($modelName))  $rule‐>shareInstances[] = $modelName; 

https://r.je/mvc­php­router­dependency­injection.html 19/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

            

            

            //Add the rule to the DIC 

            $this‐>dice‐>addRule('$autoRoute_' . $className, $rule); 

        } 

         

        //Create the route object 

        return $this‐>dice‐>create('$autoRoute_' . $className); 

    } 

Finally, in case neither of those rules match a route, return the default rule:

namespace Router\Rule; 

class Default implements \Router\Rule { 

    private $dice; 

    public function __construct(\Dice\Dice $dice) { 

        $this‐>dice = $dice; 

    } 

    public function find(array $route) { 

        return $this‐>dice‐>create('$route_default');     

    } 

https://r.je/mvc­php­router­dependency­injection.html 20/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

Why break them up? It allows flexibility in the application. The router can be a Convention based router
or a Configuration based router or a Convention­over­configuration based router simply by adding the
relevant rules to it. And the rules don't have to use Dice. Other rules could be used such as filename
based routing. This frees up the router from any true dependencies making it more portable and easier to
reuse. The initialisation code would look like this:

$dice = new \Dice\Dice; 

$router = new \Router\Router; 

$router‐>addRule(new \Router\Rule\Configuration($dice)); 

$router‐>addRule(new \Router\Rule\Convention($dice)); 

$router‐>addRule(new \Router\Rule\Default($dice)); 

$route = $router‐>getRoute(explode('/', $url));

As you can see, without much effort at all, a very powerful extensible router has been created with very
little code.

Conclusion
You now have a very powerful router which is fully extensible and creates all your models, views and
controllers! By using Dice you take a lot of the boilerplate code out of the framework entry point and
simplify everything.

In the next article, I'll show you how to use the generated route with a dispatcher and you'll have a fully
functional entry point for your own MVC framework!

About the author

https://r.je/mvc­php­router­dependency­injection.html 21/22
1/31/2016 Using a Dependency Injection Container as part of a Router in MVC ­ Tom Butler

All content is by Tom Butler, a Ph.D student, Web Developer and University Lecturer based in Milton
Keynes, UK. Interests: Programming, best practices, PC Gaming, live music, gradually improving at
Flying Trapeze.

Related Articles
1. Dice ­ PHP Dependency Injection Container
2. The View gets its own data from the model
3. MVC in PHP Tutorial: Hello World
4. Maximising View reusability with View Helpers
5. MVC in PHP tutorial part 2: Real world program

More...
Contact
About
Home

https://r.je/mvc­php­router­dependency­injection.html 22/22

Вам также может понравиться