Using different Date and Datetime format in CakePHP 1.2

December 20th, 2007 by Nik Chankov Leave a reply »

Here I would like to show you a behaviour which will help you to handle different date format than yyyy-mm-dd supported from the database. The reason why I created this behaviour is because in most of my projects I had a requirement to display the date format in human readable format.

So, without extra words here is the behaviour class /Update: thanks to Shark from the comments below we have datetime support as well/:

<?php
/**
 * Prevent deletion if child record found
 *
 * @author  Nik Chankov
 * @url http://nik.chankov.net
 * @see http://nik.chankov.net/2007/12/20/using-different-date-format-in-cakephp-12/
 */

class DateFormatterBehavior extends ModelBehavior {
   
    /**
     * Class Vars
     * All these variables can be set from Configure class
     */

    //Data format for humans
    var $dateFormat = 'dd/mm/yyyy';
    //Dataformat for database
    var $databaseFormat = 'yyyy-mm-dd';
    //delimeted for humans
    var $delimiterDateFormat = '/';
    //delimiter for database
    var $delimiterDatabaseFormat = '-';
    //delimiter between date and time
    var $delimiterDateTimeFormat = ' ';
    /**
     * Empty Setup Function
    */

    function setup(&$model) {
        //Getting user defined vars
        $dateFormat = Configure::read('DateBehaviour.dateFormat');
        if($dateFormat != null){
            $this->dateFormat = $dateFormat;
        }
        $databaseFormat = Configure::read('DateBehaviour.databaseFormat');
        if($databaseFormat != null){
            $this->databaseFormat = $databaseFormat;
        }
        $delimiterDateFormat = Configure::read('DateBehaviour.delimiterDateFormat');
        if($delimiterDateFormat != null){
            $this->delimiterDateFormat = $delimiterDateFormat;
        }
        $delimiterDatabaseFormat = Configure::read('DateBehaviour.delimiterDatabaseFormat');
        if($delimiterDatabaseFormat != null){
            $this->delimiterDatabaseFormat = $delimiterDatabaseFormat;
        }
        $delimiterDateTimeFormat = Configure::read('DateBehaviour.delimiterDateTimeFormat');
        if($delimiterDateTimeFormat != null){
            $this->delimiterDateTimeFormat = $delimiterDateTimeFormat;
        }
        $this->model = $model;
    }
   
    /**
     * Function which convert one date from format1 to format2
     * basically this function play with those three elements of the date - dd, mm, yyyy
     * with delimiter you define which one of the elements is where
     *
     * @param string $date date string formated with format1
     * @param string $format1 format in which is formatted the $date variable by if it's comming from database is yyyy-mm-dd
     * @param string $format2 new format for the date.
     * @param char $delimiter separater between different elements of the date string /i.e. dash (-), dot(.), space ( ), etc/
     * @return string date formated with $format2
     * @access restricted
     */

    function _convertDate($date, $format1, $format2, $delimiterDateFormat, $delimiterDatabaseFormat){
        if($date == null OR $date == ''){
            return '';
        }
        //Split date and time
        $date = explode($this->delimiterDateTimeFormat, $date);
        $date_array = explode($delimiterDateFormat, $date[0]);
        $format1_array = explode($delimiterDateFormat, $format1);
        $format2_array = explode($delimiterDatabaseFormat, $format2);
       
        $current_array = array_combine($format1_array, $date_array);
        $new_array = array_combine($format2_array, $date_array);
        foreach($new_array as $key=>$value){
            $new_array[$key] = $current_array[$key];
        }
        if(isset($date[1])){
            //merge date and time again
            return implode($delimiterDatabaseFormat, $new_array).$this->delimiterDateTimeFormat.$date[1];
        }else{
            return implode($delimiterDatabaseFormat, $new_array);
        }
    }
   
    /**
     *Function which handle the convertion of the data arrays from database to user defined format and up side down
     * @param array $data data array from and to database
     * @param int $direction with 2 possible values '1' determine that data is going to database, '2' determine that data is pulled from database
     * @return array converted array;
     * @access restricted
     */

    function _changeDate($data, $direction){
        //just return false if the data var is false
        if($data == false){
            return false;
        }
        //Detecting the direction
        switch($direction){
            case 1:
                $format1 = $this->dateFormat;
                $format2 = $this->databaseFormat;
                $delimiterDateFormat = $this->delimiterDateFormat;
                $delimiterDatabaseFormat = $this->delimiterDatabaseFormat;
                break;
            case 2:
                $format1 = $this->databaseFormat;
                $format2 = $this->dateFormat;
                $delimiterDateFormat = $this->delimiterDatabaseFormat;
                $delimiterDatabaseFormat = $this->delimiterDateFormat;
                break;
            default:
                return false;
        }
        //result model
        foreach($data as $key=>$value){
            if($direction == 2){
                foreach($value as $key1=>$value1){
                    if($this->model->name == $key1){ //if it's current model;
                        $columns = $this->model->getColumnTypes();
                    } else {
                        //Fix for loading models on the fly
                        if(isset($this->model->{$key1})){
                            $columns = $this->model->{$key1}->getColumnTypes();
                        } else {
                            if($key1 != 'Parent'){
                                App::import('Model', $key1);
                                $model_on_the_fly = new $key1();
                                $columns = $model_on_the_fly->getColumnTypes();
                            }
                        }
                    }
                    foreach($value1 as $k=>$val){  
                        if(!is_array($val)){
                            if(in_array($k, array_keys($columns))){
                                if(($columns[$k] == 'date' || $columns[$k] == 'datetime') && ($k != 'created' && $k != 'modified')){
                                    if($val == '0000-00-00' || $val == '0000-00-00 00:00:00' || $val == ''){ //also clear the empty 0000-00-00 values
                                        $data[$key][$key1][$k] = null;
                                    } else {
                                        $data[$key][$key1][$k] = $this->_convertDate($val, $format1, $format2, $delimiterDateFormat, $delimiterDatabaseFormat);
                                    }
                                }
                            }
                        }
                    }  
                }
            } else {
                if($this->model->name == $key){ //if it's current model;
                    $columns = $this->model->getColumnTypes();
                } else {
                    //Fix for loading models on the fly
                    if(isset($this->model->{$key})){
                        $columns = $this->model->{$key}->getColumnTypes();
                    } else {
                        if($key != 'Parent'){
                            App::import('Model', $key);
                            $model_on_the_fly = new $key();
                            $columns = $model_on_the_fly->getColumnTypes();
                        }
                    }
                }
                foreach($value as $k=>$val){  
                    if(!is_array($val)){
                        if(in_array($k, array_keys($columns))){
                            if(($columns[$k] == 'date' || $columns[$k] == 'datetime') && ($k != 'created' && $k != 'modified')){
                                if($val == '0000-00-00' || $val == '0000-00-00 00:00:00' || $val == ''){ //also clear the empty 0000-00-00 values
                                    $data[$key][$k] = null;
                                } else {
                                    $data[$key][$k] = $this->_convertDate($val, $format1, $format2, $delimiterDateFormat, $delimiterDatabaseFormat);
                                }
                            }
                        }
                    }
                }
            }
        }
        return $data;
    }
   
    //Function before save.
    function beforeSave($model) {
        $model->data = $this->_changeDate($model->data, 1); //direction is from interface to database
        return true;
    }
   
    function afterFind(&$model, $results){
        $results = $this->_changeDate($results, 2); //direction is from database to interface
        return $results;
    }
}
?>

This code will work very good with the Advanced Date Picker Helper which I recently updated with CakePHP1.2 conventions /changing the way how to set initial variables/.

How to use it:
1. Save the class in /app/models/behaviours/date_formatter.php file.

2. If you have a requirement to set date format in a project, this probably mean that the requirement will apply to all fields and forms in the project, so it will be better if this behaviour apply for all models in the project. The easiest way is to add this to the AppModel class. Open your app_model.php and add the behaviour in it:

<?
class AppModel extends Model{
    ....
    var $actsAs = array(..., 'DateFormatter');
    ....
}

3. Set following Configuration parameters in configuration file /I prefer to use bootstrap.php/:

Configure::write('DateBehaviour.dateFormat', 'dd-mm-yyyy');
Configure::write('DateBehaviour.delimiterDateFormat', '-');

Basically the first one define the human date format and the second is the delimiter between day,month and year parts of the date string.

Bear in mind the if you want to use this behaviour in combination with DatePicker Helper you need to define also the Configure::write(‘DatePicker.format’, ‘%d-%m-%Y’)!

Here I will give an example of the configuration variables:

Configure::write('DateBehaviour.dateFormat', 'dd-mm-yyyy');
Configure::write('DateBehaviour.delimiterDateFormat', '-');
Configure::write('DatePicker.format', '%d-%m-%Y');

The following configuration will output format in the fields like: 29-12-2007 and when it’s going to be saved into DB the firmat will be 2007-12-29

Configure::write('DateBehaviour.dateFormat', 'mm/dd/yyyy');
Configure::write('DateBehaviour.delimiterDateFormat', '/');
Configure::write('DatePicker.format', '%m/%d/%Y');

Will output: 12/29/2007 , when it’s going to be saved into DB the format will be 2007-12-29 and so on.

Notice: There are some limitations about this behaviour here I will list them to prevent any confusions:

  • The behaviour working only for CakePHP 1.2
  • The behaviour support only numeric values of date format i.e. 01-12-2007, 2007-05-31 etc. I know it’s possible to handle full features of the date format, but I will appreciate if somebody give me some ideas how to make this quick and easy.
  • This behaviour working only for first level of Model relations, so if you set $recursive of the model to be more than 1 the children records wont be affected. I decided to leave this as it is now, because there is very nice Time Helper which will handle time formatting for presentation purpose without any problems. The main goal of this behaviour is to handle “fetch from DB and load into inputs of the form” and “fetch from the form and format the inputs properly for the DB insertion”.

I would appreciate if there are any suggestions or comments for this class!

Happy coding! ;)

Advertisement

49 comments

  1. On the trac.cakephp.org opened ticket (or two) with
    solution how to extend Dbo class and enable afterFind callback for non-primary models.
    This solution work well but only for afterFind callback.
    beforeFind method require global changes in Dbo and model classes.

  2. I have similar but more universal autoField behavior. Last release allow to call callback functions for any type fields or just add new fields based on exists as a strng mask.

  3. Just a detail, but I would rename the variables $delimiter1 and $delimiter2 to something that says what the variables are about, maybe $delimiterForOutput and $delimiterForDatabase?

  4. Nik Chankov says:

    @all – thanks for youur comments.
    @Daniel you are right I also made some mistakes while using these vars so I changed them according to other 2 vars now they are $delimiterDateFormat and $delimiterDatabaseFormat. Thanks for the hint ;)

  5. heavyboots says:

    This is a thing of beauty!!! I was just about to sit down to rewrite my ancient, grungy procedural PHP date formatting code for the new version of the company intranet and thank god “cakephp 1.2 afterfind” popped this up first. :)

    One very, very minor note: Spelling-wise, date_formatter should actually have two t’s in it. I initially saved my behavior file as date_formatter.php and it took about 15 minutes to figure out why it wasn’t triggering, lol!

    Again, thanks very much. I am going to check out Advanced DatePicker next!

  6. Nik Chankov says:

    @heavyboots – I am glad that this helps. You are right, this small error really could make big head aches. ;) It’s already fixed.

  7. Trav says:

    Hey Nik,

    Great work! I have followed your instructions but it just wont work. There are no errors reported. I just don’t think its running it or something. I would appreciate any assistance you can give me. Let me know if you need more information (as i know i haven’t given you much to work with).

    Cheers,
    Trav

  8. Drazen says:

    Hi, well I have problem.

    I have simple model with User, Customer, Invoice and InvoiceItem, and needed date format “dd.mm.yyyy”.

    invoice index page ist generated with bake.
    and than I have

    Notice (8): Undefined property: User::$Invoice [APP\models\behaviors\date_formatter.php, line 110]

    on line
    $columns = $this->model->{$key1}->getColumnTypes();

  9. Trav says:

    @Drazen – I have that issue as well. Maybe Nik could post a solution.

  10. Trav says:

    I think the Issue lie with the model setup. A temp fix is:

    Replace line 110:
    $columns = $this->model->{$key1}->getColumnTypes();

    With:
    $mod = new $key1;
    $columns = $mod->getColumnTypes();

    warning….this may be a bad hack! I’m just new to cake!

    Cheers,
    Trav.

  11. Nik Chankov says:

    Guys, sorry for this delay.

    Indeed there was a problem in this behaviour and so far the solution is the same as Trav suggest – to create new model on the fly /if the model is not in the closest relation/ and to get the fields from there.

    The code snippet has been updated.

    In fact the difference from he Trav’s suggestion is that I check if this model class is loaded in the memory. Trav we thinking the same ;)

    I don’t think it’s a problem using this method and I also think that it’s not the most elegant solution. It’s possible to store columns into Session var, but so far I will leave it as it is now.

    Hope how it wont give any troubles.

  12. Drazen says:

    inbetween I was going in this direction, but hadn’t cakephp knowledge for a solution.

    occures when there is a relation belongsTo and haveMany in model. There is also a “Check” query generated. (query content is a “Check” string)

    this fix do the trick (no it’s not a trick its solution for a while :-)

    I need this also for a decimal separator conversion.
    Number format in Europe shuld be comma not a point.
    http://en.wikipedia.org/wiki/Image:DecimalSeparator.png
    and I did’n find other solution.

    I’m making a link in cakephp-de group.

    thank You all!

  13. Fernando Mormul says:

    Hi,

    Just a correction to work properly wih PHP4

    Line 189:
    function beforeSave($model) {

    Add & to work properly:
    function beforeSave(&$model) {

  14. Pepito grillo says:

    Hi,

    There’s a problem in the behaviour. Related with created and modified fields. The model class put this fiels automatically, and it consults the database format directly (cake/libs/model/model.php, line 1150). That happens before the beforeSave function, so, when the behaviour change the date and try to save it it is in the incorrect format.

    I think that modifying the behaviour to don’t touch ‘modified’ and ‘created’ fields it will work fine.

    Hope it could help.

  15. Pepito grillo says:

    Ups, also another thing. The code was modified with delimiterDateFormat instead delimiter1, but in the text instruction, following the code, the config params are:
    Configure::write(‘DateBehaviour.delimeter1′, ‘-’);
    So it doesn’t work until you change it. I don’t know if you can change this in the article easily, but I think that it could help people to use the behaviour quicker.

    Thanks for the code!

  16. Nik Chankov says:

    @Pepito grillo – Thanks for the correction about the variable issue. About your other comment related to the “created” and “modified” fields. I just check my example and I haven’t seen any problem with these fields. Is it possible this problem to be caused by other reason? I am working on latest version of CakePHP1.2 on XAMPP and my created and modified field are working properly.
    By the way is it possible that instead od DATETIME for them you set as type a DATE? This could be the root of the problem :)

  17. Pepito grillo says:

    You are right :)

  18. Mark says:

    Does this only work for Datetime fields?
    or for date fields as well?

    how about multilangual support?
    lets say, german and englisch.
    how could you switch between those two?
    dd.mm.yyyy – and dd/mm/yyyy etc

    thx, mark

  19. Nik Chankov says:

    Mark, this behavior is working for mainly for Date fields.
    About multilingual – it’s support only numeric formats such as, 23-03-2008, 03-23-2008 etc. so it doesn’t need to be multilingual.
    Yes you can switch between formats by setting following vars in your configuration. I usually do this in bootstrap.php file with code:

    Configure::write('DateBehaviour.dateFormat', 'dd.mm.yyyy');
    <br>
    Configure::write('DateBehaviour.delimiterDateFormat', '.');

    basically this define the date format which should be in the input fields as well as delimeter, so the script would know what are the parts of the date and how to reformat them in order to fit properly in the database.

    hope this make sense.

  20. Mark says:

    i though more about beeing able to switch the “Format Settings” like dd.mm.yyyy to mm/dd/jjjj at runtime – lets say, if a user changes the language from german to englisch e.g.
    so he gets his familiar kind of date format.
    thats what i meant with multiligual^^

    but i think this is possible – in the language changing component, there has to be function to alter the Configure:: setting.
    didnt test it yet, though.

  21. Nik Chankov says:

    Mark, yep this is the solution. When you switching the lang you need to Configure::write the language specific time format and users directly will see their familiar format in the fields.

  22. Josh Crowder says:

    I’m glad I found this! I was just about to spend endless hours making a new helper that handles the form->input() but this is a much much cooler. Is there a way to change it so it can handle datetime

  23. Nik Chankov says:

    Hi Josh,

    This behavior is done especially for date format. I think it would be possible to make it datetime, but it need different function or at least to extend the current one. The good thing is that time format is unified to hh:mi:ss :)

    If I was on you I would extend _convertDate() function so before converting the dates I would separate the time string from the date one.

    In this case in settings I need to have $timeDelimeter so if the date is let’s say in format:
    dd-mm-yyyy hh:mi:ss and the database require:
    yyyy-mm-dd hh:mi:ss I need to set $timeDelimeter = ” “;

    You know what? I think datetime format is pretty good idea and if you not in a hurry I will modify this behavior to support datetime as well :)

  24. hugh says:

    The I’ve just changed _convertDate. It seems to work with non numeric date values. I’ve only tested it with dates like 19-Sep-2008. I also had to change the default date formats to Y-m-d supported by strtotime() and the delimiter fields are no longer requried.

    /**
     * @param $date Date string to be converted
     * @param $format The format string the date should be converted to.
     */

    function _convertDate($date, $format)
    {
            if($date == null OR $date == ''){
                return '';
            }

            $time = strtotime($date);
            $dateStr = date($format, $time);
            return $dateStr;
    }
  25. Nik Chankov says:

    Hugh,

    I know that this way the code is more short and elegant, but especially for numeric date format it’s quite difficult to judge what is the format. Let’s say What is the date format of: 10.09.2008? Is it 9-th of October, or 10-th of September? :)

    I also know that it’s possible to set date_default_timezone_set(). But I don’t know why, but I don’t trust this convention at all.

    In another hand, this behavior is made mainly to support date inputs, where it’s possible to add the date by keyboard and I believe that only digits are more convenient.

    I would say that I could put this function as additional let’s say __convertFreeDate() and if you want to use it, there could be set a trigger which will switch between these 2 functions.

    Anyway, it’s good that this behavior helps :)

  26. Shark says:

    Hi Nik,

    Nice behavior!!, but i’ll need it for datetime too haha.

    Did you modify this behavior to support datetime already?

  27. Nik Chankov says:

    Wow, Shark that was quick – 30 minutes for datatime support. In fact I had the same idea, But I wanted to put date-time delimiter in a parameter of the class just in case.

    With your permission I will replace your code in that post so everybody could use it.

    Thanks again.

  28. Shark says:

    Haha, it’s fine by me!

    Good work Nik!!

  29. Josh says:

    For some reason it doesnt like created and modified, any reason why?

  30. Nik Chankov says:

    The reason is very simple – created and modified fields are “computed”, they don’t need to be filled by user :)

  31. Josh says:

    Thats what I thought but why is it effecting it, is there work around for this?

  32. Nik Chankov says:

    You know why? Because recently this behavior was only for date fields and these fields were datetime :)

    I will add exception for them right now.

  33. Nik Chankov says:

    The behavior is changed, now it should be fine, thanks for pointing this out.

  34. Lucas says:

    THAAAAANK YOOOOUUU!

  35. Ezequiel Nuske says:

    Great code! Just what I was looking for. By the way, I had some trouble with models that worked with Alias.

    The problem was solved by replacing:

    if($this->model->name == $key1){ //if it's current model;

    with

    if($this->model->alias == $key1){ //if it's current model;

    Thanks again for releasing this code!

  36. DEAFWISH says:

    you (and those who helped with this) totally rock#!!#!

  37. Great Code. Many thanks!

  38. Steve says:

    This is great, thanks! But.. it works on my local computer fine, but I just uploaded it to my server (Linux/Apache) and it stopped working, all dates are 0000-00-00 00:00:00! (It also messes up an account expiry date I have for my login users (using authake), so some users cant login). Hmmm I am stumped!

  39. Nik Chankov says:

    Well, if it’s working on your machine, and on the server doesn’t this mean that settings on the server are messed up or the locale variable in php or mysql is set to different than yyyy-mm-dd HH:ii:ss format.

  40. d-fens says:

    hi,

    i have problems with validation for dd.mm.yyyy: somehow everything is accepted, how must this be done?

    ‘birthday’ => array(
    ‘rule’ => array(‘rule’ => array(‘date’, array(‘dmy’))),
    ‘message’ => ‘Please supply a valid date.’
    ),

  41. dipali says:

    how to use this behaviour in my list page to make modified,created date dispalyed in dd/mm/yy format? what should i change additionally, i’ve made behaviour, config file chenges ….

  42. Nik Chankov says:

    dipali, this behaviour is created to serve the data in forms, so, let’s say you want to allow users to fill some data fields in a form.

    If you want to display formated date, just use Time Helper and especially it’s format function. :)

  43. Daniel says:

    I have enjoyed this date behaviour but recently encountered a problem with it.

    I was getting the following error.

    Fatal error: Class ‘OrganisationLegalInformation ‘ not found in /websites/sitename/app/models/behaviors/date_formatter.php on line 159

    I wanted to post the solution in case someone else encounters the same problem.

    The site worked perfectly withought the behavior and at first I could not figure out what the problem was.

    I found that the class name had an extra space that would not allow it to be called.

    If you modify the code

    157 if($key != ‘Parent’){
    158 App::import(‘Model’, $key);
    159 $model_on_the_fly = new $key();

    to

    157 if($key != ‘Parent’){
    158 $key = trim($key);
    159 App::import(‘Model’, $key);

    It solves the problem. I hope this helps some people out their that may encounter the same issue.

    If their is another way please let me know.

  44. Jose Luis says:

    Hi, I’m trying to set up your behaviour, and I’m getting this error:

    Fatal error: Class ‘Otherdemandado’ not found in /home/joseluisgonl/web/juridico/models/behaviors/date_formatter.php on line 130

    I see it is about loading on the fly models, and specially a HABTM that is not called the same as its class.

    Thanks in advance for your help! great behaviour

  45. Nik Chankov says:

    Although I am using it, I would suggest you to use $timeHelper->format() when you displaying date on the screen and to use jQuery Datepicker especially:
    http://jqueryui.com/demos/datepicker/#alt-field

    I think this will be enough to “survive” without this behavior. :)

Leave a Reply