Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0 #1

Merged
merged 8 commits into from
Apr 27, 2024
Merged

V0 #1

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ name: CI
on: [pull_request]
jobs:
tests:
name: Laravel Dt0 (PHP ${{ matrix.php-versions }})
name: Laravel Dt0 (PHP ${{ matrix.php-versions }} / Orchestra ${{ matrix.orchestra-versions }})
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: [ '8.1', '8.2' ]
orchestra-versions: [ '8.0', '9.0' ]
exclude:
- php-versions: 8.1
orchestra-versions: 9.0
steps:
- name: Checkout
uses: actions/checkout@v3
Expand All @@ -31,6 +35,12 @@ jobs:
- name: Remove composer.lock
run: rm -f composer.lock

- name: Remove pint dependency
run: composer remove "laravel/pint" --dev --no-update

- name: Install Orchestra ${{ matrix.orchestra-versions }}
run: composer require "orchestra/testbench:^${{ matrix.orchestra-versions }}" --dev --no-update

- name: Install Composer dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader

Expand Down
12 changes: 5 additions & 7 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
key: 8.1-composer-${{ hashFiles('**/composer.json') }}
restore-keys: 8.1-composer-

- name: Remove composer.lock
run: rm -f composer.lock
Expand All @@ -44,9 +44,7 @@ jobs:
- name: Compute Coverage
run: vendor/bin/phpunit --coverage-clover ./coverage.xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
files: ./coverage.xml
flags: unittests
name: codecov-Dt0
token: ${{ secrets.CODECOV_TOKEN }}
131 changes: 124 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,145 @@
# Laravel Dt0

[![CI](https://github.com/fab2s/laravel-dt0/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/laravel-dt0/actions/workflows/ci.yml) [![CI](https://github.com/fab2s/laravel-dt0/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/laravel-dt0/actions/workflows/ci.yml)
[![CI](https://github.com/fab2s/laravel-dt0/actions/workflows/ci.yml/badge.svg)](https://github.com/fab2s/laravel-dt0/actions/workflows/ci.yml) [![QA](https://github.com/fab2s/laravel-dt0/actions/workflows/qa.yml/badge.svg)](https://github.com/fab2s/laravel-dt0/actions/workflows/qa.yml) [![codecov](https://codecov.io/gh/fab2s/laravel-dt0/graph/badge.svg?token=YE6AYEDA64)](https://codecov.io/gh/fab2s/laravel-dt0) [![Latest Stable Version](http://poser.pugx.org/fab2s/laravel-dt0/v)](https://packagist.org/packages/fab2s/laravel-dt0) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![License](http://poser.pugx.org/fab2s/dt0/license)](https://packagist.org/packages/fab2s/dt0)

Laravel support for [Dt0](https://github.com/fab2s/dt0), a DTO (_Data-Transport-Object_) PHP implementation than can both secure mutability and implement convenient ways to take control over input and output in various format.
[Laravel](https://laravel.com/) support for [fab2s/dt0](https://github.com/fab2s/dt0), a [DTO](https://en.wikipedia.org/wiki/Data_transfer_object) (_Data-Transport-Object_) PHP implementation that can both secure mutability and implement convenient ways to take control over input and output in various formats.

## Installation

`Dt0` can be installed using composer:

```shell
composer require "fab2s/dt0"
composer require "fab2s/laravel-dt0"
```

Once done, you can start playing :
## Usage

```php
`Laravel Dt0` only adds Validation implementation and model attribute casting to `Dt0`. All other features will work exactly the same. Have a look at [`Dt0`](https://github.com/fab2s/dt0) to find out more.

## Validation

`Laravel Dt0` is able to leverage the full power of Laravel validation on each of its public properties. The validation is performed on the input data prior to any property casting or instantiation.

`Laravel Dt0` comes with a [`Validator`](./src/Validator.php) out of the box that can leverage the full power of [laravel validation](https://laravel.com/docs/master/validation).

To use it on any `Dt0`, just add the [`Validate`](https://github.com/fab2s/dt0/blob/main/src/Attribute/Validate.php) class attribute :

````php
#[Validate(Validator::class)] // same as #[Validate(new Validator)]
class MyDt0 extends Dt0 {
// ...
}
````

### Rules can be added in three ways:

- using the second argument of the [`Validate`](https://github.com/fab2s/dt0/blob/main/src/Attribute/Validate.php) **class attribute**:

````php
use fab2s\Dt0\Attribute\Rule;
use fab2s\Dt0\Attribute\Rules;
use fab2s\Dt0\Attribute\Validate;
use fab2s\Dt0\Laravel\Dt0;
use fab2s\Dt0\Laravel\Validator;

#[Validate(
Validator::class,
new Rules(
propName: new Rule('string|size:2'),
// ...
),
)]
class MyDt0 extends Dt0 {
public readonly string $propName;
}
````

- using the [`Rules`](https://github.com/fab2s/dt0/blob/main/src/Attribute/Rules.php) **class attribute**:

````php
use fab2s\Dt0\Attribute\Rule;
use fab2s\Dt0\Attribute\Rules;
use fab2s\Dt0\Attribute\Validate;
use fab2s\Dt0\Laravel\Dt0;
use fab2s\Dt0\Laravel\Validator;

#[Validate(Validator::class)]
#[Rules(
propName: new Rule(['required', 'string', 'size:2']),
// ...
)]
class MyDt0 extends Dt0 {
public readonly string $propName;
}
````

- using the [`Rule`](https://github.com/fab2s/dt0/blob/main/src/Attribute/Rule.php) **property attribute**:

````php
use fab2s\Dt0\Attribute\Rule;
use fab2s\Dt0\Attribute\Rules;
use fab2s\Dt0\Attribute\Validate;
use fab2s\Dt0\Laravel\Dt0;
use fab2s\Dt0\Laravel\Validator;
use fab2s\Dt0\Laravel\Tests\Artifacts\Rules\Lowercase;

#[Validate(Validator::class)]
class MyDt0 extends Dt0 {
#[Rule(new Lowercase)] // or any custom rule instance
public readonly string $propName;
}
````

Combo of the above three are permitted as illustrated in [`ValidatableDt0`](./tests/Artifacts/ValidatableDt0.php).

> In case of redundancy, priority will be first in `Validate`, `Rules` then `Rule`.
> Dt0 has no opinion of the method used to define rules. They will all perform the same as they are compiled once per process and kept ready for any reuse.

Validation is performed using `withValidation` method:

```php
// either get a Dt0 instance or a ValidationException
$dt0 = SomeValidatableDt0::withValidation(...\Illuminate\Http\Request::all());
$dt0 = SomeValidatableDt0::withValidation(...Request::all());
```

## Model Attribute casting

Should you want to use a `Dt0` as a Laravel Model attribute, you can directly cast it as your `Dt0` thanks to the generic cast [`Dt0Cast`](./src/Casts/Dt0Cast.php).

Only requirement is for your Dt0 to extend [`fab2s\Dt0\Laravel\Dt0`](./src/Dt0.php) or to extend [`fab2s\Dt0\Dt0`](https://github.com/fab2s/dt0/blob/main/src/Dt0.php) _and_ use [`fab2s\Dt0\Laravel\LaravelDt0Trait`](./src/LaravelDt0Trait.php).

````php
use Illuminate\Database\Eloquent\Model;

class SomeModel extends Model
{
protected $casts = [
'some_dt0' => SomeDt0::class,
'some_nullable_dt0' => SomeNullableDt0::class.':nullable',
];
}

$model = new SomeModel;

$model->some_dt0 = '{"field":"value"}';
// or
$model->some_dt0 = ['field' => 'value'];
// or
$model->some_dt0 = SomeDt0::from(['field' => 'value']);

// then
$model->some_dt0->equals(SomeDt0::from('{"field":"value"}')); // true

$model->some_dt0 = null; // throws a NotNullableException
$model->some_nullable_dt0 = null; // works

// can thus be tried
$model->some_nullable_dt0 = SomeNullableDt0::tryFrom($anyInput);
````

## Requirements

`Dt0` is tested against php 8.1 and 8.2
`Dt0` is tested against php 8.1 and 8.2 and Laravel 10 / 11

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
],
"require": {
"php": "^8.1",
"fab2s/dt0": "*",
"fab2s/dt0": "v0.x-dev",
"illuminate/translation": "^10.0|^11.0",
"illuminate/validation": "^10.0|^11.0"
},
Expand Down
69 changes: 69 additions & 0 deletions src/Caster/CollectionOfCaster.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/*
* This file is part of fab2s/laravel-dt0.
* (c) Fabrice de Stefanis / https://github.com/fab2s/dt0
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Dt0\Laravel\Caster;

use fab2s\Dt0\Caster\ArrayType;
use fab2s\Dt0\Caster\CasterInterface;
use fab2s\Dt0\Caster\ScalarType;
use fab2s\Dt0\Caster\ScalarTypeCaster;
use fab2s\Dt0\Dt0;
use fab2s\Dt0\Exception\CasterException;
use fab2s\Dt0\Property\Property;
use Illuminate\Support\Collection;

class CollectionOfCaster implements CasterInterface
{
public readonly ArrayType|ScalarType|string $logicalType;
protected ?ScalarTypeCaster $scalarTypeCaster;

/**
* @throws CasterException
*/
public function __construct(
/** @var class-string<Dt0|UnitEnum>|ScalarType|string */
public readonly ScalarType|string $type,
) {
if (is_string($type)) {
$logicalType = match (true) {
is_subclass_of($type, Dt0::class) => ArrayType::DT0,
is_subclass_of($type, UnitEnum::class) => ArrayType::ENUM,
default => ScalarType::tryFrom($type),
};
} else {
$logicalType = $type;
}

if (! $logicalType) {
throw new CasterException('[' . Dt0::classBasename(static::class) . "] $type is not an ArrayType nor a ScalarType");
}

$this->logicalType = $logicalType;
$this->scalarTypeCaster = $this->logicalType instanceof ScalarType ? new ScalarTypeCaster($this->logicalType) : null;
}

public function cast(mixed $value): ?Collection
{
if (! is_iterable($value)) {
return null;
}

$result = Collection::make();

foreach ($value as $item) {
$result->push(match ($this->logicalType) {
ArrayType::DT0 => $this->type::tryFrom($item),
ArrayType::ENUM => Property::tryEnum($this->type, $item),
default => $this->scalarTypeCaster->cast($item),
});
}

return $result;
}
}
79 changes: 79 additions & 0 deletions src/Casts/Dt0Cast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

/*
* This file is part of fab2s/laravel-dt0.
* (c) Fabrice de Stefanis / https://github.com/fab2s/dt0
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Dt0\Laravel\Casts;

use fab2s\Dt0\Exception\Dt0Exception;
use fab2s\Dt0\Laravel\Dt0;
use fab2s\Dt0\Laravel\Exceptions\NotNullableException;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use JsonException;

class Dt0Cast implements CastsAttributes
{
/**
* @var class-string<Dt0>
*/
protected string $dt0Class;
protected bool $isNullable = false;

/**
* @param class-string<Dt0> $enumClass
* @param string[] ...$options
*/
public function __construct(string $enumClass, ...$options)
{
$this->dt0Class = $enumClass;

$this->isNullable = in_array('nullable', $options);
}

/**
* Cast the given value.
*
* @param Model $model
*
* @throws NotNullableException
* @throws JsonException
* @throws Dt0Exception
*/
public function get($model, string $key, $value, array $attributes): ?Dt0
{
return $this->resolve($model, $key, $value);
}

/**
* Prepare the given value for storage.
*
* @param Model $model
*
* @throws Dt0Exception
* @throws NotNullableException
* @throws JsonException
*/
public function set($model, string $key, $value, array $attributes): ?string
{
return $this->resolve($model, $key, $value)?->toJson();
}

/**
* @throws Dt0Exception
* @throws NotNullableException
* @throws JsonException
*/
protected function resolve(Model $model, string $key, mixed $value): ?Dt0
{
if ($value === null) {
return $this->isNullable ? null : throw NotNullableException::make($key, $model);
}

return $this->dt0Class::from($value);
}
}
19 changes: 19 additions & 0 deletions src/Dt0.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of fab2s/laravel-dt0.
* (c) Fabrice de Stefanis / https://github.com/fab2s/dt0
* This source file is licensed under the MIT license which you will
* find in the LICENSE file or at https://opensource.org/licenses/MIT
*/

namespace fab2s\Dt0\Laravel;

use fab2s\Dt0\Dt0 as BaseDt0;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Support\Arrayable;

abstract class Dt0 extends BaseDt0 implements Arrayable, Castable
{
use LaravelDt0Trait;
}
Loading
Loading