CakePHP: The Good, The Bad, and The Hurty

As I noted in a previous article, I opted to use the CakePHP framework for the Holidailies 2008 rewrite. It's a PHP framework for web application development. It implements the MVC (model-view-controller) design pattern commonly found in web frameworks (such as Ruby on Rails). I made a very good choice moving Holidailies from complete custom code to a framework. I'm not completely happy, however, with my choice of framework. I'll tell you about some of the things I've found.

What I Liked

The best thing is that it implements magic ORM (object-relational mapping). Manual ORM, such as that implemented by Hibernate, is a major annoyance. You have to create all these useless getter/setter classes, construct an XML index, and go through a lot of work to build the ORM. And heaven help you if you make any schema changes. It's painstaking and fragile.

CakePHP, on the other hand, just starts up, reads the database tables, makes some default assumptions, and figures it all out on its own. All you need to do is specify the relations between tables, column validations, and any overrides to the defaults ("make the foreign key into the users table submitter_id instead of user_id"). The Ruby on Rails framework does this too, and it's one of the reasons why it's so popular.

CakePHP uses "introspection" to construct specialized find() methods on the fly. If, for instance, your table has an email column, it will automatically create a findByEmail() method.

The CakePHP Form helper does a pretty adequate job in guessing how to present the form input for a given column. (That's a compliment, not faint praise.) All you do is say something like:

$form->input('username');

and it makes a pretty good guess what to do. It doesn't do nearly as good a job on output, though. You are still stuck manually constructing tables to display record values. CakePHP does have a plug-in system, so maybe somebody has contributed a plug-in that addresses this.

I found the combination of the manual, API reference, and linked code listings completely sufficient for most basic needs. I did find on occasion I did have to read the code to understand what I wanted, but I'm ok with that. The more difficult areas, such as caching, are not explained as well.

What I Didn't Like

The biggest drawback to CakePHP is that it doesn't completely implement object orientation. The result of Model::find() (the method to do a database lookup) is not an object or list of objects, but a blob of data (a PHP array).

Instead of everything being an object, you just get a single object through which manipulations are performed. So working on a user record goes something like this:

$this->User->id = $id;
$data = $this->User->read();
$data['User']['some_field'] = $some_value;
#    .
#    .
#    .
$this->User->id = $id;
$this->User->save($data);

As the example shows, you need to set the id member of the User object to work on a particular record. Also note that read() is not returning an object, but a blob of data.

If a fully object-oriented paradigm was implemented, we would instead do something like:

$user = $this->User->read($id);
$user->some_field = $some_value;
#    .
#    .
#    .
$user->save();

That's much cleaner and more robust. This points to the most severe shortcoming to CakePHP: its lack of full OO.

Another area where we can see this is model configuration, particularly column validations. The validations are where you enforce restrictions on database values: make sure a name is given, the URL is valid, the birth date is in the modern Christian era, etc.

For instance, to configure validations for the user object you'd say something like:

class User extends AppModel {
    var $name = 'User';
    var $validate = array(
        'username' => array(
            'unique' => array(
                'rule' => 'isUnique',
                'message' => 'This username has already been taken.',
            ),
            'required' => array(
                'rule' => 'notEmpty',
                'message' => 'This is required.',
            ),
            'maxLength' => array(
                'rule' => array('maxLength', 20),
                'message' => 'Cannot be longer than 20 characters.',
            ),
        ),
       //   .
       //   .
       //   .
    );

Oh, yech!

One problem with this is that it's very brittle. It's very easy to mess up the validations array. Even worse, if you make a mistake, CakePHP probably will just silently ignore it. The rule points to a callback method in the model, and if it doesn't exist (like you made a typo in the name) then CakePHP silently ignores it.

The right way to do this would be to create a ModelValidation base class, and validations are constructed as subclasses of that. This means you'd get something like:

class User extends AppModel {
    var $name = 'User';
    var $validate = array(
        'username' => array(
            new ValidateIsUnique('This username has already been taken.'),
            new ValidateNotEmpty(),
            new ValidateMaxLength(20),
        ),
       //   .
       //   .
       //   .
    );

This looks much nicer. The validation expressions are much more robust. Also note that we now can depend on the validations to provide sensible default error messages, instead of having to specify each one.

This is great, but there is one very big problem with this approach: it's not legal PHP!

You can't use anything but constants for class member static initializations. Even a simple mathematical expression throws an error:

class Two {
    var $value = 1+1; // FAIL
}

This could be solved by doing the initialization in the User constructor.

This brings me to the final major problem with CakePHP: it's PHP. PHP really is a crap language. I think the biggest shortcoming for PHP in an application such as this is the lack of method references. You need that to specify callbacks correctly, which is key to this sort of application. The PHP mechanism is to specify callbacks as a string that contains the name of the callback function, like so:

'rule' => 'notEmpty'

The notation I'm looking for would be something like this:

'rule' => &notEmpty()

What Caused Me Pain

I encountered a number of problems along the way.

The first thing I wish I realized is that, by default, the fetches are pretty greedy. When I pull a full list of users from the database, I end up pulling nearly the entire database. I wish I had defined my models with $recursive disabled, and then turn it on as needed. Or, better yet, I wish CakePHP allowed me to express different defaults for Model::find("first") (greedy fetching is ok here) versus Model::find("all") (greedy fetching is a problem).

The test framework is not working for me. The provided tests don't pass, and I can't make a single unit test work correctly. This is not acceptable.

The furnished "Access Control List" system is overly ornate and a poor fit for the application. I'm sorry I wasted my time looking into this. Instead, it was easier to construct a small number of access verification methods in the AppController base class, and use them in the various controller methods (or the beforeFilter() if it applies to an entire controller).

The URL generation and form action determination are a bit flaky. If I specify a target URL of "/controller/action", sometimes it magically ends up with an id on the end, sometimes it doesn't. I've found I sometimes have to do trial and error to get the behavior I want.

The Final Result

Clearly there are some significant problems with the CakePHP framework. Even still, Holidailies under CakePHP is significantly better than it was a year ago. For instance, I got an emailed suggestion from Margaret asking for some additional RSS feeds. Thanks to the framework, it was easy to do.

I might use CakePHP again if I had another small web project I wanted to get out quickly. If, however, I had a major project, I'd probably evaluate a different framework.

Comments

Comments have been closed for this entry.

great post

hi chip,

Nice summary. I agree with most of the things that you said. I have used cakephp since it was first released and recently moved to Cakephp 1.2. I agree validation needs some improvement and find is too greedy. However, in my personal opinion, I think its good that CakePHP returns blob of data rather than object. To me, it seems returning object opens up bag of opportunities to accidentally corrupt your data.

Agree

I totally agree with CakePHP not being OO enough. Simply because it still supports PHP4, which is already ended by PHP. They should move quickly to PHP5 (suppose in Cake2, but I don't see it anytime soon, like a year or longer).

There is another way to avoid the initiation of member property: to init in the __construct() of the extending class.

I am curious ...

If mod_rails were mature (i.e. to the point where major distros package it), would you find yourself more likely look at RoR? Deployment has always been a pita, which is how I wound up looking at cakephp myself for some projects. Maybe it's a silly complaint, but dealing with endless streams of associative arrays eventually took its toll on my sanity and I started looking elsewhere.

Ultimately I stumbled upon Django, and haven't looked back since.

Oh, and hi Chip! Long time no see. We should catch up some time off-line.

Doug

Yes, indeed

You bet. I don't use RoR due to the deployment issues. If there was a way to run it under Apache (with performance better than CGI, but without fiddle farting around with FastCGI processes) then I'd consider it.

Agree Not OO

( I'm running mod_rails on a bunch of production sites, and it's fantastic, had no problems with it whatsoever).

Just spent the weekend evaluating Cake PHP, as I've been looking for a good MVC framework with a good ORM under PHP.

To those out there saying they prefer it that Cake returns a blob of data, rather than an array of objects, I just don't understand them.

Firstly, it throws the Uniform Access Principal out of the window, people can google that for reasons why it is a very good thing, obeying that principle has saved me huge loads of work in the past.

Also, consider this situation that I had in the app I was creating to teach myself a bit of Cake.

I had users, and users had many rss_feeds, and rss_feeds had many rss_feed_items.

When listing the rss feeds I wanted to show the details of the latest item. So I have a method in the RssFeed model called 'latest_item' which returns the latest rss_feed_item.

However on the find to get the rss feeds for the user, we just get a blob of data back. When looping through the data in the view, I can't just call $rss_feed->latest_item(); to get the latest item, as it's just an array, not an object. I've either got to mess around instantiating objects in the view to get access to these methods or (as I suspect Cake would expect you to do) have the latest_item method take an 'id' parameter which is the id of the RssFeed, and form a query based on that.

It seems to me that Cake doesn't view classes in an object oriented style, but more as just a collection of functions related to that table in the database.

After this weekend's evaluation, I really couldn't recommend Cake to anyone. My search continues for something that can stand up against RoR in PHP...

Dynamic recursive fetching

Quote:"I wish I had defined my models with $recursive disabled, and then turn it on as needed."

See the models section of the cakephp manual for disabling recursive fetching on the fly when using the 'find' method (its the last parameter to the find method):
"Set $recursive to -1 if you want no associated data fetched during the find."