RSS
 

Posts Tagged ‘Cake1.3’

Console for CakePHP

03 Oct

Windows and Cake1.x

Nothing easier than that! 2 minutes and it works.

I prefer WAMP (XAMPP sucks). But I describe it for both. WAMP has the advantage that you can easily switch php versions (5.2.10, 5.3.0, 5.3.1, …). For the path it is annoying to edit the version number all the time. So (WAMP ONLY!) copy your desired php version from /bin/php/php5.x.x to /bin/php/php_for_console. If you now need to switch the php version you can simply replace the files in this folder. You don’t have to edit the system path. For XAMPP there is usually only one php directory, anyway.

We need to add these two paths to the System Path: C:...\xampp\php; (for XAMPP) OR C:...\wamp\bin\php\php_for_console (for WAMP with copied folder) OR C:...\wamp\bin\php\php5.x.x (for WAMP without the above trick – not recommended)

AND C:...\project\cake\console (your project is usually inside www or htdocs folder)

Here you could set up a second cake folder, as well, which only serves the console (e.g. “/C:...\wamp\bin\cake”). This way the cake version for the console can easily be managed – separately from any cakephp app.

We need to open “my pc properties” -> “environment variables”. In “path” we need to append the 2 paths from above.

Separate them with semicolons: ...;C:...\xampp\php;C:...\project\cake\console Done! It should now work right away on any Windows (XP, Vista, 7). Not sure if a restart is necessary, though.

Please note: If you have multiple partitions, don’t put your cake/php stuff (WAMP, XAMPP) on C:. There might be some user rights issues. You are better off using D:/ or any other partition. And you shouldn’t have any persistent data on the system partition, anyway. I usually have windows and installed programs one C:\, everything else is on other partitions.

Don’t just open a console. In order to use the console for any of your apps you need to navigate to the APP folder. Otherwise “cake …” etc will not find the shell scripts in your app: C:...\project\app>

Hot Tip: To quickly open a cake console on any of your apps, simply press SHIFT + right click on the app folder. You will now see a new button “Open console here” / “Eingabeaufforderung hier öffnen”. That’s it. With “cake” you should see the welcome screen and a list of all possible shells.

And – if you did everything right – you should be able to execute your shells like so:

cake command args -params
For example to bake a model:
cake bake model

Linux

See my new article about it

NEW – Cake2.x

For Cake2 we need to adjust the project path like so C:...\project\lib\Cake\Console Nothing fancy after all.

HOT TIP

If you need to run both 1.x and 2.x on the same system, don’t append anything besides the pure PHP path to the System path. You can then create two keyboard shortcuts (like I do) for the different versions with a tool of your choice. I created cw1# (“cake windows v1)” for ..\cake\console\cake cw# for ..\lib\Cake\Console\cake So I am in the app path and just type cw# bake which automatically translates cw# into the above path and then, of course, starts up the bake shell. With the ..\ syntax CLI always uses the correct cake version (in /trunk/lib if your project app is in /trunk/app).

 
No Comments

Posted in CakePHP

 

Setting up PHPDesigner for CakePHP

22 Sep

There are many great IDEs for PHP development. One is PHPDesigner, which is mainly for windows.

With some minor adjustments it works pretty well with cake.

Preferences

Go to: Tools -> Preferences [CTRL + E]

  • General File Encoding: UTF8 Trim on save: Trim Right (no need to save empty spaces to the right) Detect Changes: Yes (very useful!) Sublink -> Document Types: Add *.ctp to PHP
  • Editor Tabs: YES (we want to use one tab as indend) Tabs to spaces: NO (should not be converted) Tab Width: 2 (so one tab will be 2 spaces wide)
  • Debugger Syntax Check: I like to use a little bit different color here (a brighter red).

Those are just the most important ones. You can play with other settings, as well.

Project Setup

Go to: Projects -> Project Manager [F11]

New => Add “Title” => Select “Root Folder” (should be your app directory!) => Add “Libraries” (I usually add the cake folder, but you could also add the vendors folder here) => Just hit “Next” until it’s finished. You can now open the project. After a few seconds/minutes all functions are indexed and can be looked up with “Rightclick -> Go to declaration”.

All PHP-internal functions can be looked up with F1 (Quick lookup in the PHP Manual).

To read more about code completion, see my other article about it. It describes how you can use a shell script to dump all components, helpers, … into one “dummy” file to help PHPDesigner index and auto-complete all library functions.

Here is one example of how all cake and app functions get auto-completed and displayed:

Final Tips

Sometimes the files you open are still ANSI. If you add UTF8-Signs save it as UTF8 in order to display them correctly. Otherwise it will either be nonsense or nothing at all. After editing or creating Projects you need to properly close and restart your editor. Otherwise changes might be lost after the next restart. This is some bug of PHPEditor, I guess.

PHPDesigner and Linux

The best of all IDEs is nativly only available for Windows. For all those poor rats out there that use Linux: www.zephid.dk/2008/07/01/using-phpdesigner-under-linux/ Seems to work (if you believe the comments).

 
No Comments

Posted in PHP

 

Saving Model Data and Security

21 Sep

In google groups there are quite a few discussions every month about security against (primary key) injection, xss or other things. And yes, the default templates will not protect you from any of this. They are meant to produce quick “standard” output. But they are not prepared for the real world, at all. It is actually quite irresponsible not to use custom templates or manually adjust the forms/views. UPDATE: I could finally convince them to use h() in their default templates! A big WIN for security!

A while ago i wrote about how to create custom templates. See this article for details. You will also notice, that the controller actions and views are protected against basic – and only basic – injections. That is mainly xss-attacks. By using h() in the views we can make sure that dangerous strings get destroyed. The custom templates also ensure, that the record we edit/delete exists.

What it still doesn’t do is protecting the model data.

Note: some of the following could be achieved by adding the security component. But sometimes, it is not possible or not desired.

Protection against too many fields

Another tricky one. Again, Firebug can be used to violate your forms. Lets say, a user wants to have more rights – and he happens to know that User.role_id must be 2 instead of 1 in order to be “admin”. In the User edit form (besides “email”, “birthday” and stuff) he could then inject a form input called “role_id” which usually is not there. With a normal $this->save() operation this would actually work! After the next login his role would be 2 (= admin). What can we do about something like that?

Cake has a “fieldList” as third parameter of the save() function build in:

if ($this->User->save($this->data, true, array('id', 'gender', 'birthday'))) { # note the id (needed here)
Id, gender and birthday are passed, any other field in $this->data will be omitted (especially our role_id^^). I use this in all sensitive forms (user and role related).

Careful: If you don’t pass the id, it will be omitted and the model cannot set its primary key (and might perform “add” instead of “edit”!

Primary key injection on edit actions

This is the not quite the same as the above topics. All edit actions have the primary key in the forms. I usually remove them, but sometimes I am too lazy to do that (because you need to alter the form url then as well). So the id actually exists – but won’t be checked prior to the save. Well, here are some custom solutions – there are more general ones, of course. But I just want to point out the problem and a way to fix it. These could be added to your bake templates, for instance.

Lets take our custom template from above (see link) and modify it:

function edit($id = null) {
    if (empty($id) || !($data = $this->User->read(null, $id))) {
        //error flash message
        $this->redirect(array('action' => 'index'));
    }
 
    if (!empty($this->data)) {
        # solution 1: just override with the "correct" one:
     $this->data['User']['id'] = $id;
 
        # solution 2: check against "correct" one
     if ($this->data['User']['id'] != $id) {
            //BLACKHOLE
        }
 
        if ($this->User->save($this->data)) {
            //OK
        } else {
            //ERROR
        }
    }
}
Solution 1 is self-explanatory and cannot go wrong. Solution 2: After we made sure the record exists, we check the passed id against the record id. If they don’t match someone tries to trick us. Otherwise the record can be saved. Solution 3: Remove the id from the passed arguments (third parameter of save() – but makes it more complicated) Solution 4: Put the check in beforeValidate() of your app_model.php. Usually – with the above template you have just “read” the record, so the model has the id in $this->id. It could then return false if the passed id and $this->id do not match. Thats actually my favorite. But I am not sure if it works in all cases. You may only check if you are actually saving a record – meaning: both ids have to be not empty. Only then it makes sense to apply this specific piece of validation.

Protection against missing fields

This is a tricky one. Most beginners (I didn’t either) dont realize that a form can be modified with tools like and Firebug (Firefox Addon) and that Cake would not bother. Lets say we have a registration form and your gender has to be provided. But you don’t want to. So you simply remove this input from the form and submit it. Violà – it worked!

Why does it work? Cake automatically applies the validation “needed” for this form. All fields that are passed (it doesn’t matter if they are empty or not – they just have to exist on submit) will be checked. Validation rules for fields that did not get passed will be omitted. And thats our problem. Right here.

OK, we need to make sure that the form fields are actually part of the model data before the validation kicks in. I wrote a app model extension which can be used – you could also write a model function which one by one adds the required fields if the field does not exist.

Put this in your app model:

/**
 * set + guaranteeFields!
 * extends the core set function (only using data!!!)
 * 2010-03-11 ms
 */
function set($data, $data2 = null, $requiredFields = array()) {
    if (!empty($requiredFields)) {
        $data = $this->guaranteeFields($requiredFields, $data);
    }
    return parent::set($data, $data2);
}
 
/**
 * make sure required fields exists - in order to properly validate them
 * @param array: field1, field2 - or field1, Model2.field1 etc
 * @param array: data (optional, otherwise the array with the required fields will be returned)
 * @return array
 * 2010-03-11 ms
 */
function guaranteeFields($requiredFields, $data = null) {
    $guaranteedFields = array();
    foreach ($requiredFields as $column) {
        if (strpos($column, '.') !== false) {
            list($model, $column) = explode('.', $column, 2);
        } else {
            $model = $this->alias;
        }
        $guaranteedFields[$model][$column] = ''; # now field exists in any case!
 }
    if ($data === null) {
        return $guaranteedFields;
    }
    if (!empty($guaranteedFields)) {
        $data = Set::merge($guaranteedFields, $data);
    }
    return $data;
}
We can now make sure removing any form field will have no effect :) I call it “enforced whitelisting”.

Case 1: Using $this->Model->set($this->data) and $this->Model->save() – Important: note the NULL in save(), we may not pass the data a second time!

$this->User->set($this->data, null, array('gender', 'birthday', ...));
$this->User->save();

Case 2: Using directly $this->Model->guaranteeFields($fields, $this->data) and $this->Model->save($this->data)

$data = $this->User->guaranteeFields(array('gender', 'birthday', ...), $this->data);
$this->User->save($data);

Lets combine them

$this->data['User']['id'] = $id; // on edit
$this->User->set($this->data, null, array('gender', 'birthday', ...));
if ($this->User->save(null, true, array('id', 'gender', 'birthday', ...))) { # note the id (needed here)
 //OK
} else {
    //ERROR
}
Now thats protection :) Only the fields that you want to are passed and all the fields that should be there are there. And the primary key is the one it is supposed to be. If your validation rules are correct, you got the perfect form handling, I guess.

Blacklisting

// put this in your app_model.php
function blacklist($blackList = array()) {
    return array_diff(array_keys($this->schema()), $blackList);
}
Although not recommended for general usage, this might be useful for some rare occasions. It returns all fields of the current database table except the ones you blacklisted.

Usage:

$this->User->save(null, true, $this->blacklist(array('birthday'));
If you want to pass non-existing fields to the model for validation you need to array_merge() them!
$this->User->save(null, true, array_merge(array('virtual_field'), $this->blacklist(array('birthday')));

Server-side and Client-side Validation

First one is what we just discussed. Client-side usually is JS or browser-specific validation. You should NEVER rely on client-side validation – it can easily be tricked. It is nice to have it – “additionally” in order to prevent unnecessary posts, but the real deal should be what your model validation returns.

SQL Injection

I always smile about other webprojects that are vulnerable to this attack. Its just hilarious. Thats why you use a framework. Because it takes care of the basic problems. And this is one of it. There are only very few things to be aware of: Always manually escape manual SQL queries! CakePHP only takes care of the queries run by $this->find() etc. So if you ever have to write your own SQL script, you will need to do that yourself.

The myth about “required” and “allowEmpty”

They seem to be the same, don’t they? But there is a huge difference between them (the cookbook does mention it here). “required” is like my “Protection against missing fields”, only hard-coded. These fields will validate false, if this field is not present on validation. It does not matter if the field is empty or not. The word “required” only applies to the database field, not its content. “allowEmpty” is equal to “minLength 1″ and ONLY validates this field if the field has been passed to the model. If not, it will not return false. You cannot make sure that a specific field is always filled out with this parameter alone.

If you want to make sure, that the field cannot be avoided AND has to be filled out, you need to combine both rules:

var $validate = array(
    'status' => array(
        'maxLength' => array( # this can be any label you want
         'required' => true, # careful!
         'allowEmpty' => false,
            'rule' => array('maxLength', 5),
            'message' => array('valErrMaxCharacters %s', 5), # a modification of mine to allow better i18n support
         'last' => true # important for more than one rule per field!!!
     ),
        ... # other rules
 ),
    ... # other fields
);
The “careful” stands for: this can get ugly really fast. I never use “required” hard-coded. As soon as you use ajax toggling of some fields or forms which only change a small part of the record, you will get validation errors because the required fields are not present (although you don’t even want to change them). You would need to unset() those rules manually. See the above “Protection against missing fields” for a cleaner approach.

Anyway, i hope this clears up things a little bit.

If you really feel that you need to use “required” you should only use it combined with ‘on’ => ‘create’, as well. In 99% of all cases you end up with never validating “edit” validations otherwise. That’t because you often save only a part of the data where you normally wouldn’t provide those required fields. So as a guideline: - Either don’t use required attribute at all and use my whitelisting approach from above - Use required with “on create” IF (!) the field always has to be present for all create actions. For other actions use whitelisting and guaranteeFields

A few words to validation rules in general

You can add rules for “imaginary fields”, as well. The fields don’t have to exist in the database model. This way you can easily use inputs which are validated separately and combined later on (eg: pwd and pwd_repeat resulting in the final sha1-hashed “password”). Always use “last”=>true for your rules. Unfortunately, the default value is still false here. “true” makes sure that after the first rule already returned false, the following ones are not checked as well (overhead and totally useless). Also sort your rule array properly. The rules are checked from top to bottom, so it would make sense to put all complicated stuff at the bottom and first check on simple “notEmpty” or “email” rules. The most likely ones to fail at the beginning. Why should you check “isUnique” (database query!) before “notEmpty”? Why should you check an email address exists in the database if it is invalid in the first place?

Last words

Why didn’t I use Security Component for some of the above tasks? Well, it was a bitch to use in the first months. All the time some kind of black holes (white screen of death) without any indication what went wrong. Login/Logout, some other forms as well. With the first ajax forms it got even more messy and i deactivated it at last. Works quite well without it, if you protect the forms manually. With lots of ajax stuff going on you have to do that yourself, anyway… And against certain attacks the Security Component won’t help anyway (PrimaryKey injection in edit forms etc). So at some point you will have to deal with it yourself. You have to decide to what point. But better sooner than later. If you adjust your bake templates before you start a new project it will save you lots and lots of time while having secure forms and procedures.

 
9 Comments

Posted in CakePHP

 

Necessary Core Hacks – Cake1.3

12 Sep

As of now there are some litte adjustments that are either necessary or at least very useful to apply

I keep this list here up to date and will remove or add hacks if something changes (usually the cake team is quite fast in applying appropriate patches).

For me, the very first and most important one for cake1.3 was this:

Disabling the old $helper syntax

Otherwise you gain nothing from the new one ($this->Helper), because the old ones are refererred to them overriding any local variable and totally messing everything up

Example: $this->Vote->check() $vote->check() both work on 1.3 (downwards comp.) BUT: If you try to use $vote in any other way (locale variable), it will screw up $this->Vote too (which is our helper). Lets say we have a controller and model called “votes” and “Vote:

// retrieve all model records
foreach ($votes as $vote) {
    // BAM - big problem! helper $vote AND helper $this->Vote are not usable anymore, neither is $vote as variable
}

The solution: getting rid of the old one altogether

Thats how it’s done – put this into your configs:

$config['Core'] = array(
'disableOldHelperSyntax' => 1
);
And change line 710 of /cake/libs/view/view.php (in the cake core!) from
${$name} =& $helper;
to
if (!Configure::read('Core.disableOldHelperSyntax')) {
    ${$name} =& $helper;
}
Any project that does have the above 1 in the “disableOldHelperSyntax” setting will not use the old syntax anymore. The core itself is not modified by it!

// inside a view
foreach ($votes as $vote) {
    echo $this->Vote->calc($vote); // everything is fine now
}

Some could argue: “just use variables that will never be used as helper”. But thats crap – maybe some day there is such a helper and your code gets screwed up. So until this backwards comp. is removed this is my answer for that problem. You are now free to use any local variable you want (well, almost anyway). Ticket: Link

Proper logging feature for debug = 0

Although this is probably the most useful debugging feature ever it didnt quite make it into the core. We all know that even for debug = 0 there are now log files of errors/warnings etc. Thats already very helpful if there are any errors occuring in live mode. But usually it just tells you THAT something went wrong, not WHERE. This little patch logs the stracktrace of this error to /app/tmp/logs/trace – very adjustable per configs settings

The new “write” function in /cake/libs/log/file_log.php looks like this:

/**
* Implements writing to log files.
*
* @param string $type The type of log you are making.
* @param string $message The message you want to log.
* @return boolean success of write.
*/
function write($type, $message) {
    $debugTypes = array('notice', 'info', 'debug');
 
    if ($type == 'error' || $type == 'warning') {
        $filename = $this->_path  . 'error.log';
    } elseif (in_array($type, $debugTypes)) {
        $filename = $this->_path . 'debug.log';
    } else {
        $filename = $this->_path . $type . '.log';
    }
    $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n";
    $log = new File($filename, true);
 
    /** CORE-MOD for ability to trace logged events - 2010-07-16 ms **/
    $debug = (int)Configure::read('debug');
    if (($debug == 0 && (Configure::read('System.trace') === true || Configure::read('System.trace.prod')) || $debug > 0 && (Configure::read('System.trace') === true || Configure::read('System.trace.dev'))) && ($stackTracing = Configure::read('System.stackTracing'))) {
        $continueTracing = true;
        $path = $this->_path . 'traces' . DS;
        if (!file_exists($path)) {
            if (!mkdir($path, 0777)) {
                // abort; raise notice?
                $continueTracing = false;
            }
        }
        if ($stackTracing !== true) { // log all events otherwise
            $stackTracing = (array)$stackTracing;
            if (!in_array($type, $stackTracing)) {
                // event to log not in list; abort
                $continueTracing = false;
            }
        }
        if ($continueTracing) {
            $trace = new File($path . $type . '_' . time() . '.log', true);
 
        if (!class_exists('CustomDebugger')) {
                require (LIBS . 'log' . DS . 'custom_debugger.php');
            }
            $CustomDebugger = new CustomDebugger();
 
            $traceOutput = '';
            $traceOutput = $CustomDebugger->trace();
            $trace->append("Message: '" . $message . "'\n" .
                "Referer: '". env('HTTP_REFERER'). "'\n" .
                "Location: '". env('REDIRECT_URL'). "'\n" .
                "Router::url(): '". Router::url(array(), true). "'\n" .
                "UID: '". (defined('UID')?UID:'---') ."'\n" . // I even log the user who caused this
                $traceOutput . "\n\n"
            , true);
            $trace->close();
        }
    }
    /** CORE-MOD end **/
 
    if ($log->writable()) {
        return $log->append($output);
    }
}
I did put the cake debugger into the same folder – slightly modified (using “txt” output). You could do the same.

Anyway, the configs could look like this:

$config['System'] = array(
    'stackTracing' => array('debug', 'warning', 'error', 'notice', 'info', '301', 'cacheprob'),
    'trace' => array('dev'=>true, 'prod'=>true),
);
It does only trace the above error types. and you can chose between development and productive environment. Still place for improvment of course, but I already discovered hundreds of small bugs that I would have never found without it. I would have known that those errors happenened, but actually finding them is usually impossible if the error message is something like “Notice (8): unserialize() [function.unserialize]: Error at offset 11676 of 11722 bytes in [H:...\cake\libs\cache\file.php, line 176]“. It is that simple: The core files trigger the error but there is no way to tell which app file actually is responsable if no trace is logged.

Ticket-status on this matter: cakephp.lighthouseapp.com/projects/42648/tickets/267

Changing default value of Configure::read() to NULL

There is no way to get all the set configure values. So this little tweak makes it possible. It doesnt make sense to use “debug” as default, anyway.

In /cake/libs/configure.php at line 154:

function read($var = null) { // changed default to NULL
    $_this =& Configure::getInstance();
 
    # fix for retrieving all configure vars (debugging purposes etc) - 2010-09-12 ms
 if ($var === null) {
        return (array)$_this;
    }
    # end fix
 ...
Without passing any parameter it will now return all set config arrays.

Misc

Some pending tickets or things i considered changing (but didn’t do it yet) are located here.

In the model.php, I don’t like “last”=>false as default value (should be true – logically):

$default = array(
    'allowEmpty' => null,
    'required' => null,
    'rule' => 'blank',
    'last' => false,
    'on' => null
);
Ticket: cakephp.lighthouseapp.com/projects/42648-cakephp/tickets/924

Form validation “required” in form helper is not exactly that what you would expect – or want. Details coming

 
2 Comments

Posted in CakePHP

 

Useful hidden functions

28 Aug

There are some handy cake functions you might not have used or even heard of, yet.

Debugging

Most beginners use print_r() to debug variables. Without <pre> tags its really hard to read, though. Use pr($var) or debug($var) to debug this variable and output its content. very useful inside functions, or in the view (if you don’t know what your sql result array contains, for instance). If you are looking for a more powerful pr() function in order to display not only the content but also what type it is (int, bool, string, array, object or simply NULL), check out my returns() function in the bootstrap goodies section.

Models

save, saveField, saveAll(), deleteAll(), … return usually true/false or the record itself. Sometimes it would be nice to know how many records have been deleted/modified.

$this->Model->getAffectedRows(); // works for INSERT, UPDATE, REPLACE or DELETE query
This will do the trick. The integer result can then be used in the flash message.

By the way: There is also ->getNumRows(); for SELECT or SHOW queries.

Logging

If you want to log certain events, you can easily use the build in functions:

CakeLog::write('geocode', 'Address \''.$address.'\' has been geocoded');
In this example every time the geocode webservice geocodes an address string, it will be logged in /tmp/logs/geocode.log. Quite handy in some cases – especially for debugging purposes.

Logging Fatal Errors as well By default CakePHP cannot log fatal errors. They are especially helpful for finding more serious coding bugs, though. You can easily override the shutdownFunction in your bootstrap.php

register_shutdown_function('shutdownFunction');
 
/**
 * custom shutdown function
 */
function shutDownFunction() { 
    $error = error_get_last(); 
    if ($error['type'] == 1 && class_exists('CakeLog')) { 
        CakeLog::write('error', 'Fatal Error in '.$error['file']. ' (line '.$error['line'].'):' . $error['message']);
    } 
}

Retrieving the CakePHP version

Configure::version();

Basic Functions

What if you need to the class name and you are not sure if the plugin name is passed as well, like “Plugin.User”? I always did this myself, until i stumbled upon the cake function for it: pluginSplit()

list($plugin, $class) = pluginSplit($name);

Other

It helps to open the core files and have a look inside. You might discover some functions you might have written yourself although it is available in the core. Examples are string.php with tokenize() and insert() or set.php with filter(), flatten() or countDim().

 
No Comments

Posted in CakePHP

 

User Add Console Script

04 Jul

If you have a fresh setup of your app and no users in the database, its not easy to “register” a new one (with the admin role and everything). The other case would be if you changed the security salt. Now you need new passwords, as well.

The second part is not yet covered by my script – the first one is, though. The idea is to insert an admin user in order to login for the first time. All you need is a name, an email and a password.

<?php
 
class UserShell extends Shell {
    var $tasks = array();
    var $uses = array('User');
 
    //TODO: refactor (smaller sub-parts)
    function main() {
        if (App::import('Component','AuthExt')) {
            $this->Auth = new AuthExtComponent();
        } else {
            App::import('Component','Auth');
            $this->Auth = new AuthComponent();
        }
 
        while (empty($username)) {
            $continue = $this->in(__('Set \'username\'?', true),array('y', 'n', 'q'), 'y');
            if ($continue == 'q') { die('Abort'); } elseif ($continue == 'n') { break; }
            $username = $this->in(__('Username (2 characters at least)', true));
        }
        while (empty($password)) {
            $password = $this->in(__('Password (2 characters at least)', true));
        }
 
 
 
        if (isset($this->User->Role) && is_object($this->User->Role)) {
            $roles = $this->User->Role->find('list');
 
            if (!empty($roles)) {
                $this->out('');
                pr ($roles);
            }
 
            $roleIds = array_keys($roles);
            while (!empty($roles) && empty($role)) {
                $role = $this->in(__('Role', true), $roleIds);
            }
        } elseif (method_exists($this->User, 'roles')) {
            $roles = User::roles();
 
            if (!empty($roles)) {
                $this->out('');
                pr ($roles);
            }
 
            $roleIds = array_keys($roles);
            while (!empty($roles) && empty($role)) {
                $role = $this->in(__('Role', true), $roleIds);
            }
        }
        if (empty($roles)) {
            $this->out('No Role found (either no table, or no data)');
            $role = $this->in(__('Please insert a role manually', true));
        }
 
        $this->out('');
        $pwd = $this->Auth->password($password);
 
        $data = array('User'=>array(
            'password' => $pwd,
            'active' => 1
        ));
        if (!empty($username)) {
            $data['User']['username'] = $username;
        }
        if (!empty($email)) {
            $data['User']['email'] = $email;
        }
        if (!empty($role)) {
            $data['User']['role_id'] = $role;
        }
        $this->out('');
        pr ($data);
        $this->out('');
        $this->out('');
        $continue = $this->in(__('Continue? ', true),array('y', 'n'), 'n');
        if ($continue != 'y') {
            die('Not Executed!');
        }
 
        $this->out('');
        $this->hr();
        if ($this->User->save($data)) {
            $this->out('User inserted! ID: '.$this->User->id);
        } else {
            $this->out('User could not be inserted (email, nick duplicate!!!)');
        }
    }
}
?>

Notes

The code obviously needs some refactoring. And its written for my applications – it might not work out of the box with other cake apps. Maybe we could make it more generic, as well.

It assumes that you have either AuthExt or Auth running for Authentication, that you have a User and a Role model and that the fields are “username”, “email”, “password” and “role_id”. Additionally it sets “active” to 1 – this field is checked in my login procedure. But this script should be fairly easily adjustable for your needs.

Ideas for further improvement are: - user add (for adding) - user edit (for editing specific user/password) - user reset (to reset all passwords to 123 or whatever)

 
No Comments

Posted in CakePHP

 

Code-Completion Console Script

28 Jun

Many good IDEs for Webdevelopment cannot automatically understand the structure of a framework and its classes/objects. They need a small file initializing the objects so that they know in which context they appear.

E.g.: $this->Session (Session Helper) its impossible for the IDE to know that the the helpers are inside the View class. Same goes for controller components and model behaviors

I wrote a little script which produces mockup code. My IDE “PHPDesigner” works very well with it.

Usage

Drop this script into the /shells folder (either vendors or app). Now just type cake cc inside the cake shell.

Code

<?php
 
App::import('Core', 'Folder');
App::import('Core', 'File');
 
/**
 * Code Completion
 * 2009-12-26 ms
 */
class CcShell extends Shell {
    var $uses = array();
 
    private $content = '';
 
    function main() {
        $this->out('AutoComplete Dump');
 
        //TODO: ask for version (1.2 etc - defaults to 1.3!)
 
        $this->filename = APP.'code_completion__.php';
 
        # get classes
     $this->models();
        $this->components();
        $this->helpers();
        //TODO: behaviors
 
        # write to file
     $this->_dump();
 
        $this->out('...done');
    }
 
    /**
     * @deprecated
     * now use: Configure::listObjects()
     */
    function __getFiles($folder) {
        $handle = new Folder($folder);
        $handleFiles = $handle->read(true, true);
        $files = $handleFiles[1];
        foreach ($files as $key => $file) {
            $file = extractPathInfo('file', $file);
 
            if (mb_strrpos($file, '_') === mb_strlen($file) - 1) { # ending with _ like test_.php
             unset($files[$key]);
            } else {
                $files[$key] = Inflector::camelize($file);
            }
        }
        return $files;
    }
 
 
    public function _getFiles($type) {
    $files = App::objects($type);
    # lib
    $paths = (array)App::path($type.'s');
    $libFiles = App::objects($type, $paths[0] . 'lib' . DS, false);
 
    $plugins = App::objects('plugin');
    if (!empty($plugins)) {
      foreach ($plugins as $plugin) {
         $pluginFiles = App::objects($type, App::pluginPath($plugin) . $type.'s' . DS, false);
          if (!empty($pluginFiles)) {
              foreach ($pluginFiles as $t) {
                  $files[] = $t; //"$plugin.$type";
              }
          }
      }
    }
    $files = array_merge($files, $libFiles);
    $files = array_unique($files);
 
        $appIndex = array_search('App', $files);
        if ($appIndex !== false) {
            unset($files[$appIndex]);
        }
 
        # no test/tmp files etc (helper.test.php or helper.OLD.php)
    foreach ($files as $key => $file) {
            if (strpos($file, '.') !== false || !preg_match('/^[\da-zA-Z_]+$/', $file)) {
                unset($files[$key]);
            }
        }
    return $files;
    }
 
 
    function models() {
        //$files = App::objects('component', null, false);
        $files = $this->_getFiles('model');
        //$files = $this->_getFiles(COMPONENTS);
 
        $content = LF.'<?php'.LF;
        $content .= '/*** model start ***/'.LF;
        $content .= 'class AppModel extends Model {'.LF;
        if (!empty($files)) {
            $content .= $this->_prepModels($files);
        }
        $content .= '}'.LF;
        $content .= '/*** model end ***/'.LF;
        $content .= '?>';
 
        $this->content .= $content;
    }
 
    function components() {
        $files = $this->_getFiles('component');
 
        $content = LF.'<?php'.LF;
        $content .= '/*** component start ***/'.LF;
        $content .= 'class AppController extends Controller {'.LF;
        if (!empty($files)) {
            $content .= $this->_prepComponents($files);
        }
        $content .= '}'.LF;
        $content .= '/*** component end ***/'.LF;
        $content .= '?>';
 
        $this->content .= $content;
    }
 
    function helpers() {
        $files = $this->_getFiles('helper');
 
        $content = LF.'<?php'.LF;
        $content .= '/*** helper start ***/'.LF;
        $content .= 'class AppHelper extends Helper {'.LF;
        if (!empty($files)) {
            $content .= $this->_prepHelpers($files);
        }
        $content .= '}'.LF;
        $content .= '/*** helper end ***/'.LF;
        $content .= '?>';
 
        $this->content .= $content;
    }
 
    function _prepModels($files) {
        $res = '';
        foreach ($files as $name) {
            $res .= '
    /**
    * '.$name.'
    */
    public $'.$name.';
'.LF;
        }
 
        $res .= '   function __construct() {';
 
        foreach ($files as $name) {
            $res .= '
        $this->'.$name.' = new '.$name.'();';
        }
 
        $res .= '}'.LF;
        return $res;
    }
 
    function _prepComponents($files) {
        $res = '';
        foreach ($files as $name) {
            $res .= '
    /**
    * '.$name.'Component
    */
    public $'.$name.';
'.LF;
        }
 
        $res .= '   function __construct() {';
 
        foreach ($files as $name) {
            $res .= '
        $this->'.$name.' = new '.$name.'Component();';
        }
 
        $res .= '}'.LF;
        return $res;
    }
 
    function _prepHelpers($files) {
        # new ones
     $res = '';
 
        foreach ($files as $name) {
            $res .= '
    /**
    * '.$name.'Helper
    */
    public $'.$name.';
'.LF;
        }
 
        $res .= '   function __construct() {';
 
        foreach ($files as $name) {
            $res .= '
        $this->'.$name.' = new '.$name.'Helper();';
        }
 
        # old ones
     $res .= ''.LF;
        /*
        foreach ($files as $name) {
        $res .= '
        $'.lcfirst($name).' = new '.$name.'Helper();
        ';
        }
        $res .= LF;
        */
 
        $res .= '   }'.LF;
 
        return $res;
    }
 
 
    function _dump() {
        $file = new File($this->filename, true);
 
        $content = '<?php exit();'.LF;
        $content .= '//Add in some helpers so the code assist works much better'.LF;
        $content .= '//Printed: '.date('d.m.Y, H:i:s').LF;
        $content .= '?>'.LF;
        $content .= $this->content;
        return $file->write($content);
    }
}
 
?>

Result

The file will look like:

class AppModel extends Model {
    /**
    * Address
    */
    public $Address;
    ...
 
    function __construct() {
        $this->Address = new Address();
        ...
    }
}
class AppController extends Controller {
    /**
    * AclComponent
    */
    public $Acl;
    ...
    function __construct() {
        $this->Acl = new AclComponent();
        ...
    }
}
class AppHelper extends Helper {
    /**
    * AjaxHelper
    */
    public $Ajax;
    ...
    function __construct() {
        $this->Ajax = new AjaxHelper();
        ...
    }
}

Final notes

Feel free to update the TODOs and send me the improved file. A test file would be awesome, too, i guess :)

For LF constants see my article about “bootstrap goodies”.

 
3 Comments

Posted in CakePHP

 

Helper? Component? Lib?

26 Jun

Some ideas what to use if you want to add some additional feature. Feel free to comment on this below.

Level of Independence

We need to ask ourselves if this feature needs to interact with other cake elements, like controller, some components, models, …

If it needs to save to the session, or if it needs some controller functionality, it will have to be a component. With initialize(&$controllerReference) and startup(&$controllerReference) this is very easy to accomplish.

But with Cake13 libs have been introduced. Not every piece of “controller” code necessarily needs to be component anymore. So if you retrieve an RSS feed or get the weather from a weather channel web service you could just make a clean and independent lib class. No need to extend the cake object or even pass the controller reference. Less memory and dependency is a good thing. And its easier to test, anyway.

Helpers are used if the result is in relation to the view – in other words if it gets printed/echoed right away. If you want to retrieve some web service information and save it to the database use a component instead.

Database related?

Often times we need to adjust some model data, we either use a component first and then pass it to the model or we use beforeValidate() and beforeSave() in the model. Same goes for the other direction (from model to controller): afterFind() or a component call afterwards. This is fine for custom changes. As soon as it could be something useful for several models, it might make sense to build a behavior. The code gets cleaner and your models more powerful.

Examples would be: “Last Editor/Last Change”, “Geocoding”, “Auto-Capitalize first letter of name/title”, “Format/Localize Date/Time”, …

Reducing code redundancy

Now that we have a vague understanding where to use what type of tool, we should think about cutting down the redundancy. Lets say we use the vendor class “phpThump”. We would have to write two wrappers. one for the helper (display images in the view) and one for the component (uploading images and resizing), maybe even for some behavior (validating + uploading + resizing). This wrapper handles default values from Configure::read() and other cake related settings. In this scenario we should build one single library in /libs, maybe called “phpthumb_lib.php”. Here we put our wrapper with our custom functions. Then we build a helper (view side) as well as a component or a behavior (controller side). They will import and use the library file. This is a cleaner approach because changes in the library class will be available in all files it is used in. Bonus: The main library file is easier to test. And therefore testing the other classes afterwards is easier, too.

Generally speaking, all web services should be some kind of library file (some would make a data source out of it). It doesn’t matter then if we use it in components or helpers, because it will fit either way. A helper in a controller, though, is not really a nice thing.

Thats – by the way – something i don’t like about the core helpers. They have functionality which is often useful in the controller (replacing a timestamp with localized date in session flash messages). So there should be a library called “Time” or whatever which is then extended or used in the helper. But, if needed, it can be used in the controller, as well. Same goes for “Text” and “Number”.

Plugin or not?

If your feature is not site-specific but very generic it probably makes sense to build a plugin. This way all other apps can easily use the same plugin. Additionally, test cases, assets etc are all combined in one folder – clean and extendable.

Examples: A bookmark helper usually can be used in several apps, whereas a custom helper with two functions for this one app will not be very useful anywhere else.

Usage of those resources

For components, add it to the controller in order to use it in the corresponding actions:

var $components = array('MyC'); # file is in /app/controllers/components named my_c.php
And in one of the controller’s actions:
$this->MyC->foo();

For helpers, add it to the controller in order to use it in the corresponding views:

var $helpers = array('MyH'); # file is in /app/views/helpers named my_h.php
And in one of the controller’s views:
$this->MyH->foo();

Libs can be used everywhere – include them at runtime:

App::import('Lib', 'MyL'); # file is in /app/libs named my_l.php
$myL = new MyL();
$myL->foo();
Possible in controllers, components, behaviors, view, helpers, elements and everything else.

Behaviors and other elements are used similar to the above.

For Plugins simply add the plugin name: “Text” becomes “Plugin.Text” etc

Hacks for special use cases

Sometimes we need to break MVC in order to avoid redundance (and stay DRY). A typical szenario is when we need a core helper in the controller (e.g. TextHelper). We need to manually include it then at runtime:

App::import('Helper', 'Text');
$text = new TextHelper();
$myText = $text->truncate($myText);
I want to emphasize that this should only be done if not possible any other way. You would also have to manually start and attach all helpers which are used inside the helper. Pretty annoying :)

I proposed a while ago that most of the core helpers should actually extend or at least use libs which contain the relevant functionality. This way we can use the libs in the controller and we can use their methods in the view via helper wrappers. But as of right now this is not yet planned for Cake2.0 or higher.

Update 2012-04-03

For Cake2.0 and above please see the updated article on this topic!

 
No Comments

Posted in CakePHP

 

Static Enums or “Semihardcoded Attributes”

24 Jun

There are many cases where an additional model + table + relation would be total overhead. Like those little “status”, “level”, “type”, “color”, “category” attributes. Often those attributes are implemented as “enums” in sql – but cake doesn’t support them natively.

If there are only a few values to choose from and if they don’t change very often, you might want to consider this approach. I use it a hundred times. Its very efficient and easily expandable.

Why not using hardcoded integer values like 0, 1, 2 etc? The answer is simple: It is usually really bad style to do so. What if you want to update your code a few weeks later – you probably don’t even know anymore what 1 or 2 stand for. Constants like TYPE_ACTIVE TYPE_PENDING, though, are way more descriptive.

Let’s get started

a) Add tinyint(2) unsigned field called for example “status” (singular) “Tinyint(2 / 3) unsigned” covers 0…127 / 0…255 – which should always be enough for enums. if you need more, you SHOULD make an extra relation as real table. Do not use tinyint(1) as cake interprets this as a toggle field, which we don’t want!

b) Put this in your app_model.php:

/**
 * static enums
 * @access static
 */
public static function enum($value, $options, $default = '') {
    if ($value !== null) {
        if (array_key_exists($value, $options)) {
            return $options[$value];
        }
        return $default;
    }
    return $options;
}

c) Put something like this in any model where you want to use enums:

/*
 * static enum: Model::function()
 * @access static
 */
 public static function statuses($value = null) {
    $options = array(
        self::STATUS_NEW => __('statusNew',true),
        self::STATUS_UNREAD => __('statusUnread',true),
        self::STATUS_READ => __('statusRead',true),
        self::STATUS_ANSWERED => __('statusAnswered',true),
        self::STATUS_DELETED => __('statusDeleted',true),
    );
    return parent::enum($value, $options);
}
 
const STATUS_NEW = 0; # causes sound, then marks itself as "unread"
const STATUS_UNREAD = 1;
const STATUS_READ = 2;
const STATUS_ANSWERED = 4;
const STATUS_DELETED = 5;
// add more - order them as you like

d) Use them in your controller logic, model functions, view forms, …:

//view form
...
echo $this->Form->input('status', array('options'=>Notification::statuses()));
//controller action
...
if ($this->data['Notification']['status'] == Notification::STATUS_READ)) {...}
//controller logic on find
...
$options = array('conditions'=>array('Notification.user_id'=>$uid, 'Notification.status <='=>Notification::STATUS_UNREAD));
$notifications = $this->Notification->find('all', $options);
//view index/view
...
<?php echo h($notification['Notification']['title']);?>
<?php echo Notification::statuses($notification['Notification']['status']); // returns translated text ?>
=> NOTE: example with “statuses”, could also be priorities, gender, types, categories, … etc anything that is not often changed or extended

[b]Thats it![/b]

Conclusion: fast and easy to extend in the future if neccessary it also saves a lot of overhead by using (tiny)ints instead of strings AND it does not need any additional table joins! which makes it even more performant

further advantages - can be used from anywhere (model, controller, view, behavior, component, …) - reorder them by just changing the order of the array values in the model - auto-translated right away (i18n without any translation tables – very fast) - create multiple static functions for different views (“optionsForAdmins”, “optionsForUsers” etc). the overhead is minimal.

what you cant do: - sort by the value of the keys (only by keys): small, medium, low => no sorting by their name ASC/DESC, only by key ASC/DESC

Final tips: if you use them in an index view many times repeatedly, if would make sense to write them into an array before using them. otherwise the translation will be transformed every time. caching would also work!

validation/emptyFields: use ‘empty’ attribute in form helper options array for default “blank” with either ‘empty’=>array(0=>’xyz’) to allow 0 values or ‘empty’=>’xyz’ to require one value (combined with validation rule “numeric”)

Combination with LazyLoading

This approach gets more complicated in combination with “Lazy Loading” of Models (if even implemented). In this case the related models are not imported until actually needed. So if you use constants from your model before this happens, you get a fatal error! You would need to App::import() all models which you need for static enum access. This can be done in the controller actions and adds not more than 1-2 lines of code.

I am working on a PHP5.3 ONLY solution right now which uses the brand new __callStatic() method and works well with LazyLoading. Stay tuned…

For Cake2.0, all you have to do is make sure that the classes are defined before you access their constants:

App::uses('MyModel', 'PluginName.Model');
You can do that right before you use it or define it globally in your Controller class for example.

UPDATE August 2011 – Enums in baking templates

Include your enums in your templates for “cake bake” in order to have them in your views out of the box. All you need to do: a) Add the pluralized static method of the enum field to the model (e.g. status => statuses):

/**
     * @static
     */
    public static function statuses($value = null) {
        $array = array(
            self::STATUS_INACTIVE => __('Inactive', true),
            self::STATUS_ACTIVE => __('Active', true),
        );
        return parent::enum($value, $array);
    }
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
b) Adjust your bake templates
foreach ($fields as $field) {
    if (strpos($action, 'add') !== false && $field == $primaryKey) {
        ...
    } elseif ($schema[$field]['type'] == 'integer' && method_exists($modelClass, $enumMethod = lcfirst(Inflector::camelize(Inflector::pluralize($field))))) {
        echo "\t\techo \$this->Form->input('{$field}', array('options'=>".Inflector::camelize($modelClass)."::".$enumMethod."()));\n";
    } ...
this is the code for the form.ctp template

... 
} elseif ($schema[$field]['type'] == 'integer' && method_exists($modelClass, $enumMethod = lcfirst(Inflector::camelize(Inflector::pluralize($field))))) {
    echo "\t\t<td>\n\t\t\t<?php echo ".$modelClass."::".$enumMethod."(\${$singularVar}['{$modelClass}']['{$field}']); ?>\n\t\t</td>\n";
} 
...
and this for index.ctp (view.ctp works similar)

So if the bake script finds the static method “statuses” in the model it will then present the dropdown in the forms as well as the translated (+i18n) value in the views (index/view).

UPDATE Oktober 2011 – Regroup and Reorder

With a little trick it is now possible to return only a small subset (and in different order) for specific form elements. This is the improved enum method for your AppModel:

/**
 * @param string $value or array $keys or NULL for complete array result
 * @param array $options (actual data)
 * @return mixed string/array
 */
public static function enum($value, $options, $default = '') {
    if ($value !== null && !is_array($value)) {
        if (array_key_exists($value, $options)) {
            return $options[$value];
        }
        return $default;
    } elseif ($value !== null) {
        $newOptions = array();
        foreach ($value as $v) {
            $newOptions[$v] = $options[$v];
        }
        return $newOptions;
    }
    return $options;
}
The current version can now also be found in my Tools plugin.

Lets say, we have our status array from above. But for users we dont want them to be able to set the record to “deleted”. And lets say we are in the Message Model:

'options'=>Message::statuses(array(Message::STATUS_NEW, Message::STATUS_UNREAD, Message::STATUS_READ, Message::STATUS_ANSWERED))
We pass this on as options for the FormHelper and the deleted status is not available. No extra methods or configuration required. The order in which the keys are passed will decide the order of the translated enum values.

Update 2012-02-26 – Cake2 and Bitmaps

If you are looking for combining several booleans into a single database field check out my Bitmapped Behavior.

 
3 Comments

Posted in CakePHP

 

Working with forms

23 Jun

The Cookbook only covers the real basics. I want to go over some common issues in a more detailed way.

Dummy Values

Dummy values for select fields are usually added in the view:

/* (1) */
echo $this->Form->input('select_field', array('empty'=>'please select'));
/* (2) */
echo $this->Form->input('select_field', array('empty'=>array(0=>'please select')));
(1) is used if validation is “notEmpty” for this field (key = empty string) or if it is “numeric” and the user has to chose a value other than the dummy one. (2) is used if validation is “numeric” and you want to allow the dummy value as default value.

Of course, there are other cases, too. Note: We assume that the controller passes a variable named “selectField” with the options array to the view. You could also manually add options with the ‘options=>array(…)’ param.

Default Values

Many beginners make the mistake to use the options params “value”, “selected” or “checked” for the default value. But this field will then be populated with the same default value every time after posting the form. This breaks the logic of most forms. The validation errors are supposed to show what is wrong and the posted content should remain in the form fields. So how is it done right?

Use the controller for populating the form with default values

/* inside add/edit actions */
if (!empty($this->data)) {
    $this->Country->create();
    if ($this->Country->save($this->data)) {
        ...
    } else {
        ...
    }
} else {
    /* Now here you can put your default values */
    $this->data['Country']['active'] = 1;
    $this->data['Country']['lat'] = 0.0;
    ...
}
It doesn’t matter what type they are (checkbox, select, radio, textarea, text). For checkboxes and radio buttons it should be either 0 (not checked – by default) or 1 (checked).

Default Values – hidden!

Many make the mistake to use hidden fields for some values that are not supposed to be edited by users. But they still can be modified using Firefug etc. So what is the more correct approach here?

if (!empty($this->data)) {
    $this->Post->create();
    # add the content before passing it on to the model
 $this->data['Post']['status'] = '2';
    if ($this->Post->save($this->data)) {
        ... 
    }
}
We don’t even create hidden inputs in the form but add the hidden values in the controller “on demand”. Right before the model gets them and processes them. No tempering possible then. No overhead in the forms.

2.0 Note: Cake2 Security Component does now hash the hidden input content (!), as well. But if you don’t want or can’t use this component the above still applies.

Disallowing some fields to be edited

Most beginners would make the (huge) mistake to simply mark the input fields as read-only or hidden field. Well, again, Firebug can easily modify those fields. If you really want to use this strategy, make sure (!) that they are not passed on to the model. This can be accomplished with the third parameter in the save() method of the model:

# we dont want the "read-only" fields to be edited (like e.g. 'approved')
if ($this->Post->save($this->data, true, array('name', 'title', 'content'))) {
Now only those 3 fields get modified, no matter what. See my security post for details how to secure forms.

A cleaner approach usually is to simply echo the content (without any inputs). For this to work it is important NOT to just pass it along $this->data. After posting the form those fields are not in the data array anymore and will cause errors. The correct approach (I even baked my templates this way) would be to pass the record itself to the view as well:

function edit($id = null) {
    if (empty($id) || !($post = $this->Post->find('first', array('conditions'=>array('Post.id'=>$id))))) {
        //ERROR and redirect
    }
    if (!empty($this->data)) {
        $this->data['Post']['id'] = $post['Post']['id'];
        if ($this->Post->save($this->data, true, array('id', 'title', 'content', ...))) {
            //OK and redirect
        } else {
            //ERROR + validation errors without redirect
        }
    }
    if (empty($this->data)) {
        $this->data = $post;
    }
 
    $this->set(compact('post'));
}
Now we have full access to the record even after posting the form: echo $post['Post']['approved']; etc.

Validating first (manually)

This comes in handy, if you need to do something prior to saving the posted data:

# very important if you use validates()
$this->Post->set($this->data);
 
if ($this->Post->validates()) { 
    // do something here
    # false = no need to validate again (already done in validates()!)
 $this->Post->User->save(null, false);
    ...
} else { 
    // ERROR 
}
This is also very useful if you don’t want to save the record. Maybe you want to write an email or something.

Note: In save() we do not pass $this->data again because callback functions inside the model could already have altered the passed data. We would override this. So we pass “null” instead. If you actually need to alter $this->data here, be sure all your important modifications happen inside beforeSave() – as beforeValidate() is now not called anymore with “false” as second param.

Custom controller validations

This is useful if the result of a component matters for the validation process and you want to manually invalidate a field:

$this->Post->set($this->data);
 
if (!$this->MyComponent->foo()) {
    $this->Post->invalidate('field', 'error message');
}
 
# if foo() returns false, validates() will never return true
if ($this->Post->validates()) { 
    $this->Post->User->save(null, false);
    ...
} else { 
    // ERROR 
}

Modifications prior to validation/saving

Sometimes you want to make sure, that your display field (e.g. “name”/”title”) is uppercase for the first letter, and lowercase for the rest. As any other modification as well you would want to put this in the model as beforeValidate() or beforeSave(). Usually the first one is the better choice. After validation there should only be necessary changes that don’t affect the validated fields if possible. And if so, make those don’t break the validation rules. In our case we even wont to auto-prefix urls in order to properly validate them. So here we have no choice other than using beforeValidate():

function beforeValidate() {
    if (!empty($this->data[$this->alias]['name'])) {
        $this->data[$this->alias]['name'] = ucfirst(mb_strtolower($this->data[$this->alias]['name']));
    }
    /* adding http:// if necessary (if only www... was submitted) */
    if (!empty($this->data[$this->alias]['homepage'])) {
        $this->data[$this->alias]['homepage'] = CustomComponent::autoPrefixUrl($this->data[$this->alias]['homepage']);
    }
    /* very important! */
    return true;
}
If you don’t return true, the saving process will abort. This usually doesn’t make sense in beforeValidate() – and is mainly done in beforeSave().

Note: Most generic beforeSave() and beforeValidate() functionality could easily be transformed into a behavior.

 
No Comments

Posted in CakePHP