The Workshop: CakePHP Part Two - php[architect] Magazine July 2018
Joe • August 24, 2020
learning packages phparch writing cakephpWarning:
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:
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:
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:
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.
As Seen On
Recent Posts
- PHP to Rust via Copilot
- Compiling Python 3.12 from Source Tarball on Linux
- HTML Form Processing with PHP - php[architect] Magazine August 2014
- The Workshop: Local Dev with Lando - php[architect] Magazine November 2022
- Getting back into the swing of things
- All Posts
Categories
- ansible
- apache
- applesilicon
- aws
- blackwidow
- cakephp
- community
- composer
- conferences
- copilot
- data-storage
- day-job
- devops
- docker
- fpv
- general
- github
- givecamp
- homestead
- jigsaw
- joindin
- keyboard
- laravel
- learning
- linux
- maker
- mamp
- mentoring
- music
- nonprofit
- opensource
- packages
- php
- phparch
- projects
- provisioning
- python
- razer
- rust
- s3
- security
- slimphp
- speaking
- static-sites
- storage
- testing
- tiny-whoop
- today-i-learned
- training
- ubuntu
- vagrant
- version-control
- windows
- writing
- wsl
- wsl2
- zend-zray