diff --git a/README.md b/README.md index 60c5fa5..89a89c7 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,45 @@ $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 description: -- 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` +- `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` > **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/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": { diff --git a/src/LaravelBook/Ardent/Ardent.php b/src/LaravelBook/Ardent/Ardent.php index 49386fd..e2d89dc 100755 --- a/src/LaravelBook/Ardent/Ardent.php +++ b/src/LaravelBook/Ardent/Ardent.php @@ -31,206 +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: - * - * 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') - * ); - * } - * - * - * @see \Illuminate\Database\Eloquent\Model::hasOne - * @see \Illuminate\Database\Eloquent\Model::hasMany - * @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 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::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: + * + * 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') + * ); + * } + * + * + * @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); + }); + } + } + } + } public function getObservableEvents() { return array_merge( @@ -259,132 +262,153 @@ 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 param 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.'); - } - if (isset($relation[1]) && $relationType == self::MORPH_TO) { - throw new \InvalidArgumentException($errorHeader. - ' is a morphTo relation and should not contain additional arguments.'); - } - - $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: - case self::BELONGS_TO: - $verifyArgs(array('foreignKey')); - return $this->$relationType($relation[1], $relation['foreignKey']); - - case self::BELONGS_TO_MANY: - $verifyArgs(array('table', 'foreignKey', 'otherKey')); - $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'), 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', 'relation')); + return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey'], $relation['relation']); + + case self::BELONGS_TO_MANY: + $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) { + $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'], $relation['localKey']); + } + } + + /** + * 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. + * 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) { + + // 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. - $relation = $caller['function']; - 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. - $instance = new $related; - - $otherKey = $otherKey ?: $instance->getKeyName(); - + // actually be responsible for retrieving and hydrating every relations. $query = $instance->newQuery(); + $otherKey = $otherKey ?: $instance->getKeyName(); + return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); } @@ -420,102 +444,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->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 { $customMessages = (empty($customMessages))? static::$customMessages : $customMessages; if ($this->forceEntityHydrationFromInput || (empty($this->attributes) && $this->autoHydrateEntityFromInput)) { @@ -544,296 +568,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); - } + $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. @@ -848,33 +871,34 @@ public function validateUniques(array $rules = array(), array $customMessages = 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); - } - } + /** + * 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}. * - * @param bool $excludeDeleted * @return \Illuminate\Database\Eloquent\Builder */ - public function newQuery($excludeDeleted = true) { - $builder = new Builder($this->newBaseQueryBuilder()); + 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 @@ -882,11 +906,6 @@ public function newQuery($excludeDeleted = true) { // 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; + return $this->applyGlobalScopes($builder); } }