Unexpected behavior with drop-down option order using minYear, maxYear in CakePHP

I noticed something interesting when testing a checkout form today build in CakePHP. The options for the drop-down were in descending order. The options were for the credit card expiration date field. The range was set with minYear and maxYear attributes.

The code in my view (CakePHP version 1.3.7).

echo $this->Form->input('cc_expires', array('type' => 'date',
  'label' => 'Expiration Date',
  'dateFormat' => 'MY',
  'empty' => true,
  'separator' => ' ',
  'minYear' => date('Y'),
  'maxYear' => date('Y', strtotime('+7 years')));

Although the options range is correct, this seemed unintuitive. In addition, I felt it was slightly poor usability. So I wanted to fix the order.

CakePHP default option order

CakePHP default option order

I dug around in The Book. Nothing. I was about to submit a ticket. But before I do, I typically check my version and the core. Upon searching for minYear I found the function in question – year(). Apparently an undocumented attribute exists – orderYear. After adding 'orderYear' => 'asc' to the options array I got the desired output.

Two notes here. First, CakePHP has many undocumented features. It never hurts to dig around the core. Second, the orderYear attribute is completely unnecessary in this case. In fact, it is only used for the *year* drop-down. It could be easily determined from comparing minYear and maxYear. In this case, minYear is 2012 which is less than maxYear is 2019. Display in ascending order.

Control option order using orderYear for year in CakePHP

CakePHP option order with orderYear

Maybe orderYear has uses. But today it wasted my time.

</rant>

CakePHP Auth Component loginRedirect Behavior

The other day I noticed some unexpected behavior for one of the web applications I developed in CakePHP using the Auth Component. After logging in, I was redirected to my previously visited page. This would be expected had my session expired or I needed to log in first. However, this occurred after logging out of the web application.

After debugging the login() and logout() actions I noticed that Auth.redirect was not being cleared from the session. I inspected the core file for the Auth Component (auth.php) to see when Auth.redirect updated. It turns out that the startup() method had some interesting code.


if ($loginAction == $url) {
  // ...
  if (!$this->Session->check('Auth.redirect') && !$this->loginRedirect && env('HTTP_REFERER')) {
    $this->Session->write('Auth.redirect', $controller->referer(null, true));
  }

The interesting piece is the inclusion of $this->loginRedirect. According to the documentation:

The AuthComponent remembers what controller/action pair you were trying to get to before you were asked to authenticate yourself by storing this value in the Session, under the Auth.redirect key. However, if this session value is not set (if you’re coming to the login page from an external link, for example), then the user will be redirected to the URL specified in loginRedirect.

Yet, according to the code above, $loginAction sets Auth.redirect unless it or loginRedirect is set. I added the following code to my Auth Component configuration:

'Auth' => array('autoRedirect' => false, 'loginRedirect' => '/', …

After doing so, I expected that the Auth Component would no longer remember the requested URL if I was not logged in. But, contrary to the documentation, I was still redirected when attempting to access a secure area of the site before logging in.

Honestly, my head exploded on this one. I’m still a little fuzzy on loginRedirect. But here’s my two cents. The original issue only occurred after logout. Since logout redirects to loginAction, the referrer was the previously requested page. Although logout() cleared Auth.redirect, the code above stored the referrer in Auth.redirect. Upon setting loginRedirect, the logic above failed. Since this code only runs when $loginAction == $url, it does not prevent Auth from remembering the requested URL when it matters.

I find many developers dislike the Auth Component. Whether it’s too complex or not enough features, I don’t know. What I do know is there is value in using native functionality. So, to be clear, this post is about the unclear documentation for the loginRedirect property and not bashing the Auth Component.

Guidance Text using CakePHP’s Form Helper

Often form fields have guidance text. Some simple example of how the data should be entered, like Enter your username. In the application world (e.g. iPhone) this appears within the field. HTML5 offers this same effect as a new attribute. However, at the moment you would need to implement an over label solution using CSS and or JavaScript. Either way, the instructional text or markup needs to exist for that form element. Furthermore, if the text were more instructional, you may not want to use <label> as the markup.

In CakePHP this presented a challenge. If you are using the Form Helper, the markup is generated for you. I wanted a solution that would inject my guidance text or instructional markup into CakePHP’s generated output.

By default, the following simple method:

<?php echo $this->Form->input('username'); ?>

will output:

<div class="input text required">
    <label for="UserUsername">First Name</label>
    <input type="text" id="UserUsername" maxlength="20" name="data[User][username]">
</div>

I was struggling to figure out how to achieve the following output:

<div class="input text required">
    <label for="UserUsername">Username</label>
    <input type="text" id="UserUsername" maxlength="20" name="data[User][username]">
    <p class="guidance">Must be the same as your AD Account</p>
</div>

Then I found it. The Form Helper input method accepts options of before, after, separator, etc. I had originally assumed options were only for the automagic date fields. But sure enough, when tried the following, I achieved my desired output:

<?php echo $this->Form->input('username', array('after' => '<p class="guidance">Must be the same as your AD Account</p>')); ?>

I look forward to the evolution of options passed into the Form Helper. In this specific case, personally I would have felt better about setting a guidance option rather than the after. Maybe with the adoption of HTML5, CakePHP may very well offer such an option. Until then, I hope this helps.

Conditional Validation in CakePHP

I’ve been using CakePHP as the framework for an Intranet project. Although new to CakePHP, I knew that for a long term project an MVC, rapid development framework provided a strong foundation. That has proven itself time and again in the last 9 months. Sometimes though, determining how to do something within a framework can be difficult.

The other day, I had a requirement for one of the applications for conditional data validation. CakePHP provides flexible and simple validation of your Model data that combined forms very complex rules. Unfortunately, nothing out of the box handled conditional validation.

To be fair, there are some native options that may work for you in simple cases. You could add 'allowEmpty' => true to your rules. Yet, setting anything at the Model rule level applies across all Controllers. Conditional validation validates the Model one way in Controller A, but differently in Controller B. Yes, I know that’s lame for data integrity. But we developers must support real world demands from clients. Anyway, there are also ways to validate from the controller. These looked promising. But in practice, the code quickly becomes bloated. You begin to have calls to validate() before every save(). Even more for multi-model saveAll() calls. You also have to list the Model fields to validate in the Controller. Too much coupling in my opinion. So this becomes a maintenance issue beyond a few isolated cases. Another alternative is create custom validation rules. Similar to the above though, code becomes bloated outside a few rules. In addition, you are now responsible for validating the data yourself and can’t take advantage of CakePHP’s core rules (e.g. email, ssn).

Before I began developing anything, I did a quick Google search. The only thing I found was an outdated Model Behavior. So I decided to make my own with the following goals:

  • I wanted it to be as native as possible. This meant keeping the core validation rules. I merely wanted to enable, disable, or modify validation rules on the fly.
  • I wanted to keep with Fat Models, Skinny Controllers. Meaning low coupling – always a plus. This way the controller simply tells the model how to validate from a high level. Not explicitly what to validate.

So here’s what I came up with. Validation rules could be set by passing a condition to a Model method. All the logic to setup the appropriate validation is encapsulated within this method. The controller simply calls this function with the argument to perform conditional validation for that Model. An example call would be:

$this->Employee->setValidationRules('it');
...
$this->Employee->save($this-data);

If no conditions are passed, the Model would use the default validation rules. In order to set these automatically, I overrode the constructor method. The reason I put this here instead of leaving it as a Model property was to allow the Controller to reset the validation rules.

function __construct($id = false, $table = null, $ds = null) {
    parent::__construct($id, $table, $ds);

    $this->setValidationRules();
}

function setValidationRules($condition = null) {
    if ($condition == 'it') {
        // turn off field requirements for nea_it users as they don't have this data yet
        unset($this->validate['computer_type']);
        unset($this->validate['computer_service_tag']);
        $this->validate['company_email'] => array('boolean' => array('rule' => array('email', 'allowEmpty' => true)));
    }
    else {
        // default validation rules
        $this->validate = array('rental_car_cards' => array('boolean' => array('rule' => array('boolean'))),
            'company_car' => array('boolean' => array('rule' => array('boolean'))),
            'company_car_allowance' => array('boolean' => array('rule' => array('boolean'))),
            'company_cell_phone' => array('boolean' => array('rule' => array('boolean'))),
            'company_cell_phone_plan' => array('dependent' => array('rule' => array('notempty'))),
            'computer' => array('boolean' => array('rule' => array('boolean'))),
            'computer_type' => array('dependent' => array('rule' => array('notempty'))),
            'computer_service_tag' => array('dependent' => array('rule' => array('notempty'))),
            'company_email' => array('boolean' => array('rule' => array('email'))));
    }
}

It’s primitive. But it does satisfy the client’s spec and meets my goals. I think adding some callback hooks to reset the validation automatically could be helpful. Then conditional validation would behave much like bindModel() or $this->Model->recursive where it only effects the next call to the Model. Which is more Cakeish. Yet validating data on the same Model under two different conditions in the same request is probably very rare. In the end, I think it’s a straightforward way to do conditional validation in CakePHP.

CakePHP not inheriting custom app_controller and app_model

My CakePHP project fell down a rabbit hole earlier over something in hindsight was a no-brainer. I created a custom app_controller.php and app_model.php in my app directory. I copied the respective files from cake/libs/controller and cake/libs/model. I added my customizations and refreshed the page. Nothing. I checked the filenames and output a few debug() calls. Still nothing. I added beforeFilter() with just an echo. Nothing! My controllers and models weren’t inheriting any of the custom parent methods. Finally, it hit me – clear the cache. It worked.

Maybe that is a rookie mistake. Nonetheless, hopefully that saves someone the 15 minutes I lost. Clearing the cache was actually the solution to a problem a few months back. Which is actually the only reason I tried it. So when in doubt with CakePHP, clear the cache.