سنستخدم في هذا المقال كل ما تعلّمناه في المقالين السابقين من سلسلة مقالات لارافيل للمبتدئين لإنشاء مشروع حقيقي، حيث سننشئ مدونة صغيرة تحتوي على منشورات فقط بدون فئات Categories أو وسوم Tags، وكل منشور له عنوان ومحتوى، إذ لن ننشئ تطبيق مدونة كامل المواصفات، وسنوضّح كيفية استرداد البيانات من قاعدة البيانات، وكيفية إنشاء أو تحديث معلومات جديدة، وكيفية حفظها في قاعدة البيانات، وكيفية حذفها، حيث يمثّل الاختصار CRUD عمليات الإنشاء Create والقراءة Read والتحديث Update والحذف Delete.
تصميم بنية قاعدة البيانات
سننشئ أولًا ملف تهجير Migration للجدول post
، ويجب أن يحتوي هذا الجدول على العنوان والمحتوى، ويمكنك توليد ملف تهجير باستخدام الأمر التالي:
php artisan make:migration create_posts_table
وسيتضمن الملف ما يلي:
<?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->text('content'); }); } /** * عكس عمليات التهجير */ public function down(): void { Schema::dropIfExists('posts'); } };
أنشأنا في هذا المثال خمسة أعمدة في الجدول post
باستدعاء 5 توابع وهي:
-
ينشئ التابع
id()
عمود المعرّفid
الذي يُستخدَم عادة للفهرسة. -
ينشئ التابع
timestamps()
عمودين هما:created_at
وuptated_at
، وسيُحدَّث هذان العمودان تلقائيًا عند إنشاء السجل وتحديثه. -
ينشئ التابع
string('title')
العمودtitle
الذي نوعهVARCHAR
ويبلغ طوله الافتراضي 255 بايتًا. -
ينشئ التابع
string('content')
عمود المحتوىcontent
.
يمكنك تطبيق التغييرات من خلال تشغيل الأمر التالي:
php artisan migrate
ويجب أن ينشأ جدول posts
جديد كما يلي:
يمكننا الآن إنشاء النموذج Model المقابل لهذا الجدول باستخدام الأمر التالي:
php artisan make:model Post
وسيتضمّن هذا النموذج app/Models/Post.php
ما يلي:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Post extends Model { use HasFactory; }
لننشئ الآن متحكم الموارد Resource Controller المقابل باستخدام الأمر التالي:
php artisan make:controller PostController --resource
وأخيرًا، سجّل هذا المتحكم في الموجِّه Router كما يلي:
use App\Http\Controllers\PostController; Route::resource('posts', PostController::class);
يمكنك التحقق من الوِجهات Routes المُسجَّلة من خلال تشغيل الأمر التالي:
php artisan route:list
وسيظهر الخرج التالي:
GET|HEAD posts ................................................................. posts.index › PostController@index POST posts ................................................................. posts.store › PostController@store GET|HEAD posts/create ........................................................ posts.create › PostController@create GET|HEAD posts/{post} ............................................................ posts.show › PostController@show PUT|PATCH posts/{post} ........................................................ posts.update › PostController@update DELETE posts/{post} ...................................................... posts.destroy › PostController@destroy GET|HEAD posts/{post}/edit ....................................................... posts.edit › PostController@edit
يحتوي الخرج السابق على معلومات مثل توابع الطلبات Request Methods وتوابع المتحكّمات Controller Methods بالإضافة إلى أسماء الوِجهات، وتُعَد هذه المعلومات مهمة جدًا وسنحتاج إليها لاحقًا في هذا المقال.
عمليات CRUD
حان الوقت الآن لنتعمّق في التطبيق نفسه، فمن غير المحتمل أن ننشئ جميع المتحكمات أولًا ثم نصمّم القوالب ثم ننتقل إلى الموجّهات عند إنشاء تطبيقات واقعية، إذ يجب أن نفكّر من وجهة نظر المستخدم وفي الإجراءات التي قد يرغب المستخدم في اتخاذها. يجب أن يكون لدى المستخدم القدرة على إجراء أربع عمليات لكل مورد، والذي هو Post
في حالتنا، وهذه العمليات هي:
- الإنشاء Create: يجب أن يكون المستخدم قادرًا على إنشاء موارد جديدة وحفظها في قاعدة البيانات.
- القراءة Read: يجب أن يكون المستخدم قادرًا على قراءة الموارد سواءً من خلال استرداد قائمة الموارد أو التحقق من تفاصيل مورد محدّد.
- التحديث Update: يجب أن يكون المستخدم قادرًا على تحديث الموارد الموجودة وتحديث سجل قاعدة البيانات المقابلة لها.
- الحذف Delete: يجب أن يكون المستخدم قادرًا على إزالة مورد من قاعدة البيانات.
ويشار إلى هذه العمليات مع بعضها البعض باسم عمليات CRUD.
إجراء الإنشاء Create
لا تزال قاعدة بياناتنا فارغة حاليًا، لذا قد يرغب المستخدم في إنشاء منشورات جديدة، فلنبدأ بإجراء الإنشاء (C)، حيث يمكن إكمال هذا الإجراء من خلال استخدام التابعين التاليين:
-
تابع المتحكم
create()
الذي يعرض استمارة، مما يسمح للمستخدم بملء العنوان والمحتوى. -
تابع المتحكم
store()
الذي يحفظ المنشور الذي أنشأناه حديثًا في قاعدة البيانات، ويعيد توجيه المستخدم إلى صفحة القائمة.
يطابق التابع create()
نمط URL الذي هو /posts/create
(التابع GET
)، ويطابق التابع store()
نمط URL الذي هو /post
(التابع POST
).
إليك مراجعةً مختصرة لتوابع HTTP في حال احتياجك إلى تجديد بعض المعلومات:
-
تابع
GET
هو تابع طلبات HTTP الأكثر استخدامًا، ويُستخدَم لطلب البيانات والموارد من الخادم. -
يُستخدَم تابع
POST
لإرسال البيانات إلى الخادم، ويُستخدَم لإنشاء أو تحديث المورد. -
يعمل تابع
HEAD
مثل تابعGET
تمامًا، باستثناء أن استجابة HTTP ستحتوي على الترويسة فقط بدون الجسم، ويستخدم المطورون هذا التابع لأغراض تنقيح الأخطاء Debugging. -
يُعَد تابع
PUT
مشابهًا لتابعPOST
، مع اختلاف واحد بسيط، حيث إذا أرسلتَ باستخدام التابعPOST
موردًا موجودًا مسبقًا على الخادم، فلن يسبّب هذا الإجراء أيّ فرق، ولكن سيكرّر تابعPUT
هذا المورد في كل مرة نقدّم فيها الطلب. -
يزيل تابع
DELETE
موردًا من الخادم.
لنبدأ بالتابع create()
، حيث سيتضمّن ملف المتحكم app/Http/Controllers/PostController.php
ما يلي:
<?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; . . . class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بإنشاء مورد جديد */ public function create(): View { return view('posts.create'); } . . . }
سيُنفّذ هذا التابع عندما ترسل طلب GET
إلى /posts/create
، وسيؤشّر إلى العرض views/posts/create.blade.php
. لاحظ تغيير الاستجابة Response
إلى عرض View
في السطر 16، لأن هذه التوابع يجب أن تعيد عرضًا.
يجب بعد ذلك أن ننشئ العرض المقابل، حيث سنبدأ بعرض تخطيط الصفحة layout
، إذ سيتضمّن الملف views/layout.blade.php
ما يلي:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.tailwindcss.com"></script> @yield('title') </head> <body class="container mx-auto font-serif"> <div class="bg-white text-black font-serif"> <div id="nav"> <nav class="flex flex-row justify-between h-16 items-center shadow-md"> <div class="px-5 text-2xl"> <a href="{{ route('posts.index') }}"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="{{ route('posts.create') }}" class="hover:underline hover:underline-offset-1" >New Post</a > <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > </div> </nav> </div> @yield('content') <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4">. . .</div> </div> </footer> </div> </body> </html>
لاحظ في السطرين 16 و 20 وجود الأقواس المزدوجة المتعرجة {{ }}
التي تسمح بتنفيذ شيفرة PHP البرمجية ضمن القالب، وبالتالي سيعيد التابع route('posts.index')
الوِجهة التي اسمها posts.index
، حيث يمكنك التحقق من أسماء الوِجهات من خلال الرجوع إلى خرج الأمر php artisan route:list
.
لننتقل بعد ذلك إلى العرض create
، حيث يجب أن تكون منظمًا في هذا العرض. يُعَد العرض create
مُخصّصًا للإجراءات المتعلقة بالمنشور، لذا أنشأنا المجلد post
لتخزين هذا العرض. سيتضمن العرض views/posts/create.blade.php
ما يلي:
@extends('layout') @section('title') <title>Create</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2> <form action="{{ route('posts.store') }}" method="POST"> {{ csrf_field() }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" ></textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection
توجد بعض الأشياء التي يجب ملاحظتها عند استخدام الاستمارات لنقل البيانات، وهي:
-
السطر 6: تحدِّد سمة Attribute الإجراء
action
ما يحدث عند إرسال هذه الاستمارة، وتوجّه المتصفح في هذه الحالة لزيارة الوِجهةposts.store
باستخدام تابع HTTP الذي هوPOST
. -
السطر 7
{{ csrf_field() }}
: يُعَد CSRF هجومًا ضارًا يستهدف تطبيقات الويب، وتوفر الدالةcsrf_field()
الحماية من هذا النوع من الهجمات. اطلع على مقال تعرف على أمان مواقع الويب لمعرفة المزيد عن هجمات تزوير الطلبات عبر المواقع Cross-Site Request Forgery -أو CSRF اختصارًا. -
السمة
name
في السطر 12 و 20: يُربَط ما يدخله المستخدم بمتغير عند إرسال الاستمارة، وتحدّد السمةname
اسم هذا المتغير، فمثلًا إذا كانname="title"
، فسيُربَط دخل المستخدم بالمتغيرtitle
، ويمكن الوصول إلى قيمته باستخدام التعليمة$request->input('title')
، حيث سنرى كيفية عملها لاحقًا. -
السطر 27: يجب ضبط السمة
type
على القيمةsubmit
حتى تعمل هذه الاستمارة.
شغّل الآن خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/posts/create
حيث ستظهر الصفحة التالية:
إذا نقرتَ على زر الإرسال "Submit"، فسيرسل المتصفح طلب POST
إلى الخادم، وسيُنفَّذ التابع store()
هذه المرة، وسيحتوي طلب POST
على دخل المستخدم، ويمكن الوصول إليه كما يلي في الملف PostController.php
:
<?php namespace App\Http\Controllers; . . . class PostController extends Controller { . . . /** * تخزين مورد أنشأناه حديثًا في وحدة التخزين */ public function store(Request $request): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // إنشاء نسخة جديدة من المنشور Post ووضع البيانات المطلوبة في العمود المقابل $post = new Post; $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.index'); } . . . }
سيُعاد توجيهك إلى الوِجهة posts.index
بعد تخزين البيانات. عُد إلى متصفحك واكتب عنوانًا ومحتوًى جديدًا، ثم انقر على زر الإرسال. لم ننشئ العرض index
بعد، لذا ستُعاد رسالة خطأ، ولكن إذا تحققتَ من قاعدة البيانات، فيجب وجود سجل جديد مضاف كما يلي:
إجراء القائمة List
لننتقل الآن إلى عملية القراءة Read، حيث يوجد نوعان مختلفان من عمليات القراءة هما: الأول هو إجراء القائمة الذي يعيد قائمةً بجميع المنشورات إلى المستخدم، والإجراء الثاني سنوضّحه في الفقرة التالية. يقابل إجراء القائمة التابعَ index()
في الملف PostController.php
كما يلي:
<?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { /** * عرض قائمة الموارد */ public function index(): View { $posts = Post::all(); return view('posts.index', [ 'posts' => $posts, ]); } . . . }
ويكون عرض index
المقابل هو views/post/index.blade.php
كما يلي:
@extends('layout') @section('title') <title>Page Title</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> @foreach($posts as $post) <h2 class="text-2xl font-semibold underline mb-2"> <a href="{{ route('posts.show', ['post' => $post->id]) }}" >{{ $post->title }}</a > </h2> <p class="mb-4">{{ Illuminate\Support\Str::words($post->content, 100) }}</p> @endforeach </div> @endsection
تتكرر حلقة foreach
في السطر 5 على جميع المنشورات $posts
المُسترَدة، ثم تسنِد كل قيمة إلى المتغير $post
. لاحظ في السطر7 كيفية تمرير معرّف id
المنشور إلى الوِجهة posts.show
، ثم ستمرِّر هذه الوِجهة هذا المتغير إلى تابع المتحكم show()
الذي سنراه لاحقًا. يُعَد التابع Str::words()
في السطر 11 دالةً مساعدة في لغة PHP، وسيأخذ أول 100 كلمة من المحتوى content
فقط.
إجراء العرض Show
عملية القراءة الثانية هي إجراء العرض الذي يعرض تفاصيل مورد معين، ويتحقّق هذا الإجراء باستخدام التابع show()
في الملف PostController.php
كما يلي:
<?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض المورد المُحدَّد */ public function show(string $id): View { $post = Post::all()->find($id); return view('posts.show', [ 'post' => $post, ]); } . . . }
ويكون عرض show
المقابل هو views/post/show.blade.php
كما يلي:
@extends('layout') @section('title') <title>{{ $post->title }}</title> @endsection @section('content') <div class="max-w-screen-lg mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-2">{{ $post->title }}</h2> <p class="mb-4">{{ $post->content }}</p> <div class="grid grid-cols-2 gap-x-2"> <a href="{{ route('posts.edit', ['post' => $post->id]) }}" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" >Update</a > <form action="{{ route('posts.destroy', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('DELETE') }} <button type="submit" class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Delete </button> </form> </div> </div> @endsection
هناك بعض الأشياء التي يجب أن نلاحظها في المثال السابق. أولًا، لاحظ كيف أن زر التحديث "Update" هو رابط بسيط، ولكن زر الحذف "Delete" هو استمارة، لأن الرابط سيخبر المتصفح بإرسال طلب GET
إلى الخادم، بينما نحتاج شيئًا آخر لإجراء الحذف، حيث إذا نظرتَ داخل الاستمارة، فستجد أنها تحتوي على السمة method="POST"
، ولكننا نحتاج التابع DELETE
لإجراء الحذف. تدعم لغة HTML التابعين GET
و POST
افتراضيًا، وإذا كنت بحاجة شيء آخر، فيجب أن تضبط السمة method="POST"
، وأن تستخدم التابع method_field()
بدلًا من ذلك.
إجراء التحديث Update
تستخدم عملية التحديث التابع edit()
الذي يعرض استمارة HTML، والتابع update()
الذي يجري تغييرات على قاعدة البيانات، لذا ضع هذين التابعين في الملف PostController.php
كما يلي:
<?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * عرض الاستمارة الخاصة بتعديل المورد المُحدَّد */ public function edit(string $id): View { $post = Post::all()->find($id); return view('posts.edit', [ 'post' => $post, ]); } /** * تحديث المورد المحدَّد في وحدة التخزين */ public function update(Request $request, string $id): RedirectResponse { // الحصول على البيانات من الطلب $title = $request->input('title'); $content = $request->input('content'); // البحث عن المنشور المطلوب ووضع البيانات المطلوبة في العمود المقابل $post = Post::all()->find($id); $post->title = $title; $post->content = $content; // حفظ البيانات $post->save(); return redirect()->route('posts.show', ['post' => $id]); } . . . }
ويكون عرض edit
المقابل هو views/post/edit.blade.php
كما يلي:
@extends('layout') @section('title') <title>Edit</title> @endsection @section('content') <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Edit post</h2> <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" > {{ csrf_field() }} {{ method_field('PUT') }} <label for="title">Title:</label><br /> <input type="text" id="title" name="title" value="{{ $post->title }}" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" /><br /> <br /> <label for="content">Content:</label><br /> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300" > {{ $post->content }}</textarea ><br /> <br /> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center" > Submit </button> </form> </div> @endsection
إجراء الحذف Delete
وأخيرًا، يوجد التابع destroy()
للحذف، إذًا لنضعه في الملف PostController.php
كما يلي:
<?php namespace App\Http\Controllers; use App\Models\Post; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; class PostController extends Controller { . . . /** * إزالة المورد المُحدَّد من وحدة التخزين */ public function destroy(string $id): RedirectResponse { $post = Post::all()->find($id); $post->delete(); return redirect()->route('posts.index'); } }
لا يتطلب هذا الإجراء عرضًا، بما أنه يعيد توجيهك إلى posts.index
بعد اكتمال الإجراء.
ترجمة -وبتصرُّف- للمقال Laravel for Beginners #3 - The CRUD Operations لصاحبه Eric Hu.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.