CakePHP 3 … fully grown (step 5 — Model layer and testing)

(Get the app code on github.)

Finally we are at the point where some interesting things are about to happen.

Let’s recap a little so far:

So here comes the fun part.

Let’s build the server-side part of our application in CakePHP 3.

Hmm… Where to start?

The Model layer.

I have to say that if you are familiar with CakePHP 2.x ORM, working with the new Model layer is either going to bring you a great deal of pain or pleasure… (or both in that order).
Indeed, there will be a bit of a learning curve to get used to the new and improved way of doing things, so although I am a bit sorry to say it, but if you’ve spent the last two years learning the intricacies of CakePHP 2.x ORM you are in for a bit of a surprise… although don’t despair, your knowledge will not go wasted.

In CakePHP 3 the ORM has been rebuilt from the ground-up and personally I find it a lot more flexible, better structured and “officially” speaking the separation of concerns principle is truly respected.

Back in the day we used to have a CakePHP Model and that was pretty much all there is to it (the Model layer). You could call a find() method (or some variation thereof) to retrieve your data and you’d get an array of data back.
For most purposes it worked very well, but as soon as you needed to do something more complicated (sub-queries anyone?)… things got rather nasty and in turn you could see plenty of poorly written CakePHP apps.

But let’s not dwell on the past, as today we have:

  • Table object, which provides a direct representation of your table.
  • Entity object, basically represents a record row (i.e. if you have a bunch of Products in a table named “products”, then a single product or item representation would be handled by the Entity object named “Product”, in this example.)… so to keep in mind: Table — collection of records, Entity — single row/record.
  • Query object, allows for construction and manipulation of queries. A little more on that below.

All of these are described at length in the CakePHP Cookbook, therefore I don’t think there is a need to go over each one in detail.

Instead, let’s just take a look at the code for our table, if you recall, we have table called “todos”, therefore a file representing this table should be created ( src/Model/Table/TodoTable.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
<?php
namespace App\Model\Table;

use Cake\I18n\Time;
use Cake\ORM\Query;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class TodosTable extends Table {

/**
 * initialize method
 *
 * @param array $config list of config options
 * @return void
 */

  public function initialize(array $config) {
    $this->addBehavior('Timestamp', [
      'events' => [
        'Model.beforeSave' => [
        'created' => 'new',
        'updated' => 'always'
      ]
    ]]);
  }

/**
 * Default validator method
 *
 * @param Validator $validator cakephp validator object
 * @return Validator $validator cakephp validator object
 */

  public function validationDefault(Validator $validator) {
    $validator
    ->allowEmpty('todo', 'update')
    ->notEmpty('todo');

    return $validator;
  }

/**
 * Custom finder method, returns recent to-do's based on status
 *
 * @param Query $query  cakephp query object
 * @param array $options list of options
 * @return query $query cakephp query object
 */

  public function findRecent(Query $query, array $options = ['status' => 0]) {
    return $query
        ->where(['is_done' => $options['status']])
        ->order(['updated' => 'DESC'])
        ->formatResults(function ($results, $query) {
          return $results->map(function ($row) {
            $timeCreated = new Time($row->created);
            $timeUpdated = new Time($row->updated);

            $row->created = $timeCreated->timeAgoInWords();
            $row->updated = $timeUpdated->timeAgoInWords();
            $row->todo = htmlspecialchars($row->todo);

            return $row;
          });
        });
  }
}

Although it looks like a lot of code, most of our application is contained right here. Let’s try to break it down.

First, I am attaching the Timestamp behavior to handle our “created” an “updated” fields. (Note I diverged slightly from the book where the default field is “modified”, and not “updated”… however the problem is readily fixed by providing a few extra settings as you see on lines 20 -22).

Next, comes our validationDefault() method. Although it looks a bit different from the way things were in the previous versions of CakePHP, the code within it should be pretty straight forward. I am checking to make sure that the “todo” field is not empty. During the “update” of the record, however, there is no need to attempt to validate this field.

The next method findRecent() is a custom finder method, which is quite powerful in the new ORM.
Notice, that this method does not return a result set. Traditionally you’d want this method to return a query object. What happens here is that the query object gets created and the actual SQL is ready to be executed at this point. However to trigger the execution of the SQL to actually “extract” your results requires another step (more about this when I talk about controllers).

There is a couple of other interesting things here…
I am using both formatResults() method and map() methods (thanks, Jose, for the hint) to modify certain things in my result-set. To understand the code a little better what we do here is modify each row value to our liking. For example I am protecting/sanitizing the “todo” field by using PHP’s htmlspcialchars() method. Additionally I am modifying the timestamp to be human-readable rather than Database date-time format, which looks rather ugly.

That’s all there is to our Table. This single method will allow us to get both finished and incomplete to-do’s by toggling the ‘status’ option.

Next, I must talk about unit testing. Actually as a somewhat decent developer I should start every application the TDD (test driven development) way. What that means I write my tests before I ever think about writing the production code.

For the sake of this example, please forgive me for not doing so from the get-go, but here comes our first test. After all, if our table has been setup the right way then the testing should be a no-brainer.

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
<?php
namespace App\Test\TestCase\Model\Table;

use Cake\I18n\Time;
use Cake\ORM\Query;
use Cake\ORM\TableRegistry;
use Cake\TestSuite\TestCase;

class TodosTest extends TestCase {

/**
 * fixtures
 *
 * @var array
 */

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

/**
 * setUp() method
 *
 * @return void
 */

  public function setUp() {
    parent::setUp();
    $this->Todos = TableRegistry::get('Todos');
  }

/**
 * test saving of a to-do, validation
 *
 * @return void
 */

  public function testSaving() {
    $data = ['todo' => ''];
    $todo = $this->Todos->newEntity($data);
    $resultingError = $this->Todos->validator()->errors($data);
    $expectedError = [
      'todo' => [
        'This field cannot be left empty'
      ]
    ];
    $this->assertEquals($expectedError, $resultingError);

    $total = $this->Todos->find()->count();
    $this->assertEquals(2, $total);

    $data = ['todo' => 'testing'];
    $todo = $this->Todos->newEntity($data);
    $this->Todos->save($todo);
    $newTotal = $this->Todos->find()->count();
    $this->assertEquals(3, $newTotal);
  }

/**
 * test custom finder method
 *
 * @return void
 */

  public function testRecent() {
    $result = $this->Todos->find('recent', ['status' => 0]);
    $recent = $result->first()->toArray();
    $expected = [
        'id' => 1,
        'todo' => 'First To-do',
        'created' => 'on 11/21/13',
        'updated' => 'on 11/21/13',
        'is_done' => false
    ];

    $this->assertEquals($expected, $recent);
  }

/**
 * test saving of a to-do with evil data
 *
 * @return void
 */

  public function testSaveEvilScript() {
    $data = ['todo' => '<script>alert("hi")</script>', 'is_done' => 1];
    $todo = $this->Todos->newEntity($data);
    $this->Todos->save($todo);
    $newTotal = $this->Todos->find()->count();
    $this->assertEquals(3, $newTotal);

    $result = $this->Todos->find('recent', ['status' => 1])->where(['id' => 3])->first();
    $this->assertEquals('&lt;script&gt;alert(&quot;hi&quot;)&lt;/script&gt;', $result->todo);
  }

/**
 * test to make sure custom finder returns the dates in a human-readable format
 *
 * @return void
 */

  public function testFindTimeAgoInWords() {
    $todos = TableRegistry::get('Todos');
    $todo = $todos->get(1);
    $todos->patchEntity($todo, ['updated' => new Time(date('Y-m-d H:i:s', strtotime('-3 seconds ago')))]);
    $todos->save($todo);
    $result = $todos->find('recent', ['status' => 0])->where(['id' => 1])->first();
    $this->assertContains('second', $result->updated);

    $todos = TableRegistry::get('Todos');
    $todo = $todos->get(1);
    $todos->patchEntity($todo, ['created' => new Time(date('Y-m-d H:i:s', strtotime('-3 seconds ago')))]);
    $todos->save($todo);
    $result = $todos->find('recent', ['status' => 0])->where(['id' => 1])->first();
    $this->assertContains('second', $result->created);
  }
}

Although this test is lengthy in code, it is rather simple in what it actually accomplishes.

First we try testSaving()… this ensures that both the validation and the actual storage of the data to the database, works as expected.
(Based on the assertions you can see that we expect things to error-out with an empty “to-do” and save correctly, when a “to-do” has some value, on lines 42 and 51 respectively).

Additionally I test that an “evil” script cannot be inserted into our page by ensuring that the htmlspecialchars() method works its magic on a “to-do”. Take a look at line 59 in the table and the above method testSaveEvilScript().

Lastly, I am testing testFindTimeAgoInWords(), which ensures that a human-readable timestamp is returned, rather than something that looks like a posting on a personal ad for robots.

Of course, our testing would not be possible without a fixture (to better phrase it, having some fixture/seed data to work with, makes testing a lot easier):

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

I will provide you the sample fixture below.

For now suffice it to say, that I am done with the model layer and its testing. Although we have not looked at the entity object in detail, it is simply not required for our application… and CakePHP is smart enough to instantiate entity objects for you and hydrate them as necessary. More on that, in the CakePHP cookbook.

Finally, I will talk about the controller (and testing of it, of course, in the following post).

… and as promised here’s a sample fixture, which I am using for my tests:

<?php
namespace App\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

class TodosFixture extends TestFixture {

  public $import = ['table' => 'todos'];

  public $records = [
    [
      'id' => 1,
      'todo' => 'First To-do',
      'is_done' => '0',
      'created' => '2013-11-21 12:00:00',
      'updated' => '2013-11-21 12:00:00'
    ],
    [
      'id' => 2,
      'todo' => 'Complete To-do',
      'is_done' => '1',
      'created' => '2013-11-21 12:00:00',
      'updated' => '2013-11-21 12:00:00'
    ]
  ];
}

Related Posts

%d bloggers like this: