We recently adopted a website at my job, which was built with a “custom framework”. Yes, we’ve all come across these, if not built them ourselves. Often the first thing that comes to mind is: “I can obviously do this better”. That’s what I thought too, but figured it would be smarter to use established PHP components.
The why
The problem with these frameworks is that often nobody but the creator actually know how they work. You could either spend a lot of time getting to know the framework OR modernize it using modern PHP components, like the Symfony components. I decided to do the latter.
This framework was probably not all that bad for its time (the wayback machine tells me it originates from +/- 2010): It’s OOP, has a front controller, a relatively logical directory structure and it utilizes a self-made active record pattern.
So why even bother to modernize it, you ask? I was asked to implement a relatively large new feature, which also involved removing a bunch of old code. I saw this as an opportunity to use today’s best practices to implement the new feature.
And, besides the previously mentioned “not that bad parts” about the framework it still had some pretty bad parts:
- Every single file is located in the web root. I even came across a manual for the framework that anyone was able to reach. No thanks: I already have enough nightmares about WordPress as it is.
- Every single class is located in the “classes” directory. From entities(which also happen to be repositories) to the router.
- Above, I called it router, but it was more of a slug to php file matcher
- No namespaces.
- No separate environment configs.
- No templating engine.
There’s probably more, but you get the idea.
The how
I recently read up on Fabien Potencier’s “Create your own framework” posts (must read by the way!) and started thinking if I could come up with an even better way to implement the MVC pattern than the Symfony standard edition.
Well, I couldn’t. It could be because i’ve grown so accustomed to Symfony’s way of doing things, but it could also be because I don’t have much experience as a software architect. Either way, I decided to stick with the Symfony standard edition for all my projects.
This lead me to the conclusion that the best course of action to modernize this framework was to slowly start using Symfony’s components.
Step 1: Move all php files out of the web root
At first the directory structure looked something like this, with every single file in the public_html directory:
project/ public_html/ classes/ css/ images/ js/ manual/ pages/ private/ admin/ templates/ tests/ uploads/ index.php
I decided to move everything except the js, css, images and uploads directory (and of course index.php) outside of the web root and ended up with the following structure:
project/ classes/ manual/ pages/ private/ admin/ public_html/ css/ images/ js/ uploads/ index.php templates/ tests/
Looks better already doesn’t it? Luckily the references to each directory were all relative and in one place, so that didn’t give me too many problems.
Step 2: Introducing namespaces
Note: Step 3 kind of makes this a redundant step. I kept it in anyway to remind everyone to always plan ahead.
The current autoloader didn’t do much more than fetch a php file with the corresponding name from the classes directory. Upgrading it to be able to use namespaces was pretty easy:
As you can see, adding one line allows you to use namespaces. Besides this change to the autoloader, I also renamed the classes directory to src. This describes the meaning of the directory better IMO and it is what Symfony uses.
Since I could now use namespaces, I also decided to adopt Symfony's bundle directory structure
project/ ... src AppBundle ... Controller Utils ... ...Step 3: Use Composer as a package manager
The framework only had two dependencies at the moment we adopted it, so using Composer is kind of overkill. But what if we want to use Symfony's DependencyInjection component or the HttpFoundation? Better do it the right way immediately.
I decided to immediatly use the Symfony HttpFoundation, so using Composer took nothing more than executing composer require symfony/http-foundation in the project root:
$ cd /var/www/html/project $ composer require symfony/http-foundationTo enable autoloading for our src directory there were two things left to do, first add the following to composer.json:
# composer.json "autoload": { "psr-4": { "": "src/" } }Second, disable the old autoloader and include Composer's autoload file:
// public_html/index.php require __DIR__ . '/../vendor/autoload.php';And done. We can now use Composer to install and manage dependencies.
Step 4: Apply Symfony's practices
I really like the idea behind the HTTP Kernel, which transforms a Request object into a Response object, so I decided to implement this, even though it wouldn't offer me much in the short term.
First I had to require the symfony/http-kernel:
$ cd /var/www/html/project $ composer require symfony/http-kernelNow that we have the http-kernel dependency installed, we can use it to improve our code.
The framework's old bootstrap code looked a bit like this:
// public_html/index.php Application::init(); Page::print();So, all I had to do was create a Kernel class, which implements Symfony\Component\HttpKernel\HttpKernelInterface and move the above functions to the Kernel's handle method.
// src/AppBundle/Kernel/AppKernel.php namespace AppBundle\Kernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class AppKernel implements HttpKernelInterface { public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) { $response = new Response(); ob_start(); \MorSite::initialiseer(); \Pagina::printJezelf(); $content = ob_get_clean(); $response->setContent($content); return $response; } }All that's left is changing the bootstrap file:
// public_html/index.php use Symfony\Component\HttpFoundation\Request; use AppBundle\Kernel\AppKernel; $request = Request::createFromGlobals(); $kernel = new AppKernel(); $response = $kernel->handle($request); $response->send();So what have we actually gained from all this? Firstly, we can now use the Request object. So instead of having to access the superglobals $_POST and $_GET, we can simply do:
$foo = $request->get("bar"); // Instead of $foo = isset($_POST['bar'] ? $_POST['bar'] : null;Secondly, since we're using a Http Kernel, we can easily swap out the Kernel with another. You could for example use one Kernel for your legacy code and one Kernel for your "modern" code. I haven't yet decided if and how I am going to do this, but it is a possibility.
Step 5: Write better code
This step might seem obvious, but I'll say it anyway. Now that you have more powerful tools at your disposal, you should actually use them. And use them well. Otherwise, in a few years time someone else might look at your code and think "I can obviously do this better".