?Talamanca = My medicine! | https://goo.gl/ceaBN7
ساخت یه 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") }}">
برای پشتیبانی از 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>
export default {
props: {
title: {
type: String,
default: "Example Card"
},
},
}
و اون رو توی
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>
export default {
mounted() {
document.title = "CRUD Example - Dashboard"
}
}
و خب وقت نوشتن روت ها هستش . اول یه فایل به اسم 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>
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!");
});
}
},
}
خب ما سمت چپ قالب یه منو داریم پس دو تا لینک داشبورد و صفحه لیست کاربرا رو بهش اضافه میکنم
<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 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 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>
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);
}
}
},
}
و روت هام رو به 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>
و خب اینم از CRUD ی که با Laravel , Vue ساخته م و برای اینکه چشماتون درد نیاد و هم کدها رو هلو بپر تو گلو دستتون باشه یه نسخه از این رو روی گیت هاب آپلود میکنم.
https://github.com/amir9480/laravel-vue-crud-example
همچنین اگه دنبال یه روش سریع برای ساخت پنل ادمین میگردین پکیج من برای لاراول رو نگاهی بندازین.
تو جنگ با باگ هاتون موفق باشین.
سایر نوشته هام:
مطلبی دیگر از این انتشارات
ساخت فیلترهای جستجوی پیشرفته در لاراول
مطلبی دیگر از این انتشارات
وظایف اخلاقی یک مهندس نرمافزار در قبال دنیا
مطلبی دیگر از این انتشارات
آموزش زبان برنامهنویسی Rust – قسمت۱۳- شروع کار با Enumeration ها