|
| 1 | +Create your own framework... on top of the Symfony2 Components (part 4) |
| 2 | +======================================================================= |
| 3 | + |
| 4 | +Before we start with today's topic, let's refactor our current framework just |
| 5 | +a little to make templates even more readable:: |
| 6 | + |
| 7 | + <?php |
| 8 | + |
| 9 | + // example.com/web/front.php |
| 10 | + |
| 11 | + require_once __DIR__.'/../src/autoload.php'; |
| 12 | + |
| 13 | + use Symfony\Component\HttpFoundation\Request; |
| 14 | + use Symfony\Component\HttpFoundation\Response; |
| 15 | + |
| 16 | + $request = Request::createFromGlobals(); |
| 17 | + |
| 18 | + $map = array( |
| 19 | + '/hello' => 'hello', |
| 20 | + '/bye' => 'bye', |
| 21 | + ); |
| 22 | + |
| 23 | + $path = $request->getPathInfo(); |
| 24 | + if (isset($map[$path])) { |
| 25 | + ob_start(); |
| 26 | + extract($request->query->all()); |
| 27 | + include sprintf(__DIR__.'/../src/pages/%s.php', $map[$path]); |
| 28 | + $response = new Response(ob_get_clean()); |
| 29 | + } else { |
| 30 | + $response = new Response('Not Found', 404); |
| 31 | + } |
| 32 | + |
| 33 | + $response->send(); |
| 34 | + |
| 35 | +As we now extract the request query parameters, simplify the ``hello.php`` |
| 36 | +template as follows:: |
| 37 | + |
| 38 | + <!-- example.com/src/pages/hello.php --> |
| 39 | + |
| 40 | + Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> |
| 41 | + |
| 42 | +Now, we are in good shape to add new features. |
| 43 | + |
| 44 | +One very important aspect of any website is the form of its URLs. Thanks to |
| 45 | +the URL map, we have decoupled the URL from the code that generates the |
| 46 | +associated response, but it is not yet flexible enough. For instance, we might |
| 47 | +want to support dynamic paths to allow embedding data directly into the URL |
| 48 | +instead of relying on a query string: |
| 49 | + |
| 50 | + # Before |
| 51 | + /hello?name=Fabien |
| 52 | + |
| 53 | + # After |
| 54 | + /hello/Fabien |
| 55 | + |
| 56 | +To support this feature, we are going to use the Symfony2 Routing component. |
| 57 | +As always, add it to ``composer.json`` and run the ``php composer.phar |
| 58 | +update`` command to install it:: |
| 59 | + |
| 60 | +.. code-block:: json |
| 61 | +
|
| 62 | + { |
| 63 | + "require": { |
| 64 | + "symfony/class-loader": "2.1.*", |
| 65 | + "symfony/http-foundation": "2.1.*", |
| 66 | + "symfony/routing": "2.1.*" |
| 67 | + } |
| 68 | + } |
| 69 | +
|
| 70 | +From now on, we are going to use the generated Composer autoloader instead of |
| 71 | +our own ``autoload.php``. Remove the ``autoload.php`` file and replace its |
| 72 | +reference in ``front.php``:: |
| 73 | + |
| 74 | + <?php |
| 75 | + |
| 76 | + // example.com/web/front.php |
| 77 | + |
| 78 | + require_once __DIR__.'/../vendor/.composer/autoload.php'; |
| 79 | + |
| 80 | + // ... |
| 81 | + |
| 82 | +Instead of an array for the URL map, the Routing component relies on a |
| 83 | +``RouteCollection`` instance:: |
| 84 | + |
| 85 | + use Symfony\Component\Routing\RouteCollection; |
| 86 | + |
| 87 | + $routes = new RouteCollection(); |
| 88 | + |
| 89 | +Let's add a route that describe the ``/hello/SOMETHING`` URL and add another |
| 90 | +one for the simple ``/bye`` one:: |
| 91 | + |
| 92 | + use Symfony\Component\Routing\Route; |
| 93 | + |
| 94 | + $routes->add('hello', new Route('/hello/{name}', array('name' => 'World'))); |
| 95 | + $routes->add('bye', new Route('/bye')); |
| 96 | + |
| 97 | +Each entry in the collection is defined by a name (``hello``) and a ``Route`` |
| 98 | +instance, which is defined by a route pattern (``/hello/{name}``) and an array |
| 99 | +of default values for route attributes (``array('name' => 'World')``). |
| 100 | + |
| 101 | +.. note:: |
| 102 | + |
| 103 | + Read the official `documentation`_ for the Routing component to learn more |
| 104 | + about its many features like URL generation, attribute requirements, HTTP |
| 105 | + method enforcements, loaders for YAML or XML files, dumpers to PHP or |
| 106 | + Apache rewrite rules for enhanced performance, and much more. |
| 107 | + |
| 108 | +Based on the information stored in the ``RouteCollection`` instance, a |
| 109 | +``UrlMatcher`` instance can match URL paths:: |
| 110 | + |
| 111 | + use Symfony\Component\Routing\RequestContext; |
| 112 | + use Symfony\Component\Routing\Matcher\UrlMatcher; |
| 113 | + |
| 114 | + $context = new RequestContext(); |
| 115 | + $context->fromRequest($request); |
| 116 | + $matcher = new UrlMatcher($routes, $context); |
| 117 | + |
| 118 | + $attributes = $matcher->match($request->getPathInfo()); |
| 119 | + |
| 120 | +The ``match()`` method takes a request path and returns an array of attributes |
| 121 | +(notice that the matched route is automatically stored under the special |
| 122 | +``_route`` attribute):: |
| 123 | + |
| 124 | + print_r($matcher->match('/bye')); |
| 125 | + array ( |
| 126 | + '_route' => 'bye', |
| 127 | + ); |
| 128 | + |
| 129 | + print_r($matcher->match('/hello/Fabien')); |
| 130 | + array ( |
| 131 | + 'name' => 'Fabien', |
| 132 | + '_route' => 'hello', |
| 133 | + ); |
| 134 | + |
| 135 | + print_r($matcher->match('/hello')); |
| 136 | + array ( |
| 137 | + 'name' => 'World', |
| 138 | + '_route' => 'hello', |
| 139 | + ); |
| 140 | + |
| 141 | +.. note:: |
| 142 | + |
| 143 | + Even if we don't strictly need the request context in our examples, it is |
| 144 | + used in real-world applications to enforce method requirements and more. |
| 145 | + |
| 146 | +The URL matcher throws an exception when none of the routes match:: |
| 147 | + |
| 148 | + $matcher->match('/not-found'); |
| 149 | + |
| 150 | + // throws a Symfony\Component\Routing\Exception\ResourceNotFoundException |
| 151 | + |
| 152 | +With this knowledge in mind, let's write the new version of our framework:: |
| 153 | + |
| 154 | + <?php |
| 155 | + |
| 156 | + // example.com/web/front.php |
| 157 | + |
| 158 | + require_once __DIR__.'/../vendor/.composer/autoload.php'; |
| 159 | + |
| 160 | + use Symfony\Component\HttpFoundation\Request; |
| 161 | + use Symfony\Component\HttpFoundation\Response; |
| 162 | + use Symfony\Component\Routing; |
| 163 | + |
| 164 | + $request = Request::createFromGlobals(); |
| 165 | + $routes = include __DIR__.'/../src/app.php'; |
| 166 | + |
| 167 | + $context = new Routing\RequestContext(); |
| 168 | + $context->fromRequest($request); |
| 169 | + $matcher = new Routing\Matcher\UrlMatcher($routes, $context); |
| 170 | + |
| 171 | + try { |
| 172 | + extract($matcher->match($request->getPathInfo())); |
| 173 | + ob_start(); |
| 174 | + include sprintf(__DIR__.'/../src/pages/%s.php', $_route); |
| 175 | + |
| 176 | + $response = new Response(ob_get_clean()); |
| 177 | + } catch (Routing\Exception\ResourceNotFoundException $e) { |
| 178 | + $response = new Response('Not Found', 404); |
| 179 | + } catch (Exception $e) { |
| 180 | + $response = new Response('An error occurred', 500); |
| 181 | + } |
| 182 | + |
| 183 | + $response->send(); |
| 184 | + |
| 185 | +There are a few new things in the code:: |
| 186 | + |
| 187 | +* Route names are used for template names; |
| 188 | + |
| 189 | +* ``500`` errors are now managed correctly; |
| 190 | + |
| 191 | +* Request attributes are extracted to keep our templates simple:: |
| 192 | + |
| 193 | + <!-- example.com/src/pages/hello.php --> |
| 194 | + |
| 195 | + Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> |
| 196 | + |
| 197 | +* Routes configuration has been moved to its own file: |
| 198 | + |
| 199 | + .. code-block:: php |
| 200 | +
|
| 201 | + <?php |
| 202 | +
|
| 203 | + // example.com/src/app.php |
| 204 | +
|
| 205 | + use Symfony\Component\Routing; |
| 206 | +
|
| 207 | + $routes = new Routing\RouteCollection(); |
| 208 | + $routes->add('hello', new Routing\Route('/hello/{name}', array('name' => 'World'))); |
| 209 | + $routes->add('bye', new Routing\Route('/bye')); |
| 210 | +
|
| 211 | + We now have a clear separation between the configuration (everything |
| 212 | + specific to our application in ``app.php``) and the framework (the generic |
| 213 | + code that powers our application in ``front.php``). |
| 214 | + |
| 215 | +With less than 30 lines of code, we have a new framework, more powerful and |
| 216 | +more flexible than the previous one. Enjoy! |
| 217 | + |
| 218 | +Using the Routing component has one big additional benefit: the ability to |
| 219 | +generate URLs based on Route definitions. When using both URL matching and URL |
| 220 | +generation in your code, changing the URL patterns should have no other |
| 221 | +impact. Want to know how to use the generator? Insanely easy:: |
| 222 | + |
| 223 | + use Symfony\Component\Routing; |
| 224 | + |
| 225 | + $generator = new Routing\Generator\UrlGenerator($routes, $context); |
| 226 | + |
| 227 | + echo $generator->generate('hello', array('name' => 'Fabien)); |
| 228 | + // outputs /hello/Fabien |
| 229 | + |
| 230 | +The code should be self-explanatory; and thanks to the context, you can even |
| 231 | +generate absolute URLs:: |
| 232 | + |
| 233 | + echo $generator->generate('hello', array('name' => 'Fabien), true); |
| 234 | + // outputs something like http://example.com/somewhere/hello/Fabien |
| 235 | + |
| 236 | +.. tip:: |
| 237 | + |
| 238 | + Concerned about performance? Based on your route definitions, create a |
| 239 | + highly optimized URL matcher class that can replace the default |
| 240 | + ``UrlMatcher``:: |
| 241 | + |
| 242 | + $dumper = new Routing\Matcher\Dumper\PhpMatcherDumper($routes); |
| 243 | + |
| 244 | + echo $dumper->dump(); |
| 245 | + |
| 246 | + Want even more performance? Dump your routes as a set of Apache rewrite |
| 247 | + rules:: |
| 248 | + |
| 249 | + $dumper = new Routing\Matcher\Dumper\ApacheMatcherDumper($routes); |
| 250 | + |
| 251 | + echo $dumper->dump(); |
| 252 | + |
| 253 | +.. _`documentation`: http://symfony.com/doc/current/components/routing.html |
0 commit comments