Optimizing CakePHP Websites

Published on Jan 20, 2010 by Jamie Munro

CakePHP offers a lot of functionality to us as developers. The ability to develop websites rapidly provides a trade-off in how quickly the website will load. As we expand our skills, we will learn the techniques that will slow down/speed up performance.



Objectives



  • Apply techniques to speed up CakePHP’s load time

  • Optimize our queries

  • Cache query results



Much like that last article on search engine optimization, improving your CakePHP projects is in no way an exact science.


There are many contributing factors that will help speed up certain aspects that may slow down others. In this article, I will cover various aspects of improvements that I’ve used successfully and explain how it improved our overall efficiency.


Hopefully the how will be the factor that is most clear. If you fully understand how different techniques help you will be able to pick the best methods for your project.



Debug Mode


I guess I may have lied; there is one thing that will work on every project and that is turning debugging off.


By default, when you setup a new CakePHP project the debug mode is set to 2. This is done inside of app\config\core.php with the following line:


Configure::write(‘debug’, 2);

Debug mode of 2 means that CakePHP will display your database queries at the bottom of each page. It will also display all PHP errors and notices.


These errors and debug information are extremely important during development. However, when you are ready for production, be sure to always set it to 0.


By setting debug to 0, CakePHP will begin caching several elements of your project. By caching elements, your website is immediately sped up because CakePHP, for example, does not have to check the structure of each model on every page load.









TIP: If you have successfully launched a website and set debug mode to 0, in the future when you add new controllers, or add new controller actions, or even change your database tables you will need to clear CakePHP’s cache. My favorite solution is to temporarily change debug mode to 1, reload the website, and then set debug mode back to 0. This will ensure your new data is retrieved and once debug is back to 0, CakePHP will continue to cache your data.



Recursive Find Statements


You may notice that when we use the bakery, our index function has the following line of code prior to performing the pagination find statement:


$this->Model->recursive = 0;

The following line tells CakePHP to join our tables related to this model. For example, if we had albums that have many photos, in our albums controller, when we perform the find statement, not only will it retrieve all of our albums, it will also return all of the photos for each album.


In several instances this certainly is desired. However, it is important to analyze each query individually to ensure we have the correct recursive level setting.


To ensure that we only query the one model and do not perform any join statements, use the following setting for recursion:


$this->Model->recursive = -1;


One of my new favorite techniques is to actually set all models to be containable and then in my queries tell CakePHP to only contain specific models.  Continuing with the example above, we can assume that albums are also tied to users.  If we simply left recursive as 0, it would also return all of our user data.  We could update our query and tell it to only contain photo, this will limit the number of joins we make.

requestAction Avoidance


This is a tough one that I still struggle with on a daily basis. A request action allows you to call other controller functions inside another view or controller. This makes re-using code extremely beneficial during development. However, each time you do a request action, CakePHP goes through the entire dispatch process (which adds a fair amount of overhead for each request).


At this point, I have yet to find a satisfactory alternative to this and each time I make a different decision; other times I continue to use requestActions because I determine it will not hurt my load time in that instance.


My rule of thumb is as follows, if I can alter the code, split it up more and avoid request actions without too much duplicated code or complications, I will do that. If it’s relatively difficult and time consuming, I will take the hit and use a requestAction() call.


A solution that has worked well for me is to place the views HTML code in an element. Then instead of using a requestAction I load an element() from the two views. This will not work 100% yet, more work is required. We also need to be sure to include the code in the controller from our other function.


To accomplish this, I will move and re-organize the code around. If it works, this is a great place to create components; if that doesn’t work, creating private functions in your controllers will work as well.



Caching Query Results


By default, most database servers will cache our database queries for us; however, regardless if the server caches your query, CakePHP still needs to perform the action and parse the results and build us our useful arrays of data.


By caching our query results with CakePHP, we can avoid all of this processing. Time savings here can be incredible based on the data being cached and processed.


Over time, I have built an excellent process that allows me to cache any query results I wish using CakePHP’s built-in caching system.



Before we begin, I need to post a big disclaimer…BE VERY CAREFUL WITH WHAT DATA YOU CACHE!  As Peter Parker’s Uncle Sam once told him, “With great power, comes great responsibility”.  The same applies here.  Caching your data can be extremely useful, but it can be very bad if you cache data incorrectly.  You will begin seeing incorrect data appear, errors because the data is not what was expected, the list could go on and on.

Don’t be scared though, we just need to use it correctly and we will have great success!

Step 1, create an app_model.php.  This file should live in the root of your “app” folder.  Below is an example of my app_model.php, it contains one function called find().


class AppModel extends Model {
	function find($conditions = null, $fields = array(), $order = null, $recursive = null) {
		$doQuery = true;
		// check if we want the cache
		if (!empty($fields['cache'])) {
			$cacheConfig = null;
			// check if we have specified a custom config, e.g. different expiry time
			if (!empty($fields['cacheConfig']))
				$cacheConfig = $fields['cacheConfig'];

			$cacheName = $this->name . '-' . $fields['cache'];
			// if so, check if the cache exists
			if (($data = Cache::read($cacheName, $cacheConfig)) === false) {
				$data = parent::find($conditions, $fields, $order, $recursive);
				Cache::write($cacheName, $data, $cacheConfig);
			}
			$doQuery = false;
		}
		if ($doQuery)
			$data = parent::find($conditions, $fields, $order, $recursive);
		return $data;
	}
}


The following code overrides the find() function in the main model class.  It looks for an array key called “cache”.  This is a new key that we are implementing.  If this key is found, we generate our cache name.  It’s the modelName-cacheName.  We automatically append the model name to help prevent cross-table contamination incase we accidently used the same name twice!

We also look for another new key called cacheConfig.  If this exists, it allows us to specify a different timeout period for our cached data.  By default, it uses CakePHP’s default caching.  I’m not exactly sure what it is, I think it’s in the one week range.

We then proceed to read the cache with that name.  If it does not exist, we execute the query and save the results to the cache for next time.

If we wished to cache data for a shorter (or longer) period of time, we would create a new config item in our app/config/core.php file as shown here:


Cache::config('short', array(
'engine' => 'File',
'duration'=> '+5 minutes',
'probability'=> 100,
'path' => CACHE . 'short' . DS,
));


The following code creates a config setting named “short”.  We tell it to only cache our data for 5 minutes.  This is great for something on the homepage that we don’t want to reload every time, but don’t want it to be cached for a long time either.

While we are in our app/config/core.php, it’s a good idea to ensure the following line is uncommented:


Configure::write('Cache.check', true);


Ok, now we have everything setup to use, so how do we use it?  Good question, let’s pretend we have a lookup table for a list of countries.  It’s pretty safe to assume that we will not be changing the data fairly often, so we should cache the query results.  To accomplish this, we do the following (this assumes we have a “Country” model and a “Countries” controller)


// get country list using default config
$countries = $this->Country->find('list', array('cache' => 'countryList'));
// get country list using a custom config
$countries = $this->Country->find('list', array('cache' => 'countryList', 'cacheConfig' => 'long'));


The following code executes a straight forward find statement.  The first time it runs, CakePHP will execute the find query, parse the results, and save the data to the cache.  The next time though, it will find the cached results and return those to us without the need to perform a database query and process the results.

To take this one extra step and make it even more useful, assuming we have a countries_controller.php file that allows us to add, edit, and delete countries.  We can update these three functions to remove our cached data.  This way, next time the countries are queried, it will not load the stale data; it will load the fresh data.

To accomplish this add the following line inside add, edit, and delete functions.  I would place it inside the if ($this->Country->save(…)) statement so we only do it on a successful action:


// we need to remove the status cache now
Cache::delete('Country-countryList');


It’s important to note that I did NOT just use countryList like in the find statement.  Instead, I prefixed the model name “Country” and a hyphen as well.

That’s all there is to it.  For a good starting place, I would cache all of your “lookup” table queries, similar to the country list above.  From there, I’ll let your imagination do the trick.  One piece of advice though, if you are caching a query that contains a “where id = $logged_in_user” (or any other conditional statements), be sure you include the $logged_in_user in the name of the “cache” key; otherwise, you will load the wrong person’s data!



Summary


The above methods are my favorite ways of optimizing CakePHP. At this point, it’s a good idea to remember not to assume all speed issues are caused by the framework. It is still to important to confirm that we’ve ruled out other issues. For example:

  • Poor database indexes

  • Poor database queries

  • Too many database queries

  • Too many Javascript files

  • Too many CSS files

  • Etc, Etc, Etc…


If you think one of the issues above may be the cause, I would like to refer you to an excellent blog article I wrote a while ago.


The article is called “YSlow – Helping Slow Web Pages Load Faster”. It provides some excellent techniques to improve any website, not just CakePHP specific websites.

Tags: CakePHP | Optimization

Related Posts

blog comments powered by Disqus