1
\$\begingroup\$

as a learning project i have decided to try and implement/build my own take on a PHP MVC framework. I realise there are alot of good PHP frameworks out there but rather than learning how to use a framework, i wanted to learn how frameworks work. I have tried to follow the best practices but had to make some compramises here and there. I would really appreciate if you could review/rip apart my code and help me improve my 'frameworks' core. Quick note before starting this framework is built with composer libraries, will detail as and when.

To start, system routes are defined in a yaml config file. Each route has a http method or methods to match against, the path to match against, and the controller action pair used to handle the request/response.

@file routing.yml

routes:

    # Home Page Route
    home_page:
        method:        GET
        path:          /
        controller:    Controller\Home\HomeController
        action:        indexAction

    # Login Page Route
    login_page:
        method:        GET
        path:          /login
        controller:    Controller\Authentication\LoginController
        action:        indexAction

These routes are then parsed to a Route object, implementating RouteInterface and then added to a RouteCollection object.

Using this RouteCollection object (and others) we build a Router object. Which is an extension of the Klein/Klein router package.

Note: One big thing that will jump out here is that we are injecting the dependency injection container (PHP-DI) into the router directly. This is so on route match we can fully load the required controller (including all services/dependencies etc etc)

<?php
@file Router.php

class Router extends Klein {

    private $container;
    private $routeCollection;
    private $urlGenerator;

    //Set properties initialise klein
    public function __construct(Container $container, RouterCollectionInterface $routeCollection, UrlGeneratorInterface $urlGenerator){
        $this->container = $container;
        $this->routeCollection = $routeCollection;
        $this->urlGenerator = $urlGenerator;
        parent::__construct();
    }

    //Generate URL from route key rather hardcoding everywhere.
    public function generateRouteUrl($routeKey, $paramaters = array(){
        return $this->urlGenerator->generate(
            $this->routeCollection->findByKey($routeKey)->getPath(),
            $parameters
        );
    }

    //Do the routing
    //Loop over all routes from the route collection, build native klein matcher.
    //Use the container to then load routes controller, before returning the output from its action method.
    public function route(){
        foreach($this->routeCollection->getAll() as $route){

            //This is the klein matcher call (does the actual routing) 
            //Note aswell we are injection the router into the controller
            //This is done because the router on route match holds request/response object used to fetch information (eg, GET/POST params)
            $this->respond($route->getMethod(), $route->getPath(), function() use ($route){
                $controller = $this->controller->make($route->getController(), array(
                    'router' => $this,
                    'twig' => $this->container->get('Twig')
                ));
                return $controller->{$route->getAction()}();
            });

        }
    }

}

So on successful route, the routes defined controller/action pairing is called. All controllers in the system/framework extend an AbstractController like so. I will include a few methods to show how the injected router is leveraged.

<?php 
@file AbstractController.php

abstract class AbstractController {

    private $router
    private $twig;

    public function __construct(Router $router, Twig_Environment $twig){
        $this->router = $router;
        $this->twig = $twig;
    }

    //Get a HTTP GET variable for the request
    //As mentioned on successful route, the router holds request and response object.
    //The request holds parameters etc etc etc
    //The body of this methods follows klein rules to extract a get variable
    protected getGetParameter($key, $default = FALSE){
        return $this->router->request()->paramsGet()->get($key, $default);
    }

    //Get the HTTP response code.
    //Extract response code from the routers response object.
    //Router request/response objects built on match by klein library
    //The body of this method follows klein rules to get response code.
    protected function getResponseCode(){
        return $this->router->response()->code();
    }

    //Redirect to another router
    //Use route keys defined in routing.yml instead of hard urls
    //Use route url generator then follow klein rules for redirect.
    protected function redirectToRoute($routeKey, $paramters = array(), $code = 302){
        $this->router->response()->redirect(
            $this->router->generateRouteUrl($routeKey, $parameters),
            $code
        );
    }

    //Render a twig template
    protected function render($templateFile, $parameters = array()){
        return $this->twig->render($templateFile, $parameters);
    }
}

An example of an actual controller, as defined in routing.yml. This is where the fetching of the controller in the router using the dependency injection container comes into it's own. It allows the controllers required services to be injected using property injection and annotations.

<?php
@file HomeController.php

class HomeController extends AbstractController{

    /**
     * @Inject
     * @var NewsService
     */
    private $newsService;

    //Through loading the controller in the router via an injected container
    //the service properties are automatically attached using the @Inject annotation.
    //- Basic example, grab news from service, show it in home page.
    public function indexAction(){
        $latestNews = $this->newsService->getLatestNews();
        return $this->render('homepage.twig', $latestNews)
    }

}

I appreciate that this is a bit of a long one but if anyone has time to review my approach and provide pointers/feedback it would be much appreciated.

Also if you need any further details just let me know and ill update the question.

Thanks again!

UPDATES:

Just a quick one, i would like to point out that my actual files use php docblocks psr-4 namespace etc etc but these have been stripped out to make for a more streamlined code review. I have tried to make the comments that i have added in more specific to the questions/reasoning within this question.

\$\endgroup\$
  • 1
    \$\begingroup\$ If I find some more time I'll go into some more details. But a general comment: there are no proper docblocks. Best to use the phpdoc found here: phpdoc.org/docs/latest/guides/docblocks.html This not only makes reading your code easier, but it also eases in understanding a method since our eyes are accustomed to these type of comments \$\endgroup\$ – Pinoniq May 18 '16 at 10:52
  • \$\begingroup\$ Thanks for the feedback pinoniq, in my actual files there are proper doc blocks . I have changed the comments here so they are more specific to the question, i wanted the comments to tie into the text within this question to help provide context and 'whys' for the methods. Thanks again for your input though, much appreicated, good looking out! \$\endgroup\$ – JParkinson1991 May 18 '16 at 10:59
  • 1
    \$\begingroup\$ @Goon3r suggest you share your actual code. The first thing I noticed is that you don't appear to use a code standard, and you're missing doc blocks \$\endgroup\$ – Robbie Averill May 18 '16 at 20:42
  • 1
    \$\begingroup\$ as @RobbieAverill mentioned. You should include your original comments. comments are a big part of the code (often comprising of 60% of characters in a file, or even more). For me they are part of a code review. They help us read the intent of a function (@ param, @ return, ...). \$\endgroup\$ – Pinoniq May 19 '16 at 11:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.