Skip to content

Commit

Permalink
Implements v1.0.0 (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
artyuum authored Dec 2, 2022
1 parent b4e2600 commit 769bc36
Show file tree
Hide file tree
Showing 45 changed files with 1,690 additions and 460 deletions.
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"version": 2,
"updates": [
{
"allow": [
{
"dependency-type": "all"
}
],
"directory": "/",
"package-ecosystem": "composer",
"schedule": {
"interval": "weekly"
},
"versioning-strategy": "widen"
},
{
"directory": "/",
"package-ecosystem": "github-actions",
"schedule": {
"interval": "weekly"
}
},
]
}
16 changes: 4 additions & 12 deletions .github/workflows/php.yml → .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
name: PHP Composer

on:
push:
branches:
- master
- wip/*
pull_request:
branches:
- master
- wip/*
name: Checks

on: push

permissions:
contents: read
Expand Down Expand Up @@ -37,7 +29,7 @@ jobs:
run: composer install --prefer-dist --no-progress

- name: Run PHPStan
run: composer phpstan
run: vendor/bin/phpstan analyze

- name: Run test suite
run: vendor/bin/phpunit
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@
# Composer
vendor/
composer.lock

# PHPUnit
.phpunit.cache

# PHPCSFixer
.php-cs-fixer.cache
44 changes: 44 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
->notPath(__DIR__ . '/src/Resources/config/services.php')
->notPath(__DIR__ . '/src/DependencyInjection/Configuration.php')
;

$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
'array_indentation' => true,
'method_chaining_indentation' => true,
'no_useless_else' => true,
'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
'global_namespace_import' => true,
'braces' => true,
'indentation_type' => true,
'binary_operator_spaces' => [
'operators' => [
'=>' => 'align_single_space_minimal',
],
],
'yoda_style' => [
'equal' => false,
'identical' => false,
'less_and_greater' => false,
],
'concat_space' => [
'spacing' => 'one',
],
'no_unreachable_default_argument_value' => true,
'no_useless_return' => true,
'phpdoc_order' => true,
'strict_comparison' => true,
'strict_param' => true,
])
->setIndent(str_pad('', 4))
->setFinder(
$finder
);
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog
This library follows [semantic versioning](https://semver.org).

See the [releases pages](https://github.com/artyuum/request-dto-mapper-bundle/releases).

## v0.0.2
Allowed PHP 8.0 in composer.json

## v0.0.1
First release.
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Contributing
If you'd like to contribute, please fork the repository and make changes as you'd like. Be sure to follow the same coding style & naming used in this library to produce a consistent code.
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2021-present Dynèsh HASSANALY

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
234 changes: 204 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,226 @@
# Request DTO Mapper Bundle
This bundle provides an easy way to automatically map the incoming request data to a DTO and optionally validate it.
![image](https://user-images.githubusercontent.com/17199757/193117824-e5eec5b6-f4c0-4c96-af9b-fa2bc6096806.png)

This bundle provides an easy way to automatically map the incoming request data to a DTO and optionally validate it. It's using the powerful [Serializer](https://symfony.com/doc/current/components/serializer.html) component under the hood along with the [Validator](https://symfony.com/doc/current/components/validator.html) component (optional).

## Requirements
- PHP ^8.0
- Symfony ^5.0 or ^6.0

## Installation
```
composer require artyuum/request-dto-mapper-bundle
```bash
composer require artyuum/request-dto-mapper-bundle
```

## Configuration
This is the default configuration (`config/packages/artyuum_request_dto_mapper_bundle.yaml`):
```yml
artyuum_request_dto_mapper_bundle:
enabled: true # whether to enable/disable the argument resolver
# config/packages/artyum_request_dto_mapper.yaml
artyum_request_dto_mapper:

# Used if the attribute does not specify any (must be a FQCN implementing "\Artyum\RequestDtoMapperBundle\Extractor\ExtractorInterface").
default_extractor: null # Example: Artyum\RequestDtoMapperBundle\Extractor\JsonExtractor

# The configuration related to the denormalizer (https://symfony.com/doc/current/components/serializer.html).
denormalizer:

# Used when mapping the request data to the DTO if the attribute does not set any.
default_options: []

# Used when mapping the request data to the DTO (merged with the values passed by the attribute or "default_options").
additional_options: []

# The configuration related to the validator (https://symfony.com/doc/current/validation.html).
validation:

# Whether to validate the DTO after mapping it.
enabled: false

# Used when validating the DTO if the attribute does not set any.
default_groups: []

# Used when validating the DTO (merged with the values passed by the attribute or "default_groups").
additional_groups: []

# Whether to throw an exception if the DTO validation failed (constraint violations).
throw_on_violation: true
```
## Usage
This is a simple example of how to make a DTO that will be used by the bundle:
This is a simple step-by-step example of how to make a DTO that will be used by the bundle.
1. Create the DTO that represents the structure of the content the user will send to your controller.
```php
/**
* @Dto(methods={"POST"}, source="json", validationGroups={"create"})
* @Dto(methods={"PATCH"}, source="json", validationGroups={"edit"})
*/
class ArtistPayload implements DtoInterface {
class PostPayload {
/**
* @Assert\Sequentially({
* @Assert\NotBlank(groups={"create"}),
* @Assert\NotBlank,
* @Assert\Type("string")
* }, groups={"create", "edit"})
* })
*
* @var string|null should contain the artist's name
* @var string|null
*/
public $name = null;
public $content;
}
```

Your DTOs **must** implement the [`DtoInterface`](/src/Mapper/DtoInterface.php) and define the request context using the `@Dto` annotation.
2. Inject the DTO into your controller & configure it using the [Dto attribute](/src/Attribute/Dto.php).

Here is the list of options used in the `@Dto` annotation:
| name | type | default | required | description |
|----------------------|---------|---------|----------|-------------------------------------------------------------------------------------------------------------------|
| `methods` | array | - | **yes** | The HTTP method(s) on which to apply the options below. |
| `source` | string | - | **yes** | The source from where the data will be extracted from the request. (`json`, `query_strings` or `body_parameters`) |
| `validation` | boolean | *true* | no | Whether or not to validate the DTO before passing it to the controller. |
| `validationGroups` | array | *null* | no | The validation groups to be used when validating the DTO. |
```php
use Artyum\RequestDtoMapperBundle\Attribute\Dto;
use Artyum\RequestDtoMapperBundle\Extractor\JsonExtractor;

## Events
- **[PreDtoMappingEvent](/src/Event/PreDtoMappingEvent.php)** - disptached before the mapping is made, this allows you to alter the Serializer/Denormalizer options, or the Request object.
- **[PreDtoValidationEvent](/src/Event/PreDtoValidationEvent.php)** - dispatched before the validation is made, this allows you to alter the DTO object (if the validation is enabled).
- **[PostDtoMappingEvent](/src/Event/PostDtoMappingEvent.php)** - disptached at the very end of the process, this allows you to alter the DTO before it's passed to the controller.
class CreatePostController extends AbstractController
{
#[Dto(extractor: JsonExtractor::class, subject: PostPayload::class, validate: true)]
public function __invoke(PostPayload $postPayload): Response
{
// At this stage, your DTO has automatically been mapped (from the JSON input) and validated.
// Your controller can safely be executed knowing that the submitted content
// matches your requirements (defined in your DTO through the validator constraints).
}
}
```

**Alternatively**, you can set the attribute directly on the argument:
```php
public function __invoke(#[Dto(extractor: JsonExtractor::class, validate: true)] PostPayload $postPayload): Response
{
}
```

If you have set some default options in the configuration file (the default extractor to use, whether to enable the validation), you can even make it shorter:
```php
public function __invoke(#[Dto] PostPayload $postPayload): Response
{
}
```

3. That's it!

## Attribute
The [Dto attribute](src/Attribute/Dto.php) has the following seven properties:

### 1. Extractor
The FQCN (Fully-Qualified Class Name) of a class that implements the `ExtractorInterface`. It basically contains the extraction logic and it's called by the mapper in order to extract the data from the request.

The bundle already comes with 3 built-in extractors that should meet most of your use-cases:
- [BodyParameterExtractor](/src/Extractor/BodyParameterExtractor.php) (extracts the data from `$request->request->all()`)
- [JsonExtractor](/src/Extractor/JsonExtractor.php) (extracts the data from `$request->toArray()`)
- [QueryStringExtractor](/src/Extractor/QueryStringExtractor.php) (extracts the data from `$request->query->all()`)

If an error occurs when the `extract()` method is called from the extractor class, the [ExtractionFailedException](src/Exception/ExtractionFailedException.php) will be thrown.

If these built-in extractor classes don't meet your needs, you can implement your own extractor like this:

```php
use Artyum\RequestDtoMapperBundle\Extractor\ExtractorInterface;
use Symfony\Component\HttpFoundation\Request;

class CustomExtractor implements ExtractorInterface
{
// you can optionally inject dependencies
public function __construct() {
}

public function extract(Request $request): array
{
// your custom extraction logic here
}
}
```
Then pass it to the `Dto` attribute like this:

```php
#[Dto(extractor: CustomExtractor::class)]
```

If you don't set any value, the default value (defined in the bundle's configuration file) will be used.

**Note:** All classes implementing `ExtractorInterface` are automatically tagged as "artyum_request_dto_mapper.extractor",
and this is needed by the mapper in order to retrieve the needed extractor class instance from the container.

## Known limitations
1. Doesn't work with DTOs that have dependencies. (e.g. injecting a class in the DTO's constructor)
### 2. Subject
The FQCN (Fully-Qualified Class Name) of the DTO you want to map (it must be present as your controller argument).

The "subject" property is required **only** if you're setting the attribute directly on the method. Example:

```php
#[Dto(subject: PostPayload::class)]
public function __invoke(PostPayload $postPayload): Response
{
}
```

If you're setting the attribute on the method argument instead, the "subject" value can be omitted and won't be read by the mapper. Example:
```php
public function __invoke(#[Dto] PostPayload $postPayload): Response
{
}
```

### 3. Methods
It can contain a single or an array of HTTP methods that will "enable" the mapping/validation depending on the current HTTP method. In the following example, the DTO will be mapped & validated only if the request method is "GET".
```php
#[Dto(methods: 'GET')]
```
or
```php
#[Dto(methods: ['GET'])]
```

If the array is empty (this is the default value), the mapper will always map the DTO and validate it.

### 4. Denormalization Options
The options that will be passed to the [Denormalizer](https://symfony.com/doc/current/components/serializer.html) before mapping the DTO.

Example:
```php
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;

#[Dto(denormalizerOptions: [ObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true])]
```

If an error occurs when the `denormalize()` method is called from the Denormalizer, the [DtoMappingFailedException](src/Exception/DtoMappingFailedException.php) will be thrown.

### 5. Validate
Whether to validate the DTO (once the mapping is done). Internally, the [validator component](https://symfony.com/doc/current/validation.html) will be used, and if you do not have it installed a `LogicException` will be thrown.

Example:
```php
#[Dto(validate: true)]
```

If the validation failed (due to the constraint violations), the constraint violations will be available as request attribute:
```php
$request->attributes->get('_constraint_violations')
```

If you don't set any value, the configured value (defined in the bundle's configuration file) will be used.

### 6. Validation Groups
The [validation groups](https://symfony.com/doc/current/form/validation_groups.html) to pass to the validator.

Example:
```php
#[Dto(validationGroups: ['creation'])]
```

If you don't set any value, the configured value (defined in the bundle's configuration file) will be used.

### 7. Throw on violation
When the validation failed, the [DtoValidationFailedException](/src/Exception/DtoValidationFailedException.php) will be thrown, and you will be able to get a list of these violations by calling the `getViolations()` method.

Setting the value to `false` will prevent the exception from being thrown, and your controller will still be executed.

Example:
```php
#[Dto(throwOnViolation: false)]
```

If you don't set any value, the configured value (defined in the bundle's configuration file) will be used.

## Events
- [PreDtoMappingEvent](/src/Event/PreDtoMappingEvent.php) - dispatched before the mapping is made.
- [PostDtoMappingEvent](/src/Event/PostDtoMappingEvent.php) - dispatched once the mapping is made.
- [PreDtoValidationEvent](/src/Event/PreDtoValidationEvent.php) - dispatched before the validation is made (if the validation is enabled).
- [PostDtoValidationEvent](/src/Event/PostDtoValidationEvent.php) - dispatched once the validation is made (if the validation is enabled).
Loading

0 comments on commit 769bc36

Please sign in to comment.