قوالب Laravel ومحرك قوالب Blade
Blade هو "محرك التخطيط" في Laravel — هنا تتحول البيانات إلى الصفحات التي يراها المستخدمون، وتقضي التخطيطات والمكونات والوراثة على الكود المكرر.
1. ما ستتعلمه
- وراثة تخطيط Blade: @extends و@section و@yield
- هياكل التحكم: @if/@foreach/@switch/@auth/@guest
- مكونات Blade: مكونات صنفية مقابل مكونات مجهولة
- View Composers: مشاركة البيانات
- أصول الواجهة الأمامية: Vite يجمع Tailwind CSS / Alpine.js
2. قصة حقيقية لمطور واجهة أمامية
(1) المشكلة: الاضطرار لكتابة شريط التنقل بشكل متكرر في كل صفحة
كتب Charlie 10 صفحات لـ ShopMetrics، ينسخ يدويًا 60 سطرًا من HTML لشريط التنقل في كل صفحة. عندما طلب Bob نقل رابط "التسعير" من شريط التنقل إلى قائمة منسدلة، كان على Charlie تعديل جميع الملفات العشرة واحدًا تلو الآخر — لكن عندما وصل إلى الملف السابع، فاته صنف، مما تسبب في عدم تناسق أنماط شريط التنقل على الموقع المباشر، مما أدى إلى ثلاث شكاوى من Alice.
(2) حلول تخطيط Blade
يستخدم Blade @extends لوراثة التخطيط و@section لملء المحتوى؛ شريط التنقل يُعرَّف مرة واحدة فقط ويتزامن تلقائيًا عبر الموقع بأكمله.
// resources/views/layouts/app.blade.php — تعريف التخطيط مرة واحدة
<body>
<nav>...</nav> <!-- التنقل يظهر في كل صفحة -->
@yield('content') <!-- كل صفحة تملأ هذا الموضع -->
</body>
// resources/views/shops/index.blade.php — وراثة وملء
@extends('layouts.app')
@section('content')
<h1>جميع المتاجر</h1>
@endsection
(3) العائد
بعد أن نفّذ Charlie تخطيط Blade، أصبح شريط التنقل يحتاج فقط إلى صيانة ملف واحد. تغيّر طلب Bob من "تعديل 10 ملفات" إلى "تعديل ملف واحد"، بدون أي حذف.
3. وراثة تخطيط Blade
(1) تعريف التخطيط
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'ShopMetrics')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
@include('partials.navbar')
<main class="container mx-auto">
@yield('content')
</main>
@include('partials.footer')
</body>
</html>
(2) وراثة التخطيطات
<!-- resources/views/shops/index.blade.php -->
@extends('layouts.app')
@section('title', 'جميع المتاجر')
@section('content')
<div class="grid grid-cols-3 gap-4">
@foreach($shops as $shop)
<div class="card">{{ $shop->name }}</div>
@endforeach
</div>
@endsection
| الأمر | الوظيفة | التشبيه |
|---|---|---|
@extends |
تحديد التخطيط المراد وراثته | اختيار قالب |
@section |
تعريف كتل المحتوى | ملء الفراغات في القالب |
@yield |
إخراج كتلة المحتوى | عنصر نائب في القالب |
@include |
استيراد عرض فرعي | إدراج مقطع مشترك |
(1) ▶ مثال: تخطيط عمودي لـ ShopMetrics
<!-- resources/views/layouts/dashboard.blade.php -->
@extends('layouts.app')
@section('content')
<div class="flex">
@include('partials.sidebar')
<div class="flex-1 p-6">
@yield('dashboard-content')
</div>
</div>
@endsection
<!-- resources/views/dashboard/analytics.blade.php -->
@extends('layouts.dashboard')
@section('title', 'لوحة التحليلات')
@section('dashboard-content')
<h1>نظرة عامة على التحليلات</h1>
<div id="chart"></div>
@endsection
الناتج:
// تم التنفيذ بنجاح
4. هياكل التحكم
(1) العبارات الشرطية
@if($shop->is_active)
<span class="badge-green">نشط</span>
@elseif($shop->is_suspended)
<span class="badge-yellow">معلق</span>
@else
<span class="badge-gray">غير نشط</span>
@endif
@auth
<a href="{{ route('dashboard') }}">لوحة التحكم</a>
@endauth
@guest
<a href="{{ route('login') }}">تسجيل الدخول</a>
@endguest
(2) عبارات التكرار
@foreach($shops as $shop)
<tr>
<td>{{ $shop->name }}</td>
<td>{{ $shop->revenue }}</td>
</tr>
@if($loop->last)
</tbody></table>
@endif
@endforeach
@forelse($orders as $order)
<li>{{ $order->total }}</li>
@empty
<li>لا توجد طلبات بعد.</li>
@endforelse
| متغير التكرار | الوصف |
|---|---|
$loop->index |
الفهرس الحالي (يبدأ من 0) |
$loop->iteration |
الدورة الحالية (تبدأ من 1) |
$loop->first |
هل هذا الأول؟ |
$loop->last |
هل هذا الأخير؟ |
$loop->count |
الإجمالي |
(1) ▶ مثال: العرض الشرطي في لوحة تحكم ShopMetrics
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.dashboard')
@section('dashboard-content')
@auth
<h1>مرحبًا، {{ auth()->user()->name }}!</h1>
@endauth
@if($shops->count() > 0)
<div class="grid grid-cols-3 gap-4">
@foreach($shops as $shop)
<div class="card {{ $loop->first ? 'border-blue-500' : '' }}">
<h3>{{ $shop->name }}</h3>
<p>الإيرادات: ${{ number_format($shop->revenue, 2) }}</p>
</div>
@endforeach
</div>
@else
<p>لا توجد متاجر بعد. <a href="{{ route('shops.create') }}>أنشئ واحدًا!</a></p>
@endif
@endsection
الناتج:
// تم التنفيذ بنجاح
5. مكونات Blade
(1) المكونات المجهولة
<!-- resources/views/components/shop-card.blade.php -->
@props(['shop', 'highlight' => false])
<div class="card {{ $highlight ? 'border-gold' : '' }}">
<h3>{{ $shop->name }}</h3>
<p>{{ $shop->description }}</p>
{{ $slot }}
</div>
<!-- الاستخدام -->
<x-shop-card :shop="$shop" :highlight="true">
<p>مميز هذا الأسبوع!</p>
</x-shop-card>
(2) المكونات الصنفية
php artisan make:component Alert
# ينشئ: app/View/Components/Alert.php
# و: resources/views/components/alert.blade.php
// app/View/Components/Alert.php
class Alert extends Component
{
public function __construct(
public string $type = 'info',
public ?string $message = null,
) {}
public function render(): View
{
return view('components.alert');
}
}
<!-- resources/views/components/alert.blade.php -->
<div class="alert alert-{{ $type }}">
{{ $message ?? $slot }}
</div>
<!-- الاستخدام -->
<x-alert type="success" message="تم إنشاء المتجر!" />
<x-alert type="warning">تحقق من إعداداتك.</x-alert>
| البُعد | مجهول | صنفي |
|---|---|---|
| الملف | قالب Blade فقط | صنف PHP + قالب Blade |
| المنطق | بلا/ضئيل | قد يحتوي على منطق أعمال |
| Props | إعلان @props() |
معاملات المنشئ |
| مناسب لـ | مكونات عرض بسيطة | مكونات تتطلب منطقًا |
(1) ▶ مثال: مكون بطاقة إحصائيات ShopMetrics
<!-- resources/views/components/stat-card.blade.php -->
@props(['label', 'value', 'icon', 'trend' => null])
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-500 text-sm">{{ $label }}</p>
<p class="text-2xl font-bold">{{ $value }}</p>
@if($trend)
<p class="text-sm {{ $trend > 0 ? 'text-green-500' : 'text-red-500' }}">
{{ $trend > 0 ? '+' : '' }}{{ $trend }}%
</p>
@endif
</div>
<span class="text-3xl">{{ $icon }}</span>
</div>
</div>
<!-- الاستخدام في لوحة التحكم -->
<x-stat-card label="إجمالي الإيرادات" value="$12,450" icon="$" :trend="15.3" />
<x-stat-card label="المتاجر النشطة" value="23" icon="#" :trend="-2.1" />
الناتج:
// تم التنفيذ بنجاح
6. View Composer
يضخ View Composers البيانات المشتركة تلقائيًا في العروض، مما يلغي الحاجة لتمريرها بشكل متكرر في كل دالة متحكم.
// app/Providers/AppServiceProvider.php
public function boot(): void
{
// مشاركة معلومات المستأجر مع جميع عروض لوحة التحكم
View::composer('dashboard.*', function ($view) {
$view->with('tenant', Tenant::current());
});
// مشاركة بيانات التنقل مع عروض محددة
View::composer(['layouts.app', 'shops.*'], NavigationComposer::class);
}
| النوع | وقت التنفيذ | الغرض |
|---|---|---|
View::composer() |
في كل مرة يُعرض فيها العرض | حساب البيانات ديناميكيًا |
View::creator() |
عند إنشاء نسخة من العرض | ربط البيانات مبكرًا |
View::share() |
جميع العروض | ثوابت عامة |
(1) ▶ مثال: View Composer لتنقل ShopMetrics
// app/View/Composers/NavigationComposer.php
class NavigationComposer
{
public function compose(View $view): void
{
$view->with([
'navShops' => auth()->check()
? auth()->user()->shops()->take(5)->get()
: collect(),
'navNotifications' => auth()->check()
? auth()->user()->unreadNotifications()->count()
: 0,
]);
}
}
// التسجيل في AppServiceProvider
View::composer('layouts.app', NavigationComposer::class);
الناتج:
// تم التنفيذ بنجاح
7. أصول الواجهة الأمامية: Vite
يستخدم Laravel 11 Vite افتراضيًا لتجميع أصول الواجهة الأمامية.
(1) تثبيت Tailwind CSS وAlpine.js
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install alpinejs
(2) تهيئة Vite
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true,
}),
],
});
(3) الإدخال في Blade
<!-- في رأس التخطيط -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
| الأمر | الوظيفة |
|---|---|
npm run dev |
بدء خادم تطوير Vite (إعادة تحميل ساخنة) |
npm run build |
تجميع أصول الإنتاج (مضغوطة) |
(1) ▶ مثال: تنقل ShopMetrics مع تفاعلات Alpine.js
<!-- resources/views/partials/navbar.blade.php -->
<nav class="bg-white shadow" x-data="{ open: false }">
<div class="container mx-auto px-4">
<div class="flex justify-between h-16">
<a href="{{ route('home') }}" class="font-bold text-xl">ShopMetrics</a>
<div class="flex items-center">
@auth
<div class="relative" @click="open = !open">
<button class="flex items-center">
{{ auth()->user()->name }}
@if($navNotifications > 0)
<span class="badge-red">{{ $navNotifications }}</span>
@endif
</button>
<div x-show="open" x-transition class="dropdown">
<a href="{{ route('dashboard') }}">لوحة التحكم</a>
<a href="{{ route('shops.index') }}">المتاجر</a>
<form action="{{ route('logout') }}" method="POST">
@csrf
<button type="submit">تسجيل الخروج</button>
</form>
</div>
</div>
@else
<a href="{{ route('login') }}">تسجيل الدخول</a>
@endauth
</div>
</div>
</div>
</nav>
الناتج:
// تم التنفيذ بنجاح
8. مثال شامل: صفحة لوحة تحكم ShopMetrics
<!-- ============================================
شامل: صفحة لوحة تحكم ShopMetrics
يغطي: وراثة التخطيط، المكونات، التوجيهات، Alpine.js
============================================ -->
<!-- resources/views/dashboard/index.blade.php -->
@extends('layouts.dashboard')
@section('title', 'لوحة التحكم — ShopMetrics')
@section('dashboard-content')
<div class="space-y-6" x-data="{ period: '7d' }">
<!-- محدد الفترة -->
<div class="flex gap-2">
<button @click="period = '7d'"
:class="period === '7d' ? 'btn-primary' : 'btn-secondary'">7 أيام</button>
<button @click="period = '30d'"
:class="period === '30d' ? 'btn-primary' : 'btn-secondary'">30 يومًا</button>
<button @click="period = '90d'"
:class="period === '90d' ? 'btn-primary' : 'btn-secondary'">90 يومًا</button>
</div>
<!-- بطاقات الإحصائيات -->
<div class="grid grid-cols-4 gap-4">
<x-stat-card label="إجمالي الإيرادات" value="${{ number_format($totalRevenue, 0) }}" icon="$" :trend="$revenueTrend" />
<x-stat-card label="الطلبات" value="{{ number_format($orderCount) }}" icon="#" :trend="$orderTrend" />
<x-stat-card label="المتاجر النشطة" value="{{ $activeShops }}" icon="#" :trend="$shopTrend" />
<x-stat-card label="معدل التحويل" value="{{ number_format($conversionRate, 1) }}%" icon="%" :trend="$conversionTrend" />
</div>
<!-- الطلبات الأخيرة -->
<div class="bg-white rounded-lg shadow">
<h2 class="p-4 border-b font-semibold">الطلبات الأخيرة</h2>
@forelse($recentOrders as $order)
<div class="p-4 border-b flex justify-between">
<span>{{ $order->product_name }}</span>
<span>${{ number_format($order->total, 2) }}</span>
</div>
@empty
<p class="p-4 text-gray-500">لا توجد طلبات في هذه الفترة.</p>
@endforelse
</div>
</div>
@endsection
❓ أسئلة شائعة
{{ dd($variable) }} لتفريغ المتغير وإيقاف الصفحة مباشرة؛ أو ثبّت ملحق Laravel Debugbar لعرض جميع المتغيرات واستعلامات SQL واستخدام الذاكرة.view('xxx', ['key' => $value])، وهو مناسب للبيانات الخاصة بذلك العرض؛ View Composers يضخ البيانات المشتركة تلقائيًا، وهو مناسب للبيانات التي تحتاجها عروض متعددة (مثل شريط التنقل أو عدد الإشعارات).📖 ملخص
- يستخدم Blade @extends و@section و@yield لتنفيذ وراثة التخطيط والقضاء على HTML المكرر
- توجيهات التحكم @if و@foreach و@auth و@guest تُستخدم للتعامل مع المنطق الشرطي في القوالب
- المكونات المجهولة مناسبة للمكونات البسيطة، بينما المكونات الصنفية مناسبة للمكونات ذات المنطق
- View Composers يضخ البيانات المشتركة تلقائيًا في العروض، مما يلغي حاجة المتحكمات لتمرير القيم بشكل متكرر
- Vite هو أداة البناء الافتراضية للواجهة الأمامية في Laravel 11 ويدعم الإعادة الساخنة
- {{ }} يهرب من الأحرف تلقائيًا لمنع XSS؛ استخدم {!! !!} بحذر عند إخراج HTML خام
📝 تمارين
-
تمرين أساسي (⭐): أنشئ ملف تخطيط
layouts/app.blade.phpلـ ShopMetrics يتضمن شريط تنقل وتذييل، ثم أنشئ صفحة رئيسيةhome/index.blade.phpترث من هذا التخطيط واملأها بالمحتوى. -
تمرين متقدم (⭐⭐): أنشئ مكون Blade
<x-alert>يدعمtype(success/warning/error) ومحتوى slot، لاستخدامه في سيناريوهات ShopMetrics عند نجاح الإنشاء أو فشل التحقق. -
تحدٍ (⭐⭐⭐): استخدم Alpine.js لتنفيذ مكون شريط جانبي قابل للطي، ودمجه في تخطيط لوحة التحكم، مع دعم الطي التلقائي على الأجهزة المحمولة، وحفظ الحالة في localStorage.



