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

Add a WithData trait #2

Merged
merged 5 commits into from
Nov 2, 2021
Merged
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
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, ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also add an example for a request to make it very clear.


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