From ca161b983640d84ef9fa806bf4742d597e5f97a0 Mon Sep 17 00:00:00 2001 From: bexarcreative-daniel <daniel@bexarcreative.com> Date: Thu, 26 Dec 2013 09:55:31 -0600 Subject: [PATCH 1/8] Added hasManyThrough relationship type from Laravel 4.1. Made all named indexes consistent with Laravel 4.1 on Ardent relationship definitions. Revised documentation to be more readible when it comes to relationship key requirements. --- README.md | 40 ++++++++++++++----- src/LaravelBook/Ardent/Ardent.php | 64 +++++++++++++++++++++---------- 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 60c5fa5..aa60e7c 100644 --- a/README.md +++ b/README.md @@ -300,7 +300,7 @@ $user->save(array(), array(), array(), Have you ever written an Eloquent model with a bunch of relations, just to notice how cluttered your class is, with all those one-liners that have almost the same content as the method name itself? -In Ardent you can cleanly define your relationships in an array with their information, and they will work just like if you had defined they in methods. Here's an example: +In Ardent you can cleanly define your relationships in an array with their information, and they will work just as if you had defined them as methods. Here's an example: ```php class User extends \LaravelBook\Ardent\Ardent { @@ -315,22 +315,44 @@ $user = User::find($id); echo "{$user->address->street}, {$user->address->city} - {$user->address->state}"; ``` -The array syntax is as follows: +The array syntax should follow the form of 1 to 3 unnamed (numeric) index values followed by optional (and sometimes required) named index values. See the following for complete desciption: -- First indexed value: relation name, being one of +**First index value** + +This value should be the relation name, being one of [`hasOne`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_hasOne), [`hasMany`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_hasMany), +[`hasManyThrough`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_hasManyThrough), [`belongsTo`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_belongsTo), [`belongsToMany`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_belongsToMany), [`morphTo`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_morphTo), [`morphOne`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_morphOne), [`morphMany`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_morphMany), -or one of the related constants (`Ardent::HAS_MANY` or `Ardent::MORPH_ONE` for example). -- Second indexed: class name, with complete namespace. The exception is `morphTo` relations, that take no additional argument. -- named arguments, following the ones defined for the original Eloquent methods: - - `foreignKey` [optional], valid for `hasOne`, `hasMany`, `belongsTo` and `belongsToMany` - - `table`,`otherKey` [optional],`timestamps` [boolean, optional], and `pivotKeys` [array, optional], valid for `belongsToMany` - - `name`, `type` and `id`, used by `morphTo`, `morphOne` and `morphMany` (the last two requires `name` to be defined) +or one of the related constants (`Ardent::HAS_MANY`, `Ardent::MORPH_ONE`, etc.). + +**Second index value** + +This value should be the related model class name, _with complete namespace_. The `morphTo` relationship does not require a second indexed value and will throw an exception if provided. + +**Third index value** + +This value should be the related "through" model class name, _with complete namespace_. This index is only required by the `hasManyThrough` relationship type. + +**Named indexes** + +Following the first, second, and third numeric indexes are named index values from the original Eloquent methods: + +- `foreignKey`: optional for `hasOne`, `hasMany`, `belongsTo`, `belongsToMany` +- `firstKey`: optional for `hasManyThrough` +- `secondKey`: optional for `hasManyThrough` +- `otherKey`: optional for `belongsTo`, `belongsToMany` +- `localKey`: optional for `hasOne`, `hasMany`, `morphOne`, `morphMany` +- `table`: optional for `belongsToMany` +- `timestamps`: optional for `belongsToMany` +- `pivotKeys`: optional for `belongsToMany` +- `name`: optional for `morphTo` and required for `morphOne`, `morphMany` +- `type`: optional for `morphTo`, `morphOne`, `morphMany` +- `id`: optional for `morphTo`, `morphOne`, `morphMany` > **Note:** This feature was based on the easy [relations on Yii 1.1 ActiveRecord](http://www.yiiframework.com/doc/guide/1.1/en/database.arr#declaring-relationship). diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php index 52b5613..b6c8e9a 100644 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -158,6 +158,7 @@ abstract class Ardent extends Model { * * @see \Illuminate\Database\Eloquent\Model::hasOne * @see \Illuminate\Database\Eloquent\Model::hasMany + * @see \Illuminate\Database\Eloquent\Model::hasManyThrough * @see \Illuminate\Database\Eloquent\Model::belongsTo * @see \Illuminate\Database\Eloquent\Model::belongsToMany * @see \Illuminate\Database\Eloquent\Model::morphTo @@ -172,6 +173,8 @@ abstract class Ardent extends Model { const HAS_MANY = 'hasMany'; + const HAS_MANY_THROUGH = 'hasManyThrough'; + const BELONGS_TO = 'belongsTo'; const BELONGS_TO_MANY = 'belongsToMany'; @@ -188,7 +191,7 @@ abstract class Ardent extends Model { * @var array */ protected static $relationTypes = array( - self::HAS_ONE, self::HAS_MANY, + self::HAS_ONE, self::HAS_MANY, self::HAS_MANY_THROUGH, self::BELONGS_TO, self::BELONGS_TO_MANY, self::MORPH_TO, self::MORPH_ONE, self::MORPH_MANY ); @@ -269,16 +272,24 @@ protected function handleRelationalArray($relationName) { if (!in_array($relationType, static::$relationTypes)) { throw new \InvalidArgumentException($errorHeader. - ' should have as first param one of the relation constants of the Ardent class.'); + ' should have as first parameter one of the relation constants of the Ardent class.'); } if (!isset($relation[1]) && $relationType != self::MORPH_TO) { throw new \InvalidArgumentException($errorHeader. - ' should have at least two params: relation type and classname.'); + ' should have at least two parameters: relation type and classname.'); } if (isset($relation[1]) && $relationType == self::MORPH_TO) { throw new \InvalidArgumentException($errorHeader. ' is a morphTo relation and should not contain additional arguments.'); } + if (isset($relation[2]) && $relationType != self::HAS_MANY_THROUGH) { + throw new \InvalidArgumentException($errorHeader. + ' is not a hasManyThrough relation and should not contain additional arguments.'); + } + if (!isset($relation[2]) && $relationType == self::HAS_MANY_THROUGH) { + throw new \InvalidArgumentException($errorHeader. + ' is a hasManyThrough relation and should have atleast three parameters: relation type, related model classname and through model classname.'); + } $verifyArgs = function (array $opt, array $req = array()) use ($relationName, &$relation, $errorHeader) { $missing = array('req' => array(), 'opt' => array()); @@ -305,12 +316,19 @@ protected function handleRelationalArray($relationName) { switch ($relationType) { case self::HAS_ONE: case self::HAS_MANY: + $verifyArgs(array('foreignKey', 'localKey')); + return $this->$relationType($relation[1], $relation['foreignKey'], $relation['localKey']); + + case self::HAS_MANY_THROUGH: + $verifyArgs(array('firstKey', 'secondKey')); + return $this->$relationType($relation[1], $relation[2], $relation['firstKey'], $relation['secondKey']); + case self::BELONGS_TO: - $verifyArgs(array('foreignKey')); - return $this->$relationType($relation[1], $relation['foreignKey']); + $verifyArgs(array('foreignKey', 'otherKey')); + return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey']); case self::BELONGS_TO_MANY: - $verifyArgs(array('table', 'foreignKey', 'otherKey')); + $verifyArgs(array('table', 'foreignKey', 'otherKey', 'pivoteKeys', 'timestamps')); $relationship = $this->$relationType($relation[1], $relation['table'], $relation['foreignKey'], $relation['otherKey']); if(isset($relation['pivotKeys']) && is_array($relation['pivotKeys'])) $relationship->withPivot($relation['pivotKeys']); @@ -324,7 +342,7 @@ protected function handleRelationalArray($relationName) { case self::MORPH_ONE: case self::MORPH_MANY: - $verifyArgs(array('type', 'id'), array('name')); + $verifyArgs(array('type', 'id', 'localKey'), array('name')); return $this->$relationType($relation[1], $relation['name'], $relation['type'], $relation['id']); } } @@ -348,36 +366,40 @@ public function __call($method, $parameters) { /** * Define an inverse one-to-one or many relationship. - * Overriden from {@link Eloquent\Model} to allow the usage of the intermediary methods to handle the {@link - * $relationsData} array. + * Overriden from {@link Eloquent\Model} to allow the usage + * of the intermediary methods to handle the {@link $relationsData} array. * * @param string $related * @param string $foreignKey * @param string $otherKey + * @param string $relation * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function belongsTo($related, $foreignKey = NULL, $otherKey = NULL, $relation = NULL) { - $backtrace = debug_backtrace(false); + + // If no relation name was given, we will use this debug backtrace to extract + // the calling method's name and use that as the relationship name as most + // of the time this will be what we desire to use for the relatinoships. + $backtrace = debug_backtrace(false); $caller = ($backtrace[1]['function'] == 'handleRelationalArray')? $backtrace[3] : $backtrace[1]; - - // If no foreign key was supplied, we can use a backtrace to guess the proper - // foreign key name by using the name of the relationship function, which - // when combined with an "_id" should conventionally match the columns. $relation = $caller['function']; + // If no foreign key was supplied, we can use a backtrace to guess the proper + // foreign key name by using the name of the relationship function, which + // when combined with an "_id" should conventionally match the columns. if (is_null($foreignKey)) { $foreignKey = snake_case($relation).'_id'; } - // Once we have the foreign key names, we'll just create a new Eloquent query - // for the related models and returns the relationship instance which will - // actually be responsible for retrieving and hydrating every relations. $instance = new $related; - - $otherKey = $otherKey ?: $instance->getKeyName(); - + + // Once we have the foreign key names, we'll just create a new Eloquent query + // for the related models and returns the relationship instance which will + // actually be responsible for retrieving and hydrating every relations. $query = $instance->newQuery(); + $otherKey = $otherKey ?: $instance->getKeyName(); + return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); } @@ -555,7 +577,7 @@ public function validate(array $rules = array(), array $customMessages = array() * @param Closure $beforeSave * @param Closure $afterSave * @param bool $force Forces saving invalid data. - + * * @return bool * @see Ardent::save() * @see Ardent::forceSave() From 6364e1eb3804becb15051e79bfef20b365590a23 Mon Sep 17 00:00:00 2001 From: Daniel LaBarge <daniel@bexarcreative.com> Date: Sun, 9 Feb 2014 20:12:39 -0600 Subject: [PATCH 2/8] Fixed typo. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa60e7c..cda7daf 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ $user = User::find($id); echo "{$user->address->street}, {$user->address->city} - {$user->address->state}"; ``` -The array syntax should follow the form of 1 to 3 unnamed (numeric) index values followed by optional (and sometimes required) named index values. See the following for complete desciption: +The array syntax should follow the form of 1 to 3 unnamed (numeric) index values followed by optional (and sometimes required) named index values. See the following for complete description: **First index value** From 02f8533214fe5d056327e1b9e4ea218fa6c1ad71 Mon Sep 17 00:00:00 2001 From: Daniel LaBarge <daniel@bexarcreative.com> Date: Sun, 9 Feb 2014 20:16:30 -0600 Subject: [PATCH 3/8] Converted spaces to tabs. --- src/LaravelBook/Ardent/Ardent.php | 1440 ++++++++++++++--------------- 1 file changed, 704 insertions(+), 736 deletions(-) mode change 100644 => 100755 src/LaravelBook/Ardent/Ardent.php diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php old mode 100644 new mode 100755 index b6c8e9a..fc0f4b3 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -31,209 +31,209 @@ */ abstract class Ardent extends Model { - /** - * The rules to be applied to the data. - * - * @var array - */ - public static $rules = array(); - - /** - * The array of custom error messages. - * - * @var array - */ - public static $customMessages = array(); - - /** - * The message bag instance containing validation error messages - * - * @var \Illuminate\Support\MessageBag - */ - public $validationErrors; - - /** - * Makes the validation procedure throw an {@link InvalidModelException} instead of returning - * false when validation fails. - * - * @var bool - */ - public $throwOnValidation = false; - - /** - * Forces the behavior of findOrFail in very find method - throwing a {@link ModelNotFoundException} - * when the model is not found. - * - * @var bool - */ - public static $throwOnFind = false; - - /** - * If set to true, the object will automatically populate model attributes from Input::all() - * - * @var bool - */ - public $autoHydrateEntityFromInput = false; - - /** - * By default, Ardent will attempt hydration only if the model object contains no attributes and - * the $autoHydrateEntityFromInput property is set to true. - * Setting $forceEntityHydrationFromInput to true will bypass the above check and enforce - * hydration of model attributes. - * - * @var bool - */ - public $forceEntityHydrationFromInput = false; - - /** - * If set to true, the object will automatically remove redundant model - * attributes (i.e. confirmation fields). - * - * @var bool - */ - public $autoPurgeRedundantAttributes = false; - - /** - * Array of closure functions which determine if a given attribute is deemed - * redundant (and should not be persisted in the database) - * - * @var array - */ - protected $purgeFilters = array(); - - protected $purgeFiltersInitialized = false; - - /** - * List of attribute names which should be hashed using the Bcrypt hashing algorithm. - * - * @var array - */ - public static $passwordAttributes = array(); - - /** - * If set to true, the model will automatically replace all plain-text passwords - * attributes (listed in $passwordAttributes) with hash checksums - * - * @var bool - */ - public $autoHashPasswordAttributes = false; - - /** - * If set to true will try to instantiate the validator as if it was outside Laravel. - * - * @var bool - */ - protected static $externalValidator = false; - - /** - * A Translator instance, to be used by standalone Ardent instances. - * - * @var \Illuminate\Validation\Factory - */ - protected static $validationFactory; - - /** - * Can be used to ease declaration of relationships in Ardent models. - * Follows closely the behavior of the relation methods used by Eloquent, but packing them into an indexed array - * with relation constants make the code less cluttered. - * - * It should be declared with camel-cased keys as the relation name, and value being a mixed array with the - * relation constant being the first (0) value, the second (1) being the classname and the next ones (optionals) - * having named keys indicating the other arguments of the original methods: 'foreignKey' (belongsTo, hasOne, - * belongsToMany and hasMany); 'table' and 'otherKey' (belongsToMany only); 'name', 'type' and 'id' (specific for - * morphTo, morphOne and morphMany). - * Exceptionally, the relation type MORPH_TO does not include a classname, following the method declaration of - * {@link \Illuminate\Database\Eloquent\Model::morphTo}. - * - * Example: - * <code> - * class Order extends Ardent { - * protected static $relations = array( - * 'items' => array(self::HAS_MANY, 'Item'), - * 'owner' => array(self::HAS_ONE, 'User', 'foreignKey' => 'user_id'), - * 'pictures' => array(self::MORPH_MANY, 'Picture', 'name' => 'imageable') - * ); - * } - * </code> - * - * @see \Illuminate\Database\Eloquent\Model::hasOne - * @see \Illuminate\Database\Eloquent\Model::hasMany - * @see \Illuminate\Database\Eloquent\Model::hasManyThrough - * @see \Illuminate\Database\Eloquent\Model::belongsTo - * @see \Illuminate\Database\Eloquent\Model::belongsToMany - * @see \Illuminate\Database\Eloquent\Model::morphTo - * @see \Illuminate\Database\Eloquent\Model::morphOne - * @see \Illuminate\Database\Eloquent\Model::morphMany - * - * @var array - */ - protected static $relationsData = array(); - - const HAS_ONE = 'hasOne'; - - const HAS_MANY = 'hasMany'; - - const HAS_MANY_THROUGH = 'hasManyThrough'; - - const BELONGS_TO = 'belongsTo'; - - const BELONGS_TO_MANY = 'belongsToMany'; - - const MORPH_TO = 'morphTo'; - - const MORPH_ONE = 'morphOne'; - - const MORPH_MANY = 'morphMany'; - - /** - * Array of relations used to verify arguments used in the {@link $relationsData} - * - * @var array - */ - protected static $relationTypes = array( - self::HAS_ONE, self::HAS_MANY, self::HAS_MANY_THROUGH, - self::BELONGS_TO, self::BELONGS_TO_MANY, - self::MORPH_TO, self::MORPH_ONE, self::MORPH_MANY - ); - - /** - * Create a new Ardent model instance. - * - * @param array $attributes - * @return \LaravelBook\Ardent\Ardent - */ - public function __construct(array $attributes = array()) { - - parent::__construct($attributes); - $this->validationErrors = new MessageBag; - } - - /** - * The "booting" method of the model. - * Overrided to attach before/after method hooks into the model events. - * - * @see \Illuminate\Database\Eloquent\Model::boot() - * @return void - */ - public static function boot() { - parent::boot(); - - $myself = get_called_class(); - $hooks = array('before' => 'ing', 'after' => 'ed'); - $radicals = array('sav', 'validat', 'creat', 'updat', 'delet'); - - foreach ($radicals as $rad) { - foreach ($hooks as $hook => $event) { - $method = $hook.ucfirst($rad).'e'; - if (method_exists($myself, $method)) { - $eventMethod = $rad.$event; - self::$eventMethod(function($model) use ($method){ - return $model->$method($model); - }); - } - } - } - } + /** + * The rules to be applied to the data. + * + * @var array + */ + public static $rules = array(); + + /** + * The array of custom error messages. + * + * @var array + */ + public static $customMessages = array(); + + /** + * The message bag instance containing validation error messages + * + * @var \Illuminate\Support\MessageBag + */ + public $validationErrors; + + /** + * Makes the validation procedure throw an {@link InvalidModelException} instead of returning + * false when validation fails. + * + * @var bool + */ + public $throwOnValidation = false; + + /** + * Forces the behavior of findOrFail in very find method - throwing a {@link ModelNotFoundException} + * when the model is not found. + * + * @var bool + */ + public static $throwOnFind = false; + + /** + * If set to true, the object will automatically populate model attributes from Input::all() + * + * @var bool + */ + public $autoHydrateEntityFromInput = false; + + /** + * By default, Ardent will attempt hydration only if the model object contains no attributes and + * the $autoHydrateEntityFromInput property is set to true. + * Setting $forceEntityHydrationFromInput to true will bypass the above check and enforce + * hydration of model attributes. + * + * @var bool + */ + public $forceEntityHydrationFromInput = false; + + /** + * If set to true, the object will automatically remove redundant model + * attributes (i.e. confirmation fields). + * + * @var bool + */ + public $autoPurgeRedundantAttributes = false; + + /** + * Array of closure functions which determine if a given attribute is deemed + * redundant (and should not be persisted in the database) + * + * @var array + */ + protected $purgeFilters = array(); + + protected $purgeFiltersInitialized = false; + + /** + * List of attribute names which should be hashed using the Bcrypt hashing algorithm. + * + * @var array + */ + public static $passwordAttributes = array(); + + /** + * If set to true, the model will automatically replace all plain-text passwords + * attributes (listed in $passwordAttributes) with hash checksums + * + * @var bool + */ + public $autoHashPasswordAttributes = false; + + /** + * If set to true will try to instantiate the validator as if it was outside Laravel. + * + * @var bool + */ + protected static $externalValidator = false; + + /** + * A Translator instance, to be used by standalone Ardent instances. + * + * @var \Illuminate\Validation\Factory + */ + protected static $validationFactory; + + /** + * Can be used to ease declaration of relationships in Ardent models. + * Follows closely the behavior of the relation methods used by Eloquent, but packing them into an indexed array + * with relation constants make the code less cluttered. + * + * It should be declared with camel-cased keys as the relation name, and value being a mixed array with the + * relation constant being the first (0) value, the second (1) being the classname and the next ones (optionals) + * having named keys indicating the other arguments of the original methods: 'foreignKey' (belongsTo, hasOne, + * belongsToMany and hasMany); 'table' and 'otherKey' (belongsToMany only); 'name', 'type' and 'id' (specific for + * morphTo, morphOne and morphMany). + * Exceptionally, the relation type MORPH_TO does not include a classname, following the method declaration of + * {@link \Illuminate\Database\Eloquent\Model::morphTo}. + * + * Example: + * <code> + * class Order extends Ardent { + * protected static $relations = array( + * 'items' => array(self::HAS_MANY, 'Item'), + * 'owner' => array(self::HAS_ONE, 'User', 'foreignKey' => 'user_id'), + * 'pictures' => array(self::MORPH_MANY, 'Picture', 'name' => 'imageable') + * ); + * } + * </code> + * + * @see \Illuminate\Database\Eloquent\Model::hasOne + * @see \Illuminate\Database\Eloquent\Model::hasMany + * @see \Illuminate\Database\Eloquent\Model::hasManyThrough + * @see \Illuminate\Database\Eloquent\Model::belongsTo + * @see \Illuminate\Database\Eloquent\Model::belongsToMany + * @see \Illuminate\Database\Eloquent\Model::morphTo + * @see \Illuminate\Database\Eloquent\Model::morphOne + * @see \Illuminate\Database\Eloquent\Model::morphMany + * + * @var array + */ + protected static $relationsData = array(); + + const HAS_ONE = 'hasOne'; + + const HAS_MANY = 'hasMany'; + + const HAS_MANY_THROUGH = 'hasManyThrough'; + + const BELONGS_TO = 'belongsTo'; + + const BELONGS_TO_MANY = 'belongsToMany'; + + const MORPH_TO = 'morphTo'; + + const MORPH_ONE = 'morphOne'; + + const MORPH_MANY = 'morphMany'; + + /** + * Array of relations used to verify arguments used in the {@link $relationsData} + * + * @var array + */ + protected static $relationTypes = array( + self::HAS_ONE, self::HAS_MANY, self::HAS_MANY_THROUGH, + self::BELONGS_TO, self::BELONGS_TO_MANY, + self::MORPH_TO, self::MORPH_ONE, self::MORPH_MANY + ); + + /** + * Create a new Ardent model instance. + * + * @param array $attributes + * @return \LaravelBook\Ardent\Ardent + */ + public function __construct(array $attributes = array()) { + + parent::__construct($attributes); + $this->validationErrors = new MessageBag; + } + + /** + * The "booting" method of the model. + * Overrided to attach before/after method hooks into the model events. + * + * @see \Illuminate\Database\Eloquent\Model::boot() + * @return void + */ + public static function boot() { + parent::boot(); + + $myself = get_called_class(); + $hooks = array('before' => 'ing', 'after' => 'ed'); + $radicals = array('sav', 'validat', 'creat', 'updat', 'delet'); + + foreach ($radicals as $rad) { + foreach ($hooks as $hook => $event) { + $method = $hook.ucfirst($rad).'e'; + if (method_exists($myself, $method)) { + $eventMethod = $rad.$event; + self::$eventMethod(function($model) use ($method){ + return $model->$method($model); + }); + } + } + } + } /** * Register a validating model event with the dispatcher. @@ -255,150 +255,150 @@ public static function validated($callback) { static::registerModelEvent('validated', $callback); } - /** - * Looks for the relation in the {@link $relationsData} array and does the correct magic as Eloquent would require - * inside relation methods. For more information, read the documentation of the mentioned property. - * - * @param string $relationName the relation key, camel-case version - * @return \Illuminate\Database\Eloquent\Relations\Relation - * @throws \InvalidArgumentException when the first param of the relation is not a relation type constant, - * or there's one or more arguments missing - * @see Ardent::relationsData - */ - protected function handleRelationalArray($relationName) { - $relation = static::$relationsData[$relationName]; - $relationType = $relation[0]; - $errorHeader = "Relation '$relationName' on model '".get_called_class(); - - if (!in_array($relationType, static::$relationTypes)) { - throw new \InvalidArgumentException($errorHeader. - ' should have as first parameter one of the relation constants of the Ardent class.'); - } - if (!isset($relation[1]) && $relationType != self::MORPH_TO) { - throw new \InvalidArgumentException($errorHeader. - ' should have at least two parameters: relation type and classname.'); - } - if (isset($relation[1]) && $relationType == self::MORPH_TO) { - throw new \InvalidArgumentException($errorHeader. - ' is a morphTo relation and should not contain additional arguments.'); - } - if (isset($relation[2]) && $relationType != self::HAS_MANY_THROUGH) { - throw new \InvalidArgumentException($errorHeader. - ' is not a hasManyThrough relation and should not contain additional arguments.'); - } - if (!isset($relation[2]) && $relationType == self::HAS_MANY_THROUGH) { - throw new \InvalidArgumentException($errorHeader. - ' is a hasManyThrough relation and should have atleast three parameters: relation type, related model classname and through model classname.'); - } - - $verifyArgs = function (array $opt, array $req = array()) use ($relationName, &$relation, $errorHeader) { - $missing = array('req' => array(), 'opt' => array()); - - foreach (array('req', 'opt') as $keyType) { - foreach ($$keyType as $key) { - if (!array_key_exists($key, $relation)) { - $missing[$keyType][] = $key; - } - } - } - - if ($missing['req']) { - throw new \InvalidArgumentException($errorHeader.' - should contain the following key(s): '.join(', ', $missing['req'])); - } - if ($missing['opt']) { - foreach ($missing['opt'] as $include) { - $relation[$include] = null; - } - } - }; - - switch ($relationType) { - case self::HAS_ONE: - case self::HAS_MANY: - $verifyArgs(array('foreignKey', 'localKey')); - return $this->$relationType($relation[1], $relation['foreignKey'], $relation['localKey']); - - case self::HAS_MANY_THROUGH: - $verifyArgs(array('firstKey', 'secondKey')); - return $this->$relationType($relation[1], $relation[2], $relation['firstKey'], $relation['secondKey']); - - case self::BELONGS_TO: - $verifyArgs(array('foreignKey', 'otherKey')); - return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey']); - - case self::BELONGS_TO_MANY: - $verifyArgs(array('table', 'foreignKey', 'otherKey', 'pivoteKeys', 'timestamps')); - $relationship = $this->$relationType($relation[1], $relation['table'], $relation['foreignKey'], $relation['otherKey']); - if(isset($relation['pivotKeys']) && is_array($relation['pivotKeys'])) - $relationship->withPivot($relation['pivotKeys']); - if(isset($relation['timestamps']) && $relation['timestamps']==true) - $relationship->withTimestamps(); - return $relationship; - - case self::MORPH_TO: - $verifyArgs(array('name', 'type', 'id')); - return $this->$relationType($relation['name'], $relation['type'], $relation['id']); - - case self::MORPH_ONE: - case self::MORPH_MANY: - $verifyArgs(array('type', 'id', 'localKey'), array('name')); - return $this->$relationType($relation[1], $relation['name'], $relation['type'], $relation['id']); - } - } - - /** - * Handle dynamic method calls into the method. - * Overrided from {@link Eloquent} to implement recognition of the {@link $relationsData} array. - * - * @param string $method - * @param array $parameters - * @return mixed - */ - public function __call($method, $parameters) { - if (array_key_exists($method, static::$relationsData)) { - return $this->handleRelationalArray($method); - } - - return parent::__call($method, $parameters); - } + /** + * Looks for the relation in the {@link $relationsData} array and does the correct magic as Eloquent would require + * inside relation methods. For more information, read the documentation of the mentioned property. + * + * @param string $relationName the relation key, camel-case version + * @return \Illuminate\Database\Eloquent\Relations\Relation + * @throws \InvalidArgumentException when the first param of the relation is not a relation type constant, + * or there's one or more arguments missing + * @see Ardent::relationsData + */ + protected function handleRelationalArray($relationName) { + $relation = static::$relationsData[$relationName]; + $relationType = $relation[0]; + $errorHeader = "Relation '$relationName' on model '".get_called_class(); + + if (!in_array($relationType, static::$relationTypes)) { + throw new \InvalidArgumentException($errorHeader. + ' should have as first parameter one of the relation constants of the Ardent class.'); + } + if (!isset($relation[1]) && $relationType != self::MORPH_TO) { + throw new \InvalidArgumentException($errorHeader. + ' should have at least two parameters: relation type and classname.'); + } + if (isset($relation[1]) && $relationType == self::MORPH_TO) { + throw new \InvalidArgumentException($errorHeader. + ' is a morphTo relation and should not contain additional arguments.'); + } + if (isset($relation[2]) && $relationType != self::HAS_MANY_THROUGH) { + throw new \InvalidArgumentException($errorHeader. + ' is not a hasManyThrough relation and should not contain additional arguments.'); + } + if (!isset($relation[2]) && $relationType == self::HAS_MANY_THROUGH) { + throw new \InvalidArgumentException($errorHeader. + ' is a hasManyThrough relation and should have atleast three parameters: relation type, related model classname and through model classname.'); + } + + $verifyArgs = function (array $opt, array $req = array()) use ($relationName, &$relation, $errorHeader) { + $missing = array('req' => array(), 'opt' => array()); + + foreach (array('req', 'opt') as $keyType) { + foreach ($$keyType as $key) { + if (!array_key_exists($key, $relation)) { + $missing[$keyType][] = $key; + } + } + } + + if ($missing['req']) { + throw new \InvalidArgumentException($errorHeader.' + should contain the following key(s): '.join(', ', $missing['req'])); + } + if ($missing['opt']) { + foreach ($missing['opt'] as $include) { + $relation[$include] = null; + } + } + }; + + switch ($relationType) { + case self::HAS_ONE: + case self::HAS_MANY: + $verifyArgs(array('foreignKey', 'localKey')); + return $this->$relationType($relation[1], $relation['foreignKey'], $relation['localKey']); + + case self::HAS_MANY_THROUGH: + $verifyArgs(array('firstKey', 'secondKey')); + return $this->$relationType($relation[1], $relation[2], $relation['firstKey'], $relation['secondKey']); + + case self::BELONGS_TO: + $verifyArgs(array('foreignKey', 'otherKey')); + return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey']); + + case self::BELONGS_TO_MANY: + $verifyArgs(array('table', 'foreignKey', 'otherKey', 'pivoteKeys', 'timestamps')); + $relationship = $this->$relationType($relation[1], $relation['table'], $relation['foreignKey'], $relation['otherKey']); + if(isset($relation['pivotKeys']) && is_array($relation['pivotKeys'])) + $relationship->withPivot($relation['pivotKeys']); + if(isset($relation['timestamps']) && $relation['timestamps']==true) + $relationship->withTimestamps(); + return $relationship; + + case self::MORPH_TO: + $verifyArgs(array('name', 'type', 'id')); + return $this->$relationType($relation['name'], $relation['type'], $relation['id']); + + case self::MORPH_ONE: + case self::MORPH_MANY: + $verifyArgs(array('type', 'id', 'localKey'), array('name')); + return $this->$relationType($relation[1], $relation['name'], $relation['type'], $relation['id']); + } + } + + /** + * Handle dynamic method calls into the method. + * Overrided from {@link Eloquent} to implement recognition of the {@link $relationsData} array. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) { + if (array_key_exists($method, static::$relationsData)) { + return $this->handleRelationalArray($method); + } + + return parent::__call($method, $parameters); + } /** * Define an inverse one-to-one or many relationship. * Overriden from {@link Eloquent\Model} to allow the usage - * of the intermediary methods to handle the {@link $relationsData} array. + * of the intermediary methods to handle the {@link $relationsData} array. * * @param string $related * @param string $foreignKey * @param string $otherKey - * @param string $relation + * @param string $relation * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function belongsTo($related, $foreignKey = NULL, $otherKey = NULL, $relation = NULL) { - // If no relation name was given, we will use this debug backtrace to extract - // the calling method's name and use that as the relationship name as most - // of the time this will be what we desire to use for the relatinoships. - $backtrace = debug_backtrace(false); + // If no relation name was given, we will use this debug backtrace to extract + // the calling method's name and use that as the relationship name as most + // of the time this will be what we desire to use for the relatinoships. + $backtrace = debug_backtrace(false); $caller = ($backtrace[1]['function'] == 'handleRelationalArray')? $backtrace[3] : $backtrace[1]; $relation = $caller['function']; - // If no foreign key was supplied, we can use a backtrace to guess the proper - // foreign key name by using the name of the relationship function, which - // when combined with an "_id" should conventionally match the columns. + // If no foreign key was supplied, we can use a backtrace to guess the proper + // foreign key name by using the name of the relationship function, which + // when combined with an "_id" should conventionally match the columns. if (is_null($foreignKey)) { $foreignKey = snake_case($relation).'_id'; } $instance = new $related; - // Once we have the foreign key names, we'll just create a new Eloquent query - // for the related models and returns the relationship instance which will - // actually be responsible for retrieving and hydrating every relations. + // Once we have the foreign key names, we'll just create a new Eloquent query + // for the related models and returns the relationship instance which will + // actually be responsible for retrieving and hydrating every relations. $query = $instance->newQuery(); - $otherKey = $otherKey ?: $instance->getKeyName(); + $otherKey = $otherKey ?: $instance->getKeyName(); return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); } @@ -435,102 +435,102 @@ public function morphTo($name = null, $type = null, $id = null) { return $this->belongsTo($class, $id); } - /** - * Get an attribute from the model. - * Overrided from {@link Eloquent} to implement recognition of the {@link $relationsData} array. - * - * @param string $key - * @return mixed - */ - public function getAttribute($key) { - $attr = parent::getAttribute($key); - - if ($attr === null) { - $camelKey = camel_case($key); - if (array_key_exists($camelKey, static::$relationsData)) { - $this->relations[$key] = $this->$camelKey()->getResults(); - return $this->relations[$key]; - } - } - - return $attr; - } - - /** - * Configures Ardent to be used outside of Laravel - correctly setting Eloquent and Validation modules. - * @todo Should allow for additional language files. Would probably receive a Translator instance as an optional argument, or a list of translation files. - * - * @param array $connection Connection info used by {@link \Illuminate\Database\Capsule\Manager::addConnection}. - * Should contain driver, host, port, database, username, password, charset and collation. - */ - public static function configureAsExternal(array $connection) { - $db = new DatabaseCapsule; - $db->addConnection($connection); - $db->setEventDispatcher(new Dispatcher(new Container)); - //TODO: configure a cache manager (as an option) - - // Make this Capsule instance available globally via static methods - $db->setAsGlobal(); - - $db->bootEloquent(); - - $translator = new Translator('en'); - $translator->addLoader('file_loader', new PhpFileLoader()); - $translator->addResource('file_loader', - dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR.'en'. - DIRECTORY_SEPARATOR.'validation.php', 'en'); - - self::$externalValidator = true; - self::$validationFactory = new ValidationFactory($translator); - self::$validationFactory->setPresenceVerifier(new DatabasePresenceVerifier($db->getDatabaseManager())); - } - - /** - * Instatiates the validator used by the validation process, depending if the class is being used inside or - * outside of Laravel. - * - * @param $data - * @param $rules - * @param $customMessages - * @return \Illuminate\Validation\Validator - * @see Ardent::$externalValidator - */ - protected static function makeValidator($data, $rules, $customMessages) { - if (self::$externalValidator) { - return self::$validationFactory->make($data, $rules, $customMessages); - } else { - return Validator::make($data, $rules, $customMessages); - } - } - - /** - * Validate the model instance - * - * @param array $rules Validation rules - * @param array $customMessages Custom error messages - * @return bool - * @throws InvalidModelException - */ - public function validate(array $rules = array(), array $customMessages = array()) { - if ($this->fireModelEvent('validating') === false) { - if ($this->throwOnValidation) { - throw new InvalidModelException($this); - } else { - return false; - } - } - - // check for overrides, then remove any empty rules - $rules = (empty($rules))? static::$rules : $rules; - foreach ($rules as $field => $rls) { - if ($rls == '') { - unset($rules[$field]); - } - } - - if (empty($rules)) { - $success = true; - } else { + /** + * Get an attribute from the model. + * Overrided from {@link Eloquent} to implement recognition of the {@link $relationsData} array. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) { + $attr = parent::getAttribute($key); + + if ($attr === null) { + $camelKey = camel_case($key); + if (array_key_exists($camelKey, static::$relationsData)) { + $this->relations[$key] = $this->$camelKey()->getResults(); + return $this->relations[$key]; + } + } + + return $attr; + } + + /** + * Configures Ardent to be used outside of Laravel - correctly setting Eloquent and Validation modules. + * @todo Should allow for additional language files. Would probably receive a Translator instance as an optional argument, or a list of translation files. + * + * @param array $connection Connection info used by {@link \Illuminate\Database\Capsule\Manager::addConnection}. + * Should contain driver, host, port, database, username, password, charset and collation. + */ + public static function configureAsExternal(array $connection) { + $db = new DatabaseCapsule; + $db->addConnection($connection); + $db->setEventDispatcher(new Dispatcher(new Container)); + //TODO: configure a cache manager (as an option) + + // Make this Capsule instance available globally via static methods + $db->setAsGlobal(); + + $db->bootEloquent(); + + $translator = new Translator('en'); + $translator->addLoader('file_loader', new PhpFileLoader()); + $translator->addResource('file_loader', + dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR.'en'. + DIRECTORY_SEPARATOR.'validation.php', 'en'); + + self::$externalValidator = true; + self::$validationFactory = new ValidationFactory($translator); + self::$validationFactory->setPresenceVerifier(new DatabasePresenceVerifier($db->manager)); + } + + /** + * Instatiates the validator used by the validation process, depending if the class is being used inside or + * outside of Laravel. + * + * @param $data + * @param $rules + * @param $customMessages + * @return \Illuminate\Validation\Validator + * @see Ardent::$externalValidator + */ + protected static function makeValidator($data, $rules, $customMessages) { + if (self::$externalValidator) { + return self::$validationFactory->make($data, $rules, $customMessages); + } else { + return Validator::make($data, $rules, $customMessages); + } + } + + /** + * Validate the model instance + * + * @param array $rules Validation rules + * @param array $customMessages Custom error messages + * @return bool + * @throws InvalidModelException + */ + public function validate(array $rules = array(), array $customMessages = array()) { + if ($this->fireModelEvent('validating') === false) { + if ($this->throwOnValidation) { + throw new InvalidModelException($this); + } else { + return false; + } + } + + // check for overrides, then remove any empty rules + $rules = (empty($rules))? static::$rules : $rules; + foreach ($rules as $field => $rls) { + if ($rls == '') { + unset($rules[$field]); + } + } + + if (empty($rules)) { + $success = true; + } else { $customMessages = (empty($customMessages))? static::$customMessages : $customMessages; if ($this->forceEntityHydrationFromInput || (empty($this->attributes) && $this->autoHydrateEntityFromInput)) { @@ -559,327 +559,295 @@ public function validate(array $rules = array(), array $customMessages = array() } } - $this->fireModelEvent('validated', false); - - if (!$success && $this->throwOnValidation) { - throw new InvalidModelException($this); - } - - return $success; - } - - /** - * Save the model to the database. Is used by {@link save()} and {@link forceSave()} as a way to DRY code. - * - * @param array $rules - * @param array $customMessages - * @param array $options - * @param Closure $beforeSave - * @param Closure $afterSave - * @param bool $force Forces saving invalid data. - * - * @return bool - * @see Ardent::save() - * @see Ardent::forceSave() - */ - protected function internalSave(array $rules = array(), - array $customMessages = array(), - array $options = array(), - Closure $beforeSave = null, - Closure $afterSave = null, - $force = false - ) { - if ($beforeSave) { - self::saving($beforeSave); - } - if ($afterSave) { - self::saved($afterSave); - } - - $valid = $this->validate($rules, $customMessages); - - if ($force || $valid) { - return $this->performSave($options); - } else { - return false; - } - } - - /** - * Save the model to the database. - * - * @param array $rules - * @param array $customMessages - * @param array $options - * @param Closure $beforeSave - * @param Closure $afterSave - * - * @return bool - * @see Ardent::forceSave() - */ - public function save(array $rules = array(), - array $customMessages = array(), - array $options = array(), - Closure $beforeSave = null, - Closure $afterSave = null - ) { - return $this->internalSave($rules, $customMessages, $options, $beforeSave, $afterSave, false); - } - - /** - * Force save the model even if validation fails. - * - * @param array $rules - * @param array $customMessages - * @param array $options - * @param Closure $beforeSave - * @param Closure $afterSave - * @return bool - * @see Ardent::save() - */ - public function forceSave(array $rules = array(), - array $customMessages = array(), - array $options = array(), - Closure $beforeSave = null, - Closure $afterSave = null - ) { - return $this->internalSave($rules, $customMessages, $options, $beforeSave, $afterSave, true); - } - - - /** - * Add the basic purge filters - * - * @return void - */ - protected function addBasicPurgeFilters() { - if ($this->purgeFiltersInitialized) { - return; - } - - $this->purgeFilters[] = function ($attributeKey) { - // disallow password confirmation fields - if (Str::endsWith($attributeKey, '_confirmation')) { - return false; - } - - // "_method" is used by Illuminate\Routing\Router to simulate custom HTTP verbs - if (strcmp($attributeKey, '_method') === 0) { - return false; - } - - // "_token" is used by Illuminate\Html\FormBuilder to add CSRF protection - if (strcmp($attributeKey, '_token') === 0) { - return false; - } - - return true; - }; - - $this->purgeFiltersInitialized = true; - } - - /** - * Removes redundant attributes from model - * - * @param array $array Input array - * @return array - */ - protected function purgeArray(array $array = array()) { - - $result = array(); - $keys = array_keys($array); - - $this->addBasicPurgeFilters(); - - if (!empty($keys) && !empty($this->purgeFilters)) { - foreach ($keys as $key) { - $allowed = true; - - foreach ($this->purgeFilters as $filter) { - $allowed = $filter($key); - - if (!$allowed) { - break; - } - } - - if ($allowed) { - $result[$key] = $array[$key]; - } - } - } - - return $result; - } - - /** - * Saves the model instance to database. If necessary, it will purge the model attributes - * of unnecessary fields. It will also replace plain-text password fields with their hashes. - * - * @param array $options - * @return bool - */ - protected function performSave(array $options) { - - if ($this->autoPurgeRedundantAttributes) { - $this->attributes = $this->purgeArray($this->getAttributes()); - } - - if ($this->autoHashPasswordAttributes) { - $this->attributes = $this->hashPasswordAttributes($this->getAttributes(), static::$passwordAttributes); - } - - return parent::save($options); - } - - /** - * Get validation error message collection for the Model - * - * @return \Illuminate\Support\MessageBag - */ - public function errors() { - return $this->validationErrors; - } - - /** - * Automatically replaces all plain-text password attributes (listed in $passwordAttributes) - * with hash checksum. - * - * @param array $attributes - * @param array $passwordAttributes - * @return array - */ - protected function hashPasswordAttributes(array $attributes = array(), array $passwordAttributes = array()) { - - if (empty($passwordAttributes) || empty($attributes)) { - return $attributes; - } - - $result = array(); - foreach ($attributes as $key => $value) { - - if (in_array($key, $passwordAttributes) && !is_null($value)) { - if ($value != $this->getOriginal($key)) { - $result[$key] = Hash::make($value); - } - } else { - $result[$key] = $value; - } - } - - return $result; - } - - /** - * When given an ID and a Laravel validation rules array, this function - * appends the ID to the 'unique' rules given. The resulting array can - * then be fed to a Ardent save so that unchanged values - * don't flag a validation issue. Rules can be in either strings - * with pipes or arrays, but the returned rules are in arrays. - * - * @param int $id - * @param array $rules - * - * @return array Rules with exclusions applied - */ - protected function buildUniqueExclusionRules(array $rules = array()) { - - if (!count($rules)) - $rules = static::$rules; - - foreach ($rules as $field => &$ruleset) { - // If $ruleset is a pipe-separated string, switch it to array - $ruleset = (is_string($ruleset))? explode('|', $ruleset) : $ruleset; - - foreach ($ruleset as &$rule) { - if (strpos($rule, 'unique') === 0) { - $params = explode(',', $rule); - - $uniqueRules = array(); - - // Append table name if needed - $table = explode(':', $params[0]); - if (count($table) == 1) - $uniqueRules[1] = $this->table; - else - $uniqueRules[1] = $table[1]; - - // Append field name if needed - if (count($params) == 1) - $uniqueRules[2] = $field; - else - $uniqueRules[2] = $params[1]; - - if (isset($this->primaryKey)) { - $uniqueRules[3] = $this->{$this->primaryKey}; - $uniqueRules[4] = $this->primaryKey; - } - else { - $uniqueRules[3] = $this->id; - } - - $rule = 'unique:' . implode(',', $uniqueRules); - } // end if strpos unique - - } // end foreach ruleset - } - - return $rules; - } - - /** - * Update a model, but filter uniques first to ensure a unique validation rule - * does not fire - * - * @param array $rules - * @param array $customMessages - * @param array $options - * @param Closure $beforeSave - * @param Closure $afterSave - * @return bool - */ - public function updateUniques(array $rules = array(), - array $customMessages = array(), - array $options = array(), - Closure $beforeSave = null, - Closure $afterSave = null - ) { - $rules = $this->buildUniqueExclusionRules($rules); - - return $this->save($rules, $customMessages, $options, $beforeSave, $afterSave); - } - - /** - * Validates a model with unique rules properly treated. - * - * @param array $rules Validation rules - * @param array $customMessages Custom error messages + $this->fireModelEvent('validated', false); + + if (!$success && $this->throwOnValidation) { + throw new InvalidModelException($this); + } + + return $success; + } + + /** + * Save the model to the database. Is used by {@link save()} and {@link forceSave()} as a way to DRY code. + * + * @param array $rules + * @param array $customMessages + * @param array $options + * @param Closure $beforeSave + * @param Closure $afterSave + * @param bool $force Forces saving invalid data. + * * @return bool - * @see Ardent::validate() + * @see Ardent::save() + * @see Ardent::forceSave() */ - public function validateUniques(array $rules = array(), array $customMessages = array()) { - $rules = $this->buildUniqueExclusionRules($rules); - return $this->validate($rules, $customMessages); + protected function internalSave(array $rules = array(), + array $customMessages = array(), + array $options = array(), + Closure $beforeSave = null, + Closure $afterSave = null, + $force = false + ) { + if ($beforeSave) { + self::saving($beforeSave); + } + if ($afterSave) { + self::saved($afterSave); + } + + $valid = $this->validate($rules, $customMessages); + + if ($force || $valid) { + return $this->performSave($options); + } else { + return false; + } + } + + /** + * Save the model to the database. + * + * @param array $rules + * @param array $customMessages + * @param array $options + * @param Closure $beforeSave + * @param Closure $afterSave + * + * @return bool + * @see Ardent::forceSave() + */ + public function save(array $rules = array(), + array $customMessages = array(), + array $options = array(), + Closure $beforeSave = null, + Closure $afterSave = null + ) { + return $this->internalSave($rules, $customMessages, $options, $beforeSave, $afterSave, false); + } + + /** + * Force save the model even if validation fails. + * + * @param array $rules + * @param array $customMessages + * @param array $options + * @param Closure $beforeSave + * @param Closure $afterSave + * @return bool + * @see Ardent::save() + */ + public function forceSave(array $rules = array(), + array $customMessages = array(), + array $options = array(), + Closure $beforeSave = null, + Closure $afterSave = null + ) { + return $this->internalSave($rules, $customMessages, $options, $beforeSave, $afterSave, true); + } + + /** + * Add the basic purge filters + * + * @return void + */ + protected function addBasicPurgeFilters() { + if ($this->purgeFiltersInitialized) { + return; + } + + $this->purgeFilters[] = function ($attributeKey) { + // disallow password confirmation fields + if (Str::endsWith($attributeKey, '_confirmation')) { + return false; + } + + // "_method" is used by Illuminate\Routing\Router to simulate custom HTTP verbs + if (strcmp($attributeKey, '_method') === 0) { + return false; + } + + // "_token" is used by Illuminate\Html\FormBuilder to add CSRF protection + if (strcmp($attributeKey, '_token') === 0) { + return false; + } + + return true; + }; + + $this->purgeFiltersInitialized = true; + } + + /** + * Removes redundant attributes from model + * + * @param array $array Input array + * @return array + */ + protected function purgeArray(array $array = array()) { + + $result = array(); + $keys = array_keys($array); + + $this->addBasicPurgeFilters(); + + if (!empty($keys) && !empty($this->purgeFilters)) { + foreach ($keys as $key) { + $allowed = true; + + foreach ($this->purgeFilters as $filter) { + $allowed = $filter($key); + + if (!$allowed) { + break; + } + } + + if ($allowed) { + $result[$key] = $array[$key]; + } + } + } + + return $result; + } + + /** + * Saves the model instance to database. If necessary, it will purge the model attributes + * of unnecessary fields. It will also replace plain-text password fields with their hashes. + * + * @param array $options + * @return bool + */ + protected function performSave(array $options) { + + if ($this->autoPurgeRedundantAttributes) { + $this->attributes = $this->purgeArray($this->getAttributes()); + } + + if ($this->autoHashPasswordAttributes) { + $this->attributes = $this->hashPasswordAttributes($this->getAttributes(), static::$passwordAttributes); + } + + return parent::save($options); + } + + /** + * Get validation error message collection for the Model + * + * @return \Illuminate\Support\MessageBag + */ + public function errors() { + return $this->validationErrors; + } + + /** + * Automatically replaces all plain-text password attributes (listed in $passwordAttributes) + * with hash checksum. + * + * @param array $attributes + * @param array $passwordAttributes + * @return array + */ + protected function hashPasswordAttributes(array $attributes = array(), array $passwordAttributes = array()) { + + if (empty($passwordAttributes) || empty($attributes)) { + return $attributes; + } + + $result = array(); + foreach ($attributes as $key => $value) { + + if (in_array($key, $passwordAttributes) && !is_null($value)) { + if ($value != $this->getOriginal($key)) { + $result[$key] = Hash::make($value); + } + } else { + $result[$key] = $value; + } + } + + return $result; + } + + /** + * When given an ID and a Laravel validation rules array, this function + * appends the ID to the 'unique' rules given. The resulting array can + * then be fed to a Ardent save so that unchanged values + * don't flag a validation issue. Rules can be in either strings + * with pipes or arrays, but the returned rules are in arrays. + * + * @param int $id + * @param array $rules + * + * @return array Rules with exclusions applied + */ + protected function buildUniqueExclusionRules(array $rules = array()) { + + if (!count($rules)) + $rules = static::$rules; + + foreach ($rules as $field => &$ruleset) { + // If $ruleset is a pipe-separated string, switch it to array + $ruleset = (is_string($ruleset))? explode('|', $ruleset) : $ruleset; + + foreach ($ruleset as &$rule) { + if (strpos($rule, 'unique') === 0) { + $params = explode(',', $rule); + + $uniqueRules = array(); + + // Append table name if needed + $table = explode(':', $params[0]); + if (count($table) == 1) + $uniqueRules[1] = $this->table; + else + $uniqueRules[1] = $table[1]; + + // Append field name if needed + if (count($params) == 1) + $uniqueRules[2] = $field; + else + $uniqueRules[2] = $params[1]; + + if (isset($this->primaryKey)) { + $uniqueRules[3] = $this->{$this->primaryKey}; + $uniqueRules[4] = $this->primaryKey; + } + else { + $uniqueRules[3] = $this->id; + } + + $rule = 'unique:' . implode(',', $uniqueRules); + } // end if strpos unique + + } // end foreach ruleset + } + + return $rules; } - /** - * Find a model by its primary key. - * If {@link $throwOnFind} is set, will use {@link findOrFail} internally. - * - * @param mixed $id - * @param array $columns - * @return Ardent|Collection - */ - public static function find($id, $columns = array('*')) { - $debug = debug_backtrace(false); - - if (static::$throwOnFind && $debug[1]['function'] != 'findOrFail') { - return self::findOrFail($id, $columns); - } else { - return parent::find($id, $columns); - } - } + /** + * Update a model, but filter uniques first to ensure a unique validation rule + * does not fire + * + * @param array $rules + * @param array $customMessages + * @param array $options + * @param Closure $beforeSave + * @param Closure $afterSave + * @return bool + */ + public function updateUniques(array $rules = array(), + array $customMessages = array(), + array $options = array(), + Closure $beforeSave = null, + Closure $afterSave = null + ) { + $rules = $this->buildUniqueExclusionRules($rules); + + return $this->save($rules, $customMessages, $options, $beforeSave, $afterSave); + } /** * Get a new query builder for the model's table. From d221181b23f8f1c38abafd901cdba9ea1f2f33da Mon Sep 17 00:00:00 2001 From: Igor Santos <igorsantos07@gmail.com> Date: Mon, 24 Feb 2014 17:45:39 -0300 Subject: [PATCH 4/8] Current merge of bexarcreativeinc/master with the original master --- src/LaravelBook/Ardent/Ardent.php | 35 +++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php index fc0f4b3..2a68be9 100755 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -376,7 +376,7 @@ public function __call($method, $parameters) { * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function belongsTo($related, $foreignKey = NULL, $otherKey = NULL, $relation = NULL) { - + // If no relation name was given, we will use this debug backtrace to extract // the calling method's name and use that as the relationship name as most // of the time this will be what we desire to use for the relatinoships. @@ -482,7 +482,7 @@ public static function configureAsExternal(array $connection) { self::$externalValidator = true; self::$validationFactory = new ValidationFactory($translator); - self::$validationFactory->setPresenceVerifier(new DatabasePresenceVerifier($db->manager)); + self::$validationFactory->setPresenceVerifier(new DatabasePresenceVerifier($db->getDatabaseManager())); } /** @@ -849,6 +849,37 @@ public function updateUniques(array $rules = array(), return $this->save($rules, $customMessages, $options, $beforeSave, $afterSave); } + /** + * Validates a model with unique rules properly treated. + * + * @param array $rules Validation rules + * @param array $customMessages Custom error messages + * @return bool + * @see Ardent::validate() + */ + public function validateUniques(array $rules = array(), array $customMessages = array()) { + $rules = $this->buildUniqueExclusionRules($rules); + return $this->validate($rules, $customMessages); + } + + /** + * Find a model by its primary key. + * If {@link $throwOnFind} is set, will use {@link findOrFail} internally. + * + * @param mixed $id + * @param array $columns + * @return Ardent|Collection + */ + public static function find($id, $columns = array('*')) { + $debug = debug_backtrace(false); + + if (static::$throwOnFind && $debug[1]['function'] != 'findOrFail') { + return self::findOrFail($id, $columns); + } else { + return parent::find($id, $columns); + } + } + /** * Get a new query builder for the model's table. * Overriden from {@link \Model\Eloquent} to allow for usage of {@link throwOnFind}. From 40f191d8e6a04c006467353933de58936ad14c7e Mon Sep 17 00:00:00 2001 From: bexarcreative-daniel <daniel@bexarcreative.com> Date: Fri, 7 Mar 2014 14:14:08 -0600 Subject: [PATCH 5/8] Fixed typo for `pivotKeys`. Added missing `relation` value. Ensured `pivotKeys` and `timestamps` are optional named arguments. --- README.md | 1 + src/LaravelBook/Ardent/Ardent.php | 16 +++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cda7daf..89a89c7 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ Following the first, second, and third numeric indexes are named index values fr - `table`: optional for `belongsToMany` - `timestamps`: optional for `belongsToMany` - `pivotKeys`: optional for `belongsToMany` +- `relation`: optional for `belongsTo` and `belongsToMany` - `name`: optional for `morphTo` and required for `morphOne`, `morphMany` - `type`: optional for `morphTo`, `morphOne`, `morphMany` - `id`: optional for `morphTo`, `morphOne`, `morphMany` diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php index 4024fd6..83c4418 100755 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -331,16 +331,18 @@ protected function handleRelationalArray($relationName) { return $this->$relationType($relation[1], $relation[2], $relation['firstKey'], $relation['secondKey']); case self::BELONGS_TO: - $verifyArgs(array('foreignKey', 'otherKey')); - return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey']); + $verifyArgs(array('foreignKey', 'otherKey', 'relation')); + return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey'], $relation['relation']); case self::BELONGS_TO_MANY: - $verifyArgs(array('table', 'foreignKey', 'otherKey', 'pivoteKeys', 'timestamps')); - $relationship = $this->$relationType($relation[1], $relation['table'], $relation['foreignKey'], $relation['otherKey']); - if(isset($relation['pivotKeys']) && is_array($relation['pivotKeys'])) + $verifyArgs(array('table', 'foreignKey', 'otherKey', 'relation', 'pivotKeys', 'timestamps')); + $relationship = $this->$relationType($relation[1], $relation['table'], $relation['foreignKey'], $relation['otherKey'], $relation['relation']); + if(isset($relation['pivotKeys']) && is_array($relation['pivotKeys'])) { $relationship->withPivot($relation['pivotKeys']); - if(isset($relation['timestamps']) && $relation['timestamps']==true) + } + if(isset($relation['timestamps']) && $relation['timestamps']===true) { $relationship->withTimestamps(); + } return $relationship; case self::MORPH_TO: @@ -350,7 +352,7 @@ protected function handleRelationalArray($relationName) { case self::MORPH_ONE: case self::MORPH_MANY: $verifyArgs(array('type', 'id', 'localKey'), array('name')); - return $this->$relationType($relation[1], $relation['name'], $relation['type'], $relation['id']); + return $this->$relationType($relation[1], $relation['name'], $relation['type'], $relation['id'], $relation['localKey']); } } From 9879183f7cdee6d023d0a111f087b615ba4704cd Mon Sep 17 00:00:00 2001 From: bexarcreative-daniel <daniel@bexarcreative.com> Date: Fri, 23 May 2014 20:52:15 +0000 Subject: [PATCH 6/8] Patched newQuery() for Laravel 4.2. --- src/LaravelBook/Ardent/Ardent.php | 42 ++++++++++++++----------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php index 83c4418..9750bce 100755 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -889,27 +889,23 @@ public static function find($id, $columns = array('*')) { } } - /** - * Get a new query builder for the model's table. - * Overriden from {@link \Model\Eloquent} to allow for usage of {@link throwOnFind}. - * - * @param bool $excludeDeleted - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQuery($excludeDeleted = true) { - $builder = new Builder($this->newBaseQueryBuilder()); - $builder->throwOnFind = static::$throwOnFind; - - // Once we have the query builders, we will set the model instances so the - // builder can easily access any information it may need from the model - // while it is constructing and executing various queries against it. - $builder->setModel($this)->with($this->with); - - if ($excludeDeleted and $this->softDelete) - { - $builder->whereNull($this->getQualifiedDeletedAtColumn()); - } - - return $builder; - } + /** + * Get a new query builder for the model's table. + * Overriden from {@link \Model\Eloquent} to allow for usage of {@link throwOnFind}. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQuery() { + $builder = $this->newEloquentBuilder( + $this->newBaseQueryBuilder() + ); + $builder->throwOnFind = static::$throwOnFind; + + // Once we have the query builders, we will set the model instances so the + // builder can easily access any information it may need from the model + // while it is constructing and executing various queries against it. + $builder->setModel($this)->with($this->with); + + return $this->applyGlobalScopes($builder); + } } From 7f23ff6adcc88ad9f194a5e62ad570224b3c97c3 Mon Sep 17 00:00:00 2001 From: Daniel LaBarge <daniel@bexarcreative.com> Date: Fri, 23 May 2014 16:13:43 -0500 Subject: [PATCH 7/8] Updated composer.json to be compatible with Laravel 4.2 dependencies --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 2cdc960..226f3e6 100644 --- a/composer.json +++ b/composer.json @@ -24,10 +24,10 @@ "email": "contact@laravelbook.com" }, "require": { - "php": ">=5.3.0", - "illuminate/support": "~4.1", - "illuminate/database": "~4.1", - "illuminate/validation": "~4.1" + "php": ">=5.4.0", + "illuminate/support": "~4.2", + "illuminate/database": "~4.2", + "illuminate/validation": "~4.2" }, "autoload": { "psr-0": { From b09f2d3d173edbdb98bd27070435218a460797c9 Mon Sep 17 00:00:00 2001 From: Daniel LaBarge <daniel@bexarcreative.com> Date: Fri, 23 May 2014 17:09:17 -0500 Subject: [PATCH 8/8] Fixed tab spacing to be style guide compliant. --- src/LaravelBook/Ardent/Ardent.php | 38 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php index 9750bce..e2d89dc 100755 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -889,23 +889,23 @@ public static function find($id, $columns = array('*')) { } } - /** - * Get a new query builder for the model's table. - * Overriden from {@link \Model\Eloquent} to allow for usage of {@link throwOnFind}. - * - * @return \Illuminate\Database\Eloquent\Builder - */ - public function newQuery() { - $builder = $this->newEloquentBuilder( - $this->newBaseQueryBuilder() - ); - $builder->throwOnFind = static::$throwOnFind; - - // Once we have the query builders, we will set the model instances so the - // builder can easily access any information it may need from the model - // while it is constructing and executing various queries against it. - $builder->setModel($this)->with($this->with); - - return $this->applyGlobalScopes($builder); - } + /** + * Get a new query builder for the model's table. + * Overriden from {@link \Model\Eloquent} to allow for usage of {@link throwOnFind}. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQuery() { + $builder = $this->newEloquentBuilder( + $this->newBaseQueryBuilder() + ); + $builder->throwOnFind = static::$throwOnFind; + + // Once we have the query builders, we will set the model instances so the + // builder can easily access any information it may need from the model + // while it is constructing and executing various queries against it. + $builder->setModel($this)->with($this->with); + + return $this->applyGlobalScopes($builder); + } }