Book Image

CakePHP 2 Application Cookbook

Book Image

CakePHP 2 Application Cookbook

Overview of this book

Table of Contents (20 chapters)
CakePHP 2 Application Cookbook
Credits
Foreword
About the Authors
About the Reviewer
www.PacktPub.com
Preface
Index

Listing and viewing records


To begin, we'll need a way to view the products available and also allow the option to select and view any one of those products.

In this recipe, we'll create a listing of products as well as a page where we can view the details of a single product.

Getting ready

To go through this recipe, we'll first need a table of data to work with. So, create a table named products using the following SQL statement:

CREATE TABLE products (
  id VARCHAR(36) NOT NULL,
  name VARCHAR(100),
  details TEXT,
  available TINYINT(1) UNSIGNED DEFAULT 1,
  created DATETIME,
  modified DATETIME,
  PRIMARY KEY(id)
);

We'll then need some sample data to test with, so now run this SQL statement to insert some products:

INSERT INTO products (id, name, details, available, created, modified)
VALUES
('535c460a-f230-4565-8378-7cae01314e03', 'Cake', 'Yummy and sweet', 1, NOW(), NOW()),
('535c4638-c708-4171-985a-743901314e03', 'Cookie', 'Browsers love cookies', 1, NOW(), NOW()),
('535c49d9-917c-4eab-854f-743801314e03', 'Helper', 'Helping you all the way', 1, NOW(), NOW());

Before we begin, we'll also need to create ProductsController. To do so, create a file named ProductsController.php in app/Controller/ and add the following content:

<?php
App::uses('AppController', 'Controller');

class ProductsController extends AppController {

  public $helpers = array('Html', 'Form');

  public $components = array('Session', 'Paginator');

}

Now, create a directory named Products/ in app/View/. Then, in this directory, create one file named index.ctp and another named view.ctp.

How to do it...

Perform the following steps:

  1. Define the pagination settings to sort the products by adding the following property to the ProductsController class:

    public $paginate = array(
      'limit' => 10
    );
  2. Add the following index() method in the ProductsController class:

    public function index() {
      $this->Product->recursive = -1;
      $this->set('products', $this->paginate());
    }
  3. Introduce the following content in the index.ctp file that we created:

    <h2><?php echo __('Products'); ?></h2>
    <table>
      <tr>
        <th><?php echo $this->Paginator->sort('id'); ?></th>
        <th><?php echo $this->Paginator->sort('name'); ?></th>
        <th><?php echo $this->Paginator->sort('created'); ?></th>
      </tr>
      <?php foreach ($products as $product): ?>
        <tr>
          <td><?php echo $product['Product']['id']; ?></td>
          <td>
            <?php
            echo $this->Html->link($product['Product']['name'], array('controller' => 'products', 'action' => 'view', $product['Product']['id']));
            ?>
          </td>
          <td><?php echo $this->Time->nice($product['Product']['created']); ?></td>
        </tr>
      <?php endforeach; ?>
    </table>
    <div>
      <?php echo $this->Paginator->counter(array('format' => __('Page {:page} of {:pages}, showing {:current} records out of {:count} total, starting on record {:start}, ending on {:end}'))); ?>
    </div>
    <div>
      <?php
      echo $this->Paginator->prev(__('< previous'), array(), null, array('class' => 'prev disabled'));
      echo $this->Paginator->numbers(array('separator' => ''));
      echo $this->Paginator->next(__('next >'), array(), null, array('class' => 'next disabled'));
      ?>
    </div>
  4. Returning to the ProductsController class, add the following view() method to it:

    public function view($id) {
      if (!($product = $this->Product->findById($id))) {
        throw new NotFoundException(__('Product not found'));
      }
      $this->set(compact('product'));
    }
  5. Introduce the following content in the view.ctp file:

    <h2><?php echo h($product['Product']['name']); ?></h2>
    <p>
      <?php echo h($product['Product']['details']); ?>
    </p>
    <dl>
      <dt><?php echo __('Available'); ?></dt>
      <dd><?php echo __((bool)$product['Product']['available'] ? 'Yes' : 'No'); ?></dd>
      <dt><?php echo __('Created'); ?></dt>
      <dd><?php echo $this->Time->nice($product['Product']['created']); ?></dd>
      <dt><?php echo __('Modified'); ?></dt>
      <dd><?php echo $this->Time->nice($product['Product']['modified']); ?></dd>
    </dl>
  6. Now, navigating to /products in your web browser will display a listing of the products, as shown in the following screenshot:

  7. Clicking on one of the product names in the listing will redirect you to a detailed view of the product, as shown in the following screenshot:

How it works...

We started by defining the pagination setting in our ProductsController class, which defines how the results are treated when returning them via the Paginator component (previously defined in the $components property of the controller). Pagination is a powerful feature of CakePHP, which extends well beyond simply defining the number of results or sort order.

We then added an index() method to our ProductsController class, which returns the listing of products. You'll first notice that we accessed a $Product property on the controller. This is the model that we are acting against to read from our table in the database. We didn't create a file or class for this model, as we're taking full advantage of the framework's ability to determine the aspects of our application through convention. Here, as our controller is called ProductsController (in plural), it automatically assumes a Product (in singular) model. Then, in turn, this Product model assumes a products table in our database. This alone is a prime example of how CakePHP can speed up development by making use of these conventions.

You'll also notice that in our ProductsController::index() method, we set the $recursive property of the Product model to -1. This is to tell our model that we're not interested in resolving any associations on it. Associations are other models that are related to this one. This is another powerful aspect of CakePHP. It allows you to determine how models are related to each other, allowing the framework to dynamically generate those links so that you can return results with the relations already mapped out for you. We then called the paginate() method to handle the resolving of the results via the Paginator component.

Tip

It's common practice to set the $recursive property of all models to -1 by default. This saves heavy queries where associations are resolved to return the related models, when it may not be necessary for the query at hand. This can be done via the AppModel class, which all models extend, or via an intermediate class that you may be using in your application.

We had also defined a view($id) method, which is used to resolve a single product and display its details. First, you probably noticed that our method receives an $id argument. By default, CakePHP treats the arguments in methods for actions as parts of the URL. So, if we have a product with an ID of 123, the URL would be /products/view/123. In this case, as our argument doesn't have a default value, in its absence from the URL, the framework would return an error page, which states that an argument was required. You will also notice that our IDs in the products table aren't sequential numbers in this case. This is because we defined our id field as VARCHAR(36). When doing this, CakePHP will use a Universally Unique Identifier (UUID) instead of an auto_increment value.

Tip

To use a UUID instead of a sequential ID, you can use either CHAR(36) or BINARY(36). Here, we used VARCHAR(36), but note that it can be less performant than BINARY(36) due to collation.

The use of UUID versus a sequential ID is usually preferred due to obfuscation, where it's harder to guess a string of 36 characters, but also more importantly, if you use database partitioning, replication, or any other means of distributing or clustering your data.

We then used the findById() method on the Product model to return a product by it's ID (the one passed to the action). This method is actually a magic method. Just as you can return a record by its ID, by changing the method to findByAvailable(). For example, you would be able to get all records that have the given value for the available field in the table. These methods are very useful to easily perform queries on the associated table without having to define the methods in question.

We also threw NotFoundException for the cases in which a product isn't found for the given ID. This exception is HTTP aware, so it results in an error page if thrown from an action.

Finally, we used the set() method to assign the result to a variable in the view. Here we're using the compact() function in PHP, which converts the given variable names into an associative array, where the key is the variable name, and the value is the variable's value. In this case, this provides a $product variable with the results array in the view. You'll find this function useful to rapidly assign variables for your views.

We also created our views using HTML, making use of the Paginator, Html, and Time helpers. You may have noticed that the usage of TimeHelper was not declared in the $helpers property of our ProductsController. This is because CakePHP is able to find and instantiate helpers from the core or the application automatically, when it's used in the view for the first time. Then, the sort() method on the Paginator helper helps you create links, which, when clicked on, toggle the sorting of the results by that field. Likewise, the counter(), prev(), numbers(), and next() methods create the paging controls for the table of products.

You will also notice the structure of the array that we assigned from our controller. This is the common structure of results returned by a model. This can vary slightly, depending on the type of find() performed (in this case, all), but the typical structure would be as follows (using the real data from our products table here):

Array
(
  [0] => Array
  (
    [Product] => Array
    (
      [id] => 535c460a-f230-4565-8378-7cae01314e03
      [name] => Cake
      [details] => Yummy and sweet
      [available] => true
      [created] => 2014-06-12 15:55:32
      [modified] => 2014-06-12 15:55:32
    )
  )
  [1] => Array
  (
    [Product] => Array
    (
      [id] => 535c4638-c708-4171-985a-743901314e03
      [name] => Cookie
      [details] => Browsers love cookies
      [available] => true
      [created] => 2014-06-12 15:55:33
      [modified] => 2014-06-12 15:55:33
    )
  )
  [2] => Array
  (
    [Product] => Array
    (
      [id] => 535c49d9-917c-4eab-854f-743801314e03
      [name] => Helper
      [details] => Helping you all the way
      [available] => true
      [created] => 2014-06-12 15:55:34
      [modified] => 2014-06-12 15:55:34
    )
  )
)

We also used the link() method on the Html helper, which provides us with the ability to perform reverse routing to generate the link to the desired controller and action, with arguments if applicable. Here, the absence of a controller assumes the current controller, in this case, products.

Finally, you may have seen that we used the __() function when writing text in our views. This function is used to handle translations and internationalization of your application. When using this function, if you were to provide your application in various languages, you would only need to handle the translation of your content and would have no need to revise and modify the code in your views.

There are other variations of this function, such as __d() and __n(), which allow you to enhance how you handle the translations. Even if you have no initial intention of providing your application in multiple languages, it's always recommended that you use these functions. You never know, using CakePHP might enable you to create a world class application, which is offered to millions of users around the globe!

See also