Skip to content

Commit

Permalink
GITBOOK-4: change request with no subject merged in GitBook
Browse files Browse the repository at this point in the history
  • Loading branch information
d8vjork authored and gitbook-bot committed Oct 17, 2023
1 parent 6f06760 commit 8864151
Show file tree
Hide file tree
Showing 3 changed files with 439 additions and 230 deletions.
6 changes: 6 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Table of contents

* [Getting started](README.md)
* [Creating DTOs](creating-dtos.md)
* [Usage](usage.md)
* [TypeScript generator](typescript.md)
328 changes: 328 additions & 0 deletions docs/creating-dtos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
---
description: >-
Creating data transfer object classes and adding properties to them in a
proper way so they can be mapped.
---

# Creating DTOs

## Mapped data types

Apart from being capable of passing native typed PHP properties it can also cast to a tiny range of content that the framework as well as PHP provides:

* Delimited lists (`1,3,7` or `hello-world,foo,bar`) can be converted to arrays or collections with typed child properties like models, dates, etc.
* Models binding from [**simple binding**](creating-dtos.md#models-binding) to [**morph binding**](creating-dtos.md#morph-binding) using custom attributes for the binding and loading relationships.
* Enums backed values can be converted to native PHP enums.
* Dates in string (`2023-09-07 06:35:53`) can be converted to `Carbon` instances.
* Authenticated user using `WithDefaultValue` PHP attribute with the Laravel's interface `Illuminate\Contracts\Auth\Authenticatable`.

### Models binding

When typing a variable with an Eloquent model class it will be mapped to this model querying the database when the instance is not passed through to the DTO.

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use App\Models\Tag;

​final class CreatePostData extends DataTransferObject
{
public function __construct(
public string $title,
public string $content,
public Tag $tag,
) {
//
}
}
```

So sending a `tag` to this to be able to map (do a model binding) you should send the following:

```php
CreatePostData::fromArray(['tag' => 1]);
```

This will query the tag with the id = 1.

#### Customise binding attribute

Now in case we have a `Post` entity with an unique `slug` column that we can use to simplify its binding:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use OpenSoutheners\LaravelDto\Attributes\BindModel;
use App\Models\Post;
use App\Models\Tag;

​final class UpdatePostData extends DataTransferObject
{
public function __construct(
#[BindModel(using: 'slug')]
public Post $post,
public ?string $title = null,
public ?string $content = null,
public ?Tag $tag = null,
) {
//
}
}
```

This way you can send a post by using the slug:

```php
UpdatePostData::fromArray(['post' => 'hello-world']);
```

#### Binding with relationships loaded

Ever dreamed about loading relationships from route bindings [**using DTOs directly in your controllers**](usage.md#usage-in-controllers)? Now is the time to show you **an unique feature from this package**:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use OpenSoutheners\LaravelDto\Attributes\BindModel;
use App\Models\Post;
use App\Models\Tag;

​final class UpdatePostData extends DataTransferObject
{
public function __construct(
#[BindModel(using: 'slug', with: ['author', 'author.roles'])]
public Post $post,
public ?string $title = null,
public ?string $content = null,
public ?Tag $tag = null,
) {
//
}
}
```

### Morph binding

In an example case when you have a relationship on your tags called _taggable_ (a morph relationship) which can attach posts and films models, this is the way to deal with these on DTOs:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use App\Models\Post;
use App\Models\Film;

final class CreateTagData extends DataTransferObject
{
public function __construct(
public string $name,
public Post|Film $taggable,
public string $taggableType,
) {
//
}
}
```

#### Customise binding attribute

In an example when you have slugs only in posts but not films entities you can add the following to determine which morph type will have its binding customised:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use OpenSoutheners\LaravelDto\Attributes\BindModel;
use App\Models\Post;
use App\Models\Film;

final class CreateTagData extends DataTransferObject
{
public function __construct(
public string $name,
#[BindModel(using: [Post::class => 'slug'])]
public Post|Film $taggable,
public string $taggableType,
) {
//
}
}
```

#### Binding with relationships loaded

You may need to add model classes as array keys so DTO mapping will know **which relationship to load on each morph type**:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use OpenSoutheners\LaravelDto\Attributes\BindModel;
use App\Models\Post;
use App\Models\Film;

final class CreateTagData extends DataTransferObject
{
public function __construct(
public string $name,
#[BindModel(with: [
Post::class => ['author', 'author.role'],
Film::class => 'reviews',
])]
public Post|Film $taggable,
public string $taggableType,
) {
//
}
}
```

This way we are binding a `taggable` entity that when is a post will be using slug on this property, while films will use their defaults (`id`).

When loading a post will be getting its `author` and author's `role`, if otherwise is a film it will only load its reviews.

### Mapping collections

We determine as collections arrays and Laravel's collections because of some particular mapping process we do to them, lets imagine we send this to our backend:

```json
{
"tags": "1,3,91"
}
```

Using the following DTO:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;

final class CreatePostData extends DataTransferObject
{
public function __construct(
public array $tags
) {
//
}
}
```

We should get a array from this delimited list, now let's say we wanted to have integers, we could just use a docblock to help us.

```php
use OpenSoutheners\LaravelDto\DataTransferObject;

final class CreatePostData extends DataTransferObject
{
/**
* @param int[] $tags
*/
public function __construct(
public array $tags
) {
//
}
}
```

Now we've an array of integers and so our IDE can also help us when using this property. **But we're not limited to only native types, we can also use models! Sending a comma-delimited list of IDs and typing this properly.**

Same will go for **Laravel collections**, just typing it properly like so:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use Illuminate\Support\Collection;

class CreatePostData extends DataTransferObject
{
/**
* @param \Illuminate\Support\Collection<\App\Models\Tag> $tags
*/
public function __construct(
public Collection $tags
) {
//
}
}
```

The example at the top will bind tag IDs or an array of IDs or tags model instances to a collection of tags instances.

### Mapping with default values

In case you want some default data mapped whenever you use `fromArray` you can use `withDefaults` like:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use App\Enums\PostStatus;

final class CreatePostData extends DataTransferObject
{
public function __construct(
public string $title,
public PostStatus $status,
public string|null $description = null,
public array $tags = []
) {
//
}

/**
* Add default data to data transfer object.
*/
public function withDefaults(): void
{
// Filled will check wether description is on the request or property not null (depending on the context)
if (! $this->filled('description')) {
$this->description = 'Example of a description...';
}

if (empty($this->tags)) {
$this->tags = ['generic', 'post'];
}
}
}
```

#### Default value using attribute

You can use PHP attributes instead, which simplifies it even more as you **only need to send the raw value without it being mapped**:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use OpenSoutheners\LaravelDto\Attributes\WithDefaultValue;

final class CreatePostData extends DataTransferObject
{
public function __construct(
public string $title,
public PostStatus $status,
#[WithDefaultValue('Example of a description...')]
public string|null $description = null,
#[WithDefaultValue(['generic', 'post'])]
public array $tags = []
) {
//
}
}
```

Also this attribute has special usages like the ones following below.

#### Default value as authenticated user

{% hint style="info" %}
This was created after using `Authenticatable` interface as a property type because this interface is not completed with all the types that a `User` model has (or any model used in the authentication of your Laravel application).
{% endhint %}

If the right contract is sent it will grab the authenticated user:

```php
use OpenSoutheners\LaravelDto\DataTransferObject;
use OpenSoutheners\LaravelDto\Attributes\WithDefaultValue;
use Illuminate\Contracts\Auth\Authenticatable;

final class CreatePostData extends DataTransferObject
{
public function __construct(
public string $title,
public PostStatus $status,
#[WithDefaultValue(Authenticatable::class)]
public User $author,
public string|null $description = null,
public array $tags = []
) {
//
}
}
```
Loading

0 comments on commit 8864151

Please sign in to comment.