ساخت یه CRUD با Laravel و Vue

Create | Read | Update | Deleteجز اون چیزایی که همه برنامه نویسا مجبورن باهاش سر و کله بزنن. اکثر قسمت های پنل ادمین یه وبسایت همیناس. من این جا میخام نشونتون بدم خودم چطوری یه CRUDSPAدرست میکنمخب اول از همه چیز لاراول رو نصب میکنم و همچنین php artisan make:authبرای اینکه میخام فقط کاربرها بتونن به صفحه مورد نظر دسترسی داشته باشن بعد محتویات فایل .env رو مطابق نیازم دستکاری میکنم و میگریت رو انجام میدمبعد داخل پوشه ساخته شده بساط لاراول میکس رو فراهم میکنمnpm installاگه npm بلد نیستید یا نصب ندارید حتما نصب کنید و یاد بگیرین :)خب برای اینکه آدرس دهی داشته باشیم توی این کراد باید vue-router گرامی رو نصب کنیم.npm install vue-routerو خب من چون خیلی آدم خسته ایم و همچنین از طراحی و این داستانا کلا هیچی حالیم نیست و همه از ذائقه غیر هنریم انتقاد میکنن من میرم و یه قالب آماده تهیه میکنم برای پنل که به نظرم این یکی خیلی تمیز و ساده س.برای شروع اول یه روت آماده میکنم برای نمایش پنل ادمین. من برای جلوگیری از شلوغ شدن web.phpیه فایل دیگه میسازم توی routes مثلا به اسم admin.php و روت های مربوط به پنل ادمین رو اونجا قرار میدم. شما میتونین به همون روش عادی توی web.php روت ها رو تعریف کنین اجباری در کار نیست برای معرفی کردنش به لاراول هم فایل app/Providers/RouteServiceProvider.php رو باید ادیت کنمتوی تابع mapWebRoutesاین خط کد رو اضافه میکنمRoute::middleware(['web', 'auth']) ->prefix('admin') ->name('admin.') ->namespace($this->namespace.'\\Admin') ->group(base_path('routes/admin.php'));این خط میاد به اول اسم همه روت ها admin. اضافه میکنه. تمام آدرس های توی admin.php با admin شروع میشن. همشون میدلویر دارن برای جلوگیری از دسترسی کاربر وارد نشده به سایت(بعدا یه کار کنین فقط ادمین بیاد ) دارن و البته تمام کنترلر های مربوط به ادمین رو توی پوشه Admin میسازم تا با بقیه قاطی پاطی نشه. ( باز هم تکرار میکنم این روش منه اجباری در کار نیست )خب حالا یه کنترلر میسازم برای نمایش داشبوردphp artisan make:controller Admin\DashboardController کد های مورد نیاز رو توی public و view حاضر میکنم اسمش رو هم میذارمmaster.blade.phpتابع نمایش داشبورد رو توی کنترلر مینویسمpublic function dashboard() { return view('admin.master'); }و فایل روت admin.php رو این شکلی مینویسمRoute::get('/', 'DashboardController@dashboard')->name('dashboard');و درنهایت سایت رو بازمیکنم و یه اکانت برای خودم میسازم و آدرس پایین رو بازمیکنمlocalhost/adminاگه تا اینجای کار یه کم گیج شدین نگران نباشین انتهای مطلب سورس کد رو گذاشتم کد همه چیز رو میگه :) فقط کافیه بدونین blade چه شکلی کار میکنه.خب من حالا میخام نوشتن اولین کامپونت Vue رو با این قسمت Card که وسط صفحه میبینین که CRUD Example نوشته و action داره شروع کنم و تبدیلش کنم به یه کامپنونت تا ازش استفاده کنماول فایل webpack.mix.js رو یکم ادیت میکنم تا محل خروجی فایل رو عوض کنمmix.js('resources/assets/js/app.js', 'public/asset/admin/js') .sass('resources/assets/sass/app.scss', 'public/asset/admin/css');و بعد کامند لاراول میکس رو اجرا میکنم تا خروجی ها رو ببینم که توی public/asset هستش و همچنین آدرس ها رو اضافه میکنم به view<link rel=&quotstylesheet&quot href=&quot{{ mix(&quotasset/admin/css/app.css&quot) }}&quot><script src=&quot{{ mix(&quotasset/admin/js/app.js&quot) }}&quot>برای پشتیبانی از csrf این کد رو به داخل head اضافه میکنم<meta name=&quotcsrf-token&quot content=&quot{{ csrf_token() }}&quot> و وقتشه همه چیز رو بیارم توی میکس. اول از همه فایل های boostrap و jquery رو حذف میکنم از داخل قالب چون اونا توی app.css , app.js به صورت پیش فرض موجود هستن. همچنین اسکریپت های مربوط به چارت ها رو هم پاک میکنم چون توی این برنامه بهشون احتیاجی ندارم. میمونه یه فایل metismenu که باید بگردید روی npm و پکیجش رو پیدا کنین یا با mix به فایل app.js اضافه ش کنین که خب طی یه پرس و جو از عمو گوگل این پکیج رو روی npm یافتم و نصبش کردمnpm install metismenuبعد تست مجدد متوجه شدم که این قالب از بوت استرپ سه استفاده میکرده و ما بهش داریم چهار رو میدیم و قیافه سایت شبیه مگس شده ! بنابراین بوت استرپ رو میبریمش به نسخه سهnpm install bootstrap@^3.*و فایل app.scss رو به این شکل اصلاح میکنم// Bootstrap @import '~bootstrap/dist/css/bootstrap.css'; @import &quot~metismenu/dist/metisMenu.css&quot @import '../../../public/asset/admin/vendor/font-awesome/css/font-awesome.min.css'; @import '../../../public/asset/admin/css/sb-admin-2.css';مطمئن بشین آدرس ها رو درست دادین چون تو لاراول 5.7 ساختار پوشه resource تغییر میکنه و من این برنامه رو با 5.6 نوشتم :)از طرف دیگه ما باید کدهایی که سازنده قالب نوشته رو به کدهای خودمون اضافه کنیم برای این یکی من فایل webpack.mix.js رو دستکاری میکنم mix.js([ 'resources/assets/js/app.js', 'public/asset/admin/js/sb-admin-2.js' ], 'public/asset/admin/js') .sass('resources/assets/sass/app.scss', 'public/asset/admin/css');و در نهایت metisMenu رو به فایل app.js اضافه میکنمrequire('./bootstrap'); require('metismenu');در انتها یه div به body اضافه کنین و تمام کدهای body رو توی اون بنویسین تا vue.js بتونیم استفاده کنیم البته میتونیم el رو برابر #wrapper بذاریم ولی من ترجیح میدم المنت جدید بسازم<div id=&quotapp&quot> کدهای قالب </div>حالا با خروجی گرفتن و تنها دو فایل app.css | app.js باید بتونین همون صفحه رو مشاهده کنین.خب حالا از جاده خاکی بزنیم توی راه اصلی و اون کامپوننت Card رو بسازیممن فایل جدید نمیسازم و ExampleComponent.vueاسمش رو تغییر میدم به CardComponent.vueو این کدها رو توش مینویسم <template> <div class=&quotpanel panel-default&quot> <div class=&quotpanel-heading&quot> <i class=&quotfa fa-bar-chart-o fa-fw&quot></i>{{ title }} <div class=&quotpull-right&quot> <div class=&quotbtn-group&quot> <button type=&quotbutton&quot class=&quotbtn btn-default btn-xs dropdown-toggle&quot data-toggle=&quotdropdown&quot> Actions <span class=&quotcaret&quot></span> </button> <ul class=&quotdropdown-menu pull-right&quot role=&quotmenu&quot> <li><a href=&quot#&quot>Action</a> </li> <li><a href=&quot#&quot>Another action</a> </li> <li><a href=&quot#&quot>Something else here</a> </li> <li class=&quotdivider&quot></li> <li><a href=&quot#&quot>Separated link</a> </li> </ul> </div> </div> </div> <!-- /.panel-heading --> <div class=&quotpanel-body&quot> <slot></slot> </div> <!-- /.panel-body --> </div> </template> export default { props: { title: { type: String, default: &quotExample Card&quot }, }, } و اون رو توی app.jsثبت میکنمVue.component('card-component', require('./components/CardComponent.vue'));و درنهایت به viewخودم جهت تست این خط رو مینویسم <card-component title=&quotTEST TEST TEST&quot> Hello World </card-component>خب من میخام ساخت CRUD برای مدل User رو شروع کنماول یه کنترلر میسازمphp artisan make:controller Admin\UserController --resource --model=Userو روت لازم رو براش مینویسمRoute::resource('/users', 'UserController');بعد داخل کنترلر درون توابع index | create | show | editاین کد رو مینویسمreturn view('admin.master');یادتون نره میتونین با ساخت یه trait کدتون رو تمیزتر بنویسین ولی من برای این مثال همین طوری مینویسم چون یه کلاس بیشتر نداریماول از همه کامپونتت صفحه اصلی داشبورد رو مینویسم اسمش رو میذارم DashboardComponent<template> <card-component title=&quotTEST TEST TEST&quot> Hello World </card-component> </template> export default { mounted() { document.title = &quotCRUD Example - Dashboard&quot } } و خب وقت نوشتن روت ها هستش . اول یه فایل به اسم routes.js درست میکنم. و این محتویات رو توش مینویسم. این ها برای vue-router هستexport default [ { path: '/admin', component: require(&quot./components/DashboardComponent.vue&quot), name: 'admin.dashboard'} ];و app.js رو برای استفاده از vue-router به این شکل اصلاح میکنمrequire('./bootstrap'); require('metismenu'); import Vue from 'vue'; import VueRouter from 'vue-router'; import routes from './routes'; Vue.use(VueRouter); const router = new VueRouter({ mode: 'history', routes }); Vue.component('card-component', require('./components/CardComponent.vue')); const app = new Vue({ el: '#app', router });و توی view خودمون باید تگ مربوطه رو وسط صفحه بنویسیم<router-view></router-view>خب همه چی حاضره و اگه localhost/admin رو بازکنین میتونین محتویات DashboardComponent رو ببینین. برای ادامه کار کامپوننت صفحه جدول کاربرا رو میسازم ( crud/users/ListComponent) رو و برای اینکه بتونم لیست کاربرا رو داشته باشم تابع index UserController رو این شکلی مینویسم.public function index(Request $request) { if($request->ajax()) { return User::paginate(20); } return view('admin.master'); }این شکلی کار میکنه که چک میکنه axios درخواست ajax داده پس کلا کاربرا رو بر میگردونه وگرنه داشبورد رو نشون میده.به routes.js این روت رو اضافه میکنم { path: '/admin/users', component: require(&quot./components/crud/users/ListComponent.vue&quot), name: 'admin.users.index'}و ListComponent رو این شکلی مینویسم<template> <card-component title=&quotUsers List&quot> <div class=&quottable-responsive&quot> <table class=&quottable table-striped table-bordered table-hover&quot> <thead> <tr> <th>#</th> <th>Name</th> <th>Email</th> <th>Created at</th> <th>Last update</th> <th>Actions</th> </tr> </thead> <tbody> <tr v-if=&quotusers === null&quot> <td colspan=&quot6&quot> <h2>Loading...</h2> </td> </tr> <tr v-else-if=&quotusers.data.length > 0&quot v-for=&quotuser in users.data&quot> <td>{{ user.id }}</td> <td>{{ user.name }}</td> <td>{{ user.email }}</td> <td>{{ user.created_at }}</td> <td>{{ user.updated_at }}</td> <td></td> </tr> <tr v-else> <td colspan=&quot6&quot> <h3>No Users Exists</h3> </td> </tr> </tbody> </table> </div> <ul v-if=&quotusers != null&quot class=&quotpagination&quot> <li v-for=&quotpage in users.last_page&quot :class=&quot{active : users.current_page == page}&quot><router-link :to=&quot{ name:'admin.users.index', query: { page } }&quot>{{ page }}</router-link></li> </ul> </card-component> </template> export default { data() { return { users: null } }, mounted() { document.title = &quotCRUD Example - users&quot this.loadUsers(); }, watch: { '$route.query'(newValue, oldValue) { this.loadUsers(); } }, methods: { loadUsers() { var self = this; this.user = null; axios.get(&quot/admin/users?page=&quot+(this.$route.query.page ? this.$route.query.page : 1)) .then(function(res){ self.users = res.data; }) .catch(function(error) { alert(&quotOOPS... something went wrong!&quot); }); } }, } خب ما سمت چپ قالب یه منو داریم پس دو تا لینک داشبورد و صفحه لیست کاربرا رو بهش اضافه میکنم<li> <router-link :to=&quot{name:'admin.dashboard'}&quot><i class=&quotfa fa-dashboard fa-fw&quot></i> Dashboard</router-link> </li> <li> <router-link :to=&quot{name:'admin.users.index'}&quot><i class=&quotfa fa-user fa-fw&quot></i> Users</router-link> </li>خب میخوام یه کار خرکی انجام بدم تاریخ last update رو میخام به سبک چند روز پیش و فلان بنویسم به نظرم اینطوری بهتره. برای شروع اول این تابع رو توی مدل User مینویسمpublic function getLastUpdateAttribute() { return $this->updated_at->diffForHumans(); }و برای اینکه بتونم توی جاوااسکریپت بهش دسترسی داشته باشم باید $appends رو برای مدل تعریف کنمprotected $appends = [ 'last_update' ];و در نهایت در موقع نمایش به جای updated_at این رو مینویسم<td>{{ user.last_update }}</td>حالا بریم دومین قدم برای CRUD میخام برای هر آیتم قابلیت حذف شدن رو اضافه کنم. اول تابع destroy توی کنترلمون رو آماده میکنمpublic function destroy(User $user) { $user->delete(); return ['success' => true]; }و متدی رو به ListComponent اضافه میکنم که کاربر رو میگیره و عملیات حذف رو انجام میده. deleteUser(user) { if(confirm(&quotAre you sure you want delete user '&quot+user.name+&quot'?&quot)) { var self = this; axios.delete(&quot/admin/users/&quot+user.id).then(function(res){ alert(&quotThe user deleted successfully!&quot); }) .catch(function(error) { alert(&quotOOPS... something went wrong!&quot); }).then(function(){ self.loadUsers(); }); } }و در نهایت توی ستون action دکمه ای رو جهت کلیک برای حذف میذارم<td> <button type=&quotbutton&quot class=&quotbtn btn-danger btn-circle&quot @click.prevent=&quotdeleteUser(user)&quot><i class=&quotfa fa-trash&quot></i></button> </td>برای ساخت قسمت فرم هم اول یه request میسازم.php artisan make:request Admin\UserRequestداخل متد authorize رو به return true تغییر میدم و rules رو به شکل زیر مینویسمpublic function rules() { return [ 'name' => 'required|string|max:191', 'email' => 'required|email|unique:users,email,'.optional($this->user)->id.'|max:191', 'password' => ($this->isMethod(&quotpost&quot)?'required':'nullable').'|string|min:6', ]; }و این کلاس رو جایگزین میکنم توی ورودی های store و update کنترلرممتد view کنترلرم رو ادیت میکنم برای اینکه بتونم به صورت json به اطلاعات دسترسی داشته باشمpublic function show(Request $request, User $user) { if($request->ajax()) { return $user; } return view('admin.master'); }و store و update رو این شکلی با هم مخلوطشون میکنم که دوباره کاری انجام ندم!public function store(UserRequest $request) { $user = new User; return $this->update($request, $user); } public function update(UserRequest $request, User $user) { $inputs = $request->only('name', 'email', 'password'); if(empty($inputs['password'])) unset($inputs['password']); else $inputs['password'] = bcrypt($inputs['password']); $user->fill($inputs); $user->save(); return ['success' => true]; } خب آخرین کامپوننتی که باید بسازم فرمی هست که اطلاعات کاربر رو میفرسته (crud/users/FormComponent) و محتویات زیر رو در اون قرار میدم.<template> <card-component :title=&quotpageTitle&quot> <router-link :to=&quot{ name:'admin.users.index' }&quot type=&quotbutton&quot class=&quotbtn btn-primary&quot>Back</router-link> <router-link v-if=&quot$route.params && $route.name == 'admin.users.show'&quot :to=&quot{ name:'admin.users.edit', params:{user_id: $route.params.user_id} }&quot type=&quotbutton&quot class=&quotbtn btn-warning&quot>Edit</router-link> <br><br> <div v-for=&quotformError in formErrors&quot class=&quotalert alert-danger&quot> {{ formError }} </div> <div v-for=&quotformMessage in formMessages&quot class=&quotalert alert-success&quot> {{ formMessage }} </div> <form role=&quotform&quot> <div class=&quotform-group&quot> <label>Name</label> <input type=&quottext&quot v-model=&quotuser.name&quot :readonly=&quot$route.name == 'admin.users.show'&quot class=&quotform-control&quot> </div> <div class=&quotform-group&quot> <label>Email</label> <input type=&quotemail&quot v-model=&quotuser.email&quot :readonly=&quot$route.name == 'admin.users.show'&quot class=&quotform-control&quot> </div> <div class=&quotform-group&quot :hidden=&quot$route.name == 'admin.users.show'&quot> <label>Password</label> <input type=&quotpassword&quot v-model=&quotuser.password&quot :readonly=&quot$route.name == 'admin.users.show'&quot class=&quotform-control&quot> <p class=&quothelp-block&quot v-if=&quot$route.name == 'admin.users.edit'&quot>Keep empty to prevent changing password</p> </div> <div class=&quotform-group&quot v-if=&quot$route.name == 'admin.users.show'&quot> <label>Created at</label> <input type=&quotemail&quot v-model=&quotuser.created_at&quot disabled class=&quotform-control&quot> </div> <div class=&quotform-group&quot v-if=&quot$route.name == 'admin.users.show'&quot> <label>Last update</label> <input type=&quotemail&quot v-model=&quotuser.last_update&quot disabled class=&quotform-control&quot> </div> <button v-if=&quot$route.name != 'admin.users.show'&quot type=&quotbutton&quot class=&quotbtn btn-default&quot @click.prevent=&quotsubmit&quot :disabled=&quotloading&quot>Submit</button> </form> </card-component> </template> export default { data() { return { user: { name: &quot&quot, email: &quot&quot, password: &quot&quot, created_at: &quot&quot, last_update: &quot&quot }, pageTitle: &quotCreate user&quot, formErrors: {}, formMessages: [], loading: true } }, mounted() { if(this.$route.name == &quotadmin.users.edit&quot) { this.pageTitle = &quotEdit user&quot this.load(); } else if(this.$route.name == &quotadmin.users.show&quot) { this.pageTitle = &quotShow user&quot this.load(); } else { this.loading= false; } document.title = &quotCRUD Example - &quot+this.pageTitle; }, methods: { load() { var self = this; axios.get(&quot/admin/users/&quot+ this.$route.params.user_id) .then(function(res){ self.user = res.data; self.loading = false; }) .catch(function(error){ alert(&quotOOPS... something went wrong!&quot); }); }, submit() { var self = this; this.loading = true; this.formErrors = {}; var errorHandler = function(error){ if(error.response && error.response.status == 422) { var formErrors = {}; for(var i in error.response.data.errors) formErrors[i] = error.response.data.errors[i][0]; self.formErrors = formErrors; } else { alert(&quotOOPS... something went wrong!&quot); } self.loading = false; }; if(this.$route.name == &quotadmin.users.create&quot) { axios.post(&quot/admin/users&quot, this.user) .then(function(res){ self.formMessages = [&quotNew user added successfully.&quot]; setTimeout(function(){ self.$router.push({name: &quotadmin.users.index&quot}); }, 2000); }) .catch(errorHandler); } else if(this.$route.name == &quotadmin.users.edit&quot) { axios.put(&quot/admin/users/&quot+this.$route.params.user_id, this.user) .then(function(res){ self.formMessages = [&quotThe user updated successfully.&quot]; setTimeout(function(){ self.$router.push({name: &quotadmin.users.index&quot}); }, 2000); }) .catch(errorHandler); } } }, } و روت هام رو به routes.js اضافه میکنم { path: '/admin/users/create', component: require(&quot./components/crud/users/FormComponent.vue&quot), name: 'admin.users.create'}, { path: '/admin/users/:user_id', component: require(&quot./components/crud/users/FormComponent.vue&quot), name: 'admin.users.show', props: true}, { path: '/admin/users/:user_id/edit', component: require(&quot./components/crud/users/FormComponent.vue&quot), name: 'admin.users.edit', props: true}حالا برای دکمه افزودن یه دکمه بالای کامپوننت لیست میسازم<router-link :to=&quot{ name:'admin.users.create' }&quot type=&quotbutton&quot class=&quotbtn btn-success&quot>Create</router-link>و همچنین برای دکمه های مشاهده و اصلاح آیتم ها کنار هر آیتم و کنار دکمه حذف قرارشون میدم<router-link type=&quotbutton&quot class=&quotbtn btn-success btn-circle&quot :to=&quot{name:'admin.users.show', params:{user_id:user.id}}&quot><i class=&quotfa fa-eye&quot></i></router-link> <router-link type=&quotbutton&quot class=&quotbtn btn-warning btn-circle&quot :to=&quot{name:'admin.users.edit', params:{user_id:user.id}}&quot><i class=&quotfa fa-cut&quot></i></router-link>ListCreateEditShowو خب اینم از CRUD ی که با Laravel , Vue ساخته م و برای اینکه چشماتون درد نیاد و هم کدها رو هلو بپر تو گلو دستتون باشه یه نسخه از این رو روی گیت هاب آپلود میکنم.https://github.com/amir9480/laravel-vue-crud-exampleهمچنین اگه دنبال یه روش سریع برای ساخت پنل ادمین میگردین پکیج من برای لاراول رو نگاهی بندازین. https://github.com/sanjabteam/sanjab تو جنگ با باگ هاتون موفق باشین.سایر نوشته هام: https://virgool.io/@amiralizadeh9480/%D8%B3%D8%A7%D8%AE%D8%AA-%DA%A9%D9%88%D8%AA%D8%A7%D9%87-%DA%A9%D9%86%D9%86%D8%AF%D9%87-%D9%84%DB%8C%D9%86%DA%A9-%D8%A8%D8%A7-%D9%84%D8%A7%D8%B1%D8%A7%D9%88%D9%84-vcrlsheusa5f https://virgool.io/@amiralizadeh9480/%D8%B3%D8%A7%D8%AE%D8%AA-%D9%81%D8%B1%D9%85-%D8%AA%D9%85%D8%A7%D8%B3-%D8%A8%D8%A7-%D9%85%D8%A7-%D8%B3%D8%A7%D8%AF%D9%87-%D8%AF%D8%B1-%D9%84%D8%A7%D8%B1%D8%A7%D9%88%D9%84-nlkf9yro0a0c https://virgool.io/JavaScript8/%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-%D9%81%D8%A7%DB%8C%D9%84-%D9%87%D8%A7%DB%8C-%D8%AA%D8%B1%D8%AC%D9%85%D9%87-%D9%84%D8%A7%D8%B1%D8%A7%D9%88%D9%84-%D8%AF%D8%B1-%D8%AC%D8%A7%D9%88%D8%A7-%D8%A7%D8%B3%DA%A9%D8%B1%DB%8C%D9%BE%D8%AA-mxxpowoydz0z