The Workshop: CakePHP Part Two - php[architect] Magazine July 2018

Joe • August 24, 2020

learning packages phparch writing cakephp

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the July 2018 issue from http://phparch.com for the professionally edited version.

The Workshop: CakePHP, Part 2

by Joe Ferguson

Last month we covered the basics of CakePHP and how to get started creating routes, controllers, database tables, and retrieving data. This month we're going to dive into returning HTML views, creating and validating forms to create new widgets.

We are going to start off by refactoring the routes code we built in the previous article "The Workshop: CakePHP, Part 1". You can find the code repository via Github.com. If you want to follow along with the article as we progress, check out branch part-one.

We left off with three routes: one for our / homepage, /widgets/:id for viewing an individual widget, and /widgets/ for viewing all of our widgets. The homepage is managed by the display method in our Controller/PagesController.php, while our Controller/WidgetsController.php handles viewing of both an individual widget and all widgets.

If we inspect our /widgets URL we see a JSON dump of all of our widgets. We're accomplishing this by creating a new response and setting the type to json and setting the body field to our results:

$widgets = $this->getTableLocator()->get('Widgets');
$results = $widgets->find()->toArray();

return new Response([
    'type' => 'json',
    'body' => json_encode($results)
]);

If we're building an API this is likely all we care about from our WidgetController however we wouldn't want to show our end users JSON.

HTML Views and Templates

CakePHP has a built in template engine called "CakePHP Template", these files have a .ctp extension and support PHP's alternative syntax for control structures. You can use echo to send data from a variable to the view:

<?php echo $variable; ?>

In our Controller/WidgetsController.php file we can remove our response object creation and simply use $this->set() to pass an array with our data to the widget's view. The keys become variable names available in our template. Because we are following CakePHP's conventions the framework will look for a view file with a name matching our controller and method names. So for our WidgetsController's index method CakePHP will try to render the view file Template/Widgets/index.ctp. You can override this but for convention you shouldn't (unless you have a really good reason). Once we have used set() we can render the view with $this->render() to return our HTML view while also processing any PHP found in the template.

public function index()
{
    $widgets = $this->getTableLocator()->get('Widgets');
    $results = $widgets->find()->toArray();
    $this->set(['widgets' => $results]);
    $this->render();
}

Cake templates are responsible for displaying the HTML template to the user and therefore you will find PHP at the top of the view then followed by the HTML of the view. Any logic related to formatting or parsing of the data from your controller action would happen in your view template.

Note: Unlike Twig, Cake's templating system does not automatically escape output. You should use the h() helper method in your template to escape HTML output. See Setting View Variables

As we build our src/Template/Widgets/index.ctp we have some standard error checking:

<?php
use Cake\Core\Configure;
use Cake\Network\Exception\NotFoundException;

$this->layout = false;

if (!Configure::read('debug')) :
    throw new NotFoundException(
        'Please replace src/Template/Widgets/view.ctp with your own version or re-enable debug mode.'
    );
endif;

?>

Below the PHP we have our standard HTML where we'll use Bootstrap to help us display our data. Once we get past our boilerplate and add a table for displaying our widgets we will want to loop over $widgets in our view:

<tbody>
<?php foreach ($widgets as $widget): ?>
<tr>
    <td><?= h($widget->name) ?></td>
    <td><?= h($widget->description) ?></td>
    <td><?= h($widget->price) ?></td>
</tr>
<?php endforeach; ?>
</tbody>

Now we're ready to view our /widgets URL in the browser and we see:

Screen Shot 2018-06-18 at 14.02.08.png

Re-using Layouts

Before we get too far you might have noticed we hardcoded the entire page's HTML into that one view which is not very efficient considering we have other pages to create which will use similar or same code. We should refactor our view to use a Layout file.

Layout files are CakePHP Templates which leave holes in specific sections to be filled in later by views. This allows us to easily reuse our header, navigation, and footer sections without having to copy and paste the same code in every single view we create.

We're going to edit the existing template at Template/Layout/default.ctp and add all of our boilerplate HTML above our Widgets table, and the rest under. What we're left with is our layout template:

<!-- Our head tags, meta tags, CSS includes before here-->
    <?= $this->Flash->render() ?>
    <?= $this->fetch('content') ?>
<!-- Our footer, JS includes, etc follow -->

We keep $this->Flash->render() so that any flash messages from one page load to the next are displayed to the user. This would be were we would flash validation or other kinds of useful error (or informational) messages to our users.

The hole we leave for our template is $this->fetch('content') any code in our Template/Widgets/index.ctp template will be executed and rendered at this point in our layout. Now our view templates only need their own markup instead of the boilerplate needed on every page.

To tell CakePHP to use a layout, we need to change the line $this->layout = false; to $this->layout = 'default';. Instead of setting $this->layout in our view template We could refactor our index() method to specify the layout to use.

$widgets = $this->getTableLocator()->get('Widgets');
$results = $widgets->find()->toArray();
$this->viewBuilder()->setLayout('default');
$this->set(['widgets' => $results]);
$this->render();

My preference here is to specify the layout in the view template just because that's how I'm used to working with Twig and Blade template engines. Neither method is 'wrong' you should use whichever makes the most sense to you; remember to be consistent.

By using the same conventions we used for the index view we can easily implement an individual view for a single widget by updating our view() method in the Controller/WidgetsController.php file:

public function view($id)
{
    $widget = $this->getTableLocator()->get('Widgets');
    $widget = $widget->get($id);
    $this->set(['widget' => $widget]);
    $this->render();
}

Next we fill out some minimal information in our Template/Widgets/view.ctp file:

<?php
$this->layout = 'default';
?>
<div class="col-md-6">
    <h1><?= h($widget->name) ?></h1>
    <h2><?= h($widget->price) ?></h2>
    <p><?= h($widget->description) ?></p>
</div>

If we visit /widgets/1 we see the output in Figure 2 showing that our view() method and view template are working:

Figure 2

Creating Forms

Viewing our widgets is fine. However we want to be able to add widgets to our database as well. We'll start by creating an add() method on our Controller/WidgetsController.php:

public function add()
{
    $this->render();
}

Next we'll want to add a route in config/routes.php:

$routes->connect('/widgets/add', [
    'controller' => 'Widgets',
    'action' => 'add',
])->setMethods(['GET']);

CakePHP comes with a built in FormBuilder which allows you to easily build forms without having to write raw HTML form elements. To build our widget add form we'll use the form builder to see how it works:

<?php
$this->layout = 'default';
?>
<div class="col-md-6">
    <?php echo $this->Form->create(); ?>
    <?php echo $this->Form->control('name', ['label' => 'Name:', 'class' => 'form-control']); ?>
    <?php echo $this->Form->control('price', ['label' => 'Price:', 'class' => 'form-control']); ?>
    <?php echo $this->Form->control('description', ['type' => 'textarea','label' => 'Description:', 'class' => 'form-control']); ?>
    <?php echo $this->Form->submit('Add Widget', ['class' => 'form-control btn btn-primary']); ?>
    <?= $this->Form->end(); ?>
</div>

To open our form we use $this->Form->create() which will generate the following HTML code:

<form method="post" accept-charset="utf-8" action="/widgets/add">

Because we are calling create() from within the Template/Widgets/add.ctp template CakePHP knows we want to POST to the /widgets/add endpoint. This is another helpful convention. You can feel free to override this with your own endpoint but I recommend sticking with the conventions whenever possible.

We'll use Form Controls to create our input fields. The $this->Form->control() method takes a control name string as the first argument and then an array of options as the second. The options array is how we pass in a label name and class names to use for our elements. Finally we use $this->Form->submit() to create our submit button with the text 'Add Widget' displayed.

Before we try to submit our form we'll want to add another route to config/routes.php to send POST requests to another controller method which will process the request.

$routes->connect('/widgets/add', [
    'controller' => 'Widgets',
    'action' => 'create', 
])->setMethods(['POST']);    

Note we have set the method to POST

To test that our POST route is working we add our create() method to our Controller/WidgetsController.php:

public function create()
{
    echo "<pre>";
    var_dump($_POST);
    exit();
}

Let's go back to /widgets/add and then fill out our form with a new widget:

Figure 3

Once we submit that we can see in the browser:

/home/vagrant/cake/src/Controller/WidgetsController.php:63:
array (size=4)
  '_method' => string 'POST' (length=4)
  'name' => string 'Best Widget Ever' (length=16)
  'price' => string '100' (length=3)
  'description' => string 'The best widget you'll ever have.' (length=33)

Now we can see that the global $_POST does contain our data we input into the form.

Validating Forms

Before we just take our user submitted data and throw it into our database we should validate it first (and sanitize it!)

Never ever trust user submitted data. Always assume it is hostile and will destroy your application. If this is a new concept to you please check out the links provided by http://www.phptherightway.com/#web_application_security

We can use CakePHP's build it Validator class and set up rules for validating our incoming data:

$validator = new Validator();
$validator
    ->requirePresence('name')
    ->notEmpty('name', 'Please fill this field')
    ->add('name', [
        'length' => [
            'rule' => ['minLength', 5],
            'message' => 'Names need to be at least 5 characters long',
        ]
    ])
    ->requirePresence('price')
    ->notEmpty('price', 'Please fill in the price.')
    ->integer('price')
    ->requirePresence('description')
    ->notEmpty('description', 'Please fill in the price.');

$errors = $validator->errors($this->request->getData());

if (empty($errors))
{
    // Save our Widget
}

If the incoming data passes all of our rules the array will be empty. If any validation rules fail, the $errors array will hold the error messages. If we submit the blank form, we'll trigger all of our validation rules:

/home/vagrant/cake/src/Controller/WidgetsController.php:74:
array (size=3)
  'name' => 
    array (size=1)
      '_empty' => string 'Please fill this field' (length=22)
  'price' => 
    array (size=1)
      '_empty' => string 'Please fill in the price.' (length=25)
  'description' => 
    array (size=1)
      '_empty' => string 'Please fill in the price.' (length=25)

Create a Widget

Now that we have our validation in place we need to do something with the data provided by the user. We need to take the data they give us and use it to create a new widget but we also want to sanitize the data they send us:

if (empty($errors))
{
    $widgets = $this->getTableLocator()->get('Widgets');
    $widget = $widgets->newEntity();

    $widget->name = filter_var($this->request->getData('name'),
        FILTER_SANITIZE_STRING
    );
    $widget->price = filter_var($this->request->getData('price'),
        FILTER_SANITIZE_NUMBER_INT
    );
    $widget->description = filter_var($this->request->getData('description'),
        FILTER_SANITIZE_STRING
    );

    $widget->created = time();
    $widget->modified = time();

    if ($widgets->save($widget)) {
        return $this->redirect('/widgets/' . $widget->id);
    }
}

We use getTableLocator() to return an instance of our Widgets table and then newEntity() creates a new blank object. Next we sanitize our incoming data with filter_var().

You can find out more about filter_var() and what our specific flags mean in the official PHP documentation

Because we're not using Entities we have to specify values for the created and modified fields. You can also bind forms to entities for writing even less code than we have here.

Cleaning the Kitchen

Now that we've covered HTML views, layouts, form helpers, and validating forms you are ready to jump into your next great application idea with CakePHP. While 3.6 is the current stable version of CakePHP version 4.x is already underway. I look forward to seeing what the great community and contributors bring in version 4.

All of the code written has been to demonstrate framework basics but you should always remember to check the documentation for best practices and updates. The code covered was written using CakePHP 3.6.x

Warning:

This post content may not be current, please double check the official documentation as needed.

This post may also be in an unedited form with grammatical or spelling mistakes, purchase the July 2018 issue from http://phparch.com for the professionally edited version.