تذکر: این یک پست آموزشی برای عموم نیست! بلکه تنها جایی برای یادداشتهای من حین یادگیریه تا بهتر به خاطر بسپارم و در صورت لزوم به اونها مراجعه کنم.
قسمت ۱۵۸ تا ۱۷۴
منبع: ویدئوهای آموزشی وبپروگ
اول از همه لاراول رو نصب میکنیم:
composer create-project --prefer-dist laravel/laravel todoApp
اول یه دور پکیجهای موجود در package.json رو نصب میکنیم:
npm i
حالا پکیجهای مورد نیازمون رو نصب میکنیم:
npm i bootstrap jquery sass sass-loader npm install @popperjs/core --save
حالا باید پکیجهای جاوااسکریپتی مورد نیاز رو به فایل bootstrap.js (راه انداز ما که داخل resources هست) اضافه کنیم؛ تکلیف فایلهای css مربوط به بوت استرپ (کتابخانه) چی میشه؟ باید داخل پوشهٔ resources یک پوشه به اسم sass ایجاد کنیم و یک فایل به اسم app.scss که داخلش فایل sass بوت استرپ رو import کنیم؛ این require و import کردن داخل فایل bootstrap.js و app.scss از node_modules صورت میگیره که با دستور npm i ایجاد شده:
محتویات فایل bootstrap.js:
try{ window.Popper = require('popper.js').default; window.$ = window.jQuery = require('jquery'); require('bootstrap') } catch(e){ }
نحوه import داخل فایل app.scss:
@import '~bootstrap/scss/bootstrap';
توی قطعه کد بالا، علامت ~ نشان گر پوشهٔ node_module هست.
از اونجایی که از لاراول ۸ استفاده می کنیم (بر خلاف لاراول ۹ که از vite استفاده می کنه)، باید داخل فایل webpack.mix.js راهنمایی کامپایل sass رو اضافه کنیم؛ چرا js رو اضافه نکنیم؟ چون از قبل خودش اون رو داره:
mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css', [ // ]);
در نهایت هم برای کامپایل شدن فایلهای js و sass باید دستور زیر رو بزنیم:
npm run dev
حالا راحت می تونیم با asset helper function از این فایلها داخل فایل های blade خودمون استفاده کنیم.
خب برای ادامهٔ کار و ایجاد صفحات خودمون، اول از همه یک master layout درست میکنیم که بقیهٔ صفحات ما از اون ارثبری کنن، داخل پوشهٔ public/layouts میتونیم یک فایل master حالا با هر اسمی که خواستیم ایجاد کنیم:
// public/layouts/app.blade.php <!doctype html> <html lang="en"> <head> <title>Title</title> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS v5.2.0-beta1 --> <link rel="stylesheet" href="{{ asset('css/app.css') }}" > </head> <body> @yield('content') <script src="{{ asset('js/app.js') }}" > </body> </html>
همونطور که توی کد بالا داریم میبینیم؛ با asset helper function به فایل app.css و app.js داخل پوشهٔ public دسترسی پیدا کردیم؛ از طرفی با yield directive اومدیم و به سایر صفحاتی که از این صفحه ارثبری میکنن امکان این رو دادیم که به یک section به اسم content داشته باشن و محتوای اونها به جای این بخش yield شده نمایش داده بشه.
حالا میتونیم اینطوری از صفحه بالا ارثبری کنیم:
//about.blade.php @extends('layouts.app') @section('content') ... @endsection
php artisan make:model Todo -m
دستور بالا برای ما Model و migration (با آپشن -m) رو ایجاد میکنه.
محتوای فایل migration ما به صورت زیره، ما سه تا فیلد برای پروژهٔ todo خودمون نیاز داشتیم که بهش اضافه کردیم؛ به ترتیب شامل عنوان، توضیحات و یک فلگ برای تشخیص اینکه اون تسک انجام شده یا خیر.
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTodosTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('todos', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('description'); $table->boolean('completed')->default(0); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('todos'); } }
حالا میزنیم:
php artisan migrate
خب بریم سراغ Controller:
php artisan make:controller TodoController
حالا باید برای عملکردهای مختلف برنامهٔ خودمون متدهای مختلفی تعریف کنیم و همینطور به routeها سر و سامان بدیم:
Route::get('/', [TodoController::class, 'index'])->name('todos.index');
البته حتما باید کلاس TodoController رو هم use کنیم:
use App\Http\Controllers\TodoController;
حالا میریم سراغ اولین متد از کلاس TodoController:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class TodoController extends Controller { public function index() { return 'Home Page!'; } }
حالا قصد داریم که index خودمون (صفحه اصلی) رو پیادهسازی کنیم؛ صفحه index ما اینطوری میشه:
طبیعتا برای نمایش یک چنین صفحهای باید view اون رو پیادهسازی کنیم و توی قطعه کد بالا به جای return کردن "Home Page" باید اون view پیادهسازی شده blade رو برگردونیم؛ ما میتونیم view مربوط به index خودمون رو داخل پوشهٔ todos و فایل index.blade.php ایجاد کنیم و مقدار زیر رو داخل controller برگشت بدیم:
return view('todos.index');
خب، ما قبلا یک پوشه ایجاد کرده بودیم به اسم layouts و داخل اون یک master layout به اسم app.blade.php ایجاد کرده بودیم که ساختار فایل html ما رو شامل میشد و یک section به اسم content داخل body ایجاد کردیم که باید توی view مربوط به index استفاده بشه؛ محتوای فایل index که در مسیر resources/views/todos/index.blade.php قرار میگیره:
@extends('layouts.app') @section('content') ... @endsection
خب، فرم خیلی ابتدایی view صفحهٔ index ما اینطوری میشه:
<div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <h4>تسک ها</h4> <div class="card"> <div class="card-header"> تسک ها </div> <div class="card-body"> <ul class="list-group"> @foreach ($todos as $todo) <li class="list-group-item"> {{ $todo->title }} </li> @endforeach </ul> </div> </div> </div> </div> </div>
باید دقت داشته باشیم که مقدار متغیر todos رو باید داخل controller از مدل Todo بخونیم و با آرایه یا compact پاس بدیم به view:
<?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; class TodoController extends Controller { public function index() { $todos = Todo::all(); return view('todos.index', compact('todos')); } }
اگر بخوایم کد css به پروژه اضافه کنیم؛ مثلا فونت رو فارسی کنیم؛ باید اونها رو داخل resources/sass/app.scss وارد کنیم؛ بعد که npm run dev یا npm run watch رو ران کردیم؛ این کدها داخل فایل css پوشهٔ public کامپایل میشن:
// Bootstrap @import '~bootstrap/scss/bootstrap'; @font-face { font-family: "Vazir" src: url("../fonts/Vazir.eot"); /* IE9 Compat Modes */ src: url("../fonts/Vazir.eot?#iefix") format("embedded-opentype"), url("../fonts/Vazir.woff2") format("woff2"), url("../fonts/Vazir.woff") format("woff"), url("../fonts/Vazir.ttf") format("truetype"); /* Safari, Android, iOS */ } body{ direction: rtl; text-align: right !important; font-family: "Vazir" !important; }
فایلهای فونت ما هم داخل resources/fonts قرار میگیرن. باید توجه داشته باشیم که اگر حتی اگر یکی از فونت های بالا توی مسیر مشخص شده قرار نداشته باشن، با مشکل مواجه میشیم و کامپایل صورت نمی گیره.
حالا اگر تعداد تسکهای داخل جدول todos زیاد بود تکلیف چی میشه؟ باید paginate رو بهش اضافه کنیم؛ برای این کار داخل TodoController باید متد paginate رو روی مدل ران کنیم:
<?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; class TodoController extends Controller { public function index() { $todos = Todo::paginate(5); return view('todos.index', compact('todos')); } }
حالا باید فرانت pagination رو به پروژه اضافه کنیم؛ برای این کار یک div به صورت زیر به view خودمون اضافه میکنیم:
<div class="mt-2 d-flex justify-content-center">{{ $todos->links() }}</div>
از اونجایی که این پروژه روی لاراول ۸ هست و این نسخه از لاراول از Tailwindcss برای فرانت استفاده میکنه، و ما داریم از bootstrap استفاده می کنیم؛ ظاهر paginate ما بهم ریخته هست؛ برای حل این مشکل باید قطعه کد زیر رو به متد boot فایل AppServiceProvider اضافه کنیم:
Paginator::useBootstrap();
حتما هم باید Paginator رو use کنیم:
use Illuminate\Pagination\Paginator;
اما از اونجایی که پروژهٔ ما rtl هست؛ باید مشکل جهت buttonهای راست و چپ رو اوکی کنیم:
.page-item:first-child .page-link { border-top-left-radius: 0; border-bottom-left-radius: 0; border-top-right-radius: 0.35rem; border-bottom-right-radius: 0.35rem; } .page-item:last-child .page-link { border-top-right-radius: 0; border-bottom-right-radius: 0; border-top-left-radius: 0.35rem; border-bottom-left-radius: 0.35rem; }
در حالت عادی وقتی میخوایم یک تسک (todo) خاص رو نشون بدیم؛ میاییم و یک route تعریف میکنیم که مثلا id اون تسک رو میگیره که به یک متد داخل controller پاس میده و اونجا با متد find یا findOrFail اون تسک رو پیدا می کنیم و به view پاس میدیم تا نشون بده. اما توی مبحث Roue Model Binding توی آرگومان ورودی متد کنترلر خودمون، یک متغیر از نوع همون مدل خاص ایجاد میکنیم و دقیقا به همون نام، در این حالت خود لاراول میاد و اون تسک رو از table میکشه بیرون و بعد میتونیم اون رو به view پاس بدیم.
سناریوی اول:
// web.php Route::get('/todos/{id}', [TodoController::class, 'show'])->name('todos.index'); // TodoController <?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; class TodoController extends Controller { public function index() { $todos = Todo::paginate(1); return view('todos.index', compact('todos')); } public function show($val) { $todo = Todo::findOrFail($val); dd($todo); } }
توی قطعه کد بالا، وقتی بزنیم localhost:8000/todos/2 مقدار ۲ برای تابع show داخل TodoController ارسال میشه و اونجا dd میکنیم یا حالا مثلا وقتی تسک رو با مدل استخراج کردیم هر بلایی که خواستیم سرش میاریم.
اما سناریوی Route Model Binding به صورت زیر هست:
// web.php Route::get('/todos/{id}', [TodoController::class, 'show'])->name('todos.index'); // TodoController <?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; class TodoController extends Controller { public function index() { $todos = Todo::paginate(1); return view('todos.index', compact('todos')); } public function show(Todo $id) { dd($id); } }
توی کد بالا باید دقت کنیم که پارامتر ورودی تابع show دقیقا باید هم نام با متغیری باشه که توی route دریافت میشه.
حالا با توجه به این چیزی که یاد گرفتیم؛ میخوایم صفحهٔ show رو هم ایجاد کنیم تا بتونیم یک تسک خاص رو ببینیم تا بعدا روش عملیاتی مثل ویرایش و... رو انجام بدیم:
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <h4 class="text-center mt-5 mb-3">{{ $todo->title }}</h4> <div class="card"> <div class="card-header"> توضیحات </div> <div class="card-body"> {{ $todo->description }} </div> </div> </div> </div> </div> @endsection
قطعه کد بالا که مربوط میشه به show.blade.php کلیات رو از master layout ما به اسم app.blade.php به ارث برده.
قطعه کد زیر که مربوط به TodoController میشه، بعد از اینکه route آیدی رو به تابع show پاس داد، اینجا با تکنیک Route Model Binding مقدار اون تسک رو از table میخونیم و به view پاس میدیم تا برامون نشونش بده:
<?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; class TodoController extends Controller { public function index() { $todos = Todo::paginate(5); return view('todos.index', compact('todos')); } public function show(Todo $todo) { return view('todos.show', compact('todo')); } }
حالا باید بریم سراغ کامل کردن index خودمون، چطوری؟ باید به ازای هر تسکی که داریم؛ یک دکمهٔ نمایش قرار بدیم که وقتی کاربر روش کلیک کرد اون تسک نمایش داده بشه؛ شکل ظاهری چیزی که میخوایم اینطوری میشه:
بنابراین یک تگ a با کلاس button به صورت زیر برای صفحهٔ index خودمون ایجاد میکنیم:
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <h4>تسک ها</h4> <div class="card"> <div class="card-header"> تسک ها </div> <div class="card-body"> <ul class="list-group"> @foreach ($todos as $todo) <li class="list-group-item d-flex justify-content-between"> {{ $todo->title }} <a class="btn btn-sm btn-dark" href="{{ route('todos.show' , ['todo' => $todo->id]) }}">نمایش</a> </li> @endforeach </ul> </div> </div> <div class="d-flex justify-content-center mt-5">{{ $todos->links() }}</div> </div> </div> </div> @endsection
توی کد بالا، route helper function در واقع داره اسم روتی که به:
/todos/{todo}
اشاره داره رو کال میکنه و آرگومان دومش یک آرایه هست که داریم میگیم عمو جان این todo که داخل curly braces نیاز داری، در واقع id هر تسک توی table ما داخل database هست:
route('todos.show' , ['todo' => $todo->id])
خب توی index باید یه دکمه بذاریم برای ایجاد تسک، اینطوری:
و باید یک روت تعریف کنیم برای صفحهٔ ایجاد تسک:
پس باید توی web.php روت خودمون رو اضافه کنیم:
Route::get('/todos/create', [TodoController::class, 'create'])->name('todos.create');
باید متدش رو هم داخل کنترلر بسازیم:
public function create() { return view('todos.create'); }
نکتهٔ مهم:
به روتهای زیر خوب نگاه کنید:
Route::get('/', [TodoController::class, 'index'])->name('todos.index'); Route::get('/todos/{todo}', [TodoController::class, 'show'])->name('todos.show'); Route::get('/todos/create', [TodoController::class, 'create'])->name('todos.create');
وقتی که ما میخوایم یک تسک جدید create کنیم و به view مربوط به اضافه کردن پست بریم؛ میزنیم localhost:8000/todos/create اما میبینیم که ۴۰۴ برای ما میاد! چرا؟ چون اگر به ترتیب روتها توجه کنیم میبینیم که اول روت /todos/{todo} اجرا میشه و در واقع ما داریم متد show رو کال میکنیم؛ برای حل این مشکل میتونیم روت create رو بالای روت show قرار بدیم یا اینکه مسیر دسترسی رو عوض کنیم؛ مثلا:
/todos/show/{todo}
خب، حالا وقتی کاربر بزنه localhost:8000/todos/create باید view زیر نمایش داده بشه:
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8 mt-5"> <div class="card"> <div class="card-header"> ایجاد تسک جدید </div> <div class="card-body"> <form action="{{ route('todos.store') }}" method="POST"> @csrf <div class="form-group"> <label for="title">عنوان</label> <input type="text" id="title" name="title" class="form-control"> </div> <div class="form-group"> <label for="description">توضیحات</label> <textarea id="description" name="description" class="form-control"></textarea> </div> <button class="btn btn-dark" type="submit">ارسال</button> </form> </div> </div> </div> </div> </div> @endsection
قسمت مهم توی view بالا فرمی هست که با متد post داره مقادیرش رو به یک روتی به اسم todos.store سابمیت میکنه، این روت هم یک متدی به store رو داخل کنترلر صدا میزنه و اونجا با Request مقادیر ارسالی از فرم رو میخونیم و در نهایت کاری که نیازه رو باهاش انجام میدیم، مثلا ذخیره داخل دیتابیس و...، نکته مهم دیگهاش هم استفاده از دایرکتیو @csrf هست که حتما باید برای جلوگیری از حملات قرار بگیره:
// Route: Route::post('/todos', [TodoController::class, 'store'])->name('todos.store'); // TodoController Method: public function store(Request $rerquest) { dd($request->all()); }
اینم کد مربوط به اضافه شدن دکمهٔ «ایجاد تسک» داخل صفحهٔ index (کامل نذاشتم و فقط کد دکمه هست):
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="d-flex justify-content-between align-items-center my-3"> <h4>تسک ها</h4> <a class="btn btn-sm btn-outline-dark" href="{{ route('todos.create') }}">ایجاد تسک</a> </div>
دکمه بالا، ما رو به روت todos.create هدایت می کنه که بالاتر تعریف کردیم؛ این روت در واقع متد store رو داخل کنترلر صدا میزنه و مقادیر ارسالی از طریق فرم با post رو با Request دریافت میکنیم.
خب قبل از اینکه بریم سراغ ذخیره کردن تسک داخل database بریم سراغ validation دیتای ورودی:
خب، ما میتونیم validation رو داخل متد store انجام بدیم؛ برای این کار باید روی request متد validate رو اجرا کنیم:
public function store(Request $request) { $request->validate([ 'title' => 'required', 'description' => 'required' ]); dd('Done'); }
طبق قطعه کد بالا، فرم بعد از اینکه submit شد، ما میتونیم به دیتای اون از طریق Request داخل متد store دسترسی داشته باشیم و متد validate رو روش اجرا کنیم؛ اگر اوکی بود که ادامهٔ دستور ران میشه و اگر خیر، error به همون برگشت داده میشه که میتونیم هندلش کنیم.
یکی از راههای هندل error اینه که یک فایل blade جدا براش ایجاد کنیم و کدهای بررسی و نمایش error رو توش قرار بدیم و توی blade اصلی خودمون یعنی create اون رو include کنیم. برای این کار یک پوشه به اسم sections داخل پوشهٔ views ایجاد میکنیم و فایل error.blade.php رو اونجا ایجاد میکنیم:
@if (count($errors) > 0) <div class="alert alert-danger"> <ul class="mb-0"> @foreach ($errors->all() as $error) <li class="alert-text">{{ $error }}</li> @endforeach </ul> </div> @endif
خب توی قطعه کد بالا، با استفاده از if directive چک کردیم که آیا اندازهٔ آرایهٔ errors ما که در صورت validate نشدن به همون view برگشت داده میشه بزرگ تر از صفر هست یا خیر! اگر بزرگتر باشه یعنی اینکه خطا(هایی) داریم. حالا بعدش با استفاده از html و bootstrap سعی کردیم که اون خطا(ها) رو داخل یک ul نمایش بدیم.
حالا این قطعه کد رو چیکار کنیم؟ باید include بشه داخل create view:
اما متن error برگشتی انگلیسی هست و نیاز به فارسیسازی داره قبلا در موردش بحث کردیم و واردش نمیشیم. به طور خلاصه باید فایلهای ترجمه فارسی رو داخل پوشهٔ fa داخل پوشهٔ lang بذاریم و مقادیر locale رو fa و همینطور timezone رو به Asia/Tehran تغییر بدیم (داخل config/app.php).
فایل error.balde.php ما error رو به صورت alert بوت استرپ نشون میده، اگر بخوایم error رو زیر اون فیلد مربوطه نشون بدیم؛ می تونیم از @error directive داخل فایل create.blade.php استفاده کنیم:
<form action="{{ route('todos.store') }}" method="POST"> @csrf <div class="form-group"> <label for="title">عنوان</label> <input type="text" id="title" name="title" class="form-control" value="{{ old('title') }}" @error('title') style="border-color: red;" @enderror> @error('title') <p class="invalid-feedback d-flex">{{ $message }}</p> @enderror </div> <div class="form-group"> <label for="description">توضیحات</label> <textarea id="description" name="description" class="form-control" @error('title') style="border-color: red;" @enderror>{{ old('description') }}</textarea> @error('description') <p class="invalid-feedback d-flex">{{ $message }}</p> @enderror </div> <button class="btn btn-dark" type="submit">ارسال</button> </form>
البته توی قطعه کد بالا، علاوه بر اینکه اومدیم و خطا رو زیر هر input نمایش دادیم، با استفاده از دایرکتیو error رنگ input رو هم تغییر دادیم و از old() هم برای حفظ مقادیر وارد شده در صورت بروز خطا و بازگشت به همون view استفاده کردیم.
خب برای این کار خیلی ساده میتونیم از دستور create مربوط به مدل خودمون استفاده کنیم؛ یعنی وقتی که اطلاعات فرم رو پاس دادیم به متد store کنترلر و بعد از validate شدن، کافیه که با create اون ریکورد رو توی دیتابیس ایجاد کنیم:
<?php public function store(Request $request) { $request->validate([ 'title' => 'required', 'description' => 'required', ]); Todo::create([ 'title' => $request->title, 'description' => $request->description ]); return redirect()->route('todos.index'); } ?>
در آخر هم با استفاده از دستورز redirect دوباره کاربر رو به view اصلی منتقل میکنیم. برای ذخیرهسازی باید حواسمون به mass assign error باشه، برای جلوگیری از این error وقتی میخوایم یک recored جدید رو توی دیتابیس ایجاد کنیم باید یا متغیر fillable و یا guarded رو داخل مدل ست کنیم:
protected $fillable = ['title', 'description' , 'completed']; protected $guarded = [];
تفاوت متغیر fillable و guarded در اینه که توی fillable مقادیری رو میذاریم که میتونن توی جدول ذخیره بشن و توی guarded مقادیری که نمیتونن! پس اگر از guarded استفاده کنیم و خالیش بذاریم؛ همهٔ ستون ها اجازه ذخیره شدن دارن.
اگر اسم مدل و جدول داخل دیتابیس متفاوت باشه، باید متغیر table رو هم داخل مدل ست کنیم:
protected $table = 'todos';
نکته: توی index ترتیب نمایش تسکها از قدیمیترین به جدیدترین هست؛ یعنی همون فرمی که داخل table دیتابیس میبینیم؛ برای اینکه جدیدترین تسک رو اول ببینیم؛ کافیه از متد latest() مربوط به مدل استفاده کنیم:
<?php public function index() { $todos = Todo::latest()->paginate(3); return view('todos.index', compact('todos')); } ?>
با استفاده از این کتابخونه میتونیم همچین پیامهایی رو به برنامهٔ خودمون اضافه کنیم:
برای نصب و استفاده از این کتابخونه کافیه که قدم به قدم docی که توی گیتهاب اومده رو دنبال کنیم:
۱- نصب backend کتابخونه با composer:
composer require uxweb/sweet-alert
۲- نصب فرانت کتابخونه با npm (میشه cdn هم اضافه کرد):
npm install sweetalert --save-dev
۳- لود کردن کدهای فرانت داخل bootstrap.js:
// resources/js/bootstrap.js require("sweetalert");
۴- کامپایل فایلهای js:
npm run dev
۵- استفاده از کدهای sweet alert داخل پروژه، برای این کار باید اون رو داخل view با include directive اضافه کنیم:
<!DOCTYPE html> <html lang="en"> <head> <!-- Scripts --> <script src="{{ asset('js/app.js') }}"> </head> <body> @include('sweet::alert') </body> </html>
حالا میتونیم از SweetAlert که فساد این کتابخونه هست یا از alert helper function داخل کنترلر خودمون دقیقا قبل از redirect استفاده کنیم:
use SweetAlert public function store() { SweetAlert::message('Robots are working!'); return Redirect::home(); }
استفاده از توابع کمکی:
public function destroy() { Auth::logout(); alert()->success('You have been logged out.', 'Good bye!'); return home(); }
حالا به راحتی بریم برای اضافه کردن sweet alert به بخش create:
public function store(Request $request) { $request->validate([ 'title' => 'required', 'description' => 'required', ]); Todo::create([ 'title' => $request->title, 'description' => $request->description ]); alert()->success('تسک با موفقیت ایجاد شد', 'باتشکر'); return redirect()->route('todos.index'); }
برای ادیت کردن یک todo اول از همه یک route ایجاد میکنیم:
Route::get('/todos/{todo}/edit', [TodoController::class , 'edit'])->name('todos.edit');
و حالا متد edit رو توی TodoController ایجاد میکنیم:
public function edit(Todo $todo) { return view('todos.edit', compact('todo')); }
توی متد بالا از تکنیک Route Model Binding برای دسترسی به ریکورد توی دیتابیس استفاده کردیم و مقدار اون تسک یا todo رو به view ویرایش یا edit پاس دادیم. همینطور که داریم میبینیم ما داریم به یک view به اسم edit که داخل پوشهٔ resources/views/todos قرار داره منتقل میشیم؛ پس باید این view رو ایجاد کنیم:
//edit.blade.php @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8 mt-5"> {{-- @include('sections.errors') --}} <div class="card"> <div class="card-header"> ویرایش تسک </div> <div class="card-body"> <form action="{{ route('todos.update' , ['todo' => $todo->id]) }}" method="POST"> @csrf @method('put') <div class="form-group"> <label for="title">عنوان</label> <input type="text" id="title" name="title" class="form-control @error('title') form-control-invalid @enderror" value="{{ $todo->title }}"> @error('title') <p class="invalid-feedback d-block"> <strong>{{ $message }}</strong> </p> @enderror </div> <div class="form-group"> <label for="description">توضیحات</label> <textarea id="description" name="description" class="form-control @error('description')form-control-invalid @enderror">{{$todo->description }}</textarea> @error('description') <p class="invalid-feedback d-block"> <strong>{{ $message }}</strong> </p> @enderror </div> <button class="btn btn-dark" type="submit">ویرایش</button> </form> </div> </div> </div> </div> </div> @endsection
توی فرم بالا داریم مقادیر ویرایش شده و همینطور id اون تسک رو به یک روت به اسم todos.update پاس میدیم؛ این روت در واقع از نوع put هست (از این نوع برای آپدیت استفاده میشه)، همینطور داخل خود فرم با method directive نوع ارسال رو از نوع put تعریف کردیم:
Route::put('/todos/{todo}', [TodoController::class , 'update'])->name('todos.update');
علت استفاده از method directive چیه؟
ما توی خود html متد فرم رو از نوع post قرار دادیم چون خود html فقط post و get رو میپذیره، پس چاره چیه؟ استفاده از این directive که میاد و یک input از نوع hidden ایجاد میکنه که توش نوع متد رو پاس میده و لاراول از این طریق میتونه متد رو تشخیص بده:
خب بریم برای ایجاد متد update:
public function update(Request $request, Todo $todo) { $request->validate([ 'title' => 'required', 'description' => 'required', ]); $todo->update([ 'title' => $request->title, 'description' => $request->description ]); alert()->success('تسک با موفقیت ویرایش شد', 'باتشکر'); return redirect()->route('todos.index'); }
توی متد بالا، پارامترهای request و todo دریافت شدن، وقتی ما فرم رو submit میکنیم؛ به مقادیر فرم از طریق request دسترسی داریم و به مقادیر فعلی (ویرایش نشده یا قبلی اون todo) با todo (تکنیک RMB)، حالا اول مقادیر ورودی از فرم رو validate می کنیم که یه وقت خالی ارسال نشده باشه و بعدش اون تسک رو با متد update ویرایش می کنیم؛ در نهایت با sweet alert یک پیام نمایش میدیم و ریدایرکت میکنیم به index.
حالا باید توی صفحهٔ show، دکمههای ویرایش و حذف رو هم اضافه کنیم (در مورد حذف جلوتر صحبت میشه):
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <h4 class="text-center mt-5 mb-3">{{ $todo->title }}</h4> <div class="card"> <div class="card-header"> توضیحات </div> <div class="card-body"> {{ $todo->description }} </div> <hr> <div class="d-flex mr-3 mb-3"> <a class="btn btn-sm btn-outline-dark" href="{{ route('todos.edit', ['todo' => $todo->id]) }}"> ویرایش </a> <form class="mr-2" action="{{ route('todos.delete', ['todo' => $todo->id]) }}" method="POST"> @csrf @method('delete') <button class="btn btn-sm btn-danger">حذف</button> </form> </div> </div> </div> </div> </div> @endsection
اول از همه روتش رو ایجاد میکنیم:
Route::delete('/todos/{todo}', [TodoController::class , 'delete'])->name('todos.delete');
اینم از متدش توی کنترلر:
public function delete(Todo $todo) { $todo->delete(); alert()->error('تسک با موفقیت حذف شد', 'دقت کنید'); return redirect()->route('todos.index'); }
توی این متد هم از RMB برای دسترسی به اون تسک استفاده کردیم و متد delete رو روی اون todo ران کردیم.
بخش view پروژه (دکمه حذف) رو هم بالاتر قرار دادیم؛ برای این کار یک فرم تعریف کردیم با روتی که به متد delete اشاره داره و آیدی اون تسک رو ارسال میکنه و با method directive بهش گفتیم که متد از نوع delete هست تا لاراول به درستی هندلش کنه.
اینجا ما داریم hard delete میکنیم و برای soft delete باید use SoftDeletes رو به بدنه کلاس Todo (مدل) اضافه کنیم و همینطور مقدار زیر رو به migration و از نو migrate بزنیم:
$table->softDeletes();
برای migrate:
php artisan migrate:fresh
حالا ستون deleted_at به table دیتابیس اضافه میشه و با ران کردن متد delete دیگه حذف فیزیکی صورت نمیگیره بلکه فقط این ستون مقدار میگیره و نمایش داده نمیشه.
وقتی میخواستیم migration جدول todos رو ایجاد کنیم یک فیلد اضافه کرده بودیم به اسم complete که مقدار ۱ یا ۰ میگرفت؛ وقتی صفر باشه یعنی اون تسک هنوز انجام نشده و اگر ۱ باشه یعنی کامل شده؛ پس باید یک button به ازای هر تسک به index اضافه کنیم که وقتی تسکی انجام شد روی «انجام شد» کلیک بشه و بعدش دیگه این باتن رو نمایش نشده، حالا میشه ux بهتری هم متصور شد؛ اما ما اینجا این کار رو میخوایم بکنیم.
@extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="d-flex justify-content-between align-items-center my-3"> <h4>تسک ها</h4> <a class="btn btn-sm btn-outline-dark" href="{{ route('todos.create') }}">ایجاد تسک</a> </div> <div class="card"> <div class="card-header"> تسک ها </div> <div class="card-body"> <ul class="list-group"> @foreach ($todos as $todo) <li class="list-group-item d-flex justify-content-between"> {{ $todo->title }} <div> <a class="btn btn-sm btn-dark" href="{{ route('todos.show', ['todo' => $todo->id]) }}"> نمایش </a> {{-- @if (!$todo->completed) --}} @if ($todo->completed == 0) <a class="btn btn-sm btn-outline-info" href="{{ route('todos.complete', ['todo' => $todo->id]) }}"> انجام شد </a> @endif </div> </li> @endforeach </ul> </div> </div> <div class="d-flex justify-content-center mt-5">{{ $todos->links() }}</div> </div> </div> </div> @endsection
توی view اصلی (index) دکمهٔ «انجام شد» رو اضافه کردیم؛ این دکمه به روت todos.complete اشاره میکنه و مقدار id اون todo رو برای این روت ارسال میکنه؛ شکل روت ما اینطوری میشه:
Route::get('/todos/{todo}/complete', [TodoController::class , 'complete'])->name('todos.complete');
حالا این روت داره ما رو به متد complete کنترلر TodoController هدایت میکنه:
public function complete(Todo $todo) { $todo->update([ 'completed' => 1 ]); alert()->success('تسک مورد نظر به وضعیت انجام شد تغییر پیدا کرد', ' باتشکر'); return redirect()->route('todos.index'); }
توی این متد با استفاده از RMB اون متد رو پیدا میکنیم و عمل update اون فیلد رو انجام میدیم؛ بعدش پیام و ریدایرکت.
توی index از if directive استفاده کردیم و گفتیم که اگر complete برابر با صفر بود؛ دکمه رو نشون بده؛ اگر نبود چیزی نشون نده.