Multi-record Forms

6 08 2007

One question that comes up time and again on the Google Group is “How do I make a single form for more than one record?”, which I call multi-record forms. Multi-record forms have a repeated section in the form for multiple entries of the same data. The example that I am going to use is an Author with many Books.

In CakePHP this technique will be most commonly used with hasMany associations. Note that this technique is not specific to CakePHP but the code and conventions used are.

Building the Form

Our example form consists of an Authors name, date of birth and up to 5 book records with title, year of publication and publisher.

Here is the view.

<form action="<?= $html->url('/authors/add'); ?>" method="post">
  <fieldset>
    <legend>Author Details</legend>
    <?= $form->input('Author.name'); ?>
    <?= $form->input('Author.dob', array('type' => 'date')); ?>
  </fieldset>
  <fieldset>
    <legend>Books Published</legend>
    <?php for ($i=0; $i<5; $i++) : ?>
    <fieldset>
      <legend>Book<?= $i; ?></legend>
      <label for="BookTitle<?= $i; ?>">Title</label> <input type="text" name="data[Book][<?= $i; ?>][title]" id="BookTitle<?= $i; ?>" />
      <?= $form->error('Book.'.$i.'.title'); ?><br />
      <label for="BookPublication<?= $i; ?>">Publication Year</label> <input type="text" name="data[Book][<?= $i; ?>][publication]" id="BookPublication<?= $i; ?>" />
      <?= $form->error('Book.'.$i.'.publication'); ?><br />
      <label for="BookPublisher<?= $i; ?>">Publisher</label> <input type="text" name="data[Book][<?= $i; ?>][publisher]" id="BookPublisher<?= $i; ?>" />
      <?= $form->error('Book.'.$i.'.publisher'); ?>
    </fieldset>
    <?php endfor; ?>
  </fieldset>
</form>

The first fieldset is the Author details and is a constructed like a regular form. The Books Published fieldset contains 5 sub-fieldsets that are created by a loop.

Each sub-fieldset contains inputs with names in the format:
name="data[ModelName][index][fieldName]"
N.B. Notice that there are no quotes surrounding the text in the square brackets.

Using existing Data

For an edit page the majority of the view would be the same. The loop would change to:

<?php for ($i=0; $i<count($this->data['Book']); $i++) : ?>
<fieldset>
  <legend>Book<?= $i; ?></legend>
  <input type="hidden" name="data[Book][<?= $i; ?>][id]" id="BookId<?= $i; ?>" value="<?= $this->data['Book'][$i]['id']; ?>" />
  <label for="BookTitle<?= $i; ?>">Title</label> <input type="text" name="data[Book][<?= $i; ?>][title]" id="BookTitle<?= $i; ?>" value="<?= $this->data['Book'][$i]['title']; ?>" />
  <?= $form->error('Book.'.$i.'.title'); ?><br />
  <label for="BookPublication<?= $i; ?>">Publication Year</label> <input type="text" name="data[Book][<?= $i; ?>][publication]" id="BookPublication<?= $i; ?>" value="<?= $this->data['Book'][$i]['publication']; ?>" />
  <?= $form->error('Book.'.$i.'.publication'); ?><br />
  <label for="BookPublisher<?= $i; ?>">Publisher</label> <input type="text" name="data[Book][<?= $i; ?>][publisher]" id="BookPublisher<?= $i; ?>" value="<?= $this->data['Book'][$i]['publisher']; ?>" />
  <?= $form->error('Book.'.$i.'.publisher'); ?>
</fieldset>
<?php endfor; ?>

We simply changed the loop to terminate when all the books have been looped over, added a value attribute to the inputs and added an hidden id field so that our records get updated correctly.

Saving the Submitted Form

To save the submitted form we loop over each index in the Books array and save it to the database with the authors id.

It is important that Model::create() gets called during each iteration otherwise you will end up inserting on the first iteration and then updating each subsequent iteration.

By using Model::set() we can take advantage of validation rules in the model. However, any validation error messages wont be associated with the correct Book fields. To get around this we manually invoke Model::validates() and if this fails we reassign the invalid fields to a temporary array. Then if there are any errors we feed this temporary array back to the model so that the view has the correct data.

function add(){
  if (!empty($this->data)) {
    if ($this->Author->save()) {
      // grab the id of the Author we just saved
      $author_id = $this->Author-id;
      
      $invalidBookFields = array();
      foreach($this->data['Book'] as $index => $book) {
        $book = array('Book' => $book);
        $book['Book']['author_id'] = $author_id;

        // clear any previous Book data
        $this->Author->Book->create();
        // We set the Book data this way so that validation is processed correctly
        $this->Author->Book->set($book);
        if (!$this->Author->Book->validates()) {
          // save the validationErrors and reset for the next iteration
          $invalidBookFields[$index] = $this->Author->Book->validationErrors;
          unset($this->Author->Book->validationErrors);
        } else {
          $this->Author->Book->save(); 
        }
      }
      
      if (empty($invalidBookFields)) {
      	// success - set message or redirect
      } else {
      	// put all the errors back in the model so they make it back to the view
      	$this->Author->Book->validationErrors = $invalidBookFields;
      }
    }
  }
}

Enhancements

If you need to make sure that all Books are valid before saving any of them you will need to loop the Books array twice. The first loop should validate each Book, then if there are no errors the second loop saves each Book.

If you do not know before hand how many items you need on your form you can use javascript or AJAX to dynamically add form elements as you need them.





Using AclBehavior in CakePHP 1.2

15 07 2007

CakePHP 1.2 has a new AclBehavior which makes using the inbuilt ACL easier. AclBehavior makes the association between your model and the Aro (or Aco) database entry automatic. It handles the insert, updates and deletes whenever you modify your model. All that is required to use the AclBehavior is a function called parentNode().

AclBehavior can be used for creating AROs or ACOs. Any model may be an Access Request Object, but typically it is either the User model or a Group model that authorization is done on. In either case it is the same for any model you choose. AclBehavior defaults to creating AROs which is what we want. As for ACOs, usually these are controllers and/or actions, which don’t lend themselves to the AclBehavior so well.

First step is to set up your database ACL tables with

cake acl initdb

Basics

You need to include the behaviour and create a function called parentNode. This is the minimum required to use AclBehavior.

<?php
class User extends AppModel {
  var $name = 'User';
  var $actsAs = array('Acl');

  function parentNode(){
  }
}?>

What is the parentNode() function for?

It is used when you have cascading, or inherited permissions, for example a Group model. The parentNode() function must return the id of the parent Model (not the id of the parent Aro). Typically this will be the parent_id of your Model, if you are using the standard convention for trees in Cake.

<?php
class Group extends AppModel {
  var $name = 'Group';
  var $actsAs = array('Acl');

  function parentNode(){
    if (!$this->id) {
      return null;
    }
		
    $data = $this->read();
		
    if (!$data['Group']['parent_id']){
      return null;
    } else {
      return $data['Group']['parent_id'];
    }
  }

}
?>

That’s it. Your model is now associated with an Aro entry in the aro table in your database.

Further Reading

Keep an eye out for my upcoming article on the Auth Component. See my post Using AuthComponent and ACL in CakePHP 1.2
Also see Groups with ACL for using groups and users together
In the meantime I highly recommend the article Access Control for all (Part 1) by AD7six





All About Validation in CakePHP 1.2 – Part 2

6 07 2007

In my previous article I discussed the new constructs for Model::validate. This article follows on from this and is a run down of the new validation methods and rules available.

First up there are a couple of regular expressions defined, which are the same from CakePHP 1.1 and are pretty self explanatory. These are VALID_NOT_EMPTY, VALID_NUMBER, VALID_EMAIL, and VALID_YEAR (between 1000-2999).

The general format of the following validation methods is

var $validate = array('field' => array('rule' => 'ruleName'));

Where parameters are required, such as between and cc, the format is

var $validate = array('field' => 
        array('rule' => array('ruleName', 'param1', 'param2'));

alphaNumeric
Allows only digits and a-z or A-Z.

between
Checks that a strings length is between a min and max value.

blank
Checks if a field is empty and treats whitespace characters as empty.

cc
Credit Card Number validation, includes luhn check and Card Type to number format. Takes one parameter which can be one of :-

  • fast – skips Card Type to number format check
  • all – checks the number against all card types until it finds a match
  • array of card types – like all but limited to a subset of cards.

e.g.

var $validate = array('field' => 
        array('rule' => array('cc', array('Visa')));

compare
Allows you to compare two numeric values. Takes two parameters:-

  • Operator – one of <, >, <=, >=, == or !=
  • Comparison value to compare against

custom
Allows you to use custom regular expressions. Takes the custom regex as the only parameter.

date
Validates a string as a date. Can take one parameter;

  • Format – default is ‘ymd’. other options are:
    • dmy
    • mdy
    • ymd
    • dMy – short or long month names
    • Mdy
    • My
    • my

decimal
Checks that a number has a decimal point or is scientific notation. Takes the number of decimal places required after the point as the only parameter. If places is null it will check for scientific notation.

email
Checks for a valid email address. If a parameter of true is passed it will also attempt to verify the host. If the parameter passed is false, or none is passed it behaves the same as VALID_EMAIL.

ip
Checks for IPv4 dot notation. e.g. 192.168.0.1

minLength
Checks a string for a minimum length. Length is passed as the only parameter.

maxLength
Checks a string for a maximum length. Length is passed as the only parameter.

money
Checks that a string is numbers, optionally grouped into blocks of 3 separated by a space, comma or period, with an optional block of 2 at the end. Can take a parameter of ‘right’ if you expect the currency symbol at the end, the default is at the start.

numeric
Simply calls is_numeric()

phone
Checks for a valid phone format. Takes regex and country as parameters. Currently only supports ‘us’ country option.

postal
Checks for a valid post code format. Takes regex and country as parameters. Currently only supports ‘us’, ‘uk’ and ‘ca’ country options.

ssn
Checks for a valid social security number format. Takes regex and country as parameters. Currently only supports ‘us’, ‘dk’ and ‘nl’ country options.

url
Checks for valid URL format. Supports http(s), ftp(s), file, news and gopher protocols

userDefined
Calls a userdefined method of the current model passing along any parameters. , the first of which is the method to call. Personally I think this is redundant as you can simply replace userDefined with your method name and it works the same.

There are also some incomplete methods listed below that will be coming soon.

number – checks that a number is within a given range
multiple – will be used for selects and multiple selects
equalTo – direct comparison to another value
file – checks for a file.





All About Validation in CakePHP 1.2

3 07 2007

Validation in v1.1 of CakePHP was quite simple, and many found it lacking in features and flexibility. This is evidenced by the number of alternatives that people have written such as Daniel Hofstetter, Evan Sagge and Adeel Khan‘s ruby-esque approach.

However in CakePHP 1.2 there has been a major rework of the Validation class‘s inner workings, and the way Model::invalidFields() works.

The New $validate

The new $validate can take a number of different constructs now. There are 3 ways to define your validation rules and you can mix and match as needed.

Construct 1: CakePHP 1.1 way
The old CakePHP 1.1 $validate construct will still work

var $validate = array('fieldName' => 'ruleName')

Construct 2: Single Rule per field
You can now define more complex rules using the following construct. (Parameters explained later)

var $validate = array(
  'fieldName' => array(
    'rule' => 'ruleName' // or 'rule' => array('ruleName',  'param1', 'param2' ...)
    'required' => true,'allowEmpty' => false,
    'on' => 'create', // or update
    'message' => 'Your Error Message'
    )
  );

Constrct 3: Multiple Rules per field
Similar to Construct 2, however you can define multiple validation rules for a single field.

var $validate = array(
  'fieldName' => array(
    'rule_index' => array(
      'rule' => 'ruleName' // or 'rule' => array('ruleName',  'param1', 'param2' ...)
      'required' => true,'allowEmpty' => false,
      'on' => 'create', // or update
      'message' => 'Your Error Message'
      )
    )
  );

The new Parameters

The new $validate supports a number of parameters. The only required parameter is ‘rule’.

rule – mixed:
The Rule parameter defines the validation method and takes either a single value or an array. Rule may be (in order of preference) a method of your model, a method of the Validation class or a Regular Expression. If Rule is an array and ‘ruleName’ is a method, all other members of the array will be passed to the method. eg.

var $validate = array('username' => array('rule' => array('between', 6, 20)));

allowEmpty – bool:
This defines the behaviour when an empty value for the field is found. ‘allowEmpty’ => false will cause the validation to fail when the field data is empty. N.B: This rule is only enforced when there is an actual fieldName index in the data array.

Default is false.

required – bool:
‘required’ => true means that an index with fieldName must exist in the data array, i.e. validation will fail if isset($data[‘ModelName’][‘fieldName’]) fails. N.B Required does not care if the value is empty – see allowEmpty above.

Default is false.

on – string (‘create’ or ‘update’):
If on is defined , the validation rule will only be applied on model ‘create’ or ‘update’. If not defined it is applied everytime. This may be useful for situations such as created_by is required on record creation, but on update it must not be defined.

Default is null, i.e. apply rule everytime.

message – string:
Message is the error message that will be stored in validationErrors. If message is not define, it will attempt to use rule_index (in Construct 3) if it is a string, otherwise it will default to ‘This field cannot be left blank’.

There is also another parameter ‘last’ defined in the code but it is not used as yet. Not sure what it’s use will be either.

A Full Example

This is the actual validate variable from my User model. It contains a mixture of the above constructs and techniques.

var $validate = array(
  'username'   => array(
    VALID_NOT_EMPTY,
    'alphanumeric' => array(
      'rule' => 'alphanumeric',
      'message' => 'Username may only consist of letter and numbers'), 
    'length' => array(
      'rule' => array('between', 6, 20),
      'message' => 'Username must be between 6 and 20 characters in length')
  ),
  'name'     => VALID_NOT_EMPTY,
  'email'   => array(
    'Invalid email format' => VALID_EMAIL, 
    VALID_NOT_EMPTY
  ),
  'passwrd'   => array(
    VALID_NOT_EMPTY,
    'length' => array(
      'rule' => array('minLength', 6),
      'message' => 'Password must be at least 6 characters in length'),
    'strong' => array(
      'rule' => 'isStrong', 
      'message' => 'Your password is not strong enough')
  )
);

Disclaimer: My knowledge of the new validation was gained from the Bakery Article Multiple Rules of Validation, CakeBaker’s article Validation with CakePHP 1.2 and mostly from looking at the source code. If I have interpreted something incorrectly, please let me know.





Validation Gotcha in CakePHP 1.2

26 06 2007

While trying to write a simple login function for a user model today I came across a small gotcha that it is not apparent at first.

In CakePHP 1.1 if you want to manually validate a model you would write:

if ($this->Model->validates($this->data)){
    //success
}

However in CakePHP 1.2 this throws a nice deprecated warning:

Warning (512): (Model::validates) Parameter usage is deprecated, set the $data property instead [CORE/cake/libs/model/model.php, line 1660]

I first tried just an empty validates() expecting it to use Controller::data, however this was appearing to pass validation. Not good when the entire form was empty with VALID_NOT_EMPTY’s on every field. A quick look-see in the source of model.php reveals that Model::invalidFields() expects Model::data to be populated.

As pointed out by nate in the comments my original solution is not recommended.

The updated solution is to use:

$this->Model->set($this->data);
if ($this->Model->validates()){
    // success
}




CakePHP n00b Tip #1

14 06 2007

This marks the start of an ongoing series of tips for newcomers to Cake. It will be based on posts to the Cake PHP Google Group as well as things that I myself come across.

Action parameters from the Url

Passing parameters to actions via the url is simple. Urls are in the format http://site.com/controller/action/param1/param2 and so on. Your actions will be called with parameters in the order they are in the url .ie action($param1, $parm2)

This is a common scenario when using hasMany, for example Company hasMany Employees. On the company details view you want to add a “New Employee” link that automatically assigns the company_id.

Read the rest of this entry »