This blog is about the PHP solutions I have to think of when I'm developing systems almost every single day...

Wednesday, February 27, 2008

Finally got the behaviour working

Wow!! CakePHP is great!! Now I've even got my own behavior working. This is the latest models/behaviors/comment.php which I have come up with.


<?php
class CommentBehavior extends ModelBehavior
{
var $settings=array();
var $runtime=array();
var $Comment;

function __construct(){
$this->Comment=ClassRegistry::init('Comment','model');
}

function setup(&$model,$config = array()){
$this->settings[$model->alias]['assocAlias'] = $model->alias.'Comment';
return true;
}

public function beforeFind(&$model, $query){
$commentcond['Comment.model']=$model->alias;
if(!empty($query['conditions']) && is_array($query['conditions'])) {
foreach($query['conditions'] as $fieldName => $constraint) {
if(!strstr($fieldName,'Comment.')) {
continue;
}

$commentcond[$fieldName] = $constraint;
unset($query['conditions'][$fieldName]);
}
}

$this->runtime[$model->alias]['query']['conditions'] = $commentcond;
return $query;
}

public function afterFind(&$model,$results,$primary){
extract($this->settings[$model->alias]);
foreach($results as &$result){
if(!isset($result[$model->alias][$model->primaryKey])){
continue(1);
}
$commentcond=$this->runtime[$model->alias]['query']['conditions'];
$commentcond['Comment.foreign_key']=$result[$model->alias][$model->primaryKey];
$comment=$this->Comment->findAll($commentcond);
if(empty($comment)){
continue(1);
}
$result[$assocAlias]=$comment;
}
return $results;
}

public function beforeSave(&$model)
{
extract($this->settings[$model->alias]);
$this->runtime[$model->alias]['beforeSave'][$assocAlias] = $model->data[$assocAlias];
return true;
}

public function afterSave(&$model,$created)
{
extract($this->settings[$model->alias]);
$data=$this->runtime[$model->alias]['beforeSave'];
unset($this->runtime[$model->alias]['beforeSave']);

foreach($data[$assocAlias] as &$comment) {
if($created) {
$comment['foreign_key'] = $model->getLastInsertID();
} else {
$comment['foreign_key'] = $model->id;
}

if(!isset($comment['id'])) {
$this->Comment->create();
}

$comment['model'] = $model->alias;
$this->Comment->save($comment,false);
}
return true;
}
}

class Comment extends AppModel
{
var $name="Comment";
var $useTable="comments";
}
?>

And so to use it in the the model declaration you have to have the line:

var $actsAs=array('Comment');

And that's all set. You will receive the Comment parts too if you do a findAll() on it. To save Comment you'd have to do something like this:

$dcomment['Comment']['id']=1;
$dcomment['Comment']['description']="is it have funny cartooniiist dileb";
$this->data['MeetingComment'][]=$dcomment;

Saving that meeting would also save the meeting comment. Note the first line is if you are editing the comment. If you exclude it then you will actually save a new comment. Cool stuff. Now off to creating the controls for it.

Warning!!! The error is cached

I tried to create my own behavior in CakePHP today. Using ideas from the attachments solution, my first step was just to try to read in data that I've already manually keyed into the database. Associate them with each other. This is what I have done so far (goes into models/behaviors/comment.php):


<?php
class CommentBehavior extends ModelBehavior
{
var $settings=array();
var $runtime=array();
var $Comment;

function __construct(){
$this->Comment=ClassRegistry::init('Comment','model');
}

function setup(&$model,$config = array()){
$this->settings[$model->alias]['assocAlias'] = $model->alias.'Comment';
return true;
}

public function beforeFind(&$model, $query){
$commentcond['Comment.model']=$model->alias;
if(!empty($query['conditions']) && is_array($query['conditions'])) {
foreach($query['conditions'] as $fieldName => $constraint) {
if(!strstr($fieldName,'Comment.')) {
continue;
}

$commentcond[$fieldName] = $constraint;
unset($query['conditions'][$fieldName]);
}
}

$this->runtime[$model->alias]['query']['conditions'] = $commentcond;
return $query;
}

public function afterFind(&$model,$results,$primary){
extract($this->settings[$model->alias]);
foreach($results as &$result){
if(!isset($result[$model->alias][$model->primaryKey])){
continue(1);
}
$commentcond=$this->runtime[$model->alias]['query']['conditions'];
$commentcond['Comment.foreign_key']=$result[$model->alias][$model->primaryKey];
$comment=$this->Comment->findAll($commentcond);
if(empty($comment)){
continue(1);
}
$result[$assocAlias]=$comment;
}
return $results;
}
}

class Comment extends AppModel
{
var $name="Comment";
var $useTable="comments";
}
?>


Quite straight forward I think. But earlier on I made a mistake and cakephp spit out a request address not found error. I cut down all the additional code until I got to the very basic which you see above and it still gives out that error. I was stumped. Well, a quick search into the cakephp google groups shows that there is caching for the models and so I deleted the content of tmp/cache/models. It works after that. Alhamdullillah.. :)

Monday, February 25, 2008

CakePHP $html->link Variables

You can send additional variables to the next page with additional keys in the array like this:


echo $html->link(__('text to link',true),array('controller'=>'dest_controller','action'=>'dest_action','additional_variable'=>'variable_data'));

And the data can be accessed in the controller with the $this->params['named'] variable.

What will you find?

Hmm... The lack of clear documentation is getting clearer and clearer as I find that I have to code dive more and more to learn how to use CakePHP. Latest one is how on earth do you use the find method?

According to the api, the declaration is like this:

function find($conditions = null, $fields = array(), $order = null, $recursive = null) {

So it accepts conditions, then fields, order and recursive value. But if you had searched a little more on the internet before looking at the api you might have found some people did this:

$model->find('list',array('conditions'=>array('id'=>1)));

And looking into the code you will find that if $conditions is a string and it is a valid find method then you are left with pretty much just two variables. $conditions become the type of find you want, $fields become an array of variables pertaining to the search which could have keys consisting of 'conditions' (like the example above), 'fields', 'limit', 'offset', 'order', 'page' and an array of 'joins'. The type (which we get from the variable $conditions) could either be 'count', 'first' or 'list'. The default (aka if you didn't set it in $conditions and it uses all the variables like the way they are named) is 'first'.

The reason why I was diving into the code initially was I wanted to make a list for the select box option. I was pretty sure that cakephp could easily take care of that. And inside the code I see it. The 'list' find type is already suited for this and it uses $this->displayField to display the option to be selected by the user and $this->primaryKey for the value to put into that option. The $this->primaryKey seems to default to id which is already good enough but $this->displayField should really be set in your model so that it would be used automatically. Once it is set, you're good to go. Just:

$this->Model->find('list',array('conditions'=>array('parent_id'=>'1')))

And you would already have all the data you need for your select box. It's pretty convenient but to get there is the real challenge.

Thursday, February 21, 2008

Fixup for the attach to anything plugin for cakephp

There is a great plugin for cakephp. It basically allows you to attach files to any models that you have. Quite useful if you have many models which needs attachments. It is at http://bakery.cakephp.org/articles/view/attachments .

This is a great piece of code. Got so much features already built in. Hats off to the developers and all the contributors.

But there is a few things I would like to mention here on how to get it working. It took me a whole day just to figure it out.

The first part is the database. The script included in the download is missing a the auto increment extra. So you should change the file in config/sql/attachments.php so that the line with:


'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary'),


is changed to:

'id' => array('type'=>'integer', 'null' => false, 'default' => NULL, 'key' => 'primary', 'extra' => 'auto_increment'),


Note the auto increment part at the end. Once that is done you can create it with bake. The command is just (running from your current cake root directory):
cake/console/cake schema run create attachments

That should set up your database if all your configuration is right.

And then I find that I need to change the file models/behaviors/attachment.php. In there under the function setup I have to add:

if(!is_array($config)){
$config=$this->defaultSettings;
}

Right at the beginning of the function so that there would be a default if there wasn't one passed by the user (And actually I don't have a clue how to pass one actually).

And then in the same file under the beforeSave function I have to add the following code:

foreach($attachment as $key=>$data){
if(is_array($data)){
foreach($data as $skey=>$sdata){
$uh[$skey][$key]=$sdata;
}
}
}
$attachment=$uh;


before line 365 just before the test:

if(isset($attachment['file'])) {


because it seems the arrangments of the array is wrong for the Transfer object. But apart from that it works pretty well.

Tuesday, February 19, 2008

A soft delete behaviour

Found this behavior here http://bakery.cakephp.org/articles/view/soft-delete-behavior . This could potentially be very useful.

Monday, February 18, 2008

Internationalization with CakePHP

I just found this article http://bakery.cakephp.org/articles/view/p28n-the-top-to-bottom-persistent-internationalization-tutorial, it looks like a great tutorial on internationalization with CakePHP. Hope to be able to use it soon.

Sunday, February 17, 2008

Output helper javascript into headers properly

With some pointers taken from cakebaker at this article: http://cakebaker.42dh.com/2007/09/28/how-to-write-and-test-your-own-head-helper/ , I was finally able to properly output the datepicker javascript into the header part of the file like it was supposed to. Basically the idea is get a reference to the view using the ClassRegistry::getObject method to get the view. And use the view's method addScript to add a script to the header part of the view. Here's the modified version of the datePicker helper.


<?php
/**
* Autocomplete Helper
*
* @author Nik Chankov
* @website http://nik.chankov.net
* @version 1.0.0
*
* @updated 2008-02-13
* @author Abdullah
* @website http://abdullahsolutions.com
* @changes Used helpers array. Also used beforeRender so that the javascripts and theme is automatically loaded
*/

class DatePickerHelper extends FormHelper {

var $format = '%Y-%m-%d';
var $helpers = array('Javascript','Html');

/**
*Setup the format if exist in Configure class
*/
function _setup(){
$format = Configure::read('DatePicker.format');
if($format != null){
$this->format = $format;
}
else{
$this->format = '%Y-%m-%d';
}
}

function beforeRender(){
$view = ClassRegistry::getObject('view');
if (is_object($view)) {
$view->addScript($this->Javascript->link('jscalendar/calendar.js'));
$view->addScript($this->Javascript->link('jscalendar/lang/calendar-en.js'));
$view->addScript($this->Javascript->link('common.js'));
$view->addScript($this->Html->css('../js/jscalendar/skins/aqua/theme'));
}
}

/**
* The Main Function - picker
*
* @param string $field Name of the database field. Possible usage with Model.
* @param array $options Optional Array. Options are the same as in the usual text input field.
*/
function picker($fieldName, $options = array()) {
$this->_setup();
$this->setFormTag($fieldName);
$htmlAttributes = $this->domId($options);
$divOptions['class'] = 'date';
$options['type'] = 'text';
$options['div']['class'] = 'date';
$time='';
if(isset($options['showstime'])){
if($options['showstime']===true) {
$time=',"24"';
$this->format.=" %k:%M";
}
unset($options['showstime']);
}
$options['after'] = $this->Html->link($this->Html->image('../js/jscalendar/img.gif'), '#', array('onClick'=>"return showCalendar('".$htmlAttributes['id']."', '".$this->format."'$time); return false;"), null, false);
$output = $this->input($fieldName, $options);

return $output;
}

function flat($fieldName, $options = array()){
$this->_setup();
$this->setFormTag($fieldName);
$htmlAttributes = $this->domId($options);
$divOptions['class'] = 'date';
$options['type'] = 'hidden';
$options['div']['class'] = 'date';
$hoder = '<div id="'.$htmlAttributes['id'].'_cal'.'"></div><script type="text/javascript">showFlatCalendar("'.$htmlAttributes['id'].'", "'.$htmlAttributes['id'].'_cal'.'", "'.$this->format.'", function(cal, date){document.getElementById(\''.$htmlAttributes['id'].''.'\').value = date});</script>';
$output = $this->input($fieldName, $options).$hoder;

return $output;
}
}
?>


Now we can output better pages with this helper.

Friday, February 15, 2008

Ajax with Cakephp resources

Found a great blog which lists many ajax resources for cakephp at http://ahsanity.wordpress.com/2007/02/23/get-started-with-ajax-in-cakephp/. Have to dive into there someday.

Login redirect causing problems

Okay... once in a while when I'm developing a cakephp application using the previously posted method of changing the app_controller.php to redirect if the user has not logged in causes some problems. Namely firefox will say that it has detected that we are being redirected in a way which will never finish. Solved it a few times but because I didn't write it down I had to solve it again. Apparently the problem is that there is some error with your application. Maybe a missing table or field in your database, maybe a mistaken setting. Whichever way it is you just switch off redirects for a while so that the problem will be properly displayed, fix the problem and enable redirects again.

Thursday, February 14, 2008

Bindable behaviours

Bindings seems to be very useful in cakephp. http://bakery.cakephp.org/articles/view/bindable-behavior-control-your-model-bindings offers an interesting read on how to do it.

HABTM Add & Delete Advanced Behaviour

HABTM is quite a hard concept to wrap your head around. And not knowing much about the black box that is cake make it a tad harder. But thanks to Brandon Parise from his post here: http://bakery.cakephp.org/articles/view/add-delete-habtm-behavior, you don't need to hack a automatic handler for deleting and adding HABTM items.

Tuesday, February 12, 2008

Using the Coolest DHTML/Javascript Calendar in CakePHP

The helpers in CakePHP are great but some things aren't all that great. One of them is the form date helper. It is really annoying to have to click on 3 select box for every date you want to key in. So finally I tried looking around and found http://nik.chankov.net/2007/09/13/advanced-datepicker-helper-for-cakephp/. Followed the instruction on the page and it worked like a charm. Only problem is that apart from adding the helper in the controller, you also have to type in the lines to include the javascript in every view that uses it (that is at least the add and edit view). So I've changed the Helper a bit so that it would include the link automatically before it render.


<?php
/**
* Autocomplete Helper
*
* @author Nik Chankov
* @website http://nik.chankov.net
* @version 1.0.0
*
* @updated 2008-02-13
* @author Abdullah
* @website http://abdullahsolutions.com
* @changes Used helpers array. Also used beforeRender so that the javascripts and theme is automatically loaded
*/

class DatePickerHelper extends FormHelper {

var $format = '%Y-%m-%d';
var $helpers = array('Javascript','Html');

/**
*Setup the format if exist in Configure class
*/
function _setup(){
$format = Configure::read('DatePicker.format');
if($format != null){
$this->format = $format;
}
}

function beforeRender(){
echo $this->Javascript->link('jscalendar/calendar.js');
echo $this->Javascript->link('jscalendar/lang/calendar-en.js');
echo $this->Javascript->link('common.js');
echo $this->Html->css('../js/jscalendar/skins/aqua/theme');
}

/**
* The Main Function - picker
*
* @param string $field Name of the database field. Possible usage with Model.
* @param array $options Optional Array. Options are the same as in the usual text input field.
*/
function picker($fieldName, $options = array()) {
$this->_setup();
$this->setFormTag($fieldName);
$htmlAttributes = $this->domId($options);
$divOptions['class'] = 'date';
$options['type'] = 'text';
$options['div']['class'] = 'date';
$options['after'] = $this->Html->link($this->Html->image('../js/jscalendar/img.gif'), '#', array('onClick'=>"return showCalendar('".$htmlAttributes['id']."', '".$this->format."'); return false;"), null, false);

$output = $this->input($fieldName, $options);

return $output;
}

function flat($fieldName, $options = array()){
$this->_setup();
$this->setFormTag($fieldName);
$htmlAttributes = $this->domId($options);
$divOptions['class'] = 'date';
$options['type'] = 'hidden';
$options['div']['class'] = 'date';
$hoder = '
';
$output = $this->input($fieldName, $options).$hoder;

return $output;
}
}
?>

Finally a working login in CakePHP

Waaah!!! It took so long for me to finally get this working right. I can hardly believe it. Based on the very helpful tutorial I've posted before, I finally got a you've got to login first before anything at all kind of website.

I used the validateLogin function in the said tutorial in my User model and the login, logout function in my UsersController. Then I modified app_controller to have a beginFilter like so:


function beforeFilter() {
if($this->Session->check('User') == false){
if($this->name!='Users' || ($this->action!='login' && $this->action!='logout')){
$this->redirect(array('controller'=>'users','action'=>'login'));
}
}
}


So I didn't change the beforeFilter in the UserController like the tutorial but in the app_controller. And based on the __validateLoginStatus function I changed a bit to check whether there is a valid user session first. If there isn't one then if they are trying to view other controllers (the name variable is the name of the current controller) other than Users or if they are even using users but doing other actions apart from login and logout then redirect to the users login page. Fuh.. finally...

And just to mention a bit, if you want to check for the session in a view (maybe different menu for logged in user) then use $session directly as in:

if($session->check('User'))

And now the path is open to more development.. :)

Monday, February 11, 2008

Create a login in CakePHP

By refering to http://bakery.cakephp.org/articles/view/simple-form-authentication-in-1-2-x-x, I was able to do a basic login page in CakePHP.

Update on rendering element in CakePHP

I am not sure whether I made a mistake in the previous post or something has changed in cake. Anyhow, I wasn't able to do a simple $this->renderElement call to render my elements. After a bit of testing and trying it out some more, it seems you've got to actually echo out the result. So in the end it had to be like this:


<?php echo $this->element('topbanner');>


Notice also that I'm just saying just element rather than renderElement.

About Me

My Photo
Abdullah Zainul Abidin
I am what I am because of what I am...
View my complete profile