Backend, Frontend, Weekend
При разработке API на Laravel часто возникает необходимость тестировать валидацию входящих данных. Один из способов — вручную писать тесты с различными вариантами входных параметров. Однако этот процесс может быть трудоемким и рутинным.
Чтобы упростить задачу, я разработал пакет laravel-request-testdata, который автоматически создает тестовые данные на основе правил валидации Laravel Request.
Ссылка на репозиторий – https://github.com/prog-time/laravel-request-testdata
Рассмотрим стандартный Laravel Request с простыми правилами:
class SimpleRequest extends FormRequest
{
public function rules(): array
{
'name' => 'required|string',
'email' => 'required|email',
}
}
Теперь используем laravel-request-testdata для получения тестовых данных:
use App\Http\Requests\SimpleRequest;
use ProgTime\RequestTestData\RequestDataGenerator;
$request = new SimpleRequest();
$testData = RequestDataGenerator::generate($request);
Выходные данные могут выглядеть так:
[
'name' => 'May Walker',
'email' => 'ola.lagua@example.com',
]
Проанализировав передаваемый Request класс, модуль возвращает массив с параметрами для запроса. Полученные данные вы можете использовать в авто-тестах вашего приложения.
При таком подходе вам нужно меньше следить за актуальностью тестов при редактирование правил валидации + это избавляет вас от необходимости вручную прописывать тестовые данные.
В процессе планирования разработки модуля я долго изучал вариации правил валидации в Laravel и постарался описать все возможные кейсы правил валидации.
Данный модуль может обрабатывать правила валидации в разном формате.
Вы можете описать правила в виде строки, как это было сделано в предыдущих примерах, а можете передать массив со списком параметром:
[
'name' => ['required', 'string', 'min:3', 'max:50'],
'age' => ['nullable', 'integer', 'min:18'],
'password' => ['required', 'string', 'min:8'],
]
По мимо типичных правил валидации, модуль также понимает такие правила, как: in, exists, unique и так далее.
[
'status' => 'required|string|in:pending,approved,rejected',
'category_id' => 'required|exists:categories,id',
'email' => 'required|email|unique:users,email',
]
В некоторых Request классах правила валидации описываются в формате Rule конструкций. Это может быть Rule::unique для проверки на уникальность или Rule::in для проверки на соответствие конкретным значениям.
[
'email' => ['required', 'email', Rule::unique('users')],
'role' => ['required', Rule::in(['admin', 'user', 'moderator'])],
]
Что касается валидации загружаемых файлов, то тут всё немного сложнее. На данный момент мой модуль может сгенерировать файлы таких типов, как: yml, xml, svg, sql, png, log, json, jpg, html, gif и csv.
Количество доступных форматов будет постепенно увеличиваться, по мере развития данного модуля.
Для данной проблемы есть обходной вариант, который мы рассмотрим ниже.
Бываю моменты когда вам нужно для тестирования передавать свои данные, которые более корректно смогут настроить проверку работы приложения.
Для этого необходимо в Request классе создать метод requestTestData(). Данный метод должен возвращать параметры запроса с заполненными тестовыми данными.
public function requestTestData(): array
{
$faker = \Faker\Factory::create();
return [
'email' => $faker->email(),
'age' => 25,
];
}
Через метод requestTestData() вы также можете передавать тестовые файлы в форматах, которые на данный момент не поддерживаются моим модулем.
Таким образом я постарался разработать полезный модуль, который позволит сократить время на написание тестов и облегчит поддержку уже написанных авто-тестов.
Данный модуль не требует дополнительной настройки, его легко можно установить через composer и использовать в своих тестах.
Я надеюсь вам понравилось моё решение. Я буду очень благодарен если вы поддержите данный модуль звёздочкой на GitHub и напишите свой комментарий под данным постом.
Спасибо за то что дочитали данный пост до конца!
Если вы видите это, значит, я еще не придумал, что написать.
В нашей продуктовой компании используется много Laravel проектов, и сервисов которые решают свои задачи. Под каждый проект мы создавали поддомены, например:
При этом каждые проекты разделены под разные региональные зоны: .ru
, .by
, .kz
, .tr
. А также у нас существует несколько брендов, в каждой региональной зоне, это привело к тому, что проекты начали занимать множество доменов и поддоменов и каждый раз, когда нам нужно развернуть новый проект или провести эксперимент – приходилось мучаться с созданием новых доменов, выпуском сертификатов и настройкой логирования.
Мы устали плодить поддомены под каждый проект и решили перевести всё на единый домен domain.tech
, где региональность и бренд задаётся поддоменом третьего уровня, а проекты разделены внутри этого домена.
Было: {project}.{brand}.domain.{region}
Стало: {brand}-{region}.domain.tech/{project}
Как могло быть еще (для нас это показалось не так удобно): {brand}.domain.tech/{region}/{project}
Плюсы
domain.tech
.Минусы
Чтобы перейти на единую доменную систему – необходимо настроить nginx правильным образом: роутинг по приложениям осуществляется при помощи location, в рамках одного server_name
Пример конфигурации nginx:
server {
listen 80;
listen 443 ssl;
server_name *.domain.tech;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location /api/ {
proxy_pass http://api-container;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /api;
}
location /call-center/ {
proxy_pass http://call-center-container;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /call-center;
rewrite ^/call-center/(.*)$ /$1 break;
}
location /office/ {
proxy_pass http://office-container;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /api;
}
}
Здесь мы проксируем запрос на основе названия проекта и передаём запрос в контейнер (или fpm) нужного нам, при этом убираем из запроса название проекта, чтобы Laravel в проекте штатно обрабатывал роуты, якобы запрос пришёл на корень проекта. Это позволит мигрировать текущие проекты и разворачивать локально как обычно, без каких либо сложностей и имитации распределенной URL структуры.
Важно прокинуть заголовок X-Forwarded-Path
, по этому заголовку Laravel сможет понять, как правильно генерировать ссылки для Frontend части и редиректов. Чтобы ваш Laravel проект начал доверять этому заголовку – нужно настроить TrustProxies.
Проверьте, что у вас включен middleware \Illuminate\Http\Middleware\TrustProxies::class
Добавьте проверьте настройки config/trustedproxy.php
<?php
use Symfony\Component\HttpFoundation\Request;
return [
'proxies' => [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.0/8',
],
'headers' => Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_PREFIX |
Request::HEADER_X_FORWARDED_AWS_ELB
];
В headers
должен быть разрешён заголовок HEADER_X_FORWARDED_PREFIX
.
В proxies
можно указать маски всех локальных IP адресов, так не придётся мучаться с настройками динамических сетей.
Если ваш nginx находится не в локальной сети, то добавьте IP адрес сервера nginx в ваш список proxies
Нужно правильно задать APP_URL
, вписав туда фактическую ссылку на проект, это позволит генерировать правильные ссылки через CLI окружение (консоль, команды, очереди).
Теперь нужно проверить, чтобы во всех шаблонах для путей статических файлов использовался хелпер asset()
, если поизучать исходники этой функции, то вы увидите, что генерация ссылок происходит с использованием заголовка X-Forwarded-Path
, X-Forwarded-Proto
, X-Forwarded-Host
именно для этого мы и прокидывали все эти заголовки.
Ссылки должны строиться через хелпер url()
, это позволит точно также генерировать правильные ссылки внутри проекта.
После того, как мы убедились, что для статических файлов мы используем asset()
, а для внутренних путей url()
, нужно заменить все глобальные пути в JS файлах на относительные. Например если мы отправляли запросы таким образом: axios.get('/api/entity')
, это нужно переделать вот так: axios.get('api/entity')
Тоже самое нужно проделать во всех href="/example"
и src="/example"
атрибутах, например для <a>
, <script>
и <link>
если по каким-то причинам мы не смогли использовать asset()
и url()
хелперы. Такое может случиться, если ссылки на статические файлы нам приходят из другого проекта, а сами статические файлы хранятся в текущем проекте.
После того, как мы перевели ссылки с глобальных на статические – необходимо в начало тега <head>
прописать атрибут <base href="url('/')/">
этот лайфках позволит работать с относительными ссылками из корня проекта.
У вас могут начаться проблемы с css файлами, в которых ссылки на статические изображения забиты внутри public css файла, например background-image: url(assets/img/example.png)
. Чтобы решить эту проблему – перенесите генерацию стилей в vite или vue файлы, тогда css и assets файлы будут находится в одной build директории и у вас получится решить эту проблему. Если изображений несколько и они небольшие (например несколько иконок), то вы можете не использовать подгрузку отдельного файла с изображением, а представить в виде base64 ссылки или svg прямо внутри css url()
хелпера.
Для того, чтобы собранные файлы из public/build стали относительными – нужно задать относительный путь в base параметре vite:
export default {
base: './',
// ...
}
Если вы начинаете новый проект, то я советую вам изначально для ваших frontend путей использовать BASE_URL
, пусть оно будет всегда по умолчанию '/'
, но в таком случае вы не столкнётесь с множеством проблем, если ваш проект будет хоститься не из корня домена. BASE_URL
мы можете передавать из проекта, самым примитивным образом из <head>
тега:
<script>window.BASE_URL = "{{ url('/') }}"; </script>
Также полезным будет иметь такую хелпер функцию, похожую на Laravel:
function url(path) {
const origin = window.location.origin;
// Очищаем лишний первый и последний слеши
const baseUrl = (window.BASE_URL ?? '').replace(/^\/|\/$/g, '');
// Очищаем лишний первый слеш
path = (path ?? '').replace(/^\//g, '');
// Выводим 3 компонента, разделяя слешами
return [origin, baseUrl, path].join('/');
}
Этой функцией вы сможете также как и на Laravel генерировать ссылки на Frontend части вашего приложения, относительно базового URL.
Мой Telegram канал 👉 https://t.me/agoalofalife_channel
В жизни каждого программиста возникает тот момент когда необходимо выбрать рекурсию или цикл. Почти с большей вероятностью в жизни каждого разработчика стоит выбор между rm
или rm -f
в консоли. Но! Без сомнений для каждого web-разработчика был опыт в взаимодействия по API с внешними системами или с другой командой. Проще говоря, у вас есть API и вам надо написать код – который будет принимать данные и отправлять их.
Так вот … у каждого web ремесленника, спустя годы может выработаться свое видение как реализовывать такие вещи и сегодня я хочу добавить в общий котел инструментов – библиотеку для интеграции с API и дать краткое представление об этой “альтернативе”.
Барабанная дробь… Тишина … кулисы открылись и вот он: Saloon !
Пакет, который поможет вам сделать свой SDK или интеграцию с внешней системой. Что собственного в нем такого интересного – рассмотрим с “высоты птичьего полета”.
Под капотом у библиотеки используется Guzzle, что не удивительно.
Первое что стоит выделить – это структура всей “этой истории”. Структура врочем важна как и во многих вещах в коде.(и не только в коде кстати 😊).
Например, в вашем любимом фреймворке есть структура(папки, названия классов и т.д) – если вы им пользуетесь то другой разработчик ожидает ее увидеть(так конечно не всегда – но часто должно быть эх…). В этом пакете она тоже есть и это его преимущество. Вверху иерархии находиться Connectors
там определяется базовая логика и данные. Например, что там может быть:
<?php
use Saloon\Http\Connector;
class ForgeConnector extends Connector
{
// попытки отправлять
public ?int $tries = 3;
// интервал в ms через который пытаться
public ?int $retryInterval = 500;
public function __construct(public readonly string $token) {}
public function resolveBaseUrl(): string
{
return 'https://forge.laravel.com/api/v1'; // можно взять через .env
}
protected function defaultHeaders(): array
{
return [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
}
// добавляем заголовки для аутентификации
protected function defaultAuth(): BasicAuthenticator
{
return new BasicAuthenticator($this->username, $this->password);
}
// можно написать свою стратегию когда надо делать retry а когда нет
// еще и перед retry переключиться на fallback решение или что-то
// в заголовки добавить
public function handleRetry(
FatalRequestException|RequestException $exception,
Request $request,
): bool
{
if ($exception instanceof RequestException &&
$exception->getResponse()->status() === 401
){
$request->authenticate(
new TokenAuthenticator($this->getNewToken())
);
}
return true;
}
}
Далее – частью этой структуры в коде являются Requests
– каждый класс это отдельный запрос, со своим path
в url, GET аргументами в query url или в body если например у вас POST запрос.
<?php
use Saloon\Enums\Method;
use Saloon\Http\Request;
class GetServersRequest extends Request
{
protected Method $method = Method::GET;
public function resolveEndpoint(): string
{
return '/servers';
}
// массив сам подставиться в query url
protected function defaultQuery(): array
{
return [
'sort' => 'name', // ?sort=name
'filter[active]' => 'true', // ?filter[active]=true
];
}
}
В дополнении, можно выбрать формат отправки в body XML or JSON а может что еще – я на текущий момент кроме JSON ничего и не использовал.
Обработка ответа после запроса это отдельная тема. В пакете есть отдельный класс с различными методами. Там в целом ничего нового. НО:
->isOk()
->json()
и так далее.<?php
use Saloon\Http\Request;
use Saloon\Http\Response;
class GetServerRequest extends Request
{
// {...}
// метод который определяет создания Dto
public function createDtoFromResponse(Response $response): Server
{
$data = $response->json();
return new Server(
id: $data['id'],
name: $data['name'],
ipAddress: $data['ip'],
);
}
}
А вот пример вызовы в клиентском коде
<?php
$connector = new ForgeConnector;
$response = $connector->send(new GetServerRequest(id: 12345));
// Возращаем свое dto которые мы сконфигурировали выше
/** @var Server $server */
$server = $response->dto();
Тут конечно нюанс в виде комментария(аннотации) сверху. Так как type из метода “универсальный” – вам надо будет добавлять такие комментарии – чтобы ide понимала о каком типе идет речь.
Ну а далее все прекрасно – вы в этом Dto пишите свои методы которые красиво и изящно вам выдают информацию в вашей бизнес модели.
Об этом немало важном нюансе тоже подумали создатели библиотеки. Обработка ошибок есть в двух видах:
getRequestException
. Что еще мне понравилось, это отдельный метод для определения – был ли запрос failed или нет. Как в отдельном Request классе, так и в Connector по усмотрению.<?php
use Saloon\Http\Request;
use Saloon\Http\Response;
class ErrorRequest extends Request
{
// {...}
public function hasRequestFailed(Response $response): ?bool
{
return str_contains($response->body(), 'Server Error');
}
}
Все мы знаем какие бывают API и иногда полезно пометить запрос Failed – даже если статус 200. 😁
Пример 👇
<?php
$forge = new ForgeConnector;
$response = $forge->send(new ErrorRequest);
$response->failed(); // true
$response->status(); // 500
$response->body(); // {"message": "Server Error"}
Мониторинг важная часть и пару копеек о ней в этом пакете.
Сам автор в документации предлагает логировать через дополнительный функционал плагинов. Вы создаете trait а потом его добавляете или в глобальный connector
или в класс конкретного request
. Далее уже магия за кулисами автоматически вызовет методы у которых prefix начинается c boot. (где-то я уже видел??)
Второй вариант это использовать middleware – знакомая большинству конструкция – которая может перехватывать вызовы до и после. На моем личном опыте я не использовал оба варианта. В дополнение к вышеперечисленным способам – есть еще plugin для интеграции с laravel. Я “подцепился” на события при отправке и получении запроса и далее уже писал в лог информацию.
Документация здесь
Если вы ранее работали с тестами http client в Laravel – тот тут похожая концепция. Мы можем делать “моки” для ответов по url(так же sequence – проще говоря написать несколько ответов). Так и проверять что мы отправили в requests.
Пример 👇
use Saloon\Http\Faking\MockClient;
test('my test', function () {
$mockClient = new MockClient([
GetServersRequest::class => MockResponse::make(body: '', status: 200),
]);
$connector = new ForgeConnector;
$connector->withMockClient($mockClient);
]);
Все выглядит очень похоже как в laravel
Помимо ключевых возможностей есть еще множество дополнительных функций которые будут полезны:
я не пользовался но из документации выглядить неплохо
В целом мне библиотека понравилась своей готовой структурой и экосистемой для различных случаев. Конечно может чего-то не хватать – но ее можно расширить за счет различных механизмов типа middleware, plugins – в добавок есть интеграция с Laravel. Учитываете что вам придется использовать Pest для тестирования (особо ничего сложного – просто нюанс).
Еще мне не понравилась работа в клиентском коде. Вам придется создавать для каждого запроса (может есть альтернативы и я плохо смотрел, если нашли напишите в комментариях!) объект и писать следующую конструкцию.
<?php
$forge = new ForgeConnector;
$request = new GetServersRequest;
$response = $forge->send($request);
$status = $response->status();
$body = $response->body();
Безусловно мы можем прокидывать ForgeConnector
через container и, но создавать каждый request объект не выглядит как код SDK client моей мечты. Поэтому вы можете создать обертку над всем этим и прокинуть только тот метод, который будет явно указывать что делать и какие аргументы передать. Например, так
<?php
//.. получили $client из container
// а внутри уже все что выше инкапсулированно
$client->getServers();
<?php
class Client {
// реализация в вашей обертке ...
public function getServers(): Response {
$request = new GetServersRequest;
return $forge->send($request);
}
}
{message}