Представления (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');
}
}