# laravel 專案建立+ 後台產生+ 雜七雜八套件 ###### tags: `Laravel` --- # 時間管理 php套件 - carbon https://carbon.nesbot.com/docs/ --- # Route 列出所有Route列表 ``` php artisan route:list ``` --- # Middleware (中介層) ## 使用方式 1. 下在route中 在單一一個Route後面加上middleware ``` Route::get('admin/profile', function () { // })->middleware('auth'); ``` Route群組起來(https://laravel.com/docs/master/routing#route-groups),加上middleware ``` Route::middleware(['auth'])->group(function () { Route::get('/', function () { // 使用 auth 中间件 }); }); ``` 更進階的做法 prefix(路由前綴) + group(群組) + Auth middleware(中介層) ``` Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function(){ Route::get('/', function () { // 使用 auth 中间件 }); }); ``` --- e.g. ``` Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function(){ Route::get('/','AdminController@index'); Route::get('product', 'ProductController@index'); Route::get('product/create', 'ProductController@create'); Route::post('product/store', 'ProductController@store'); Route::get('product/edit/{id}', 'ProductController@edit'); Route::post('product/update/{id}', 'ProductController@update'); Route::post('product/destroy/{id}', 'ProductController@destroy'); Route::get('news', 'NewsController@index'); Route::get('news/create', 'NewsController@create'); Route::post('news/store', 'NewsController@store'); Route::get('news/edit/{id}', 'NewsController@edit'); Route::post('news/update/{id}', 'NewsController@update'); Route::post('news/destroy/{id}', 'NewsController@destroy'); }); ``` --- # 建立後臺步驟 ## 準備步驟 1. Run Authentication(https://laravel.com/docs/master/authentication) 2. 由於Laravel 更新7版,UI更新到2.0 執行下面第二句就能安裝UI ``` composer require laravel/ui --dev composer require laravel/ui "^1.0" --dev -vvv php artisan ui vue --auth npm run dev ``` <!-- composer require laravel/ui:^2.4 --> https://laravel.com/docs/7.x/frontend 建立使用者資料表 ``` php artisan migrate ``` 2. 修改`$redirectTo` 修改以下路徑的php檔案,要將其中的$redirectTo從 '/home' => '/admin'. * app\Http\Controllers\Auth底下的 * ConfirmPasswordController.php * LoginController.php * RegisterController.php * ResetPasswordController.php * VerificationController.php * app\Http\Middleware底下的 * RedirectIfAuthenticated.php 共有六處要做修改。 * app\Http\Providers底下的 * RouteServiceProvider.php ```public const admin = '/admin';``` 可改可不改 改了 註冊後不會錯誤 重新導向admin的route 3. 建立後臺要使用的Route列表 e.g. 以產品管理為例,可以使用以下指令來快速產生ProductController,加上參數`--resource`可以再把Controller中的程式碼預先長出來。 ``` php artisan make:controller ProductController --resource ``` 通常來說,在後台的特定管理動作,會包含新增、編輯、刪除三個動作。 統計共會使用到以下Route步驟 ``` Route::get('product', 'ProductController@index'); Route::get('product/create', 'ProductController@create'); Route::post('product/store', 'ProductController@store'); Route::get('product/edit/{id}', 'ProductController@edit'); Route::post('product/update/{id}', 'ProductController@update'); Route::post('product/destroy/{id}', 'ProductController@destroy'); ``` 如果要再仔細說明一下其中每個Route的作用 ``` Route::get('product', 'ProductController@index'); //----------------> 列出所有產品的頁面 Route::get('product/create', 'ProductController@create'); //---------> 到建立產品的頁面 Route::post('product/store', 'ProductController@store'); //----------> "儲存"產品資料 Route::get('product/edit/{id}', 'ProductController@edit'); //--------> 到特定產品的頁面 Route::post('product/update/{id}', 'ProductController@update'); //---> "更新"產品資料 Route::post('product/destroy/{id}', 'ProductController@destroy'); //---> "刪除"產品資料 ``` 4. 修改app layout (resources\views\layouts\app.blade.php) 原始檔案 ``` <!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Scripts --> <script src="{{ asset('js/app.js') }}" defer></script> <!-- Fonts --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <!-- Styles --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm"> <div class="container"> <a class="navbar-brand" href="{{ url('/') }}"> {{ config('app.name', 'Laravel') }} </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- Left Side Of Navbar --> <ul class="navbar-nav mr-auto"> </ul> <!-- Right Side Of Navbar --> <ul class="navbar-nav ml-auto"> <!-- Authentication Links --> @guest <li class="nav-item"> <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a> </li> @if (Route::has('register')) <li class="nav-item"> <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a> </li> @endif @else <li class="nav-item dropdown"> <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> {{ Auth::user()->name }} <span class="caret"></span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> {{ __('Logout') }} </a> <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> @csrf </form> </div> </li> @endguest </ul> </div> </div> </nav> <main class="py-4"> @yield('content') </main> </div> </body> </html> ``` 需修改以下步驟 * 補上yield css跟yield js `@yield('css')` `@yield('js')` * 把app.js 移到 </body>前 ``` <script src="{{ asset('js/app.js') }}"></script> ``` 最終檔案如下 ``` <!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- CSRF Token --> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravel') }}</title> <!-- Fonts --> <link rel="dns-prefetch" href="//fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet"> <!-- Styles --> <link href="{{ asset('css/app.css') }}" rel="stylesheet"> @yield('css') </head> <body> <div id="app"> <nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm"> <div class="container"> <a class="navbar-brand" href="{{ url('/') }}"> {{ config('app.name', 'Laravel') }} </a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <!-- Left Side Of Navbar --> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a class="nav-link" href="/admin/news">最新消息管理</a> </li> </ul> <!-- Right Side Of Navbar --> <ul class="navbar-nav ml-auto"> <!-- Authentication Links --> @guest <li class="nav-item"> <a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a> </li> @if (Route::has('register')) <li class="nav-item"> <a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a> </li> @endif @else <li class="nav-item dropdown"> <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre> {{ Auth::user()->name }} <span class="caret"></span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <a class="dropdown-item" href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> {{ __('Logout') }} </a> <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;"> @csrf </form> </div> </li> @endguest </ul> </div> </div> </nav> <main class="py-4"> @yield('content') </main> </div> <!-- Scripts --> <script src="{{ asset('js/app.js') }}"></script> @yield('js') </body> </html> ``` 5.建立index.create.edit 三個頁面 * resources\views\admin\news\index.blade.php * resources\views\admin\news\create.blade.php * resources\views\admin\news\edit.blade.php --- ## Index 基本上要有以下功能 1. 新增項目的按鈕。 2. 要列出所有的項目,提供編輯及刪除功能。 -- 在列表的部分可以使用Datatable, Datatable中有一個Bootstrap Styling(https://datatables.net/examples/styling/bootstrap4),可以套用該套件,就可以快速有分頁/查詢的功能列表。 要小心再分頁中如果有要用到按鈕事件綁定請參考以下連結 (https://www.gyrocode.com/articles/jquery-datatables-why-click-event-handler-does-not-work/) JQuery On Click Function not working after appending HTML (https://www.codewall.co.uk/jquery-on-click-function-not-working-after-appending-html/) ### DataTable使用注意事項 * 記得去引用以下檔案 ``` <link rel="stylesheet" href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css"> <script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js"></script> ``` * Datatable要記得初始化,可以再去做其他參數設定,如依照特定欄位去排序(https://datatables.net/reference/option/order) ``` $(document).ready(function() { $('#example').DataTable({ "order": [1,"desc"] }); }); ``` --- 最終`index.blade.php`檔案如下 ``` @extends('layouts.app') @section('css') <link rel="stylesheet" href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css"> @endsection @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-12"> <div class="card"> <div class="card-header">最新消息管理 - Index</div> <div class="card-body"> <a class="btn btn-success" href="/admin/news/create">新增最新消息</a> <hr> <table id="example" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>標題(title)</th> <th>排序(sort)</th> <th width="120">功能</th> </tr> </thead> <tbody> @foreach ($items as $item) <tr> <td>{{ $item->title}}</td> <td>{{ $item->sort}}</td> <td> <a class="btn btn-success btn-sm" href="/admin/news/edit/{{ $item->id}}">編輯</a> <a class="btn btn-danger btn-sm" href="#" data-itemid="{{$item->id}}">刪除</a> <form class="destroy-form" data-itemid="{{$item->id}}" action="/admin/news/destroy/{{$item->id}}" method="POST" style="display: none;"> @csrf </form> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </div> </div> @endsection @section('js') <script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script> <script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js"></script> <script> $(document).ready(function() { $('#example').DataTable({ "order": [1,"desc"] }); $('#example').on('click','.btn-danger',function(){ event.preventDefault(); var r = confirm("你確定要刪除此項目嗎?"); if (r == true) { var itemid = $(this).data("itemid"); $(`.destroy-form[data-itemid="${itemid}"]`).submit(); } }); }); </script> @endsection ``` --- ## Create 在Create頁面中,主要是做到新增資料的動作,會使用到html中的Form Post(表單發送)。 -- Laravel的 From Post的範例如下,通常放於create.blade.php ``` <form method="post" action="/admin/news/store" enctype="multipart/form-data"> @csrf <div class="form-group row"> <label for="title" class="col-sm-2 col-form-label">最新消息標題</label> <div class="col-sm-10"> <input type="text" class="form-control" id="title" name="title"> </div> </div> <div class="form-group row"> <label for="sort" class="col-sm-2 col-form-label">sort</label> <div class="col-sm-10"> <input type="number" class="form-control" id="inputEmail3" value="" name="sort"> </div> </div> <div class="form-group row"> <div class="col-sm-10"> <button type="submit" class="btn btn-primary">SEND</button> </div> </div> </form> ``` 在做Form的時候要注意以下內容 * **Form的屬性** `<form></form>`標籤中的三個屬性,分別是`method`、`action`跟`enctype` * method: form表單發送大多是使用post,因為get會將參數顯示在網址列上,且長度有限制。 * action: 表單要發送到的路徑位置,請參考Route接收的路徑 * enctype: 表單的編碼方式,如要上傳檔案,一定要加上。 * **CSRF Token** 表單發送時一定要加上 `@csrf` * **input的name屬性** 未來在contrller接收資料時,是看input的name,一定要加上。 -- 接收Form資料時,Controller的寫法(NewsController.php) ``` public function store(Request $request) { /* *抓出所有表單發送的資料 *$request->all(); */ $title = $request->title; $sort = $request->sort; DB::table('news')->insert( ['title' => $title, 'sort' => $sort] ); return redirect('/admin/news'); } ``` 可以於一開始測試時先使用`$request->all();` 把資料使用`dd($request->all());` 將其所有表單內容抓出來確認。 確認資料欄位皆正確之後,在塞進資料庫中。 -- 新增資料進資料庫的動作通常稱為Inserts,在Laravel中有兩種比較常見Inserts的作法。 1.使用DB `DB::table('users')->insert( ['email' => 'john@example.com', 'votes' => 0] );` 2.使用ORM 方法1 ``` public function store(Request $request) { // Validate the request... $flight = new Flight; $flight->name = $request->name; $flight->save(); } ``` 方法2 ``` $flight = App\Flight::create(['name' => 'Flight 10']); ``` 方法3 ``` $flight = App\Flight::create($request->all()); ``` --- # ORM - 使用Model連結至資料庫 建立Model ``` php artisan make:model News ``` 範例Model ``` <?php namespace App; use Illuminate\Database\Eloquent\Model; class News extends Model { protected $table = 'news'; protected $fillable = ['title','sort']; } ``` --- ## 在Controller中使用ORM 首先要記得引用class e.g. 使用News的Modal => `use App\News;` 新增 ``` //方法1 $news = new News; $news->title = $request->title; $news->sort = $request->sort; $news->save(); ``` ``` //方法2 News::create($request->all()); ``` 修改 ``` //方法1 $news = News::find($id); $news->title = $request->title; $news->sort = $request->sort; $news->save(); ``` ``` //方法2 $news = News::find($id); $news->update($request->all()); ``` --- # 檔案上傳 ## Laravel - filesystem方法 (https://nicole929chan.wordpress.com/2018/05/04/laravel5-file-storage/) (https://learnku.com/articles/5615/some-thoughts-about-the-storagelink-artisan-command) ``` php artisan storage:link ``` 跑過storage:link之後,類似建立一個捷徑的資料夾 public/storage → storage/app/public ``` //Controller public function store(Request $request) { $requsetData = $request->all(); //上傳檔案 $file_name = $request->file('img')->store('','public'); $requsetData['img'] = $file_name; Product::create($requsetData); return redirect('/admin/product'); } //view <img src="{{asset('/storage/'.$item->img)}}" alt=""> ``` ## 暴力直接move檔案到/public中 ``` $imageName = time().'.'.$request->image->getClientOriginalExtension(); $request->image->move(public_path('/uploaded_images'), $imageName); ``` 刪除時會用到File::delete 要記得use Illuminate\Support\Facades\File; ``` public function store(Request $request) { $requsetData = $request->all(); if($request->hasFile('img')) { $file = $request->file('img'); $path = $this->fileUpload($file,'product'); $requsetData['img'] = $path; } Product::create($requsetData); return redirect('/admin/product'); } public function update(Request $request, $id) { $item = Product::find($id); $requsetData = $request->all(); if($request->hasFile('img')) { $old_image = $item->img; $file = $request->file('img'); $path = $this->fileUpload($file,'product'); $requsetData['img'] = $path; File::delete(public_path().$old_image); } $item->update($requsetData); return redirect('/admin/product'); } public function destroy($id) { $item = Product::find($id); $old_image = $item->img; if(file_exists(public_path().$old_image)){ File::delete(public_path().$old_image); } $item->delete(); return redirect('/admin/product'); } private function fileUpload($file,$dir){ //防呆:資料夾不存在時將會自動建立資料夾,避免錯誤 if( ! is_dir('upload/')){ mkdir('upload/'); } //防呆:資料夾不存在時將會自動建立資料夾,避免錯誤 if ( ! is_dir('upload/'.$dir)) { mkdir('upload/'.$dir); } //取得檔案的副檔名 $extension = $file->getClientOriginalExtension(); //檔案名稱會被重新命名 $filename = strval(time().md5(rand(100, 200))).'.'.$extension; //移動到指定路徑 move_uploaded_file($file, public_path().'/upload/'.$dir.'/'.$filename); //回傳 資料庫儲存用的路徑格式 return '/upload/'.$dir.'/'.$filename; } ``` --- **Summernoe 上傳圖片方法** 1. 使用原本的base64 2. 另外上傳圖片(沒有刪除機制,會占用硬碟空間) ``` 前端 <script> $(document).ready(function() { $('#description').summernote({ height: 150, lang: 'zh-TW', callbacks: { onImageUpload: function(files) { for(let i=0; i < files.length; i++) { $.upload(files[i]); } }, onMediaDelete : function(target) { $.delete(target[0].getAttribute("src")); } }, }); $.upload = function (file) { let out = new FormData(); out.append('file', file, file.name); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_upload_img', contentType: false, cache: false, processData: false, data: out, success: function (img) { $('#description').summernote('insertImage', img); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }; $.delete = function (file_link) { $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_delete_img', data: {file_link:file_link}, success: function (img) { console.log("delete:",img); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); } }); </script> 後端 //Route Web.php Route::post('/ajax_upload_img','AdminController@ajax_upload_img'); Route::post('/ajax_delete_img','AdminController@ajax_delete_img'); //Controller AdminController public function ajax_upload_img() { // A list of permitted file extensions $allowed = array('png', 'jpg', 'gif','zip'); if(isset($_FILES['file']) && $_FILES['file']['error'] == 0){ $extension = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION); if(!in_array(strtolower($extension), $allowed)){ echo '{"status":"error"}'; exit; } $name = strval(time().md5(rand(100, 200))); $ext = explode('.', $_FILES['file']['name']); $filename = $name . '.' . $ext[1]; //防呆:資料夾不存在時將會自動建立資料夾,避免錯誤 if( ! is_dir('upload/')){ mkdir('upload/'); } //防呆:資料夾不存在時將會自動建立資料夾,避免錯誤 if ( ! is_dir('upload/img')) { mkdir('upload/img'); } $destination = public_path().'/upload/img/'. $filename; //change this directory $location = $_FILES["file"]["tmp_name"]; move_uploaded_file($location, $destination); echo "/upload/img/".$filename;//change this URL } exit; } public function ajax_delete_img(Request $request){ if(file_exists(public_path().$request->file_link)){ File::delete(public_path().$request->file_link); } } ``` --- # **Auth in web.php** Auth::routes(); ``` // Laravel 5.7, Laravel 5.8, and Laravel 6.0 // Authentication Routes... Route::get('login', 'Auth\LoginController@showLoginForm')->name('login'); Route::post('login', 'Auth\LoginController@login'); Route::post('logout', 'Auth\LoginController@logout')->name('logout'); // Registration Routes... Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register'); Route::post('register', 'Auth\RegisterController@register'); // Password Reset Routes... Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request'); Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email'); Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset'); Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update'); // Email Verification Routes... Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice'); Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify'); // v6.x /* Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify'); // v5.x */ Route::get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend'); ``` ``` Auth::routes(['register' => false,'reset' => false,'verify' => false]); ``` --- # ORM - Relationships 一對多 (三層) ## Model ``` //產品類別 <?php namespace App; use Illuminate\Database\Eloquent\Model; class ProductType extends Model { protected $table = 'product_type'; protected $fillable = ['type_name']; public function products() { return $this->hasMany('App\Product','type_id')->orderBy('sort', 'desc'); } } //產品 <?php namespace App; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected $table = 'products'; protected $fillable = ['title','description','img','sort','type_id']; public function productType() { return $this->belongsTo('App\ProductType','type_id'); } public function product_imgs() { return $this->hasMany('App\ProductImg','type_id'); } } //產品圖片 <?php namespace App; use Illuminate\Database\Eloquent\Model; class ProductImg extends Model { protected $table = 'product_imgs'; protected $fillable = ['product_id','img']; } ``` ## Controller ``` 產品類別 再加上 產品(多個) $ProductTypes = ProductType::orderBy('sort', 'desc')->with('products')->get(); //view @foreach ($ProductTypes as $ProductType) //$ProductType 為單一類別 @foreach ($ProductType->products as $product) //$product 為單一產品 @endforeach @endforeach ``` --- # 多張圖片上傳 ## KeyWords * laravel file upload multiple (https://stackoverflow.com/questions/39846148/laravel-5-3-multiple-file-uploads) * laravel create get id (https://stackoverflow.com/questions/37075148/laravel-get-the-id-of-usercreate-and-insert-new-row-using-that-id) (https://laravelcode.com/post/laravel-55-get-last-inserted-id-with-example) * js refresh div (https://stackoverflow.com/questions/33801650/how-do-i-refresh-a-div-content) * js input onchange ajax (https://stackoverflow.com/questions/23712799/post-input-onchange-with-ajax) ``` //HTML的部分 //iuput的name後面要有方括弧 //要有屬性 multiple <input type="file" id="imgs" name="imgs[]" multiple required> //Controller $requsetData = $request->all(); //單一檔案 if($request->hasFile('img')) { $file = $request->file('img'); $path = $this->fileUpload($file,'product'); $requsetData['img'] = $path; } $new_product = Product::create($requsetData); $new_product_id = $new_product->id; //多個檔案 $files = $request->file('imgs'); if($request->hasFile('imgs')) { foreach ($files as $file) { //上傳圖片 $path = $this->fileUpload($file,'product'); //新增資料進DB $product_img = new ProductImg; $product_img->product_id = $new_product_id; $product_img->img = $path; $product_img->save(); } } ``` ## 完整的Controller ``` <?php namespace App\Http\Controllers; use App\Product; use App\ProductImg; use App\ProductType; use Illuminate\Http\Request; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; class ProductController extends Controller { public function index() { $items = Product::with('productType')->get(); return view('admin.product.index',compact("items")); } public function create() { $productTypes = ProductType::all(); return view('admin.product.create',compact('productTypes')); } public function store(Request $request) { $requsetData = $request->all(); //單一檔案 if($request->hasFile('img')) { $file = $request->file('img'); $path = $this->fileUpload($file,'product'); $requsetData['img'] = $path; } $new_product = Product::create($requsetData); $new_product_id = $new_product->id; //多個檔案 if($request->hasFile('imgs')) { $files = $request->file('imgs'); foreach ($files as $file) { //上傳圖片 $path = $this->fileUpload($file,'product_imgs'); //新增資料進DB $product_img = new ProductImg; $product_img->product_id = $new_product_id; $product_img->img = $path; $product_img->save(); } } return redirect('/admin/product'); } public function edit($id) { $productTypes = ProductType::all(); $item = Product::where('id',$id)->with('product_imgs')->first(); return view('admin.product.edit',compact('item','productTypes')); } public function update(Request $request, $id) { $item = Product::find($id); $requsetData = $request->all(); if($request->hasFile('img')) { //如果使用者有重新上傳圖片 $old_image = $item->img; //抓取舊圖片路徑 File::delete(public_path().$old_image); //把舊圖片刪除 //上傳圖片 $file = $request->file('img'); $path = $this->fileUpload($file,'product'); $requsetData['img'] = $path; } //多個檔案 if($request->hasFile('imgs')) { $files = $request->file('imgs'); foreach ($files as $file) { //上傳圖片 $path = $this->fileUpload($file,'product_imgs'); //新增資料進DB $product_img = new ProductImg; $product_img->product_id = $id; $product_img->img = $path; $product_img->save(); } } $item->update($requsetData); return redirect('/admin/product'); } public function destroy($id) { $item = Product::find($id); //單一圖片的刪除 $old_image = $item->img; if(file_exists(public_path().$old_image)){ File::delete(public_path().$old_image); } $item->delete(); //多張圖片的刪除 $product_imgs = ProductImg::where('product_id',$id)->get(); foreach($product_imgs as $product_img){ $old_product_img = $product_img->img; if(file_exists(public_path().$old_product_img)){ File::delete(public_path().$old_product_img); } $product_img->delete(); } return redirect('/admin/product'); } private function fileUpload($file,$dir){ //防呆:資料夾不存在時將會自動建立資料夾,避免錯誤 if( ! is_dir('upload/')){ mkdir('upload/'); } //防呆:資料夾不存在時將會自動建立資料夾,避免錯誤 if ( ! is_dir('upload/'.$dir)) { mkdir('upload/'.$dir); } //取得檔案的副檔名 $extension = $file->getClientOriginalExtension(); //檔案名稱會被重新命名 $filename = strval(time().md5(rand(100, 200))).'.'.$extension; //移動到指定路徑 move_uploaded_file($file, public_path().'/upload/'.$dir.'/'.$filename); //回傳 資料庫儲存用的路徑格式 return '/upload/'.$dir.'/'.$filename; } } ``` ## Edit Page ``` @extends('layouts.app') @section('css') <style> .product_imgs{ position: relative; } .product_imgs .btn-danger{ border-radius: 50%; position: absolute; right: 20px; top: 5px; } .product_imgs .sort{ display: flex; margin-top: 5px; } .product_imgs label{ margin: 0 5px; line-height: 37px; } .product_imgs input{ width: 100%; } </style> @endsection @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-12"> <div class="card"> <div class="card-header">產品管理 - Edit</div> <div class="card-body"> <form method="post" action="/admin/product/update/{{$item->id}}" enctype="multipart/form-data"> @csrf <div class="form-group row"> <label for="type_id" class="col-sm-2 col-form-label">產品類別</label> <div class="col-sm-10"> <select class="form-control" name="type_id" id="type_id"> @foreach ($productTypes as $productType) <option value="{{ $productType->id }}" @if($item->type_id === $productType->id) selected @endif>{{ $productType->type_name }}</option> @endforeach </select> </div> </div> <hr> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">現有產品圖片</label> <div class="col-sm-10"> <img class="img-fluid" src="{{$item->img}}" alt=""> </div> </div> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">重新上傳產品圖片 <br><small class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label> <div class="col-sm-10"> <input type="file" class="form-control" id="img" value="" name="img"> </div> </div> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">現有產品組圖片</label> @foreach ($item->product_imgs as $product_img) <div class="col-sm-2 product_imgs" data-productimgid="{{$product_img->id}}"> <img class="img-fluid" src="{{$product_img->img}}" alt=""> <button class="btn btn-danger btn-sm" data-productimgid="{{$product_img->id}}" type="button">X</button> <div class="sort"> <label for="imgs">Sort</label> <input class="form-control" type="text"> </div> </div> @endforeach </div> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">重新上傳產品組圖片 <br><small class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label> <div class="col-sm-10"> <input type="file" class="form-control" id="imgs" name="imgs[]" multiple> </div> </div> <hr> <div class="form-group row"> <label for="title" class="col-sm-2 col-form-label">標題</label> <div class="col-sm-10"> <input type="text" class="form-control" id="title" value="{{$item->title}}" name="title" required> </div> </div> <div class="form-group row"> <label for="description" class="col-sm-2 col-form-label">敘述</label> <div class="col-sm-10"> <textarea class="form-control" name="description" id="description" rows="5">{{$item->description}}</textarea> </div> </div> <div class="form-group row"> <label for="sort" class="col-sm-2 col-form-label">排序(sort)</label> <div class="col-sm-10"> <input type="number" class="form-control" id="sort" value="{{$item->sort}}" name="sort" value="1" required> </div> </div> <hr> <div class="form-group row"> <div class="col-sm-12 text-center"> <button type="submit" class="btn btn-primary">SEND</button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection @section('js') <script> $(document).ready(function() { $('#description').summernote({ height: 150, lang: 'zh-TW', callbacks: { onImageUpload: function(files) { for(let i=0; i < files.length; i++) { $.upload(files[i]); } }, onMediaDelete : function(target) { $.delete(target[0].getAttribute("src")); } }, }); $.upload = function (file) { let out = new FormData(); out.append('file', file, file.name); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_upload_img', contentType: false, cache: false, processData: false, data: out, success: function (img) { $('#description').summernote('insertImage', img); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }; $.delete = function (file_link) { $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_delete_img', data: {file_link:file_link}, success: function (img) { console.log("delete:",img); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); } $('.product_imgs .btn-danger').click(function () { var product_imgs_id = $(this).data('productimgid'); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_delete_product_imgs', data: {product_imgs_id: product_imgs_id}, success: function (res) { $( `.product_imgs[data-productimgid='${product_imgs_id}']` ).remove(); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }); }); </script> @endsection ``` ## web ajax Route and controller ``` //Web.php Route::post('/ajax_delete_product_imgs','AdminController@ajax_delete_product_imgs'); //AdminController public function ajax_delete_product_imgs(Request $request) { $product_imgs_id = $request->product_imgs_id; //多張圖片組的單一圖片刪除 $product_img = ProductImg::where('id',$product_imgs_id)->first(); $old_product_img = $product_img->img; if(file_exists(public_path().$old_product_img)){ File::delete(public_path().$old_product_img); } $product_img->delete(); echo '{"status":"success","message":"delete file success"}'; } ``` --- # 各個類別限定三個產品的作法 ``` $ProductTypes = ProductType::orderBy('sort', 'desc')->with('products')->get(); foreach( $ProductTypes as $type){ $type->products = $type->products->take(3); } ``` --- # 多張圖片排序 - 使用Muuri(未完成) or 手動輸入排序數字 Edit-Page ``` @extends('layouts.app') @section('css') <style> .product_imgs { position: relative; } .product_imgs .btn-danger { border-radius: 50%; position: absolute; right: 5px; top: 5px; } .product_imgs .sort { display: flex; margin-top: 5px; } .product_imgs label { margin: 0 5px; line-height: 37px; } .product_imgs input { width: 100%; } .grid { position: relative; min-height: 120px; } .item { display: block; position: absolute; width: 100px; height: 100px; margin: 5px; z-index: 1; } .item.muuri-item-dragging { z-index: 3; } .item.muuri-item-releasing { z-index: 2; } .item.muuri-item-hidden { z-index: 0; } .item-content { position: relative; width: 100%; height: 100%; } </style> @endsection @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-12"> <div class="card"> <div class="card-header">產品管理 - Edit</div> <div class="card-body"> <form method="post" action="/admin/product/update/{{$item->id}}" enctype="multipart/form-data"> @csrf <input type="text" id="new_sort" name="new_sort" value="" hidden> <div class="form-group row"> <label for="type_id" class="col-sm-2 col-form-label">產品類別</label> <div class="col-sm-10"> <select class="form-control" name="type_id" id="type_id"> @foreach ($productTypes as $productType) <option value="{{ $productType->id }}" @if($item->type_id === $productType->id) selected @endif>{{ $productType->type_name }}</option> @endforeach </select> </div> </div> <hr> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">現有產品圖片</label> <div class="col-sm-10"> <img class="img-fluid" src="{{$item->img}}" alt=""> </div> </div> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">重新上傳產品圖片 <br><small class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label> <div class="col-sm-10"> <input type="file" class="form-control" id="img" value="" name="img"> </div> </div> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">現有產品組圖片</label> <div class="col-sm-10"> <div class="grid"> @foreach ($item->product_imgs as $product_img) <div class="item" data-productimgid="{{$product_img->id}}"> <div class="item-content"> <div class="product_imgs" data-productimgid="{{$product_img->id}}"> <img class="img-fluid" src="{{$product_img->img}}" alt=""> <button class="btn btn-danger btn-sm" data-productimgid="{{$product_img->id}}" type="button">X</button> <div class="sort"> <label for="imgs">Sort</label> <input class="form-control" onchange="post_ajax_sort(this,{{$product_img->id}})" type="text" value="{{$product_img->sort}}"> </div> </div> </div> </div> @endforeach </div> </div> </div> <div class="form-group row"> <label for="img" class="col-sm-2 col-form-label">重新上傳產品組圖片 <br><small class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label> <div class="col-sm-10"> <input type="file" class="form-control" id="imgs" name="imgs[]" multiple> </div> </div> <hr> <div class="form-group row"> <label for="title" class="col-sm-2 col-form-label">標題</label> <div class="col-sm-10"> <input type="text" class="form-control" id="title" value="{{$item->title}}" name="title" required> </div> </div> <div class="form-group row"> <label for="description" class="col-sm-2 col-form-label">敘述</label> <div class="col-sm-10"> <textarea class="form-control" name="description" id="description" rows="5">{{$item->description}}</textarea> </div> </div> <div class="form-group row"> <label for="sort" class="col-sm-2 col-form-label">排序(sort)</label> <div class="col-sm-10"> <input type="number" class="form-control" id="sort" value="{{$item->sort}}" name="sort" value="1" required> </div> </div> <hr> <div class="form-group row"> <div class="col-sm-12 text-center"> <button type="submit" class="btn btn-primary">SEND</button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection @section('js') <script src="https://unpkg.com/web-animations-js@2.3.2/web-animations.min.js"></script> <script src="https://unpkg.com/muuri@0.8.0/dist/muuri.min.js"></script> <script> $(document).ready(function() { var grid = new Muuri('.grid',{ dragEnabled: true, }).on('move', function () { getOrder(grid); }); function getOrder(grid) { var currentItems = grid.getItems(); var currentItemIds = currentItems.map(function (item) { return item.getElement().getAttribute('data-productimgid') }); $('#new_sort').val(currentItemIds.join()); } $('#description').summernote({ height: 150, lang: 'zh-TW', callbacks: { onImageUpload: function(files) { for(let i=0; i < files.length; i++) { $.upload(files[i]); } }, onMediaDelete : function(target) { $.delete(target[0].getAttribute("src")); } }, }); $.upload = function (file) { let out = new FormData(); out.append('file', file, file.name); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_upload_img', contentType: false, cache: false, processData: false, data: out, success: function (img) { $('#description').summernote('insertImage', img); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }; $.delete = function (file_link) { $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_delete_img', data: {file_link:file_link}, success: function (img) { console.log("delete:",img); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); } $('.product_imgs .btn-danger').click(function () { var product_imgs_id = $(this).data('productimgid'); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_delete_product_imgs', data: {product_imgs_id: product_imgs_id}, success: function (res) { $( `.product_imgs[data-productimgid='${product_imgs_id}']` ).remove(); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }); }); function post_ajax_sort(element,product_imgs_id) { var product_imgs_id; var sort_value = element.value; $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/admin/ajax_sort_product_imgs', data: {product_imgs_id: product_imgs_id,sort_value: sort_value}, success: function (res) { // $( `.product_imgs[data-productimgid='${product_imgs_id}']` ).remove(); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); } </script> @endsection ``` 後端送JS Route ``` Route::post('/ajax_sort_product_imgs','AdminController@ajax_sort_product_imgs'); ``` Controller ``` public function ajax_sort_product_imgs(Request $request) { $product_imgs_id = $request->product_imgs_id; $sort_value = $request->sort_value; $ProductImg = ProductImg::find($product_imgs_id); $ProductImg->sort = $sort_value; $ProductImg->save(); echo '{"status":"success","message":"sort img success"}'; } ``` --- # 自動產生Model 由建立好的DB後,可自行產生其Model https://github.com/krlove/eloquent-model-generator 範例: ``` php artisan krlove:generate:model OrderStatus --table-name=order_status php artisan krlove:generate:model DonateCashData --table-name=donate_cash_data ``` > 要記得在Migration或DB設定好關聯,即可在Model中自動附帶上relationship --- # 購物車 在選擇使用Laravel shopping cart購物車套件的時候,要注意支援的Laravel版本 這次選擇的是 darryldecode/laravelshoppingcart (https://github.com/darryldecode/laravelshoppingcart) 依照不同的版本,支援Laravel 5.1~ ,Laravel 5.5, 5.6 or 5.7~,6 For Laravel 5.1~ ``` composer require "darryldecode/cart:~2.0" ``` For Laravel 5.5, 5.6 or 5.7~,6 ``` composer require "darryldecode/cart:~4.0" or composer require "darryldecode/cart" ``` --- 購物車有分是否要指定User or 不分User的版本 下面為不分User的版本範例 可以看到`\Cart::`之後直接接著methods ``` <?php namespace App\Http\Controllers; use Darryldecode\Cart\Cart; use Illuminate\Http\Request; class CartController extends Controller { public function addProductToCar(){ \Cart::add(455, 'Sample Item', 100.99, 2, array()); } public function getContent() { $content = \Cart::getContent(); } public function TotalCart() { $total = \Cart::getTotal(); } } ``` --- ## 加入購物車 下面為指定User,並新增加入購物車的範例程式 在View部分主要是以Ajax方式,將產品編號(productID)傳至後端 由後端Controller進行加入購物車的動作,並統計所有購物車內的產品數量 將統計後的數字傳至前端,更新Navbar上的購物車數字 以下為製作時須注意事項 * 於前端要送出ajax要注意crsf token,要於ajax補上相關設定 * 加入購物車需要填入相關參數,除屬性(attributess)之外,其餘皆不能為空值 * 使用購物車時要確定有沒有要指定使用者,如果有要加上`\Cart::session($userID)` * 上述的`userID`為使用者的ID,要再透過以下方式取得`$userId = auth()->user()->id;` 完整範例程式碼如下 ``` $userId = auth()->user()->id; // or any string represents user identifier \Cart::session($userId)->add('456', 'Product Name', '200', 1, array()); ``` ### WEB ``` // testcart Route::post('/addcart', 'cartcontroller@addcart'); Route::get('/getcontent', 'cartcontroller@getcontent'); Route::get('/totalcart', 'cartcontroller@totalcart'); ``` ### Controller ``` public function addcart(Request $request) { $product_id = $request->product_id; $product = Product::find($product_id); $userId = auth()->user()->id; // or any string represents user identifier \Cart::session($userId)->add($product_id, $product->title, $product->price, 1, array()); $cartTotalQuantity = \Cart::session($userId)->getTotalQuantity(); return $cartTotalQuantity; } ``` ### Product Html按鈕 加入購物車按鈕 ``` <button class="btn btn-danger addcart" data-productid="{{$product->id}}">加入購物車</button> ``` JS按鍵事件綁定 + Ajax發送 ``` $('.addcart').click(function () { var product_id = $(this).data('productid'); console.log(product_id); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/addcart', data: {product_id:product_id}, success: function (res) { $('#cartTotalQuantity').text(res); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }); ``` ### front layout 在Navbar上要顯示出購物車的數量 ``` <i class="icon-shopping-cart"> <span id="cartTotalQuantity"> {{-- {{ \Cart::getTotalQuantity() }} 沒指定人的寫法 --}} {{-- 指定對象的PHP原生寫法 --}} @guest 0 @else <?php $userId = auth()->user()->id; $cartTotalQuantity = \Cart::session($userId)->getTotalQuantity(); echo $cartTotalQuantity; ?> @endguest </span> </i> ``` --- ## 結帳頁 + 修改購物車上的產品數量 ### Web ``` Route::get('/cart', 'CartController@cart'); //結帳頁 Route::post('/changeProductQty','CartController@changeProductQty'); //修改產品數量ajax ``` ### Controller ``` public function cart() { $content = \Cart::getContent()->sort(); //取得購物車產品後排序 $total = \Cart::getTotal(); return view("front.cart",compact('content','total')); } public function changeProductQty(Request $request) { $product_id = $request->product_id; $new_qty = $request->new_qty; \Cart::update($product_id , array( 'quantity' => array( 'relative' => false, 'value' => $new_qty ), )); return "suceess"; } ``` ### View ``` @extends('layouts.front_layout') @section('css') @endsection @section('content') <div class="container"> <h1>Cart結帳頁</h1> <div> <table class="table table-dark"> <thead> <tr> <th scope="col">#</th> <th scope="col">ProductName</th> <th scope="col">Price</th> <th scope="col">Qty</th> <th scope="col">SubTotal</th> <th width="50">Delete</th> </tr> </thead> <tbody> {{$content}} @foreach ($content as $item) <tr> <th scope="row">1</th> <td>{{$item->name}}</td> <td>{{$item->price}}</td> <td><input type="text" value="{{$item->quantity}}" class="product_qty" data-productid="{{$item->id}}"></td> <td>{{$item->price * $item->quantity}}</td> <td><button class="btn btn-danger btn-sm">X</button></td> </tr> @endforeach </tbody> </table> </div> <div> <h2>總計:$ {{$total}}</h2> </div> <button class="text-center btn btn-success">確定結帳</button> </div> @endsection @section('js') <script> $('.product_qty').on('change', function() { // console.log("onchangeValue:",this.value); // console.log("onchangeProductID:",this.getAttribute("data-productid")); var new_qty = this.value; var product_id = this.getAttribute("data-productid"); $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); $.ajax({ method: 'POST', url: '/changeProductQty', data: { product_id:product_id, new_qty:new_qty }, success: function (res) { document.location.reload(true); }, error: function (jqXHR, textStatus, errorThrown) { console.error(textStatus + " " + errorThrown); } }); }); </script> @endsection ``` ## Order - 訂單管理 ### Migration 資料表`Order` 的Migration 主要紀錄訂單的編號、寄件人相關資訊、付費方式及寄送時間 ``` <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateOrderTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('order', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('order_no'); $table->string('receive_name'); $table->string('receive_phone'); $table->string('receive_mobile'); $table->string('receive_address'); $table->string('receive_email'); $table->string('receipt'); $table->string('time_to_send'); $table->string('status')->default('新訂單'); $table->integer('total_price'); $table->string('remark','2000'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('order'); } } ``` 資料表`OrderItems` 的Migration 主要紀錄訂單中的購買品項(product_id)及其數量(Qty) 與Order關聯,為一對多關係 一個訂單中會有多個產品,每個產品會買的數量都可能會不同 ``` <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateOrderItemsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('order_items', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('order_id'); $table->foreign('order_id')->references('id')->on('order'); $table->unsignedBigInteger('product_id'); $table->foreign('product_id')->references('id')->on('products'); $table->integer('qty'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('order_items'); } } ``` ### Controller 負責將訂單資料及購買的產品細項存進資料庫,確定訂單建立成功後,才會跑第三方支付 -- 先將request->all()取出,放至變數$request_data中 再添加order_no的部分進陣列中,最後再以ORM - Create的方式建立訂單 ``` public function send_check_out(Request $request) { $request_data = $request->all(); $request_data["order_no"] = "201912061"; $request_data["total_price"] = \Cart::getTotal(); $new_order = Order::create($request_data); $cat_contents = \Cart::getContent()->sort(); foreach ($cat_contents as $item) { $OrderItem = new OrderItems(); $OrderItem->order_id = $new_order->id; $OrderItem->product_id = $item->id; $OrderItem->qty = $item->quantity; $OrderItem->price = $item->price; $OrderItem->save(); } } ``` --- # 訂單建立+金流串接 * 綠界金流API串接文件(https://www.ecpay.com.tw/Content/files/ecpay_011.pdf) * Laravel 串接綠界非官方套件 (https://github.com/tsaiyihua/laravel-ecpay) ## Laravel 串接綠界套件安裝 Composer安裝套件 ``` composer require tsaiyihua/laravel-ecpay ``` Config設定 ``` php artisan vendor:publish --tag=ecpay ``` .env設定 ``` ECPAY_MERCHANT_ID=2000132 ECPAY_HASH_KEY=5294y06JbISpM5x9 ECPAY_HASH_IV=v77hoKGq4kWxNNIS ``` --- **web.php** ``` Route::prefix('cart_ecpay')->group(function(){ //當消費者付款完成後,綠界會將付款結果參數以幕後(Server POST)回傳到該網址。 Route::post('notify', 'CartController@notifyUrl')->name('notify'); //付款完成後,綠界會將付款結果參數以幕前(Client POST)回傳到該網址 Route::post('return', 'CartController@returnUrl')->name('return'); }); ``` **middleware > VerifyCsrfToken.php** ``` <?php namespace App\Http\Middleware; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware; class VerifyCsrfToken extends Middleware { /** * Indicates whether the XSRF-TOKEN cookie should be set on the response. * * @var bool */ protected $addHttpCookie = true; /** * The URIs that should be excluded from CSRF verification. * * @var array */ protected $except = [ 'cart_ecpay/return','cart_ecpay/notify' ]; } ``` **controller** ``` <?php namespace App\Http\Controllers; use App\Order; use App\Product; use Carbon\Carbon; use App\OrderItems; use Darryldecode\Cart\Cart; use Illuminate\Http\Request; use TsaiYiHua\ECPay\Checkout; use TsaiYiHua\ECPay\Services\StringService; use TsaiYiHua\ECPay\Collections\CheckoutResponseCollection; class CartController extends Controller { public function __construct(Checkout $checkout,CheckoutResponseCollection $checkoutResponse) { $this->checkout = $checkout; $this->checkoutResponse = $checkoutResponse; } public function addProductToCar(Request $request){ $product_id = $request->product_id; $product = Product::find($product_id); \Cart::add($product_id, $product->title, $product->price, 1, array()); $cartTotalQuantity = \Cart::getTotalQuantity(); return $cartTotalQuantity; } public function cart() { $content = \Cart::getContent()->sort(); $total = \Cart::getTotal(); return view("front.cart",compact('content','total')); } public function changeProductQty(Request $request) { $product_id = $request->product_id; $new_qty = $request->new_qty; \Cart::update($product_id , array( 'quantity' => array( 'relative' => false, 'value' => $new_qty ), )); return "suceess"; } public function deleteProductInCart(Request $request) { $product_id = $request->product_id; \Cart::remove($product_id); return "suceess"; } public function cart_check_out() { $content = \Cart::getContent()->sort(); $total = \Cart::getTotal(); return view("front.cart_check_out",compact('content','total')); } public function send_check_out(Request $request) { //建立訂單 $request_data = $request->all(); $request_data["order_no"] = Carbon::now()->format('Ymd'); $request_data["total_price"] = \Cart::getTotal(); $new_order = Order::create($request_data); $new_order->order_no = 'hk'.Carbon::now()->format('Ymd').$new_order->id; $new_order->save(); $cat_contents = \Cart::getContent()->sort(); $items=[]; foreach ($cat_contents as $item) { $OrderItem = new OrderItems(); $OrderItem->order_id = $new_order->id; $OrderItem->product_id = $item->id; $OrderItem->qty = $item->quantity; $OrderItem->price = $item->price; $OrderItem->save(); $product = Product::find($item->id); $product_name = $product->title; $new_ary = [ 'name' => $product_name, 'qty' => $item->quantity, 'price' => $item->price, 'unit' => '個' ]; array_push($items, $new_ary); } //第三方支付 $formData = [ 'UserId' => 1, // 用戶ID , Optional 'ItemDescription' => '產品簡介', 'Items' => $items, 'OrderId' => 'hk'.Carbon::now()->format('Ymd').$new_order->id, // 'ItemName' => 'Product Name', // 'TotalAmount' => \Cart::getTotal(), 'PaymentMethod' => 'Credit', // ALL, Credit, ATM, WebATM ]; //清空購物車 \Cart::clear(); return $this->checkout->setNotifyUrl(route('notify'))->setReturnUrl(route('return'))->setPostData($formData)->send(); } public function notifyUrl(Request $request){ $serverPost = $request->post(); $checkMacValue = $request->post('CheckMacValue'); unset($serverPost['CheckMacValue']); $checkCode = StringService::checkMacValueGenerator($serverPost); if ($checkMacValue == $checkCode) { return '1|OK'; } else { return '0|FAIL'; } } public function returnUrl(Request $request){ $serverPost = $request->post(); $checkMacValue = $request->post('CheckMacValue'); unset($serverPost['CheckMacValue']); $checkCode = StringService::checkMacValueGenerator($serverPost); if ($checkMacValue == $checkCode) { if (!empty($request->input('redirect'))) { return redirect($request->input('redirect')); } else { //付款完成,下面接下來要將購物車訂單狀態改為已付款 //目前是顯示所有資料將其DD出來 dd($this->checkoutResponse->collectResponse($serverPost)); } } } } ``` --- # 權限管理 Step1.新增role欄位 於User Table新增一個新的欄位`role`之後用於管理身分 分為`user`、`admin`、`super_admin`三個身分 可於CreateUser的migration中直接新增以下程式,新增欄位role並將預設值為user ``` $table->string('role')->default('user'); ``` Step2.新增Middleware Step2.1 新增IsAdmin與IsSuperAdmin 兩個MiddleWare ``` php artisan make:middleware IsAdmin php artisan make:middleware IsSuperAdmin ``` Step2.2 設定app/Http/Kernel.php 在`$routeMiddleware`陣列中,新增 ``` 'admin' => \App\Http\Middleware\IsAdmin::class, 'super_admin' => \App\Http\Middleware\IsSuperAdmin::class, ``` 修改後的陣列如下 ``` protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'admin' => \App\Http\Middleware\IsAdmin::class, 'super_admin' => \App\Http\Middleware\IsSuperAdmin::class, ]; ``` Step2.3 修改IsAdmin與IsSuperAdmin MiddleWare檔案 IsAdmin MiddleWare ``` public function handle($request, Closure $next) { if (Auth::user()->role == "admin" || Auth::user()->role == "super_admin") { return $next($request); } return abort(403, 'Unauthorized action.'); } ``` IsSuperAdmin MiddleWare ``` public function handle($request, Closure $next) { if (Auth::user()->role == "super_admin") { return $next($request); } return abort(403, 'Unauthorized action.'); } ``` Step2.3 調整web中 在admin群組中的middlewire 原本的,只要有登入就可以進入到以下頁面 ``` Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function(){ ... }); ``` -- 要修改為 除了登入之外,還是要role為admin或者是super_admin身分才可以 ``` //admin Route::group(['prefix' => 'admin', 'middleware' => ['auth','admin']], function(){ ... }); //super_admin Route::group(['prefix' => 'admin', 'middleware' => ['auth','super_admin']], function(){ ... }); ``` Step3. 新增有權限的帳號 主要是要在後台新增一個帳號管理之頁面 於帳號管理的頁面中顯示所有目前的帳號、刪除帳號、新增帳號 新增帳號要可以選擇權限為`admin`、`super_admin` Route ``` Route::group(['prefix' => 'admin', 'middleware' => ['auth','super_admin']], function(){ //帳號管理 Route::get('account', 'AccountController@index'); Route::get('account/create', 'AccountController@create'); Route::post('account/store', 'AccountController@store'); Route::post('account/destroy/{id}', 'AccountController@destroy'); ... }); ``` Controller ``` <?php namespace App\Http\Controllers; use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Hash; class AccountController extends Controller { public function index() { $items = User::all(); return view('admin.account.index',compact('items')); } public function create() { return view('admin.account.create'); } public function store(Request $request) { $this->validator($request->all())->validate(); $this->create_account($request->all()); return redirect('/admin/account'); } public function destroy(Request $request,$id) { } protected function validator(array $data) { return Validator::make($data, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'role' => ['required', 'string'], ]); } /** * Create a new user instance after a valid registration. * * @param array $data * @return \App\User */ protected function create_account(array $data) { return User::create([ 'name' => $data['name'], 'email' => $data['email'], 'role' => $data['role'], 'password' => Hash::make($data['password']), ]); } } ``` View - Index ``` @extends('layouts.app') @section('css') @endsection @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-12"> <div class="card"> <div class="card-header">帳號管理 - Index</div> <div class="card-body"> <a class="btn btn-success" href="/admin/account/create">新增帳號</a> <hr> <table id="example" class="table table-striped table-bordered" style="width:100%"> <thead> <tr> <th>使用者名稱(name)</th> <th>Email</th> <th>權限(role)</th> <th width="120">功能</th> </tr> </thead> <tbody> @foreach ($items as $item) <tr> <td>{{ $item->name}}</td> <td>{{ $item->email}}</td> <td>{{ $item->role}}</td> <td> <a class="btn btn-danger btn-sm" href="#" data-itemid="{{$item->id}}">刪除</a> <form class="destroy-form" data-itemid="{{$item->id}}" action="/admin/news/destroy/{{$item->id}}" method="POST" style="display: none;"> @csrf </form> </td> </tr> @endforeach </tbody> </table> </div> </div> </div> </div> </div> @endsection @section('js') <script> $(document).ready(function() { $('#example').DataTable({ "order": [1,"desc"] }); $('#example').on('click','.btn-danger',function(){ event.preventDefault(); var r = confirm("你確定要刪除此項目嗎?"); if (r == true) { var itemid = $(this).data("itemid"); $(`.destroy-form[data-itemid="${itemid}"]`).submit(); } }); }); </script> @endsection ``` View - Create ``` @extends('layouts.app') @section('content') <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">Create Account</div> <div class="card-body"> <form method="POST" action="/admin/account/store"> @csrf <div class="form-group row"> <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label> <div class="col-md-6"> <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus> @error('name') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label> <div class="col-md-6"> <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email"> @error('email') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="role" class="col-md-4 col-form-label text-md-right">Role</label> <div class="col-md-6"> <select class="form-control" name="role" id="role"> <option value="admin">Admin</option> <option value="super_admin">SuperAdmin</option> </select> </div> </div> <div class="form-group row"> <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label> <div class="col-md-6"> <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password"> @error('password') <span class="invalid-feedback" role="alert"> <strong>{{ $message }}</strong> </span> @enderror </div> </div> <div class="form-group row"> <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label> <div class="col-md-6"> <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password"> </div> </div> <div class="form-group row mb-0"> <div class="col-md-6 offset-md-4"> <button type="submit" class="btn btn-primary"> {{ __('Register') }} </button> </div> </div> </form> </div> </div> </div> </div> </div> @endsection ``` # 表單機器人驗證 biscolab/laravel-recaptcha (https://github.com/biscolab/laravel-recaptcha) (https://laravel-recaptcha-docs.biscolab.com/docs/intro) --- Installation ``` $ composer require biscolab/laravel-recaptcha ``` Configuration ``` $ php artisan vendor:publish --provider="Biscolab\ReCaptcha\ReCaptchaServiceProvider" ``` Add API Keys to .env file ``` # in your .env file RECAPTCHA_SITE_KEY=YOUR_API_SITE_KEY RECAPTCHA_SECRET_KEY=YOUR_API_SECRET_KEY ``` Complete configuration Open config/recaptcha.php configuration file and set version: ``` return [ 'api_site_key' => env('RECAPTCHA_SITE_KEY', ''), 'api_secret_key' => env('RECAPTCHA_SECRET_KEY', ''), // changed in v4.0.0 'version' => 'v2', // supported: "v3"|"v2"|"invisible" // @since v3.4.3 changed in v4.0.0 'curl_timeout' => 10, 'skip_ip' => [], // array of IP addresses - String: dotted quad format e.g.: "127.0.0.1" // @since v3.2.0 changed in v4.0.0 'default_validation_route' => 'biscolab-recaptcha/validate', // @since v3.2.0 changed in v4.0.0 'default_token_parameter_name' => 'token', // @since v3.6.0 changed in v4.0.0 'default_language' => null, // @since v4.0.0 'default_form_id' => 'biscolab-recaptcha-invisible-form', // Only for "invisible" reCAPTCHA // @since v4.0.0 'explicit' => false, // true|false // @since v4.0.0 'tag_attributes' => [ 'theme' => 'light', // "light"|"dark" 'size' => 'normal', // "normal"|"compact" 'tabindex' => 0, 'callback' => null, // DO NOT SET "biscolabOnloadCallback" 'expired-callback' => null, // DO NOT SET "biscolabOnloadCallback" 'error-callback' => null, // DO NOT SET "biscolabOnloadCallback" ] ]; ``` # mail發送 ### 安裝 ``` composer require guzzlehttp/guzzle ``` .envfile ``` MAIL_DRIVER=smtp MAIL_HOST=smtp.gmail.com MAIL_PORT=465 MAIL_USERNAME=teacherTest0929@gmail.com MAIL_PASSWORD=QWEasdzxc0929 MAIL_ENCRYPTION=SSL ``` MAIL_DRIVER是使用的服務 預設smtp google stmp服務的細項 ->Gmail SMTP 伺服器需求:https://support.google.com/a/answer/176600?hl=zh-Hant MAIL_USERNAME:要發送郵件的帳號 MAIL_PASSWORD:郵件的密碼 記得開安全性權限:https://blog.user.today/gmail-smtp-authentication-required/ ``` php artisan make:mail OrderShipped --markdown=emails.orders.shipped ``` .config/mail.php ``` 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'teacherTest0929@gmail.com'), 'name' => env('MAIL_FROM_NAME', '測試用'), ], ``` !!!!記得import class 在要送mail的地方下 ``` public function store(Request $request) { Mail::to($content->email)->send(new OrderShipped($content)); //to(放收件人email) } ``` ### 更改信件樣式 改信件的header footer Ctrl+p +p 搜尋 footer.blade.php header.blade.php 就可以修改標題及頁尾 將表單傳送過來的資料塞進信箱中 Mail.OrderShipped.php ``` public function __construct(ContentUs $content) { //ContentUs 是model名稱 記得import $this->content = $content; //這邊變數要與下面的with變數來源相同 } /** * Build the message. * * @return $this */ public function build() { return $this->subject('感謝你的來信')->markdown('emails.orders.shipped')->with('content',$this->content); //$message->subject($subject); (定義信件標題) //$message->attach($pathToFile, array $options = []); (寄送附件) //$message->with(變數名稱, 變數來源) (上方__construct預先定義的資料庫內容,引入何種資料庫在constuct定義,並無限制) } ``` # 購物車前置作業->訂單產生 (要有產品頁) 流程:進入產品頁->按下購買->購買產品頁(選擇數量以及規格) ->加進購物車後->到結帳頁面填寫收件人資料->在建立訂單。 ### 建立說明 此流程只教學建立訂單的頁面。 建立兩張資料表 一張order(主要訂單) 一張order_detial(訂單明細) ``` php artisan make:migration create_orders php artisan make:migration create_orders_detial ``` order資料表: ``` public function up() { Schema::create('orders', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('user_id')->nullable(); $table->string('Recipient_name'); $table->string('Recipient_phone'); $table->string('Recipient_address'); $table->string('shipment_time')->default('不指定'); $table->string('totalPrice'); $table->string('ship_status')->default('未結帳'); $table->string('Purchase_status')->default('未送達'); $table->timestamps(); }); } ``` user_id:當有登入時 存入userID Recipient_name:收件人名子 Recipient_phone:收件人電話 Recipient_address:收件人地址 shipment_time:送達時間 totalPrice:購物車產品總金額 ship_status:預設未結帳 Purchase_status:預設未送達 order_detial ``` public function up() { Schema::create('orders_detail', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('order_id'); $table->string('product_id'); $table->string('qty'); $table->string('price'); $table->timestamps(); }); } ``` order_id:訂單的ID product_id:購買產品的ID (一對多)一張訂單可能有好幾個產品 qty:產品數量 price:產品價格 示意圖 ![](https://i.imgur.com/894l6Jh.png) !!記得設定好按鈕的action 以及對應的web.php 裡的EX: route:get/post('/cart_checkout',Frontcontroller@cart_check) Frontcontroller裡面的 ### 建立主要與次要訂單流程 ``` public function post_cart_check(Request $request) { // $sessionKey = Auth::id(); // $items = \Cart::session($sessionKey)->getContent(); // dd($items); $Recipient_name = $request->Recipient_name; $Recipient_phone = $request->Recipient_phone; $Recipient_address = $request->Recipient_address; $shipment_time = $request->shipment_time; $order = new Order(); $sessionKey = Auth::id(); //主要訂單建立 $order->user_id= $sessionKey; $order->Recipient_name = $Recipient_name; $order->Recipient_phone= $Recipient_phone; $order->Recipient_address = $Recipient_address; $order->shipment_time = $shipment_time; $order->totalPrice = \Cart::session($sessionKey)->getTotal(); $order->save(); //訂單詳細建立 $items = \Cart::session($sessionKey)->getContent(); foreach($items as $row) { $order_detial= new Order_detail(); $order_detial->order_id = $order->id; $order_detial->product_id= $row->id; $order_detial->qty = $row->quantity; $order_detial->price =$row->price; $order_detial->save(); } } ``` ```$Recipient_name = $request->Recipient_name;``` //將from表單傳送過來的收件人姓名用變數Recipient_name存起來 ```$Recipient_phone = $request->Recipient_phone;``` //將from表單傳送過來的收件人電話用變數Recipient_phone存起來 ```$Recipient_address = $request->Recipient_address;``` //將from表單傳送過來的收件人地址用變數Recipient_address存起來 ```$shipment_time = $request->shipment_time;``` //將from表單傳送過來的收貨時間用變數shipment_time存起來 ```$order = new Order();```//使用order這個模型 用$order變數 ```$sessionKey = Auth::id();```//如果有登入的話可以直接使用使用者ID $order->(根據資料庫欄位填寫)= 相對應的值 !!!記得save 在結尾的地方!!! $order->save(); ```$items = \Cart::session($sessionKey)->getContent();```這段是上面購物車的內容 將購物車每一筆的產品存入items 若選擇不用登入的方式 改寫下面寫法 $items = \Cart::getContent(); 使用foreach將items裡面的每個產品變成$row的變數代入 Ex兩樣產品做兩次 //```$order_detial= new Order_detail();``` 使用Order_detail的Model存入$order_detial 注意 $order_detial->order_id = $order->id; 這裡的$order_id 是從上面訂單建立後才有的訂單編號注意Create順序 在model中要關聯兩個資料表 由於orders可以有許多orders_detail的產品細項 所以是一對多的概念 在order的MOdel中要使用下列語法關聯 ``` public function order_detail() { return $this->hasMany('App\Order_detail'); } ``` 預設是不用更改forgin_key local_key 訂單在資料庫的示意圖 連結:https://i.imgur.com/tYztdpA.png ![](https://i.imgur.com/tYztdpA.png)