Working with the Set class

One of the most debated decisions CakePHP has ever made was returning arrays as a result of a model find operation. While ORM purists may argue that each returned item should be an instance of a model class, arrays prove themselves very useful, fast, and flexible for manipulating characteristics that can be impossible to achieve with a pure object approach.

The Set class was introduced to give the developer even more power when dealing with array based data structures. With a simple method call, we can manipulate an array with ease, avoiding us the pain of having to build long and complex code blocks.

This recipe shows how to use some of the most useful methods this class provides, while introducing other available methods that may be useful under different scenarios.

Getting ready

To go through this recipe, we need some data to work with. Create the following tables, and populate them with data, by issuing these SQL statements:

CREATE TABLE `students`(
`id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY(`id`)
);
CREATE TABLE `categories`(
`id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY(`id`)
);
CREATE TABLE `exams`(
`id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
`category_id` INT UNSIGNED NOT NULL,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY(`id`),
FOREIGN KEY `exams__categories`(`category_id`) REFERENCES `categories`(`id`)
);
CREATE TABLE `grades`(
`id` INT UNSIGNED AUTO_INCREMENT NOT NULL,
`student_id` INT UNSIGNED NOT NULL,
`exam_id` INT UNSIGNED NOT NULL,
`grade` FLOAT UNSIGNED NOT NULL,
PRIMARY KEY(`id`),
FOREIGN KEY `grades__students`(`student_id`) REFERENCES `students`(`id`),
FOREIGN KEY `grades__exams`(`exam_id`) REFERENCES `exams`(`id`)
);
INSERT INTO `students`(`id`, `name`) VALUES
(1, 'John Doe'),
(2, 'Jane Doe'),
INSERT INTO `categories`(`id`, `name`) VALUES
(1, 'Programming Language'),
(2, 'Databases'),
INSERT INTO `exams`(`id`, `category_id`, `name`) VALUES
(1, 1, 'PHP 5.3'),
(2, 1, 'C++'),
(3, 1, 'Haskell'),
(4, 2, 'MySQL'),
(5, 2, 'MongoDB'),
INSERT INTO `grades`(`student_id`, `exam_id`, `grade`) VALUES
(1, 1, 10),
(1, 2, 8),
(1, 3, 7.5),
(1, 4, 9),
(1, 5, 6),
(2, 1, 7),
(2, 2, 9.5),
(2, 3, 6),
(2, 4, 10),
(2, 5, 9);

Create a controller in a file named exams_controller.php and place it in your app/controllers folder, with the following contents:

<?php
class ExamsController extends AppController {
public function index() {
}
}
?>

Create a file named exam.php and place it in your app/models folder, with the following contents:

<?php
class Exam extends AppModel {
public $belongsTo = array('Category'),
public $hasMany = array('Grade'),
}
?>

Create a file named grade.php and place it in your app/models folder, with the following contents:

<?php
class Grade extends AppModel {
public $belongsTo = array(
'Exam',
'Student'
);
}
?>

How to do it...

  1. Edit your app/controllers/exams_controller.php file and insert the following contents in its index() method:
    $gradeValues = Set::extract(
    $this->Exam->find('all'),
    '/Grade/grade'
    );
    $average = array_sum($gradeValues) / count($gradeValues);
    $categories = $this->Exam->Category->find('all'),
    $mappedCategories = Set::combine(
    $categories,
    '/Category/id',
    '/Category/name'
    );
    $gradeRows = $this->Exam->Grade->find('all', array(
    'recursive' => 2
    ));
    $grades = Set::format(
    $gradeRows,
    '%s got a %-.1f in %s (%s)',
    array(
    '/Student/name',
    '/Grade/grade',
    '/Exam/name',
    '/Exam/Category/name'
    )
    );
    $categories = Set::map($categories);
    $this->set(compact('average', 'grades', 'categories'));
    
  2. Create a folder named exams and place it in your app/views folder. Create a file named index.ctp and place it in your app/views/exams folder, with the following contents:
    <h2>Average: <strong><?php echo $average; ?></strong></h2>
    <ul>
    <?php foreach($grades as $string) { ?>
    <li><?php echo $string; ?></li>
    <?php } ?>
    </ul>
    <h2>Categories:</h2>
    <ul>
    <?php foreach($categories as $category) { ?>
    <li><?php echo $category->id; ?>: <?php echo $category->name; ?></li>
    <?php } ?>
    </ul>
    

If you now browse to http://localhost/exams, you should see the average grade for all exams, a detailed list of what each student got on each exam, and the list of all categories, as shown in the following screenshot:

How to do it...

How it works...

We start by using the Set::extract() method to extract information out of the result obtained after fetching all rows from the Exam model. The information we are interested in retrieving is the list of all grades. The extract() method takes up to three arguments:

  • path: An X-Path 2.0-compatible expression that shows the path to the information that should be extracted.

    Note

    The Set class supports only a subset of the X-Path 2.0 specification. Expressions such as //, which are valid in X-Path, are not available in Set. Continue reading this recipe to learn what expressions are supported.

  • data: The array data structure from which to extract the information.
  • options: These are optional settings. At the time of this writing, only the option flatten (a boolean) is available. Setting it to false will return the extracted field as part of the resulting structure. Defaults to true.

The path argument offers a flexible approach when defining what information we are interested in. To further understand its syntax, consider the data structure that results from fetching all Exam records, together with their Category information, and all associated Grade records:

$data = $this->Exam->find('all'),

In X-Path 2.0, the path is an expression separated by the forward slash (/), while each part in that expression represents a subpath (CakePHP's Set::extract() method also enforces a starting slash.) Therefore, the expression /children refers to a path that includes only elements named children, while the expression /children/grandchildren will select items named grandchildren that are descendents of items named children. When we refer to the name of an item, we are referring to the key in the array structure.

Note

More information about X-Path 2.0 can be obtained at http://www.w3.org/TR/xpath20.

If we intended to grab only the Exam fields (thus discarding the information regarding Category and Grade), we would use the following:

Set::extract('/Exam', $data);

This would return an array of elements, each element indexed by Exam, and having as its value all the fields for the Exam key. If we were only interested in the name field, we would add another subpath to the expression:

Set::extract('/Exam/name', $data);

We can also further limit a path by adding conditional expressions. A conditional expression filters the elements (using the Set::matches() method), by applying one of the typical comparison operators (<, <=, >, >=, =, !=) to each element that matches the path. To obtain all Grade records where the value of the grade field is less than 8, we would use the following expression (notice how the conditional expression is applied to a subpath and is surrounded with brackets):

Set::extract('/Grade[grade<8]', $data);

Instead of a comparison operator, we can use position expressions, which can be any of the following:

  • :first: Refers to the first matching element.
  • :last: Refers to the last matching element.
  • number: Refers to the element located in the position indicated by number, where number is a number greater than or equal to 1.
  • start:end: Refers to all elements starting at position start, and ending at position end. Both start and end are numbers greater than, or equal to, 1.

To filter the data set so that only the second and third elements of all Grade records are returned, using the subset of records where grade is greater than or equal to 8, and obtaining only the value for the grade field, we would do:

Set::extract('/Grade[grade>=8]/grade[2:3]', $data);

Going back to the recipe, we started by extracting only the value of the grade field for each Grade record. This Set::extract() call returns an array of grade values, so we can then use PHP's array_sum() and count() functions to calculate the average grading.

Note

A handful of examples of the Set::extract() method, and other Set methods, can be obtained from its test case. Look into your CakePHP core folder for the tests/cases/libs/set.test.php file and go through the different test cases.

We then use the Set::combine() method. This method takes up to four arguments:

  • data: The array data structure on which to operate.
  • path1: The X-Path 2.0 path used to fetch the keys of the resulting array.
  • path2: The X-Path 2.0 path used to fetch the values of the resulting array. If not specified, the values will be set to null.
  • groupPath: The X-Path 2.0 path to use when looking to group the resulting items so each item is a subitem of the corresponding group.

Using the /Category/id expression as keys and /Category/name as values, we obtain an indexed array, where the keys are the Category IDs, and the values their respective Category names.

The groupPath argument can serve useful in many scenarios. Consider the need of obtaining the grades for all exams for a particular student, grouped by the category of the exam. Using the following:

$records = $this->Exam->Grade->find('all', array(
'conditions' => array('Student.id' => 1),
'recursive' => 2
));
$data = Set::combine(
$records,
'/Exam/name',
'/Grade/grade',
'/Exam/Category/name'
);

We would obtain what we need in an easy to navigate array:

array(
'Programming Language' => array(
'PHP 5.3' => '10',
'C++' => '8',
'Haskell' => '7.5'
),
'Databases' => array(
'MySQL' => '9',
'MongoDB' => '6'
)
)

The recipe continues by fetching all grades, and then using the Set::format() method to obtain a list of formatted strings. This method takes three arguments:

  • data: The data to format.
  • format: The sprintf()-based string that contains the format to use.
  • keys: The array of X-Path 2.0 paths to use when replacing the sprintf() conversion specifications included in format.

    Note

    To learn more about the sprintf() based conversion specifications see http://php.net/sprintf.

Set::format() applies the format string to each item in the data array, and returns an array of formatted strings. In the recipe we used the string %s got a %-.1f in %s (%s). This string contains four conversion specifications: a string, a floating number (which we are forcing to only include one decimal digit), and two other strings. This means that our keys argument should contain four paths. Each of those paths will be used, in sequence to replace their corresponding conversion specification.

The recipe ends by using the Set::map() method, which can be useful if you want to deal with objects, rather than arrays. This method takes two optional arguments:

  • class: The class name to be used when creating an instance of an object. This argument is normally used to specify the data, and the tmp argument is used to specify the class name.
  • tmp: If the first argument is an array, then this argument behaves as the class argument. Otherwise it is safely ignored.

Simply calling this method with the data to convert will convert that data to a set of generic object instances, recursively. If the class argument is used, then the class name specified in that argument will be used when creating the respective object instances.

There's more...

The usefulness of the Set class does not end here. There are several other methods that were not covered in this recipe, but can help us when developing our CakePHP applications. Some of these methods are:

  • merge(): Acts as a combination of two PHP methods: array_merge() and array_merge_recursive(), allowing the proper merging of arrays when the same key exists in at least two of the arguments, and they are themselves arrays. In this case, it performs another Set::merge() on those elements.
  • filter(): Filter empty elements out of an array, leaving in real values that evaluate to empty (0, and'0')
  • pushDiff(): Pushes the differences from one array to another, inserting the nonexistent keys from the second argument to the first, recursively.
  • numeric(): Determines if the elements in the array contain only numeric values.
  • diff(): Computes and returns the different elements between two arrays.
  • reverse(): Converts an object into an array. This method can be seen as the opposite of the Set::map() method.
  • sort(): Sorts an array by the value specified in an X-Path 2.0 compatible path.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.93.0