1
0
Fork 0

Redesign and refactor handling of page view submission

This commit is contained in:
Krzysztof Sikorski 2022-04-21 00:21:35 +02:00
parent a733008cb4
commit fc5fbb0ccb
Signed by: krzysztof-sikorski
GPG Key ID: 4EB564BD08FE8476
12 changed files with 255 additions and 193 deletions

View File

@ -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';
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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()