RSS
 

Archive for July, 2010

Extended core validation rules

19 Jul

I18n Translation

Some translate the rules in the view – but it usually creates redundancy. In some projects this might be intentional. I like to keep the error messages centralized, though.

For that, you can just override the core translation rule – add this to app_model.php:

/**
 * Overrides the Core invalidate function from the Model class
 * with the addition to use internationalization (I18n and L10n)
 * @param string $field Name of the table column
 * @param mixed $value The message or value which should be returned
 * @param bool $translate If translation should be done here
 */
public function invalidate($field, $value = null, $translate = true) {
    if (!is_array($this->validationErrors)) {
        $this->validationErrors = array();
    }
    if (empty($value)) {
        $value = true;
    } else {
        $value = (array )$value;
    }
 
    //TODO: make more generic?
    if (is_array($value)) {
        $value[0] = $translate?__($value[0], true) : $value[0];
 
        if (count($value) > 3) { # string %s %s string, trans1, trans2
         $value = sprintf($value[0], $value[1], $value[2], $value[3]);
        } elseif (count($value) > 2) { # string %s %s string, trans1, trans2
         $value = sprintf($value[0], $value[1], $value[2]);
        } elseif (count($value) > 1) { # string %s string, trans1
         $value = sprintf($value[0], $value[1]);
        } else {
            $value = $value[0];
        }
    }
    $this->validationErrors[$field] = $value;
}

Usage (some examples):

var $validate = array(
    'username' => array(
        'notEmpty' => array(
            'rule' => array('notEmpty'),
            'message' => 'valErrEmpty', // some short form to translate
            //normal sentences would of course work, too
        ),
    ),
    'pwd' => array(
        'between' => array(
            'rule' => array('between', 6, 30),
            'message' => array('valErrBetweenCharacters %s %s', 6, 30), // short form string with variables
            // maybe resulting in something like "Between 6 and 30 chars" as defined in locale.po
        )
    ),
    'code' => array(
        'maxLength' => array(
            'rule' => array('maxLength', 5),
            'message' => array('The code cannot be longer than %s chars', 5), // normal text with a variable
        )
    ),
    ...
);
I like the short forms like “valErrBetweenCharacters %s %s” – they don’t have a certain “grammar”, so if you want to change the translation result “Between %s and %s chars” to “Please insert between %s and %s chars” this can be done with one change in locale.po instead of changing all xxx places you used this rule.

Working with dynamic error messages

If you are in need for a way to work with dynamically concatenated message strings you need to use the __construct method:

public function __construct($id = false, $table = null, $ds = null) {
    parent::__construct($id, $table, $ds);
 
    $this->validate['code']['maxLength']['message'] = array('valErrCodeLength %s', Configure::read('Code.maxLength'));
}
This will either override the existing placeholder message or add it to the rule array if it didn’t contain a message param yet. You can also add complete rules dynamically this way:
$this->validate['code']['extraRule'] = array(...);
 
// or replace ALL rules for a field:
$this->validate['code'] = array('ruleOne'=>array(...), 'ruleTwo'=>array(...), ...);
Just make sure your rules all contain the last => true setting then.

Custom validation rules

Here is one example how to use a custom validation rule with one param (custom rules can be placed in the model for local validation or the app_model for global validation):

public $validate = array(
    'pwd_repeat' => array(
        'validateIdentical' => array(
            'rule' => array('validateIdentical', 'pwd'), // we want to compare this to "pwd"
            'message' => 'valErrPwdNotMatch',
        ),
    )
);
 
// in app_model.php:
/**
 * checks if the content of 2 fields are equal
 * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
* //TODO: make it more generic with Model.field syntax
 */
public function validateIdentical($field, $compareWith) {
    return ($this->data[$this->alias][$compareWith] === array_shift($field));
}

Multiple validation rules

The Cookbook states: “By default CakePHP tries to validate a field using all the validation rules declared for it and returns the error message for the last failing rule” I don’t really like this default behavior – what sense does it make to validate all if only the last error can be returned anyway. But as long as “last” is not true by default, we have to manually set it:

var $validate = array(
    'login' => array(
        'loginRule-1' => array(
            'rule' => 'alphaNumeric',
            'message' => '...',
            'last' => true
         ),
        'loginRule-2' => array(
            'rule' => array('minLength', 8),
            'message' => '...'
        )
    )
);
The rules are validated top down. So if the first one fails, it will now return the error right away. Usually the following validation rules rely on a positive result of the predecessor. At least, thats how you should arrange your rules.

One example: [email] (in this order – each with a specific error message) - notEmpty - email - undisposable (custom – per vendor) - notBlocked (custom – per webservice)

As you can see, it would not make sense to check all of them every time. It really slows down the validation – especially if the email address is not even valid. So we first want to check the simple stuff and then move on to the advanced (and sometimes more time consuming) rules. All rules get “last”=>true to achieve that.

Other quite handy custom translation rules

They can be put into app_model.php:

/**
 * checks a record, if it is unique - depending on other fields in this table (transfered as array)
 * example in model: 'rule' => array ('uniqueRecord',array('belongs_to_table_id','some_id','user_id')),
 * if all keys (of the array transferred) match a record, return false, otherwise true
 * @param ARRAY other fields
 * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
 */
public function validateUnique($arguments, $fields = array(), $options = null) {
    $id = (!empty($this->data[$this->alias]['id'])?$this->data[$this->alias]['id'] : 0);
 
    foreach ($arguments as $key => $value) {
        $fieldName = $key;
        $fieldValue = $value; // equals: $this->data[$this->alias][$fieldName]
    }
 
    if (empty($fieldName) || empty($fieldValue)) { // return true, if nothing is transfered (check on that first)
        return true;
    }
 
    $conditions = array($this->alias.'.'.$fieldName => $fieldValue, // Model.field => $this->data['Model']['field']
        $this->alias.'.id !=' => $id, );
 
    foreach ((array )$fields as $dependingField) {
        if (!empty($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
            $conditions[$this->alias.'.'.$dependingField] = $this->data[$this->alias][$dependingField];
 
        } elseif (!empty($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
            $conditions[$this->alias.'.'.$dependingField] = $this->data['Validation'][$dependingField];
 
        } elseif (!empty($id)) {
            # manual query! (only possible on edit)
         $res = $this->find('first', array('fields' => array($this->alias.'.'.$dependingField), 'conditions' => array($this->alias.'.id' => $this->data[$this->alias]['id'])));
            if (!empty($res)) {
                $conditions[$this->alias.'.'.$dependingField] = $res[$this->alias][$dependingField];
            }
        }
    }
 
    $this->recursive = -1;
    if (count($conditions) > 2) {
        $this->recursive = 0;
    }
    $res = $this->find('first', array('fields' => array($this->alias.'.id'), 'conditions' => $conditions));
    if (!empty($res)) {
        return false;
    }
 
    return true;
}
 
// validateUrl and validateUndisposable are to come

More

See the Cookbook for details on that matter (deprecated 1.2).

 
No Comments

Posted in CakePHP

 

Redirect Root Domain to WWW Subdomain

13 Jul

The Problem

“When you have two different addresses pointing to the same page, like www.example.com/offers.html and example.com/offers.html, many search engines (or so we are led to believe) will treat those two URLs as two separate pages. When you, as a human, see those two pages and notice they are identical, you will automatically realise (correctly) that they are actually the same page. Apparently, the search engines do not make this assumption, and will regard those as different pages with duplicate content.” (source: www.thesitewizard.com)

I personally think that apart from the SEO problem there shouldn’t be two different urls to the same content in the first place.

So what do we do?

The bad thing to do would be to disable one of the domains (root or www). We want to select one (www.example.com) as default domain and if someone just enters “example.com” he will be automatically redirected to our default domain. Again – the bad thing would be to use meta redirects. We want to use so called permanent redirects ( code 301) in order to notify search engines and browsers about the reason we want to redirect.

Using Mod Rewrite

For the cake app to be available per “www.example.com” (and a 301 redirect from mydomain.de) you just need to modify the htaccess file in the /app/webroot/ folder:

<IfModule mod_rewrite.c>
    RewriteEngine On
 
    RewriteCond %{HTTP_HOST} !^www. [NC]
    RewriteCond %{HTTP_HOST} !^localhost [NC]
    RewriteRule ^(.*)$ http://www.%{HTTP_HOST}/$1 [R=301,L]
 
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
</IfModule>
The “localhost” part is not necessary – but it prevents your local htaccess file to redirect on your development computer. That is if you have your htaccess file in svn or git and therefore both versions (local and stage) are the same.

The reason why i did not lay out the other direction (www to root) is simple: I don’t think this makes sense. Most people not so comfortable with the internet usually type the complete address anyway – they just expect a “www” in front of the domain for the main website. And with the internet getting more and more complex, some guidelines should just remain. One is that you don’t use root domains for websites. (I could go into details here – about other side effects like “technical” cookie problems occuring with root domain sites etc – but i will leave that out for now)

Using Apache Directives

This is way faster because the server doesn’t have to open the htaccess files for it. It can directly use what it has to have available anyway.

Inside /etc/apache2/sites-available/ there should be your domain file. Between <Directory> and </Directory> you can add the above lines. To actually increase the performance we now need to disable htaccess files for this domain. Add AllowOverride None directly above your new lines. This prevents the server from looking for, opening and processing htaccess files.

Note: Usually this is only available on root servers.

 
 

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

 

Subversion and multiple CakePHP apps

02 Jul

A few days ago i already wrote about how to set up a svn for cake. In this second part i will cover the aspect of managing several apps.

The Problem

We have several apps, each with its own cake and vendor folder. If you need to update from 1.3.1 to 1.3.2 you would need to do that for each of your cake cores. Woudn’t it be nice if you changed one and all others would be updated automatically?

The Solution

Lets say, our apps have the following svn rep. urls: http://123.456.789.000/app1 and http://123.456.789.000/app2

Both have the same structure: /root/trunk/app/… (/root/trunk/vendors/…) (/root/trunk/cake/…)

Both vendor and cake folder we now want to substitute with our shared folders

Sharing Cake Core and Vendors

We now put our cake core in svn under the following url: http://123.456.789.000/cake13

and vendors: http://123.456.789.000/vendors

In both apps, we need to edit the properties for the /trunk folder with the following schema: svn:externals svn_url local_folder Result: svn:externals http://123.456.789.000/cake13 cake svn:externals http://123.456.789.000/vendors vendors

If you update the cake13 or vendors svn, both apps will get the updated version. Especially if you have more than 2 apps, this will be very helpful.

Extending to other folders

/app/plugins is a very common folder where you should use svn:externals instead of just copying your plugins to all apps. One change in any plugin version will affect the other ones.

/app/libs can have subfolders, too. So /app/libs/lib/… could contain a “library of libs” for all apps. The same goes for /app/views/helpers, /app/controller/components, /app/models/behaviors/ etc

Your test cases should be in “lib” folders then, too. E.g: /app/controller/components/lib/upload.php /app/tests/cases/components/lib/upload.php Both inside those shared lib folders which are attached with svn:externals (in this case parent folder “components”).

This might make sense in some cases. Most of the time, though, it’s better to create some kind of “tools” plugin where you can combine all your “library” files (common libs, helpers, components, behaviors, …). And the test cases are easier managed, as well.

Final tips

You don’t necessarily need seperate svns for each external folder. They could all reside in one. E.g with “common” svn:

svn:externals http://123.456.789.000/common/cake13 cake svn:externals http://123.456.789.000/common/subfolder1/subfolder2/vendors vendors

Any svn subfolder is valid

NEW: Use github reps as external svns

Now you can include github reps in svn reps. Its as easy as including svn:externals https://svn.github.com/[user]/[repository] in your folder properties.

This is especially useful, if you want to include one of the many cake plugins available at github without manually including it (changes would not be adopted). Thats how i include my tools plugin in all my apps: svn:externals https://svn.github.com/dereuromark/tools tools (in my /app/plugins folder properties)

Note: Currently it does not seem to work the other way around. Although planned to be available very soon, it didn’t work for me committing to such a git-svn rep externally included. But most of the time, a simple checkout is more than enough. If you need to change the rep, you can do that with git itself and then update the svn rep (it will get the changes then anyway).

 
4 Comments

Posted in CakePHP, SVN