RSS
 

Posts Tagged ‘Model’

Schema reference for table less models

01 May

If you ever need a table less model with only some manual schema information (mainly for validation and form helper to work), you might find the documentation a little bit incomplete.
There are more than just those types available.
Here is a full list.

PS: Quite some time ago, I used this for my ContactForm. This is a live example on how to use such a table less model and how validation can still be performed on these virtual fields.

How is it used?

class MySchemaLessModelName extends AppModel {
	protected $_schema = array(
	   'status' => array(
	       'type' => 'boolean',
	       'length' => 1,
	       'default' => 0,
	       'null' => false,
	       'comment' => 'some optional comment'
	   ),
	   ...   
	);
}

Why is this necessary/recommended?

By manually describing your table less fields cake you can have the same form helper magic. So the FormHelper will display a checkbox for boolean fields, and textareas for text fields etc.
Also you get all the validation (length or required for example) out of the box. You can also set up the $validate array for additional validation, of course.

Full schema reference

Boolean (tinyint 1):

'approved' => array(
	'type' => 'boolean',
	'length' => 1,
	'default' => 0,
	'null' => false,
	'comment' => 'some optional comment'
),

Best to make this field unsigned in your database (although cake cannot recognize this as of now).

Integer (int 10/11):

'clicks' => array(
	'type' => 'integer',
	'length' => 10,
	'default' => 0,
	'null' => false,
	'comment' => 'some optional comment'
),

This field can also be unsigned if there cannot be any negative values.

String (varchar 1…255):

'subject' => array(
	'type' => 'string',
	'length' => 255,
	'default' => '',
	'null' => false,
	'comment' => 'some optional comment'
),

Text (text):

'comment' => array(
	'type' => 'text',
	'length' => null,
	'default' => '',
	'null' => false,
	'comment' => 'some optional comment'
),

Decimal (float):

'amount' => array(
	'type' => 'float',
	'length' => '6,2',
	'default' => 0.00,
	'null' => false,
	'comment' => 'some optional comment'
),

Foreign key as UUID (char 36):

'user_id' => array(
	'type' => 'string',
	'length' => 36,
	'default' => '',
	'null' => false,
	'comment' => 'some optional comment'
),

Date (date)

'published' => array(
	'type' => 'date',
	'length' => null,
	'default' => null,
	'null' => true,
	'comment' => 'some optional comment'
),

Datetime (datetime)

'modified' => array(
	'type' => 'datetime',
	'length' => null,
	'default' => null,
	'null' => true,
	'comment' => 'some optional comment'
),

Enum (at least my version of it – since cake doesn’t have a core solution here)

'status' => array(
	'type' => 'integer',
	'length' => 2,
	'default' => 0,
	'null' => false,
	'comment' => 'some optional comment'
),

See static-enums-or-semihardcoded-attributes for how to use these tinyint(2) enums.

Note: All fields (except for date/datetime) are set to "not null" in the above examples (which will make the form helper and validation require those). Those can be null, though, too, if desired.

Hot Tip

If you are looking for a quick way to find this out yourself:

Create a table "apples" and an Apple model and add all types of fields you want to debug then call the model schema() like so:

debug($this->Apple->schema());

This is how I confirmed the above.

A second more persistent way would be to run the following command for such a test table (or any other real table for that matter):

cake schema generate

It will dump a schema file in your APP/Config/Schema folder with the current tables as cake schema definition.

Anything missing?

I hope I didn’t forget anything. Otherwise please correct me!

 
3 Comments

Posted in CakePHP

 

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.

To summarize: 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