├── .github └── workflows │ └── php.yml ├── .gitignore ├── .styleci.yml ├── README.md ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── src ├── Cast │ ├── CarbonIntervalCast.php │ └── DateIntervalCast.php └── Exception │ ├── DateIntervalCastException.php │ └── InvalidIsoDuration.php └── tests ├── CastsIntervalsTest.php ├── TestCase.php └── migrations └── 2020_02_26_055304_create_test_table.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP 2 | 3 | on: [push] 4 | 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | max-parallel: 15 10 | matrix: 11 | php-versions: ['7.3', '7.4'] 12 | name: Tests - PHP ${{ matrix.php-versions }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v1 18 | with: 19 | php-version: ${{ matrix.php-versions }} 20 | extension: xdebug 21 | coverage: xdebug 22 | - name: Install dependencies 23 | run: composer install --no-interaction --prefer-dist --no-suggest 24 | - name: Lint composer.json 25 | run: composer validate 26 | - name: Run Tests 27 | run: composer test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | disabled: 3 | - concat_without_spaces 4 | - not_operator_with_successor_space 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel DateInterval / CarbonInterval Cast 2 | 3 | [![Build Status](https://img.shields.io/github/workflow/status/atymic/laravel-dateinterval-cast/PHP?style=flat-square)](https://github.com/atymic/laravel-dateinterval-cast/actions) 4 | [![StyleCI](https://styleci.io/repos/243181977/shield)](https://styleci.io/repos/243181977) 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/atymic/laravel-dateinterval-cast.svg?style=flat-square)](https://packagist.org/packages/atymic/laravel-dateinterval-cast) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/atymic/laravel-dateinterval-cast.svg?style=flat-square)](https://packagist.org/packages/atymic/laravel-dateinterval-cast) 7 | 8 | Laravel has built-in casting for `date` & `datetime` types, but if you want to use ISO 8061 durations with the native 9 | `DateInterval` class, or Carbon's `CarbonInterval` you're out of luck. 10 | 11 | This package provides two custom casts (for `DateInterval` and `CarbonInterval` respectively) using Laravel 7.x/8.x's custom 12 | casts feature. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | composer require atymic/laravel-dateinterval-cast 18 | ``` 19 | 20 | ## Using this package 21 | 22 | In your model's `$casts`, assign the property you wish to enable casting on to either of the casts provided by the package. 23 | You should use a `varchar`/`string` field in your database table. 24 | 25 | ```php 26 | class TestModel extends Model 27 | { 28 | /** 29 | * The attributes that should be cast to native types. 30 | * 31 | * @var array 32 | */ 33 | protected $casts = [ 34 | 'is_xyz' => 'boolean', 35 | 'date_interval' => DateIntervalCast::class, 36 | 'carbon_interval' => CarbonIntervalCast::class, 37 | ]; 38 | } 39 | ``` 40 | 41 | The property on the model will then be cast to an interval object, and saved to the database as a ISO 8061 duration string. 42 | If you try to assign an invalid duration (or the database table contains one, and you use a getter) an exception is thrown. 43 | 44 | 45 | ```php 46 | $model = new TestModel(); 47 | 48 | $model->carbon_interval = now()->subHours(3)->diffAsCarbonInterval(); 49 | 50 | $model->save(); // Saved as `P3H` 51 | $model->fresh(); 52 | 53 | $model->carbon_interval; // Instance of `CarbonInterval` 54 | $model->carbon_interval->forHumans(); // prints '3 hours ago' 55 | 56 | try { 57 | $model->carbon_interval = 'not_a_iso_period'; 58 | } catch (\Atymic\DateIntervalCast\Exception\InvalidIsoDuration $e) { 59 | // Exception thrown if you try to assign an invalid duration 60 | } 61 | ``` 62 | 63 | ## Contributing 64 | 65 | Contributions welcome :) 66 | Please create a PR and i'll review/merge it. 67 | 68 | 69 | ## Licence 70 | MIT 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atymic/laravel-dateinterval-cast", 3 | "description": "", 4 | "keywords": ["Laravel", "DateInterval", "Eloquent", "Cast"], 5 | "authors": [ 6 | { 7 | "name": "atymic", 8 | "email": "atymicq@gmail.com", 9 | "homepage": "https://atymic.dev" 10 | } 11 | ], 12 | "homepage": "https://github.com/atymic/laravel-dateinterval-cast", 13 | "require": { 14 | "php": "^7.3", 15 | "laravel/framework": "^7.0 || ^8.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.0", 19 | "orchestra/testbench": "^5.0 || ^6.0", 20 | "phpstan/phpstan": "^0.12.9" 21 | }, 22 | "minimum-stability": "dev", 23 | "prefer-stable": true, 24 | "license": "MIT", 25 | "autoload": { 26 | "psr-4": { 27 | "Atymic\\DateIntervalCast\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Atymic\\DateIntervalCast\\Tests\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit -c ./phpunit.xml --colors=always", 37 | "analysis": "vendor/bin/phpstan analyse" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 5 3 | paths: 4 | - src 5 | - tests -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Cast/CarbonIntervalCast.php: -------------------------------------------------------------------------------- 1 | CarbonInterval::getDateIntervalSpec($value)]; 46 | } catch (\Exception $e) { 47 | throw InvalidIsoDuration::make($value, $e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Exception/DateIntervalCastException.php: -------------------------------------------------------------------------------- 1 | getCode(), 16 | $previous 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/CastsIntervalsTest.php: -------------------------------------------------------------------------------- 1 | date_interval = new \DateInterval('P1D'); 22 | $this->assertInstanceOf(\DateInterval::class, $model->date_interval); 23 | $this->assertSame('P1D', $model->getAttributes()['date_interval']); 24 | 25 | $model->save(); 26 | $model->fresh(); 27 | 28 | $this->assertInstanceOf(\DateInterval::class, $model->date_interval); 29 | $this->assertDatabaseHas('test', ['id' => $model->id, 'date_interval' => 'P1D']); 30 | } 31 | 32 | public function testCarbonIntervalCast() 33 | { 34 | $model = new TestEloquentModelWithCustomCasts(); 35 | 36 | $model->carbon_interval = new CarbonInterval('P4D'); 37 | $this->assertInstanceOf(CarbonInterval::class, $model->carbon_interval); 38 | $this->assertSame('P4D', $model->getAttributes()['carbon_interval']); 39 | 40 | $model->save(); 41 | $model->fresh(); 42 | 43 | $this->assertInstanceOf(CarbonInterval::class, $model->carbon_interval); 44 | $this->assertDatabaseHas('test', ['id' => $model->id, 'carbon_interval' => 'P4D']); 45 | } 46 | 47 | public function testThrowsExceptionOnInvalidDateInterval() 48 | { 49 | $id = TestEloquentModelWithCustomCasts::query()->insertGetId(['carbon_interval' => 'XYZ']); 50 | 51 | $this->expectException(InvalidIsoDuration::class); 52 | $this->expectExceptionMessage('is not a valid ISO 8601 duration'); 53 | 54 | $model = TestEloquentModelWithCustomCasts::find($id); 55 | 56 | // Calls magic getter 57 | $model->carbon_interval; 58 | } 59 | } 60 | 61 | class TestEloquentModelWithCustomCasts extends Model 62 | { 63 | protected $table = 'test'; 64 | 65 | /** 66 | * The attributes that should be cast to native types. 67 | * 68 | * @var array 69 | */ 70 | protected $casts = [ 71 | 'date_interval' => DateIntervalCast::class, 72 | 'carbon_interval' => CarbonIntervalCast::class, 73 | ]; 74 | } 75 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom(__DIR__ . '/migrations'); 14 | } 15 | 16 | /** 17 | * @param \Illuminate\Foundation\Application $app 18 | * 19 | * @return void 20 | */ 21 | protected function getEnvironmentSetUp($app) 22 | { 23 | // Setup default database to use sqlite :memory: 24 | $app['config']->set('database.default', 'test'); 25 | $app['config']->set('database.connections.test', [ 26 | 'driver' => 'sqlite', 27 | 'database' => ':memory:', 28 | 'prefix' => '', 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/migrations/2020_02_26_055304_create_test_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('date_interval')->nullable(); 19 | $table->string('carbon_interval')->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('test'); 32 | } 33 | } 34 | --------------------------------------------------------------------------------