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.