Commit 171226d5 authored by romanb's avatar romanb

Continued work on the validation component.

Ticket: 150
parent d81a4245
<?php <?php
/* /*
* $Id$ * $Id$
* *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* *
* This software consists of voluntary contributions made by many individuals * This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see * and is licensed under the LGPL. For more information, see
* <http://www.phpdoctrine.com>. * <http://www.phpdoctrine.com>.
*/ */
/** /**
* Doctrine_Access * Doctrine_Access
* *
* the purpose of Doctrine_Access is to provice array access * the purpose of Doctrine_Access is to provice array access
* and property overload interface for subclasses * and property overload interface for subclasses
* *
* @package Doctrine ORM * @package Doctrine ORM
* @url www.phpdoctrine.com * @url www.phpdoctrine.com
* @license LGPL * @license LGPL
*/ */
abstract class Doctrine_Access implements ArrayAccess { abstract class Doctrine_Access implements ArrayAccess {
/** /**
* setArray * setArray
* @param array $array an array of key => value pairs * @param array $array an array of key => value pairs
* @return Doctrine_Access * @return Doctrine_Access
*/ */
public function setArray(array $array) { public function setArray(array $array) {
foreach($array as $k=>$v): foreach($array as $k=>$v):
$this->set($k,$v); $this->set($k,$v);
endforeach; endforeach;
return $this; return $this;
} }
/** /**
* __set -- an alias of set() * __set -- an alias of set()
* @see set, offsetSet * @see set, offsetSet
* @param $name * @param $name
* @param $value * @param $value
*/ */
public function __set($name,$value) { public function __set($name,$value) {
$this->set($name,$value); $this->set($name,$value);
} }
/** /**
* __get -- an alias of get() * __get -- an alias of get()
* @see get, offsetGet * @see get, offsetGet
* @param mixed $name * @param mixed $name
* @return mixed * @return mixed
*/ */
public function __get($name) { public function __get($name) {
return $this->get($name); return $this->get($name);
} }
/** /**
* __isset() * __isset()
* *
* @param string $name * @param string $name
*/ */
public function __isset($name) { public function __isset($name) {
return $this->contains($name); return $this->contains($name);
} }
/** /**
* __unset() * __unset()
* *
* @param string $name * @param string $name
*/ */
public function __unset($name) { public function __unset($name) {
return $this->remove($name); return $this->remove($name);
} }
/** /**
* @param mixed $offset * @param mixed $offset
* @return boolean -- whether or not the data has a field $offset * @return boolean -- whether or not the data has a field $offset
*/ */
public function offsetExists($offset) { public function offsetExists($offset) {
return $this->contains($offset); return $this->contains($offset);
} }
/** /**
* offsetGet -- an alias of get() * offsetGet -- an alias of get()
* @see get, __get * @see get, __get
* @param mixed $offset * @param mixed $offset
* @return mixed * @return mixed
*/ */
public function offsetGet($offset) { public function offsetGet($offset) {
return $this->get($offset); return $this->get($offset);
} }
/** /**
* sets $offset to $value * sets $offset to $value
* @see set, __set * @see set, __set
* @param mixed $offset * @param mixed $offset
* @param mixed $value * @param mixed $value
* @return void * @return void
*/ */
public function offsetSet($offset, $value) { public function offsetSet($offset, $value) {
if( ! isset($offset)) { if( ! isset($offset)) {
$this->add($value); $this->add($value);
} else } else
$this->set($offset,$value); $this->set($offset,$value);
} }
/** /**
* unset a given offset * unset a given offset
* @see set, offsetSet, __set * @see set, offsetSet, __set
* @param mixed $offset * @param mixed $offset
*/ */
public function offsetUnset($offset) { public function offsetUnset($offset) {
return $this->remove($offset); return $this->remove($offset);
} }
} }
...@@ -236,11 +236,13 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite ...@@ -236,11 +236,13 @@ abstract class Doctrine_Record extends Doctrine_Access implements Countable, Ite
public function isValid() { public function isValid() {
if( ! $this->table->getAttribute(Doctrine::ATTR_VLD)) if( ! $this->table->getAttribute(Doctrine::ATTR_VLD))
return true; return true;
// Clear the stack from any previous errors.
$this->errorStack->clear();
// Run validation process
$validator = new Doctrine_Validator(); $validator = new Doctrine_Validator();
// Run validators
$validator->validateRecord($this); $validator->validateRecord($this);
// Run custom validation
$this->validate(); $this->validate();
return $this->errorStack->count() == 0 ? true : false; return $this->errorStack->count() == 0 ? true : false;
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
* @license LGPL * @license LGPL
* @package Doctrine * @package Doctrine
*/ */
class Doctrine_Validator_ErrorStack implements ArrayAccess, Countable, IteratorAggregate { class Doctrine_Validator_ErrorStack extends Doctrine_Access implements Countable, IteratorAggregate {
/** /**
* The errors of the error stack. * The errors of the error stack.
...@@ -49,8 +49,8 @@ class Doctrine_Validator_ErrorStack implements ArrayAccess, Countable, IteratorA ...@@ -49,8 +49,8 @@ class Doctrine_Validator_ErrorStack implements ArrayAccess, Countable, IteratorA
* @param string $invalidFieldName * @param string $invalidFieldName
* @param string $errorType * @param string $errorType
*/ */
public function add($invalidFieldName, $errorType = 'general') { public function add($invalidFieldName, $errorCode = 'general') {
$this->errors[$invalidFieldName][] = array('type' => $errorType); $this->errors[$invalidFieldName][] = $errorCode;
} }
/** /**
...@@ -70,66 +70,35 @@ class Doctrine_Validator_ErrorStack implements ArrayAccess, Countable, IteratorA ...@@ -70,66 +70,35 @@ class Doctrine_Validator_ErrorStack implements ArrayAccess, Countable, IteratorA
* @param unknown_type $name * @param unknown_type $name
* @return unknown * @return unknown
*/ */
public function get($name) { public function get($fieldName) {
return $this[$name]; return isset($this->errors[$fieldName]) ? $this->errors[$fieldName] : null;
}
/** ArrayAccess implementation */
/**
* Gets all errors that occured for the specified field.
*
* @param string $offset
* @return The array containing the errors or NULL if no errors were found.
*/
public function offsetGet($offset) {
return isset($this->errors[$offset]) ? $this->errors[$offset] : null;
} }
/** /**
* Enter description here... * Enter description here...
* *
* @param string $offset * @param unknown_type $name
* @param mixed $value
* @throws Doctrine_Validator_ErrorStack_Exception Always thrown since this operation is not allowed.
*/ */
public function offsetSet($offset, $value) { public function set($fieldName, $errorCode) {
throw new Doctrine_Validator_ErrorStack_Exception("Errors can only be added through $this->add($fieldName, $errorCode);
Doctrine_Validator_ErrorStack::add()");
} }
/** /**
* Enter description here... * Enter description here...
* *
* @param unknown_type $offset * @return unknown
*/ */
public function offsetExists($offset) { public function contains($fieldName) {
return isset($this->errors[$offset]); return array_key_exists($fieldName, $this->errors);
} }
/** /**
* Enter description here... * Removes all errors from the stack.
*
* @param unknown_type $offset
* @throws Doctrine_Validator_ErrorStack_Exception Always thrown since this operation is not allowed.
*/ */
public function offsetUnset($offset) { public function clear() {
throw new Doctrine_Validator_ErrorStack_Exception("Errors can only be removed $this->errors = array();
through Doctrine_Validator_ErrorStack::remove()");
} }
/**
* Enter description here...
*
* @param unknown_type $stack
*/
/*
public function merge($stack) {
if(is_array($stack)) {
$this->errors = array_merge($this->errors, $stack);
}
}*/
/** IteratorAggregate implementation */ /** IteratorAggregate implementation */
......
<?php
class Doctrine_Validator_Required {
/**
* @param Doctrine_Record $record
* @param string $key
* @param mixed $value
* @return boolean
*/
public function validate(Doctrine_Record $record, $key, $value) {
return ($value === null);
}
}
<?php
try {
$user->name = "this is an example of too long name";
$user->Email->address = "drink@@notvalid..";
$user->save();
} catch(Doctrine_Validator_Exception $e) {
$stack = $e->getErrorStack();
foreach($stack as $component => $err) {
foreach($err as $field => $type) {
switch($type):
case Doctrine_Validator::ERR_TYPE:
print $field." is not right type";
break;
case Doctrine_Validator::ERR_UNIQUE:
print $field." is not unique";
break;
case Doctrine_Validator::ERR_VALID:
print $field." is not valid";
break;
case Doctrine_Validator::ERR_LENGTH:
print $field." is too long";
break;
endswitch;
}
}
}
?>
<?php <?php
class User extends Doctrine_Record { class User extends Doctrine_Record {
public function setUp() { public function setUp() {
$this->ownsOne("Email","User.email_id"); $this->ownsOne("Email","User.email_id");
} }
public function setTableDefinition() { public function setTableDefinition() {
// no special validators used only types // no special validators used only types
// and lengths will be validated // and lengths will be validated
$this->hasColumn("name","string",15); $this->hasColumn("name","string",15);
$this->hasColumn("email_id","integer"); $this->hasColumn("email_id","integer");
$this->hasColumn("created","integer",11); $this->hasColumn("created","integer",11);
} }
} }
class Email extends Doctrine_Record { class Email extends Doctrine_Record {
public function setTableDefinition() { public function setTableDefinition() {
// specialized validators 'email' and 'unique' used // validators 'email' and 'unique' used
$this->hasColumn("address","string",150,"email|unique"); $this->hasColumn("address","string",150, array("email", "unique" => true));
} }
} protected function validate() {
$conn = Doctrine_Manager::getInstance()->openConnection(new PDO("dsn","username","password")); if ($this->address !== 'the-only-allowed-mail@address.com') {
$user = new User(); // syntax: add(<fieldName>, <error identifier>)
$user->name = "this is an example of too long name"; $this->errorStack->add('address', 'myCustomValidationTypeError');
}
$user->save(); // throws a Doctrine_Validator_Exception }
}
$user->name = "valid name"; ?>
$user->created = "not valid"; // not valid type
$user->save(); // throws a Doctrine_Validator_Exception
$user->created = time();
$user->Email->address = "drink@.."; // not valid email address
$user->save(); // throws a Doctrine_Validator_Exception
$user->Email->address = "drink@drinkmore.info";
$user->save(); // saved
$user = $conn->create("User");
$user->Email->address = "drink@drinkmore.info"; // not unique!
$user->save(); // throws a Doctrine_Validator_Exception
?>
<?php
try {
$user->name = "this is an example of too long name";
$user->Email->address = "drink@@notvalid..";
$user->save();
} catch(Doctrine_Validator_Exception $e) {
$userErrors = $user->getErrorStack();
$emailErrors = $user->Email->getErrorStack();
/* Inspect user errors */
foreach($userErrors as $fieldName => $errorCodes) {
switch ($fieldName) {
case 'name':
// $user->name is invalid. inspect the error codes if needed.
break;
}
}
/* Inspect email errors */
foreach($emailErrors as $fieldName => $errorCodes) {
switch ($fieldName) {
case 'address':
// $user->Email->address is invalid. inspect the error codes if needed.
break;
}
}
}
?>
Validation in Doctrine is a way to enforce your business rules in the model part of the MVC architecture.
You can think of this validation as a gateway that needs to be passed right before data gets into the
persistent data store. The definition of these business rules takes place at the record level, that means
in your active record model classes (classes derived from Doctrine_Record).
The first thing you need to do to be able to use this kind of validation is to enable it globally.
This is done through the Doctrine_Manager (see the code below).<br />
<br />
Once you enabled validation, you'll get a bunch of validations automatically:<br />
<br />
- Data type validations: All values assigned to columns are checked for the right type. That means
if you specified a column of your record as type 'integer', Doctrine will validate that
any values assigned to that column are of this type. This kind of type validation tries to
be as smart as possible since PHP is a loosely typed language. For example 2 as well as "7"
are both valid integers whilst "3f" is not. Type validations occur on every column (since every
column definition needs a type).<br /><br />
- Length validation: As the name implies, all values assigned to columns are validated to make
sure that the value does not exceed the maximum length.
With Doctrine validators you can validate a whole transaction and get info of everything
that went wrong. Some Doctrine validators also act as a database level constraints. For example
adding a unique validator to column 'name' also adds a database level unique constraint into that
column.
<br \><br \>
Validators are added as a 4 argument for hasColumn() method. Validators should be separated
by '|' mark. For example email|unique would validate a value using Doctrine_Validator_Email
and Doctrine_Validator_Unique.
<br \><br \>
Doctrine has many predefined validators (see chapter 13.3). If you wish to use
custom validators just write *Validator classes and doctrine will automatically use them.
The type and length validations are handy but most of the time they're not enough. Therefore
Doctrine provides some mechanisms that can be used to validate your data in more detail.<br />
<br />
Validators: Validators are an easy way to specify further validations. Doctrine has a lot of predefined
validators that are frequently needed such as email, country, ip, range and regexp validators. You
find a full list of available validators at the bottom of this page. You can specify which validators
apply to which column through the 4th argument of the hasColumn() method.
If that is still not enough and you need some specialized validation that is not yet available as
a predefined validator you have three options:<br />
<br />
- You can write the validator on your own.<br />
- You can propose your need for a new validator to a Doctrine developer.<br />
- You can use validation hooks.<br />
<br />
The first two options are advisable if it is likely that the validation is of general use
and is potentially applicable in many situations. In that case it is a good idea to implement
a new validator. However if the validation is special it is better to use hooks provided by Doctrine.
One of these hooks is the validate() method. If you need a special validation in your active record
you can simply override validate() in your active record class (a descendant of Doctrine_Record).
Within this method you can use all the power of PHP to validate your fields. When a field
doesnt pass your validation you can then add errors to the record's error stack.
The following code snippet shows an example of how to define validators together with custom
validation:<br />
Now that you know how to specify your business rules in your models, it is time to look at how to
deal with these rules in the rest of your application.<br />
<br />
Implicit validation:<br />
Whenever a record is going to be saved to the persistent data store (i.e. through calling $record->save())
the full validation procedure is executed. If errors occur during that process an exception of the type
Doctrine_Validator_Exception will be thrown. You can catch that exception and analyze the errors by
using the instance method Doctine_Validator_Exception::getInvalidRecords(). This method returns
an ordinary array with references to all records that did not pass validation. You can then
further explore the errors of each record by analyzing the error stack of each record.
The error stack of a record can be obtained with the instance method Doctrine_Record::getErrorStack().
Each error stack is an instance of the class Doctrine_Validator_ErrorStack. The error stack
provides an easy to use interface to inspect the errors.<br />
<br />
Explicit validation:<br />
You can explicitly trigger the validation for any record at any time. For this purpose Doctrine_Record
provides the instance method Doctrine_Record::isValid(). This method returns a boolean value indicating
the result of the validation. If the method returns FALSE, you can inspect the error stack in the same
way as seen above except that no exception is thrown, so you simply obtain
the error stack of the record that didnt pass validation through Doctrine_Record::getErrorStack().<br />
<br />
The following code snippet shows an example of handling implicit validation which caused a Doctrine_Validator_Exception.
When the validation attribute is set as true all transactions will be validated, so whenever Doctrine_Record::save(),
Doctrine_Connection::flush() or any other saving method is used all the properties of all records in that transaction will have their values
validated.
<br \><br \>
Validation errors are being stacked into Doctrine_Validator_Exception.
This diff is collapsed.
...@@ -94,10 +94,10 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase { ...@@ -94,10 +94,10 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase {
$this->assertTrue($stack instanceof Doctrine_Validator_ErrorStack); $this->assertTrue($stack instanceof Doctrine_Validator_ErrorStack);
$this->assertTrue(in_array(array('type' => 'notnull'), $stack['mystring'])); $this->assertTrue(in_array('notnull', $stack['mystring']));
$this->assertTrue(in_array(array('type' => 'notblank'), $stack['myemail2'])); $this->assertTrue(in_array('notblank', $stack['myemail2']));
$this->assertTrue(in_array(array('type' => 'range'), $stack['myrange'])); $this->assertTrue(in_array('range', $stack['myrange']));
$this->assertTrue(in_array(array('type' => 'regexp'), $stack['myregexp'])); $this->assertTrue(in_array('regexp', $stack['myregexp']));
$test->mystring = 'str'; $test->mystring = 'str';
...@@ -127,19 +127,19 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase { ...@@ -127,19 +127,19 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase {
$stack = $user->getErrorStack(); $stack = $user->getErrorStack();
$this->assertTrue($stack instanceof Doctrine_Validator_ErrorStack); $this->assertTrue($stack instanceof Doctrine_Validator_ErrorStack);
$this->assertTrue(in_array(array('type' => 'length'), $stack['loginname'])); $this->assertTrue(in_array('length', $stack['loginname']));
$this->assertTrue(in_array(array('type' => 'length'), $stack['password'])); $this->assertTrue(in_array('length', $stack['password']));
$this->assertTrue(in_array(array('type' => 'type'), $stack['created'])); $this->assertTrue(in_array('type', $stack['created']));
$validator->validateRecord($email); $validator->validateRecord($email);
$stack = $email->getErrorStack(); $stack = $email->getErrorStack();
$this->assertTrue(in_array(array('type' => 'email'), $stack['address'])); $this->assertTrue(in_array('email', $stack['address']));
$email->address = "arnold@example.com"; $email->address = "arnold@example.com";
$validator->validateRecord($email); $validator->validateRecord($email);
$stack = $email->getErrorStack(); $stack = $email->getErrorStack();
$this->assertTrue(in_array(array('type' => 'unique'), $stack['address'])); $this->assertTrue(in_array('unique', $stack['address']));
} }
/** /**
...@@ -177,7 +177,7 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase { ...@@ -177,7 +177,7 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase {
$invalidRecords = $e->getInvalidRecords(); $invalidRecords = $e->getInvalidRecords();
$this->assertEqual(count($invalidRecords), 1); $this->assertEqual(count($invalidRecords), 1);
$stack = $invalidRecords[0]->getErrorStack(); $stack = $invalidRecords[0]->getErrorStack();
$this->assertTrue(in_array(array('type' => 'length'), $stack['name'])); $this->assertTrue(in_array('length', $stack['name']));
} }
try { try {
...@@ -196,8 +196,8 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase { ...@@ -196,8 +196,8 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase {
$emailStack = $a[array_search($user->Email, $a)]->getErrorStack(); $emailStack = $a[array_search($user->Email, $a)]->getErrorStack();
$userStack = $a[array_search($user, $a)]->getErrorStack(); $userStack = $a[array_search($user, $a)]->getErrorStack();
$this->assertTrue(in_array(array('type' => 'email'), $emailStack['address'])); $this->assertTrue(in_array('email', $emailStack['address']));
$this->assertTrue(in_array(array('type' => 'length'), $userStack['name'])); $this->assertTrue(in_array('length', $userStack['name']));
$this->manager->setAttribute(Doctrine::ATTR_VLD, false); $this->manager->setAttribute(Doctrine::ATTR_VLD, false);
} }
...@@ -219,8 +219,16 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase { ...@@ -219,8 +219,16 @@ class Doctrine_ValidatorTestCase extends Doctrine_UnitTestCase {
$stack = $invalidRecords[0]->getErrorStack(); $stack = $invalidRecords[0]->getErrorStack();
$this->assertEqual($stack->count(), 1); $this->assertEqual($stack->count(), 1);
$this->assertTrue(in_array(array('type' => 'notTheSaint'), $stack['name'])); $this->assertTrue(in_array('notTheSaint', $stack['name']));
} }
try {
$user->name = "The Saint";
$user->save();
} catch(Doctrine_Validator_Exception $e) {
$this->fail();
}
$this->manager->setAttribute(Doctrine::ATTR_VLD, false); $this->manager->setAttribute(Doctrine::ATTR_VLD, false);
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment