zen of coding

CakePHP 3 … the app is mature (step 6 — Controller, JSON testing and a little more…)

(Get the app code on github.)

Ah, after a few lengthy days of work, we have arrived at the conclusion of our development efforts.

In the last post I have done a little recap of the things accomplished so far.

And here, as promised, I am going to piece everything together.
What we haven’t seen so far is the Controller of the application, which is responsible for taking the requests from jQuery and passing them on to your Model layer.

One thing you’ve probably noticed is that I have not talked about any “views” in our application. (Other than index.ctp, which we’ve prepped back here).

For this application we don’t need to create any view files like we typically would, if our response was a traditional HTML page to the user. In this case, the Controller only responds as JSON, and for that purpose CakePHP 3 provides a special View object for us.

If you recall, we had some interesting “request” URL’s to our server (i.e. /todos/get.json). The .json extension tells our controller that we are requesting a JSON response (vs the default HTML, for example). To ensure that our application “understands” the meaning of this extension, it is important to enable  Router::extensions(['json']); in config/routes.php.

Now, let’s take a look at the src/Controller/TodosController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<?php
namespace App\Controller;

use Cake\Core\Configure;
use Cake\ORM\TableRegistry;

class TodosController extends AppController {

/**
 * initialize method
 *
 * @return void
 */

  public function initialize() {
    parent::initialize();
    $this->loadComponent('RequestHandler');
  }

/**
 * main action for the application
 *
 * @return void
 */

  public function index() {
    //this method is intentionally left blank
  }

/**
 * add() action to create a new to-do
 *
 * @return void
 */

  public function add() {
    $response = ['result' => 'fail'];
    $errors = $this->Todos->validator()->errors($this->request->data);
    if (empty($errors)) {
      $todo = $this->Todos->newEntity($this->request->data);
      if ($this->Todos->save($todo)) {
        $response = ['result' => 'success'];
      }
    } else {
      $response['error'] = $errors;
    }
    $this->set(compact('response'));
    $this->set('_serialize', ['response']);
  }

/**
 * gets either done or incomplete to-do's depending on the status
 *
 * @param int $status 0/1 incomplete/complete
 * @return void
 */

  public function get($status = 0) {
    $todos = $this->Todos->find('recent', ['status' => $status]);
    $this->set(compact('todos'));
    $this->set('_serialize', ['todos']);
  }

/**
 * marks the to-do as complete, i.e. changes is_done to 1
 *
 * @param int $todoId id of the record to mark as done
 * @return void
 */

  public function finish($todoId = null) {
    $response = ['result' => 'fail'];
    if (!is_null($todoId)) {
      $todos = TableRegistry::get('Todos');
      $todo = $todos->get($todoId);
      $todos->patchEntity($todo, ['is_done' => 1]);
      if ($todos->save($todo)) {
        $response = ['result' => 'success'];
      }
    }
    $this->set(compact('response'));
    $this->set('_serialize', ['response']);
  }
}

Most of the magic begins with the RequestHandler component. By adding it to the application in the initialize() method, we make our Controller much more powerful because now it can accept AJAX requests and easily respond with JSON.
You can quickly examine add(), get() and finish() actions to spot $this->set('_serialize', $something);, which essentially creates a JSON (or JSONP) response from a view variable populated by $this->set().

Specifically if you look at the get() action, you’ll probably recall our test for findRecent() in the previous post. Here you can see how we can call this method from the controller:

$todos = $this->Todos->find('recent', ['status' => $status]);

Additionally, because we’ve already took care of data sanitization and formatting in our “Todo” Model, we can pretty safely return the data back to the browser.
Eventually, depending on what the server sends back to the front-end our jQuery script will “decide” the outcome of how the the UI is affected.
In one case we’ll show an error message, in another we will update the list of the complete tasks or perhaps mark a task as complete and make it disappear from your “to-do” list and appear on your “recently done” list.
To get a little glimpse of that, you can look in the add() action.
There, for example, I am getting the validation errors from the Model by using $this->Todos->validator()->errors($this->request->data); If the validation fails our “to-do” cannot be saved, and the error is returned to the user (lines 44, 45); otherwise we proceed with the save() and return “success”.

As I did for the Table object, it is important to provide unit tests for the Controller:

srs/tests/TestCase/Controller/TodosControllerTest.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<?php
namespace App\Test\TestCase\Controller;

use Cake\I18n\Time;
use Cake\Routing\Router;
use Cake\TestSuite\IntegrationTestCase;

class TotosControllerTest extends IntegrationTestCase {

/**
 * fixtures
 *
 * @var Fixture
 */

  public $fixtures = ['app.todos'];

/**
 * test add() method
 *
 * @return void
 */

  public function testAdd() {
    $this->configRequest([
      'headers' => ['Accept' => 'application/json']
    ]);

    $this->post(Router::url(
      ['controller' => 'todos',
        'action' => 'add',
        '_ext' => 'json'
      ]),
    ['todo' => 'run test']);

    $this->assertResponseOk();
    $expected = [
      'response' => ['result' => 'success'],
    ];
    $expected = json_encode($expected, JSON_PRETTY_PRINT);
    $this->assertEquals($expected, $this->_response->body());
  }

/**
 * test get() method
 *
 * @return void
 */

  public function testGet() {
    $this->configRequest([
      'headers' => [
        'Accept' => 'application/json'
      ]
    ]);

    $this->post(Router::url(
      ['controller' => 'todos',
        'action' => 'get',
        '_ext' => 'json'
      ])
    );
    $this->assertResponseOk();

    $expected = [
      'todos' =>
        [
          [
            'id' => 1,
            'todo' => 'First To-do',
            'created' => 'on 11/21/13',
            'updated' => 'on 11/21/13',
            'is_done' => false
          ]
        ],
    ];
    $expected = json_encode($expected, JSON_PRETTY_PRINT);
    $this->assertEquals($expected, $this->_response->body());

    // get completed to-do's
    $this->post(Router::url(
      ['controller' => 'todos',
        'action' => 'get',
        '_ext' => 'json',
        1
      ])
    );
    $this->assertResponseOk();

    $expected = [
      'todos' => [
        [
          'id' => 2,
          'todo' => 'Complete To-do',
          'created' => 'on 11/21/13',
          'updated' => 'on 11/21/13',
          'is_done' => true

        ]
      ]
    ];
    $expected = json_encode($expected, JSON_PRETTY_PRINT);
    $this->assertEquals($expected, $this->_response->body());
  }

/**
 * test fnish() method
 *
 * @return void
 */

  public function testFinish() {
    $this->configRequest([
      'headers' => [
        'Accept' => 'application/json'
      ]
    ]);

    $this->get(Router::url(
      ['controller' => 'todos',
        'action' => 'finish',
        '_ext' => 'json',
        2
      ])
    );
    $this->assertResponseOk();
    $expected = [
      'response' => ['result' => 'success'],
    ];
    $expected = json_encode($expected, JSON_PRETTY_PRINT);
    $this->assertEquals($expected, $this->_response->body());

    // get completed to-do's
    $this->post(Router::url(
      ['controller' => 'todos',
        'action' => 'get',
        '_ext' => 'json',
        1
      ])
    );
    $this->assertResponseOk();
    $this->assertResponseContains('"is_done": true', $this->_response->body());
  }

}

The test builds on top of what we’ve done in the last post. Although I now know that the “get” and “save” operations are working well from the data (or Model) layer perspective, Controller testing helps to ensure that front-end response is also up to par.
As such, I can test the validity of the response when certain action is requested, the format (i.e. JSON) and finally, by using the fixture data, I can assert some expectations of the actual returned data.
I won’t go into the details of the code here, because testing is described very well in the CakePHP cookbook, and the code, IMO, is relatively easy to read.

Now we have all the pieces put together:

  • UI including CSS and mark-up
  • jQuery for the front-end and user experience logic as well as dispatching of events
  • Model layer in CakePHP to handle database interaction
  • And finally the controller to dispatch requests and responses

One last thing before we are ready to launch the app…
If you currently went to the URL root of your application at http://local.todo/ (for example), you would see a default CakePHP homepage rather than the application. The simple way to ensure that the user is always redirected to the proper destination, which would actually be http://todo.local/todos/index/ (or just /todos/), is to edit our routes file.

config/routes.php

$routes->connect('/', ['controller' => 'Todos', 'action' => 'index']);

All we did here is replaced the default (root) “/” route with the new destination.

Well done for following along. To grab the code and to help to improve this app further, please check out the github repo.

%d bloggers like this: