Better Performance – Speeding Up Your CakePHP Website

Share this article

It seems that whenever I mention CakePHP to the developer community, those who have not used it think of it as a slow framework. Indeed it isn’t the fastest according to the results of many benchmarks – out of the box that is – but what it might lack in performance it certainly makes up for in Rapid Application Development. By applying a few simple modifications, and even some more complex enhancements, CakePHP can be sped up quite a bit. By the time you work your way through even half of these changes, the performance of your your CakePHP site will be comparable to many other popular PHP frameworks, with the advantage that your development speed will never falter! There are two types of modifications that I will be describing in the following article. The first is code changes, e.g. these will work for anyone even if you are running on a shared hosting environment. The second type is geared towards users who have their own dedicated or virtual server that they can add and remove software as required. Do not fear though, if you can only follow the first set you will not be disappointed.

Upgrade CakePHP Versions

It’s important to note that people’s misconceptions around CakePHP’s speed probably date all the way back to versions 1.1 or 1.2. In version 1.3, the core development team performed a serious overhaul of CakePHP’s underlying architecture. In fact, I performed several benchmark tests comparing CakePHP versions 1.2 against 1.3 against 2.0. The results were astonishing. By simply updating from 1.2 to 1.3, I saw an immediate decrease by over 70% in the average time it took to load a page. If you’ve been delaying upgrading your version of CakePHP and you are still using version 1.2 or less, the first thing you should do is upgrade to at least version 1.3, but preferably version 2.3.x would be even better. The CakePHP Cookbook provides some very detailed migration guides:

Disable Debug Mode

Don’t laugh! This might seem obvious, but in a mad scramble to move code into production it can be very easy to forget to turn off debugging. A typical setup that I follow is to create multiple app/Config/core.php files: one for my local environment, one for dev, one for staging, and one for production. These files contain the different settings based on the target environment. The key statement to change is Configure::write('debug', 2) to Configure::write('debug', 0). The change hides all error messages and no longer refreshes the model caches. This is extremely important because, by default, each page load causes all of your models to be dynamically generated instead of cached from the first page load.

Disable Recursive Find Statements

When you perform a query using the find() method, recursion is set to 0 by default. This indicates that CakePHP should attempt to join any first level related models. For example, if you have a user model that has many user comments, each time you perform a query on the users table it will join the comments table as well. The processing time on performing, returning, and creating an associative array with this data can be significant, and I’ve actually seen CakePHP sites crash in production because of this! My preferred approach to making sure that the default recursion is none is to override the setting in app/Model/AppModel.php by adding the following code:
<?php
class AppModel extends Model {
    public $recursive = -1;
}

Cache Query Results

This is truly my favorite optimization. I’d like to think I uncovered it myself, but I’m sure that would be debated as I’ve seen other articles discuss a similar solution. In many web applications, there are probably a lot of queries going to the database that do not necessarily need to. By overriding the default find() function inside app/Model/AppModel.php, I’ve made it easy to cache the full associative array results of queries. This means that not only do I avoid hitting the database, I even avoid the processing time of CakePHP converting the results into an array. The code required is as follows:
<?php
class AppModel extends Model
{
    public $recursive = -1;

    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
            if (!empty($fields['cacheConfig'])) {
                $cacheConfig = $fields['cacheConfig'];
            }
            $cacheName = $this->name . '-' . $fields['cache'];
            // if so, check if the cache exists
            $data = Cache::read($cacheName, $cacheConfig);
            if ($data == 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;
    }
}
Subtle changes need to be made to the queries we wish to cache. A basic query which looks like this:
<?php
$this->User->find('list');
requires updating to include caching information:
<?php
$this->User->find('list', 
    array('cache' => 'userList', 'cacheConfig' => 'short')
);
Two additional values are added: the cache name and the cache config that should be used. Two final changes must be made to app/Config/core.php
. Caching must be turned on and the cacheConfig value that is used must be defined. First, uncomment the following line:
<?php
Configure::write('Cache.check', true);
And then, add a new cache config as follows (updating the parameters as required for name and expiry):
<?php
Cache::config('short', array(
    'engine' => 'File',
    'duration'=> '+5 minutes',
    'probability'=> 100,
    'path' => CACHE,
    'prefix' => 'cache_short_'
));
All of the options above can be updated to extend the duration, change the name, or even define where the cache should be stored. For a more detailed explanation on how to add, update, and purge the cache, continue reading about the specific caching optimization on my blog. Don’t consider this the end of your CakePHP caching, though. You can cache controller actions, views, and even helper functions, too. Explore the Cache Component in the CakePHP book for more information.

Install Memory Based Caching

By default, PHP sessions are stored on disk (typically in a temp folder). This means that each time you access the session, PHP needs to open the session’s file and decode the information it contains. The problem is disk I/O can be quite expensive. Accessing items from memory opposed to disk I/O is immensely faster. There are two nice approaches to session-related disk I/O. The first one is to configure a RAM disk on your server. Once configured, the drive will be mounted like any other drive and the session.save_path value in php.ini would be updated to point to the new RAM disk. The second way to is by installing software like Memcached, an open source caching system that allows objects to be stored in memory. If you’re wondering which approach is best for you, the way to decide this is by answering the following question: Will more than one server be required to access this memory simultaneously? If yes, you’ll want to choose Memcached since it can be installed on a separate system allowing other servers to access it. Whereas, if you are just looking to speed up your single web server, choosing the RAM disk solution is nice and quick and requires no additional software. Depending on your operating system, installing Memcached can be as simple as typing sudo aptitude install memcached. Once installed, you can configure PHP to store sessions in memory opposed to on disk by updating your php.ini:
session.save_handler = memcache
session.save_path = 'tcp://127.0.0.1:11211' 
If Memcached is installed on a different port or host, then modify your entries accordingly. After you have finished installing it on your server, you will also need to install the PHP memcache module. Once again depending on your operating system, one of these commands will work for you:
pecl install memcache
or:
sudo aptitude install php5-memcache

Removing Apache and Installing Nginx

Apache is still the favorite according to recent statistics, but Ngnix adoption is picking up a lot of steam when it comes to the most-heavily trafficked websites on the Internet today. In fact, Nginx is becoming an extremely popular replacement for Apache.
Apache is like Microsoft Word, it has a million options but you only need six. Nginx does those six things, and it does five of them 50 times faster than Apache. — Chris Lea
Nginx differs from Apache because it is a process based server, whereas Nginx is event driven. As your web server’s load grows, Apache quickly begins to be a memory hog. To properly handle new requests, Apache’s worker processes spin up new threads causing increase memory and wait-time creating new threads. Meanwhile, Nginx runs asynchronously and uses one or very few light-weight threads. Nginx also is extremely fast at serving static files, so if you are not using a content delivery network then you’ll definitely want to consider using Nginx for this as well. In the end, if you are short on memory Nginx will consume as little as 20-30M where Apache might be consuming upwards to 200M for the same load. Memory might be cheap for your PC, but not when it comes to paying for servers in the cloud! For a more in-depth breakdown between Apache and Nginx visit the Apache vs Nginx WikiVS page. HowToForge has an excellent article for configuring Nginx on your server. I suggest following the step-by-step guide to install and configuring Nginx with php-fpm.

Configure Nginx to use Memcached

Once you’ve installed Nginx and Memcached, making them leverage each other will even further extend your performance. Even though CakePHP applications are dynamic, it’s likely the 80-90% is still relatively static – meaning that it only changes at specific intervals. By making the following edits to your Nginx config file, Memcached will begin serving your Nginx requests that have already been processed and stored in memory. This means that only a few requests will actually invoke PHP which significantly increases your website’s speed.
server {
    listen 80;
    server_name endyourif.com www.endyourif.com;
    access_log /var/log/nginx/endyourif-access.log;
    error_log /var/log/nginx/endyourif-error.log;
    root /www/endyourif.com/;
    index index.php index.html index.htm;

    # serve static files
    location / {
        # this serves static files that exists without
        # running other rewrite tests
        if (-f $request_filename) {
            expires 30d;
            break;
        }
        # this sends all-non-existing file or directory requests
        # to index.php
        if (!-e $request_filename) {
            rewrite ^(.+)$ /index.php?q=$1 last;
        }
    }

    location ~ .php$ {
        set $memcached_key '$request_uri';
        memcached_pass 127.0.0.1:11211;
        default_type       text/html;
        error_page 404 405 502 = @no_cache;
    }

    location @no_cache {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+.php)(/.+)$;
        fastcgi_pass unix:/var/run/php5-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }
}
The above configuration specifies that static files should be served immediately. PHP files should be directed to Memcached to serve the page if it’s been cached. If it doesn’t exist in the cache, the @nocache location is reached which acts like your previous PHP setup to serve the page via php-fpm. Don’t forget to update your app/Config/core.php to feed the memcache cache. The following:
<?php
$engine = 'File';
if (extension_loaded('apc') && function_exists('apc_dec') && (php_sapi_name() !== 'cli' || ini_get('apc.enable_cli'))) {
    $engine = 'Apc';
}
becomes:
<?php
$engine = 'Memcache';
Our code for caching queries from earlier also requires updating:
<?php
Cache::config('short', array(
    'engine' => 'Memcache',
    'duration'=> '+5 minutes',
    'probability'=> 100,
    'path' => CACHE,
    'prefix' => 'cache_short_'
));

Remove MySQL and Install Percona

The final server modification I recommend is to install Percona‘s build of MySQL. The Percona team has spent many years understanding and fine-tuning the database’s architecture for optimal performance. It is best to follow the installation instructions from Percona as it describes the several different installation options.

Summary

There are quite a lot of techniques to ensure CakePHP will run lightning fast. Some changes may be more difficult to make than others, but do not become discouraged. No matter what your framework or language, optimization comes at a cost, and CakePHP’s ability to let you rapidly develop extremely complex web applications will strongly outweigh the effort involved in optimizing things once your app is complete. Image via Fotolia

Frequently Asked Questions (FAQs) on Speeding Up Your CakePHP Websites

What are some common reasons for slow CakePHP websites?

There are several reasons why a CakePHP website might be slow. These include inefficient database queries, lack of caching, not using the latest version of CakePHP, and poor server configuration. Inefficient database queries can slow down your website because they take longer to execute. Not using caching can also slow down your website because it means that the same data has to be fetched from the database every time it’s needed. Using an outdated version of CakePHP can lead to slower performance because newer versions often include performance improvements. Lastly, if your server is not properly configured, it can lead to slow website performance.

How can I optimize database queries in CakePHP?

There are several ways to optimize database queries in CakePHP. One way is to use the ‘find’ method with the ‘fields’ option to only select the fields you need. Another way is to use the ‘contain’ method to limit the associated data that’s fetched. You can also use the ‘group’ method to group results by a certain field, which can reduce the amount of data that’s fetched. Lastly, you can use the ‘cache’ method to cache the results of a query, which can speed up subsequent requests for the same data.

How can I use caching to speed up my CakePHP website?

Caching can significantly speed up your CakePHP website by storing the results of expensive operations and reusing them in subsequent requests. CakePHP provides several ways to use caching. One way is to use the ‘Cache’ class to cache the results of a database query. Another way is to use the ‘cacheAction’ option in your controller to cache the output of a controller action. You can also use the ‘cache’ helper in your views to cache parts of your views.

How can I ensure that I’m using the latest version of CakePHP?

You can check the version of CakePHP you’re using by looking at the ‘VERSION.txt’ file in the root of your CakePHP installation. If you’re not using the latest version, you can update CakePHP by downloading the latest version from the CakePHP website and replacing your current installation with it. Before updating, make sure to backup your application and test the update in a development environment.

How can I optimize my server for CakePHP?

There are several ways to optimize your server for CakePHP. One way is to use a PHP accelerator like APC or OpCache, which can significantly speed up PHP execution. Another way is to use a reverse proxy like Varnish or Nginx, which can cache static and dynamic content and reduce the load on your server. You can also optimize your database server by tuning its configuration and using a database-specific caching layer like Memcached or Redis.

How can I use the ‘debug’ mode in CakePHP to identify performance issues?

The ‘debug’ mode in CakePHP can help you identify performance issues by providing detailed information about your application’s execution. When ‘debug’ mode is enabled, CakePHP will log all SQL queries that are executed and display them at the bottom of your pages. This can help you identify inefficient queries that are slowing down your website. However, ‘debug’ mode should only be used in a development environment, as it can expose sensitive information in a production environment.

How can I use the ‘explain’ SQL command to optimize my database queries in CakePHP?

The ‘explain’ SQL command can help you optimize your database queries in CakePHP by providing detailed information about how a query is executed. You can use the ‘explain’ command by prefixing it to a query in your database client. The output of the ‘explain’ command will show you the execution plan for the query, including the tables that are accessed, the order in which they’re accessed, and the type of access (e.g., index scan, full table scan). This information can help you identify inefficient queries and optimize them.

How can I use the ‘Benchmark’ class in CakePHP to measure the performance of my code?

The ‘Benchmark’ class in CakePHP provides methods for measuring the time it takes to execute a piece of code. You can use the ‘start’ and ‘end’ methods to measure the time it takes to execute a block of code, and the ‘diff’ method to get the difference in time between two benchmarks. This can help you identify slow parts of your code and optimize them.

How can I use the ‘Log’ class in CakePHP to log performance information?

The ‘Log’ class in CakePHP provides methods for logging information about your application’s execution. You can use the ‘write’ method to log a message, and the ‘config’ method to configure the log. You can log performance information like the time it takes to execute a piece of code, the memory usage, and the number of database queries. This information can help you identify performance issues and optimize your code.

How can I use the ‘Profiler’ class in CakePHP to profile my code?

The ‘Profiler’ class in CakePHP provides methods for profiling your code. Profiling is a technique for measuring the performance of your code by collecting detailed information about its execution, such as the time it takes to execute each function, the memory usage, and the number of database queries. You can use the ‘start’ and ‘stop’ methods to profile a block of code, and the ‘get’ method to get the profiling data. This data can help you identify performance bottlenecks and optimize your code.

Jamie MunroJamie Munro
View Author

Jamie Munro is the author of Rapid Application Development with CakePHP, 20 Recipes for Programming MVC 3, and most recently 20 Recipes for Programming PhoneGap - all books are available in print and electronic format.

Advanced
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week