Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Ne-Lexa/RequestDtoBundle
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 1.3.0
Choose a base ref
...
head repository: Ne-Lexa/RequestDtoBundle
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 15 commits
  • 15 files changed
  • 1 contributor

Commits on Dec 8, 2021

  1. Merge tag '1.3.0' into develop

    1.3.0 version
    Ne-Lexa committed Dec 8, 2021
    Copy the full SHA
    a5a58a2 View commit details

Commits on Dec 9, 2021

  1. Copy the full SHA
    9ebc55c View commit details
  2. Merge branch 'hotfix/1.3.1'

    Ne-Lexa committed Dec 9, 2021
    Copy the full SHA
    f017876 View commit details
  3. Merge tag '1.3.1' into develop

    1.3.1
    Ne-Lexa committed Dec 9, 2021
    Copy the full SHA
    307162d View commit details

Commits on Feb 16, 2022

  1. allow symfony 6

    Ne-Lexa committed Feb 16, 2022
    Copy the full SHA
    ab7853d View commit details
  2. compat symfony 6

    Ne-Lexa committed Feb 16, 2022
    Copy the full SHA
    1400b09 View commit details
  3. Merge branch 'hotfix/1.3.2'

    Ne-Lexa committed Feb 16, 2022
    Copy the full SHA
    1279534 View commit details
  4. Merge tag '1.3.2' into develop

    Tagging hotfix 1.3.2 1.3.2
    Ne-Lexa committed Feb 16, 2022
    Copy the full SHA
    4af89f5 View commit details
  5. update readme

    Ne-Lexa committed Feb 16, 2022
    Copy the full SHA
    73163ae View commit details

Commits on May 10, 2022

  1. adds symfony 4.4 support

    Ne-Lexa committed May 10, 2022
    Copy the full SHA
    c05c3e9 View commit details
  2. Merge branch 'hotfix/1.3.3'

    Ne-Lexa committed May 10, 2022
    Copy the full SHA
    2192c09 View commit details
  3. update readme

    Ne-Lexa committed May 10, 2022
    Copy the full SHA
    3af5deb View commit details

Commits on Jun 21, 2022

  1. fix php8.1 deprecation

    Ne-Lexa committed Jun 21, 2022
    Copy the full SHA
    472f78e View commit details
  2. update build

    Ne-Lexa committed Jun 21, 2022
    Copy the full SHA
    8eef2bd View commit details
  3. Merge branch 'hotfix/1.3.4'

    Ne-Lexa committed Jun 21, 2022
    Copy the full SHA
    aa572d1 View commit details
14 changes: 13 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -21,17 +21,29 @@ jobs:
- '8.0'
- '8.1'
symfony_version:
- '4.4.*'
- '5.1.*'
- '5.2.*'
- '5.3.*'
- '5.4.*'
- '6.0.*'
- '6.1.*'
exclude:
- os: 'ubuntu-latest'
symfony_version: '5.1.*'
php: '8.1'
- os: 'ubuntu-latest'
symfony_version: '5.2.*'
php: '8.1'
- os: 'ubuntu-latest'
symfony_version: '6.0.*'
php: '7.4'
- os: 'ubuntu-latest'
symfony_version: '6.1.*'
php: '7.4'
- os: 'ubuntu-latest'
symfony_version: '6.1.*'
php: '8.0'

runs-on: ${{ matrix.os }}

@@ -56,7 +68,7 @@ jobs:

-
name: Set coverage args
if: matrix.php == '7.4'
if: matrix.php == '7.4' && matrix.symfony_version == '5.4.*'
run: echo "PHPUNIT_COVERAGE=1" >> $GITHUB_ENV

# -
58 changes: 50 additions & 8 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
declare(strict_types=1);

/*
* PHP Code Style Fixer (config created for version 3.3.2 (Trinacria)).
* PHP Code Style Fixer (config created for version 3.8.0 (BerSzcz against war!)).
*
* Use one of the following console commands to just see the
* changes that will be made.
@@ -116,13 +116,19 @@
'class_attributes_separation' => true,

/*
* Whitespace around the keywords of a class, trait or interfaces
* definition should be one space.
* Whitespace around the keywords of a class, trait, enum or
* interfaces definition should be one space.
*/
'class_definition' => [
'single_line' => true,
],

/*
* When referencing an internal class it must be written using the
* correct casing.
*/
'class_reference_name_casing' => true,

// Namespace must not contain spacing, comments or PHPDoc.
'clean_namespace' => true,

@@ -180,6 +186,22 @@
*/
'control_structure_continuation_position' => true,

/*
* The first argument of `DateTime::createFromFormat` method must
* start with `!`.
*
* Consider this code:
* `DateTime::createFromFormat('Y-m-d', '2022-02-11')`.
* What value will be returned? '2022-01-11 00:00:00.0'? No,
* actual return value has 'H:i:s' section like '2022-02-11
* 16:55:37.0'.
* Change 'Y-m-d' to '!Y-m-d', return value will be '2022-01-11
* 00:00:00.0'.
* So, adding `!` to format string will make return value more
* intuitive.
*/
'date_time_create_from_format_call' => true,

/*
* Class `DateTimeImmutable` should be used instead of `DateTime`.
*
@@ -455,6 +477,15 @@
],
],

/*
* Replace `get_class` calls on object variables with class keyword
* syntax.
*
* Risky!
* Risky if the `get_class` function is overridden.
*/
'get_class_to_class_keyword' => false,

// Imports or fully qualifies global classes/functions/constants.
'global_namespace_import' => [
'import_constants' => false,
@@ -661,8 +692,8 @@
'native_function_type_declaration_casing' => true,

/*
* All instances created with new keyword must be followed by
* braces.
* All instances created with `new` keyword must (not) be followed
* by braces.
*/
'new_with_braces' => true,

@@ -833,6 +864,12 @@
// PHP single-line arrays should not have trailing comma.
'no_trailing_comma_in_singleline_array' => true,

/*
* When making a method or function call on a single line there MUST
* NOT be a trailing comma after the last argument.
*/
'no_trailing_comma_in_singleline_function_call' => true,

// Remove trailing whitespace at the end of non-blank lines.
'no_trailing_whitespace' => true,

@@ -871,14 +908,16 @@
],

/*
* A `final` class must not have `final` methods and `private`
* methods must not be `final`.
* Removes `final` from methods where possible.
*
* Risky!
* Risky when child class overrides a `private` method.
*/
'no_unneeded_final_method' => true,

// Imports should not be aliased as the same name.
'no_unneeded_import_alias' => true,

/*
* In function arguments there must not be arguments with default
* values before non-default ones.
@@ -979,7 +1018,7 @@
*/
'operator_linebreak' => true,

// Orders the elements of classes/interfaces/traits.
// Orders the elements of classes/interfaces/traits/enums.
'ordered_class_elements' => [
'order' => [
'use_trait',
@@ -1544,6 +1583,9 @@
*/
'single_line_after_imports' => true,

// Single-line comments must have proper spacing.
'single_line_comment_spacing' => true,

/*
* Single-line comments and multi-line comments with only one line
* of actual content should use the `//` syntax.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -11,18 +11,20 @@ This Symfony Bundle provides request objects support for Symfony controller acti
[![Code Coverage](https://scrutinizer-ci.com/g/Ne-Lexa/RequestDtoBundle/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Ne-Lexa/RequestDtoBundle/?branch=master)
[![Packagist License](https://img.shields.io/packagist/l/nelexa/request-dto-bundle)](https://github.com/Ne-Lexa/RequestDtoBundle/blob/master/LICENSE)

# Installation
# Installation
Require the bundle with composer:
```bash
composer require nelexa/request-dto-bundle
```

## Versions & Dependencies
| Bundle version | Symfony version | PHP version(s) |
|-----------------------------|-----------------|-----------------|
| 1.0.* <br/>1.1.* <br/>1.2.0 | ^5.0 | ^7.4 |
| 1.2.1 | ^5.0 | ^7.4\|^8.0 |
| 1.3.* | ^5.1 | ^7.4\|^8.0\|^8.1 |
| Bundle version | Symfony version | PHP version(s) |
|-----------------------------|---------------------|----------------------|
| 1.0.* <br/>1.1.* <br/>1.2.0 | ^5.0 | ^7.4 |
| ~1.2.1 | ^5.0 | ^7.4 \| ^8.0 |
| 1.3.0 - 1.3.1 | ^5.1 | ^7.4 \| ^8.0 \| ^8.1 |
| ~1.3.2 | ^5.1 \| ^6.0 | ^7.4 \| ^8.0 \| ^8.1 |
| ~1.3.3 | ^4.4 \|^5.1 \| ^6.0 | ^7.4 \| ^8.0 \| ^8.1 |

## Examples of using
To specify an object as an argument of a controller action, an object must implement one of 4 interfaces:
26 changes: 13 additions & 13 deletions composer.json
Original file line number Diff line number Diff line change
@@ -10,26 +10,26 @@
"license": "MIT",
"require": {
"php": "^7.4 | ^8.0",
"symfony/dependency-injection": "^5.1",
"symfony/http-kernel": "^5.1",
"symfony/property-access": "^5.1",
"symfony/property-info": "^5.1",
"symfony/serializer": "^5.1",
"symfony/validator": "^5.1"
"symfony/dependency-injection": "^4.4 | ^5.1 | ^6.0",
"symfony/http-kernel": "^4.4 | ^5.1 | ^6.0",
"symfony/property-access": "^4.4 | ^5.1 | ^6.0",
"symfony/property-info": "^4.4 | ^5.1 | ^6.0",
"symfony/serializer": "^4.4 | ^5.1 | ^6.0",
"symfony/validator": "^4.4 | ^5.1 | ^6.0"
},
"require-dev": {
"ext-json": "*",
"ext-simplexml": "*",
"doctrine/annotations": "^1.10",
"symfony/browser-kit": "^5.1",
"symfony/css-selector": "^5.1",
"symfony/framework-bundle": "^5.1",
"symfony/mime": "^5.1",
"symfony/phpunit-bridge": "^5.1",
"symfony/var-dumper": "^5.1"
"symfony/browser-kit": "^4.4 | ^5.1 | ^6.0",
"symfony/css-selector": "^4.4 | ^5.1 | ^6.0",
"symfony/framework-bundle": "^4.4 | ^5.1 | ^6.0",
"symfony/mime": "^4.4 | ^5.1 | ^6.0",
"symfony/phpunit-bridge": "^4.4 | ^5.1 | ^6.0",
"symfony/var-dumper": "^4.4 | ^5.1 | ^6.0"
},
"conflict": {
"symfony/framework-bundle": "<5.1.0 || >=6.0.0"
"symfony/framework-bundle": "<4.4 | 5.0.*"
},
"config": {
"sort-packages": true
2 changes: 1 addition & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@
<server name="APP_ENV" value="test" force="true"/>
<server name="SHELL_VERBOSITY" value="-1"/>
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5"/>
<!-- <server name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" />-->
<server name="SYMFONY_DEPRECATIONS_HELPER" value="disabled" />
<server name="SYMFONY_PHPUNIT_REMOVE" value=""/>
</php>
<testsuites>
42 changes: 27 additions & 15 deletions src/ArgumentResolver/RequestDtoValueResolver.php
Original file line number Diff line number Diff line change
@@ -10,7 +10,9 @@
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\PropertyAccess\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class RequestDtoValueResolver implements ArgumentValueResolverInterface
@@ -50,24 +52,34 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable

try {
$obj = $this->transformer->transform($request, $argument->getType(), $format);
} catch (\TypeError|NotEncodableValueException $e) {
$problemMimeType = 'application/problem+' . $format;

throw new HttpException(
400,
'Bad Request',
$e,
[
'Content-Type' => $problemMimeType,
]
$request->attributes->set(
ConstraintViolationListValueResolver::REQUEST_ATTR_KEY,
$this->validator->validate($obj)
);

yield $obj;

return;
} catch (\TypeError|NotEncodableValueException $e) {
$exception = $e;
} catch (NotNormalizableValueException $e) {
$previousException = $e->getPrevious();

if ($previousException instanceof InvalidArgumentException) {
$e = $previousException;
}
$exception = $e;
}

$request->attributes->set(
ConstraintViolationListValueResolver::REQUEST_ATTR_KEY,
$this->validator->validate($obj)
);
$problemMimeType = 'application/problem+' . $format;

yield $obj;
throw new HttpException(
400,
'Bad Request',
$exception,
[
'Content-Type' => $problemMimeType,
]
);
}
}
20 changes: 9 additions & 11 deletions src/Normalizer/RequestDtoExceptionNormalizer.php
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@
namespace Nelexa\RequestDtoBundle\Normalizer;

use Nelexa\RequestDtoBundle\Exception\RequestDtoValidationException;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer;
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;

@@ -26,29 +25,28 @@ public function __construct(
}

/**
* @param RequestDtoValidationException $exception
* @param RequestDtoValidationException $object
* @param mixed|null $format
*
* @throws ExceptionInterface
*
* @return array|\ArrayObject|bool|float|int|string|null
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
*/
public function normalize($exception, ?string $format = null, array $context = [])
public function normalize($object, $format = null, array $context = []): array
{
$context += [
'type' => 'https://tools.ietf.org/html/rfc7807',
];
$data = $this->normalizer->normalize($exception->getErrors(), $format, $context);
$data['status'] = $exception->getStatusCode();
$data = $this->normalizer->normalize($object->getErrors(), $format, $context);
$data['status'] = $object->getStatusCode();

if ($this->debug) {
$data['class'] = \get_class($exception);
$data['trace'] = $exception->getTrace();
$data['class'] = \get_class($object);
$data['trace'] = $object->getTrace();
}

return $data;
}

public function supportsNormalization($data, ?string $format = null): bool
public function supportsNormalization($data, $format = null): bool
{
return $data instanceof RequestDtoValidationException;
}
14 changes: 8 additions & 6 deletions src/Normalizer/UploadedFileDenormalizer.php
Original file line number Diff line number Diff line change
@@ -5,7 +5,6 @@
namespace Nelexa\RequestDtoBundle\Normalizer;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;

@@ -17,17 +16,20 @@ public function hasCacheableSupportsMethod(): bool
}

/**
* @param mixed $data
* @param mixed $data Data to restore
* @param string $type The expected class to instantiate
* @param string $format Format the given data was extracted from
* @param array $context Options available to the denormalizer
*
* @return mixed
* @return mixed Data
*/
public function denormalize($data, string $type, ?string $format = null, array $context = [])
public function denormalize($data, $type, $format = null, array $context = [])
{
return $data;
}

public function supportsDenormalization($data, string $type, ?string $format = null): bool
public function supportsDenormalization($data, $type, $format = null): bool
{
return $type === UploadedFile::class || $type === File::class;
return is_a($type, File::class, true);
}
}
21 changes: 18 additions & 3 deletions src/Transform/RequestDtoTransform.php
Original file line number Diff line number Diff line change
@@ -43,9 +43,9 @@ public function transform(Request $request, string $className, string $format, a
$dto = $this->serializer->denormalize(
$request->isMethod('GET') || $request->isMethod('HEAD')
? $request->query->all()
: array_merge_recursive(
$request->files->all(),
$request->request->all()
: self::recursiveMergeDistinctArray(
$request->request->all(),
$request->files->all()
),
$className,
$format,
@@ -66,4 +66,19 @@ public function transform(Request $request, string $className, string $format, a

return $dto;
}

private static function recursiveMergeDistinctArray(array $array1, array $array2): array
{
$merged = $array1;

foreach ($array2 as $key => $value) {
if (\is_array($value) && isset($merged[$key]) && \is_array($merged[$key])) {
$merged[$key] = self::recursiveMergeDistinctArray($merged[$key], $value);
} else {
$merged[$key] = $value;
}
}

return $merged;
}
}
24 changes: 24 additions & 0 deletions tests/App/Controller/FilesUpdateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Nelexa\RequestDtoBundle\Tests\App\Controller;

use Nelexa\RequestDtoBundle\Tests\App\Request\FilesUpdateRequest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class FilesUpdateController extends AbstractController
{
public function __invoke(
Request $request,
FilesUpdateRequest $dto
): Response {
return $this->serializeResponse(
$request,
[
'dto' => $dto,
]
);
}
}
29 changes: 29 additions & 0 deletions tests/App/Request/FileDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Nelexa\RequestDtoBundle\Tests\App\Request;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;

class FileDto
{
/** @Assert\NotBlank(allowNull=true) */
public ?int $id = null;

/**
* @Assert\NotBlank(allowNull=true)
* @Assert\File
*/
public ?File $file = null;

/**
* @Assert\NotNull
* @Assert\Type("bool")
*/
public ?bool $delete = false;

/** @Assert\Type("int") */
public int $position = 0;
}
26 changes: 26 additions & 0 deletions tests/App/Request/FilesUpdateRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Nelexa\RequestDtoBundle\Tests\App\Request;

use Nelexa\RequestDtoBundle\Dto\RequestObjectInterface;
use Symfony\Component\Validator\Constraints as Assert;

class FilesUpdateRequest implements RequestObjectInterface
{
/**
* @Assert\NotBlank
* @Assert\Type("int")
*/
public ?int $id = null;

/**
* @var \Nelexa\RequestDtoBundle\Tests\App\Request\FileDto[]
* @Assert\All({
* @Assert\Type("Nelexa\RequestDtoBundle\Tests\App\Request\FileDto")
* })
* @Assert\Valid
*/
public array $files = [];
}
2 changes: 1 addition & 1 deletion tests/App/Request/ObjectFromRequest.php
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@ public function __construct(Request $request)
$this->integer = $request->request->getInt('i');
$this->boolean = $request->request->getBoolean('b', false);
$this->float = (float) $request->request->get('f', 0);
$this->array = $request->request->all('a');
$this->array = $request->request->all()['a'] ?? [];
}

public function getString(): string
4 changes: 4 additions & 0 deletions tests/App/Resources/config/routes.php
Original file line number Diff line number Diff line change
@@ -39,6 +39,10 @@
->controller(App\Controller\UploadNestedFileController::class)
->methods(['POST'])
;
$routes->add('update-files', '/files/update')
->controller(App\Controller\FilesUpdateController::class)
->methods(['POST'])
;

$routes->add('root-body', '/root/body')
->controller(App\Controller\RootBodyController::class)
142 changes: 138 additions & 4 deletions tests/BundleTest.php
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ public function testRequestAndQueryObjects(
$json = json_decode($response->getContent(), true, 512, \JSON_THROW_ON_ERROR);

foreach ($responseData as $key => $value) {
self::assertArrayHasKey($key, $json);
self::assertSame($json[$key], $value);
}
} elseif (\is_string($responseData)) {
@@ -258,6 +259,140 @@ public function provideRequestObjects(): iterable

public function provideFileRequestObjects(): iterable
{
yield 'Array Files' => [
Request::create(
'/files/update',
'POST',
[
'id' => '22',
'files' => [
[
'id' => '23',
'delete' => '0',
'position' => '0',
],
[
'id' => '28',
'delete' => '0',
'position' => '1',
],
[
'id' => '24',
'delete' => '1',
'position' => '0',
],
[
'delete' => '0',
'position' => '2',
],
[
'id' => '38',
'delete' => '0',
'position' => '3',
],
[
'id' => '34',
'delete' => '1',
'position' => '0',
],
[
'delete' => '0',
'position' => '4',
],
[
'id' => '38',
'delete' => '0',
'position' => '5',
],
],
],
[],
[
'files' => [
3 => [
'file' => new UploadedFile(
'LICENSE',
basename('LICENSE'),
null,
null,
true
),
],
6 => [
'file' => new UploadedFile(
'composer.json',
basename('composer.json'),
null,
null,
true
),
],
],
]
),
[
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
200,
'application/json',
[
'dto' => [
'id' => 22,
'files' => [
[
'id' => 23,
'file' => null,
'delete' => false,
'position' => 0,
],
[
'id' => 28,
'file' => null,
'delete' => false,
'position' => 1,
],
[
'id' => 24,
'file' => null,
'delete' => true,
'position' => 0,
],
[
'id' => null,
'file' => 'data:text/plain,' . rawurlencode(file_get_contents('LICENSE')),
'delete' => false,
'position' => 2,
],
[
'id' => 38,
'file' => null,
'delete' => false,
'position' => 3,
],
[
'id' => 34,
'file' => null,
'delete' => true,
'position' => 0,
],
[
'id' => null,
'file' => 'data:application/json;base64,' . base64_encode(file_get_contents('composer.json')),
'delete' => false,
'position' => 4,
],
[
'id' => 38,
'file' => null,
'delete' => false,
'position' => 5,
],
],
],
],
];

yield 'Upload Single File' => [
Request::create(
'/upload/single',
@@ -303,7 +438,6 @@ public function provideFileRequestObjects(): iterable
],
];

$file = 'LICENSE';
yield 'Upload Nested File' => [
Request::create(
'/upload/nested',
@@ -319,8 +453,8 @@ public function provideFileRequestObjects(): iterable
[
'dtoFile' => [
'file' => new UploadedFile(
$file,
basename($file),
'LICENSE',
basename('LICENSE'),
'text/plain',
null,
true
@@ -338,7 +472,7 @@ public function provideFileRequestObjects(): iterable
'dto' => [
'id' => 5,
'dtoFile' => [
'file' => 'data:text/plain,' . rawurlencode(file_get_contents($file)),
'file' => 'data:text/plain,' . rawurlencode(file_get_contents('LICENSE')),
'filesize' => 5120,
'mimeType' => 'text/plain',
],