Оптимизация авторизации в 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, но возвращаться будет уже наше ДТО.