Laravel 5.2

tips and tricks. 

  • New Architecture / Tips

  • Tricks

New architecture

新架構做法

由於 namespace 與 psr-4 規範,讓代碼職責劃分的更清楚,功能拆分的更詳細。

新架構做法

  1. 把整個 app 目錄當成 專案的 application,不必像先前必須創個獨立的目錄做處理分離。
  2. 功能劃分必須在前期先想好,保持每個功能只做一件事情,例如 Validators 就只放驗證的規則。
  3. 升級更加容易,準確來說不再強依賴 Laravel,可以依據自己需求掛 package。

Tips

修改預設的 App namespace。

$ artisan app:name ProjectName

Entities

放置 Model & Relation methods & Scope

觀念 & Tips

$ 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

觀念 & Tips

 

 

建立到指定位置。

* 支持參數 -m 可以順帶連 migration 都建立出來。

 

參考底層 Illuminate\Database\Eloquent\Model 可以看到更多好用的 properties。(ex: $perPage = 15 etc...)。

$ artisan make:model Project/Model

Helpers

用於放置全域會用到的輔助函數

Helpers

Laravel 建立的 helpers 放置在他的 package 當中(Illuminate\Support/helpers.php),我們也可以自己建立屬於專案使用的輔助函數。

Helpers - 建立

到 composer.json,讓 composer 知道你即將掛入檔案讓它 autoload 進來。

    "autoload": {
        "files": [
            "app/Helpers/helpers.php"
        ]
    },

到 app/Helpers 建立 helpers.php。

Helpers - 情境

情境:我們經常使用到的 markdown 功能,原本的做法會是到 Model 這一層透過 Laravel  的 get attribute 方式去改變顯示的內容,但這不太合理,這應該是由 blade 來決定是否轉換格式,如果寫在 Model 層就會和 各個 Model 綁定,變成要 parsing content 卻要修改 Model 層。

 

有了 helper,我們可以寫全域的輔助函數來因應各種需求進行 parsing。

Helpers - 情境

範例代碼:

 

if ( ! function_exists('markdown')) {
    function markdown($text = null)
    {
        return Markdown::convertToHtml($text);
    }
}

注意項目:

不應該把 helper 當成萬靈藥,所有東西都放進去呼叫,而是要依據需求跟他的職責進行區分。

Providers

放置服務提供,放置獨立功能。

Providers

建立服務提供者。

放置可獨立運作的功能,跟 helper 不同的地方是: 可以透過 boot or register method 可以進行緩加載,當使用 Service Provider 的功能時才會進行呼叫。

$ artisan make:provider Providers/ServiceProvider

Providers - 情境

情境:在 Laravel 5 之後我們沒有像之前 Laravel 4 之前可以定義 local or production environment,但在 production environment 下不應該加載像是 debugbar / ide_helper 等開發用的服務。

 

有了 Service Provider,我們可以簡單地寫一個 provider 來進行環境的判斷和加載需要的服務。

Providers - 情境

/**
* @需要加載的 Service Providers...
*/
protected $providers = [
        'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
        'Barryvdh\Debugbar\ServiceProvider',
        'GrahamCampbell\Exceptions\ExceptionsServiceProvider',
    ];

/**
* @需要綁定的 alias facade...
*/
protected $aliases = [
        'Debugbar' => 'Barryvdh\Debugbar\Facade',
    ];

把需要加載的套件與需要綁定的 facade 寫到 properties,讓後續加載或新增較為方便。

Providers - 情境

    /**
     * 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 內寫入判斷,讓服務可以順利加載。

Providers - 情境

register 和 boot method 差別在哪裡? 什麼情境下如何選擇使用它?

 

https://laravel.tw/docs/5.1/providers

Providers - 情境2

新建了一個 validation extend 指令,很多地方要用到但不知道在哪個時候加載。

Providers - 情境2

注意:我們是"擴充" Laravel validation 的機制而非像情境1一樣僅使用基本的 app->register,我們必須要等所有功能加載完才能加載這個服務。

Providers - 情境2

    public function boot()
    {
        $this->registerValidationRules(request()->all());
    }

Providers - 情境2

依據規模大小可以寫成其他 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]);
    });
}

Repositories

直接與 Entities 與 DB 做溝通的資源庫。

Repositories

這指令不是內建的,詳細作法後面會說。

Repository 用來放置直接與 Entities 和 Eloquent, DB 作直接操作的資源庫,它不應該包含邏輯判斷。

$ artisan make:repository NameRepository

Repositories - 情境

功能需求:需要取得文章列表,但可能會使用 user_id 或者是 post_id 得到列表。

Repositories - 情境

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 溝通的功能。

Repositories - 情境

那如果我要在剛剛那邊作一個邏輯判斷,如果是 user_id 就去呼叫 getListByUserId 如果不是就呼叫 getList 那我該怎麼辦?

Repositories - 情境

這時候你就需要 Service 了!

Services

用於放置 部份流程的服務層。

Services

這指令不是內建的,詳細作法後面會說。

Service 用來放置切分好的流程,讓 controller 依據需求更換流程和修改。

$ artisan make:service NameService

Services - 情境

需求:建立文章同時會新建自訂標籤、並且把登入者加入追蹤文章清單當中。

Services - 情境

    /**
     * @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;
    }

Validators

放置 自訂驗證機制的 rules 和 methods。

Validators

這指令不是內建的,詳細作法後面會說。

Validator 用來放置客製化的驗證條件,這會讓驗證機制看起來更加簡潔。

$ artisan make:validator NameValidator

Validators - 情境

在某些情況下有些客製化驗證,像是驗證文章內有沒有禁止字元(例如廣告訊息)。

Validators - 情境

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));
}

Validators - 情境

//validate words.
app('validator')->extend('forbiddenWord', function ($attribute, $value, $parameters, $validator) use ($request) {
            return app(ForbiddenValidator::class)->validate($request[$attribute]);
});

這個情境我是當成 extend 的方式作處理

Transformers

用於轉換最後資料呈現。

Transformers

這指令不是內建的,詳細作法後面會說。

Transformer 用來對最後輸出的結果進行轉換,可以選擇讓資料呈現更符合期待和需求。

$ artisan make:transformer NameTransformer

Transformers - 情境

Mobile 和 Desktop 最後回傳不同內容,並且格式必須篩選整理過。

Transformers - 情境

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 作轉換。

Transformers - 情境

'author' => ($question['anonymous']) ? 
            $this->getAnonymous() : $this->getUserInfo($question['user']),

前端有可能需要這樣子的 Json 格式,轉成 Json的格式可以透過 Helper 作轉換。

取得作者的部份則是希望都統一,因此可以加入一層抽象層作統一實作。

Transformers - 情境

    protected $anonymous = [
        'account' => 'ithelp-anonymous',
    ];

    public function getUserInfo($user)
    {
        return [
            'account'       => $user['account'],
        ];
    }

abstract Transformer

Transformers - 情境

最後,在 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'
]);

Middleware

中介層,過濾進入應用程式的 HTTP 請求。

Middleware

$ artisan make:middleware Name

相當於 4.2 的 filter 機制,只是更加彈性,可以依據需求新建和 pass value。

Middleware - 情境

被黑名單的登入使用者將不能拜訪或送值給指定 method。

Middleware - 情境

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);
}

Middleware - 情境

記得要到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,
];

Tips

Route::get('{id}', [
    'middleware' => 'withdraw:question',
    'as' => 'questions.show',
    'uses' => 'QuestionsController@show'
]);

在某條件下你希望可以 pass value 到 handler 那邊作處理。

public function handle($request, Closure $next, $type = 'question')
{
    .....
}

Tips

仔細觀察 $request,它可以取得很多資料。

$request->route()->parameters() //取得 uri 上的 property [array]

更多方法可以到Illuminate\Http\Request 挖掘。 

Request

中介層,過濾進入應用程式的 HTTP 請求。

Request

$ artisan make:request Name

在真正執行 method 前所作的請求處理,這邊通常會放上表單驗證。

Request - 情境

建立文章和客製化驗證失敗的訊息以及桌機與手機回傳的內容。

Request - 情境

public function rules()
{
    return [
        'subject' => 'required|unique:questions|max:100',
    ];
}
public function messages()
{
    return [
        'subject.required' => config('question.validation.subject.required'),
    ];
}

規則制定

改寫驗證訊息

Request - 情境

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 格式

Tricks

Console

想把一些常用的動作做成 command 型態加快速度。

Console

建立指令,建立後有很多方式可以參考官方網站。

$ artisan make:console Command

Console

但是我想像 Laravel 指令一樣是建立檔案的!

而 Laravel 已經寫好了我卻不能使用?

Console

可以!只要改變繼承類就可以了。

Console

use Illuminate\Console\GeneratorCommand;

class makeService extends GeneratorCommand
{
    ....
}

當然它會要你實現這個抽象層類的一些方法...。

Console

    //指令
    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';
    }

Console

別忘了到 Commands/Kernel.php 註冊檔案 

protected $commands = [
    Commands\Inspire::class,
    Commands\makeRepository::class,
    Commands\makeService::class,
    Commands\makeValidator::class,
    Commands\makeTransformer::class,
];

Validators

想像 Laravel 的驗證 rules 一樣,輸入特定字就會進行驗證。

return [
    'subject' => 'max:100',
];

Validators

這樣看起來棒多了,更好一點的還可以這樣做:將數量提取到 config 設定。

return [
    'subject' => 'max:100',
    'tags' => 'limitTags:5',
];
return [
    'subject' => 'max:100',
    'tags' => 'limitTags:' . config('tags.question.limit'),
];

Validators

還記得我們之前說的 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);
    });
}

Validators

BUT !

自訂驗證的時候為什麼像是 max:100 他的參數可以 assign 到 messages 裡面而我的不行呢?

Validators

因為你必須 replace 在 message 裡面寫的 :limitTags

app('validator')->replacer('limitTags', function ($message, $attribute, $rule, $parameters) {
    return str_replace(':limitTags', $parameters[0], $message);
});

Validators

    public function messages()
    {
        return [
            'tags.limit_tags' => '標籤限制 :limitTags 個!',
        ];
    }

bootstrap

我想知道我的服務提供載入的順序以及可不可以延遲加載?

bootstrap

可以看到所有服務加載的順序

bootstrap/cache/services.php

bootstrap

隱藏了更多的細節,包含 property $defer = true 就可以 達到延遲加載的效果。

Illuminate\Support\ServiceProvider

View

經歷多次研究終於有好的作法了。

View

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>

View

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>

View

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

View

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

More...

Formating

routes.php

Route::group(['prefix' => 'questions'], function() {
    Route::post('/', [
        'middleware' => ['auth', 'blockade'],
        'as' => 'questions.store',
        'uses' => 'QuestionsController@store'
    ]);
});

Formating

PSR-1 / PSR-2 formating code

Settings / Editor / Code Style / PHP / Set from / Predefined Style / PSR1/PSR2

Thank you for your listening.

Made with Slides.com