Compare commits

...

4 commits

Author SHA1 Message Date
Alexey Skobkin 9747eefe47
Replacing jms/serializer with symfony/serializer and doing some DTO refactoring. 2023-08-19 03:32:43 +03:00
Alexey Skobkin a7c17022dc
app:subscriptions:update --dry-run. 2023-08-19 03:28:45 +03:00
Alexey Skobkin 0a03a9a904
Fixing TelegramWebhookCommand setWebhook() call. 2023-08-19 01:22:37 +03:00
Alexey Skobkin 1c223f7077
Using --dry-run for app commands. 2023-08-19 01:22:09 +03:00
35 changed files with 281 additions and 945 deletions

View file

@ -14,7 +14,6 @@
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14",
"ghunti/highcharts-php": "^5.0",
"jms/serializer-bundle": "^5.2",
"knplabs/knp-paginator-bundle": "^6.2",
"league/commonmark": "^2.4",
"phpdocumentor/reflection-docblock": "^5.3",
@ -34,6 +33,7 @@
"symfony/property-info": "6.3.*",
"symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*",
"symfony/string": "6.3.*",
"symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*",

343
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "274812d219a3d19c925c0e67b5f86926",
"content-hash": "0cb307d1ad554ab97a56f5805e8c6adb",
"packages": [
{
"name": "dflydev/dot-access-data",
@ -1689,253 +1689,6 @@
},
"time": "2023-04-26T21:37:31+00:00"
},
{
"name": "jms/metadata",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/metadata.git",
"reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/metadata/zipball/7ca240dcac0c655eb15933ee55736ccd2ea0d7a6",
"reference": "7ca240dcac0c655eb15933ee55736ccd2ea0d7a6",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
},
"require-dev": {
"doctrine/cache": "^1.0",
"doctrine/coding-standard": "^8.0",
"mikey179/vfsstream": "^1.6.7",
"phpunit/phpunit": "^8.5|^9.0",
"psr/container": "^1.0|^2.0",
"symfony/cache": "^3.1|^4.0|^5.0",
"symfony/dependency-injection": "^3.1|^4.0|^5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"Metadata\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "Class/method/property metadata management in PHP",
"keywords": [
"annotations",
"metadata",
"xml",
"yaml"
],
"support": {
"issues": "https://github.com/schmittjoh/metadata/issues",
"source": "https://github.com/schmittjoh/metadata/tree/2.8.0"
},
"time": "2023-02-15T13:44:18+00:00"
},
{
"name": "jms/serializer",
"version": "3.27.0",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/serializer.git",
"reference": "e8c812460d7b47b15bc0ccd78901276bd44ad452"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/serializer/zipball/e8c812460d7b47b15bc0ccd78901276bd44ad452",
"reference": "e8c812460d7b47b15bc0ccd78901276bd44ad452",
"shasum": ""
},
"require": {
"doctrine/annotations": "^1.13 || ^2.0",
"doctrine/instantiator": "^1.0.3 || ^2.0",
"doctrine/lexer": "^2.0 || ^3.0",
"jms/metadata": "^2.6",
"php": "^7.2||^8.0",
"phpstan/phpdoc-parser": "^0.4 || ^0.5 || ^1.0"
},
"require-dev": {
"doctrine/coding-standard": "^8.1",
"doctrine/orm": "~2.1",
"doctrine/persistence": "^1.3.3|^2.0|^3.0",
"doctrine/phpcr-odm": "^1.3|^2.0",
"ext-pdo_sqlite": "*",
"jackalope/jackalope-doctrine-dbal": "^1.1.5",
"ocramius/proxy-manager": "^1.0|^2.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "^1.0.2",
"phpunit/phpunit": "^8.5.21||^9.0||^10.0",
"psr/container": "^1.0|^2.0",
"symfony/dependency-injection": "^3.0|^4.0|^5.0|^6.0",
"symfony/expression-language": "^3.2|^4.0|^5.0|^6.0",
"symfony/filesystem": "^3.0|^4.0|^5.0|^6.0",
"symfony/form": "^3.0|^4.0|^5.0|^6.0",
"symfony/translation": "^3.0|^4.0|^5.0|^6.0",
"symfony/uid": "^5.1|^6.0",
"symfony/validator": "^3.1.9|^4.0|^5.0|^6.0",
"symfony/yaml": "^3.3|^4.0|^5.0|^6.0",
"twig/twig": "~1.34|~2.4|^3.0"
},
"suggest": {
"doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.",
"symfony/cache": "Required if you like to use cache functionality.",
"symfony/uid": "Required if you'd like to serialize UID objects.",
"symfony/yaml": "Required if you'd like to use the YAML metadata format."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"JMS\\Serializer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "Library for (de-)serializing data of any complexity; supports XML, and JSON.",
"homepage": "http://jmsyst.com/libs/serializer",
"keywords": [
"deserialization",
"jaxb",
"json",
"serialization",
"xml"
],
"support": {
"issues": "https://github.com/schmittjoh/serializer/issues",
"source": "https://github.com/schmittjoh/serializer/tree/3.27.0"
},
"funding": [
{
"url": "https://github.com/goetas",
"type": "github"
}
],
"time": "2023-07-29T22:33:44+00:00"
},
{
"name": "jms/serializer-bundle",
"version": "5.3.1",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/JMSSerializerBundle.git",
"reference": "3279738a958454793ca1e318a7dab6cfcff60124"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/JMSSerializerBundle/zipball/3279738a958454793ca1e318a7dab6cfcff60124",
"reference": "3279738a958454793ca1e318a7dab6cfcff60124",
"shasum": ""
},
"require": {
"jms/metadata": "^2.5",
"jms/serializer": "^3.20",
"php": "^7.2 || ^8.0",
"symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"require-dev": {
"doctrine/coding-standard": "^8.1",
"doctrine/orm": "^2.4",
"phpunit/phpunit": "^8.0 || ^9.0",
"symfony/expression-language": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/finder": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/form": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/stopwatch": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/templating": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/uid": "^5.1 || ^6.0",
"symfony/validator": "^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"suggest": {
"symfony/expression-language": "Required for opcache preloading ^3.4 || ^4.0 || ^5.0 || ^6.0",
"symfony/finder": "Required for cache warmup, supported versions ^3.4 || ^4.0 || ^5.0 || ^6.0"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"JMS\\SerializerBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Asmir Mustafic",
"email": "goetas@gmail.com"
}
],
"description": "Allows you to easily serialize, and deserialize data of any complexity",
"homepage": "http://jmsyst.com/bundles/JMSSerializerBundle",
"keywords": [
"deserialization",
"json",
"serialization",
"xml"
],
"support": {
"issues": "https://github.com/schmittjoh/JMSSerializerBundle/issues",
"source": "https://github.com/schmittjoh/JMSSerializerBundle/tree/5.3.1"
},
"funding": [
{
"url": "https://github.com/goetas",
"type": "github"
}
],
"time": "2023-06-13T14:47:57+00:00"
},
{
"name": "knplabs/knp-components",
"version": "v4.2.0",
@ -6439,6 +6192,100 @@
],
"time": "2023-07-13T14:29:38+00:00"
},
{
"name": "symfony/serializer",
"version": "v6.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/serializer.git",
"reference": "33deb86d212893042d7758d452aa39d19ca0efe3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/serializer/zipball/33deb86d212893042d7758d452aa39d19ca0efe3",
"reference": "33deb86d212893042d7758d452aa39d19ca0efe3",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "~1.8"
},
"conflict": {
"doctrine/annotations": "<1.12",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/dependency-injection": "<5.4",
"symfony/property-access": "<5.4",
"symfony/property-info": "<5.4.24|>=6,<6.2.11",
"symfony/uid": "<5.4",
"symfony/yaml": "<5.4"
},
"require-dev": {
"doctrine/annotations": "^1.12|^2",
"phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0",
"symfony/cache": "^5.4|^6.0",
"symfony/config": "^5.4|^6.0",
"symfony/console": "^5.4|^6.0",
"symfony/dependency-injection": "^5.4|^6.0",
"symfony/error-handler": "^5.4|^6.0",
"symfony/filesystem": "^5.4|^6.0",
"symfony/form": "^5.4|^6.0",
"symfony/http-foundation": "^5.4|^6.0",
"symfony/http-kernel": "^5.4|^6.0",
"symfony/mime": "^5.4|^6.0",
"symfony/property-access": "^5.4|^6.0",
"symfony/property-info": "^5.4.24|^6.2.11",
"symfony/uid": "^5.4|^6.0",
"symfony/validator": "^5.4|^6.0",
"symfony/var-dumper": "^5.4|^6.0",
"symfony/var-exporter": "^5.4|^6.0",
"symfony/yaml": "^5.4|^6.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Serializer\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/serializer/tree/v6.3.3"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-07-31T07:08:24+00:00"
},
{
"name": "symfony/service-contracts",
"version": "v3.3.0",

View file

@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
@ -13,7 +12,6 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true],
Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
];

View file

@ -1,30 +0,0 @@
jms_serializer:
visitors:
xml_serialization:
format_output: '%kernel.debug%'
# metadata:
# auto_detection: false
# directories:
# any-name:
# namespace_prefix: "My\\FooBundle"
# path: "@MyFooBundle/Resources/config/serializer"
# another-name:
# namespace_prefix: "My\\BarBundle"
# path: "@MyBarBundle/Resources/config/serializer"
when@prod:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION
when@dev:
jms_serializer:
visitors:
json_serialization:
options:
- JSON_PRETTY_PRINT
- JSON_UNESCAPED_SLASHES
- JSON_PRESERVE_ZERO_FRACTION

View file

@ -1,11 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\Auth:
exclusion_policy: none
access_type: public_method
properties:
token:
type: string
csRfToken:
type: string
serialized_name: 'csrf_token'
error:
type: string

View file

@ -1,23 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment:
exclusion_policy: none
access_type: public_method
properties:
postId:
serialized_name: 'post_id'
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post'
max_depth: 2
number:
serialized_name: 'id'
type: integer
toCommentId:
serialized_name: 'to_comment_id'
type: integer
created:
type: string
text:
type: string
author:
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\User'
isRec:
serialized_name: 'is_rec'
type: boolean

View file

@ -1,10 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost:
exclusion_policy: none
access_type: public_method
properties:
post:
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post'
max_depth: 2
comments:
type: 'array<Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment>'
max_depth: 2

View file

@ -1,25 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\Post:
exclusion_policy: none
access_type: public_method
properties:
id:
type: string
#groups: []
tags:
type: 'array<string>'
files:
type: 'array<string>'
author:
type: Skobkin\Bundle\PointToolsBundle\DTO\Api\User
max_depth: 1
text:
type: string
created:
type: string
type:
type: string
private:
type: boolean
#callback_methods:
# post_deserialize: [foo, bar]

View file

@ -1,10 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\PostsPage:
exclusion_policy: none
access_type: public_method
properties:
posts:
type: 'array<Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost>'
max_depth: 3
hasNext:
serialized_name: 'has_next'
type: boolean

View file

@ -1,49 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\User:
exclusion_policy: none
access_type: public_method
properties:
id:
type: integer
groups: [user_short, user_full]
login:
type: string
groups: [user_short, user_full]
name:
type: string
groups: [user_short, user_full]
about:
type: string
groups: [user_full]
xmpp:
type: string
groups: [user_full]
created:
type: string
groups: [user_full]
gender:
type: boolean
groups: [user_full]
denyAnonymous:
serialized_name: 'deny_anonymous'
type: boolean
groups: [user_full]
private:
type: boolean
groups: [user_full]
birthDate:
serialized_name: 'birthdate'
type: string
groups: [user_full]
homepage:
type: string
groups: [user_full]
email:
type: string
groups: [user_full]
location:
type: string
groups: [user_full]
# TODO automatically convert string date to DateTime
#callback_methods:
# post_deserialize: [foo, bar]

View file

@ -1 +0,0 @@
services:

View file

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace Tests\Skobkin\PointToolsBundle\Service\Factory;
@ -7,8 +8,8 @@ use App\Exception\Factory\InvalidUserDataException;
use App\Factory\UserFactory;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use src\PointToolsBundle\Entity\User;
use src\PointToolsBundle\Repository\UserRepository;
use App\Entity\User;
use App\Repository\UserRepository;
class UserFactoryTest extends TestCase
{
@ -26,8 +27,8 @@ class UserFactoryTest extends TestCase
{
$testUser = new User(
self::LOCAL_USER_ID,
\DateTime::createFromFormat(UserFactory::DATE_FORMAT, self::LOCAL_USER_CREATED),
self::LOCAL_USER_LOGIN,
\DateTime::createFromFormat(UserFactory::DATE_FORMAT, self::LOCAL_USER_CREATED),
self::LOCAL_USER_NAME
);

View file

@ -40,7 +40,7 @@ class ImportUsersCommand extends Command
'CSV file path'
)
->addOption(
'check-only',
'dry-run',
null,
InputOption::VALUE_NONE,
'If set, command will not perform write operations in the database'
@ -87,9 +87,9 @@ class ImportUsersCommand extends Command
$createdAt = \DateTime::createFromFormat('Y-m-d_H:i:s', $row[3]) ?: new \DateTime();
$user = new User($row[0], $createdAt, $row[1], $row[2]);
$user = new User($row[0], $row[1], $createdAt, $row[2]);
if (!$input->getOption('check-only')) {
if (!$input->getOption('dry-run')) {
$this->em->persist($user);
$this->em->flush($user);
$this->em->detach($user);

View file

@ -50,7 +50,7 @@ class TelegramSendMessageCommand extends Command
try {
$this->messenger->sendMessageToChat(
(int) $input->getOption('chat-id'),
$message
$message,
);
} catch (\Exception $e) {
$io->error($e->getMessage());

View file

@ -50,7 +50,7 @@ class TelegramWebhookCommand extends Command
try {
$this->client->setWebhook(
url: $url,
max_connections: $this->telegramWebhookMaxConnections,
maxConnections: $this->telegramWebhookMaxConnections,
);
} catch (\Exception $e) {
$io->error(\sprintf(

View file

@ -42,7 +42,7 @@ class UpdateSubscriptionsCommand extends Command
'If set, command will check subscribers of all service users instead of service subscribers only'
)
->addOption(
'check-only',
'dry-run',
null,
InputOption::VALUE_NONE,
'If set, command will not perform write operations in the database'
@ -59,7 +59,7 @@ class UpdateSubscriptionsCommand extends Command
$progress = $io->createProgressBar();
$progress->setFormat(ProgressBar::FORMAT_DEBUG);
if (!$input->getOption('check-only')) { // Beginning transaction for all changes
if (!$input->getOption('dry-run')) { // Beginning transaction for all changes
$this->em->beginTransaction();
}
@ -92,7 +92,7 @@ class UpdateSubscriptionsCommand extends Command
$progress->finish();
// Flushing all changes at once to the database
if (!$input->getOption('check-only')) {
if (!$input->getOption('dry-run')) {
$this->em->flush();
$this->em->commit();
}

View file

@ -6,8 +6,8 @@ namespace App\Controller\Api;
use App\DTO\Api\PostsPage;
use App\Factory\Blog\PostFactory;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\{Request, Response};
use Symfony\Component\Serializer\SerializerInterface;
class CrawlerController extends AbstractApiController
{

View file

@ -3,41 +3,18 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** TODO: Refactor to public readonly */
use Symfony\Component\Serializer\Annotation\SerializedName;
class Auth implements ValidableInterface
{
private ?string $token;
private ?string $csRfToken;
private ?string $error;
public function getToken(): ?string
{
return $this->token;
}
public function setToken(?string $token): void
{
$this->token = $token;
}
public function getCsRfToken(): ?string
{
return $this->csRfToken;
}
public function setCsRfToken(?string $csRfToken): void
{
$this->csRfToken = $csRfToken;
}
public function getError(): ?string
{
return $this->error;
}
public function setError(?string $error): void
{
$this->error = $error;
public function __construct(
#[SerializedName('token')]
public readonly ?string $token,
#[SerializedName('error')]
public readonly ?string $error,
#[SerializedName('csrf_token')]
public readonly ?string $csRfToken,
) {
}
public function isValid(): bool

View file

@ -3,91 +3,26 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** TODO: Refactor to public readonly */
use Symfony\Component\Serializer\Annotation\SerializedName;
class Comment implements ValidableInterface
{
private ?string $postId;
private ?int $number;
private ?int $toCommentId;
private ?string $created;
private ?string $text;
private ?User $author;
private ?bool $isRec;
public function getPostId(): ?string
{
return $this->postId;
}
public function setPostId(?string $postId): void
{
$this->postId = $postId;
}
public function getNumber(): ?int
{
return $this->number;
}
public function setNumber(?int $number): void
{
$this->number = $number;
}
public function getToCommentId(): ?int
{
return $this->toCommentId;
}
public function setToCommentId(?int $toCommentId): void
{
$this->toCommentId = $toCommentId;
}
public function getCreated(): string
{
return $this->created;
}
public function setCreated(?string $created): void
{
$this->created = $created;
}
public function getText(): ?string
{
return $this->text;
}
public function setText(?string $text): void
{
$this->text = $text;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(?User $author): void
{
$this->author = $author;
}
public function isIsRec(): ?bool
{
return $this->isRec;
}
public function getIsRec(): ?bool
{
return $this->isRec;
}
public function setIsRec(?bool $isRec): void
{
$this->isRec = $isRec;
public function __construct(
#[SerializedName('post_id')]
public readonly ?string $postId,
#[SerializedName('id')]
public readonly ?int $number,
#[SerializedName('to_comment_id')]
public readonly ?int $toCommentId,
#[SerializedName('created')]
public readonly ?\DateTimeImmutable $created,
#[SerializedName('text')]
public readonly ?string $text,
#[SerializedName('author')]
public readonly ?User $author,
#[SerializedName('is_rec')]
public readonly ?bool $isRec,
) {
}
public function isValid(): bool

View file

@ -3,34 +3,21 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** TODO: Refactor to public readonly */
use Symfony\Component\Serializer\Annotation\{MaxDepth, SerializedName};
class MetaPost implements ValidableInterface
{
private ?Post $post;
/** @var Comment[]|null */
private ?array $comments;
public function getPost(): ?Post
{
return $this->post;
}
public function setPost(?Post $post): void
{
$this->post = $post;
}
/** @return Comment[]|null */
public function getComments(): ?array
{
return $this->comments;
}
/** @param Comment[]|null $comments */
public function setComments(?array $comments): void
{
$this->comments = $comments;
/**
* @param Comment[] $comments
*/
public function __construct(
#[SerializedName('post')]
#[MaxDepth(2)]
public readonly ?Post $post,
#[SerializedName('comments')]
#[MaxDepth(2)]
public readonly ?array $comments,
) {
}
public function isValid(): bool

View file

@ -3,108 +3,32 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** TODO: Refactor to public readonly */
use App\Enum\Blog\PostTypeEnum;
use Symfony\Component\Serializer\Annotation\{Context, MaxDepth, SerializedName};
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Post implements ValidableInterface
{
private ?string $id;
/** @var string[]|null */
private ?array $tags;
/** @var string[]|null */
private ?array $files;
private ?User $author;
private ?string $text;
private ?string $created;
private ?string $type;
private ?bool $private;
public function getId(): ?string
{
return $this->id;
}
public function setId(?string $id): void
{
$this->id = $id;
}
/** @return string[]|null */
public function getTags(): ?array
{
return $this->tags;
}
public function setTags(?array $tags): void
{
$this->tags = $tags;
}
/** @return string[]|null */
public function getFiles(): ?array
{
return $this->files;
}
/**
* @param string[]|null $files
*/
public function setFiles(?array $files): void
{
$this->files = $files;
}
public function getAuthor(): ?User
{
return $this->author;
}
public function setAuthor(?User $author): void
{
$this->author = $author;
}
public function getText(): ?string
{
return $this->text;
}
public function setText(?string $text): void
{
$this->text = $text;
}
public function getCreated(): ?string
{
return $this->created;
}
public function setCreated(?string $created): void
{
$this->created = $created;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(?string $type): void
{
$this->type = $type;
}
public function getPrivate(): ?bool
{
return $this->private;
}
public function isPrivate(): ?bool
{
return $this->private;
}
public function setPrivate(?bool $private): void
{
$this->private = $private;
public function __construct(
#[SerializedName('id')]
public readonly ?string $id,
#[SerializedName('tags')]
public readonly ?array $tags,
#[SerializedName('files')]
public readonly ?array $files,
#[SerializedName('author')]
#[MaxDepth(1)]
public readonly ?User $author,
#[SerializedName('text')]
public readonly ?string $text,
#[SerializedName('created')]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d_H:i:s'])]
public readonly ?\DateTimeImmutable $created,
#[SerializedName('type')]
public readonly ?PostTypeEnum $type,
#[SerializedName('private')]
public readonly ?bool $private,
) {
}
public function isValid(): bool

View file

@ -3,33 +3,15 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** TODO: Refactor to public readonly */
class PostsPage implements ValidableInterface
{
/** @var MetaPost[]|null */
private ?array $posts;
private ?bool $hasNext;
/** @return MetaPost[]|null */
public function getPosts(): ?array
{
return $this->posts;
}
/** @param MetaPost[]|null $posts */
public function setPosts(?array $posts): void
{
$this->posts = $posts;
}
public function getHasNext(): ?bool
{
return $this->hasNext;
}
public function setHasNext(?bool $hasNext): void
{
$this->hasNext = $hasNext;
/**
* @param MetaPost[]|null $posts
*/
public function __construct(
public readonly ?array $posts,
public readonly ?bool $hasNext,
) {
}
public function isValid(): bool

View file

@ -3,166 +3,57 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** TODO: Refactor to public readonly */
use Symfony\Component\Serializer\Annotation\{Context, Groups, SerializedName};
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class User implements ValidableInterface
{
private ?string $id;
private ?string $login;
private ?string $name;
private ?string $about;
private ?string $xmpp;
private ?string $created;
private ?bool $gender;
private ?bool $denyAnonymous;
private ?bool $private;
private ?string $birthDate;
private ?string $homepage;
private ?string $email;
private ?string $location;
private ?string $error;
public function getId(): ?string
{
return $this->id;
}
public function setId(?string $id): void
{
$this->id = $id;
}
public function getLogin(): ?string
{
return $this->login;
}
public function setLogin(?string $login): void
{
$this->login = $login;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(?string $name): void
{
$this->name = $name;
}
public function getAbout(): ?string
{
return $this->about;
}
public function setAbout(?string $about): void
{
$this->about = $about;
}
public function getXmpp(): ?string
{
return $this->xmpp;
}
public function setXmpp(?string $xmpp): void
{
$this->xmpp = $xmpp;
}
public function getCreated(): ?string
{
return $this->created;
}
public function setCreated(?string $created): void
{
$this->created = $created;
}
public function getGender(): ?bool
{
return $this->gender;
}
public function setGender(?bool $gender): void
{
$this->gender = $gender;
}
public function getDenyAnonymous(): ?bool
{
return $this->denyAnonymous;
}
public function setDenyAnonymous(?bool $denyAnonymous): void
{
$this->denyAnonymous = $denyAnonymous;
}
public function getPrivate(): ?bool
{
return $this->private;
}
public function setPrivate(?bool $private): void
{
$this->private = $private;
}
public function getBirthDate(): ?string
{
return $this->birthDate;
}
public function setBirthDate(?string $birthDate): void
{
$this->birthDate = $birthDate;
}
public function getHomepage(): ?string
{
return $this->homepage;
}
public function setHomepage(?string $homepage): void
{
$this->homepage = $homepage;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): void
{
$this->email = $email;
}
public function getLocation(): ?string
{
return $this->location;
}
public function setLocation(?string $location): void
{
$this->location = $location;
}
public function getError(): ?string
{
return $this->error;
}
public function setError(?string $error): void
{
$this->error = $error;
public function __construct(
#[SerializedName('id')]
#[Groups(['user_short', 'user_full'])]
public readonly ?int $id,
#[SerializedName('login')]
#[Groups(['user_short', 'user_full'])]
public readonly ?string $login,
#[SerializedName('name')]
#[Groups(['user_short', 'user_full'])]
public readonly ?string $name,
#[SerializedName('about')]
#[Groups(['user_full'])]
public readonly ?string $about,
#[SerializedName('xmpp')]
#[Groups(['user_full'])]
public readonly ?string $xmpp,
#[SerializedName('created')]
#[Groups(['user_full'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d_H:i:s'])]
public readonly ?\DateTimeImmutable $created,
#[SerializedName('gender')]
#[Groups(['user_full'])]
public readonly ?bool $gender,
#[SerializedName('deny_anonymous')]
#[Groups(['user_full'])]
public readonly ?bool $denyAnonymous,
#[SerializedName('private')]
#[Groups(['user_full'])]
public readonly ?bool $private,
#[SerializedName('birthdate')]
#[Groups(['user_full'])]
public readonly ?string $birthDate,
#[SerializedName('homepage')]
#[Groups(['user_full'])]
public readonly ?string $homepage,
#[SerializedName('email')]
#[Groups(['user_full'])]
public readonly ?string $email,
#[SerializedName('location')]
#[Groups(['user_full'])]
public readonly ?string $location,
) {
}
public function isValid(): bool
{
return null === $this->error && null !== $this->id && null !== $this->login;
return null !== $this->id && null !== $this->login;
}
}

View file

@ -3,7 +3,9 @@ declare(strict_types=1);
namespace App\DTO\Api;
/** @deprecated Use Symfony Validator instead */
interface ValidableInterface
{
/** @deprecated Use Symfony Validator instead */
public function isValid(): bool;
}

View file

@ -29,7 +29,7 @@ class LoadUserData extends Fixture implements OrderedFixtureInterface
public function load(ObjectManager $om)
{
foreach ($this->users as $userData) {
$user = new User($userData['id'], new \DateTime(), $userData['login'], $userData['name']);
$user = new User($userData['id'], $userData['login'], new \DateTime(), $userData['name']);
$user->updatePrivacy(!$userData['private'], $userData['whitelist-only']);
$om->persist($user);

View file

@ -22,7 +22,7 @@ class Post
private string $text;
#[ORM\Column(name: 'created_at', type: 'datetime')]
private \DateTime $createdAt;
private \DateTimeImmutable $createdAt;
#[ORM\Column(name: 'updated_at', type: 'datetime', nullable: true)]
private ?\DateTime $updatedAt;
@ -56,7 +56,7 @@ class Post
private Collection $comments;
public function __construct(string $id, User $author, \DateTime $createdAt, PostTypeEnum $type)
public function __construct(string $id, User $author, \DateTimeImmutable $createdAt, PostTypeEnum $type)
{
$this->id = $id;
$this->author = $author;
@ -91,7 +91,7 @@ class Post
return $this->text;
}
public function getCreatedAt(): \DateTime
public function getCreatedAt(): \DateTimeImmutable
{
return $this->createdAt;
}

View file

@ -54,8 +54,8 @@ class User
public function __construct(
int $id,
string $login,
\DateTime $createdAt = null,
string $login = null,
string $name = null
) {
$this->id = $id;

View file

@ -8,7 +8,7 @@ use Psr\Log\LoggerInterface;
abstract class AbstractFactory
{
public function __construct(
protected LoggerInterface $logger,
protected readonly LoggerInterface $logger,
) {
}
}

View file

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Factory\Blog;
use App\Factory\{AbstractFactory};
use App\Factory\UserFactory;
use Psr\Log\LoggerInterface;
use App\Repository\Blog\{CommentRepository, PostRepository};
class CommentFactory extends AbstractFactory
{
public function __construct(
LoggerInterface $logger,
private readonly CommentRepository $commentRepository,
private readonly PostRepository $postRepository,
private readonly UserFactory $userFactory,
) {
parent::__construct($logger);
}
}

View file

@ -4,8 +4,7 @@ declare(strict_types=1);
namespace App\Factory\Blog;
use App\Enum\Blog\PostTypeEnum;
use App\Factory\AbstractFactory;
use App\Factory\UserFactory;
use App\Factory\{AbstractFactory, UserFactory};
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use App\DTO\Api\{MetaPost, PostsPage, Post as PostDTO};
@ -23,7 +22,6 @@ class PostFactory extends AbstractFactory
private readonly PostRepository $postRepository,
private readonly UserFactory $userFactory,
private readonly FileFactory $fileFactory,
private readonly CommentFactory $commentFactory,
private readonly TagFactory $tagFactory,
) {
parent::__construct($logger);
@ -40,9 +38,10 @@ class PostFactory extends AbstractFactory
$hasNew = false;
foreach ((array) $page->getPosts() as $postData) {
/** @var MetaPost $postData */
foreach ($page->posts ?? [] as $postData) {
try {
if (null === $this->postRepository->find($postData->getPost()->getId())) {
if (null === $this->postRepository->find($postData->post->id)) {
$hasNew = true;
}
@ -50,7 +49,7 @@ class PostFactory extends AbstractFactory
$posts[] = $post;
} catch (\Exception $e) {
$this->logger->error('Error while processing post DTO', [
'post_id' => $postData->getPost()->getId(),
'post_id' => $postData->post->id,
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
@ -82,10 +81,10 @@ class PostFactory extends AbstractFactory
throw new InvalidDataException('Invalid post data');
}
$postData = $metaPost->getPost();
$postData = $metaPost->post;
try {
$author = $this->userFactory->findOrCreateFromDTO($metaPost->getPost()->getAuthor());
$author = $this->userFactory->findOrCreateFromDTO($postData->author);
} catch (\Exception $e) {
$this->logger->error('Error while creating user from DTO');
throw $e;
@ -94,14 +93,14 @@ class PostFactory extends AbstractFactory
$post = $this->findOrCreateFromDto($postData, $author);
try {
$this->updatePostTags($post, $postData->getTags() ?: []);
$this->updatePostTags($post, $postData->tags ?? []);
} catch (\Exception $e) {
$this->logger->error('Error while updating post tags');
throw $e;
}
try {
$this->updatePostFiles($post, $postData->getFiles() ?: []);
$this->updatePostFiles($post, $postData->files ?? []);
} catch (\Exception $e) {
$this->logger->error('Error while updating post files');
throw $e;
@ -112,20 +111,19 @@ class PostFactory extends AbstractFactory
private function findOrCreateFromDto(PostDTO $postData, User $author): Post
{
if (null === ($post = $this->postRepository->find($postData->getId()))) {
// Creating new post
if (null === ($post = $this->postRepository->find($postData->id))) {
$post = new Post(
$postData->getId(),
$postData->id,
$author,
new \DateTime($postData->getCreated()),
PostTypeEnum::tryFrom($postData->getType()) ?? PostTypeEnum::Post,
$postData->created,
$postData->type ?? PostTypeEnum::Post,
);
$this->postRepository->save($post);
}
$post
->setText($postData->getText())
->setPrivate($postData->getPrivate())
->setText($postData->text)
->setPrivate($postData->private)
;
return $post;

View file

@ -29,18 +29,19 @@ class UserFactory extends AbstractFactory
}
/** @var User $user */
if (null === ($user = $this->userRepository->find($userData->getId()))) {
if (null === ($user = $this->userRepository->find($userData->id))) {
$user = new User(
(int) $userData->getId(),
\DateTime::createFromFormat('Y-m-d_H:i:s', $userData->getCreated()) ?: new \DateTime()
$userData->id,
$userData->login,
$userData->created,
);
$this->userRepository->add($user);
$this->userRepository->save($user);
}
$user->updateLoginAndName($userData->getLogin(), $userData->getName());
$user->updateLoginAndName($userData->login, $userData->name);
if (null !== $userData->getDenyAnonymous() && null !== $userData->getPrivate()) {
$user->updatePrivacy(!$userData->getDenyAnonymous(), $userData->getPrivate());
if (null !== $userData->denyAnonymous && null !== $userData->private) {
$user->updatePrivacy(!$userData->denyAnonymous, $userData->private);
}
return $user;

View file

@ -9,14 +9,14 @@ use App\Exception\Api\{ApiException,
NotFoundException,
UnauthorizedException,
ServerProblemException};
use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
use Symfony\Component\Serializer\Context\Normalizer\ObjectNormalizerContextBuilder;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\{HttpClientInterface, ResponseInterface};
class AbstractApi
abstract class AbstractApi
{
protected HttpClientInterface $client;
// TODO: check if these are still needed
@ -32,24 +32,24 @@ class AbstractApi
}
/** Make GET request and return DTO objects */
public function getGetJsonData(string $path, array $parameters = [], string $type, DeserializationContext $context = null): array|object|null
public function getGetJsonData(string $path, array $parameters = [], string $type, array $groups = []): array|object|null
{
return $this->serializer->deserialize(
$this->getGetResponseBody($path, $parameters),
$type,
'json',
$context
$this->createContext($groups),
);
}
/** Make POST request and return DTO objects */
public function getPostJsonData(string $path, array $parameters = [], string $type, DeserializationContext $context = null): array|object|null
public function getPostJsonData(string $path, array $parameters = [], string $type, array $groups = []): array|object|null
{
return $this->serializer->deserialize(
$this->getPostResponseBody($path, $parameters),
$type,
'json',
$context
$this->createContext($groups)
);
}
@ -65,6 +65,13 @@ class AbstractApi
return $this->sendPostRequest($path, $parameters)->getContent();
}
private function createContext(array $groups): array
{
return (new ObjectNormalizerContextBuilder())
->withGroups($groups)
->toArray();
}
private function sendGetRequest(string $path, array $parameters = []): ResponseInterface
{
$this->logger->debug('Sending GET request', ['path' => $path, 'parameters' => $parameters]);
@ -103,7 +110,7 @@ class AbstractApi
// Temporary fix until @arts fixes this bug
if ('{"error": "UserNotFound"}' === $response->getContent()) {
throw new NotFoundException('Not found', SymfonyResponse::HTTP_NOT_FOUND);
} elseif ('{"message": "Forbidden", "code": 403, "error": "Forbidden"}' === (string) $response->getBody()) {
} elseif ('{"message": "Forbidden", "code": 403, "error": "Forbidden"}' === (string) $response->getContent()) {
throw new ForbiddenException('Forbidden', SymfonyResponse::HTTP_FORBIDDEN);
}

View file

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Service\Api;
use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface;
use App\Factory\Blog\PostFactory;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/** Basic Point.im user API functions from /api/post */
class PostApi extends AbstractApi
{
public function __construct(
HttpClientInterface $pointApiClient,
SerializerInterface $serializer,
LoggerInterface $logger,
private readonly PostFactory $postFactory,
) {
parent::__construct($pointApiClient, $logger, $serializer);
}
}

View file

@ -11,8 +11,8 @@ use App\Exception\Api\{ForbiddenException,
UserNotFoundException
};
use App\Factory\UserFactory;
use JMS\Serializer\{DeserializationContext, SerializerInterface};
use Psr\Log\LoggerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/** Basic Point.im user API functions from /api/user/* */
@ -89,8 +89,8 @@ class UserApi extends AbstractApi
$usersList = $this->getGetJsonData(
self::PREFIX.urlencode($login).'/subscribers',
[],
'array<'.UserDTO::class.'>',
DeserializationContext::create()->setGroups(['user_short']),
UserDTO::class.'[]',
['user_short'],
);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, null, $login);
@ -108,8 +108,8 @@ class UserApi extends AbstractApi
$usersList = $this->getGetJsonData(
self::PREFIX.'id/'.$id.'/subscribers',
[],
'array<'.UserDTO::class.'>',
DeserializationContext::create()->setGroups(['user_short']),
UserDTO::class.'[]',
['user_short'],
);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, $id);
@ -128,7 +128,7 @@ class UserApi extends AbstractApi
self::PREFIX.'login/'.urlencode($login),
[],
UserDTO::class,
DeserializationContext::create()->setGroups(['user_full']),
['user_full'],
);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, null, $login);
@ -147,7 +147,7 @@ class UserApi extends AbstractApi
self::PREFIX.'id/'.$id,
[],
UserDTO::class,
DeserializationContext::create()->setGroups(['user_full']),
['user_full'],
);
} catch (NotFoundException $e) {
throw new UserNotFoundException('User not found', 0, $e, $id);

View file

@ -47,18 +47,6 @@
"migrations/.gitignore"
]
},
"jms/serializer-bundle": {
"version": "5.2",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "4.0",
"ref": "cc04e10cf7171525b50c18b36004edf64cb478be"
},
"files": [
"config/packages/jms_serializer.yaml"
]
},
"knplabs/knp-paginator-bundle": {
"version": "v6.2.0"
},