├── .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 | '
- '.implode('
- ', Arr::get($response, 'feedback')).'
'
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 | [](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [](https://laravel.com/) [](https://packagist.org/packages/hnhdigital-os/laravel-model-schema) [](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 |
--------------------------------------------------------------------------------