├── .styleci.yml ├── .gitignore ├── tests ├── bootstrap.php ├── MockModel.php └── ModelSchemaTest.php ├── ISSUE_TEMPLATE.md ├── .travis.yml ├── .codeclimate.yml ├── phpmd.xml ├── phpunit.xml.dist ├── LICENSE ├── composer.json ├── src ├── Concerns │ ├── GuardsAttributes.php │ ├── HidesAttributes.php │ └── HasAttributes.php ├── Exceptions │ └── ValidationException.php └── Model.php ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /build 3 | /.phpunit.result.cache -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | ./cc-test-reporter 10 | - chmod +x ./cc-test-reporter 11 | - ./cc-test-reporter before-build 12 | after_script: 13 | - ./cc-test-reporter after-build -t clover --exit-code $TRAVIS_TEST_RESULT 14 | allowed_failures: 15 | - 7.4snapshot -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | checks: 3 | similar-code: 4 | enabled: true 5 | config: 6 | languages: 7 | - php 8 | identical-code: 9 | enabled: true 10 | config: 11 | languages: 12 | - php 13 | plugins: 14 | phpcodesniffer: 15 | enabled: true 16 | fixme: 17 | enabled: true 18 | phpmd: 19 | enabled: true 20 | checks: 21 | Controversial/CamelCaseParameterName: 22 | enabled: false 23 | Controversial/CamelCaseVariableName: 24 | enabled: false 25 | Controversial/CamelCasePropertyName: 26 | enabled: false 27 | CleanCode/StaticAccess: 28 | enabled: false 29 | Naming/ShortMethodName: 30 | enabled: false 31 | exclude_patterns: 32 | - "tests/" -------------------------------------------------------------------------------- /phpmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | . 20 | 21 | tests 22 | vendor 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) H&H Digital 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hnhdigital-os/laravel-model-schema", 3 | "description": "Changes how the Eloquent Model provides attributes.", 4 | "keywords": ["laravel","illuminate","attributes"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Rocco Howard", 9 | "email": "rocco@hnh.digital" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.0", 14 | "laravel/framework": "^9.21|^10.0", 15 | "hnhdigital-os/laravel-null-carbon": "~3.0" 16 | }, 17 | "require-dev": { 18 | "illuminate/database": "^9.21|^10.0", 19 | "orchestra/testbench": "^7.0|^8.0", 20 | "phpunit/phpunit": "^9.3" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "HnhDigital\\ModelSchema\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "HnhDigital\\ModelSchema\\Tests\\": "tests/" 30 | } 31 | }, 32 | "config": { 33 | "optimize-autoloader": true, 34 | "preferred-install": "dist", 35 | "process-timeout": 2000, 36 | "sort-packages": true 37 | }, 38 | "prefer-stable": true, 39 | "minimum-stability" : "dev" 40 | } 41 | -------------------------------------------------------------------------------- /src/Concerns/GuardsAttributes.php: -------------------------------------------------------------------------------- 1 | getAttributesFromSchema('fillable', false, true); 15 | } 16 | 17 | /** 18 | * Set the fillable attributes for the model. 19 | * 20 | * @param array $fillable 21 | * @return $this 22 | */ 23 | public function fillable(array $fillable) 24 | { 25 | $this->setSchema('fillable', $fillable, true, true, false); 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * Get the guarded attributes for the model. 32 | * 33 | * @return array 34 | */ 35 | public function getGuarded() 36 | { 37 | $guarded_create = ! $this->exists ? $this->getAttributesFromSchema('guarded-create', false, true) : []; 38 | $guarded_update = $this->exists ? $this->getAttributesFromSchema('guarded-update', false, true) : []; 39 | $guarded = $this->getAttributesFromSchema('guarded', false, true); 40 | 41 | return array_merge($guarded_create, $guarded_update, $guarded); 42 | } 43 | 44 | /** 45 | * Set the guarded attributes for the model. 46 | * 47 | * @param array $guarded 48 | * @return $this 49 | */ 50 | public function guard(array $guarded) 51 | { 52 | $this->setSchema('guarded', $guarded, true, true, false); 53 | 54 | return $this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We welcome everyone to submit pull requests with: 4 | 5 | * fixes for issues 6 | * change suggestions 7 | * updating of documentation 8 | 9 | Please NOTE: 10 | 11 | Not every pull request will automatically be accepted. I will review each carefully to make sure it is in line with the direction I want the package to continue in. This might mean that some pull requests are not accepted, or might stay unmerged until a place for them can be determined. 12 | 13 | ## Coding standards 14 | 15 | You MUST use the following coding standards: 16 | 17 | * [PSR-1](http://www.php-fig.org/psr/1/) 18 | * [PSR-2](http://www.php-fig.org/psr/2/) 19 | * [PSR-4](http://www.php-fig.org/psr/4/) 20 | * [PSR-12](http://www.php-fig.org/psr/12/) 21 | 22 | We use StyleCI to ensure these coding standards are consistently achieved. 23 | 24 | ## Documentation 25 | 26 | You SHOULD update any relevant documentation. 27 | 28 | ## Making changes 29 | 30 | You MUST do the following: 31 | 32 | * Write commit messages that make sense and in past tense. 33 | * Write (or update) unit tests. 34 | * Run `composer test` and ensure everything passes. 35 | 36 | We use Travis and Code Climate to help us achieve successful unit testing, code quality, and coverage. 37 | 38 | ## Pull requests 39 | 40 | You SHOULD do the following when preparing your request: 41 | 42 | * Rebase your branch before submitting pull request 43 | * Add a descriptive header that explains in a single sentence what problem the PR solves. 44 | * Add a detailed description with animated screen-grab GIFs visualizing how it works. 45 | * Explain why you think it should be implemented one way vs. another, highlight performance improvements, etc. 46 | 47 | Thanks! 48 | [Rocco Howard](https://github.com/RoccoHoward) -------------------------------------------------------------------------------- /tests/MockModel.php: -------------------------------------------------------------------------------- 1 | attributes : $this->attributes[$key]; 14 | } 15 | 16 | /** 17 | * Describes the model. 18 | * 19 | * @var array 20 | */ 21 | protected static $schema = [ 22 | 'id' => [ 23 | 'cast' => 'integer', 24 | 'guarded' => true, 25 | ], 26 | 'uuid' => [ 27 | 'cast' => 'uuid', 28 | 'guarded' => true, 29 | 'rules' => 'nullable', 30 | ], 31 | 'name' => [ 32 | 'cast' => 'string', 33 | 'rules' => 'required|min:2|max:255', 34 | 'fillable' => true, 35 | ], 36 | 'is_alive' => [ 37 | 'cast' => 'boolean', 38 | 'default' => true, 39 | ], 40 | 'enable_notifications' => [ 41 | 'cast' => 'boolean', 42 | 'default' => false, 43 | 'auth' => 'check_role', 44 | ], 45 | 'is_admin' => [ 46 | 'cast' => 'boolean', 47 | 'default' => false, 48 | ], 49 | 'created_at' => [ 50 | 'cast' => 'datetime', 51 | 'guarded-update' => true, 52 | 'hidden' => true, 53 | ], 54 | 'updated_at' => [ 55 | 'cast' => 'datetime', 56 | 'hidden' => true, 57 | ], 58 | 'deleted_at' => [ 59 | 'cast' => 'datetime', 60 | 'hidden' => true, 61 | 'rules' => 'nullable', 62 | ], 63 | ]; 64 | 65 | /** 66 | * Protect the Is Admin attribute. 67 | * 68 | * There would be logic in here to determine the user or role. 69 | */ 70 | public function authIsAdminAttribute() 71 | { 72 | return false; 73 | } 74 | 75 | /** 76 | * Set attribute. 77 | * 78 | * @param bool $value 79 | * @return void 80 | */ 81 | public function setIsAliveAttribute($value) 82 | { 83 | $this->attributes['is_alive'] = $this->castAsBool($value); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * Check role. 90 | * 91 | * @return bool 92 | */ 93 | public function authCheckRole() 94 | { 95 | return false; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Exceptions/ValidationException.php: -------------------------------------------------------------------------------- 1 | false, 18 | 'feedback' => '', 19 | 'fields' => [], 20 | 'changes' => [], 21 | ]; 22 | 23 | /** 24 | * Exception constructor. 25 | * 26 | * @param string $message 27 | * @param int $code 28 | * @param Exception|null $previous 29 | * @param Validator|null $validator 30 | */ 31 | public function __construct($message = null, $code = 0, Exception $previous = null, $validator = null) 32 | { 33 | $this->validator = $validator; 34 | 35 | parent::__construct($message, $code, $previous); 36 | } 37 | 38 | /** 39 | * Get the validator. 40 | * 41 | * @return Validator 42 | */ 43 | public function getValidator() 44 | { 45 | return $this->validator; 46 | } 47 | 48 | public function setResponse($response) 49 | { 50 | self::$response = $response; 51 | } 52 | 53 | /** 54 | * Get a response to return. 55 | * 56 | * @return string|array 57 | */ 58 | public function getResponse($route, $parameters = [], $config = []) 59 | { 60 | // Copy standard response. 61 | $response = self::$response; 62 | 63 | // Fill the response. 64 | Arr::set($response, 'is_error', true); 65 | Arr::set($response, 'message', $this->getMessage()); 66 | Arr::set($response, 'fields', array_keys($this->validator->errors()->messages())); 67 | Arr::set($response, 'feedback', $this->validator->errors()->all()); 68 | Arr::set($response, 'errors', $this->validator->errors()); 69 | 70 | if (Arr::has($config, 'feedback.html')) { 71 | Arr::set( 72 | $response, 73 | 'feedback', 74 | '' 75 | ); 76 | } 77 | 78 | // JSON response required. 79 | if (request()->ajax() || request()->wantsJson()) { 80 | return response() 81 | ->json($response, 422); 82 | } 83 | 84 | // Redirect response, flash to session. 85 | session()->flash('is_error', true); 86 | session()->flash('message', Arr::get($response, 'message', '')); 87 | session()->flash('feedback', Arr::get($response, 'feedback', '')); 88 | session()->flash('fields', Arr::get($response, 'fields', [])); 89 | 90 | // Redirect to provided route. 91 | return redirect() 92 | ->route($route, $parameters) 93 | ->withErrors($this->getValidator()) 94 | ->withInput(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@genealabs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/Concerns/HidesAttributes.php: -------------------------------------------------------------------------------- 1 | getAttributesFromSchema('hidden', false, true); 17 | } 18 | 19 | /** 20 | * Set the hidden attributes for the model. 21 | * 22 | * @param array $hidden 23 | * @return $this 24 | */ 25 | public function setHidden(array $hidden) 26 | { 27 | $this->setSchema('hidden', $hidden, true, true, false); 28 | 29 | return $this; 30 | } 31 | 32 | /** 33 | * Add hidden attributes for the model. 34 | * 35 | * @param array|string|null $attributes 36 | * @return $this 37 | */ 38 | public function addHidden($attributes = null) 39 | { 40 | $hidden = array_merge( 41 | $this->getHidden(), 42 | is_array($attributes) ? $attributes : func_get_args() 43 | ); 44 | 45 | $this->setSchema('hidden', $hidden, true, true, false); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * Get the visible attributes for the model. 52 | * 53 | * @return array 54 | */ 55 | public function getVisible() 56 | { 57 | return array_merge(array_diff( 58 | $this->getAttributesFromSchema(), 59 | $this->getAttributesFromSchema('hidden', false, true) 60 | ), $this->appends); 61 | } 62 | 63 | /** 64 | * Set the visible attributes for the model. 65 | * 66 | * @param array $visible 67 | * @return $this 68 | */ 69 | public function setVisible(array $visible) 70 | { 71 | $this->setSchema('hidden', $visible, false, true, true); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Add visible attributes for the model. 78 | * 79 | * @param array|string|null $attributes 80 | * @return $this 81 | */ 82 | public function addVisible($attributes = null) 83 | { 84 | $this->setSchema('hidden', is_array($attributes) ? $attributes : func_get_args(), false); 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * Make the given, typically hidden, attributes visible. 91 | * 92 | * @param array|string $attributes 93 | * @return $this 94 | */ 95 | public function makeVisible($attributes) 96 | { 97 | $hidden = array_diff($this->getHidden(), Arr::wrap($attributes)); 98 | 99 | $this->setSchema('hidden', $hidden, true, true, false); 100 | 101 | if (! empty($this->getVisible())) { 102 | $this->addVisible($attributes); 103 | } 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Make the given, typically visible, attributes hidden. 110 | * 111 | * @param array|string $attributes 112 | * @return $this 113 | */ 114 | public function makeHidden($attributes) 115 | { 116 | $attributes = Arr::wrap($attributes); 117 | 118 | $this->setSchema('visible', array_diff($this->getVisible(), $attributes), true); 119 | $this->setSchema('hidden', array_unique(array_merge($this->getHidden(), $attributes)), true); 120 | 121 | return $this; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ___ ___ _ _ _____ _ 3 | | \/ | | | | / ___| | | 4 | | . . | ___ __| | ___| \ `--. ___| |__ ___ _ __ ___ __ _ 5 | | |\/| |/ _ \ / _` |/ _ \ |`--. \/ __| '_ \ / _ \ '_ ` _ \ / _` | 6 | | | | | (_) | (_| | __/ /\__/ / (__| | | | __/ | | | | | (_| | 7 | \_| |_/\___/ \__,_|\___|_\____/ \___|_| |_|\___|_| |_| |_|\__,_| 8 | ``` 9 | 10 | Combines the casts, fillable, and hidden properties into a single schema array property amongst other features. 11 | 12 | Other features include: 13 | 14 | * Custom configuration entries against your model attributes. 15 | * Store your attribute rules against your model and store and validate your model data automatically. 16 | * Custom casting is now possible using this package! See [Custom casts](#custom-casts). 17 | 18 | [![Latest Stable Version](https://poser.pugx.org/hnhdigital-os/laravel-model-schema/v/stable.svg)](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [![Total Downloads](https://poser.pugx.org/hnhdigital-os/laravel-model-schema/downloads.svg)](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [![Latest Unstable Version](https://poser.pugx.org/hnhdigital-os/laravel-model-schema/v/unstable.svg)](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [![Built for Laravel](https://img.shields.io/badge/Built_for-Laravel-green.svg)](https://laravel.com/) [![License](https://poser.pugx.org/hnhdigital-os/laravel-model-schema/license.svg)](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [![Donate to this project using Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg)](https://patreon.com/RoccoHoward) 19 | 20 | This package has been developed by H&H|Digital, an Australian botique developer. Visit us at [hnh.digital](http://hnh.digital). 21 | 22 | ## Documentation 23 | 24 | * [Prerequisites](#prerequisites) 25 | * [Installation](#installation) 26 | * [Configuration](#configuration) 27 | * [Custom casts](#custom-casts) 28 | * [Contributing](#contributing) 29 | * [Reporting issues](#reporting-issues) 30 | * [Credits](#credits) 31 | * [License](#license) 32 | 33 | ## Prerequisites 34 | 35 | * PHP >= 8.0.2 36 | * Laravel >= 9.0 37 | 38 | ## Installation 39 | 40 | Via composer: 41 | 42 | `$ composer require hnhdigital-os/laravel-model-schema ~3.0` 43 | 44 | ## Configuration 45 | 46 | ### Enable the model 47 | 48 | Enable the model on any given model. 49 | 50 | ```php 51 | use HnhDigital\ModelSchema\Model; 52 | 53 | class SomeModel extends Model 54 | { 55 | 56 | } 57 | ``` 58 | 59 | We recommend implementing a shared base model that you extend. 60 | 61 | ### Convert your current properties 62 | 63 | The schema for a model is implemented using a protected property. 64 | 65 | Here's an example: 66 | 67 | ```php 68 | /** 69 | * Describe your model. 70 | * 71 | * @var array 72 | */ 73 | protected static $schema = [ 74 | 'id' => [ 75 | 'cast' => 'integer', 76 | 'guarded' => true, 77 | ], 78 | 'name' => [ 79 | 'cast' => 'string', 80 | 'rules' => 'max:255', 81 | 'fillable' => true, 82 | ], 83 | 'created_at' => [ 84 | 'cast' => 'datetime', 85 | 'guarded' => true, 86 | 'log' => false, 87 | 'hidden' => true, 88 | ], 89 | 'updated_at' => [ 90 | 'cast' => 'datetime', 91 | 'guarded' => true, 92 | 'hidden' => true, 93 | ], 94 | 'deleted_at' => [ 95 | 'cast' => 'datetime', 96 | 'rules' => 'nullable', 97 | 'hidden' => true, 98 | ], 99 | ]; 100 | ``` 101 | 102 | Ensure the parent boot occurs after your triggers so that any attribute changes are done before this packages triggers the validation. 103 | 104 | ```php 105 | /** 106 | * Boot triggers. 107 | * 108 | * @return void 109 | */ 110 | public static function boot() 111 | { 112 | self::updating(function ($model) { 113 | // Doing something. 114 | }); 115 | 116 | parent::boot(); 117 | } 118 | ``` 119 | 120 | Models implementing this package will now throw a ValidationException exception if they do not pass validation. Be sure to catch these. 121 | 122 | ```php 123 | try { 124 | $user = User::create(request()->all()); 125 | } catch (HnhDigital\ModelSchema\Exceptions\ValidationException $exception) { 126 | // Do something about the validation. 127 | 128 | // You can add things to the validator. 129 | $exception->getValidator()->errors()->add('field', 'Something is wrong with this field!'); 130 | 131 | // We've implemented a response. 132 | // This redirects the same as a validator with errors. 133 | return $exception->getResponse('user::add'); 134 | } 135 | ``` 136 | 137 | ## Custom casts 138 | 139 | This package allows the ability to add custom casts. Simply create a trait, and register the cast on boot. 140 | 141 | 142 | ```php 143 | trait ModelCastAsMoneyTrait 144 | { 145 | /** 146 | * Cast value as Money. 147 | * 148 | * @param mixed $value 149 | * 150 | * @return Money 151 | */ 152 | protected function castAsMoney($value, $currency = 'USD', $locale = 'en_US'): Money 153 | { 154 | return new Money($value, $currency, $locale); 155 | } 156 | 157 | /** 158 | * Convert the Money value back to a storable type. 159 | * 160 | * @return int 161 | */ 162 | protected function castMoneyToInt($key, $value): int 163 | { 164 | if (is_object($value)) { 165 | return (int) $value->amount(); 166 | } 167 | 168 | return (int) $value->amount(); 169 | } 170 | 171 | /** 172 | * Register the casting definitions. 173 | */ 174 | public static function bootModelCastAsMoneyTrait() 175 | { 176 | static::registerCastFromDatabase('money', 'castAsMoney'); 177 | static::registerCastToDatabase('money', 'castMoneyToInt'); 178 | static::registerCastValidator('money', 'int'); 179 | } 180 | } 181 | ``` 182 | 183 | Defining your attributes would look like this: 184 | 185 | ```php 186 | ... 187 | 188 | 'currency' => [ 189 | 'cast' => 'string', 190 | 'rules' => 'min:3|max:3', 191 | 'fillable' => true, 192 | ], 193 | 'total_amount' => [ 194 | 'cast' => 'money', 195 | 'cast-params' => '$currency:en_US', 196 | 'default' => 0, 197 | 'fillable' => true, 198 | ], 199 | ... 200 | ``` 201 | 202 | Casting parameters would include a helper function, or a local model method. 203 | 204 | eg `$currency:user_locale()`, or `$currency():$locale()` 205 | 206 | ### Available custom casts 207 | 208 | * Cast the attribute to [Money](https://github.com/hnhdigital-os/laravel-model-schema-money) 209 | 210 | ## Contributing 211 | 212 | Please review the [Contribution Guidelines](https://github.com/hnhdigital-os/laravel-model-schema/blob/master/CONTRIBUTING.md). 213 | 214 | Only PRs that meet all criterium will be accepted. 215 | 216 | ## Reporting issues 217 | 218 | When reporting issues, please fill out the included [template](https://github.com/hnhdigital-os/laravel-model-schema/blob/master/ISSUE_TEMPLATE.md) as completely as possible. Incomplete issues may be ignored or closed if there is not enough information included to be actionable. 219 | 220 | ## Code of conduct 221 | 222 | Please observe and respect all aspects of the included [Code of Conduct](https://github.com/hnhdigital-os/laravel-model-schema/blob/master/CODE_OF_CONDUCT.md). 223 | 224 | ## Credits 225 | 226 | * [Rocco Howard](https://github.com/RoccoHoward) 227 | * [All Contributors](https://github.com/hnhdigital-os/laravel-model-schema/contributors) 228 | 229 | ## License 230 | 231 | The MIT License (MIT). Please see [License File](https://github.com/hnhdigital-os/laravel-model-schema/blob/master/LICENSE) for more information. 232 | -------------------------------------------------------------------------------- /tests/ModelSchemaTest.php: -------------------------------------------------------------------------------- 1 | configureDatabase(); 36 | } 37 | 38 | /** 39 | * Configure database. 40 | * 41 | * @return void 42 | */ 43 | private function configureDatabase() 44 | { 45 | $db = new DB(); 46 | 47 | $db->addConnection([ 48 | 'driver' => 'sqlite', 49 | 'database' => ':memory:', 50 | 'charset' => 'utf8', 51 | 'collation' => 'utf8_unicode_ci', 52 | 'prefix' => '', 53 | ]); 54 | 55 | $db->bootEloquent(); 56 | $db->setAsGlobal(); 57 | 58 | $this->pdo = DB::connection()->getPdo(); 59 | 60 | $db->schema()->create('mock_model', function (Blueprint $table) { 61 | $table->engine = 'InnoDB'; 62 | $table->integer('id')->primary()->autoincrement(); 63 | $table->char('uuid', 36)->nullable(); 64 | $table->string('name'); 65 | $table->boolean('is_alive')->default(true); 66 | $table->boolean('enable_notifications')->default(false); 67 | $table->boolean('is_admin')->default(false); 68 | $table->timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); 69 | $table->timestamp('updated_at')->nullable(); 70 | $table->timestamp('deleted_at')->nullable(); 71 | }); 72 | } 73 | 74 | /** 75 | * Assert a number of simple string searches. 76 | * 77 | * @return void 78 | */ 79 | public function testSchema() 80 | { 81 | $model = new MockModel(); 82 | 83 | $this->assertEquals($model->getSchema(), MockModel::schema()); 84 | } 85 | 86 | /** 87 | * Assert data based on model's schema returns correctly via static or instantiated model. 88 | * 89 | * @return void 90 | */ 91 | public function testData() 92 | { 93 | $model = new MockModel(); 94 | 95 | /** 96 | * Casts. 97 | */ 98 | $casts = [ 99 | 'id' => 'integer', 100 | 'uuid' => 'uuid', 101 | 'name' => 'string', 102 | 'is_alive' => 'boolean', 103 | 'is_admin' => 'boolean', 104 | 'enable_notifications' => 'boolean', 105 | 'created_at' => 'datetime', 106 | 'updated_at' => 'datetime', 107 | 'deleted_at' => 'datetime', 108 | ]; 109 | 110 | $this->assertEquals($casts, MockModel::fromSchema('cast', true)); 111 | $this->assertEquals($casts, $model->getCasts()); 112 | 113 | /** 114 | * Fillable. 115 | */ 116 | $fillable = [ 117 | 'name', 118 | ]; 119 | 120 | $this->assertEquals($fillable, $model->getFillable()); 121 | 122 | /** 123 | * Rules. 124 | */ 125 | $rules = [ 126 | 'uuid' => 'string|nullable', 127 | 'name' => 'string|required|min:2|max:255', 128 | 'is_alive' => 'boolean', 129 | 'is_admin' => 'boolean', 130 | 'enable_notifications' => 'boolean', 131 | 'created_at' => 'date', 132 | 'updated_at' => 'date', 133 | 'deleted_at' => 'date|nullable', 134 | ]; 135 | 136 | $this->assertEquals($rules, $model->getAttributeRules()); 137 | 138 | /** 139 | * Attributes. 140 | */ 141 | $attributes = [ 142 | 'id', 143 | 'uuid', 144 | 'name', 145 | 'is_alive', 146 | 'enable_notifications', 147 | 'is_admin', 148 | 'created_at', 149 | 'updated_at', 150 | 'deleted_at', 151 | ]; 152 | 153 | $this->assertEquals($attributes, $model->getValidAttributes()); 154 | 155 | /** 156 | * Guarded attributes. 157 | */ 158 | $guarded = [ 159 | 'id', 160 | 'uuid', 161 | ]; 162 | 163 | $this->assertEquals($guarded, MockModel::fromSchema('guarded')); 164 | 165 | /** 166 | * Guarded from updates attributes. 167 | */ 168 | $guarded = [ 169 | 'created_at', 170 | ]; 171 | 172 | $this->assertEquals($guarded, MockModel::fromSchema('guarded-update')); 173 | 174 | /** 175 | * Date attributes. 176 | */ 177 | $dates = [ 178 | 'created_at', 179 | 'updated_at', 180 | 'deleted_at', 181 | ]; 182 | 183 | $this->assertEquals($dates, $model->getDates()); 184 | } 185 | 186 | /** 187 | * Assert a number of checks using attributes that exist or not exist against this model. 188 | * 189 | * @return void 190 | */ 191 | public function testAttributes() 192 | { 193 | $model = new MockModel(); 194 | 195 | $this->assertTrue($model->isValidAttribute('name')); 196 | $this->assertFalse($model->isValidAttribute('name1')); 197 | $this->assertTrue($model->hasWriteAccess('name')); 198 | $this->assertFalse($model->hasWriteAccess('id')); 199 | } 200 | 201 | /** 202 | * Assert a number of checks using boolean values. 203 | * 204 | * @return void 205 | */ 206 | public function testCasting() 207 | { 208 | $model = new MockModel(); 209 | 210 | $model->is_alive = true; 211 | $this->assertEquals(true, $model->getAttributes('is_alive')); 212 | 213 | $model->is_alive = false; 214 | $this->assertEquals(false, $model->getAttributes('is_alive')); 215 | 216 | $model->is_alive = '0'; 217 | $this->assertEquals(false, $model->getAttributes('is_alive')); 218 | 219 | $model->is_alive = '1'; 220 | $this->assertEquals(true, $model->getAttributes('is_alive')); 221 | } 222 | 223 | /** 224 | * Assert changing hidden/visbile state. 225 | * 226 | * @return void 227 | */ 228 | public function testHidingAttributes() 229 | { 230 | $model = new MockModel(); 231 | 232 | $this->assertSame([ 233 | 'created_at', 234 | 'updated_at', 235 | 'deleted_at', 236 | ], $model->getHidden()); 237 | 238 | $this->assertSame([ 239 | 'id', 240 | 'uuid', 241 | 'name', 242 | 'is_alive', 243 | 'enable_notifications', 244 | 'is_admin', 245 | ], $model->getVisible()); 246 | 247 | $model->addHidden('uuid'); 248 | 249 | $this->assertSame([ 250 | 'uuid', 251 | 'created_at', 252 | 'updated_at', 253 | 'deleted_at', 254 | ], $model->getHidden()); 255 | 256 | $model->setHidden(['created_at', 'updated_at', 'deleted_at']); 257 | 258 | $this->assertSame([ 259 | 'created_at', 260 | 'updated_at', 261 | 'deleted_at', 262 | ], $model->getHidden()); 263 | 264 | $model->addVisible('created_at'); 265 | 266 | $this->assertSame([ 267 | 'updated_at', 268 | 'deleted_at', 269 | ], $model->getHidden()); 270 | 271 | $model->setVisible([ 272 | 'id', 273 | 'uuid', 274 | 'name', 275 | 'is_alive', 276 | 'enable_notifications', 277 | 'is_admin', 278 | ]); 279 | 280 | $this->assertSame([ 281 | 'created_at', 282 | 'updated_at', 283 | 'deleted_at', 284 | ], $model->getHidden()); 285 | 286 | $model->makeHidden(['created_at', 'updated_at', 'deleted_at']); 287 | 288 | $this->assertSame([ 289 | 'created_at', 290 | 'updated_at', 291 | 'deleted_at', 292 | ], $model->getHidden()); 293 | 294 | $model->makeVisible(['created_at', 'updated_at']); 295 | 296 | $this->assertSame([ 297 | 'deleted_at', 298 | ], $model->getHidden()); 299 | } 300 | 301 | /** 302 | * Assert changing the fillable state of an attribute. 303 | * 304 | * @return void 305 | */ 306 | public function testChangingFillableState() 307 | { 308 | $model = new MockModel(); 309 | 310 | /** 311 | * Pre-change Fillable. 312 | */ 313 | $fillable = [ 314 | 'name', 315 | ]; 316 | 317 | $this->assertEquals($fillable, $model->getFillable()); 318 | 319 | $model->fillable(['is_alive']); 320 | 321 | /** 322 | * Post change Fillable. 323 | */ 324 | $fillable = [ 325 | 'is_alive', 326 | ]; 327 | 328 | $this->assertEquals($fillable, $model->getFillable()); 329 | } 330 | 331 | /** 332 | * Assert changing the guarded state of an attribute. 333 | * 334 | * @return void 335 | */ 336 | public function testChangingGuardedState() 337 | { 338 | $model = new MockModel(); 339 | 340 | /** 341 | * Pre-change guarded. 342 | */ 343 | $guarded = [ 344 | 'id', 345 | 'uuid', 346 | ]; 347 | 348 | $this->assertEquals($guarded, $model->getGuarded()); 349 | 350 | $model->guard(['id']); 351 | 352 | /** 353 | * Post change guarded. 354 | */ 355 | $guarded = [ 356 | 'id', 357 | ]; 358 | 359 | $this->assertEquals($guarded, $model->getGuarded()); 360 | } 361 | 362 | /** 363 | * Assert write access of an attribute. 364 | * 365 | * @return void 366 | */ 367 | public function testWriteAccess() 368 | { 369 | $model = new MockModel(); 370 | 371 | $this->assertFalse($model->hasWriteAccess('id')); 372 | $this->assertTrue($model->hasWriteAccess('name')); 373 | 374 | MockModel::unguard(); 375 | $this->assertTrue($model->hasWriteAccess('id')); 376 | MockModel::reguard(); 377 | 378 | $this->assertFalse($model->hasWriteAccess('is_admin')); 379 | 380 | $this->assertFalse($model->hasWriteAccess('enable_notifications')); 381 | } 382 | 383 | /** 384 | * Assert write access of an attribute. 385 | * 386 | * @return void 387 | */ 388 | public function testDefaultValues() 389 | { 390 | $model = new MockModel(); 391 | 392 | $this->assertEquals([], $model->getDirty()); 393 | 394 | $model->setDefaultValuesForAttributes(); 395 | 396 | $dirty = [ 397 | 'is_alive' => true, 398 | 'is_admin' => false, 399 | 'enable_notifications' => false, 400 | ]; 401 | 402 | $this->assertEquals($dirty, $model->getDirty()); 403 | 404 | $model = MockModel::create([ 405 | 'name' => 'test', 406 | ]); 407 | 408 | $model->is_alive = false; 409 | 410 | $this->assertEquals($model, $model->setDefaultValuesForAttributes()); 411 | $this->assertEquals(['is_alive' => false], $model->getDirty()); 412 | } 413 | 414 | /** 415 | * Assert creating a model fails when validation fails. 416 | * 417 | * @return void 418 | */ 419 | public function testCreateModelValidationException() 420 | { 421 | $this->expectException(\HnhDigital\ModelSchema\Exceptions\ValidationException::class); 422 | 423 | $model = MockModel::create([ 424 | 'name' => 't', 425 | ]); 426 | 427 | $this->assertTrue($model->exists()); 428 | } 429 | 430 | /** 431 | * Assert creating a model fails when validation fails. 432 | * 433 | * @return void 434 | */ 435 | public function testCreateModelCatchValidationException() 436 | { 437 | $model = MockModel::create([ 438 | 'name' => 'test', 439 | ]); 440 | 441 | $this->assertEquals([], $model->getInvalidAttributes()); 442 | 443 | $model->name = 't'; 444 | 445 | try { 446 | $model->save(); 447 | } catch (\HnhDigital\ModelSchema\Exceptions\ValidationException $exception) { 448 | } 449 | 450 | $invalid_attributes = [ 451 | 'name' => [ 452 | 'validation.min.string', 453 | ], 454 | ]; 455 | 456 | $this->assertEquals($invalid_attributes, $model->getInvalidAttributes()); 457 | $this->assertInstanceOf(\Illuminate\Validation\Validator::class, $exception->getValidator()); 458 | 459 | try { 460 | $model = MockModel::create(); 461 | } catch (\HnhDigital\ModelSchema\Exceptions\ValidationException $exception) { 462 | } 463 | 464 | $this->assertInstanceOf(\Illuminate\Validation\Validator::class, $exception->getValidator()); 465 | } 466 | 467 | /** 468 | * Create model, test NullCarbon is returned. 469 | * 470 | * @return void 471 | */ 472 | public function testCreateModel() 473 | { 474 | $model = MockModel::create([ 475 | 'name' => 'test', 476 | ]); 477 | 478 | $this->assertTrue($model->exists()); 479 | 480 | $this->assertInstanceOf(\Carbon\Carbon::class, $model->created_at); 481 | $this->assertInstanceOf(\HnhDigital\NullCarbon\NullCarbon::class, $model->deleted_at); 482 | 483 | $model = $model->fresh(); 484 | 485 | $this->assertInstanceOf(\Carbon\Carbon::class, $model->created_at); 486 | $this->assertInstanceOf(\HnhDigital\NullCarbon\NullCarbon::class, $model->deleted_at); 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/Model.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | use Illuminate\Database\Eloquent\Concerns\GuardsAttributes as EloquentGuardsAttributes; 15 | use Illuminate\Database\Eloquent\Concerns\HasAttributes as EloquentHasAttributes; 16 | use Illuminate\Database\Eloquent\Concerns\HidesAttributes as EloquentHidesAttributes; 17 | use Illuminate\Database\Eloquent\Model as EloquentModel; 18 | use Illuminate\Support\Arr; 19 | use Illuminate\Support\Collection; 20 | 21 | /** 22 | * This is the Model class. 23 | * 24 | * @author Rocco Howard 25 | */ 26 | class Model extends EloquentModel 27 | { 28 | use EloquentGuardsAttributes, Concerns\GuardsAttributes { 29 | Concerns\GuardsAttributes::getFillable insteadof EloquentGuardsAttributes; 30 | Concerns\GuardsAttributes::fillable insteadof EloquentGuardsAttributes; 31 | Concerns\GuardsAttributes::getGuarded insteadof EloquentGuardsAttributes; 32 | Concerns\GuardsAttributes::guard insteadof EloquentGuardsAttributes; 33 | } 34 | 35 | use EloquentHidesAttributes, Concerns\HidesAttributes { 36 | Concerns\HidesAttributes::getHidden insteadof EloquentHidesAttributes; 37 | Concerns\HidesAttributes::setHidden insteadof EloquentHidesAttributes; 38 | Concerns\HidesAttributes::addHidden insteadof EloquentHidesAttributes; 39 | Concerns\HidesAttributes::getVisible insteadof EloquentHidesAttributes; 40 | Concerns\HidesAttributes::setVisible insteadof EloquentHidesAttributes; 41 | Concerns\HidesAttributes::addVisible insteadof EloquentHidesAttributes; 42 | Concerns\HidesAttributes::makeVisible insteadof EloquentHidesAttributes; 43 | Concerns\HidesAttributes::makeHidden insteadof EloquentHidesAttributes; 44 | } 45 | 46 | use EloquentHasAttributes, Concerns\HasAttributes { 47 | EloquentHasAttributes::asDateTime as eloquentAsDateTime; 48 | EloquentHasAttributes::getDirty as eloquentGetDirty; 49 | Concerns\HasAttributes::__set insteadof EloquentHasAttributes; 50 | Concerns\HasAttributes::asDateTime insteadof EloquentHasAttributes; 51 | Concerns\HasAttributes::getDirty insteadof EloquentHasAttributes; 52 | Concerns\HasAttributes::getCasts insteadof EloquentHasAttributes; 53 | Concerns\HasAttributes::getDates insteadof EloquentHasAttributes; 54 | Concerns\HasAttributes::castAttribute insteadof EloquentHasAttributes; 55 | Concerns\HasAttributes::setAttribute insteadof EloquentHasAttributes; 56 | } 57 | 58 | /** 59 | * Describe the database table. 60 | * 61 | * @var string 62 | */ 63 | protected static $db_table; 64 | 65 | /** 66 | * Describes the schema for this model. 67 | * 68 | * @var array 69 | */ 70 | protected static $schema = []; 71 | 72 | /** 73 | * The Collection of the schema. 74 | * 75 | * @var array 76 | */ 77 | protected static $schema_collection; 78 | 79 | /** 80 | * Stores schema requests. 81 | * 82 | * @var array 83 | */ 84 | protected static $schema_cache = []; 85 | 86 | /** 87 | * Describes the schema for this instantiated model. 88 | * 89 | * @var array 90 | */ 91 | protected $model_schema = []; 92 | 93 | /** 94 | * Stores schema requests. 95 | * 96 | * @var array 97 | */ 98 | private $model_schema_cache = []; 99 | 100 | /** 101 | * Cache. 102 | * 103 | * @var array 104 | */ 105 | private $model_cache = []; 106 | 107 | /** 108 | * Get the schema for this model. 109 | * 110 | * @param string|null $key 111 | * @return array 112 | */ 113 | public static function schema($key = null) 114 | { 115 | if (is_null($key)) { 116 | return static::$schema; 117 | } 118 | 119 | return Arr::get(static::$schema, $key, []); 120 | } 121 | 122 | /** 123 | * Get the schema for this model as a collection. 124 | * 125 | * @return Collection 126 | */ 127 | public static function schemaCollection() 128 | { 129 | if (! empty(static::$schema_collection[static::getDbTable()])) { 130 | return static::$schema_collection[static::getDbTable()]; 131 | } 132 | 133 | static::$schema_collection[static::getDbTable()] = collect(); 134 | 135 | foreach (static::$schema as $attribute_name => $schema) { 136 | $schema['attribute'] = $attribute_name; 137 | static::$schema_collection[static::getDbTable()]->add($schema); 138 | } 139 | 140 | return static::$schema_collection[static::getDbTable()]; 141 | } 142 | 143 | /** 144 | * Get table name. 145 | * 146 | * @return string 147 | */ 148 | public function getTable($column_name = null) 149 | { 150 | if (! empty(static::$db_table)) { 151 | $table = static::$db_table; 152 | 153 | if (! is_null($column_name)) { 154 | return $table.'.'.$column_name; 155 | } 156 | 157 | return $table; 158 | } 159 | 160 | return parent::getTable($column_name); 161 | } 162 | 163 | /** 164 | * Get table name (static). 165 | * 166 | * @return string 167 | */ 168 | public static function getDbTable($column_name = null) 169 | { 170 | if (! is_null($column_name)) { 171 | return static::$db_table.'.'.$column_name; 172 | } 173 | 174 | return static::$db_table; 175 | } 176 | 177 | /** 178 | * Get attributes from the schema of this model. 179 | * 180 | * @param null|string $entry 181 | * @return array 182 | * 183 | * @SuppressWarnings(PHPMD.BooleanArgumentFlag) 184 | */ 185 | public static function fromSchema($entry = null, $with_value = false) 186 | { 187 | if (is_null($entry)) { 188 | return array_keys(static::schema()); 189 | } 190 | 191 | $attributes = static::schemaCache(static::getDbTable().'_'.$entry.'_'.(int) $with_value); 192 | 193 | if ($attributes !== false) { 194 | return $attributes; 195 | } 196 | 197 | $attributes = []; 198 | 199 | foreach (static::schema() as $key => $config) { 200 | if (! Arr::has($config, $entry)) { 201 | continue; 202 | } 203 | if ($with_value) { 204 | $attributes[$key] = $config[$entry]; 205 | 206 | continue; 207 | } 208 | 209 | $attributes[] = $key; 210 | } 211 | 212 | static::schemaCache($entry.'_'.(int) $with_value, $attributes); 213 | 214 | return $attributes; 215 | } 216 | 217 | /** 218 | * Set or get Cache for this key. 219 | * 220 | * @param string $key 221 | * @param array $data 222 | * @return void 223 | */ 224 | private static function schemaCache(...$args) 225 | { 226 | if (count($args) == 1) { 227 | $key = array_pop($args); 228 | if (isset(static::$schema_cache[$key])) { 229 | return static::$schema_cache[$key]; 230 | } 231 | 232 | return false; 233 | } 234 | 235 | [$key, $value] = $args; 236 | 237 | static::$schema_cache[$key] = $value; 238 | } 239 | 240 | /** 241 | * Break cache for this key. 242 | * 243 | * @param string $key 244 | * @return void 245 | * 246 | * @SuppressWarnings(PHPMD.UnusedPrivateMethod) 247 | */ 248 | private static function unsetSchemaCache($key) 249 | { 250 | unset(static::$schema_cache[$key]); 251 | } 252 | 253 | /** 254 | * Get schema for this instantiated model. 255 | * 256 | * @return array 257 | */ 258 | public function getSchema() 259 | { 260 | return array_replace_recursive(static::schema(), $this->model_schema); 261 | } 262 | 263 | /** 264 | * Get attributes from the schema of this model. 265 | * 266 | * @param null|string $entry 267 | * @return array 268 | * 269 | * @SuppressWarnings(PHPMD.BooleanArgumentFlag) 270 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 271 | */ 272 | public function getAttributesFromSchema($entry = null, $with_value = false, $is_value = null) 273 | { 274 | if (is_null($entry)) { 275 | return array_keys($this->getSchema()); 276 | } 277 | 278 | $attributes = $this->getSchemaCache($entry.'_'.(int) $with_value); 279 | 280 | if ($attributes !== false) { 281 | return $attributes; 282 | } 283 | 284 | $attributes = []; 285 | 286 | foreach ($this->getSchema() as $key => $config) { 287 | if (! is_null($is_value) && is_callable($is_value)) { 288 | if (! $is_value(Arr::get($config, $entry))) { 289 | continue; 290 | } 291 | } elseif (! is_null($is_value) && $is_value != Arr::get($config, $entry)) { 292 | continue; 293 | } 294 | 295 | if (! Arr::has($config, $entry)) { 296 | continue; 297 | } 298 | 299 | if ($with_value) { 300 | $attributes[$key] = $config[$entry]; 301 | continue; 302 | } 303 | 304 | $attributes[] = $key; 305 | } 306 | 307 | $this->setSchemaCache($entry.'_'.(int) $with_value, $attributes); 308 | 309 | return $attributes; 310 | } 311 | 312 | /** 313 | * Cache for this key. 314 | * 315 | * @param string $key 316 | * @param array $data 317 | * @return void 318 | */ 319 | private function setSchemaCache($key, $data) 320 | { 321 | $this->model_schema_cache[$key] = $data; 322 | } 323 | 324 | /** 325 | * Get cache for this key. 326 | * 327 | * @param string $key 328 | * @return void 329 | */ 330 | private function getSchemaCache($key) 331 | { 332 | if (isset($this->model_schema_cache[$key])) { 333 | return $this->model_schema_cache[$key]; 334 | } 335 | 336 | return false; 337 | } 338 | 339 | /** 340 | * Break cache for this key. 341 | * 342 | * @param string $key 343 | * @return void 344 | */ 345 | private function breakSchemaCache($key) 346 | { 347 | unset($this->model_schema_cache[$key]); 348 | } 349 | 350 | /** 351 | * Set an entry within the schema. 352 | * 353 | * @param string $entry 354 | * @param string|array $keys 355 | * @param bool $reset 356 | * @param mixed $reset_value 357 | * @return array 358 | * 359 | * @SuppressWarnings(PHPMD.BooleanArgumentFlag) 360 | */ 361 | public function setSchema($entry, $keys, $set_value, $reset = false, $reset_value = null) 362 | { 363 | // Reset existing values in the schema. 364 | if ($reset) { 365 | $current = $this->getSchema($entry); 366 | 367 | foreach (array_keys($current) as $key) { 368 | if (is_null($reset_value)) { 369 | unset($this->schema[$key][$entry]); 370 | continue; 371 | } 372 | 373 | Arr::set($this->model_schema, $key.'.'.$entry, $reset_value); 374 | } 375 | } 376 | 377 | // Update each of the keys. 378 | foreach (Arr::wrap($keys) as $key) { 379 | Arr::set($this->model_schema, $key.'.'.$entry, $set_value); 380 | } 381 | 382 | // Break the cache. 383 | $this->breakSchemaCache($entry.'_0'); 384 | $this->breakSchemaCache($entry.'_1'); 385 | 386 | return $this; 387 | } 388 | 389 | /** 390 | * Assign an array of data to this model. 391 | * 392 | * @param Collection|array $data 393 | * @return $this 394 | */ 395 | public function assign($data) 396 | { 397 | foreach ($data as $key => $value) { 398 | $this->{$key} = $value; 399 | } 400 | 401 | return $this; 402 | } 403 | 404 | /** 405 | * Cache this value. 406 | * 407 | * @param string $key 408 | * @param mixed $value 409 | * @return mixed 410 | */ 411 | protected function cache($key, $value) 412 | { 413 | if (Arr::has($this->model_cache, $key)) { 414 | return Arr::get($this->model_cache, $key); 415 | } 416 | 417 | $result = is_callable($value) ? $value() : $value; 418 | 419 | Arr::set($this->model_cache, $key, $result); 420 | 421 | return $result; 422 | } 423 | 424 | /** 425 | * Determine if the model instance has been soft-deleted. 426 | * This is a NullCarbon workaround. 427 | * 428 | * @return bool 429 | */ 430 | public function isTrashed() 431 | { 432 | if (is_a($this->{$this->getDeletedAtColumn()}, \HnhDigital\NullCarbon\NullCarbon::class)) { 433 | return $this->{$this->getDeletedAtColumn()}->getTimestamp() > 0; 434 | } 435 | 436 | return ! is_null($this->{$this->getDeletedAtColumn()}); 437 | } 438 | 439 | /** 440 | * Boot events. 441 | * 442 | * @return void 443 | */ 444 | protected static function booted() 445 | { 446 | // Boot event for creating this model. 447 | // Set default values if specified. 448 | // Validate dirty attributes before commiting to save. 449 | static::creating(function ($model) { 450 | $model->setDefaultValuesForAttributes(); 451 | if (! $model->savingValidation()) { 452 | $validator = $model->getValidator(); 453 | $issues = $validator->errors()->all(); 454 | 455 | $message = sprintf( 456 | "Validation failed on creating %s.\n%s", 457 | $model->getTable(), 458 | implode("\n", $issues) 459 | ); 460 | 461 | throw new Exceptions\ValidationException($message, 0, null, $validator); 462 | } 463 | }); 464 | 465 | // Boot event once model has been created. 466 | // Add missing attributes using the schema. 467 | static::created(function ($model) { 468 | $model->addMissingAttributes(); 469 | }); 470 | 471 | // Boot event for updating this model. 472 | // Validate dirty attributes before commiting to save. 473 | static::updating(function ($model) { 474 | if (! $model->savingValidation()) { 475 | $validator = $model->getValidator(); 476 | $issues = $validator->errors()->all(); 477 | 478 | $message = sprintf( 479 | "Validation failed on saving %s (%s).\n%s", 480 | $model->getTable(), 481 | $model->getKey(), 482 | implode("\n", $issues) 483 | ); 484 | 485 | throw new Exceptions\ValidationException($message, 0, null, $validator); 486 | } 487 | }); 488 | } 489 | } 490 | -------------------------------------------------------------------------------- /src/Concerns/HasAttributes.php: -------------------------------------------------------------------------------- 1 | 'castAsString', 31 | 'int' => 'castAsInt', 32 | 'integer' => 'castAsInt', 33 | 'real' => 'castAsFloat', 34 | 'float' => 'castAsFloat', 35 | 'double' => 'castAsFloat', 36 | 'decimal' => 'castAsFloat', 37 | 'string' => 'castAsString', 38 | 'bool' => 'castAsBool', 39 | 'boolean' => 'castAsBool', 40 | 'object' => 'castAsObject', 41 | 'array' => 'castAsArray', 42 | 'json' => 'castAsArray', 43 | 'collection' => 'castAsCollection', 44 | 'commalist' => 'castAsCommaList', 45 | 'date' => 'castAsDate', 46 | 'datetime' => 'castAsDateTime', 47 | 'timestamp' => 'castAsDateTime', 48 | ]; 49 | 50 | /** 51 | * Model custom cast to database definitions. 52 | * 53 | * @var array 54 | */ 55 | protected static $cast_to = [ 56 | 57 | ]; 58 | 59 | /** 60 | * Default cast to database definitions. 61 | * 62 | * @var array 63 | */ 64 | protected static $default_cast_to = [ 65 | 'bool' => 'castToBoolean', 66 | 'boolean' => 'castToBoolean', 67 | 'date' => 'castToDateTime', 68 | 'object' => 'castToJson', 69 | 'array' => 'castToJson', 70 | 'json' => 'castToJson', 71 | 'collection' => 'castToJson', 72 | 'commalist' => 'castToCommaList', 73 | ]; 74 | 75 | /** 76 | * Cast type to validation type. 77 | * 78 | * @var array 79 | */ 80 | protected static $cast_validation = [ 81 | 82 | ]; 83 | 84 | /** 85 | * Default cast type to validation type. 86 | * 87 | * @var array 88 | */ 89 | protected static $default_cast_validation = [ 90 | 'uuid' => 'string', 91 | 'bool' => 'boolean', 92 | 'int' => 'integer', 93 | 'real' => 'numeric', 94 | 'float' => 'numeric', 95 | 'double' => 'numeric', 96 | 'decimal' => 'numeric', 97 | 'datetime' => 'date', 98 | 'timestamp' => 'date', 99 | ]; 100 | 101 | /** 102 | * Validator. 103 | * 104 | * @var Validator. 105 | */ 106 | private $validator; 107 | 108 | /** 109 | * Set's mising attributes. 110 | * 111 | * This covers situations where values have defaults but are not fillable, or 112 | * date field. 113 | * 114 | * @return void 115 | */ 116 | public function addMissingAttributes() 117 | { 118 | foreach ($this->getSchema() as $key => $settings) { 119 | if (! Arr::has($this->attributes, $key)) { 120 | Arr::set($this->attributes, $key, Arr::get($settings, 'default', null)); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Return a list of the attributes on this model. 127 | * 128 | * @return array 129 | */ 130 | public function getValidAttributes() 131 | { 132 | return array_keys($this->getSchema()); 133 | } 134 | 135 | /** 136 | * Is key a valid attribute? 137 | * 138 | * @return bool 139 | */ 140 | public function isValidAttribute($key) 141 | { 142 | return Arr::has($this->getSchema(), $key); 143 | } 144 | 145 | /** 146 | * Has write access to a given key on this model. 147 | * 148 | * @param string $key 149 | * @return bool 150 | */ 151 | public function hasWriteAccess($key) 152 | { 153 | if (static::$unguarded) { 154 | return true; 155 | } 156 | 157 | // Attribute is guarded. 158 | if (in_array($key, $this->getGuarded())) { 159 | return false; 160 | } 161 | 162 | $method = $this->getAuthMethod($key); 163 | 164 | if ($method !== false) { 165 | return $this->$method($key); 166 | } 167 | 168 | // Check for the presence of a mutator for the auth operation 169 | // which simply lets the developers check if the current user 170 | // has the authority to update this value. 171 | if ($this->hasAuthAttributeMutator($key)) { 172 | $method = 'auth'.Str::studly($key).'Attribute'; 173 | 174 | return $this->{$method}($key); 175 | } 176 | 177 | return true; 178 | } 179 | 180 | /** 181 | * Set default values on this attribute. 182 | * 183 | * @return $this 184 | */ 185 | public function setDefaultValuesForAttributes() 186 | { 187 | // Only works on new models. 188 | if ($this->exists) { 189 | return $this; 190 | } 191 | 192 | // Defaults on attributes. 193 | $defaults = $this->getAttributesFromSchema('default', true); 194 | 195 | // Remove attributes that have been given values. 196 | $defaults = Arr::except($defaults, array_keys($this->getDirty())); 197 | 198 | // Unguard. 199 | static::unguard(); 200 | 201 | // Allocate values. 202 | foreach ($defaults as $key => $value) { 203 | $this->{$key} = $value; 204 | } 205 | 206 | // Reguard. 207 | static::reguard(); 208 | 209 | return $this; 210 | } 211 | 212 | /** 213 | * Check if model has attribute. 214 | */ 215 | public function hasAttribute($attribute): bool 216 | { 217 | return in_array($attribute, $this->fromSchema()); 218 | } 219 | 220 | /** 221 | * Get the casts array. 222 | * 223 | * @return array 224 | */ 225 | public function getCasts() 226 | { 227 | if ($this->getIncrementing()) { 228 | return array_merge( 229 | [ 230 | $this->getKeyName() => $this->getKeyType(), 231 | ], 232 | $this->getAttributesFromSchema('cast', true) 233 | ); 234 | } 235 | 236 | return $this->getAttributesFromSchema('cast', true); 237 | } 238 | 239 | /** 240 | * Get the casts params array. 241 | * 242 | * @return array 243 | */ 244 | public function getCastParams() 245 | { 246 | if ($this->getIncrementing()) { 247 | return array_merge( 248 | [ 249 | $this->getKeyName() => $this->getKeyType(), 250 | ], 251 | $this->getAttributesFromSchema('cast', true) 252 | ); 253 | } 254 | 255 | return $this->getAttributesFromSchema('cast-params', true); 256 | } 257 | 258 | /** 259 | * Cast an attribute to a native PHP type. 260 | * 261 | * @param string $key 262 | * @param mixed $value 263 | * @return mixed 264 | */ 265 | protected function castAttribute($key, $value) 266 | { 267 | $method = $this->getCastFromMethod($key); 268 | 269 | if ($method === false) { 270 | return parent::castAttribute($key, $value); 271 | } 272 | 273 | if (stripos($method, 'date') === false && is_null($value)) { 274 | return $value; 275 | } 276 | 277 | // Casting method is local. 278 | if (is_string($method) && method_exists($this, $method)) { 279 | $paramaters = $this->getCastAsParamaters($key); 280 | 281 | return $this->$method($value, ...$paramaters); 282 | } 283 | 284 | return parent::castAttribute($key, $value); 285 | } 286 | 287 | /** 288 | * Get the method to cast the value of this given key. 289 | * (when using getAttribute). 290 | * 291 | * @param string $key 292 | * @return string|array 293 | */ 294 | protected function getCastFromMethod($key) 295 | { 296 | return $this->getCastFromDefinition($this->getCastType($key)); 297 | } 298 | 299 | /** 300 | * Get the method to cast this attribte type. 301 | * 302 | * @param string $type 303 | * @return string|array|bool 304 | */ 305 | protected function getCastFromDefinition($type) 306 | { 307 | // Custom definitions. 308 | if (Arr::has(static::$cast_from, $type)) { 309 | return Arr::get(static::$cast_from, $type); 310 | } 311 | 312 | // Fallback to default. 313 | if (Arr::has(static::$default_cast_from, $type)) { 314 | return Arr::get(static::$default_cast_from, $type); 315 | } 316 | 317 | return false; 318 | } 319 | 320 | /** 321 | * Get the method to cast this attribte tyepca. 322 | * 323 | * @param string $type 324 | * @return string|array|bool 325 | */ 326 | protected function getCastAsParamaters($key) 327 | { 328 | $cast_params = $this->getCastParams(); 329 | 330 | $paramaters = explode(':', Arr::get($cast_params, $key, '')); 331 | $parsed = $this->parseCastParamaters($paramaters); 332 | 333 | return $parsed; 334 | } 335 | 336 | /** 337 | * Parse the given cast parameters. 338 | * 339 | * @param array $paramaters 340 | * @return array 341 | */ 342 | private function parseCastParamaters($paramaters) 343 | { 344 | foreach ($paramaters as &$value) { 345 | // Local callable method. ($someMethod()) 346 | if (substr($value, 0, 1) === '$' && stripos($value, '()') !== false) { 347 | $method = substr($value, 1, -2); 348 | $value = is_callable([$this, $method]) ? $this->{$method}() : null; 349 | 350 | // Local attribute. ($some_attribute) 351 | } elseif (substr($value, 0, 1) === '$') { 352 | $key = substr($value, 1); 353 | $value = $this->{$key}; 354 | 355 | // Callable function (eg helper). (some_function()) 356 | } elseif (stripos($value, '()') !== false) { 357 | $method = substr($value, 0, -2); 358 | $value = is_callable($method) ? $method() : null; 359 | } 360 | 361 | // String value. 362 | } 363 | 364 | return $paramaters; 365 | } 366 | 367 | /** 368 | * Get the attributes that should be converted to dates. 369 | * 370 | * @return array 371 | */ 372 | public function getDates() 373 | { 374 | return $this->cache(__FUNCTION__, function () { 375 | $casts = $this->getCasts(); 376 | 377 | $dates = []; 378 | 379 | foreach ($casts as $key => $cast) { 380 | if ($cast == 'datetime') { 381 | $dates[] = $key; 382 | } 383 | } 384 | 385 | return $dates; 386 | }); 387 | } 388 | 389 | /** 390 | * Get the auths array. 391 | * 392 | * @return array 393 | */ 394 | protected function getAuths() 395 | { 396 | return $this->getAttributesFromSchema('auth', true); 397 | } 398 | 399 | /** 400 | * Get auth method. 401 | * 402 | * @param string $key 403 | * @return string|array 404 | */ 405 | public function getAuthMethod($key) 406 | { 407 | if (Arr::has($this->getAuths(), $key)) { 408 | $method = 'auth'.Str::studly(Arr::get($this->getAuths(), $key)); 409 | 410 | return method_exists($this, $method) ? $method : false; 411 | } 412 | 413 | return false; 414 | } 415 | 416 | /** 417 | * Set a given attribute on the model. 418 | * 419 | * @param string $key 420 | * @param mixed $value 421 | * @return $this 422 | */ 423 | public function setAttribute($key, $value) 424 | { 425 | // First we will check for the presence of a mutator for the set operation 426 | // which simply lets the developers tweak the attribute as it is set on 427 | // the model, such as "json_encoding" an listing of data for storage. 428 | if ($this->hasSetMutator($key)) { 429 | $method = 'set'.Str::studly($key).'Attribute'; 430 | 431 | return $this->{$method}($value); 432 | } 433 | 434 | // Get the method that modifies this value before storing. 435 | $method = $this->getCastToMethod($key); 436 | 437 | // Get the rules for this attribute. 438 | $rules = $this->getAttributeRules($key); 439 | 440 | // Skip casting if null is allowed. 441 | if (is_null($value) && stripos($rules, 'nullable') !== false) { 442 | $this->attributes[$key] = $value; 443 | 444 | return $this; 445 | } 446 | 447 | // Casting method is local. 448 | if (is_string($method) && method_exists($this, $method)) { 449 | $value = $this->$method($key, $value); 450 | } 451 | 452 | // If this attribute contains a JSON ->, we'll set the proper value in the 453 | // attribute's underlying array. This takes care of properly nesting an 454 | // attribute in the array's value in the case of deeply nested items. 455 | if (Str::contains($key, '->')) { 456 | return $this->fillJsonAttribute($key, $value); 457 | } 458 | 459 | $this->attributes[$key] = $value; 460 | 461 | return $this; 462 | } 463 | 464 | /** 465 | * Determine if a auth check exists for an attribute. 466 | * 467 | * @param string $key 468 | * @return bool 469 | */ 470 | public function hasAuthAttributeMutator($key) 471 | { 472 | return method_exists($this, 'auth'.Str::studly($key).'Attribute'); 473 | } 474 | 475 | /** 476 | * Get the method to cast the value of this given key. 477 | * (when using setAttribute). 478 | * 479 | * @param string $key 480 | * @return string|array 481 | */ 482 | protected function getCastToMethod($key) 483 | { 484 | return $this->getCastToDefinition($this->getCastType($key)); 485 | } 486 | 487 | /** 488 | * Get the method to cast this attribte back to it's original form. 489 | * 490 | * @param string $type 491 | * @return string|array|bool 492 | */ 493 | protected function getCastToDefinition($type) 494 | { 495 | // Custom definitions. 496 | if (Arr::has(static::$cast_to, $type)) { 497 | return Arr::get(static::$cast_to, $type); 498 | } 499 | 500 | // Fallback to default. 501 | if (Arr::has(static::$default_cast_to, $type)) { 502 | return Arr::get(static::$default_cast_to, $type); 503 | } 504 | 505 | return false; 506 | } 507 | 508 | /** 509 | * Cast value as an bool. 510 | * 511 | * @param mixed $value 512 | * @return bool 513 | */ 514 | protected function castAsBool($value) 515 | { 516 | // If value is provided as string verison of true/false, convert to boolean. 517 | if ($value === 'true' || $value === 'false') { 518 | $value = $value === 'true'; 519 | } 520 | 521 | return (bool) (int) $value; 522 | } 523 | 524 | /** 525 | * Cast value as an int. 526 | * 527 | * @param mixed $value 528 | * @return int 529 | */ 530 | protected function castAsInt($value) 531 | { 532 | return (int) $value; 533 | } 534 | 535 | /** 536 | * Cast value as a float. 537 | * 538 | * @param mixed $value 539 | * @return float 540 | */ 541 | protected function castAsFloat($value) 542 | { 543 | switch ((string) $value) { 544 | case 'Infinity': 545 | return INF; 546 | case '-Infinity': 547 | return -INF; 548 | case 'NaN': 549 | return NAN; 550 | } 551 | 552 | return (float) $value; 553 | } 554 | 555 | /** 556 | * Cast value as a strng. 557 | * 558 | * @param mixed $value 559 | * @return string 560 | */ 561 | protected function castAsString($value) 562 | { 563 | return (string) $value; 564 | } 565 | 566 | /** 567 | * Cast value . 568 | * 569 | * @param mixed $value 570 | * @return array 571 | */ 572 | protected function castAsArray($value) 573 | { 574 | return $this->fromJson($value); 575 | } 576 | 577 | /** 578 | * Cast value as an object. 579 | * 580 | * @param mixed $value 581 | * @return array 582 | */ 583 | protected function castAsObject($value) 584 | { 585 | return $this->fromJson($value, true); 586 | } 587 | 588 | /** 589 | * Cast date as a DateTime instance. 590 | * 591 | * @return DateTime 592 | */ 593 | protected function castAsDate($value) 594 | { 595 | return $this->castAsDateTime($value); 596 | } 597 | 598 | /** 599 | * Return a datetime as DateTime object. 600 | * 601 | * @param mixed $value 602 | * @return \Illuminate\Support\Carbon 603 | */ 604 | protected function castAsDateTime($value) 605 | { 606 | if (is_null($value) || $value === '') { 607 | return new NullCarbon(); 608 | } 609 | 610 | return $this->eloquentAsDateTime($value); 611 | } 612 | 613 | /** 614 | * Cast comma list to array. 615 | * 616 | * @return array 617 | */ 618 | protected function castAsCommaList($value) 619 | { 620 | if (is_array($value)) { 621 | return $value; 622 | } 623 | 624 | return explode(',', $value); 625 | } 626 | 627 | /** 628 | * Ensure all DateTime casting is redirected. 629 | * 630 | * @param mixed $value 631 | * @return \Illuminate\Support\Carbon 632 | */ 633 | protected function asDateTime($value) 634 | { 635 | return $this->castAsDateTime($value); 636 | } 637 | 638 | /** 639 | * Cast to boolean. 640 | * 641 | * @return bool 642 | */ 643 | protected function castToBoolean($key, $value) 644 | { 645 | unset($key); 646 | 647 | return $this->castAsBool($value); 648 | } 649 | 650 | /** 651 | * Cast to JSON. 652 | * 653 | * @return bool 654 | */ 655 | protected function castToJson($key, $value) 656 | { 657 | return $this->castAttributeAsJson($key, $value); 658 | } 659 | 660 | /** 661 | * Cast date as a DateTime instance. 662 | * 663 | * @return DateTime 664 | */ 665 | protected function castToDateTime($key, $value) 666 | { 667 | unset($key); 668 | 669 | return $this->fromDateTime($value); 670 | } 671 | 672 | /** 673 | * Cast array to string. 674 | * 675 | * @return array 676 | */ 677 | protected function castToCommaList($key, $value) 678 | { 679 | unset($key); 680 | 681 | if (is_string($value)) { 682 | return $value; 683 | } 684 | 685 | return implode(',', $value); 686 | } 687 | 688 | /** 689 | * Get the attributes that have been changed since last sync. 690 | * 691 | * @return array 692 | */ 693 | public function getDirty() 694 | { 695 | return $this->eloquentGetDirty(); 696 | } 697 | 698 | /** 699 | * Validate the model before saving. 700 | * 701 | * @return array 702 | */ 703 | public function savingValidation() 704 | { 705 | // Create translator if missing. 706 | if (is_null(app('translator'))) { 707 | app()->bind('translator', new Translator(new FileLoader(new Filesystem, 'lang'), 'en')); 708 | } 709 | 710 | $dirty_attributes = $this->preValidationCast(); 711 | 712 | // Remove casting class attributes from being validated. 713 | foreach ($this->getCasts() as $key => $cast) { 714 | if (class_exists($cast) 715 | && ! Arr::has(static::$default_cast_validation, $cast) 716 | && ! Arr::has(static::$default_cast_from, $cast)) { 717 | unset($dirty_attributes[$key]); 718 | } 719 | } 720 | 721 | $this->validator = new Validator(app('translator'), $dirty_attributes, $this->getAttributeRules()); 722 | 723 | if ($this->validator->fails()) { 724 | return false; 725 | } 726 | 727 | return true; 728 | } 729 | 730 | /** 731 | * Before validating, ensure the values are correctly casted. 732 | * 733 | * Mostly integer or boolean values where they can be set to either. 734 | * eg 1 for true. 735 | * 736 | * @return array 737 | */ 738 | private function preValidationCast() 739 | { 740 | $rules = $this->getAttributeRules(); 741 | 742 | // Check each dirty attribute. 743 | foreach ($this->getDirty() as $key => $value) { 744 | $this->attributes[$key] = static::preCastAttribute(Arr::get($rules, $key, ''), $value); 745 | } 746 | 747 | return $this->getDirty(); 748 | } 749 | 750 | /** 751 | * Pre-cast attribute to the correct value. 752 | * 753 | * @param mixed $rules 754 | * @param mixed $value 755 | * @return mixed 756 | */ 757 | public static function preCastAttribute($rules, $value) 758 | { 759 | // Get the rules. 760 | if (is_string($rules)) { 761 | $rules = explode('|', $rules); 762 | } 763 | 764 | // First item is always the cast type. 765 | $cast_type = Arr::get($rules, 0, false); 766 | 767 | // Check if the value can be nullable. 768 | $is_nullable = in_array('nullable', $rules); 769 | 770 | return self::castType($cast_type, $value, $is_nullable); 771 | } 772 | 773 | /** 774 | * Cast a value to native. 775 | * 776 | * @param string $cast_type 777 | * @param mixed $value 778 | * @param bool $is_nullable 779 | * @return mixed 780 | * 781 | * @SuppressWarnings(PHPMD.CyclomaticComplexity) 782 | * @SuppressWarnings(PHPMD.NPathComplexity) 783 | */ 784 | private static function castType($cast_type, $value, $is_nullable) 785 | { 786 | if ($value === 'NULL') { 787 | $value = null; 788 | } 789 | 790 | // Is null and allows null. 791 | if (is_null($value) && $is_nullable) { 792 | return $value; 793 | } 794 | 795 | // Boolean type. 796 | if ($cast_type === 'boolean') { 797 | $value = $value === 'true' ? true : $value; 798 | $value = $value === 'false' ? false : $value; 799 | $value = boolval($value); 800 | 801 | return $value; 802 | } 803 | 804 | // Numeric type. 805 | if ($cast_type === 'numeric') { 806 | return (float) preg_replace('/[^0-9.-]*/', '', $value); 807 | } 808 | 809 | // Integer type. 810 | if ($cast_type === 'integer') { 811 | if (empty($value) && $is_nullable) { 812 | return null; 813 | } 814 | 815 | if (is_numeric($value)) { 816 | return intval($value); 817 | } 818 | 819 | return 0; 820 | } 821 | 822 | if (class_exists($cast_type)) { 823 | return $value; 824 | } 825 | 826 | $value = strval($value); 827 | 828 | // Empty value and allows null. 829 | if (empty($value) && $is_nullable) { 830 | $value = null; 831 | } 832 | 833 | return $value; 834 | } 835 | 836 | /** 837 | * Get rules for attributes. 838 | * 839 | * @param string|null $attribute_key 840 | * @return array|string 841 | */ 842 | public function getAttributeRules($attribute_key = null) 843 | { 844 | $result = []; 845 | $attributes = $this->getAttributesFromSchema(); 846 | $casts = $this->getCasts(); 847 | $casts_back = $this->getAttributesFromSchema('cast-back', true); 848 | $rules = $this->getAttributesFromSchema('rules', true); 849 | 850 | // Build full rule for each attribute. 851 | foreach ($attributes as $key) { 852 | $result[$key] = []; 853 | } 854 | 855 | // If any casts back are configured, replace the value found in casts. 856 | // Handy if we read integer values as datetime, but save back as an integer. 857 | $casts = array_merge($casts, $casts_back); 858 | 859 | // Build full rule for each attribute. 860 | foreach ($casts as $key => $cast_type) { 861 | $cast_validator = $this->parseCastToValidator($cast_type); 862 | 863 | if (! empty($cast_validator)) { 864 | $result[$key][] = $cast_validator; 865 | } 866 | 867 | if ($this->exists) { 868 | $result[$key][] = 'sometimes'; 869 | } 870 | } 871 | 872 | // Assign specified rules. 873 | foreach ($rules as $key => $rule) { 874 | $result[$key][] = $this->verifyRule($rule); 875 | } 876 | 877 | // Key name could be composite. Only remove from rules if singlular. 878 | $key_name = $this->getKeyName(); 879 | 880 | if (is_string($key_name)) { 881 | unset($result[$key_name]); 882 | } 883 | 884 | foreach ($result as $key => $rules) { 885 | $result[$key] = implode('|', $rules); 886 | } 887 | 888 | if (! is_null($attribute_key)) { 889 | return Arr::get($result, $attribute_key, ''); 890 | } 891 | 892 | return $result; 893 | } 894 | 895 | /** 896 | * Verify the rules. 897 | * 898 | * @param string $rules 899 | * @return string 900 | */ 901 | public function verifyRule($rule) 902 | { 903 | $rules = explode('|', $rule); 904 | 905 | foreach ($rules as &$entry) { 906 | if (stripos($entry, 'unique') !== false) { 907 | if (stripos($entry, ':') === false) { 908 | $entry .= ':'.$this->getTable(); 909 | } 910 | } 911 | } 912 | 913 | return implode('|', $rules); 914 | } 915 | 916 | /** 917 | * Convert attribute type to validation type. 918 | * 919 | * @param string $type 920 | * @return string 921 | */ 922 | private function parseCastToValidator($type) 923 | { 924 | if (Arr::has(static::$cast_validation, $type)) { 925 | return Arr::get(static::$cast_validation, $type); 926 | } 927 | 928 | if (Arr::has(static::$default_cast_validation, $type)) { 929 | return Arr::get(static::$default_cast_validation, $type); 930 | } 931 | 932 | return $type; 933 | } 934 | 935 | /** 936 | * Get the validator instance. 937 | * 938 | * @return Validator 939 | */ 940 | public function getValidator() 941 | { 942 | return $this->validator; 943 | } 944 | 945 | /** 946 | * Get invalid attributes. 947 | * 948 | * @return array 949 | */ 950 | public function getInvalidAttributes() 951 | { 952 | if (is_null($this->getValidator())) { 953 | return []; 954 | } 955 | 956 | return $this->getValidator()->errors()->messages(); 957 | } 958 | 959 | /** 960 | * Dynamically set attributes on the model. 961 | * 962 | * @param string $key 963 | * @param mixed $value 964 | * @return void 965 | */ 966 | public function __set($key, $value) 967 | { 968 | if ($this->isValidAttribute($key) && $this->hasWriteAccess($key)) { 969 | $this->setAttribute($key, $value); 970 | } 971 | } 972 | 973 | /** 974 | * Register cast from database definition. 975 | * 976 | * @param string $cast 977 | * @param mixed $method 978 | * @return void 979 | */ 980 | public static function registerCastFromDatabase($cast, $method) 981 | { 982 | Arr::set(static::$cast_from, $cast, $method); 983 | } 984 | 985 | /** 986 | * Register cast to database definition. 987 | * 988 | * @param string $cast 989 | * @param mixed $method 990 | * @return void 991 | */ 992 | public static function registerCastToDatabase($cast, $method) 993 | { 994 | Arr::set(static::$cast_to, $cast, $method); 995 | } 996 | 997 | /** 998 | * Register cast to database definition. 999 | * 1000 | * @param string $cast 1001 | * @param string $validator 1002 | * @return void 1003 | */ 1004 | public static function registerCastValidator($cast, $validator) 1005 | { 1006 | Arr::set(static::$cast_validation, $cast, $validator); 1007 | } 1008 | } 1009 | --------------------------------------------------------------------------------