mirror of
https://github.com/Sylius/Sylius.git
synced 2026-07-14 09:02:12 +00:00
[ApiBundle] Extract validation logic from GetStatisticsAction controller
This commit is contained in:
parent
6e71fe259f
commit
2296ce7db9
17 changed files with 613 additions and 162 deletions
|
|
@ -13,7 +13,9 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Sylius\Bundle\ApiBundle\Controller;
|
namespace Sylius\Bundle\ApiBundle\Controller;
|
||||||
|
|
||||||
|
use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException;
|
||||||
use Sylius\Bundle\ApiBundle\Query\GetStatistics;
|
use Sylius\Bundle\ApiBundle\Query\GetStatistics;
|
||||||
|
use Sylius\Bundle\ApiBundle\Validator\Constraints as Assert;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
@ -21,174 +23,74 @@ use Symfony\Component\Messenger\Exception\HandlerFailedException;
|
||||||
use Symfony\Component\Messenger\HandleTrait;
|
use Symfony\Component\Messenger\HandleTrait;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
use Symfony\Component\Validator\Constraints as BaseAssert;
|
||||||
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
|
||||||
final class GetStatisticsAction
|
final class GetStatisticsAction
|
||||||
{
|
{
|
||||||
use HandleTrait;
|
use HandleTrait;
|
||||||
|
|
||||||
/** @var array<array{queryParameter: string, message: string}> */
|
|
||||||
private array $violations = [];
|
|
||||||
|
|
||||||
/** @var array<string> */
|
|
||||||
private array $requiredParameters = [
|
|
||||||
'channelCode' => 'string',
|
|
||||||
'startDate' => 'dateTime',
|
|
||||||
'dateInterval' => 'dateInterval',
|
|
||||||
'endDate' => 'dateTime',
|
|
||||||
];
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
MessageBusInterface $queryBus,
|
MessageBusInterface $queryBus,
|
||||||
private SerializerInterface $serializer,
|
private SerializerInterface $serializer,
|
||||||
|
private ValidatorInterface $validator,
|
||||||
) {
|
) {
|
||||||
$this->messageBus = $queryBus;
|
$this->messageBus = $queryBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws \Throwable
|
||||||
|
*/
|
||||||
public function __invoke(Request $request): Response
|
public function __invoke(Request $request): Response
|
||||||
{
|
{
|
||||||
$this->validateRequiredParameters($request);
|
$constraint = new BaseAssert\Collection([
|
||||||
|
'channelCode' => new Assert\Code(),
|
||||||
|
'startDate' => new BaseAssert\DateTime('Y-m-d\TH:i:s', message: 'sylius.date_time.invalid'),
|
||||||
|
'dateInterval' => new Assert\DateInterval(),
|
||||||
|
'endDate' => new BaseAssert\DateTime('Y-m-d\TH:i:s', message: 'sylius.date_time.invalid'),
|
||||||
|
]);
|
||||||
|
|
||||||
$parameters = $request->query->all();
|
$violations = $this->validator->validate($request->query->all(), $constraint);
|
||||||
|
|
||||||
if (count($this->violations) > 0) {
|
if (count($violations) > 0) {
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
data: $this->serializer->serialize($this->violations, 'json'),
|
data: $this->serializer->serialize($violations, 'json'),
|
||||||
status: Response::HTTP_BAD_REQUEST,
|
status: Response::HTTP_BAD_REQUEST,
|
||||||
json: true,
|
json: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$parameters = $request->query->all();
|
||||||
$period = new \DatePeriod(
|
$period = new \DatePeriod(
|
||||||
new \DateTimeImmutable($parameters['startDate']),
|
new \DateTimeImmutable($parameters['startDate']),
|
||||||
new \DateInterval($parameters['dateInterval']),
|
new \DateInterval($parameters['dateInterval']),
|
||||||
new \DateTimeImmutable($parameters['endDate']),
|
new \DateTimeImmutable($parameters['endDate']),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
$violations = $this->validator->validate($period, new Assert\DatePeriod());
|
||||||
$this->validateEndDateIsNotBeforeStartDate($period);
|
|
||||||
$this->validateEndDateAgainstInterval($period);
|
if (count($violations) > 0) {
|
||||||
} catch (\InvalidArgumentException $exception) {
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
data: $this->serializer->serialize(['message' => $exception->getMessage()], 'json'),
|
data: $this->serializer->serialize($violations, 'json'),
|
||||||
status: Response::HTTP_BAD_REQUEST,
|
status: Response::HTTP_BAD_REQUEST,
|
||||||
json: true,
|
json: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = new GetStatistics($period, $parameters['channelCode']);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $this->handle($query);
|
$result = $this->handle(new GetStatistics($period, $parameters['channelCode']));
|
||||||
|
$status = Response::HTTP_OK;
|
||||||
} catch (HandlerFailedException $exception) {
|
} catch (HandlerFailedException $exception) {
|
||||||
return new JsonResponse(
|
$exception = $exception->getPrevious();
|
||||||
data: $this->serializer->serialize(['message' => $exception->getMessage()], 'json'),
|
$result = ['message' => $exception->getMessage()];
|
||||||
status: Response::HTTP_NOT_FOUND,
|
|
||||||
json: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new JsonResponse(data: $this->serializer->serialize($result, 'json'), json: true);
|
if ($exception instanceof ChannelNotFoundException) {
|
||||||
}
|
$status = Response::HTTP_NOT_FOUND;
|
||||||
|
} else {
|
||||||
private function validateRequiredParameters(Request $request): void
|
throw $exception;
|
||||||
{
|
|
||||||
foreach ($this->requiredParameters as $parameterName => $parameterType) {
|
|
||||||
$parameter = $request->query->get($parameterName);
|
|
||||||
|
|
||||||
if ($parameter === null) {
|
|
||||||
$this->addViolation($parameterName, sprintf('Parameter "%s" is required.', $parameterName));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($parameter)) {
|
|
||||||
$this->addViolation($parameterName, sprintf('Parameter "%s" cannot be empty.', $parameterName));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($parameterType === 'dateTime' && !$this->isISO8601DateTimeWithNoTimezone($parameter)) {
|
|
||||||
$this->addViolation($parameterName, sprintf(
|
|
||||||
'Parameter "%s" must be a valid ISO8601 date time string without timezone.',
|
|
||||||
$parameterName,
|
|
||||||
));
|
|
||||||
} elseif ($parameterType === 'dateInterval' && !$this->isValidInterval($parameter)) {
|
|
||||||
$this->addViolation($parameterName, sprintf(
|
|
||||||
'Parameter "%s" must be a valid DateInterval string.',
|
|
||||||
$parameterName,
|
|
||||||
));
|
|
||||||
} elseif ($parameterType === 'int' && filter_var($parameter, \FILTER_VALIDATE_INT) === false) {
|
|
||||||
$this->addViolation($parameterName, sprintf(
|
|
||||||
'Parameter "%s" must be an integer.',
|
|
||||||
$parameterName,
|
|
||||||
));
|
|
||||||
} elseif (!is_string($parameter) || is_numeric($parameter)) {
|
|
||||||
$this->addViolation($parameterName, sprintf(
|
|
||||||
'Parameter "%s" must be a string.',
|
|
||||||
$parameterName,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private function isISO8601DateTimeWithNoTimezone(?string $dateTime = null): bool
|
return new JsonResponse(data: $this->serializer->serialize($result, 'json'), status: $status, json: true);
|
||||||
{
|
|
||||||
return preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/', $dateTime) === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isValidInterval(?string $interval = null): bool
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
new \DateInterval($interval);
|
|
||||||
} catch (\Exception) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates that the end date is a multiple of the interval.
|
|
||||||
* The end date is adjusted by subtracting one second to make it inclusive (closed interval).
|
|
||||||
* If the adjusted end date does not match the provided end date, an exception is thrown.
|
|
||||||
*/
|
|
||||||
private function validateEndDateAgainstInterval(\DatePeriod $datePeriod): void
|
|
||||||
{
|
|
||||||
$currentDate = clone $datePeriod->getStartDate();
|
|
||||||
$endDate = $datePeriod->getEndDate();
|
|
||||||
$interval = $datePeriod->getDateInterval();
|
|
||||||
|
|
||||||
while ($currentDate <= $endDate) {
|
|
||||||
$currentDate = $currentDate->add($interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** We shift to make closed interval. */
|
|
||||||
$intervalEndDate = $currentDate->modify('-1 second');
|
|
||||||
|
|
||||||
if ($intervalEndDate != $endDate) {
|
|
||||||
throw new \InvalidArgumentException(sprintf(
|
|
||||||
sprintf(
|
|
||||||
'End date "%s" must be multiple of interval, expected "%s"',
|
|
||||||
$endDate->format('Y-m-d H:i:s'),
|
|
||||||
$intervalEndDate->format('Y-m-d H:i:s'),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function validateEndDateIsNotBeforeStartDate(\DatePeriod $period): void
|
|
||||||
{
|
|
||||||
if ($period->getStartDate() > $period->getEndDate()) {
|
|
||||||
throw new \InvalidArgumentException(sprintf(
|
|
||||||
'End date "%s" must be after start date "%s"',
|
|
||||||
$period->getEndDate()->format('Y-m-d H:i:s'),
|
|
||||||
$period->getStartDate()->format('Y-m-d H:i:s'),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addViolation(string $parameterName, string $message): void
|
|
||||||
{
|
|
||||||
$this->violations[] = ['queryParameter' => $parameterName, 'message' => $message];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
<service id="Sylius\Bundle\ApiBundle\Controller\GetStatisticsAction">
|
<service id="Sylius\Bundle\ApiBundle\Controller\GetStatisticsAction">
|
||||||
<argument type="service" id="sylius.query_bus" />
|
<argument type="service" id="sylius.query_bus" />
|
||||||
<argument type="service" id="serializer" />
|
<argument type="service" id="serializer" />
|
||||||
|
<argument type="service" id="validator" />
|
||||||
</service>
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
|
|
||||||
|
|
@ -188,5 +188,17 @@
|
||||||
<argument type="service" id="Sylius\Bundle\ApiBundle\Validator\CatalogPromotion\ForVariantsScopeValidator.inner" />
|
<argument type="service" id="Sylius\Bundle\ApiBundle\Validator\CatalogPromotion\ForVariantsScopeValidator.inner" />
|
||||||
<argument type="service" id="Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface" />
|
<argument type="service" id="Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\DateIntervalValidator">
|
||||||
|
<tag name="validator.constraint_validator" alias="sylius_api_validator_date_interval" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\EndDateIsNotBeforeStartDateValidator">
|
||||||
|
<tag name="validator.constraint_validator" alias="sylius_api_validator_date_period_end_date_is_not_before_start_date" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service id="Sylius\Bundle\ApiBundle\Validator\Constraints\EndDateAgainstIntervalValidator">
|
||||||
|
<tag name="validator.constraint_validator" alias="sylius_api_validator_end_date_against_interval" />
|
||||||
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,20 @@ sylius:
|
||||||
is_verified: 'Account with email %email% is currently verified.'
|
is_verified: 'Account with email %email% is currently verified.'
|
||||||
address:
|
address:
|
||||||
without_country: 'The address without country cannot exist'
|
without_country: 'The address without country cannot exist'
|
||||||
|
code:
|
||||||
|
invalid: 'The code should contain only letters, numbers, dashes ("-" symbol) and underscores ("_" symbol).'
|
||||||
contact_request:
|
contact_request:
|
||||||
email_is_invalid: 'The provided email is invalid.'
|
email_is_invalid: 'The provided email is invalid.'
|
||||||
country:
|
country:
|
||||||
not_exist: 'The country %countryCode% does not exist.'
|
not_exist: 'The country %countryCode% does not exist.'
|
||||||
|
date_interval:
|
||||||
|
empty: 'The date interval cannot be empty.'
|
||||||
|
invalid: 'The date interval is not valid ISO 8601 interval.'
|
||||||
|
date_period:
|
||||||
|
end_date_is_not_before_start_date: 'The end date is not before start date.'
|
||||||
|
end_date_must_be_multiple_of_interval: 'End date "%givenDate%" must be multiple of interval, expected "%expectedDate%".'
|
||||||
|
date_time:
|
||||||
|
invalid: 'The date time is not valid ISO 8601 date time in Y-m-d\TH:i:s format.'
|
||||||
order:
|
order:
|
||||||
not_empty: 'An empty order cannot be completed.'
|
not_empty: 'An empty order cannot be completed.'
|
||||||
payment_method:
|
payment_method:
|
||||||
|
|
|
||||||
35
src/Sylius/Bundle/ApiBundle/Validator/Constraints/Code.php
Normal file
35
src/Sylius/Bundle/ApiBundle/Validator/Constraints/Code.php
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints\Compound;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Constraints\Regex;
|
||||||
|
use Symfony\Component\Validator\Constraints\Type;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class Code extends Compound
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $options
|
||||||
|
*/
|
||||||
|
protected function getConstraints(array $options): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new NotBlank(message: 'sylius.code.not_blank'),
|
||||||
|
new Type('string'),
|
||||||
|
new Regex(['pattern' => '/^[\w-]*$/'], message: 'sylius.code.invalid'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class DateInterval extends Constraint
|
||||||
|
{
|
||||||
|
public string $message = 'sylius.date_interval.invalid';
|
||||||
|
|
||||||
|
public function validatedBy(): string
|
||||||
|
{
|
||||||
|
return 'sylius_api_validator_date_interval';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class DateIntervalValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $value
|
||||||
|
* @param DateInterval $constraint
|
||||||
|
*/
|
||||||
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
Assert::string($value);
|
||||||
|
Assert::isInstanceOf($constraint, DateInterval::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new \DateInterval($value);
|
||||||
|
} catch (\Exception) {
|
||||||
|
$this->context->addViolation($constraint->message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraints\Compound;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class DatePeriod extends Compound
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $options
|
||||||
|
*/
|
||||||
|
protected function getConstraints(array $options): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
new EndDateIsNotBeforeStartDate(),
|
||||||
|
new EndDateAgainstInterval(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class EndDateAgainstInterval extends Constraint
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
public $message = 'sylius.date_period.end_date_must_be_multiple_of_interval';
|
||||||
|
|
||||||
|
public function validatedBy(): string
|
||||||
|
{
|
||||||
|
return 'sylius_api_validator_end_date_against_interval';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class EndDateAgainstIntervalValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param \DatePeriod $value
|
||||||
|
* @param EndDateAgainstInterval $constraint
|
||||||
|
*
|
||||||
|
* Validates that the end date is a multiple of the interval.
|
||||||
|
* The end date is adjusted by subtracting one second to make it inclusive (closed interval).
|
||||||
|
* If the adjusted end date does not match the provided end date, an exception is thrown.
|
||||||
|
*/
|
||||||
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
Assert::isInstanceOf($value, \DatePeriod::class);
|
||||||
|
Assert::isInstanceOf($constraint, EndDateAgainstInterval::class);
|
||||||
|
|
||||||
|
$currentDate = clone $value->getStartDate();
|
||||||
|
$endDate = $value->getEndDate();
|
||||||
|
$interval = $value->getDateInterval();
|
||||||
|
|
||||||
|
while ($currentDate <= $endDate) {
|
||||||
|
$currentDate = $currentDate->add($interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** We shift to make closed interval. */
|
||||||
|
$intervalEndDate = $currentDate->modify('-1 second');
|
||||||
|
|
||||||
|
if ($intervalEndDate != $endDate) {
|
||||||
|
$this
|
||||||
|
->context
|
||||||
|
->buildViolation($constraint->message)
|
||||||
|
->setParameter('%givenDate%', $endDate->format('Y-m-d H:i:s'))
|
||||||
|
->setParameter('%expectedDate%', $intervalEndDate->format('Y-m-d H:i:s'))
|
||||||
|
->addViolation()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class EndDateIsNotBeforeStartDate extends Constraint
|
||||||
|
{
|
||||||
|
public string $message = 'sylius.date_period.end_date_is_not_before_start_date';
|
||||||
|
|
||||||
|
public function validatedBy(): string
|
||||||
|
{
|
||||||
|
return 'sylius_api_validator_date_period_end_date_is_not_before_start_date';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Webmozart\Assert\Assert;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
final class EndDateIsNotBeforeStartDateValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param \DatePeriod $value
|
||||||
|
* @param EndDateIsNotBeforeStartDate $constraint
|
||||||
|
*/
|
||||||
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
Assert::isInstanceOf($value, \DatePeriod::class);
|
||||||
|
Assert::isInstanceOf($constraint, EndDateIsNotBeforeStartDate::class);
|
||||||
|
|
||||||
|
if ($value->getStartDate() > $value->getEndDate()) {
|
||||||
|
$this->context->addViolation($constraint->message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use PhpSpec\ObjectBehavior;
|
||||||
|
use Sylius\Bundle\ApiBundle\Validator\Constraints\DateInterval;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
|
||||||
|
final class DateIntervalValidatorSpec extends ObjectBehavior
|
||||||
|
{
|
||||||
|
function it_is_a_constraint_validator(): void
|
||||||
|
{
|
||||||
|
$this->shouldImplement(ConstraintValidatorInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_throws_an_exception_if_value_is_not_an_instance_of_date_interval(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->shouldThrow(\InvalidArgumentException::class)
|
||||||
|
->during('validate', [new \stdClass(), new DateInterval()])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_throws_an_exception_if_constraint_is_not_an_instance_of_date_interval(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->shouldThrow(\InvalidArgumentException::class)
|
||||||
|
->during('validate', [
|
||||||
|
new \DateInterval('P1D'),
|
||||||
|
new class() extends Constraint {
|
||||||
|
},
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_adds_violation_if_date_interval_format_is_invalid(ExecutionContextInterface $executionContext): void
|
||||||
|
{
|
||||||
|
$this->initialize($executionContext);
|
||||||
|
$constraint = new DateInterval();
|
||||||
|
|
||||||
|
$executionContext->addViolation($constraint->message)->shouldBeCalled();
|
||||||
|
|
||||||
|
$this->validate('INVALID_DATE_INTERVAL_FORMAT', $constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_does_not_add_violation_if_date_interval_format_is_valid(
|
||||||
|
ExecutionContextInterface $executionContext,
|
||||||
|
): void {
|
||||||
|
$this->initialize($executionContext);
|
||||||
|
$constraint = new DateInterval();
|
||||||
|
|
||||||
|
$executionContext->addViolation($constraint->message)->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$this->validate('P1D', $constraint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use PhpSpec\ObjectBehavior;
|
||||||
|
use Sylius\Bundle\ApiBundle\Validator\Constraints\EndDateAgainstInterval;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
|
||||||
|
|
||||||
|
final class EndDateAgainstIntervalValidatorSpec extends ObjectBehavior
|
||||||
|
{
|
||||||
|
function it_is_a_constraint_validator(): void
|
||||||
|
{
|
||||||
|
$this->shouldImplement(ConstraintValidatorInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_throws_an_exception_if_value_is_not_an_instance_of_date_period(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->shouldThrow(\InvalidArgumentException::class)
|
||||||
|
->during('validate', [new \stdClass(), new EndDateAgainstInterval()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_throws_an_exception_if_constraint_is_not_an_instance_of_end_date_against_interval(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->shouldThrow(\InvalidArgumentException::class)
|
||||||
|
->during('validate', [
|
||||||
|
new \DatePeriod(new \DateTimeImmutable(), new \DateInterval('P1D'), new \DateTimeImmutable()),
|
||||||
|
new class() extends Constraint {
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_adds_violation_if_end_date_is_not_a_multiple_of_interval(
|
||||||
|
ExecutionContextInterface $executionContext,
|
||||||
|
ConstraintViolationBuilderInterface $constraintViolationBuilder,
|
||||||
|
): void {
|
||||||
|
$this->initialize($executionContext);
|
||||||
|
$constraint = new EndDateAgainstInterval();
|
||||||
|
|
||||||
|
$constraintViolationBuilder
|
||||||
|
->setParameter('%expectedDate%', '2019-01-02 23:59:59')
|
||||||
|
->willReturn($constraintViolationBuilder)
|
||||||
|
;
|
||||||
|
$constraintViolationBuilder
|
||||||
|
->setParameter('%givenDate%', '2019-01-02 23:59:58')
|
||||||
|
->willReturn($constraintViolationBuilder)
|
||||||
|
;
|
||||||
|
$constraintViolationBuilder->addViolation()->shouldBeCalled();
|
||||||
|
|
||||||
|
$executionContext->buildViolation($constraint->message)->willReturn($constraintViolationBuilder);
|
||||||
|
|
||||||
|
$this->validate(
|
||||||
|
new \DatePeriod(
|
||||||
|
new \DateTimeImmutable('2019-01-01 00:00:00'),
|
||||||
|
new \DateInterval('P1D'),
|
||||||
|
new \DateTimeImmutable('2019-01-02 23:59:58'),
|
||||||
|
),
|
||||||
|
$constraint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_does_not_add_violation_if_end_date_is_a_multiple_of_interval(
|
||||||
|
ExecutionContextInterface $executionContext,
|
||||||
|
): void {
|
||||||
|
$this->initialize($executionContext);
|
||||||
|
$constraint = new EndDateAgainstInterval();
|
||||||
|
|
||||||
|
$executionContext->buildViolation($constraint->message)->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$this->validate(
|
||||||
|
new \DatePeriod(
|
||||||
|
new \DateTimeImmutable('2019-01-01 00:00:00'),
|
||||||
|
new \DateInterval('P1D'),
|
||||||
|
new \DateTimeImmutable('2019-01-03 23:59:59'),
|
||||||
|
),
|
||||||
|
$constraint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Sylius package.
|
||||||
|
*
|
||||||
|
* (c) Sylius Sp. z o.o.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints;
|
||||||
|
|
||||||
|
use PhpSpec\ObjectBehavior;
|
||||||
|
use Sylius\Bundle\ApiBundle\Validator\Constraints\EndDateIsNotBeforeStartDate;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidatorInterface;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
|
||||||
|
final class EndDateIsNotBeforeStartDateValidatorSpec extends ObjectBehavior
|
||||||
|
{
|
||||||
|
function it_is_a_constraint_validator(): void
|
||||||
|
{
|
||||||
|
$this->shouldImplement(ConstraintValidatorInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_throws_an_exception_if_value_is_not_an_instance_of_date_period(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->shouldThrow(\InvalidArgumentException::class)
|
||||||
|
->during('validate', [new \stdClass(), new EndDateIsNotBeforeStartDate()])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_throws_an_exception_if_constraint_is_not_an_instance_of_end_date_is_not_before_start_date(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->shouldThrow(\InvalidArgumentException::class)
|
||||||
|
->during('validate', [
|
||||||
|
new \DatePeriod(new \DateTimeImmutable(), new \DateInterval('P1D'), new \DateTimeImmutable()),
|
||||||
|
new class() extends Constraint {
|
||||||
|
},
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_adds_violation_if_end_date_is_before_start_date(
|
||||||
|
ExecutionContextInterface $executionContext,
|
||||||
|
): void {
|
||||||
|
$this->initialize($executionContext);
|
||||||
|
$constraint = new EndDateIsNotBeforeStartDate();
|
||||||
|
|
||||||
|
$executionContext->addViolation($constraint->message)->shouldBeCalled();
|
||||||
|
|
||||||
|
$this->validate(
|
||||||
|
new \DatePeriod(
|
||||||
|
new \DateTimeImmutable('2020-01-01'),
|
||||||
|
new \DateInterval('P1D'),
|
||||||
|
new \DateTimeImmutable('2019-01-01'),
|
||||||
|
),
|
||||||
|
$constraint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function it_does_not_add_violation_if_end_date_is_after_start_date(
|
||||||
|
ExecutionContextInterface $executionContext,
|
||||||
|
): void {
|
||||||
|
$this->initialize($executionContext);
|
||||||
|
$constraint = new EndDateIsNotBeforeStartDate();
|
||||||
|
|
||||||
|
$executionContext->addViolation($constraint->message)->shouldNotBeCalled();
|
||||||
|
|
||||||
|
$this->validate(
|
||||||
|
new \DatePeriod(
|
||||||
|
new \DateTimeImmutable('2019-01-01'),
|
||||||
|
new \DateInterval('P1D'),
|
||||||
|
new \DateTimeImmutable('2020-01-01'),
|
||||||
|
),
|
||||||
|
$constraint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,8 +36,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->headerBuilder()
|
->headerBuilder()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertResponse(
|
$this->assertResponse(
|
||||||
|
|
@ -62,8 +61,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->headerBuilder()
|
->headerBuilder()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$response = $this->client->getResponse();
|
$response = $this->client->getResponse();
|
||||||
|
|
@ -76,7 +74,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
{
|
{
|
||||||
$this->loadFixturesFromFiles([
|
$this->loadFixturesFromFiles([
|
||||||
'product/product_with_many_locales.yaml',
|
'product/product_with_many_locales.yaml',
|
||||||
'authentication/api_administrator.yaml'
|
'authentication/api_administrator.yaml',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->client->request(
|
$this->client->request(
|
||||||
|
|
@ -86,8 +84,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->headerBuilder()
|
->headerBuilder()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertResponse(
|
$this->assertResponse(
|
||||||
|
|
@ -113,8 +110,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->withJsonLdContentType()
|
->withJsonLdContentType()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
content: json_encode([
|
content: json_encode([
|
||||||
'type' => '/api/v2/admin/product-association-types/similar_products',
|
'type' => '/api/v2/admin/product-association-types/similar_products',
|
||||||
'owner' => '/api/v2/admin/products/CUP',
|
'owner' => '/api/v2/admin/products/CUP',
|
||||||
|
|
@ -149,8 +145,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->withJsonLdContentType()
|
->withJsonLdContentType()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
content: json_encode([
|
content: json_encode([
|
||||||
'associatedProducts' => [
|
'associatedProducts' => [
|
||||||
'/api/v2/admin/products/TANKARD',
|
'/api/v2/admin/products/TANKARD',
|
||||||
|
|
@ -217,8 +212,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->withJsonLdContentType()
|
->withJsonLdContentType()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
content: '{}',
|
content: '{}',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -248,8 +242,7 @@ final class ProductAssociationsTest extends JsonApiTestCase
|
||||||
->withJsonLdContentType()
|
->withJsonLdContentType()
|
||||||
->withJsonLdAccept()
|
->withJsonLdAccept()
|
||||||
->withAdminUserAuthorization('api@example.com')
|
->withAdminUserAuthorization('api@example.com')
|
||||||
->build()
|
->build(),
|
||||||
,
|
|
||||||
content: json_encode([
|
content: json_encode([
|
||||||
'type' => sprintf('/api/v2/admin/product-association-types/%s', $association->getType()->getCode()),
|
'type' => sprintf('/api/v2/admin/product-association-types/%s', $association->getType()->getCode()),
|
||||||
'owner' => sprintf('/api/v2/admin/products/%s', $association->getOwner()->getCode()),
|
'owner' => sprintf('/api/v2/admin/products/%s', $association->getOwner()->getCode()),
|
||||||
|
|
|
||||||
|
|
@ -214,24 +214,6 @@ final class StatisticsTest extends JsonApiTestCase
|
||||||
|
|
||||||
public function invalidQueryParameters(): iterable
|
public function invalidQueryParameters(): iterable
|
||||||
{
|
{
|
||||||
yield 'invalid channelCode as bool value' => [
|
|
||||||
'parameters' => [
|
|
||||||
'channelCode' => true,
|
|
||||||
'startDate' => '2023-01-01T00:00:00',
|
|
||||||
'dateInterval' => 'P1M',
|
|
||||||
'endDate' => '2023-12-31T23:59:59',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'invalid channelCode as int value' => [
|
|
||||||
'parameters' => [
|
|
||||||
'channelCode' => 1,
|
|
||||||
'startDate' => '2023-01-01T00:00:00',
|
|
||||||
'dateInterval' => 'P1M',
|
|
||||||
'endDate' => '2023-12-31T23:59:59',
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
yield 'invalid channelCode as float value' => [
|
yield 'invalid channelCode as float value' => [
|
||||||
'parameters' => [
|
'parameters' => [
|
||||||
'channelCode' => 1.1,
|
'channelCode' => 1.1,
|
||||||
|
|
@ -276,6 +258,16 @@ final class StatisticsTest extends JsonApiTestCase
|
||||||
'endDate' => '2023-12-31T23:59:59',
|
'endDate' => '2023-12-31T23:59:59',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
yield 'unexpected parameter' => [
|
||||||
|
'parameters' => [
|
||||||
|
'channelCode' => 'WEB',
|
||||||
|
'startDate' => '2023-01-01T00:00:00',
|
||||||
|
'dateInterval' => 'P1M1D',
|
||||||
|
'endDate' => '2023-12-31T23:59:59',
|
||||||
|
'unexpected_parameter' => 'unexpected_value',
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function invalidPeriods(): iterable
|
public function invalidPeriods(): iterable
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue