├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Ardent │ ├── Ardent.php │ ├── Builder.php │ └── InvalidModelException.php └── lang │ ├── en │ └── validation.php │ └── pt-br │ └── validation.php └── tests └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | # the project itself is not installable, so we'll make sure nothing from composer gets into the repo 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | 7 | before_script: 8 | - curl -s http://getcomposer.org/installer | php 9 | - php composer.phar install --dev 10 | 11 | script: phpunit -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Ardent's Changelog 2 | ================== 3 | 4 | Please, look at the [project's release][1] page :) 5 | 6 | [1]:https://github.com/laravelbook/ardent/releases 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # [BSD 3-clause New License](http://choosealicense.com/licenses/bsd-3-clause/) 2 | 3 | ### Copyright (c) 2015 - Max Ehsan, Igor Santos. All rights reserved 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 19 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 21 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 22 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 24 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 25 | SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ardent 2 | ====== 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/laravelbook/ardent/v/stable.svg)](https://packagist.org/packages/laravelbook/ardent) 5 | [![License](https://poser.pugx.org/laravelbook/ardent/license.svg)](https://packagist.org/packages/laravelbook/ardent) 6 | [![Total Downloads](https://poser.pugx.org/laravelbook/ardent/downloads.svg)](https://packagist.org/packages/laravelbook/ardent) 7 | [![Monthly Downloads](https://poser.pugx.org/laravelbook/ardent/d/monthly.png)](https://packagist.org/packages/laravelbook/ardent) 8 | [![Daily Downloads](https://poser.pugx.org/laravelbook/ardent/d/daily.png)](https://packagist.org/packages/laravelbook/ardent) 9 | 10 | 11 | Self-validating smart models for Laravel Framework 5's Eloquent ORM. 12 | 13 | Based on the Aware bundle for Laravel 3 by Colby Rabideau. 14 | 15 | Copyright (C) 2013-2015 [Max Ehsan](http://laravelbook.com/) & [Igor Santos](http://www.igorsantos.com.br) 16 | 17 | ## Changelog 18 | 19 | Visit our [Releases list](https://github.com/laravelbook/ardent/releases). The changelog is made there :) 20 | 21 | ## Installation 22 | 23 | Add `laravelbook/ardent` as a requirement to `composer.json` (see our latest stable version on the badges!): 24 | 25 | ```javascript 26 | { 27 | "require": { 28 | "laravelbook/ardent": "3.*" 29 | } 30 | } 31 | ``` 32 | 33 | Update your packages with `composer update` or install with `composer install`. 34 | 35 | You can also add the package using `composer require laravelbook/ardent` and later specifying the version you want (for now, `dev-master` is your best bet). 36 | 37 | ### Usage outside of Laravel (since [1.1](https://github.com/laravelbook/ardent/tree/v1.1.0)) 38 | 39 | If you're want to use Ardent as a standalone ORM package you're invited to do so by using the 40 | following configuration in your project's boot/startup file (changing the properties according 41 | to your database, obviously): 42 | 43 | ```php 44 | \LaravelArdent\Ardent\Ardent::configureAsExternal(array( 45 | 'driver' => 'mysql', 46 | 'host' => 'localhost', 47 | 'port' => 3306, 48 | 'database' => 'my_system', 49 | 'username' => 'myself', 50 | 'password' => 'h4ckr', 51 | 'charset' => 'utf8', 52 | 'collation' => 'utf8_unicode_ci' 53 | ), 'en'); //English is the default messages language, may be left empty 54 | ``` 55 | 56 | ------------------------------------------------------------------------------------------------------------ 57 | 58 | ## Documentation 59 | 60 | * [Introduction](#introduction) 61 | * [Getting Started](#getting-started) 62 | * [Effortless Validation with Ardent](#effortless-validation-with-ardent) 63 | * [Retrieving Validation Errors](#retrieving-validation-errors) 64 | * [Overriding Validation](#overriding-validation) 65 | * [Custom Validation Error Messages](#custom-validation-error-messages) 66 | * [Custom Validation Rules](#custom-validation-rules) 67 | * [Model hooks](#model-hooks-since-20) 68 | * [Cleaner definition of relationships](#cleaner-definition-of-relationships-since-20) 69 | * [Automatically Hydrate Ardent Entities](#automatically-hydrate-ardent-entities) 70 | * [Automatically Purge Redundant Form Data](#automatically-purge-redundant-form-data) 71 | * [Automatically Transform Secure-Text Attributes](#automatically-transform-secure-text-attributes) 72 | * [Updates with Unique Rules](#updates-with-unique-rules) 73 | 74 | 75 | ## Introduction 76 | 77 | How often do you find yourself re-creating the same boilerplate code in the applications you build? Does this typical form processing code look all too familiar to you? 78 | 79 | ```php 80 | Route::post('register', function() { 81 | $rules = array( 82 | 'name' => 'required|between:3,80|alpha_dash', 83 | 'email' => 'required|between:5,64|email|unique:users', 84 | 'password' => 'required|min:6|confirmed', 85 | 'password_confirmation' => 'required|min:6' 86 | ); 87 | 88 | $validator = Validator::make(Input::all(), $rules); 89 | 90 | if ($validator->passes()) { 91 | User::create(array( 92 | 'name' => Input::get('name'), 93 | 'email' => Input::get('email'), 94 | 'password' => Hash::make(Input::get('password')) 95 | )); 96 | 97 | return Redirect::to('/')->with('message', 'Thanks for registering!'); 98 | } else { 99 | return Redirect::to('/')->withErrors($validator->getMessages()); 100 | } 101 | } 102 | ); 103 | ``` 104 | 105 | Implementing this yourself often results in a lot of repeated boilerplate code. As an added bonus, you controllers (or route handlers) get prematurely fat, and your code becomes messy, ugly and difficult to understand. 106 | 107 | What if someone else did all the heavy-lifting for you? What if, instead of regurgitating the above mess, all you needed to type was these few lines?... 108 | 109 | ```php 110 | Route::post('register', function() { 111 | $user = new User; 112 | if ($user->save()) { 113 | return Redirect::to('/')->with('message', 'Thanks for registering!'); 114 | } else { 115 | return Redirect::to('/')->withErrors($user->errors()); 116 | } 117 | } 118 | ); 119 | ``` 120 | 121 | **Enter Ardent!** 122 | 123 | **Ardent** - the magic-dust-powered, wrist-friendly, one-stop solution to all your dreary input sanitization boilerplates! 124 | 125 | Puns aside, input validation functionality can quickly become tedious to write and maintain. Ardent deals away with these complexities by providing helpers for automating many repetitive tasks. 126 | 127 | Ardent is not just great for input validation, though - it will help you significantly reduce your Eloquent data model code. Ardent is particularly useful if you find yourself wearily writing very similar code time and again in multiple individual applications. 128 | 129 | For example, user registration or blog post submission is a common coding requirement that you might want to implement in one application and reuse again in other applications. With Ardent, you can write your *self-aware, smart* models just once, then re-use them (with no or very little modification) in other projects. Once you get used to this way of doing things, you'll honestly wonder how you ever coped without Ardent. 130 | 131 | **No more repetitive brain strain injury for you!** 132 | 133 | 134 | ## Getting Started 135 | 136 | `Ardent` aims to extend the `Eloquent` base class without changing its core functionality. Since `Ardent` itself is a descendant of `Illuminate\Database\Eloquent\Model`, all your `Ardent` models are fully compatible with `Eloquent` and can harness the full power of Laravels awesome OR/M. 137 | 138 | To create a new Ardent model, simply make your model class derive from the `Ardent` base class. In the next examples we will use the complete namespaced class to make examples cleaner, but you're encouraged to make use of `use` in all your classes: 139 | 140 | ```php 141 | use LaravelArdent\Ardent\Ardent; 142 | 143 | class User extends Ardent {} 144 | ``` 145 | 146 | > **Note:** You can freely *co-mingle* your plain-vanilla Eloquent models with Ardent descendants. If a model object doesn't rely upon user submitted content and therefore doesn't require validation - you may leave the Eloquent model class as it is. 147 | 148 | 149 | ## Effortless Validation with Ardent 150 | 151 | Ardent models use Laravel's built-in [Validator class](http://laravel.com/docs/validation). Defining validation rules for a model is simple and is typically done in your model class as a static variable: 152 | 153 | ```php 154 | class User extends \LaravelArdent\Ardent\Ardent { 155 | public static $rules = array( 156 | 'name' => 'required|between:3,80|alpha_dash', 157 | 'email' => 'required|between:5,64|email|unique:users', 158 | 'password' => 'required|min:6|confirmed', 159 | 'password_confirmation' => 'required|min:6', 160 | ); 161 | } 162 | ``` 163 | 164 | > **Note**: you're free to use the [array syntax](http://laravel.com/docs/5.0/validation#basic-usage) for validation rules as well. _I hope you don't mind the old Laravel docs link, but as good as Laravel documentation is, clear reference on pipe/array syntaxes for Validation rules is unfortunately gone since 5.1._ 165 | 166 | Ardent models validate themselves automatically when `Ardent->save()` is called. 167 | 168 | ```php 169 | $user = new User; 170 | $user->name = 'John doe'; 171 | $user->email = 'john@doe.com'; 172 | $user->password = 'test'; 173 | 174 | $success = $user->save(); // returns false if model is invalid 175 | ``` 176 | 177 | > **Note:** You can also validate a model at any time using the `Ardent->validate()` method. 178 | 179 | 180 | ## Retrieving Validation Errors 181 | 182 | When an Ardent model fails to validate, a `Illuminate\Support\MessageBag` object is attached to the Ardent object which contains validation failure messages. 183 | 184 | Retrieve the validation errors message collection instance with `Ardent->errors()` method or `Ardent->validationErrors` property. 185 | 186 | Retrieve all validation errors with `Ardent->errors()->all()`. Retrieve errors for a *specific* attribute using `Ardent->validationErrors->get('attribute')`. 187 | 188 | > **Note:** Ardent leverages Laravel's MessagesBag object which has a [simple and elegant method](http://laravel.com/docs/validation#working-with-error-messages) of formatting errors. 189 | 190 | 191 | ## Overriding Validation 192 | 193 | There are two ways to override Ardent's validation: 194 | 195 | #### 1. Forced Save 196 | `forceSave()` validates the model but saves regardless of whether or not there are validation errors. 197 | 198 | #### 2. Override Rules and Messages 199 | both `Ardent->save($rules, $customMessages)` and `Ardent->validate($rules, $customMessages)` take two parameters: 200 | 201 | - `$rules` is an array of Validator rules of the same form as `Ardent::$rules`. 202 | - The same is true of the `$customMessages` parameter (same as `Ardent::$customMessages`) 203 | 204 | An array that is **not empty** will override the rules or custom error messages specified by the class for that instance of the method only. 205 | 206 | > **Note:** the default value for `$rules` and `$customMessages` is empty `array()`; thus, if you pass an `array()` nothing will be overriden. 207 | 208 | 209 | ## Custom Validation Error Messages 210 | 211 | Just like the Laravel Validator, Ardent lets you set custom error messages using the [same syntax](http://laravel.com/docs/validation#custom-error-messages). 212 | 213 | ```php 214 | class User extends \LaravelArdent\Ardent\Ardent { 215 | public static $customMessages = array( 216 | 'required' => 'The :attribute field is required.', 217 | ... 218 | ); 219 | } 220 | ``` 221 | 222 | 223 | ## Custom Validation Rules 224 | 225 | You can create custom validation rules the [same way](http://laravel.com/docs/validation#custom-validation-rules) you would for the Laravel Validator. 226 | 227 | 228 | ## Model Hooks (since [2.0](https://github.com/laravelbook/ardent/tree/v2.0.0)) 229 | 230 | Ardent provides some syntatic sugar over Eloquent's model events: traditional model hooks. They are an easy way to hook up additional operations to different moments in your model life. They can be used to do additional clean-up work before deleting an entry, doing automatic fixes after validation occurs or updating related models after an update happens. 231 | 232 | All `before` hooks, when returning `false` (specifically boolean, not simply "falsy" values) will halt the operation. So, for example, if you want to stop saving if something goes wrong in a `beforeSave` method, just `return false` and the save will not happen - and obviously `afterSave` won't be called as well. 233 | 234 | Here's the complete list of available hooks: 235 | 236 | - `before`/`afterCreate()` 237 | - `before`/`afterSave()` 238 | - `before`/`afterUpdate()` 239 | - `before`/`afterDelete()` 240 | - `before`/`afterValidate()` - when returning false will halt validation, thus making `save()` operations fail as well since the validation was a failure. 241 | 242 | For example, you may use `beforeSave` to hash a users password (actually, it would be a better idea to use [auto-hashing](#automatically-transform-secure-text-attributes)!): 243 | 244 | ```php 245 | class User extends \LaravelArdent\Ardent\Ardent { 246 | public function beforeSave() { 247 | // if there's a new password, hash it 248 | if($this->isDirty('password')) { 249 | $this->password = Hash::make($this->password); 250 | } 251 | 252 | return true; 253 | //or don't return nothing, since only a boolean false will halt the operation 254 | } 255 | } 256 | ``` 257 | 258 | ### Additionals beforeSave and afterSave (since 1.0) 259 | 260 | `beforeSave` and `afterSave` can be included at run-time. Simply pass in closures with the model as argument to the `save()` (or `forceSave()`) method. 261 | 262 | ```php 263 | $user->save(array(), array(), array(), 264 | function ($model) { // closure for beforeSave 265 | echo "saving the model object..."; 266 | return true; 267 | }, 268 | function ($model) { // closure for afterSave 269 | echo "done!"; 270 | } 271 | ); 272 | ``` 273 | 274 | > **Note:** the closures should have one parameter as it will be passed a reference to the model being saved. 275 | 276 | 277 | ## Cleaner definition of relationships (since [2.0](https://github.com/laravelbook/ardent/tree/v2.0.0)) 278 | 279 | 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? 280 | 281 | In Ardent you can cleanly define your relationships in an array with their information, and they will work just like if you had defined them in methods. Here's an example: 282 | 283 | ```php 284 | class User extends \LaravelArdent\Ardent\Ardent { 285 | public static $relationsData = array( 286 | 'address' => array(self::HAS_ONE, 'Address'), 287 | 'orders' => array(self::HAS_MANY, 'Order'), 288 | 'groups' => array(self::BELONGS_TO_MANY, 'Group', 'table' => 'groups_have_users') 289 | ); 290 | } 291 | 292 | $user = User::find($id); 293 | echo "{$user->address->street}, {$user->address->city} - {$user->address->state}"; 294 | ``` 295 | 296 | The array syntax is as follows: 297 | 298 | - First indexed value: relation name, being one of 299 | [`hasOne`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_hasOne), 300 | [`hasMany`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_hasMany), 301 | [`belongsTo`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_belongsTo), 302 | [`belongsToMany`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_belongsToMany), 303 | [`morphTo`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_morphTo), 304 | [`morphOne`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_morphOne), 305 | [`morphMany`](http://laravel.com/api/class-Illuminate.Database.Eloquent.Model.html#_morphMany), 306 | or one of the related constants (`Ardent::HAS_MANY` or `Ardent::MORPH_ONE` for example). 307 | - Second indexed: class name, with complete namespace. The exception is `morphTo` relations, that take no additional argument. 308 | - named arguments, following the ones defined for the original Eloquent methods: 309 | - `foreignKey` [optional], valid for `hasOne`, `hasMany`, `belongsTo` and `belongsToMany` 310 | - `table`,`otherKey` [optional],`timestamps` [boolean, optional], and `pivotKeys` [array, optional], valid for `belongsToMany` 311 | - `name`, `type` and `id`, used by `morphTo`, `morphOne` and `morphMany` (the last two requires `name` to be defined) 312 | 313 | > **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). 314 | 315 | 316 | ## Automatically Hydrate Ardent Entities 317 | 318 | Ardent is capable of hydrating your entity model class from the form input submission automatically! 319 | 320 | Let's see it action. Consider this snippet of code: 321 | 322 | ```php 323 | $user = new User; 324 | $user->name = Input::get('name'); 325 | $user->email = Input::get('email'); 326 | $user->password = Hash::make(Input::get('password')); 327 | $user->save(); 328 | ``` 329 | 330 | Let's invoke the *magick* of Ardent and rewrite the previous snippet: 331 | 332 | ```php 333 | $user = new User; 334 | $user->save(); 335 | ``` 336 | 337 | That's it! All we've done is remove the boring stuff. 338 | 339 | Believe it or not, the code above performs essentially the same task as its older, albeit rather verbose sibling. Ardent populates the model object with attributes from user submitted form data. No more hair-pulling trying to find out which Eloquent property you've forgotten to populate. Let Ardent take care of the boring stuff, while you get on with the fun stuffs! 340 | It follows the same [mass assignment rules](http://laravel.com/docs/eloquent#mass-assignment) internally, depending on the `$fillable`/`$guarded` properties. 341 | 342 | To enable the auto-hydration feature, simply set the `$autoHydrateEntityFromInput` instance variable to `true` in your model class. However, to prevent filling pre-existent properties, if you want auto-hydration also for update scenarios, you should use instead `$forceEntityHydrationFromInput`: 343 | 344 | ```php 345 | class User extends \LaravelArdent\Ardent\Ardent { 346 | public $autoHydrateEntityFromInput = true; // hydrates on new entries' validation 347 | public $forceEntityHydrationFromInput = true; // hydrates whenever validation is called 348 | } 349 | ``` 350 | 351 | 352 | ## Automatically Purge Redundant Form Data 353 | 354 | Ardent models can *auto-magically* purge redundant input data (such as *password confirmation*, hidden CSRF `_token` or custom HTTP `_method` fields) - so that the extra data is never saved to database. Ardent will use the confirmation fields to validate form input, then prudently discard these attributes before saving the model instance to database! 355 | 356 | To enable this feature, simply set the `$autoPurgeRedundantAttributes` instance variable to `true` in your model class: 357 | 358 | ```php 359 | class User extends \LaravelArdent\Ardent\Ardent { 360 | public $autoPurgeRedundantAttributes = true; 361 | } 362 | ``` 363 | 364 | You can also purge additional fields. The attribute `Ardent::$purgeFilters` is an array of closures to which you can add your custom rules. Those closures receive the attribute key as argument and should return `false` for attributes that should be purged. Like this: 365 | 366 | ```php 367 | function __construct($attributes = array()) { 368 | parent::__construct($attributes); 369 | 370 | $this->purgeFilters[] = function($key) { 371 | $purge = array('tempData', 'myAttribute'); 372 | return ! in_array($key, $purge); 373 | }; 374 | } 375 | ``` 376 | 377 | 378 | ## Automatically Transform Secure-Text Attributes 379 | 380 | Suppose you have an attribute named `password` in your model class, but don't want to store the plain-text version in the database. The pragmatic thing to do would be to store the hash of the original content. Worry not, Ardent is fully capable of transmogrifying any number of secure fields automatically for you! 381 | 382 | To do that, add the attribute name to the `Ardent::$passwordAttributes` static array variable in your model class, and set the `$autoHashPasswordAttributes` instance variable to `true`: 383 | 384 | ```php 385 | class User extends \LaravelArdent\Ardent\Ardent { 386 | public static $passwordAttributes = array('password'); 387 | public $autoHashPasswordAttributes = true; 388 | } 389 | ``` 390 | 391 | Ardent will automatically replace the plain-text password attribute with secure hash checksum and save it to database. It uses the Laravel `Hash::make()` method internally to generate hash. _Note: It's advised to use Eloquent's [`$hidden`](https://laravel.com/docs/5.2/eloquent-serialization#hiding-attributes-from-json) attribute so the password, even hashed, won't come out that easily if you're building an API or similar :)_ 392 | 393 | In case you're using Ardent standalone, you can use `Ardent::$hasher` to verify the field value, using something like `User::$hasher->check($given_password, $user->password)`. 394 | 395 | 396 | ## Updates with Unique Rules 397 | 398 | Ardent can assist you with unique updates. According to the Laravel Documentation, when you update (and therefore validate) a field with a unique rule, you have to pass in the unique ID of the record you are updating. Without passing this ID, validation will fail because Laravel's Validator will think this record is a duplicate. 399 | 400 | From the Laravel Documentation: 401 | 402 | ```php 403 | 'email' => 'unique:users,email,10' 404 | ``` 405 | 406 | In the past, programmers had to manually manage the passing of the ID and changing of the ruleset to include the ID at runtime. Not so with Ardent. Simply set up your rules with `unique`, call function `updateUniques` and Ardent will take care of the rest. 407 | 408 | #### Example: 409 | 410 | In your extended model define your rules 411 | 412 | ```php 413 | public static $rules = array( 414 | 'email' => 'required|email|unique', 415 | 'password' => 'required|between:4,20|confirmed', 416 | 'password_confirmation' => 'between:4,20', 417 | ); 418 | ``` 419 | 420 | In your controller, when you need to update, simply call 421 | 422 | ```php 423 | $model->updateUniques(); 424 | ``` 425 | 426 | If required, you can runtime pass rules to `updateUniques`, otherwise it will use the static rules provided by your model. 427 | 428 | Note that in the above example of the rules, we did not tell the Validator which table or even which field to use as it is described in the Laravel Documentation (ie `unique:users,email,10`). Ardent is clever enough to figure it out. (Thank you to github user @Sylph) 429 | 430 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-ardent/ardent", 3 | "description": "Self-validating smart models for Laravel 5's Eloquent ORM", 4 | "keywords": ["validation", "laravel", "framework", "eloquent", "database", "sql", "orm", "activerecord", "active record"], 5 | "type": "library", 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Igor Santos", 10 | "homepage": "http://igorsantos.com.br", 11 | "email": "igorsantos07@gmail.com", 12 | "role": "Maintainer" 13 | }, 14 | { 15 | "name": "Max Ehsan", 16 | "role": "Original developer" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/laravelbook/ardent/issues" 21 | }, 22 | "require": { 23 | "illuminate/support": "~5.1", 24 | "illuminate/database": "~5.1", 25 | "illuminate/validation": "~5.1", 26 | "illuminate/events": "~5.1", 27 | "illuminate/hashing": "~5.1" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "LaravelArdent\\": "src/" 32 | } 33 | }, 34 | "minimum-stability": "dev" 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Ardent/Ardent.php: -------------------------------------------------------------------------------- 1 | 166 | * class Order extends Ardent { 167 | * protected static $relations = array( 168 | * 'items' => array(self::HAS_MANY, 'Item'), 169 | * 'owner' => array(self::HAS_ONE, 'User', 'foreignKey' => 'user_id'), 170 | * 'pictures' => array(self::MORPH_MANY, 'Picture', 'name' => 'imageable') 171 | * ); 172 | * } 173 | * 174 | * 175 | * @see \Illuminate\Database\Eloquent\Model::hasOne 176 | * @see \Illuminate\Database\Eloquent\Model::hasMany 177 | * @see \Illuminate\Database\Eloquent\Model::hasManyThrough 178 | * @see \Illuminate\Database\Eloquent\Model::belongsTo 179 | * @see \Illuminate\Database\Eloquent\Model::belongsToMany 180 | * @see \Illuminate\Database\Eloquent\Model::morphTo 181 | * @see \Illuminate\Database\Eloquent\Model::morphOne 182 | * @see \Illuminate\Database\Eloquent\Model::morphMany 183 | * @see \Illuminate\Database\Eloquent\Model::morphToMany 184 | * @see \Illuminate\Database\Eloquent\Model::morphedByMany 185 | * 186 | * @var array 187 | */ 188 | protected static $relationsData = array(); 189 | 190 | /** This class "has one model" if its ID is an FK in that model */ 191 | const HAS_ONE = 'hasOne'; 192 | 193 | /** This class "has many models" if its ID is an FK in those models */ 194 | const HAS_MANY = 'hasMany'; 195 | 196 | const HAS_MANY_THROUGH = 'hasManyThrough'; 197 | 198 | /** This class "belongs to a model" if it has a FK from that model */ 199 | const BELONGS_TO = 'belongsTo'; 200 | 201 | const BELONGS_TO_MANY = 'belongsToMany'; 202 | 203 | const MORPH_TO = 'morphTo'; 204 | 205 | const MORPH_ONE = 'morphOne'; 206 | 207 | const MORPH_MANY = 'morphMany'; 208 | 209 | const MORPH_TO_MANY = 'morphToMany'; 210 | 211 | const MORPHED_BY_MANY = 'morphedByMany'; 212 | 213 | /** 214 | * Array of relations used to verify arguments used in the {@link $relationsData} 215 | * 216 | * @var array 217 | */ 218 | protected static $relationTypes = array( 219 | self::HAS_ONE, self::HAS_MANY, self::HAS_MANY_THROUGH, 220 | self::BELONGS_TO, self::BELONGS_TO_MANY, 221 | self::MORPH_TO, self::MORPH_ONE, self::MORPH_MANY, 222 | self::MORPH_TO_MANY, self::MORPHED_BY_MANY 223 | ); 224 | 225 | /** 226 | * Create a new Ardent model instance. 227 | * 228 | * @param array $attributes 229 | * @return \LaravelArdent\Ardent\Ardent 230 | */ 231 | public function __construct(array $attributes = array()) { 232 | parent::__construct($attributes); 233 | $this->validationErrors = new MessageBag; 234 | } 235 | 236 | /** 237 | * The "booting" method of the model. 238 | * Overrided to attach before/after method hooks into the model events. 239 | * 240 | * @see \Illuminate\Database\Eloquent\Model::boot() 241 | * @return void 242 | */ 243 | public static function boot() { 244 | parent::boot(); 245 | 246 | $myself = get_called_class(); 247 | $hooks = array('before' => 'ing', 'after' => 'ed'); 248 | $radicals = array('sav', 'validat', 'creat', 'updat', 'delet'); 249 | 250 | foreach ($radicals as $rad) { 251 | foreach ($hooks as $hook => $event) { 252 | $method = $hook.ucfirst($rad).'e'; 253 | if (method_exists($myself, $method)) { 254 | $eventMethod = $rad.$event; 255 | self::$eventMethod(function($model) use ($method){ 256 | return $model->$method($model); 257 | }); 258 | } 259 | } 260 | } 261 | } 262 | 263 | public function getObservableEvents() { 264 | return array_merge( 265 | parent::getObservableEvents(), 266 | array('validating', 'validated') 267 | ); 268 | } 269 | 270 | /** 271 | * Register a validating model event with the dispatcher. 272 | * 273 | * @param Closure|string $callback 274 | * @return void 275 | */ 276 | public static function validating($callback) { 277 | static::registerModelEvent('validating', $callback); 278 | } 279 | 280 | /** 281 | * Register a validated model event with the dispatcher. 282 | * 283 | * @param Closure|string $callback 284 | * @return void 285 | */ 286 | public static function validated($callback) { 287 | static::registerModelEvent('validated', $callback); 288 | } 289 | 290 | /** 291 | * Looks for the relation in the {@link $relationsData} array and does the correct magic as Eloquent would require 292 | * inside relation methods. For more information, read the documentation of the mentioned property. 293 | * 294 | * @param string $relationName the relation key, camel-case version 295 | * @return \Illuminate\Database\Eloquent\Relations\Relation 296 | * @throws \InvalidArgumentException when the first param of the relation is not a relation type constant, 297 | * or there's one or more arguments missing 298 | * @see Ardent::relationsData 299 | */ 300 | protected function handleRelationalArray($relationName) { 301 | $relation = static::$relationsData[$relationName]; 302 | $relationType = $relation[0]; 303 | $errorHeader = "Relation '$relationName' on model '".get_called_class(); 304 | 305 | if (!in_array($relationType, static::$relationTypes)) { 306 | throw new \InvalidArgumentException($errorHeader. 307 | ' should have as first param one of the relation constants of the Ardent class.'); 308 | } 309 | if (!isset($relation[1]) && $relationType != self::MORPH_TO) { 310 | throw new \InvalidArgumentException($errorHeader. 311 | ' should have at least two params: relation type and classname.'); 312 | } 313 | if (isset($relation[1]) && $relationType == self::MORPH_TO) { 314 | throw new \InvalidArgumentException($errorHeader. 315 | ' is a morphTo relation and should not contain additional arguments.'); 316 | } 317 | 318 | $verifyArgs = function (array $opt, array $req = array()) use ($relationName, &$relation, $errorHeader) { 319 | $missing = array('req' => array(), 'opt' => array()); 320 | 321 | foreach (array('req', 'opt') as $keyType) { 322 | foreach ($$keyType as $key) { 323 | if (!array_key_exists($key, $relation)) { 324 | $missing[$keyType][] = $key; 325 | } 326 | } 327 | } 328 | 329 | if ($missing['req']) { 330 | throw new \InvalidArgumentException($errorHeader.' 331 | should contain the following key(s): '.join(', ', $missing['req'])); 332 | } 333 | if ($missing['opt']) { 334 | foreach ($missing['opt'] as $include) { 335 | $relation[$include] = null; 336 | } 337 | } 338 | }; 339 | 340 | switch ($relationType) { 341 | case self::HAS_ONE: 342 | case self::HAS_MANY: 343 | $verifyArgs(['foreignKey', 'localKey']); 344 | return $this->$relationType($relation[1], $relation['foreignKey'], $relation['localKey']); 345 | 346 | case self::HAS_MANY_THROUGH: 347 | $verifyArgs(['firstKey', 'secondKey', 'localKey'], ['through']); 348 | return $this->$relationType($relation[1], $relation['through'], $relation['firstKey'], $relation['secondKey'], $relation['localKey']); 349 | 350 | case self::BELONGS_TO: 351 | $verifyArgs(['foreignKey', 'otherKey', 'relation']); 352 | return $this->$relationType($relation[1], $relation['foreignKey'], $relation['otherKey'], $relation['relation']); 353 | 354 | case self::BELONGS_TO_MANY: 355 | $verifyArgs(['table', 'foreignKey', 'otherKey', 'relation']); 356 | $relationship = $this->$relationType($relation[1], $relation['table'], $relation['foreignKey'], $relation['otherKey'], $relation['relation']); 357 | if(isset($relation['pivotKeys']) && is_array($relation['pivotKeys'])) { 358 | $relationship->withPivot($relation['pivotKeys']); 359 | } 360 | if(isset($relation['timestamps']) && $relation['timestamps']) { 361 | $relationship->withTimestamps(); 362 | } 363 | return $relationship; 364 | 365 | case self::MORPH_TO: 366 | $verifyArgs(['name', 'type', 'id']); 367 | return $this->$relationType($relation['name'], $relation['type'], $relation['id']); 368 | 369 | case self::MORPH_ONE: 370 | case self::MORPH_MANY: 371 | $verifyArgs(['type', 'id', 'localKey'], ['name']); 372 | return $this->$relationType($relation[1], $relation['name'], $relation['type'], $relation['id'], $relation['localKey']); 373 | 374 | case self::MORPH_TO_MANY: 375 | $verifyArgs(['table', 'foreignKey', 'otherKey', 'inverse'], ['name']); 376 | return $this->$relationType($relation[1], $relation['name'], $relation['table'], $relation['foreignKey'], $relation['otherKey'], $relation['inverse']); 377 | 378 | case self::MORPHED_BY_MANY: 379 | $verifyArgs(['table', 'foreignKey', 'otherKey'], ['name']); 380 | return $this->$relationType($relation[1], $relation['name'], $relation['table'], $relation['foreignKey'], $relation['otherKey']); 381 | } 382 | } 383 | 384 | /** 385 | * Handle dynamic method calls into the method. 386 | * Overrided from {@link Eloquent} to implement recognition of the {@link $relationsData} array. 387 | * 388 | * @param string $method 389 | * @param array $parameters 390 | * @return Relation|Builder|mixed 391 | */ 392 | public function __call($method, $parameters) { 393 | if (array_key_exists($method, static::$relationsData)) { 394 | return $this->handleRelationalArray($method); 395 | } 396 | 397 | return parent::__call($method, $parameters); 398 | } 399 | 400 | 401 | /** 402 | * Define an inverse one-to-one or many relationship. 403 | * Overriden from {@link Eloquent\Model} to allow the usage of the intermediary methods to handle the {@link 404 | * $relationsData} array. 405 | * 406 | * @param string $related 407 | * @param string $foreignKey 408 | * @param string $otherKey 409 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 410 | */ 411 | public function belongsTo($related, $foreignKey = NULL, $otherKey = NULL, $relation = NULL) { 412 | 413 | // If no foreign key was supplied, we can use a backtrace to guess the proper 414 | // foreign key name by using the name of the relationship function, which 415 | // when combined with an "_id" should conventionally match the columns. 416 | if (is_null($relation)) { 417 | $backtrace = debug_backtrace(false, 4); 418 | if ($backtrace[1]['function'] == 'handleRelationalArray') { 419 | $relation = $backtrace[1]['args'][0]; 420 | } else { 421 | $relation = $backtrace[3]['function']; 422 | } 423 | } 424 | 425 | if (is_null($foreignKey)) { 426 | $foreignKey = snake_case($relation).'_id'; 427 | } 428 | 429 | // Once we have the foreign key names, we'll just create a new Eloquent query 430 | // for the related models and returns the relationship instance which will 431 | // actually be responsible for retrieving and hydrating every relations. 432 | $instance = new $related; 433 | 434 | $otherKey = $otherKey ?: $instance->getKeyName(); 435 | 436 | $query = $instance->newQuery(); 437 | 438 | return new BelongsTo($query, $this, $foreignKey, $otherKey, $relation); 439 | } 440 | 441 | /** 442 | * Define an polymorphic, inverse one-to-one or many relationship. 443 | * Overriden from {@link Eloquent\Model} to allow the usage of the intermediary methods to handle the {@link 444 | * $relationsData} array. 445 | * 446 | * @param string $name 447 | * @param string $type 448 | * @param string $id 449 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 450 | */ 451 | public function morphTo($name = null, $type = null, $id = null, $ownerKey = null) { 452 | // If no name is provided, we will use the backtrace to get the function name 453 | // since that is most likely the name of the polymorphic interface. We can 454 | // use that to get both the class and foreign key that will be utilized. 455 | if (is_null($name)) 456 | { 457 | $backtrace = debug_backtrace(false); 458 | $caller = ($backtrace[1]['function'] == 'handleRelationalArray')? $backtrace[3] : $backtrace[1]; 459 | 460 | $name = snake_case($caller['function']); 461 | } 462 | 463 | // Next we will guess the type and ID if necessary. The type and IDs may also 464 | // be passed into the function so that the developers may manually specify 465 | // them on the relations. Otherwise, we will just make a great estimate. 466 | list($type, $id) = $this->getMorphs($name, $type, $id); 467 | 468 | $class = $this->$type; 469 | 470 | return $this->belongsTo($class, $id); 471 | } 472 | 473 | /** 474 | * Get an attribute from the model. 475 | * Overrided from {@link Eloquent} to implement recognition of the {@link $relationsData} array. 476 | * 477 | * @param string $key 478 | * @return mixed 479 | */ 480 | public function getAttribute($key) { 481 | $attr = parent::getAttribute($key); 482 | 483 | if ($attr === null) { 484 | $camelKey = camel_case($key); 485 | if (array_key_exists($camelKey, static::$relationsData)) { 486 | $this->relations[$key] = $this->$camelKey()->getResults(); 487 | return $this->relations[$key]; 488 | } 489 | } 490 | 491 | return $attr; 492 | } 493 | 494 | /** 495 | * Configures Ardent to be used outside of Laravel - correctly setting Eloquent and Validation modules. 496 | * @todo Should allow for additional language files. Would probably receive a Translator instance as an optional argument, or a list of translation files. 497 | * 498 | * @param array $connection Connection info used by {@link \Illuminate\Database\Capsule\Manager::addConnection}. 499 | * Should contain driver, host, port, database, username, password, charset and collation. 500 | */ 501 | public static function configureAsExternal(array $connection, $lang = 'en') { 502 | $db = new DatabaseCapsule; 503 | $db->addConnection($connection); 504 | $db->setEventDispatcher(new Dispatcher(new Container)); 505 | //TODO: configure a cache manager (as an option) 506 | 507 | // Make this Capsule instance available globally via static methods 508 | $db->setAsGlobal(); 509 | 510 | $db->bootEloquent(); 511 | 512 | $translator = new Translator($lang); 513 | $translator->addLoader('file_loader', new PhpFileLoader()); 514 | $translator->addResource('file_loader', 515 | dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR.$lang. 516 | DIRECTORY_SEPARATOR.'validation.php', $lang); 517 | 518 | self::$external = true; 519 | self::$validationFactory = new ValidationFactory($translator); 520 | self::$validationFactory->setPresenceVerifier(new DatabasePresenceVerifier($db->getDatabaseManager())); 521 | 522 | self::$hasher = new BcryptHasher(); 523 | } 524 | 525 | /** 526 | * Instatiates the validator used by the validation process, depending if the class is being used inside or 527 | * outside of Laravel. 528 | * 529 | * @param $data 530 | * @param $rules 531 | * @param $customMessages 532 | * @param $customAttributes 533 | * @return \Illuminate\Validation\Validator 534 | * @see Ardent::$externalValidator 535 | */ 536 | protected static function makeValidator($data, $rules, $customMessages, $customAttributes) { 537 | return self::$external? 538 | self::$validationFactory->make($data, $rules, $customMessages, $customAttributes) : 539 | Validator::make($data, $rules, $customMessages, $customAttributes); 540 | } 541 | 542 | /** 543 | * Validate the model instance 544 | * 545 | * @param array $rules Validation rules 546 | * @param array $customMessages Custom error messages 547 | * @param array $customAttributes Custom attributes 548 | * @return bool 549 | * @throws InvalidModelException 550 | */ 551 | public function validate(array $rules = array(), array $customMessages = array(), array $customAttributes = array()) { 552 | if ($this->fireModelEvent('validating') === false) { 553 | if ($this->throwOnValidation) { 554 | throw new InvalidModelException($this); 555 | } else { 556 | return false; 557 | } 558 | } 559 | 560 | // check for overrides, then remove any empty rules 561 | $rules = (empty($rules))? static::$rules : $rules; 562 | foreach ($rules as $field => $rls) { 563 | if ($rls == '') { 564 | unset($rules[$field]); 565 | } 566 | } 567 | 568 | if (empty($rules)) { 569 | $success = true; 570 | } else { 571 | $customMessages = (empty($customMessages))? static::$customMessages : $customMessages; 572 | $customAttributes = (empty($customAttributes))? static::$customAttributes : $customAttributes; 573 | 574 | if ($this->forceEntityHydrationFromInput || (empty($this->attributes) && $this->autoHydrateEntityFromInput)) { 575 | $this->fill(Input::all()); 576 | } 577 | 578 | $data = $this->getAttributes(); // the data under validation 579 | 580 | // perform validation 581 | $this->validator = static::makeValidator($data, $rules, $customMessages, $customAttributes); 582 | $success = $this->validator->passes(); 583 | 584 | if ($success) { 585 | // if the model is valid, unset old errors 586 | if ($this->validationErrors === null || $this->validationErrors->count() > 0) { 587 | $this->validationErrors = new MessageBag; 588 | } 589 | } else { 590 | // otherwise set the new ones 591 | $this->validationErrors = $this->validator->messages(); 592 | 593 | // stash the input to the current session 594 | if (!self::$external && Input::hasSession()) { 595 | Input::flash(); 596 | } 597 | } 598 | } 599 | 600 | $this->fireModelEvent('validated', false); 601 | 602 | if (!$success && $this->throwOnValidation) { 603 | throw new InvalidModelException($this); 604 | } 605 | 606 | return $success; 607 | } 608 | 609 | /** 610 | * Save the model to the database. Is used by {@link save()} and {@link forceSave()} as a way to DRY code. 611 | * 612 | * @param array $rules 613 | * @param array $customMessages 614 | * @param array $options 615 | * @param Closure $beforeSave 616 | * @param Closure $afterSave 617 | * @param bool $force Forces saving invalid data. 618 | * 619 | * @return bool 620 | * @see Ardent::save() 621 | * @see Ardent::forceSave() 622 | */ 623 | protected function internalSave(array $rules = array(), 624 | array $customMessages = array(), 625 | array $options = array(), 626 | Closure $beforeSave = null, 627 | Closure $afterSave = null, 628 | $force = false 629 | ) { 630 | if ($beforeSave) { 631 | self::saving($beforeSave); 632 | } 633 | if ($afterSave) { 634 | self::saved($afterSave); 635 | } 636 | 637 | $valid = $this->validateUniques($rules, $customMessages); 638 | 639 | if ($force || $valid) { 640 | return $this->performSave($options); 641 | } else { 642 | return false; 643 | } 644 | } 645 | 646 | /** 647 | * Save the model to the database. 648 | * 649 | * @param array $rules 650 | * @param array $customMessages 651 | * @param array $options 652 | * @param Closure $beforeSave 653 | * @param Closure $afterSave 654 | * 655 | * @return bool 656 | * @see Ardent::forceSave() 657 | */ 658 | public function save(array $rules = array(), 659 | array $customMessages = array(), 660 | array $options = array(), 661 | Closure $beforeSave = null, 662 | Closure $afterSave = null 663 | ) { 664 | return $this->internalSave($rules, $customMessages, $options, $beforeSave, $afterSave, false); 665 | } 666 | 667 | /** 668 | * Force save the model even if validation fails. 669 | * 670 | * @param array $rules 671 | * @param array $customMessages 672 | * @param array $options 673 | * @param Closure $beforeSave 674 | * @param Closure $afterSave 675 | * @return bool 676 | * @see Ardent::save() 677 | */ 678 | public function forceSave(array $rules = array(), 679 | array $customMessages = array(), 680 | array $options = array(), 681 | Closure $beforeSave = null, 682 | Closure $afterSave = null 683 | ) { 684 | return $this->internalSave($rules, $customMessages, $options, $beforeSave, $afterSave, true); 685 | } 686 | 687 | 688 | /** 689 | * Add the basic purge filters 690 | * 691 | * @return void 692 | */ 693 | protected function addBasicPurgeFilters() { 694 | if ($this->purgeFiltersInitialized) { 695 | return; 696 | } 697 | 698 | $this->purgeFilters[] = function ($attributeKey) { 699 | // disallow password confirmation fields 700 | if (Str::endsWith($attributeKey, '_confirmation')) { 701 | return false; 702 | } 703 | 704 | // "_method" is used by Illuminate\Routing\Router to simulate custom HTTP verbs 705 | if (strcmp($attributeKey, '_method') === 0) { 706 | return false; 707 | } 708 | 709 | // "_token" is used by Illuminate\Html\FormBuilder to add CSRF protection 710 | if (strcmp($attributeKey, '_token') === 0) { 711 | return false; 712 | } 713 | 714 | return true; 715 | }; 716 | 717 | $this->purgeFiltersInitialized = true; 718 | } 719 | 720 | /** 721 | * Removes redundant attributes from model 722 | * 723 | * @param array $array Input array 724 | * @return array 725 | */ 726 | protected function purgeArray(array $array = array()) { 727 | 728 | $result = array(); 729 | $keys = array_keys($array); 730 | 731 | $this->addBasicPurgeFilters(); 732 | 733 | if (!empty($keys) && !empty($this->purgeFilters)) { 734 | foreach ($keys as $key) { 735 | $allowed = true; 736 | 737 | foreach ($this->purgeFilters as $filter) { 738 | $allowed = $filter($key); 739 | 740 | if (!$allowed) { 741 | break; 742 | } 743 | } 744 | 745 | if ($allowed) { 746 | $result[$key] = $array[$key]; 747 | } 748 | } 749 | } 750 | 751 | return $result; 752 | } 753 | 754 | /** 755 | * Saves the model instance to database. If necessary, it will purge the model attributes 756 | * of unnecessary fields. It will also replace plain-text password fields with their hashes. 757 | * 758 | * @param array $options 759 | * @return bool 760 | */ 761 | protected function performSave(array $options) { 762 | 763 | if ($this->autoPurgeRedundantAttributes) { 764 | $this->attributes = $this->purgeArray($this->getAttributes()); 765 | } 766 | 767 | if ($this->autoHashPasswordAttributes) { 768 | $this->attributes = $this->hashPasswordAttributes($this->getAttributes(), static::$passwordAttributes); 769 | } 770 | 771 | return parent::save($options); 772 | } 773 | 774 | /** 775 | * Get validation error message collection for the Model 776 | * 777 | * @return \Illuminate\Support\MessageBag 778 | */ 779 | public function errors() { 780 | return $this->validationErrors; 781 | } 782 | 783 | /** 784 | * Hashes the password, working without the Hash facade if this is an instance outside of Laravel. 785 | * @param $value 786 | * @return string 787 | */ 788 | protected function hashPassword($value) { 789 | return self::$external? self::$hasher->make($value) : Hash::make($value); 790 | } 791 | 792 | /** 793 | * Automatically replaces all plain-text password attributes (listed in $passwordAttributes) 794 | * with hash checksum. 795 | * 796 | * @param array $attributes 797 | * @param array $passwordAttributes 798 | * @return array 799 | */ 800 | protected function hashPasswordAttributes(array $attributes = array(), array $passwordAttributes = array()) { 801 | 802 | if (empty($passwordAttributes) || empty($attributes)) { 803 | return $attributes; 804 | } 805 | 806 | $result = array(); 807 | foreach ($attributes as $key => $value) { 808 | 809 | if (in_array($key, $passwordAttributes) && !is_null($value)) { 810 | if ($value != $this->getOriginal($key)) { 811 | $result[$key] = $this->hashPassword($value); 812 | } 813 | } else { 814 | $result[$key] = $value; 815 | } 816 | } 817 | 818 | return $result; 819 | } 820 | 821 | /** 822 | * Appends the model ID to the 'unique' rules given. The resulting array can 823 | * then be fed to a Ardent save so that unchanged values don't flag a validation 824 | * issue. It can also be used with {@link Illuminate\Foundation\Http\FormRequest} 825 | * to painlessly validate model requests. 826 | * Rules can be in either strings with pipes or arrays, but the returned rules 827 | * are in arrays. 828 | * @param array $rules 829 | * @return array Rules with exclusions applied 830 | */ 831 | public function buildUniqueExclusionRules(array $rules = array()) { 832 | 833 | if (!count($rules)) 834 | $rules = static::$rules; 835 | 836 | foreach ($rules as $field => &$ruleset) { 837 | // If $ruleset is a pipe-separated string, switch it to array 838 | $ruleset = (is_string($ruleset))? explode('|', $ruleset) : $ruleset; 839 | 840 | foreach ($ruleset as &$rule) { 841 | if (strpos($rule, 'unique:') === 0) { 842 | // Stop splitting at 4 so final param will hold optional where clause 843 | $params = explode(',', $rule, 4); 844 | 845 | $uniqueRules = array(); 846 | 847 | // Append table name if needed 848 | $table = explode(':', $params[0]); 849 | if (count($table) == 1) { 850 | $uniqueRules[1] = $this->getTable(); 851 | } else { 852 | $uniqueRules[1] = $table[1]; 853 | } 854 | 855 | // Append field name if needed 856 | if (count($params) == 1) { 857 | $uniqueRules[2] = $field; 858 | } else { 859 | $uniqueRules[2] = $params[1]; 860 | } 861 | 862 | if (isset($this->primaryKey)) { 863 | if (isset($this->{$this->primaryKey})) { 864 | $uniqueRules[3] = $this->{$this->primaryKey}; 865 | 866 | // If optional where rules are passed, append them otherwise use primary key 867 | $uniqueRules[4] = isset($params[3])? $params[3] : $this->primaryKey; 868 | } 869 | } else { 870 | if (isset($this->id)) { 871 | $uniqueRules[3] = $this->id; 872 | } 873 | } 874 | 875 | $rule = 'unique:'.implode(',', $uniqueRules); 876 | } 877 | } 878 | } 879 | 880 | return $rules; 881 | } 882 | 883 | /** 884 | * Update a model, but filter uniques first to ensure a unique validation rule 885 | * does not fire 886 | * 887 | * @param array $rules 888 | * @param array $customMessages 889 | * @param array $options 890 | * @param Closure $beforeSave 891 | * @param Closure $afterSave 892 | * @return bool 893 | */ 894 | public function updateUniques(array $rules = array(), 895 | array $customMessages = array(), 896 | array $options = array(), 897 | Closure $beforeSave = null, 898 | Closure $afterSave = null 899 | ) { 900 | $rules = $this->buildUniqueExclusionRules($rules); 901 | 902 | return $this->save($rules, $customMessages, $options, $beforeSave, $afterSave); 903 | } 904 | 905 | /** 906 | * Validates a model with unique rules properly treated. 907 | * 908 | * @param array $rules Validation rules 909 | * @param array $customMessages Custom error messages 910 | * @return bool 911 | * @see Ardent::validate() 912 | */ 913 | public function validateUniques(array $rules = array(), array $customMessages = array()) { 914 | $rules = $this->buildUniqueExclusionRules($rules); 915 | return $this->validate($rules, $customMessages); 916 | } 917 | 918 | /** 919 | * Find a model by its primary key. 920 | * If {@link $throwOnFind} is set, will use {@link findOrFail} internally. 921 | * 922 | * @param mixed $id 923 | * @param array $columns 924 | * @return Ardent|Collection 925 | */ 926 | public static function find($id, $columns = array('*')) { 927 | $debug = debug_backtrace(false); 928 | 929 | if (static::$throwOnFind && $debug[1]['function'] != 'findOrFail') { 930 | return self::findOrFail($id, $columns); 931 | } else { 932 | //mimicking Eloquent's __callStatic() + __call() behaviour so we don't loop forever 933 | return (new static)->__call('find', [$id, $columns]); 934 | } 935 | } 936 | 937 | /** 938 | * Get a new query builder for the model's table. 939 | * Overriden from {@link \Model\Eloquent} to allow for usage of {@link throwOnFind} in our {@link Builder}. 940 | * 941 | * @see Model::newQueryWithoutScopes() 942 | * @return \Illuminate\Database\Eloquent\Builder 943 | */ 944 | public function newQueryWithoutScopes() { 945 | $builder = new Builder($this->newBaseQueryBuilder()); 946 | $builder->throwOnFind = static::$throwOnFind; 947 | 948 | return $builder->setModel($this)->with($this->with); 949 | } 950 | 951 | /** 952 | * Returns the validator object created after {@link validate()}. 953 | * @return \Illuminate\Validation\Validator 954 | */ 955 | public function getValidator() { 956 | return $this->validator; 957 | } 958 | } 959 | -------------------------------------------------------------------------------- /src/Ardent/Builder.php: -------------------------------------------------------------------------------- 1 | maybeFail('find', func_get_args()); 16 | } 17 | 18 | public function first($columns = array('*')) { 19 | return $this->maybeFail('first', func_get_args()); 20 | } 21 | 22 | /** 23 | * Will test if it should run a normal method or its "orFail" version, and behave accordingly. 24 | * @param string $method called method 25 | * @param array $args given arguments 26 | * @return mixed 27 | */ 28 | protected function maybeFail($method, $args) { 29 | $debug = debug_backtrace(false); 30 | $orFail = $method.'OrFail'; 31 | $func = ($this->throwOnFind && $debug[2]['function'] != $orFail)? array($this, $orFail) : "parent::$method"; 32 | return call_user_func_array($func, $args); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Ardent/InvalidModelException.php: -------------------------------------------------------------------------------- 1 | model = $model; 28 | $this->errors = $model->errors(); 29 | } 30 | 31 | /** 32 | * Returns the model with invalid attributes. 33 | * @return Ardent 34 | */ 35 | public function getModel() { 36 | return $this->model; 37 | } 38 | 39 | /** 40 | * Returns directly the message bag instance with the model's errors. 41 | * @return \Illuminate\Support\MessageBag 42 | */ 43 | public function getErrors() { 44 | return $this->errors; 45 | } 46 | } -------------------------------------------------------------------------------- /src/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | array( 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Validation Language Lines 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The following language lines contain the default error messages used by 11 | | the validator class. Some of these rules have multiple versions such 12 | | such as the size rules. Feel free to tweak each of these messages. 13 | | 14 | */ 15 | 16 | "accepted" => "The :attribute must be accepted.", 17 | "active_url" => "The :attribute is not a valid URL.", 18 | "after" => "The :attribute must be a date after :date.", 19 | "alpha" => "The :attribute may only contain letters.", 20 | "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", 21 | "alpha_num" => "The :attribute may only contain letters and numbers.", 22 | "before" => "The :attribute must be a date before :date.", 23 | "between" => array( 24 | "numeric" => "The :attribute must be between :min - :max.", 25 | "file" => "The :attribute must be between :min - :max kilobytes.", 26 | "string" => "The :attribute must be between :min - :max characters.", 27 | ), 28 | "confirmed" => "The :attribute confirmation does not match.", 29 | "date" => "The :attribute is not a valid date.", 30 | "date_format" => "The :attribute does not match the format :format.", 31 | "different" => "The :attribute and :other must be different.", 32 | "digits" => "The :attribute must be :digits digits.", 33 | "digits_between" => "The :attribute must be between :min and :max digits.", 34 | "email" => "The :attribute format is invalid.", 35 | "exists" => "The selected :attribute is invalid.", 36 | "image" => "The :attribute must be an image.", 37 | "in" => "The selected :attribute is invalid.", 38 | "integer" => "The :attribute must be an integer.", 39 | "ip" => "The :attribute must be a valid IP address.", 40 | "max" => array( 41 | "numeric" => "The :attribute may not be greater than :max.", 42 | "file" => "The :attribute may not be greater than :max kilobytes.", 43 | "string" => "The :attribute may not be greater than :max characters.", 44 | ), 45 | "mimes" => "The :attribute must be a file of type: :values.", 46 | "min" => array( 47 | "numeric" => "The :attribute must be at least :min.", 48 | "file" => "The :attribute must be at least :min kilobytes.", 49 | "string" => "The :attribute must be at least :min characters.", 50 | ), 51 | "not_in" => "The selected :attribute is invalid.", 52 | "numeric" => "The :attribute must be a number.", 53 | "regex" => "The :attribute format is invalid.", 54 | "required" => "The :attribute field is required.", 55 | "required_if" => "The :attribute field is required when :other is :value.", 56 | "required_with" => "The :attribute field is required when :values is present.", 57 | "required_without" => "The :attribute field is required when :values is not present.", 58 | "same" => "The :attribute and :other must match.", 59 | "size" => array( 60 | "numeric" => "The :attribute must be :size.", 61 | "file" => "The :attribute must be :size kilobytes.", 62 | "string" => "The :attribute must be :size characters.", 63 | ), 64 | "unique" => "The :attribute has already been taken.", 65 | "url" => "The :attribute format is invalid.", 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Custom Validation Language Lines 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Here you may specify custom validation messages for attributes using the 73 | | convention "attribute.rule" to name the lines. This makes it quick to 74 | | specify a specific custom language line for a given attribute rule. 75 | | 76 | */ 77 | 78 | 'custom' => array(), 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Custom Validation Attributes 83 | |-------------------------------------------------------------------------- 84 | | 85 | | The following language lines are used to swap attribute place-holders 86 | | with something more reader friendly such as E-Mail Address instead 87 | | of "email". This simply helps us make messages a little cleaner. 88 | | 89 | */ 90 | 91 | 'attributes' => array(), 92 | ) 93 | ); 94 | -------------------------------------------------------------------------------- /src/lang/pt-br/validation.php: -------------------------------------------------------------------------------- 1 | array( 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Validation Language Lines 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The following language lines contain the default error messages used by 11 | | the validator class. Some of these rules have multiple versions such 12 | | such as the size rules. Feel free to tweak each of these messages. 13 | | 14 | */ 15 | 16 | "accepted" => "O campo :attribute deve ser marcado", 17 | "active_url" => "O campo :attribute não é uma URL válida.", 18 | "after" => "O campo :attribute deve ser uma data após :date.", 19 | "alpha" => "O campo :attribute só pode conter letras.", 20 | "alpha_dash" => "O campo :attribute só pode conter letras, números e hífens..", 21 | "alpha_num" => "O campo :attribute só pode conter letras e números", 22 | "before" => "O campo :attribute só pode conter uma data antes de :date.", 23 | "between" => array( 24 | "numeric" => "O campo :attribute deve conter valores entre :min - :max.", 25 | "file" => "O campo :attribute deve ter o tamanho entre :min - :max kilobytes.", 26 | "string" => "O campo :attribute deve ter entre :min - :max caracteres.", 27 | ), 28 | "confirmed" => "A confirmação do campo :attribute não corresponde.", 29 | "date" => "O campo :attribute contém uma data inválida.", 30 | "date_format" => "O campo :attribute não conrrespode ao formato :format.", 31 | "different" => "Os campos :attribute e :other devem ser diferentes.", 32 | "digits" => "O campo :attribute deve ser :digits digitos.", 33 | "digits_between" => "O campo :attribute deve estar entre :min e :max digitos.", 34 | "email" => "O campo :attribute tem formato inválido.", 35 | "exists" => "O campo selecionado :attribute é inválido.", 36 | "image" => "O campo :attribute deve conter uma imagem.", 37 | "in" => "O valor selecionado em :attribute é invalido.", 38 | "integer" => "O campo :attribute deve conter um valor númerico.", 39 | "ip" => "O campo :attribute deve conter um endereço de IP válido.", 40 | "max" => array( 41 | "numeric" => "O campo :attribute não pode ser maior que :max.", 42 | "file" => "O campo :attribute não pode ser maior que :max kilobytes.", 43 | "string" => "O campo :attribute não pode ser maior que :max caracteres.", 44 | ), 45 | "mimes" => "O campo :attribute deve conter um arquivo do tipo: :values.", 46 | "min" => array( 47 | "numeric" => "O campo :attribute deve ser no mínimo :min.", 48 | "file" => "O campo :attribute deve ser no mínimo :min kilobytes.", 49 | "string" => "O campo :attribute deve ser no mínimo :min caracteres.", 50 | ), 51 | "not_in" => "O campo :attribute selecionado é inválido.", 52 | "numeric" => "O campo :attribute deve ser númerico.", 53 | "regex" => "O campo :attribute é inválido.", 54 | "required" => "O campo :attribute é obrigatório.", 55 | "required_if" => "O campo :attribute é obrigatório quando :other é :value.", 56 | "required_with" => "O campo :attribute é obrigatório quando :values está presente.", 57 | "required_without" => "O campo :attribute é obrigatório quando :values não está presente.", 58 | "same" => "Os campos :attribute e :other devem conrrespoder.", 59 | "size" => array( 60 | "numeric" => "O campo :attribute deve ser :size.", 61 | "file" => "O campo :attribute deve ser de :size kilobytes.", 62 | "string" => "O campo :attribute deve ser de :size caracteres.", 63 | ), 64 | "unique" => "O campo :attribute já existe.", 65 | "url" => "O campo :attribute tem formato inválido.", 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Custom Validation Language Lines 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Here you may specify custom validation messages for attributes using the 73 | | convention "attribute.rule" to name the lines. This makes it quick to 74 | | specify a specific custom language line for a given attribute rule. 75 | | 76 | */ 77 | 78 | 'custom' => array(), 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Custom Validation Attributes 83 | |-------------------------------------------------------------------------- 84 | | 85 | | The following language lines are used to swap attribute place-holders 86 | | with something more reader friendly such as E-Mail Address instead 87 | | of "email". This simply helps us make messages a little cleaner. 88 | | 89 | */ 90 | 91 | 'attributes' => array(), 92 | ) 93 | ); 94 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-ardent/ardent/ae8983fa82060ed9280cc6a2c9b267a966124b0f/tests/.gitkeep --------------------------------------------------------------------------------