Table of Contents |
3. Model3.1 OverviewThe Mad_Model component is an object-relational mapping (ORM) layer for the framework. We follow the ActiveRecord pattern where tables map to classes, rows map to objects, and columns to object attributes. The implementation is close to that of Ruby on Rails. The model layer is where the domain (business) logic of the application lies. This involves data retrieval, manipulation, and validation. An informal test I like is to imagine adding a radically different layer to an application, such as a command-line interface to a Web application. If there's any functionality you have to duplicate to do this, that's a sign of where domain logic has leaked into the presentation. -- martin fowler 3.2 Generating StubsThe framework provides a tool to generate stub files for new Mad_Model, and related classes. We can use script/generate script to do this. This script should be run from the application's root directory as such:
$> cd application_name/
# php ./script/generate model {ModelName}
$> php ./script/generate model User
This will create the following 4 file stubs which include the model class, the unit test stub file, the migration, and the yml fixture file. We will talk more about the testing files in the chapter describing the Test package.
3.3 Tables and ClassesWhen you create a subclass of Mad_Model you are creating a wrapper for a database table. The mapping of the table class is determined by specific naming conventions. The table should be named as the plural and underscored version of the model class name.
We can create a object to access data in this table by instantiating a new User object. // wrap the 'users' table by creating a User object $user = new User; 3.4 Columns and AttributesEach Model instance corresponds to a row in the database table. The object's attributes will correspond directly to the database columns, and are dynamically added using introspection of the database structure. // find a user record with the primary key of "1" $user = User::find(1); // get specific column info $name = $user->name; $phone = $user->phone; // set attributes and save back to db $user->name = 'Donny'; $user->phone = '555-1212'; $user->save(); 3.5 MigrationsWhen we generated the User model, the generator also created our users migration file. All migrations are stored within the db/migrate/ directory of our application, and keep a version history of database changes within the source tree. You'll see that the migration file for our users table has a numbered prefix of 001, which designates it as version 1 of our database history. This gives us a powerful tool for applying and rolling back any changes we make to the database. This is especially useful for teams that need to keep in sync with each other's database changes. Migrations are written in PHP, which lets you easily make applications that are more platform and database independent. In each migration file, we'll see the up and down methods. These will instruct our migration what to do when migrating up to revision number 1 of our database, or reverting back down to revision number 0.
class CreateUser extends Mad_Model_Migration_Base
{
public function up()
{
$t = $this->createTable('users', $options = array());
$t->column('username', 'string');
$t->column('company_id', 'integer');
$t->end();
}
public function down()
{
$this->dropTable('users');
}
}
When we migrate up in this migration, we'll be creating the users table. When migrating down, well drop the users table again. We can execute the migration with script/task db:migrate. Navigate to your application's root directory to run this. $> php ./script/task db:migrate == 001 CreateUsers: migrating =========================================== -- createTable(users) -> 9.1460s == 001 CreateUsers: migrated (12.5820s) ================================= Running this script will migrate to the newest version of your database schema, which in our case has successfully updated us to version 1. It will determine the newest version by scanning the filenames of the files in db/migrate/ to find the highest sequentially numbered migration. To instruct the task to migrate to a specific version, we can add the VERSION= argument to the script. $> php ./script/task db:migrate VERSION=0 == 001 CreateUsers: reverting =========================================== -- dropTable(users) -> 23.0070s == 001 CreateUsers: reverted (23.1660s) ================================= Here we have specified in the migrate command to revert back to VERSION=0. When executed, the migration drops the user table that we had specified in the down method of this migration. Mad keeps track of the migration version you are on by automatically creating a table named schema_info the first time you run a migration. This table use a single column named version to remember the version number. mysql> use my_app_development; Database changed mysql> select * from schema_info; +---------+ | version | +---------+ | 0 | +---------+ 1 row in set (0.00 sec) We can run migrations in production mode by adding the MAD_ENV=production to the list of arguments to script/task db:migrate. Let's now take a look at all the different operations we can perform within a migration file. 3.5.1 Create a TableEach $t->column() call within the createTable('users') block specifies a column for the table we are creating. The first argument is the column name, and the second is the data type. Since column type keywords vary across different database platforms, Mad uses a database independent syntax to specify the type of column we are creating. The valid types are binary, boolean, date, datetime, decimal, float, integer, string, text, time, and timestamp. The last argument to the column creation method is a hash of options for the column. This is where you can specify if this column uses a null constraint, default value, or character limit. We've taken advantage of these options to limit our password column to 40 characters, and add a default value of 0 to the is_admin column. Mad will automatically create a primary key column named id for each table. There are a couple reserved names for special columns used to store the date and time of when user record was created or updated. These columns are named created_at and updated_at. Mad_Model will automatically insert the current time into these columns when we insert or update user records. We'll typically add these columns to all tables that have data being modified by the application.
$t = $this->createTable('user', $options = array());
$t->column('username', 'string', array('null' => false));
$t->column('password', 'string', array('limit' => 40));
$t->column('company_id' 'integer');
$t->column('is_admin', 'boolean', array('default' => '0'));
$t->column('profile', 'text');
// magic cols
$t->column('created_at', 'datetime');
$t->column('updated_at', 'datetime');
$t->end();
An optional $options array can be given as the 2nd argument to createTable():
3.5.2 Rename a TableWe can rename a table:
$this->renameTable('users', 'clients');
3.5.3 Drop a Table
$this->dropTable('users');
3.5.4 Add a Column
$this->addColumn('users', 'fax_number', 'string', array('limit' => 10));
3.5.5 Remove a Column
$this->removeColumn('users', 'fax_number');
3.5.6 Change Column Default
// change the default value to '1'
$this->changeColumnDefault('users', 'is_admin', '1');
3.5.7 Change a Column
// change type, limit
$this->changeColumn('users', 'phone', 'integer', array('limit' => '10'));
// change precision/scale
$this->changeColumn('users', 'cash_on_hand', 'decimal',
array('precision' => '5', 'scale' => '2'));
3.5.8 Rename a Column
$this->renameColumn('users', 'phone', 'phone_number');
3.5.9 Add an Index
// single column index
$this->addIndex('users', 'company_id');
// multi-column index
$this->addIndex('users', array('name', 'company_id'));
// unique index
$this->addIndex('users', 'email', array('unique' => true));
// named index
$this->addIndex('users', 'is_admin', array('name' => 'admin'));
3.5.10 Remove an Index
// single-column index
$this->removeIndex('users', array('column' => 'company_id'));
// multi-column index
$this->removeIndex('users', array('column' => array('name', 'company_id')));
// named index
$this->removeIndex('users', array('name' => 'admin'));
3.5.11 Executing Arbitrary SQLEven though we have methods to cover nearly any operation you need to perform on a table, you can always drop back down to plain old SQL to do what you need. Always try to use a database independent syntax while doing this. We don't want to break the great portability that migrations give us by using proprietary MySQL syntax.
$this->execute("INSERT INTO users (id, name) VALUES (1, 'derek')");
3.6 CRUD
Model makes it very easy to perform the four basic operations on database
tables: Create, Read, Update, and Delete. The operations in this section work
work with a 3.6.1 Creating new RowsSince tables are represented as classes, and each object represents a row in the database, it would make sense that we would create a new object to insert a new record. We have to make sure that we use save() to insert the record or it only exists in memory. // insert folder by setting properties $folder = new Folder; $folder->name = 'My New Folder'; $folder->description = 'Folder Description'; $folder->save(); Mad_Model objects also take an array as an optional constructor argument. This can be used as a shortcut for loading attributes for a new object.
// set the properties using an attribute array
$folder = new Folder(array('name' => 'My New Folder',
'description' => 'Folder Description'));
$folder->save();
You'll notice we didn't pass in the primary key to this object before saving. This is because the primary key for this particular object is auto-incremented. We can get the id by referencing it after the object has been saved. // save and get the newly inserted id $folder->save(); $newFolderId = $folder->id; Another way to insert records is using the convenience method create(), which allows use to statically insert data without instantiating the object. Under the hood, this method basically performs the operation we outlined above.
// create single records
$folder = Folder::create(array('name' => 'My New Folder',
'description' => 'Folder Description'));
We can also create multiple objects by passing in an array of attribute hashes.
// create multiple records
$folder = Folder::create(array(
array('name' => 'Folder 1',
'description' => 'Folder Description 1'),
array('name' => 'Folder 2',
'description' => 'Folder Description 2')));
3.6.2 Find Existing RowsThe simplest way of specifying a row in the table is by using its primary key. Every model supports the find() method which is very versatile. Rows can be retrieved using a single primary key, or an array of primary keys. If any of the IDs given do not exist, the find() will throw a Mad_Model_Exception_RecordNotFound. This is because Model assumes that when searching by primary keys, that the specific IDs given should be present in the database (otherwise, where would those IDs come from?). // retrieve a single folder by primary key $folder = Folder::find(123); // retrieve a collection of folders by primary key $folders = Folder::find(array(123, 456, 789)); More often than not you will need more power. The above example just scratches the surface of find(). Find has a completely different method of working when you pass it either 'all' or 'first' as the first argument. The 'first' string when passed in will restrict the result set to a single record, and the 'all' string will return an array of Folder objects that match the given conditions.
// retrieve the first Folder
$folder = Folder::find('first');
// retrieve all Folders
$folders = Folder::find('all');
3.6.3 find() optionsfind()'s real power comes in its 2nd argument which is an array of options that can be passed in to build the sql statement with as little pain as possible. Lets start with the 'conditions' option to see how Model works with SQL.
// find folders within the parent_id=181 with more than 10 documents
$folders = Folder::find('all', array('conditions' => 'parent_id = :parent_id AND
document_count > :count'),
array(':parent_id' => '181',
':count' => '10'));
// loop through the collection
foreach ($folders as $folder) {
print $folder->name;
}
// get specific element of the collection
$specificFolder = $folders[3];
Notice that the 3rd argument to find() is an array of bind variables. It is extremely important to always bind your variables to maximize performance and avoid sql injection attacks. The result will be a Mad_Model_Collection object which will be conveniently accessible with array type syntax. This means you can do a foreach() over it or access specific elements. If we were to run the same find using 'first' instead of 'all', the result would be a single Folder object. One thing you'll notice about the above statement is that we're not afraid of SQL. The 'conditions' argument as well as many of the other options of find() are indeed straight SQL. The aim is not to completely reduce all SQL to some complex Object model, but to embrace the simplicity of SQL while reducing the duplication we type when writing it. The options available as the 2nd argument to find() are as follows:
select: This parameter allows you to specify to only bring back certain columns of the result set to be populated in the return object.
// only retrieve the id property in the result
$folder = Folder::find('first', array('select' => 'id'));
// this is populated
print $folder->id;
// name was NOT retrieved, so it is NULL
print $folder->name;
from: This option allows you to specify the from fragment of the SQL query.
// alias to select from the folders2 table
$folder = Folder::find('first', array('select' => 'f.*',
'from' => 'folders2 f'));
conditions: This option allows you to specify the WHERE clause of the SQL statement.
// find folders with parent_id=181 and more than 10 documents
$folders = Folder::find('all', array('conditions' => 'parent_id = :parent_id AND
document_count > :count'),
array(':parent_id' => '181',
':count' => '10'));
order: This option allows you to specify the ORDER clause of the SQL statement
// find all folders and order by name column, and description descending
$folders = Folder::find('all', array('order' => 'name, description DESC'));
group: This option allows you to specify the GROUP clause of the SQL statement
// find all folders and group by the name
$folders = Folder::find('all', array('group' => 'name'));
limit: This option allows you to limit the number of results to bring back. This is similar to the MySQL LIMIT parameter
// find folder records 0 - 10
$folders = Folder::find('all', array('limit' => '10'));
offset: This option works in conjunction with 'limit' to return a limited result set. We can use this option to specify the initial record to return.
// find folder records 20 - 30
$folders = Folder::find('all', array('limit' => '10', 'offset' => '20'));
include: This option allows us to eager load associated objects for a model. We can specify the association name, and the find query will automatically query and populate the those objects in a single query. The syntax for the options can be specified in a few different ways. It can be given a single association name (string), an array of association names, or an assocation name with its own association includes. This allows limitless eager loading of any association available.
// single query for all documents in all folders
$folders = Folder::find('all', array('include' => 'Documents'));
// single query for all sub-folders and documents in all folders
$folders = Folder::find('all', array('include' =>
array('Folders', 'Documents')));
// single query for all documents with their associated
// types in all folders
$folders = Folder::find('all', array('include' =>
array('Documents' => 'DocumentType')));
// single query for all subfolders, and documents with their
// associated types in all folders
$folders = Folder::find('all', array('include' =>
array('Folders',
'Documents' => 'DocumentType')));
3.6.4 Executing SQLOften there will be times when find() will not be enough to perform the SELECT query you need. In these cases we would create a custom method on the model to perform the sql operation. In the case of Folder, we have added a method to find all root level folders. By using the findBySql method, we will get an array of Folder objects back from the query results.
/**
* Find all root folders
* @return Array[Folders]
*/
public static function findRootFolders()
{
$sql = "SELECT *
FROM folders f
WHERE f.parent_id=:id
ORDER BY f.name";
return self::findBySql('all', $sql, array(':id' => 0));
}
Now we can use this custom method to return Folder objects just as if we were using find('all').
// find the root folders
$folders = Folder::findRootFolders();
foreach ($folders as $folder) {
print $folder->name;
}
This example is for demonstrative purposes only, and doesn't
necessarily reflect a best practice. While creating the reusable
3.6.5 Counting RowsWe can count records for a given model using count(). This takes all the same parameters as find(), but obviously, some of them are useless (like 'order'). // count total folders $folderCnt = Folder::count(); 3.6.6 Does Row Exist?Sometimes you simply want to check if a record exists. This can be done using the exists() method // check if id=123 exists $folderExists = Folder::exists(123); 3.6.7 Updating Existing RowsThere are numerous ways to update existing records in the database. save: allows us to save attribute changes done to a instance of a model object. // retrieve id=123, and update its name column $folder = Folder::find(123); $folder->name = 'New Name'; $folder->save(); updateAttribute: allows us to change a single attribute and save the model in a single step. This method automatically performs save() on the changes.
// retrieve id=123, and update its name column
$folder = Folder::find(123);
$folder->updateAttribute('name', 'New Name');
updateAttribues: allows us to change multiple attributes and save the model in a single step. This method automatically performs save() on the changes.
// retrieve id=123, and update its name/description columns
$folder = Folder::find(123);
$folder->updateAttributes(array('name' => 'New Name',
'description' => 'New Description'));
update: is a static method available to update records directly without instantiating a model object. The first argument is the primary key(s) of the model to update, and the second argument is the array of attribute changes.
// update id=123's name/description columns
Folder::update(123, array('name' => 'New Name',
'description' => 'New Description'));
// update multiple (123, 456) folder's name/description columns
Folder::update(array(123, 456), array('name' => 'New Name',
'description' => 'New Description'));
updateAll: is a static method available to update all records that match a given condition. The first argument is the update SET statement, and the 2nd argument is the WHERE statement. The third argument is the bind variables for the SQL.
// update folders with parent=123, to now have parent=456
Folder::updateAll('parent_id=:new_parent', 'parent_id=:old_parent',
array(':new_parent' => 456,
':old_parent' => 123));
3.6.8 Deleting Rowsdestroy: allows us to delete an instance of a model from the database. Trying to access attributes of the model after it has been destroyed will throw an Exception. // delete id=123 $folder = Folder::find(123); $folder->destroy(); // error thrown (object is invalid now) print $folder->name; delete: performs a delete from the database using a static call so that there is no need to instantiate the model first. This method can also take an array of primary keys as the argument to delete multiple rows. // delete id=123 Folder::delete(123); // delete id in (123, 456) Folder::delete(array(123, 456)); deleteAll: allows us to delete all records matching the given criteria. The first argument is the WHERE statement, and the second is the bind variables. Calling delete all without any arguments will simply empty the table for the model (be careful!).
// delete all folder records
Folder::deleteAll();
// delete all folders where parent_id=123
Folder::deleteAll('parent_id=:id', array(':id' => 123));
3.6.9 Mind Your ExceptionsThe framework uses PHP Exceptions to handle all errors that happen. For the sake of brevity, I have left all exception handling out of the above examples. I beg you to not do the same. There are a few situations where handling exceptions is essential. The first situation is when finding records by their primary key(s). It is assumed that the id you give find() exists in the database. A Mad_Model_Exception_RecordNotFound is thrown when that record is missing. You should handle any situation where this could occur. A common scenario I can think of is where we get that id from somewhere such as a session where it could possibly be empty.
// set the folder to null if we don't find the record
try {
$folderId = $this->session['last_folder_id'];
$folder = Folder::find($folderId);
} catch (Mad_Model_Exception_RecordNotFound $e) {
$folder = null;
}
// only display the folder if we found it
if ($folder) {
print $folder->name;
}
3.7 AssociationsThe real fun in Model comes with the associations. Model allows you to tie model objects together through database foreign-key relationships. Once we have the correct relationships declared in the _initialize method of the model, we can refer directly to related objects of that model. If we were to say that "Folder has many Documents", we could then reference the documents within a folder model through the relationship.
// print the name of each document within the folder.
$folder = Folder::find(123);
foreach ($folder->documents as $document) {
print $document->name;
}
There are four different relationships that can be defined between models.
In all the relationship methods, the first argument is the name of the association to be added. By default, you will want to make this the Name of the associated class. For example, a Document 'belongsTo' a Folder.
class Document extends Mad_Model_Base
{
public function _initialize()
{
$this->belongsTo('Folder')
}
}
The plurality of the class name changes with one-to-many and many-to-many relationships so that it reads in a more natural way. Notice how a Document belongsTo Folder, while a Folder hasMany Documents.
class Folder extends Mad_Model_Base
{
public function _initialize()
{
$this->hasMany('Documents')
}
}
While this makes our associations nice and easy to read, the name of the association is not tied down to the name of the model. This comes in handy if you need multiple relationships to the same model. The second argument in all relationship definitions is an array of options to configure the relationship. If you create a custom name for an association (not based directly on the name of the associated model), you will have to specify which model class it refers to using the className option.
class Folder extends Mad_Model_Base
{
public function _initialize()
{
$this->hasMany('Docs', array('className' => 'Documents'));
}
}
We can now refer to this association as docs instead of documents.
// use the relationship
$folder = Folder::find(123);
foreach ($folder->docs as $doc) {
print $doc->name;
}
Each association has specific options, as well as specific properties/methods that are dynamically added when the association is declared. 3.7.1 Belongs ToThe belongsTo() method allows us to specify a one-to-one relationship with another model. This declaration must be made in the model that contains the foreign key. options:
class Document extends Mad_Model_Base
{
public function _initialize()
{
$this->belongsTo('Folder')
}
}
We can now use the relationship referring to the associated object as folder // use the relationship $doc = Document::find(123); print $doc->folder->name; properties/methods added with belongsTo:
// access associated object
$folder = $document->folder;
// assign associated object & save new association
$document->folder = Folder::find(123);
$document->save();
// build new object to use as association & save new object/association
$folder = $document->buildFolder(array('name' => 'New Folder'));
$document->save();
// build new object to use as association & save new association.
// This option will automatically save the associated object, but !not!
// the actual association with the current object until you use save().
$folder = $document->createFolder(array('name' => 'New Folder'));
$document->save();
3.7.2 Has OneThe hasOne() method also allows us to specify a one-to-one relationship with another model. This declaration is made in the model that contains the primary key. options:
class User extends Mad_Model_Base
{
public function _initialize()
{
$this->hasOne('AvatarImage')
}
}
We can now use the relationship referring to the associated object as avatarImage // use the relationship $user = User::find(123); print $user->avatarImage->name; properties/methods added with hasOne:
// access associated object
$avatarImage = $user->avatarImage;
// assign associated object & save new association
$user->avatarImage = new AvatarImage(array('name' => 'profile.gif'));
$user->save();
// build new object to use as association & save new object/association
$user->buildAvatarImage(array('name' => 'profile.gif'));
$user->save();
// build new object to use as association & save new association.
// This option will automatically save the associated object, but !not!
// the actual association with the current object until you use save().
$metadata->createAvatarImage(array('name' => 'privileged.gif'));
$user->save();
3.7.3 Has ManyThe hasMany() method allows us to specify a one-to-many relationship with another model. This declaration is made in the model that contains the primary key. options:
class Folder extends Mad_Model_Base
{
public function _initialize()
{
$this->hasMany('Documents')
}
}
We can now use the relationship referring to the associated objects as documents
// use the relationship
$folder = Folder::find(123);
foreach ($folder->documents as $document) {
print $document->name;
}
properties/methods added with hasMany:
// access collection of associated objects
$documents = $folder->documents;
// assign array of associated objects and save associations
$folder->documents = array(Document::find(123), Document::find(234));
$folder->save();
// access array of associated object's primary keys
$documentIds = $folder->documentIds;
// set associated objects by primary keys
$folder->documentIds = array(123, 234);
$folder->save();
// get the count of associated objects
$docCount = $folder->documentCount;
// add an associated object to the collection and save association
$folder->addDocument(Document::find(123));
$folder->save();
// replace the associated collection with the given list. Will only perform
// update/inserts when necessary
$folder->replaceDocuments(array(Document::find(123), Document::find(234)));
$folder->replaceDocuments(array(123, 234));
$folder->save();
// delete specific associated objects from the collection
$folder->deleteDocuments(array(Document::find(123), Document::find(234)));
$folder->deleteDocuments(array(123, 234));
$folder->save();
// clear all associated objects
$folder->clearDocuments();
$folder->save();
// search for a subset of documents within the associated collection
$docs = $folder->findDocuments('all', array('conditions' => 'document_type_id = :type'),
array(':type' => 1));
// build new object to add to association collection & save new object/association
$document = $folder->buildDocument(array('name' => 'New Document'));
$document->save();
// build new object to add to association collection & save new association.
// This option will automatically save the associated object, but !not!
// the actual association with the current object until you use save().
$document = $folder->createDocument(array('name' => 'New Document'));
$document->save();
3.7.4 Has Many 'through'The hasMany(Objects, array('through' => 'JoinTable')) method uses the hasMany() method with an additional through option to create a many-to-many relationship with an join model. This is the preferred approach to creating many-to-many relationship, and should be used instead of the HABTM association whwerever possible. This declaration is made in both models in the relationship. options are the same as a hasMany association, but use the additional:
The Join table in this type of association is a model in itself, and should have a primary key. We make association declarations in all three models involved. The join Model should have belongsTo declarations that refer back to the base models.
class Tag extends Mad_Model_Base
{
public function _initialize()
{
$this->hasMany('Documents', array('through' => 'Taggings'));
}
}
...
class Document extends Mad_Model_Base
{
public function _initialize()
{
$this->hasMany('Tags', array('through' => 'Taggings'));
}
}
...
class Tagging extends Mad_Model_Base
{
public function _initialize()
{
$this->belongsTo('Tag');
$this->belongsTo('Document');
}
}
This sets up the association in both directions. We can now use the relationship referring to Tag's associated objects as documents, and Document's associated objects as or tags.
// use the 'through' relationship
$tag = Tag::find(123);
foreach ($tag->documents as $doc) {
print $doc->name;
}
$doc = Document::find(123);
foreach ($doc->tags as $tag) {
print $tag->name;
}
The properties added with this association are the same as those added with a normal hasMany association. 3.7.5 HABTMThe hasAndBelongsToMany() method allows us to specify a many-to-many relationship with another model using a join table. This declaration is made in both models in the relationship. options:
class Tag extends Mad_Model_Base
{
public function _initialize()
{
$this->hasAndBelongsToMany('Documents')
}
}
...
class Document extends Mad_Model_Base
{
public function _initialize()
{
$this->hasAndBelongsToMany('Tags')
}
}
This sets up the association in both directions. We can now use the relationship referring to Tag's associated objects as documents, and Document's associated objects as or tags.
// use the relationship
$tag = Tag::find(123);
foreach ($tag->documents as $doc) {
print $doc->name;
}
$doc = Document::find(123);
foreach ($doc->tags as $tag) {
print $tag->name;
}
properties/methods added with hasAndBelongsToMany:
// access collection of associated objects
$documents = $tag->documents;
// assign array of associated objects and save associations
$tag->documents = array(Document::find(123), Document::find(234));
$tag->save();
// access array of associated object's primary keys
$documentIds = $tag->documentIds;
// set associated objects by primary keys
$tag->documentIds = array(123, 234);
$tag->save();
// get the count of associated objects
$docCount = $tag->documentCount;
// add an associated object to the collection and save association
$tag->addDocument(Document::find(123));
$tag->save();
// replace the associated collection with the given list. Will only perform
// update/inserts when necessary
$tag->replaceDocuments(array(Document::find(123), Document::find(234)));
$tag->replaceDocuments(array(123, 234));
$tag->save();
// delete specific associated objects from the collection
$tag->deleteDocuments(array(Document::find(123), Document::find(234)));
$tag->deleteDocuments(array(123, 234));
$tag->save();
// clear all associated objects
$tag->clearDocuments();
$tag->save();
// search for a subset of documents within the associated collection
$docs = $tag->findDocuments('all', array('conditions' => 'document_type_id = :type'),
array(':type' => 1));
3.7.6 Foreign KeysThe default behavior of foreignKey and associationForeignKey are to inspect the data object for the model and use the singular format of the table name suffixed with _id. So when we state that a Document belongs_to Folder, we are looking for a folder_id column in the documents table. You will have to specify the foreignKey column if it does not match the pk name of the associated table.
class Document extends Mad_Model_Base
{
public function _initialize()
{
// no need to specify foreignKey when
// Document has a document_type_id column
$this->belongsTo('DocumentType');
// we have to specificy the key when we don't follow conventions
// if the folders foreign key in the documents table is "parent_id"
$this->belongsTo('Folder', array('foreignKey' => 'parent_id'));
}
}
3.8 Validation3.8.1 OverviewWhen you are using the Model to insert or modify data in the database, most of the time you will need to validate data. The framework has a standard way to do this so that you can easily check the data given by a user and return a user-friendly message of any changes that need to be made to save the data. 3.8.2 Validation DeclarationsThe fastest way to add validation to a model is using validation declarations within the _initialize() method. There are currently 6 different type of declarations that can be made:
validatesFormatOf can ensure that the value is alpha, digit, alnum, or that the value matches a given regexp pattern.
protected function _initialize()
{
$this->validatesFormatOf('date_value', array('with' => '/\d{4}-\d{2}-\d{2}/'),
'message' => 'has to be formatted (YYYY-MM-DD)');
$this->validatesFormatOf('number_value', array('on' => 'update',
'with' => '[digit]'));
}
validatesInclusionOf validates that the value falls within an array of acceptable values.
protected function _initialize()
{
$this->validatesInclusionOf('answer', array('in' => array('yes', 'no')));
}
validatesLengthOf can ensure that the string length of the value is above, below, exactly matches, or within a range of sizes.
protected function _initialize()
{
$this->validatesLengthOf(array('name', 'description'),
array('maximum' => 3000));
$this->validatesLengthOf('description', array('minimum' => 10,
'tooShort' => 'must have a better description'));
}
validatesNumericalityOf ensures that the value is numeric, and can specify if decimals are allowed.
protected function _initialize()
{
$this->validatesNumericalityOf('number_value');
$this->validatesNumericalityOf('age', array('allowNull' => true));
}
validatesPresenceOf ensures that a value is present.
protected function _initialize()
{
$this->validatesPresenceOf(array('id', 'name'));
}
validatesUniquenessOf ensures that the value doesnm't already exist in the database. It can also scope this uniqueness only validate when in combination with another column's value.
protected function _initialize()
{
$this->validatesUniquenessOf('name', array('scope' => 'parent_id'));
}
3.8.3 Validation MethodsThere are currently three different methods for validating data:
When you add one or more of the above methods to your model, it will automatically be registered to execute before data is saved. Adding errors from within these methods is done via the errors->add() or errors->addToBase() methods.
Class Folder extends Mad_Model_Base
{
/**
* This method will execute before any update/insert operation
* it makes sure that the description is not empty, and that the name
* isn't changed.
*/
public function validate()
{
// argument to add() are the attribute name and message
if (empty($this->description)) {
$this->errors->add("description", "cannot be blank");
}
// we can also add errors not associated with a attribute
if (empty($this->name)) {
$this->errors->addToBase('Fix the name!');
}
}
}
3.8.4 Getting ErrorsWhen a validation error is encountered during a save operation, a list of errors is added to the model in the object's errors property. The save() method will return false when errors are encountered. The errors property on the object is actually an instance of Mad_Model_Errors, which is an iterable list of errors. To get an array with the full error messages encountered, we will use the $folder->errors->fullMessages() method.
$folder = Folder::find(123);
$folder->description = '';
if (!$folder->save()) {
$errors = $folder->errors->fullMessages();
foreach ($errors as $error) {
print "$error\n";
}
}
Alternately, we can use exception handling to catch validation errors. This only works when we use the saveEx() method to save our object. It is preferred to not use exception handling when accessing errors. The errors attribute mentioned above is more useful when we are using form helpers to do the dirty work of displaying errors.
// use exception handling
try {
$folder = Folder::find(123);
$folder->description = '';
$folder->saveEx();
// this should fail when the description can't be empty!
} catch (Mad_Model_Exception_Validation $e) {
foreach ($e->getMessages() as $message) {
print $message;
}
}
3.9 CallbacksModel has ways of monitoring and intercepting the execution inserts, updates, and deletes via the standard Model methods. We can write code that gets invoked at any significant event in the life cycle of the model object.
A good example of this might be to perform some type of pre-validation formatting of data.
public function User extends Mad_Model_Base
{
public function beforeValidation()
{
// make sure web page has leading 'http://'
if (!strstr($this->website, 'http://')) {
$this->website = "http://$this->website";
}
}
}
|
||||||||||||