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

Create | Read | Update | Delete
جز اون چیزایی که همه برنامه نویسا مجبورن باهاش سر و کله بزنن. اکثر قسمت های پنل ادمین یه وبسایت همیناس.
من این جا میخام نشونتون بدم خودم چطوری یه
CRUD
SPA
درست میکنم


خب اول از همه چیز لاراول رو نصب میکنم و همچنین

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="stylesheet" href="{{ mix("asset/admin/css/app.css") }}">
<script src="{{ mix("asset/admin/js/app.js") }}"></script>

برای پشتیبانی از csrf این کد رو به داخل head اضافه میکنم

<meta name="csrf-token" content="{{ csrf_token() }}"> 

و وقتشه همه چیز رو بیارم توی میکس. اول از همه فایل های 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 "~metismenu/dist/metisMenu.css";
@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="app">
کدهای قالب
</div>

حالا با خروجی گرفتن و تنها دو فایل app.css | app.js باید بتونین همون صفحه رو مشاهده کنین.

خب حالا از جاده خاکی بزنیم توی راه اصلی و اون کامپوننت Card رو بسازیم
من فایل جدید نمیسازم و
ExampleComponent.vue
اسمش رو تغییر میدم به
CardComponent.vue
و این کدها رو توش مینویسم

<template>
 <div class="panel panel-default">
 <div class="panel-heading">
 <i class="fa fa-bar-chart-o fa-fw"></i>{{ title }}
 <div class="pull-right">
 <div class="btn-group">
 <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
                        Actions
 <span class="caret"></span>
 </button>
 <ul class="dropdown-menu pull-right" role="menu">
 <li><a href="#">Action</a>
 </li>
 <li><a href="#">Another action</a>
 </li>
 <li><a href="#">Something else here</a>
 </li>
 <li class="divider"></li>
 <li><a href="#">Separated link</a>
 </li>
 </ul>
 </div>
 </div>
 </div>
 <!-- /.panel-heading -->
 <div class="panel-body">
 <slot></slot>
 </div>
 <!-- /.panel-body -->
 </div>
</template>

<script>
 export default {
 props: {
 title: {
 type: String,
 default: "Example Card"
            },
        },
    }
</script>

و اون رو توی
app.js
ثبت میکنم

Vue.component('card-component', require('./components/CardComponent.vue'));

و درنهایت به
view
خودم جهت تست این خط رو مینویسم

<card-component title="TEST TEST TEST">
                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="TEST TEST TEST">
        Hello World
 </card-component>
</template>

<script>
export default {
 mounted() {
 document.title = "CRUD Example - Dashboard";
        }
    }
</script>

و خب وقت نوشتن روت ها هستش . اول یه فایل به اسم routes.js درست میکنم. و این محتویات رو توش مینویسم. این ها برای vue-router هست

export default [
    { path: '/admin', component: require("./components/DashboardComponent.vue"), 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("./components/crud/users/ListComponent.vue"), name: 'admin.users.index'}

و ListComponent رو این شکلی مینویسم

<template>
 <card-component title="Users List">
 <div class="table-responsive">
 <table class="table table-striped table-bordered table-hover">
 <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="users === null">
 <td colspan="6">
 <h2>Loading...</h2>
 </td>
 </tr>
 <tr v-else-if="users.data.length > 0" v-for="user in users.data">
 <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="6">
 <h3>No Users Exists</h3>
 </td>
 </tr>
 </tbody>
 </table>
 </div>
 <ul v-if="users != null" class="pagination">
 <li v-for="page in users.last_page" :class="{active : users.current_page == page}"><router-link :to="{ name:'admin.users.index', query: { page } }">{{ page }}</router-link></li>
 </ul>
 </card-component>
</template>

<script>
 export default {
 data() {
 return {
 users: null
            }
        },
 mounted() {
  document.title = "CRUD Example - users";
 this.loadUsers();
        },
 watch: {
 '$route.query'(newValue, oldValue) {
 this.loadUsers();
            }
        },
 methods: {
 loadUsers() {
 var self = this;
 this.user = null;
 axios.get("/admin/users?page="+(this.$route.query.page ? this.$route.query.page : 1))
                .then(function(res){
 self.users = res.data;
                })
                .catch(function(error) {
 alert("OOPS... something went wrong!");
                });
            }
        },
    }
</script>

خب ما سمت چپ قالب یه منو داریم پس دو تا لینک داشبورد و صفحه لیست کاربرا رو بهش اضافه میکنم

<li>
 <router-link :to="{name:'admin.dashboard'}"><i class="fa fa-dashboard fa-fw"></i> Dashboard</router-link>
 </li>
 <li>
 <router-link :to="{name:'admin.users.index'}"><i class="fa fa-user fa-fw"></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("Are you sure you want delete user '"+user.name+"'?")) {
 var self = this;
 axios.delete("/admin/users/"+user.id).then(function(res){
 alert("The user deleted successfully!");
                    })
                    .catch(function(error) {
 alert("OOPS... something went wrong!");
                    }).then(function(){
 self.loadUsers();
                    });
                }
            }

و در نهایت توی ستون action دکمه ای رو جهت کلیک برای حذف میذارم

<td>
 <button type="button" class="btn btn-danger btn-circle" @click.prevent="deleteUser(user)"><i class="fa fa-trash"></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("post")?'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="pageTitle">
 <router-link :to="{ name:'admin.users.index' }" type="button" class="btn btn-primary">Back</router-link>
 <router-link v-if="$route.params && $route.name == 'admin.users.show'" :to="{ name:'admin.users.edit', params:{user_id: $route.params.user_id} }" type="button" class="btn btn-warning">Edit</router-link>
 <br><br>
 <div v-for="formError in formErrors" class="alert alert-danger">
            {{ formError }}
 </div>
 <div v-for="formMessage in formMessages" class="alert alert-success">
            {{ formMessage }}
 </div>
 <form role="form">
 <div class="form-group">
 <label>Name</label>
 <input type="text" v-model="user.name" :readonly="$route.name == 'admin.users.show'" class="form-control">
 </div>
 <div class="form-group">
 <label>Email</label>
 <input type="email" v-model="user.email" :readonly="$route.name == 'admin.users.show'" class="form-control">
 </div>
 <div class="form-group" :hidden="$route.name == 'admin.users.show'">
 <label>Password</label>
 <input type="password" v-model="user.password" :readonly="$route.name == 'admin.users.show'" class="form-control">
 <p class="help-block" v-if="$route.name == 'admin.users.edit'">Keep empty to prevent changing password</p>
 </div>
 <div class="form-group" v-if="$route.name == 'admin.users.show'">
 <label>Created at</label>
 <input type="email" v-model="user.created_at" disabled="disabled" class="form-control">
 </div>
 <div class="form-group" v-if="$route.name == 'admin.users.show'">
 <label>Last update</label>
 <input type="email" v-model="user.last_update" disabled="disabled" class="form-control">
 </div>
 <button v-if="$route.name != 'admin.users.show'" type="button" class="btn btn-default" @click.prevent="submit" :disabled="loading">Submit</button>
 </form>
 </card-component>
</template>

<script>
 export default {
 data() {
 return {
 user: {
 name: "",
 email: "",
 password: "",
 created_at: "",
 last_update: ""
                },
 pageTitle: "Create user",
 formErrors: {},
 formMessages: [],
 loading: true
            }
        },
 mounted() {
 if(this.$route.name == "admin.users.edit") {
 this.pageTitle = "Edit user";
 this.load();
            } else if(this.$route.name == "admin.users.show") {
 this.pageTitle = "Show user";
 this.load();
            } else {
 this.loading= false;
            }
 document.title = "CRUD Example - "+this.pageTitle;
        },
 methods: {
 load() {
 var self = this;
 axios.get("/admin/users/"+ this.$route.params.user_id)
                .then(function(res){
 self.user = res.data;
 self.loading = false;
                })
                .catch(function(error){
 alert("OOPS... something went wrong!");
                });
            },
 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("OOPS... something went wrong!");
                    }
 self.loading = false;
                };
 if(this.$route.name == "admin.users.create") {
 axios.post("/admin/users", this.user)
                    .then(function(res){
 self.formMessages = ["New user added successfully."];
 setTimeout(function(){
 self.$router.push({name: "admin.users.index"});
                        }, 2000);
                    })
                    .catch(errorHandler);
                } else if(this.$route.name == "admin.users.edit") {
 axios.put("/admin/users/"+this.$route.params.user_id, this.user)
                    .then(function(res){
 self.formMessages = ["The user updated successfully."];
 setTimeout(function(){
 self.$router.push({name: "admin.users.index"});
                        }, 2000);
                    })
                    .catch(errorHandler);
                }

            }
        },
    }
</script>

و روت هام رو به routes.js اضافه میکنم

   { path: '/admin/users/create', component: require("./components/crud/users/FormComponent.vue"), name: 'admin.users.create'},
    { path: '/admin/users/:user_id', component: require("./components/crud/users/FormComponent.vue"), name: 'admin.users.show', props: true},
    { path: '/admin/users/:user_id/edit', component: require("./components/crud/users/FormComponent.vue"), name: 'admin.users.edit', props: true}

حالا برای دکمه افزودن یه دکمه بالای کامپوننت لیست میسازم

<router-link :to="{ name:'admin.users.create' }" type="button" class="btn btn-success">Create</router-link>

و همچنین برای دکمه های مشاهده و اصلاح آیتم ها کنار هر آیتم و کنار دکمه حذف قرارشون میدم

<router-link type="button" class="btn btn-success btn-circle" :to="{name:'admin.users.show', params:{user_id:user.id}}"><i class="fa fa-eye"></i></router-link>
 <router-link type="button" class="btn btn-warning btn-circle" :to="{name:'admin.users.edit', params:{user_id:user.id}}"><i class="fa fa-cut"></i></router-link>
List
List
Create
Create


Edit
Edit
Show
Show

و خب اینم از CRUD ی که با Laravel , Vue ساخته م و برای اینکه چشماتون درد نیاد و هم کدها رو هلو بپر تو گلو دستتون باشه یه نسخه از این رو روی گیت هاب آپلود میکنم.
https://github.com/amir9480/laravel-vue-crud-example

پی نوشت: اگه یکم سورس کد با لاراول حالت عادی فرق داره به خاطر اینه که من برای استفاده شخصی خودم یه پکیج نوشتم که کدها و پکیج هایی که هر بار مجبورم بعد نصب لاراول به برنامم اضافه کنم رو یک جا جمع کردم که دوباره کاری نشه وگرنه این همون لاراوله فرق چندانی نداره .
https://github.com/amir9480/laravel-kit
تو جنگ با باگ هاتون موفق باشین.

سایر نوشته هام:

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