Sunday, December 27, 2015

Using CKEditor with CakePHP

CKEditor is a popular FOSS rich-text editor used in many software products and by a whole slew of companies.  This tutorial will show you how to include it in your CakePHP applications.

I’ll be using an app baked off the “articles” table provided in the CakePHP Blog Tutorial.

Downloading CKEditor

Visit the CKEditor web site and download one of the packages (Basic, Standard, or Full).  Unzip the package and copy it over to your project’s webroot folder.


Configuring your App

After baking (“bin/cake bake all articles”) the articles table, you will have a set of CRUD screens.  We’re going to add CKEditor to the “create” (add) and “update” (edit) screens, but first let’s go to our default.ctp and include the CKEditor JavaScript file.

// src/Template/Layout/default.ctp

    <?= $this->Html->css('base.css') ?>
    <?= $this->Html->css('cake.css') ?>
    <?= $this->Html->script('/ckeditor/ckeditor.js') ?>

    <?= $this->fetch('meta') ?>
    <?= $this->fetch('css') ?>
    <?= $this->fetch('script') ?>
...

Next, modify edit.ctp so that it can use CKEditor.  Still using the articles table from the CakePHP Blog Tutorial, the default form code looks like this:

// src/Template/Articles/edit.ctp
...
    <?= $this->Form->create($article) ?>
    <fieldset>
        <legend><?= __('Edit Article') ?></legend>
        <?php
            echo $this->Form->input('title');
            echo $this->Form->input('body');
        ?>
    </fieldset>
    <?= $this->Form->button(__('Submit')) ?>
    <?= $this->Form->end() ?>
...

We need to modify the form input for body so that it has an ID:

// src/Template/Articles/edit.ctp
...
    <?= $this->Form->create($article) ?>
    <fieldset>
        <legend><?= __('Edit Article') ?></legend>
        <?php
            echo $this->Form->input('title');
            echo $this->Form->input('body', ['id' => 'richTextEditor']);
        ?>
    </fieldset>
    <?= $this->Form->button(__('Submit')) ?>
    <?= $this->Form->end() ?>
...

By associating an ID with the form input, we can then replace the default editor box with CKEditor.  I used “richTextEditor” as my ID name, but it can be anything you choose; it just has to made the name used in the script call (below).  Add the following to the bottom of your edit.ctp:

// src/Template/Articles/edit.ctp

<script>
    CKEDITOR.replace('richTextEditor');
</script>

That’s it; now when you visit the edit page in your browser, you should see something like this:


In the above screenshot, the Basic editor is shown; the Standard and Full versions add more buttons and widgets.

Do the same thing to your add.ctp so that when we add a new article we can use CKEditor as well.  Be sure to add something that uses the rich text features so that we can fully test it.


Displaying the Content

If you added or updated an article to use rich text and you then visit the view page, you’ll see something like this:


Not pretty!  We’re seeing the HTML tags on our view screen.  Fortunately, there’s an easy fix.  Edit your view.ctp file and change this line:

    <div class="row">
        <h4><?= __('Body') ?></h4>
        <?= $this->Text->autoParagraph(h($article->body)); ?>
    </div>

To this:

    <div class="row">
        <h4><?= __('Body') ?></h4>
        <?= $this->Text->autoParagraph($article->body); ?>
    </div>

And now your view screen should display the content with the HTML tags interpreted rather than displayed:


Easy, huh?  CKEditor is a great way to add functionality to your web site or application, improving the appearance of large text blocks and giving your end-users a capability that they've come to expect.

Sunday, December 13, 2015

Custom Finder Methods, Part Two

Our next custom finder is very similar to the first, only this one executes a where with a SQL join.  In the last tutorial, we modified our table and controller to allow for finding all employees by last name; this time we'll show all employees for a given department.  Rather than use the department's name field, which may contain spaces, I've opted to select using code, a 1-4 character short-code from the departments table.

Model


Since we're still displaying employees, we've chosen the employees model and controller for the new code.  First, the custom finder:

// src/Model/Table/EmployeesTable.php
...
    public function findByDepartment(Query $query, array $options)
    {
        $departments = TableRegistry::get('Departments');
        
        $query
            ->where(['1' => '1'])
            ->join([
                'table' => 'departments',
                'alias' => 'd',
           'type' => 'INNER',
           'conditions' => 
              ['d.id = Employees.department_id',
              'd.code' => $options['dcode']]
        ]);
        return $query;
    }

Because we're in the model for the employees table but will be accessing department information as well, we need to load departments with TableRegistry.

In our query, you'll see the very strange "where 1=1" reference.  Even though this where doesn't need a "where" clause, I included it here for reference purposes; "1" will always equal "1" so it doesn't affect the result set.

As the example shows, we've passed the name of the table to which we're joining, the type of join (in this case, "INNER"), and the conditions of the join.  The id key field of departments should match the foreign key department_id of employees, and the code should match what was passed to us in the URL.

Controller


In the employees controller, I've added a method called "indept" so that one can access "employees/indept/XYZ":

    public function indept($dcode = null)
    {
        $this->set('employees', $this->paginate(
            $this->Employees->find(
                'byDepartment', 
                ['dcode' => $dcode])
                ->contain(['Departments'])
                ));
        $this->set('_serialize', ['employees']);
        
        if (is_null($dcode)) {
            $this->Flash->error(__('No value passed for department code.'));
        }       

        $this->render('index');
    }

What should immediately jump out to us is that the code here is virtually identical to what we used in the previously-created named method.  Not very DRY, huh?  Go ahead and put your code in as shown above and make sure that everything works; then we'll clean it up.

What we've done do far should allow both of the following URLs to produce the desired results:

  • /employees/named/Smith
  • /employees/indept/DIT

But we probably want to give our end-users a little more direct method of accessing these queries.

View (Template)


I thought it would be cool to use Google's Material Icons for the view changes, and this requires a quick side-trip into our default template.  Edit default.ctp and put in a line to include the Google Material Icons style sheet after cake.css:

// src/Template/Layout/default.ctp
...
    <?= $this->Html->css('base.css') ?>
    <?= $this->Html->css('cake.css') ?>
    <?= $this->Html->css('https://fonts.googleapis.com/icon?family=Material+Icons') ?>

    <?= $this->fetch('meta') ?>
    <?= $this->fetch('css') ?>
...

Now we have access all of the icon set and we can use them in our other templates.

Edit index.ctp in the employees folder and make the following changes:

// src/Template/Employees/index.ctp
...
<thead>
    <tr>
        <th><?= $this->Paginator->sort('first_name') ?></th>
        <th><?= $this->Paginator->sort('last_name') ?></th>
        <th><?= __(' ') ?></th>
        <th><?= $this->Paginator->sort('hire_date') ?></th>
        <th><?= $this->Paginator->sort('department_id') ?></th>
        <th><?= __(' ') ?></th>
        <th><?= $this->Paginator->sort('birth_date') ?></th>
        <th class="actions"><?= __('Actions') ?></th>
    </tr>
</thead>
<tbody>
    <?php foreach ($employees as $employee): ?>
    <tr>
        <td><?= h($employee->first_name) ?></td>
        <td><?= h($employee->last_name) ?></td>
        <td><i class="material-icons"><?= $this->Html->link(__('search'), ['action' => 'named', $employee->last_name]) ?></i></td>
        <td><?= h($employee->hire_date) ?></td>
        <td><?= $employee->has('department') ? $this->Html->link($employee->department->name, ['controller' => 'Departments', 'action' => 'view', $employee->department->id]) : '' ?></td>
        <td><i class="material-icons"><?= $this->Html->link(__('search'), ['action' => 'indept', $employee->department->code]) ?></i></td>
        <td><?= h($employee->birth_date) ?></td>
        <td class="actions">
            <?= $this->Html->link(__('View'), ['action' => 'view', $employee->id]) ?>
            <?= $this->Html->link(__('Edit'), ['action' => 'edit', $employee->id]) ?>
            <?= $this->Form->postLink(__('Delete'), ['action' => 'delete', $employee->id], ['confirm' => __('Are you sure you want to delete # {0}?', $employee->id)]) ?>
        </td>
    </tr>
    <?php endforeach; ?>
</tbody>
...

I ditched the "id" field to make more room, moved birth_date to where "created" was, then added blank headings and the material icon for "search".  The search icon after last_name links into the named method, passing the last name, and the search icon after the department name links to the indept method with the department's short code.

It should all look something like this:



Clicking on the search icons will now take you to another screen, showing the fruits of our labors... and more search icons (because we're using index.ctp for our search results).

This, too, is something we can clean up in part three.

Thursday, December 10, 2015

A DBMS Side Trip

A little break from the direction of my last post; in finishing up the Custom Finder Methods tutorial I realized that I really wanted to work with a database that was configured for CakePHP's naming conventions.  Plus I decided to make a couple of other modifications, such as dropping the dept_emp linking table (instead, employees are owned by departments) and adding a users table for subsequent authentication.

Here's the modified database, from which I'll be doing several future tutorials.

An Employee Database


DROP DATABASE IF EXISTS employees;
CREATE DATABASE IF NOT EXISTS employees;
USE employees;

SELECT 'CREATING DATABASE STRUCTURE' as 'INFO';


DROP TABLE IF EXISTS dept_emp_links,

                     dept_managers,
                     titles,
                     salaries, 
                     employees, 
                     departments;

   set storage_engine = InnoDB;


select CONCAT('storage engine: ', @@storage_engine) as INFO;


CREATE TABLE users (

    id          INT             NOT NULL AUTO_INCREMENT,
    username    VARCHAR(50)     NOT NULL,
    password    VARCHAR(255)    NOT NULL,
    role        ENUM ('administrator','editor', 'contibutor', 'viewer')  NOT NULL,
    created     DATETIME DEFAULT NULL,
    modified    DATETIME DEFAULT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE departments (

    id          INT             NOT NULL AUTO_INCREMENT,
    code        CHAR(4)         NOT NULL,
    name        VARCHAR(40)     NOT NULL,
    created     DATETIME DEFAULT NULL,
    modified    DATETIME DEFAULT NULL,
    PRIMARY KEY (id),
    UNIQUE  KEY (code)
);

CREATE TABLE employees (

    id          INT             NOT NULL AUTO_INCREMENT,
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(20)     NOT NULL,
    last_name   VARCHAR(30)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,    
    hire_date   DATE            NOT NULL,
    department_id   INT         NOT NULL,
    created     DATETIME DEFAULT NULL,
    modified    DATETIME DEFAULT NULL,
    KEY         (department_id),
    FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE CASCADE,
    PRIMARY KEY (id)
);

CREATE TABLE dept_managers (

    id           INT             NOT NULL AUTO_INCREMENT,
    department_id    INT         NOT NULL,
    employee_id      INT         NOT NULL,
    from_date    DATE            NOT NULL,
    to_date      DATE            NOT NULL,
    created     DATETIME DEFAULT NULL,
    modified    DATETIME DEFAULT NULL,
    KEY         (employee_id),
    KEY         (department_id),
    FOREIGN KEY (employee_id)  REFERENCES employees (id)    ON DELETE CASCADE,
    FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE CASCADE,
    PRIMARY KEY (id)
); 


CREATE TABLE positions (

    id          INT             NOT NULL AUTO_INCREMENT,
    employee_id INT             NOT NULL,
    title       VARCHAR(50)     NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE,
    created     DATETIME DEFAULT NULL,
    modified    DATETIME DEFAULT NULL,
    KEY         (employee_id),
    FOREIGN KEY (employee_id)  REFERENCES employees (id)    ON DELETE CASCADE,
    PRIMARY KEY (id)
); 
        
CREATE TABLE salaries (
    id          INT             NOT NULL AUTO_INCREMENT,
    employee_id INT             NOT NULL,
    salary      INT             NOT NULL,
    from_date   DATE            NOT NULL,
    to_date     DATE            NOT NULL,
    created     DATETIME DEFAULT NULL,
    modified    DATETIME DEFAULT NULL,
    KEY         (employee_id),
    FOREIGN KEY (employee_id)  REFERENCES employees (id)    ON DELETE CASCADE,
    PRIMARY KEY (id)
);

For convenience, you can copy the above into a file and run the whole thing from the command line.  For example, if you store the script as "employees.sql" you could use:

mysql -u root -p < employees.sql

An App for the Tutorials


Create a CakePHP skeleton app:

composer create-project --prefer-dist cakephp/app cakehrms

Then edit your config/app.php file with the correct parameters needed for your new database:

    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            //'port' => 'nonstandard_port_number',
            'username' => 'root',
            'password' => 'yourpasswordhere',
            'database' => 'employees',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
            'log' => false,

Next, bake:

bin/cake bake all --everything

Now you have a CRUD app on the new database from which you can work through several upcoming tutorials from this site.


Tuesday, December 8, 2015

Custom Finder Methods, Part One

Custom finder methods in CakePHP "are the ideal way to package up commonly used queries, allowing you to abstract query details into a simple to use method."

The examples below use the MySQL employee demo database; after running the scripts to install the database, bake up some basic CRUD with:

bin/cake bake all --everything

This will provide the basis upon which to follow many of the tutorials on this site.

A Simple Finder on Name


Let's start with a finder method, defined in the model for the employees table, that will return employees based on their last name:

/src/Model/Table/EmployeesTable.php
...
    public function findByName(Query $query, array $options)
    {
        $query->where([
            'Employees.last_name' => $options['lname']
        ]);
        return $query;
    }

We've created a new function, "findByName" that compares last_name in the Employees table to a passed value ('lname') from the $options array.

In our employees controller, we need a corresponding method:

/src/Controller/EmployeesController.php
...
    public function named($lname = null)
    {
        $this->set('employees', $this->paginate(
            $this->Employees->find(
                'byName', 
                ['lname' => $lname])));
        $this->set('_serialize', ['employees']);
        
        if (is_null($lname)) {
            $this->Flash->error(__('No value passed for last name.'));
        }       

        $this->render('index');
    }

You'll notice that this is almost identical to the index method with the exception of the finder method.  We could have put this in index and executed it conditionally, but I thought it cleaner to have a separate method.

Following CakePHP's idea of "convention over configuration," our finder method - named "findByName," is evoked by calling "find('byName')".  We're passing the last name which will become part of the $options array.

If no name was passed, we're flashing an error message.

We'll need a view for this, but the view in this case is identical to the existing "index.ctp" for employees; for now, let's just use it.

You should now be able to see a list of employees with a given last name by passing the last name in the URL, such as:

http://localhost:8765/employees/named/Facello



If you call "named" without passing a value, you'll see our error message:



More Complex Finders


So far we've walked through the logic of a finder method and we've created a method that can be called from anywhere to return a list of employees by last name.

In part two of this tutorial, we'll produce a list of all employees by their department number.

Thursday, December 3, 2015

Displaying Dates in CakePHP Templates

I didn't find it exactly intuitive in the documentation, so I thought I'd post up a short 'n' sweet about how to format dates and times in CakePHP templates.

Cake\I18n\Time::i18nFormat with IntlDateFormatter provides some easy options.  For example:

<td><?= $this->Time->i18nFormat($employee->birth_date, [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE]) ?></td>

PHP's IntlDateFormatter class contains a number of constants (e.g. NONE, FULL, LONG, MEDIUM, SHORT) that can be used in combination to show variations on the date and time.  In the example above, I used "MEDIUM" for the date and "NONE" for the time to get the result shown below:



Setting both the date and time to "FULL" results in:



You can, of course, format your dates/times manually as well:

<td><?= $this->Time->i18nFormat($employee->hire_date, 'yyyy-MM-dd HH:mm:ss') ?></td>

The above results in:



According to the CakePHP docs on dates and times, "anything you can do with Carbon and DateTime, you can do with (CakePHP's) Time.

Saturday, November 14, 2015

Using PHPUnit with CakePHP

This post will walk you through some of the basics of getting started with PHPUnit and CakePHP.

Adding PHPUnit


After creating a new CakePHP project, open your composer.json file and find the following lines:

    "suggest": {
        "phpunit/phpunit": "Allows automated tests to be run without system-wide install."
    },

Move this line up to the "require-dev" section:

    "require-dev": {
        "psy/psysh": "@stable",
        "cakephp/debug_kit": "~3.0",
        "cakephp/bake": "~1.0",
        "phpunit/phpunit": "*",
        "phpunit/php-invoker": "*"
    },

Next, run "composer update" to update your CakePHP project.

Database Setup


In this example, we have a users table defined in MySQL as follows:

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(255),
    role VARCHAR(20),
    created DATETIME DEFAULT NULL,
    modified DATETIME DEFAULT NULL
);

This table will reside in your "production" database; you should create a "test" database as well (there is no need to create the table in the test database).

Edit config/app.php and put in the references to both databases:

    'Datasources' => [
        'default' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            /**
             * CakePHP will use the default DB port based on the driver selected
             * MySQL on MAMP uses port 8889, MAMP users will want to uncomment
             * the following line and set the port accordingly
             */
            //'port' => 'nonstandard_port_number',
            'username' => 'root',
            'password' => '*********',
            'database' => 'mydatabase',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
...
        /**
         * The test connection is used during the test suite.
         */
        'test' => [
            'className' => 'Cake\Database\Connection',
            'driver' => 'Cake\Database\Driver\Mysql',
            'persistent' => false,
            'host' => 'localhost',
            //'port' => 'nonstandard_port_number',
            'username' => 'root',
            'password' => '*********',
            'database' => 'mydatabase_test',
            'encoding' => 'utf8',
            'timezone' => 'UTC',
            'cacheMetadata' => true,
...

Now you're ready to bake your users table.

CRUD Controller Tests


The following are examples of the test segments usable in the controller of a CRUD application.  The tests in the controller are considered integration tests because their actions span multiple components.

The bake process will create stubs for you.

Fixtures


Fixtures provide your test database with test data.  After baking your users table, access tests/Fixture/UsersFixture.php and make sure you have at least three rows of test data:

    /**
     * Records
     *
     * @var array
     */
    public $records = [
        [
            'id' => 1,
            'username' => 'john.doe',
            'password' => 'loremipsum',
            'role' => 'admin',
            'created' => '2015-08-08 20:51:50',
            'modified' => '2015-08-08 20:51:50'
        ],
        [
            'id' => 2,
            'username' => 'jane.doe',
            'password' => 'loremipsum',
            'role' => 'editor',
            'created' => '2015-08-08 20:51:50',
            'modified' => '2015-08-08 20:51:50'
        ],
        [
            'id' => 3,
            'username' => 'jack.doe',
            'password' => 'loremipsum',
            'role' => 'viewer',
            'created' => '2015-08-08 20:51:50',
            'modified' => '2015-08-08 20:51:50'
        ],
    ];

Adding a few extra rows is the only thing needed in tests/Fixture/UsersFixture.php for the rest of the items shown in this document.

Users List


The bake process will have also created for you the file  tests/TestCase/Controller/UsersControllerTest.php.  Edit this file and make sure that you have the following use statements:

use App\Controller\UsersController;
use Cake\TestSuite\IntegrationTestCase;
use Cake\ORM\TableRegistry;

Alter testIndex() as follows:

    public function testIndex()
    {
        $this->get('/users?page=1');

        // Check for a 2xx response code

        $this->assertResponseOk();

        // Assert partial response content

        $this->assertResponseContains('john.doe');
    }

In the example above, we've done a get for the first page of the users list.  First we check that the page responded with assertResponseOk(); next, we verify that content from one of the rows that we know should be there is in fact displayed with assertResponseContains('john.doe').

Users View


To test that the "veiw" portion of our CRUD process is working, edit testView() and add the following:

    public function testView()
    {
        $this->get('/users/view/2');

        // Check for a 2xx response code

        $this->assertResponseOk();

        // Assert partial response content

        $this->assertResponseContains('jane.doe');
    }

This is almost identical to what we did for testIndex() except we're getting a specific row.

Users Add


The testAdd() method is a little more detailed.  Edit it and add the following:

    public function testAdd()
    {
        $this->get('/users/add');

        // Check for a 2xx response code

        $this->assertResponseOk();

        $data = [

            'id' => 15,
            'username' => 'ken.kitchen',
            'password' => 'qwerty',
            'created' => time(),
            'modified' => time()
        ];
        $this->post('/users/add', $data);

        // Check for a 2xx response code

        $this->assertResponseSuccess();

        // Assert view variables

        $users = TableRegistry::get('Users');
        $query = $users->find()->where(['username' => $data['username']]);
        $this->assertEquals(1, $query->count());
    }

First, we get the "add" page and verify that it responded.  Next, $data is created with information for the new row.  A post is invoked with $data as a parameter, and we verify that a non-error response was received with assertResponseSuccess().

Finally, we query the Users table for the row that we just created and verify, with count(), that a row exists.

Users Edit


The "edit" portion of this tutorial is still under development.

Users Delete


Finally, edit the testDelete() method and add the following:

    public function testDelete()
    {
        $this->delete('/users/delete/3');

        // Check for a 2xx/3xx response code

        $this->assertResponseSuccess();

        $users = TableRegistry::get('Users');

        $data = $users->find()->where(['id' => 3]);
        $this->assertEquals(0, $data->count());
    }

In this example, we're passing "3" into our get to invoke the page that deletes the row with that id.  Next, we attempt to count() the rows where "id = 3" with the expectation that there will be no rows returned.

Running your tests


With all of the test code added, run PHPUnit from the command line in the root folder of your project:

$ vendor/bin/phpunit

Your results should look something like this:



In this example, some of our tests show as incomplete" because, at this point, we've yet to edit the model tests.  However, all of the new assertions that we created in this tutorial ran successfully.

Saturday, October 31, 2015

Making a Simple Cell

This tutorial covers the basics of creating a view cell based upon an “articles” table, similar to the one from the blog tutorial.  It is written using CakePHP 3.

In this tutorial, we’re going to create a “recent articles” cell that can be used on the front page, a sidebar, or pretty much anywhere else (that’s the beauty of a cell).  To get started, I recommend completing the blog tutorial and using that project as your starting point.

From your project’s root directory, bake a template for your new cell:

bin/cake bake cell RecentArticles

This will create two files:

src/View/Cell/RecentArticlesCell.php
src/Template/Cell/RecentArticles/display.ctp

RecentArticlesCell.php is where the logic for your cell will go, and RecentArticles/display.ctp is the default display template.

In the empty display method of RecentArticlesCell.php, we want to put in the necessary code to return the five most recent articles posted to our Articles table:

    public function display()
    {   
        $this->loadModel('Articles');
        $recent = $this->Articles->find()
            ->limit(5)
            ->order(['modified' => 'DESC']);
        $this->set('recent_articles', $recent);
    }

As you can see, we’re bringing in the model for “Articles” and using Cake ORM (Object-Relational Mapping) to return the articles, limited the query to five rows and sorting them by the “modified” date in descending order.

Next, we “set” the template variable “recent_articles” to contain our data.  This allows us to show the data in the “display.ctp” file that was generated in template/cell/RecentArticles by our bake.

<table cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th><?= $this->Paginator->sort('title') ?></th>
            <th><?= $this->Paginator->sort('modified') ?></th>
            <th class="actions"><?= __('Actions') ?></th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($recent_articles as $recent_article): ?>
        <tr>
            <td><?= h($recent_article->title) ?></td>
            <td><?= h($recent_article->modified) ?></td>
            <td class="actions">
                <?= $this->Html->link(__('View'),
                    ['controller' => 'articles',
                    'action' => 'view',
                    $recent_article->id]) ?>
            </td>
        </tr>
        <?php endforeach; ?>
    </tbody>
</table>

In our display.ctp, we’re doing a foreach, creating the new variable $recent_article to iterate through the passed-in $recent_articles.  Now, all we have to do is use our cell on a page.

To display the cell, on - say - your home.ctp page, we simply need to include the following line anywhere on the page:

<?= $this->cell('RecentArticles') ?>

That’s it; that’s a cell.  All of the logic needed to produce the content of the cell is conveniently encapsulated in RecentArticlesCell.php, preventing clutter in our main controller and allow us to follow DRY standards.  Likewise the display.ctp houses the additional HTML and logic needed to display the cell’s contents without convoluting our home.ctp.  The cell can be used throughout our application, with changes to the formatting and content being done in one place.