Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
<?php

$fileHeaderComment = <<<COMMENT
This file is part of the Symfony package.

(c) Fabien Potencier <[email protected]>

For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
COMMENT;

$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->exclude('config')
Expand All @@ -26,7 +17,6 @@
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'],
'linebreak_after_opening_tag' => true,
'mb_str_functions' => true,
'no_php4_constructor' => true,
Expand All @@ -38,6 +28,8 @@
'strict_comparison' => true,
'strict_param' => true,
'blank_line_between_import_groups' => false,
'concat_space' => ['spacing' => 'one'],
'yoda_style' => false,
])
->setFinder($finder)
->setCacheFile(__DIR__.'/var/.php-cs-fixer.cache')
Expand Down
39 changes: 39 additions & 0 deletions Task.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Техническое задание для прохождения собеседования на вакансию middle-разработчик (Symfony) Indigolab.

Реализовать API метод регистрации/авторизации пользователя по номеру телефона.

1. Запрос кода подтверждения.
Метод POST

Логика:
1. Сгенерировать 4-х значный код, сохранить в БД и вернуть в ответе.
2. Реализовать контроль отправки - 1 СМС в минуту на номер телефона.
3. Если код запарашивается повторно в течение минуты - вернуть сгенерированный ранее код. Иначе - сгенерировать новый код и вернуть его.
4. Реализовать блокировку - если отправили 3 кода за последние 10-15 минут - блокировка на час. Вернуть соответствующий ответ.

Фактическая отправка СМС не требуется - вместо этого в составе ответа сервера вернуть поле, содержащее этот код.
Текущий код подтверждения сохранять в базе или кэше.

2. Получение кода подтверждения.
Метод POST

Логика:
1. Код указан неверный - вернуть ошибку.
2. Код указан верный:
2.1 Если пользователя с таким номером телефона ещё нет в системе - создаём и возвращаем что-то типа "Вы успешно зарегистрировались" и его id
2.2 Если пользователь с этим номером телефона уже есть - "Вы успешно авторизовались" и его id
Для временного хранения номера телефона незарегистрированных пользователей может использоваться как кэш, так и БД. После успешной регистрации номер телефона должен быть привязан к пользователю (Вид связи - на Ваше усмотрение: отдельное поле пользователя, OneToOne, OneToMany(один пользователь - много телефонов))

Требования:
1. PHP 8.4
2. Symfony 7.2
3. БД - PostgreSQL
4. Реализация сущностей и репозиториев: предпочтительно реализация репозиториев, основанных на Doctrine DBAL и моделей с фабричными методами, т.к. абсолютное большинство сервисов компании используют протокол rpc, при котором использование ORM не представляется возможным. ORM также допускается.
5. Получение/возвращение данных в формате json. Наименование полей - на Ваше усмотрение.
6. Выполненное задание - ссылка на открытый git-репозиторий. Все переменные окружения - в .env.

Будет большим плюсом:
1. Контейнеризация с использованием docker:
1.1. Формат именования контейнеров: Префикс - "фамилия_имя-" (например, ivanov_ivan-php, ivanov_ivan-postgres и т.д.). Допускаются и составные префиксы, к примеру, ivanov_ivan-indigolab-php, ivanov_ivan-il_test-php и т.д.
1.2. Настройка основного контейнера с кодом для работы с отладчиком xdebug. Желательно, xdebug.client_host, xdebug.idekey - в .env.
2. Использование кэширония при помощи Redis для контроля отправки СМС.
8 changes: 6 additions & 2 deletions config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ security:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: phoneNumber
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
provider: app_user_provider

# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
Expand Down
1 change: 1 addition & 0 deletions config/routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ controllers:
path: ../src/Controller/
namespace: App\Controller
type: attribute
prefix: '/api'
Empty file removed migrations/.gitignore
Empty file.
55 changes: 55 additions & 0 deletions migrations/Version20250323083449.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250323083449 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 TABLE IF NOT EXISTS messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
$this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(\'messenger_messages\', NEW.queue_name::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;');
$this->addSql('DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages;');
$this->addSql('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages();');
}

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 TABLE IF EXISTS messenger_messages');
}
}
47 changes: 47 additions & 0 deletions migrations/Version20250323090258.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20250323090258 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create users table';
}

public function up(Schema $schema): void
{
$this->addSql('
CREATE TABLE IF NOT EXISTS users
(
id SERIAL PRIMARY KEY,
name VARCHAR(255) DEFAULT NULL,
phone_number VARCHAR(50) NOT NULL,
roles JSON NOT NULL,
updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
');

$this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_PHONE_NUMBER ON users (phone_number)');
}

public function down(Schema $schema): void
{
$this->addSql('DROP TABLE IF EXISTS users');
}
}
36 changes: 36 additions & 0 deletions migrations/Version20250323103107.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20250323103107 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create phone_verification_code table';
}

public function up(Schema $schema): void
{
$this->addSql('
CREATE TABLE phone_verification_code
(
id SERIAL PRIMARY KEY,
phone_number VARCHAR(50) NOT NULL,
code VARCHAR(10) NOT NULL,
attempts SMALLINT NOT NULL DEFAULT 0,
is_used BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
)
');
}

public function down(Schema $schema): void
{
$this->addSql('DROP TABLE IF EXISTS phone_verification_code');
}
}
Empty file removed src/Controller/.gitignore
Empty file.
44 changes: 44 additions & 0 deletions src/Controller/UserController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace App\Controller;

use App\Dto\Request\GetPhoneCodeDto;
use App\Service\PhoneVerificationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;

final class UserController extends AbstractController
{
/**
* @throws \Exception
*/
#[Route(
'/user/request-code',
name: 'user_request_code',
methods: ['POST'])
]
public function requestCode(
#[MapRequestPayload] GetPhoneCodeDto $requestDto,
PhoneVerificationService $verificationService,
): JsonResponse {
$phoneCodeDto = $verificationService->getPhoneCode($requestDto);

return $this->json($phoneCodeDto);
}

// #[Route('/verify-code', methods: ['POST'])]
// public function verifyCode(Request $request): JsonResponse
// {
// $phoneNumber = $request->request->get('phone_number');
// $code = $request->request->get('code');
//
// try {
// $user = $this->verificationService->verifyCode($phoneNumber, $code);
// return $this->json(['success' => true, 'user_id' => $user->getId()]);
// } catch (\Exception $e) {
// return $this->json(['error' => $e->getMessage()], 400);
// }
// }
}
27 changes: 27 additions & 0 deletions src/Dto/Request/GetPhoneCodeDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace App\Dto\Request;

use Symfony\Component\Validator\Constraints as Assert;

class GetPhoneCodeDto
{
#[Assert\NotBlank(message: 'Phone number cannot be blank.')]
#[Assert\Regex(
pattern: '/^\+79\d{9}$/',
message: 'Phone number must be in the format +79151234567'
)]
private string $phoneNumber;

public function getPhoneNumber(): string
{
return $this->phoneNumber;
}

public function setPhoneNumber(string $phoneNumber): void
{
$this->phoneNumber = $phoneNumber;
}
}
20 changes: 20 additions & 0 deletions src/Dto/Response/PhoneCodeDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Dto\Response;

class PhoneCodeDto
{
private string $code;

public function __construct(string $code)
{
$this->code = $code;
}

public function getCode(): string
{
return $this->code;
}
}
Empty file removed src/Entity/.gitignore
Empty file.
Loading