vasichmen/laravel-foundation

Laravel Package Foundation

v2.3.19 2024-04-23 20:02 UTC

README

Описание

Пакет с базовыми классами репозиториев, сервисов, реквестов для приложений на laravel. Базовый функционал включает в себя наиболее часто требующиеся методы и классы в любом приложении.

Установка

  • Установить пакет
composer require vasichmen/laravel-foundation

Примеры использования

AppServiceProvider

use Laravel\Foundation\Abstracts\AbstractAppServiceProvider;

class AppServiceProvider extends AbstractAppServiceProvider
{
    protected array $modelList = [
        User::class, //репозитории подключатся по классам моделей из App\Repositories как singleton, будут доступны через app(UserRepository::class)
    ];

    protected array $serviceList = [
        AuthService::class,  //сервисы подключаются как singleton и доступны через app(AuthService::class)
    ];

}

Базовая авторизация

Создаем новый мидлвар, наследуясь от AbstractBasicAuthMiddleware.php

use Laravel\Foundation\Abstracts\AbstractBasicAuthMiddleware;

class ExternalApiAuthMiddleware extends AbstractBasicAuthMiddleware
{
    protected string $key = 'external_api';

}

Регистрируем в Http\Kernel.php в поле $routeMiddleware:

'external-auth' => ExternalBasicAuthMiddleware::class,

После чего надо добавить в конфиг auth.php секцию с настройками логинов и паролей:

 'basic' => [
        'external_api' => [
            'user' => env('EXTERNAL_API_USER', 'user'),
            'password' => env('EXTERNAL_API_PASSWORD', 'password'),
        ],
    ],

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

Route::prefix('external')
    ->middleware('external-auth')
    ->group(function () {
        Route::get('test-user-token', [AuthController::class, 'testUserToken']);
    });

Полезные трейты

EnumTranslatable

Трейт, который можно подключить в любой enum, он добавляет метод trans() в элемент перечисления. Чтобы заработал перевод надо добавить файл /lang/<lang>/enums.php с содержимым примерно такого вида:

return [
    SystemStereotypeEnum::class => [
        SystemStereotypeEnum::Typical->value => 'Типовая система',
        SystemStereotypeEnum::Real->value => 'Реальная система',
    ],
]

Получить перевод можно так:

 SystemStereotypeEnum::Typical->trans($args);

Параметром $args можно передать массив аргументов для функции laravel trans(). Этот метод используется в методе AbstractResource->getEnum()

EnumValues

Добавляет к перечислениям статический метод values(), который возвращает массив всех значений этого перечисления

EnumDBCheckAlterable

Добавляет в миграцию метод alterEnum() для изменения поля с ограничением значений.

$this->alterEnum('subscriptions', 'type', ['tag','space','user']);

PartitionedByHash

Добавляет в миграцию метод makePartitionedTable, создающий таблицу, разделенную на партиции (только для Postgres)

$this->makePartitionedTable('materials', 100, function (Blueprint $table) {
            $table->string('name');
            $table->uuid('owner_id');
            $table->uuid('author_id');
            $table->timestamps();

            $table->foreign('owner_id')->references('id')->on('users');
            $table->foreign('author_id')->references('id')->on('users');
        });

Logger

Добавляет метод formatLogMessage, который работает по принципу sprintf, но может красиво форматировать эксепшены, массивы, коллекции итд.

Реквест и DTO

Реквест определяет правила валидации и (при необходимости) сообщения об ошибках, наследуется от AbstractRequest. DTO определяет структуру данных для прозрачности передачи данных внутри приложения, наследуется от AbstractDto. Для стандартизации форматов запросов есть GetListRequestDTO, в нем определены основные поля, используемые при получении списков с фильтрацией и пагинацией.

Для подключения валидации надо добавить \Laravel\Foundation\ServiceProviders\RequestServiceProvider::class в конфиг app.php

Чтоб подключить DTO к реквесту надо в реквесте переопределить поле $dtoClassName - имя класса DTO. При вызове метода $request->validated() проверяется существование класса DTO, его принадлежность к базовому классу и в конструктор передается массив параметров из реквеста. Внутри DTO в конструкторе ключи массива из реквеста приводятся в camelCase и данные из них записываются в соответствующие поля класса DTO. Таким образом для создания DTO достаточно только определить список полей с такими же именами, как в реквесте, но в camelCase. Для обратного преобразования в массив в DTO есть метод toArray().

Пример реквеста с сортировкой и постраничкой

use App\DTO\Requests\RefreshTokenRequestDTO;
use Laravel\Foundation\Abstracts\AbstractRequest;


class RefreshTokenRequest extends AbstractRequest
{
    use \Laravel\Foundation\Traits\RequestSortable;

    protected ?string $dtoClassName = RefreshTokenRequestDTO::class;
    
    public function rules()
    {
        return [
            'some_long_parameter' => 'required',
            ...$this->sorted(),
            ...$this->paginated(),
        ];
    }
}

Пример DTO

use Laravel\Foundation\Abstracts\AbstractDto;

class RefreshTokenRequestDTO extends AbstractDto
{
    public string $someLongParameter;
    public array $sort;
    public int $page;
    public int $perPage;
}

Такой реквест при вызове у него метода validated() вернет объект DTO. Чтобы возвращался массив, надо убрать указание DTO из реквеста. В поле sort в DTO вернется массив ключ=>значение, где ключ - название столбца, значение - направление сортировки

Если в классе DTO поле не будет найдено, то сгенерируется исключение DTOPropertyNotExists. Такое поведение задано по умолчанию для самопроверки, его можно изменить, переопределив метод parseData(), передав вторым параметром false, например вот так:

class RefreshTokenRequestDTO extends AbstractDto
{
    protected function parseData(array $data, bool $throwIfNoProperty = true): void
    {
        parent::parseData($data, false); 
    }
}

Модель

class User extends \Laravel\Foundation\Abstracts\AbstractModel
{
 ...
 
   // Можно переопределить метод сброса кастомного кэша
   // Метод вызывается при обновлении/удалении модели, в нем надо определить сброс кэша по кастомным тегам 
   public static function invalidateCustomCache(AbstractModel $user): void 
    {
        /** @var User $user */
        Cache::tags(['tag_1','tag_2'])
            ->forget(self::getCacheKey('some key data'));
    }
}

Использование кэширования при работе с моделями

Подключить провайдер \Laravel\Foundation\ServiceProviders\CacheServiceProvider::class в конфиг app.php

Простое кэширование с автоматическим сбросом при вызове событий eloquent: updated,created,deleted:

$result = $abstractModel
            ->cacheFor(config('cache.ttl'))
            ->where('feed_id',$feedId)
            ->get();

Установка кастомных тегов кэша. Такой кэш автоматически сбрасываться не будет, для сброса надо переопределять метод invalidateCustomCache в модели

$result = $abstractModel
            ->cacheFor(config('cache.ttl'))
            ->cacheTags([self::getCacheTag('feeds', $feedId)]) //устанавливаем кастомные теги
            ->cacheKey(self::getCacheKey($feedId)) //задаем ключ кэша 
            ->where('feed_id',$feedId)
            ->get();

Для корректного хранения кэша в одной БД redis от нескольких микросервисов надо задать переменную SERVICE_NAME в .env

Методы getCacheTag, getCacheKey подключаются из трейта CacheKeysTrait

Репозиторий

Все репозитории наследуются от AbstractRepository. Для каждой модели создается репозиторий и регистрируется через провайдер. Для создания select запросов используется RepositoryBuilder. В нем определены основные методы фильтрации, получения связей и выборки результатов. Можно вызывать из RepositoryBuilder напрямую методы Builder, они проксируются на внутренний объект построителя запросов. Для остальных операций есть статические методы в AbstractRepository: create, update, updateOrCreate, delete, getModel.

Примеры:

UserRepository::query()
    ->filters(['code'=>'code_1','count'=>1])
    ->query('query string',['column1','column2']) //поисковый запрос по определенным столбцам 
    ->cacheFor(config('cache.ttl')) //длительность хранения кэша
    ->orderBy(['name'=>'asc','id'=>'desc'])
    ->with(['roles']) //загрузка отношений
    ->withCount(['subscribers']) //получение числа связанных объектов
    ->get(); //полная выборка в виде Collection
    
 UserRepository::query()
    ->filters(['code'=>'code_1', 'count'=>1]) //применение фильтров
    ->orderBy(['name'=>'asc',]) //сортировка по полю name по возрастанию
    ->limit(10) //10 элементов на странице
    ->offset(1) //первая страница
    ->paginate() //Возвращается LengthAwarePaginator

UserRepository::query()
    ->fromGetListDto($someGetListDto) //установка настроек из заданного объекта GetListRequestDTO
    ->paginate();

Ресурс

Ресурсы делаются под каждую возвращаемую сущность (обычно это модели). Все ресурсы наследуются от AbstractResource. По сути ресурсы повторяют собой стандартные laravel JsonResource, но дополняются некоторыми методами.

Пример ресурса:

use Laravel\Foundation\Abstracts\AbstractResource;

class UserResource extends AbstractResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
     */
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'login' => $this->login,
            ...$this->getEnum('information_processed')
            ...$this->getRelation('roles', RoleResource::class),
        ];
    }
}

Метод $this->getEnum рендерит enum в структуру вида {code:<enum_code>,name:<название enum>}. Для получения названия вызывается метод trans трейта EnumTranslatable. Если enum не расширен этим трейтом, то будет ошибка NeedTranslatableEnumException.

Метод $this->getRelation добавляет в выдачу связь eloquent модели. Первым параметром передается название связи, вторым - класс ресурса для элементов этой связи. Если связь не загружена, то ключ добавлен не будет. Если связь загружена, то в ответ добавится ключ с названием связи.

Если надо вывести коллекцию элементов через ресурс, то есть стандартный статический метод collection:

return new DataResultPresenter(
            'bookmarks' => BookmarkResource::collection($bookmarkCollection),
        );

Презентер

Обычно не требуется создавать кастомные презенторы и всегда пользуемся DataResultPresenter для вывода простых данных или расширяем этот класс.

  return new DataResultPresenter([
            'token' => new TokenResource($token),
            'user' => new UserResource($user),
        ]);

Презентер с пагинацией

В качестве основного презентера с пагинацией выступает PaginatedDataPresenter

Пример использования без агрегаций:

return new PaginatedDataPresenter($lengthAwarePaginatedData, null, SomeModelResource::class);

Первым параметром передается объект LengthAwarePaginator, полученный из метода getList репозитория. Если коллекция получена другим способом, то можно установить в пагинатор нужную коллекцию через метод setCollection.

Вторым параметром передается коллекция или массив агрегаций (то, что рендерится в ключе filters). Обычно агрегации это возможные значения фильтров при установленных текущих фильтрах. Стандартная реализация заточена под ответ от elasticsearch, но можно переопределить метод aggregationToArray и задать любую другую логику.

Третьим параметром передается класс ресурса, который надо применить к элементам коллекции. Если надо рендерить элемент массива не через ресурс, а каким-то другим способом, то можно передать третьим параметром null и переопределить метод bodyToArray, задать в нем свою логику обработки элемента коллекции.