CMS Tutorial - Creating the Articles Controller

With our model created, we need a controller for our articles. Controllers inCakePHP handle HTTP requests and execute business logic contained in modelmethods, to prepare the response. We’ll place this new controller in a filecalled ArticlesController.php inside the src/Controller directory.Here’s what the basic controller should look like:

  1. <?php
  2. // src/Controller/ArticlesController.php
  3.  
  4. namespace App\Controller;
  5.  
  6. class ArticlesController extends AppController
  7. {
  8. }

Now, let’s add an action to our controller. Actions are controller methods thathave routes connected to them. For example, when a user requestswww.example.com/articles/index (which is also the same aswww.example.com/articles), CakePHP will call the index method of yourArticlesController. This method should query the model layer, and preparea response by rendering a Template in the View. The code for that action wouldlook like this:

  1. <?php
  2. // src/Controller/ArticlesController.php
  3.  
  4. namespace App\Controller;
  5.  
  6. class ArticlesController extends AppController
  7. {
  8. public function index()
  9. {
  10. $this->loadComponent('Paginator');
  11. $articles = $this->Paginator->paginate($this->Articles->find());
  12. $this->set(compact('articles'));
  13. }
  14. }

By defining function index() in our ArticlesController, users can nowaccess the logic there by requesting www.example.com/articles/index.Similarly, if we were to define a function called foobar(), users would beable to access that at www.example.com/articles/foobar. You may be temptedto name your controllers and actions in a way that allows you to obtain specificURLs. Resist that temptation. Instead, follow the CakePHP Conventionscreating readable, meaningful action names. You can then useRouting to connect the URLs you want to the actions you’vecreated.

Our controller action is very simple. It fetches a paginated set of articlesfrom the database, using the Articles Model that is automatically loaded via namingconventions. It then uses set() to pass the articles into the Template (whichwe’ll create soon). CakePHP will automatically render the template after ourcontroller action completes.

Create the Article List Template

Now that we have our controller pulling data from the model, and preparing ourview context, let’s create a view template for our index action.

CakePHP view templates are presentation-flavored PHP code that is inserted insidethe application’s layout. While we’ll be creating HTML here, Views can alsogenerate JSON, CSV or even binary files like PDFs.

A layout is presentation code that is wrapped around a view. Layout filescontain common site elements like headers, footers and navigation elements. Yourapplication can have multiple layouts, and you can switch between them, but fornow, let’s just use the default layout.

CakePHP’s template files are stored in src/Template inside a foldernamed after the controller they correspond to. So we’ll have to createa folder named ‘Articles’ in this case. Add the following code to yourapplication:

  1. <!-- File: src/Template/Articles/index.ctp -->
  2.  
  3. <h1>Articles</h1>
  4. <table>
  5. <tr>
  6. <th>Title</th>
  7. <th>Created</th>
  8. </tr>
  9.  
  10. <!-- Here is where we iterate through our $articles query object, printing out article info -->
  11.  
  12. <?php foreach ($articles as $article): ?>
  13. <tr>
  14. <td>
  15. <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
  16. </td>
  17. <td>
  18. <?= $article->created->format(DATE_RFC850) ?>
  19. </td>
  20. </tr>
  21. <?php endforeach; ?>
  22. </table>

In the last section we assigned the ‘articles’ variable to the view usingset(). Variables passed into the view are available in the view templates aslocal variables which we used in the above code.

You might have noticed the use of an object called $this->Html. This is aninstance of the CakePHP HtmlHelper. CakePHP comeswith a set of view helpers that make tasks like creating links, forms, andpagination buttons easy. You can learn more about Helpers in theirchapter, but what’s important to note here is that the link() method willgenerate an HTML link with the given link text (the first parameter) and URL(the second parameter).

When specifying URLs in CakePHP, it is recommended that you use arrays ornamed routes. These syntaxes allow you toleverage the reverse routing features CakePHP offers.

At this point, you should be able to point your browser tohttp://localhost:8765/articles/index. You should see your list view,correctly formatted with the title and table listing of the articles.

Create the View Action

If you were to click one of the ‘view’ links in our Articles list page, you’dsee an error page saying that action hasn’t been implemented. Lets fix that now:

  1. // Add to existing src/Controller/ArticlesController.php file
  2.  
  3. public function view($slug = null)
  4. {
  5. $article = $this->Articles->findBySlug($slug)->firstOrFail();
  6. $this->set(compact('article'));
  7. }

While this is a simple action, we’ve used some powerful CakePHP features. Westart our action off by using findBySlug() which isa Dynamic Finder. This method allows us to create a basic query thatfinds articles by a given slug. We then use firstOrFail() to either fetchthe first record, or throw a NotFoundException.

Our action takes a $slug parameter, but where does that parameter come from?If a user requests /articles/view/first-post, then the value ‘first-post’ ispassed as $slug by CakePHP’s routing and dispatching layers. If wereload our browser with our new action saved, we’d see another CakePHP errorpage telling us we’re missing a view template; let’s fix that.

Create the View Template

Let’s create the view for our new ‘view’ action and place it insrc/Template/Articles/view.ctp

  1. <!-- File: src/Template/Articles/view.ctp -->
  2.  
  3. <h1><?= h($article->title) ?></h1>
  4. <p><?= h($article->body) ?></p>
  5. <p><small>Created: <?= $article->created->format(DATE_RFC850) ?></small></p>
  6. <p><?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?></p>

You can verify that this is working by trying the links at /articles/index ormanually requesting an article by accessing URLs like/articles/view/first-post.

Adding Articles

With the basic read views created, we need to make it possible for new articlesto be created. Start by creating an add() action in theArticlesController. Our controller should now look like:

  1. // src/Controller/ArticlesController.php
  2.  
  3. namespace App\Controller;
  4.  
  5. use App\Controller\AppController;
  6.  
  7. class ArticlesController extends AppController
  8. {
  9.  
  10. public function initialize()
  11. {
  12. parent::initialize();
  13.  
  14. $this->loadComponent('Paginator');
  15. $this->loadComponent('Flash'); // Include the FlashComponent
  16. }
  17.  
  18. public function index()
  19. {
  20. $articles = $this->Paginator->paginate($this->Articles->find());
  21. $this->set(compact('articles'));
  22. }
  23.  
  24. public function view($slug)
  25. {
  26. $article = $this->Articles->findBySlug($slug)->firstOrFail();
  27. $this->set(compact('article'));
  28. }
  29.  
  30. public function add()
  31. {
  32. $article = $this->Articles->newEntity();
  33. if ($this->request->is('post')) {
  34. $article = $this->Articles->patchEntity($article, $this->request->getData());
  35.  
  36. // Hardcoding the user_id is temporary, and will be removed later
  37. // when we build authentication out.
  38. $article->user_id = 1;
  39.  
  40. if ($this->Articles->save($article)) {
  41. $this->Flash->success(__('Your article has been saved.'));
  42. return $this->redirect(['action' => 'index']);
  43. }
  44. $this->Flash->error(__('Unable to add your article.'));
  45. }
  46. $this->set('article', $article);
  47. }
  48. }

Note

You need to include the Flash component inany controller where you will use it. Often it makes sense to include it inyour AppController.

Here’s what the add() action does:

  • If the HTTP method of the request was POST, try to save the data using the Articles model.
  • If for some reason it doesn’t save, just render the view. This gives us achance to show the user validation errors or other warnings.
    Every CakePHP request includes a request object which is accessible using$this->request. The request object contains information regarding therequest that was just received. We use theCake\Http\ServerRequest::is() method to check that the requestis a HTTP POST request.

Our POST data is available in $this->request->getData(). You can use thepr() or debug() functions to print it out if you want tosee what it looks like. To save our data, we first ‘marshal’ the POST data intoan Article Entity. The Entity is then persisted using the ArticlesTable wecreated earlier.

After saving our new article we use FlashComponent’s success() method to seta message into the session. The success method is provided using PHP’smagic method features. Flashmessages will be displayed on the next page after redirecting. In our layout we have<?= $this->Flash->render() ?> which displays flash messages and clears thecorresponding session variable. Finally, after saving is complete, we useCake\Controller\Controller::redirect to send the user back to thearticles list. The param ['action' => 'index'] translates to URL/articles i.e the index action of the ArticlesController. You can referto Cake\Routing\Router::url() function on the API to see the formats in which you can specify a URLfor various CakePHP functions.

Create Add Template

Here’s our add view template:

  1. <!-- File: src/Template/Articles/add.ctp -->
  2.  
  3. <h1>Add Article</h1>
  4. <?php
  5. echo $this->Form->create($article);
  6. // Hard code the user for now.
  7. echo $this->Form->control('user_id', ['type' => 'hidden', 'value' => 1]);
  8. echo $this->Form->control('title');
  9. echo $this->Form->control('body', ['rows' => '3']);
  10. echo $this->Form->button(__('Save Article'));
  11. echo $this->Form->end();
  12. ?>

We use the FormHelper to generate the opening tag for an HTMLform. Here’s the HTML that $this->Form->create() generates:

  1. <form method="post" action="/articles/add">

Because we called create() without a URL option, FormHelper assumes wewant the form to submit back to the current action.

The $this->Form->control() method is used to create form elementsof the same name. The first parameter tells CakePHP which fieldthey correspond to, and the second parameter allows you to specifya wide array of options - in this case, the number of rows for thetextarea. There’s a bit of introspection and conventions used here. Thecontrol() will output different form elements based on the modelfield specified, and use inflection to generate the label text. You cancustomize the label, the input or any other aspect of the form controls usingoptions. The $this->Form->end() call closes the form.

Now let’s go back and update our src/Template/Articles/index.ctpview to include a new “Add Article” link. Before the <table>, addthe following line:

  1. <?= $this->Html->link('Add Article', ['action' => 'add']) ?>

Adding Simple Slug Generation

If we were to save an Article right now, saving would fail as we are notcreating a slug attribute, and the column is NOT NULL. Slug values aretypically a URL-safe version of an article’s title. We can use thebeforeSave() callback of the ORM to populate our slug:

  1. // in src/Model/Table/ArticlesTable.php
  2. namespace App\Model\Table;
  3.  
  4. use Cake\ORM\Table;
  5. // the Text class
  6. use Cake\Utility\Text;
  7.  
  8. // Add the following method.
  9.  
  10. public function beforeSave($event, $entity, $options)
  11. {
  12. if ($entity->isNew() && !$entity->slug) {
  13. $sluggedTitle = Text::slug($entity->title);
  14. // trim slug to maximum length defined in schema
  15. $entity->slug = substr($sluggedTitle, 0, 191);
  16. }
  17. }

This code is simple, and doesn’t take into account duplicate slugs. But we’llfix that later on.

Add Edit Action

Our application can now save articles, but we can’t edit them. Lets rectify thatnow. Add the following action to your ArticlesController:

  1. // in src/Controller/ArticlesController.php
  2.  
  3. // Add the following method.
  4.  
  5. public function edit($slug)
  6. {
  7. $article = $this->Articles->findBySlug($slug)->firstOrFail();
  8. if ($this->request->is(['post', 'put'])) {
  9. $this->Articles->patchEntity($article, $this->request->getData());
  10. if ($this->Articles->save($article)) {
  11. $this->Flash->success(__('Your article has been updated.'));
  12. return $this->redirect(['action' => 'index']);
  13. }
  14. $this->Flash->error(__('Unable to update your article.'));
  15. }
  16.  
  17. $this->set('article', $article);
  18. }

This action first ensures that the user has tried to access an existing record.If they haven’t passed in an $slug parameter, or the article does not exist,a NotFoundException will be thrown, and the CakePHP ErrorHandler will renderthe appropriate error page.

Next the action checks whether the request is either a POST or a PUT request. Ifit is, then we use the POST/PUT data to update our article entity by using thepatchEntity() method. Finally, we call save() set the appropriate flashmessage and either redirect or display validation errors.

Create Edit Template

The edit template should look like this:

  1. <!-- File: src/Template/Articles/edit.ctp -->
  2.  
  3. <h1>Edit Article</h1>
  4. <?php
  5. echo $this->Form->create($article);
  6. echo $this->Form->control('user_id', ['type' => 'hidden']);
  7. echo $this->Form->control('title');
  8. echo $this->Form->control('body', ['rows' => '3']);
  9. echo $this->Form->button(__('Save Article'));
  10. echo $this->Form->end();
  11. ?>

This template outputs the edit form (with the values populated), alongwith any necessary validation error messages.

You can now update your index view with links to edit specificarticles:

  1. <!-- File: src/Template/Articles/index.ctp (edit links added) -->
  2.  
  3. <h1>Articles</h1>
  4. <p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
  5. <table>
  6. <tr>
  7. <th>Title</th>
  8. <th>Created</th>
  9. <th>Action</th>
  10. </tr>
  11.  
  12. <!-- Here's where we iterate through our $articles query object, printing out article info -->
  13.  
  14. <?php foreach ($articles as $article): ?>
  15. <tr>
  16. <td>
  17. <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
  18. </td>
  19. <td>
  20. <?= $article->created->format(DATE_RFC850) ?>
  21. </td>
  22. <td>
  23. <?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
  24. </td>
  25. </tr>
  26. <?php endforeach; ?>
  27.  
  28. </table>

Update Validation Rules for Articles

Up until this point our Articles had no input validation done. Lets fix that byusing a validator:

  1. // src/Model/Table/ArticlesTable.php
  2.  
  3. // add this use statement right below the namespace declaration to import
  4. // the Validator class
  5. use Cake\Validation\Validator;
  6.  
  7. // Add the following method.
  8. public function validationDefault(Validator $validator)
  9. {
  10. $validator
  11. ->allowEmptyString('title', false)
  12. ->minLength('title', 10)
  13. ->maxLength('title', 255)
  14.  
  15. ->allowEmptyString('body', false)
  16. ->minLength('body', 10);
  17.  
  18. return $validator;
  19. }

The validationDefault() method tells CakePHP how to validate your data whenthe save() method is called. Here, we’ve specified that both the title, andbody fields must not be empty, and have certain length constraints.

CakePHP’s validation engine is powerful and flexible. It provides a suite offrequently used rules for tasks like email addresses, IP addresses etc. and theflexibility for adding your own validation rules. For more information on thatsetup, check the Validation documentation.

Now that your validation rules are in place, use the app to try to addan article with an empty title or body to see how it works. Since we’ve used theCake\View\Helper\FormHelper::control() method of the FormHelper tocreate our form elements, our validation error messages will be shownautomatically.

Add Delete Action

Next, let’s make a way for users to delete articles. Start with adelete() action in the ArticlesController:

  1. // src/Controller/ArticlesController.php
  2.  
  3. public function delete($slug)
  4. {
  5. $this->request->allowMethod(['post', 'delete']);
  6.  
  7. $article = $this->Articles->findBySlug($slug)->firstOrFail();
  8. if ($this->Articles->delete($article)) {
  9. $this->Flash->success(__('The {0} article has been deleted.', $article->title));
  10. return $this->redirect(['action' => 'index']);
  11. }
  12. }

This logic deletes the article specified by $slug, and uses$this->Flash->success() to show the user a confirmationmessage after redirecting them to /articles. If the user attempts todelete an article using a GET request, allowMethod() will throw an exception.Uncaught exceptions are captured by CakePHP’s exception handler, and a niceerror page is displayed. There are many built-inExceptions that can be used to indicate the variousHTTP errors your application might need to generate.

Warning

Allowing content to be deleted using GET requests is very dangerous, as webcrawlers could accidentally delete all your content. That is why we usedallowMethod() in our controller.

Because we’re only executing logic and redirecting to another action, thisaction has no template. You might want to update your index template with linksthat allow users to delete articles:

  1. <!-- File: src/Template/Articles/index.ctp (delete links added) -->
  2.  
  3. <h1>Articles</h1>
  4. <p><?= $this->Html->link("Add Article", ['action' => 'add']) ?></p>
  5. <table>
  6. <tr>
  7. <th>Title</th>
  8. <th>Created</th>
  9. <th>Action</th>
  10. </tr>
  11.  
  12. <!-- Here's where we iterate through our $articles query object, printing out article info -->
  13.  
  14. <?php foreach ($articles as $article): ?>
  15. <tr>
  16. <td>
  17. <?= $this->Html->link($article->title, ['action' => 'view', $article->slug]) ?>
  18. </td>
  19. <td>
  20. <?= $article->created->format(DATE_RFC850) ?>
  21. </td>
  22. <td>
  23. <?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
  24. <?= $this->Form->postLink(
  25. 'Delete',
  26. ['action' => 'delete', $article->slug],
  27. ['confirm' => 'Are you sure?'])
  28. ?>
  29. </td>
  30. </tr>
  31. <?php endforeach; ?>
  32.  
  33. </table>

Using View\Helper\FormHelper::postLink() will create a linkthat uses JavaScript to do a POST request deleting our article.

Note

This view code also uses the FormHelper to prompt the user with aJavaScript confirmation dialog before they attempt to delete anarticle.

With a basic articles management setup, we’ll create the basic actionsfor our Tags and Users tables.