Bootstrapping Micro-Symfony
Inspiration strikes! You've just come up with a crazy awesome idea - the kind of idea that's sure to save the world and impress your friends all at once. Now all you need to do is start coding.
Of course, Symfony - the fully-featured full-stack framework will be perfect for this. Then again, what if we used Silex? The micro-framework approach might be a better way to get started so the world can quickly find out what its been missing!
But Silex isn't Symfony: it lacks a lot of the tools. And if your project grows, it can't easily evolve into a Symfony app: they're just too different.
There's another option: use Symfony as a micro-framework, meaning with as few files, directories and complications as possible, without sacrificing features. And since no official Symfony micro-framework exists right now, let's create one.
Composer Bootstrap
Start with a completely empty directory: so empty that we need to run composer init
to bootstrap a composer.json
file:
composer init
Fill in the name - none of this matters too much in a private project. Ok, dependencies. We only need one: symfony/symfony
. But I also want one more library: sensio/framework-extra-bundle
- this is for annotation routing. If you don't want that, you only need symfony/symfony
.
Our blank project now has 1 file - composer.json
- with just 2 dependencies:
{ | |
"name": "knpuniversity/micro-symfony", | |
"require": { | |
"symfony/symfony": "^2.7", | |
"sensio/framework-extra-bundle": "^3.0" | |
}, | |
// ... lines 7 - 12 | |
} |
Go back to run composer install
:
composer install
Now our empty project has a vendor/
directory.
Bootstrapping Symfony
Now that we have Symfony how do we bootstrap the framework? We need two things: a kernel class and a front controller script that boots and executes it.
Start with the kernel: create a new AppKernel.php
file at the root of your project. You can call this file anything, but I want to stay consistent with normal Symfony when possible. Make this extend Symfony's Kernel
class. Since we're not in a namespaced class, PhpStorm auto-completes the namespaces inline. Ew! Add a use
statement instead: let's keep things somewhat organized people:
// ... lines 1 - 2 | |
use Symfony\Component\HttpKernel\Kernel; | |
// ... lines 4 - 5 | |
class AppKernel extends Kernel | |
{ | |
// ... lines 8 - 22 | |
} |
Kernel
is an abstract class. Use cmd+n - or the menu option, Code->Generate - then "Implement Methods". Select the two methods at the bottom:
// ... lines 1 - 2 | |
use Symfony\Component\HttpKernel\Kernel; | |
use Symfony\Component\Config\Loader\LoaderInterface; | |
class AppKernel extends Kernel | |
{ | |
public function registerBundles() | |
{ | |
// ... lines 10 - 16 | |
} | |
public function registerContainerConfiguration(LoaderInterface $loader) | |
{ | |
// ... line 21 | |
} | |
} |
registerBundles()
Get rid of the comments. So first, let's instantiate all the bundles we need. In a normal, new, Symfony project, there are 8 or more bundles here by default. We just need 3: a new FrameworkBundle
, TwigBundle
- this is used to render error and exception pages, so you may want this, even if you're building an API. And since I'll use annotations for my routing, use SensioFrameworkExtraBundle
. If you like YAML routing, you don't even need this last one. Return the $bundles
:
// ... lines 1 - 5 | |
class AppKernel extends Kernel | |
{ | |
public function registerBundles() | |
{ | |
$bundles = array( | |
new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(), | |
new \Symfony\Bundle\TwigBundle\TwigBundle(), | |
new \Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), | |
); | |
return $bundles; | |
} | |
// ... lines 18 - 22 | |
} |
registerContainerConfiguration()
For the next method - move the inline namespace to be a use
statement on top. When the kernel boots, it needs to configure each bundle - it needs things like the database password, where to log, etc. That's the purpose of registerContainerConfiguration()
.
Use $loader->load()
and point this to a new config/config.yml
file:
// ... lines 1 - 5 | |
class AppKernel extends Kernel | |
{ | |
// ... lines 8 - 18 | |
public function registerContainerConfiguration(LoaderInterface $loader) | |
{ | |
$loader->load(__DIR__.'/config/config.yml'); | |
} | |
} |
And now create the config
directory and that file so that it isn't pointing into outerspace. Leave it blank for now.
Front Controller: index.php
That's a functional kernel! We need a front controller that can instantiate and execute it. Create a web/
directory with an index.php
inside. Symfony has two front controllers: app.php
and app_dev.php
: we'll have just one because I want less less less. We're going for the micro of micro!
In my browser, we're looking at the symfony/symfony-standard repository, because I want to steal some things. Open the web/
directory and find app_dev.php
. Copy its contents into index.php
. Now we're going to slim this down a bit:
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Debug\Debug; | |
// If you don't want to setup permissions the proper way, just uncomment the following PHP line | |
// read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information | |
//umask(0000); | |
// This check prevents access to debug front controllers that are deployed by accident to production servers. | |
// Feel free to remove this, extend it, or make something more sophisticated. | |
if (isset($_SERVER['HTTP_CLIENT_IP']) | |
|| isset($_SERVER['HTTP_X_FORWARDED_FOR']) | |
|| !(in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', 'fe80::1', '::1')) || php_sapi_name() === 'cli-server') | |
) { | |
header('HTTP/1.0 403 Forbidden'); | |
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.'); | |
} | |
$loader = require_once __DIR__.'/../app/bootstrap.php.cache'; | |
Debug::enable(); | |
require_once __DIR__.'/../app/AppKernel.php'; | |
$kernel = new AppKernel('dev', true); | |
$kernel->loadClassCache(); | |
$request = Request::createFromGlobals(); | |
$response = $kernel->handle($request); | |
$response->send(); | |
$kernel->terminate($request, $response); |
Delete the IP-checking stuff and uncomment out the umask
- that makes dealing with permissions easier. For the loader, require Composer's standard autoloader in vendor/autoload.php
. And AppKernel
is not in an app/
directory - so update the path:
// ... lines 1 - 4 | |
umask(0000); | |
// ... lines 6 - 10 | |
$loader = require_once __DIR__.'/../vendor/autoload.php'; | |
require_once __DIR__.'/../AppKernel.php'; | |
// ... lines 13 - 24 |
When you create the kernel, you pass it an environment and whether or not we're in debug
mode: that controls whether errors are shown. We'll tackle these properly soon, but right now, hardcode two new variables $env = 'dev'
and $debug = true
. Pass these into AppKernel
:
// ... lines 1 - 7 | |
$env = 'dev'; | |
$debug = true; | |
// ... lines 10 - 17 | |
$kernel = new AppKernel($env, $debug); | |
// ... lines 19 - 24 |
Oh, and put an if
statement around the Debug::enable()
line so it only runs in debug mode. This wraps PHP's error handler and gives us better errors:
// ... lines 1 - 13 | |
if ($debug) { | |
Debug::enable(); | |
} | |
// ... lines 17 - 24 |
And in the spirit of making everything as small as possible, remove the loadClassCache()
line: this adds a small performance boost. If you're feeling less micro, you can keep it.
Testing it out
That's it for the front controller! Time to try things. Open up a new terminal tab, move into the web directory, then start the built-in web server on port 8000:
cd web/
php -S localhost:8000
Let's try it - we're hoping to see a working Symfony 404 page, because we don't have any routes yet. Okay, not working yet - but this isn't so bad: it's a Symfony error, we're missing some kernel.secret
parameter.
Adding the Base Config
The reason is that we do need a little bit of configuration in config.yml
. Add a framework
key with a secret
under it that's set to anything:
framework: | |
secret: ABC123 | |
// ... lines 3 - 7 |
When we refresh now, it's a different error: something about a Twig template that's not defined. These weird errors are due to some missing configuration. Here's the minimum you need. Add a router
key with a resource
sub-key that points to a routing file. Use %kernel.root_dir%/config/routing.yml
. %kernel.root_dir%
points to wherever your kernel lives, which is the app/
directory in normal Symfony, but the root folder for us minimalists.
You also need a templating
key with an engines
sub-key set to twig
:
framework: | |
secret: ABC123 | |
router: | |
resource: '%kernel.root_dir%/config/routing.yml' | |
templating: | |
engines: [twig] |
That's all you need to get things working. Oh, and don't forget to create a config/routing.yml
file - it can be blank for now:
Refresh! It lives! "No route found for GET /" - that's our working 404 page. We'll fix that missing CSS in a bit, but this is a functional Symfony project: it just doesn't have any routes yet.
The Symfony Plugin
One of the coolest things about using Symfony is the PhpStorm plugin for it, which we cover in the Lean and Mean dev with PHP Storm tutorial.
If you have it installed, search "symfony" in your settings to find it. Check "Enable" and remove app/
from all the configurable paths. Our project is smaller than normal: instead of app/cache
and app/logs
, you just have cache
and logs
at the root. Init a new git
repository and add these to your .gitignore
file along with vendor/
:
/vendor/ | |
/cache/ | |
/logs/ |
Count the files: other than composer stuff, we have 1, 2, 3, 4 files, and a functioning Symfony framework project. #micro
Whoh! Its so awesome. Thx for great work!