Thursday, February 18, 2010

CakePHP Pagination & Custom Routes Issue

Custom Routes are something that make CakePHP very interesting! If you are reading this post you must have experimented with what we call custom routing. The problem might arise when we want to paginate models which use custom routing.

The problems may be many-folded which all simplifies to one simple issue:

UNFORMATTED URL
Say, you want to display url in the following format using custom routes

CakePHP default url structure
http://example.com/categories/index/categoryID

Your WELL FORMATTED URL structure:
http://example.com/category-name-ID

Obviously, your custom route element points to 'index' action of 'categories' controller with two parameters - category-name and 'categoryID'.

Router::connect(

 '/:slug-:id/*',
  array('controller' => 'categories', 'action' => 'index'),
  array(
  // order matters
  'pass' => array('slug','id'),
 'id'=> '[0-9]+'
  )
  );

This works perfectly for URLs like
http://example.com/my-first-category-ID1
http://example.com/my-second-category-ID2

But it may cause problem when you try to paginate

Cake will not pick  your params (here 'slug' and 'id') passed through url if you do not force Cake to do so while paginating.  

To fix this issue, you MAY use:

 $paginator->options(array('url' => $this->passedArgs));  
or, as I have told here earlier following CakePHP book.

But this will format your paginated urls like
http://example.com/categories/index/param1/param2/page:xxxx

You can obviously browse pages with the above url. But it does not look decent. So, your entire effort with custom routing might just not work. 

You may still get a bad URL.

DO NOT WORRY!

While defining paginator options in your view file, follow religiously $html->link() structure. For an example I have put forward the structure of  $paginator->options() below in a view file:

file:// /app/views/categories/index.php

$paginator->options(array('url'=> array(
'controller' => 'categories', 
  'action' => 'index',
'slug'=>$this->params['slug']),
'id'=>$this->params['id'])
  ));

Now Cake will make well formatted URL automatically, and your paginated url should look like

http://example.com/category-name-id1/page:2
http://example.com/category-name-id1/page:3

Done!
I hope it helps someone.
Does it?

Please Note
I have marked asterisk symbol (*) with RED color while talking about custom route elements. This (*) MUST be there for pagination to work properly. 
Good night.

Saturday, February 13, 2010

How to Find Records using MySQL Match Against Query in CakePHP

To use MySQL Match Against Query in CakePHP:

$needle = 'Search String';

$conditions = array( 
 "MATCH(Post.title) 
  AGAINST('$needle' IN BOOLEAN MODE)" 
  );
 $matches = $this->Post->find('all', array('conditions' => $conditions));

It does not need any explanation. I hope.

Friday, February 12, 2010

CakePHP Select Empty

Say, you have a select box displaying dropdown options for categories. You want to give the user liberty to select nothing. In CakePHP it is pretty simple. Use 'empty' options in your view file.


<?php echo $form->input('category_id', array( 'empty' => '(choose one)')); ?>

Hope this helps someone.
Happy baking.

Tuesday, February 9, 2010

CakePHP hasAndBelongsToMany (HABTM) Join

Coming back to one of the most critical basics of Cake  - it is hasAndBelongsToMany (HABTM) relationship. You may know it is a type of relationship between two different database tables (Models) in CakePHP. A case study will explain its importance:

Say, you have Category model and Post model.
Each category has many posts and each post belongs to more than one categories.

Had there been only one category per post, it would be enough to use category_id field in posts table. But we are talking about posts each of which may have more than one categories. In this case besides categories and posts table, you need one extra table. By convention - this table should be named as categories_posts (note the alphabetical order of join tables). The table categories_posts will have following fields : id, category_id, post_id.

In your post model define the relationship.

<?php


class Post extends AppModel {
  var $name = 'Post';
  var $hasAndBelongsToMany = array(
  'Category' =>
  array(
  'className'              => 'Category',
  'joinTable'              => 'categories_posts',
  'foreignKey'             => 'post_id',
  'associationForeignKey'  => 'category_id'
  )
  );
  }


?>

This will show you all categories for a given post when you fire:

$this->Recipe->find();

Now the biggest question - how to SAVE records for HABTM association.

The default CakePHP functioning in this regard is not at all adequate. Say you want to add a new category to an existing post which already has a category defined by HABTM association - by default Cake will delete your existing category records for that post from categories_posts table before inserting the new record. That's an absolute mess if you do not really intend to desire existing records.  However, there is the savior.

ExtendAssociations Behavior.
This behavior allows you to easily add or delete HABTM associations!

A full detail of this Behavior can be found in Bakery!

You can download code for this behavior, and save that code as 'extend_associations.php' under '/app/models/behaviors' folder.

In your Post model, add the following code:


 <?php  
var $actsAs = 'ExtendAssociations'; 
?>  

So, our modified Post model should look like this:


 <?php  
class Post extends AppModel { 
    
var $name = 'Post'; 


var $actsAs = 'ExtendAssociations'; 
    
var $hasAndBelongsToMany = array( 
        
'Category' => array( 
            
'className' => 'Category', 
            
'joinTable' => 'categories_posts', 
            
'foreignKey' => 'post_id', 
            
'associationForeignKey' => 'category_id', 
        
), 
    
); 

?>  

Defining ACTION in PostsController
file: // app/controllers/posts_controller.php
Now in your PostsController - to ADD categories to a post use:


 <?php 
// to add only one category for a given post (say, post_id = 1)
$this->Post->habtmAdd('Category', 1, 1); 
// to add multiple categories 
$this->Post->habtmAdd('Category', 1, array(1, 2, 3)); 
?>  

// to DELETE categories for a given post (say, post_id = 1)


 <?php 
// delete a category 
$this->Post->habtmDelete('Category', 1, 1); 
// to delete multiple categories
$this->Post->habtmDelete('Category', 1, array(1, 3)); 
//  to delete all categories 
$this->Post->habtmDeleteAll('Tag', 1); 
?>   


That's it.
Good night.
Sweet Baking.

Friday, February 5, 2010

How to Remove Mailed-by Header in CakePHP Email Component

It took me quite sometime to fix this issue.

You can send emails with built-in CakePHP Email Component. But if you are sending email to any gmail address (like your-name@gmail.com), you can see an annoying 'mailed-by' header. It shows the name of your server, like mailed-by: dreamhost.com. It discloses your webhost. You may not like it. You may want to remove/hide this information. To do so, you need only one line of CakePHP code.

I have used following sendEmail() function in my /app/app_controller.php

file:// /app/app_controller.php


function sendEmail($subject, $view, $to=null, $from = null, $fromName = null) {
/* This function will be used to send email in my CakePHP application */
$this->Email->to = $to;
$this->Email->subject = env('SERVER_NAME') . ' – ' . $subject;
$this->Email->from = $fromName.' <'. $from.'>';
$this->Email->template = 'default';
$this->Email->additionalParams = '-fnoreply@yourdomain.com';
/* the above line is required to remove 'mailed-by' header' */ 
$this->Email->sendAs = 'both';   // you probably want to use both :)
return $this->Email->send($view);
}


Done.

Note again, whenever you use CakePHP Email Component, just add one extra line:

$this->Email->additionalParams = '-fnoreply@yourdomain.com';

And you can safely remove that annoying 'mailed-by' header.

That's it.

Just a guess, noreply@yourdomain.com is NOT essential. You may change it to anything you want.

Take care.

Responses awaited.