Symfony: Keep it Simple with @Route and Templates

Symfony: Keep it Simple with @Route and Templates

Hey Symfony world. So you probably saw that Symfony released these best practices and I’ll admit I was partially responsible for these. But apart from that, I’m actually really really excited about them because they’re going to allow us reduce the complexity that we have in our application.

Note

The best practices are aimed at your non-shared application code. If you are building something that you need to share internally or to the world, you’ll want to do a little more work. See Best Practices for Reusable Bundles

Complexity Versus Simplicity Versus Easy

Last week at Symfony Live, I spent my entire presentation actually talking about complexity and how we can reduce it in our applications. The opposite of complexity is simplicity which means “easily understood”. Simplicity is already something that we already really want in our own code. We want to come back to our code in 6 months and say “this make sense to me” - not “what was I thinking here? I don’t remember, I need to dive in and figure out what I was thinking”.

I wanted to show a few of my favorite best practices that are going to reduce complexity. In particular a few things that are going to give us all less directories, less files and will make our projects a lot smaller and easier to navigate.

Our Project

I’m starting with a fresh Symfony 2.5 project because all of the changes we’re going to talk about are things that can be done in any version of Symfony. The project has an AppBundle in it. Now notice the interesting thing here is that we don’t have a vendor namespace:

src/
    AppBundle/
        AppBundle.php
        DataFixtures/
        Entity/

That’s normally the src/MyCompany/AppBundle. This just isn’t necessary in your own projects. You’re not going to collide with anybody else’s namespaces because all reusable third-party bundles do have a vendor namespace. So don’t put one in yours, it just makes things longer.

Other than that, we have a really simple Post entity:

// src/AppBundle/Entity/Post.php
// ...

/**
 * @ORM\Entity
 */
class Post
{
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(name="title", type="string", length=255)
     */
    private $title;

    /**
     * @ORM\Column(name="contents", type="text")
     */
    private $contents;

    // ...
}

I’ve added some data fixtures using Alice and Faker. I’ll talk about that another time, but that’s a really great way to do your fixtures. And the only other changes is in app/config/routing.yml. I already have a line that import annotations from my Controller/ directory”

# app/config/routing.yml
app_bundle_annotations:
    resource: "@AppBundle/Controller"
    type: annotation

This is likely a change that you’ll see out of the box in Symfony 2.6.

I don’t have that directory yet, so let’s go ahead and create it otherwise Symfony will throw an error at us:

mkdir src/AppBundle/Controller

I’ve already initialize the database and loaded my fixtures. I have my build-in web server already running so let’s go and try it out:

composer install
php app/console doctrine:database:create
php app/console doctrine:schema:create
php app/console doctrine:fixtures:load
php app/console server:run
http://localhost:8000

And there is our beautiful 404 page, because of course we don’t have a homepage yet.

Creating the Simplest Page Ever

So let’s go ahead and create a page. The first page I want to create is something that lists all posts. I’m using PHPStorm with the awesome Symfony2 plugin so I have that nice Symfony2 controller option there. But if you don’t, just create the controller by hand:

// src/AppBundle/Controller/PostController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class PostController extends Controller
{
}

The @Route Annotation

So instead of having an extra routing.yml file, we’re just going to use the @Route annotation and give it a path. Of course we need to remove the $name argument since we don’t have that in our route anymore and I’ll give it an inspirational die statement so we can make sure things are working:

// src/AppBundle/Controller/PostController.php
// ...

/**
 * @Route("/posts")
 */
public function indexAction()
{
    die('it works!');
}

Now, as many of you know, every time you have an annotation, you need to have a use statement for it. So I’ll let PHPStorm help me here and auto-complete that use statement. But you can also just go Google for SensioFrameworkExtraBundle, which is what gives us the @Route annotation. Scroll down a little bit and you’ll see all of the use statements you’ll need if you use this library:

// src/AppBundle/Controller/PostController.php
namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class PostController extends Controller
{
    // indexAction lives here ...
}

So we have the @Route annotation, we have our control method, so lets try going to /post and it works!

http://localhost:8000/posts

So no surprises there: the @Route annotation is all we need.

Simple Template Organization

So let’s finish this page. It should be fairly straightforward: we’re going to use Doctrine to query for all the posts and then pass them into a template:

/**
 * @Route("/posts")
 */
public function indexAction()
{
    $posts = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findAll();

    return $this->render('Post/index.html.twig', array(
        'posts' => $posts,
    ));
}

Now, notice that my template name does not have any colons in it. Normally we have this AppBundle:Post:index.html.twig thing. One of my favorite new best practices is to store your templates in the app/Resources/views directory. And when you do this, you don’t need any colons: you can just say Post/index.html.twig and it’s going to look for that in the app/Resources/views directory.

Note

I’m using an upper-case Post, but the best-practices say to use a lowercase post. I actually like the lowercase better, but choose whatever you want.

I’ll create a template and of course make it extend base.html.twig. And for the same reason here we don’t need the :: before. We can just say base.html.twig and it’s going to look in the app/Resources/views directory:

{# app/Resources/views/Post/index.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
<h1>POSTS!</h1>

<ul>
    {% for post in posts %}
        <li>
            {{ post.title }}
        </li>
    {% endfor %}
</ul>
{% endblock %}

Now this may seem like a small detail, but there’s 2 massive advantages to this. One, nobody liked or understood the colon syntax, especially beginners. I hated teaching it: every time I told them the AppBundle:Post:index.html.twig thing, it didn’t make sense to anybody. The second thing is that we no longer have our templates spread out across our project or our bundles. So if you have a frontend developer working with you, they can easily find those templates because they’re all sitting in one directory. A lot of times complexity is about perceived complexity: the more files and directories you have, the harder things are going to luck.

And no surprises, when we refresh, we have a working page. So one thing I want to highlight is that we only touched two files: our controller and our template.

Creating the Show Page

So now I want to create a page that is going to show just one blog post, and it’s going to be even easier. Just like before, we’ll start with @Route. The only difference here is that we’ll have the {id} wildcard. And as you already know we’re going to map that to an $id argument in our controller. And because I love die statements, we’ll try that just to test it out:

// src/AppBundle/Controller/PostController.php
// ...

/**
 * @Route("/posts/{id}")
 */
public function showAction($id)
{
    die('Mr Testers');
}

Add an id on the end of the URL and there’s our die statement:

http://localhost:8000/posts/5

The (In)Famous ParamConverter Trick

So now I want to show you one controversial trick. Normally if we have {id} in the URL, then we have a $id argument. But you can also change that argument if you type-hint it with Post, which is our entity. Then Doctrine is going to automatically query for that Post based on the {id} in the URL. And if it doesn’t find one, it’s going to throw a 404 page:

// src/AppBundle/Controller/PostController.php
// ...

/**
 * @Route("/posts/{id}")
 */
public function showAction(Post $post)
{
    var_dump($post);die;
}

And in this case, you can see it works perfectly. This comes from the ParamConverter of the SensioFrameworkExtraBundle and the only gotcha is that the name of your wildcard - so {id} for us - needs to match up with the property. So we have an {id} wildcard and we have an id property. If we change that to be {postId}, it’s not going to work because it doesn’t match our property name. Yes there are ways to configure the ParamConverter to figure this all out. But right now the configuration is actually really ugly, so I use this when it’s easy and if it’s not easy I just query myself. It’s not a big deal.

Let’s finish this up. We’ll render a template. Notice the controller is basically only one line, which is nice. And then we’ll create a template just to make sure that things are actually working. Print out the title, print out the contents and refresh to see some nice Latin on the screen:

{# app/Resources/views/Post/show.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
<h1>{{ post.title }}</h1>

<div>
    {{ post.contents }}
</div>
{% endblock %}

Route Names and Requirements

The other common thing that routes need are names. And actually right now, our routes do have a name. If we go over to router:debug, we’re going to see that Symfony has given an auto-generated names to each of our routes, which is fine, but I don’t exactly trust that:

php app/console router:debug

So the minute I actually need to link to one of these pages, I’m going to pass a name option to the @Route annotation to give it a specific name:

/**
 * @Route("/posts/{id}", name="post_show")
 */
public function showAction(Post $post)
{
    // ...
}

Once we’ve done that, linking to it is just like anything else: we got to Twig, we use the path() function, and everything is going to work perfectly:

{# app/Resources/views/Post/index.html.twig #}
{# ... #}

{% for post in posts %}
    <li>
        <a href="{{ path('post_show', { 'id': post.id }) }}">
            {{ post.title }}
        </a>
    </li>
{% endfor %}

Beyond the path and the name of the route, the only other common thing for routes is to add requirements. If you Google for @Route Symfony annotation, you’ll find the documentation page that shows you how to add those. It’s just another option on the @Route annotation:

/**
 * @Route("/posts/{id}", name="post_show", requirements={"id"="\d+"})
 */
public function showAction(Post $post)
{
    // ...
}

And since this is all we really do with routes, it doesn’t really get any messier than this.

Keep it Simple, Pass Along Feedback

And that’s really it. With the @Route annotation and putting all of your templates in the same directory, your project already starts to get a lot smaller. So keep things simple, try this out, and let me know what you think.

Seeya next time :).

Leave a comment!

  • 2016-06-24 weaverryan

    Hey Sudhir!

    Awesome! Congratulations :).

    I'm not too sure about the PHP templating - I've never used it before. But, we do show both PHP and Twig version in our templating docs on Symfony.com: http://symfony.com/doc/current.... Just look at the PHP tag of any of the code blocks and compare with the Twig tab. I don't know if something is missing from your code-block above, but in your base.html.php, the code doesn't look right at all - it's missing the

  • 2016-06-21 Sudhir Gupta

    hi weaverryan

    Thanks you for your all help. you really did a lot for me.
    I got success with FOSUserBundle (i will put GitHub link shortly for others users)

    Here i have another question.
    i wanted to use PHP template instead of twig.
    i did all setting such as

    in config.yml
    templating:
    engines: ['php']


    i make a base.html.php
    here is the code



    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>output('title', 'Hello Application') ?></title>
    output('css') ?>
    </head>
    <body>
    output('_content');
    ?>
    </body>
    </html>

    Now my main index.php page inside default have logical data which will override base.html.php.

    Here is the code-:

    <img src="https://s19.postimg.org/66j5lg5jn/image.png" border="0" alt="" target="_new"/>

    The problem is coming here in $view['slots']->start('_content')
    i want the every like which is written in block of _content should be appear on base page (as symfony documents says)
    unfortunately its not rendering. i am getting blank body.

    what is the correct way to override Body section. (i am assuming that any alternate of {% block body %}

    Thank you.

  • 2015-12-21 weaverryan

    Hey Daniel!

    In earlier Symfony days, we recommended that you create 1 bundle for every feature. Many tutorials (including on this site) still reflect this. But we realized that this can add extra over-organization that's not necessary: usually your bundles are all coupled together anyways. So, having multiple bundles isn't bad: especially if you really do have some big sections of your code that you'd like to logically organize (e.g. admin, frontend), but I almost always just have that one AppBundle. If you *are* building some piece that will be re-used among multiple Symfony projects, that's actually the perfect time to isolate things into a bundle.

    Cheers!

  • 2015-12-18 daniel

    Hi, so you are saying that is best to have all the stuff under appBundle? even for a more complex app cms, with user auth, admin, different sections... i saw other tutorials where do you have myfolderapp/bundle1...bundle2...

  • 2015-06-03 Steve

    Great idea Mike I will try this, thanks

  • 2015-06-03 weaverryan

    If you're going to use Assetic (still a fine tool), I think this is great! The big point is: don't overcomplicate. But since you have Assetic, and Assetic is plenty good at moving files around, putting them here works nice. Plus, you still have all of your files together :). This makes even more sense if you're using SASS or LESS, because it means that you won't have to expose your public files (I keep my assets out of web/ for that reason - they're processed through SASS).

    Cheers!

  • 2015-06-02 Michael Sypes

    This may be a dumb idea, but I did experiment with something like what you describe, and having my base template read thusly:

    {% stylesheets "%kernel.root_dir%/Resources/public/css/*" filter='cssrewrite' %}
    <link rel="stylesheet" href="{{ asset_url }}"/>
    {% endstylesheets %}

  • 2015-05-21 weaverryan

    Hmm, having some pre-reqs for any setup done before the screencast starts is indeed a good idea. I'll think about getting that added. Thanks!

  • 2015-05-21 Mike

    Thanks, that worked for me. It should be nice when you add this as note as sort of a preparation (pre requirements). Thanks!

  • 2015-05-21 weaverryan

    Nice work! And the issue you hit, by the way, was taken care of in Symfony 2.6 (this project is still built on 2.5).

    There are fixtures in this project (http://knpuniversity.com/scree... and you should be able to load them with:

    php app/console doctrine:fixtures:load

    And don't forget to create your database/schema first :)

    php app/console doctrine:database:create
    php app/console doctrine:schema:create

    Cheers!

  • 2015-05-21 Mike

    composer install gives:

    ------

    localhost:symfony-best-practices-start mikew$ composer install

    Loading composer repositories with package information

    Installing dependencies (including require-dev) from lock file

    Your requirements could not be resolved to an installable set of packages.

    Problem 1

    - Installation request for symfony/icu v1.2.2 -> satisfiable by symfony/icu[v1.2.2].

    - symfony/icu v1.2.2 requires ext-intl * -> the requested PHP extension intl is missing from your system.

    Problem 2

    - symfony/icu v1.2.2 requires ext-intl * -> the requested PHP extension intl is missing from your system.

    - symfony/symfony v2.5.5 requires symfony/icu ~1.0 -> satisfiable by symfony/icu[v1.2.2].

    - Installation request for symfony/symfony v2.5.5 -> satisfiable by symfony/symfony[v2.5.5].

    ------

    I solved this by doing a 'composer update'

    I think this does the job. However, what I am missing (I think) is a sql script for the mysql database.

  • 2015-05-20 weaverryan

    Hey Mike!

    Good news - easy fix :) - though I don't know what *caused* your issue exactly. That `bootstrap.php.cache` file is a little "bootstrap" file that's built after you run composer install. If you downloaded Symfony using the new installer, this should all be taken care of for you, but no worries either way. Here's the fix:

    1) Download Composer (if you don't already have it)
    2) Run `composer install` (or `php composer.phar install` if you have a .phar version).

    Then just try running the server again. Ping me if you have any other issues!

    Cheers!

  • 2015-05-20 Mike

    When I download the code and try to run (2.6) it using 'php app/console server:run', the I get:

    Warning: require_once(/Users/mike/development/environment/symfony-best-practices/start/app/bootstrap.php.cache): failed to open stream: No such file or directory in /Users/mike/development/environment/symfony-best-practices/start/app/console on line 10

    Fatal error: require_once(): Failed opening required '/Users/mike/development/environment/symfony-best-practices/start/app/bootstrap.php.cache' (include_path='.:') in /Users/mike/development/environment/symfony-best-practices/start/app/console on line 10

    localhost:start mike$ php app/console router:debug

    Warning: require_once(/Users/mike/development/environment/symfony-best-practices/start/app/bootstrap.php.cache): failed to open stream: No such file or directory in /Users/mike/development/environment/symfony-best-practices/start/app/console on line 10

    Fatal error: require_once(): Failed opening required '/Users/mike/development/environment/symfony-best-practices/start/app/bootstrap.php.cache' (include_path='.:') in /Users/mike/development/environment/symfony-best-practices/start/app/console on line 10

  • 2015-03-18 Kirill Smelov

    Always welcome. If i find out reason of first bag, first of all i share it here, so stay tuned!

  • 2015-03-18 weaverryan

    Hey again!

    1) If you ever have a chance to look into this (or try it in a MySQL project to see if it behaves differently), I'd love to know :).

    2) Ah, great news! I think that's a cool feature, and I hadn't thought about using it before. Glad you shared!

    Cheers!

  • 2015-03-18 Kirill Smelov

    Thanks for fast reply =)
    1) In my current project i use Postgres, honestly, i dont know, if reason of this bug hides in database. About requirements - i have been using yml for configuration, but in annotations example, for what i say, will be like this:
    * @Route("/blog/{page}" requirements={
    * "page": "\d+"
    * })

    2) Setting default value of entity in param to null prevents system exception. I have been using it all the time =)

  • 2015-03-18 weaverryan

    Hi Kirill!

    Thanks for adding some details :). Replies!

    1) I'm not aware of this - I often use the type-hinting trick (but I rarely/never actually add the @ParamConverter annotations) and I haven't seen this. Perhaps you have something special in your project? Or perhaps a different database backend (I usually use MySQL)? It could actually be a bug based on something that's slightly different in your project. I'd be interested to hear!

    2) You're right about this, but that's also the purpose of it. But are you saying that you *do* actually set a default value (e.g. Post $post = null) currently so that you can process it in the controller? Or are you saying that you *wish* you could do this? I'm actually not familiar if giving it a default value prevents the exception. If it doesn't, I think that's actually an interesting idea.

    Cheers!

  • 2015-03-17 Kirill Smelov

    Hi Ryan! First of all - really good article and video for begginers in Symfony2, but i want to clear situation about Type Hinting in action parameters. Yes, this is great way to remove some routine code from your method, but there is few hidden troubles in my opinion:

    1) If you provide as parameter non-integer value, it throw some exception. not 404 as we want(i belive so). So, we need to use requirements for our id paramter in route configuration like \d+

    2) If you provide non-existing id, it will throw an error and dont let us to process it anyway because of program even will not enter in our method. In this siituation i'd like to set default value of param entity to null and check this for null value in controller first of all. That allow me to process "Not-found case" in way, that i want.

  • 2015-02-28 Victor Bocharsky

    Thanks for clarify it! Looks like with cache it will be really useful

  • 2015-02-27 weaverryan

    Hey Bocharsky!

    Ah, I somehow missed this! Cool question :). Here are a few points

    1) I typically use render() when what I'm doing will result in a "chunk" of HTML. So, I'd use a Twig filter to get me a simple value, but render() to get me a "chunk" of HTML. That's simply because render() allows you to render another Twig template, so it's convenient in that way.

    2) BUT, the big warning with render() is that the sub-requests are expensive from a performance standpoint. But of course, that's in part by design: render() calls are cacheable. So, if the action I'm doing is heavy, then I lean towards using render() and caching it (instead of a Twig extension). But if what I'm doing is uncacheable or I would need many render()'s, then I might try to use Twig extensions. I want to maximize my cacheable render() calls (i.e. render_esi()) and minimize my uncacheable render() calls.

    3) You mentioned the issue wit the "requests" in sub-requests, and you're absolutely right of course :). But, there is a really cool way around this. For example, suppose you need the value of a query parameter - ?search=XX - in a sub-request controller. To accomplish this, pass in the value:


    {{ render(controller('AcmeArticleBundle:Article:searchform', {
    'search': app.request.query.get('search')
    })) }}

    With this, you can now have a `$search` argument to your controller. ALSO, if you cached this render() call, it would have a different cached version for each value of "search". In other words, when you pass variables like this, they become part of the cache key for the rendered fragment.

    Cheers!

  • 2015-02-26 Guest

    Hey Ryan! Can you speak something about it? :)

  • 2015-01-29 Victor Bocharsky

    Hi Ryan! Can you talk a bit about best practices with Embedding Controllers like {{ render(controller('AcmeArticleBundle:Article:recentArticles')) }}, for example. Does it a normally to use them or their usage should be avoid and better to use services in conjunction with twig extensions (functions)?

    P.S. A few months ago I had some problems with Embedding Controllers when use request object in it (As I understood, there is a sub-request, not a master request) so I switched to a services with twig functions and don't use it anymore.

  • 2015-01-15 Steve

    Thats great thanks Ryan.

  • 2015-01-14 weaverryan

    Hey Steve!

    If you've sym-linked app/Resources to, say, web/app, then use *that* directory in your path. For example, say you have your app/Resources/public/css/app.css, and this means you have a symlink'ed path of web/app/css/app.css. Use that path::

    {% stylesheets 'app/css/app.css' ... %}

    This is actually a little better, because there are problems using the "@" syntax with Assetic when you try to use relative links to images in your CSS (e.g. background: url('../images/...').

    I hope that helps!

  • 2015-01-13 Steve

    Thanks for the quick reply and what you say makes perfect sense especially as we're talking about simplicity.

    However I'm using assetic to manage my css etc. In my base.html.twig file I have the following

    {% block stylesheets %}

    {% stylesheets

    '@AppBundle/Resources/public/css/app.css'

    output='css/compiled/all.css' %}

    <link rel="stylesheet" href="{{ asset_url }}"/>

    {% endstylesheets %}

    {% endblock %}

    How would I change the "@AppBundle/Resources" so that all assets live in the app/Resources folder or the web root as suggested but still use assetic?

    Cheers

    Steve

  • 2015-01-13 Victor Bocharsky

    As Martin Fowler said: "There are only two hard things in Computer Science: cache invalidation and *naming* things." :) I hope JMSDiExtraBundle will be with more convenience syntax eventually )

  • 2015-01-13 weaverryan

    Yea, I *really* like how the phonecat tutorial is setup - I went through it and had the same impressions. I'll use that as some inspiration :).

    And for me, you hit the point perfectly with these changes: *less files*. For me, that's why the idea behind JMSDIExtraBundle is so interesting - it removes an extra file. I *have* certainly gotten feedback from people that use this and love it. In a perfect world, we'd have it, but simplify its syntax a bit.

    Cheers!

  • 2015-01-13 weaverryan

    Hi Steve!

    Cool question :). Since css/js really have nothing to do with your Symfony app, I recommend storing them in the web/ directory directly, in whatever organize you want (e.g. web/css, web/js, web/assets/css, etc). It's nice and simple and very easy for frontend people to understand :).

    If you *do* want to do something like app/Resources/public, you can certainly do that - just manually create a symbolic link from it to web/app (or something like that) and commit that symlink to your repository. Alternatively, you can use a system like Gulp to "build" your assets and put them into the web directory. I think we'll have a screencast on that soon.

    Cheers!

  • 2015-01-13 Steve

    Hi.

    As the use of a single bundle (AppBundle) is aiming at simplifying applications and making projects easier for frontend devs, is there a way to have assets (css & js) stored in the /app/Resources/public folder rather than the src/AppBundle/Resources/public folder.

    If this is possible then how is it done?

    Thanks

    Steve

  • 2015-01-12 Victor Bocharsky

    Now I understand, thanks ) I also enjoy the way like Angular phonecat tutorial provided ( https://github.com/angular/ang... ). Every step tagged with Git, and I can easy checkout project code between different steps, it's more convenient as for me, but maybe more complex for update.

    What about JMSDiExtraBundle, I didn't use it yet, only look narrowly. He seems to me a bit complicated at first glance, maybe with good IDE's autocomplete or good practice it will be fine. Now I use an yaml configuration for my services (I strongly don't like xml, need to write a lot of code and less readability). But service with yaml/xml config - it's already 2 files, not one like with annotation.

  • 2015-01-12 weaverryan

    Hey bocharsky25!

    I'll answer both of your questions at once :).

    About JMSDiExtraBundle, I'm not sure about it - in fact maybe you can share you experience. I've only tried it a few small times, but the syntax seemed very verbose. Specifically, the syntax needed for constructor injection, which is what I normally use: http://jmsyst.com/bundles/JMSD.... I wish I could basically say something like

    /**
    * @DI\Inject("em", id="doctrine.orm.entity_manager")
    */

    So for me, it's been something I like, but feels to clunky. What is your experience?

    About GitHub - it *is* updated... but not exactly as you'd expect. If you use the download button on this page, you'll get the start and finish version of this project (though at this time, that's only available if you buy the course). What you see on GitHub is the starting version of the project only. We use an internal system that builds the "steps" of the tutorial based on the diff files in the _tuts directory. That helps me keep track and update all the individual steps in a tutorial.

    Cheers!

  • 2015-01-09 Victor Bocharsky

    Also why did you don't commit latest changes to GitHub? There isn't PostController with actions and templates.

  • 2015-01-08 Victor Bocharsky

    I agree, using the @Route annotation is considerably reduces the complexity of app, especially for beginners, but I wonder what do you think about JMSDiExtraBundle for service declaration with dependency injection via annotation?