MidТеория10 min

Запросы и ответы

Request class, получение данных, файлы, Form Requests, типы ответов (JSON, redirect, download, stream)

HTTP Request

Laravel оборачивает входящий HTTP-запрос в объект Illuminate\Http\Request, который расширяет Symfony HttpFoundation\Request.

Получение экземпляра Request

<?php
declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

final class UserController extends Controller
{
    // Method 1: Type-hint in controller method (recommended)
    public function store(Request $request): JsonResponse
    {
        $name = $request->input('name');
        return response()->json(['name' => $name]);
    }

    // Method 2: Type-hint in route closure
    // Route::post('/users', function (Request $request) { ... });

    // Method 3: Global helper
    // $request = request();
    // $name = request('name');
    // $name = request()->input('name');
}

Информация о запросе

<?php
declare(strict_types=1);

use Illuminate\Http\Request;

function inspectRequest(Request $request): array
{
    return [
        // URL information
        'path' => $request->path(),               // "users/1" (without domain)
        'url' => $request->url(),                  // "http://example.com/users/1"
        'fullUrl' => $request->fullUrl(),           // "http://example.com/users/1?page=2"
        'fullUrlWithQuery' => $request->fullUrlWithQuery(['sort' => 'name']),

        // HTTP method
        'method' => $request->method(),            // "GET", "POST", etc.
        'isGet' => $request->isMethod('get'),      // true/false
        'isPost' => $request->isMethod('post'),    // true/false

        // Request checks
        'is_ajax' => $request->ajax(),             // X-Requested-With: XMLHttpRequest
        'is_json' => $request->isJson(),           // Content-Type contains json
        'expects_json' => $request->expectsJson(), // Accept header wants JSON
        'is_secure' => $request->secure(),         // HTTPS
        'is_prefetch' => $request->prefetch(),     // Prefetch request

        // Client info
        'ip' => $request->ip(),                    // Client IP
        'ips' => $request->ips(),                  // All IPs (with proxies)
        'userAgent' => $request->userAgent(),      // Browser user agent

        // Headers
        'accept' => $request->header('Accept'),
        'bearer' => $request->bearerToken(),       // Authorization: Bearer <token>
        'content_type' => $request->header('Content-Type'),

        // Route info
        'route_name' => $request->route()?->getName(),
        'route_params' => $request->route()?->parameters(),
    ];
}

Получение входных данных

<?php
declare(strict_types=1);

use Illuminate\Http\Request;

function getInputData(Request $request): void
{
    // === BASIC INPUT ===

    // Get single value (from any source: query, post, json)
    $name = $request->input('name');
    $name = $request->input('name', 'default'); // With default

    // Dot notation for nested data
    $city = $request->input('address.city');

    // Array input
    $names = $request->input('users.*.name'); // From users array

    // Get ALL input
    $all = $request->all(); // All input including query string

    // Get input from QUERY STRING only
    $page = $request->query('page');
    $page = $request->query('page', 1); // With default
    $allQuery = $request->query(); // All query params

    // === DYNAMIC PROPERTIES ===

    // Access input as property (checks input, then route params)
    $name = $request->name;

    // === SELECTIVE RETRIEVAL ===

    // Get only specified keys
    $data = $request->only(['name', 'email']);
    // Returns: ['name' => 'John', 'email' => '[email protected]']

    // Get all EXCEPT specified keys
    $data = $request->except(['password', '_token']);
    // Returns all input except password and _token

    // === PRESENCE CHECKS ===

    // Check if input exists (even if empty string)
    $exists = $request->has('name');        // true if key exists
    $exists = $request->has(['name', 'email']); // true if ALL exist

    // Check if ANY of the keys exist
    $exists = $request->hasAny(['name', 'nickname', 'username']);

    // Check if exists AND not empty
    $filled = $request->filled('name');     // true if exists and not empty

    // Check if missing or empty
    $missing = $request->isNotFilled('name');

    // Execute callback when present
    $request->whenHas('coupon', function (string $coupon) {
        // Apply coupon
    });

    // Execute callback when filled (not empty)
    $request->whenFilled('coupon', function (string $coupon) {
        // Apply coupon
    }, function () {
        // Coupon is empty or missing
    });

    // Check if key is missing entirely
    $missing = $request->missing('key');

    // === TYPE-SAFE RETRIEVAL ===

    $request->string('name');    // Returns Stringable
    $request->integer('page');   // Returns int
    $request->float('amount');   // Returns float
    $request->boolean('active'); // Returns bool (handles "1", "true", "on", "yes")
    $request->date('birthday');  // Returns Carbon instance
    $request->date('birthday', 'd.m.Y'); // With format
    $request->enum('status', OrderStatus::class); // Returns enum or null

    // Collection of input
    $request->collect('items'); // Returns Collection
    $request->collect();       // All input as Collection
}

Ловушка экзамена: $request->has('key') возвращает true даже для пустой строки -- он проверяет только наличие ключа. $request->filled('key') возвращает true только если ключ присутствует И значение не пустое. $request->boolean() корректно обрабатывает "1", "true", "on", "yes" как true.

Работа с JSON-данными

<?php
declare(strict_types=1);

use Illuminate\Http\Request;

function handleJsonInput(Request $request): void
{
    // When Content-Type is application/json,
    // input() automatically decodes JSON body

    // POST body: {"user": {"name": "John", "roles": ["admin", "editor"]}}

    $name = $request->input('user.name');         // "John"
    $roles = $request->input('user.roles');        // ["admin", "editor"]
    $firstRole = $request->input('user.roles.0');  // "admin"

    // Get raw JSON body
    $json = $request->json();        // ParameterBag instance
    $name = $request->json('user.name'); // Dot notation

    // Get raw content
    $rawBody = $request->getContent(); // Raw request body string
}

Загрузка файлов

<?php
declare(strict_types=1);

use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;

function handleFileUpload(Request $request): void
{
    // Check if file was uploaded
    $hasFile = $request->hasFile('photo');

    // Get uploaded file
    /** @var UploadedFile|null $photo */
    $photo = $request->file('photo');

    if ($photo && $photo->isValid()) {
        // File properties
        $originalName = $photo->getClientOriginalName(); // "photo.jpg"
        $extension = $photo->getClientOriginalExtension(); // "jpg"
        $mimeType = $photo->getMimeType();                // "image/jpeg"
        $size = $photo->getSize();                         // bytes
        $hashName = $photo->hashName();                    // Random name: "aB3f...jpg"

        // Store file — returns relative path
        $path = $photo->store('photos');
        // Saves to: storage/app/photos/randomname.jpg
        // Returns: "photos/randomname.jpg"

        // Store with custom name
        $path = $photo->storeAs('photos', 'profile.jpg');
        // Saves to: storage/app/photos/profile.jpg

        // Store to specific disk
        $path = $photo->store('photos', 's3');

        // Store publicly
        $path = $photo->storePublicly('photos', 's3');

        // Store in public disk
        $path = $photo->store('avatars', 'public');
        // Accessible at: /storage/avatars/randomname.jpg
    }

    // Multiple files
    if ($request->hasFile('documents')) {
        /** @var UploadedFile[] $files */
        $files = $request->file('documents');

        foreach ($files as $file) {
            $file->store('documents');
        }
    }
}

Form Requests (валидация в отдельном классе)

Form Request -- это отдельный класс для инкапсуляции логики валидации и авторизации.

# Create a Form Request
php artisan make:request StoreUserRequest
<?php
declare(strict_types=1);

namespace App\Http\Requests;

use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;

final class StoreUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        // Return true to allow all users
        // Return false to reject with 403
        // Use Gate/Policy for complex authorization
        return $this->user()?->can('create', User::class) ?? false;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
     */
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' => [
                'required',
                'email:rfc,dns',
                Rule::unique('users', 'email'),
            ],
            'password' => [
                'required',
                'confirmed',
                Password::min(8)
                    ->letters()
                    ->mixedCase()
                    ->numbers()
                    ->symbols()
                    ->uncompromised(),
            ],
            'role' => ['required', Rule::in(['user', 'editor', 'admin'])],
            'avatar' => ['nullable', 'image', 'max:2048', 'dimensions:min_width=100'],
            'tags' => ['nullable', 'array', 'max:10'],
            'tags.*' => ['string', 'max:50'],
        ];
    }

    /**
     * Custom attribute names for error messages.
     */
    public function attributes(): array
    {
        return [
            'name' => 'имя пользователя',
            'email' => 'электронная почта',
            'password' => 'пароль',
        ];
    }

    /**
     * Custom error messages.
     */
    public function messages(): array
    {
        return [
            'name.required' => 'Имя обязательно для заполнения.',
            'email.unique' => 'Пользователь с таким email уже существует.',
            'password.confirmed' => 'Пароли не совпадают.',
        ];
    }

    /**
     * Prepare the data for validation.
     * Called BEFORE validation.
     */
    protected function prepareForValidation(): void
    {
        $this->merge([
            'email' => strtolower($this->email),
            'name' => trim($this->name),
        ]);
    }

    /**
     * Get the validated data with additional processing.
     * Called AFTER successful validation.
     */
    public function validated($key = null, $default = null): array
    {
        $data = parent::validated($key, $default);

        // Post-validation processing
        if (isset($data['password'])) {
            $data['password'] = bcrypt($data['password']);
        }

        return $data;
    }

    /**
     * Handle a failed authorization attempt.
     */
    protected function failedAuthorization(): void
    {
        throw new \Illuminate\Auth\Access\AuthorizationException(
            'You are not authorized to create users.'
        );
    }
}

Использование Form Request в контроллере

<?php
declare(strict_types=1);

namespace App\Http\Controllers;

use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\User;

final class UserController extends Controller
{
    // Form Request is automatically validated BEFORE method is called
    // If validation fails:
    //   - Web: redirect back with errors
    //   - API: JSON 422 response with errors
    public function store(StoreUserRequest $request): JsonResponse
    {
        // $request->validated() returns ONLY validated data
        $user = User::create($request->validated());

        return response()->json($user, 201);
    }

    public function update(UpdateUserRequest $request, User $user): JsonResponse
    {
        // safe() returns ValidatedInput object
        $safe = $request->safe();

        // Use only specific validated fields
        $user->update($safe->only(['name', 'email']));

        // Merge additional data with validated
        $data = $safe->merge(['updated_by' => auth()->id()]);

        return response()->json($user);
    }
}

Для экзамена: Form Request выполняет валидацию АВТОМАТИЧЕСКИ до вызова метода контроллера. Если валидация не проходит: для web-запросов -- redirect back с ошибками, для API-запросов (Accept: application/json) -- JSON-ответ с кодом 422. Метод authorize() проверяется ДО валидации.

Update Request с уникальным полем

<?php
declare(strict_types=1);

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class UpdateUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        // Get the user being updated from route parameter
        $userId = $this->route('user')->id;

        return [
            'name' => ['sometimes', 'string', 'max:255'],
            'email' => [
                'sometimes',
                'email:rfc,dns',
                // Ignore current user when checking uniqueness
                Rule::unique('users', 'email')->ignore($userId),
            ],
        ];
    }
}

HTTP Responses

Типы ответов

<?php
declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

final class ResponseExamplesController extends Controller
{
    // 1. String response
    public function stringResponse(): string
    {
        return 'Hello, World!';
        // Content-Type: text/html, Status: 200
    }

    // 2. Array response (auto-converted to JSON)
    public function arrayResponse(): array
    {
        return ['name' => 'John', 'age' => 30];
        // Content-Type: application/json, Status: 200
    }

    // 3. JSON response
    public function jsonResponse(): JsonResponse
    {
        return response()->json([
            'success' => true,
            'data' => ['name' => 'John'],
        ], 200, [
            'X-Custom-Header' => 'value',
        ]);
    }

    // 4. View response
    public function viewResponse(): View
    {
        return view('users.index', [
            'users' => User::paginate(15),
        ]);
    }

    // 5. Response with custom headers and cookies
    public function customResponse(): Response
    {
        return response('Content here', 200)
            ->header('Content-Type', 'text/plain')
            ->header('X-Custom', 'value')
            ->withHeaders([
                'X-Header-One' => 'value1',
                'X-Header-Two' => 'value2',
            ])
            ->cookie('name', 'value', 60); // 60 minutes
    }

    // 6. No content response (204)
    public function noContentResponse(): Response
    {
        return response()->noContent();
        // Status: 204, No body
    }
}

Redirect ответы

<?php
declare(strict_types=1);

use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Route;

function redirectExamples(): RedirectResponse
{
    // Redirect to URL
    return redirect('/home');

    // Redirect to named route
    return redirect()->route('users.show', ['user' => 1]);

    // Redirect to named route (shortcut)
    return to_route('users.show', ['user' => 1]);

    // Redirect back (to previous page)
    return redirect()->back();
    return back(); // Shortcut

    // Redirect back with input (for form re-population)
    return back()->withInput();

    // Redirect back with only specific input
    return back()->withInput($request->only('email'));

    // Redirect with flash data
    return redirect()
        ->route('users.index')
        ->with('success', 'User created!');

    // Redirect with multiple flash messages
    return redirect()
        ->route('users.index')
        ->with([
            'success' => 'User created!',
            'user_id' => 42,
        ]);

    // Redirect with errors
    return back()->withErrors([
        'email' => 'This email is already taken.',
    ]);

    // Redirect to controller action
    return redirect()->action([UserController::class, 'index']);

    // Redirect to external URL
    return redirect()->away('https://google.com');

    // Redirect with status code
    return redirect('/new-location', 301);

    // Intended redirect (after login)
    return redirect()->intended('/dashboard');
    // Redirects to the URL the user was trying to access before login
}

Download и File ответы

<?php
declare(strict_types=1);

use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;

function fileResponses()
{
    // Download a file
    return response()->download(
        storage_path('app/reports/report.pdf'),
        'monthly-report.pdf',  // Custom filename
        ['Content-Type' => 'application/pdf']
    );

    // Download and delete after sending
    return response()->download($path)->deleteFileAfterSend();

    // Display file inline (in browser, not download)
    return response()->file(
        storage_path('app/documents/contract.pdf'),
        ['Content-Type' => 'application/pdf']
    );

    // Stream download (generate on-the-fly)
    return response()->streamDownload(function () {
        echo "Name,Email,Role\n";

        User::chunk(100, function ($users) {
            foreach ($users as $user) {
                echo "{$user->name},{$user->email},{$user->role}\n";
            }
        });
    }, 'users.csv', [
        'Content-Type' => 'text/csv',
    ]);
}

Streamed Response

<?php
declare(strict_types=1);

use Symfony\Component\HttpFoundation\StreamedResponse;

function streamedResponse(): StreamedResponse
{
    // Stream response (useful for large data)
    return response()->stream(function () {
        $handle = fopen(storage_path('app/large-file.csv'), 'r');

        while (! feof($handle)) {
            echo fread($handle, 8192); // Read in 8KB chunks
            flush(); // Send to client immediately
        }

        fclose($handle);
    }, 200, [
        'Content-Type' => 'text/csv',
        'X-Accel-Buffering' => 'no', // Disable Nginx buffering
    ]);
}

Responsable Interface

<?php
declare(strict_types=1);

namespace App\Http\Responses;

use Illuminate\Contracts\Support\Responsable;
use Illuminate\Http\JsonResponse;

final readonly class ApiSuccessResponse implements Responsable
{
    public function __construct(
        private mixed $data,
        private string $message = 'Success',
        private int $status = 200,
        private array $meta = [],
    ) {}

    public function toResponse($request): JsonResponse
    {
        $response = [
            'success' => true,
            'message' => $this->message,
            'data' => $this->data,
        ];

        if ($this->meta !== []) {
            $response['meta'] = $this->meta;
        }

        return response()->json($response, $this->status);
    }
}

// Usage in controller:
// return new ApiSuccessResponse(
//     data: UserResource::collection($users),
//     message: 'Users fetched successfully',
//     meta: ['total' => $users->total()],
// );

Для Senior: Интерфейс Responsable позволяет возвращать любой объект из контроллера, если он реализует метод toResponse(). Laravel автоматически вызывает его для формирования ответа. Это мощный паттерн для стандартизации API-ответов.

Inline валидация

<?php
declare(strict_types=1);

use Illuminate\Http\Request;

function inlineValidation(Request $request): void
{
    // Method 1: validate() on Request
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'body' => 'required|string',
        'category_id' => 'required|exists:categories,id',
    ]);
    // Returns only validated data
    // Throws ValidationException on failure

    // Method 2: validateWithBag() for named error bags
    $validated = $request->validateWithBag('post', [
        'title' => 'required|string|max:255',
    ]);

    // Method 3: Validator facade (for custom error handling)
    $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
        'email' => 'required|email',
    ]);

    if ($validator->fails()) {
        // Custom error handling
        return redirect()->back()
            ->withErrors($validator)
            ->withInput();
    }

    $validated = $validator->validated();

    // Method 4: Validator with after hook
    $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
        'email' => 'required|email',
    ]);

    $validator->after(function ($validator) {
        if ($this->somethingElseIsInvalid()) {
            $validator->errors()->add('field', 'Something is wrong with this field!');
        }
    });
}

Проверь себя

Какой метод Request возвращает данные, приведённые к конкретному типу?

Когда выполняется метод `authorize()` в Form Request по отношению к валидации?

Что делает `redirect()->intended('/dashboard')` и когда его использовать?

Что возвращает контроллер при возврате массива (array)?

Чем отличается `$request->has('key')` от `$request->filled('key')`?