├── .docker └── app │ └── Dockerfile ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── composer.json ├── config └── custom_casts.php ├── docker-compose.yml ├── phpunit.xml ├── src ├── CustomCastBase.php ├── CustomCastsServiceProvider.php ├── HasCustomCasts.php └── helpers.php └── tests ├── Integration ├── CanHandleModelEventsTest.php ├── MiscTest.php ├── ModelWithAliasedCustomCastsTest.php ├── ModelWithCustomCastsTest.php ├── ModelWithDefaultValueForCustomCastFieldTest.php └── ModelWithNullableCustomCastFieldTest.php ├── Support ├── CustomCasts │ ├── Base64Cast.php │ └── EventHandlingCast.php └── Models │ ├── ModelWithAliasedCustomCasts.php │ ├── ModelWithCustomCasts.php │ ├── ModelWithDefaultValue.php │ ├── ModelWithDefaultValueForCustomCasts.php │ ├── ModelWithEventHandlingCast.php │ ├── ModelWithMutatorAndCustomCasts.php │ └── ModelWithNullableValueForCustomCasts.php ├── TestCase.php └── database └── migrations └── 0000_00_00_000000_create_package_test_tables.php /.docker/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3-cli-stretch 2 | 3 | # 4 | # Packages 5 | # 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y --no-install-recommends \ 9 | git zip unzip 10 | 11 | RUN apt-get clean \ 12 | && apt-get autoremove -y \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Enable debuging 16 | RUN mv /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini 17 | 18 | # 19 | # Composer 20 | # 21 | 22 | ENV COMPOSER_ALLOW_SUPERUSER=1 23 | ENV COMPOSER_NO_INTERACTION=1 24 | ENV COMPOSER_MEMORY_LIMIT=-1 25 | 26 | RUN curl -sS https://getcomposer.org/installer | php \ 27 | && mv composer.phar /usr/local/bin/composer 28 | 29 | # Set default workdir 30 | WORKDIR /var/www/html 31 | 32 | # Global path 33 | ENV PATH="/var/www/html/vendor/bin:${PATH}" 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # OS generated files and junk 3 | # 4 | 5 | .DS_Store 6 | .DS_Store? 7 | ._* 8 | Thumbs.db 9 | Icon? 10 | .Trashes 11 | ehthumbs.db 12 | *.log 13 | *.cache 14 | 15 | # 16 | # PhpStorm 17 | # 18 | 19 | .idea 20 | 21 | # 22 | # Project 23 | # 24 | 25 | vendor 26 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.3 6 | - 8.0 7 | 8 | before_script: 9 | - composer install --prefer-source --no-interaction 10 | - composer dump-autoload 11 | 12 | script: 13 | - vendor/bin/phpunit 14 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Vladimir Ković 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Custom Casts 2 | 3 | [![Build](https://api.travis-ci.org/vkovic/laravel-custom-casts.svg?branch=master)](https://travis-ci.org/vkovic/laravel-custom-casts) 4 | [![Downloads](https://poser.pugx.org/vkovic/laravel-custom-casts/downloads)](https://packagist.org/packages/vkovic/laravel-custom-casts) 5 | [![Stable](https://poser.pugx.org/vkovic/laravel-custom-casts/v/stable)](https://packagist.org/packages/vkovic/laravel-custom-casts) 6 | [![License](https://poser.pugx.org/vkovic/laravel-custom-casts/license)](https://packagist.org/packages/vkovic/laravel-custom-casts) 7 | 8 | ### Make your own cast type for Laravel model attributes 9 | 10 | Laravel custom casts works similarly to [Eloquent attribute casting](https://laravel.com/docs/6.x/eloquent-mutators#attribute-casting), but with custom-defined logic (in a separate class). This means we can use the same casting logic across multiple models — we might write [image upload logic](https://github.com/vkovic/laravel-custom-casts/tree/v1.0.2#example-casting-user-image) and use it everywhere. In addition to casting to custom types, this package allows custom casts to listen and react to underlying model events. 11 | 12 | Let's review some Laravel common cast types and examples of their usage: 13 | 14 | ```php 15 | namespace App; 16 | 17 | use Illuminate\Database\Eloquent\Model; 18 | 19 | class User extends Model 20 | { 21 | protected $casts = [ 22 | 'is_admin' => 'boolean', 23 | 'login_count' => 'integer' 24 | 'height' => 'decimal:2' 25 | ]; 26 | } 27 | ``` 28 | 29 | In addition to `boolean`, `integer`, and `decimal`, out of the box Laravel supports `real`, `float`, `double`, `string`, `object`, `array`, `collection`, `date`, `datetime`, and `timestamp` casts. 30 | 31 | Sometimes it is convenient to handle more complex types with custom logic, and for casts to be able to listen and react to model events. This is where this package come in handy. 32 | 33 | >Handling events directly from custom casts can be very useful if, for example, we're storing an image using a custom casts and we need to delete it when the model is deleted. *Check out the [old documentation](https://github.com/vkovic/laravel-custom-casts/tree/v1.0.2#example-casting-user-image) for this example.* 34 | 35 | 36 | ### :package: vkovic packages :package: 37 | 38 | Please check out my other packages — they are all free, well-written, and some of them are useful :smile:. If you find something interesting, consider giving me a hand with package development, suggesting an idea or some kind of improvement, starring the repo if you like it, or simply check out the code - there's a lot of useful stuff under the hood. 39 | 40 | - [**vkovic/laravel-commando**](http://bit.ly/2GT7DV7) ~ Collection of useful `artisan` commands 41 | - *Coming soon* [**vkovic/laravel-event-log**](http://bit.ly/2MFtCn8) ~ Easily log and access logged events, optionally with additional data and the related model 42 | 43 | ## Compatibility 44 | 45 | The package is compatible with **Laravel** versions `5.5`, `5.6`, `5.7`, `5.8` and `6` 46 | 47 | and **Lumen** versions `5.5`, `5.6`, `5.7`, `5.8`. 48 | 49 | Laravel 7+ has native support for [Custom Casts](https://laravel.com/docs/7.x/eloquent-mutators#custom-casts) that are incompatible with this library. 50 | 51 | Minimum supported version of PHP is `7.1`. 52 | PHP `8` is also supported. 53 | 54 | ## Installation 55 | 56 | Install the package via Composer: 57 | 58 | ```bash 59 | composer require vkovic/laravel-custom-casts 60 | ``` 61 | 62 | ## Usage 63 | 64 | ### Utilizing a custom cast class 65 | 66 | To enable custom casts in a model, use the `HasCustomCasts` trait and define which attributes will be casted using `$casts` - per Laravel standards. 67 | 68 | ```php 69 | // File: app/User.php 70 | 71 | namespace App; 72 | 73 | use App\CustomCasts\NameCast; 74 | use Illuminate\Database\Eloquent\Model; 75 | use Vkovic\LaravelCustomCasts\HasCustomCasts; 76 | 77 | class User extends Model 78 | { 79 | use HasCustomCasts; 80 | 81 | protected $casts = [ 82 | 'is_admin' => 'boolean', // <-- Laravel default cast type 83 | 'name' => NameCast::class // <-- Our custom cast class (see the section below) 84 | ]; 85 | } 86 | ``` 87 | 88 | ### Defining a custom cast class 89 | 90 | This class will be responsible for our custom casting logic. 91 | 92 | ```php 93 | // File: app/CustomCasts/NameCast.php 94 | 95 | namespace App\CustomCasts; 96 | 97 | use Vkovic\LaravelCustomCasts\CustomCastBase; 98 | 99 | class NameCast extends CustomCastBase 100 | { 101 | public function setAttribute($value) 102 | { 103 | return ucwords($value); 104 | } 105 | 106 | public function castAttribute($value) 107 | { 108 | return $this->getTitle() . ' ' . $value; 109 | } 110 | 111 | protected function getTitle() 112 | { 113 | return ['Mr.', 'Mrs.', 'Ms.', 'Miss'][rand(0, 3)]; 114 | } 115 | } 116 | ``` 117 | 118 | The required `setAttribute` method receives the `$value` being set on the model field, and should return a raw value to store in the database. 119 | 120 | The optional `castAttribute` method receives the raw `$value` from the database, and should return a mutated value. If this method is omitted, the raw database value will be returned. 121 | 122 | For the sake of this example we'll implement one more method which will attach a random title to a user when their name is retrieved from database. 123 | 124 | ### Testing a custom cast class 125 | 126 | Let's create a user and see what happens. 127 | 128 | ```php 129 | $user = new App\User; 130 | $user->name = 'john doe'; 131 | 132 | $user->save(); 133 | ``` 134 | 135 | This will create our new user and store their name in the database, with the first letter of each word uppercased. 136 | 137 | When we retrieve the user and try to access their name, title will be prepended to it — just like we defined in our custom `NameCast` class. 138 | 139 | ```php 140 | dd($user->name); // 'Mr. John Doe' 141 | ``` 142 | 143 | ### Handling model events 144 | 145 | Let's say that we want to notify our administrator when a user's name changes. 146 | 147 | ```php 148 | // File: app/CustomCasts/NameCast.php 149 | 150 | public function updated() 151 | { 152 | $attribute = $this->attribute; 153 | 154 | if($this->model->isDirty($attribute)) { 155 | // Notify admin about name change 156 | } 157 | } 158 | ``` 159 | 160 | In addition to the `updated` method, we can define other methods for standard model events: 161 | `retrieved`, `creating`, `created`, `updating`, `saving`, `saved`, `deleting`, `deleted`, `restoring` and `restored`. 162 | 163 | ### Other functionality 164 | 165 | As you can see from the above code, we can easily access the casted attribute name as well as an instance of the underlying model. 166 | 167 | ```php 168 | // File: app/CustomCasts/NameCast.php 169 | 170 | // Get the name of the model attribute being casted 171 | dd($this->attribute); // 'name' 172 | 173 | // Access our `User` model 174 | dd(get_class($this->model)); // 'App/User' 175 | ``` 176 | 177 | We can also retrieve all casted attributes and their corresponding classes directly from the model. 178 | 179 | ```php 180 | // File: app/User.php 181 | 182 | dd($this->getCustomCasts()); // ['name' => 'App/CustomCasts/NameCast'] 183 | ``` 184 | 185 | ### Using aliased casts 186 | 187 | You may find it easier to use aliases for custom casts, e.g.: 188 | 189 | ```php 190 | protected $casts = [ 191 | 'avatar' => 'image' // <-- You prefer this ... 192 | // --- 193 | 'avatar' => ImageCast::class // <-- ... over this 194 | ]; 195 | ``` 196 | 197 | To make the magic happen, first add the package's service provider to the `providers` array: 198 | 199 | ```php 200 | // File: config/app.php 201 | 202 | 'providers' => [ 203 | // ... 204 | 205 | /* 206 | * Package Service Providers... 207 | */ 208 | Vkovic\LaravelCustomCasts\CustomCastsServiceProvider::class 209 | 210 | // ... 211 | ] 212 | ``` 213 | 214 | Once the provider is added, publish the config file which will be used to associate aliases with their corresponding custom cast classes: 215 | 216 | ```bash 217 | php artisan vendor:publish --provider="Vkovic\LaravelCustomCasts\CustomCastsServiceProvider" 218 | ``` 219 | 220 | This command should create a config file located at `config/custom_casts.php`. Open it up and check out the comments for examples of config options. 221 | 222 | ### Use it without Laravel 223 | 224 | This package can also be used without full Laravel installation, with something like `jenssegers/model` or if your project 225 | is using `illuminate/database` library. 226 | 227 | > #### More examples 228 | > You can find more examples in the [old documentation](https://github.com/vkovic/laravel-custom-casts/tree/v1.0.2#example-casting-user-image). 229 | 230 | ## Contributing 231 | 232 | If you plan to modify this Laravel package you should run the tests that come with it. 233 | The easiest way to accomplish this is with `Docker`, `docker-compose`, and `phpunit`. 234 | 235 | First, we need to initialize Docker container (see `docker-composer.yaml` for details). 236 | 237 | ```bash 238 | docker-compose up --exit-code-from app 239 | ``` 240 | 241 | After that, we can run tests and watch the output: 242 | 243 | ```bash 244 | docker-compose run --rm app phpunit 245 | ``` 246 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vkovic/laravel-custom-casts", 3 | "description": "Make your own custom cast type for Laravel model attributes", 4 | "keywords": [ 5 | "laravel", 6 | "model", 7 | "casts", 8 | "cast", 9 | "datatype" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Vladimir Ković", 15 | "email": "vlada.kovic@gmail.com" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4": { 20 | "Vkovic\\LaravelCustomCasts\\": "src" 21 | }, 22 | "files": [ 23 | "src/helpers.php" 24 | ] 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Vkovic\\LaravelCustomCasts\\Test\\": "tests" 29 | } 30 | }, 31 | "require": { 32 | "php": "^7.1|^8.0" 33 | }, 34 | "require-dev": { 35 | "illuminate/database": "^5.5|^6.20.14|^8.20", 36 | "orchestra/testbench": "^3.5|^4.0|^6.0", 37 | "orchestra/database": "^3.5|^4.0|^5.0", 38 | "phpunit/phpunit": "^6.3|^7.0|^8.0|^9.4" 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit" 42 | }, 43 | "minimum-stability": "dev" 44 | } 45 | -------------------------------------------------------------------------------- /config/custom_casts.php: -------------------------------------------------------------------------------- 1 | ImageCast::class, 14 | | 15 | */ 16 | ]; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: .docker/app/Dockerfile 8 | volumes: 9 | - ./:/var/www/html/ 10 | command: 11 | - /bin/bash 12 | - -c 13 | - | 14 | [ -d vendor/bin ] || composer install 15 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/CustomCastBase.php: -------------------------------------------------------------------------------- 1 | model = $model; 22 | $this->attribute = $attribute; 23 | } 24 | 25 | /** 26 | * Enforce implementation in child classes 27 | * 28 | * Intercept value passed to model under specified field ($attribute) 29 | * and change it to our will, and/or add some logic, before it's going 30 | * to be saved to database 31 | * 32 | * @param mixed $value Default value passed to model attribute 33 | * 34 | * @return mixed 35 | */ 36 | abstract public function setAttribute($value); 37 | 38 | /** 39 | * Cast attribute (from db value to our custom format) 40 | * 41 | * @param mixed $value Value from database field 42 | * 43 | * @return mixed|null Our customized value 44 | */ 45 | public function castAttribute($value) 46 | { 47 | return $value; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CustomCastsServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 19 | package_path('config') => config_path() 20 | ]); 21 | } 22 | 23 | /** 24 | * Register the application services. 25 | * 26 | * @return void 27 | */ 28 | public function register() 29 | { 30 | $this->mergeConfigFrom(package_path('config/custom_casts.php'), 'custom_casts'); 31 | } 32 | } -------------------------------------------------------------------------------- /src/HasCustomCasts.php: -------------------------------------------------------------------------------- 1 | getObservableEvents(); 36 | 37 | foreach ($instance->getCustomCasts() as $attribute => $customCastClass) { 38 | $customCastObject = $instance->getCustomCastObject($attribute); 39 | 40 | foreach ($observableEvents as $event) { 41 | if (method_exists($customCastObject, $event)) { 42 | self::registerListenerForAttribute($event, $attribute); 43 | } 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Registers event listener for specific custom cast attribute 50 | * 51 | * @param string $event 52 | * @param string $attribute 53 | */ 54 | protected static function registerListenerForAttribute($event, $attribute): void 55 | { 56 | static::registerModelEvent( 57 | $event, 58 | /** @param self $model */ 59 | static function ($model) use ($attribute, $event) { 60 | $model->getCustomCastObject($attribute)->$event(); 61 | } 62 | ); 63 | } 64 | 65 | /** 66 | * Hook into setAttribute logic and enable our custom cast do the job. 67 | * 68 | * This method is will override method in HasAttributes trait. 69 | * 70 | * @param $attribute 71 | * @param $value 72 | * 73 | * @return mixed 74 | * 75 | * @see \Illuminate\Database\Eloquent\Concerns\HasAttributes::setAttribute() 76 | */ 77 | public function setAttribute($attribute, $value) 78 | { 79 | // Give mutator priority over custom casts 80 | if ($this->hasSetMutator($attribute)) { 81 | return $this->setMutatedAttributeValue($attribute, $value); 82 | } 83 | 84 | if ($this->isCustomCasts($attribute)) { 85 | $this->attributes[$attribute] = $this->setCustomCast($attribute, $value); 86 | 87 | return $this; 88 | } 89 | 90 | return parent::setAttribute($attribute, $value); 91 | } 92 | 93 | /** 94 | * Filter valid custom casts out of Model::$casts array 95 | * 96 | * @return array - key: model attribute (field name) 97 | * - value: custom cast class name 98 | */ 99 | public function getCustomCasts() 100 | { 101 | if ($this->customCasts !== null) { 102 | return $this->customCasts; 103 | } 104 | 105 | $customCasts = []; 106 | 107 | foreach ($this->casts as $attribute => $type) { 108 | $castClass = $this->getCastClass($type); 109 | 110 | if (is_subclass_of($castClass, CustomCastBase::class)) { 111 | $customCasts[$attribute] = $castClass; 112 | } 113 | } 114 | 115 | $this->customCasts = $customCasts; 116 | 117 | return $customCasts; 118 | } 119 | 120 | /** 121 | * Cast attribute (from db value to our custom format) 122 | * 123 | * @param $attribute 124 | * @param $value 125 | * 126 | * 127 | * @return mixed|null 128 | * 129 | * @see \Illuminate\Database\Eloquent\Concerns\HasAttributes::castAttribute() 130 | */ 131 | protected function castAttribute($attribute, $value) 132 | { 133 | if ($this->isCustomCasts($attribute)) { 134 | return $this->castCustomCast($attribute, $value); 135 | } 136 | 137 | return parent::castAttribute($attribute, $value); 138 | } 139 | 140 | /** 141 | * Cast attribute (from db value to our custom format) 142 | * 143 | * @param $attribute 144 | * @param $value 145 | * 146 | * @return mixed|null 147 | */ 148 | protected function castCustomCast($attribute, $value) 149 | { 150 | return $this->getCustomCastObject($attribute)->castAttribute($value); 151 | } 152 | 153 | /** 154 | * Cast attribute (from db value to our custom format) 155 | * 156 | * @param $attribute 157 | * @param $value 158 | * 159 | * @return mixed|null 160 | */ 161 | protected function setCustomCast($attribute, $value) 162 | { 163 | return $this->getCustomCastObject($attribute)->setAttribute($value); 164 | } 165 | 166 | /** 167 | * Returns true if attribute is custom cast 168 | * 169 | * @param $attribute 170 | * 171 | * @return bool 172 | */ 173 | protected function isCustomCasts($attribute): bool 174 | { 175 | return array_key_exists($attribute, $this->getCustomCasts()); 176 | } 177 | 178 | /** 179 | * Lazy load custom cast object and return it 180 | * 181 | * @param $attribute 182 | * 183 | * @return \Vkovic\LaravelCustomCasts\CustomCastBase 184 | */ 185 | protected function getCustomCastObject($attribute) 186 | { 187 | if (!isset($this->customCastObjects[$attribute])) { 188 | $customCastClass = $this->getCastClass($this->casts[$attribute]); 189 | $customCastObject = new $customCastClass($this, $attribute); 190 | 191 | $this->customCastObjects[$attribute] = $customCastObject; 192 | } 193 | 194 | return $this->customCastObjects[$attribute]; 195 | } 196 | 197 | /** 198 | * Get the cast class name for the given cast type. 199 | * Cast type can either be FQCN of custom cast class 200 | * or user assigned alias defined in config. 201 | * 202 | * @param string $castType 203 | * 204 | * @return string 205 | */ 206 | protected function getCastClass($castType) 207 | { 208 | return function_exists('config') 209 | ? config("custom_casts.$castType", $castType) 210 | : $castType; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | col_1 = ''; 19 | $model->save(); 20 | 21 | $eventsReceived = self::getEventsReceived($model); 22 | 23 | $this->assertContains('creating', $eventsReceived); 24 | $this->assertContains('created', $eventsReceived); 25 | } 26 | 27 | /** 28 | * @test 29 | */ 30 | public function can_handle_updating_event() 31 | { 32 | // Manually create a record in db 33 | DB::table('table_a')->insert(['col_1' => 'a']); 34 | 35 | $model = ModelWithEventHandlingCast::first(); 36 | $model->col_1 = 'b'; 37 | $model->save(); 38 | 39 | $eventsReceived = self::getEventsReceived($model); 40 | 41 | $this->assertContains('updating', $eventsReceived); 42 | $this->assertContains('updated', $eventsReceived); 43 | } 44 | 45 | /** 46 | * @test 47 | */ 48 | public function can_handle_deleting_events() 49 | { 50 | // Manually create a record in db 51 | DB::table('table_a')->insert(['col_1' => '']); 52 | 53 | $model = ModelWithEventHandlingCast::first(); 54 | $model->delete(); 55 | 56 | $eventsReceived = self::getEventsReceived($model); 57 | 58 | $this->assertContains('deleting', $eventsReceived); 59 | $this->assertContains('deleted', $eventsReceived); 60 | } 61 | 62 | protected static function getEventsReceived(Model $model) 63 | { 64 | $customCastObject = parent::getProtectedProperty($model, 'customCastObjects')['col_1']; 65 | 66 | return $customCastObject->eventsReceived; 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /tests/Integration/MiscTest.php: -------------------------------------------------------------------------------- 1 | assertNull($model->col_1); 29 | $this->assertSame(base64_encode('col_1_value'), $model->refresh()->col_1); 30 | 31 | // 32 | // Our logic 33 | // 34 | 35 | $model = ModelWithDefaultValueForCustomCasts::create(); 36 | 37 | $this->assertNull($model->col_1); 38 | $this->assertSame('col_1_value', $model->refresh()->col_1); 39 | } 40 | } 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/Integration/ModelWithAliasedCustomCastsTest.php: -------------------------------------------------------------------------------- 1 | col_1 = $string; 22 | $model->save(); 23 | 24 | // Get raw data (as stdClass) without using `Model` 25 | $tableRow = DB::table('table_a')->first(); 26 | 27 | // Raw data should be base 64 encoded string 28 | $this->assertSame(base64_encode($string), $tableRow->col_1); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function can_access_attribute_via_aliased_custom_casts() 35 | { 36 | $string = Str::random(); 37 | $b64String = base64_encode($string); 38 | 39 | // Save field directly without using `Model` 40 | DB::table('table_a')->insert([ 41 | 'col_1' => $b64String 42 | ]); 43 | 44 | $model = ModelWithAliasedCustomCasts::first(); 45 | 46 | // Retrieved data should be same as initial string 47 | $this->assertSame($string, $model->col_1); 48 | } 49 | } 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/Integration/ModelWithCustomCastsTest.php: -------------------------------------------------------------------------------- 1 | col_1 = $string; 25 | $model->save(); 26 | 27 | // Get raw data (as stdClass) without using `Model` 28 | $tableRow = DB::table('table_a')->find(1); 29 | 30 | // Raw data should be base 64 encoded string 31 | $this->assertSame(base64_encode($string), $tableRow->col_1); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function can_access_attribute_via_custom_casts() 38 | { 39 | $string = Str::random(); 40 | $b64String = base64_encode($string); 41 | 42 | // Save field directly without using `Model` 43 | DB::table('table_a')->insert([ 44 | 'col_1' => $b64String 45 | ]); 46 | 47 | $model = ModelWithCustomCasts::first(); 48 | 49 | // Retrieved data should be same as initial string 50 | $this->assertSame($string, $model->col_1); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function can_mutate_attribute_via_custom_casts_when_using_create() 57 | { 58 | $string = Str::random(); 59 | 60 | // Write model data via `Model` object 61 | ModelWithCustomCasts::create([ 62 | 'col_1' => $string 63 | ]); 64 | 65 | // Get raw data (as stdClass) without using `Model` 66 | $tableRow = DB::table('table_a')->find(1); 67 | 68 | // Raw data should be base 64 encoded string 69 | $this->assertSame(base64_encode($string), $tableRow->col_1); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function can_update_custom_cast_attribute() 76 | { 77 | DB::table('table_a')->insert([ 78 | 'col_1' => '' 79 | ]); 80 | 81 | $string = Str::random(); 82 | 83 | $model = ModelWithCustomCasts::first(); 84 | $model->col_1 = $string; 85 | $model->save(); 86 | 87 | $tableRow = DB::table('table_a')->first(); 88 | 89 | $this->assertSame(base64_encode($string), $tableRow->col_1); 90 | } 91 | 92 | /** 93 | * @test 94 | */ 95 | public function mutator_has_priority_over_custom_casts() 96 | { 97 | $model = new ModelWithMutatorAndCustomCasts; 98 | $model->col_1 = 'mutated_via_custom_casts'; 99 | $model->save(); 100 | 101 | $tableRow = DB::table('table_a')->first(); 102 | 103 | $this->assertSame('mutated_via_mutator', $tableRow->col_1); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function accessor_has_priority_over_custom_casts() 110 | { 111 | DB::table('table_a')->insert(['col_1' => '']); 112 | 113 | $model = ModelWithMutatorAndCustomCasts::first(); 114 | 115 | $this->assertSame('accessed_via_accessor', $model->col_1); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function can_get_list_of_custom_casts() 122 | { 123 | $model1 = new ModelWithCustomCasts; 124 | $model2 = new ModelWithAliasedCustomCasts; 125 | 126 | // This is actual custom casts defined in both models (from above) 127 | // but in second as and alias (which should resolve to a class) 128 | $customCasts = [ 129 | 'col_1' => Base64Cast::class, 130 | ]; 131 | 132 | $this->assertSame($customCasts, $model1->getCustomCasts()); 133 | $this->assertSame($customCasts, $model2->getCustomCasts()); 134 | } 135 | } 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /tests/Integration/ModelWithDefaultValueForCustomCastFieldTest.php: -------------------------------------------------------------------------------- 1 | save(); // Save with default value (defined in migrations) 18 | 19 | $tableRow = DB::table('table_b')->first(); 20 | 21 | $this->assertSame('col_1_value', base64_decode($tableRow->col_1)); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Integration/ModelWithNullableCustomCastFieldTest.php: -------------------------------------------------------------------------------- 1 | save(); // Save with null value (defined in migrations) 18 | 19 | $tableRow = DB::table('table_c')->first(); 20 | 21 | $this->assertNull($tableRow->col_1); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/Support/CustomCasts/Base64Cast.php: -------------------------------------------------------------------------------- 1 | eventsReceived[] = 'booted'; 27 | } 28 | 29 | public function retrieved() 30 | { 31 | $this->eventsReceived[] = 'retrieved'; 32 | } 33 | 34 | public function creating() 35 | { 36 | $this->eventsReceived[] = 'creating'; 37 | } 38 | 39 | public function created() 40 | { 41 | $this->eventsReceived[] = 'created'; 42 | } 43 | 44 | public function updating() 45 | { 46 | $this->eventsReceived[] = 'updating'; 47 | } 48 | 49 | public function updated() 50 | { 51 | $this->eventsReceived[] = 'updated'; 52 | } 53 | 54 | public function saving() 55 | { 56 | $this->eventsReceived[] = 'saving'; 57 | } 58 | 59 | public function saved() 60 | { 61 | $this->eventsReceived[] = 'saved'; 62 | } 63 | 64 | public function deleting() 65 | { 66 | $this->eventsReceived[] = 'deleting'; 67 | } 68 | 69 | public function deleted() 70 | { 71 | $this->eventsReceived[] = 'deleted'; 72 | } 73 | 74 | public function restoring() 75 | { 76 | $this->eventsReceived[] = 'restoring'; 77 | } 78 | 79 | public function restored() 80 | { 81 | $this->eventsReceived[] = 'restored'; 82 | } 83 | } -------------------------------------------------------------------------------- /tests/Support/Models/ModelWithAliasedCustomCasts.php: -------------------------------------------------------------------------------- 1 | 'base64' 9 | ]; 10 | } -------------------------------------------------------------------------------- /tests/Support/Models/ModelWithCustomCasts.php: -------------------------------------------------------------------------------- 1 | Base64Cast::class 18 | ]; 19 | } -------------------------------------------------------------------------------- /tests/Support/Models/ModelWithDefaultValue.php: -------------------------------------------------------------------------------- 1 | Base64Cast::class 18 | ]; 19 | } -------------------------------------------------------------------------------- /tests/Support/Models/ModelWithEventHandlingCast.php: -------------------------------------------------------------------------------- 1 | EventHandlingCast::class 11 | ]; 12 | } -------------------------------------------------------------------------------- /tests/Support/Models/ModelWithMutatorAndCustomCasts.php: -------------------------------------------------------------------------------- 1 | attributes['col_1'] = 'mutated_via_mutator'; 10 | } 11 | 12 | public function getCol1Attribute() 13 | { 14 | return 'accessed_via_accessor'; 15 | } 16 | } -------------------------------------------------------------------------------- /tests/Support/Models/ModelWithNullableValueForCustomCasts.php: -------------------------------------------------------------------------------- 1 | Base64Cast::class 13 | ]; 14 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(package_path('tests/database/migrations')); 24 | } 25 | 26 | /** 27 | * Define environment setup 28 | * 29 | * @param Application $app 30 | * 31 | * @return void 32 | */ 33 | protected function getEnvironmentSetUp($app) 34 | { 35 | $app['config']->set('database.default', 'testbench'); 36 | $app['config']->set('database.connections.testbench', [ 37 | 'driver' => 'sqlite', 38 | 'database' => ':memory:', 39 | ]); 40 | 41 | $app['config']->set('custom_casts.base64', Base64Cast::class); 42 | } 43 | 44 | /** 45 | * Call protected or private method on object 46 | * 47 | * @param object $object 48 | * @param string $methodName 49 | * @param mixed $args 50 | * 51 | * @return mixed 52 | * 53 | * @throws \ReflectionException 54 | */ 55 | protected static function callProtectedMethod($object, $methodName, $args) 56 | { 57 | $class = new \ReflectionClass($object); 58 | $method = $class->getMethod($methodName); 59 | $method->setAccessible(true); 60 | 61 | return $method->invokeArgs($object, (array) $args); 62 | } 63 | 64 | /** 65 | * Get protected or private property of an object 66 | * 67 | * @param object $object 68 | * @param string $property 69 | * 70 | * @return mixed 71 | */ 72 | protected static function getProtectedProperty($object, $property) 73 | { 74 | $reflection = new \ReflectionObject($object); 75 | $property = $reflection->getProperty($property); 76 | $property->setAccessible(true); 77 | 78 | return $property->getValue($object); 79 | } 80 | } -------------------------------------------------------------------------------- /tests/database/migrations/0000_00_00_000000_create_package_test_tables.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $tableRow->text('col_1'); 19 | 20 | $tableRow->timestamps(); 21 | }); 22 | 23 | Schema::create('table_b', function (Blueprint $tableRow) { 24 | $tableRow->increments('id'); 25 | $tableRow->text('col_1')->default(base64_encode('col_1_value')); 26 | 27 | $tableRow->timestamps(); 28 | }); 29 | 30 | Schema::create('table_c', function (Blueprint $tableRow) { 31 | $tableRow->increments('id'); 32 | $tableRow->text('col_1')->nullable(); 33 | 34 | $tableRow->timestamps(); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::dropIfExists('table_a'); 46 | Schema::dropIfExists('table_b'); 47 | Schema::dropIfExists('table_c'); 48 | } 49 | } --------------------------------------------------------------------------------