Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • SensioLabs Professional services to help you with Symfony
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by SensioLabs
  1. Home
  2. Documentation
  3. Create Framework
  4. The Front Controller
  • Documentation
  • Book
  • Reference
  • Bundles
  • Cloud

The Front Controller

Edit this page

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:

1
2
3
4
5
6
7
8
9
10
// 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:

1
2
3
4
5
6
7
8
// 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:

1
2
3
4
5
6
7
// framework/index.php
require_once __DIR__.'/init.php';

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

$response->setContent(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();

And for the "Goodbye" page:

1
2
3
4
5
// framework/bye.php
require_once __DIR__.'/init.php';

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

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

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

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// framework/front.php
require_once __DIR__.'/vendor/autoload.php';

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

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

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

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

$response->send();

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

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

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

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

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

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

/hello and /bye are the page paths.

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 set up 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 accesses the same script (front.php) for all pages, we can secure the code further by moving all other PHP files outside of the web root directory:

1
2
3
4
5
6
7
8
9
10
11
example.com
├── composer.json
├── composer.lock
├── src
│   └── pages
│       ├── hello.php
│       └── bye.php
├── vendor
│   └── autoload.php
└── web
    └── front.php

Now, configure your web server root directory to point to web/ and all other files will no longer be accessible from the client.

To test your changes in a browser (http://localhost:4321/hello?name=Fabien), run the Symfony Local Web Server:

1
$ symfony server:start --port=4321 --passthru=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 echoing the content and calling the setContent() directly from the front controller script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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:

1
2
3
4
<!-- example.com/src/pages/hello.php -->
<?php $name = $request->query->get('name', 'World') ?>

Hello <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>

We have the first version of our framework:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 = [
    '/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.

Previous page The HttpFoundation Component
Next page The Routing Component
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version
    We stand with Ukraine.
    Version:

    Symfony 6.2 is backed by

    Symfony 6.2 is backed by

    Make sure your project is risk free

    Make sure your project is risk free

    Symfony Code Performance Profiling

    Symfony Code Performance Profiling

    Symfony footer

    ↓ Our footer now uses the colors of the Ukrainian flag because Symfony stands with the people of Ukraine.

    Avatar of Stefan Gehrig, a Symfony contributor

    Thanks Stefan Gehrig (@sgehrig) for being a Symfony contributor

    4 commits • 279 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • Symfony at a Glance
      • Symfony Components
      • Case Studies
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • SymfonyConnect
      • Support
      • How to be Involved
      • Code of Conduct
      • Events & Meetups
      • Projects using Symfony
      • Downloads Stats
      • Contributors
      • Backers
    • Blog

      • Events & Meetups
      • A week of symfony
      • Case studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Documentation
      • Living on the edge
      • Releases
      • Security Advisories
      • SymfonyInsight
      • Twig
      • SensioLabs
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Deployed on

    Follow Symfony

    Search by Algolia