Skip to content

Commit

Permalink
Merge pull request #2 from spatie/request-data
Browse files Browse the repository at this point in the history
Add a WithData trait
  • Loading branch information
rubenvanassche authored Nov 2, 2021
2 parents fe5f394 + 9e30c15 commit e14ce17
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
80 changes: 70 additions & 10 deletions docs/as-a-data-transfer-object/creating-a-data-object.md
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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{
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
```
17 changes: 17 additions & 0 deletions src/Exceptions/InvalidDataClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Spatie\LaravelData\Exceptions;

use Exception;

class InvalidDataClass extends Exception
{
public static function create(?string $class)
{
$message = $class === null
? 'Could not create a Data object, no data class was given'
: "Could not create a Data object, `{$class}` does not implement `Data`";

return new self($message);
}
}
28 changes: 28 additions & 0 deletions src/WithData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Spatie\LaravelData;

use Spatie\LaravelData\Exceptions\InvalidDataClass;
use Spatie\LaravelData\Resolvers\DataFromSomethingResolver;

trait WithData
{
public function getData(): Data
{
$dataClass = match (true) {
/** @psalm-suppress UndefinedThisPropertyFetch */
property_exists($this, 'dataClass') => $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
);
}
}
64 changes: 64 additions & 0 deletions tests/DataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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);
}
}
10 changes: 5 additions & 5 deletions tests/RuleInferrers/RequiredRuleInferrerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand All @@ -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;
});

Expand All @@ -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;
});

Expand All @@ -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;
});

Expand All @@ -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;
});

Expand Down

0 comments on commit e14ce17

Please sign in to comment.