Skip to content

Commit 26be81c

Browse files
committed
added part 3
1 parent 4b39fc0 commit 26be81c

File tree

1 file changed

+248
-0
lines changed

1 file changed

+248
-0
lines changed

book/part3.rst

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
Create your own framework... on top of the Symfony2 Components (part 3)
2+
=======================================================================
3+
4+
Up until now, our application is simplistic as there is only one page. To
5+
spice things up a little bit, let's go crazy and add another page that says
6+
goodbye::
7+
8+
<?php
9+
10+
// framework/bye.php
11+
12+
require_once __DIR__.'/autoload.php';
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
17+
$request = Request::createFromGlobals();
18+
19+
$response = new Response('Goodbye!');
20+
$response->send();
21+
22+
As you can see for yourself, much of the code is exactly the same as the one
23+
we have written for the first page. Let's extract the common code that we can
24+
share between all our pages. Code sharing sounds like a good plan to create
25+
our first "real" framework!
26+
27+
The PHP way of doing the refactoring would probably be the creation of an
28+
include file::
29+
30+
<?php
31+
32+
// framework/init.php
33+
34+
require_once __DIR__.'/autoload.php';
35+
36+
use Symfony\Component\HttpFoundation\Request;
37+
use Symfony\Component\HttpFoundation\Response;
38+
39+
$request = Request::createFromGlobals();
40+
$response = new Response();
41+
42+
Let's see it in action::
43+
44+
<?php
45+
46+
// framework/index.php
47+
48+
require_once __DIR__.'/init.php';
49+
50+
$input = $request->get('name', 'World');
51+
52+
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
53+
$response->send();
54+
55+
And for the "Goodbye" page::
56+
57+
<?php
58+
59+
// framework/bye.php
60+
61+
require_once __DIR__.'/init.php';
62+
63+
$response->setContent('Goodbye!');
64+
$response->send();
65+
66+
We have indeed moved most of the shared code into a central place, but it does
67+
not feel like a good abstraction, doesn't it? First, we still have the
68+
``send()`` method in all pages, then our pages does not look like templates,
69+
and we are still not able to test this code properly.
70+
71+
Moreover, adding a new page means that we need to create a new PHP script,
72+
which name is exposed to the end user via the URL
73+
(``http://example.com/goodbye.php``): there is a direct mapping between the PHP
74+
script name and the client URL. This is because the dispatching of the request
75+
is done by the web server directly. It might be a good idea to move this
76+
dispatching to our code for better flexibility. This can be easily achieved by
77+
routing all client requests to a single PHP script.
78+
79+
.. tip::
80+
81+
Exposing a single PHP script to the end user is a design pattern called
82+
the "`front controller`_".
83+
84+
Such a script might look like the following::
85+
86+
<?php
87+
88+
// framework/front.php
89+
90+
require_once __DIR__.'/autoload.php';
91+
92+
use Symfony\Component\HttpFoundation\Request;
93+
use Symfony\Component\HttpFoundation\Response;
94+
95+
$request = Request::createFromGlobals();
96+
$response = new Response();
97+
98+
$map = array(
99+
'/hello' => __DIR__.'/hello.php',
100+
'/bye' => __DIR__.'/bye.php',
101+
);
102+
103+
$path = $request->getPathInfo();
104+
if (isset($map[$path])) {
105+
require $map[$path];
106+
} else {
107+
$response->setStatusCode(404);
108+
$response->setContent('Not Found');
109+
}
110+
111+
$response->send();
112+
113+
And here is for instance the new ``hello.php`` script::
114+
115+
<?php
116+
117+
// framework/hello.php
118+
119+
$input = $request->get('name', 'World');
120+
$response->setContent(sprintf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8')));
121+
122+
In the ``front.php`` script, ``$map`` associates URL paths with their
123+
corresponding PHP script paths.
124+
125+
As a bonus, if the client ask for a path that is not defined in the URL map,
126+
we return a custom 404 page; you are now in control of your website.
127+
128+
To access a page, you must now use the ``front.php`` script:
129+
130+
* ``http://example.com/front.php/hello?name=Fabien``
131+
132+
* ``http://example.com/front.php/bye``
133+
134+
``/hello`` and ``/bye`` are the page *path*s.
135+
136+
.. tip::
137+
138+
Most web servers like Apache or nginx are able to rewrite the incoming
139+
URLs and remove the front controller script so that your users will be
140+
able to type ``http://example.com/hello?name=Fabien``, which looks much
141+
better.
142+
143+
So, the trick is the usage of the ``Request::getPathInfo()`` method which
144+
returns the path of the Request by removing the front controller script name
145+
including its sub-directories (only if needed -- see above tip).
146+
147+
.. tip::
148+
149+
You don't even need to setup a web server to test the code. Instead,
150+
replace the ``$request = Request::createFromGlobals();`` call to something
151+
like ``$request = Request::create('/hello?name=Fabien');`` where the
152+
argument is the URL path you want to simulate.
153+
154+
Now that the web server always access the same script (``front.php``) for all
155+
our pages, we can secure our code further by moving all other PHP files
156+
outside the web root directory:
157+
158+
example.com
159+
├── composer.json
160+
│ src
161+
│ ├── autoload.php
162+
│ └── pages
163+
│ ├── hello.php
164+
│ └── bye.php
165+
├── vendor
166+
└── web
167+
└── front.php
168+
169+
Now, configure your web server root directory to point to ``web/`` and all
170+
other files won't be accessible from the client anymore.
171+
172+
.. note::
173+
174+
For this new structure to work, you will have to adjust some paths in
175+
various PHP files; the changes are left as an exercise for the reader.
176+
177+
The last thing that is repeated in each page is the call to ``setContent()``.
178+
We can convert all pages to "templates" by just echoing the content and
179+
calling the ``setContent()`` directly from the front controller script::
180+
181+
<?php
182+
183+
// example.com/web/front.php
184+
185+
// ...
186+
187+
$path = $request->getPathInfo();
188+
if (isset($map[$path])) {
189+
ob_start();
190+
include $map[$path];
191+
$response->setContent(ob_get_clean());
192+
} else {
193+
$response->setStatusCode(404);
194+
$response->setContent('Not Found');
195+
}
196+
197+
// ...
198+
199+
And the ``hello.php`` script can now be converted to a template::
200+
201+
<!-- example.com/src/pages/hello.php -->
202+
203+
<?php $name = $request->get('name', 'World') ?>
204+
205+
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>
206+
207+
We have our framework for today::
208+
209+
<?php
210+
211+
// example.com/web/front.php
212+
213+
require_once __DIR__.'/../src/autoload.php';
214+
215+
use Symfony\Component\HttpFoundation\Request;
216+
use Symfony\Component\HttpFoundation\Response;
217+
218+
$request = Request::createFromGlobals();
219+
$response = new Response();
220+
221+
$map = array(
222+
'/hello' => __DIR__.'/../src/pages/hello.php',
223+
'/bye' => __DIR__.'/../src/pages/bye.php',
224+
);
225+
226+
$path = $request->getPathInfo();
227+
if (isset($map[$path])) {
228+
ob_start();
229+
include $map[$path];
230+
$response->setContent(ob_get_clean());
231+
} else {
232+
$response->setStatusCode(404);
233+
$response->setContent('Not Found');
234+
}
235+
236+
$response->send();
237+
238+
Adding a new page is a two step process: add an entry in the map and create a
239+
PHP template in ``src/pages/``. From a template, get the Request data via the
240+
``$request`` variable and tweak the Response headers via the ``$response``
241+
variable.
242+
243+
.. note::
244+
245+
If you decide to stop here, you can probably enhance your framework by
246+
extracting the URL map to a configuration file.
247+
248+
.. _`front controller`: http://symfony.com/doc/current/book/from_flat_php_to_symfony2.html#a-front-controller-to-the-rescue

0 commit comments

Comments
 (0)