Lesson Tuesday

Lesson goals:

  • Learn to create classes in Silex which can be saved
  • Learn about static methods

Let's build a To Do List app. We'll make a Task class, and make it only hold a description. Wouldn't it be nice if we could use a form to create the tasks one by one, and save the list of tasks as we go? Easily done in Silex.

Let's start by creating the class declaration for a Task. Then we'll instantiate a couple of Tasks and print their descriptions in the browser to test that our methods work correctly.

Create a ToDoList project folder with the Silex dependencies as we have in the previous lessons. It should include the app, web and src folders, with the same index.php file in the web folder. Make sure you run composer install from the top level of the project directory and start your server in the web folder. Then type this into a new file called Task.php, which should be saved in your src directory.

src/Task.php
<?php 
class Task
{
    private $description;

    function __construct($description)
    {
        $this->description = $description;    
    }

    function setDescription($new_description)
    {
        $this->description = (string) $new_description;
    }

    function getDescription()
    {
        return $this->description;
    }
}
?>

Our Task class has one private property called $description. We set it using the constructor.

Now make the skeleton app.php file in your app folder, including a require_once statement for your new Task class.

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

    $app = new Silex\Application();

    return $app;
?>

Let's create a route at the root URL /. To start, we'll hard-code 3 Task objects stored in an array called $list_of_tasks (later we'll allow users to create their own tasks). Put this into your app.php file right above the last line return $app; and after the $app = new Silex\Application();:

app/app.php
    $app->get("/", function() {
        $test_task = new Task("Learn PHP.");
        $another_test_task = new Task("Learn Drupal.");
        $third_task = new Task("Visit France.");

        $list_of_tasks = array($test_task, $another_test_task, $third_task);
        $output = "";

        foreach ($list_of_tasks as $task) {
            $output = $output . "<p>" . $task->getDescription() . "</p>";
        }

        return $output;
    });

When you go to http://localhost:8000 in the browser it should say:

Learn PHP.

Learn Drupal.

Visit France.

Now we need a place to store tasks so that we don't need to instantiate them manually as we did above. We're going to do that by storing the objects in cookies on the users' browser. To access the users' cookies, we use a built-in PHP variable called $_SESSION. Place the following code after the require_once lines and before the $app = new Silex\Application(); in the app.php file:

app/app.php
session_start();
if (empty($_SESSION['list_of_tasks'])) {
    $_SESSION['list_of_tasks'] = array();
}

The first line starts our session. The next line then checks it for the key 'list_of_tasks' using the empty function, which returns true if there is no value stored in the variable. If the key doesn't exist, we create it and set it to an empty array.

$_SESSION is a superglobal variable like $_GET. Remember, superglobal means we can access it from anywhere in our code.

We’re going to store an array of all our Tasks inside of $_SESSION as a value at the key 'list_of_tasks'. We want to store a Task by calling a save method on it. Let's write that next:

src/Task.php
function save()
{
    array_push($_SESSION['list_of_tasks'], $this);
}

Now your Task class should look like this:

src/Task.php
class Task
{
    private $description;

    function __construct($description)
    {
        $this->description = $description;  
    }

    function setDescription($new_description)
    {
        $this->description = (string) $new_description;
    }

    function getDescription()
    {
        return $this->description;
    }

    function save()
    {
        array_push($_SESSION['list_of_tasks'], $this);
    }
}

Now let's convert the rest of our code from using the hard-coded tasks to using the session. First, go into the / route in the app.php file and delete these lines:

app/app.php
$test_task = new Task("Learn PHP.");
$another_test_task = new Task("Learn Drupal.");
$third_task = new Task("Visit France.");

$list_of_tasks = array($test_task, $another_test_task, $third_task);

Now, we need some way to loop through all of our saved tasks in $_SESSION['list_of_tasks']. To do this we will write a special kind of method called a static method. It's a getter, but it works on the whole class: its job will be to return the list of all our tasks. Add this method to the end of your class declaration in Task.php:

src/Task.php
static function getAll()
{
    return $_SESSION['list_of_tasks'];
}

We create a static method in the same way that we declare a normal method, except that we put the keyword static before the keyword function.

And we will call this method in our foreach loop in the / route to give us an array of all $tasks to loop through and display. So in app.php, instead of this:

app/app.php
foreach ($list_of_tasks as $task) {
    $output = $output . "<p>" . $task->getDescription() . "</p>";
}

We want this:

app/app.php
foreach (Task::getAll() as $task) {
    $output = $output . "<p>" . $task->getDescription() . "</p>";
}

All the methods we've written in previous lessons deal with a single instance of a class: for example, finding the area of a particular rectangle, or telling if it is a square or not. But now we are working with multiple instances of the Task class.

Static methods get called on the class itself (here, on Task), rather than on one instance. They're useful when you want to do something that involves more than one instance, or to create some functionality that has to do with the class rather than any individual instance.

New programmers often get confused about when to write a static method rather than a regular method, also called an instance method (because it acts on a particular instance).

Here's an analogy I've found to be helpful. You can think of classes like a car factory, and instances like the cars that the factory makes. Want to make a new car? That's the factory's job, so it should be a static method. Want to know what color a car is? That's about a particular car, so it should be an instance method. Want to know how many cars have been made? The factory creates the cars, so it knows how many have been made; any individual car doesn't know how many other cars have been made. Want to know how many green cars were made? Again, the factory knows about all of the cars, so it knows how many green cars were made. Want to change, buy or destroy a car? That's about a particular car, so it should be an instance method.

Now let's add in a form to create new tasks when we press the submit button. This is where HTTP POST requests come in. Make your '/' route look like this:

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

    $output = "";

    foreach (Task::getAll() as $task) {
        $output = $output . "<p>" . $task->getDescription() . "</p>";
    }

    $output = $output . "
        <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>
    ";

    return $output;
});

We loop through any tasks stored in the session and print their descriptions. Then we display the form which will create a new task when submitted. The form action is set to send the request to the URL at '/tasks', and the method is set to POST because we are creating a task. So let's add the route for this URL to our app.php file. This should go right above the last line that says return $app;

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>
        ";
    });

We instantiate a new task when the submit button is pressed by getting the user's description out of the superglobal $_POST. We used the superglobal $_GET to read data out of our earlier forms, because forms send GET requests by default. Since we set our form to use the POST method here: <form action='/' method='post'>, we will read data out of it using the $_POST variable.

$_POSTand $_GETare both associative arrays, and our task's description is stored under the key 'description', which was set by the name attribute in the form's input tag here <input id='description' name='description' type='text'>, just like in a GET request.

After instantiating the Task we save it and then return some text with the task description and a link to go back to the list of all tasks in our root path.

Lastly let's clean our root path a little bit by wrapping our task display text in an if statement using the empty function again. We only want to list tasks if some exist. We are also going to create a new variable called $all_tasks and set it equal to the output of the static method Task::getAll(). This way, we only need to call the getAll() method once, and then we can use the variable $all_tasks to first check and see if tasks exist, and then if they do, we can loop through them and print out their descriptions. Now your entire route should look like this:

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

        $output = "";

        $all_tasks = Task::getAll();

        if (!empty($all_tasks)) {
            $output = $output . "
                <h1>To Do List</h1>
                <p>Here are all your tasks:</p>
                ";

            foreach ($all_tasks as $task) {
                $output = $output . "<p>" . $task->getDescription() . "</p>";
            }

        }

        $output = $output . "
            <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>
        ";

        return $output;
    });

Before we display any tasks, we start with an empty $output string. Then we call the static method Task::getAll();. If it returns an empty array, there are no tasks to display yet. If it is not empty, then there are tasks to display, so inside of our if statement we loop through them and display the description for each one by adding it to the $output string.

Then we add the HTML for the form to create a new Task to our $output string and return it. Now we can create and save objects by submitting a post request from a form.

Also, let's clean up one more thing before moving on. We will be learning a better way to display HTML soon, but until then this trick will make your life easier. Instead of constructing our $output strings like this:

$output = $output . "more text";

We can use this shorthand to do the same thing.

$output .= "more text";

Just put the . (concatenation operator) to the left of an equals sign. That means add the string "more text" to whatever is already stored in $output.

Let's add one more thing to our to do list. Right now, all we can do is add items to it, let's add a button to clear all of the tasks in our list. We will add the HTML for this to the end of the $output string in our root path route. Put this right above the last line where we say return $output;:

app/app.php
$output .= "
    <form action='/delete_tasks' method='post'>
        <button type='submit'>delete</button>
    </form>
";

Underneath the task form we now have a separate form with only a submit button inside of it. When the user clicks this button the form submits a post request to the URL /delete_tasks. Let's create that route next. Add this to the bottom of your app.php file right above the line that says return $app.

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

    Task::deleteAll();

    return "
        <h1>List Cleared!</h1>
        <p><a href='/'>Home</a></p>
    ";
});

When this route is called, we call a static method on the class Task called deleteAll. Then we just return some confirmation text and a link back to the root path.

Now all we have to do is declare that deleteAll method. It makes sense for it to be a static method because clearing out all stored instances of an object is not a job for one particular object. Using our metaphor, we are asking the Car factory to basically set its inventory on fire - so that's a factory job, not a job for a particular car. Add this method to the bottom of your Task.php file.

src/Task.php
static function deleteAll()
{
    $_SESSION['list_of_tasks'] = array();
}

When this method is called, we just reset $_SESSION['list_of_tasks'] to a blank array.

Now your app.php file should look like this:

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->get("/", function() {

        $output = "";

        $all_tasks = Task::getAll();

        if (!empty($all_tasks)) {        
            $output .= "
                <h1>To Do List</h1>
                <p>Here are all your tasks:</p>
            ";

            foreach ($all_tasks as $task) {
                $output .= "<p>" . $task->getDescription() . "</p>";
            }
        }

        $output .= "
            <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>
        ";

        $output .= "
            <form action='/delete_tasks' method='post'>
                <button type='submit'>Clear</button>
            </form>
        ";


        return $output;
    });

    $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>
        ";
    });

    $app->post("/delete_tasks", function() {

        Task::deleteAll();

        return "
            <h1>List cleared!</h1>
            <p><a href='/'>Home</a></p>
        ";
    });


    return $app;
?>

And your Task.php file should look like this:

src/Task.php
<?php 
class Task
{
    private $description;

    function __construct($description)
    {
        $this->description = $description;  
    }

    function setDescription($new_description)
    {
        $this->description = (string) $new_description;
    }

    function getDescription()
    {
        return $this->description;
    }

    function save()
    {
        array_push($_SESSION['list_of_tasks'], $this);
    }

    static function getAll()
    {
        return $_SESSION['list_of_tasks'];
    }

    static function deleteAll()
    {
        $_SESSION['list_of_tasks'] = array();
    }
}
?>

To save data in the cookies on a users' browser we use a built-in PHP superglobal variable called $_SESSION. To access it we must add these lines at the beginning of our app.php file:

app/app.php
session_start();
if (empty($_SESSION['list_of_tasks'])) {
    $_SESSION['list_of_tasks'] = array();
}

Then create a save method in your class:

src/Task.php
function save()
{
    array_push($_SESSION['list_of_tasks'], $this);
}

You'll also need a method to retrieve all instances of a class that you have saved. Since this deals with more than one instance, you should make it a static method. To declare a static method, just put the keyword static before the keyword function.

src/Task.php
static function getAll()
{
    return $_SESSION['list_of_tasks'];
}

Call a static method by writing the name of the class, followed by 2 colons, and then the method name, followed by a pair of parentheses.

Task::getAll() 

You'll also want a static method to delete all the things you have saved. That looks like this:

src/Task.php
static function deleteAll()
{
    $_SESSION['list_of_tasks'] = array();
}

POST requests

Remember, forms send GET requests by default. This is fine if you are submitting a search query or viewing a list of objects. But if you are modifying something by saving or deleting objects, your form should be sending a POST request. For example, here are 2 forms sending POST requests on submit:

<form action='/delete_tasks' method='post'>
<form action='/tasks' method='post'>

These forms will call these two routes:

$app->post("/delete_tasks", function() { ... }
$app->post("/tasks", function() { ... }