Redesign and refactor handling of page view submission
This commit is contained in:
parent
a733008cb4
commit
fc5fbb0ccb
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Contract;
|
||||
|
||||
// TODO: convert into native enum when PHP 8.1 is available on production
|
||||
final class PageViewSubmissionResultStatus
|
||||
{
|
||||
public const SUCCESS = 'success';
|
||||
public const ERROR_JSON_DECODE = 'json-decode';
|
||||
public const ERROR_JSON_SCHEMA = 'json-schema';
|
||||
public const ERROR_ACCESS_TOKEN = 'access-token';
|
||||
}
|
|
@ -5,10 +5,8 @@ declare(strict_types=1);
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Contract\Config\AppRoutes;
|
||||
use App\DTO\NexusRawDataSubmissionResult;
|
||||
use App\Service\NexusRawDataManager;
|
||||
use App\Service\NexusRawDataValidator;
|
||||
use JsonException;
|
||||
use App\Contract\PageViewSubmissionResultStatus;
|
||||
use App\Service\PageViewSubmissionHandler;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
@ -17,16 +15,11 @@ use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
|||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
use function json_decode;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
final class SubmitJsonController
|
||||
{
|
||||
public function __construct(
|
||||
private Environment $twigEnvironment,
|
||||
private NexusRawDataValidator $validator,
|
||||
private NexusRawDataManager $nexusRawDataManager,
|
||||
private PageViewSubmissionHandler $pageViewSubmissionHandler,
|
||||
private SerializerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
@ -39,33 +32,23 @@ final class SubmitJsonController
|
|||
$userAccessTokenValue = $request->request->get(key: 'userAccessToken');
|
||||
$jsonData = $request->request->get(key: 'jsonData');
|
||||
|
||||
try {
|
||||
$decodedJsonData = json_decode(json: $jsonData, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
$responseData = new NexusRawDataSubmissionResult(
|
||||
isValid: false,
|
||||
errorSource: NexusRawDataSubmissionResult::ERROR_SOURCE_JSON_DECODE,
|
||||
errors: [$e->getMessage()]
|
||||
);
|
||||
return $this->createJsonResponse(data: $responseData, status: Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$validationResult = $this->validator->validate(decodedJsonData: $decodedJsonData);
|
||||
if (false === $validationResult->isValid()) {
|
||||
return $this->createJsonResponse(data: $validationResult, status: Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$submissionResult = $this->nexusRawDataManager->handleSubmission(
|
||||
$submissionResult = $this->pageViewSubmissionHandler->handle(
|
||||
userAccessTokenValue: $userAccessTokenValue,
|
||||
decodedJsonData: $decodedJsonData
|
||||
jsonData: $jsonData,
|
||||
);
|
||||
|
||||
return $this->createJsonResponse(data: $submissionResult, status: Response::HTTP_CREATED);
|
||||
$responseStatus = match ($submissionResult->getStatus()) {
|
||||
PageViewSubmissionResultStatus::SUCCESS => Response::HTTP_CREATED,
|
||||
PageViewSubmissionResultStatus::ERROR_ACCESS_TOKEN => Response::HTTP_UNAUTHORIZED,
|
||||
default => Response::HTTP_BAD_REQUEST,
|
||||
};
|
||||
|
||||
return $this->createJsonResponse(data: $submissionResult, status: $responseStatus);
|
||||
}
|
||||
|
||||
$content = $this->twigEnvironment->render(name: 'submit-json/index.html.twig');
|
||||
|
||||
return new Response($content);
|
||||
return new Response(content: $content);
|
||||
}
|
||||
|
||||
private function createJsonResponse(mixed $data, int $status): Response
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
use App\Contract\Config\AppSerializationGroups;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||
|
||||
final class NexusRawDataSubmissionResult
|
||||
{
|
||||
public const ERROR_SOURCE_JSON_DECODE = 'json-decode';
|
||||
public const ERROR_SOURCE_JSON_SCHEMA = 'json-schema';
|
||||
public const ERROR_SOURCE_ACCESS_TOKEN = 'access-token';
|
||||
|
||||
public function __construct(
|
||||
private bool $isValid,
|
||||
private ?string $errorSource = null,
|
||||
private ?array $errors = null,
|
||||
) {
|
||||
}
|
||||
|
||||
#[
|
||||
Groups(groups: [AppSerializationGroups::DEFAULT]),
|
||||
SerializedName(serializedName: 'isValid'),
|
||||
]
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->isValid;
|
||||
}
|
||||
|
||||
#[
|
||||
Groups(groups: [AppSerializationGroups::DEFAULT]),
|
||||
SerializedName(serializedName: 'errorSource'),
|
||||
]
|
||||
public function getErrorSource(): ?string
|
||||
{
|
||||
return $this->errorSource;
|
||||
}
|
||||
|
||||
#[
|
||||
Groups(groups: [AppSerializationGroups::DEFAULT]),
|
||||
SerializedName(serializedName: 'errors'),
|
||||
]
|
||||
public function getErrors(): ?array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
use App\Contract\Config\AppSerializationGroups;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Serializer\Annotation\SerializedName;
|
||||
|
||||
final class PageViewSubmissionResult
|
||||
{
|
||||
#[
|
||||
Groups(groups: [AppSerializationGroups::DEFAULT]),
|
||||
SerializedName(serializedName: 'status'),
|
||||
]
|
||||
private string $status;
|
||||
|
||||
#[
|
||||
Groups(groups: [AppSerializationGroups::DEFAULT]),
|
||||
SerializedName(serializedName: 'errors'),
|
||||
]
|
||||
private array $errors;
|
||||
|
||||
public function __construct(
|
||||
string $status,
|
||||
array $errors = [],
|
||||
) {
|
||||
$this->errors = $errors;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getErrors(): ?array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\DTO;
|
||||
|
||||
final class PageViewSubmissionValidatorResult
|
||||
{
|
||||
public function __construct(
|
||||
private bool $isValid,
|
||||
private ?array $errors,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return $this->isValid;
|
||||
}
|
||||
|
||||
public function getErrors(): ?array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Contract\Service\ClockInterface;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
|
||||
final class DateTimeFactory
|
||||
{
|
||||
public function __construct(private ClockInterface $clock)
|
||||
{
|
||||
}
|
||||
|
||||
public function create(string $dateTimeString): ?DateTimeInterface
|
||||
{
|
||||
try {
|
||||
$instance = new DateTimeImmutable(datetime: $dateTimeString);
|
||||
} catch (Exception) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$instance->setTimezone(timezone: $this->clock->getUtcTimeZone());
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Doctrine\Entity\PageView;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
final class NexusRawDataFactory
|
||||
{
|
||||
public function createFromJsonDataSubmission(object $decodedJsonData, DateTimeZone $timeZone): PageView
|
||||
{
|
||||
$pageView = new PageView();
|
||||
|
||||
$data = get_object_vars(object: $decodedJsonData);
|
||||
|
||||
if (array_key_exists(key: 'requestStartedAt', array: $data)) {
|
||||
$requestStartedAt = new DateTimeImmutable(datetime: $data['requestStartedAt']);
|
||||
$requestStartedAt->setTimezone(timezone: $timeZone);
|
||||
$pageView->setRequestStartedAt(requestStartedAt: $requestStartedAt);
|
||||
}
|
||||
if (array_key_exists(key: 'responseCompletedAt', array: $data)) {
|
||||
$responseCompletedAt = new DateTimeImmutable(datetime: $data['responseCompletedAt']);
|
||||
$responseCompletedAt->setTimezone(timezone: $timeZone);
|
||||
$pageView->setResponseCompletedAt(responseCompletedAt: $responseCompletedAt);
|
||||
}
|
||||
if (array_key_exists(key: 'method', array: $data)) {
|
||||
$pageView->setMethod(method: $data['method']);
|
||||
}
|
||||
if (array_key_exists(key: 'url', array: $data)) {
|
||||
$pageView->setUrl(url: $data['url']);
|
||||
}
|
||||
if (array_key_exists(key: 'formData', array: $data)) {
|
||||
$pageView->setFormData(formData: $data['formData']);
|
||||
}
|
||||
if (array_key_exists(key: 'responseBody', array: $data)) {
|
||||
$pageView->setResponseBody(responseBody: $data['responseBody']);
|
||||
}
|
||||
|
||||
return $pageView;
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Contract\Service\ClockInterface;
|
||||
use App\DTO\NexusRawDataSubmissionResult;
|
||||
use App\Service\Repository\UserAccessTokenRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
final class NexusRawDataManager
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private NexusRawDataFactory $nexusRawDataFactory,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private UserAccessTokenRepository $userAccessTokenRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handleSubmission(
|
||||
string $userAccessTokenValue,
|
||||
object $decodedJsonData
|
||||
): NexusRawDataSubmissionResult {
|
||||
$timeZone = $this->clock->getUtcTimeZone();
|
||||
$currentDateTime = $this->clock->getCurrentDateTime();
|
||||
|
||||
$nexusRawData = $this->nexusRawDataFactory->createFromJsonDataSubmission(
|
||||
decodedJsonData: $decodedJsonData,
|
||||
timeZone: $timeZone
|
||||
);
|
||||
|
||||
$userAccessToken = $this->userAccessTokenRepository->findByValue(value: $userAccessTokenValue);
|
||||
if (null === $userAccessToken) {
|
||||
$errors = [
|
||||
sprintf('Invalid access token value: %s', $userAccessTokenValue),
|
||||
];
|
||||
return new NexusRawDataSubmissionResult(
|
||||
isValid: false,
|
||||
errorSource: NexusRawDataSubmissionResult::ERROR_SOURCE_ACCESS_TOKEN,
|
||||
errors: $errors
|
||||
);
|
||||
}
|
||||
|
||||
$owner = $userAccessToken->getOwner();
|
||||
|
||||
$nexusRawData->setCreatedAt(createdAt: $currentDateTime);
|
||||
$nexusRawData->setLastModifiedAt(lastModifiedAt: $currentDateTime);
|
||||
$nexusRawData->setOwner(owner: $owner);
|
||||
|
||||
$this->entityManager->persist($nexusRawData);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new NexusRawDataSubmissionResult(isValid: true, errorSource: null, errors: null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Contract\PageViewSubmissionResultStatus;
|
||||
use App\Contract\Service\ClockInterface;
|
||||
use App\Doctrine\Entity\PageView;
|
||||
use App\DTO\PageViewSubmissionResult;
|
||||
use App\Service\Repository\PageViewRepository;
|
||||
use App\Service\Repository\UserAccessTokenRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use JsonException;
|
||||
|
||||
use function array_key_exists;
|
||||
use function get_object_vars;
|
||||
use function json_decode;
|
||||
use function sprintf;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
final class PageViewSubmissionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ClockInterface $clock,
|
||||
private UserAccessTokenRepository $userAccessTokenRepository,
|
||||
private PageViewSubmissionValidator $validator,
|
||||
private DateTimeFactory $dateTimeFactory,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private PageViewRepository $pageViewRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(string $userAccessTokenValue, string $jsonData): PageViewSubmissionResult
|
||||
{
|
||||
// validate the access token
|
||||
$userAccessToken = $this->userAccessTokenRepository->findByValue(value: $userAccessTokenValue);
|
||||
if (null === $userAccessToken) {
|
||||
$errors = [
|
||||
sprintf('Invalid access token value: %s', $userAccessTokenValue),
|
||||
];
|
||||
return new PageViewSubmissionResult(
|
||||
status: PageViewSubmissionResultStatus::ERROR_ACCESS_TOKEN,
|
||||
errors: $errors,
|
||||
);
|
||||
}
|
||||
|
||||
$owner = $userAccessToken->getOwner();
|
||||
|
||||
// decode input data from submitted JSON string
|
||||
try {
|
||||
$decodedJsonData = json_decode(json: $jsonData, flags: JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
return new PageViewSubmissionResult(
|
||||
status: PageViewSubmissionResultStatus::ERROR_JSON_DECODE,
|
||||
errors: [$e->getMessage()],
|
||||
);
|
||||
}
|
||||
|
||||
// validate format of input data
|
||||
$validationResult = $this->validator->validate(decodedJsonData: $decodedJsonData);
|
||||
if (false === $validationResult->isValid()) {
|
||||
return new PageViewSubmissionResult(
|
||||
status: PageViewSubmissionResultStatus::ERROR_JSON_SCHEMA,
|
||||
errors: $validationResult->getErrors(),
|
||||
);
|
||||
}
|
||||
|
||||
// build and persist PageView instance
|
||||
$currentDateTime = $this->clock->getCurrentDateTime();
|
||||
$pageView = $this->buildPageView(decodedJsonData: $decodedJsonData);
|
||||
$this->pageViewRepository->persist(owner: $owner, pageView: $pageView, currentDateTime: $currentDateTime);
|
||||
|
||||
return new PageViewSubmissionResult(status: PageViewSubmissionResultStatus::SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
private function buildPageView(object $decodedJsonData): PageView
|
||||
{
|
||||
$pageView = new PageView();
|
||||
|
||||
$data = get_object_vars(object: $decodedJsonData);
|
||||
|
||||
if (array_key_exists(key: 'requestStartedAt', array: $data)) {
|
||||
$requestStartedAt = $this->dateTimeFactory->create(dateTimeString: $data['requestStartedAt']);
|
||||
$pageView->setRequestStartedAt(requestStartedAt: $requestStartedAt);
|
||||
}
|
||||
|
||||
if (array_key_exists(key: 'responseCompletedAt', array: $data)) {
|
||||
$responseCompletedAt = $this->dateTimeFactory->create(dateTimeString: $data['responseCompletedAt']);
|
||||
$pageView->setResponseCompletedAt(responseCompletedAt: $responseCompletedAt);
|
||||
}
|
||||
|
||||
if (array_key_exists(key: 'method', array: $data)) {
|
||||
$pageView->setMethod(method: $data['method']);
|
||||
}
|
||||
|
||||
if (array_key_exists(key: 'url', array: $data)) {
|
||||
$pageView->setUrl(url: $data['url']);
|
||||
}
|
||||
|
||||
if (array_key_exists(key: 'formData', array: $data)) {
|
||||
$pageView->setFormData(formData: $data['formData']);
|
||||
}
|
||||
|
||||
if (array_key_exists(key: 'responseBody', array: $data)) {
|
||||
$pageView->setResponseBody(responseBody: $data['responseBody']);
|
||||
}
|
||||
|
||||
return $pageView;
|
||||
}
|
||||
}
|
|
@ -4,12 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Service;
|
||||
|
||||
use App\DTO\NexusRawDataSubmissionResult;
|
||||
use App\DTO\PageViewSubmissionValidatorResult;
|
||||
use App\Kernel;
|
||||
use Opis\JsonSchema\Errors\ErrorFormatter;
|
||||
use Opis\JsonSchema\Validator;
|
||||
|
||||
final class NexusRawDataValidator
|
||||
/**
|
||||
* Facade for validator from "Opis JSON Schema" package
|
||||
*/
|
||||
final class PageViewSubmissionValidator
|
||||
{
|
||||
private const SCHEMA_ID = 'https://nexus-archive.zerozero.pl/submit-json';
|
||||
private Validator $validator;
|
||||
|
@ -27,21 +30,17 @@ final class NexusRawDataValidator
|
|||
|
||||
private function getSchemaPath(): string
|
||||
{
|
||||
return $this->kernel->getProjectDir() . '/assets/NexusRawDataJsonSchema.json';
|
||||
return $this->kernel->getProjectDir() . '/assets/PageViewSubmissionJsonSchema.json';
|
||||
}
|
||||
|
||||
public function validate(mixed $decodedJsonData): NexusRawDataSubmissionResult
|
||||
public function validate(mixed $decodedJsonData): PageViewSubmissionValidatorResult
|
||||
{
|
||||
$validationResult = $this->validator->validate(data: $decodedJsonData, schema: self::SCHEMA_ID);
|
||||
if ($validationResult->isValid()) {
|
||||
return new NexusRawDataSubmissionResult(isValid: true, errorSource: null, errors: null);
|
||||
return new PageViewSubmissionValidatorResult(isValid: true, errors: null);
|
||||
}
|
||||
|
||||
$errors = $this->errorFormatter->format(error: $validationResult->error(), multiple: true);
|
||||
return new NexusRawDataSubmissionResult(
|
||||
isValid: false,
|
||||
errorSource: NexusRawDataSubmissionResult::ERROR_SOURCE_JSON_SCHEMA,
|
||||
errors: $errors
|
||||
);
|
||||
return new PageViewSubmissionValidatorResult(isValid: false, errors: $errors);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Service\Repository;
|
||||
|
||||
use App\Doctrine\Entity\PageView;
|
||||
use App\Doctrine\Entity\User;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
|
@ -15,6 +16,15 @@ final class PageViewRepository
|
|||
) {
|
||||
}
|
||||
|
||||
public function persist(User $owner, PageView $pageView, DateTimeInterface $currentDateTime): void
|
||||
{
|
||||
$pageView->setCreatedAt(createdAt: $currentDateTime);
|
||||
$pageView->setLastModifiedAt(lastModifiedAt: $currentDateTime);
|
||||
$pageView->setOwner(owner: $owner);
|
||||
$this->entityManager->persist($pageView);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function getTotalCount(): int
|
||||
{
|
||||
$queryBuilder = $this->entityManager->createQueryBuilder()
|
||||
|
|
Reference in New Issue