JUNIOR-BLOG

Для всех кто увлекается программированием

Главная

Оптимизация авторизации в Yii2: избавляемся от Active Record в UserIdentity

Описывать проблемы с AR я не буду, лично для меня это холиварный вопрос. Есть контекст проекта от него лучше и отталкиваться. Перечислю лишь несколько плюсов и минусов, которые я вынес для себя (возможно они и ошибочные).

За:

  • быстрый старт;
  • маппинг;
  • связи;
  • валидация.

Для типовых CRUD приложух и маленьких проектов.

 

Против:

  • Тесная связь между моделью и таблицей;
  • Труднее тестировать;
  • Производительность. 

 

Теперь попробуем реализовать авторизацию без AR.

Аутентификация

Аутентификация — это процесс проверки подлинности пользователя. Обычно используется идентификатор (например, username или адрес электронной почты) и секретный токен (например, пароль или ключ доступа), чтобы судить о том, что пользователь именно тот, за кого себя выдаёт. Аутентификация является основной функцией формы входа. [Документация]

У нас есть по умолчанию класс yii\web\User, которые имплиментирует yii\web\IdentityInterface, собствено наша задача состоит в том чтобы заменить базовый класс на собственную имплементацию yii\web\IdentityInterface

Миграция

Создадим простую табличку для хранения пользователей:

<?php

use yii\db\Migration;

class m250828_121616_create_users_table extends Migration
{

    public function safeUp()
    {
        $this->createTable('{{%users}}', [
            'id' => $this->primaryKey(),
            'username' => $this->string()->notNull()->unique(),
            'password_hash' => $this->string()->notNull(),
            'type' => $this->string()->notNull(),
            'first_name' => $this->string()->notNull(),
            'name' => $this->string()->notNull(),
            'last_name' => $this->string()->notNull(),
            'birth_day' => $this->date()->notNull(),
            'number_phone' => $this->string()->notNull(),
            'access_token' => $this->string()->null(),
            'auth_key' => $this->string()->null(),
            'created_at' => $this->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
            'updated_at' => $this->timestamp()->defaultExpression('CURRENT_TIMESTAMP'),
        ]);
    }

    public function safeDown()
    {
        $this->dropTable('{{%users}}');
    }
}

Теперь создадим DTO которое будет отвечать за пользователя. Причем некоторые личные данные вынесем в отдельное дто и для типов юзеров создадим енам.

Общий DTO:

<?php

namespace app\auth\dto;

use app\auth\enums\UserTypeEnum;
use app\repositories\User\dto\UserInfoDto;

class UserIdentityDto
{
    public function __construct(
        public int $id,
        public string $username,
        public string $password,
        public string $createdAt,
        public string $updatedAt,
        public UserTypeEnum $type,
        public UserInfoDto $userInfoDto,
        public ?string $accessToken = null,
        public ?string $authKey = null,
    ) {
    }
}

Личные данные:

<?php

namespace app\repositories\User\dto;

class UserInfoDto
{
    public function __construct(
        public string $firstName,
        public string $name,
        public string $lastName,
        public string $birthDate,
        public string $numberPhone,
    )
    {
    }
}

Перечисление:

<?php

namespace app\auth\enums;

enum UserTypeEnum: string
{
    case OWNER = 'owner';
    case MANAGER = 'manager';
}

 

Теперь создаем класс UserIdentity и реализуем его от IdentityInterface. В качестве свойства в конструктор передаем зависимость от ранее созданного DTO.

<?php
declare(strict_types=1);

namespace app\auth;

use app\auth\dto\UserIdentityDto;
use app\repositories\User\UserRepository;
use Yii;
use yii\web\IdentityInterface;

class UserIdentity implements IdentityInterface
{
    public UserIdentityDto $user;

    public function __construct(
        UserIdentityDto $user
    ) {
        $this->user = $user;
    }


    /**
     * @param int $id
     */
    public static function findIdentity($id): IdentityInterface|static|null
    {
        $repository = new UserRepository();
        $user = $repository->getById($id);
        return $user !== null ? new self($user) : null;
    }

    /**
     * Авторизация через токен
     * @param string $token
     * @param $type
     * @return IdentityInterface|static|null
     */
    public static function findIdentityByAccessToken($token, $type = null)
    {
        return null;
    }

    public static function findByUsername(string $username): ?UserIdentity
    {
        $repository = new UserRepository();
        $user = $repository->getByUsername($username);
        return $user !== null ? new self($user) : null;
    }

    public function getId(): int
    {
        return $this->user->id;
    }

    public function getAuthKey(): ?string
    {
        return $this->user->authKey;
    }

    /**
     * Для cookie авторизации запомнить меня
     */
    public function validateAuthKey($authKey): bool
    {
        return $this->user->authKey === $authKey;
    }

    public function validatePassword(string $password): bool
    {
        return Yii::$app->security->validatePassword($password, $this->user->password);
    }
}

По сути самыми необходимыми методами являются findIdentity, findByUsername, validatePassword. Ну и чтобы не засорять клас выносим все запросы в репозиторий, чей код ниже:

class UserRepository extends BaseRepository
{
    public const string TABLE_NAME = 'users';

    public function getById(int $id): ?UserIdentityDto
    {
        $result = $this->getQuery()
            ->from(self::TABLE_NAME)
            ->where(['id' => $id])
            ->one();
        if ($result === false) {
            return null;
        }
        return $this->mapToDto($result);
    }

    public function getByUsername(string $username): ?UserIdentityDto
    {
        $result = $this->getQuery()
            ->from(self::TABLE_NAME)
            ->where(['username' => $username])
            ->one();
        if ($result === false) {
            return null;
        }
        return $this->mapToDto($result);
    }
    /**
     * @param array{
     *     id: int,
     *     username: string,
     *     password_hash: string,
     *     created_at: string,
     *     updated_at: string,
     *     type: string,
     *     first_name: string,
     *     name: string,
     *     last_name: string,
     *     birth_day: string,
     *     number_phone: string,
     *     access_token: null|string,
     *     auth_key: null|string,
     * } $data
     * @return UserIdentityDto
     */
    private function mapToDto(array $data): UserIdentityDto
    {
        $userInfo = new UserInfoDto(
            firstName: $data['first_name'],
            name: $data['name'],
            lastName: $data['last_name'],
            birthDate: $data['birth_day'],
            numberPhone: $data['number_phone'],
        );
        return new UserIdentityDto(
            id: $data['id'],
            username: $data['username'],
            password: $data['password_hash'],
            createdAt: $data['created_at'],
            updatedAt: $data['updated_at'],
            type: UserTypeEnum::from($data['type']),
            userInfoDto: $userInfo,
            accessToken: $data['access_token'],
            authKey: $data['auth_key'],
        );
    }
}

 

Все запросы всегда возращают DTO. И последний шаг  config→web.php  прокидываем наш класс.

'user' => [
            'identityClass' => UserIdentity::class,
            'enableAutoLogin' => true,
        ],


Вот впринципе и все по сути для дальнейшей разработки ничего не поменялось чтобы получить идентити тостаточно как и раньше писать Yii::$app->user->identity, но возвращаться будет уже наше ДТО.

Похожие статьи

Комментарии