Пакет с базовыми классами репозиториев, сервисов, реквестов для приложений на laravel. Базовый функционал включает в себя наиболее часто требующиеся методы и классы в любом приложении.
- Установить пакет
composer require vasichmen/laravel-foundation
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']);
});
Трейт, который можно подключить в любой enum, он добавляет метод trans() в элемент перечисления.
Чтобы заработал перевод надо добавить файл /lang/<lang>/enums.php
с содержимым примерно такого вида:
return [
SystemStereotypeEnum::class => [
SystemStereotypeEnum::Typical->value => 'Типовая система',
SystemStereotypeEnum::Real->value => 'Реальная система',
],
]
Получить перевод можно так:
SystemStereotypeEnum::Typical->trans($args);
Параметром $args
можно передать массив аргументов для функции laravel trans().
Этот метод используется в методе AbstractResource->getEnum()
Добавляет к перечислениям статический метод values(), который возвращает массив всех значений этого перечисления
Добавляет в миграцию метод alterEnum() для изменения поля с ограничением значений.
$this->alterEnum('subscriptions', 'type', ['tag','space','user']);
Добавляет в миграцию метод 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');
});
Добавляет метод formatLogMessage, который работает по принципу sprintf, но может красиво форматировать эксепшены, массивы, коллекции итд.
Реквест определяет правила валидации и (при необходимости) сообщения об ошибках, наследуется от 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(),
];
}
}
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, задать в нем свою логику обработки элемента коллекции.