07 May

Experimental Searchable Behavior in CakePHP

Best Practices, CakePHP

I always have bad time writing code for search in any new project. So, I decided to write a quick Searchable behavior in CakePHP so as to avoid doing it again and again in any project.

Please note: This only handles text based searches, and it's a very basic version, written in less than two hours.

Here's how it works:

  1. In the model which you want to be searchable, just add:

    PHP:
    1. var $actsAs = array('Searchable' => array());

  2. Now in controller you can use function called Model::stringSearch()

    PHP:
    1. // here your $keyword can be something like ‘nicole scherzinger’
    2. $videos = $this->Video->stringSearch($keywords, array('title', 'description'));

Algorithm behind its functioning (inside Model::stringSearch()):

  1. Search procedure 1: Consider $keywords as phrase, and fetch ids based on this phrase from the passed fields. So if you have passed on :

    $keywords = 'nicole scherzinger';

    $videos = $this->Video->stringSearch($keywords, array('title', 'description'));

    It will be treated as "nicole scherzinger", the results returned here will be marked as highest relevant results.

  2. Search Procedure 2: Find 'nicole' and 'scherzinger' together in given fields. Relevancy --
  3. Search Procedure 3: Find 'nicole' or 'scherzinger'. Relevancy decreased again.
  4. Now it combines all the ids returned from all 3 types of searches, in the same order of their relevancy. Then fetches records using findAllbyId() and returns them to caller.

Download it here. Hope you people like it.

P.S. My girlfriend will be angry when she will see Nicole scherzinger here… So, if you're reading it, I'm sorry I was just working on this MP3 and Videos project, you know?? hehe :)

- Abhimanyu Grover

15 Apr

Designing for Reusability: Taking advantage of Plugins in CakePHP

Best Practices, CakePHP, Ideas

Do you ever wanted to accomplish a better reusable CMS which helps you do things faster? If yes, you definitely need to know more about CakePHP Plugins. This manual about plugins is not just enough, there are many cool things you can do with them.

In this post, I will explain you, how to create a reusable Backend Plugin, which can easily fit in with any of your project. Although, I've written a tutorial before on how to create an administrator panel, but that is not so RAD approach.

Creating necessary files for plugin:

/app/plugins/backend

Ok, now comes interesting part:

Telling Plugin about our Admin Links

We will need our other controllers to tell Admin plugin about all the admin URLs/function. For this, we will create two new class variables called $admin_links and $admin_menu_order in all our controllers.

PHP:
  1. <?
  2. Class ClientsController extends AppController
  3. {
  4.     Var $name = ‘Clients’;
  5.     var $adminLinks = array('add' => 'Add New Client, 'index' => 'Show all clients);   // add points to admin_add() and ‘Add New Client’ corresponds to the anchor of link.
  6.     var $adminOrder = 1;   // Ordering inside admin panel
  7.  
  8.     // functions etc goes here..
  9. }
  10. ?>

Add these variables in all necessary controller files.

Plugin Connectivity to the rest of system

Since our plugin needs to look each and every controller present in system. I decided to create BackendController::cacheAdminLinks(),this function will get all admin links as specified in controllers, then store it to a temporary cache file. This way, all controllers need not to be loaded in every call.

PHP:
  1. <?php
  2.  
  3. class BackendController extends BackendAppController
  4. {
  5.     var $name = 'Backend';
  6.     var $uses = array();
  7.  
  8.     function cacheAdminLinks()
  9.     {
  10.         $adminLinks = array();
  11.         $ControllersPaths = array()// to store all controller paths
  12.  
  13.         // collect all controller paths to find backend links from
  14.         $allPaths = paths();
  15.         $appControllers = $allPaths['Controllers'];
  16.  
  17.         // find controllers of other plugins
  18.         $folder =& new Folder(APP.'plugins');
  19.         $plugins = $folder->ls();
  20.  
  21.         $ControllersPaths = am($ControllersPaths, $appControllers);   // merge app with all paths
  22.  
  23.         foreach($plugins[0] as $plugin)
  24.         {
  25.             $pluginControllers = $allPaths[Inflector::camelize($plugin)]['Controllers'];
  26.             $ControllersPaths = am($ControllersPaths, $pluginControllers)//add plugin controllers path
  27.         }
  28.        
  29.         $controllers = array()// to store all controllers here
  30.         foreach($ControllersPaths as $path)
  31.         {
  32.             $folder->cd($path);
  33.             $controllerFiles = $folder->find('.+_controller\.php$');
  34.             $controllers = am($controllers, array_map(array(&$this, '__controllerize'), $controllerFiles));
  35.         }
  36.  
  37.         $controllers = array_unique($controllers);
  38.  
  39.         foreach($controllers as $controller)
  40.         {
  41.             App::import('Controller', $controller);
  42.             $className = $controller."Controller";
  43.             $controllerInst = &new $className();
  44.            
  45.             if(isset($controllerInst->adminLinks))
  46.                 $adminLinks[$controllerInst->adminOrder] = array('name' => $controllerInst->name, $controllerInst->adminLinks);
  47.  
  48.         }
  49.  
  50.         // reorder & reform admin links in better form
  51.         $tmp_adminLinks = array();
  52.         foreach($adminLinks as $adminLink)
  53.         {
  54.             $reformedLinks = array();
  55.             $links = $adminLink[0];
  56.             foreach($adminLink[0] as $funcName => $link)
  57.             {
  58.                 $tmp_reformedLink = array('url' => $this->_getAdminUrl($adminLink['name'], $funcName) , 'title' => $link);
  59.                 $reformedLinks[]$tmp_reformedLink;
  60.             }
  61.  
  62.             $tmp_adminLinks[$adminLink['name']] = $reformedLinks;
  63.  
  64.         }
  65.         $adminLinks = $tmp_adminLinks;
  66.  
  67.         if (Configure::read('debug')==3)
  68.             $cacheExpires = '+5 seconds';
  69.         elseif (Configure::read('debug')==1 || Configure::read('debug')==2)
  70.             $cacheExpires = '+60 seconds';
  71.         else
  72.             $cacheExpires = '+24 hours';
  73.  
  74.         cache(LINKS_CACHE_FILE, serialize($adminLinks), $cacheExpires);
  75.         exit;
  76.  
  77.     }
  78.    
  79.     function _getAdminUrl($singular_name, $funcName)
  80.     {
  81.         return '/'.Configure::read('Routing.admin').'/'.low(Inflector::pluralize($singular_name)).'/'.$funcName;
  82.     }
  83.  
  84.     function __controllerize($file)
  85.     {
  86.         return Inflector::camelize(r('_controller.php', '', $file));
  87.     }
  88. }
  89. ?>

Setting up $layout & $admin_links at every admin call

Now, this is a bit tricky part – for every plugin you create in your app, you wouldn't want to add custom hacks anywhere throughout your app for every plugin. This is where you can utilize plugin hooks, as introduced by Felix in this post. I wouldn't go too deep to explain how it works, but it just basically allows you to create a file hooks.php inside your plugin folder. You can attach these hooks with your app, in this case just attach it to AppController::beforeFilter()

PHP:
  1. function beforeFilter()
  2. {
  3.         callHooks('beforeFilter', null, $this);   // you need to have this function defined somewhere – I guess bootstrap.php in best place
  4. }

In our hooks.php, we will create following function:

PHP:
  1. // this function will be called by AppController::beforeFilter() on every HTTP request
  2. function BackendbeforeFilterHook(&$controller)
  3. {
  4.     // if its the administrator/manager - change the layout
  5.     $pos = strpos($_SERVER['REQUEST_URI'], 'admin');
  6.     if($pos == true)
  7.     {
  8.         $controller->plugin='backend';
  9.         $controller->layout='admin';
  10.         $adminLinks = unserialize(cache(LINKS_CACHE_FILE, null));
  11.  
  12.         $controller->set('adminLinks', $adminLinks);
  13.  
  14.     }
  15. }

As you can see, an instance of controller is being passed to this function, and you can modify it as per your needs. So, we just overwritten the layout to admin.ctp inside app/plugins/backend/views/layouts and as also set our admin links to view from cache (saved by BackendController::cacheAdminLinks() )

Create admin.ctp

PHP:
  1. <div id="content">
  2.  
  3.     <div id="menu">
  4.         <? foreach($adminLinks as $title=>$adminLink) { ?>
  5.             <div class="admin_box">
  6.                 <?=$title;?>
  7.                 <ul>
  8.                 <? foreach($adminLink as $link) { ?>
  9.                 <li><a href="<?=$html->url($link['url']);?>"><?=$link['title'];?></a></li>
  10.                 <? } ?>
  11.                 </ul>
  12.             </div>
  13.         <? } ?>
  14.     </div>
  15.     <div id="admin_content">
  16.     <?php
  17.         if ($session->check('Message.flash')):
  18.                 $session->flash();
  19.         endif;
  20.     ?>
  21.     <?php echo $content_for_layout; ?>
  22.     </div>
  23.  
  24. </div>

This plugin gives you a nice looking basic backend panel, by just dropping a folder. [except you have to have callHooks() in your bootstrap.php, however this function is not just for this specific plugin, it will be utilized while working with other plugins as well]

I think the idea of connecting plugins with hooks is amazing, I just realized that it can increase reusability greatly. Just drop folders to your app, and add up a new functionality instantly. There was a project called SpliceIt started in Feb 2006 – which was fully based on plugins, but its not active anymore.

Hope you liked this idea and tutorial. I would like to thank my friend Jacob Mather, he originally inspired me to implement reusability this way.

- Abhimanyu Grover

28 Feb

Killer Applications with CakePHP, JQuery and Adobe Air

CakePHP, Latest Developments, jQuery

Have you ever wished to program desktop applications for your own web app? If you are like me, that is, most of the lazy web programmers… you never put your hands on any desktop language. Even though all of us knows what flexibility can be given to a web app if we develop its desktop based client.

I came across a new development framework, Adobe AIR, which lets you build cool desktop based application simply with web technologies. If you are good at HTML and AJAX you can easily start with this new framework. I was surprised to see that there are so many applications already using this. After just 3 hrs of experimentation with this, I started loving this new tool. It is very cool. Some advantages which impressed me are:

  • Real Fast: I built a stock market monitoring tool in just 3 hrs. It didn't look like I had to learn something new, everything went smooth. It was like developing AJAX enabled HTML pages and testing them in browser. Once you're done, you simply create xml configuration files, copy a JS from framework, compile it, and you're done.
  • Use your favorite JS Library: This is an amazing feature. My personal favorite is JQuery and I use it in almost every project. Please note: you have to have the latest version of JQuery to get it working with Adobe AIR. They recently patched for Adobe AIR only. I already wasted 30 minutes in solving security issues.
  • Cool CSS Extensions: Check this out.
  • Easy Debugging: Any errors/exceptions appear in command window.
  • Easy Drag and Drop: Super-easy functions like dragstart, drag, dragend, dragenter, dragover, dragleave, and drop.

Check out my stock market tool, just a basic version:

Everything is linked to a backend application which runs on CakePHP. Login function (UsersController::login) returns session_key to the desktop application if login is successful.

JavaScript:
  1. function process_login()<br />
  2. {<br />
  3. $("#status").html("Logging in...");<br />
  4. user=$('#user').attr('value');<br />
  5. pass=$('#pass').attr('value');<br />
  6. $.get("http://localhost/StockBack/users/login/"+user+"/"+pass, function(session_key){<br />
  7. if(session_key=='false')<br />
  8. {<br />
  9. $("#status").html("Unable to login");<br />
  10. }<br />
  11. else<br />
  12. {<br />
  13. $("#status").html("Logged in successfully");<br />
  14. $("#login").hide("slow");<br />
  15. $("#panel").show("slow");<br />
  16. }<br />
  17. });<br />
  18. }

(Sorry for posting code like this but my code editor in wordpress sucks – if anyone knows a good one, please recommend me)

To start learning, some important links are:

Developing Adobe AIR Applications with HTML and Ajax

Adobe AIR Quick Starts for HTML

Adobe AIR Language Reference for HTML Developers

The Adobe AIR HTML documentation set (a ZIP file) is available for download here:

http://www.adobe.com/go/learn_air_html_docs