diff --git a/create_framework/dependency-injection.rst b/create_framework/dependency-injection.rst new file mode 100644 index 00000000000..4187f669937 --- /dev/null +++ b/create_framework/dependency-injection.rst @@ -0,0 +1,243 @@ +The 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:: + + // example.com/src/Simplex/Framework.php + + namespace Simplex; + + use Symfony\Component\Routing; + use Symfony\Component\HttpKernel; + use Symfony\Component\EventDispatcher\EventDispatcher; + + class Framework extends HttpKernel\HttpKernel + { + public function __construct($routes) + { + $context = new Routing\RequestContext(); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + $resolver = new HttpKernel\Controller\ControllerResolver(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher)); + $dispatcher->addSubscriber(new HttpKernel\EventListener\ResponseListener('UTF-8')); + + parent::__construct($dispatcher, $resolver); + } + } + +The front controller code would become more concise:: + + // 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 (an easy 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 easily 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: + +.. code-block:: bash + + $ composer require symfony/dependency-injection + +Create a new file to host the dependency injection container configuration:: + + // example.com/src/container.php + + use Symfony\Component\DependencyInjection; + use Symfony\Component\DependencyInjection\Reference; + + $sc = new DependencyInjection\ContainerBuilder(); + $sc->register('context', 'Symfony\Component\Routing\RequestContext'); + $sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') + ->setArguments(array($routes, new Reference('context'))) + ; + $sc->register('resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver'); + + $sc->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener') + ->setArguments(array(new Reference('matcher'))) + ; + $sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener') + ->setArguments(array('UTF-8')) + ; + $sc->register('listener.exception', 'Symfony\Component\HttpKernel\EventListener\ExceptionListener') + ->setArguments(array('Calendar\\Controller\\ErrorController::exceptionAction')) + ; + $sc->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher') + ->addMethodCall('addSubscriber', array(new Reference('listener.router'))) + ->addMethodCall('addSubscriber', array(new Reference('listener.response'))) + ->addMethodCall('addSubscriber', array(new Reference('listener.exception'))) + ; + $sc->register('framework', 'Simplex\Framework') + ->setArguments(array(new Reference('dispatcher'), new Reference('resolver'))) + ; + + return $sc; + +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. + +.. note:: + + 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:: + + // example.com/web/front.php + + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + + $routes = include __DIR__.'/../src/app.php'; + $sc = include __DIR__.'/../src/container.php'; + + $request = Request::createFromGlobals(); + + $response = $sc->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:: + + // example.com/src/Simplex/Framework.php + + namespace Simplex; + + use Symfony\Component\HttpKernel\HttpKernel; + + class Framework extends HttpKernel + { + } + +.. note:: + + 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:: + + $sc->register('listener.string_response', 'Simplex\StringResponseListener'); + $sc->getDefinition('dispatcher') + ->addMethodCall('addSubscriber', array(new Reference('listener.string_response'))) + ; + +Beside 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:: + + $sc->setParameter('debug', true); + + echo $sc->getParameter('debug'); + +These parameters can be used when defining object definitions. Let's make the +charset configurable:: + + $sc->register('listener.response', 'Symfony\Component\HttpKernel\EventListener\ResponseListener') + ->setArguments(array('%charset%')) + ; + +After this change, you must set the charset before using the response listener +object:: + + $sc->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:: + + $sc->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') + ->setArguments(array('%routes%', new Reference('context'))) + ; + +And the related change in the front controller:: + + $sc->setParameter('routes', include __DIR__.'/../src/app.php'); + +We have obviously barely scratched the surface of what you can do with the +container: from class names as parameters, to overriding existing object +definitions, from scope 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. + +If you want to learn more, read the source code of the `Silex`_ +micro-framework, and especially its `Application`_ class. + +Have fun! + +.. _`Pimple`: https://github.com/fabpot/Pimple +.. _`Silex`: https://silex.sensiolabs.org/ +.. _`Application`: https://github.com/fabpot/Silex/blob/master/src/Silex/Application.php diff --git a/create_framework/event-dispatcher.rst b/create_framework/event-dispatcher.rst new file mode 100644 index 00000000000..fce91ac59bd --- /dev/null +++ b/create_framework/event-dispatcher.rst @@ -0,0 +1,306 @@ +The EventDispatcher Component +============================= + +Our framework is still missing a major characteristic of any good framework: +*extensibility*. Being extensible means that the developer should be able to +easily hook into the framework life cycle to modify the way the request is +handled. + +What kind of hooks are we talking about? Authentication or caching for +instance. To be flexible, hooks must be plug-and-play; the ones you "register" +for an application are different from the next one depending on your specific +needs. Many software have a similar concept like Drupal or Wordpress. In some +languages, there is even a standard like `WSGI`_ in Python or `Rack`_ in Ruby. + +As there is no standard for PHP, we are going to use a well-known design +pattern, the *Observer*, to allow any kind of behaviors to be attached to our +framework; the Symfony EventDispatcher Component implements a lightweight +version of this pattern: + +.. code-block:: bash + + $ composer require symfony/event-dispatcher + +How does it work? The *dispatcher*, the central object of the event dispatcher +system, notifies *listeners* of an *event* dispatched to it. Put another way: +your code dispatches an event to the dispatcher, the dispatcher notifies all +registered listeners for the event, and each listener do 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\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + use Symfony\Component\Routing\Exception\ResourceNotFoundException; + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + use Symfony\Component\EventDispatcher\EventDispatcher; + + class Framework + { + protected $matcher; + protected $resolver; + protected $dispatcher; + + public function __construct(EventDispatcher $dispatcher, UrlMatcherInterface $matcher, ControllerResolverInterface $resolver) + { + $this->matcher = $matcher; + $this->resolver = $resolver; + $this->dispatcher = $dispatcher; + } + + public function handle(Request $request) + { + $this->matcher->getContext()->fromRequest($request); + + try { + $request->attributes->add($this->matcher->match($request->getPathInfo())); + + $controller = $this->resolver->getController($request); + $arguments = $this->resolver->getArguments($request, $controller); + + $response = call_user_func_array($controller, $arguments); + } catch (ResourceNotFoundException $e) { + $response = new Response('Not Found', 404); + } catch (\Exception $e) { + $response = new Response('An error occurred', 500); + } + + // dispatch a response event + $this->dispatcher->dispatch('response', new ResponseEvent($response, $request)); + + return $response; + } + } + +Each time the framework handles a Request, a ``ResponseEvent`` event is +now dispatched:: + + // example.com/src/Simplex/ResponseEvent.php + + namespace Simplex; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\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:: + + // 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'); + }); + + $framework = new Simplex\Framework($dispatcher, $matcher, $resolver); + $response = $framework->handle($request); + + $response->send(); + +.. note:: + + 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:: + + $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``:: + + $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); + +.. tip:: + + 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:: + + // 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:: + + // 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:: + + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('response', array(new Simplex\ContentLengthListener(), 'onResponse'), -255); + $dispatcher->addListener('response', array(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. Of course, +there is a solution: use subscribers instead of listeners:: + + $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``:: + + // example.com/src/Simplex/GoogleListener.php + + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class GoogleListener implements EventSubscriberInterface + { + // ... + + public static function getSubscribedEvents() + { + return array('response' => 'onResponse'); + } + } + +And here is the new version of ``ContentLengthListener``:: + + // example.com/src/Simplex/ContentLengthListener.php + + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + class ContentLengthListener implements EventSubscriberInterface + { + // ... + + public static function getSubscribedEvents() + { + return array('response' => array('onResponse', -255)); + } + } + +.. tip:: + + 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. + +.. _`WSGI`: http://www.python.org/dev/peps/pep-0333/#middleware-components-that-play-both-sides +.. _`Rack`: http://rack.rubyforge.org/ diff --git a/create_framework/front-controller.rst b/create_framework/front-controller.rst new file mode 100644 index 00000000000..90e7e69dbb0 --- /dev/null +++ b/create_framework/front-controller.rst @@ -0,0 +1,239 @@ +The Front Controller +==================== + +Up until now, our application is simplistic as there is only one page. To +spice things up a little bit, let's go crazy and add another page that says +goodbye:: + + // 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:: + + // framework/index.php + + require_once __DIR__.'/init.php'; + + $input = $request->get('name', 'World'); + + $response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); + $response->send(); + +And for the "Goodbye" page:: + + // 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, +which name 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 easily achieved by +routing all client requests to a single PHP script. + +.. tip:: + + Exposing a single PHP script to the end user is a design pattern called + the "`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 = array( + '/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 + + $input = $request->get('name', 'World'); + $response->setContent(sprintf('Hello %s', htmlspecialchars($input, 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*. + +.. tip:: + + 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). + +.. tip:: + + You don't even need to setup a web server to test the code. Instead, + replace the ``$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 access the same script (``front.php``) for all +pages, we can secure the code further by moving all other PHP files outside the +web root directory: + +.. code-block:: text + + example.com + ├── composer.json + │ src + │ └── pages + │ ├── hello.php + │ └── bye.php + ├── vendor + └── web + └── front.php + +Now, configure your web server root directory to point to ``web/`` and all +other files won't be accessible from the client anymore. + +To test your changes in a browser (``http://localhost:4321/?name=Fabien``), run +the PHP built-in server: + +.. code-block:: bash + + $ php -S 127.0.0.1:4321 -t web/ web/front.php + +.. note:: + + For this new structure to work, you will have to adjust some paths in + various PHP files; the changes are left as an exercise for the reader. + +The last thing that is repeated in each page is the call to ``setContent()``. +We can convert all pages to "templates" by just echoing the content and calling +the ``setContent()`` directly from the front controller script:: + + // example.com/web/front.php + + // ... + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + ob_start(); + include $map[$path]; + $response->setContent(ob_get_clean()); + } else { + $response->setStatusCode(404); + $response->setContent('Not Found'); + } + + // ... + +And the ``hello.php`` script can now be converted to a template:: + + + + get('name', 'World') ?> + + Hello + +We have the first version of our framework:: + + // example.com/web/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 = array( + '/hello' => __DIR__.'/../src/pages/hello.php', + '/bye' => __DIR__.'/../src/pages/bye.php', + ); + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + ob_start(); + include $map[$path]; + $response->setContent(ob_get_clean()); + } else { + $response->setStatusCode(404); + $response->setContent('Not Found'); + } + + $response->send(); + +Adding a new page is a two step process: add an entry in the map and create a +PHP template in ``src/pages/``. From a template, get the Request data via the +``$request`` variable and tweak the Response headers via the ``$response`` +variable. + +.. note:: + + If you decide to stop here, you can probably enhance your framework by + extracting the URL map to a configuration file. + +.. _`front controller`: http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html#a-front-controller-to-the-rescue diff --git a/create_framework/http-foundation.rst b/create_framework/http-foundation.rst new file mode 100644 index 00000000000..a0e0183ba58 --- /dev/null +++ b/create_framework/http-foundation.rst @@ -0,0 +1,305 @@ +The HttpFoundation Component +============================ + +Before diving into the framework creation process, let's first step back and +let's take a look at why you would like to use a framework instead of keeping +your plain-old PHP applications as is. Why using a framework is actually a good +idea, even for the simplest snippet of code and why creating your framework on +top of the Symfony components is better than creating a framework from scratch. + +.. note:: + + We won't talk about the obvious and traditional benefits of using a + framework when working on big applications with more than a few + developers; the Internet has already plenty of good resources on that + topic. + +Even if the "application" we wrote in the previous chapter was simple enough, +it suffers from a few problems:: + + // framework/index.php + + $input = $_GET['name']; + + printf('Hello %s', $input); + +First, if the ``name`` query parameter is not defined in the URL query string, +you will get a PHP warning; so let's fix it:: + + // framework/index.php + + $input = isset($_GET['name']) ? $_GET['name'] : 'World'; + + printf('Hello %s', $input); + +Then, this *application is not secure*. Can you believe it? Even this simple +snippet of PHP code is vulnerable to one of the most widespread Internet +security issue, XSS (Cross-Site Scripting). Here is a more secure version:: + + $input = isset($_GET['name']) ? $_GET['name'] : 'World'; + + header('Content-Type: text/html; charset=utf-8'); + + printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')); + +.. note:: + + 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). + +As you can see for yourself, the simple code we had written first is not that +simple anymore if we want to avoid PHP warnings/notices and make the code +more secure. + +Beyond security, this code is not even easily testable. 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 + + class IndexTest extends \PHPUnit_Framework_TestCase + { + public function testHello() + { + $_GET['name'] = 'Fabien'; + + ob_start(); + include 'index.php'; + $content = ob_get_clean(); + + $this->assertEquals('Hello Fabien', $content); + } + } + +.. note:: + + If our application were just slightly bigger, we would have been able to + find even more problems. If you are curious about them, read the `Symfony + versus Flat PHP`_ chapter of the Symfony documentation. + +At this point, if you are not convinced that security and testing are indeed +two very good reasons to stop writing code the old way and adopt a framework +instead (whatever adopting a framework means in this context), you can stop +reading this book now and go back to whatever code you were working on before. + +.. note:: + + Of course, using a framework should give you more than just security and + testability, but the more important thing to keep in mind is that the + framework you choose must allow you to write better code faster. + +Going OOP with the HttpFoundation Component +------------------------------------------- + +Writing web code is about interacting with HTTP. So, the fundamental +principles of our framework should be around the `HTTP specification`_. + +The HTTP specification describes how a client (a browser for instance) +interacts with a server (our application via a web server). The dialog between +the client and the server is specified by well-defined *messages*, requests +and responses: *the client sends a request to the server and based on this +request, the server returns a response*. + +In PHP, the request is represented by global variables (``$_GET``, ``$_POST``, +``$_FILE``, ``$_COOKIE``, ``$_SESSION``...) and the response is generated by +functions (``echo``, ``header``, ``setcookie``, ...). + +The first step towards better code is probably to use an Object-Oriented +approach; that's the main goal of the Symfony HttpFoundation component: +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: + +.. code-block:: bash + + $ composer require symfony/http-foundation + +Running this command will also automatically download the Symfony +HttpFoundation component and install it under the ``vendor/`` directory. + +.. sidebar:: Class Autoloading + + When installing a new dependency, Composer also generates a + ``vendor/autoload.php`` file that allows any class to be easily + `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-0`_, we can just let Composer and PHP do the hard work for us. + +Now, let's rewrite our application by using the ``Request`` and the +``Response`` classes:: + + // framework/index.php + + require_once __DIR__.'/vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + + $input = $request->get('name', 'World'); + + $response = new Response(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'))); + + $response->send(); + +The ``createFromGlobals()`` method creates a ``Request`` object based on the +current PHP global variables. + +The ``send()`` method sends the ``Response`` object back to the client (it +first outputs the HTTP headers followed by the content). + +.. tip:: + + 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 +the HTTP messages. You can create whatever request you want and you are in +charge of sending the response whenever you see fit. + +.. note:: + + 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 easily 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); + +.. tip:: + + To debug a Response, cast it to a string; it will return the HTTP + representation of the response (headers and content). + +Last but not the 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 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(array('10.0.0.1')); + + if ($myIp == $request->getClientIp(true)) { + // 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 goal 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 using a technology that already works? + +.. note:: + + If you want to learn more about the HttpFoundation component, you can have + a look at the :namespace:`Symfony\\Component\\HttpFoundation` API or read + its dedicated :doc:`documentation `. + +Believe 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 forgot to talk about one added benefit: using the HttpFoundation +component is the start of better interoperability between all frameworks and +applications using it (like `Symfony`_, `Drupal 8`_, `phpBB 4`_, `ezPublish +5`_, `Laravel`_, `Silex`_, and `more`_). + +.. _`Twig`: http://twig.sensiolabs.com/ +.. _`Symfony versus Flat PHP`: http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html +.. _`HTTP specification`: http://tools.ietf.org/wg/httpbis/ +.. _`audited`: http://symfony.com/blog/symfony2-security-audit +.. _`Symfony`: http://symfony.com/ +.. _`Drupal 8`: http://drupal.org/ +.. _`phpBB 4`: http://www.phpbb.com/ +.. _`ezPublish 5`: http://ez.no/ +.. _`Laravel`: http://laravel.com/ +.. _`Silex`: http://silex.sensiolabs.org/ +.. _`Midgard CMS`: http://www.midgard-project.org/ +.. _`Zikula`: http://zikula.org/ +.. _`autoloaded`: http://php.net/autoload +.. _`PSR-0`: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md +.. _`more`: http://symfony.com/components/HttpFoundation diff --git a/create_framework/http-kernel-controller-resolver.rst b/create_framework/http-kernel-controller-resolver.rst new file mode 100644 index 00000000000..b0393b4cdd3 --- /dev/null +++ b/create_framework/http-kernel-controller-resolver.rst @@ -0,0 +1,198 @@ +The HttpKernel Component: the Controller Resolver +================================================= + +You might think that our framework is already pretty solid and you are +probably right. But let's see how we can improve it nonetheless. + +Right now, all our examples use procedural code, but remember that controllers +can be any valid PHP callbacks. Let's convert our controller to a proper +class:: + + class LeapYearController + { + public function indexAction($request) + { + if (is_leap_year($request->attributes->get('year'))) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + } + +Update the route definition accordingly:: + + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array( + 'year' => null, + '_controller' => array(new LeapYearController(), 'indexAction'), + ))); + +The move is pretty straightforward and makes a lot of sense as soon as you +create more pages but you might have noticed a non-desirable side-effect... +The ``LeapYearController`` class is *always* instantiated, even if the +requested URL does not match the ``leap_year`` route. This is bad for one main +reason: performance wise, all controllers for all routes must now be +instantiated for every request. It would be better if controllers were +lazy-loaded so that only the controller associated with the matched route is +instantiated. + +To solve this issue, and a bunch more, let's install and use the HttpKernel +component: + +.. code-block:: bash + + $ composer require symfony/http-kernel + +The HttpKernel component has many interesting features, but the one we need +right now is the *controller resolver*. A controller resolver knows how to +determine the controller to execute and the arguments to pass to it, based on +a Request object. All controller resolvers implement the following interface:: + + namespace Symfony\Component\HttpKernel\Controller; + + interface ControllerResolverInterface + { + function getController(Request $request); + + function getArguments(Request $request, $controller); + } + +The ``getController()`` method relies on the same convention as the one we +have defined earlier: the ``_controller`` request attribute must contain the +controller associated with the Request. Besides the built-in PHP callbacks, +``getController()`` also supports strings composed of a class name followed by +two colons and a method name as a valid callback, like 'class::method':: + + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array( + 'year' => null, + '_controller' => 'LeapYearController::indexAction', + ))); + +To make this code work, modify the framework code to use the controller +resolver from HttpKernel:: + + use Symfony\Component\HttpKernel; + + $resolver = new HttpKernel\Controller\ControllerResolver(); + + $controller = $resolver->getController($request); + $arguments = $resolver->getArguments($request, $controller); + + $response = call_user_func_array($controller, $arguments); + +.. note:: + + As an added bonus, the controller resolver properly handles the error + management for you: when you forget to define a ``_controller`` attribute + for a Route for instance. + +Now, let's see how the controller arguments are guessed. ``getArguments()`` +introspects the controller signature to determine which arguments to pass to +it by using the native PHP `reflection`_. + +The ``indexAction()`` method needs the Request object as an argument. +```getArguments()`` knows when to inject it properly if it is type-hinted +correctly:: + + public function indexAction(Request $request) + + // won't work + public function indexAction($request) + +More interesting, ``getArguments()`` is also able to inject any Request +attribute; the argument just needs to have the same name as the corresponding +attribute:: + + public function indexAction($year) + +You can also inject the Request and some attributes at the same time (as the +matching is done on the argument name or a type hint, the arguments order does +not matter):: + + public function indexAction(Request $request, $year) + + public function indexAction($year, Request $request) + +Finally, you can also define default values for any argument that matches an +optional attribute of the Request:: + + public function indexAction($year = 2012) + +Let's just inject the ``$year`` request attribute for our controller:: + + class LeapYearController + { + public function indexAction($year) + { + if (is_leap_year($year)) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + } + +The controller resolver also takes care of validating the controller callable +and its arguments. In case of a problem, it throws an exception with a nice +message explaining the problem (the controller class does not exist, the +method is not defined, an argument has no matching attribute, ...). + +.. note:: + + With the great flexibility of the default controller resolver, you might + wonder why someone would want to create another one (why would there be an + interface if not?). Two examples: in Symfony, ``getController()`` is + enhanced to support `controllers as services`_; and in + `FrameworkExtraBundle`_, ``getArguments()`` is enhanced to support + parameter converters, where request attributes are converted to objects + automatically. + +Let's conclude with the new version of our framework:: + + // example.com/web/front.php + + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + use Symfony\Component\HttpKernel; + + function render_template(Request $request) + { + extract($request->attributes->all()); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + return new Response(ob_get_clean()); + } + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $context->fromRequest($request); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + $resolver = new HttpKernel\Controller\ControllerResolver(); + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + + $controller = $resolver->getController($request); + $arguments = $resolver->getArguments($request, $controller); + + $response = call_user_func_array($controller, $arguments); + } catch (Routing\Exception\ResourceNotFoundException $e) { + $response = new Response('Not Found', 404); + } catch (Exception $e) { + $response = new Response('An error occurred', 500); + } + + $response->send(); + +Think about it once more: our framework is more robust and more flexible than +ever and it still has less than 40 lines of code. + +.. _`reflection`: http://php.net/reflection +.. _`FrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html +.. _`controllers as services`: http://symfony.com/doc/current/cookbook/controller/service.html diff --git a/create_framework/http-kernel-httpkernel-class.rst b/create_framework/http-kernel-httpkernel-class.rst new file mode 100644 index 00000000000..b66f11e0ee7 --- /dev/null +++ b/create_framework/http-kernel-httpkernel-class.rst @@ -0,0 +1,204 @@ +The 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 easy enough 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:: + + // example.com/src/Simplex/Framework.php + + namespace Simplex; + + use Symfony\Component\HttpKernel\HttpKernel; + + class Framework extends HttpKernel + { + } + +And the new front controller:: + + // example.com/web/front.php + + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + use Symfony\Component\HttpKernel; + use Symfony\Component\EventDispatcher\EventDispatcher; + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + $resolver = new HttpKernel\Controller\ControllerResolver(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber(new HttpKernel\EventListener\RouterListener($matcher)); + + $framework = new Simplex\Framework($dispatcher, $resolver); + + $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 ``ExceptionListener`` to +make your error management configurable:: + + $errorHandler = function (HttpKernel\Exception\FlattenException $exception) { + $msg = 'Something went wrong! ('.$exception->getMessage().')'; + + return new Response($msg, $exception->getStatusCode()); + }; + $dispatcher->addSubscriber(new HttpKernel\EventListener\ExceptionListener($errorHandler)); + +``ExceptionListener`` gives you a ``FlattenException`` instance instead of the +thrown ``Exception`` 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:: + + $listener = new HttpKernel\EventListener\ExceptionListener('Calendar\\Controller\\ErrorController::exceptionAction'); + $dispatcher->addSubscriber($listener); + +The error controller reads as follows:: + + // example.com/src/Calendar/Controller/ErrorController.php + + namespace Calendar\Controller; + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Exception\FlattenException; + + class ErrorController + { + public function exceptionAction(FlattenException $exception) + { + $msg = 'Something went wrong! ('.$exception->getMessage().')'; + + return new Response($msg, $exception->getStatusCode()); + } + } + +Voilà! Clean and customizable error management without efforts. And of course, +if your controller 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')); + +This one was easy too! Let's take another one: do you want out of the box +support for streamed responses? Just subscribe to +``StreamedResponseListener``:: + + $dispatcher->addSubscriber(new HttpKernel\EventListener\StreamedResponseListener()); + +And in your controller, return a ``StreamedResponse`` instance instead of a +``Response`` instance. + +.. tip:: + + Read the `Internals`_ chapter of the Symfony documentation 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:: + + class LeapYearController + { + public function indexAction(Request $request, $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:: + + // example.com/src/Simplex/StringResponseListener.php + + namespace Simplex; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent; + use Symfony\Component\HttpFoundation\Response; + + class StringResponseListener implements EventSubscriberInterface + { + public function onView(GetResponseForControllerResultEvent $event) + { + $response = $event->getControllerResult(); + + if (is_string($response)) { + $event->setResponse(new Response($response)); + } + } + + public static function getSubscribedEvents() + { + return array('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()); + +.. note:: + + 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 and Silex frameworks, 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. + +.. _`Internals`: http://symfony.com/doc/current/book/internals.html#events diff --git a/create_framework/http-kernel-httpkernelinterface.rst b/create_framework/http-kernel-httpkernelinterface.rst new file mode 100644 index 00000000000..e0a07731e36 --- /dev/null +++ b/create_framework/http-kernel-httpkernelinterface.rst @@ -0,0 +1,190 @@ +The 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``:: + + namespace Symfony\Component\HttpKernel; + + interface HttpKernelInterface + { + /** + * @return Response A Response instance + */ + function handle(Request $request, $type = self::MASTER_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:: + + // example.com/src/Framework.php + + // ... + + use Symfony\Component\HttpKernel\HttpKernelInterface; + + class Framework implements HttpKernelInterface + { + // ... + + public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) + { + // ... + } + } + +Even if this change looks trivial, it brings us a lot! Let's talk about one of +the most impressive one: transparent `HTTP caching`_ support. + +The ``HttpCache`` class implements a fully-featured reverse proxy, written in +PHP; it implements ``HttpKernelInterface`` and wraps another +``HttpKernelInterface`` instance:: + + // example.com/web/front.php + + $framework = new Simplex\Framework($dispatcher, $matcher, $resolver); + $framework = new HttpKernel\HttpCache\HttpCache($framework, new HttpKernel\HttpCache\Store(__DIR__.'/../cache')); + + $framework->handle($request)->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:: + + // example.com/src/Calendar/Controller/LeapYearController.php + + public function indexAction(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; + } + +.. tip:: + + If, like me, you are running your framework from the command line by + simulating requests (``Request::create('/is_leap_year/2012')``), you can + easily 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()); + +.. note:: + + 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`_ chapter of the +Symfony documentation. + +The Response class contains many other methods that let you configure the +HTTP cache very easily. One of the most powerful is ``setCache()`` as it +abstracts the most frequently used caching strategies into one simple array:: + + $date = date_create_from_format('Y-m-d H:i:s', '2005-10-15 10:00:00'); + + $response->setCache(array( + 'public' => true, + 'etag' => 'abcde', + 'last_modified' => $date, + 'max_age' => 10, + 's_maxage' => 10, + )); + + // it is equivalent to the following code + $response->setPublic(); + $response->setEtag('abcde'); + $response->setLastModified($date); + $response->setMaxAge(10); + $response->setSharedMaxAge(10); + +When using the validation model, the ``isNotModified()`` method allows you to +easily cut on the response time by short-circuiting the response generation as +early as possible:: + + $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:: + + 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:: + + $framework = new HttpKernel\HttpCache\HttpCache( + $framework, + new HttpKernel\HttpCache\Store(__DIR__.'/../cache'), + new HttpKernel\HttpCache\ESI() + ); + +.. note:: + + 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:: + + $framework = new HttpCache($framework, new Store(__DIR__.'/../cache'), new ESI(), array('debug' => true)); + +The debug mode adds a ``X-Symfony-Cache`` header to each response that +describes what the cache layer did: + +.. code-block:: text + + 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! + +.. _`HTTP caching`: http://symfony.com/doc/current/book/http_cache.html +.. _`ESI`: http://en.wikipedia.org/wiki/Edge_Side_Includes +.. _`Varnish`: https://www.varnish-cache.org/ diff --git a/create_framework/index.rst b/create_framework/index.rst new file mode 100644 index 00000000000..10517e1565c --- /dev/null +++ b/create_framework/index.rst @@ -0,0 +1,17 @@ +Create your PHP Framework +========================= + +.. toctree:: + + introduction + http-foundation + front-controller + routing + templating + http-kernel-controller-resolver + separation-of-concerns + unit-testing + event-dispatcher + http-kernel-httpkernelinterface + http-kernel-httpkernel-class + dependency-injection diff --git a/create_framework/introduction.rst b/create_framework/introduction.rst new file mode 100644 index 00000000000..8975d349b37 --- /dev/null +++ b/create_framework/introduction.rst @@ -0,0 +1,126 @@ +Introduction +============ + +`Symfony`_ is a reusable set of standalone, decoupled and cohesive PHP +components that solve common web development problems. + +Instead of using these low-level components, you can use the ready-to-be-used +Symfony full-stack web framework, which is based on these components... or +you can create your very own framework. This tutorial is about the latter. + +Why would you Like to Create your Own Framework? +------------------------------------------------ + +Why would you like to create your own framework in the first place? If you +look around, everybody will tell you that it's a bad thing to reinvent the +wheel and that you'd better choose an existing framework and forget about +creating your own altogether. Most of the time, they are right but there are +a few good reasons to start creating your own framework: + +* To learn more about the low level architecture of modern web frameworks in + general and about the Symfony full-stack framework internals in particular; + +* To create a framework tailored to your very specific needs (just be sure + first that your needs are really specific); + +* To experiment creating a framework for fun (in a learn-and-throw-away + approach); + +* To refactor an old/existing application that needs a good dose of recent web + development best practices; + +* To prove the world that you can actually create a framework on your own (... + but with little effort). + +This tutorial will gently guide you through the creation of a web framework, +one step at a time. At each step, you will have a fully-working framework that +you can use as is or as a start for your very own. It will start with a simple +framework and more features will be added with time. Eventually, you will have +a fully-featured full-stack web framework. + +And of course, each step will be the occasion to learn more about some of the +Symfony Components. + +.. tip:: + + If you don't have time to read the whole book, or if you want to get + started fast, you can also have a look at `Silex`_, a micro-framework + based on the Symfony Components. The code is rather slim and it leverages + many aspects of the Symfony Components. + +Many modern web frameworks advertize themselves as being MVC frameworks. This +tutorial won't talk about the MVC pattern, as the Symfony Components are able to +create any type of frameworks, not just the ones that follow the MVC +architecture. Anyway, if you have a look at the MVC semantics, this book is +about how to create the Controller part of a framework. For the Model and the +View, it really depends on your personal taste and you can use any existing +third-party libraries (Doctrine, Propel or plain-old PDO for the Model; PHP or +Twig for the View). + +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. + +Before You Start +---------------- + +Reading about how to create a framework is not enough. You will have to follow +along and actually type all the examples included in this tutorial. For that, +you need a recent version of PHP (5.3.9 or later is good enough), a web server +(like Apache, NGinx or PHP's built-in web server), a good knowledge of PHP and +an understanding of Object Oriented programming. + +Ready to go? Read on! + +Bootstrapping +------------- + +Before you can even think of creating the first framework, you need to think +about some conventions: where you will store the code, how you will name the +classes, how you will reference external dependencies, etc. + +To store your new framework, create a directory somewhere on your machine: + +.. code-block:: bash + + $ mkdir framework + $ cd framework + +Dependency Management +~~~~~~~~~~~~~~~~~~~~~ + +To install the Symfony Components that you need for your framework, you are going +to use `Composer`_, a project dependency manager for PHP. If you don't have it +yet, :doc:`download and install Composer ` now. + +Our Project +----------- + +Instead of creating our framework from scratch, we are going to write the same +"application" over and over again, adding one abstraction at a time. Let's +start with the simplest web application we can think of in PHP:: + + // framework/index.php + + $input = $_GET['name']; + + printf('Hello %s', $input); + +If you have PHP 5.4, you can use the PHP built-in server to test this great +application in a browser (``http://localhost:4321/index.php?name=Fabien``). +Otherwise, use your own server (Apache, Nginx, etc.): + +.. code-block:: bash + + $ php -S 127.0.0.1:4321 + +In the next chapter, we are going to introduce the HttpFoundation Component +and see what it brings us. + +.. _`Symfony`: http://symfony.com/ +.. _`documentation`: http://symfony.com/doc +.. _`Silex`: http://silex.sensiolabs.org/ +.. _`Composer`: http://packagist.org/about-composer diff --git a/create_framework/map.rst.inc b/create_framework/map.rst.inc new file mode 100644 index 00000000000..574c0f5e769 --- /dev/null +++ b/create_framework/map.rst.inc @@ -0,0 +1,12 @@ +* :doc:`/create_framework/introduction` +* :doc:`/create_framework/http-foundation` +* :doc:`/create_framework/front-controller` +* :doc:`/create_framework/routing` +* :doc:`/create_framework/templating` +* :doc:`/create_framework/http-kernel-controller-resolver` +* :doc:`/create_framework/separation-of-concerns` +* :doc:`/create_framework/unit-testing` +* :doc:`/create_framework/event-dispatcher` +* :doc:`/create_framework/http-kernel-httpkernelinterface` +* :doc:`/create_framework/http-kernel-httpkernel-class` +* :doc:`/create_framework/dependency-injection` diff --git a/create_framework/routing.rst b/create_framework/routing.rst new file mode 100644 index 00000000000..6bbb46f64b8 --- /dev/null +++ b/create_framework/routing.rst @@ -0,0 +1,222 @@ +The Routing Component +===================== + +Before we start diving into the Routing component, let's refactor our current +framework just a little to make templates even more readable:: + + // example.com/web/front.php + + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + + $request = Request::createFromGlobals(); + + $map = array( + '/hello' => 'hello', + '/bye' => 'bye', + ); + + $path = $request->getPathInfo(); + if (isset($map[$path])) { + ob_start(); + extract($request->query->all(), EXTR_SKIP); + include sprintf(__DIR__.'/../src/pages/%s.php', $map[$path]); + $response = new Response(ob_get_clean()); + } else { + $response = new Response('Not Found', 404); + } + + $response->send(); + +As we now extract the request query parameters, simplify the ``hello.php`` +template as follows:: + + + + Hello + +Now, we are in good shape to add new features. + +One very important aspect of any website is the form of its URLs. Thanks to +the URL map, we have decoupled the URL from the code that generates the +associated response, but it is not yet flexible enough. For instance, we might +want to support dynamic paths to allow embedding data directly into the URL +instead of relying on a query string: + + # Before + /hello?name=Fabien + + # After + /hello/Fabien + +To support this feature, add the Symfony Routing component as a dependency: + +.. code-block:: bash + + $ composer require symfony/routing + +Instead of an array for the URL map, the Routing component relies on a +``RouteCollection`` instance:: + + use Symfony\Component\Routing\RouteCollection; + + $routes = new RouteCollection(); + +Let's add a route that describe the ``/hello/SOMETHING`` URL and add another +one for the simple ``/bye`` one:: + + use Symfony\Component\Routing\Route; + + $routes->add('hello', new Route('/hello/{name}', array('name' => 'World'))); + $routes->add('bye', new Route('/bye')); + +Each entry in the collection is defined by a name (``hello``) and a ``Route`` +instance, which is defined by a route pattern (``/hello/{name}``) and an array +of default values for route attributes (``array('name' => 'World')``). + +.. note:: + + Read the official `documentation`_ for the Routing component to learn more + about its many features like URL generation, attribute requirements, HTTP + method enforcements, loaders for YAML or XML files, dumpers to PHP or + Apache rewrite rules for enhanced performance, and much more. + +Based on the information stored in the ``RouteCollection`` instance, a +``UrlMatcher`` instance can match URL paths:: + + use Symfony\Component\Routing\RequestContext; + use Symfony\Component\Routing\Matcher\UrlMatcher; + + $context = new RequestContext(); + $context->fromRequest($request); + $matcher = new UrlMatcher($routes, $context); + + $attributes = $matcher->match($request->getPathInfo()); + +The ``match()`` method takes a request path and returns an array of attributes +(notice that the matched route is automatically stored under the special +``_route`` attribute):: + + print_r($matcher->match('/bye')); + array ( + '_route' => 'bye', + ); + + print_r($matcher->match('/hello/Fabien')); + array ( + 'name' => 'Fabien', + '_route' => 'hello', + ); + + print_r($matcher->match('/hello')); + array ( + 'name' => 'World', + '_route' => 'hello', + ); + +.. note:: + + Even if we don't strictly need the request context in our examples, it is + used in real-world applications to enforce method requirements and more. + +The URL matcher throws an exception when none of the routes match:: + + $matcher->match('/not-found'); + + // throws a Symfony\Component\Routing\Exception\ResourceNotFoundException + +With this knowledge in mind, let's write the new version of our framework:: + + // example.com/web/front.php + + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $context->fromRequest($request); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + try { + extract($matcher->match($request->getPathInfo()), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + $response = new Response(ob_get_clean()); + } catch (Routing\Exception\ResourceNotFoundException $e) { + $response = new Response('Not Found', 404); + } catch (Exception $e) { + $response = new Response('An error occurred', 500); + } + + $response->send(); + +There are a few new things in the code:: + +* Route names are used for template names; + +* ``500`` errors are now managed correctly; + +* Request attributes are extracted to keep our templates simple:: + + + + Hello + +* Route configuration has been moved to its own file: + + .. code-block:: php + + // example.com/src/app.php + + use Symfony\Component\Routing; + + $routes = new Routing\RouteCollection(); + $routes->add('hello', new Routing\Route('/hello/{name}', array('name' => 'World'))); + $routes->add('bye', new Routing\Route('/bye')); + + return $routes; + + We now have a clear separation between the configuration (everything + specific to our application in ``app.php``) and the framework (the generic + code that powers our application in ``front.php``). + +With less than 30 lines of code, we have a new framework, more powerful and +more flexible than the previous one. Enjoy! + +Using the Routing component has one big additional benefit: the ability to +generate URLs based on Route definitions. When using both URL matching and URL +generation in your code, changing the URL patterns should have no other +impact. Want to know how to use the generator? Insanely easy:: + + use Symfony\Component\Routing; + + $generator = new Routing\Generator\UrlGenerator($routes, $context); + + echo $generator->generate('hello', array('name' => 'Fabien')); + // outputs /hello/Fabien + +The code should be self-explanatory; and thanks to the context, you can even +generate absolute URLs:: + + echo $generator->generate('hello', array('name' => 'Fabien'), true); + // outputs something like http://example.com/somewhere/hello/Fabien + +.. tip:: + + Concerned about performance? Based on your route definitions, create a + highly optimized URL matcher class that can replace the default + ``UrlMatcher``:: + + $dumper = new Routing\Matcher\Dumper\PhpMatcherDumper($routes); + + echo $dumper->dump(); + +.. _`documentation`: http://symfony.com/doc/current/components/routing.html diff --git a/create_framework/separation-of-concerns.rst b/create_framework/separation-of-concerns.rst new file mode 100644 index 00000000000..5f8384f23b0 --- /dev/null +++ b/create_framework/separation-of-concerns.rst @@ -0,0 +1,181 @@ +The Separation of Concerns +========================== + +One down-side of our framework right now is that we need to copy and paste the +code in ``front.php`` each time we create a new website. 40 lines of code is +not that much, but it would be nice if we could wrap this code into a proper +class. It would bring us better *reusability* and easier testing to name just +a few benefits. + +If you have a closer look at the code, ``front.php`` has one input, the +Request, and one output, the Response. Our framework class will follow this +simple principle: the logic is about creating the Response associated with a +Request. + +Let's create our very own namespace for our framework: ``Simplex``. Move the +request handling logic into its own ``Simplex\\Framework`` class:: + + // example.com/src/Simplex/Framework.php + + namespace Simplex; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Matcher\UrlMatcher; + use Symfony\Component\Routing\Exception\ResourceNotFoundException; + use Symfony\Component\HttpKernel\Controller\ControllerResolver; + + class Framework + { + protected $matcher; + protected $resolver; + + public function __construct(UrlMatcher $matcher, ControllerResolver $resolver) + { + $this->matcher = $matcher; + $this->resolver = $resolver; + } + + public function handle(Request $request) + { + $this->matcher->getContext()->fromRequest($request); + + try { + $request->attributes->add($this->matcher->match($request->getPathInfo())); + + $controller = $this->resolver->getController($request); + $arguments = $this->resolver->getArguments($request, $controller); + + return call_user_func_array($controller, $arguments); + } catch (ResourceNotFoundException $e) { + return new Response('Not Found', 404); + } catch (\Exception $e) { + return new Response('An error occurred', 500); + } + } + } + +And update ``example.com/web/front.php`` accordingly:: + + // example.com/web/front.php + + // ... + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + $resolver = new HttpKernel\Controller\ControllerResolver(); + + $framework = new Simplex\Framework($matcher, $resolver); + $response = $framework->handle($request); + + $response->send(); + +To wrap up the refactoring, let's move everything but routes definition from +``example.com/src/app.php`` into yet another namespace: ``Calendar``. + +For the classes defined under the ``Simplex`` and ``Calendar`` namespaces to +be autoloaded, update the ``composer.json`` file: + +.. code-block:: javascript + + { + "require": { + "symfony/http-foundation": "2.5.*", + "symfony/routing": "2.5.*", + "symfony/http-kernel": "2.5.*" + }, + "autoload": { + "psr-0": { "Simplex\\": "src/", "Calendar\\": "src/" } + } + } + +.. note:: + + For the Composer autoloader to be updated, run ``composer update``. + +Move the controller to ``Calendar\\Controller\\LeapYearController``:: + + // example.com/src/Calendar/Controller/LeapYearController.php + + namespace Calendar\Controller; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Calendar\Model\LeapYear; + + class LeapYearController + { + public function indexAction(Request $request, $year) + { + $leapyear = new LeapYear(); + if ($leapyear->isLeapYear($year)) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + } + +And move the ``is_leap_year()`` function to its own class too:: + + // example.com/src/Calendar/Model/LeapYear.php + + namespace Calendar\Model; + + class LeapYear + { + public function isLeapYear($year = null) + { + if (null === $year) { + $year = date('Y'); + } + + return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100); + } + } + +Don't forget to update the ``example.com/src/app.php`` file accordingly:: + + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array( + 'year' => null, + '_controller' => 'Calendar\\Controller\\LeapYearController::indexAction', + ))); + +To sum up, here is the new file layout: + +.. code-block:: text + + example.com + ├── composer.json + │ src + │ ├── app.php + │ └── Simplex + │ └── Framework.php + │ └── Calendar + │ └── Controller + │ │ └── LeapYearController.php + │ └── Model + │ └── LeapYear.php + ├── vendor + └── web + └── front.php + +That's it! Our application has now four different layers and each of them has +a well defined goal: + +* ``web/front.php``: The front controller; the only exposed PHP code that + makes the interface with the client (it gets the Request and sends the + Response) and provides the boiler-plate code to initialize the framework and + our application; + +* ``src/Simplex``: The reusable framework code that abstracts the handling of + incoming Requests (by the way, it makes your controllers/templates easily + testable -- more about that later on); + +* ``src/Calendar``: Our application specific code (the controllers and the + model); + +* ``src/app.php``: The application configuration/framework customization. diff --git a/create_framework/templating.rst b/create_framework/templating.rst new file mode 100644 index 00000000000..43e77f92623 --- /dev/null +++ b/create_framework/templating.rst @@ -0,0 +1,184 @@ +Templating +========== + +The astute reader has noticed that our framework hardcodes the way specific +"code" (the templates) is run. For simple pages like the ones we have created +so far, that's not a problem, but if you want to add more logic, you would be +forced to put the logic into the template itself, which is probably not a good +idea, especially if you still have the separation of concerns principle in +mind. + +Let's separate the template code from the logic by adding a new layer: the +controller: *The controller's mission is to generate a Response based on the +information conveyed by the client's Request.* + +Change the template rendering part of the framework to read as follows:: + + // example.com/web/front.php + + // ... + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + $response = call_user_func('render_template', $request); + } catch (Routing\Exception\ResourceNotFoundException $e) { + $response = new Response('Not Found', 404); + } catch (Exception $e) { + $response = new Response('An error occurred', 500); + } + +As the rendering is now done by an external function (``render_template()`` +here), we need to pass to it the attributes extracted from the URL. We could +have passed them as an additional argument to ``render_template()``, but +instead, let's use another feature of the ``Request`` class called +*attributes*: Request attributes is a way to attach additional information +about the Request that is not directly related to the HTTP Request data. + +You can now create the ``render_template()`` function, a generic controller +that renders a template when there is no specific logic. To keep the same +template as before, request attributes are extracted before the template is +rendered:: + + function render_template($request) + { + extract($request->attributes->all(), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + return new Response(ob_get_clean()); + } + +As ``render_template`` is used as an argument to the PHP ``call_user_func()`` +function, we can replace it with any valid PHP `callbacks`_. This allows us to +use a function, an anonymous function, or a method of a class as a +controller... your choice. + +As a convention, for each route, the associated controller is configured via +the ``_controller`` route attribute:: + + $routes->add('hello', new Routing\Route('/hello/{name}', array( + 'name' => 'World', + '_controller' => 'render_template', + ))); + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + $response = call_user_func($request->attributes->get('_controller'), $request); + } catch (Routing\Exception\ResourceNotFoundException $e) { + $response = new Response('Not Found', 404); + } catch (Exception $e) { + $response = new Response('An error occurred', 500); + } + +A route can now be associated with any controller and of course, within a +controller, you can still use the ``render_template()`` to render a template:: + + $routes->add('hello', new Routing\Route('/hello/{name}', array( + 'name' => 'World', + '_controller' => function ($request) { + return render_template($request); + } + ))); + +This is rather flexible as you can change the Response object afterwards and +you can even pass additional arguments to the template:: + + $routes->add('hello', new Routing\Route('/hello/{name}', array( + 'name' => 'World', + '_controller' => function ($request) { + // $foo will be available in the template + $request->attributes->set('foo', 'bar'); + + $response = render_template($request); + + // change some header + $response->headers->set('Content-Type', 'text/plain'); + + return $response; + } + ))); + +Here is the updated and improved version of our framework:: + + // example.com/web/front.php + + require_once __DIR__.'/../vendor/autoload.php'; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing; + + function render_template($request) + { + extract($request->attributes->all(), EXTR_SKIP); + ob_start(); + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); + + return new Response(ob_get_clean()); + } + + $request = Request::createFromGlobals(); + $routes = include __DIR__.'/../src/app.php'; + + $context = new Routing\RequestContext(); + $context->fromRequest($request); + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); + + try { + $request->attributes->add($matcher->match($request->getPathInfo())); + $response = call_user_func($request->attributes->get('_controller'), $request); + } catch (Routing\Exception\ResourceNotFoundException $e) { + $response = new Response('Not Found', 404); + } catch (Exception $e) { + $response = new Response('An error occurred', 500); + } + + $response->send(); + +To celebrate the birth of our new framework, let's create a brand new +application that needs some simple logic. Our application has one page that +says whether a given year is a leap year or not. When calling +``/is_leap_year``, you get the answer for the current year, but you can +also specify a year like in ``/is_leap_year/2009``. Being generic, the +framework does not need to be modified in any way, just create a new +``app.php`` file:: + + // example.com/src/app.php + + use Symfony\Component\Routing; + use Symfony\Component\HttpFoundation\Response; + + function is_leap_year($year = null) { + if (null === $year) { + $year = date('Y'); + } + + return 0 == $year % 400 || (0 == $year % 4 && 0 != $year % 100); + } + + $routes = new Routing\RouteCollection(); + $routes->add('leap_year', new Routing\Route('/is_leap_year/{year}', array( + 'year' => null, + '_controller' => function ($request) { + if (is_leap_year($request->attributes->get('year'))) { + return new Response('Yep, this is a leap year!'); + } + + return new Response('Nope, this is not a leap year.'); + } + ))); + + return $routes; + +The ``is_leap_year()`` function returns ``true`` when the given year is a leap +year, ``false`` otherwise. If the year is ``null``, the current year is +tested. The controller is simple: it gets the year from the request +attributes, pass it to the `is_leap_year()`` function, and according to the +return value it creates a new Response object. + +As always, you can decide to stop here and use the framework as is; it's +probably all you need to create simple websites like those fancy one-page +`websites`_ and hopefully a few others. + +.. _`callbacks`: http://php.net/callback#language.types.callback +.. _`websites`: http://kottke.org/08/02/single-serving-sites diff --git a/create_framework/unit-testing.rst b/create_framework/unit-testing.rst new file mode 100644 index 00000000000..7bccdceb4bd --- /dev/null +++ b/create_framework/unit-testing.rst @@ -0,0 +1,190 @@ +Unit Testing +============ + +You might have noticed some subtle but nonetheless important bugs in the +framework we built in the previous chapter. When creating a framework, you +must be sure that it behaves as advertised. If not, all the applications based +on it will exhibit the same bugs. The good news is that whenever you fix a +bug, you are fixing a bunch of applications too. + +Today's mission is to write unit tests for the framework we have created by +using `PHPUnit`_. Create a PHPUnit configuration file in +``example.com/phpunit.xml.dist``: + +.. code-block:: xml + + + + + + + ./tests + + + + +This configuration defines sensible defaults for most PHPUnit settings; more +interesting, the autoloader is used to bootstrap the tests, and tests will be +stored under the ``example.com/tests/`` directory. + +Now, let's write a test for "not found" resources. To avoid the creation of +all dependencies when writing tests and to really just unit-test what we want, +we are going to use `test doubles`_. Test doubles are easier to create when we +rely on interfaces instead of concrete classes. Fortunately, Symfony provides +such interfaces for core objects like the URL matcher and the controller +resolver. Modify the framework to make use of them:: + + // example.com/src/Simplex/Framework.php + + namespace Simplex; + + // ... + + use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; + + class Framework + { + protected $matcher; + protected $resolver; + + public function __construct(UrlMatcherInterface $matcher, ControllerResolverInterface $resolver) + { + $this->matcher = $matcher; + $this->resolver = $resolver; + } + + // ... + } + +We are now ready to write our first test:: + + // example.com/tests/Simplex/Tests/FrameworkTest.php + + namespace Simplex\Tests; + + use Simplex\Framework; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\Exception\ResourceNotFoundException; + + class FrameworkTest extends \PHPUnit_Framework_TestCase + { + public function testNotFoundHandling() + { + $framework = $this->getFrameworkForException(new ResourceNotFoundException()); + + $response = $framework->handle(new Request()); + + $this->assertEquals(404, $response->getStatusCode()); + } + + protected function getFrameworkForException($exception) + { + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); + $matcher + ->expects($this->once()) + ->method('match') + ->will($this->throwException($exception)) + ; + $resolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); + + return new Framework($matcher, $resolver); + } + } + +This test simulates a request that does not match any route. As such, the +``match()`` method returns a ``ResourceNotFoundException`` exception and we +are testing that our framework converts this exception to a 404 response. + +Executing this test is as simple as running ``phpunit`` from the +``example.com`` directory: + +.. code-block:: bash + + $ phpunit + +.. note:: + + If you don't understand what the hell is going on in the code, read the + PHPUnit documentation on `test doubles`_. + +After the test ran, you should see a green bar. If not, you have a bug +either in the test or in the framework code! + +Adding a unit test for any exception thrown in a controller is just as easy:: + + public function testErrorHandling() + { + $framework = $this->getFrameworkForException(new \RuntimeException()); + + $response = $framework->handle(new Request()); + + $this->assertEquals(500, $response->getStatusCode()); + } + +Last, but not the least, let's write a test for when we actually have a proper +Response:: + + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Controller\ControllerResolver; + + public function testControllerResponse() + { + $matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); + $matcher + ->expects($this->once()) + ->method('match') + ->will($this->returnValue(array( + '_route' => 'foo', + 'name' => 'Fabien', + '_controller' => function ($name) { + return new Response('Hello '.$name); + } + ))) + ; + $resolver = new ControllerResolver(); + + $framework = new Framework($matcher, $resolver); + + $response = $framework->handle(new Request()); + + $this->assertEquals(200, $response->getStatusCode()); + $this->assertContains('Hello Fabien', $response->getContent()); + } + +In this test, we simulate a route that matches and returns a simple +controller. We check that the response status is 200 and that its content is +the one we have set in the controller. + +To check that we have covered all possible use cases, run the PHPUnit test +coverage feature (you need to enable `XDebug`_ first): + +.. code-block:: bash + + $ phpunit --coverage-html=cov/ + +Open ``example.com/cov/src_Simplex_Framework.php.html`` in a browser and check +that all the lines for the Framework class are green (it means that they have +been visited when the tests were executed). + +Thanks to the simple object-oriented code that we have written so far, we have +been able to write unit-tests to cover all possible use cases of our +framework; test doubles ensured that we were actually testing our code and not +Symfony code. + +Now that we are confident (again) about the code we have written, we can +safely think about the next batch of features we want to add to our framework. + +.. _`PHPUnit`: http://www.phpunit.de/manual/current/en/index.html +.. _`test doubles`: http://www.phpunit.de/manual/current/en/test-doubles.html +.. _`XDebug`: http://xdebug.org/ diff --git a/index.rst b/index.rst index 2ef2df24f45..cdfd5ff67f8 100644 --- a/index.rst +++ b/index.rst @@ -88,3 +88,15 @@ Contribute to Symfony: contributing/index .. include:: /contributing/map.rst.inc + +Create your Own Framework +------------------------- + +Want to create your own framework based on Symfony? + +.. toctree:: + :hidden: + + create_framework/index + +.. include:: /create_framework/map.rst.inc