diff --git a/CHANGELOG.md b/CHANGELOG.md index b83ff5ec..27e71cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-data-resource` will be documented in this file. +## 1.0.2 - 2021-11-02 + +- add a `WithData` trait for quicker getting data from objects + ## 1.0.1 - 2021-10-28 - fix required rules being added when not allowed diff --git a/docs/as-a-data-transfer-object/creating-a-data-object.md b/docs/as-a-data-transfer-object/creating-a-data-object.md index 9b55a0e6..5559a008 100644 --- a/docs/as-a-data-transfer-object/creating-a-data-object.md +++ b/docs/as-a-data-transfer-object/creating-a-data-object.md @@ -1,6 +1,5 @@ --- -title: Creating a data object -weight: 1 +title: Creating a data object weight: 1 --- Let's get started with the following simple data object: @@ -28,7 +27,8 @@ But with this package, you can initialize the data object also with an array: SongData::from(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); ``` -You can use the `from` method to create a data object from nearly anything. For example, let's say you have an Eloquent model like this: +You can use the `from` method to create a data object from nearly anything. For example, let's say you have an Eloquent +model like this: ```php class Song extends Model{ @@ -46,9 +46,12 @@ The package will find the required properties within the model and use them to c ## Magical creation -It is possible to overwrite or extend the behaviour of the `from` method for specific types. So you can construct a data object in a specific manner for that type. This can be done by adding a static method starting with 'from' to the data object. +It is possible to overwrite or extend the behaviour of the `from` method for specific types. So you can construct a data +object in a specific manner for that type. This can be done by adding a static method starting with 'from' to the data +object. -For example, we want to change how we create a data object from a model. We can add a `fromModel` static method that takes the model we want to use as a parameter: +For example, we want to change how we create a data object from a model. We can add a `fromModel` static method that +takes the model we want to use as a parameter: ```php class SongData extends Data @@ -74,7 +77,8 @@ SongData::from(Song::firstOrFail($id)); Instead of the default method, the `fromModel` method will be called to create a data object from the found model. -You're truly free to add as many from methods as you want. For example, you could add one to create a data object from a string: +You're truly free to add as many from methods as you want. For example, you could add one to create a data object from a +string: ```php class SongData extends Data @@ -107,21 +111,77 @@ There are a few requirements to enable magical data object creation: - The method can only take **one typed parameter** for which you want to create an object - The method cannot be called **from** -When the package cannot find such a method for a type given to the data object's `from` method. Then the data object will try to create itself from the following types: +When the package cannot find such a method for a type given to the data object's `from` method. Then the data object +will try to create itself from the following types: - An *Eloquent model* by calling `toArray` on it - A *Laravel request* by calling `all` on it - An *Arrayable* by calling `toArray` on it - An *array* -When a data object cannot be created using magical methods or the default methods, a `CannotCreateDataFromValue` exception will be thrown. +When a data object cannot be created using magical methods or the default methods, a `CannotCreateDataFromValue` +exception will be thrown. ## Optional creation -It is impossible to return `null` from a data object's `from` method since we always expect a data object when calling `from`. To solve this, you can call the `optional` method: +It is impossible to return `null` from a data object's `from` method since we always expect a data object when +calling `from`. To solve this, you can call the `optional` method: ```php SongData::optional(null); // returns null ``` -Underneath the optional method will call the `from` method when a value is given, so you can still magically create data objects. When a null value is given, it will return null. +Underneath the optional method will call the `from` method when a value is given, so you can still magically create data +objects. When a null value is given, it will return null. + +## Quickly getting data from Models, Requests, ... + +By adding the `WithData` trait to a Model, Request or any class that can be magically be converted to a data object, +you'll enable support for the `getData` method. This method will automatically generate a data object for the object it +is called upon. + +For example, let's retake a look at the `Song` model we saw earlier. We can add the `WithData` trait as follows: + +```php +class Song extends Model{ + use WithData; + + protected $dataClass = SongData::class; +} +``` + +Now we can quickly get the data object for the model as such: + +```php +Song::firstOrFail($id)->getData(); // A SongData object +``` + +We can do the same with a FormRequest, we don't use a property here to define the data class but use a method instead: + +```php +class SongRequest extends Request +{ + use WithData; + + protected function dataClass(): string + { + return SongData::class; + } +} +``` + +Now within a controller where the request is injected, we can get the data object like this: + +```php +class SongController +{ + public function __invoke(SongRequest $request): SongData + { + $data = $request->getData(); + + $song = Song::create($data); + + return $data; + } +} +``` diff --git a/src/Exceptions/InvalidDataClass.php b/src/Exceptions/InvalidDataClass.php new file mode 100644 index 00000000..cd933113 --- /dev/null +++ b/src/Exceptions/InvalidDataClass.php @@ -0,0 +1,17 @@ + $this->dataClass, + method_exists($this, 'dataClass') => $this->dataClass(), + default => null, + }; + + if (! is_a($dataClass, Data::class, true)) { + throw InvalidDataClass::create($dataClass); + } + + return resolve(DataFromSomethingResolver::class)->execute( + $dataClass, + $this + ); + } +} diff --git a/tests/DataTest.php b/tests/DataTest.php index 0c4a2984..2595f464 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -5,6 +5,9 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use DateTime; +use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\ValidationException; use Spatie\LaravelData\Attributes\WithTransformer; use Spatie\LaravelData\Data; @@ -23,6 +26,7 @@ use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithoutConstructor; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; +use Spatie\LaravelData\WithData; class DataTest extends TestCase { @@ -610,4 +614,64 @@ public function it_can_create_a_data_object_from_a_model() $this->assertNull($data->nullable_date); $this->assertTrue($data->enum->equals(DummyEnum::published())); } + + /** @test */ + public function it_can_add_the_with_data_trait_to_a_request() + { + $formRequest = new class () extends FormRequest { + use WithData; + + public string $dataClass = SimpleData::class; + }; + + $formRequest->replace([ + 'string' => 'Hello World', + ]); + + $data = $formRequest->getData(); + + $this->assertEquals(SimpleData::from('Hello World'), $data); + } + + /** @test */ + public function it_can_add_the_with_data_trait_to_a_model() + { + $model = new class () extends Model { + use WithData; + + protected string $dataClass = SimpleData::class; + }; + + $model->fill([ + 'string' => 'Hello World', + ]); + + $data = $model->getData(); + + $this->assertEquals(SimpleData::from('Hello World'), $data); + } + + /** @test */ + public function it_can_define_the_with_data_trait_data_class_by_method() + { + $arrayable = new class () implements Arrayable { + use WithData; + + public function toArray() + { + return [ + 'string' => 'Hello World', + ]; + } + + protected function dataClass(): string + { + return SimpleData::class; + } + }; + + $data = $arrayable->getData(); + + $this->assertEquals(SimpleData::from('Hello World'), $data); + } } diff --git a/tests/RuleInferrers/RequiredRuleInferrerTest.php b/tests/RuleInferrers/RequiredRuleInferrerTest.php index 8c4cccae..563f5be2 100644 --- a/tests/RuleInferrers/RequiredRuleInferrerTest.php +++ b/tests/RuleInferrers/RequiredRuleInferrerTest.php @@ -24,7 +24,7 @@ public function setUp(): void /** @test */ public function it_wont_add_a_required_rule_when_a_property_is_non_nullable() { - $dataProperty = $this->getProperty(new class() extends Data { + $dataProperty = $this->getProperty(new class () extends Data { public string $string; }); @@ -36,7 +36,7 @@ public function it_wont_add_a_required_rule_when_a_property_is_non_nullable() /** @test */ public function it_wont_add_a_required_rule_when_a_property_is_nullable() { - $dataProperty = $this->getProperty(new class() extends Data { + $dataProperty = $this->getProperty(new class () extends Data { public ?string $string; }); @@ -48,7 +48,7 @@ public function it_wont_add_a_required_rule_when_a_property_is_nullable() /** @test */ public function it_wont_add_a_required_rule_when_a_property_already_contains_a_required_rule() { - $dataProperty = $this->getProperty(new class() extends Data { + $dataProperty = $this->getProperty(new class () extends Data { public string $string; }); @@ -60,7 +60,7 @@ public function it_wont_add_a_required_rule_when_a_property_already_contains_a_r /** @test */ public function it_wont_add_a_required_rule_when_a_property_already_contains_a_required_object_rule() { - $dataProperty = $this->getProperty(new class() extends Data { + $dataProperty = $this->getProperty(new class () extends Data { public string $string; }); @@ -72,7 +72,7 @@ public function it_wont_add_a_required_rule_when_a_property_already_contains_a_r /** @test */ public function it_wont_add_a_required_rule_when_a_property_already_contains_a_boolean_rule() { - $dataProperty = $this->getProperty(new class() extends Data { + $dataProperty = $this->getProperty(new class () extends Data { public string $string; });