From 44f5ad8e21eed884c3c15c0184519a8eea534b8d Mon Sep 17 00:00:00 2001
From: Ilya Vasilenko <i.vasilenko@iqdev.digital>
Date: Mon, 1 Jul 2024 11:51:16 +0500
Subject: [PATCH] quests

---
 README.md                                     |   4 +
 app/composer.json                             |   3 +
 app/composer.lock                             | 205 ++++++++++-
 app/config/bundles.php                        |   1 +
 app/config/packages/cache.yaml                |   6 +-
 app/config/packages/doctrine.yaml             |   3 +
 app/config/packages/messenger.yaml            |   3 +
 app/config/packages/nelmio_cors.yaml          |  10 +
 app/config/services.yaml                      |   8 +
 app/migrations/Version20240624095415.php      |  81 +++++
 app/migrations/Version20240624101121.php      |  62 ++++
 app/src/Controller/ProfileController.php      |  51 +++
 app/src/Controller/QuestController.php        | 247 +++++++++++++
 app/src/Entity/Appointment.php                |  70 ++++
 app/src/Entity/Favorite.php                   |  71 ++++
 app/src/Entity/Genre.php                      |  53 +++
 app/src/Entity/Like.php                       |  72 ++++
 app/src/Entity/Quest.php                      | 328 ++++++++++++++++++
 app/src/Entity/QuestImage.php                 |  91 +++++
 app/src/Entity/Review.php                     | 164 +++++++++
 app/src/Entity/Tag.php                        |  90 +++++
 app/src/Entity/Theme.php                      |  53 +++
 app/src/Entity/User.php                       | 151 ++++++++
 app/src/Listeners/DateListener.php            |  38 ++
 .../Handler/QuestEndMessageHandler.php        |  62 ++++
 .../Messenger/Handler/QuestMessageHandler.php |  44 +++
 .../Handler/QuestStartMessageHandler.php      |  62 ++++
 app/src/Messenger/Message/QuestMessage.php    |  63 ++++
 app/src/Messenger/Objects/QuestsEnd.php       |   8 +
 app/src/Messenger/Objects/QuestsStart.php     |   8 +
 app/src/Redis/Redis.php                       |  78 +++++
 app/src/Redis/RedisFilter.php                 |  38 ++
 app/src/Repository/AppointmentRepository.php  |  43 +++
 app/src/Repository/FavoriteRepository.php     |  53 +++
 app/src/Repository/GenreRepository.php        |  43 +++
 app/src/Repository/LikeRepository.php         |  43 +++
 app/src/Repository/QuestImageRepository.php   |  43 +++
 app/src/Repository/QuestRepository.php        | 171 +++++++++
 app/src/Repository/ReviewRepository.php       |  53 +++
 app/src/Repository/TagRepository.php          |  43 +++
 app/src/Repository/ThemeRepository.php        |  43 +++
 app/src/Scheduler/QuestProvider.php           |  26 ++
 .../Service/Action/Classes/CreateReview.php   |  77 ++++
 .../Service/Action/Classes/DeleteReview.php   |  61 ++++
 .../Service/Action/Classes/GetFavorites.php   |  46 +++
 app/src/Service/Action/Classes/GetFilter.php  |  54 +++
 .../Action/Classes/GetFilterParams.php        |  43 +++
 .../Action/Classes/GetProfileQuests.php       |  46 +++
 app/src/Service/Action/Classes/GetQuest.php   |  64 ++++
 app/src/Service/Action/Classes/GetQuests.php  |  47 +++
 app/src/Service/Action/Classes/GetReviews.php |  47 +++
 app/src/Service/Action/Classes/LikeReview.php |  78 +++++
 app/src/Service/Action/Classes/SetFilter.php  |  60 ++++
 .../Service/Action/Classes/SubscribeQuest.php |  83 +++++
 .../Service/Action/Classes/UnlikeReview.php   |  73 ++++
 .../Action/Classes/UnsubscribeQuest.php       |  76 ++++
 .../Service/Action/Classes/UpdateReview.php   |  65 ++++
 app/src/Service/Dto/BaseDto.php               |  38 +-
 .../Service/Dto/Classes/CreateReviewDto.php   |  28 ++
 app/src/Service/Dto/Classes/FilterDto.php     |  47 +++
 app/src/Service/Dto/Classes/IdDto.php         |  16 +
 .../Service/Dto/Classes/UpdateReviewDto.php   |  28 ++
 app/src/Service/FilterService.php             |  12 +
 .../Service/Response/Classes/Data/Filter.php  |  42 +++
 .../Response/Classes/FavoritesResponse.php    |  34 ++
 .../Response/Classes/FilterParamsResponse.php |  28 ++
 .../Response/Classes/FilterResponse.php       |  34 ++
 .../Response/Classes/QuestResponse.php        |  34 ++
 .../Response/Classes/QuestsResponse.php       |  34 ++
 app/src/Service/Response/Classes/Response.php |   2 +-
 .../Response/Classes/ReviewsResponse.php      |  34 ++
 app/symfony.lock                              |  12 +
 72 files changed, 4122 insertions(+), 10 deletions(-)
 create mode 100644 app/config/packages/nelmio_cors.yaml
 create mode 100644 app/migrations/Version20240624095415.php
 create mode 100644 app/migrations/Version20240624101121.php
 create mode 100644 app/src/Controller/QuestController.php
 create mode 100644 app/src/Entity/Appointment.php
 create mode 100644 app/src/Entity/Favorite.php
 create mode 100644 app/src/Entity/Genre.php
 create mode 100644 app/src/Entity/Like.php
 create mode 100644 app/src/Entity/Quest.php
 create mode 100644 app/src/Entity/QuestImage.php
 create mode 100644 app/src/Entity/Review.php
 create mode 100644 app/src/Entity/Tag.php
 create mode 100644 app/src/Entity/Theme.php
 create mode 100644 app/src/Listeners/DateListener.php
 create mode 100644 app/src/Messenger/Handler/QuestEndMessageHandler.php
 create mode 100644 app/src/Messenger/Handler/QuestMessageHandler.php
 create mode 100644 app/src/Messenger/Handler/QuestStartMessageHandler.php
 create mode 100644 app/src/Messenger/Message/QuestMessage.php
 create mode 100644 app/src/Messenger/Objects/QuestsEnd.php
 create mode 100644 app/src/Messenger/Objects/QuestsStart.php
 create mode 100644 app/src/Redis/Redis.php
 create mode 100644 app/src/Redis/RedisFilter.php
 create mode 100644 app/src/Repository/AppointmentRepository.php
 create mode 100644 app/src/Repository/FavoriteRepository.php
 create mode 100644 app/src/Repository/GenreRepository.php
 create mode 100644 app/src/Repository/LikeRepository.php
 create mode 100644 app/src/Repository/QuestImageRepository.php
 create mode 100644 app/src/Repository/QuestRepository.php
 create mode 100644 app/src/Repository/ReviewRepository.php
 create mode 100644 app/src/Repository/TagRepository.php
 create mode 100644 app/src/Repository/ThemeRepository.php
 create mode 100644 app/src/Scheduler/QuestProvider.php
 create mode 100644 app/src/Service/Action/Classes/CreateReview.php
 create mode 100644 app/src/Service/Action/Classes/DeleteReview.php
 create mode 100644 app/src/Service/Action/Classes/GetFavorites.php
 create mode 100644 app/src/Service/Action/Classes/GetFilter.php
 create mode 100644 app/src/Service/Action/Classes/GetFilterParams.php
 create mode 100644 app/src/Service/Action/Classes/GetProfileQuests.php
 create mode 100644 app/src/Service/Action/Classes/GetQuest.php
 create mode 100644 app/src/Service/Action/Classes/GetQuests.php
 create mode 100644 app/src/Service/Action/Classes/GetReviews.php
 create mode 100644 app/src/Service/Action/Classes/LikeReview.php
 create mode 100644 app/src/Service/Action/Classes/SetFilter.php
 create mode 100644 app/src/Service/Action/Classes/SubscribeQuest.php
 create mode 100644 app/src/Service/Action/Classes/UnlikeReview.php
 create mode 100644 app/src/Service/Action/Classes/UnsubscribeQuest.php
 create mode 100644 app/src/Service/Action/Classes/UpdateReview.php
 create mode 100644 app/src/Service/Dto/Classes/CreateReviewDto.php
 create mode 100644 app/src/Service/Dto/Classes/FilterDto.php
 create mode 100644 app/src/Service/Dto/Classes/IdDto.php
 create mode 100644 app/src/Service/Dto/Classes/UpdateReviewDto.php
 create mode 100644 app/src/Service/FilterService.php
 create mode 100644 app/src/Service/Response/Classes/Data/Filter.php
 create mode 100644 app/src/Service/Response/Classes/FavoritesResponse.php
 create mode 100644 app/src/Service/Response/Classes/FilterParamsResponse.php
 create mode 100644 app/src/Service/Response/Classes/FilterResponse.php
 create mode 100644 app/src/Service/Response/Classes/QuestResponse.php
 create mode 100644 app/src/Service/Response/Classes/QuestsResponse.php
 create mode 100644 app/src/Service/Response/Classes/ReviewsResponse.php

diff --git a/README.md b/README.md
index 362f296..a6a08a1 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,10 @@
 
 ## Инструкция
 
+## Воркер отправки Email квестов
+
+Запустить команду - `bin/console messenger:consume -v scheduler_quests`
+
 ## Kafka
 <details>
 <summary>Инструкция</summary>
diff --git a/app/composer.json b/app/composer.json
index 804133d..7020df0 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 df47980..e212146 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 386a29b..facf965 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 6899b72..ddb1ec3 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 d42c52d..3c43cba 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 6dc7da1..bf81559 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 0000000..fa67677
--- /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 02884eb..39dc3d2 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 0000000..030c3a2
--- /dev/null
+++ b/app/migrations/Version20240624095415.php
@@ -0,0 +1,81 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DoctrineMigrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+
+/**
+ * Auto-generated Migration: Please modify to your needs!
+ */
+final class Version20240624095415 extends AbstractMigration
+{
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function up(Schema $schema): void
+    {
+        // this up() migration is auto-generated, please modify it to your needs
+        $this->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 0000000..08ca8e6
--- /dev/null
+++ b/app/migrations/Version20240624101121.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace DoctrineMigrations;
+
+use Doctrine\DBAL\Schema\Schema;
+use Doctrine\Migrations\AbstractMigration;
+
+/**
+ * Auto-generated Migration: Please modify to your needs!
+ */
+final class Version20240624101121 extends AbstractMigration
+{
+    public function getDescription(): string
+    {
+        return '';
+    }
+
+    public function up(Schema $schema): void
+    {
+        // this up() migration is auto-generated, please modify it to your needs
+        $this->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 2f94488..8386604 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 0000000..08593cb
--- /dev/null
+++ b/app/src/Controller/QuestController.php
@@ -0,0 +1,247 @@
+<?php
+
+namespace App\Controller;
+
+use App\Entity\Quest;
+use App\Service\Action\ActionServiceInterface;
+use App\Service\Action\Classes\CreateReview;
+use App\Service\Dto\Classes\CreateReviewDto;
+use App\Service\Dto\Classes\FilterDto;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\Classes\RegisterCodeDto;
+use App\Service\Dto\Classes\UpdateReviewDto;
+use App\Service\Response\Classes\FilterParamsResponse;
+use App\Service\Response\Classes\FilterResponse;
+use App\Service\Response\Classes\QuestResponse;
+use App\Service\Response\Classes\QuestsResponse;
+use App\Service\Response\Classes\Response;
+use Nelmio\ApiDocBundle\Annotation\Model;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\Routing\Attribute\Route;
+use OpenApi\Attributes as OA;
+
+#[Route('/api', name: 'api_')]
+#[OA\Tag(name: 'Квесты')]
+class QuestController extends AbstractController
+{
+    #[Route('/quests', name: '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.quests')]
+        ActionServiceInterface $actionService
+    ): JsonResponse
+    {
+        return $actionService->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 0000000..77eaeef
--- /dev/null
+++ b/app/src/Entity/Appointment.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\AppointmentRepository;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: AppointmentRepository::class)]
+class Appointment
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'appointments')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?User $related_user = null;
+
+    #[ORM\ManyToOne(inversedBy: 'appointments')]
+    private ?Quest $quest = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    public function getId(): ?int
+    {
+        return $this->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 0000000..16d9e4d
--- /dev/null
+++ b/app/src/Entity/Favorite.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\FavoriteRepository;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: FavoriteRepository::class)]
+class Favorite
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'favorites')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?User $related_user = null;
+
+    #[ORM\ManyToOne]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Quest $quest = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    public function getId(): ?int
+    {
+        return $this->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 0000000..efc9063
--- /dev/null
+++ b/app/src/Entity/Genre.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\GenreRepository;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: GenreRepository::class)]
+class Genre
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    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;
+    }
+}
diff --git a/app/src/Entity/Like.php b/app/src/Entity/Like.php
new file mode 100644
index 0000000..ec6a2be
--- /dev/null
+++ b/app/src/Entity/Like.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\LikeRepository;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: LikeRepository::class)]
+#[ORM\Table(name: '`like`')]
+class Like
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'likes')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Review $review = null;
+
+    #[ORM\ManyToOne(inversedBy: 'likes')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?User $related_user = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    public function getId(): ?int
+    {
+        return $this->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 0000000..cbe9696
--- /dev/null
+++ b/app/src/Entity/Quest.php
@@ -0,0 +1,328 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\QuestRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: QuestRepository::class)]
+class Quest
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\Column(type: Types::TEXT)]
+    private ?string $short_description = null;
+
+    #[ORM\Column(type: Types::TEXT, nullable: true)]
+    private ?string $full_description = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $final_date = null;
+
+    #[ORM\Column(nullable: true)]
+    private ?int $max_appointments = null;
+
+    /**
+     * @var Collection<int, QuestImage>
+     */
+    #[ORM\OneToMany(targetEntity: QuestImage::class, mappedBy: 'quest')]
+    private Collection $gallery;
+
+    /**
+     * @var Collection<int, Appointment>
+     */
+    #[ORM\OneToMany(targetEntity: Appointment::class, mappedBy: 'quest')]
+    private Collection $appointments;
+
+    /**
+     * @var Collection<int, Review>
+     */
+    #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'quest')]
+    private Collection $reviews;
+
+    /**
+     * @var Collection<int, Tag>
+     */
+    #[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<int, QuestImage>
+     */
+    #[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<int, Appointment>
+     */
+    #[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<int, Review>
+     */
+    #[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<int, Tag>
+     */
+    #[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 0000000..4e7513c
--- /dev/null
+++ b/app/src/Entity/QuestImage.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\QuestImageRepository;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: QuestImageRepository::class)]
+class QuestImage
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'gallery')]
+    private ?Quest $quest = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $path = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $type = null;
+
+    public function getId(): ?int
+    {
+        return $this->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 0000000..3d38444
--- /dev/null
+++ b/app/src/Entity/Review.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\ReviewRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: ReviewRepository::class)]
+class Review
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne(inversedBy: 'reviews')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?User $related_user = null;
+
+    #[ORM\Column(type: Types::TEXT, nullable: true)]
+    private ?string $text = null;
+
+    #[ORM\Column]
+    private ?int $rating = null;
+
+    #[ORM\ManyToOne(inversedBy: 'reviews')]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?Quest $quest = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $create_date = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $update_date = null;
+
+    /**
+     * @var Collection<int, Like>
+     */
+    #[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<int, Like>
+     */
+    #[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 0000000..0bbbd7e
--- /dev/null
+++ b/app/src/Entity/Tag.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\TagRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: TagRepository::class)]
+class Tag
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    /**
+     * @var Collection<int, Quest>
+     */
+    #[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<int, Quest>
+     */
+    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 0000000..9c898ac
--- /dev/null
+++ b/app/src/Entity/Theme.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\ThemeRepository;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[ORM\Entity(repositoryClass: ThemeRepository::class)]
+class Theme
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\Column(length: 255)]
+    private ?string $name = null;
+
+    #[ORM\Column(type: Types::DATETIME_MUTABLE)]
+    private ?\DateTimeInterface $date = null;
+
+    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;
+    }
+}
diff --git a/app/src/Entity/User.php b/app/src/Entity/User.php
index 348b043..89d0897 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<int, Appointment>
+     */
+    #[ORM\OneToMany(targetEntity: Appointment::class, mappedBy: 'related_user')]
+    private Collection $appointments;
+
+    /**
+     * @var Collection<int, Review>
+     */
+    #[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'related_user')]
+    private Collection $reviews;
+
+    /**
+     * @var Collection<int, Like>
+     */
+    #[ORM\OneToMany(targetEntity: Like::class, mappedBy: 'related_user')]
+    private Collection $likes;
+
+    /**
+     * @var Collection<int, Favorite>
+     */
+    #[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<int, Appointment>
+     */
+    #[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<int, Review>
+     */
+    #[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<int, Like>
+     */
+    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<int, Favorite>
+     */
+    #[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 0000000..1cb534a
--- /dev/null
+++ b/app/src/Listeners/DateListener.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Listeners;
+
+use App\Entity\Appointment;
+use App\Entity\Like;
+use App\Entity\Review;
+use Doctrine\Bundle\DoctrineBundle\Attribute\AsEntityListener;
+use Doctrine\ORM\Event\PreFlushEventArgs;
+use Doctrine\ORM\Events;
+
+#[AsEntityListener(event: Events::preFlush, method: 'prePersistAppointment', entity: Appointment::class)]
+#[AsEntityListener(event: Events::preFlush, method: 'prePersistReview', entity: Review::class)]
+#[AsEntityListener(event: Events::preFlush, method: 'prePersistLike', entity: Like::class)]
+class DateListener
+{
+    public function prePersistAppointment(Appointment $appointment, PreFlushEventArgs $args): void
+    {
+        if (!$appointment->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 0000000..367fae3
--- /dev/null
+++ b/app/src/Messenger/Handler/QuestEndMessageHandler.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Messenger\Handler;
+
+use App\Entity\Quest;
+use App\Messenger\Message\QuestMessage;
+use App\Messenger\Objects\QuestsEnd;
+use Doctrine\Persistence\ManagerRegistry;
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+use Symfony\Component\Messenger\MessageBusInterface;
+
+#[AsMessageHandler]
+class QuestEndMessageHandler
+{
+    public function __construct(
+        protected MessageBusInterface $bus,
+        private ManagerRegistry       $doctrine,
+        private string $fromEmail,
+    )
+    {
+    }
+
+    /**
+     * Обработка письма из очереди
+     *
+     * @param QuestsEnd $message
+     *
+     * @return void
+     */
+    public function __invoke(QuestsEnd $message): void
+    {
+        $quests = $this->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}\" прошёл!",
+                        <<<HTML
+                        <div>
+                            Здравствуйте, {$userName}!
+                        </div>
+                        <div>
+                            Квест {$questName} уже прошёл, какие у Вас остались впечатления?
+                        </div>
+                        <div>
+                            Оставьте отзыв на сайте
+                        </div>
+                        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 0000000..164ea66
--- /dev/null
+++ b/app/src/Messenger/Handler/QuestMessageHandler.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Messenger\Handler;
+
+use App\Messenger\Message\QuestMessage;
+use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Mime\Email;
+
+#[AsMessageHandler]
+class QuestMessageHandler
+{
+    public function __construct(
+        protected MessageBusInterface $bus,
+        private MailerInterface $mailer
+    )
+    {
+    }
+
+    /**
+     * Обработка письма из очереди
+     *
+     * @param QuestMessage $message
+     *
+     * @return void
+     * @throws TransportExceptionInterface
+     */
+    public function __invoke(QuestMessage $message): void
+    {
+        $mail = new Email();
+        $mail
+            ->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 0000000..4aac3f7
--- /dev/null
+++ b/app/src/Messenger/Handler/QuestStartMessageHandler.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace App\Messenger\Handler;
+
+use App\Entity\Quest;
+use App\Messenger\Message\QuestMessage;
+use App\Messenger\Objects\QuestsStart;
+use Doctrine\Persistence\ManagerRegistry;
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+use Symfony\Component\Messenger\MessageBusInterface;
+
+#[AsMessageHandler]
+class QuestStartMessageHandler
+{
+    public function __construct(
+        protected MessageBusInterface $bus,
+        private ManagerRegistry       $doctrine,
+        private string $fromEmail,
+    )
+    {
+    }
+
+    /**
+     * Обработка письма из очереди
+     *
+     * @param QuestsStart $message
+     *
+     * @return void
+     */
+    public function __invoke(QuestsStart $message): void
+    {
+        $quests = $this->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 дня!",
+                        <<<HTML
+                        <div>
+                            Здравствуйте, {$userName}!
+                        </div>
+                        <div>
+                            Квест "{$questName}" стартует уже через 3 дня!
+                        </div>
+                        <div>
+                            Чтобы не забыть, запланируйте его у себя в календаре!
+                        </div>
+                        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 0000000..042b4e9
--- /dev/null
+++ b/app/src/Messenger/Message/QuestMessage.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace App\Messenger\Message;
+
+class QuestMessage
+{
+    public function __construct(
+        public ?string $from,
+        public ?string $to,
+        public ?string $subject,
+        public ?string $body
+    )
+    {
+    }
+
+    public function getSubject(): string
+    {
+        return $this->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 0000000..2f6930a
--- /dev/null
+++ b/app/src/Messenger/Objects/QuestsEnd.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace App\Messenger\Objects;
+
+class QuestsEnd
+{
+
+}
\ No newline at end of file
diff --git a/app/src/Messenger/Objects/QuestsStart.php b/app/src/Messenger/Objects/QuestsStart.php
new file mode 100644
index 0000000..2ffe96c
--- /dev/null
+++ b/app/src/Messenger/Objects/QuestsStart.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace App\Messenger\Objects;
+
+class QuestsStart
+{
+
+}
\ No newline at end of file
diff --git a/app/src/Redis/Redis.php b/app/src/Redis/Redis.php
new file mode 100644
index 0000000..2afbce2
--- /dev/null
+++ b/app/src/Redis/Redis.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Redis;
+
+use Exception;
+use Psr\Cache\InvalidArgumentException;
+use Symfony\Component\Cache\Adapter\RedisAdapter;
+
+class Redis
+{
+    private static self $instance;
+
+    private RedisAdapter $cache;
+
+    /**
+     * @throws Exception
+     */
+    private function __construct()
+    {
+        $client = RedisAdapter::createConnection('redis://redis');
+        $client->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 0000000..95a3b88
--- /dev/null
+++ b/app/src/Redis/RedisFilter.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Redis;
+
+use App\Service\Dto\DtoServiceInterface;
+
+class RedisFilter
+{
+    private int $userId;
+    private Redis $redis;
+
+    public function __construct(int $userId)
+    {
+        $this->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 0000000..5bf5ad8
--- /dev/null
+++ b/app/src/Repository/AppointmentRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Appointment;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Appointment>
+ */
+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 0000000..b96991e
--- /dev/null
+++ b/app/src/Repository/FavoriteRepository.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Favorite;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Favorite>
+ */
+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 0000000..e29a15d
--- /dev/null
+++ b/app/src/Repository/GenreRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Genre;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Genre>
+ */
+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 0000000..123d631
--- /dev/null
+++ b/app/src/Repository/LikeRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Like;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Like>
+ */
+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 0000000..679bfe7
--- /dev/null
+++ b/app/src/Repository/QuestImageRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\QuestImage;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<QuestImage>
+ */
+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 0000000..e44dfde
--- /dev/null
+++ b/app/src/Repository/QuestRepository.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Quest;
+use App\Redis\RedisFilter;
+use App\Service\Dto\Classes\FilterDto;
+use DateInterval;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\ORM\QueryBuilder;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Quest>
+ */
+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 0000000..32b5d5d
--- /dev/null
+++ b/app/src/Repository/ReviewRepository.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Review;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Review>
+ */
+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 0000000..1b3c116
--- /dev/null
+++ b/app/src/Repository/TagRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Tag;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Tag>
+ */
+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 0000000..9022761
--- /dev/null
+++ b/app/src/Repository/ThemeRepository.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Theme;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Theme>
+ */
+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 0000000..84f6a5e
--- /dev/null
+++ b/app/src/Scheduler/QuestProvider.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace App\Scheduler;
+
+use App\Messenger\Objects\QuestsEnd;
+use App\Messenger\Objects\QuestsStart;
+use Symfony\Component\Scheduler\Attribute\AsSchedule;
+use Symfony\Component\Scheduler\RecurringMessage;
+use Symfony\Component\Scheduler\Schedule;
+use Symfony\Component\Scheduler\ScheduleProviderInterface;
+
+#[AsSchedule(name: 'quests')]
+class QuestProvider implements ScheduleProviderInterface
+{
+    public function getSchedule(): Schedule
+    {
+        $schedule = new Schedule();
+
+        $schedule->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 0000000..0567147
--- /dev/null
+++ b/app/src/Service/Action/Classes/CreateReview.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Entity\Review;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\CreateReviewDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.review.create', public: true)]
+class CreateReview extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.review.create')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var CreateReviewDto $dto */
+        $dto = $this->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 0000000..63cd87e
--- /dev/null
+++ b/app/src/Service/Action/Classes/DeleteReview.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Review;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.review.delete', public: true)]
+class DeleteReview extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.id')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var IdDto $dto */
+        $dto = $this->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 0000000..bdf9586
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetFavorites.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Favorite;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.favorites', public: true)]
+class GetFavorites extends UserBaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.favorites')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    public function runAction(): void
+    {
+        if ($userId = $this->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 0000000..194da5c
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetFilter.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Redis\Redis;
+use App\Redis\RedisFilter;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\FilterDto;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.filter.get', public: true)]
+class GetFilter extends UserBaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.filter')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    public function runAction(): void
+    {
+        if ($this->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 0000000..7b3bbed
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetFilterParams.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Genre;
+use App\Entity\Tag;
+use App\Entity\Theme;
+use App\Service\Action\BaseActionService;
+use App\Service\Response\Classes\Data\Filter;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.filter.params', public: true)]
+class GetFilterParams extends BaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.filter.params')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    public function runAction(): void
+    {
+        $filter = new Filter();
+
+        $filter->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 0000000..2f00de4
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetProfileQuests.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.profile.quests', public: true)]
+class GetProfileQuests extends UserBaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.quests')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    public function runAction(): void
+    {
+        if ($userId = $this->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 0000000..d593c25
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetQuest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\DtoServiceInterface;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest', public: true)]
+class GetQuest extends UserBaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.quest')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.id')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var IdDto $dto */
+        $dto = $this->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 0000000..5abf867
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetQuests.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quests', public: true)]
+class GetQuests extends UserBaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.quests')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+
+    public function runAction(): void
+    {
+        if ($userId = $this->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 0000000..f0f24c5
--- /dev/null
+++ b/app/src/Service/Action/Classes/GetReviews.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Entity\Review;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.reviews', public: true)]
+class GetReviews extends UserBaseActionService
+{
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.reviews')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    public function runAction(): void
+    {
+        if ($userId = $this->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 0000000..2560c9a
--- /dev/null
+++ b/app/src/Service/Action/Classes/LikeReview.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Like;
+use App\Entity\Review;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.review.like', public: true)]
+class LikeReview extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.id')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var IdDto $dto */
+        $dto = $this->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 0000000..45803cc
--- /dev/null
+++ b/app/src/Service/Action/Classes/SetFilter.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Redis\Redis;
+use App\Redis\RedisFilter;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\DtoServiceInterface;
+use App\Service\Response\ResponseServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.filter.set', public: true)]
+class SetFilter extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.filter')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    #[Required] public function initResponse(
+        #[Autowire(service: 'response.quests')]
+        ResponseServiceInterface $responseService
+    ): void
+    {
+        parent::initResponse($responseService);
+    }
+
+    public function runAction(): void
+    {
+        $dto = $this->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 0000000..f7a460b
--- /dev/null
+++ b/app/src/Service/Action/Classes/SubscribeQuest.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Appointment;
+use App\Entity\Quest;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.subscribe', public: true)]
+class SubscribeQuest extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.id')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var IdDto $dto */
+        $dto = $this->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 0000000..d03566e
--- /dev/null
+++ b/app/src/Service/Action/Classes/UnlikeReview.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Like;
+use App\Entity\Review;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.review.unlike', public: true)]
+class UnlikeReview extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.id')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var IdDto $dto */
+        $dto = $this->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 0000000..34b7ac7
--- /dev/null
+++ b/app/src/Service/Action/Classes/UnsubscribeQuest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\IdDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.unsubscribe', public: true)]
+class UnsubscribeQuest extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.id')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var IdDto $dto */
+        $dto = $this->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 0000000..ede7432
--- /dev/null
+++ b/app/src/Service/Action/Classes/UpdateReview.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Service\Action\Classes;
+
+use App\Entity\Quest;
+use App\Entity\Review;
+use App\Service\Action\UserBaseActionService;
+use App\Service\Dto\Classes\CreateReviewDto;
+use App\Service\Dto\Classes\UpdateReviewDto;
+use App\Service\Dto\DtoServiceInterface;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
+use Symfony\Contracts\Service\Attribute\Required;
+
+#[AsAlias(id: 'action.quest.review.update', public: true)]
+class UpdateReview extends UserBaseActionService
+{
+    #[Required] public function initDto(
+        #[Autowire(service: 'dto.review.update')]
+        DtoServiceInterface $dtoService
+    ): void
+    {
+        parent::initDto($dtoService);
+    }
+
+    public function runAction(): void
+    {
+        /** @var UpdateReviewDto $dto */
+        $dto = $this->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 363af97..220b815 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 0000000..c12ca37
--- /dev/null
+++ b/app/src/Service/Dto/Classes/CreateReviewDto.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Service\Dto\Classes;
+
+use App\Service\Dto\BaseDto;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Validator\Constraints as Assert;
+
+#[AsAlias(id: 'dto.review.create', public: true)]
+class CreateReviewDto extends BaseDto
+{
+    #[Assert\NotBlank(
+        message: 'Не получен текст отзыва.',
+    )]
+    public ?string $text = null;
+
+    #[Assert\Range(
+        notInRangeMessage: 'Оценку можно поставить от {{ min }} до {{ max }}.',
+        min: 0,
+        max: 10,
+    )]
+    public ?int $rating = null;
+
+    #[Assert\NotBlank(
+        message: 'Не передан Id квеста.',
+    )]
+    public ?int $questId = null;
+}
\ No newline at end of file
diff --git a/app/src/Service/Dto/Classes/FilterDto.php b/app/src/Service/Dto/Classes/FilterDto.php
new file mode 100644
index 0000000..c74fce3
--- /dev/null
+++ b/app/src/Service/Dto/Classes/FilterDto.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace App\Service\Dto\Classes;
+
+use App\Service\Dto\BaseDto;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'dto.filter', public: true)]
+class FilterDto extends BaseDto
+{
+    /**
+     * @var string|null
+     */
+    #[Groups(['data'])]
+    public ?string $search;
+
+    /**
+     * @var string|null
+     */
+    #[Groups(['data'])]
+    public ?string $sortField;
+
+    /**
+     * @var string|null
+     */
+    #[Groups(['data'])]
+    public ?string $sort;
+
+    /**
+     * @var string[]
+     */
+    #[Groups(['data'])]
+    public array $tags;
+
+    /**
+     * @var string[]
+     */
+    #[Groups(['data'])]
+    public array $genres;
+
+    /**
+     * @var string[]
+     */
+    #[Groups(['data'])]
+    public array $themes;
+}
\ No newline at end of file
diff --git a/app/src/Service/Dto/Classes/IdDto.php b/app/src/Service/Dto/Classes/IdDto.php
new file mode 100644
index 0000000..b43229a
--- /dev/null
+++ b/app/src/Service/Dto/Classes/IdDto.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Service\Dto\Classes;
+
+use App\Service\Dto\BaseDto;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Validator\Constraints as Assert;
+
+#[AsAlias(id: 'dto.id', public: true)]
+class IdDto extends BaseDto
+{
+    #[Assert\NotBlank(
+        message: 'Не передан Id.',
+    )]
+    public int $id;
+}
\ No newline at end of file
diff --git a/app/src/Service/Dto/Classes/UpdateReviewDto.php b/app/src/Service/Dto/Classes/UpdateReviewDto.php
new file mode 100644
index 0000000..0e37201
--- /dev/null
+++ b/app/src/Service/Dto/Classes/UpdateReviewDto.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Service\Dto\Classes;
+
+use App\Service\Dto\BaseDto;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Validator\Constraints as Assert;
+
+#[AsAlias(id: 'dto.review.update', public: true)]
+class UpdateReviewDto extends BaseDto
+{
+    #[Assert\NotBlank(
+        message: 'Не получен текст отзыва.',
+    )]
+    public ?string $text = null;
+
+    #[Assert\Range(
+        notInRangeMessage: 'Оценку можно поставить от {{ min }} до {{ max }}.',
+        min: 0,
+        max: 10,
+    )]
+    public ?int $rating = null;
+
+    #[Assert\NotBlank(
+        message: 'Не передан Id отзыва.',
+    )]
+    public ?int $reviewId = null;
+}
\ No newline at end of file
diff --git a/app/src/Service/FilterService.php b/app/src/Service/FilterService.php
new file mode 100644
index 0000000..3fc5662
--- /dev/null
+++ b/app/src/Service/FilterService.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace App\Service;
+
+class FilterService
+{
+
+    public function __construct()
+    {
+
+    }
+}
\ No newline at end of file
diff --git a/app/src/Service/Response/Classes/Data/Filter.php b/app/src/Service/Response/Classes/Data/Filter.php
new file mode 100644
index 0000000..402484b
--- /dev/null
+++ b/app/src/Service/Response/Classes/Data/Filter.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace App\Service\Response\Classes\Data;
+
+use App\Entity\Genre;
+use App\Entity\Tag;
+use App\Entity\Theme;
+use App\Repository\QuestRepository;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+class Filter
+{
+    /**
+     * @var Tag[]
+     */
+    #[Groups(['filter'])]
+    public array $tags;
+
+    /**
+     * @var Genre[]
+     */
+    #[Groups(['filter'])]
+    public array $genres;
+
+    /**
+     * @var Theme[]
+     */
+    #[Groups(['filter'])]
+    public array $themes;
+
+    /**
+     * @var string[]
+     */
+    #[Groups(['filter'])]
+    public array $sortFields = QuestRepository::SORT_FIELDS;
+
+    /**
+     * @var string[]
+     */
+    #[Groups(['filter'])]
+    public array $sorts = QuestRepository::SORT_TYPES;
+}
\ No newline at end of file
diff --git a/app/src/Service/Response/Classes/FavoritesResponse.php b/app/src/Service/Response/Classes/FavoritesResponse.php
new file mode 100644
index 0000000..f153ed8
--- /dev/null
+++ b/app/src/Service/Response/Classes/FavoritesResponse.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Service\Response\Classes;
+
+use App\Entity\Favorite;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'response.favorites', public: true)]
+class FavoritesResponse extends Response
+{
+    /**
+     * @var Favorite[]
+     */
+    #[Groups(["data"])]
+    public array $data;
+
+    /**
+     * @param Favorite[] $quest
+     *
+     * @return $this
+     */
+    public function setData(array $quest): self
+    {
+        $this->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 0000000..4bd7e6b
--- /dev/null
+++ b/app/src/Service/Response/Classes/FilterParamsResponse.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Service\Response\Classes;
+
+use App\Entity\Favorite;
+use App\Service\Response\Classes\Data\Filter;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'response.filter.params', public: true)]
+class FilterParamsResponse extends Response
+{
+    /**
+     * @var Filter
+     */
+    #[Groups(["data"])]
+    public Filter $data;
+
+    public function setData(Filter $filter)
+    {
+        $this->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 0000000..0945d55
--- /dev/null
+++ b/app/src/Service/Response/Classes/FilterResponse.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Service\Response\Classes;
+
+use App\Service\Dto\Classes\FilterDto;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'response.filter', public: true)]
+class FilterResponse extends Response
+{
+    /**
+     * @var FilterDto
+     */
+    #[Groups(["data"])]
+    public FilterDto $data;
+
+    /**
+     * @param FilterDto $quest
+     *
+     * @return $this
+     */
+    public function setData(FilterDto $quest): self
+    {
+        $this->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 0000000..bfff252
--- /dev/null
+++ b/app/src/Service/Response/Classes/QuestResponse.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Service\Response\Classes;
+
+use App\Entity\Quest;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'response.quest', public: true)]
+class QuestResponse extends Response
+{
+    /**
+     * @var Quest
+     */
+    #[Groups(["data"])]
+    public Quest $data;
+
+    /**
+     * @param Quest $quest
+     *
+     * @return $this
+     */
+    public function setData(Quest $quest): self
+    {
+        $this->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 0000000..fa03a6b
--- /dev/null
+++ b/app/src/Service/Response/Classes/QuestsResponse.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Service\Response\Classes;
+
+use App\Entity\Quest;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'response.quests', public: true)]
+class QuestsResponse extends Response
+{
+    /**
+     * @var Quest[]
+     */
+    #[Groups(["data"])]
+    public array $data;
+
+    /**
+     * @param Quest[] $questList
+     *
+     * @return $this
+     */
+    public function setData(array $questList): self
+    {
+        $this->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 b0f32f1..28e6f51 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 0000000..40da240
--- /dev/null
+++ b/app/src/Service/Response/Classes/ReviewsResponse.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Service\Response\Classes;
+
+use App\Entity\Review;
+use Symfony\Component\DependencyInjection\Attribute\AsAlias;
+use Symfony\Component\Serializer\Annotation\Groups;
+
+#[AsAlias(id: 'response.reviews', public: true)]
+class ReviewsResponse extends Response
+{
+    /**
+     * @var Review[]
+     */
+    #[Groups(["data"])]
+    public array $data;
+
+    /**
+     * @param Review[] $quest
+     *
+     * @return $this
+     */
+    public function setData(array $quest): self
+    {
+        $this->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 6babe26..ceb1210 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": {
-- 
GitLab