由於 namespace 與 psr-4 規範,讓代碼職責劃分的更清楚,功能拆分的更詳細。
修改預設的 App namespace。
$ artisan app:name ProjectName
放置 Model & Relation methods & Scope
$ artisan make:model Entities/Name
<?php
namespace Ithelp\Entities;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
protected $fillable = [
'sub_user_id', 'status', 'type', 'user_id', 'question_id'
];
protected $table = 'notifications';
public function questions()
{
return $this->morphedByMany(Question::class, 'notifiable');
}
}
只放 Model properties / Relations / Scope
建立到指定位置。
* 支持參數 -m 可以順帶連 migration 都建立出來。
參考底層 Illuminate\Database\Eloquent\Model 可以看到更多好用的 properties。(ex: $perPage = 15 etc...)。
$ artisan make:model Project/Model
用於放置全域會用到的輔助函數
Laravel 建立的 helpers 放置在他的 package 當中(Illuminate\Support/helpers.php),我們也可以自己建立屬於專案使用的輔助函數。
到 composer.json,讓 composer 知道你即將掛入檔案讓它 autoload 進來。
"autoload": {
"files": [
"app/Helpers/helpers.php"
]
},
到 app/Helpers 建立 helpers.php。
情境:我們經常使用到的 markdown 功能,原本的做法會是到 Model 這一層透過 Laravel 的 get attribute 方式去改變顯示的內容,但這不太合理,這應該是由 blade 來決定是否轉換格式,如果寫在 Model 層就會和 各個 Model 綁定,變成要 parsing content 卻要修改 Model 層。
有了 helper,我們可以寫全域的輔助函數來因應各種需求進行 parsing。
範例代碼:
if ( ! function_exists('markdown')) {
function markdown($text = null)
{
return Markdown::convertToHtml($text);
}
}
注意項目:
不應該把 helper 當成萬靈藥,所有東西都放進去呼叫,而是要依據需求跟他的職責進行區分。
放置服務提供,放置獨立功能。
建立服務提供者。
放置可獨立運作的功能,跟 helper 不同的地方是: 可以透過 boot or register method 可以進行緩加載,當使用 Service Provider 的功能時才會進行呼叫。
$ artisan make:provider Providers/ServiceProvider
情境:在 Laravel 5 之後我們沒有像之前 Laravel 4 之前可以定義 local or production environment,但在 production environment 下不應該加載像是 debugbar / ide_helper 等開發用的服務。
有了 Service Provider,我們可以簡單地寫一個 provider 來進行環境的判斷和加載需要的服務。
/**
* @需要加載的 Service Providers...
*/
protected $providers = [
'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
'Barryvdh\Debugbar\ServiceProvider',
'GrahamCampbell\Exceptions\ExceptionsServiceProvider',
];
/**
* @需要綁定的 alias facade...
*/
protected $aliases = [
'Debugbar' => 'Barryvdh\Debugbar\Facade',
];
把需要加載的套件與需要綁定的 facade 寫到 properties,讓後續加載或新增較為方便。
/**
* Register the application services.
*
* @return void
*/
public function register()
{
if ($this->app->isLocal() && ! empty($this->providers)) {
foreach ($this->providers as $provider) {
$this->app->register($provider);
}
if ( ! empty($this->aliases)) {
foreach ($this->aliases as $alias => $facade) {
$this->app->alias($alias, $facade);
}
}
}
}
在 register method 內寫入判斷,讓服務可以順利加載。
register 和 boot method 差別在哪裡? 什麼情境下如何選擇使用它?
https://laravel.tw/docs/5.1/providers
新建了一個 validation extend 指令,很多地方要用到但不知道在哪個時候加載。
注意:我們是"擴充" Laravel validation 的機制而非像情境1一樣僅使用基本的 app->register,我們必須要等所有功能加載完才能加載這個服務。
public function boot()
{
$this->registerValidationRules(request()->all());
}
依據規模大小可以寫成其他 method 進行呼叫。
public function registerValidationRules($request)
{
//validate words.
app('validator')->extend('forbiddenWord', function ($attribute, $value, $parameters, $validator) use ($request) {
return app(ForbiddenValidator::class)->validate($request[$attribute]);
});
}
直接與 Entities 與 DB 做溝通的資源庫。
這指令不是內建的,詳細作法後面會說。
Repository 用來放置直接與 Entities 和 Eloquent, DB 作直接操作的資源庫,它不應該包含邏輯判斷。
$ artisan make:repository NameRepository
功能需求:需要取得文章列表,但可能會使用 user_id 或者是 post_id 得到列表。
public function getListById($question_id = null)
{
$this->model->find($question_id);
}
public function getListByUserId($user_id = null)
{
$this->model->where('user_id', $user_id)->get();
}
注意:不應該在 Repository 這層去寫任何的邏輯判斷,讓她保持簡單跟單一職責:只做和資料庫或 Entities 溝通的功能。
那如果我要在剛剛那邊作一個邏輯判斷,如果是 user_id 就去呼叫 getListByUserId 如果不是就呼叫 getList 那我該怎麼辦?
這時候你就需要 Service 了!
用於放置 部份流程的服務層。
這指令不是內建的,詳細作法後面會說。
Service 用來放置切分好的流程,讓 controller 依據需求更換流程和修改。
$ artisan make:service NameService
需求:建立文章同時會新建自訂標籤、並且把登入者加入追蹤文章清單當中。
/**
* @param array $data
* @return static
*/
public function createByAuthUser(array $data)
{
//透過 Laravel relations 建立文章
$question = auth()->user()->questions()->create($data);
//轉換標籤
$this->question->setTransFormTags();
//寫入標籤
$question->tag(mb_strtolower($data['tags']));
//加入追蹤
$question->traces()->save($this->trace->saveAuthUser());
return $question;
}
放置 自訂驗證機制的 rules 和 methods。
這指令不是內建的,詳細作法後面會說。
Validator 用來放置客製化的驗證條件,這會讓驗證機制看起來更加簡潔。
$ artisan make:validator NameValidator
在某些情況下有些客製化驗證,像是驗證文章內有沒有禁止字元(例如廣告訊息)。
public function validate($param = null)
{
//從 Repo 得到存在資料庫內的關鍵字
$badWords = implode("|", $this->forbidden->getAllKeyWordOnList()->toArray());
//防止空資料時造成 preg_match 失敗而作的例外處理
if (empty($badWords)) return true;
//回傳是否通過驗證
return boolval( ! preg_match("/{$badWords}/si", $param, $matches));
}
//validate words.
app('validator')->extend('forbiddenWord', function ($attribute, $value, $parameters, $validator) use ($request) {
return app(ForbiddenValidator::class)->validate($request[$attribute]);
});
這個情境我是當成 extend 的方式作處理
用於轉換最後資料呈現。
這指令不是內建的,詳細作法後面會說。
Transformer 用來對最後輸出的結果進行轉換,可以選擇讓資料呈現更符合期待和需求。
$ artisan make:transformer NameTransformer
Mobile 和 Desktop 最後回傳不同內容,並且格式必須篩選整理過。
class QuestionTransformer extends Transformer
{
public function transform($question)
{
return [
'question_id' => $question['id'],
'subject' => $question['subject'],
'description' => markdown($question['description']),
'views' => (integer) $question['views'],
'created_at' => js_time($question['created_at']),
'updated_at' => js_time($question['updated_at']),
'tags' => $this->fetchTags($question['tags']),
'author' => ($question['anonymous']) ? $this->getAnonymous() : $this->getUserInfo($question['user']),
'pushes_count' => 100,
];
}
}
前端有可能需要這樣子的 Json 格式,轉成 Json的格式可以透過 Helper 作轉換。
'author' => ($question['anonymous']) ?
$this->getAnonymous() : $this->getUserInfo($question['user']),
前端有可能需要這樣子的 Json 格式,轉成 Json的格式可以透過 Helper 作轉換。
取得作者的部份則是希望都統一,因此可以加入一層抽象層作統一實作。
protected $anonymous = [
'account' => 'ithelp-anonymous',
];
public function getUserInfo($user)
{
return [
'account' => $user['account'],
];
}
abstract Transformer
最後,在 controller 最後輸出時就可以透過 helper 判斷 device,並且透過 transformer 輸出需要的 json 資料。
return is_mobile([
'desktop' => view('partials.questions.components.show', compact('question')),
'mobile' => respond([
'data' => [
'question' => $this->transformer->transform($question),
],
'status' => 'success'
]);
中介層,過濾進入應用程式的 HTTP 請求。
$ artisan make:middleware Name
相當於 4.2 的 filter 機制,只是更加彈性,可以依據需求新建和 pass value。
被黑名單的登入使用者將不能拜訪或送值給指定 method。
public function handle($request, Closure $next)
{
if (auth()->user()->status == config('blockade.forbidden')) {
return is_mobile([
'desktop' => redirect(config('blockade.redirectURL'))->with('error', config('blockade.message')),
'mobile' => validate_failed(config('blockade.message'))
]);
}
return $next($request);
}
記得要到app/Http/Kernel.php 註冊你的 class。
protected $routeMiddleware = [
/** @mission middleware */
'access' => \Ithelp\Http\Middleware\Access::class,
/** @custom middleware validation. */
'blockade' => \Ithelp\Http\Middleware\Blockade::class,
'withdraw' => \Ithelp\Http\Middleware\Withdraw::class,
];
Route::get('{id}', [
'middleware' => 'withdraw:question',
'as' => 'questions.show',
'uses' => 'QuestionsController@show'
]);
在某條件下你希望可以 pass value 到 handler 那邊作處理。
public function handle($request, Closure $next, $type = 'question')
{
.....
}
仔細觀察 $request,它可以取得很多資料。
$request->route()->parameters() //取得 uri 上的 property [array]
更多方法可以到Illuminate\Http\Request 挖掘。
中介層,過濾進入應用程式的 HTTP 請求。
$ artisan make:request Name
在真正執行 method 前所作的請求處理,這邊通常會放上表單驗證。
建立文章和客製化驗證失敗的訊息以及桌機與手機回傳的內容。
public function rules()
{
return [
'subject' => 'required|unique:questions|max:100',
];
}
public function messages()
{
return [
'subject.required' => config('question.validation.subject.required'),
];
}
規則制定
改寫驗證訊息
public function response(array $errors)
{
if (is_mobile()) {
foreach ($errors as $error) {
//validate_failed 是自己寫的 helper 不是內建的
return validate_failed($error);
}
}
return parent::response($errors);
}
覆寫 response method 讓手機版返回 json 格式
想把一些常用的動作做成 command 型態加快速度。
建立指令,建立後有很多方式可以參考官方網站。
$ artisan make:console Command
但是我想像 Laravel 指令一樣是建立檔案的!
而 Laravel 已經寫好了我卻不能使用?
可以!只要改變繼承類就可以了。
use Illuminate\Console\GeneratorCommand;
class makeService extends GeneratorCommand
{
....
}
當然它會要你實現這個抽象層類的一些方法...。
//指令
protected $name = 'make:service';
//描述
protected $description = 'Create a new form service class';
//類型
protected $type = 'Service';
//樣板位置
protected function getStub()
{
return __DIR__.'/stubs/service.stub';
}
//放置目錄
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Services';
}
別忘了到 Commands/Kernel.php 註冊檔案
protected $commands = [
Commands\Inspire::class,
Commands\makeRepository::class,
Commands\makeService::class,
Commands\makeValidator::class,
Commands\makeTransformer::class,
];
想像 Laravel 的驗證 rules 一樣,輸入特定字就會進行驗證。
return [
'subject' => 'max:100',
];
這樣看起來棒多了,更好一點的還可以這樣做:將數量提取到 config 設定。
return [
'subject' => 'max:100',
'tags' => 'limitTags:5',
];
return [
'subject' => 'max:100',
'tags' => 'limitTags:' . config('tags.question.limit'),
];
還記得我們之前說的 validationServiceProvider嗎?這時候就可以派上用場了。
public function registerValidationRules($request)
{
//validate tags.
app('validator')->extend('limitTags', function ($attribute, $value, $parameters, $validator) use ($request) {
return app(TagValidator::class)->limit($request[$attribute], $value);
});
}
自訂驗證的時候為什麼像是 max:100 他的參數可以 assign 到 messages 裡面而我的不行呢?
因為你必須 replace 在 message 裡面寫的 :limitTags
app('validator')->replacer('limitTags', function ($message, $attribute, $rule, $parameters) {
return str_replace(':limitTags', $parameters[0], $message);
});
public function messages()
{
return [
'tags.limit_tags' => '標籤限制 :limitTags 個!',
];
}
我想知道我的服務提供載入的順序以及可不可以延遲加載?
可以看到所有服務加載的順序
bootstrap/cache/services.php
隱藏了更多的細節,包含 property $defer = true 就可以 達到延遲加載的效果。
Illuminate\Support\ServiceProvider
經歷多次研究終於有好的作法了。
commons/layouts/index.blade.php 主要最外框
<!DOCTYPE html>
<html lang="zh">
<head>
@include('commons.components.head')
@yield('style')
</head>
<body>
@include('commons.components.navbar')
@include('commons.components.messages')
@yield('header')
@yield('content')
@yield('footer')
@yield('editor')
@yield('script')
</body>
</html>
commons/components/ 放置共用組件和需要的js/css
<script src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/messenger/1.4.2/js/messenger.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.14/vue.min.js"></script>
commons/partials/{function}/index.blade.php
各個 function 內層框,可以依照需求加 css/js
@extends('commons.layouts.index')
@section('style')
<link href="{{ asset('css/prism.css') }}" rel="stylesheet">
@stop
@section('content')
@yield('container')
@stop
commons/partials/{function}/components/*
功能的各頁面,包含 container
@extends('partials.questions.index')
@section('container')
<div class="container">
Title: {{ $question->subject }} <br />
description: {!! markdown($question->description) !!}<br />
Tags:
@foreach($question->tags as $tag)
{{ $tag->name }}
@endforeach
</div>
@include('partials.answers.components.form', ['qid' => $question->id])
@stop
routes.php
Route::group(['prefix' => 'questions'], function() {
Route::post('/', [
'middleware' => ['auth', 'blockade'],
'as' => 'questions.store',
'uses' => 'QuestionsController@store'
]);
});
PSR-1 / PSR-2 formating code
Settings / Editor / Code Style / PHP / Set from / Predefined Style / PSR1/PSR2