Lesson Tuesday

Now that we know a bit about REST, let's add the ability to update and destroy resources in our To Do app. We will start with updating via PATCH.

We need to create an update method for our Category class, which will update the attributes of a particular category. We will then be able to call this method in our app.php file. Let's start with a spec:

tests/CategoryTest.php
function testUpdate()
{
    //Arrange
    $name = "Work stuff";
    $test_category = new Category($name);
    $test_category->save();

    $new_name = "Home stuff";

    //Act
    $test_category->update($new_name);

    //Assert
    $this->assertEquals("Home stuff", $test_category->getName());
}

And here is the method to make this pass:

src/Category.php
function update($new_name)
{
    $executed = $GLOBALS['DB']->exec("UPDATE categories SET name = '{$new_name}' WHERE id = {$this->getId()};");
    if ($executed) {
       $this->setName($new_name);
       return true;
    } else {
       return false;
    }
}

We use the SQL UPDATE statement to update the categories table at the record where the primary key ID is the ID of the object we are calling the method on. We're not using our return value for this method in the test, but we still want to set up a branch in the same way as our save() method so that IF our query does not execute the local object does not update. Now that our test is passing, let's change the Silex files to work with our new method.

Here is the app.php file as we left it last:

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

    $app = new Silex\Application();

    $server = 'mysql:host=localhost:8889;dbname=to_do';
    $username = 'root';
    $password = 'root';
    $DB = new PDO($server, $username, $password);

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

    $app->get("/", function() use ($app) {
        return $app['twig']->render('index.html.twig', array('categories' => Category::getAll()));
    });

    $app->get("/tasks", function() use ($app) {
        return $app['twig']->render('tasks.html.twig', array('tasks' => Task::getAll()));
    });

    $app->post("/tasks", function() use ($app) {
        $description = $_POST['description'];
        $category_id = $_POST['category_id'];
        $task = new Task($description, $id = null, $category_id);
        $task->save();
        $category = Category::find($category_id);
        return $app['twig']->render('category.html.twig', array('category' => $category, 'tasks' => $category->getTasks()));
    });

    $app->get("/categories", function() use ($app) {
        return $app['twig']->render('categories.html.twig', array('categories' => Category::getAll()));
    });

    $app->get("/categories/{id}", function($id) use ($app) {
        $category = Category::find($id);
        return $app['twig']->render('category.html.twig', array('category' => $category, 'tasks' => $category->getTasks()));
    });

    $app->post("/categories", function() use ($app) {
        $category = new Category($_POST['name']);
        $category->save();
        return $app['twig']->render('categories.html.twig', array('categories' => Category::getAll()));
    });

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

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


    return $app;
?>

We will start by making a new route to a page where we can edit one particular category:

app/app.php
$app->get("/categories/{id}/edit", function($id) use ($app) {
    $category = Category::find($id);
    return $app['twig']->render('category_edit.html.twig', array('category' => $category));
});

Here we are passing in the ID of the category as part of the URL and setting it equal to the variable $id. Then we find the correct category object using our find method and pass it into a new view called category_edit.html.twig. First we need to add in a link to our category.html.twig page to access this new route.

views/category.html.twig
<p><a href="/categories/{{ category.getId }}/edit">Edit this category</a></p>

<p><a href='/'>Home</a></p>

Now we can make this new category_edit.html.twig page to be rendered when someone clicks on the edit link.

views/category_edit.html.twig
<h1>Update the {{ category.getName }} category </h1>

<form action="/categories/{{ category.getId }}" method="post">
  <input name="_method" type="hidden" value="patch">

  <label for="name">Rename your category:</label>
  <input id="name" name="name" type="text">

  <button type="submit">Update</button>
</form>

Since web browsers can only make GET and POST requests, we need some way to communicate to our app that even though the form is submitted with the POST method, the server should treat it like a PATCH request. That's what the line <input name="_method" type="hidden" value="patch"> is for. It's a common way for forms to communicate to the server to treat the request as a different method than the "real" one it's actually using.

Let's update our app.php file to contain this new route and to add in some new configurations to make the_method input work.

app/app.php
use Symfony\Component\HttpFoundation\Request;
Request::enableHttpMethodParameterOverride();

$app->get("/", function() use ($app) {
     return $app['twig']->render('index.twig', array('categories' => Category::getAll()));
});

…

$app->get("/categories/{id}/edit", function($id) use ($app) {
    $category = Category::find($id);
    return $app['twig']->render('category_edit.html.twig', array('category' => $category));
});

$app->patch("/categories/{id}", function($id) use ($app) {
    $name = $_POST['name'];
    $category = Category::find($id);
    $category->update($name);
    return $app['twig']->render('category.html.twig', array('category' => $category, 'tasks' => $category->getTasks()));
});

First we added at the top of the file use Symfony\Component\HttpFoundation\Request; and Request::enableHttpMethodParameterOverride(); to be able to use our _method input. Next we added in get("/categories/{id}/edit"... to show us the new edit page for the category and the route patch("/categories/{id}"... to handle the updating of our database. Now we can fire up the server and make sure that our changes work.

Let's tackle the DELETE method now. Luckily it is very similar to the PATCH method. We will start by writing a method to delete a particular category. One thing to think about before we get too far is that when we delete a category, we want to delete all of the tasks that are attached to it as well. So we will write two specs for this new method. The first:

tests/CategoryTest.php
function testDelete()
{
    //Arrange
    $name = "Work stuff";
    $test_category = new Category($name);
    $test_category->save();

    $name_2 = "Home stuff";
    $test_category_2 = new Category($name_2);
    $test_category_2->save();


    //Act
    $test_category->delete();

    //Assert
    $this->assertEquals([$test_category_2], Category::getAll());
}

And the method to make this pass:

src/Category.php
function delete()
{
    $executed = $GLOBALS['DB']->exec("DELETE FROM categories WHERE id = {$this->getId()};");
    if ($executed) {
       return true;
    } else {
       return false;
    }
}

And the spec to make sure it deletes from the tasks table as well:

tests/CategoryTest.php
function testDeleteCategoryTasks()
{
    //Arrange
    $name = "Work stuff";
    $test_category = new Category($name);
    $test_category->save();

    $description = "Build website";
    $category_id = $test_category->getId();
    $test_task = new Task($description, $category_id);
    $test_task->save();


    //Act
    $test_category->delete();

    //Assert
    $this->assertEquals([], Task::getAll());
}

And we will add in a line to the delete() method to make this pass:

src/Category.php
function delete()
{
    $executed = $GLOBALS['DB']->exec("DELETE FROM categories WHERE id = {$this->getId()};");
     if (!$executed) {
         return false;
     }
     $executed = $GLOBALS['DB']->exec("DELETE FROM tasks WHERE category_id = {$this->getId()};");
     if (!$executed) {
         return false;
     } else {
         return true;
     }
}

Let's run our tests and make sure these are passing. Then we can move on to updating our Silex app files. Since we don't need a separate page to delete an object, let's add in a button on the category_edit.html.twig file to allow a user to delete a particular category.

views/category_edit.html.twig
<h1>Update the {{ category.getName }} category </h1>

<form action="/categories/{{ category.getId }}" method="post">
  <input name="_method" type="hidden" value="patch">

  <label for="name">Rename your category:</label>
  <input id="name" name="name" type="text">

  <button type="submit">Update</button>
</form>

<form action="/categories/{{ category.getId }}" method="post">
  <input name="_method" type="hidden" value="delete">

  <button type="submit">Delete this category</button>
</form>

This form just contains a button, which will send a "faked" DELETE request to the route delete("/categories/{id}"... in our app.php file. Let's add in that new route now:

app.php
$app->delete("/categories/{id}", function($id) use ($app) {
    $category = Category::find($id);
    $category->delete();
    return $app['twig']->render('index.html.twig', array('categories' => Category::getAll()));
});

In this route, we find the correct category from the ID in the URL, call the delete() method on it, and then render index.html.twig. Because index.html.twig needs to see all of the categories, we need to include Category::getAll() in this route.

All right, we should be able to see these changes reflected in our browser.