Archive for 'Programming'

CakePHP Auto Magic Search Conditions

Hey folks, so this is a nifty trick I implemented after being inspired from overriding CakePHP Model::find(). There's nothing to it really, just making use of the extreme flexibility CakePHP's architecture allows, provided you wrap your head around the CakePHP way of doing things :)

Before commencing, I would really advise anyone out there who wants to make it to a good level in CakePHP to read Matt's book. To make this post shorter I will assume that you fully understand how to override Model::find() for CakePHP as Matt explains in his book as "My Way". These are the functions that you will need:

  1.  
  2. class AppModel extends Model {
  3. public $searchValues = null;
  4.  
  5. public function searchCondition ($type, $options = array()) {
  6. $type = str_replace('-', '_', $type);
  7. $method = sprintf('__sc%s', Inflector::camelize($type));
  8.  
  9. if (!method_exists($this, $method)) {
  10. trigger_error('Method '.$this->name.'::'.$method.'() Does Not Exist', E_USER_ERROR);
  11. }
  12. return $this->{$method}($options);
  13. }
  14.  
  15. public function searchConditions() {
  16. $conds = array();
  17. foreach($this->searchValues as $val) {
  18. $_conds = $this->searchCondition($val, array());
  19. $conds = am($conds, $_conds);
  20. }
  21. return $conds;
  22. }
  23. }
  24.  
  25. class User extends AppModel {
  26.  
  27. protected function __scAge ($options = array()) {
  28. $age = Set::classicExtract($this->searchValues, 'age');
  29. $ageComp = Set::classicExtract($this->searchValues, 'age_comparison');
  30. $conds = array();
  31. if (!empty($age) && is_numeric($age) && !empty($ageComp)) {
  32. $dob = date('Y-m-d', strtotime("-$age years", strtotime(date('Y-m-d'))));
  33. $ageConds = array(
  34. '>' => array('User.dob >' => $dob)
  35. , '<' => array('User.dob <' => $dob)
  36. , '=' => array('User.dob' => $dob)
  37. );
  38. $conds = $ageConds[$ageComp];
  39. }
  40.  
  41. return $conds;
  42. }
  43.  
  44. }
  45.  
  46. class UsersController extends AppController {
  47.  
  48. public function search() {
  49. $data = array();
  50. if ($this->RequestHandler->isPost() || $this->RequestHandler->isPput()) {
  51. $this->User->searchValues = $this->data['User'];
  52. $conditions = $this->User->searchConditions();
  53. $data = $this->User->find('all', array('contain'=>false, 'conditions'=>$conditions));
  54. }
  55. }
  56.  
  57. }
  58.  

Hold on ... don't panic, I assure you it's quite simple :) - First off, the AppModel::searchCondition() will generate the condition for you and you would need to write up the proper conditions within the model itself which is the User model in our case.
As you will notice that I am trying to find the "age" of the particular user. I only have the User's date of birth stored as "dob" field in the users table. There is NO "age" field in the users table of course. So in essence, you may or may not relate actual field names of the table. This is what makes it really powerful. And this is what can allow me to find a User's age via more than 1 way. This is a very simple demonstration what can be done. For reasons of simplicity I have not used the $options variable which you will see as the second parameter of the AppModel::searchCondition(). In the code above you will see only the 'conditions' being returned, in my actual implementations I return all the options of a normal Model::find() call. That allows me to set joins and any virtual fields I like. It's pretty nifty once you dwell on the whole process.

I can simply write up another search condition filter in my User model,

  1.  
  2. protected function __scOlderThan($options = array()) {
  3. $age = Set::classicExtract($this->saerchValues, 'order_than');
  4. $conds = array();
  5. if (!empty($age) && is_numeric($age)) {
  6. $conds = array("(DATE_FORMAT(FROM_DAYS(TO_DAYS(NOW())-TO_DAYS(User.dob)), '%Y')+0) > $age");
  7. }
  8. return $conds;
  9. }
  10.  

And this would work if I supply a valid numeric value in "older_than" field I can create on the fly using the FormHelper in my form so this would be supplied as Controller::data['User']['older_than'] in the controller.

You can easily follow this concept and play with the code to also include all the options in the Model::find() call as I've mentioned above. When you do this, retrieving data sets becomes a breeze ! ... It's like you tame your model to behave the way you want whenever the particular fields are given !

P.S. shoot ! ... so this post I've tried to put together under too many distractions :( ... I hope I make it clear though.

Using MySQL INNER JOIN in CakePHP Pagination

First of all, I've got to hand it over to Matt he really did a BBBIIIGGG favor to the CakePHP community by publishing his guide to advanced CakePHP Techniques. This guide / book will give a great insight into the framework to anyone who is a seasoned programmer and is picking up Cake for the first or so time. And I'm going to be floating ideas to compliment the advanced techniques and all in all promote good programming practices of what I'm aware of.

Now, I've seen a lot of shitty code when it comes to CakePHP. Yes, I've even seen mysql_query() calls in views ! ... yes, I've lived that day and kept my sanity in tact. But I can't blame the programmers too because obviously they were newbies and were under a lot of pressure to "get things going" by their blood sucking employers. Anyhoo ... this might be the subject of another post, BUT I really had to get it out of my system ... *phew* ... feel so light now :)

So, MySQL INNER JOINS ... when should you use them ? - simple answer: when you want to filter out data in your result set. And it's quicker than filtering out results in the "WHERE" clause. Don't have any metrics to show to support this conclusion right now, but I speak in the light of many tests I've conducted on large datasets. A more simpler theory is that the "WHERE" clause needs to filter out a lot more rows in a result-set obtained as a result of using "LEFT JOIN". CakePHP's logic however is sound to use LEFT JOIN as the intention is not to filter out the records, it's merely to include whichever records belongs to the conditions you supply. That's why it's "Containable" behavior is so cool (special thanks to Felix on that for maturing it and making a part of the Cake's core).

The more you familiarize yourself with Cake's datasource classes the better. The most excellent example was published on the bakery by Nate on how to use JOINs in CakePHP. I think this should be made part of the documentation too. This could actually make you get rid of "overriding" Controller::paginate() function. When you come to know about the flexibility offerred by the datasource class you love it even more :P - a simple example:

  1.  
  2. class PostsController extends AppController {
  3.  
  4. public function by_tag ( $tag ) {
  5. /**
  6.   * This will fetch Posts tagged $tag (say, 'PHP')
  7.   */
  8. $this->paginate['Post'] = array(
  9. 'limit' => 10
  10. , 'contain' => ''
  11. , 'conditions' => array(
  12. 'Post.published' => 1
  13. )
  14. , 'fields' => array('Post.*', 'Tag.*')
  15. , 'joins' => array(
  16. 'table' => 'posts_tags'
  17. , 'type' => 'INNER'
  18. , 'alias' => 'PostTag'
  19. , 'conditions' => array(
  20. 'Post.id = PostTag.post_id'
  21. )
  22. )
  23. , array(
  24. 'table' => 'tags'
  25. , 'alias' => 'Tag'
  26. , 'type' => 'INNER'
  27. , 'conditions' => array(
  28. "PostTag.tag_id = Tag.id AND Tag.name = '$tag'"
  29. )
  30. )
  31. )
  32. );
  33.  
  34. $data = $this->paginate('Post');
  35. $this->set(compact('data'));
  36. }
  37. }
  38.  

This is just a simple example of what you can achieve by adding simple joins in your Model::find() conditions and of course in the paginate part. I've stretched it a bit further. I've actually used sub-queries and sub-joins, really complex stuff when paginating some complex data sets. Thanks to the 'joins' I never had to override the Controller::paginate() method ever. Just for the sake of example, let's say I want to retrieve posts tagged in 'PHP' and 'CakePHP' written by users who have a rating above 3. Of course this can be done in other ways, here is one using a sub-query join in CakePHP elegantly:

  1.  
  2.  
  3. // work this out wherever you want it - in your model or controller
  4. // but if I were you, I'd put this in my model
  5.  
  6. $join = "SELECT posts.id AS POST_ID FROM posts JOIN authors ON (posts.author_id = authors.id)";
  7. $join = $join.' '."JOIN users_ratings ON (authors.user_id = users_ratings.user_id AND users_ratings.rating > 3)"
  8. $join = $join.' '."WHERE 1=1";
  9.  
  10. // in your controller
  11. $this->paginate['Post'] = array(
  12. 'limit' => 10
  13. , 'contain' => ''
  14. , 'conditions' => array(
  15. 'Post.published' => 1
  16. )
  17. , 'fields' => array('Post.*', 'Tag.*')
  18. , 'joins' => array(
  19. 'table' => 'posts_tags'
  20. , 'type' => 'INNER'
  21. , 'alias' => 'PostTag'
  22. , 'conditions' => array(
  23. 'Post.id = PostTag.post_id'
  24. )
  25. )
  26. , array(
  27. 'table' => 'tags'
  28. , 'alias' => 'Tag'
  29. , 'type' => 'INNER'
  30. , 'conditions' => array(
  31. "PostTag.tag_id = Tag.id AND Tag.name IN('PHP', 'CakePHP')"
  32. )
  33. )
  34. , array(
  35. 'table' => '('.$join.')'
  36. , 'alias' => 'FILTERED_RESULTS'
  37. , 'type' => 'INNER'
  38. , 'conditions' => array(
  39. "Post.id = FILTERED_RESULTS.POST_ID"
  40. )
  41. )
  42. )
  43. );
  44.  

And this will elegantly filter out the posts you need :P

Conclusion:  you can really write any kind of a query and really devise a condition based system that would add filters auto-magically. (I will present such a system in another post) - Remember, CakePHP is all about auto-magic ! ... which is actually the culmination of "convention over configuration" so use it to the fullest !

CakePHP Archivable Behavior

Alright folks ... yeh I know I've been out of the picture really long and me blog is looking deserted for real now. Anyhow, I've got a bunch of posts in the pipeline. Thanks to Ahmed of SoccerLens for convincing me to start posting again :).

Enough chit chat ... so the cool thing I bring you here my friends is this Behavior I just baked up for a client. The Archivable Behavior. I love CakePHP's behavior architecture and had Mariano Iglesias's SoftDeletable behavior in mind before baking this baby.

What it does ?

It simply puts the record you want to delete in another table. I see no use bloating my existing table by adding a "deleted" field, especially when it would need to go through this process many times. Imagine, how big your table would get when you simply soft delete a record and it just sits there and is rarely used. I've seen this kind of methodology run into trouble when you are search the table. MySQL has to go through a lot of unwanted, deleted records and extract the active ones, it slows down the search process, especially when you are using UUIDs.

Usage:

This example follows for a Model, say, "MyPosts".


public $actsAs = array('Archivable'=>array('table'=>'my_posts_archives'));

If you don't specify a the table key, it will simply look for the table, 'my_posts_archived'. The schema for the archives table is simple. It's the same as the table my_posts but it has an additional field, my_post_id. You're going to have to create that table yourself. If you're like me and use PhPMyAdmin it should be really simple by going into Table -> Operations -> copy.

Once you're done with setting up the table and attaching the Archivable behavior with your model, anytime you execute the statement $this->MyPost->del($id); it will simply move the record from my_posts table to my_posts_archives table.
You can also use $this->MyPost->unarchive($id); to achieve reverse, i.e. remove the archived record from the archive table and move it back to the main table. As simple as that ! :P

Notes:

I've baked this on CakePHP 1.3.0.0 with PHP 5.2.4. Although this should run smooth on CakePHP 1.2.x too but if you are using PHP 5.3 you're gonna have to make some adjustments when objects are passed via reference in the code.

Download the Behavior

Have fun ya'all ;)

Why Avoid The “else” Construct ?

Till now I have seen a fair amount of code, the good, the bad and o yes! the ugly and the really really ugly. Ok, so you are a programmer, what makes you think a chunk of code is "good" or "bad" ? - I haven't had the opportunity to discuss this with fellow programmers in detail, so I think the best way to air this would be to document this.

First thing that comes to my mind about good code is READABILITY ... it's imperative the code should be readable and understandable. This however, is automatically achieved when you follow the most optimum way of execution and is even prettier in OOP. Structured code provokes ugliness. And I have noticed that the code gets uglier when there are more and more if-elseif-else construct nesting. I have seen programmers abusing this construct a lot ! You are better off using a SWITCH-CASE construct (which should also be used if and only if necessary).

I know you must be thinking: So what's up with the "else" construct ? and what does this guy have against it ? - Well, in all honesty, I don't have anything against it. It's just that the "else" construct refrains me to write beatiful code :P - Ask yourself, why would you need a lot of else statements anyways ? (do keep in mind that for menu choices you always have the good 'ol switch-case). This may not hold true when you are executing via the structured approach. When using OOP, it just doesn't make much sense. Of course, a simple "if-else" is ok as its simple and readable too. But when you are dwelling into if-elseif { if - else }-else { if - else } it becomes more complex and consequently turns into a nightmare. Imagine yourself trying to debug a 100 lines of code out which at least 40 are consumed by the nested "if" construct. Hehe, I know ! - It scares me too :)

So what does it mean when you have used nested "if-elseif-else" structure a one too many times ? I answer this based on my own code more than a year ago. It means:

1. You are not breaking down problems into smaller chunks, that's for sure! - consequently you have become that greedy Ostrich who swallowed the whole freshly baked potato! so obviously the aftermath is the same. You jump, cry and get frustrated when you need to refactor the code or even worst, debug it.

2. Poor design ! - Yes ! poor design ! I don't care if you agree with me here or not, but you don't have a robust and scalable design before you put your cowboy hat on and went riding that keyboard. You did not think twice before coding. The rationale goes like this: An "if-else" construct denotes a change in variables and you address that change by changing the execution flow. Making the right decisions when a change occurrs. In an OO way, that variable change denotes a "state" change. So you do what you should according to that particular state. Similar to your reactions based on your emotional state,

Happy => Smile || Laugh

, Angry => Frown || Break something || Punch someone

, Sad => Cry || Whiskey++

In OO you can actually encapsulate execution as behaviors according to the change in state. As a result, you design is robust and scalable. All you need to do is invoke the right behavior for the right state. For that you'd probably need a simple IF statement.

3. Obviously either you haven't heard or you aren't interested in the philosophy behind "return home early" - a term coined by Felix from debuggable.com - READ IT as it addresses programming psychology you might make use of.

Doh ! I've run out of time. I'm going to try to put some code slides up. You look at the code and decide for youself.