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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy