diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 0b58df5..0000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -docker/pgdata diff --git a/.editorconfig b/.editorconfig index 6a96a28..43b709b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,9 +11,5 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false -[*.txt] -trim_trailing_whitespace = false -insert_final_newline = false - [Makefile] indent_style = tab diff --git a/.env b/.env index caa68b0..5ab67d8 100644 --- a/.env +++ b/.env @@ -40,6 +40,8 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 MAILER_DSN=null://null ###< symfony/mailer ### -# Настройки Xdebug -XDEBUG_CLIENT_HOST=host.docker.internal -XDEBUG_IDEKEY=PHPSTORM +###> redis ### +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_DB=0 +###< redis ### diff --git a/.gitignore b/.gitignore index 81e7c50..231ac97 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ phpstan.neon ###> IDE files ### /docker/pgdata /docker/.env +/docker/.env.* +docker-compose.override.yml ###> IDE files ### diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index cf1ac31..dd3d561 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -31,7 +31,10 @@ 'blank_line_between_import_groups' => false, 'concat_space' => ['spacing' => 'one'], 'yoda_style' => false, + 'global_namespace_import' => true, + 'native_function_invocation' => false, + 'native_constant_invocation' => false, ]) ->setFinder($finder) - ->setCacheFile(__DIR__.'/var/.php-cs-fixer.cache') + ->setCacheFile(__DIR__ . '/var/.php-cs-fixer.cache') ; diff --git a/Makefile b/Makefile index 7f2c632..30e59d0 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ phpcsfixer-fix: vendor/bin/php-cs-fixer fix phpstan: - vendor/bin/phpstan analyse + vendor/bin/phpstan analyse --memory-limit=512M phpstan-baseline: vendor/bin/phpstan analyse src tests --generate-baseline @@ -31,5 +31,16 @@ docker-down: docker-build: docker-compose --env-file ./docker/.env build + docker-compose --env-file ./docker/.env up -d + +docker-rebuild: + docker-compose --env-file ./docker/.env build --no-cache + docker-compose --env-file ./docker/.env up -d + +docker-config: + docker-compose --env-file ./docker/.env config + +shell: + docker-compose exec php bash .PHONY: tests diff --git a/README.md b/README.md index d1ad23b..bb5b2b1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## IndigoLab test project ### Requirements -- PHP >= 8.4 +- PHP >= 8.3 - Composer >= 2 - Make >= 4 - Docker diff --git a/composer.json b/composer.json index 0989e8f..88451ca 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "doctrine/orm": "^3.3", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.1", + "predis/predis": "^2.3", "symfony/asset": "7.2.*", "symfony/asset-mapper": "7.2.*", "symfony/console": "7.2.*", diff --git a/composer.lock b/composer.lock index ed1bf11..b07a0d7 100644 --- a/composer.lock +++ b/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": "b4837eceef89d66d005bfaf9e0351fcd", + "content-hash": "35b869e222ab030b26afef9d50b8405c", "packages": [ { "name": "composer/semver", @@ -1693,6 +1693,67 @@ }, "time": "2025-02-19T13:28:12+00:00" }, + { + "name": "predis/predis", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "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" + }, + "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.3.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2024-11-21T20:00:02+00:00" + }, { "name": "psr/cache", "version": "3.0.0", diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..886f3d6 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -22,3 +22,15 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + # Конфиг Redis клиента + Predis\Client: + arguments: + - scheme: 'tcp' + host: '%env(REDIS_HOST)%' + port: '%env(REDIS_PORT)%' + database: '%env(REDIS_DB)%' + # Redis сервис + App\Service\RedisService: + arguments: + $redisClient: '@Predis\Client' diff --git a/docker-compose.yml b/docker-compose.yml index 5dfca3f..73c0c18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,68 +1,88 @@ version: '3.8' services: - ilyaguev_igor-indigolab-php: + app: build: context: . - dockerfile: ./docker/php/Dockerfile - container_name: ilyaguev_igor-indigolab-php + dockerfile: docker/php/Dockerfile + args: + UID: ${UID} + GID: ${GID} + APP_ENV: ${APP_ENV} + container_name: "${PROJECT_NAME}-app" restart: unless-stopped - volumes: - - .:/var/www/html + user: "${UID:-1000}:${GID:-1000}" + working_dir: /var/www/html environment: - - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@ilyaguev_igor-indigolab-postgres:5432/${POSTGRES_DB} - - REDIS_URL=redis://ilyaguev_igor-indigolab-redis:6379 -# - XDEBUG_CLIENT_HOST=${XDEBUG_CLIENT_HOST} -# - XDEBUG_IDEKEY=${XDEBUG_IDEKEY} + APP_ENV: ${APP_ENV} + APP_DEBUG: ${APP_DEBUG} + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PROJECT_NAME}-postgres:${POSTGRES_PORT}/${POSTGRES_DB} + REDIS_URL: redis://${PROJECT_NAME}-redis:${REDIS_PORT} + XDEBUG_MODE: ${XDEBUG_MODE:-off} + XDEBUG_TRIGGER: ${XDEBUG_TRIGGER:-TRIGGER} + XDEBUG_CONFIG: "client_host=${XDEBUG_HOST:-host.docker.internal} discover_client_host=1 log=/var/log/xdebug/xdebug.log" + PHP_IDE_CONFIG: "serverName=Docker" + TZ: ${TZ} + volumes: + - ./:/var/www/html depends_on: - - ilyaguev_igor-indigolab-postgres - - ilyaguev_igor-indigolab-redis + - db + - redis networks: - - indigolab-network + - app-network - ilyaguev_igor-indigolab-nginx: - image: nginx:latest - container_name: ilyaguev_igor-indigolab-nginx + nginx: + image: nginx:1.25-alpine + container_name: "${PROJECT_NAME}-nginx" restart: unless-stopped ports: - "${HTTP_PORT}:80" + - "${HTTPS_PORT}:443" volumes: - ./docker/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf - ./public:/var/www/html/public depends_on: - - ilyaguev_igor-indigolab-php + - app networks: - - indigolab-network + - app-network - ilyaguev_igor-indigolab-postgres: - image: postgres:latest - container_name: ilyaguev_igor-indigolab-postgres + db: + image: postgres:16 + container_name: "${PROJECT_NAME}-postgres" restart: unless-stopped environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_ROOT_PASSWORD: ${POSTGRES_ROOT_PASSWORD} - TZ: "Europe/Moscow" + TZ: ${TZ} ports: - "${POSTGRES_PORT}:5432" volumes: - - ./docker/pgdata:/var/lib/postgresql/data + - pgdata:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ] + interval: 5s + timeout: 5s + retries: 5 networks: - - indigolab-network + - app-network - ilyaguev_igor-indigolab-redis: - image: redis:latest - container_name: ilyaguev_igor-indigolab-redis + redis: + image: redis:7-alpine + container_name: "${PROJECT_NAME}-redis" restart: unless-stopped + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD} + TZ: ${TZ} ports: - - "6379:6379" + - "${REDIS_PORT}:6379" networks: - - indigolab-network + - app-network volumes: - postgres_data: + pgdata: networks: - indigolab-network: + app-network: driver: bridge diff --git a/docker/.env.dist b/docker/.env.dist index 59f9971..6fb8145 100644 --- a/docker/.env.dist +++ b/docker/.env.dist @@ -1,7 +1,31 @@ -POSTGRES_DB=indigolab +# Общие +PROJECT_NAME=project_name +TZ=Europe/Moscow +UID=1000 +GID=1000 + +# PHP +APP_ENV=dev +APP_DEBUG=true +PHP_MEMORY_LIMIT=256M +PHP_MAX_EXECUTION_TIME=120 + +# Настройки Xdebug +XDEBUG_MODE=develop,debug,coverage +XDEBUG_TRIGGER=start_debug +XDEBUG_HOST=host.docker.internal + +# Nginx +HTTP_PORT=8000 +HTTPS_PORT=4430 + +# DB +POSTGRES_DB=postgres_db POSTGRES_USER=postgres_user POSTGRES_PASSWORD=postgres_password POSTGRES_ROOT_PASSWORD=postgres_root_password POSTGRES_PORT=5432 -HTTP_PORT=8000 +# Redis +REDIS_PASSWORD=redis_password +REDIS_PORT=6379 diff --git a/docker/nginx/conf.d/default.conf b/docker/nginx/conf.d/default.conf index e98e818..c233948 100644 --- a/docker/nginx/conf.d/default.conf +++ b/docker/nginx/conf.d/default.conf @@ -5,17 +5,12 @@ server { root /var/www/html/public; index index.php; - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Credentials' 'true' always; - add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,HEAD,OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Origin,Content-Type,Accept,Authorization' always; - location / { try_files $uri /index.php$is_args$args; } -location ~ ^/index\.php(/|$) { - fastcgi_pass ilyaguev_igor-indigolab-php:9000; + location ~ ^/index\.php(/|$) { + fastcgi_pass app:9000; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; @@ -26,9 +21,10 @@ location ~ ^/index\.php(/|$) { internal; } -location ~ \.php$ { + location ~ \.php$ { return 404; } -error_log /var/log/nginx/project_error.log; - access_log /var/log/nginx/project_access.log; + + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; } diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index fe4ac80..2e1a65c 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -1,42 +1,69 @@ FROM php:8.3-fpm +# Устанавливаем пользователя/группу из аргументов +ARG UID=${UID} +ARG GID=${GID} +ARG APP_ENV=${APP_ENV} + # Устанавливаем необходимые зависимости RUN apt-get update && apt-get install -y \ - git \ - libmcrypt-dev \ + bash-completion \ libzip-dev \ libpq-dev \ + libicu-dev \ + unzip \ + curl \ zip \ - && rm -rf /var/lib/apt/lists/* + git + +# Расширения PHP +RUN docker-php-ext-install zip pdo pdo_mysql pdo_pgsql + +# Алиасы и автодополнение +RUN echo "alias ll='ls -alF'" >> /etc/bash.bashrc +RUN echo 'source /etc/bash_completion' >> /etc/bash.bashrc -# Устанавливаем расширения PHP -RUN docker-php-ext-install zip pdo pdo_pgsql +# Пользователь и права +RUN groupmod -g ${GID} www-data && \ + usermod -u ${UID} www-data && \ + chown -R ${UID}:${GID} /var/www/html -# Настройка PHP -COPY docker/php/php.ini /usr/local/etc/php/conf.d/ +# Логи Xdebug (только для dev) +RUN if [ "$APP_ENV" = "dev" ]; then \ + mkdir -p /var/log/xdebug && \ + chown -R ${UID}:${GID} /var/log/xdebug && \ + chmod -R 766 /var/log/xdebug; \ + fi + +# Конфиги PHP +COPY docker/php/conf.d/php.ini /usr/local/etc/php/conf.d/ +COPY docker/php/conf.d/xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini # Устанавливаем Composer RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ && php composer-setup.php --install-dir=/usr/local/bin --filename=composer \ && php -r "unlink('composer-setup.php');" -# Устанавливаем Xdebug -RUN pecl install xdebug && docker-php-ext-enable xdebug - -# Копируем конфигурацию Xdebug -#COPY xdebug.ini /usr/local/etc/php/conf.d/xdebug.ini +# Xdebug (только для dev) +RUN if [ "$APP_ENV" = "dev" ]; then \ + pecl install xdebug && \ + docker-php-ext-enable xdebug && \ + pecl clear-cache \ + rm -rf /tmp/pear; \ + fi # Устанавливаем рабочую директорию WORKDIR /var/www/html -# Копируем весь исходный код -COPY . /var/www/html +# Копируем все файлы +COPY . . -# Устанавливаем зависимости Composer -RUN composer install +# Устанавливаем зависимости +RUN if [ "$APP_ENV" = "prod" ]; then \ + composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist; \ + else \ + composer install --no-interaction --prefer-dist; \ + fi # Указываем пользователя для выполнения команд -USER www-data - -# Права для веб-сервера -#RUN chown -R www-data:www-data /var/www/html/var +USER ${UID}:${GID} diff --git a/docker/php/conf.d/php.ini b/docker/php/conf.d/php.ini new file mode 100644 index 0000000..e6ee1c2 --- /dev/null +++ b/docker/php/conf.d/php.ini @@ -0,0 +1,5 @@ +date.timezone = Europe/Moscow +short_open_tag = Off +log_errors = On +error_reporting = E_ALL +display_errors = Off diff --git a/docker/php/conf.d/xdebug.ini b/docker/php/conf.d/xdebug.ini index 1904814..a0627aa 100644 --- a/docker/php/conf.d/xdebug.ini +++ b/docker/php/conf.d/xdebug.ini @@ -1,4 +1,6 @@ -zend_extension=xdebug.so -xdebug.mode=develop,debug -xdebug.client_host=${XDEBUG_CLIENT_HOST} -xdebug.idekey=${XDEBUG_IDEKEY} +zend_extension=xdebug + +[xdebug] +xdebug.start_with_request = trigger +xdebug.discover_client_host = 0 +xdebug.idekey=PHPSTORM diff --git a/docker/php/php.ini b/docker/php/php.ini deleted file mode 100644 index e69de29..0000000 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a1a6c96..801c999 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,11 +6,8 @@ parameters: paths: - src - tests -# - bin/ -# - config/ -# - public/ -# - src/ -# - tests/ + ignoreErrors: + excludePaths: # bootstrapFiles: # - vendor/bin/.phpunit/phpunit/vendor/autoload.php diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 966dd71..f500ede 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -5,6 +5,7 @@ use App\Dto\Request\RequestPhoneCodeDto; use App\Dto\Request\VerifyPhoneCodeDto; use App\Service\PhoneVerificationService; +use Exception; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Attribute\MapRequestPayload; @@ -13,7 +14,7 @@ final class UserController extends AbstractController { /** - * @throws \Exception + * @throws Exception */ #[Route( '/request-code', @@ -30,7 +31,7 @@ public function requestCode( } /** - * @throws \Exception + * @throws Exception */ #[Route('/verify-code', methods: ['POST'])] public function verifyCode( diff --git a/src/Entity/PhoneVerificationCode.php b/src/Entity/PhoneVerificationCode.php index 2238280..ec94c8b 100644 --- a/src/Entity/PhoneVerificationCode.php +++ b/src/Entity/PhoneVerificationCode.php @@ -5,6 +5,7 @@ namespace App\Entity; use App\Repository\PhoneVerificationCodeRepository; +use DateTimeImmutable; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -29,11 +30,11 @@ class PhoneVerificationCode private bool $isUsed = false; #[ORM\Column] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; private function __construct() { - $this->createdAt = new \DateTimeImmutable(); + $this->createdAt = new DateTimeImmutable(); } public function getId(): int @@ -89,7 +90,7 @@ public function setIsUsed(bool $isUsed): static return $this; } - public function getCreatedAt(): \DateTimeImmutable + public function getCreatedAt(): DateTimeImmutable { return $this->createdAt; } diff --git a/src/Entity/User.php b/src/Entity/User.php index 4a827e3..a50975f 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -3,6 +3,7 @@ namespace App\Entity; use App\Repository\UserRepository; +use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; @@ -29,14 +30,14 @@ class User implements UserInterface private ?string $name = null; #[ORM\Column(nullable: true)] - private ?\DateTimeImmutable $updatedAt = null; + private ?DateTimeImmutable $updatedAt = null; #[ORM\Column] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; private function __construct() { - $this->createdAt = new \DateTimeImmutable(); + $this->createdAt = new DateTimeImmutable(); } public function getId(): int @@ -112,19 +113,19 @@ public function setName(?string $name): static return $this; } - public function getUpdatedAt(): ?\DateTimeImmutable + public function getUpdatedAt(): ?DateTimeImmutable { return $this->updatedAt; } - public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static + public function setUpdatedAt(?DateTimeImmutable $updatedAt): static { $this->updatedAt = $updatedAt; return $this; } - public function getCreatedAt(): \DateTimeImmutable + public function getCreatedAt(): DateTimeImmutable { return $this->createdAt; } diff --git a/src/Repository/PhoneVerificationCodeRepository.php b/src/Repository/PhoneVerificationCodeRepository.php index 591e987..09bae9e 100644 --- a/src/Repository/PhoneVerificationCodeRepository.php +++ b/src/Repository/PhoneVerificationCodeRepository.php @@ -3,6 +3,7 @@ namespace App\Repository; use App\Entity\PhoneVerificationCode; +use DateTimeImmutable; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Types\Types; @@ -23,7 +24,7 @@ public function __construct(ManagerRegistry $registry) */ public function getRecentCodesCount( string $phoneNumber, - \DateTimeImmutable $recentDateTime, + DateTimeImmutable $recentDateTime, ): int { $sql = ' SELECT COUNT(1) diff --git a/src/Service/PhoneVerificationService.php b/src/Service/PhoneVerificationService.php index 5805058..d5ec68b 100644 --- a/src/Service/PhoneVerificationService.php +++ b/src/Service/PhoneVerificationService.php @@ -11,12 +11,17 @@ use App\Entity\User; use App\Repository\PhoneVerificationCodeRepository; use App\Repository\UserRepository; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; +use Exception; +use Random\RandomException; class PhoneVerificationService { private const CODES_COUNT_LIMIT = 3; private const CODES_COUNT_LIMIT_PERIOD = '15 minutes'; + private const PHONE_NUMBER_BLOCK_PERIOD_SEC = 3600; + public RedisService $redisService; private EntityManagerInterface $em; private PhoneVerificationCodeRepository $codeRepository; @@ -26,14 +31,16 @@ public function __construct( EntityManagerInterface $em, PhoneVerificationCodeRepository $codeRepository, UserRepository $userRepository, + RedisService $redisService, ) { $this->em = $em; $this->codeRepository = $codeRepository; $this->userRepository = $userRepository; + $this->redisService = $redisService; } /** - * @throws \Exception + * @throws Exception */ public function getPhoneCode(RequestPhoneCodeDto $getPhoneCodeDto): PhoneCodeDto { @@ -41,7 +48,7 @@ public function getPhoneCode(RequestPhoneCodeDto $getPhoneCodeDto): PhoneCodeDto if ($this->isBlockedPhoneNumber($phoneNumber)) { // TODO: Будет лучше сделать свое кастомное более специфичное исключение и выкидывать его - throw new \Exception('Номер заблокирован'); + throw new Exception('Номер заблокирован'); } $this->checkRequestLimit($phoneNumber); @@ -64,24 +71,20 @@ public function getPhoneCode(RequestPhoneCodeDto $getPhoneCodeDto): PhoneCodeDto private function isBlockedPhoneNumber(string $phoneNumber): bool { - /* - * TODO: - * Можно создать еще одну таблицу и сохранять в нее заблокированные номера, чтобы потом их проверять, - * но кажется это не очень, поэтому даже не стал создавать эту таблицу - * Лучше и быстрее будет сохранять в redis ключ со сроком жизни 1 час и просто проверять существование этого ключа - * ключ вида 'user_phone_+7910123456789' - */ - // $block = $this->blockRepository->isPhoneNumberBlocked($phoneNumber); - - return false; + return $this->redisService->has( + sprintf( + 'user_phone_%s_blocked', + $phoneNumber + ) + ); } /** - * @throws \Exception + * @throws Exception */ private function checkRequestLimit(string $phoneNumber): void { - $recentDateTime = new \DateTimeImmutable('-' . self::CODES_COUNT_LIMIT_PERIOD); + $recentDateTime = new DateTimeImmutable('-' . self::CODES_COUNT_LIMIT_PERIOD); $recentCodesCount = $this->codeRepository->getRecentCodesCount($phoneNumber, $recentDateTime); @@ -89,21 +92,26 @@ private function checkRequestLimit(string $phoneNumber): void $this->blockPhoneNumber($phoneNumber); // TODO: Будет лучше сделать свое кастомное более специфичное исключение и выкидывать его - throw new \Exception('Номер заблокирован'); + throw new Exception('Номер заблокирован'); } } private function blockPhoneNumber(string $phoneNumber): void { - // TODO: - // Тут создаем или запись в таблице БД с заблокированными номерами (нежелательно) - // или создаем ключ в redis (предпочтительнее) вида 'user_phone_+7910123456789' + $this->redisService->set( + sprintf( + 'user_phone_%s_blocked', + $phoneNumber + ), + 1, + self::PHONE_NUMBER_BLOCK_PERIOD_SEC + ); } /** * @param array $codeData * - * @throws \Exception + * @throws Exception */ private function isActualExistedCode(array $codeData): bool { @@ -113,15 +121,18 @@ private function isActualExistedCode(array $codeData): bool // TODO: // Возникли проблемы с временем, пришлось сделать костыль с timezone, // надо будет с этим разобраться. Возможно в docker контейнере все наладится - $createdAt = new \DateTimeImmutable($codeCreatedAt); - $now = new \DateTimeImmutable('-1 minute'); + $createdAt = new DateTimeImmutable($codeCreatedAt); + $now = new DateTimeImmutable('-1 minute'); return $createdAt > $now; } + /** + * @throws RandomException + */ private function generateCode(): string { - return mb_str_pad((string) random_int(0, 9999), 4, '0', \STR_PAD_LEFT); + return mb_str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT); } private function createPhoneVerificationCode(string $phoneNumber, string $newVerificationCode): void @@ -136,29 +147,29 @@ private function createPhoneVerificationCode(string $phoneNumber, string $newVer } /** - * @throws \Exception + * @throws Exception */ public function verifyCode(string $phoneNumber, string $code): AuthDto { $lastCodeData = $this->codeRepository->getLastCode($phoneNumber); if ($lastCodeData === false) { - throw new \Exception('Код не найден'); + throw new Exception('Код не найден'); } if (!$this->isActualExistedCode($lastCodeData)) { - throw new \Exception('Код истек, запросите новый'); + throw new Exception('Код истек, запросите новый'); } /** @var PhoneVerificationCode $verificationCode */ $verificationCode = $this->codeRepository->find($lastCodeData['id']); if ($lastCodeData['is_used']) { - throw new \Exception('Код уже использован'); + throw new Exception('Код уже использован'); } if ($lastCodeData['code'] !== $code) { - throw new \Exception('Код неверный'); + throw new Exception('Код неверный'); } $user = $this->userRepository->findOneBy(['phoneNumber' => $lastCodeData['phone_number']]); @@ -168,7 +179,7 @@ public function verifyCode(string $phoneNumber, string $code): AuthDto } /* - * Тут, думаю, стоит использовать в транзакцию, + * Тут, думаю, стоит использовать транзакцию, * чтобы не получилось, что юзер создался, а код не пометился как использованный */ $this->em->beginTransaction(); @@ -181,7 +192,7 @@ public function verifyCode(string $phoneNumber, string $code): AuthDto $this->em->flush(); $this->em->commit(); - } catch (\Exception $e) { + } catch (Exception $e) { $this->em->rollback(); throw $e; diff --git a/src/Service/RedisService.php b/src/Service/RedisService.php new file mode 100644 index 0000000..c4858ff --- /dev/null +++ b/src/Service/RedisService.php @@ -0,0 +1,49 @@ +redisClient = $redisClient; + } + + public function set(string $key, mixed $value, ?int $ttl = null): void + { + $serialized = serialize($value); + $this->redisClient->set($key, $serialized); + + if ($ttl !== null) { + $this->redisClient->expire($key, $ttl); + } + } + + public function get(string $key): mixed + { + $value = $this->redisClient->get($key); + + return $value !== null ? unserialize($value) : null; + } + + public function delete(string $key): void + { + $this->redisClient->del([$key]); + } + + public function has(string $key): bool + { + return $this->redisClient->exists($key) > 0; + } + + public function expire(string $key, int $ttl): bool + { + return (bool) $this->redisClient->expire($key, $ttl); + } +}