Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/omarfawzi/DTO-Hydrator

Dynamic Hydration for Dto's used by Transformers in API Platform .
https://github.com/omarfawzi/DTO-Hydrator

api api-platform symfony-bundle

Last synced: about 1 month ago
JSON representation

Dynamic Hydration for Dto's used by Transformers in API Platform .

Awesome Lists containing this project

README

        

[![Software License][ico-license]](LICENSE.md)
[![Latest Stable Version](https://poser.pugx.org/salamov/dto-hydrator/v/stable)](https://packagist.org/packages/salamov/dto-hydrator)
[![Total Downloads](https://poser.pugx.org/salamov/dto-hydrator/downloads)](https://packagist.org/packages/salamov/dto-hydrator)

# Description

Dynamic Hydration for DTO's used by Transformers in [API Platform](https://api-platform.com) .

## Install

`composer require salamov/dto-hydrator`

## Usage

While using API Platform , using custom DTO's (defining custom input/output in the `@ApiResource`) requires you to deal with custom
transformers specially when your DTO represents an Entity .

1. When using an input transformer **i.e : after the request body is denormalized into a DTO** ,
you should dehydrate the DTO **i.e** transform it to an entity in order to be persisted .

2. When using an output transformer **i.e : after the entity is retrieved and waiting to be normalized** ,
you should hydrate the DTO **i.e** transform the entity into a DTO in order to be displayed .

3. The embedded objects inside the Dto must be an `@ApiResource` in order to generate the swagger documentation

4. If the embedded objects defines `id` within the denormalized
request body it will be referenced and the data will be updated , either it will be persisted as a new `Entity`.

## Example

```
use Symfony\Component\Serializer\Annotation\Groups;

class OrderDto {

/**
* @var integer $id
* @Groups({"order:read","order:put"})
*/
protected $id;

/**
* @var Customer $customer
* @Groups({"order:read","order:put"})
*/
protected $customer;

/**
* @var Product[] $products
* @Groups({"order:read","order:put"})
*/
protected $products;

/**
* @return int
*/
public function getId(): int
{
return $this->id;
}

/**
* @param int $id
*/
public function setId(int $id)
{
$this->id = $id;
}

/**
* @return Customer
*/
public function getCustomer(): Customer
{
return $this->customer;
}

/**
* @param Customer $customer
*/
public function setCustomer(Customer $customer)
{
$this->customer = $customer;
}

/**
* @return Product[]
*/
public function getProducts(): array
{
return $this->products;
}

/**
* @param Product[] $products
*/
public function setProducts(array $products)
{
$this->products = $products;
}
}

```

```
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiProperty;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Table
* @ORM\Entity
* @ApiResource(
* itemOperations={
* "get":{
* "method":"get",
* "input":false,
* "output":OrderDto::class,
* "status":200,
* "normalization_context":{"groups":{"order:read"}}
* },
* "put":{
* "method":"put",
* "input":OrderDto::class,
* "output":OrderDto::class,
* "status":200,
* "denormalization_context":{"groups":{"order:put"}},
* "normalization_context":{"groups":{"order:read"}}
* }
* },
* collectionOperations={}
* )
*/
class Order {
/**
* @var integer
*
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
* @ApiProperty(identifier=true)
*/
protected $id;

/**
* @var Customer $customer
* @ORM\ManyToOne(targetEntity="Customer",cascade={"persist"})
* @ORM\JoinColumn(name="customerId", referencedColumnName="id")
*/
protected $customer;

/**
* @var Product[] $products
* @ORM\OneToMany(targetEntity="Product",cascade={"persist"})
*/
protected $products;

/**
* @return int
*/
public function getId(): int
{
return $this->id;
}

/**
* @param int $id
*/
public function setId(int $id)
{
$this->id = $id;
}

/**
* @return Customer
*/
public function getCustomer(): Customer
{
return $this->customer;
}

/**
* @param Customer $customer
*/
public function setCustomer(Customer $customer)
{
$this->customer = $customer;
}

/**
* @return Product[]
*/
public function getProducts(): array
{
return $this->products;
}

/**
* @param Product[] $products
*/
public function setProducts(array $products)
{
$this->products = $products;
}
}
```

Above we defined an `OrderDto` for the `Order` Entity with two embedded objects
`Customer` and `Products` one is related to `Order` by a `ManyToOne` relations ship
and the other is related to `Order` by a `OneToMany` relationship.

### GET Request

```
GET https://domain.com/api/orders/{id}
```

The above request will go first through your custom dataProvider (if you have one) retrieving
the `Order` Entity to be transformed and normalized , if you defined an `Ouput` for your `@ApiResource`
then you should define a transformer your transformer would look like this :

```
use DtoHydrator\Factory\DtoDtoFactory;
use DtoHydrator\Dto\Hydrator;

class OrderDtoOutputTransformer {

/** @var DtoFactory $factory */
protected $factory;

/**
* OrderDtoOutputTransformer constructor.
*
* @param DtoFactory $factory
*/
public function __construct(DtoFactory $factory) { $this->factory = $factory; }

/**
* Checks whether the transformation is supported for a given data and context.
*
* @param object|array $data object on normalize / array on denormalize
* @param string $to
* @param array $context
* @return bool
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return OrderDto::class === $to && $data instanceof Order;
}

/**
* Transforms the given object to something else, usually another object.
* This must return the original object if no transformation has been done.
*
* @param Order $order
*
* @param string $to
* @param array $context
* @return OrderDto
*/
public function transform($order, string $to, array $context = []): OrderDto
{
$orderDto = new OrderDto();

$this->factory->make(Hydrator::class)->hydrate($order,$orderDto,$context);

return $orderDto;
}

}
```

### PUT Request

```
PUT https://domain.com/api/orders/{id}

body={
id:1,
customer:{
id:1,
fname:"Omar",
lnaem:"Salamov"
},
products:[
{
id:10,
name:"Long Sleeves",
brand: "Gap"
},
{
id:11,
name:"Short Sleves",
brand: "H&M"
}
]
}
```

The above request will be denormalized to your `Input` class defined in the `@ApiResource`
then to your `InputTransformer` in order to be transformed to an `Entity` and persisted , so your `InputTransformer`
would look like this :

```
use DtoHydrator\Factory\DtoDtoFactory;
use DtoHydrator\Dto\Dehydrator;
use Doctrine\ORM\EntityManagerInterface;

class OrderDtoInputTransformer {

/** @var DtoFactory $factory */
protected $factory;

/** @var EntityManagerInterface $entityManager */
protected $entityManager;

/**
* OrderDtoInputTransformer constructor.
*
* @param DtoFactory $factory
* @param EntityManagerInterface $entityManager
*/
public function __construct(DtoFactory $factory, EntityManagerInterface $entityManager)
{
$this->factory = $factory;
$this->entityManager = $entityManager;
}

/**
* Checks whether the transformation is supported for a given data and context.
*
* @param object|array $data object on normalize / array on denormalize
* @param string $to
* @param array $context
* @return bool
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return Order::class === $to && in_array('order:put',$context['groups']);
}

/**
* Transforms the given object to something else, usually another object.
* This must return the original object if no transformation has been done.
*
* @param OrderDto $order
* @param string $to
* @param array $context
* @return Order
* @throws Throwable
*/
public function transform($orderDto, string $to, array $context = []): Order
{
/** @var Order $order */
$order = $this->entityManager->getReference(Order::class, $orderDto->getId());

// in case you are creating a new order
// you will need to instantiate a new instance from order instead of referencing

$this->factory->make(Dehydrator::class)->hydrate($orderDto,$order,$context);

return $order;
}
}
```

**N.B :** If your body/embedded body contains an `id` it will be directly referenced to
the corresponding entity for that `id` and the data will be updated , if not it will be created .

## Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square