MidТеория5 min

Views - представления в Laravel

Передача данных в представления, View Composers, View Creators, sharing data, метод first() и организация представлений в Laravel 11

Представления (Views) в Laravel отвечают за отображение данных пользователю. Они отделяют логику контроллера от слоя представления, следуя принципу MVC. В Laravel 11 представления хранятся в директории resources/views и обычно используют шаблонизатор Blade.

Создание и возврат представлений

Простейший способ вернуть представление из маршрута или контроллера:

// routes/web.php
Route::get('/welcome', function () {
    return view('welcome');
});

Представления могут быть организованы в поддиректории. Для обращения к вложенным представлениям используется "точечная" нотация:

// resources/views/admin/dashboard.blade.php
return view('admin.dashboard');

Проверка существования представления

use Illuminate\Support\Facades\View;

if (View::exists('admin.dashboard')) {
    return view('admin.dashboard');
}

Передача данных в представления

Массив данных

Самый распространённый способ - передача ассоциативного массива вторым аргументом:

return view('user.profile', [
    'user' => $user,
    'posts' => $posts,
    'stats' => $this->calculateStats($user),
]);

Метод with()

Цепочка вызовов with() позволяет добавлять данные поштучно:

return view('user.profile')
    ->with('user', $user)
    ->with('posts', $posts)
    ->with('isAdmin', true);

Хелпер compact()

PHP-функция compact() создаёт массив из переменных:

public function show(User $user): \Illuminate\View\View
{
    $posts = $user->posts()->latest()->paginate(15);
    $commentsCount = $user->comments()->count();
    $isFollowing = auth()->user()?->isFollowing($user) ?? false;

    // compact() creates ['posts' => $posts, 'commentsCount' => $commentsCount, ...]
    return view('user.profile', compact(
        'user',
        'posts',
        'commentsCount',
        'isFollowing'
    ));
}

Типизация возвращаемого значения

В Laravel 11 рекомендуется указывать тип возвращаемого значения:

use Illuminate\View\View;
use Illuminate\Http\RedirectResponse;

public function edit(int $id): View
{
    $article = Article::findOrFail($id);

    return view('articles.edit', compact('article'));
}

public function update(UpdateArticleRequest $request, int $id): RedirectResponse
{
    // ...
    return redirect()->route('articles.show', $id);
}

Метод first() - первое доступное представление

Метод View::first() возвращает первое существующее представление из массива. Это полезно для переопределения шаблонов пакетами или модулями:

use Illuminate\Support\Facades\View;

// Returns the first view that exists
return View::first(
    ['custom.dashboard', 'admin.dashboard', 'dashboard'],
    ['stats' => $stats]
);

Практический пример с мультитенантностью

declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\View\View;

final class PageController extends Controller
{
    public function show(string $slug): View
    {
        $page = Page::where('slug', $slug)->firstOrFail();
        $tenant = tenant();

        // Try tenant-specific view first, then fall back to default
        return view()->first([
            "tenants.{$tenant->slug}.pages.{$slug}",
            "tenants.{$tenant->slug}.pages.default",
            "pages.{$slug}",
            'pages.default',
        ], compact('page', 'tenant'));
    }
}

Sharing данных со всеми представлениями

Метод View::share() делает данные доступными во всех представлениях приложения. Обычно вызывается в сервис-провайдере:

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

final class ViewServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Available in ALL views as $appName
        View::share('appName', config('app.name'));
        View::share('supportEmail', config('mail.support_address'));

        // Dynamic shared data
        View::share('currentYear', now()->year);
    }
}

::alert{type="warning"} Внимание на экзамене: View::share() делает переменную доступной абсолютно во ВСЕХ представлениях. Злоупотребление этим методом может привести к проблемам с производительностью, если шарить тяжёлые данные. Для избирательной передачи данных используйте View Composers. ::

View Composers

View Composers - это коллбэки или методы классов, которые вызываются при рендеринге определённого представления. Они позволяют привязать данные к представлению без повторения кода в каждом контроллере.

Регистрация через замыкание

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

final class ViewServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Closure-based composer for a single view
        View::composer('layouts.sidebar', function (\Illuminate\View\View $view) {
            $view->with('recentPosts', Post::latest()->take(5)->get());
        });

        // Composer for multiple views using wildcard
        View::composer('admin.*', function (\Illuminate\View\View $view) {
            $view->with('pendingOrders', Order::pending()->count());
            $view->with('unreadNotifications', auth()->user()?->unreadNotifications()->count() ?? 0);
        });

        // Composer for specific set of views
        View::composer(
            ['dashboard', 'profile', 'settings'],
            function (\Illuminate\View\View $view) {
                $view->with('userStats', $this->getUserStats());
            }
        );
    }
}

Класс Composer

Для сложной логики рекомендуется выносить Composer в отдельный класс:

declare(strict_types=1);

namespace App\View\Composers;

use App\Repositories\NavigationRepository;
use Illuminate\View\View;

final class NavigationComposer
{
    public function __construct(
        private readonly NavigationRepository $navigation,
    ) {}

    /**
     * Bind data to the view.
     * This method is called every time the view is rendered.
     */
    public function compose(View $view): void
    {
        $view->with('mainMenu', $this->navigation->getMainMenu());
        $view->with('footerMenu', $this->navigation->getFooterMenu());
        $view->with('breadcrumbs', $this->navigation->getBreadcrumbs());
    }
}

Регистрация класса Composer:

use App\View\Composers\NavigationComposer;

// In ViewServiceProvider::boot()
View::composer('layouts.app', NavigationComposer::class);

// For all views (use with caution!)
View::composer('*', NavigationComposer::class);

Полный пример ViewServiceProvider

declare(strict_types=1);

namespace App\Providers;

use App\View\Composers\NavigationComposer;
use App\View\Composers\SidebarComposer;
use App\View\Composers\FooterComposer;
use App\View\Creators\AlertCreator;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;

final class ViewServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Class-based composers
        View::composer('layouts.app', NavigationComposer::class);
        View::composer('layouts.sidebar', SidebarComposer::class);
        View::composer(['layouts.footer', 'emails.*'], FooterComposer::class);

        // View Creator (called when view is instantiated, not rendered)
        View::creator('alerts', AlertCreator::class);

        // Closure composers
        View::composer('components.currency-selector', function (\Illuminate\View\View $view) {
            $view->with('currencies', cache()->remember(
                'currencies',
                now()->addHour(),
                fn () => Currency::active()->orderBy('code')->get()
            ));
        });

        // Global shared data
        View::share('appVersion', config('app.version'));
    }
}

View Creators

View Creators аналогичны Composers, но вызываются сразу при создании экземпляра представления, а не в момент рендеринга. Это ключевое различие на экзамене.

declare(strict_types=1);

namespace App\View\Creators;

use Illuminate\View\View;

final class AlertCreator
{
    /**
     * Called when the view is INSTANTIATED (before compose).
     */
    public function create(View $view): void
    {
        $view->with('alerts', session('alerts', []));
    }
}

Регистрация:

View::creator('alerts', AlertCreator::class);

::alert{type="info"} Composer vs Creator: Composer вызывается при рендеринге (compose), Creator - при создании экземпляра (create). На практике разница минимальна, но на экзамене это важно. ::

Оптимизация представлений

Кэширование представлений

# Compile all Blade templates
php artisan view:cache

# Clear compiled views
php artisan view:clear

Организация директорий

resources/views/
  layouts/
    app.blade.php
    admin.blade.php
    auth.blade.php
  components/
    alert.blade.php
    modal.blade.php
    form/
      input.blade.php
      select.blade.php
  pages/
    home.blade.php
    about.blade.php
  admin/
    dashboard.blade.php
    users/
      index.blade.php
      edit.blade.php
  emails/
    welcome.blade.php
    invoice.blade.php
  errors/
    404.blade.php
    500.blade.php

Возврат представлений с HTTP-статусами

// Return view with 200 status (default)
return view('pages.home');

// Return view with custom status and headers
return response()
    ->view('errors.maintenance', ['retryAfter' => 3600], 503)
    ->header('Retry-After', '3600');

// Return view with specific content type
return response()
    ->view('feeds.rss', ['articles' => $articles])
    ->header('Content-Type', 'application/rss+xml');

Передача данных из маршрутов

Route::get('/about', function () {
    return view('pages.about', [
        'team' => User::where('is_team_member', true)->get(),
        'version' => app()->version(),
    ]);
});

// Shorter syntax with view() route
Route::view('/about', 'pages.about', [
    'pageTitle' => 'About Us',
]);

::alert{type="warning"} Route::view() удобен для статических страниц, но данные передаются как литералы. Нельзя использовать динамические вызовы (Eloquent-запросы) в третьем аргументе Route::view(). ::

Тестирование представлений

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\User;
use Tests\TestCase;

final class ProfileViewTest extends TestCase
{
    public function test_profile_view_receives_user_data(): void
    {
        $user = User::factory()->create(['name' => 'John Doe']);

        $response = $this->actingAs($user)->get('/profile');

        $response->assertStatus(200);
        $response->assertViewIs('user.profile');
        $response->assertViewHas('user', $user);
        $response->assertViewHas('user', fn (User $viewUser) => $viewUser->id === $user->id);
    }

    public function test_dashboard_has_shared_data(): void
    {
        $response = $this->actingAs(User::factory()->create())
            ->get('/dashboard');

        $response->assertViewHas('appName');
        $response->assertViewHasAll(['user', 'stats', 'notifications']);
        $response->assertViewMissing('adminPanel');
    }
}

Проверь себя

Какой метод вызывается в классе View Composer для привязки данных?

В чём ключевое отличие View Creator от View Composer?

Что произойдёт при вызове Route::view('/about', 'pages.about', ['users' => User::all()])?

Что вернёт метод View::first(['custom.page', 'default.page'], $data)?

Какой метод используется для передачи данных во ВСЕ представления приложения?