diff --git a/README.md b/README.md index 362f2960e4d28895dde5c1c00ddbf88f7ef11f76..a6a08a1cd2f29b18fecba2ba8731681e8a9c34be 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ ## Инструкция +## Воркер отправки Email квестов + +Запустить команду - `bin/console messenger:consume -v scheduler_quests` + ## Kafka
Инструкция diff --git a/app/composer.json b/app/composer.json index 804133de31d0d2376b8fa6c92f68a5c9d260c9b2..7020df0f0724267b600d0f8707996b6bc825d63a 100644 --- a/app/composer.json +++ b/app/composer.json @@ -14,6 +14,8 @@ "doctrine/orm": "^3.2", "lexik/jwt-authentication-bundle": "^3.0", "nelmio/api-doc-bundle": "^4.27", + "nelmio/cors-bundle": "^2.5", + "predis/predis": "^2.2", "symfony/asset": "7.0.*", "symfony/cache": "7.0.*", "symfony/console": "7.0.*", @@ -25,6 +27,7 @@ "symfony/messenger": "7.0.*", "symfony/mime": "7.0.*", "symfony/runtime": "7.0.*", + "symfony/scheduler": "7.0.*", "symfony/security-bundle": "7.0.*", "symfony/serializer": "7.0.*", "symfony/twig-bundle": "7.0.*", diff --git a/app/composer.lock b/app/composer.lock index df4798038f84eca925af6362f3e88f58e2452ed3..e212146e5c8118a70b540ce4c75637b9f37bbc8a 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -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": "a47e42ecd6c25174dcfe545065022162", + "content-hash": "54d204742929222e6a66061365bec4eb", "packages": [ { "name": "doctrine/cache", @@ -1664,6 +1664,68 @@ }, "time": "2024-06-12T23:47:19+00:00" }, + { + "name": "nelmio/cors-bundle", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/nelmio/NelmioCorsBundle.git", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nelmio/NelmioCorsBundle/zipball/3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "reference": "3a526fe025cd20e04a6a11370cf5ab28dbb5a544", + "shasum": "" + }, + "require": { + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.6", + "symfony/phpunit-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Nelmio\\CorsBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nelmio", + "homepage": "http://nelm.io" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/nelmio/NelmioCorsBundle/contributors" + } + ], + "description": "Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application", + "keywords": [ + "api", + "cors", + "crossdomain" + ], + "support": { + "issues": "https://github.com/nelmio/NelmioCorsBundle/issues", + "source": "https://github.com/nelmio/NelmioCorsBundle/tree/2.5.0" + }, + "time": "2024-06-24T21:25:28+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1886,6 +1948,67 @@ }, "time": "2024-05-31T08:52:43+00:00" }, + { + "name": "predis/predis", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ~9.4.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v2.2.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2023-09-13T16:42:03+00:00" + }, { "name": "psr/cache", "version": "3.0.0", @@ -4816,6 +4939,86 @@ ], "time": "2024-04-18T09:29:19+00:00" }, + { + "name": "symfony/scheduler", + "version": "v7.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/scheduler.git", + "reference": "91a0c028f2183b111e92e32061bb9db9a9599133" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/scheduler/zipball/91a0c028f2183b111e92e32061bb9db9a9599133", + "reference": "91a0c028f2183b111e92e32061bb9db9a9599133", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0" + }, + "require-dev": { + "dragonmantank/cron-expression": "^3.1", + "symfony/cache": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Scheduler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sergey Rabochiy", + "email": "upyx.00@gmail.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides scheduling through Symfony Messenger", + "homepage": "https://symfony.com", + "keywords": [ + "cron", + "schedule", + "scheduler" + ], + "support": { + "source": "https://github.com/symfony/scheduler/tree/v7.0.8" + }, + "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": "2024-06-02T15:49:03+00:00" + }, { "name": "symfony/security-bundle", "version": "v7.0.8", diff --git a/app/config/bundles.php b/app/config/bundles.php index 386a29b1da7b000598112d980c463d18ef32bcfe..facf965f4101e598e0514bd9d9c077d78f6c72a2 100644 --- a/app/config/bundles.php +++ b/app/config/bundles.php @@ -10,4 +10,5 @@ return [ Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], ]; diff --git a/app/config/packages/cache.yaml b/app/config/packages/cache.yaml index 6899b72003fca67f5a56b945cd3e07f5c8a33774..ddb1ec38c4474dab9b4fd6ad4111c5e952ea859c 100644 --- a/app/config/packages/cache.yaml +++ b/app/config/packages/cache.yaml @@ -8,8 +8,10 @@ framework: # Other options include: # Redis - #app: cache.adapter.redis - #default_redis_provider: redis://localhost + default_redis_provider: 'redis://redis' + pools: + custom_cache_pool: + adapter: cache.adapter.redis # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) #app: cache.adapter.apcu diff --git a/app/config/packages/doctrine.yaml b/app/config/packages/doctrine.yaml index d42c52d6d2573bc94f3423d8f6d63a8eac3b61d8..3c43cba7220ae959751ad5bf27a8bb62db46053c 100644 --- a/app/config/packages/doctrine.yaml +++ b/app/config/packages/doctrine.yaml @@ -22,6 +22,9 @@ doctrine: dir: '%kernel.project_dir%/src/Entity' prefix: 'App\Entity' alias: App + result_cache_driver: + type: pool + pool: custom_cache_pool when@test: doctrine: diff --git a/app/config/packages/messenger.yaml b/app/config/packages/messenger.yaml index 6dc7da12750522c0d51bb45032c65c70d8f51bca..bf8155995e5d4207c7a728ce63f471eda6bbb02d 100644 --- a/app/config/packages/messenger.yaml +++ b/app/config/packages/messenger.yaml @@ -4,6 +4,8 @@ framework: # failure_transport: failed transports: + sync: 'sync://' + send_transport: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' options: @@ -26,6 +28,7 @@ framework: routing: 'App\Messenger\Message\SendMessage': send_transport + 'App\Messenger\Message\QuestMessage': send_transport # Route your messages to the transports # 'App\Message\YourMessage': async diff --git a/app/config/packages/nelmio_cors.yaml b/app/config/packages/nelmio_cors.yaml new file mode 100644 index 0000000000000000000000000000000000000000..fa676773376376ea28d1a6bdceddbad0a4236b11 --- /dev/null +++ b/app/config/packages/nelmio_cors.yaml @@ -0,0 +1,10 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] + allow_methods: ['GET', 'POST'] + allow_headers: ['*'] + expose_headers: ['Link'] + max_age: 3600 + paths: + '^/api/': ~ diff --git a/app/config/services.yaml b/app/config/services.yaml index 02884eb17e3bae4741b0165e463a6f60db3e9f74..39dc3d24820c87fd5339aad0f967809e5dc4456f 100644 --- a/app/config/services.yaml +++ b/app/config/services.yaml @@ -36,6 +36,14 @@ services: $confirmType: '%confirm_type%' $fromEmail: '%from_email%' + App\Messenger\Handler\QuestEndMessageHandler: + arguments: + $fromEmail: '%from_email%' + + App\Messenger\Handler\QuestStartMessageHandler: + arguments: + $fromEmail: '%from_email%' + App\Listeners\KernelExceptionListener: tags: - { name: kernel.event_listener, event: kernel.exception } diff --git a/app/migrations/Version20240624095415.php b/app/migrations/Version20240624095415.php new file mode 100644 index 0000000000000000000000000000000000000000..030c3a231e62cb8b604c0a8340afc7e9d86132b1 --- /dev/null +++ b/app/migrations/Version20240624095415.php @@ -0,0 +1,81 @@ +addSql('CREATE SEQUENCE appointment_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE favorite_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "like_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE quest_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE quest_image_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE review_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE appointment (id INT NOT NULL, related_user_id INT NOT NULL, quest_id INT DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_FE38F84498771930 ON appointment (related_user_id)'); + $this->addSql('CREATE INDEX IDX_FE38F844209E9EF4 ON appointment (quest_id)'); + $this->addSql('CREATE TABLE favorite (id INT NOT NULL, related_user_id INT NOT NULL, quest_id INT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_68C58ED998771930 ON favorite (related_user_id)'); + $this->addSql('CREATE INDEX IDX_68C58ED9209E9EF4 ON favorite (quest_id)'); + $this->addSql('CREATE TABLE "like" (id INT NOT NULL, review_id INT NOT NULL, related_user_id INT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_AC6340B33E2E969B ON "like" (review_id)'); + $this->addSql('CREATE INDEX IDX_AC6340B398771930 ON "like" (related_user_id)'); + $this->addSql('CREATE TABLE quest (id INT NOT NULL, name VARCHAR(255) NOT NULL, short_description TEXT NOT NULL, full_description TEXT DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, final_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, max_appointments INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE quest_image (id INT NOT NULL, quest_id INT DEFAULT NULL, path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_96809DC3209E9EF4 ON quest_image (quest_id)'); + $this->addSql('CREATE TABLE review (id INT NOT NULL, related_user_id INT NOT NULL, quest_id INT NOT NULL, text TEXT DEFAULT NULL, rating INT NOT NULL, create_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, update_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_794381C698771930 ON review (related_user_id)'); + $this->addSql('CREATE INDEX IDX_794381C6209E9EF4 ON review (quest_id)'); + $this->addSql('ALTER TABLE appointment ADD CONSTRAINT FK_FE38F84498771930 FOREIGN KEY (related_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE appointment ADD CONSTRAINT FK_FE38F844209E9EF4 FOREIGN KEY (quest_id) REFERENCES quest (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE favorite ADD CONSTRAINT FK_68C58ED998771930 FOREIGN KEY (related_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE favorite ADD CONSTRAINT FK_68C58ED9209E9EF4 FOREIGN KEY (quest_id) REFERENCES quest (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "like" ADD CONSTRAINT FK_AC6340B33E2E969B FOREIGN KEY (review_id) REFERENCES review (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE "like" ADD CONSTRAINT FK_AC6340B398771930 FOREIGN KEY (related_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE quest_image ADD CONSTRAINT FK_96809DC3209E9EF4 FOREIGN KEY (quest_id) REFERENCES quest (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C698771930 FOREIGN KEY (related_user_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE review ADD CONSTRAINT FK_794381C6209E9EF4 FOREIGN KEY (quest_id) REFERENCES quest (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE appointment_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE favorite_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "like_id_seq" CASCADE'); + $this->addSql('DROP SEQUENCE quest_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE quest_image_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE review_id_seq CASCADE'); + $this->addSql('ALTER TABLE appointment DROP CONSTRAINT FK_FE38F84498771930'); + $this->addSql('ALTER TABLE appointment DROP CONSTRAINT FK_FE38F844209E9EF4'); + $this->addSql('ALTER TABLE favorite DROP CONSTRAINT FK_68C58ED998771930'); + $this->addSql('ALTER TABLE favorite DROP CONSTRAINT FK_68C58ED9209E9EF4'); + $this->addSql('ALTER TABLE "like" DROP CONSTRAINT FK_AC6340B33E2E969B'); + $this->addSql('ALTER TABLE "like" DROP CONSTRAINT FK_AC6340B398771930'); + $this->addSql('ALTER TABLE quest_image DROP CONSTRAINT FK_96809DC3209E9EF4'); + $this->addSql('ALTER TABLE review DROP CONSTRAINT FK_794381C698771930'); + $this->addSql('ALTER TABLE review DROP CONSTRAINT FK_794381C6209E9EF4'); + $this->addSql('DROP TABLE appointment'); + $this->addSql('DROP TABLE favorite'); + $this->addSql('DROP TABLE "like"'); + $this->addSql('DROP TABLE quest'); + $this->addSql('DROP TABLE quest_image'); + $this->addSql('DROP TABLE review'); + } +} diff --git a/app/migrations/Version20240624101121.php b/app/migrations/Version20240624101121.php new file mode 100644 index 0000000000000000000000000000000000000000..08ca8e64327bc0140176c448a72786caca340abb --- /dev/null +++ b/app/migrations/Version20240624101121.php @@ -0,0 +1,62 @@ +addSql('CREATE SEQUENCE genre_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE tag_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE theme_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE genre (id INT NOT NULL, name VARCHAR(255) NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE tag (id INT NOT NULL, name VARCHAR(255) NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE tag_quest (tag_id INT NOT NULL, quest_id INT NOT NULL, PRIMARY KEY(tag_id, quest_id))'); + $this->addSql('CREATE INDEX IDX_61FBE111BAD26311 ON tag_quest (tag_id)'); + $this->addSql('CREATE INDEX IDX_61FBE111209E9EF4 ON tag_quest (quest_id)'); + $this->addSql('CREATE TABLE theme (id INT NOT NULL, name VARCHAR(255) NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('ALTER TABLE tag_quest ADD CONSTRAINT FK_61FBE111BAD26311 FOREIGN KEY (tag_id) REFERENCES tag (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE tag_quest ADD CONSTRAINT FK_61FBE111209E9EF4 FOREIGN KEY (quest_id) REFERENCES quest (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE quest ADD theme_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE quest ADD genre_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE quest ADD CONSTRAINT FK_4317F81759027487 FOREIGN KEY (theme_id) REFERENCES theme (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE quest ADD CONSTRAINT FK_4317F8174296D31F FOREIGN KEY (genre_id) REFERENCES genre (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_4317F81759027487 ON quest (theme_id)'); + $this->addSql('CREATE INDEX IDX_4317F8174296D31F ON quest (genre_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE quest DROP CONSTRAINT FK_4317F8174296D31F'); + $this->addSql('ALTER TABLE quest DROP CONSTRAINT FK_4317F81759027487'); + $this->addSql('DROP SEQUENCE genre_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE tag_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE theme_id_seq CASCADE'); + $this->addSql('ALTER TABLE tag_quest DROP CONSTRAINT FK_61FBE111BAD26311'); + $this->addSql('ALTER TABLE tag_quest DROP CONSTRAINT FK_61FBE111209E9EF4'); + $this->addSql('DROP TABLE genre'); + $this->addSql('DROP TABLE tag'); + $this->addSql('DROP TABLE tag_quest'); + $this->addSql('DROP TABLE theme'); + $this->addSql('DROP INDEX IDX_4317F81759027487'); + $this->addSql('DROP INDEX IDX_4317F8174296D31F'); + $this->addSql('ALTER TABLE quest DROP theme_id'); + $this->addSql('ALTER TABLE quest DROP genre_id'); + } +} diff --git a/app/src/Controller/ProfileController.php b/app/src/Controller/ProfileController.php index 2f9448814b83803201199e561c3edde09a81374d..8386604cb5b28c55808e4d07f42585ad46d885b6 100644 --- a/app/src/Controller/ProfileController.php +++ b/app/src/Controller/ProfileController.php @@ -7,8 +7,11 @@ use App\Service\Dto\Classes\ChangeProfileDto; use App\Service\Dto\Classes\ImageDto; use App\Service\Dto\Classes\RecoveryCodeDto; use App\Service\Dto\Classes\RecoveryDto; +use App\Service\Response\Classes\FavoritesResponse; use App\Service\Response\Classes\ProfileResponse; +use App\Service\Response\Classes\QuestsResponse; use App\Service\Response\Classes\Response; +use App\Service\Response\Classes\ReviewsResponse; use Nelmio\ApiDocBundle\Annotation\Model; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -36,6 +39,54 @@ class ProfileController extends AbstractController return $actionService->getResponse(); } + #[Route('/profile/favorites', name: 'favorites', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: FavoritesResponse::class, groups: ["message", "data", "card"]) + ) + )] + public function favorites( + #[Autowire(service: 'action.favorites')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/profile/reviews', name: 'reviews', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: ReviewsResponse::class, groups: ["message", "data", "card"]) + ) + )] + public function reviews( + #[Autowire(service: 'action.reviews')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/profile/quests', name: 'profile_quests', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: QuestsResponse::class, groups: ["message", "data", "card"]) + ) + )] + public function quests( + #[Autowire(service: 'action.profile.quests')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + #[Route('/profile/delete', name: 'profile_delete', methods: ['GET'])] #[OA\Response( response: 200, diff --git a/app/src/Controller/QuestController.php b/app/src/Controller/QuestController.php new file mode 100644 index 0000000000000000000000000000000000000000..08593cb3150f88eef19ebf6d1778033a0b690615 --- /dev/null +++ b/app/src/Controller/QuestController.php @@ -0,0 +1,247 @@ +getResponse(); + } + + #[Route('/quest', name: 'quest', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: IdDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: QuestResponse::class, groups: ["message", "data", "detail"]) + ) + )] + public function quest( + #[Autowire(service: 'action.quest')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/filter/set', name: 'filter_set', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: FilterDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: QuestsResponse::class, groups: ["message", "data", "card"]) + ) + )] + public function setFilter( + #[Autowire(service: 'action.filter.set')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/filter/get', name: 'filter_get', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: FilterResponse::class, groups: ["message", "data"]) + ) + )] + public function getFilter( + #[Autowire(service: 'action.filter.get')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/filter/params', name: 'filter_params', methods: ['GET'])] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: FilterParamsResponse::class, groups: ["message", "data", "filter"]) + ) + )] + public function getFilterParams( + #[Autowire(service: 'action.filter.params')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/subscribe', name: 'quest_subscribe', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: IdDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function subscribe( + #[Autowire(service: 'action.quest.subscribe')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/unsubscribe', name: 'quest_unsubscribe', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: IdDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function unsubscribe( + #[Autowire(service: 'action.quest.unsubscribe')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/review/create', name: 'quest_review_create', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: CreateReviewDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function createReview( + #[Autowire(service: 'action.quest.review.create')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/review/update', name: 'quest_review_update', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: UpdateReviewDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function updateReview( + #[Autowire(service: 'action.quest.review.update')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/review/delete', name: 'quest_review_delete', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: IdDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function deleteReview( + #[Autowire(service: 'action.quest.review.delete')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/review/like', name: 'quest_review_like', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: IdDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function likeReview( + #[Autowire(service: 'action.quest.review.like')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } + + #[Route('/quest/review/unlike', name: 'quest_review_unlike', methods: ['POST'])] + #[OA\RequestBody( + content: new OA\JsonContent(ref: new Model(type: IdDto::class)) + )] + #[OA\Response( + response: 200, + description: 'Ответ', + content: new OA\JsonContent( + ref: new Model(type: Response::class, groups: ["message"]) + ) + )] + public function unlikeReview( + #[Autowire(service: 'action.quest.review.unlike')] + ActionServiceInterface $actionService + ): JsonResponse + { + return $actionService->getResponse(); + } +} \ No newline at end of file diff --git a/app/src/Entity/Appointment.php b/app/src/Entity/Appointment.php new file mode 100644 index 0000000000000000000000000000000000000000..77eaeefb3b7279ab0462c92314af52f577257df8 --- /dev/null +++ b/app/src/Entity/Appointment.php @@ -0,0 +1,70 @@ +id; + } + + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getQuest(): ?Quest + { + return $this->quest; + } + + public function setQuest(?Quest $quest): static + { + $this->quest = $quest; + + return $this; + } + + #[Groups(['all', 'profile'])] + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } +} diff --git a/app/src/Entity/Favorite.php b/app/src/Entity/Favorite.php new file mode 100644 index 0000000000000000000000000000000000000000..16d9e4de58ed268375f125d0e1d41ccf46c4691a --- /dev/null +++ b/app/src/Entity/Favorite.php @@ -0,0 +1,71 @@ +id; + } + + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } + + #[Groups(['all', 'card'])] + public function getQuest(): ?Quest + { + return $this->quest; + } + + public function setQuest(?Quest $quest): static + { + $this->quest = $quest; + + return $this; + } + + #[Groups(['all', 'card'])] + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } +} diff --git a/app/src/Entity/Genre.php b/app/src/Entity/Genre.php new file mode 100644 index 0000000000000000000000000000000000000000..efc906337330fd5a2b2b470944f4e5e8e615b33f --- /dev/null +++ b/app/src/Entity/Genre.php @@ -0,0 +1,53 @@ +id; + } + + #[Groups(['all', 'card', 'detail', 'filter'])] + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } +} diff --git a/app/src/Entity/Like.php b/app/src/Entity/Like.php new file mode 100644 index 0000000000000000000000000000000000000000..ec6a2be82b71925b2e246f77c41f9ce3a517564b --- /dev/null +++ b/app/src/Entity/Like.php @@ -0,0 +1,72 @@ +id; + } + + public function getReview(): ?Review + { + return $this->review; + } + + public function setReview(?Review $review): static + { + $this->review = $review; + + return $this; + } + + #[Groups(['all', 'detail', 'review', 'card'])] + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } + + #[Groups(['all', 'detail', 'review', 'card'])] + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } +} diff --git a/app/src/Entity/Quest.php b/app/src/Entity/Quest.php new file mode 100644 index 0000000000000000000000000000000000000000..cbe969605c980fc88756f6c0c42b0e65c497158c --- /dev/null +++ b/app/src/Entity/Quest.php @@ -0,0 +1,328 @@ + + */ + #[ORM\OneToMany(targetEntity: QuestImage::class, mappedBy: 'quest')] + private Collection $gallery; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Appointment::class, mappedBy: 'quest')] + private Collection $appointments; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'quest')] + private Collection $reviews; + + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Tag::class, mappedBy: 'quests')] + private Collection $tags; + + #[ORM\ManyToOne] + private ?Theme $theme = null; + + #[ORM\ManyToOne] + private ?Genre $genre = null; + + public function __construct() + { + $this->gallery = new ArrayCollection(); + $this->appointments = new ArrayCollection(); + $this->reviews = new ArrayCollection(); + $this->tags = new ArrayCollection(); + } + + #[Groups(['all', 'card', 'detail', 'profile'])] + public function getId(): ?int + { + return $this->id; + } + + #[Groups(['all', 'card', 'detail', 'profile'])] + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + #[Groups(['all', 'card', 'profile'])] + public function getShortDescription(): ?string + { + return $this->short_description; + } + + public function setShortDescription(string $short_description): static + { + $this->short_description = $short_description; + + return $this; + } + + #[Groups(['all', 'detail'])] + public function getFullDescription(): ?string + { + return $this->full_description; + } + + public function setFullDescription(?string $full_description): static + { + $this->full_description = $full_description; + + return $this; + } + + #[Groups(['all', 'card', 'profile'])] + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } + + #[Groups(['all', 'detail'])] + public function getFinalDate(): ?\DateTimeInterface + { + return $this->final_date; + } + + public function setFinalDate(\DateTimeInterface $final_date): static + { + $this->final_date = $final_date; + + return $this; + } + + #[Groups(['all', 'detail'])] + public function getMaxAppointments(): ?int + { + return $this->max_appointments; + } + + public function setMaxAppointments(?int $max_appointments): static + { + $this->max_appointments = $max_appointments; + + return $this; + } + + /** + * @return Collection + */ + #[Groups(['all', 'card', 'detail', 'profile'])] + public function getGallery(): Collection + { + return $this->gallery; + } + + public function addGallery(QuestImage $gallery): static + { + if (!$this->gallery->contains($gallery)) { + $this->gallery->add($gallery); + $gallery->setQuest($this); + } + + return $this; + } + + public function removeGallery(QuestImage $gallery): static + { + if ($this->gallery->removeElement($gallery)) { + // set the owning side to null (unless already changed) + if ($gallery->getQuest() === $this) { + $gallery->setQuest(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + #[Groups(['all'])] + public function getAppointments(): Collection + { + return $this->appointments; + } + + public function addAppointment(Appointment $appointment): static + { + if (!$this->appointments->contains($appointment)) { + $this->appointments->add($appointment); + $appointment->setQuest($this); + } + + return $this; + } + + #[Groups(['all', 'detail'])] + public function getAppointmentCount(): int + { + return count($this->appointments); + } + + public function removeAppointment(Appointment $appointment): static + { + if ($this->appointments->removeElement($appointment)) { + // set the owning side to null (unless already changed) + if ($appointment->getQuest() === $this) { + $appointment->setQuest(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + #[Groups(['all', 'detail'])] + public function getReviews(): Collection + { + return $this->reviews; + } + + public function addReview(Review $review): static + { + if (!$this->reviews->contains($review)) { + $this->reviews->add($review); + $review->setQuest($this); + } + + return $this; + } + + public function removeReview(Review $review): static + { + if ($this->reviews->removeElement($review)) { + // set the owning side to null (unless already changed) + if ($review->getQuest() === $this) { + $review->setQuest(null); + } + } + + return $this; + } + + #[Groups(['all', 'card', 'detail', 'profile'])] + public function getRating(): float + { + $count = count($this->reviews); + $rating = 0; + foreach ($this->reviews as $review) { + $rating += $review->getRating() ?: 0; + } + + if ($count > 0) { + return $rating / $count; + } + + return 0; + } + + /** + * @return Collection + */ + #[Groups(['all', 'card', 'detail'])] + public function getTags(): Collection + { + return $this->tags; + } + + public function addTag(Tag $tag): static + { + if (!$this->tags->contains($tag)) { + $this->tags->add($tag); + $tag->addQuest($this); + } + + return $this; + } + + public function removeTag(Tag $tag): static + { + if ($this->tags->removeElement($tag)) { + $tag->removeQuest($this); + } + + return $this; + } + + #[Groups(['all', 'card', 'detail'])] + public function getTheme(): ?Theme + { + return $this->theme; + } + + public function setTheme(?Theme $theme): static + { + $this->theme = $theme; + + return $this; + } + + #[Groups(['all', 'card', 'detail'])] + public function getGenre(): ?Genre + { + return $this->genre; + } + + public function setGenre(?Genre $genre): static + { + $this->genre = $genre; + + return $this; + } +} diff --git a/app/src/Entity/QuestImage.php b/app/src/Entity/QuestImage.php new file mode 100644 index 0000000000000000000000000000000000000000..4e7513cb07ca676758240f4f0f356d4ce2d2aea4 --- /dev/null +++ b/app/src/Entity/QuestImage.php @@ -0,0 +1,91 @@ +id; + } + + public function getQuest(): ?Quest + { + return $this->quest; + } + + public function setQuest(?Quest $quest): static + { + $this->quest = $quest; + + return $this; + } + + public function getPath(): ?string + { + return $this->path; + } + + #[Groups(['all', 'card', 'detail', 'profile'])] + public function getPublicPath(): ?string + { + if ($this->path !== null) { + return str_replace('/app/public', '', $this->path); + } + return null; + } + + public function setPath(string $path): static + { + $this->path = $path; + + return $this; + } + + #[Groups(['all', 'card', 'detail', 'profile'])] + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } +} diff --git a/app/src/Entity/Review.php b/app/src/Entity/Review.php new file mode 100644 index 0000000000000000000000000000000000000000..3d384446a0a75b9443d641e78f62679e5917ef10 --- /dev/null +++ b/app/src/Entity/Review.php @@ -0,0 +1,164 @@ + + */ + #[ORM\OneToMany(targetEntity: Like::class, mappedBy: 'review', orphanRemoval: true)] + private Collection $likes; + + public function __construct() + { + $this->likes = new ArrayCollection(); + } + + #[Groups(['all', 'detail', 'card'])] + public function getId(): ?int + { + return $this->id; + } + + #[Groups(['all', 'detail', 'card'])] + public function getRelatedUser(): ?User + { + return $this->related_user; + } + + public function setRelatedUser(?User $related_user): static + { + $this->related_user = $related_user; + + return $this; + } + + #[Groups(['all', 'detail', 'card'])] + public function getText(): ?string + { + return $this->text; + } + + public function setText(?string $text): static + { + $this->text = $text; + + return $this; + } + + #[Groups(['all', 'detail', 'card'])] + public function getRating(): ?int + { + return $this->rating; + } + + public function setRating(int $rating): static + { + $this->rating = $rating; + + return $this; + } + + public function getQuest(): ?Quest + { + return $this->quest; + } + + public function setQuest(?Quest $quest): static + { + $this->quest = $quest; + + return $this; + } + + #[Groups(['all', 'detail', 'card'])] + public function getCreateDate(): ?\DateTimeInterface + { + return $this->create_date; + } + + public function setCreateDate(\DateTimeInterface $create_date): static + { + $this->create_date = $create_date; + + return $this; + } + + #[Groups(['all', 'detail', 'card'])] + public function getUpdateDate(): ?\DateTimeInterface + { + return $this->update_date; + } + + public function setUpdateDate(\DateTimeInterface $update_date): static + { + $this->update_date = $update_date; + + return $this; + } + + /** + * @return Collection + */ + #[Groups(['all', 'detail', 'card'])] + public function getLikes(): Collection + { + return $this->likes; + } + + public function addLike(Like $like): static + { + if (!$this->likes->contains($like)) { + $this->likes->add($like); + $like->setReview($this); + } + + return $this; + } + + public function removeLike(Like $like): static + { + if ($this->likes->removeElement($like)) { + // set the owning side to null (unless already changed) + if ($like->getReview() === $this) { + $like->setReview(null); + } + } + + return $this; + } +} diff --git a/app/src/Entity/Tag.php b/app/src/Entity/Tag.php new file mode 100644 index 0000000000000000000000000000000000000000..0bbbd7e1bac2c544797708501df3872c8105bbe4 --- /dev/null +++ b/app/src/Entity/Tag.php @@ -0,0 +1,90 @@ + + */ + #[ORM\ManyToMany(targetEntity: Quest::class, inversedBy: 'tags')] + private Collection $quests; + + public function __construct() + { + $this->quests = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + #[Groups(['all', 'card', 'detail', 'filter'])] + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } + + /** + * @return Collection + */ + public function getQuests(): Collection + { + return $this->quests; + } + + public function addQuest(Quest $quest): static + { + if (!$this->quests->contains($quest)) { + $this->quests->add($quest); + } + + return $this; + } + + public function removeQuest(Quest $quest): static + { + $this->quests->removeElement($quest); + + return $this; + } +} diff --git a/app/src/Entity/Theme.php b/app/src/Entity/Theme.php new file mode 100644 index 0000000000000000000000000000000000000000..9c898aca619ad086c54e0a58d3e39da66927e8f7 --- /dev/null +++ b/app/src/Entity/Theme.php @@ -0,0 +1,53 @@ +id; + } + + #[Groups(['all', 'card', 'detail', 'filter'])] + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getDate(): ?\DateTimeInterface + { + return $this->date; + } + + public function setDate(\DateTimeInterface $date): static + { + $this->date = $date; + + return $this; + } +} diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php index 348b043e42771781be5de044820ca04043e80c65..89d0897184ac05377da592d08c9cf17a5d24caa5 100644 --- a/app/src/Entity/User.php +++ b/app/src/Entity/User.php @@ -76,9 +76,37 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: UserHistory::class, mappedBy: 'related_user', cascade: ['persist', 'remove'],fetch: 'EAGER')] private Collection $userHistories; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Appointment::class, mappedBy: 'related_user')] + private Collection $appointments; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'related_user')] + private Collection $reviews; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Like::class, mappedBy: 'related_user')] + private Collection $likes; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Favorite::class, mappedBy: 'related_user')] + private Collection $favorites; + public function __construct() { $this->userHistories = new ArrayCollection(); + $this->appointments = new ArrayCollection(); + $this->reviews = new ArrayCollection(); + $this->likes = new ArrayCollection(); + $this->favorites = new ArrayCollection(); } #[Groups(['all'])] @@ -393,4 +421,127 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $array = json_decode($data, true, 512, JSON_THROW_ON_ERROR); return self::createByArray($array, $groups); } + + /** + * @return Collection + */ + #[Groups(['all', 'profile'])] + public function getAppointments(): Collection + { + return $this->appointments; + } + + public function addAppointment(Appointment $appointment): static + { + if (!$this->appointments->contains($appointment)) { + $this->appointments->add($appointment); + $appointment->setRelatedUser($this); + } + + return $this; + } + + public function removeAppointment(Appointment $appointment): static + { + if ($this->appointments->removeElement($appointment)) { + // set the owning side to null (unless already changed) + if ($appointment->getRelatedUser() === $this) { + $appointment->setRelatedUser(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + #[Groups(['all', 'reviews'])] + public function getReviews(): Collection + { + return $this->reviews; + } + + public function addReview(Review $review): static + { + if (!$this->reviews->contains($review)) { + $this->reviews->add($review); + $review->setRelatedUser($this); + } + + return $this; + } + + public function removeReview(Review $review): static + { + if ($this->reviews->removeElement($review)) { + // set the owning side to null (unless already changed) + if ($review->getRelatedUser() === $this) { + $review->setRelatedUser(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getLikes(): Collection + { + return $this->likes; + } + + public function addLike(Like $like): static + { + if (!$this->likes->contains($like)) { + $this->likes->add($like); + $like->setRelatedUser($this); + } + + return $this; + } + + public function removeLike(Like $like): static + { + if ($this->likes->removeElement($like)) { + // set the owning side to null (unless already changed) + if ($like->getRelatedUser() === $this) { + $like->setRelatedUser(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + #[Groups(['all', 'favorites'])] + public function getFavorites(): Collection + { + return $this->favorites; + } + + public function addFavorite(Favorite $favorite): static + { + if (!$this->favorites->contains($favorite)) { + $this->favorites->add($favorite); + $favorite->setRelatedUser($this); + } + + return $this; + } + + public function removeFavorite(Favorite $favorite): static + { + if ($this->favorites->removeElement($favorite)) { + // set the owning side to null (unless already changed) + if ($favorite->getRelatedUser() === $this) { + $favorite->setRelatedUser(null); + } + } + + return $this; + } } diff --git a/app/src/Listeners/DateListener.php b/app/src/Listeners/DateListener.php new file mode 100644 index 0000000000000000000000000000000000000000..1cb534ae73b49e9210b92d6044176f0936b4dfdd --- /dev/null +++ b/app/src/Listeners/DateListener.php @@ -0,0 +1,38 @@ +getDate()) { + $appointment->setDate(new \DateTime()); + } + } + + public function prePersistReview(Review $review, PreFlushEventArgs $args): void + { + if (!$review->getCreateDate()) { + $review->setCreateDate(new \DateTime()); + } + $review->setUpdateDate(new \DateTime()); + } + + public function prePersistLike(Like $like, PreFlushEventArgs $args): void + { + if (!$like->getDate()) { + $like->setDate(new \DateTime()); + } + } +} \ No newline at end of file diff --git a/app/src/Messenger/Handler/QuestEndMessageHandler.php b/app/src/Messenger/Handler/QuestEndMessageHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..367fae3779bf1fc6989c619c979b13fe789ff50a --- /dev/null +++ b/app/src/Messenger/Handler/QuestEndMessageHandler.php @@ -0,0 +1,62 @@ +doctrine->getRepository(Quest::class)->getEndQuests(); + + foreach ($quests as $quest) { + $appointments = $quest->getAppointments(); + $questName = $quest->getName(); + foreach ($appointments as $appointment) { + $user = $appointment->getRelatedUser(); + if ($user) { + $userName = $user->getFullName(); + $questMessage = new QuestMessage( + $this->fromEmail, + $user->getEmail(), + "Квест \"{$questName}\" прошёл!", + << + Здравствуйте, {$userName}! + +
+ Квест {$questName} уже прошёл, какие у Вас остались впечатления? +
+
+ Оставьте отзыв на сайте +
+ HTML + ); + $this->bus->dispatch($questMessage); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/Messenger/Handler/QuestMessageHandler.php b/app/src/Messenger/Handler/QuestMessageHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..164ea6636cef517430d9b2f11a61b5bfc24d511b --- /dev/null +++ b/app/src/Messenger/Handler/QuestMessageHandler.php @@ -0,0 +1,44 @@ +subject($message->getSubject()) + ->from($message->getFrom()) + ->to($message->getTo()) + ->html($message->getBody()); + try { + $this->mailer->send($mail); + } catch (\Exception $exception) { + throw new \Exception('Ошибка отправки письма'); + } + } +} \ No newline at end of file diff --git a/app/src/Messenger/Handler/QuestStartMessageHandler.php b/app/src/Messenger/Handler/QuestStartMessageHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..4aac3f7532a8736d98ed612682148b67fe3bc0ed --- /dev/null +++ b/app/src/Messenger/Handler/QuestStartMessageHandler.php @@ -0,0 +1,62 @@ +doctrine->getRepository(Quest::class)->getStartQuests(); + + foreach ($quests as $quest) { + $appointments = $quest->getAppointments(); + $questName = $quest->getName(); + foreach ($appointments as $appointment) { + $user = $appointment->getRelatedUser(); + if ($user) { + $userName = $user->getFullName(); + $questMessage = new QuestMessage( + $this->fromEmail, + $user->getEmail(), + "Квест \"{$questName}\" стартует уже через 3 дня!", + << + Здравствуйте, {$userName}! + +
+ Квест "{$questName}" стартует уже через 3 дня! +
+
+ Чтобы не забыть, запланируйте его у себя в календаре! +
+ HTML + ); + $this->bus->dispatch($questMessage); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/Messenger/Message/QuestMessage.php b/app/src/Messenger/Message/QuestMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..042b4e9c35e60594c1138a594de3210a6b02d448 --- /dev/null +++ b/app/src/Messenger/Message/QuestMessage.php @@ -0,0 +1,63 @@ +subject; + } + + public function setSubject(string $subject): self + { + $this->subject = $subject; + + return $this; + } + + public function getBody(): string + { + return $this->body; + } + + public function setBody(string $body): self + { + $this->body = $body; + + return $this; + } + + public function getFrom(): string + { + return $this->from; + } + + public function setFrom(string $from): self + { + $this->from = $from; + + return $this; + } + + public function getTo(): string + { + return $this->to; + } + + public function setTo(string $to): self + { + $this->to = $to; + + return $this; + } +} \ No newline at end of file diff --git a/app/src/Messenger/Objects/QuestsEnd.php b/app/src/Messenger/Objects/QuestsEnd.php new file mode 100644 index 0000000000000000000000000000000000000000..2f6930a660d40f1f83079b1db8d543f7f3ab641e --- /dev/null +++ b/app/src/Messenger/Objects/QuestsEnd.php @@ -0,0 +1,8 @@ +connect('127.0.0.1', $_ENV['REDIS_PORT']); + if ($client->isConnected()) { + $this->cache = new RedisAdapter($client); + } else { + throw new \RuntimeException('Redis не подключен'); + } + } + + public static function getInstance(): Redis + { + if (!isset(self::$instance)) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Запись значения + * + * @param string $key + * @param mixed $value + * + * @return $this + * @throws InvalidArgumentException + */ + public function set(string $key, mixed $value): self + { + $item = $this->cache->getItem($key); + $item->set($value); + $this->cache->save($item); + + return $this; + } + + /** + * @param string $key + * + * @return mixed + * @throws InvalidArgumentException + */ + public function get(string $key): mixed + { + return $this->cache->getItem($key)->get(); + } + + public function delete(string $key): self + { + $this->cache->deleteItem($key); + + return $this; + } + + public function has(string $key): bool + { + return $this->cache->hasItem($key); + } +} \ No newline at end of file diff --git a/app/src/Redis/RedisFilter.php b/app/src/Redis/RedisFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..95a3b88883311c556e51e519210d1e4309ac4b6c --- /dev/null +++ b/app/src/Redis/RedisFilter.php @@ -0,0 +1,38 @@ +userId = $userId; + $this->redis = Redis::getInstance(); + } + + public function get(): ?DtoServiceInterface + { + $filter = $this->redis->get($this->getCode()); + + if ($filter instanceof DtoServiceInterface) { + return $filter; + } + + return null; + } + + public function set(DtoServiceInterface $filterDto): void + { + $this->redis->set($this->getCode(), $filterDto); + } + + private function getCode(): string + { + return 'filter_' . $this->userId; + } +} \ No newline at end of file diff --git a/app/src/Repository/AppointmentRepository.php b/app/src/Repository/AppointmentRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..5bf5ad8f4d0293f70b9c1354995c188976cad8bc --- /dev/null +++ b/app/src/Repository/AppointmentRepository.php @@ -0,0 +1,43 @@ + + */ +class AppointmentRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Appointment::class); + } + + // /** + // * @return Appointment[] Returns an array of Appointment objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('a.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Appointment + // { + // return $this->createQueryBuilder('a') + // ->andWhere('a.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/FavoriteRepository.php b/app/src/Repository/FavoriteRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..b96991e965d526b707f7b2ee64e0d832f367de58 --- /dev/null +++ b/app/src/Repository/FavoriteRepository.php @@ -0,0 +1,53 @@ + + */ +class FavoriteRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Favorite::class); + } + + public function findAllByUser(int $userId): array + { + return $this->createQueryBuilder('f') + ->setParameter('user_id', $userId) + ->leftJoin('f.related_user', 'user') + ->andWhere('user.id = :user_id') + ->getQuery() + ->getResult(); + } + + // /** + // * @return Favorite[] Returns an array of Favorite objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('f') + // ->andWhere('f.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('f.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Favorite + // { + // return $this->createQueryBuilder('f') + // ->andWhere('f.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/GenreRepository.php b/app/src/Repository/GenreRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..e29a15de34af6d65bf93a4387e9a997c3b5d885a --- /dev/null +++ b/app/src/Repository/GenreRepository.php @@ -0,0 +1,43 @@ + + */ +class GenreRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Genre::class); + } + + // /** + // * @return Genre[] Returns an array of Genre objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('g.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Genre + // { + // return $this->createQueryBuilder('g') + // ->andWhere('g.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/LikeRepository.php b/app/src/Repository/LikeRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..123d631fc85476df0df5d8d0d5730382a9c8d1e5 --- /dev/null +++ b/app/src/Repository/LikeRepository.php @@ -0,0 +1,43 @@ + + */ +class LikeRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Like::class); + } + + // /** + // * @return Like[] Returns an array of Like objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('l') + // ->andWhere('l.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('l.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Like + // { + // return $this->createQueryBuilder('l') + // ->andWhere('l.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/QuestImageRepository.php b/app/src/Repository/QuestImageRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..679bfe74356d1559b7da47b8f698f1843e2fd848 --- /dev/null +++ b/app/src/Repository/QuestImageRepository.php @@ -0,0 +1,43 @@ + + */ +class QuestImageRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, QuestImage::class); + } + +// /** +// * @return QuestImage[] Returns an array of QuestImage objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('q') +// ->andWhere('q.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('q.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?QuestImage +// { +// return $this->createQueryBuilder('q') +// ->andWhere('q.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/app/src/Repository/QuestRepository.php b/app/src/Repository/QuestRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..e44dfde606e8afb8a08a632cc9025e1faffe3d55 --- /dev/null +++ b/app/src/Repository/QuestRepository.php @@ -0,0 +1,171 @@ + + */ +class QuestRepository extends ServiceEntityRepository +{ + public const SORT_TYPES = [ + 'ASC', + 'DESC' + ]; + public const SORT_FIELDS = [ + 'name', + 'date', + 'final_date' + ]; + + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Quest::class); + } + + public function findAllByFilter(int $userId): array + { + $queryBuilder = $this->getFilterQuery($userId); + return $queryBuilder->getQuery()->getResult(); + } + + public function findCompletedByFilter(int $userId): array + { + $queryBuilder = $this->getFilterQuery($userId); + + $currentDate = new \DateTime(); + $queryBuilder->setParameter('current', $currentDate) + ->andWhere('q.date < :current'); + + $queryBuilder->setParameter('user_id', $userId) + ->leftJoin('q.appointments', 'appointments') + ->andWhere('appointments.related_user = :user_id'); + + return $queryBuilder->getQuery()->getResult(); + } + + public function findOneById(int $id, int $userId): ?Quest + { + $queryBuilder = $this->getBaseQuery($userId); + $queryBuilder->setParameter('detail_id', $id) + ->andWhere('q.id = :detail_id'); + return $queryBuilder->getQuery() + ->enableResultCache(3600, $id) + ->getOneOrNullResult(); + } + + private function getBaseQuery(int $userId): QueryBuilder + { + return $this->createQueryBuilder('q'); + } + + private function getFilterQuery(int $userId): QueryBuilder + { + $queryBuilder = $this->getBaseQuery($userId); + + $redisFilter = new RedisFilter($userId); + $filter = $redisFilter->get(); + if ($filter instanceof FilterDto) { + if (!empty($filter->search)) { + $text = $filter->search; + $queryBuilder->setParameter('search', "%$text%") + ->orWhere('q.name LIKE :search') + ->orWhere('q.short_description LIKE :search') + ->orWhere('q.full_description LIKE :search'); + } + if ($genres = $filter->genres) { + $queryBuilder->setParameter('genres', $genres) + ->leftJoin('q.genre', 'genre') + ->andWhere('genre.name IN (:genres)'); + } + if ($themes = $filter->themes) { + $queryBuilder->setParameter('themes', $themes) + ->leftJoin('q.theme', 'theme') + ->andWhere('theme.name IN (:themes)'); + } + if ($tags = $filter->tags) { + $queryBuilder->setParameter('tags', $tags) + ->leftJoin('q.tags', 'tags') + ->andWhere('tags.name IN (:tags)'); + } + if ($filter->sort + && $filter->sortField + && in_array($filter->sort, self::SORT_TYPES, true) + && in_array($filter->sortField, self::SORT_FIELDS, true) + ) { + $queryBuilder->orderBy('q.'.$filter->sortField, $filter->sort); + } + } + return $queryBuilder; + } + + public function getStartQuests(): array + { + $queryBuilder = $this->createQueryBuilder('q'); + + $startDate = new \DateTime(); + $startDate->setTime(0,0); + $startDate->add(new DateInterval('P3D')); + $endDate = new \DateTime(); + $endDate->setTime(23,59, 59); + $endDate->add(new DateInterval('P3D')); + + $queryBuilder->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->andWhere('q.date >= :start') + ->andWhere('q.date <= :end'); + + return $queryBuilder->getQuery()->getResult(); + } + + public function getEndQuests(): array + { + $queryBuilder = $this->createQueryBuilder('q'); + + $startDate = new \DateTime(); + $startDate->setTime(0,0); + $startDate->sub(new DateInterval('P1D')); + $endDate = new \DateTime(); + $endDate->setTime(23,59, 59); + $endDate->sub(new DateInterval('P1D')); + + $queryBuilder->setParameter('start', $startDate) + ->setParameter('end', $endDate) + ->andWhere('q.date >= :start') + ->andWhere('q.date <= :end'); + + return $queryBuilder->getQuery()->getResult(); + } + + // /** + // * @return Quest[] Returns an array of Quest objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('q') + // ->andWhere('q.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('q.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Quest + // { + // return $this->createQueryBuilder('q') + // ->andWhere('q.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/ReviewRepository.php b/app/src/Repository/ReviewRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..32b5d5d1bf40d4354163d2a94c9913c53e964ed8 --- /dev/null +++ b/app/src/Repository/ReviewRepository.php @@ -0,0 +1,53 @@ + + */ +class ReviewRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Review::class); + } + + public function findAllByUser(int $userId): array + { + return $this->createQueryBuilder('r') + ->setParameter('user_id', $userId) + ->leftJoin('r.related_user', 'user') + ->andWhere('user.id = :user_id') + ->getQuery() + ->getResult(); + } + + // /** + // * @return Review[] Returns an array of Review objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('r') + // ->andWhere('r.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('r.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Review + // { + // return $this->createQueryBuilder('r') + // ->andWhere('r.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/TagRepository.php b/app/src/Repository/TagRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..1b3c116d0fdc4a553a8adf4a2e557c41197fb897 --- /dev/null +++ b/app/src/Repository/TagRepository.php @@ -0,0 +1,43 @@ + + */ +class TagRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Tag::class); + } + + // /** + // * @return Tag[] Returns an array of Tag objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Tag + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Repository/ThemeRepository.php b/app/src/Repository/ThemeRepository.php new file mode 100644 index 0000000000000000000000000000000000000000..902276194ab43c6501f431014977c0938cec7086 --- /dev/null +++ b/app/src/Repository/ThemeRepository.php @@ -0,0 +1,43 @@ + + */ +class ThemeRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Theme::class); + } + + // /** + // * @return Theme[] Returns an array of Theme objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Theme + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/app/src/Scheduler/QuestProvider.php b/app/src/Scheduler/QuestProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..84f6a5ebc3b7031e49c27fb31746a3bcd22b874f --- /dev/null +++ b/app/src/Scheduler/QuestProvider.php @@ -0,0 +1,26 @@ +add( + RecurringMessage::every('1 day', new QuestsStart(), from: '09:00'), + RecurringMessage::every('1 day', new QuestsEnd(), from: '10:00') + ); + + return $schedule; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/CreateReview.php b/app/src/Service/Action/Classes/CreateReview.php new file mode 100644 index 0000000000000000000000000000000000000000..056714718ad1ae753fec60432ea23b3db92aa643 --- /dev/null +++ b/app/src/Service/Action/Classes/CreateReview.php @@ -0,0 +1,77 @@ +getDto(); + if ($dto->questId && $this->user->getId()) { + /** @var Quest|null $quest */ + $quest = $this->doctrine->getRepository(Quest::class)->findOneById($dto->questId, $this->user->getId()); + if ($quest) { + $reviews = $quest->getReviews(); + $exists = null; + foreach ($reviews as $review) { + if ($review->getRelatedUser() === $this->user) { + $exists = $review; + } + } + if ($exists) { + $this->responseService->addError('Ваш отзыв на квест уже существует'); + } else { + try { + $newReview = new Review(); + $newReview->setRelatedUser($this->user); + $newReview->setQuest($quest); + $newReview->setText($dto->text); + $newReview->setRating($dto->rating); + $em = $this->doctrine->getManager(); + $em->persist($newReview); + $em->flush(); + $this->responseService->addMessage('Отзыв добавлен'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка сохранения отзыва'); + } + } + } + } else { + $this->responseService->addError('Не передан ID квеста'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/DeleteReview.php b/app/src/Service/Action/Classes/DeleteReview.php new file mode 100644 index 0000000000000000000000000000000000000000..63cd87e576c1fd846c62a7162fe42644c0e38d22 --- /dev/null +++ b/app/src/Service/Action/Classes/DeleteReview.php @@ -0,0 +1,61 @@ +getDto(); + if ($dto->id) { + $review = $this->doctrine->getRepository(Review::class)->find($dto->id); + if ($review) { + try { + $em = $this->doctrine->getManager(); + $em->remove($review); + $em->flush(); + $this->responseService->addMessage('Отзыв удален'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка удаления отзыва'); + } + } else { + $this->responseService->addError('Не найден отзыв'); + } + } else { + $this->responseService->addError('Не получен ID отзыва'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetFavorites.php b/app/src/Service/Action/Classes/GetFavorites.php new file mode 100644 index 0000000000000000000000000000000000000000..bdf95869af888073c6280e2fe880a2cd06281a87 --- /dev/null +++ b/app/src/Service/Action/Classes/GetFavorites.php @@ -0,0 +1,46 @@ +user->getId()) { + $this->responseService->setData($this->doctrine->getRepository(Favorite::class)->findAllByUser($userId)); + } else { + $this->responseService->addError('Пользователь не сохранен'); + } + } + + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetFilter.php b/app/src/Service/Action/Classes/GetFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..194da5c856b9bf17c4e761591b30edf3fc5dcf07 --- /dev/null +++ b/app/src/Service/Action/Classes/GetFilter.php @@ -0,0 +1,54 @@ +user->getId()) { + $redisFilter = new RedisFilter($this->user->getId()); + $filter = $redisFilter->get(); + if ($filter instanceof FilterDto) { + $this->responseService->setData($filter); + } else { + $this->responseService->addError('Неверный формат фильтра'); + } + } else { + $this->responseService->addError('Пользователь не сохранен'); + } + } + + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetFilterParams.php b/app/src/Service/Action/Classes/GetFilterParams.php new file mode 100644 index 0000000000000000000000000000000000000000..7b3bbed44df53927e8674e60d492228eda59307d --- /dev/null +++ b/app/src/Service/Action/Classes/GetFilterParams.php @@ -0,0 +1,43 @@ +themes = $this->doctrine->getRepository(Theme::class)->findAll(); + + $filter->genres = $this->doctrine->getRepository(Genre::class)->findAll(); + + $filter->tags = $this->doctrine->getRepository(Tag::class)->findAll(); + + $this->responseService->setData($filter); + } + + public function needDto(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetProfileQuests.php b/app/src/Service/Action/Classes/GetProfileQuests.php new file mode 100644 index 0000000000000000000000000000000000000000..2f00de42f4f7418c435f986fed1f3750f23e8bdf --- /dev/null +++ b/app/src/Service/Action/Classes/GetProfileQuests.php @@ -0,0 +1,46 @@ +user->getId()) { + $this->responseService->setData($this->doctrine->getRepository(Quest::class)->findCompletedByFilter($userId)); + } else { + $this->responseService->addError('Пользователь не сохранен'); + } + } + + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetQuest.php b/app/src/Service/Action/Classes/GetQuest.php new file mode 100644 index 0000000000000000000000000000000000000000..d593c25cb706eb5d857a506f132051b1790bdea6 --- /dev/null +++ b/app/src/Service/Action/Classes/GetQuest.php @@ -0,0 +1,64 @@ +getDto(); + + if ($dto->id && $this->user->getId()) { + $quest = $this->doctrine->getRepository(Quest::class)->findOneById($dto->id, $this->user->getId()); + if ($quest) { + $this->responseService->setData($quest); + } else { + $this->responseService->addError('Квест не найден'); + } + } else { + $this->responseService->addError('Не получен Id'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetQuests.php b/app/src/Service/Action/Classes/GetQuests.php new file mode 100644 index 0000000000000000000000000000000000000000..5abf867f6df8cbf7034ee776d8b0fad790cb1374 --- /dev/null +++ b/app/src/Service/Action/Classes/GetQuests.php @@ -0,0 +1,47 @@ +user->getId()) { + $this->responseService->setData($this->doctrine->getRepository(Quest::class)->findAllByFilter($userId)); + } else { + $this->responseService->addError('Пользователь не сохранен'); + } + } + + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/GetReviews.php b/app/src/Service/Action/Classes/GetReviews.php new file mode 100644 index 0000000000000000000000000000000000000000..f0f24c5d61234edf2bab08a98a8d01e5bea522c8 --- /dev/null +++ b/app/src/Service/Action/Classes/GetReviews.php @@ -0,0 +1,47 @@ +user->getId()) { + $this->responseService->setData($this->doctrine->getRepository(Review::class)->findAllByUser($userId)); + } else { + $this->responseService->addError('Пользователь не сохранен'); + } + } + + public function needDto(): bool + { + return false; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/LikeReview.php b/app/src/Service/Action/Classes/LikeReview.php new file mode 100644 index 0000000000000000000000000000000000000000..2560c9aa469475bd713223ddab64dec980d9cfdc --- /dev/null +++ b/app/src/Service/Action/Classes/LikeReview.php @@ -0,0 +1,78 @@ +getDto(); + if ($dto->id) { + $review = $this->doctrine->getRepository(Review::class)->find($dto->id); + if ($review) { + $likes = $review->getLikes(); + $exists = null; + foreach ($likes as $like) { + if ($like->getRelatedUser() === $this->user) { + $exists = $like; + } + } + if ($exists) { + $this->responseService->addError('Вы уже оценили отзыв'); + } elseif ($review->getRelatedUser() === $this->user) { + $this->responseService->addError('Нельзя оценить свой отзыв'); + } else { + try { + $newLike = new Like(); + $newLike->setRelatedUser($this->user); + $newLike->setReview($review); + $em = $this->doctrine->getManager(); + $em->persist($newLike); + $em->flush(); + $this->responseService->addMessage('Вам нравится отзыв'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка оценки отзыва'); + } + } + } else { + $this->responseService->addError('Не найден отзыв'); + } + } else { + $this->responseService->addError('Не получен ID отзыва'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SetFilter.php b/app/src/Service/Action/Classes/SetFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..45803cc63471a366ac9cbca99cdd8a06279b2023 --- /dev/null +++ b/app/src/Service/Action/Classes/SetFilter.php @@ -0,0 +1,60 @@ +getDto(); + if ($this->user->getId() && $dto) { + $redisFilter = new RedisFilter($this->user->getId()); + $redisFilter->set($dto); + $this->responseService->setData($this->doctrine->getRepository(Quest::class)->findAllByFilter($this->user->getId()) ?: []); + } else { + $this->responseService->addError('Ошибка сохранения фильтра'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/SubscribeQuest.php b/app/src/Service/Action/Classes/SubscribeQuest.php new file mode 100644 index 0000000000000000000000000000000000000000..f7a460bc09fc5fc92af8b104a97418ee2593649a --- /dev/null +++ b/app/src/Service/Action/Classes/SubscribeQuest.php @@ -0,0 +1,83 @@ +getDto(); + if ($dto->id && $this->user->getId()) { + /** @var Quest|null $quest */ + $quest = $this->doctrine->getRepository(Quest::class)->findOneById($dto->id, $this->user->getId()); + if ($quest) { + if ($quest->getAppointmentCount() >= $quest->getMaxAppointments()) { + $this->responseService->addError('Максимальное количество участников'); + } elseif (new \DateTime() > $quest->getFinalDate()) { + $this->responseService->addError('Время записи на квест окончено, записаться нельзя'); + } else { + $appointments = $quest->getAppointments(); + $exists = false; + foreach ($appointments as $appointment) { + if ($appointment->getRelatedUser() === $this->user) { + $exists = true; + } + } + if (!$exists) { + try { + $newAppointment = new Appointment(); + $newAppointment->setQuest($quest); + $newAppointment->setRelatedUser($this->user); + $em = $this->doctrine->getManager(); + $em->persist($newAppointment); + $em->flush(); + $this->responseService->addMessage('Вы записаны на квест'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка записи на квест'); + } + } else { + $this->responseService->addError('Вы уже записаны'); + } + } + } else { + $this->responseService->addError('Квест не найден'); + } + } else { + $this->responseService->addError('Не получен Id'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/UnlikeReview.php b/app/src/Service/Action/Classes/UnlikeReview.php new file mode 100644 index 0000000000000000000000000000000000000000..d03566e597ae7c0b0ed95be31b3387260ee3291b --- /dev/null +++ b/app/src/Service/Action/Classes/UnlikeReview.php @@ -0,0 +1,73 @@ +getDto(); + if ($dto->id) { + $review = $this->doctrine->getRepository(Review::class)->find($dto->id); + if ($review) { + $likes = $review->getLikes(); + $exists = null; + foreach ($likes as $like) { + if ($like->getRelatedUser() === $this->user) { + $exists = $like; + } + } + if ($exists) { + try { + $em = $this->doctrine->getManager(); + $em->remove($exists); + $em->flush(); + $this->responseService->addMessage('Вам больше не нравится отзыв'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка оценки отзыва'); + } + } else { + $this->responseService->addError('Оценка отзыва не найдена'); + } + } else { + $this->responseService->addError('Не найден отзыв'); + } + } else { + $this->responseService->addError('Не получен ID отзыва'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/UnsubscribeQuest.php b/app/src/Service/Action/Classes/UnsubscribeQuest.php new file mode 100644 index 0000000000000000000000000000000000000000..34b7ac70a71b18a5c2863633571160a13dd2c9c1 --- /dev/null +++ b/app/src/Service/Action/Classes/UnsubscribeQuest.php @@ -0,0 +1,76 @@ +getDto(); + if ($dto->id && $this->user->getId()) { + $quest = $this->doctrine->getRepository(Quest::class)->findOneById($dto->id, $this->user->getId()); + if ($quest) { + if (new \DateTime() > $quest->getFinalDate()) { + $this->responseService->addError('Время записи на квест окончено, отписаться нельзя'); + } else { + $appointments = $quest->getAppointments(); + $exists = null; + foreach ($appointments as $appointment) { + if ($appointment->getRelatedUser() === $this->user) { + $exists = $appointment; + } + } + if ($exists) { + try { + $em = $this->doctrine->getManager(); + $em->remove($exists); + $em->flush(); + $this->responseService->addMessage('Вы отписаны от квеста'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка отписки от квеста'); + } + } else { + $this->responseService->addError('Вы не подписаны на квест'); + } + } + } else { + $this->responseService->addError('Квест не найден'); + } + } else { + $this->responseService->addError('Не получен Id'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Action/Classes/UpdateReview.php b/app/src/Service/Action/Classes/UpdateReview.php new file mode 100644 index 0000000000000000000000000000000000000000..ede7432fa9d5015bdf6823c77bbeb51f187c3569 --- /dev/null +++ b/app/src/Service/Action/Classes/UpdateReview.php @@ -0,0 +1,65 @@ +getDto(); + if ($dto->reviewId && $this->user->getId()) { + $review = $this->doctrine->getRepository(Review::class)->find($dto->reviewId); + if ($review) { + try { + $review->setText($dto->text); + $review->setRating($dto->rating); + $em = $this->doctrine->getManager(); + $em->persist($review); + $em->flush(); + $this->responseService->addMessage('Отзыв обновлен'); + } catch (\Exception $exception) { + $this->responseService->addError('Ошибка сохранения отзыва'); + } + } else { + $this->responseService->addError('Не найден отзыв'); + } + } else { + $this->responseService->addError('Не передан ID отзыва'); + } + } + + public function needDto(): bool + { + return true; + } + + public function checkDelete(): bool + { + return true; + } + + public function checkConfirm(): bool + { + return true; + } +} \ No newline at end of file diff --git a/app/src/Service/Dto/BaseDto.php b/app/src/Service/Dto/BaseDto.php index 363af9757865249aafbea400550fbf5999a32af0..220b8155cc64f939629a94cb715a37b30dccb953 100644 --- a/app/src/Service/Dto/BaseDto.php +++ b/app/src/Service/Dto/BaseDto.php @@ -14,15 +14,25 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Service\Attribute\Required; abstract class BaseDto implements DtoServiceInterface { private ?Request $request = null; + private ?ValidatorInterface $validator = null; - public function __construct( - private ?ValidatorInterface $validator, - ?RequestStack $requestStack = null, - ) + #[Required] + public function initValidator( + ?ValidatorInterface $validator + ): void + { + $this->validator = $validator; + } + + #[Required] + public function initRequest( + ?RequestStack $requestStack = null + ): void { if ($requestStack) { $this->request = $requestStack->getCurrentRequest(); @@ -39,6 +49,23 @@ abstract class BaseDto implements DtoServiceInterface public function getClass(): ?DtoServiceInterface { if ($this->request) { + return self::getClassByData($this->request->getContent()); + } + + return null; + } + + public static function getClassByArray(?array $data = null): ?DtoServiceInterface + { + if (empty($data)) { + return null; + } + return self::getClassByData(json_encode($data, JSON_THROW_ON_ERROR) ?: ''); + } + + private static function getClassByData(string $data): ?DtoServiceInterface + { + if (!empty($data)) { try { $normalizer = new ObjectNormalizer( null, @@ -50,12 +77,11 @@ abstract class BaseDto implements DtoServiceInterface [$normalizer, new DateTimeNormalizer()], [new JsonEncoder()] ); - return $serializer->deserialize($this->request->getContent(), static::class, 'json'); + return $serializer->deserialize($data, static::class, 'json'); } catch (\Exception $exception) { return null; } } - return null; } diff --git a/app/src/Service/Dto/Classes/CreateReviewDto.php b/app/src/Service/Dto/Classes/CreateReviewDto.php new file mode 100644 index 0000000000000000000000000000000000000000..c12ca3712e4783df79a57c66aefd93b73c205b41 --- /dev/null +++ b/app/src/Service/Dto/Classes/CreateReviewDto.php @@ -0,0 +1,28 @@ +data = $quest; + + return $this; + } + + public function getGroups(): array + { + return ['card']; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/FilterParamsResponse.php b/app/src/Service/Response/Classes/FilterParamsResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..4bd7e6b4222f3cee121bf0b5f1dabc149ed2f000 --- /dev/null +++ b/app/src/Service/Response/Classes/FilterParamsResponse.php @@ -0,0 +1,28 @@ +data = $filter; + } + + public function getGroups(): array + { + return ['filter']; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/FilterResponse.php b/app/src/Service/Response/Classes/FilterResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..0945d55d6ca7c381e00bd50930160c6d8ef05466 --- /dev/null +++ b/app/src/Service/Response/Classes/FilterResponse.php @@ -0,0 +1,34 @@ +data = $quest; + + return $this; + } + + public function getGroups(): array + { + return ['data']; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/QuestResponse.php b/app/src/Service/Response/Classes/QuestResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..bfff252b8007c9444d63f9fde5614389dd12d65a --- /dev/null +++ b/app/src/Service/Response/Classes/QuestResponse.php @@ -0,0 +1,34 @@ +data = $quest; + + return $this; + } + + public function getGroups(): array + { + return ['detail']; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/QuestsResponse.php b/app/src/Service/Response/Classes/QuestsResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..fa03a6b5766095211a4465f9a260870a45af3c26 --- /dev/null +++ b/app/src/Service/Response/Classes/QuestsResponse.php @@ -0,0 +1,34 @@ +data = $questList; + + return $this; + } + + public function getGroups(): array + { + return ['card']; + } +} \ No newline at end of file diff --git a/app/src/Service/Response/Classes/Response.php b/app/src/Service/Response/Classes/Response.php index b0f32f13cb1594628ea9188f757dbbc4a0acf0d4..28e6f51c8be36c242cbb0c95cf3aac7539ffac10 100644 --- a/app/src/Service/Response/Classes/Response.php +++ b/app/src/Service/Response/Classes/Response.php @@ -134,7 +134,7 @@ class Response implements ResponseServiceInterface $this->message = implode(', ', array_merge($this->messages, $this->errors)); - if (isset($this->data) && !empty($this->data)) { + if (isset($this->data)) { $groups = ['data']; $groups = array_merge($groups, $this->getGroups()); } diff --git a/app/src/Service/Response/Classes/ReviewsResponse.php b/app/src/Service/Response/Classes/ReviewsResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..40da2403f393f3386a1ae8e7215cd2d909efe992 --- /dev/null +++ b/app/src/Service/Response/Classes/ReviewsResponse.php @@ -0,0 +1,34 @@ +data = $quest; + + return $this; + } + + public function getGroups(): array + { + return ['card']; + } +} \ No newline at end of file diff --git a/app/symfony.lock b/app/symfony.lock index 6babe261381e9ef543850a43b480f6db498d3ca3..ceb121029f701bf0d1f19deee43a26acdbd03db3 100644 --- a/app/symfony.lock +++ b/app/symfony.lock @@ -51,6 +51,18 @@ "config/routes/nelmio_api_doc.yaml" ] }, + "nelmio/cors-bundle": { + "version": "2.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, "symfony/console": { "version": "7.0", "recipe": {