فصل چهارم: لایه مدل

ایجاد، ویرایش، و حذف رکوردها

در درس پیش به لایهٔ مدل‌های لاراول پا گذاشتیم و مطمئن شدیم که برای هر دو جدولی که ساخته‌ایم، مدل‌هایی از نوع الیکوئنت داریم که در جای درستشان قرار دارند و گوش به فرمان ما هستند.

حالا وقت آن است که ببینیم با این مدل‌های بکر و دست‌نخورده چه می‌توانیم بکنیم و چگونه می‌توانیم داده‌هایی را برای ذخیره‌سازی در دیتابیس به الیکوئنت بسپاریم و چگونه اطلاعاتی را به‌روزرسانی کنیم یا حذف نماییم.

یک رکورد جدید

ایجاد رکورد اطلاعاتی جدید در لاراول، فرآیند نسبتاً پیچیده‌ای است. ابتدا باید یک شیء تازه از کلاس الیکوئنت خود بسازیم، فیلدهای آن را مطابق نیاز خود تکمیل کنیم‌ و بعد آن را ذخیره نماییم.

$mission = new App\Models\Mission();
$mission->code = '1234';
$mission->title = 'First Test Mission';
$mission->operator = "Test Operator";
$mission->save();

این فرآیند را از آن جهت پیچیده خواندم که به نوشتن پنج خط برنامه نیاز دارد و کارها در لاراول اغلب ساده‌تر از این حرف‌هاست.

مثلاً این پرسش پیش می‌آید که چرا اول یک شیء بسازیم و بعد مقدار بدهیم، و چرا از همان اول مقدارها را ندهیم و خودش هر کاری را که لازم است انجام نمی‌دهد؟

عجله نکنید. راه‌های ساده‌تری هم هست که خواهیم دید.

فعلاً می‌توانیم نتیجهٔ اجرای دستورهای بالا را تماشا کنیم و لذت ببریم.

First Record in Missions Table

دقت کنید که چطور created_at و updated_at بدون اشاره و درخواست ما، به صورت خودکار تکمیل شد.

کمی آسان‌تر آزمایش کنیم

اگرچه تمام مطالب این فصل را می‌توانیم به همین روش در تینکر بیازماییم و بیاموزیم، اما راه ساده‌تری هم هست. باید کمی از خودمان جلو بزنیم و از ابزاری استفاده کنیم که هنوز مقدماتش را فرا نگرفته‌ایم: از کنترلر!

آنچه باید بسازیم را بسیار سریع، و بدون شرح و تفصیل می‌گویم و شما هم انجام دهید. بعداً دربارهٔ واقعیت آنچه کردیم خواهیم آموخت.

اول

به کمک آرتیزان، یک فایل کنترلر می‌سازیم و نامش را ‍EloquentTestController می‌گذاریم که بعدها یادمان باشد کنترلری بوده که با آن الیکوئنت‌هایمان را آزمایش کرده‌ایم.

php artisan make:controller EloquentTestController

فایلی به نام EloquentTestController.php در همان جایی که سایر کنترلرها (پوشهٔ app/Http/Controllers‍) قرار خواهند گرفت ساخته می‌شود که قاعدتاً محتوایی به شکل زیر در آن درج شده است.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class EloquentTestController extends Controller
{
    //
}

دوم

متدی به نام index‍‍ (یا هر نام دیگری) در آن می‌سازیم.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class EloquentTestController extends Controller
{
    public function index()
    {
        return 'Salam';
    }
}

اگر انتظار دارید با آنچه که در این متد نوشتیم برنامه بیاید و به ما سلام کند،‌ سخت در اشتباهید. یک کار دیگر هم باید انجام شود.

سوم

فایل web.php را در پوشهٔ routes بیابید و این خط را به انتهای آن اضافه کنید:

Route::get('/' , 'EloquentTestController@index');
احتمالاً نیازی به توضیح نیست و خودتان می‌توانید حدس بزنید که با این کار به لاراول می‌گوییم در مواجهه با مسیر ریشهٔ برنامه، سراغ فایل EloquentTestController برود و متد index را از داخل آن بیابد و اجرا کند.

تمام

حالا اگر زحمت بکشید و مسیر پروژه را در مرورگر وارد کنید، خواهید دید که این بار برنامه به شما سلام خواهد کرد.

یک رکورد جدید

بیایید همان یک رکورد جدید را از اول، و این بار در کنترلری که برای آزمایش‌هایمان ساختیم آزمایش کنیم و ببینیم چه می‌شود.

پیش از شروع می‌توانید رکورد قبلی را پاک کنید، می‌توانید هم اهمیتی ندهید. به هر حال این یک آزمایش است.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->createOneRecord();
    }

    private function createOneRecord()
    {
        $mission = new Mission();
        $mission->code = '5678';
        $mission->title = 'Second Test Mission';
        $mission->operator = "Test Operator #2";
        $mission->save();

        return "Another test record is inserted.";
    }
}

تنها تفاوت ماجرا این است که این بار نیم‌اسپیس در بالای فایل قرار می‌گیرد (خط ۵) و دیگر لازم نیست نام طولانی کلاس مدل خود را وسط برنامه (خط ۱۶) تحمل کنیم و تازه همین کار ساده را هم اغلب ویرایشگرهای کد (IDE)، خودبه‌خود انجام می‌دهند.

در پایان (خط ۲۲)، یک متن کور و بی‌فایده را ‍‍return کرده‌ایم تا هنگامی که صفحهٔ مرورگر را بازخوانی می‌کنیم، خالی از عریضه نباشد.

در این مثال پردازش را به متد دیگری بردیم و متد index را تنها برای صدا کردن آن متد دیگر مورد استفاده قرار دادیم. این کار دو فایده دارد. اول آن که کد گویاتری نصیب ما خواهد کرد و این یک عادت خوب برنامه‌نویسی است که حتی وقتی برای آزمون و خطا چیزی می‌نویسیم، متعهد به تمیزی و مرتبی آن باشیم. فایدهٔ دوم آن است که وقتی این درس تمام شد و کد روی گیتهاب قرار گرفت، همهٔ مثال‌ها در جای خود باقی خواهند ماند.

یک رکورد جدید با بازخورد

بازخورد کوری که در انتهای متد قبلی گرفتیم به هیچ دردی نمی‌خورد. اصلاً جالب نیست بدون آن که بدانیم فرآیند ذخیره‌سازی موفق بوده یا نبوده، همیشه یک پیام ثابت را به کاربر خود نشان بدهیم.

بیایید کمی صورت داستان را بهتر کنیم و مثلاً شناسهٔ رکوردی که ذخیره شده را در خروجی نمایش دهیم تا کاربر بداند که راست گفته‌ایم.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->createOneRecordAndReturnId();
    }

    private function createOneRecordAndReturnId()
    {
        $mission = new Mission();
        $mission->code = '5678';
        $mission->title = 'Second Test Mission';
        $mission->operator = "Test Operator #2";
        $mission->save();

        if ($mission->id) {
            return "Another test record is inserted, with id #" . $mission->id;
        } else {
            return "Something went wrong!";
        }
    }

دقت کنید که برای دریافت شناسهٔ رکوردی که ذخیره کرده‌ایم، هیچ کار خاصی نکردیم! خاطرتان هست که این کار را بدون فریمورک چطور باید انجام می‌دادیم؟

شما که این کتاب را می‌خوانید، قاعدتاً برنامه‌نویس پی‌اچ‌پی هستید و می‌دانید که کلاس بالا بسته نشده و حدس می‌زنید که بقیه‌ٔ متدها پایین‌تر قرار گرفته‌اند و به همین دلیل، چیزی در این باره نمی‌گویم.

رکوردهایمان را بشماریم

برای آن که مطمئن شویم داده‌ها واقعاً ذخیره شده‌اند و لاراول فریبمان نداده است، می‌توانیم نگاهی به داخل جدول‌هایمان، مثلاً از طریق برنامهٔ phpMyAdmin بیاندازیم.

Inserted Records]

همان طور که می‌بینید، من پنجرهٔ مرورگرم را بیش از یک بار Refresh کرده‌ام و خوشبختانه با ذخیرهٔ داده‌های یکسان، اشکالی به وجود نیامده است.

حالا فرض کنیم که حوصلهٔ این کار را نداریم و می‌خواهیم تعداد رکوردهایی که ساخته‌ایم را در همین پنجرهٔ مرورگری که پروژه‌مان را نشان می‌دهد ببینیم.

برای این کار، یک متد دیگر در کنترلر آزمایشی خود می‌سازیم.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->countAllRecords();
    }

    private function countAllRecords()
    {
        return Mission::count();
    }

ملاحظه می‌فرمایید که شمردن رکوردهای جدول، با فراخوانی متد استاتیک ()count از مدلی که در درس پیش ساختیم و خالی رها کردیم انجام شد.

رکوردهایمان را ببینیم

وقتی شمردن رکوردها به همین راحتی بود، نمایششان هم نباید زیاد سخت باشد.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->viewAllRecords();
    }

    private function viewAllRecords()
    {
        return Mission::all();
    }

متد استاتیک ()all از مدل الیکوئنتی که در درس پیش ساختیم و خالی رهایش کردیم، همهٔ رکوردهای یک جدول را بدون قید و شرط به ما بازمی‌گرداند.

آنچه با اجرای کد در مرورگر می‌بینیم (بسته به این که کدهای بالا را چند بار اجرا کرده باشیم) چیزی شبیه این خواهد بود.

Inserted Records]

طبیعتاً رنگ و لعاب این خروجی را می‌توان اصلاح کرد و اگر منصفانه نگاه کنیم، کسی در عمل به چنین کاری که همهٔ رکوردهای یک جدول را بدون قید و شرط بیابد و یکجا به خروجی تحویل دهد، نیازی ندارد. تنها زمانی که ممکن است چنین کنیم، وقتی‌ست که مشغول نوشتن یک API هستیم و این از جذابیت‌های لاراول است که با در نظر گرفتن این موضوع، خروجی را نه به صورت یک شیء از اشیای پی‌اچ‌پی، بلکه به صورت JSON بازمی‌گرداند!

به کاراکترهای ابتدایی و انتهایی خروجی که نشانه‌های JSON هستند دقت کنید و اگر باز هم باور نمی‌کنید، خروجی را در سرویسی مثل Online Json Viewer آزمایش کنید.

یک رکورد خاص را ببینیم

فرض کنید که شناسهٔ یکی از رکوردهایمان را داریم و می‌خواهیم آن را پیدا کنیم و ببینیم.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->viewOneRecord(2);
    }

    private function viewOneRecord($id)
    {
        return Mission::find($id);
    }

متد استاتیک ()find از مدل الیکوئنتی که در درس پیش ساختیم و (باز هم تکرار می‌کنم) خالی رهایش کردیم، یک رکورد را با استفاده از شناسهٔ آن می‌یابد و بازمی‌گرداند.

خروجی، کلاسی از نوع الیکوئنت است، اما چون همین طور بی‌محابا از طریق کنترلر بازگردانده شده، با این فرض که لابد کاربردی برای تولید API دارد، به JSON تبدیل شده است. خیالتان راحت باشد. ما در عمل returnهایی چنین خشک و خالی نخواهیم داشت، مگر آن که واقعاً در کار نوشتن API باشیم.

Inserted Records]

اگر هم آنچه را که می‌خواهیم نیابد، به جای خطا دادن، مقدار null را بازمی‌گرداند و در مثال سادهٔ ما، کاربر پنجره‌ای خالی می‌بیند.

تغییر یک رکورد

هرچقدر که ساخت رکورد در لاراول با دشواری روبه‌رو بود، تغییر یک رکورد هم هست!

همهٔ کاری که باید انجام دهیم این است که ابتدا رکوردی را با شناسه‌اش بیابیم، هر چه می‌خواهیم را تغییر دهیم، و سپس آن را ذخیره کنیم.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->editARecord(2);
    }

    private function editARecord($id)
    {
        $mission = Mission::find($id);
        $mission->title .= ' - Edited!' ;
        $mission->save();

        return "Done!";
    }

حالا می‌توانیم نتیجه را با آنچه که در همین درس آموخته‌ایم ببینیم و مطمئن شویم که حقه‌ای در کار نیست.

حذف یک رکورد

مطمئنم با توجه به متدهایی که در این درس آموختیم، می‌توانید حدس بزنید که برای حذف یک رکورد چه باید کرد.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->deleteARecord(2);
    }

    private function deleteARecord($id)
    {
        $mission = Mission::find($id);
        $mission->delete();

        return 'Deleted';
    }

متد ()delete از مدل الیکوئنتی که در درس پیش ساختیم و خالی رهایش کردیم، مسئول پاک کردن رکوردهاست.

این کد کار می‌کند، اما کمی مسخره به نظر می‌رسد!

اگر شناسهٔ یک رکورد را می‌دانیم، هیچ لزومی ندارد که اول آن را بیابیم و بخوانیم و بعد پاکش کنیم.

متد دیگری به نام ()destroy برای همین روزهاست.

<?php

namespace App\Http\Controllers;

use App\Models\Mission;

class EloquentTestController extends Controller
{
    public function index()
    {
        return $this->deleteARecord(3);
    }

    private function destroyARecord($id)
    {
        return Mission::destroy($id);
    }

متد ()destroy مقداری منطقی بازمی‌گرداند که نشان‌گر موفق یا ناموفق بودن عمل حذف است و یک خوبی معرکهٔ دیگر هم دارد:

این متد، آرایه هم می‌پذیرد. یعنی می‌تواند چند رکورد را هم‌زمان حذف نماید.

return Mission:destroy([1,2,3]);

یک تمرین ساده

با این ترتیب که من بنویسم و شما بخوانید و عبور کنید، چیزی یاد نمی‌گیرید. اگر می‌خواهید از وقتی که صرف می‌کنید نصیبی ببرید سعی کنید تمرین‌هایی که از این پس در لابه‌لای متن می‌گنجانم را انجام دهید.

با آنچه که در این درس آموخته‌اید:

  • صد رکورد تصادفی جدید بسازید
  • همهٔ آن‌ها را تک‌تک پاک کنید.
  • دقت کنید که تنها رکوردهایی که خودتان ساخته‌اید را پاک کنید و کاری به کار رکوردهایی که از قبل وجود داشتند نداشته باشید.

.

تأکید می‌کنم این کار را به صورت لاراولی انجام دهید. اگر به این درس اکتفا نمی‌کنید و سراغ درس‌های بعد یا مستندات لاراول می‌روید، اشکالی ندارد، اما به هیچ وجه از آموخته‌های غیر لاراولی خودتان، یا بدتر از آن، از درج مستقیم دستورات mySql استفاده نکنید.

این تمرین پاسخ مشخصی ندارد و مهم این است که از راه درست نوشته شود و درست کار کند. یکی از پاسخ‌های موجود را، به نحوی که از این درس جلو نزند و کد نسبتاً تمیزی داشته باشد، می‌نویسم و در کنترلری جداگانه، در گیتهاب آپولو می‌گذارم و نشانی دقیقش را نمی‌دهم تا خودتان پیدایش کنید.

اگر دوست داشتید، می‌توانید پاسخ‌هایی را که می‌نویسید به صورت Issue (با برچسب پرسش) در گیتهاب کتاب ثبت کنید تا درباره‌شان گفت‌وگو کنیم.

جان کلام

در این درس، آموختیم که چطور مدل‌های الیکوئنتی که در درس قبل ساختیم و خالی رها کردیم، به کار می‌آیند. ما متدهای موجود در همین الیکوئنت‌های صفرکیلومتر را برای سه عملکرد اصلی «ساخت رکورد جدید»، «به‌روزرسانی» و «حذف» به کار گرفتیم و نیم‌نگاهی هم به کوئری‌های ساده‌ای همچون شمارش کردن (()count) و یافتن (()find) انداختیم.

این البته تمام ماجرا نیست. برای همین سه عملکرد اصلی این درس، راه‌های ساده‌تری هم وجود دارد که در درس بعد به آن‌ها خواهیم پرداخت.

پروژه آپولوی شما در پایان این درس باید چیزی شبیه این شده باشد.