RSS
 

Posts Tagged ‘Model’

Working with virtual fields in Cake1.3

13 Oct

In earlier versions of CakePHP we had to use Behaviors like “MultipleDisplayFields” or whatever.
With $Model->virtualFields it is now possible to natively use find() methods combined with virtual fields.
The nice thing about it is, that the pagination can work with those fields, too. You can even sort on them in paginated views. A huge advantage over earlier Cake versions.

An example (User model):

$virtualFields = array('fullname'=>'CONCAT(User.firstname, " ", User.lastname)'); // "john doe"

It has several downsides, though.
Virtual fields do not work across models. They cannot be used in combination with “fields” in the find() options.
More on it can be found in the official cookbook.

I tried to come up with a solution that fixes some of those incapabilities.

Introducing the custom model method

$fields = am(array('username', 'email'), $this->User->virtualFields('fullname'));
$user = $this->User->find('first', array('fields'=>$fields, 'conditions'=>...));

This allows us to only fetch specific “fields” while including the virtualField “fullname”

$fields = $this->User->virtualFields(array('fullname', 'OtherUser.fullname'));
$fields = am(array('User.id', 'OtherUser.id'), $fields);
$user = $this->User->find('first', array('contain'=>array('OtherUser'), 'fields'=>$fields, ...));

Virtual fields of other models are also no problem. In this case I simply joined the model User to itself (hasOne OtherUser).
The result would be sth like:

array (
  'User' =>
  array (
    'id' => '2',
    'fullname' => 'First+Last',
  ),
  'Test' =>
  array (
    'id' => '2',
    'fullname' => 'First-Last',
  ),
);

Note the difference in the join part (+/-). It shows that the query works (OtherUser has – and User has + as joining piece).

A little bit trickier were the temporary virtual fields. Those could be passed along and would be used only for this query (TODO! right now they are added to virtualFields but not removed anymore).

$fields = $this->User->virtualFields(array('fullname', 'othername'=>'CONCAT('.$this->User->alias.'.firstname, "-", '.$this->User->alias.'.lastname)', 'OtherUser.othername'=>'CONCAT('.$this->OtherUser->alias.'.firstname, "*", '.$this->OtherUser->alias.'.lastname)'));
$fields = am(array('User.id', 'OtherUser.id'), $fields);
$user = $this->User->find('first', array('contain'=>array('Test'), 'fields'=>$fields, 'order'=>array()));

The result again – as expected:

array (
  'User' =>
  array (
    'id' => '2',
    'fullname' => 'First+Last',
    'othername' => 'First-Last',
  ),
  'OtherUser' =>
  array (
    'id' => '2',
    'othername' => 'First*Last',
  ),
)

Even plugin models should work (not tried yet) with

$this->User->virtualFields(array('Plugin.PluginModel.fullname'));

Although the “Plugin” part usually can be omitted if the relation from User to PluginModel is already established (and the User model can access the model property without initializing the foreign model).

Code

The code to be placed in your AppModel:

/**
 * combine virtual fields with fields values of find()
 * USAGE:
 * $this->Model->find('all', array('fields' => $this->Model->virtualFields('full_name')));
 * Also adds the field to the virtualFields array of the model (for correct result)
 * TODO: adding of fields only temperory!
 * @param array $virtualFields to include
 * 2011-10-13 ms
 */
public function virtualFields($fields = array()) {
	$res = array();
	foreach ((array)$fields as $field => $sql) {
		if (is_int($field)) {
			$field = $sql;
			$sql = null;
		}
		$plugin = $model = null;
		if (($pos = strrpos($field, '.')) !== false) {
			$model = substr($field, 0, $pos);
			$field = substr($field, $pos+1);
 
			if (($pos = strrpos($model, '.')) !== false) {
				list($plugin, $model) = pluginSplit($model);
			}
		}
		if (empty($model)) {
			$model = $this->alias;
			if ($sql === null) {
				$sql = $this->virtualFields[$field];
			} else {
				$this->virtualFields[$field] = $sql;
			}
		} else {
			if (!isset($this->$model)) {
				$fullModelName = ($plugin ? $plugin.'.' : '') . $model;
				$this->$model = ClassRegistry::init($fullModelName);
			}
			if ($sql === null) {
				$sql = $this->$model->virtualFields[$field];
			} else {
				$this->$model->virtualFields[$field] = $sql;
			}
		}
		$res[] = $sql.' AS '.$model.'__'.$field;
	}
	return $res;
}

If anyone sees a cleaner approach etc, let me know :)

 
1 Comment

Posted in CakePHP

 

Working with models

08 Mar

Since there is so much to talk about it here, I will cut right to the chase.

We all know about “Fat Models, Slim Controllers”. So basically, as much as possible of the code should be in the model. For a good reason: If you need that code in another controller, you can now easily access it. Having it inside a controller does not give you this option. Thats another principle: “Don’t repeat yourself”.

What if I use the same model in different contexts?

We sometimes need the “User” (class name!) Model to be more than just that. Imagine a forum with posts. We could have “User”, “LastUser”, “FirstUser” etc. For all those we have custom belongsTo relations in our models.

If we had something like this in our code, we could get in trouble:

function beforeValidate() {
	if (isset($this->data['User']['title'])) {
		$this->data['User']['title'] = ucfirst($this->data['User']['title']);
	}
	return true;
}

So always use $this->alias:

function beforeValidate() {
	parent::beforeValidate();
	if (isset($this->data[$this->alias]['title'])) {
		$this->data[$this->alias]['title'] = ucfirst($this->data[$this->alias]['title']);
	}
	return true;
}

Now we can set up any additional relation without breaking anything:

var $belongsTo = array(
	'LastUser' => array(
		'className' 	=> 'User',
		'foreignKey'	=> 'last_user_id',
		'fields' => array('id', 'username')
	)
);

$this->alias is now “LastUser” for this relation.

Always try to outsource code, that repeats itself, into behaviors or the AppModel callbacks.
So if the “ucfirst()” code snippet from above is used in pretty much all models, you can either use beforeValidate() of the AppModel:

function beforeValidate() {
	parent::beforeValidate();
	if (isset($this->data[$this->alias][$this->displayField])) {
		$this->data[$this->alias][$this->displayField] = ucfirst($this->data[$this->alias][$this->displayField]);
	}
	return true;
}

or you can create a behavior:

class DoFancyBehavior extends ModelBehavior {
 
	function setup(Model $Model, $config = array()) {
		$this->config[$Model->alias] = $this->default;
		$this->config[$Model->alias] = array_merge($this->config[$Model->alias], $config);
		// [...]
		$this->Model = $Model;
	}
 
	function beforeValidate(Model $Model) {
		$this->_doFancyUcfirst($Model->data);
		return true;
	}
 
	function _doFancyUcfirst(&$data) {
		if (isset($data[$this->Model->alias][$this->Model->displayField])) {
			$data[$this->Model->alias][$this->Model->displayField] = ucfirst($data[$this->Model->alias][$this->Model->displayField]);
		}
	}
 
}

With the behavior you can define which models will and which won’t “behave” fancy.
But of course the same would be possible for the AppModel method using an internal variable ($this->behaveFancy: true/false) and checking on it in our callback.

 
No Comments

Posted in CakePHP

 

Validating multiple models at once

20 Jun

There are forms where you want to add/edit not only fields of the current model, but also of a related one (usually hasMany or belongsTo).

Example:
User and Post

$this->Post->set($this->data);
$this->Post->User->set($this->data);
 
$val1 = $this->Post->validates();
$val2 = $this->Post->User->validates();
 
if ($val1 && $val2) {
	// OK (save both models separatly in order to use the user_id)
	$this->Post->User->save(null, false);
	$this->data['Post']['user_id'] = $this->Post->User->id;
	$this->Post->save($this->data, false);
} else {
	//ERROR 
}

So whats going on here?
We first set the posted data to the models wie want to validate. This is important when using validates() instead of save(). Then we validate them and only if both pass we continue saving.

Why not directly? Why using $val1 and $val2?
Well, for that you have to know something about php as programming language and how conditions are processed.
If (condition1 && condition2 && ...) {} else {} stops checking conditions as soon as the first condition fails. this is “smart” because it saves time. If the first condition fails, it will jump to the else block no matter what the other conditions return. So why bothering to check them?
But we want to validate both models no matter what – so we have to process them before and only compare the return values! If you miss that you won’t see the validation errors of the second model in the view (if the first one failed, anyway).

Cake automatically passes the errors to the view, if the Model.field syntax is used there:

echo $this->Form->input('User.email');
echo $this->Form->input('User.username');
echo $this->Form->input('comment'); // or Post.comment

Thats it. You can extend this with as many models as you like.

Using PHP Tricks

If you read my other article you might have found another approach:

if ($this->Post->validates() & $this->Post->User->validates()) {}

This works because one & will first check all conditions before deciding what to do.

NEVER use two & (&&) in this situation – for validating multiple models at once. It’s wrong!

Using cake’s find(all)

If your data is in model + related models structure you can also use this approach. It is faster and shorter (but I sometimes like control over my form processing):

if ($this->Post->saveAll($data, array('validate'=>'only'))) {
	//saveAll with validate false or single set of saves with validate false
}

This will only validate all the inputs without saving it.
Either way you should then set validate to false afterwards (no need to re-validate the data on save).

 
1 Comment

Posted in CakePHP