vasichmen / laravel-foundation
Laravel Package Foundation
Requires
- php: ^8.1
- ext-json: *
- graylog2/gelf-php: ^2.0
- guzzlehttp/guzzle: >=7.0
- laravel/framework: ^9.32|^10.0
- monolog/monolog: ^3.0|^2.0
This package is auto-updated.
Last update: 2024-04-23 20:09:05 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, задать в нем свою логику обработки элемента коллекции.