اذهب إلى المحتوى

لارافيل للمبتدئين-الجزء الرابع: إنشاء لوحة تحكم لمدونة بسيطة


Ola Abbas

سننشئ في هذا المقال تطبيق مدونة باستخدام لارافيل ونجعل تطبيقنا كامل الميزات ويحتوي على منشورات وفئات Categories ووسوم Tags، حيث ناقشنا في المقال السابق عمليات CRUD للمنشورات، وسنكرر اليوم هذه العمليات نفسها على كل من الفئات والوسوم، كما سنناقش كيفية التعامل مع العلاقات فيما بينها.

تهيئة مشروع لارافيل جديد

سنبدأ بمشروع لارافيل جديد، حيث سننشئ مجلد العمل وننتقل إليه، ولكن تأكّد من تشغيل دوكر Docker، ثم نفّذ الأمر التالي:

curl -s https://laravel.build/<app_name> | bash

انتقل إلى مجلد التطبيق وابدأ تشغيل الخادم كما يلي:

cd <app_name>
./vendor/bin/sail up

لننشئ اسمًا بديلًا للأداة sail لتسهيل الأمور، لذا شغّل الأمر التالي:

alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

يمكنك من الآن فصاعدًا تشغيل الأداة Sail مباشرةً دون تحديد المسار بأكمله كما يلي:

sail up

استيثاق المستخدم User authentication

يأتي إطار عمل لارافيل مع نظام بيئي كبير فهو  يحتوي على العديد من الأدوات والموارد الإضافية التي تسهل على المطورين برمجة التطبيقات، وتُعَد حزمة Breeze من لارافيل جزءًا من هذا النظام، فهي توفر طريقة سريعة لإعداد استيثاق المستخدم وتسجيله في تطبيق لارافيل. تتضمّن حزمة Breeze عروضًا Views ومتحكّمات Controllers مبنية مسبقًا خاصة بالاستيثاق، بالإضافة إلى مجموعة من واجهات برمجة التطبيقات الخلفية للتعامل مع استيثاق المستخدم وتسجيله، وصُمِّمت هذه الحزمة لتكون سهلة التثبيت والضبط مع وجود الحد الأدنى من الإعداد المطلوب.

استخدم الأوامر التالية لتثبيت حزمة Breeze من لارافيل:

sail composer require laravel/breeze --dev
sail artisan breeze:install
sail artisan migrate
sail npm install
sail npm run dev

ستولّد هذه العملية تلقائيًا كل من المتحكمات والبرمجيات الوسيطة والعروض المطلوبة اللازمة لإنشاء نظام استيثاق مستخدم أساسي في تطبيقك. يمكنك الوصول إلى صفحة التسجيل من العنوان http://127.0.0.1/register، ثم تسجّل حسابًا جديدًا وسيُعاد توجيهك إلى لوحة التحكم.

01 register

لن نناقش في هذا المقال كيفية عمل نظام استيثاق المستخدم، لأنه يرتبط ببعض المفاهيم المتقدمة إلى حدٍ ما، ولكن يوصَى بشدة بإلقاء نظرة على الملفات التي تولّدت هنا، فهي توفر نظرة أعمق حول كيفية العمل في لارافيل.

إعداد قاعدة البيانات

يجب بعد ذلك أن يكون لدينا تصور حول كيفية ظهور تطبيق المدونة، حيث نحتاج أولًا إلى قاعدة بيانات يمكنها تخزين المنشورات والفئات والوسوم، وسيكون لكل جدول قاعدة بيانات البنيةُ التالية:

جدول المنشورات Posts:

المفتاح نوعه
id عدد صحيح كبير BigInteger
created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد
updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد
title سلسلة نصية String
cover سلسلة نصية String
content نص Text
is_published قيمة منطقية Boolean

جدول الفئات Categories:

المفتاح نوعه
id عدد صحيح كبير
created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد
updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد
name سلسلة نصية String

جدول الوسوم Tags:

المفتاح نوعه
id عدد صحيح كبير
created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد
updated_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد
name سلسلة نصية

يجب أن يكون هناك أيضًا جدول للمستخدمين users، ولكن لاحاجة لأن ننشئه فقد ولّدته حزمة Breeze من لارافيل مسبقًا، لذا سنتخطى هذه الخطوة حاليًا.

يكون لهذه الجداول علاقات مع بعضها البعض كما هو موضح فيما يلي:

  • كل مستخدم لديه منشورات متعددة.
  • كل فئة لديها العديد من المنشورات.
  • كل وسم لديه العديد من المنشورات.
  • يعودة كل منشور إلى مستخدم واحد.
  • يعود كل منشور إلى فئة واحدة.
  • كل منشور له العديد من الوسوم.

يمكننا إنشاء هذه العلاقات، ولكن يجب تعديل جدول المنشورات كما يلي:

جدول المنشورات مع العلاقات:

المفتاح نوعه
id عدد صحيح كبير
created_at timestamps يملأ تلقائيًا عند إنشاء سجل جديد
updated_at timestamps يملأ تلقائيًا عند تحديث سجل جديد
title سلسلة نصية String
cover سلسلة نصية String
content نص Text
is_published قيمة منطقية
user_id عدد صحيح كبير
category_id عدد صحيح كبير

ونحتاج أيضًا إلى جدول منفصل لعلاقة المنشور مع الوسم كما يلي:

جدول يمثل علاقة المنشور مع الوسم Post/Tag:

المفتاح نوعه
post_id عدد صحيح كبير
tag_id عدد صحيح كبير

تطبيق بنية قاعدة البيانات

يمكن تطبيق التصميم السابق من خلال توليد النماذج Models وملفات التهجير Migration باستخدام الأوامر التالية:

sail artisan make:model Post --migration
sail artisan make:model Category --migration
sail artisan make:model Tag --migration

بالإضافة إلى توليد ملف تهجير منفصل للجدول post_tag باستخدام الأمر التالي:

sail artisan make:migration create_post_tag_table

وسينشأ ملف التهجير database/migrations/create_posts_table.php التالي:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
   /**
    * تشغيل عمليات التهجير
    */
   public function up(): void
   {
       Schema::create('posts', function (Blueprint $table) {
           $table->id();
           $table->timestamps();
           $table->string('title');
           $table->string('cover');
           $table->text('content');
           $table->boolean('is_published');

           $table->bigInteger('user_id');
           $table->bigInteger('category_id');
       });
   }

   /**
    * عكس عمليات التهجير
    */
   public function down(): void
   {
       Schema::dropIfExists('posts');
   }
};

وسينشأ ملف التهجير database/migrations/create_categories_table.php التالي:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
   /**
    * تشغيل عمليات التهجير
    */
   public function up(): void
   {
       Schema::create('categories', function (Blueprint $table) {
           $table->id();
           $table->timestamps();
           $table->string('name');
       });
   }

   /**
    * عكس عمليات التهجير
    */
   public function down(): void
   {
       Schema::dropIfExists('categories');
   }
};

وسينشأ ملف التهجير database/migrations/create_tags_table.php التالي:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
   /**
    * تشغيل عمليات التهجير
    */
   public function up(): void
   {
       Schema::create('tags', function (Blueprint $table) {
           $table->id();
           $table->timestamps();
           $table->string('name');
       });
   }

   /**
    * عكس عمليات التهجير
    */
   public function down(): void
   {
       Schema::dropIfExists('tags');
   }
};

وسينشأ أيضُا ملف التهجير database/migrations/create_post_tag_table.php التالي:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
   /**
    * تشغيل عمليات التهجير
    */
   public function up(): void
   {
       Schema::create('post_tag', function (Blueprint $table) {
           $table->id();
           $table->timestamps();
           $table->bigInteger('post_id');
           $table->bigInteger('tag_id');
       });
   }

   /**
    * عكس عمليات التهجير
    */
   public function down(): void
   {
       Schema::dropIfExists('post_tag');
   }
};

طبّق هذه التغييرات باستخدام الأمر التالي:

sail artisan migrate

يجب بعد ذلك تفعيل الإسناد الجماعي Mass Assignment لحقول محدَّدة بالنسبة للنماذج المقابلة حتى نتمكّن من استخدام تابعَي create أو update معها كما ناقشنا في المقال السابق، ويجب أيضًا تعريف العلاقات بين جداول قاعدة البيانات.

سيكون ملف النموذج app/Models/Post.php كما يلي:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Post extends Model
{
   use HasFactory;

   protected $fillable = [
       "title",
       'content',
       'cover',
       'is_published'
   ];

   public function user(): BelongsTo
   {
       return $this->belongsTo(User::class);
   }

   public function category(): BelongsTo
   {
       return $this->belongsTo(Category::class);
   }

   public function tags(): BelongsToMany
   {
       return $this->belongsToMany(Tag::class);
   }
}

وسيكون ملف النموذج app/Models/Category.php كما يلي:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Category extends Model
{
   use HasFactory;

   protected $fillable = [
       'name',
   ];

   public function posts(): HasMany
   {
       return $this->hasMany(Post::class);
   }
}

ويكون ملف النموذج app/Models/Tag.php كما يلي:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Tag extends Model
{
   use HasFactory;

   protected $fillable = [
       'name',
   ];

   public function posts(): BelongsToMany
   {
       return $this->belongsToMany(Post::class);
   }
}

ويكون ملف النموذج app/Models/User.php كما يلي:

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
   use HasApiTokens, HasFactory, Notifiable;

   /**
    * السمات‫ Attributes التي نسندها إسنادًا جماعيًا
    *
    * @var array<int, string>
    */
   protected $fillable = [
       'name',
       'email',
       'password',
   ];

   /**
    * ‫السمات التي يجب أن تكون مخفية لعملية السَلسلة Serialization
    *
    * @var array<int, string>
    */
   protected $hidden = [
       'password',
       'remember_token',
   ];

   /**
    * السمات التي يجب تغيير نوعها‫ Cast
    *
    * @var array<string, string>
    */
   protected $casts = [
       'email_verified_at' => 'datetime',
   ];

   public function posts(): HasMany
   {
       return $this->hasMany(Post::class);
   }
}

المتحكمات Controllers والوِجهات Routes

نحتاج إلى إنشاء متحكم موارد واحد لكل مورد (منشور وفئة ووسم) باستخدام الأوامر التالية:

php artisan make:controller PostController --resource
php artisan make:controller CategoryController --resource
php artisan make:controller TagController --resource

ننشئ بعد ذلك وِجهات لكل من هذه المتحكمات في الملف routes/web.php كما يلي:

<?php

use App\Http\Controllers\CategoryController;
use App\Http\Controllers\PostController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\TagController;
use Illuminate\Support\Facades\Route;

// وِجهات لوحة التحكم
Route::prefix('dashboard')->group(function () {

   // الصفحة الرئيسية للوحة التحكم
   Route::get('/', function () {
       return view('dashboard');
   })->name('dashboard');

   // مورد الفئة للوحة التحكم
   Route::resource('categories', CategoryController::class);

   // مورد الوسم للوحة التحكم
   Route::resource('tags', TagController::class);

   // مورد المنشور للوحة التحكم
   Route::resource('posts', PostController::class);

})->middleware(['auth', 'verified']);

Route::middleware('auth')->group(function () {
   Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
   Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
   Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__ . '/auth.php';

لاحظ تجميع جميع الوِجهات باستخدام البادئة ‎/dashboard، ويكون للمجموعة البرمجيةُ الوسيطة auth، مما يعني أنه يجب على المستخدم تسجيل الدخول للوصول إلى لوحة التحكم.

متحكمات الفئة والوسم Category/Tag

يُعََد المتحكمان CategoryController و TagController واضحَين إلى حدٍ ما، ويمكنك إعدادهما بالطريقة نفسها التي أنشأنا بها المتحكم PostController في المقال السابق.

إذًا لننشئ أولًا متحكم الفئة app/Http/Controllers/CategoryController.php كما يلي:

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class CategoryController extends Controller
{
   /**
    * عرض قائمة الموارد
    */
   public function index(): View
   {
       $categories = Category::all();

       return view('categories.index', [
           'categories' => $categories
       ]);
   }

   /**
    * عرض الاستمارة الخاصة بإنشاء مورد جديد
    */
   public function create(): View
   {
       return view('categories.create');
   }

   /**
    * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين
    */
   public function store(Request $request): RedirectResponse
   {
       // الحصول على البيانات من الطلب
       $name = $request->input('name');

       // إنشاء نسخة جديدة من المنشور‫ Post ووضع البيانات المطلوبة في العمود المقابل
       $category = new Category();
       $category->name = $name;

       // حفظ البيانات
       $category->save();

       return redirect()->route('categories.index');
   }

   /**
    * عرض المورد المُحدَّد
    */
   public function show(string $id): View
   {
       $category = Category::all()->find($id);
       $posts = $category->posts();

       return view('categories.show', [
           'category' => $category,
           'posts' => $posts
       ]);
   }

   /**
    * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد
    */
   public function edit(string $id): View
   {
       $category = Category::all()->find($id);

       return view('categories.edit', [
           'category' => $category
       ]);
   }

   /**
    * تحديث المورد المُحدَّد في وحدة التخزين
    */
   public function update(Request $request, string $id): RedirectResponse
   {
       // الحصول على البيانات من الطلب
       $name = $request->input('name');

       // البحث عن الفئة المطلوبة ووضع البيانات المطلوبة في العمود المقابل
       $category = Category::all()->find($id);
       $category->name = $name;

       // حفظ البيانات
       $category->save();

       return redirect()->route('categories.index');
   }

   /**
    * إزالة المورد المٌحدَّد من وحدة التخزين
    */
   public function destroy(string $id): RedirectResponse
   {
       $category = Category::all()->find($id);

       $category->delete();

       return redirect()->route('categories.index');
   }
}

ولننشئ الآن متحكم الوسم app/Http/Controllers/TagController.php كما يلي:

<?php

namespace App\Http\Controllers;

use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TagController extends Controller
{
   /**
    * عرض قائمة الموارد
    */
   public function index(): View
   {
       $tags = Tag::all();

       return view('tags.index', [
           'tags' => $tags
       ]);
   }

   /**
    * عرض الاستمارة الخاصة بإنشاء مورد جديد
    */
   public function create(): View
   {
       return view('tags.create');
   }
   /**
    * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين
    */
   public function store(Request $request): RedirectResponse
   {
       // الحصول على البيانات من الطلب
       $name = $request->input('name');

       // إنشاء نسخة جديدة من المنشور‫ Post ووضع البيانات المطلوبة في العمود المقابل
       $tag = new Tag();
       $tag->name = $name;

       // حفظ البيانات
       $tag->save();

       return redirect()->route('tags.index');
   }

   /**
    * عرض المورد المُحدَّد
    */
   public function show(string $id): View
   {
       $tag = Tag::all()->find($id);
       $posts = $tag->posts();

       return view('tags.show', [
           'tag' => $tag,
           'posts' => $posts
       ]);
   }

   /**
    * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد
    */
   public function edit(string $id): View
   {
       $tag = Tag::all()->find($id);

       return view('tags.edit', [
           'tag' => $tag
       ]);
   }

   /**
    * تحديث المورد المُحدَّد في وحدة التخزين
    */
   public function update(Request $request, string $id): RedirectResponse
   {
       // الحصول على البيانات من الطلب
       $name = $request->input('name');

       // البحث عن الوسم المطلوب ووضع البيانات المطلوبة في العمود المقابل
       $tag = Tag::all()->find($id);
       $tag->name = $name;

       // حفظ البيانات
       $tag->save();

       return redirect()->route('tags.index');
   }

   /**
    * إزالة المورد المُحدَّد من وحدة التخزين
    */
   public function destroy(string $id): RedirectResponse
   {
       $tag = Tag::all()->find($id);

       $tag->delete();

       return redirect()->route('tags.index');
   }
}

تذكّر أنه يمكنك التحقق من اسم الوِجهات باستخدام الأمر التالي:

sail artisan route:list

متحكم المنشور Post

يُعَد متحكم المنشور PostController أكثر تعقيدًا بعض الشيء، إذ يجب عليك التعامل مع عمليات رفع الصور والعلاقات بين الجداول في قاعدة البيانات في التابع store()‎. إذًا لننشئ هذا المتحكم app/Http/Controllers/PostController.php كما يلي:

<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;

class PostController extends Controller
{
   . . .

   /**
    * تخزين المورد الذي أنشأناه حديثًا في وحدة التخزين
    */
   public function store(Request $request): RedirectResponse
   {
       // الحصول على البيانات من الطلب
       $title = $request->input('title');
       $content = $request->input('content');

       if ($request->input('is_published') == 'on') {
           $is_published = true;
       } else {
           $is_published = false;
       }

       // ‫إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل
       $post = new Post();
       $post->title = $title;
       $post->content = $content;
       $post->is_published = $is_published;

       // حفظ صورة الغلاف
       $path = $request->file('cover')->store('cover', 'public');
       $post->cover = $path;

       // ضبط المستخدم
       $user = Auth::user();
       $post->user()->associate($user);

       // ضبط الفئة
       $category = Category::find($request->input('category'));
       $post->category()->associate($category);

       // حفظ المنشور
       $post->save();

       // ضبط الوسوم
       $tags = $request->input('tags');

       foreach ($tags as $tag) {
           $post->tags()->attach($tag);
       }

       return redirect()->route('posts.index');
   }

   . . .
}

هناك بعض الأشياء التي يجب ملاحظتها في التابع store()‎، حيث سنستخدم في الأسطر من 28 إلى 32 مربع اختيار HTML لتمثيل الحقل is_published، وتكون قيمته إما 'on' أو null، ولكن تُحفَظ قيمته في قاعدة البيانات بوصفها true أو false، لذلك يجب استخدام التعليمة if لحل هذه المشكلة.

يمكننا استرداد الملفات في السطور من 41 إلى 42 من خلال استخدام التابع file()‎ بدلًا من التابع input()‎، ويُحفظ الملف في القرص public ضمن المجلد cover.

نحصل في السطور من 45 إلى 46 على المستخدم الحالي باستخدام التابع Auth::user()‎، ونربط المنشور بالمستخدم باستخدام التابع associate()‎، وتفعَل السطور من 49 إلى 50 الشيء نفسه بالنسبة للفئة، وتذكّر أنه يمكنك تطبيق ذلك فقط مع المتغير ‎$post وليس ‎$user أو ‎$category، لأن العمودان user_id و category_id موجودان في الجدول posts.

وأخيرًا، يجب حفظ المنشور الحالي في قاعدة البيانات، ثم استرداد قائمة الوسوم وإرفاق كل منها بالمنشور واحدًا تلوَ الآخر باستخدام التابع attach()‎ بالنسبة للوسوم كما هو موضح في السطور من 56 إلى 60.

تجري الأمور بطريقة مشابهة بالنسبة إلى التابع update()‎، باستثناء أنه يجب إزالة جميع الوسوم الموجودة قبل أن تتمكّن من إرفاق الوسوم الجديدة كما يلي:

$post->tags()->detach();

العروض Views

تذكر دائمًا أن تكون منظّمًا عند إنشاء نظام عرض، حيث سنتّبع البنية التالية في مثالنا:

resources/views
├── auth
├── categories
│   ├── create.blade.php
│   ├── edit.blade.php
│   ├── index.blade.php
│   └── show.blade.php
├── components
├── layouts
├── posts
│   ├── create.blade.php
│   ├── edit.blade.php
│   ├── index.blade.php
│   └── show.blade.php
├── profile
├── tags
│   ├── create.blade.php
│   ├── edit.blade.php
│   ├── index.blade.php
│   └── show.blade.php
├── dashboard.blade.php
└── welcome.blade.php

أنشأنا ثلاثة مجلدات هي: posts و categories و tags، ولكل منها أربعة قوالب هي: create و edit و index و show باستثناء المجلد posts، لأن وجود صفحة show للمنشورات في لوحة التحكم أمر غير ضروري.

يؤدي تضمين جميع هذه العروض في مقال واحد إلى جعل هذا المقال طويلًا بلا داعٍ، لذا سنوضّح فقط صفحات إنشاء وتعديل وفهرس المنشورات، ولكن يمكنك الاطلاع على الشيفرة المصدرية الكاملة على Github.

عرض إنشاء منشور

لننشئ أولًا عرض إنشاء المنشور resources/views/posts/create.blade.php كما يلي:

<x-app-layout>
 <x-slot name="header">
   <div class="flex justify-between">
     <h2
       class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"
     >
       {{ __('Posts') }}
     </h2>
     <a href="{{ route('posts.create') }}">
       <x-primary-button>{{ __('New') }}</x-primary-button>
     </a>
   </div>

   <script
     src="https://cdn.tiny.cloud/. . ./tinymce.min.js"
     referrerpolicy="origin"
   ></script>
 </x-slot>

 <div class="py-12">
   <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
     <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
       <div class="">
         <form
           action="{{ route('posts.store') }}"
           method="POST"
           class="mt-6 space-y-3"
           enctype="multipart/form-data"
         >
           {{ csrf_field() }}
           <input type="checkbox" name="is_published" id="is_published" />
           <x-input-label for="is_published"
             >Make this post public</x-input-label
           >
           <br />
           <x-input-label for="title">{{ __('Title') }}</x-input-label>
           <x-text-input
             id="title"
             name="title"
             type="text"
             class="mt-1 block w-full"
             required
             autofocus
             autocomplete="name"
           />
           <br />

           <x-input-label for="content">{{ __('Content') }}</x-input-label>
           <textarea
             name="content"
             id="content"
             cols="30"
             rows="30"
           ></textarea>
           <br />
           <x-input-label for="cover">{{ __('Cover Image') }}</x-input-label>
           <x-text-input
             id="cover"
             name="cover"
             type="file"
             class="mt-1 block w-full"
             required
             autofocus
             autocomplete="cover"
           />
           <br />
           <x-input-label for="category">{{ __('Category') }}</x-input-label>
           <select id="category" name="category">
             @foreach($categories as $category)
             <option value="{{ $category->id }}">{{ $category->name }}</option>
             @endforeach
           </select>
           <br />
           <x-input-label for="tags">{{ __('Tags') }}</x-input-label>
           <select id="tags" name="tags[]" multiple>
             @foreach($tags as $tag)
             <option value="{{ $tag->id }}">{{ $tag->name }}</option>
             @endforeach
           </select>
           <br />
           <x-primary-button>{{ __('Save') }}</x-primary-button>
         </form>
         <script>
           tinymce.init({. . .});
         </script>
       </div>
     </div>
   </div>
 </div>
</x-app-layout>

استخدمنا في مثالنا محرّر النصوص TinyMCE، ولكن يمكنك استخدام محرّر نصوص آخر عوضًا عنه، أو استخدم
العنصر <textarea></textarea> إذا أدرتَ ذلك.

يجب أن تحتوي الاستمارة الموجودة في السطر 24 على السمة التي هي enctype="multipart/form-data"‎ لأننا لا ننقل النصوص فحسب، بل توجد ملفات أيضًا. تذكر في السطر 59 استخدام السمة type="file"‎ لأننا نرفع صورة، وستُنقَل قيمة الخيار في السطور من 67 إلى 71 إلى الواجهة الخلفية.

هناك شيئان يجب الانتباه إليهما في الأسطر من 74 إلى 78، فلاحظ أولًا السمة name="tags[]"‎، حيث تخبر هذه الأقواس [] لارافيل بنقل مصفوفة قابلة للتكرار بدلًا من النصوص. ثانيًا، تنشئ السمة multiple استمارة متعددة التحديد بدلًا من تحديد فردي مثل استمارة الفئات.

عرض تعديل المنشور

لننشئ الآن العرض resources/views/posts/edit.blade.php كما يلي:

<x-app-layout>
 <x-slot name="header">
   <div class="flex justify-between">
     <h2
       class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"
     >
       {{ __('Posts') }}
     </h2>
     <a href="{{ route('posts.create') }}">
       <x-primary-button>{{ __('New') }}</x-primary-button>
     </a>
   </div>

   <script
     src="https://cdn.tiny.cloud/. . ./tinymce.min.js"
     referrerpolicy="origin"
   ></script>
 </x-slot>

 <div class="py-12">
   <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
     <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
       <div class="">
         <form
           action="{{ route('posts.update', ['post' => $post->id]) }}"
           method="POST"
           class="mt-6 space-y-3"
           enctype="multipart/form-data"
         >
           {{ csrf_field() }} {{ method_field('PUT') }}
           <input
             type="checkbox"
             name="is_published"
             id="is_published"
             @checked($post-
           />is_published)/>
           <x-input-label for="is_published"
             >Make this post public</x-input-label
           >

           <br />
           <x-input-label for="title">{{ __('Title') }}</x-input-label>
           <x-text-input
             id="title"
             name="title"
             type="text"
             class="mt-1 block w-full"
             required
             autofocus
             autocomplete="name"
             value="{{ $post->title }}"
           />
           <br />
           <x-input-label for="content">{{ __('Content') }}</x-input-label>
           <textarea name="content" id="content" cols="30" rows="30">
{{ $post->content }}</textarea
           >
           <br />
           <x-input-label for="cover"
             >{{ __('Update Cover Image') }}</x-input-label
           >
           <img
             src="{{ Illuminate\Support\Facades\Storage::url($post->cover) }}"
             alt="cover image"
             width="200"
           />
           <x-text-input
             id="cover"
             name="cover"
             type="file"
             class="mt-1 block w-full"
             autofocus
             autocomplete="cover"
           />
           <br />

           <x-input-label for="category">{{ __('Category') }}</x-input-label>
           <select id="category" name="category">
             @foreach($categories as $category)
             <option value="{{ $category->id }}" @selected($post->
               category->id == $category->id)>{{ $category->name }}
             </option>
             @endforeach
           </select>
           <br />
           <x-input-label for="tags">{{ __('Tags') }}</x-input-label>
           <select id="tags" name="tags[]" multiple>
             @foreach($tags as $tag)
             <option value="{{ $tag->id }}" @selected($post->
               tags->contains($tag))>{{ $tag->name }}
             </option>
             @endforeach
           </select>
           <br />
           <x-primary-button>{{ __('Save') }}</x-primary-button>
         </form>
         <script>
           tinymce.init({. . .});
         </script>
       </div>
     </div>
   </div>
 </div>
</x-app-layout>

لاحظ في السطور من 24 إلى 26 أن لغة HTML لا تدعم التابع PUT افتراضيًا، لذا نستخدم السمة method="POST"‎ ثم نخبر لارافيل باستخدام التابع PUT من خلال التعليمة ‎{{ method_field('PUT') }}‎.

عرض فهرس المنشورات

لننشئ الآن العرض resources/views/posts/index.blade.php كما يلي:

<x-app-layout>
 <x-slot name="header">
   <div class="flex justify-between">
     <h2
       class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight"
     >
       {{ __('Posts') }}
     </h2>
     <a href="{{ route('posts.create') }}">
       <x-primary-button>{{ __('New') }}</x-primary-button>
     </a>
   </div>
 </x-slot>

 <div class="py-12">
   <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
     @foreach($posts as $post)
     <div
       class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-4 px-4 h-20 flex justify-between items-center"
     >
       <div class="text-gray-900 dark:text-gray-100">
         <p>{{ $post->title }}</p>
       </div>
       <div class="space-x-2">
         <a href="{{ route('posts.edit', ['post' => $post->id]) }}">
           <x-primary-button>{{ __('Edit') }}</x-primary-button></a
         >

         <form
           method="post"
           action="{{ route('posts.destroy', ['post' => $post->id]) }}"
           class="inline"
         >
           {{ csrf_field() }} {{ method_field('DELETE') }}
           <x-danger-button> {{ __('Delete') }} </x-danger-button>
         </form>
       </div>
     </div>
     @endforeach
   </div>
 </div>
</x-app-layout>

لاحظ أن زر الحذف ليس زرًا عاديًا، إذ يجب أن يكون استمارة مع تابع DELETE، لأن الرابط العادي يمتلك التابع GET فقط.

يُفترَض الآن أن تكون قادرًا على إنشاء بقية نظام العرض بسهولة.

لقطات الشاشة

وأخيرًا، إليك بعض لقطات الشاشة للوحة التحكم التي أنشأناها:

صفحة إدارة التصنيفات:

ننتقل لصفحة الإدارة كما يلي:

02 home

ثم نختار التصنيفات Categories من الصورة أعلاه لننتقل للصفحة التالية لإدارة التصنيفات:

03 category list

صفحة إنشاء تصنيف جديد:

04 create category

صفحة تحديث المنشور:

05 update post

تابع معنا المقال التالي والأخير من هذه السلسلة حيث سننشئ فيه كل ما يخص الواجهة الأمامية من التطبيق كي يتمكن المستخدمون من رؤية منشورات المدونة والتفاعل معها.

ترجمة -وبتصرُّف- للمقال Laravel for Beginners #4 - Create a Dashboard لصاحبه Eric Hu.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...