Routes, Controllers, Pages, oh my!

Let's create our first page! Actually, this is the main job of a framework: to give you a route and controller system. A route is configuration that defines the URL for a page and a controller is a function that we write that actually builds the content for that page.

And right now... our app is really small! Instead of weighing down your project with every possible feature you could ever need - after all, we're not in zero-gravity yet - a Symfony app is basically just a small route-controller system. Later, we'll install more features when we need them, like a warp drive! Those always come in handy. Adding more features is actually going to be pretty awesome. More on that later.

First Route & Controller

Open your app's main routing file: config/routes.yaml:

4 lines config/routes.yaml
#index:
# path: /
# controller: App\Controller\DefaultController::index

Hey! We already have an example! Uncomment that. Ignore the index key for now: that's the internal name of the route, but it's not important yet.

This says that when someone goes to the homepage - / - Symfony should execute an index() method in a DefaultController class. Change this to ArticleController and the method to homepage:

4 lines config/routes.yaml
index:
path: /
controller: App\Controller\ArticleController::homepage

And... yea! That's a route! Hi route! It defines the URL and tells Symfony what controller function to execute.

The controller class doesn't exist yet, so let's create it! Right-click on the Controller directory and go to "New" or press Cmd+N on a Mac. Choose "PHP Class". And, yes! Remember that Composer setup we did in Preferences? Thanks to that, PhpStorm correctly guesses the namespace! The force is strong with this one... The namespace for every class in src/ should be App plus whatever sub-directory it's in.

Name this ArticleController:

14 lines src/Controller/ArticleController.php
<?php
namespace App\Controller;
... lines 4 - 6
class ArticleController
{
... lines 9 - 12
}

And inside, add public function homepage():

14 lines src/Controller/ArticleController.php
... lines 1 - 2
namespace App\Controller;
... lines 4 - 6
class ArticleController
{
public function homepage()
{
... line 11
}
}

This function is the controller... and it's our place to build the page. To be more confusing, it's also called an "action", or "ghob" to its Klingon friends.

Anyways, we can do whatever we want here: make database queries, API calls, take soil samples looking for organic materials or render a template. There's just one rule: a controller must return a Symfony Response object.

So let's say: return new Response(): we want the one from HttpFoundation. Give it a calm message: OMG! My first page already! WOOO!:

14 lines src/Controller/ArticleController.php
... lines 1 - 2
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
class ArticleController
{
public function homepage()
{
return new Response('OMG! My first page already! WOOO!');
}
}

Ahem. Oh, and check this out: when I let PhpStorm auto-complete the Response class it added this use statement to the top of the file automatically:

14 lines src/Controller/ArticleController.php
... lines 1 - 4
use Symfony\Component\HttpFoundation\Response;
... lines 6 - 14

You'll see me do that a lot. Good job Storm!

Let's try the page! Find your browser. Oh, this "Welcome" page only shows if you don't have any routes configured. Refresh! Yes! This is our page. Our first of many.

Annotation Routes

That was pretty easy, but it can be easier! Instead of creating our routes in YAML, let's use a cool feature called annotations. This is an extra feature, so we need to install it. Find your open terminal and run:

composer require annotations

Interesting... this annotations package actually installed sensio/framework-extra-bundle. We're going to talk about how that works very soon.

Now, about these annotation routes. Comment-out the YAML route:

4 lines config/routes.yaml
#index:
# path: /
# controller: App\Controller\ArticleController::homepage

Then, in ArticleController, above the controller method, add /**, hit enter, clear this out, and say @Route(). You can use either class - but make sure PhpStorm adds the use statement on top. Then add "/":

18 lines src/Controller/ArticleController.php
... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
... lines 6 - 7
class ArticleController
{
/**
* @Route("/")
*/
public function homepage()
{
... line 15
}
}

That's it! The route is defined right above the controller, which is why I love annotation routes: everything is in one place. But don't trust me, find your browser and refresh. It's a traaaap! I mean, it works!

Tip

What exactly are annotations? They're PHP comments that are read as configuration.

Fancy Wildcard Routes

So what else can we do with routes? Create another public function called show(). I want this page to eventually display a full article. Give it a route: @Route("/news/why-asteroids-taste-like-bacon"):

26 lines src/Controller/ArticleController.php
... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
... lines 6 - 7
class ArticleController
{
... lines 10 - 17
/**
* @Route("/news/why-asteroids-taste-like-bacon")
*/
public function show()
{
... line 23
}
}

Eventually, this is how we want our URLs to look. This is called a "slug", it's a URL version of the title. As usual, return a new Response('Future page to show one space article!'):

26 lines src/Controller/ArticleController.php
... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;
class ArticleController
{
... lines 10 - 17
/**
* @Route("/news/why-asteroids-taste-like-bacon")
*/
public function show()
{
return new Response('Future page to show one space article!');
}
}

Perfect! Copy that URL and try it in your browser. It works... but this sucks! I don't want to build a route and controller for every single article that lives in the database. Nope, we need a route that can match /news/ anything. How? Use {slug}:

29 lines src/Controller/ArticleController.php
... lines 1 - 7
class ArticleController
{
... lines 10 - 17
/**
* @Route("/news/{slug}")
*/
public function show($slug)
{
... lines 23 - 26
}
}

This route now matches /news/ anything: that {slug} is a wildcard. Oh, and the name slug could be anything. But whatever you choose now becomes available as an argument to your "ghob", I mean your action.

So let's refactor our success message to say:

Future page to show the article

And then that slug:

29 lines src/Controller/ArticleController.php
... lines 1 - 7
class ArticleController
{
... lines 10 - 17
/**
* @Route("/news/{slug}")
*/
public function show($slug)
{
return new Response(sprintf(
'Future page to show the article: "%s"',
$slug
));
}
}

Try it! Refresh the same URL. Yes! It matches the route and the slug prints! Change it to something else: /why-asteroids-taste-like-tacos. So delicious! Go back to bacon... because... ya know... everyone knows that's what asteroids really taste like.

And... yes! We're 3 chapters in and you now know the first half of Symfony: the route & controller system. Sure, you can do fancier things with routes, like match regular expressions, HTTP methods or host names - but that will all be pretty easy for you now.

It's time to move on to something really important: it's time to learn about Symfony Flex and the recipe system. Yum!

Leave a comment!

  • 2018-03-22 Tess Hsu

    God, thank Victor, yes, it is work after correct the controller name, sorry again

  • 2018-03-22 Victor Bocharsky

    Hey Tess,

    A simple mistake: your controller's name is "MenuControllerController" but should be just "MenuController" ;)

    Cheers!

  • 2018-03-21 Tess Hsu

    Hi,
    I wonder if someone had try to add another controller to play with,

    My steps is followed:
    1 add A new controller called MenuController.php under src/Controller/


    namespace App\Controller;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
    use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

    class MenuControllerController extends AbstractController
    {
    /**
    * @Route("/about", name="app_aboutpage")
    */
    public function aboutpage()
    {
    return $this->render('article/aboutpage.html.twig');
    }
    }

    2 Add template in article/aboutpage.html.twig


    {% extends 'base.html.twig' %}

    {% block body %}
    <div class="container">
    <div class="row">

    <div class="row">
    test create about page
    </div>
    </div>
    </div>
    {% endblock %}

    3 modify this path in base.html.twig


    <li class="nav-item">
    <a style="color: #fff;" class="nav-link" href="{{ path('app_aboutpage') }}">About</a>
    </li>


    Then run server and it is show error message:

    FileLoaderLoadException
    The autoloader expected class "App\Controller\MenuController" to be defined in file "/Users/webdev/Documents/dossier_web/symfony/the_spacebar/vendor/composer/../../src/Controller/MenuController.php". The file was found but the class was not in it, the class name or namespace probably has a typo in /Users/webdev/Documents/dossier_web/symfony/the_spacebar/config/services.yaml (which is loaded in resource "/Users/webdev/Documents/dossier_web/symfony/the_spacebar/config/services.yaml").

    It is possible to know where did i doing wrong since I just want to do: add a Controller-> make about link to go to http://127.0.0.1:8000/about

    thank you so much advanced

  • 2018-03-13 Agung

    Hey, i also have the same error, for the route "/" is no problem, for route "/news/..." give error message, after i check there is some kind a silly mistake that i have done. After you install annotations composer will generate annotations.yaml in routes folder, and i accidentally comment it (the one you should comment is routes.yaml). I hope this will help.

  • 2018-02-09 Diego Aguiar

    Hey Mohammad

    On Symfony4 you have to install "apache-pack" https://packagist.org/packa...
    it will generate a ".htaccess" file at your public directory, so you have to configure your apache to allow overrides

    Cheers!

  • 2018-02-09 Mohammad

    I am having the same issue. I am using Vagrant and running apache on it, I have Symfony3 project and I have the symfony4 on the same server. Symfony3 has no issue while Symfony4 is not working well.

    The error is from the server, the server throw 404 on all internal pages.I know this issue happen when your apache is not configured right and you forgot the to configure the Directory.., but it is different here bcz I have the apache conf correct.

    What is making me more crazy is that I am using PHPUnit testing and the test is able to reach the internal page url but when I use the browser it throw 404, also ajax calls throw 404.

  • 2018-02-08 Ramsey Jiang

    It can find it. I fix it though I don't know why it is suddenly fixed.

  • 2018-02-08 Diego Aguiar

    Oh boy, you ain't gonna believe it, just move up your method "news" right before "showAction". The problem is that Symfony returns the first route that matches the request, and since your route "@Route("/{id}"..." is defined first and has a wildcard, then your other route "@Route("/news/{slug}")" will never be found.

    I hope it makes sense to you :)
    Cheers!

  • 2018-02-08 Ramsey Jiang

    Hey Diego,

    I can hit other routes.

    For example, the following codes I can access them.
    /**
    * @Route("/")
    */
    public function indexAction()
    {
    return new Response('OMG! My first page already! Wooooo!');
    }

    /**
    * @Route("/{id}", requirements={"id" = "\d+"}, defaults={"id" = 1})
    */
    public function showAction($id)
    {
    return new Response('The number is '. $id);
    }

    But I cannot access the next one.

    /**
    * @Route("/news/{slug}")
    */
    public function news($slug)
    {
    return new Response(sprintf('Today new is "%s"', $slug));
    }

    I've already generated .htaccess, but it still not work. Whether I need to modify .htaccess a little bit?

  • 2018-02-08 Diego Aguiar

    Hey Ramsey Jiang

    Can you hit any other route? Let me see your routing configuration.
    One more thing, maybe your ".htaccess" file is not being read by Apache, you need to add this directive into your VirtualHost config "AllowOverride All"

    Cheers!

  • 2018-02-08 Ramsey Jiang

    Hey @weaverryan

    I did that before. Yes, it generate .htaccess automatically, but http://127.0.0.1:8000/index.php/news/test still not work. Should I change somthing in .htaccess file?

  • 2018-02-07 weaverryan

    Hey Ramsey Jiang!

    Just run "composer require symfony/apache-pack". That will install a package with a recipe that will add the public/.htaccess file for you :).

    Cheers!

  • 2018-02-07 locad5

    Hey Jiang,
    if you use http://127.0.0.1:8000/news/test you work with internal web server and not apache server, you should not need the .htaccess file. Look at the routing definition,
    Cheers

  • 2018-02-07 Ramsey Jiang

    Hi Victor,

    No route found for "GET /news/scas". I use an apache server. But I don't know how modify .htaccess for it. Can you help me please?

  • 2018-02-06 Victor Bocharsky

    Hey Ramsey,

    What error do you have? Is it a Symfony error? Can you try http://127.0.0.1:8000/index.php/news/test ? Does it work? Sometimes .htaccess should be slightly tweaked in different way on different servers.

    Cheers!

  • 2018-02-06 Ramsey Jiang

    Hey @locad5
    I met the same issue. I run composer symphony/apache-pack, and it generated .htaccess. But 127.0.0.1:8000/news/test is still not work. How do you change the file of .htaccess?

  • 2018-01-18 Victor Bocharsky

    Hey locad5 ,

    Take a look at makx21 answer, probably it will give you some clarification and maybe that's your case: https://knpuniversity.com/s... . It's also depends on what version of "apache-pack" you requires, it should be one of 1.0.x as I see from contrib repo: https://github.com/symfony/...

    Cheers!

  • 2018-01-18 Victor Bocharsky

    Hey makx21,

    Good point! Thanks for sharing it with us. You can set extra.symfony.allow-contrib to true manually or by following interactive questions during the Composer installation process.

    Cheers!

  • 2018-01-18 makx21

    This is a contributed recipe, you
    will need to allow flex to use them if you haven't previously.

    composer config extra.symfony.allow-contrib true

  • 2018-01-18 Diego Aguiar

    Hmm, interesting, I thought it was related to a composer's bug from a previous version, but you are using composer's latest version, so that's not the case.
    I wonder what caused that weird behaviour...

  • 2018-01-17 locad5

    Hey Diego Aguiar
    Composer version 1.6.2
    Mac OS High Sierra version 10.13.2

  • 2018-01-17 Diego Aguiar

    Hey locad5

    That's weird, can you tell me which composer version and which OS are you using?

  • 2018-01-17 locad5

    Hey weaverryan,
    Thank for your quick response, I already run composer symphony/apache-pack, but the .htaccess was not generated automatically. I put a basic .htaccess file inside /public folder and it works now.
    Thanks for your help
    Cheers!

  • 2018-01-17 weaverryan

    Hey locad5!

    I think you just may be missing the Apache rewrite rules that tell Apache to always use execute index.php file, regardless of the URL. Fortunately, Flex can add the .htaccess file you need automatically. Run composer require symfony/apache-pack.

    Let me know if that helps!

    Cheers!

  • 2018-01-17 locad5

    Hi,
    it works with http://localhost:8000/ and it's equivalent as http://localhost/advert/public for production apache environment, using PHP annotations or not.

    However, for any other routes like http://localhost/advert/public/goodbye, apache answers 'The requested URL /advert/public/goodbye was not found on this server.'

    Using this route:

    /**
    * @Route("/goodbye")
    */

    Do you have an idea ?
    Thanks

  • 2018-01-17 elbarto

    thanks for the answer :)

  • 2018-01-16 Diego Aguiar

    Hey @elbarto (or Bart Simpson :P)

    Doctrine devs never implemented it to work in both ways

    Cheers!

  • 2018-01-16 elbarto

    Hi,

    Shouldn't it work as well when we specify the route (using annotations) with simple quotes ?
    @Route('/' , name='homepage')

    because using simple quotes gives me an error : [Syntax Error] Expected PlainValue, got ''' at position 7 in method App\Controller\HomeController::homepage()

  • 2018-01-09 Diego Aguiar

    Hey Ivan Mauricio Rey

    This tutorial is still been released, i.e. only a few chapters has a video
    If you check in the chapter's list, it says "coming soon", but maybe we should make it more visible :)

    Cheers!

  • 2018-01-09 Ivan Mauricio Rey

    There is no video on this chapter?