Lesson Tuesday

When you write your Twig templates, they should have the extension .html.twig rather than .php. So you would render create_tasks.html.twig rather than create_tasks.php.

When you write your Twig templates, they should have the extension .html.twig rather than .php. So you would render create_tasks.html.twig rather than create_tasks.php.

Lesson goals:

  • Learn to create and render twig template files
  • Learn to write if statements and foreach loops
  • Learn to pass in variables and call their methods inside of template files

Writing out all of our HTML inside of our routes is going to get very messy and annoying very quickly. Instead let's learn how to create template files using a templating engine called Twig. A template file handles displaying information inside of HTML, even when we don't know exactly what that information is going to be beforehand. Templates allow us to be a lot more organized too. All we have to worry about in our routes is manipulating the data, and then our templates will be separate files to show the results.

Twig is a dependency like Silex, so we have to add it to our composer.json file - it should look like this:

composer.json
{
    "require": {
        "silex/silex": "~1.1",
        "twig/twig":"~1.0"
    }
}

Now we have to run composer again to actually get the files that Twig needs. In the terminal, change directory to the top level of your project folder and run this command:

composer update

We already ran composer install, so since we changed something in our file instead we want to run the command update. Your terminal output should look something like this:

Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing twig/twig (v1.18.0)
    Loading from cache

Writing lock file
Generating autoload files

Now we need to tell our project that those Twig files exist. Add these lines to the top of app.php, right after this line: $app = new Silex\Application();

app/app.php
$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__.'/../views'
));

Next we are going to work on moving all of the HTML from our root path route into a template file. We tell our routes to render a template file by replacing this line:

app/app.php
...
return $output;
...

With this one:

app/app.php
return $app['twig']->render('tasks.html.twig');

This tells the $app object to use Twig to render a file called tasks.html.twig. This will be our template file, and it will display the list of our tasks along with the forms to create more tasks and clear the list.

But first, we need to give our route access to the $app variable. So instead of this:

app/app.php
$app->get("/", function() {

We write this:

app/app.php
$app->get("/", function() use ($app) {

We don't need to change anything else in our route yet. All we want it to do is correctly open a template file. We told Twig to look for our template files in a folder called views when we wrote this line: 'twig.path' => __DIR__.'/../views'. So let's create that file, tasks.html.twig, and put it in a new folder called views in your project folder. Then we'll just put some very simple HTML into it to start:

views/tasks.html.twig
<html>
<head>
    <title>To Do List</title>
</head>
<body>
    <h1>To Do List</h1>
</body>
</html>

Next, since we want this template file to list our tasks, we will need to tell it what those tasks are. We don't want to have to call any methods inside of the template file if we can help it. Remember, wherever possible, the template file should be as simple as possible and just handle displaying information. The route in app.php is where we should handle putting that information together and making sure it's in the correct form (an array, or a string etc). So we will pass an array of all tasks into the template by changing this line:

app/app.php
return $app['twig']->render('tasks.html.twig');

To this line:

app/app.php
return $app['twig']->render('tasks.html.twig', array('tasks' => Task::getAll()));

The return value from our getAll method is assigned to a variable called tasks which will now be available to us inside the template file. Now let's move the display logic from our route into the template file piece by piece.

The first thing we do in our route is say "If there are any tasks in our list, display them." Before we deal with how to loop through the tasks, let's see how we can make that if statement in a way that Twig understands. We need to look inside the variable tasks that we passed into the template and say "if it isn't empty, do stuff". We'll just test our if statement by declaring the <p>Here are all your tasks:</p> text if there are tasks. To do that, add these lines to your template:

views/tasks.html.twig
{% if tasks is not empty %}
    <p>Here are all your tasks:</p>
{% endif %}

Isn't that pretty? To check if a variable is or is not empty in Twig we literally write if my_variable is not empty. No need for $ on the variable, no parenthesis on empty. We just have to enclose logical statements like if in {% %} so that Twig knows it isn't HTML. Everything that isn't enclosed in special characters like this in Twig is treated as HTML. The last line {% endif %} just marks the end of the if statement. This code in Twig:

{% if condition %} 
    //stuff happens 
{% endif %}

is equivalent to this in regular PHP:

if (condition) {
    //stuff happens
}

Now we need to loop through our tasks inside of the if statement. This is where we get to use the tasks variable that we passed into our template by saying this:

app/app.php
return $app['twig']->render('tasks.html.twig', array('tasks' => Task::getAll()));

tasks holds an array of all our task objects, as returned by our static getAll method. So let's build a loop for this array in our template and add it inside of the {% if tasks is not empty %} if statement:

views/tasks.html.twig
<ul>
    {% for task in tasks %}
        <li>{{ task.getDescription }}</li>
    {% endfor %}
</ul>

As long as we're within the {% %} characters, we can refer to variables passed into Twig by just typing them like tasks - again, no $ or anything needed. We say "for each thing the variable tasks, put it into a new variable called task that we can use in our loop".

Each time we run through the loop, we use another pair of special characters {{ }} to print the task's description inside of a pair of <li> tags. These {{ }} characters are used to print the contents of a variable to the browser, or the result of a method call. In our case we are calling the getDescription method on each task and printing the method's return value. And when we are within Twig we call a method by using . instead of the object operator ->. Note that Twig also gives us the convenience of not needing to add parentheses at the end of a method call.

Last of all, we paste the HTML for our "Add task" and "Clear" forms in without alteration. Your whole template file should now look like this:

views/tasks.html.twig
<html>
<head>
    <title>To Do List</title>
</head>
<body>
    <h1>To Do List</h1>

    {% if tasks is not empty %}
        <p>Here are all your tasks:</p>
        <ul>
            {% for task in tasks %}
                <li>{{ task.getDescription }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action='/tasks' method='post'>
        <label for='description'>Task Description</label>
        <input id='description' name='description' type='text'>

        <button type='submit'>Add task</button>
    </form>
    <form action='/delete_tasks' method='post'>
        <button type='submit'>Clear</button>
    </form>
</body>
</html>

And to try it out, we can delete pretty much everything in our root path route. It should now look like this:

app/app.php
$app->get("/", function() use ($app) {

    return $app['twig']->render('tasks.html.twig', array('tasks' => Task::getAll()));

});

The only thing that our route needs to do is get the data that our template needs and pass it in by rendering the template. And the only thing the template has to worry about is displaying data in a readable way. Much cleaner!

Let's create templates for our two other routes. First we'll make one for the POST request to /tasks. Remember, this request is sent when you create a new task and hit the submit button. Inside of the views folder, make a file called create_task.html.twig. We can copy over the page's <h1> tag from the route.

views/create_task.html.twig
<html>
<head>
    <title>To Do List</title>
</head>
<body>
    <h1>You created a task!</h1>
</body>
</html>

Now to call this from our route, we need to change our return statement to render our template. And to do that, we need to also pass the $app variable into the route using the keyword use. So change your route in app.php from this:

app/app.php
$app->post("/tasks", function() {
    $task = new Task($_POST['description']);            
    $task->save();
    return "
        <h1>You created a task!</h1>
        <p>" . $task->getDescription() . "</p>
        <p><a href='/'>View your list of things to do.</a></p>
    ";
});

To this:

app/app.php
$app->post("/tasks", function() use ($app) {
    $task = new Task($_POST['description']);            
    $task->save();
    return $app['twig']->render('create_task.html.twig');
});

We still need to create a new Task object inside of this route using the description from the form, and then save it. Then we render our template. We can test this out in a browser now and when we create a task it'll take us to our new template and display <h1>You created a task!</h1>.

If we want to display the description of our new task, we need to pass the Task object into our template and give it a variable name. Let's call it newtask. Change your return statement to look like this:

app/app.php
return $app['twig']->render('create_task.html.twig', array('newtask' => $task));

Then we can print it in our template by adding this line:

views/create_task.html.twig
<p>{{ newtask.getDescription }}</p>

And we can paste our link back to the root path into our template without alteration. Your create_task.html.twig file should now look like this:

views/create_task.html.twig
<html>
<head>
    <title>To Do List</title>
</head>
<body>
    <h1>You created a task!</h1>
    <p>{{ newtask.getDescription }}</p>
    <p><a href='/'>View your list of things to do.</a></p>
</body>
</html>

And the final version of the route it goes with is:

app/app.php
$app->post("/tasks", function() use ($app) {
    $task = new Task($_POST['description']);            
    $task->save();
    return $app['twig']->render('create_task.html.twig', array('newtask' => $task));
});

Last of all, let's make our /delete_tasks route call a template file. Create the file delete_tasks.html.twig in your views folder. We can just copy the HTML directly out of our return statement, no need for variables. So it should look like this:

views/delete_tasks.html.twig
<html>
<head>
    <title>To Do List</title>
</head>
<body>
    <h1>List cleared!</h1>
    <p><a href='/'>Home</a></p>
</body>
</html>

And in its place we want to return our rendered Twig template. So our /delete_tasks route should look like this.

app/app.php
$app->post("/delete_tasks", function() use ($app) {
    Task::deleteAll();
    return $app['twig']->render('delete_tasks.html.twig');
});

We still need to call the static deleteAll method in our route, not in our template. Remember, ideally templates should only display information. Now our app.php file is a lot cleaner:

app/app.php
<?php
    require_once __DIR__."/../vendor/autoload.php";
    require_once __DIR__."/../src/Task.php";

    session_start();

    if (empty($_SESSION['list_of_tasks'])) {
        $_SESSION['list_of_tasks'] = array();
    }

    $app = new Silex\Application();

    $app->register(new Silex\Provider\TwigServiceProvider(), array(
        'twig.path' => __DIR__.'/../views'
    ));

    $app->get("/", function() use ($app) {

        return $app['twig']->render('tasks.html.twig', array('tasks' => Task::getAll()));

    });

    $app->post("/tasks", function() use ($app) {
        $task = new Task($_POST['description']);            
        $task->save();
        return $app['twig']->render('create_task.html.twig', array('newtask' => $task));
    });

    $app->post("/delete_tasks", function() use ($app) {
        Task::deleteAll();
        return $app['twig']->render('delete_tasks.html.twig');
    });

    return $app;
?>

Within our template files we have a lot less syntax - there are just 3 new symbols to worry about.

  • . replaces the object operator when we have to call methods.
  • {% %} used to execute statements like if statements and foreach loops.
  • {{ }} used to print the contents of variables or the result of calling a method.

And we must remember, however strange, that variables inside of Twig are now just printed without $, and methods are called without parentheses. Hooray.

Adding a template file to a route

  • Add Twig to our composer.json file:
composer.json
{
    "require": {
        "silex/silex": "~1.1",
        "twig/twig":"~1.0"
    }
}
  • Install it by running:
composer update
  • Create views folder in the top level of your project folder.
  • Add Twig to your project and tell it to look for your templates in your views folder.
app/app.php
$app->register(new Silex\Provider\TwigServiceProvider(), array(
    'twig.path' => __DIR__.'/../views'
));
  • Tell each route to render its template file by replacing this line:
app/app.php
...
return $output;
...

With this one:

app/app.php
return $app['twig']->render('tasks.html.twig');
  • To pass a variable into a Twig template, call the twig render method like this:
...
    return $app['twig']->render('tasks.html.twig', array('tasks' => Task::getAll()));
...

Within our template files we have a lot less syntax - there are just 3 new symbols to worry about.

  • . replaces the object operator when we do have to call methods.
  • {% %} used to execute statements like if statements and foreach loops.
  • {{ }} used to print the contents of variables or the result of calling a method.

And we must remember, however strange, that variables inside of Twig are now just printed without $, and methods are called without parentheses. Hooray.