vasichmen / laravel-foundation
Laravel Package Foundation
Requires
- php: ^8.4
- ext-json: *
- laravel/framework: ^9.32|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-master
- v2.19.0
- v2.18.0
- v2.17.16
- v2.17.15
- v2.17.14
- v2.17.13
- v2.17.12
- v2.17.10
- v2.17.9
- v2.17.8
- v2.17.7
- v2.17.6
- v2.17.5
- v2.17.4
- v2.17.3
- v2.17.2
- v2.17.1
- v2.17.0
- v2.16.1
- v2.16.0
- v2.15.1
- v2.15.0
- v2.14.3
- v2.14.2
- v2.14.1
- v2.14.0
- v2.13.2
- v2.13.1
- v2.13.0
- v2.12.2
- v2.12.1
- v2.12.0
- v2.11.3
- v2.11.2
- v2.11.1
- v2.11.0
- v2.10.3
- v2.10.2
- v2.10.1
- v2.10.0
- v2.9.4
- v2.9.3
- v2.9.2
- v2.9.1
- v2.9.0
- v2.8.3
- v2.8.2
- v2.8.1
- v2.8.0
- v2.7.1
- v2.7.0
- v2.6.1
- v2.6.0
- v2.5.2
- v2.5.1
- v2.5.0
- v2.4.1
- v2.4.0
- v2.3.19
- v2.3.18
- v2.3.17
- v2.3.16
- v2.3.15
- v2.3.14
- v2.3.13
- v2.3.12
- v2.3.11
- v2.3.10
- v2.3.9
- v2.3.8
- v2.3.7
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.0
- v2.1.0
- v2.0.0
- v1.2.0
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
This package is auto-updated.
Last update: 2026-10-31 09:59:35 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']); });
Полезные трейты
BaseEnumTrait
Трейт для PHP-enum: локализация и рендеринг. Подключается в перечисление:
enum SystemStereotypeEnum: string { use BaseEnumTrait; case Typical = 'typical'; case Real = 'real'; }
Чтобы заработал перевод, надо добавить в файл /lang/<lang>/enums.php ключи по классу enum. Поддерживаются оба формата — по значению кейса и по его имени:
return [ 'names' => [ SystemStereotypeEnum::class => [ SystemStereotypeEnum::Typical->value => 'Типовая система', SystemStereotypeEnum::Real->value => 'Реальная система', // либо по имени кейса 'Typical' => 'Типовая система', ], ], 'descriptions' => [ SystemStereotypeEnum::class => [ SystemStereotypeEnum::Typical->value => 'Описание типовой системы', ], ], ];
Методы трейта:
| Метод | Описание |
|---|---|
list(array $args = []) |
все значения enum в виде {id, name, description?} |
values() |
массив значений всех кейсов |
trans(array $args = []) |
локализованное название |
desc(array $args = []) |
локализованное описание |
render(?string $renderIdAs = null) |
{id, name, description?}; $renderIdAs — null/'string'/'int' |
fromTrans(string $translation) / tryFromTrans(?string $translation) |
поиск кейса по локализованному названию |
getNamespace() переопределяется для префикса namespace переводов (например, 'foundation::'), а getIdTypeForRender() — для типа id по умолчанию в render().
Трейт также используется в AbstractResource::getEnum(), при сортировке по enum-полям и при поиске через RepositoryBuilder::queries().
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, в нем определены основные поля, используемые при получении списков с фильтрацией и пагинацией (page, perPage, sort, filters, queries, select, q), а также properties — список запрошенных клиентом полей для PropertyTransformer.
Для подключения валидации надо добавить \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();
Основные возможности RepositoryBuilder:
filters(array $filters)— фильтрация по полям и связям (операторы через@, JSON-поля, pivot-связи).query(string $q, array $queryableFields)— глобальный поиск по списку столбцов.queries(array $queries)— точечный поиск по конкретным полям. Ключ может содержать несколько полей через|, а также путь по связи через точку:
UserRepository::query() ->queries([ 'code|login' => 'user', // (code ILIKE %user%) OR (login ILIKE %user%) 'organization.name' => 'Росс', // whereHas('organization', name ILIKE %Росс%) ]) ->get();
Если поле модели имеет enum-каст на класс с BaseEnumTrait, поиск выполняется по локализованным названиям значений enum.
orderBy(array $orderBy)— сортировка, в том числе по связям (organization.name).limit()/offset()/paginate()— постраничная выборка.sum($column),count(),exists(),get(),find()— получение результата.toQuery()— внутреннийLaravel\Foundation\Cache\Builderс применённой пагинацией и настройками кэша.getQueryBuilder()— внутренний builder в текущем состоянии, без подготовки пагинации и кэша.
Методы Eloquent Builder (where, with, whereHas, select, join, addSelect и т.д.) можно вызывать напрямую — они проксируются на внутренний builder.
Трансформация вычисляемых свойств
PropertyTransformer ограничивает передаваемые клиенту данные: клиент сам указывает список properties, а трансформер вычисляет минимально необходимый select/with и досчитывает вычисляемые поля.
Репозиторий сущности должен реализовать интерфейс TransformsProperties:
use Laravel\Foundation\Abstracts\AbstractRepository; use Laravel\Foundation\Repository\TransformsProperties; class UserRepository extends AbstractRepository implements TransformsProperties { public static function getPropertyConfig(): array { return [ // код вычисляемого поля 'full_name' => [ // связи, которые нужно загрузить (попадут в with) 'with' => ['profile'], // дополнительные столбцы основной таблицы (попадут в select) 'select' => ['first_name', 'last_name'], // расчёт значения: (модель, параметры setParameters) => значение 'source' => function (User $user, array $parameters) { return trim($user->first_name . ' ' . $user->last_name); }, ], ]; } }
Использование в сервисе:
$transformer = new PropertyTransformer($params->properties ?? [], UserRepository::class); $paginator = UserRepository::query() ->with($transformer->getWith()) ->select($transformer->getSelect()) ->paginate(); return $transformer ->setParameters([ // параметры, доступные внутри source-замыканий ]) ->transform($paginator); // AbstractModel | Collection | LengthAwarePaginator
Что делает PropertyTransformer:
- Простые поля модели попадают в
select, связи — вwith. Для корневыхbelongsTo-связей вselectавтоматически добавляется внешний ключ. - Поля из
getPropertyConfig()регистрируются как вычисляемые, а ихselect/withдобавляются в запрос. setParameters()задаёт контекст дляsource-замыканий.transform()рассчитывает вычисляемые поля и удаляет связи, загруженные только для расчёта, не трогая связи, запрошенные напрямую.
Если репозиторий не наследует AbstractRepository или не реализует TransformsProperties, конструктор выбросит исключение.
Ресурс
Ресурсы делаются под каждую возвращаемую сущность (обычно это модели). Все ресурсы наследуются от 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 в структуру вида {id:<значение enum>,name:<название enum>}. Для получения названия
вызывается метод trans трейта BaseEnumTrait.
Если enum не расширен этим трейтом, то будет ошибка NeedExtendedEnumException.
Метод также корректно работает с вычисляемыми полями, добавленными PropertyTransformer, и не выводит поля, отсутствующие в выборке.
Метод $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, задать в нем свою логику обработки элемента коллекции.