1
0
mirror of synced 2024-11-21 20:36:08 +03:00

JMS serializer supports (#3)

* add callback argument value resolver
* add jms serializer support
* add phpdoc
* fix FrontApiClientAuthenticator
This commit is contained in:
Akolzin Dmitry 2021-02-17 09:31:36 +03:00 committed by GitHub
parent 4aace1bcf5
commit 7516ec60cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 341 additions and 49 deletions

View File

@ -2,10 +2,10 @@
namespace RetailCrm\ServiceBundle\ArgumentResolver; namespace RetailCrm\ServiceBundle\ArgumentResolver;
use RetailCrm\ServiceBundle\Serializer\Adapter;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Serializer\SerializerInterface;
use Generator; use Generator;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
@ -22,12 +22,12 @@ class CallbackValueResolver extends AbstractValueResolver implements ArgumentVal
/** /**
* CallbackValueResolver constructor. * CallbackValueResolver constructor.
* *
* @param SerializerInterface $serializer * @param Adapter $serializer
* @param ValidatorInterface $validator * @param ValidatorInterface $validator
* @param array $requestSchema * @param array $requestSchema
*/ */
public function __construct( public function __construct(
SerializerInterface $serializer, Adapter $serializer,
ValidatorInterface $validator, ValidatorInterface $validator,
array $requestSchema array $requestSchema
) { ) {
@ -55,7 +55,7 @@ class CallbackValueResolver extends AbstractValueResolver implements ArgumentVal
public function resolve(Request $request, ArgumentMetadata $argument): Generator public function resolve(Request $request, ArgumentMetadata $argument): Generator
{ {
$parameter = $this->search($request, $argument); $parameter = $this->search($request, $argument);
$data = $this->serializer->deserialize($request->request->get($parameter), $argument->getType(), 'json'); $data = $this->serializer->deserialize($request->request->get($parameter), $argument->getType());
$this->validate($data); $this->validate($data);
yield $data; yield $data;

View File

@ -2,12 +2,10 @@
namespace RetailCrm\ServiceBundle\ArgumentResolver; namespace RetailCrm\ServiceBundle\ArgumentResolver;
use RetailCrm\ServiceBundle\Serializer\Adapter;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Generator; use Generator;
@ -19,27 +17,24 @@ use Generator;
class ClientValueResolver extends AbstractValueResolver implements ArgumentValueResolverInterface class ClientValueResolver extends AbstractValueResolver implements ArgumentValueResolverInterface
{ {
private $serializer; private $serializer;
private $denormalizer;
private $requestSchema; private $requestSchema;
/** /**
* ClientValueResolver constructor. * ClientValueResolver constructor.
* *
*
* @param Adapter $serializer
* @param ValidatorInterface $validator * @param ValidatorInterface $validator
* @param SerializerInterface $serializer
* @param DenormalizerInterface $denormalizer
* @param array $requestSchema * @param array $requestSchema
*/ */
public function __construct( public function __construct(
Adapter $serializer,
ValidatorInterface $validator, ValidatorInterface $validator,
SerializerInterface $serializer,
DenormalizerInterface $denormalizer,
array $requestSchema array $requestSchema
) { ) {
parent::__construct($validator); parent::__construct($validator);
$this->serializer = $serializer; $this->serializer = $serializer;
$this->denormalizer = $denormalizer;
$this->requestSchema = $requestSchema; $this->requestSchema = $requestSchema;
} }
@ -72,12 +67,10 @@ class ClientValueResolver extends AbstractValueResolver implements ArgumentValue
* @param string $type * @param string $type
* *
* @return object * @return object
*
* @throws ExceptionInterface
*/ */
private function handleGetData(array $data, string $type): object private function handleGetData(array $data, string $type): object
{ {
return $this->denormalizer->denormalize($data, $type); return $this->serializer->arrayToObject($data, $type);
} }
/** /**
@ -88,6 +81,6 @@ class ClientValueResolver extends AbstractValueResolver implements ArgumentValue
*/ */
private function handlePostData(string $data, string $type): object private function handlePostData(string $data, string $type): object
{ {
return $this->serializer->deserialize($data, $type, 'json'); return $this->serializer->deserialize($data, $type);
} }
} }

View File

@ -25,17 +25,31 @@ class Configuration implements ConfigurationInterface
->arrayNode('request_schema') ->arrayNode('request_schema')
->children() ->children()
->arrayNode('callback') ->arrayNode('callback')
->arrayPrototype() ->children()
->children() ->arrayNode('supports')
->scalarNode('type')->isRequired()->end() ->arrayPrototype()
->arrayNode('params') ->children()
->isRequired()->scalarPrototype()->end() ->scalarNode('type')->isRequired()->end()
->arrayNode('params')
->isRequired()->scalarPrototype()->end()
->end()
->end() ->end()
->end() ->end()
->end() ->end()
->scalarNode('serializer')
->defaultValue('retail_crm_service.symfony_serializer.adapter')
->end()
->end() ->end()
->end()
->arrayNode('client') ->arrayNode('client')
->scalarPrototype()->end() ->children()
->arrayNode('supports')
->scalarPrototype()->end()
->end()
->scalarNode('serializer')
->defaultValue('retail_crm_service.symfony_serializer.adapter')
->end()
->end()
->end() ->end()
->end() ->end()
->end() ->end()

View File

@ -7,8 +7,11 @@ use RetailCrm\ServiceBundle\ArgumentResolver\ClientValueResolver;
use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory; use RetailCrm\ServiceBundle\Response\ErrorJsonResponseFactory;
use RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator; use RetailCrm\ServiceBundle\Security\CallbackClientAuthenticator;
use RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator; use RetailCrm\ServiceBundle\Security\FrontApiClientAuthenticator;
use RetailCrm\ServiceBundle\Serializer\JMSSerializerAdapter;
use RetailCrm\ServiceBundle\Serializer\SymfonySerializerAdapter;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Reference;
/** /**
* Class RetailCrmServiceExtension * Class RetailCrmServiceExtension
@ -27,24 +30,52 @@ class RetailCrmServiceExtension extends Extension
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
$container->setParameter( $container->setParameter(
'retail_crm_service.request_schema.callback', 'retail_crm_service.request_schema.callback.supports',
$config['request_schema']['callback'] $config['request_schema']['callback']['supports']
); );
$container->setParameter( $container->setParameter(
'retail_crm_service.request_schema.client', 'retail_crm_service.request_schema.client.supports',
$config['request_schema']['client'] $config['request_schema']['client']['supports']
);
$container->setParameter(
'retail_crm_service.request_schema.callback.serializer',
$config['request_schema']['callback']['serializer']
);
$container->setParameter(
'retail_crm_service.request_schema.client.serializer',
$config['request_schema']['client']['serializer']
); );
$container
->register(SymfonySerializerAdapter::class)
->setAutowired(true);
$container->setAlias('retail_crm_service.symfony_serializer.adapter', SymfonySerializerAdapter::class);
$container
->register(JMSSerializerAdapter::class)
->setAutowired(true);
$container->setAlias('retail_crm_service.jms_serializer.adapter', JMSSerializerAdapter::class);
$container $container
->register(CallbackValueResolver::class) ->register(CallbackValueResolver::class)
->setArgument('$requestSchema', '%retail_crm_service.request_schema.callback%') ->setArguments([
new Reference($container->getParameter('retail_crm_service.request_schema.callback.serializer')),
new Reference('validator'),
$container->getParameter('retail_crm_service.request_schema.callback.supports')
])
->addTag('controller.argument_value_resolver', ['priority' => 50]) ->addTag('controller.argument_value_resolver', ['priority' => 50])
->setAutowired(true); ->setAutowired(true);
$container $container
->register(ClientValueResolver::class) ->register(ClientValueResolver::class)
->setArgument('$requestSchema', '%retail_crm_service.request_schema.client%') ->setArguments([
new Reference($container->getParameter('retail_crm_service.request_schema.client.serializer')),
new Reference('validator'),
$container->getParameter('retail_crm_service.request_schema.client.supports')
])
->addTag('controller.argument_value_resolver', ['priority' => 50]) ->addTag('controller.argument_value_resolver', ['priority' => 50])
->setAutowired(true); ->setAutowired(true);

View File

@ -20,7 +20,9 @@ Create bundle config file in `config/packages/retail_crm_service.yaml`:
```yaml ```yaml
retail_crm_service: retail_crm_service:
request_schema: ~ request_schema:
callback: ~
client: ~
``` ```
### Deserializing incoming requests ### Deserializing incoming requests
@ -47,8 +49,8 @@ add to the config:
retail_crm_service: retail_crm_service:
request_schema: request_schema:
callback: callback:
- type: App\Dto\Callback\Activity supports:
params: ["activity"] - { type: App\Dto\Callback\Activity, params: ["activity"] }
``` ```
request automatically will be deserialization to $activity. request automatically will be deserialization to $activity.
@ -73,7 +75,26 @@ add to the config:
retail_crm_service: retail_crm_service:
request_schema: request_schema:
client: client:
- App\Dto\Body supports:
- App\Dto\Body
```
#### Serializers
At this time supported [Symfony serializer](https://symfony.com/doc/current/components/serializer.html) and [JMS serializer](https://jmsyst.com/libs/serializer).
By default, the library using a Symfony serializer. For use JMS install JMS serializer bundle - `composer require jms/serializer-bundle`
You can explicitly specify the type of serializer used for request schema:
```yaml
retail_crm_service:
request_schema:
client:
supports:
# types
serializer: retail_crm_service.symfony_serializer.adapter # or retail_crm_service.jms_serializer.adapter
callback:
supports:
# types
serializer: retail_crm_service.jms_serializer.adapter # or retail_crm_service.symfony_serializer.adapter
``` ```
### Authentication ### Authentication

29
Serializer/Adapter.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace RetailCrm\ServiceBundle\Serializer;
/**
* Interface Adapter
*
* @package RetailCrm\ServiceBundle\Serializer
*/
interface Adapter
{
/**
* @param string $data
* @param string $type
* @param string $format
*
* @return object
*/
public function deserialize(string $data, string $type, string $format = 'json'): object;
/**
* @param array $data
* @param string $type
* @param string|null $format
*
* @return object
*/
public function arrayToObject(array $data, string $type, ?string $format = null): object;
}

View File

@ -0,0 +1,57 @@
<?php
namespace RetailCrm\ServiceBundle\Serializer;
use JMS\Serializer\ArrayTransformerInterface;
use JMS\Serializer\Context;
use JMS\Serializer\SerializerInterface;
/**
* Class JMSSerializerAdapter
*
* @package RetailCrm\ServiceBundle\Serializer
*/
class JMSSerializerAdapter implements Adapter
{
private $serializer;
private $transformer;
private $context;
/**
* JMSSerializerAdapter constructor.
*
* @param SerializerInterface $serializer
* @param ArrayTransformerInterface $transformer
*/
public function __construct(
SerializerInterface $serializer,
ArrayTransformerInterface $transformer
) {
$this->serializer = $serializer;
$this->transformer = $transformer;
}
/**
* {@inheritdoc }
*/
public function deserialize(string $data, string $type, string $format = 'json'): object
{
return $this->serializer->deserialize($data, $type, $format, $this->context);
}
/**
* {@inheritdoc }
*/
public function arrayToObject(array $data, string $type, ?string $format = null): object
{
return $this->transformer->fromArray($data, $type, $this->context);
}
/**
* @param Context $context
*/
public function setContext(Context $context): void
{
$this->context = $context;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace RetailCrm\ServiceBundle\Serializer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Class SymfonySerializerAdapter
*
* @package RetailCrm\ServiceBundle\Serializer
*/
class SymfonySerializerAdapter implements Adapter
{
private $serializer;
private $denormalizer;
private $context = [];
/**
* SymfonySerializerAdapter constructor.
*
* @param SerializerInterface $serializer
* @param DenormalizerInterface $denormalizer
*/
public function __construct(SerializerInterface $serializer, DenormalizerInterface $denormalizer)
{
$this->serializer = $serializer;
$this->denormalizer = $denormalizer;
}
/**
* {@inheritdoc }
*/
public function deserialize(string $data, string $type,string $format = 'json'): object
{
return $this->serializer->deserialize($data, $type, $format, $this->context);
}
/**
* {@inheritdoc }
*/
public function arrayToObject(array $data, string $type, string $format = null): object
{
return $this->denormalizer->denormalize($data, $type, $format, $this->context);
}
/**
* @param array $context
*/
public function setContext(array $context): void
{
$this->context = $context;
}
}

View File

@ -5,6 +5,7 @@ namespace RetailCrm\ServiceBundle\Tests\ArgumentResolver;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\ArgumentResolver\CallbackValueResolver; use RetailCrm\ServiceBundle\ArgumentResolver\CallbackValueResolver;
use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException; use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException;
use RetailCrm\ServiceBundle\Serializer\SymfonySerializerAdapter;
use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto; use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
@ -27,7 +28,7 @@ class CallbackValueResolverTest extends TestCase
{ {
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]); $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$this->resolver = new CallbackValueResolver( $this->resolver = new CallbackValueResolver(
$serializer, new SymfonySerializerAdapter($serializer, $serializer),
Validation::createValidatorBuilder() Validation::createValidatorBuilder()
->enableAnnotationMapping() ->enableAnnotationMapping()
->getValidator(), ->getValidator(),

View File

@ -5,6 +5,7 @@ namespace RetailCrm\ServiceBundle\Tests\ArgumentResolver;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\ArgumentResolver\ClientValueResolver; use RetailCrm\ServiceBundle\ArgumentResolver\ClientValueResolver;
use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException; use RetailCrm\ServiceBundle\Exceptions\InvalidRequestArgumentException;
use RetailCrm\ServiceBundle\Serializer\SymfonySerializerAdapter;
use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto; use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
@ -27,11 +28,10 @@ class ClientValueResolverTest extends TestCase
{ {
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]); $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$this->resolver = new ClientValueResolver( $this->resolver = new ClientValueResolver(
new SymfonySerializerAdapter($serializer, $serializer),
Validation::createValidatorBuilder() Validation::createValidatorBuilder()
->enableAnnotationMapping() ->enableAnnotationMapping()
->getValidator(), ->getValidator(),
$serializer,
$serializer,
[ [
RequestDto::class RequestDto::class
] ]

View File

@ -3,6 +3,7 @@
namespace RetailCrm\ServiceBundle\Tests\DataFixtures; namespace RetailCrm\ServiceBundle\Tests\DataFixtures;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as JMS;
/** /**
* Class RequestDto * Class RequestDto
@ -14,6 +15,7 @@ class RequestDto
/** /**
* @var string * @var string
* @Assert\NotNull() * @Assert\NotNull()
* @JMS\Type("string")
*/ */
public $param; public $param;
} }

View File

@ -21,14 +21,18 @@ class ConfigurationTest extends TestCase
[ [
'request_schema' => [ 'request_schema' => [
'callback' => [ 'callback' => [
[ 'supports' => [
'type' => 'type', [
'params' => ['param'] 'type' => 'type',
'params' => ['param']
]
] ]
], ],
'client' => [ 'client' => [
'type1', 'supports' => [
'type2' 'type1',
'type2'
]
] ]
] ]
] ]
@ -44,14 +48,14 @@ class ConfigurationTest extends TestCase
'type' => 'type', 'type' => 'type',
'params' => ['param'] 'params' => ['param']
], ],
$config['request_schema']['callback'][0] $config['request_schema']['callback']['supports'][0]
); );
static::assertEquals( static::assertEquals(
[ [
'type1', 'type1',
'type2' 'type2'
], ],
$config['request_schema']['client'] $config['request_schema']['client']['supports']
); );
} }
@ -63,7 +67,9 @@ class ConfigurationTest extends TestCase
[ [
'request_schema' => [ 'request_schema' => [
'client' => [ 'client' => [
'type', 'supports' => [
'type',
]
] ]
] ]
] ]
@ -72,6 +78,6 @@ class ConfigurationTest extends TestCase
$config = $processor->processConfiguration(new Configuration(), $configs); $config = $processor->processConfiguration(new Configuration(), $configs);
static::assertArrayHasKey('client', $config['request_schema']); static::assertArrayHasKey('client', $config['request_schema']);
static::assertEquals(['type'], $config['request_schema']['client']); static::assertEquals(['type'], $config['request_schema']['client']['supports']);
} }
} }

View File

@ -31,7 +31,10 @@ class RetailCrmServiceExtensionTest extends TestCase
$extension->load( $extension->load(
[ [
[ [
'request_schema' => [] 'request_schema' => [
'callback' => [],
'client' => []
]
] ]
], ],
$container $container
@ -44,8 +47,10 @@ class RetailCrmServiceExtensionTest extends TestCase
public function testLoad(): void public function testLoad(): void
{ {
static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.callback')); static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.callback.supports'));
static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.client')); static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.callback.serializer'));
static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.client.supports'));
static::assertTrue($this->container->hasParameter('retail_crm_service.request_schema.client.serializer'));
static::assertTrue($this->container->hasDefinition(CallbackValueResolver::class)); static::assertTrue($this->container->hasDefinition(CallbackValueResolver::class));
static::assertTrue($this->container->hasDefinition(ClientValueResolver::class)); static::assertTrue($this->container->hasDefinition(ClientValueResolver::class));
static::assertTrue($this->container->hasDefinition(ErrorJsonResponseFactory::class)); static::assertTrue($this->container->hasDefinition(ErrorJsonResponseFactory::class));

View File

@ -0,0 +1,38 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\Serializer;
use JMS\Serializer\SerializerBuilder;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\Serializer\JMSSerializerAdapter;
use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto;
class JSMSerializerAdapterTest extends TestCase
{
private $serializer;
private $transformer;
protected function setUp(): void
{
$this->serializer = SerializerBuilder::create()->build();
$this->transformer = SerializerBuilder::create()->build();
}
public function testDeserialize(): void
{
$adapter = new JMSSerializerAdapter($this->serializer, $this->transformer);
$object = $adapter->deserialize('{"param": "string"}', RequestDto::class,'json');
static::assertInstanceOf(RequestDto::class, $object);
static::assertEquals('string', $object->param);
}
public function testArrayToObject(): void
{
$adapter = new JMSSerializerAdapter($this->serializer, $this->transformer);
$object = $adapter->arrayToObject(['param' => 'string'], RequestDto::class);
static::assertInstanceOf(RequestDto::class, $object);
static::assertEquals('string', $object->param);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace RetailCrm\ServiceBundle\Tests\Serializer;
use PHPUnit\Framework\TestCase;
use RetailCrm\ServiceBundle\Serializer\SymfonySerializerAdapter;
use RetailCrm\ServiceBundle\Tests\DataFixtures\RequestDto;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class SymfonySerializerAdapterTest extends TestCase
{
private $serializer;
private $denormalizer;
protected function setUp(): void
{
$this->serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$this->denormalizer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
}
public function testDeserialize(): void
{
$adapter = new SymfonySerializerAdapter($this->serializer, $this->denormalizer);
$object = $adapter->deserialize('{"param": "string"}', RequestDto::class,'json');
static::assertInstanceOf(RequestDto::class, $object);
static::assertEquals('string', $object->param);
}
public function testArrayToObject(): void
{
$adapter = new SymfonySerializerAdapter($this->serializer, $this->denormalizer);
$object = $adapter->arrayToObject(['param' => 'string'], RequestDto::class);
static::assertInstanceOf(RequestDto::class, $object);
static::assertEquals('string', $object->param);
}
}

View File

@ -35,7 +35,8 @@
"ext-json": "*", "ext-json": "*",
"phpunit/phpunit": "^8.0 || ^9.0", "phpunit/phpunit": "^8.0 || ^9.0",
"doctrine/annotations": "^1.11", "doctrine/annotations": "^1.11",
"doctrine/cache": "^1.10" "doctrine/cache": "^1.10",
"jms/serializer-bundle": "^3.8"
}, },
"scripts": { "scripts": {
"tests": "./vendor/bin/phpunit -c phpunit.xml.dist" "tests": "./vendor/bin/phpunit -c phpunit.xml.dist"