PHP Classes

File: simfony_own_fw.md

Recommend this page to a friend!
  Classes of Slavko Srakocic   B12 PHP FW   simfony_own_fw.md   Download  
File: simfony_own_fw.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: B12 PHP FW
Manage database records with a PDO CRUD interface
Author: By
Last change: Update of simfony_own_fw.md
Date: 1 year ago
Size: 58,425 bytes
 

Contents

Class file image Download

Own PHP Framework based on Simfony components

  1. Introduction
  2. HttpFoundation Component 1. Going OOP with the HttpFoundation Component
  3. Front Controller
  4. Routing Component
  5. Templating
  6. HttpKernel Component: Controller Resolver
  7. Separation of Concerns
  8. Unit Testing
  9. EventDispatcher Component
  10. HttpKernel Component: HttpKernelInterface
  11. HttpKernel Component: The HttpKernel Class
  12. DependencyInjection Component

1\. Introduction

See https://symfony.com/doc/current/create\_framework/http\_foundation.html This poage URL is eg. : http://dev1:8083/fwphp/z\_test/simf\_ownfw/index.php?name=aaa

When creating a framework, following the MVC pattern is not the right goal. The main goal should be the Separation of Concerns; this is probably the only design pattern that you should really care about. The fundamental principles of the Symfony Components are focused on the HTTP specification. As such, the framework that you are going to create should be more accurately labelled as a HTTP framework or Request/Response framework.

Writing web code is about interacting with HTTP. In PHP, the request is represented by global variables (\\$\_GET, \\$\_POST, \\$\_FILE, \\$\_COOKIE, \\$\_SESSION...) and the response is generated by functions (echo, header, setcookie, ...).

Security and testability (PHPUnit unit tests)

First version :

$name = $_GET['name'] ?? 'World'; //if name query parameter is not defined in the URL query string     
header('Content-Type: text/html; charset=utf-8');      
//XSS (Cross-Site Scripting) - code more secure :     
printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'));     

The first step towards better code is probably to use an Object-Oriented approach; that is the main goal of the Symfony HttpFoundation component: 1\. replacing the default PHP global variables and functions by an Object-Oriented layer To use this component, add it as a dependency of the project: composer require symfony/http-foundation ... Outputs : 1 package suggestions were added by new dependencies, use \composer suggest\ to see details. Generating autoload files 3 packages you are using are looking for funding. Use \composer fund\ command to find out more!

Running this command will also automatically download the Symfony HttpFoundation component and install it under the vendor/ directory. A composer.json and a composer.lock file will be generated as well, containing the new requirement.

When installing a new dependency, Composer also generates a vendor/autoload.php file that allows any class to be autoloaded. Without autoloading, you would need to require the file where a class is defined before being able to use it. But thanks to PSR-4, we can just let Composer and PHP do the hard work for us.

2\. HttpFoundation Component

Going OOP with the HttpFoundation Component Using framework is good idea, even for the simplest snippet of code and creating your framework on top of Symfony components is better than creating a framework from scratch. We won't talk about the traditional benefits of using a framework when working on big applications with more than a few developers; the Internet already has plenty of good resources on that topic.

As you might have noticed, securing your code with htmlspecialchars is tedious and error prone. That's one of the reasons why using a template engine like Twig, where auto-escaping is enabled by default, might be a good idea (and explicit escaping is also less painful with the usage of a simple e filter).

Beyond security, this code can be complex to test. Even if there is not much to test, it strikes me that writing unit tests for the simplest possible snippet of PHP code is not natural and feels ugly. Here is a tentative PHPUnit unit test for the above code:

// framework/test.php
use PHPUnit\\Framework\\TestCase;

class IndexTest extends TestCase
{
  public function testHello()
  {
    $\_GET\['name'\] = 'Fabien';
    ob\_start();
    include 'index.php';
    $content = ob\_get\_clean();
    $this->assertEquals('Hello Fabien', $content);
  }
}

If our application were just slightly bigger, we would have been able to find even more problems. If you are curious about them, read Symfony versus Flat PHP (A Basic Blog) chapter of the book.

// framework/index.php
require\_once \_\_DIR\_\_.'/vendor/autoload.php';

use Symfony\\Component\\HttpFoundation\\Request;
use Symfony\\Component\\HttpFoundation\\Response;

//creates a Request object based on the current PHP global variables :
$request = Request::createFromGlobals();

$name = $request->query->get('name', 'World');

$response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT\_QUOTES, 'UTF-8')));

//sends Response object back to client (first outputs HTTP headers followed by content) :
$response->send();

Before the send() call, we should have added a call to the prepare() method ($response->prepare($request);) to ensure that our Response were compliant with the HTTP specification. For instance, if we were to call the page with the HEAD method, it would remove the content of the Response.


The main difference with the previous code is that you have total control of HTTP messages. You can create whatever request you want and you are in charge of sending the response whenever you see fit. We haven't explicitly set the Content-Type header in the rewritten code as the charset of the Response object defaults to UTF-8. With the Request class, you have all the request information at your fingertips thanks to a nice and simple API:

// the URI being requested (e.g. /about) minus any query parameters  
$request->getPathInfo();  
  
// retrieve GET and POST variables respectively  
$request->query->get('foo');  
$request->request->get('bar', 'default value if bar does not exist');  
  
// retrieve SERVER variables  
$request->server->get('HTTP\_HOST');  
  
// retrieves an instance of UploadedFile identified by foo  
$request->files->get('foo');  
  
// retrieve a COOKIE value  
$request->cookies->get('PHPSESSID');  
  
// retrieve an HTTP request header, with normalized, lowercase keys  
$request->headers->get('host');  
$request->headers->get('content-type');  
  
$request->getMethod(); // GET, POST, PUT, DELETE, HEAD  
$request->getLanguages(); // an array of languages the client accepts  
You can also simulate a request:  
$request = Request::create('/index.php?name=Fabien');  
With the Response class, you can tweak the response:  
  
$response = new Response();  
  
$response->setContent('Hello world!');  
$response->setStatusCode(200);  
$response->headers->set('Content-Type', 'text/html');  
  
// configure the HTTP cache headers  
$response->setMaxAge(10);  

To debug a response, cast it to a string; it will return the HTTP representation of the response (headers and content).


Last but not least, these classes, like every other class in the Symfony code, have been audited for security issues by an independent company. And being an Open-Source project also means that many other developers around the world have read the code and have already fixed potential security problems. When was the last time you ordered a professional security audit for your home-made framework?


Even something as simple as getting the client IP address can be insecure:

if ($myIp === $\_SERVER\['REMOTE\_ADDR'\]) {  
// the client is a known one, so give it some more privilege  
}  

It works perfectly fine until you add a reverse proxy in front of the production servers; at this point, you will have to change your code to make it work on both your development machine (where you don't have a proxy) and your servers:

if ($myIp === $\_SERVER\['HTTP\_X\_FORWARDED\_FOR'\] || $myIp === $\_SERVER\['REMOTE\_ADDR'\]) {  
// the client is a known one, so give it some more privilege  
    }  

Using the Request::getClientIp() method would have given you the right behavior from day one (and it would have covered the case where you have chained proxies):

   
$request = Request::createFromGlobals();  
  
if ($myIp === $request->getClientIp()) {  
// the client is a known one, so give it some more privilege  
}  

And there is an added benefit: it is secure by default. What does it mean? The $\_SERVER\['HTTP\_X\_FORWARDED\_FOR'\] value cannot be trusted as it can be manipulated by the end user when there is no proxy. So, if you are using this code in production without a proxy, it becomes trivially easy to abuse your system. That's not the case with the getClientIp() method as you must explicitly trust your reverse proxies by calling setTrustedProxies():

   
Request::setTrustedProxies(\['10.0.0.1'\]);  
  
if ($myIp === $request->getClientIp()) {  
   // the client is a known one, so give it some more privilege  
}  

So, the getClientIp() method works securely in all circumstances. You can use it in all your projects, whatever the configuration is, it will behave correctly and safely. That's one of the goals of using a framework. If you were to write a framework from scratch, you would have to think about all these cases by yourself. Why not use a technology that already works? If you want to learn more about the HttpFoundation component, you can have a look at the Symfony\\Component\\HttpFoundation API or read its dedicated documentation.

Believe it or not but we have our first framework. You can stop now if you want. Using just the Symfony HttpFoundation component already allows you to write better and more testable code. It also allows you to write code faster as many day-to-day problems have already been solved for you. As a matter of fact, projects like Drupal have adopted the HttpFoundation component; if it works for them, it will probably work for you. Don't reinvent the wheel. I've almost forgotten to talk about one added benefit: using the HttpFoundation component is the start of better interoperability between all frameworks and applications using it (like SymfonyDrupal 8phpBB 3Laravel and ezPublish 5, and more).


3\. Front Controller design pattern -----------------------------------


Add another page that says goodbye:lverInterface $argumentResolver)

// framework/bye.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$response = new Response('Goodbye!');
$response->send();

As you can see for yourself, much of the code is exactly the same as the one we have written for the first page. Let's extract the common code that we can share between all our pages. Code sharing sounds like a good plan to create our first "real" framework!

The PHP way of doing the refactoring would probably be the creation of an include file:

// framework/init.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();
Let's see it in action:

 Copy
// framework/index.php
require_once __DIR__.'/init.php';

$name = $request->query->get('name', 'World');

$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();
And for the "Goodbye" page:

 Copy
// framework/bye.php
require_once __DIR__.'/init.php';

$response->setContent('Goodbye!');
$response->send();

We have indeed moved most of the shared code into a central place, but it does not feel like a good abstraction, does it? We still have the send() method for all pages, our pages do not look like templates and we are still not able to test this code properly.

Moreover, adding a new page means that we need to create a new PHP script, the name of which is exposed to the end user via the URL (http://127.0.0.1:4321/bye.php). There is a direct mapping between the PHP script name and the client URL. This is because the dispatching of the request is done by the web server directly. It might be a good idea to move this dispatching to our code for better flexibility. This can be achieved by routing all client requests to a single PHP script.


Exposing a single PHP script to the end user is a design pattern called "front controller". Such a script might look like the following:

// framework/front.php
require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();
$response = new Response();

$map = [
    '/hello' => __DIR__.'/hello.php',
    '/bye'   => __DIR__.'/bye.php',
];

$path = $request->getPathInfo();
if (isset($map[$path])) {
    require $map[$path];
} else {
    $response->setStatusCode(404);
    $response->setContent('Not Found');
}

$response->send();

And here is for instance the new hello.php script:

// framework/hello.php  
$name = $request->query->get('name', 'World');  
$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT\_QUOTES, 'UTF-8')));

In the front.php script, $map associates URL paths with their corresponding PHP script paths.

As a bonus, if the client asks for a path that is not defined in the URL map, we return a custom 404 page. You are now in control of your website.

To access a page, you must now use the front.php script:

http://127.0.0.1:4321/front.php/hello?name=Fabien  
http://127.0.0.1:4321/front.php/bye  

/hello and /bye are the page paths.

Most web servers like Apache or nginx are able to rewrite the incoming URLs and remove the front controller script so that your users will be able to type http://127.0.0.1:4321/hello?name=Fabien, which looks much better.

The trick is the usage of the Request::getPathInfo() method which returns the path of the Request by removing the front controller script name including its sub-directories (only if needed -- see above tip).

You don't even need to set up a web server to test the code. Instead, replace $request = Request::createFromGlobals(); call to something like  $request = Request::create('/hello?name=Fabien');  where the argument is the URL path you want to simulate.

Now that the web server always accesses the same script (front.php) for all pages, we can secure the code further by moving all other PHP files outside of the web root directory:

example.com
??? composer.json
??? composer.lock
??? src
?   ??? pages
?       ??? hello.php
?       ??? bye.php
??? vendor
?   ??? autoload.php
??? web
    ??? front.php

Put another way: your code dispatches an event to the dispatcher, the dispatcher notifies all registered listeners for the event, and each listener does whatever it wants with the event. As an example, let's create a listener that transparently adds the Google Analytics code to all responses. To make it work, the framework must dispatch an event just before returning the Response instance:

// example.com/src/Simplex/Framework.php  
namespace Simplex;  
  
use Symfony\\Component\\EventDispatcher\\EventDispatcher;  
use 
Symfony\\Component\\HttpFoundation\\Request;  
use 
Symfony\\Component\\HttpFoundation\\Response;  
use 
Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface;  
use 
Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface;  
use 
Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException;  
use 
Symfony\\Component\\Routing\\Matcher\\UrlMatcherInterface;  
  
class Framework  
{  
private $dispatcher;  
private $matcher;  
private $controllerResolver;  
private $argumentResolver;  
  
public function \_\_construct(EventDispatcher 
$dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface 
$controllerResolver, ArgumentResolverInterface $argumentResolver)  
{  
$this->dispatcher = $dispatcher;  
$this->matcher = $matcher;  
$this->controllerResolver = $controllerResolver;  
$this->argumentResolver = 
$argumentResolver;  
}  
  
public function handle(Request $request)  
{  
$this->matcher->getContext()->fromRequest($request);  
  
try {  
$request->attributes->add($this->matcher->match($request->getPathInfo()));  
  
$controller = $this->controllerResolver->getController($request);  
$arguments = $this->argumentResolver->getArguments($request, $controller);  
  
$response = call\_user\_func\_array($controller, $arguments);  
} catch 
(ResourceNotFoundException $exception) {  
$response = new Response('Not 
Found', 404);  
} catch (\\Exception $exception) {  
$response = new 
Response('An error occurred', 500);  
}  
  
// dispatch a response event  
$this->dispatcher->dispatch(new ResponseEvent($response, $request), 'response');  
  
return $response;  
}  
}  
Each time the framework handles a Request, 
a ResponseEvent event is now dispatched:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
// example.com/src/Simplex/ResponseEvent.php  
namespace Simplex;  
  
use Symfony\\Component\\HttpFoundation\\Request;  
use 
Symfony\\Component\\HttpFoundation\\Response;  
use 
Symfony\\Contracts\\EventDispatcher\\Event;  
  
class ResponseEvent extends 
Event  
{  
private $request;  
private $response;  
  
public function 
\_\_construct(Response $response, Request $request)  
{  
$this->response = 
$response;  
$this->request = $request;  
}  
  
public function 
getResponse()  
{  
return $this->response;  
}  
  
public function 
getRequest()  
{  
return $this->request;  
}  
}  
The last step is the 
creation of the dispatcher in the front controller and the registration of a 
listener for the response event:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
// example.com/web/front.php  
require\_once 
\_\_DIR\_\_.'/../vendor/autoload.php';  
  
// ...  
  
use 
Symfony\\Component\\EventDispatcher\\EventDispatcher;  
  
$dispatcher = new 
EventDispatcher();  
$dispatcher->addListener('response', function 
(Simplex\\ResponseEvent $event) {  
$response = $event->getResponse();  
  
if 
($response->isRedirection()  
|| ($response->headers->has('Content-Type') && 
false === strpos($response->headers->get('Content-Type'), 'html'))  
|| 'html' 
!== $event->getRequest()->getRequestFormat()  
) {  
return;  
}  
  
$response->setContent($response->getContent().'GA CODE');  
});  
  
$controllerResolver = new ControllerResolver();  
$argumentResolver = new 
ArgumentResolver();  
  
$framework = new Simplex\\Framework($dispatcher, 
$matcher, $controllerResolver, $argumentResolver);  
$response = 
$framework->handle($request);  
  
$response->send();  
The listener is just 
a proof of concept and you should add the Google Analytics code just before the 
body tag.  
As you can see, addListener() associates a valid PHP callback to a 
named event (response); the event name must be the same as the one used in 
the dispatch() call.  
In the listener, we add the Google Analytics code only 
if the response is not a redirection, if the requested format is HTML and if the 
response content type is HTML (these conditions demonstrate the ease of 
manipulating the Request and Response data from your code).  
So far so good, 
but let's add another listener on the same event. Let's say that we want to set 
the Content-Length of the Response if it is not already set:  
1  
2  
3  
4  
5  
6  
7  
8  
$dispatcher->addListener('response', function 
(Simplex\\ResponseEvent $event) {  
$response = $event->getResponse();  
$headers = $response->headers;  
  
if (!$headers->has('Content-Length') && 
!$headers->has('Transfer-Encoding')) {  
$headers->set('Content-Length', 
strlen($response->getContent()));  
}  
});  
Depending on whether you have 
added this piece of code before the previous listener registration or after it, 
you will have the wrong or the right value for the Content-Length header. 
Sometimes, the order of the listeners matter but by default, all listeners are 
registered with the same priority, 0. To tell the dispatcher to run a listener 
early, change the priority to a positive number; negative numbers can be used 
for low priority listeners. Here, we want the Content-Length listener to be 
executed last, so change the priority to -255:  
1  
2  
3  
4  
5  
6  
7  
8  
$dispatcher->addListener('response', function (Simplex\\ResponseEvent 
$event) {  
$response = $event->getResponse();  
$headers = 
$response->headers;  
  
if (!$headers->has('Content-Length') && 
!$headers->has('Transfer-Encoding')) {  
$headers->set('Content-Length', 
strlen($response->getContent()));  
}  
}, -255);  
When creating your 
framework, think about priorities (reserve some numbers for internal listeners 
for instance) and document them thoroughly.  
Let's refactor the code a bit by 
moving the Google listener to its own class:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
// 
example.com/src/Simplex/GoogleListener.php  
namespace Simplex;  
  
class 
GoogleListener  
{  
public function onResponse(ResponseEvent $event)  
{  
$response = $event->getResponse();  
  
if ($response->isRedirection()  
|| 
($response->headers->has('Content-Type') && false === 
strpos($response->headers->get('Content-Type'), 'html'))  
|| 'html' !== 
$event->getRequest()->getRequestFormat()  
) {  
return;  
}  
  
$response->setContent($response->getContent().'GA CODE');  
}  
}  
And do 
the same with the other listener:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
// 
example.com/src/Simplex/ContentLengthListener.php  
namespace Simplex;  
  
class ContentLengthListener  
{  
public function onResponse(ResponseEvent 
$event)  
{  
$response = $event->getResponse();  
$headers = 
$response->headers;  
  
if (!$headers->has('Content-Length') && 
!$headers->has('Transfer-Encoding')) {  
$headers->set('Content-Length', 
strlen($response->getContent()));  
}  
}  
}  
Our front controller should 
now look like the following:  
1  
2  
3  
$dispatcher = new 
EventDispatcher();  
$dispatcher->addListener('response', \[new 
Simplex\\ContentLengthListener(), 'onResponse'\], -255);  
$dispatcher->addListener('response', \[new Simplex\\GoogleListener(), 
'onResponse'\]);  
Even if the code is now nicely wrapped in classes, there is 
still a slight issue: the knowledge of the priorities is "hardcoded" in the 
front controller, instead of being in the listeners themselves. For each 
application, you have to remember to set the appropriate priorities. Moreover, 
the listener method names are also exposed here, which means that refactoring 
our listeners would mean changing all the applications that rely on those 
listeners. The solution to this dilemma is to use subscribers instead of 
listeners:  
1  
2  
3  
$dispatcher = new EventDispatcher();  
$dispatcher->addSubscriber(new Simplex\\ContentLengthListener());  
$dispatcher->addSubscriber(new Simplex\\GoogleListener());  
A subscriber knows 
about all the events it is interested in and pass this information to the 
dispatcher via the getSubscribedEvents() method. Have a look at the new version 
of the GoogleListener:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
// example.com/src/Simplex/GoogleListener.php  
namespace 
Simplex;  
  
use Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;  
  
class GoogleListener implements EventSubscriberInterface  
{  
// ...  
  
public static function getSubscribedEvents()  
{  
return \['response' => 
'onResponse'\];  
}  
}  
And here is the new version 
of ContentLengthListener:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
// example.com/src/Simplex/ContentLengthListener.php  
namespace Simplex;  
  
use 
Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;  
  
class 
ContentLengthListener implements EventSubscriberInterface  
{  
// ...  
  
public static function getSubscribedEvents()  
{  
return \['response' => 
\['onResponse', -255\]\];  
}  
}  
A single subscriber can host as many 
listeners as you want on as many events as needed.  
To make your framework 
truly flexible, don't hesitate to add more events; and to make it more awesome 
out of the box, add more listeners. Again, this book is not about creating a 
generic framework, but one that is tailored to your needs. Stop whenever you see 
fit, and further evolve the code from there.  

10\. HttpKernel Component: 
HttpKernelInterface
-----------------------------------------------

In the conclusion of the second chapter of this book, I've talked about one 
great benefit of using the Symfony components: the interoperability between all 
frameworks and applications using them. Let's do a big step towards this goal by 
making our framework implement HttpKernelInterface:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
namespace Symfony\\Component\\HttpKernel;  
  
// 
...  
interface HttpKernelInterface  
{  
/\\  
\* @return Response A 
Response instance  
\*/  
public function handle(  
Request $request,  
$type 
= self::MAIN\_REQUEST,  
$catch = true  
);  
}  
HttpKernelInterface is 
probably the most important piece of code in the HttpKernel component, no 
kidding. Frameworks and applications that implement this interface are fully 
interoperable. Moreover, a lot of great features will come with it for free.  
Update your framework so that it implements this interface:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
// 
example.com/src/Framework.php  
  
// ...  
use 
Symfony\\Component\\HttpKernel\\HttpKernelInterface;  
  
class Framework 
implements HttpKernelInterface  
{  
// ...  
  
public function handle(  
Request $request,  
$type = HttpKernelInterface::MAIN\_REQUEST,  
$catch = true  
) {  
// ...  
}  
}  
With this change, a little goes a long way! Let's 
talk about one of the most impressive upsides: transparent [HTTP 
caching](https://symfony.com/doc/current/http_cache.html) support.  
The HttpCache class implements a fully-featured reverse 
proxy, written in PHP; it implements HttpKernelInterface and wraps 
another HttpKernelInterface instance:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
// example.com/web/front.php  
  
// ...  
use 
Symfony\\Component\\HttpKernel;  
  
$framework = new 
Simplex\\Framework($dispatcher, $matcher, $controllerResolver, 
$argumentResolver);  
$framework = new HttpKernel\\HttpCache\\HttpCache(  
$framework,  
new HttpKernel\\HttpCache\\Store(\_\_DIR\_\_.'/../cache')  
);  
  
$response = $framework->handle($request);  
$response->send();  
That's all it 
takes to add HTTP caching support to our framework. Isn't it amazing?  
Configuring the cache needs to be done via HTTP cache headers. For instance, to 
cache a response for 10 seconds, use the Response::setTtl() method:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
// 
example.com/src/Calendar/Controller/LeapYearController.php  
  
// ...  
public function index(Request $request, $year)  
{  
$leapYear = new 
LeapYear();  
if ($leapYear->isLeapYear($year)) {  
$response = new 
Response('Yep, this is a leap year!');  
} else {  
$response = new 
Response('Nope, this is not a leap year.');  
}  
  
$response->setTtl(10);  
  
return $response;  
}  
If you are running your framework from the command 
line by simulating requests (Request::create('/is\_leap\_year/2012')), you can 
debug Response instances by dumping their string representation (echo 
$response;) as it displays all headers as well as the response content.  
To 
validate that it works correctly, add a random number to the response content 
and check that the number only changes every 10 seconds:  
$response = new 
Response('Yep, this is a leap year! '.rand());  
When deploying to your 
production environment, keep using the Symfony reverse proxy (great for shared 
hosting) or even better, switch to a more efficient reverse proxy like Varnish.  
Using HTTP cache headers to manage your application cache is very powerful and 
allows you to tune finely your caching strategy as you can use both the 
expiration and the validation models of the HTTP specification. If you are not 
comfortable with these concepts, read the [HTTP 
caching](https://symfony.com/doc/current/http_cache.html) chapter of the Symfony documentation.  
The Response class contains 
methods that let you configure the HTTP cache. One of the most powerful 
is setCache() as it abstracts the most frequently used caching strategies into a 
single array:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
$response->setCache(\[  
'must\_revalidate' => false,  
'no\_cache' => false,  
'no\_store' => false,  
'no\_transform' => false,  
'public' => true,  
'private' => false,  
'proxy\_revalidate' => false,  
'max\_age' => 600,  
's\_maxage' => 600,  
'immutable' => true,  
'last\_modified' => new 
\\DateTime(),  
'etag' => 'abcdef'  
\]);  
  
// it is equivalent to the 
following code  
$response->setPublic();  
$response->setMaxAge(600);  
$response->setSharedMaxAge(600);  
$response->setImmutable();  
$response->setLastModified(new \\DateTime());  
$response->setEtag('abcde');  
When using the validation model, the isNotModified() method allows you to cut on 
the response time by short-circuiting the response generation as early as 
possible:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
$response->setETag('whatever\_you\_compute\_as\_an\_etag');  
  
if 
($response->isNotModified($request)) {  
return $response;  
}  
  
$response->setContent('The computed content of the response');  
  
return 
$response;  
Using HTTP caching is great, but what if you cannot cache the 
whole page? What if you can cache everything but some sidebar that is more 
dynamic that the rest of the content? Edge Side Includes (ESI) 
to the rescue! Instead of generating the whole content in one go, ESI allows you 
to mark a region of a page as being the content of a sub-request call:  
1  
2  
3  
4  
5  
This is the content of your page  
  
Is 2012 a leap year? 
  
  
Some other content  
For ESI tags to be supported by HttpCache, you need to pass it an instance of 
the ESI class. The ESI class automatically parses ESI tags and makes 
sub-requests to convert them to their proper content:  
1  
2  
3  
4  
5  
$framework = new HttpKernel\\HttpCache\\HttpCache(  
$framework,  
new 
HttpKernel\\HttpCache\\Store(\_\_DIR\_\_.'/../cache'),  
new 
HttpKernel\\HttpCache\\Esi()  
);  
For ESI to work, you need to use a reverse 
proxy that supports it like the Symfony implementation. Varnish is 
the best alternative and it is Open-Source.  
When using complex HTTP caching 
strategies and/or many ESI include tags, it can be hard to understand why and 
when a resource should be cached or not. To ease debugging, you can enable the 
debug mode:  
1  
2  
3  
4  
5  
6  
$framework = new 
HttpKernel\\HttpCache\\HttpCache(  
$framework,  
new 
HttpKernel\\HttpCache\\Store(\_\_DIR\_\_.'/../cache'),  
new 
HttpKernel\\HttpCache\\Esi(),  
\['debug' => true\]  
);  
The debug mode adds 
a X-Symfony-Cache header to each response that describes what the cache layer 
did:  
1  
2  
3  
X-Symfony-Cache: GET /is\_leap\_year/2012: stale, invalid, 
store  
  
X-Symfony-Cache: GET /is\_leap\_year/2012: fresh  
HttpCache has 
many features like support for 
the stale-while-revalidate and stale-if-error HTTP Cache-Control extensions as 
defined in RFC 5861.  
With the addition of a single interface, our framework 
can now benefit from the many features built into the HttpKernel component; HTTP 
caching being just one of them but an important one as it can make your 
applications fly!  

11\. HttpKernel Component: The HttpKernel Class
-----------------------------------------------

If you were to use our framework right now, you would probably have to add 
support for custom error messages. We do have 404 and 500 error support but the 
responses are hardcoded in the framework itself. Making them customizable is 
straightforward though: dispatch a new event and listen to it. Doing it right 
means that the listener has to call a regular controller. But what if the error 
controller throws an exception? You will end up in an infinite loop. There 
should be an easier way, right?  
Enter the HttpKernel class. Instead of 
solving the same problem over and over again and instead of reinventing the 
wheel each time, the HttpKernel class is a generic, extensible and flexible 
implementation of HttpKernelInterface.  
This class is very similar to the 
framework class we have written so far: it dispatches events at some strategic 
points during the handling of the request, it uses a controller resolver to 
choose the controller to dispatch the request to, and as an added bonus, it 
takes care of edge cases and provides great feedback when a problem arises.  
Here is the new framework code:  
1  
2  
3  
4  
5  
6  
7  
8  
// example.com/src/Simplex/Framework.php  
namespace Simplex;  
  
use 
Symfony\\Component\\HttpKernel\\HttpKernel;  
  
class Framework extends 
HttpKernel  
{  
}  
And the new front controller:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
// example.com/web/front.php  
require\_once \_\_DIR\_\_.'/../vendor/autoload.php';  
  
use 
Symfony\\Component\\EventDispatcher\\EventDispatcher;  
use 
Symfony\\Component\\HttpFoundation\\Request;  
use 
Symfony\\Component\\HttpFoundation\\RequestStack;  
use 
Symfony\\Component\\HttpFoundation\\Response;  
use Symfony\\Component\\HttpKernel;  
use Symfony\\Component\\Routing;  
  
$request = Request::createFromGlobals();  
$requestStack = new RequestStack();  
$routes = include 
\_\_DIR\_\_.'/../src/app.php';  
  
$context = new Routing\\RequestContext();  
$matcher = new Routing\\Matcher\\UrlMatcher($routes, $context);  
  
$controllerResolver = new HttpKernel\\Controller\\ControllerResolver();  
$argumentResolver = new HttpKernel\\Controller\\ArgumentResolver();  
  
$dispatcher = new EventDispatcher();  
$dispatcher->addSubscriber(new 
HttpKernel\\EventListener\\RouterListener($matcher, $requestStack));  
  
$framework = new Simplex\\Framework($dispatcher, $controllerResolver, 
$requestStack, $argumentResolver);  
  
$response = 
$framework->handle($request);  
$response->send();  
RouterListener is an 
implementation of the same logic we had in our framework: it matches the 
incoming request and populates the request attributes with route parameters.  
Our code is now much more concise and surprisingly more robust and more powerful 
than ever. For instance, use the built-in ErrorListener to make your error 
management configurable:  
1  
2  
3  
4  
5  
6  
$errorHandler = 
function (Symfony\\Component\\ErrorHandler\\Exception\\FlattenException $exception) 
{  
$msg = 'Something went wrong! ('.$exception->getMessage().')';  
  
return new Response($msg, $exception->getStatusCode());  
};  
$dispatcher->addSubscriber(new 
HttpKernel\\EventListener\\ErrorListener($errorHandler));  
ErrorListener gives 
you a FlattenException instance instead of the 
thrown Exception or Error instance to ease exception manipulation and display. 
It can take any valid controller as an exception handler, so you can create an 
ErrorController class instead of using a Closure:  
1  
2  
3  
4  
$listener = new HttpKernel\\EventListener\\ErrorListener(  
'Calendar\\Controller\\ErrorController::exception'  
);  
$dispatcher->addSubscriber($listener);  
The error controller reads as follows:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
// example.com/src/Calendar/Controller/ErrorController.php  
namespace 
Calendar\\Controller;  
  
use 
Symfony\\Component\\ErrorHandler\\Exception\\FlattenException;  
use 
Symfony\\Component\\HttpFoundation\\Response;  
  
class ErrorController  
{  
public function exception(FlattenException $exception)  
{  
$msg = 'Something 
went wrong! ('.$exception->getMessage().')';  
  
return new Response($msg, 
$exception->getStatusCode());  
}  
}  
Voilà! Clean and customizable error 
management without efforts. And if your ErrorController throws an exception, 
HttpKernel will handle it nicely.  
In chapter two, we talked about 
the Response::prepare() method, which ensures that a Response is compliant with 
the HTTP specification. It is probably a good idea to always call it just before 
sending the Response to the client; that's what the ResponseListener does:  
$dispatcher->addSubscriber(new 
HttpKernel\\EventListener\\ResponseListener('UTF-8'));  
If you want out of the 
box support for streamed responses, subscribe to StreamedResponseListener:  
$dispatcher->addSubscriber(new 
HttpKernel\\EventListener\\StreamedResponseListener());  
And in your controller, 
return a StreamedResponse instance instead of a Response instance.  
Read the [Built-in 
Symfony Events](https://symfony.com/doc/current/reference/events.html) reference to learn more about the events dispatched by 
HttpKernel and how they allow you to change the flow of a request.  
Now, let's 
create a listener, one that allows a controller to return a string instead of a 
full Response object:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
class LeapYearController  
{  
public function index($year)  
{  
$leapYear = new LeapYear();  
if ($leapYear->isLeapYear($year)) {  
return 
'Yep, this is a leap year! ';  
}  
  
return 'Nope, this is not a leap 
year.';  
}  
}  
To implement this feature, we are going to listen to 
the kernel.view event, which is triggered just after the controller has been 
called. Its goal is to convert the controller return value to a proper Response 
instance, but only if needed:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
// 
example.com/src/Simplex/StringResponseListener.php  
namespace Simplex;  
  
use Symfony\\Component\\EventDispatcher\\EventSubscriberInterface;  
use 
Symfony\\Component\\HttpFoundation\\Response;  
use 
Symfony\\Component\\HttpKernel\\Event\\ViewEvent;  
  
class 
StringResponseListener implements EventSubscriberInterface  
{  
public 
function onView(ViewEvent $event)  
{  
$response = 
$event->getControllerResult();  
  
if (is\_string($response)) {  
$event->setResponse(new Response($response));  
}  
}  
  
public static 
function getSubscribedEvents()  
{  
return \['kernel.view' => 'onView'\];  
}  
}  
The code is simple because the kernel.view event is only triggered when the 
controller return value is not a Response and because setting the response on 
the event stops the event propagation (our listener cannot interfere with other 
view listeners).  
Don't forget to register it in the front controller:  
$dispatcher->addSubscriber(new Simplex\\StringResponseListener());  
If you 
forget to register the subscriber, HttpKernel will throw an exception with a 
nice message: The controller must return a response (Nope, this is not a leap 
year. given)..  
At this point, our whole framework code is as compact as 
possible and it is mainly composed of an assembly of existing libraries. 
Extending is a matter of registering event listeners/subscribers.  
Hopefully, 
you now have a better understanding of why the simple 
looking HttpKernelInterface is so powerful. Its default 
implementation, HttpKernel, gives you access to a lot of cool features, ready to 
be used out of the box, with no efforts. And because HttpKernel is actually the 
code that powers the Symfony framework, you have the best of both worlds: a 
custom framework, tailored to your needs, but based on a rock-solid and well 
maintained low-level architecture that has been proven to work for many 
websites; a code that has been audited for security issues and that has proven 
to scale well.  

12\. DependencyInjection Component
----------------------------------

In the previous chapter, we emptied the Simplex\\Framework class by extending 
the HttpKernel class from the eponymous component. Seeing this empty class, you 
might be tempted to move some code from the front controller to it:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
// 
example.com/src/Simplex/Framework.php  
namespace Simplex;  
  
use 
Symfony\\Component\\EventDispatcher\\EventDispatcher;  
use 
Symfony\\Component\\HttpFoundation;  
use 
Symfony\\Component\\HttpFoundation\\RequestStack;  
use 
Symfony\\Component\\HttpKernel;  
use Symfony\\Component\\Routing;  
  
class 
Framework extends HttpKernel\\HttpKernel  
{  
public function 
\_\_construct($routes)  
{  
$context = new Routing\\RequestContext();  
$matcher = new Routing\\Matcher\\UrlMatcher($routes, $context);  
$requestStack = 
new RequestStack();  
  
$controllerResolver = new 
HttpKernel\\Controller\\ControllerResolver();  
$argumentResolver = new 
HttpKernel\\Controller\\ArgumentResolver();  
  
$dispatcher = new 
EventDispatcher();  
$dispatcher->addSubscriber(new 
HttpKernel\\EventListener\\ErrorListener(  
'Calendar\\Controller\\ErrorController::exception'  
));  
$dispatcher->addSubscriber(new HttpKernel\\EventListener\\RouterListener($matcher, 
$requestStack));  
$dispatcher->addSubscriber(new 
HttpKernel\\EventListener\\ResponseListener('UTF-8'));  
$dispatcher->addSubscriber(new StringResponseListener());  
  
parent::\_\_construct($dispatcher, $controllerResolver, $requestStack, 
$argumentResolver);  
}  
}  
The front controller code would become more 
concise:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
// 
example.com/web/front.php  
require\_once \_\_DIR\_\_.'/../vendor/autoload.php';  
  
use Symfony\\Component\\HttpFoundation\\Request;  
  
$request = 
Request::createFromGlobals();  
$routes = include \_\_DIR\_\_.'/../src/app.php';  
  
$framework = new Simplex\\Framework($routes);  
  
$framework->handle($request)->send();  
Having a concise front controller 
allows you to have several front controllers for a single application. Why would 
it be useful? To allow having different configuration for the development 
environment and the production one for instance. In the development environment, 
you might want to have error reporting turned on and errors displayed in the 
browser to ease debugging:  
ini\_set('display\_errors', 1);  
error\_reporting(-1);  
... but you certainly won't want that same configuration 
on the production environment. Having two different front controllers gives you 
the opportunity to have a slightly different configuration for each of them.  
So, moving code from the front controller to the framework class makes our 
framework more configurable, but at the same time, it introduces a lot of 
issues:  
We are not able to register custom listeners anymore as the 
dispatcher is not available outside the Framework class (a workaround could be 
the adding of a Framework::getEventDispatcher() method);  
We have lost the 
flexibility we had before; you cannot change the implementation of 
the UrlMatcher or of the ControllerResolver anymore;  
Related to the previous 
point, we cannot test our framework without much effort anymore as it's 
impossible to mock internal objects;  
We cannot change the charset passed 
to ResponseListener anymore (a workaround could be to pass it as a constructor 
argument).  
The previous code did not exhibit the same issues because we used 
dependency injection; all dependencies of our objects were injected into their 
constructors (for instance, the event dispatchers were injected into the 
framework so that we had total control of its creation and configuration).  
Does it mean that we have to make a choice between flexibility, customization, 
ease of testing and not to copy and paste the same code into each application 
front controller? As you might expect, there is a solution. We can solve all 
these issues and some more by using the Symfony dependency injection container:  
$ composer require symfony/dependency-injection  
Create a new file to host the 
dependency injection container configuration:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  
21  
22  
23  
24  
25  
26  
27  
28  
29  
30  
31  
32  
33  
34  
35  
36  
37  
38  
39  
40  
41  
42  
// example.com/src/container.php  
use Simplex\\Framework;  
use Symfony\\Component\\DependencyInjection;  
use 
Symfony\\Component\\DependencyInjection\\Reference;  
use 
Symfony\\Component\\EventDispatcher;  
use Symfony\\Component\\HttpFoundation;  
use Symfony\\Component\\HttpKernel;  
use Symfony\\Component\\Routing;  
  
$containerBuilder = new DependencyInjection\\ContainerBuilder();  
$containerBuilder->register('context', Routing\\RequestContext::class);  
$containerBuilder->register('matcher', Routing\\Matcher\\UrlMatcher::class)  
->setArguments(\[$routes, new Reference('context')\])  
;  
$containerBuilder->register('request\_stack', 
HttpFoundation\\RequestStack::class);  
$containerBuilder->register('controller\_resolver', 
HttpKernel\\Controller\\ControllerResolver::class);  
$containerBuilder->register('argument\_resolver', 
HttpKernel\\Controller\\ArgumentResolver::class);  
  
$containerBuilder->register('listener.router', 
HttpKernel\\EventListener\\RouterListener::class)  
\->setArguments(\[new 
Reference('matcher'), new Reference('request\_stack')\])  
;  
$containerBuilder->register('listener.response', 
HttpKernel\\EventListener\\ResponseListener::class)  
\->setArguments(\['UTF-8'\])  
;  
$containerBuilder->register('listener.exception', 
HttpKernel\\EventListener\\ErrorListener::class)  
->setArguments(\['Calendar\\Controller\\ErrorController::exception'\])  
;  
$containerBuilder->register('dispatcher', 
EventDispatcher\\EventDispatcher::class)  
\->addMethodCall('addSubscriber', \[new 
Reference('listener.router')\])  
\->addMethodCall('addSubscriber', \[new 
Reference('listener.response')\])  
\->addMethodCall('addSubscriber', \[new 
Reference('listener.exception')\])  
;  
$containerBuilder->register('framework', Framework::class)  
\->setArguments(\[  
new Reference('dispatcher'),  
new Reference('controller\_resolver'),  
new 
Reference('request\_stack'),  
new Reference('argument\_resolver'),  
\])  
;  
  
return $containerBuilder;  
The goal of this file is to configure your 
objects and their dependencies. Nothing is instantiated during this 
configuration step. This is purely a static description of the objects you need 
to manipulate and how to create them. Objects will be created on-demand when you 
access them from the container or when the container needs them to create other 
objects.  
For instance, to create the router listener, we tell Symfony that 
its class name is Symfony\\Component\\HttpKernel\\EventListener\\RouterListener and 
that its constructor takes a matcher object (new Reference('matcher')). As you 
can see, each object is referenced by a name, a string that uniquely identifies 
each object. The name allows us to get an object and to reference it in other 
object definitions.  
By default, every time you get an object from the 
container, it returns the exact same instance. That's because a container 
manages your "global" objects.  
The front controller is now only about wiring 
everything together:  
1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
// example.com/web/front.php  
require\_once 
\_\_DIR\_\_.'/../vendor/autoload.php';  
  
use 
Symfony\\Component\\HttpFoundation\\Request;  
  
$routes = include 
\_\_DIR\_\_.'/../src/app.php';  
$container = include 
\_\_DIR\_\_.'/../src/container.php';  
  
$request = Request::createFromGlobals();  
  
$response = $container->get('framework')->handle($request);  
  
$response->send();  
As all the objects are now created in the dependency 
injection container, the framework code should be the previous simple version:  
1  
2  
3  
4  
5  
6  
7  
8  
// example.com/src/Simplex/Framework.php  
namespace Simplex;  
  
use Symfony\\Component\\HttpKernel\\HttpKernel;  
  
class Framework extends HttpKernel  
{  
}  
If you want a light alternative 
for your container, consider Pimple, 
a simple dependency injection container in about 60 lines of PHP code.  
Now, 
here is how you can register a custom listener in the front controller:  
1  
2  
3  
4  
5  
6  
7  
// ...  
use Simplex\\StringResponseListener;  
  
$container->register('listener.string\_response', 
StringResponseListener::class);  
$container->getDefinition('dispatcher')  
->addMethodCall('addSubscriber', \[new Reference('listener.string\_response')\])  
;  
Besides describing your objects, the dependency injection container can 
also be configured via parameters. Let's create one that defines if we are in 
debug mode or not:  
1  
2  
3  
$container->setParameter('debug', true);  
  
echo $container->getParameter('debug');  
These parameters can be used when 
defining object definitions. Let's make the charset configurable:  
1  
2  
3  
4  
// ...  
$container->register('listener.response', 
HttpKernel\\EventListener\\ResponseListener::class)  
->setArguments(\['%charset%'\])  
;  
After this change, you must set the 
charset before using the response listener object:  
$container->setParameter('charset', 'UTF-8');  
Instead of relying on the 
convention that the routes are defined by the $routes variables, let's use a 
parameter again:  
1  
2  
3  
4  
// ...  
$container->register('matcher', Routing\\Matcher\\UrlMatcher::class)  
->setArguments(\['%routes%', new Reference('context')\])  
;  
And the related 
change in the front controller:  
$container->setParameter('routes', include 
\_\_DIR\_\_.'/../src/app.php');  
We have barely scratched the surface of what you 
can do with the container: from class names as parameters, to overriding 
existing object definitions, from shared service support to dumping a container 
to a plain PHP class, and much more. The Symfony dependency injection container 
is really powerful and is able to manage any kind of PHP class.  
Don't yell at 
me if you don't want to use a dependency injection container in your framework. 
If you don't like it, don't use it. It's your framework, not mine.  
This is 
(already) the last chapter of this book on creating a framework on top of the 
Symfony components. I'm aware that many topics have not been covered in great 
details, but hopefully it gives you enough information to get started on your 
own and to better understand how the Symfony framework works internally.