Tag: CakePHP

Speed up your pagination with a simple hack…

Before I go into the example in this little post, let me just say that this situation won’t be applicable to everyone…

But let’s consider the following:
We have a table with tens of thousands of records, that need to be paginated.

As you know, cake will execute two queries; first to get the count of total records, second to get the actual records.

The questions one might ask are:
“Would any user really go through 5 thousand pages to find what they are looking for?”
“If I bring the last 1,000 records wouldn’t that be enough for a vast majority of needs?”
“How many pages in Google do you go through, when searching for something, before you give up?”
“Is it not better to provide filters, or search tools to help your users narrow down the results to something manageable?”

If you’ve answered “Yes” to two or more questions, please consider the hack…

In your model, which needs to be paginated, do the following:

public function paginateCount($conditions = null,
                                 $recursive = 0,
                                 $extra = array()) {
   return 1000;
}

Yep, we are overriding paginateCount() and simply returning 1,000 because we know that this will be the maximum amount of records that our paginator needs to know about.
Depending on how complex the underlying query is (for example you might have JOIN’s or various conditions, which would usually need to be taken into the account in your typical count query), the above hack can dramatically increase the performance of your pagination.

Dealing with static pages v2 (or… 3?)

Over the years of cake development we’ve seen a number of ways to get rid of the the /pages/ path in the URL for static pages.

By default if you create an “about us” page, such as in app/views/pages/about.ctp, the resulting URL would be:
www.example.com/pages/about

I’m sure you’ve seen a ton of complaints and solutions about how to get rid of this seemingly “annoying” /pages/
To keep the URL’s clean, most people would obviously prefer to have www.example.com/about instead. (No /pages/ in sight).

Recently, my favorite solution has become the following setting in the routes.php:

$staticPages = array(
		'about',
		'legal',
		'policy',
		'something'
);

$staticList = implode('|', $staticPages);

Router::connect('/:static', array(
		'plugin' => false,
		'controller' => 'pages',
		'action' => 'display'), array(
				'static' => $staticList,
				'pass' => array('static')
				)
		);

Go ahead, create your about.ctp and then attempt to access it by going to www.yoursite.com/about

The Router will nicely remove the /pages/ from the URL.

Q.
Why keep the pages in the array() and then use implode()? Couldn’t I just have the following?

$staticList = 'about|legal|policy|something';

A.
You sure could, but array structure allows to keep things organized neater and if you have a large amount of pages it’s easier to scan visually to insert/remove pages as necessary. Either way, you have both options to work with.

Be mindful of the redirect(s)

Just a couple of tidbits about CakePHP redirects, prefix routing and Auth.

1. Auth mysteriously redirects your logged in user into the abyss…

After you’ve checked all your setting in beforeFilter()'s of App Controller and relevant Controllers, it still seems like a completely bizarre situation where all of a sudden your well-authenticated users get completely kicked out of their “designated” area.
One more thing to check is any element (which is called from an “Authed” view) that might be using requestAction() from some Controller, which had not been granted the right privileges. To explain in more detail, requestAction() might attempt to access information (action) from a slightly unrelated controller, to which the current user has not been granted any permission. At this point the Auth component will kick-in and do its job by redirecting the user to a homepage or other “strange” location.
This one is always tricky to spot, since the bugger is hiding in the view/element, yet behaves as though something that should be taking place in your controllers.

2. Be explicit about your prefix routing destinations

If you have more than one routing prefix, such as “user” and “admin”, there are a few ways to move from one “prefixed” area to another.

For example:

$this->redirect(array(
   'controller' => 'users',
   'action' => 'something',
   'admin' => false
));

In many cases this will get you out of the “admin” area and move to the Users Controller.
However, depending on other (routing) issues, a more detailed instruction would be:

$this->redirect(array(
  'controller' => 'users',
  'action' => 'something',
  'admin' => false,
  'plugin' => false,
  'user' => true
));

The above situation might happen in case your something action is actually an alias of prefixed user_ action (such as user_details).
The plugin key is not as common, but also good to keep in mind.

Two ways to debug your AJAX queries

Every once in a while you might have a need to trigger an AJAX action in the controller, which should return some results from the server…

When everything works, well it is certainly all fine and dandy, but if things go haywire, first thing to check out is the response given by the server… and more importantly why does it not match your expectations. One of the more typical questions is: “What query was executed on the DB, during the AJAX call”?…
Because AJAX calls are not easily logged to any console, sometimes it becomes a little troublesome to figure out.

When using CakePHP you have two methods to see what’s happening on the DB level.

1. If you use debug kit

There is one little known, but nice feature.
After you load the page, which executes some AJAX request… do the following:
1. Click the “History” tab
2. In the list you should see the given AJAX call to some controller action
3. Click it
4. Switch over the to the SQL log tab and see what has happened.
(The background of the queries should be mild green, which will signify that you are looking at a previous request in the “history” of calls)

… amazing feature indeed. Yet, unfortunately, it doesn’t work 100% of the time. (I wish I had a more in-depth answer as to why).

2. Do not despair, there is another way to log your SQL dealings with a few lines of code.

Using the same scenario as above, in your action which is being triggered by AJAX from the client, add the following snippet:

$db = ConnectionManager::getDataSource('default');
debug($db->getLog());

Now, if you are using firebug (or similar tool, which I hope you certainly do)… You’ll see the exact query and some other debug info generated by the cake’s DB drivers.

p.s. Thanks to jrbasso, for this wonderful and helpful hint.

Checking for SSL and then some…

A simple way to check and enforce SSL can be done by using the RequestHandler component.
Here’s an example:

private function checkHttps() {
  if(!$this->RequestHandler->isSSL()) {
    return $this->redirect('https://' . env('SERVER_NAME') . $this->here);
  }
}

Simple enough, right?

Yet there is a little caveat, which causes a problem in a specific load balanced environment.
To give a little further insight into the whole situation, it is not uncommon nowadays to setup your SSL certs on the load balancer and have it handle the decryption, while passing “regular” HTTP requests down to the web servers.

So what happens in this case?
If we use the approach described above what would happen is that load balancer would handle the HTTPS request and pass a regular HTTP request to apache (or your web server).
At this point the application would say: “Wait this is not secure, redirect the URL to HTTPS… and so on to the point where we’d get stuck in the infinite loop of redirects).

How to fix?

private function checkHttps() {
  $lbEnv = env('HTTP_X_FORWARDED_PROTO');
  if (!is_null($lbEnv) && (env('HTTP_X_FORWARDED_PROTO') != 'https')) {
    return $this->redirect('https://' . env('SERVER_NAME') . $this->here);
  }
}

Catch database errors before it’s too late

A quiet, little method inside model.php, can actually help us out quite a bit.
I am talking about the onError() callback.

Here’s a snippet from the API:

/**
* Called when a DataSource-level error occurs.
*
* @access public
* @link http://book.cakephp.org/view/1048/Callback-Methods#onError-1056
*/
function onError() {
}

And the little snippet below will log your DB error (presuming you are using ‘default’ connection) as well as the data that can be potentially causing the problem… well that would really depend on the actual error.

Thanks to NetersLandreau for providing the code:

class AppModel extends Model {
       public function onError() {
               $db = ConnectionManager::getDataSource('default');
               $err = $db->lastError();
               $this->log($err);
               $this->log($this->data);
       }
}

Simple, yet very useful. Nice.

A simple shell to hash your passwords

If you are moving data into a CakePHP environment and the passwords in the existing DB aren’t hashed, you can use the simple shell provided below to hash them according to the cake’s standard.
In case the passwords are already hashed using a different method, this is not going to be of any use. Instead use Security::setHash() to set the method according to your needs.

The code should be simple to understand and usage example is provided in the comments.
(Any questions or feedback, just post a comment…)

Save the following into: app/vendors/shells/password_hash.php

<?php
/**
 * Importing the security component,
 * which is used to hash the passwords
 */
App::import('Core', 'Security');

/**
 * Hashes passwords in the given table
 * Usage: "cake password_hash" (to run against the default users table and password field)
 * Or: "cake password_hash -table some_table -field another_field"
 *
 * Strict mode can be turned off, this way
 * any password even potentially a hashed one already will be re-hashed
 * see below for more info on how that works. From command line:
 * "cake password_hash -strict false"
 * Or: "cake password_hash -strict 0"
 *
 */
class PasswordHashShell extends Shell {

/**
 * True/False to keep the shell from
 * showing any output
 *
 * @var boolean
 */
	private $silent = FALSE;

/**
 * Holds the instance of the current model,
 * where the hashing needs to happen
 * (Defaults to User model)
 *
 * @var object
 */
	private $model = 'User';

/**
 * Field to hash
 * (Defaults to password)
 *
 * @var string
 */
	private $field = 'password';

/**
 * If the strict (mode) is set to TRUE
 * the shell will ignore any 40 char passwords
 * (40 chars is the default length of cake's hashed password)
 *
 * From the shell we can specify the strict mode as:
 * cake password_hash -strict false/0
 *
 * @var boolean
 */
	private $strict = TRUE;

/**
 * Generic initialization function
 *
 * (Always a good idea to call the parent method)
 *
 */
	public function initialize() {
		parent::initialize();
	}

/**
 * Starting up the shell
 * Initializing shell properties based on the passed in params,
 * or falling back to defaults
 */
	public function startup() {
		if (isset($this->params['silent']) && $this->params['silent']) {
			$this->silent = TRUE;
		}

		if (isset($this->params['table']) && $this->params['table']) {
			$modelName = Inflector::classify($this->params['table']);
			$this->model = ClassRegistry::init($modelName);
		} else {
			$this->model = ClassRegistry::init($this->model);
		}

		if (isset($this->params['field']) && $this->params['field']) {
			$this->field = $this->params['field'];
		}

		if (isset($this->params['all']) && $this->params['all']) {
				$this->strict = FALSE;
		}
	}

/**
 * Main method is launched autmoatically
 * Works as a dispatcher.
 */
	public function main() {
		$this->processField();
	}

/**
 * Takes care of finding, hashing and saving the field
 * (Uses standard CakePHP hashing method)
 * If strict mode is enabled all 40 char passwords,
 * which is default hashed length are ignored.
 */
	private function processField() {
		$conditions = 1;
		if($this->strict) {
			$conditions = array(
					'LENGTH(`' . $this->field . '`) <>' => 40
			);
		}

		$fieldsToHash = $this->model->find('all', array(
				'recursive' => -1,
				'fields' => array($this->model->primaryKey, $this->field),
				'conditions' => $conditions
		));

		foreach($fieldsToHash as $field) {
			$fieldValue = $field[$this->model->alias][$this->field];
			$recordId = $field[$this->model->alias][$this->model->primaryKey];

			$fieldValue = Security::hash($fieldValue, NULL, TRUE);

			$this->out('Changing password for: ' . $recordId);
			$this->out('Changed to: ' . $fieldValue);

			$this->model->create();
			$this->model->id = $recordId;
			$data = array(
				$this->model->alias => array(
						$this->field => $fieldValue
				)
			);
			$this->model->save($data, array(
					'validate' => FALSE,
					'callbacks' => FALSE
			));
		}
	}

/**
 * Shell output
 *
 * @param string $string
 * @param boolean $newline
 * @return boolean
 */
	public function out($string, $newline = true) {
		if(!$this->silent) {
			parent::out($string, $newline = true);
		}
		return FALSE;
  }

/**
 * Displays help contents
 *
 * @access public
 */
	public function help() {
		$this->_stop();
	}
}
?>

Solving the country/state problem with CakePHP and jQuery

I’d say that it is a pretty common issue (and maybe not quite “a problem”), where you would have a page with a billing form, which in turn, has a “Country” select as well as a “State” select.
Obviously not all countries have states, and thus comes the question of how to handle the situation.

Before going much further, I’d like to say that the whole point of the post is to show a solution to the common problem, but by no means it is the de facto standard of handling the form fields (as a matter of fact, I’ll say that there simply isn’t one).
If you consider every billing form that you’ve had to fill out in your internet life, each one, most likely, behaved differently in some way from the others.
Country, for which the application is purposed, and thus the target audience, not to mention the specific business needs of the application should guide the actual implementation.

For this example, we’ll do something like this:

  1. If selected country is “US” show the select input with the states
  2. For any other country change the select to a text input (allowing the user to enter what they please, if required)

Hmmm… a seemingly simple solution quite often requires a little bit of hackery. Or does it?
For security (or browser-related) reasons you cannot change an input type on the fly, otherwise it would be a perfectly reasonable thing to do.

… So how, after all, do we tackle this with cake and jQuery, without hacking around the problem?

First let’s consider this little snippet of a CakePHP form (view):

<div id="state-wrapper">
  <?php echo $this->Form->input('state', array('id' => 'state-select')); ?>
  <?php echo $this->Form->input('state', array('type' => 'text', 'id' => 'state-text')); ?>
</div>

As you see we have two nearly identical inputs. (Please note the different DOM id’s for later).
Also, you can guess which one is the select and which one will be a text input ;)

Now comes the fun jQuery code:

$(document).ready(function()  {
  stateText = $('#state-text').detach();
  stateSelect = $('#state-select').detach();

  checkSelected(stateText, stateSelect);

  $('#country-select').change(function() {
    checkSelected(stateText, stateSelect);
  });
});

function checkSelected(stateText, stateSelect) {
  if ($('#country-select').val() != 'US') {
    stateText.appendTo('#state-wrapper');
    stateSelect.detach();
  } else {
    stateSelect.appendTo('#state-wrapper');
    stateText.detach();
  }
}

Let’s take a closer look at this…
Upon the initial load of the page we detach() both the select and text inputs from the DOM.

First of all… what is detach()?
Yet, another beauty of jQuery, which was introduced sometime in 1.4.
The detach() method actually removes the element from the DOM… but (unlike a more common remove()) it keeps all the element’s properties, data, associated jQuery events and everything else that comes with it. This allows us to later inject the “detached” element, as though it never “left”, back into the DOM.

So, once both elements are “detached”, we go ahead and check the currently selected country.
Here, the code should be pretty simple:

  1. If country does not equal “US”, show (via appendTo()) the text input
  2. Otherwise, show the select input (with all the pre-loaded states, as they were before the “detachment”).

Ah, The power of jQuery.

p.s. A more elegant solution would be to keep a list of states/provinces/regions/etc. for alll countries, which require one.
With that we could populate the state select via AJAX once such country is selected… or remove the field altogether. Surely this option would provide more consistency and data integrity, but introduce a bit of maintenance overhead. However, I’ve already showed this approach in an older post, so we’ll just leave things a little different and simpler this time.

Self-adjusting credit card expiration year

Once in a while, it happens, that you need to build a payment form with credit card expiration year as a select input.

With CakePHP’s form helper it’s pretty easy, but what’s even nicer is that you don’t have to hard-code any values for the actual years (and then have to remember to update them as the time goes on).

With the little snippet below we build a select input which starts and defaults to the current year and goes up to seven years ahead.

(So at the time of writing the select input range is: 2010 – 2017)

$this->Form->input('card_expiration_year', array(
                           'type' => 'date',
                           'maxYear' => date('Y', strtotime('+ 7 years')),
                           'minYear' => date('Y'),
                           'dateFormat' => 'Y',
                           'default' => date('Y')));

p.s. Why seven years? (Not sure, just like the number).

Make sure your app runs within the specified time zone…

Update: 8/2/2010 As red pointed out in the recent versions of cake this is handled through core.php:
http://github.com/cakephp/cakephp/blob/master/app/config/core.php#L247

For those stuck with the earlier versions of cake, but newer version of PHP (5.3+)… please see below:

Even with a simple shared hosting, the server time zone might differ from the time zone where you do business in, yet this becomes more important if you have multiple servers (perhaps even geo-distributed).

If you rely on fields like created and modified to do some basic reporting… or maybe you simply don’t wish to add/subtract server time difference against your region of business, the following one-liner will solve all your trouble.

In bootstrap.php:

date_default_timezone_set('America/New_York');

This is what I have in mine, since we are running on the Eastern Standard US time.
Of course, you’d set the zone per your specific requirement.