This workshop slowly builds a framework.
This workshop requires you to be able to run standard Symfony 4 application. We use PHP 7.1+.
Here is the slides for the presentation: https://www.slideshare.net/TobiasNyholm/symfony-internals-workshop
Below are a short description of each exercise. When you completed one exercise and starting with the next one. You can continue with your code or "restart" from the branch mentioned in the exercise description.
Branch: 1-jbof
We start of with jbof, Just a bunch of files. Nothing will be faster than this. This is the fastest "framework" you can get.
But this is not really scalable. Lets start with adding PSR-7 requests and responses.
If you do not have any favorite PSR-7 implementation you could download nyholm/psr7
.
You can test your application with:
php -S 127.0.0.1:8080
Branch: 2-request
Let's move "our" code out from index.php. Crete a "controller" class with a function
that takes a request and returns a response. You should put your controllers under
src/
(maybe you want to create more subfolders).
It is a good practise to have your frontend controller (index.php) in a subdirectory.
That means the webserver do not have access to the root of you application and can
directly access any file. Lets put index.php in a public/
directory.
You can test your application with:
php -S 127.0.0.1:8080 -t public
Branch: 3-controller
Excellent. The framework looks pretty good now. But we do not like how we are doing the routing in index.php. It would mean that we need to edit index.php every time we want to add a new controller.
Let's implement an event loop. Run composer require "relay/relay:1.1"
. Have a quick
look at the documentation and then create a RouterMiddleware
that implements the MiddlewareInterface
as follows:
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface MiddlewareInterface
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next);
}
Make sure to build and run your array of middleware in index.php.
Note: As an alternative to relay/relay
you can use this simple Runner.php.
Branch: 4-event-loop
In this exercise we are going to add a cache system. Use your favorite cache library.
If you do not got a favorite, php-cache is a
good one. (composer require cache/filesystem-adapter
).
Create a middleware that cache the requests. So the controller is not hit twice with the same URL.
Branch: 5-cache
This is awesome. Our really fast framework is now even faster. But it is hard to update the HTML of your controller in development since the response is cached.
We want to introduce the concept of "environment" to enable the cache feature
only in "prod". In "dev" environment we want to use a null cache like cache/void-adapter
.
Install symfony/dependency-injection
and create src/Kernel.php
that should be
responsible for building the container and building the middleware array. A good
idea is to only have one public function: Kernel::handle(RequestInterface $request): ResponseInterface
.
The Kernel
should be responsible for "building" a container which all our services
are loaded. (See documentation)
After the container is built you should run ContainerBuilder::compile()
before you
use the container.
Hint: To emit (send) the response: https://github.com/Nyholm/psr7#emitting-a-response
For performance reasons, we should not build the container at every request in production environment. We should used a cached/dumped container. See the Symfony documentation about how to dump a container.
Branch: 6-container
Looking better and better now. We want to have that "admin" stuff we had in jbof. Lets add some security. Lets first separate Authentication (Who are you?) from Authorization (What are you allowed to do?). These are two new things so we need new middleware.
For this exercise, make sure to create an admin controller that only the user "alice" can see. You do not need to validate passwords.
Note: If you return with a response like:
return new Response(401, ['WWW-Authenticate'=>'Basic realm="Admin area"'], 'This page is protected');
A HTTP Basic authentication login window will show for the user. Read the input to that window by:
$auth = $request->getServerParams()['PHP_AUTH_USER'] ?? '';
$pass = $request->getServerParams()['PHP_AUTH_PW'] ?? '';
Branch: 7-security
When in "dev" environment, it is nice to have a toolbar that shows some statistics about the request. Lets try to implement that. Since this is a new feature we need a new middleware.
The toolbar should be added just before </body>
of the response.
Note: To gather statistics from ie the cache service you need to create a decorator that decorates the service.
class CacheDataCollector implements CacheItemPoolInterface
{
private $real;
private $calls;
public function __construct(CacheItemPoolInterface $cache)
{
$this->real = $cache;
}
public function getCalls()
{
return $this->calls;
}
public function getItem($key)
{
$this->calls['getItem'][] = ['key'=>$key];
return $this->real->getItem($key);
}
// ...
Branch: 8-toolbar
Lets create this controller:
use Psr\Http\Message\RequestInterface;
class ExceptionController
{
public function run(RequestInterface $request)
{
throw new \RuntimeException('This is an exception');
}
}
We want to print a helpful message in "dev" environment and a pretty "I'm sorry" page in "prod". Since this is a new feature we need a new middleware.
We built a real good framework now. It is a simple Symfony. Since we like the Symfony ecosystem so much. Lets try to refactor our framework to use more Symfony components.
Branch: 9-exception
(Only do this if you got time and energy. This exercise could easily be skipped.)
We love autowiring. It makes our service configuration small and nice. Try to autowire as many services as you can. The Symfony documentation may be a good reference.
Branch: 21-autowire
The Command Line Interface is just another frontend controller. The code is similar
to our index.php. You should require symfony/console
and create a ./bin/console
file.
You should also create a small command class to test your ./bin/console
.
Make sure you can register your command in the service container. This allows command classes to use dependency injection as normal.
Hint: There is a class Symfony\Component\Console\CommandLoader\ContainerCommandLoader
that is registered With the AddConsoleCommandPass
.
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
$container->registerForAutoconfiguration(Command::class)->addTag('console.command');
$container->addCompilerPass(new AddConsoleCommandPass());
Branch: 22-command
The time has come for us to replace the heart of our application, the event loop.
This is a major refactoring and we need to update all our middleware. Start by
downloadning symfony/event-dispatcher
. Instead of having one "loop" as we did
with relay/relay
we can now create multiple loops (or events).
Create event classes for:
- An incoming request.
- Parsing/Filtering of a response
- Exception
Our middlewares should be refactored to EventSubscribers
. The Kernel::handle
is also subject for a rewrite.
The benefit of different "loops" (or events) is that we can show the toolbar on an exception page. That was not possile before.
Hint: One feature of symfony/event-dispatcher
is that we can use
$event->stopPropagation()
which stops the current loop. That could be
useful in our cache or security middleware.
Branch: 23-event-dispatcher
To fully be able to integrate with symfony components we should be using Symfonys
implementation of request and responses. So lets remove PSR-7 and use symfony/http-foundation
.
It is a lot to rewrite but it is just simple changes. Feel free to skip ahead to next exercise.
Branch: 24-http-foundation
PHP-cache is great. But we are moving towards full Symfony. Lets use
symfony/cache
instead.
Note: This is a simple change becuase PSR-6 is awesome.
Branch: 25-cache
We've done a lot of heavy lifting ourself in our App\Kernel
. Let symfony/http-kernel
be responsible for that from now on. Our App\Kernel
should extend Symfony\Component\HttpKernel\Kernel
but we still need to define where our configuration is located.
The Symfony kernel uses a HttpKernel
to handle the request. This is done automatically
if you register a http_kernel
service:
http_kernel:
class: Symfony\Component\HttpKernel\HttpKernel
public: true
arguments:
- '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
- '@Symfony\Component\HttpKernel\Controller\ControllerResolver'
Symfony\Component\HttpKernel\Controller\ControllerResolver: ~
Branch: 26-http-kernel
Symfony 4.1 has the quickest router implemented in PHP. Lets start using it.
We want to remove our Router
middleware and define our routes in ./config/routes.yaml
instead. We are not using the FrameworkBundle just yet so we need to look at
the documentation for the routing component.
Note: Make sure to register Symfony\Component\HttpKernel\EventListener\RouterListener
in the service container.
Branch: 27-router
The Symfony\Component\HttpKernel\EventListener\RouterListener
listens to the kernel.request
event. Debug the HttpKernel::handleRaw
function to see what is happening there. Prepare short answers to the following
questions:
- What did
RouterListener
do to the$request
after thekernel.request
event has been dispatched? (line 125) - What is the purpose of
$this->resolver->getController($request)
? (line 132) - What is the purpose of
$this->argumentResolver->getArguments($request, $controller)
? (line 141) - What does this line do
$response = \call_user_func_array($controller, $arguments)
? (line 149)
Branch: 27-router
We are almost there. Let's start using the FrameworkBundle. This bundle helps you register
a lot of services. Make sure to enable the FrameworkBundle in App\Kernel
. What can
you remove now? Maybe the router configuration?
You could also have a look at Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait
. It
could be a good fit for our App\Kernel
.
Branch: 28-framework-bundle
This is the end of the workshop. You could create a new Symfony Flex probject with
composer create-project symfony/skeleton my_projecet
. Compare the differencis between a fresh
install of symfony with the framework you built.