چطور امکان «ادمین» را به سایت لاراولی در حال کار اضافه کردم؟

این نوشته به درد کسانی که لاراول بلد نیستند نمی خورد (امیدوارم به درد بقیه بخورد).مسئله این بود: یک سایت لاراولی ساده داریم که کاربر می تواند لاگین (login) کند (سایت یک کاربره تعریف شده بود. یعنی در جدول user فقط یک کاربر وجود داشت)، متن های خود را تایپ کند، متن های قبلی اش را ببیند یا ویرایش کند و واژه های متن را برچسب گذاری (tagging) کند. حالا می خواهیم امکان ادمین را هم به سایت اضافه کنیم. یعنی اگر یک کاربر ادمین بود، سطح دسترسی بالاتری داشته باشد: بتواند متن های دیگران را ببیند و همچنین تحلیل های همزمان موجود روی متن ها را هم فقط ادمین ببیند (تحلیل هایی مثل تعداد واژه، تعداد جمله، تعداد اسم، فعل، صفت و...).مشکل کجا بود؟ سایت در حال کار بود و حدود صد متن پیش از آن در جداول ذخیره شده بود.کار را باید به چند بخش تقسیم کنیم.تا قبل از آن، متن ها در جدول texts ذخیره می شدند اما بدون این که مشخص شود متن را چه کسی تایپ کرده است. پس یک ستون user_id باید به جدول texts اضافه شود. اما اضافه کردن این ستون، نباید باعث از دست رفتن داده های قبلی شود.علاوه بر این، باید مشخص کرد که هر کاربر ادمین هست یا نه. پس یک ستون is_admin هم به جدول user اضافه می کنیم.بعد باید مشخص کنیم که ادمین به چه بخش های خاصی باید دسترسی داشته باشد و نمایش چه صفحه‌هایی برای ادمین و کاربر عادی متفاوت خواهد بود.در نهایت دوباره بررسی کنیم که کاربر به صفحات مرتبط با ادمین دسترسی ندارد.راهکارهایی که من ارائه دادم، ممکن است بهینه نباشند و راهکارهای بهتری هم وجود داشته باشد. در این صورت، ممنون می شوم که راهنمایی ام کنید.1. چه کسی متن را تایپ کرده است؟اولین مرحله، کاری بود که از قبل باید انجام می شد. در واقع مستقیمن به صورت مسئله ی فعلی مرتبط نبود اما پیش‌نیاز صورت مسئله ی فعلی بود.اضافه کردن یک ستون جدید به جدول در لاراول کار ساده ای است. در مثال ما، کافی است تکه کد زیر را به migration مربوط به ساخت جدول texts اضافه کنیم:Schema::table('texts', function (Blueprint $table) { $table->foreignId('user_id')->nullable()->constrained(); });در این کد، به لاراول می گوییم که ستون user_id را به عنوان کلید خارجی به جدول texts اضافه کن. چون فرمت نام گذاری ما درست است، نیاز نیست که صراحتن به نام جدول مقصد (users) هم اشاره کنیم. خود لاراول متوجه می شود که این ستون به عنوان یک کلید خارجی به ستون id جدول users اشاره دارد. دلیل اضافه کردن nullable() را هم در ادامه توضیح می دهم.اگر این کد را به مایگریشن create_texts_table اضافه کنیم، بعد از اجرای کد زیر در ترمینال، ستون به جدول ما اضافه می شود (اما ما این کد را اجرا نمی کنیم!):php artisan migrate:freshمشکل این جاست که با اجرای این دستور، داده های قبلی ما پاک خواهندشد. پس این ستون را باید در مایگریشن جدیدی بنویسیم و به جای دستور بالا هم از دستور ساده ی migrate استفاده کنیم. پس ابتدا یک مایگریشن جدید می سازیم:php artisan make:migration add_user_id_to_texts_tableبا دستور بالا، یک فایل جدید مایگریشن برای ما ساخته می شود که می توانیم دستور مربوط به اضافه شدن کلید خارجی user_id به جدول texts را در تابع up() کلاس آن بنویسیم. خوبی اضافه کردن مایگریشن جدید چیست؟ لاراول فهرستی از مایگریشن های اجراشده را در جدول migration نگه داری می کند. حالا که این مایگریشن جدید را اضافه کردیم، کافی است از دستور زیر در ترمینال استفاده کنیم تا لاراول کاری به کار مایگریشن های قبلی نداشته باشد و فقط موارد جدید را اجرا کند. موارد جدید یعنی همان ستونی که ما اضافه کردیم. کارکرد nullable این جا مشخص می شود. اگر این تابع را در مایگریشن خود ننویسیم، لاراول برای اضافه کردن این ستون به داده های قبلی، دنبال یک مقدار پیش فرض برای user_id می گردد و چون پیدا نمی کند، خطا می دهد. با این تابع، به لاراول می گوییم که مقدار user_id را می توانی برای متن های قبلی null بگذاری.حالا دستور زیر را اجرا کنیم:php artisan migrateخب، کار تمام است؟ نه هنوز! حالا که بین دو جدول متن ها و کاربران در پایگاه داده، ارتباط برقرار کردیم (با ساختن کلید خارجی) باید این ارتباط را در مدل های مربوط به متن و کاربر هم اضافه کنیم. این کار باعث می شود لاراول، ارتباط بین دو مدل را متوجه شود. ما باید در مدل کاربر (User) بگوییم که یک کاربر می تواند چندین متن داشته باشد و در مدل متن (Text) بگوییم که یک متن می تواند فقط متعلق به یک کاربر باشد.پس در کلاس مدل User این تابع را اضافه می کنیم:public function texts() { return $this->hasMany(Text::class); }کارکرد این تابع را می توان از نوشتارش فهمید. این کاربر می تواند چندین (hasMany) متن داشته باشد (به نام تابع دقت کنید که جمع است: texts).در کلاس مدل Text هم برعکس این را می نویسیم:public function user() { return $this->belongsTo(User::class); }در تابع بالا نوشته ایم: این متن می تواند متعلق به (belongsTo) یک کاربر باشد (به نام تابع دقت کنید که مفرد است: user).کار ما در این مرحله تمام شده است.2. چه کسی ادمین است؟حالا وقت اضافه کردن ستون is_admin به جدول users است. این ستون را هم مثل بخش قبل، باید در یک مایگریشن جدید بنویسیم اما می توانیم این مورد و مورد قبلی را در یک مایگریشن بنویسیم که تعداد مایگریشن‌هایمان زیاد نشود. به هر حال، نوشتن چنین دستوری در یک مایگریشن لازم است:Schema::table('users', function (Blueprint $table) { $table->boolean('is_admin')->after('password'); });در این دستور، به لاراول می گوییم ستون is_admin را از نوع صفر و یک، بعد از ستون password به جدول users اضافه کن.این جا دیگر نیازی به تغییر در مدل ها نداریم چون این ستون، فقط مربوط به یک جدول (و یک مدل) است.3. ادمین کجا را ببیند، کاربر عادی کجا را؟حالا باید مشخص کنیم که ادمین و کاربر عادی هر کدام به چه بخش هایی دسترسی دارند. این سطح دسترسی‌ها را در دو جا باید مشخص کنیم. یکی در کنترلرها و یکی در ویوها. در کنترلرها می گوییم کاربر عادی به صفحات کاربران دیگر دسترسی نداشته باشد و در ویوها می گوییم کاربر عادی کدام بخش از یک صفحه را نتواند ببیند. یک نکته ی ریز در این جا وجود دارد. اگر صفحات ادمین و کاربر عادی کاملن از هم جدا بودند، می شد از میدلورها (middleware) در روتر خود استفاده کنیم و به طور کلی بگوییم اگر کاربر ادمین نبود، به فلان صفحه دسترسی نداشته باشد اما ساختار برنامه ی ما در این جا متفاوت است.برای دستیابی به کاربر فعلی می توان از این کد استفاده کرد:auth()->user()مثلن می خواهیم کاربر عادی در داشبورد فقط متن های خودش را ببیند اما ادمین، همه ی متن ها را ببیند.یک نکته ی کوچک: من در این جا، کنترلر متن ها را با resource ساخته ام. یعنی کنترلر متن، همه ی عملیات‌های مربوط به متن را دارد:تابع index برای نمایش فهرست متن هاتابع create برای نمایش فرم ثبت متن جدیدتابع store برای ثبت متن جدید در پایگاه دادهتابع show برای نمایش یک متنتابع edit برای نمایش فرم ویرایش یک متنتابع store برای ثبت متن ویرایش شده در پایگاه دادهتابع destroy برای حذف یک متنحالا برای این که دسترسی کاربر عادی به متن ها را کنترل کنم، در هر کدام از این توابع به تناسب می توانم تغییراتی اعمال کنم. مثلن در تابع index می خواهم بگویم که اگر کاربر ادمین بود، فهرست همه ی متن ها را ببیند اما اگر کاربر عادی بود فقط فهرست متن های خودش را ببیند. پس چنین چیزی می نویسم:public function index() { if (auth()->user()->is_admin) { $texts = text::all(); } else { $texts = text::where('user_id', auth()->user()->id)->get(); } return view('dashboard')->with('texts', $texts); }در کد بالا، گفته ام اگر کاربر ادمین بود، همه ی متن ها را برای او استخراج کن و اگر کاربر عادی بود فقط متن‌هایی را که مربوط به خودش است. این کد رو می شه با Ternary operator هم نوشت (پیشنهاد اشکان):public function index() { (auth()->user()->is_admin) ? $texts = text::all() : $texts = text::where('user_id', auth()->user()->id)->get(); return view('dashboard')->with('texts', $texts); }دو تابع create و store نیاز به تغییر ندارند چون در این برنامه، هم ادمین و هم کاربر عادی می توانند متن جدید وارد کنند.اما تغییر در سایر توابع با گذاشتن چنین ساختاری است:if ($text->user->id == auth()->user()->id || auth()->user()->is_admin) { return ...; } return abort('403');در این کد می گوییم عملیات اصلی را در دو صورت انجام بده: یا آی دی متن فعلی با آدی کاربر فعلی تطابق داشته باشد یا کاربر ادمین باشد. در غیر این دو حالت، خطای 403 را برگردان (forbidden).اگر مشابه این تغییرات را بخواهیم در ویوها انجام دهیم باید با دستورات blade کار کنیم. مثلن می خواهیم بگوییم بخشی از صفحه را فقط در صورتی به کاربر نمایش بده که کاربر ادمین باشد. در این حالت از این کد استفاده می کنیم:@if (auth()->user()->is_admin) ... @endifهمین کد به صورت inline به شکل زیر می شود:{{(auth()->user()->is_admin) ? s : e}}در کد بالا گفته ایم که اگر کاربر ادمین بود s را اجرا کن و در غیر این صورت e را. مثلن می خواهیم بگوییم، اندازه‌ی یک بلوک را بر اساس ادمین بودن یا نبودن یک کاربر تغییر بده:<div class=&quotcol-md-{{(auth()->user()->is_admin) ? 7 : 12}} col-lg-{{(auth()->user()->is_admin) ? 8 : 12}}&quot>...</div>4- بررسی نهاییبرای این که از عملکرد درست تغییراتی که داده ایم، مطمئن شویم می توانیم از سیدر (Seeder) استفاده کنیم. با سیدر می توانیم (در حالت دیباگ و نه پروداکشن) داده های آزمایشی در جداولمان اضافه کنیم. من این کد را مستقیم در تابع run() سیدر پایگاه داده، نوشتم:DB::table('users')->insert([ 'name' => 'Admin', 'email' => 'admin@test.com', 'password' => \Hash::make('admin'), 'is_admin' => true, ]); DB::table('users')->insert([ 'name' => 'Not Admin', 'email' => 'not_admin@test.com', 'password' => \Hash::make('not_admin'), 'is_admin' => false, ]);به این ترتیب دو کاربر ادمین و عادی خواهم داشت و می توانم تغییرات را با این دو کاربر تست کنم.