Laravel 學習筆記

# 前言

本篇為 Laravel 的學習筆記, 主要將看到的, 學到的技術轉換成 Q&A 的方式以加速學習



# Production 優化

# PHP

php.ini 文件

php.ini 中的 memory_limit 用於設定單個 PHP process 可以使用的系統內存最大值


# Laravel timezone 與 MySQL timezone

# 當 Laravel 收到沒有時區的時間

  • 預設此時間為 Laravel timezone, 不另外做轉換直接帶給資料庫, 如下圖:
    sequenceDiagram
    participant Client
    participant Laravel
    participant MySQL
    NOTE OVER Client,Laravel: 假設 Laravel 時區為 Asia/Taipei
    Client-->>Laravel: 給你 2021-02-05 15:27:46
    NOTE OVER Client,Laravel: Laravel 會默認上述時間為 Asia/Taipei
    Laravel-->>MySQL: 給你 2021-02-05 15:27:46

# 當 Laravel 收到有時區的時間

  • 將時間轉為 Laravel 時區, 如下圖:

    sequenceDiagram
    participant Client
    participant Laravel
    participant MySQL
    NOTE OVER Client,Laravel: 假設 Laravel 時區為 Asia/Taipei
    Client-->>Laravel: 給你 2021-02-05T15:27:46+0400
    NOTE OVER Client,Laravel: Laravel 會將上面的時間轉為 Asia/Taipei
    NOTE OVER Client,Laravel: 即 2021-02-05T19:27:46+0800
    Laravel-->>MySQL: 給你 2021-02-05 19:27:46

    # Laravel 回傳時間

    sequenceDiagram
    participant Client
    participant Laravel
    participant MySQL
    NOTE OVER Client,Laravel: 假設 Laravel 時區為 Asia/Taipei
    MySQL-->>Laravel: 給你 2021-02-05 19:27:46
    NOTE OVER Client,Laravel: Laravel 默認上面時間的時區為 Asia/Taipei
    NOTE OVER Client,Laravel: 即 2021-02-05T11:27:46+0000 (預設回傳 ISO8601)
    Laravel-->>Client: 給你 2021-02-05T11:27:46+0000

# 當 MySQL 的 column 為 datetime

sequenceDiagram
participant Laravel
participant MySQL
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 當 column type 為 datetime, 不做任何變更
NOTE OVER Laravel,MySQL: 實存 2021-02-05 19:27:46 到資料庫

# 當 MySQL 的 column 為 timestamp

sequenceDiagram
participant Laravel
participant MySQL
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: MySQL column type 為 timestamp, 假設 MySQL timezone 為 Asia/Taipei
NOTE OVER Laravel,MySQL: 使用 default timezone 將該時間轉為 UTC 儲存
NOTE OVER Laravel,MySQL: 實存 2021-02-05T11:27:46+0000, 顯示 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 之後若是更換時區, 會以 2021-02-05T11:27:46+0000 來轉換為該時區時間顯示

# 調整 MySQL timezone 對 Laravel 輸出的影響 (datetime)

sequenceDiagram
participant Laravel
participant MySQL
NOTE OVER Laravel,MySQL: 時區為 Asia/Taipei
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 實存 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 修改時區為 UTC
MySQL->>Laravel: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: datetime 不受更換時區影響

# 調整 MySQL timezone 對 Laravel 輸出的影響 (timestamp)

sequenceDiagram
participant Laravel
participant MySQL
NOTE OVER Laravel,MySQL: 時區為 Asia/Taipei
Laravel-->>MySQL: 給你 2021-02-05 19:27:46
NOTE OVER Laravel,MySQL: 實存 2021-02-05T11:27:46+0000
NOTE OVER Laravel,MySQL: 修改時區為 UTC
MySQL->>Laravel: 給你 2021-02-05 11:27:46
NOTE OVER Laravel,MySQL: timestamp 會因為修改時區而回傳不一樣的 datetime string

# Questions and Answers

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    // In controller
    public function store()
    {
    Messenger::dispatch($customer);
    }

    // In Messenger Job
    public $tries = 0;

    public function handle()
    {
    if (!$this->customer->is_active) {
    return $this->fail(
    new \Exception('Customer account is not active!')
    );
    }

    $messages = Messages::where('customer', $this->customer->id)->where('status', 'pending')
    ->orderBy('timestamp')->limit(10)->get();

    if (!$messages->count()) {
    return $this->release(5);
    }

    foreach ($messages as $message) {
    $this->chained[] = $this->serializeJob(new ProcessMessage($message));
    }

    $this->chained[] = $this->serializeJob(new self($this->customer));

    Bus::chain($this->chained)->dispatch();
    }

    // In ProcessMessage Job
    public $tries = 0;

    public function handle()
    {
    if ($this->attemps() > 5) {
    Log::error(...);

    return;
    }

    Intercom::send($this->message);

    $this->message->update([
    'status' => 'sent'
    ]);
    }
  • Answer:

    <?php
    // In controller
    // 當建立新的 customer 時, 啟動該 job
    public function store()
    {
    Messenger::dispatch($customer);
    }

    // In Messenger Job
    // $treis 設為 0, 因為這個 job 將會無限制次數的一直跑在背景, 監聽著 database
    public $tries = 0;

    public function handle()
    {
    // 如果該 customer 帳號為啟動, 則 return 該 job, 啟動時需 dispatch messenger job
    if (!$this->customer->is_active) {
    return $this->fail(
    new \Exception('Customer account is not active!')
    );
    }

    // Laravel 會將 chain's jobs payloads 存在第一個 the payload of the first job in the chain
    // 所以使用 limit() 以避免 payload size 過大
    $messages = Messages::where('customer', $this->customer->id)->where('status', 'pending')
    ->orderBy('timestamp')->limit(10)->get();

    // 如果目前資料庫中沒有未處理的 message, 則 5 秒後再看一次
    if (!$messages->count()) {
    return $this->release(5);
    }

    // 取得將會用到的 job
    foreach ($messages as $message) {
    $this->chained[] = $this->serializeJob(new ProcessMessage($message));
    }

    // 最後, 將 messenger 這個 job 放在最後, 這樣當所有的 message 都處理完成
    // 會再次執行 messenger job, 再檢查一次資料庫
    $this->chained[] = $this->serializeJob(new self($this->customer));

    // dispatch chained jobs
    Bus::chain($this->chained)->dispatch();
    }

    // In ProcessMessage Job
    // $tries 需設為 0, 這樣才不會因為失敗被 mark as failed
    // 才不會讓後面的 job 都無法被執行
    public $tries = 0;

    public function handle()
    {
    // 因為 $tries 設為 0, 所以這邊要手動的處理 failure
    // 如果嘗試超過 5 次, 就 log 並 return, 執行下一個 message job
    if ($this->attemps() > 5) {
    Log::error(...);

    return;
    }

    Intercom::send($this->message);

    $this->message->update([
    'status' => 'sent'
    ]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class RefundAttendee implements ShouldQueue, ShouldBeUnique
    {

    public $uniqueFor = 10; // ...

    public function __construct($attendee)
    {
    $this->attendee = $attendee;
    }

    public function handle()
    {
    if (!$this->attendee->invoice->wasRefunded()) {
    $this->attendee->invoice->refund();
    }
    Mail::to($this->attendee)->send(...);
    }

    public function uniqueId()
    {
    return $this->attendee->invoice->id;
    }

    }
  • Answer:
    Laravel 8 的新功能, 如果擔心一個 job 會被 dispatch 多次, 那可以 implement ShouldBeUnique interface, 當 queue 內已有該 instance, 後面 dispatch 的都會被忽略
    如果需要 dispatch 多個相同的 job class, 但根據帶入的 parameter 而不同的話, 可使用 uniqueId() 來作為 unique key
    Laravel 在底層會嘗試取得該 job 該 unique key 的 lock, 只要該 lock 存在, 後面 dispatch 的 job 都會被忽略
    通常 lock 會在該 job fails 或 finish 之後 release, 若要自定義釋放時間, 可使用 $uniqueFor property

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class ReportGenerationPlan
    {

    public static function start($report)
    {
    Bus::chain([
    new ExtractData($report),
    function () use ($report) {
    static::step2($report);
    }
    ])->catch(function () use ($report) {
    static::failed($report);
    })->dispatch();
    }

    private static function step2($report)
    {
    $jobs = $report->chunks->mapInto(TransformChunk::class);
    Bus::batch($jobs)
    ->then(function () use ($report) {
    static::step3($report);
    })->dispatch();
    }

    private static function step3($report)
    {
    Bus::chain([
    new StoreData($report),
    new GenerateSummary($report)
    ])->dispatch();
    }

    private static function failed($report)
    {
    // Run any cleaning work ...
    $report->update([
    'status' => 'failed'
    ]);
    }
    }

    // in controller
    $report = Report::create([...]);
    ReportGenerationPlan:::start($report);
  • Answer:

    START
    -> RUN ExtractData THEN
    -> DISPATCH multiple TransformChunk jobs THEN
    -> RUN all TransformChunk jobs from the batch THEN
    -> DISPATCH a chain THEN
    -> RUN StoreData THEN
    -> RUN GenerateSummary END
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function store()
    {
    $database = Database::create([
    'status' => 'provisioning'
    ]);

    Bus::chain(
    new EnsureANetworkExists(),
    new EnsureNetworkHasInternetAccess(),
    new CreateDatabase($database)
    )->catch(function () {
    $network = Network::first();
    if (!$network->usedByOtherResources()) {
    $network->delete();

    return;
    }
    if (!$network->activeDatabases()->count()) {
    $network->removeInternetAccess();
    }
    })->dispatch();
    }
  • Answer:
    多個 job 會按照 chain() 內的順序執行, 當第一個成功了才會執行下一個, 如果第一個一直 retry 直到 attempt 好耗盡而失敗, 那便會 invoke cache() 內的 closure
    先確定 network 存在, 否則則建立, 在確認 network 有 internet access, 否則則建立, 最後確認 database 是否有被 attached, 若無, 則 attach 到 network
    若失敗, 先確認 network 是否有被其他 resource 使用, 若無, 則直接刪除該 network, 若有被其他資源使用, 但無連接 active database, 則移除 internet access

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    // in controller
    public function store()
    {
    $temporaryFilePath = request()->file('video')
    ->store('uploaded-videos');
    CompressAndStoreVideo::dispatch($temporaryFilePath);
    }

    // in job
    public function handle()
    {
    if (!app('files')->exists($this->temporaryFilePath)) {
    report(new \Exception('Temporary file not found!'));

    return $this->delete();
    }

    $newFile = VideoProcessesor::compress($this->temporaryFilePath);

    app('files')->delete($this->temporaryFilePath);
    }


    public function failed(Exception $e)
    {
    // 如果之後不重試, 也可立即刪掉
    DeleteFile::dispatch($this->temporaryFilePath)->delay(now()->addHours(24));
    }
  • Answer:
    在 controller 中, 將 file 先存到 default disk, 並取得 path, dispatch path 到 job
    job handle method 中, 檢查該 path file 是否依然存在, 如果不存在則 report 並刪掉該 job
    如果存在, 則 compress 該 file, 完成後刪掉該 file
    如果失敗了, 延遲 24 小時後刪除, 在這期間可以手動嘗試

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class ModelIdentifier
    {

    public function __construct(
    $class,
    $id,
    $relations,
    $connection
    ) {
    $this->id = $id;
    $this->class = $class;
    $this->relations = $relations;
    $this->connection = $connection;
    }
    }
  • Answer:
    Laravel 為了減少 job payload size, 當 queued job 如果其中有 model, 只會記住該 model identifier, 待真正執行該 job 時才會依照 identifier 取出 model

以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 client 端傳來的時間是沒有時區的, Laravel 會默認該時間的時區為 Laravel 的 default timezone, 不多做處理直接存到資料庫
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 client 端傳來的時間有時區的, Laravel 會將傳來的時間轉換為 default timezone 的時區, 再存到資料庫
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 Laravel 從資料庫取得時間後, 會將該時間的時區默認為 Laravel default timezone, 並轉成 ISO8601 格式給 client
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 的 column type 為 datetime 時, 收到什麼就存什麼, 不另外轉換
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 的 column type 為 timestamp 時, 會將收到的時間的時區設為當下 MySQL default timezone, 並轉為 UTC timezone 儲存下來, 當使用 select 時, MySQL 會將實際上存為 UTC 的時間轉換為當下 MySQL timezone 的時間
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 時區為 datetime 時, 不管怎麼調整 MySQL 的時區, 都不會影響回傳的結果
以下的 Laravel example picture 的意思是?
  • Example:
  • Answer:
    當 MySQL 時區為 timestamp 時, 如果去修改 MySQL timezone, 會先將實際儲存的 UTC timezone 時間轉換為該 timezone 的時間, 再回傳
以下的 Laravel job middleware example code 的意思是?
  • Example:

    <?php
    public function handle($job, $next)
    {
    $this->lastFailureTimestamp = Cache::get('circuit:open');
    // Check if the circuit is open and release the job.
    if (!$this->shouldRun()) {
    return $job->release(
    $this->lastFailureTimestamp + $this->secondsToCloseCircuit + rand(1, 120)
    );
    }
    // If the circuit is closed or half-open, we will try // running the job and catch exceptions.
    try {
    $next($job);
    // If the job passes, we'll close the circuit if it's // open and reset the failures counter. $this->closeCircuit();
    } catch (RequestException $e) {
    if ($e->response->serverError()) {
    $this->handleFailure($job);
    }
    } catch (ConnectionException $e) {
    $this->handleFailure($job);
    }
    }
  • Answer:
    將 circuit pattern 的概念寫成一個 job middleware, 當 error 達到一定程度時打開 circuit breaker, 打開一段時間後啟動 half open 狀態, 放一個 job 過去, 要是該 job 成功, 則關掉 circuit breaker, 要是失敗則重新計算 circuit breaker 時間, 同時根據 exception 的不同使用不同的方式來 handle failure, request exception 或 connection exception

以下的 Laravel job example code 的意思是?
  • Example:

    <?php
    class RateLimitingJobMiddleware
    {
    public $key;

    public function __construct($key)
    {
    $this->key = $key;
    }

    public function handle($job, $next)
    {
    Redis::throttle(
    $this->key
    )
    // ...
    }
    }

    // job class
    public function middleware()
    {
    return [
    new BulkHeadingJobMiddleware('slow_service'),
    new CircuitBreakerJobMiddleware('unstable_service')
    ];
    }
  • Answer:
    可 pass parameter 到 job middleware, 以 limit 特定的 job

以下位於 job 中的 Laravel example code 的意思是?
  • Example:

    <?php
    public function middleware()
    {
    return [
    new RateLimitingJobMiddleware($this->customer->id)
    ];
    }
  • Answer:
    在 job class 中的 middleware method 中可定義該 job 會經過哪一些 middleware, 並且可 pass parameter 到 middleware

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class RateLimitingJobMiddleware
    {
    public function handle($job, $next)
    {
    Redis::throttle('job-limiter')
    ->allow(5)
    ->every(60)
    ->then(function () use ($job, $next) {
    $next($job);
    }, function () use ($job) {
    $job->release(60);
    });
    }
    }
  • Answer:
    使用 job middleware 來限制最多可以 concurrent 執行的 job 數量, Redis 的 throttle method 會鎖住 job-limiter 字段, 使該字段只能最多 5 個 current job 被 worker pick up, $next($job) 代表執行該 job 的 handle(), 也可透過 property 來取代字段, 這樣就可以限制同類型的 job

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    Carbon::parse($request->changed_at)->setTimezone(config('app.timezone'));
  • Answer:
    就算帶入的 string 本身已經是 ISO8601 格式, 如果不特別使用 setTimezone 或 toIso8601String 的話, 存入 MySQL 的 date 是不會根據當前的 timezone 去做轉換的, 也就是 Laravel 會無視 timezone 的部分, 直接將 datetime 的部分存入資料庫, 這樣就失去 ISO8601 註明 timezone 意義了

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    $branchManagers = BranchManager::query()
    ->select(['branch_managers.account', 'branch_managers.created_at', 'shops.name', 'branch_managers.status_id'])
    ->join('shops', 'branch_managers.shop_id', '=', 'shops.id')
    ->when($search, function ($query) use ($search) {
    $query->whereIn('branch_managers.id', function ($query) use ($search) {
    $query->select('id')
    ->from(function ($query) use ($search) {
    $query->select('id')
    ->from('branch_managers')
    ->whereRaw('match(account) against (? in boolean mode)', [$search])
    ->union(
    $query->newQuery()
    ->select('branch_managers.id')
    ->from('branch_managers')
    ->join('shops', 'branch_managers.shop_id', '=', 'shops.id')
    ->whereRaw('match(name) against (? in boolean mode)', [$search])
    );
    }, 'matches');
    });
    })->get();
  • Answer:
    無法直接使用 orWhereRaw (match(…) …) 這樣子的 full text search 在多個表格, 因此統一取得 id, 在使用 where in 取得最後需要的資料
    使用 union 來取得兩個 select query 取得的不重複 id
    使用 derived table, 避免 where in 與 union query 之間的 dependency

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function handle()
    {
    if ($lastFailureTimestamp = Cache::get('circuit:open')) {
    if (time() - $lastFailureTimestamp < 8 * 60) {
    return $this->release(
    $lastFailureTimestamp + 600 + rand(1, 120)
    );
    } else {
    $halfOpen = true;
    }
    }
    $response = Http::acceptJson()->timeout(10)
    ->get('...');
    if ($response->serverError()) {
    if (isset($halfOpen)) {
    Cache::put('circuit:open', time(), 600);

    return $this->release(600);
    }
    if (!$failures = Cache::get('failures')) {
    Cache::put('failures', 1, 60);
    } else {
    Cache::increment('failures');
    }
    if (Cache::get('failures') > 10) {
    Cache::put('circuit:open', time(), 600);
    }

    return $this->release(600);
    }
    Cache::forget('failures');
    Cache::forget('circuit:open');
    // Use the response to run the business logic.
    }
  • Answer:
    circuit pattern
    如果第三方服務失敗超過 10 次, 則開啟斷路器, 10 分鐘後再嘗試
    若斷路器已開啟超過 8 分鐘, 先放一個 job 過去試試看, 若成功則關閉斷路器, 失敗則更新斷路器開啟時間, 重新計算 10 分鐘

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function handle()
    {
    Redis::throttle($this->report->customer_id)->allow(5)
    ->every(60 * 60)
    ->then(function () {
    $results = ReportGenerator::generate($this->report);
    $this->report->update([
    'status' => 'done', 'results' => $results
    ]);
    }, function () {
    return $this->release(60 * 10);
    });
    }
  • Answer:
    每 1 個小時只允許每個 customer_id 可以最多產生 5 份 report, 如果 5 個 slots 都滿了, 則 acquire lock 失敗的 job 會被在 10 分鐘後 release, 60 分鐘從第一個 slot 被佔據之後開始計算時間

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    Redis::funnel($this->report->customer_id)->releaseAfter(5 * 60)
    ->block(...)
    ->limit(...)
    ->then(...)
  • Answer:
    funnel limiter 預設會在 60 秒後清掉 slot, 若該 job 執行時間超過 60 秒, limiter 會清掉該 slot, 但原本的 job 還在執行中, 這樣會變成又有一個新的 job 可被執行, 這樣就超出了我們預期 limiter 的最大限制, 可使用 releaseAfter 定義 timeout 時間

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    Redis::funnel($this->report->customer_id)->block(5)
    ->limit(...)->then(...);
  • Answer:
    funnel 預設會嘗試 acquire lock 3 秒, 可使用 block() 指定嘗試時間

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function handle()
    {
    public $tries = 10;
    public $maxExceptions = 2;
    Redis::funnel($this->report->customer_id)->limit(5)
    ->then(function () {
    $results = ReportGenerator::generate($this->report);
    $this->report->update([
    'status' => 'done', 'results' => $results
    ]);
    }, function () {
    return $this->release(10);
    });
    }
  • Answer:
    限制同一個 customer_id 最多只能同時有 5 筆 job 被執行, 若成功取得 lock 則會執行 closure, 若無法取得 lock (預設 3 秒), 則執行 second closure, 10 秒後可重新被 worker pick up

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function handle()
    {
    if ($timestamp = Cache::get('api-limit')) {
    return $this->release(
    $timestamp - time());
    }
    $response = Http::acceptJson()->timeout(10)
    ->withToken('...')->get('https://...');
    if ($response->failed() && $response->status() == 429) {
    $secondsRemaining = $response->header('Retry-After');
    Cache::put('api-limit',
    now()->addSeconds($secondsRemaining)->timestamp,
    $secondsRemaining
    );

    return $this->release($secondsRemaining
    );
    }
    // ...
    }
  • Answer:

    <?php
    public function handle()
    {
    // 所有會呼叫此外部服務的 job 會在第一個判斷重新被 release, 如果該外部服務
    // 目前處於 429 的狀態的話
    // 如果 cache 中存在 api-limit 這個 key, 代表還處於 429 狀態
    // api-limit 的 value 為實際上允許再被存取的時間, 所以這個時間扣掉當前
    // 的時間就會是距離可再被存取還需經過的秒數
    if ($timestamp = Cache::get('api-limit')) {
    return $this->release(
    $timestamp - time());
    }
    $response = Http::acceptJson()->timeout(10)
    ->withToken('...')->get('https://...');

    // 如果 response failed, 且 status 為 429, 代表 too many attempt
    // 得出可再被存取的時間點, 並將這個時間點存去 cache
    if ($response->failed() && $response->status() == 429) {
    $secondsRemaining = $response->header('Retry-After');
    Cache::put('api-limit',
    now()->addSeconds($secondsRemaining)->timestamp,
    $secondsRemaining
    );

    return $this->release($secondsRemaining
    );
    }
    // ...
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function getSpecifiedConstants($className, $needle)
    {
    return collect((new \ReflectionClass($className))->getConstants())
    ->filter(function ($value, $key) use ($needle) {
    return Str::startsWith($key, $needle);
    });
    }
  • Answer:
    使用 ReflectionClass(), 從指定的 class 中取得定義於該 class 中的 constants, 在使用特定的條件篩選

以下的 Laravel example code, 有何差異?
  • Example:

    <?php
    $rows = [$row1, $row2, $row3]
    DB::table('voucher_statements')->insert($rows);
    Voucher::insert($row1);
  • Answer:
    前者可支援多筆, 但不會 insert created_at 跟 updated_at, 後者只能一次一筆, 但會 insert created_at, updated_at

以下的 Laravel example code 會使用幾筆 SQL query?
  • Example:

    <?php
    $rows = [$row1, $row2, $row3]
    DB::table('voucher_statements')->insert($rows);
  • Answer:
    一筆

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    // Controller
    foreach (User::all() as $user) {
    $exchangeRate = Currency::exchangeRateFor($user->currency);
    GenerateInvoice::dispatch($user, $month, $exchangeRate);
    }

    // Currency Class
    public static $rates = [];

    public static function exchangeRateFor($currency)
    {
    if (!isset(static::$rates[$currency])) {
    static::$rates[$currency] = ExchangeRateApi::get('USD', $currency);
    }

    return static::$rates[$currency];
    }

    // Job handle method
    public function handle()
    {
    $amountInLocalCurrency = $amount * $this->exchangeRate;
    $taxInLocalCurrency = ($amount * 14 / 100) * $this->exchangeRate;

    $total = $amountInLocalCurrency + $taxInLocalCurrency;
    Mail::to($this->user)->send("Your usage last month was {$total}{$this->user->currency}");
    }
  • Answer:
    發送月帳單給客戶
    使用 memorization tech 取得 exchange rate, 避免重複呼叫 API
    取得 exchange rate 之後在 pass 到 job, 因為 job 運行在 memory 中的 single instance, 若是在 job 中使用 memorization tech, 那將會一直使用該 instance 中的初始得到的 exchange rate

    以下的 Laravel example code 的意思是?
  • Example:

    <?php
    // controller
    foreach (Site::all() as $site) {
    if ($site->current_visitors >= $site->threshold) {
    SendSpikeDetectionNotification::dispatch(
    $site, $site->current_visitors);
    }
    }

    // job class
    private $site;
    private $visitors;

    public function __construct(Conference $site, $visitors)
    {
    $this->site = $site;
    $this->visitors = $visitors;
    }

    public function handle()
    {
    SMS::send(
    $this->site->owner,
    "Spike detected on {$this->site->name}! Current visitors: {$this->visitors}"
    );
    }
  • Answer:
    當偵測到某個 site 的 visitor 已達 threshold, 發送 notification, 且 pass 當時的 value
    因為 job execute 可能會有 delay, 若是在 job 中使用 model, serialization 會讓 job 取得最新的 model 狀態, 因此可能會出現 notification 中的資訊並非當時觸發 threshold 的數字, 而是 delay 之後可能降下來的數字

以下兩個 job class example, 差異處在於?
  • Job Example 1:

    <?php
    private $site;

    public function __construct(Conference $site)
    {
    $this->site = $site;
    }

    public function handle()
    {
    SMS::send(
    $this->site->owner,
    "Spike detected on {$this->site->name}! Current visitors: {$this->site->current_visitors}"
    );
    }
  • Job Example 2:

    <?php
    private $site;
    private $visitors;

    public function __construct(Conference $site, $visitors)
    {
    $this->site = $site;
    $this->visitors = $visitors;
    }

    public function handle()
    {
    SMS::send(
    $this->site->owner,
    "Spike detected on {$this->site->name}! Current visitors: {$this->visitors}"
    );
    }
  • Answer:
    當 job 被 queue 時, serialization 會記下 model identifier 而非 model instance, 因此
    example 1 中, 因為是在 job 中 fetch model attribute, 會取得 model attribute 最新的狀態, 值可能會跟 dispatch 時不同
    example 2 中, 因為是在 job 中取得 job class property, 因此值會跟 dispatch 時一樣

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function store()
    {
    DB::transaction(function () use ($conference) {
    $attendee = Attendee::create([
    'conference_id' => $conference->id, 'name' => request('name'),
    'reference' => $reference = Str::uuid() // ...
    ]);
    $invoice = BillingProvider::invoice([
    'customer_reference' => $reference, 'card_token' => request('card_token'), // ...
    ]);
    });
    SendTicketInformation::dispatch($attendee);
    }
  • Answer:
    在 transaction 內, 建立 attendee 並 bill, 如果失敗則回滾並 throw exception, 如果都成功, 則 dispatch SendTicketInformation job, 若要在 transaction 內 dispatch 也可, 只要設定 delay 就可, 確保 transaction commit 完成之後 worker 才去 pick up job

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    Bus::batch($jobs)->allowFailures()
    ->then(...)
    ->catch(...)->finally(function ($batch) {
    $conference = Conference::firstWhere('refunds_batch', '=', $batch->id);
    Mail::to($conference->organizer)->send(
    'Refunding attendees completed!'
    );
    })->dispatch();
  • Answer:
    當所有 job 都執行完畢, 儘管有些 fail, 有些 succeed, 那便會觸發 finally(), 通知 organizer

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    Bus::batch($jobs)
    ->allowFailures()
    ->then(...)
    ->catch(function ($batch, $e) {
    $conference = Conference::firstWhere('refunds_batch', '=', $batch->id);
    Mail::to($conference->organizer)->send(
    'We failed to refund some of the attendees!'
    );
    })
    ->dispatch();
  • Answer:
    當首次有 job fails 時, 會執行 cache() 內的 closure, 通知 organizer, 如果有 job fails 就不會觸發 then()

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    Bus::batch($jobs)->allowFailures()->then(function ($batch) {
    $conference = Conference::firstWhere('refunds_batch', '=', $batch->id);
    Mail::to($conference->organizer)->send(
    'All attendees were refunded successfully!'
    );
    })->dispatch();
  • Answer:
    當 batch 內所有的 job 都 successfully executed, 會執行 then() 內的 closure, 發 mail 通知 organizer

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    $batch = Bus::findBatch($conference->refunds_batch);

    return [
    'progress' => $batch->progress().'%',
    'remaining_refunds' => $batch->pendingJobs,
    'has_failures' => $batch->hasFailures(),
    'is_cancelled' => $batch->canceled()
    ];
  • Answer:
    取得 batch 目前的整體進度 (1-100, int)
    取得目前 pending jobs 的數量 (int)
    取得是否有 failed job (boolean)
    取得該 batch 是否 cancelled (boolean)

以下的 Laravel example command 的意思是?
  • Example:

    php artisan queue:retry-batch {batch_id}
  • Answer:
    使用 CLI retry 指定 batch 中的 failed jobs

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    $batch = Bus::batch($jobs)->allowFailures()->dispatch();
  • Answer:
    當使用 batch dispatch jobs, 預設如果其中一個 job fails, 那該 batch 就會終止, 使用 allowFailures(), 即使有 job fails 該 batch 也會 dispatch 其他 job

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    $batch = Bus::findBatch($conference->refunds_batch);
    $batch->cancel();

    // in job class
    use Batchable;

    public function handle()
    {
    if ($this->batch()->canceled()) {
    return;
    }
    // Actual refunding code here ...
    $this->attendee->update([
    'refunded' => true
    ]);
    }
  • Answer:
    從存在資料庫的 batch_id 取得該 batch, 並執行 cancel(), 該 batch 會被 marked as cancelled
    在 job handle() 中, 判斷如果該 job 的 batch 已經被 marked as cancelled, 那立即 return 該 job, 若否則繼續執行
    藉此當我們 mark batch as cancelled 之後, 所以在 queue 中尚未執行的 job 都會被 cancelled

以下的 Laravel example command 的意思是?
  • Example:

    php artisan queue:batches-table
  • Answer:
    Laravel 會將 batch 資料存在 table, 因此需要建立該 table

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    use Illuminate\Bus\Batchable;

    class RefundAttendee implements ShouldQueue
    {
    use Batchable;
    }
  • Answer:
    若要 job 可被 batch dispatch, 需 use Batchable trait

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    $jobs = $this->conference->attendees->map(function ($attendee) {
    return new RefundAttendee($attendee);
    });
    $batch = Bus::batch($jobs)->dispatch();
    $this->conference->update([
    'refunds_batch' => $batch->id
    ]);
  • Answer:
    使用 batch(), 一次性的 queue 複數的 jobs, 相當於用一句 command 將複數的 job push 到 queue, 而不是分成多句 command
    並將 batch id 儲存在資料庫

以下的 Laravel job example 中, 要是 worker 在取得 lock 之後 crash 了, 那會發生什麼事?
  • Example:

    <?php
    public function handle()
    {
    $invoice = $this->attendee->invoice;

    Cache::lock('refund.'.$invoice->id)
    ->get(function() {
    if (! $invoice->wasRefunded()) {
    $invoice->refund();
    }

    Mail::to($this->attendee)->send(...);
    });
    }
  • Answer:
    因為 lock 並沒有設定持有秒數, 所以會永遠持有, 那當該 job reserved tag 被移除後, 其他 worker 會嘗試 pick up 該 job, 但會卡在 lock 處, 直到 $timeout, 最後耗盡 $tries 次數

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class RefundAttendee implements ShouldQueue
    {

    public $tries = 3;
    public $timeout = 60;
    public $backoff = 11;

    private $attendee;

    public function __construct(Attendee $attendee)
    {
    $this->attendee = $attendee;
    }

    public function handle()
    {
    $invoice = $this->attendee->invoice;

    Cache::lock('refund.'.$invoice->id, 10)
    ->get(function() {
    if (! $invoice->wasRefunded()) {
    $invoice->refund();
    }

    Mail::to($this->attendee)->send(...);
    });
    }
    }

    // wasRefunded
    public function wasRefunded()
    {
    $response = HTTP::timeout(5)->get('../invoice/'.$id)->throw()
    ->json();

    return $response['invoice']['status'] == 'refunded';
    }
  • Answer:
    使用 lock, 這樣當不小心 dispatch 同一個 job 兩次時, 也不用擔心 worker 會執行兩次
    設定 lock 秒數, 如果沒設定, 假如 worker obtain lock 之後 crash, 那該 job 將永遠不會被執行, 直到 $tries 耗盡, 因為 lock 並沒有釋放
    $backoff 設定 11 秒, 以避免 lock 10 秒期間 worker 不停的嘗試 obtain lock
    在真正 refund 之前, 都呼叫 billing provider 以確定該 invoice 是否已經 refunded, 以避免 refund 兩次

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    // on controller
    $this->conference->attendees->each(function ($attendee) {
    RefundAttendee::dispatch($attendee);
    });

    // on job class
    class RefundAttendee implements ShouldQueue
    {

    public $tries = 3;
    public $timeout = 60;
    private $attendee;

    public function __construct(Attendee $attendee)
    {
    $this->attendee = $attendee;
    }

    public function handle()
    {
    $this->attendee->invoice->refund();
    Mail::to($this->attendee)->send(...);
    }
    }

    // worker
    php artisan queue:work --queue=cancelations,default
    php artisan queue:work --queue=default,cancelations
  • Answer:
    假如一場 conference 忽然要取消, 我取消 refund 這場 conference 中的每一個 attendee, 可將 refund 作業分拆成多個 small job
    使用兩個 worker, 定義不同的 priority 以避免 starvation 的狀況

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    'connections' => [
    'database' => [
    'driver' => 'database',
    'retry_after' => 60,
    ],
    'database_long_running' => [
    'driver' => 'database',
    'retry_after' => 18060,
    ],
    ],
  • Answer:
    retry_after 會將 job 的 reserved tag 移除, 以避免 worker crash 後該 job 始終保持 reserved 狀態而永遠不被執行
    但一個 connection 只能設定一個 retry_after, 所以如果有 job 運行時間會比較久 的需求時, 很可能需要第二個 connection
    或是盡可能地避免這種狀況, 將需要運行比較久的 job 分拆成多個 job

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    // job
    class CancelConference implements ShouldQueue
    {

    public $timeout = 18000;
    }

    // config/queue.php
    'connections' => [
    'database' => [
    'driver' => 'database',
    'table' => 'jobs',
    'queue' => 'default',
    'retry_after' => 18060,
    ],
    ],
  • Answer:
    假如該 job timeout, 給 worker 60 秒的時間完成清理工作, 不管如何, 18060 秒之後, 該 job 都會被移除 reserved tag

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class ProvisionServer implements ShouldQueue
    {

    private $server;
    private $payload;

    public $tries = 20;
    public $maxExceptions = 3;

    public function __construct(Server $server, $payload)
    {
    $this->server = $server;
    $this->payload = $payload;
    }

    public function handle()
    {
    if (!$this->server->forge_server_id) {
    $response = Http::timeout(5)->post(
    '.../servers', $this->payload)->throw()->json();
    $this->server->update([
    'forge_server_id' => $response['id']
    ]);

    return $this->release(120);
    }
    if ($this->server->stillProvisioning($this->server)) {
    return $this->release(60);
    }
    $this->server->update([
    'is_ready' => true
    ]);
    }

    public function failed(Exception $e)
    {
    Alert::create([
    // ...
    'message' => "Provisioning failed!"
    ]);

    $this->server->delete();
    }

    }
  • Answer:
    如果沒有 forge_server_id, 代表 Forge server 尚未建立, 所以呼叫 Forge API, 建立 server 並取得 server id, 將 job 丟回 queue, 指定 120 秒後再執行一次
    如果 server 處於 provisioning 狀態, 丟回 queue, 60 秒後再執行一次
    建立完成後, 更新 Server model 中的 id_ready 為 true
    該 job 可被執行最多達 20 次, 但如果是 exception, 最多 3 次
    如果失敗, 建立 Alert 並刪除此筆 server record

以下位於 config/queue.php 的 Laravel example code 的意思是?
  • Example:

    <?php
    'connections' => [
    'database' => [
    'driver' => 'database',
    // ...
    'retry_after' => 90,
    ],
    ],
  • Answer:
    當一個 worker pick up 一個 job 時, 會在這個 job mark reserved, 這樣其他 worker 就不會嘗試 pick up 這個 job, 但當一個 worker 在執行這個 job 時 crash 了, 會導致這個 job 的 reserved mark 不會被清除, 導致這個 job 永遠不會被執行。 Laravel 為了預防這一點, 定義了一個 job 最長可以處於 reserved state 多久, 就是 retry_after

    Laravel Queue 中, 當一個 worker 執行一個 job 時, 為何其他 worker 不會執行同一個 job?

    因為當一個 worker pick up 一個 job 時, 會 mark job as reserved

    Laravel Queue 中, 當使用 job 的 retry_until = 120, 代表該 job 在 120 秒後就會立即被執行嗎?

    不是哦, 這只代表該 job 在 release 或 dispatch 後的 120 秒內不會被執行, 如果 worker 都很忙, 10 分鐘後才被執行也是有可能的

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public $tries = 0;

    public function handle()
    {
    $response = Http::timeout(...)->post(...);

    if ($response->failed()) {
    $this->release(
    now()->addMinutes(15 * $this->attempts()));
    }
    }

    public function retryUntil()
    {
    return now()->addDay();
    }

    public function failed(Exception $e)
    {
    Mail::to($this->integration->developer_email
    )->send(...);
    }
  • Answer:
    如果 request 失敗的話, 逐次的遞增再次執行該 job 的時間間隔, 最多嘗試一天(會從第一次 dispatch 時開始計算時間), 在一天內可以無限次數嘗試(依照定義的時間間隔頻率), 若超過一天則視為 failed job
    失敗的話, 採取相對應動作

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class CheckoutController
    {

    public function store()
    {
    $order = Order::create([
    'status' => Order::PENDING, // ...
    ]);
    MonitorPendingOrder::dispatch($order)->delay(900);
    // also ... delay(now()->addMinutes(15))
    }
    }

    // MonitorPendingOrder class
    public $tries = 4;

    public function handle()
    {
    if ($this->order->status == Order::CONFIRMED || $this->order->status == Order::CANCELED) {
    return;
    }
    if ($this->order->olderThan(59, 'minutes')) {
    $this->order->markAsCanceled();

    return;
    }
    SMS::send(...);
    $this->release(now()->addMinutes(15)
    );
    }
  • Answer:
    常常使用者建立訂單後, 忽然決定不買了, 但也不會手動取消這張訂單
    如上 example 邏輯
    訂單建立後每 15 分鐘提醒使用者有這張訂單存在
    若過了 59 分鐘後, 訂單並未被 confirmed, 也未 cancelled, 這時 job 就取消這張訂單
    public $tries 確保這個 job 能被執行 4 次, 這邊 4 次還是有點太少了, 可以在設大一點

以下的 Laravel example command 的意思是?
  • Example:

    php artisan queue:work --backoff=60,120
  • Answer:
    定義該 worker 在嘗試 job 的時間間隔, 比如說嘗試執行 job A 失敗了, 第一次要間隔 60 秒之後才可嘗試再次執行, 第二次之後都要間隔至少 120 秒
    Laravel 8 之前又叫做 –delay

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    namespace App\Jobs;

    class SendVerificationMessage implements ShouldQueue
    {

    public $tries = 3;
    public $backoff = [60, 120];
    }
  • Answer:
    $tries 定義該 job 最多會被執行 3 次, release() 也算一次, 如果 3 次過了還在 queue 當中, 那就會被放到 failed_job 當中
    $backoff 定義每次嘗試執行 job 的時間間隔, 第一次為 60 秒, 之後都間隔 120 秒, Laravel 8.0 之前叫做 retryAfter

以下的 Laravel example code 的意思是?
  • Example:

    php artisan queue:work --queue=payments,default 
    php artisan queue:work --queue=default,payments
  • Answer:
    讓兩個 worker 分別執行兩個 queue, 以避免一個執行完就處於 idle 狀態

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $customers = Customer::inRandomOrder()->take(1)->get();
    $regions = Region::hasCustomer($customers->first())->get();

    return view('customers', [
    'customers' => $customers,
    'regions' => $regions,
    ]);
    }

    public function scopeHasCustomer($query, Customer $customer)
    {
    $query->whereRaw('ST_Contains(regions.geometry, ?)', [$customer->location]);
    }
  • Answer:

    <?php
    public function index()
    {
    // 取得 random user
    $customers = Customer::inRandomOrder()->take(1)->get();
    // 取得該 customer 所在的 region
    $regions = Region::hasCustomer($customers->first())->get();

    return view('customers', [
    'customers' => $customers,
    'regions' => $regions,
    ]);
    }

    public function scopeHasCustomer($query, Customer $customer)
    {
    // 使用 ST_Contains function, arg1 為 regions table 的 geometry column, arg2 為帶入的 customer location, 以取得該 customer 所在的 region
    $query->whereRaw('ST_Contains(regions.geometry, ?)', [$customer->location]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $regions = Region::all();
    $customers = Customer::query()
    ->inRegion(Region::where('name', 'The Prairies')->first())
    ->get();

    return view('customers', [
    'customers' => $customers,
    'regions' => $regions,
    ]);
    }

    public function scopeInRegion($query, Region $region)
    {
    $query->whereRaw('ST_Contains(?, customers.location)', [$region->geometry]);
    }
  • Answer:

    <?php
    public function index()
    {
    $regions = Region::all();
    // 目標在於 select 出, 位於 'The Prairies' 這個 region 的 user
    $customers = Customer::query()
    ->inRegion(Region::where('name', 'The Prairies')->first())
    ->get();

    return view('customers', [
    'customers' => $customers,
    'regions' => $regions,
    ]);
    }

    public function scopeInRegion($query, Region $region)
    {
    // 使用 ST_Contains, arg1 為 geometry format, arg2 為 user location, 所以會 select 出 location 位於此 geometry 的 users
    $query->whereRaw('ST_Contains(?, customers.location)', [$region->geometry]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    protected function seedRegions()
    {
    $this->getRegions()->each(fn ($region) => Region::create([
    'name' => $region['name'],
    'color' => $region['color'],
    'geometry' => (function () use ($region) {
    return DB::raw("ST_SRID(ST_GeomFromText('".$region['geometry']."'), 4326)");
    })(),
    ]));
    }
  • Answer:
    取得 Regions 後, 在 Region table 建立資料, geometry column, 使用 ST_SRID 來設定 spatial reference ID, 4326 表示 world, 使用 ST_GeomFrom Text function, 從 text 建立 geometry data, geometry 格式如下:

    'geometry' => 'MultiPolygon (((-136.21070809681475566 57.03101434136195991, -133.72877891703720366 54.61201637520210284, -133.47202762257742847 53.45604439614419334, -131.01862636440651499 51.6878513109907729, -126.28299137770447658 48.97546429328855311, -125.05629074861900563 48.48622934024960074, -123.37102414358200519 48.11059520996249717, -122.52069777133300477 48.99007009784259736, -113.96448759224449532 48.98897113447335272, -114.59407626641420563 50.4166998690063437, -119.86365984910806048 53.47639943303917676, -119.94099441499190561 59.97238022147455894, -140.92938615814153991 60.06202748779399059, -140.74999819452486349 59.61135341849296054, -138.11897472814712273 58.90840120048653006, -136.21070809681475566 57.03101434136195991)))',
以下的 Laravel example code 中, Multipolygon 的意思是?
  • Example:

    public function getRegions()
    {
    return collect([
    [
    'name' => 'British Columbia',
    'color' => '#F56565',
    'geometry' => 'MultiPolygon (((-136.21070809681475566 57.03101434136195991, -133.72877891703720366 54.61201637520210284, -133.47202762257742847 53.45604439614419334, -131.01862636440651499 51.6878513109907729, -126.28299137770447658 48.97546429328855311, -125.05629074861900563 48.48622934024960074, -123.37102414358200519 48.11059520996249717, -122.52069777133300477 48.99007009784259736, -113.96448759224449532 48.98897113447335272, -114.59407626641420563 50.4166998690063437, -119.86365984910806048 53.47639943303917676, -119.94099441499190561 59.97238022147455894, -140.92938615814153991 60.06202748779399059, -140.74999819452486349 59.61135341849296054, -138.11897472814712273 58.90840120048653006, -136.21070809681475566 57.03101434136195991)))',
    ],
    ]);
    }
  • Answer:
    用多個 point 來代表一個區域

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    Schema::create('regions', function (Blueprint $table) {
    $table->geometry('geometry');
    });
    }
  • Answer:
    geometry column type, 可以用多個 point 連起來代表一個區塊

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $myLocation = [-79.47, 43.14];

    $stores = Store::query()
    ->selectDistanceTo($myLocation)
    ->withinDistanceTo($myLocation, 10000) // 10km
    ->orderByDistanceTo($myLocation)
    ->paginate();

    return view('stores', ['stores' => $stores]);
    }

    public function scopeOrderByDistanceTo($query, array $coordinates, string $direction = 'asc')
    {
    $direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';

    $query->orderByRaw('ST_Distance(
    location,
    ST_SRID(Point(?, ?), 4326)
    ) '.$direction, $coordinates);
    }
  • Answer:

    <?php
    public function scopeOrderByDistanceTo($query, array $coordinates, string $direction = 'asc')
    {
    // 由於 $direction 是從外部帶入, 若要直接使用於 orderByRaw, 需要消毒
    $direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';

    // ST_Distance function 取得距離, 第一個 location 為資料庫中的 location column, 第二個為 request user
    // 的 location, 由於 ST_Distance 只接受 valid geographic object, 所以要先轉成 SRID
    // 空一格之後, 再接 $direction
    $query->orderByRaw('ST_Distance(
    location,
    ST_SRID(Point(?, ?), 4326)
    ) '.$direction, $coordinates);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    Schema::create('stores', function (Blueprint $table) {
    $table->point('location', 4326);
    });
    }
  • Answer:
    point 為 geographic 的一種形式, 需使用類似 $user->location = \Illuminate\Support\Facades\DB::raw('ST_SRID(Point('.$l.', '.$a.'), 4326)'); 這種方式儲存, $l 為longitude, $a 為 Latitude

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $myLocation = [-79.47, 43.14];

    $stores = Store::query()
    ->selectDistanceTo($myLocation)
    ->withinDistanceTo($myLocation, 10000) // 10km
    ->paginate();

    return view('stores', ['stores' => $stores]);
    }


    public function scopeWithinDistanceTo($query, array $coordinates, int $distance)
    {
    $query->whereRaw('ST_Distance(
    location,
    ST_SRID(Point(?, ?), 4326)
    ) <= ?', [...$coordinates, $distance]);
    }
  • Answer:

    <?php
    public function index()
    {
    $myLocation = [-79.47, 43.14];

    $stores = Store::query()
    // select 出 distance column
    ->selectDistanceTo($myLocation)
    // 取得距離在 10km 裡的 record
    // ST_DISTANCE function 得到的結果為 meter
    // 而 10000 meter = 10km
    ->withinDistanceTo($myLocation, 10000) // 10km
    ->paginate();

    return view('stores', ['stores' => $stores]);
    }


    public function scopeWithinDistanceTo($query, array $coordinates, int $distance)
    {
    // 使用 ST_Distance function
    // arg1 為 store 的 location, 可使用 point type column
    // 在儲存時就存成 point 格式, 這樣會增進 select 效能
    // Point(?, ?) 為當前 User 的座標
    // 最後取得 distance <= 10km 的 record
    $query->whereRaw('ST_Distance(
    location,
    ST_SRID(Point(?, ?), 4326)
    ) <= ?', [...$coordinates, $distance]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $myLocation = [-79.47, 43.14];

    $stores = Store::query()
    ->selectDistanceTo($myLocation)
    ->paginate();

    return view('stores', ['stores' => $stores]);
    }

    public function scopeSelectDistanceTo($query, array $coordinates)
    {
    if (is_null($query->getQuery()->columns)) {
    $query->select('*');
    }

    $query->selectRaw('ST_Distance(
    location,
    ST_SRID(Point(?, ?), 4326)
    ) as distance', $coordinates);
    }
  • Answer:

    <?php
    public function index()
    {
    $myLocation = [-79.47, 43.14];

    // 取得當前 user 的座標與各個 store 之間的距離
    $stores = Store::query()
    ->selectDistanceTo($myLocation)
    ->paginate();

    return view('stores', ['stores' => $stores]);
    }

    public function scopeSelectDistanceTo($query, array $coordinates)
    {
    // 如果當前 $query 沒有取得任何 column 的話, 那就 select 所有 column
    // 因為如果沒這麼做的話, 下面的 query 只會取得 selectRaw 的那一個 column
    if (is_null($query->getQuery()->columns)) {
    $query->select('*');
    }

    // 使用 MySQL 的 ST_Distance 來取得兩個點的距離
    // ST_Distance function 要求的 args 必須要是 valid geographic object
    // 所以使用 ST_SRID function 來取得
    // ST_SRID args 為兩個座標, 以及使用的 spatial reference ID, 4326 代表World
    $query->selectRaw('ST_Distance(
    location,
    ST_SRID(Point(?, ?), 4326)
    ) as distance', $coordinates);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    DB::statement('CREATE FULLTEXT INDEX posts_fulltext_index ON posts(title, body) WITH PARSER ngram');
    }
  • Answer:
    加入 fulltext index 到 posts table 的 title, body column, 使用 ngram parser 取代預設 parser

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $posts = Post::query()
    ->with('author')
    ->when(request('search'), function ($query, $search) {
    $query->whereRaw('match(title, body) against(? in boolean mode)', [$search])
    ->selectRaw('*, match(title, body) against(? in boolean mode) as score', [$search]);
    }, function ($query) {
    $query->latest('published_at');
    })
    ->paginate();

    return view('posts', ['posts' => $posts]);
    }
  • Answer:

    <?php
    public function index()
    {
    $posts = Post::query()
    ->with('author')
    // 當 $request->search === true
    ->when(request('search'), function ($query, $search) {
    // 使用 match method, 須事先先加 full-text index, boolean mode 效能較佳
    $query->whereRaw('match(title, body) against(? in boolean mode)', [$search])
    // 增加 score column 代表命中程度
    ->selectRaw('*, match(title, body) against(? in boolean mode) as score', [$search]);
    // 當 $request->search === false, 照 published_at 排序, 因為 match score 排序跟 published_at 排序不同
    }, function ($query) {
    $query->latest('published_at');
    })
    ->paginate();

    return view('posts', ['posts' => $posts]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $devices = Device::query()
    ->orderByRaw('naturalsort(name)')
    ->paginate();

    return view('devices', ['devices' => $devices]);
    }
  • Answer:
    使用 natual sort, 但 MySQL 不支援, 所以必須先在 MySQL 中增加 natualsort function

MySQL 不支援 natural sort, 該怎麼做?

自己增加一個 MySQL function, 如下:

  • Example:
    <?php
    public function up()
    {
    if (config('database.default') === 'mysql') {
    // https://www.drupal.org/project/natsort
    DB::unprepared("
    drop function if exists naturalsort;
    create function naturalsort(s varchar(255)) returns varchar(255)
    no sql
    deterministic
    begin
    declare orig varchar(255) default s;
    declare ret varchar(255) default '';
    if s is null then
    return null;
    elseif not s regexp '[0-9]' then
    set ret = s;
    else
    set s = replace(replace(replace(replace(replace(s, '0', '#'), '1', '#'), '2', '#'), '3', '#'), '4', '#');
    set s = replace(replace(replace(replace(replace(s, '5', '#'), '6', '#'), '7', '#'), '8', '#'), '9', '#');
    set s = replace(s, '.#', '##');
    set s = replace(s, '#,#', '###');
    begin
    declare numpos int;
    declare numlen int;
    declare numstr varchar(255);
    lp1: loop
    set numpos = locate('#', s);
    if numpos = 0 then
    set ret = concat(ret, s);
    leave lp1;
    end if;
    set ret = concat(ret, substring(s, 1, numpos - 1));
    set s = substring(s, numpos);
    set orig = substring(orig, numpos);
    set numlen = char_length(s) - char_length(trim(leading '#' from s));
    set numstr = cast(replace(substring(orig,1,numlen), ',', '') as decimal(13,3));
    set numstr = lpad(numstr, 15, '0');
    set ret = concat(ret, '[', numstr, ']');
    set s = substring(s, numlen+1);
    set orig = substring(orig, numlen+1);
    end loop;
    end;
    end if;
    set ret = replace(replace(replace(replace(replace(replace(replace(ret, ' ', ''), ',', ''), ':', ''), '.', ''), ';', ''), '(', ''), ')', '');
    return ret;
    end;
    ");
    }

    if (config('database.default') === 'sqlite') {
    throw new \Exception('This lesson does not support SQLite.');
    }

    if (config('database.default') === 'pgsql') {
    // http://www.rhodiumtoad.org.uk/junk/naturalsort.sql
    DB::unprepared('
    create or replace function naturalsort(text)
    returns bytea language sql immutable strict as
    $f$ select string_agg(convert_to(coalesce(r[2],length(length(r[1])::text) || length(r[1])::text || r[1]),\'SQL_ASCII\'),\'\x00\')
    from regexp_matches($1, \'0*([0-9]+)|([^0-9]+)\', \'g\') r; $f$;
    ');
    }
    }

    public function down()
    {
    if (config('database.default') === 'mysql') {
    DB::unprepared('drop function if exists naturalsort');
    }

    if (config('database.default') === 'sqlite') {
    throw new \Exception('This lesson does not support SQLite.');
    }

    if (config('database.default') === 'pgsql') {
    DB::unprepared('drop function if exists naturalsort');
    }
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $devices = ['iPhone 3', 'iPhone 11'];
    sort($devices, SORT_NATUAL);
    return $devices;
    }
  • Answer:
    使用 PHP 的 natual sort 參數, 正常 sort 是 iPhone 11, iPhone 3, natual sort 是 iPhone 3, iPhone 11

什麼是 natual sorting?

跟 Alphabetical Sort 幾乎相同, 唯一的不同在於, natual sorting 會將多位數的數字當成一個單一字符
換句話說, ['3', '11', '2'] Alphabetical 排序會是 11, 2, 3, 而 natual sort 會是 2, 3, 11

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function scopeOrderByUpcomingBirthdays()
    {
    $query->orderByRaw('
    case
    when (birth_date + interval (year(?) - year(birth_date)) year) >= ?
    then (birth_date + interval (year(?) - year(birth_date)) year)
    else (birth_date + interval (year(?) - year(birth_date)) + 1 year)
    end
    ', [
    array_fill(0, 4, Carbon::now()->startOfWeek()->toDateString()),
    ]);
    }
  • Answer:

    <?php
    public function scopeOrderByUpcomingBirthdays()
    {
    // 概念為, 將 'birth_date' 加上 '今年到你生日那年總共間隔多少年' 會等於你的 birth_date, 但
    // 年份換成是今年, 如果這個值大於今天的年月日的話, 那代表你的生日還沒過, 反之, 如果這個值小於的話
    // 那代表今年你的生日已經過了, 所以會排到明年去。
    // 這樣一來便可以 order by upcoming birthday
    $query->orderByRaw('
    case
    when (birth_date + interval (year(?) - year(birth_date)) year) >= ?
    then (birth_date + interval (year(?) - year(birth_date)) year)
    else (birth_date + interval (year(?) - year(birth_date)) + 1 year)
    end
    ', [
    // 從 index 0 開始 fill, fill 4 次, fill 這個禮拜的第一天, 以 dateString 的格式
    // 這邊我覺得是看需求, 如果是 weekly email notification 的話, 那就用 startOfWeek()
    // 但若是即時查詢的話, 就要用 now(), 否則會取得生日已經過了的 model
    array_fill(0, 4, Carbon::now()->startOfWeek()->toDateString()),
    ]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function test()
    {
    Carbon::setTestNow(Carbon::parse('January 1, 2020'))
    }
  • Answer:
    使用 setTestNow() 可以設定 Carbon 的當前時間

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $users = User::query()
    ->whereBirthdayThisWeek()
    ->orderByBirthday()
    // ->orderByUpcomingBirthdays()
    ->orderBy('name')
    ->paginate();

    return view('users', ['users' => $users]);
    }

    public function scopeOrderByBirthday($query)
    {
    $query->orderByRaw('date_format(birth_date, "%m-%d")');
    }
  • Answer:

    <?php
    public function index()
    {
    $users = User::query()
    ->whereBirthdayThisWeek()
    ->orderByBirthday()
    // ->orderByUpcomingBirthdays()
    ->orderBy('name')
    ->paginate();

    return view('users', ['users' => $users]);
    }

    public function scopeOrderByBirthday($query)
    {
    // order by raw 'birth_date' 欄位, 並以 "%m-%d" 格式排序
    $query->orderByRaw('date_format(birth_date, "%m-%d")');
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    $table->rawIndex("(date_format(birth_date, '%m-%d')), name", 'users_birthday_name_index');
    });
    }
  • Answer:

    <?php
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    // 增加 compound index, 欄位為使用 date_format function
    // reformat 過的 birth_date column, 以及 name column
    $table->rawIndex("(date_format(birth_date, '%m-%d')), name", 'users_birthday_name_index');
    });
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $users = User::query()
    ->whereBirthdayThisWeek()
    ->orderByBirthday()
    // ->orderByUpcomingBirthdays()
    ->orderBy('name')
    ->paginate();

    return view('users', ['users' => $users]);
    }

    public function scopeWhereBirthdayThisWeek($query)
    {
    $dates = Carbon::now()->startOfWeek()
    ->daysUntil(Carbon::now()->endOfWeek())
    ->map(fn ($date) => $date->format('m-d'));

    $query->whereRaw('date_format(birth_date, "%m-%d") in (?,?,?,?,?,?,?)', iterator_to_array($dates));
    }
  • Answer:

    <?php
    public function index()
    {
    $users = User::query()
    // 取得生日在本週的 User model
    ->whereBirthdayThisWeek()
    ->orderByBirthday()
    ->orderBy('name')
    ->paginate();

    return view('users', ['users' => $users]);
    }

    public function scopeWhereBirthdayThisWeek($query)
    {
    // 取得 this week 的每一個 date 的日期, 並轉成 'm-d' 格式
    $dates = Carbon::now()->startOfWeek()
    ->daysUntil(Carbon::now()->endOfWeek())
    ->map(fn ($date) => $date->format('m-d'));

    // 取得 birth_date 為這個禮拜的 model, 之所以使用 in 而不是 between, 那是
    // 因為當遇到 12-25 到 1-2 的情況時, 因為前者比後者大, 會出現 query 沒有結果的
    // 問題, 而之所以使用 iterator_to_array, 那是因為 Carbon 會 return 一個
    // generator object
    $query->whereRaw('date_format(birth_date, "%m-%d") in (?,?,?,?,?,?,?)', iterator_to_array($dates));
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    Schema::create('features', function (Blueprint $table) {
    $table->rawIndex("(
    case
    when status = 'Requested' then 1
    when status = 'Approved' then 2
    when status = 'Completed' then 3
    end
    )", 'features_status_ranking_index');
    });
    }
  • Answer:

    <?php
    public function up()
    {
    Schema::create('features', function (Blueprint $table) {
    // 當使用 orderRaw("(
    // case
    // when status = 'Requested' then 1
    // when status = 'Approved' then 2
    // when status = 'Completed' then 3
    // end
    // )")
    // 時, 可加上 index
    $table->rawIndex("(
    case
    when status = 'Requested' then 1
    when status = 'Approved' then 2
    when status = 'Completed' then 3
    end
    // 命名 index
    )", 'features_status_ranking_index');
    });
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class FeaturesController extends Controller
    {
    public function index()
    {
    $features = Feature::query()
    ->withCount('comments', 'votes')
    ->when(request('sort'), function ($query, $sort) {
    switch ($sort) {
    case 'title': return $query->orderBy('title', request('direction'));
    case 'status': return $query->orderByStatus(request('direction'));
    case 'activity': return $query->orderByActivity(request('direction'));
    }
    })
    ->latest()
    ->paginate();

    return view('features', ['features' => $features]);
    }

    public function scopeOrderByActivity($query, $direction)
    {
    $query->orderBy(
    DB::raw('-(votes_count + (comments_count * 2))'),
    $direction
    );
    }
    }
  • Answer:

    <?php
    class FeaturesController extends Controller
    {
    public function index()
    {
    $features = Feature::query()
    // 取得 comments 以及 votes column counts
    ->withCount('comments', 'votes')
    // 'sort' === 'title' || 'status' || 'activity'
    // request['direction'] 為 desc, 或 asc
    ->when(request('sort'), function ($query, $sort) {
    switch ($sort) {
    case 'title': return $query->orderBy('title', request('direction'));
    case 'status': return $query->orderByStatus(request('direction'));
    case 'activity': return $query->orderByActivity(request('direction'));
    }
    })
    ->latest()
    ->paginate();

    return view('features', ['features' => $features]);
    }
    public function scopeOrderByActivity($query, $direction)
    {
    // 我可以使用 query 中實際或虛擬的 column 來排序
    // - 表示顛倒排序結果
    // 若 votes_count 以及 comments_count 取自虛擬 column, 那是無法做 index 的
    // 可以考慮實際增加兩個欄位, 或是用 virtual column
    $query->orderBy(
    DB::raw('-(votes_count + (comments_count * 2))'),
    $direction
    );
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    class FeaturesController extends Controller
    {
    public function index()
    {
    $features = Feature::query()
    ->withCount('comments', 'votes')
    ->when(request('sort'), function ($query, $sort) {
    switch ($sort) {
    case 'title': return $query->orderBy('title', request('direction'));
    case 'status': return $query->orderByStatus(request('direction'));
    case 'activity': return $query->orderByActivity(request('direction'));
    }
    })
    ->latest()
    ->paginate();

    return view('features', ['features' => $features]);
    }

    public function scopeOrderByStatus($query, $direction)
    {
    $query->orderBy(DB::raw("
    case
    when status = 'Requested' then 1
    when status = 'Approved' then 2
    when status = 'Completed' then 3
    end
    "), $direction);
    }

    }
  • Answer:

    <?php
    class FeaturesController extends Controller
    {
    public function index()
    {
    $features = Feature::query()
    // 取得 comments 以及 votes column counts
    ->withCount('comments', 'votes')
    // 'sort' === 'title' || 'status' || 'activity'
    // request['direction'] 為 desc, 或 asc
    ->when(request('sort'), function ($query, $sort) {
    switch ($sort) {
    case 'title': return $query->orderBy('title', request('direction'));
    case 'status': return $query->orderByStatus(request('direction'));
    case 'activity': return $query->orderByActivity(request('direction'));
    }
    })
    ->latest()
    ->paginate();

    return view('features', ['features' => $features]);
    }
    public function scopeOrderByStatus($query, $direction)
    {
    // 預設 'Requested', 'Approved', 'Completed' 這三個 value 會依照 Alph 方式排序
    // 若想變更排序規則, 可使用以下的方式, 將 value 依照自己想要的順序 return 成數字
    // DB 會依照數字順序排列
    $query->orderBy(DB::raw("
    case
    when status = 'Requested' then 1
    when status = 'Approved' then 2
    when status = 'Completed' then 3
    end
    "), $direction);
    }
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $users = User::query()
    ->when(request('sort') === 'town', function ($query) {
    if (config('database.default') === 'mysql' || config('database.default') === 'sqlite') {
    $query->orderByRaw('town is null')
    ->orderBy('town', request('direction'));
    }

    if (config('database.default') === 'pgsql') {
    $query->orderByNullsLast('town', request('direction'));
    }
    })
    ->orderBy('name')
    ->paginate();

    return view('users', ['users' => $users]);
    }
  • Answer:

    <?php
    public function index()
    {
    $users = User::query()
    ->when(request('sort') === 'town', function ($query) {
    if (config('database.default') === 'mysql' || config('database.default') === 'sqlite') {
    // 如果不加這一行, 那 order by 時會將 null 的顯示在最前面, 加了這一行後, 值為 null 的 row 會被排到最後
    $query->orderByRaw('town is null')
    ->orderBy('town', request('direction'));
    }

    if (config('database.default') === 'pgsql') {
    $query->orderByNullsLast('town', request('direction'));
    }
    })
    ->orderBy('name')
    ->paginate();

    return view('users', ['users' => $users]);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function boot()
    {
    Builder::macro('orderByNullsLast', function ($column, $direction = 'asc') {
    $column = $this->getGrammar()->wrap($column);
    $direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';

    return $this->orderByRaw("$column $direction nulls last");
    });
    }
  • Answer:

    <?php
    public function boot()
    {
    // 建立一個 new query builder method, 名為 orderByNullsLast
    Builder::macro('orderByNullsLast', function ($column, $direction = 'asc') {
    // wrap $column, 因為 $column 為外部帶入參數, 並且又使用 raw method, 為避免 SQL injection, 需使用 wrap
    $column = $this->getGrammar()->wrap($column);
    $direction = strtolower($direction) === 'asc' ? 'asc' : 'desc';

    // 此為 PostgreSQL 語法, 效果為將值為 null 的 column 排在最後
    return $this->orderByRaw("$column $direction nulls last");
    });
    }
Laravel 中, updateOrCreate() 與 MySQL 中的 INSERT … ON DUPLICATE KEY UPDATE 的差異是?

基本上兩者的效果相同, 但實作原理不同, 效能也不同
INSERT … ON DUPLICATE KEY UPDATE 為 MySQL 內建語法, 會透過 unique key 去判斷該 row 是否為 duplicate key
updateOrCreate() 為複數 query 實作而成的 method, 會先 select 比較, 再決定 insert or update
前者的效能較佳

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $books = Book::query()
    ->select('book.*')
    ->join('checkouts', 'checkouts.book_id', '=', 'books.id')
    ->groupBy('books.id')
    ->orderByRaw('max(checkouts.borrowed_date) desc')
    ->withLastCheckout()
    ->with('lastCheckout.user')
    ->paginate();

    return view('books', ['books' => $books]);
    }

    public function scopeWithLastCheckout($query)
    {
    $query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
    ->whereColumn('book_id', 'books.id')
    ->latest('borrowed_date')
    ->limit(1),
    ])->with('lastCheckout');
    }
  • Answer:

    <?php
    public function index()
    {
    $books = Book::query()
    ->select('books.*')
    ->join('checkouts', 'checkouts.book_id', '=', 'books.id')
    // 如果不使用 group by, 則或出現多個重複的 book model, 因為每一個 book model 都會對應到多個 checkouts model
    ->groupBy('books.id')
    // 取得最大的 borrowed_date, 即最後借出日期, 在 query 過程中, 每個 row 都會新增一個 column 名為 borrowed_date
    // 其值為 max(checkouts.borrowed_date), 最後 order by 這個 column
    // 這邊 INDEX 是不吃的, 若要優化, 可在 book table 中新增一個 last_checkout_id column, 每次 book 被 checkout 時
    // 都更新這個欄位
    ->orderByRaw('max(checkouts.borrowed_date) desc')
    // dynanmic relation 的概念, 每個 book model 對應多個 checkout model, 但我們只取其中一筆 checkout model
    ->withLastCheckout()
    // 已事先於 checkout model 中定義 user belongsTo relation, 所以直接 eager load 對應的 user model
    ->with('lastCheckout.user')
    ->paginate();

    return view('books', ['books' => $books]);
    }

    public function scopeWithLastCheckout($query)
    {
    // 新增 'last_checkout_id' column
    $query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
    // 將 checkout table 與 book table 對應起來
    ->whereColumn('book_id', 'books.id')
    // 以 borrowed_date 排序
    ->latest('borrowed_date')
    // 只取第一筆
    ->limit(1),
    // 取得 last_checkout_id 之後, 便可利用 dynanmic relation 的概念, 取得事先定義的 lastCheckout belongsTo relation
    ])->with('lastCheckout');
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $books = Book::query()
    ->orderBy(User::select('name')
    ->join('checkouts', 'checkouts.user_id', '=', 'users.id')
    ->whereColumn('checkouts.book_id', 'books.id')
    ->latest('checkouts.borrowed_date')
    ->take(1)
    )
    ->withLastCheckout()
    ->with('lastCheckout.user')
    ->paginate();

    return view('books', ['books' => $books]);
    }

    public function scopeWithLastCheckout($query)
    {
    $query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
    ->whereColumn('book_id', 'books.id')
    ->latest('borrowed_date')
    ->limit(1),
    ])->with('lastCheckout');
    }
  • Answer:

    <?php
    public function index()
    {
    $books = Book::query()
    // 以 name 排序
    ->orderBy(User::select('name')
    // 透過 join, 取得 user 的所有借書紀錄
    ->join('checkouts', 'checkouts.user_id', '=', 'users.id')
    // 透過 where, 取得每一個 book row 對應到的 data, 如果不使用 where 來把 book.id
    // 以及 checkouts.book_id 做關聯, 那 user name 永遠都會是一樣的
    // 但加上 where 之後, 就可以取得每一個 book 的所有借閱紀錄, 並取得最後借出的那個 user name
    // 最後再 order by 這個 user name
    ->whereColumn('checkouts.book_id', 'books.id')
    // 因為要取得最後借出的 user, 所以要先加以排序
    ->latest('checkouts.borrowed_date')
    // 因為要取得最後借出的 user, 因此只取最後的那一個 user
    ->take(1)
    )
    // 利用 dynanmic relation 的概念, 先新增一個 last_checkout_id 虛擬欄位, 再透過
    // book model 中的 belongs to relation, 經由這個虛擬的 last_checkout_id 取得
    // 最後 checkout 的那一筆 checkout 資料
    ->withLastCheckout()
    // checkout model 中已有先定義與 user 的 belongsTo relation, 因此可以取得與
    // 該 checkout model 相關的 user model
    ->with('lastCheckout.user')
    ->paginate();

    return view('books', ['books' => $books]);
    }

    public function scopeWithLastCheckout($query)
    {
    $query->addSelect(['last_checkout_id' => Checkout::select('checkouts.id')
    // 透過 where, 可取得與每一行 book 相關的 checkout records
    ->whereColumn('book_id', 'books.id')
    // 因為每一個 book model 都會有多筆的 checkout records, 因此須加以排序
    ->latest('borrowed_date')
    // 排序後只取得最新的那一筆, 即最後借出那一筆紀錄, 至此, 只為了取得這一筆 records 的 id
    ->limit(1),
    ])->with('lastCheckout');
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $users = User::query()
    ->orderByLastLogin()
    ->withLastLogin()
    ->paginate();

    return view('users', ['users' => $users]);
    }

    public function scopeOrderByLastLogin($query)
    {
    $query->orderByDesc(Login::select('created_at')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->take(1)
    );
    }

    public function scopeWithLastLogin($query)
    {
    $query->addSelect(['last_login_id' => Login::select('id')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->take(1),
    ])->with('lastLogin');
    }
  • Answer:

    <?php
    public function index()
    {
    $users = User::query()
    ->orderByLastLogin()
    ->withLastLogin()
    ->paginate();

    return view('users', ['users' => $users]);
    }

    public function scopeOrderByLastLogin($query)
    {
    // 每個 User 都有很多個 Login, 所以在 orderByDesc 當中使用 subquery
    // 取得與該 user 相關的 login records 之後, 再排序, 然後取第一筆
    // 所以會 orderByDesc 每個 user 的最新一筆 login
    $query->orderByDesc(Login::select('created_at')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->take(1)
    );
    }

    public function scopeWithLastLogin($query)
    {
    // 每個 User 都有 many Login, 利用 addSelect 新增一個 column 'last_login_id'
    // 正常來說 User 跟 Login 的 relationship 應該是 User hasMany Login
    // 但這邊為 dynamic model 的概念, 目的為從 hasMany 眾多 records 當中
    // 只 load 想要的那筆資料, 大幅增進效能
    $query->addSelect(['last_login_id' => Login::select('id')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->take(1),
    ])->with('lastLogin');
    }

    public function lastLogin()
    {
    // 正常來說 User 跟 Login 的 relationship 應該是 User hasMany Login
    // 但這邊為 dynamic model 的概念, 目的為從 hasMany 眾多 records 當中
    // 只 load 想要的那筆資料, 大幅增進效能

    return $this->belongsTo(Login::class);
    }
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $users = User::query()
    ->select('users.*')
    ->join('companies', 'companies.user_id', '=', 'users.id')
    ->orderBy('companies.name')
    ->with('company')
    ->paginate();

    return view('users', ['users' => $users]);
    }
  • Answer:
    當需要 orderBy hasOne relationship 的某一個 column, 務必使用 join approach

以下的 Laravel example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    $users = User::query()
    ->select('users.*')
    ->join('companies', 'companies.user_id', '=', 'users.id')
    ->orderBy('companies.name')
    ->with('company')
    ->paginate();

    return view('users', ['users' => $users]);
    }
  • Answer:
    當需要 orderBy belognsTo relationship 的某一個 column, 務必使用 join approach

Laravel 中, 當使用 pagination 時, 務必要使用 orderBy, 為什麼?

因為 pagination 會 offset 並 limit record 數量, 若無使用 orderBy 的話, 很可能每一次的排序都有所不同, 這將導致 pagination 的每一頁的結果是沒有順序性的

Laravel 中, 以下的 migration example code 中, 如果使用 orderBy(‘first_name’)->orderBy(‘last_name’), index 會生效嗎??
  • Example:

    <?php
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    $table->index(['last_name', 'first_name']);
    });
    }
  • Answer:
    不會, 因為 orderBy 順序必須符合 index 的順序

Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function index()
    {
    Auth::login(User::where('name', 'Sarah Seller')->first());

    $customers = Customer::query()
    ->visibleTo(Auth::user())
    ->with('salesRep')
    ->orderBy('name')
    ->paginate();

    return view('customers', ['customers' => $customers]);
    }

    public function scopeVisibleTo($query, User $user)
    {
    if (! $user->is_owner) {
    $query->where('sales_rep_id', $user->id);
    }
    }
  • Answer:
    情境為, 當 user 為 owner 時, 可以看到所有的 customer, 而當 user 為 salesRep 時, 只可看到自己底下的 customer
    主要概念為, 若有這樣的情境限制, 務必將 filter 做在 database layer

Laravel 中, 以下的 migration example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    $table->index(['last_name', 'first_name']);
    });
    }
  • Answer:
    增加一個 compound index, 若是使用 orderBy 時, 順序必須跟 compound index 的順序相同

Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    $term = preg_replace('/[^A-Za-z0-9]/', '', $term).'%';
    $query->whereIn('id', function ($query) use ($term) {
    $query->select('id')
    ->from(function ($query) use ($term) {
    $query->select('users.id')
    ->from('users')
    ->where('users.first_name_normalized', 'like', $term)
    ->orWhere('users.last_name_normalized', 'like', $term)
    ->union(
    $query->newQuery()
    ->select('users.id')
    ->from('users')
    ->join('companies', 'users.company_id', '=', 'companies.id')
    ->where('companies.name_normalized', 'like', $term)
    );
    }, 'matches');
    });
    });
    }
  • Answer:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    // 將 $term 中 "非 A-Za-z0-9" 的內容都替換成 '', 也就是刪去
    $term = preg_replace('/[^A-Za-z0-9]/', '', $term).'%';
    // 之所以不在
    $query->whereIn('id', function ($query) use ($term) {
    $query->select('id')
    ->from(function ($query) use ($term) {
    $query->select('users.id')
    ->from('users')
    // 從 first_name_normalized virtual column 中 query
    // 因為若使用 whereRaw 的 regular expression 語法, 將無法
    // 使用 index
    ->where('users.first_name_normalized', 'like', $term)
    ->orWhere('users.last_name_normalized', 'like', $term)
    ->union(
    $query->newQuery()
    ->select('users.id')
    ->from('users')
    ->join('companies', 'users.company_id', '=', 'companies.id')
    ->where('companies.name_normalized', 'like', $term)
    );
    }, 'matches');
    });
    });
    }
Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    $table->foreignId('company_id')->constrained('companies');
    $table->string('first_name_normalized')->virtualAs("regexp_replace(first_name, '[^A-Za-z0-9]', '')")->index();
    $table->string('last_name');
    $table->string('last_name_normalized')->virtualAs("regexp_replace(last_name, '[^A-Za-z0-9]', '')")->index();
    });
    }
  • Answer:

    <?php
    public function up()
    {
    Schema::create('users', function (Blueprint $table) {
    // foreignId 為 unsignedBigInteger 的 alias, 而 constrained 會自動為
    // users table 中的 company_id 與 companies table 中的 id column 建立 foreign key index
    $table->foreignId('company_id')->constrained('companies');
    // virtualAs 作用為建立一個 virtual column, 此行 code 會將 first_name column 中
    // 只要不是 a-zA-Z0-9 的都替換成 '', 然後再將內容存到 first_name_normalized 這個
    // 虛擬 column, 並建立 index
    $table->string('first_name_normalized')->virtualAs("regexp_replace(first_name, '[^A-Za-z0-9]', '')")->index();
    $table->string('last_name');
    // 同上
    $table->string('last_name_normalized')->virtualAs("regexp_replace(last_name, '[^A-Za-z0-9]', '')")->index();
    });
    }
Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    $term = $term.'%';
    $query->whereIn('id', function ($query) use ($term) {
    $query->select('id')
    ->from(function ($query) use ($term) {
    $query->select('users.id')
    ->from('users')
    ->where('users.first_name', 'like', $term)
    ->orWhere('users.last_name', 'like', $term)
    ->union(
    $query->newQuery()
    ->select('users.id')
    ->from('users')
    ->join('companies', 'users.company_id', '=', 'companies.id')
    ->where('companies.name', 'like', $term)
    );
    }, 'matches');
    });
    });
    }
  • Answer:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    $term = $term.'%';
    // 最終目的, 是要找到符合 query 條件的 users, 又要讓 index 生效
    // 這邊是 select * from users whereIn('id', nextQuery)
    $query->whereIn('id', function ($query) use ($term) {
    // 這邊是 select id from derived table, 之所以使用 derived table, 是為了
    // 不要讓內外 query 相互依賴, 如果單純在 whereIn 當中使用 subquery 的話, 就會
    // 產生互相依賴, 造成 index 無法生效
    $query->select('id')
    ->from(function ($query) use ($term) {
    $query->select('users.id')
    ->from('users')
    ->where('users.first_name', 'like', $term)
    ->orWhere('users.last_name', 'like', $term)
    // 使用 union 來串接兩句語法
    ->union(
    $query->newQuery()
    ->select('users.id')
    ->from('users')
    ->join('companies', 'users.company_id', '=', 'companies.id')
    ->where('companies.name', 'like', $term)
    );
    // derived table 必須要有一個 alias
    }, 'matches');
    });
    });
    }
Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    $term = $term.'%';
    $query->where(function ($query) use ($term) {
    $query->where('first_name', 'like', $term)
    ->orWhere('last_name', 'like', $term)
    ->orWhereIn('company_id', Company::query()
    ->where('name', 'like', $term)
    ->pluck('id')
    );
    });
    });

    }
  • Answer:
    雖然沒有使用 sub query 造成 query 的數量增加了, 但 users table 因為沒有使用 sub query 的關係而使 index 生效, 雖然 query 數量較多, 但速度卻變快了

Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    $term = $term.'%';
    $query->where(function ($query) use ($term) {
    $query->where('first_name', 'like', $term)
    ->orWhere('last_name', 'like', $term)
    ->orWhereIn('company_id', function ($query) use ($term) {
    $query->select('id')
    ->from('companies')
    ->where('name', 'like', $term);
    });
    });
    });
    }
  • Answer:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    // str_getcsv 會將 ' ' (空白) 分出來, " (double quote) 也分出來, return 一個 array
    // 再把這個 array 使用 each 迭代
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    // prefix 不用 %, 因為 prefix % 不適用於 index
    $term = $term.'%';
    $query->where(function ($query) use ($term) {
    $query->where('first_name', 'like', $term)
    ->orWhere('last_name', 'like', $term)
    // 這邊不使用 orWhereHas, 因為 SQL 語法會關聯兩張表,
    // 這個 dependency 會造成 company_name 不適用 index
    // 因此使用 whereIn 來避開此 dependency
    ->orWhereIn('company_id', function ($query) use ($term) {
    $query->select('id')
    ->from('companies')
    ->where('name', 'like', $term);
    });
    });
    });
    }
Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function scopeSearch($query, string $terms = null)
    {
    collect(str_getcsv($terms, ' ', '"'))->filter()->each(function ($term) use ($query) {
    $term = $term.'%';
    $query->where(function ($query) use ($term) {
    $query->where('first_name', 'like', $term)
    ->orWhere('last_name', 'like', $term)
    ->orWhereHas('company', function ($query) use ($term) {
    $query->where('name', 'like', $term);
    });
    });
    });
    }
  • Answer:
    str_getcsv 會把空白隔開的當成一個 value, " 內的也當成一個 value, 再把各個 value 變成 $term 下去 query

Laravel 中, 以下的 example code 的意思是?
  • Example:

    <?php
    public function show(Feature $feature)
    {
    $feature->load('comments.user');
    $feature->comments->each->setRelation('feature', $feature);

    return view('feature', ['feature' => $feature]);
    }
  • Answer:

    <?php
    public function show(Feature $feature)
    {
    $feature->load('comments.user');

    // 這裡手動的為每個 comments 建立 relation 名為 feature, 代入當前已載入 memory 的 $feature
    // 如此一來, 在上一行 eager load comments 之後, 每一個 comments 都還會有在上一行已經完成
    // eager load 的 feature relation, 那當我們執行 $feature->comments->feature->comments
    // 時就不會重複 SQL 語法, 也不會重複的加載 model
    // 當執行 $feature->comments->feature->comments 時, 如果不使用 eager loading, 會有
    // n+1 issue, 但如果使用 with('comments.feature.comments') 的話, 則會 n*n 的加載不必要
    // 的 model
    // 這樣的 relation 又稱為 circular relation
    $feature->comments->each->setRelation('feature', $feature);

    return view('feature', ['feature' => $feature]);
    }
Laravel 中, 動態 model 的技術概念是?

假設 User model 跟 Login model 的 relation 為 hasMany, 但我在取得 User model 時, 我只想要取得最近 login 的 Login model
所以先用 subquery 來取得一個虛擬的 column 為 login_id, 這時再用這個 login_id 透過 belongsTo 的 relation 取得單筆 model

Laravel 中, 假設今天我想要取得 hasMany relation 中的一筆 record, 這時如果使用 with() 的話會 load 所有的 model, 不使用 with() 的話又會執行很多次 query, 有什麼好的解法?
  • Example:

    <?php
    $users = User::query()
    ->with('login')
    ->orderBy('name')
    ->paginate();
  • Answer:

    <?php
    // 使用 subquery, 從 hasMany relation 中只 query 出我們要的那筆資料, 再將這筆資料變成主 query 中的一個 column
    $users = User::query()
    ->addSelect(['last_login_at' => Login::select('created_at')
    ->whereColumn('user_id', 'users.id')
    ->latest()
    ->take(1)
    ])
    ->orderBy('name')
    ->paginate();
Laravel 中, 如果使用 composer 安裝套件時, 超過容許的 memory 限制, 可以使用哪個 flag 讓容許 memory 無上限?
COMPOSER_MEMORY_LIMIT=-1
以下的 Laravel 程式碼的意思是?
  • Example:

    <?php
    $ip = data_get($this->pendingWalletSettlement->settleable, 'depositDetail.client_ipv4')
  • Answer:

    <?php
    // 從 settleable 得到的 model 裏頭, 取得他的 relation depositDetail 的 attribute client_ipv4
    $ip = data_get($this->pendingWalletSettlement->settleable, 'depositDetail.client_ipv4')
解釋以下的 Laravel example
  • Example:

    <?php
    public function scopeOf(Builder $query, ...$models)
    {
    foreach ($models as $model) {
    /** @var $model Model */
    $query->where($model->getForeignKey(), $model->getKey());
    }
    }
  • Answer:

    <?php
    // 可帶入多個 model
    public function scopeOf(Builder $query, ...$models)
    {
    // loop 每個 model
    foreach ($models as $model) {
    /** @var $model Model */
    // 取得該 model 的 `foreign key name`, 以及 `primary key value`
    // 所以可以在目標 model 使用其他 model 為條件來 query
    $query->where($model->getForeignKey(), $model->getKey());
    }
    }
安裝完新的 PHP Extension, 記得要做些什麼事? 重啟 PHP, 重啟 Web server, 若使用 valet, 記得重啟 valet
在 Laravel 中, 如何確認 model 是否已經被改過?
model->isDirty()
// true or false
在 Laravel 中, 如何得到與該 model 相關的 table 的名字?
model->getTable()
// string
在 Laravel 中, 下面的代碼是什麼意思? 若 $request 中有指定的值, 如 ‘row’, 則使用該值, 若該值不存在, 則使用第二個自己指定的值
$row = request('row', (new Deposit)->getPerPage());
在 Laravel 中, 下面的代碼是什麼意思?
foreach (
['status', 'system_order_number', 'order_number', 'user_card_number', 'system_card_number'] as
$availableFilterKey
) {
$deposits->when($request->{$availableFilterKey},
function (Builder $builder, $filterValue) use ($availableFilterKey) {
$builder->where($availableFilterKey, $filterValue);
});
}
  • foreach 依序 loop array 當中的每一個 value
  • $availableFilterKey 這裡表示 array 當中的 value
  • when 表示條件句, [boolean, executeIfTrue, executeIfFalse]
  • $builder 代表 $deposit
  • $filterValue 代表 $request->availableFilterKey
  • 參考文件
在 Laravel 中, 如何取得 query builder?
$query = yourModel::query();
在 Laravel 中, ** 代表什麼意思?
power operator, 如果是 2 ** 16, 代表 2 * 2 * .. 16 次
在 Laravel 中, 如何取得 parent 的 method?
# 如果沒複寫, 直接呼叫即可
$this->method

# 如果有複寫, 也是直接呼叫
$this->method

# 除非你有複寫, 但你要沒複寫的版本
parent::method

參考出處: https://stackoverflow.com/questions/11237511/multiple-ways-of-calling-parent-method-in-php
Laravel 的時區設定檔位置在?
config 中的 app.php
資料庫欄位 int, 什麼是 signed 跟 unsigned?
signed 的數值範圍橫跨正負數, unsigned 只有正數, 且為 signed 的兩倍, 但最小的負數為 0
可參考文件如下: https://dev.mysql.com/doc/refman/8.0/en/integer-types.html
Laravel 當中, 以下的語法代表什麼意思? 列出 $users query builder 的 query 語法以及帶入的變數
<?php
dd($users->toSql(), $users->getBindings());
Laravel 當中, 以下代碼代表什麼意思?$user->address 可獲得時, 取值, 不可獲得時, 回傳 null
<?php
return optional($user->address)->street;
Laravel 當中, 哪裡可以設定 queue 的名字?
config 資料夾中的 queue
Laravel 當中, 以下的 agent_id 驗證 exists 邏輯代表什麼意思? 在 users table 當中, 帶入的 agent_id 必須要跟 table 裡頭的 id 相同, 且 role_id 必須得跟 Role::AGENT 相同
<?php
request()->validate([
'q' => 'nullable | string | max:255',
'row' => 'nullable | int | digits_between:1,3',
'agent_id' => 'nullable | exists:users,id,role_id,'.Role::AGENT,
]);
以下的 Laravel 程式碼代表什麼意思?
<?php
$users = User::merchant()
->with('owner')
->when($q, function ($query, $q) {
return $query->where('username', 'LIKE', '%'.$q.'%')
->orWhere('name', 'LIKE', '%'.$q.'%')
->orWhere('email', 'LIKE', '%'.$q.'%');
});

public function scopeMerchant(Builder $query)
{
return $query->where('role_id', Role::MERCHANT);
}
取得 User model 的 merchant scope, eager load owner relation, 當 $q 不為 null, 執行 closure, 接上 closure 中的 query builder, 在 username, name, email 三個欄位中模糊搜尋 $q
以下的 Laravel 程式碼的 appends 邏輯是什麼? 在 Laravel 的分頁模式中, 若未將全部來自於前端的 query 帶入, 那分頁的 url 將會缺少必要的 query, 等於只有第一頁會有 帶入的 query 結果
<?php
return new UserCollection($users->paginate($row)->appends(request()->query->all()));
以下的 Laravel 程式碼中的 latest 代表什麼意思? 以 id 排序
<?php
$transactions = auth()->user()->transactions()
->whereBetween('created_at', [$startedAt, $endedAt])
->latest('id')
->with('wallet')
->paginate()
->appends($request->query->all());
以下的 Laravel 程式碼中的 fill 代表什麼意思? 將值注入 userBankCard model, 帶入參數可以是一個 array
<?php
$userBankCard->fill(
$request->only(
'card_holder_name', 'card_number', 'bank_name'
)
);
以下的 Laravel 程式碼中, 為什麼要使用 collect function? 這樣如果前端帶錯, 帶成 string 的話, 會先將 string 轉成 collection, 再轉成 array
<?php
$userBankCards = UserBankCard::when(request()->q, function ($query, $q) {
$query->where(function ($query) use ($q) {
$query->where('card_holder_name', 'LIKE', '%' . $q . '%')
->orWhere('card_number', 'LIKE', '%' . $q . '%')
->orWhere('bank_name', 'LIKE', "%$q%");
});
})
->when(request()->status, function ($query, $status) {
$query->whereIn('status', collect($status)->toArray());
})
->where('user_id', auth()->user()->getKey())
->paginate($row)
->appends(request()->query->all());
以下的 Laravel 程式碼代表什麼意思?
<?php
$depositStats = Deposit::whereBetween('created_at', [$startDate, $endDate])->where('status', Deposit::STATUS_SUCCESS)->groupBy('system_bank_card_type')->get([
'system_bank_card_type',
DB::raw('SUM(amount) AS total_amount, SUM(fee) AS total_fee, COUNT(id) AS total_count')
])->keyBy('system_bank_card_type');
取得 Deposit model, 以帶入日期過濾, 以 SUCCESS status 過濾, 以 system_bank_card_type 分類, 在取得四個值, 分別是 system_bank_card_type, total_amount, total_fee, total_count, 若照預設, 會是一個 collection 裡面有兩個 model, index 為 0 跟 1, 使用 keyBy 來將 0 跟 1 依照 system_bank_card_type 做區分, 所以會變成一個 collection 裡頭有兩個 model, 以 system_bank_card_type 做區分
以下的 Laravel 程式碼為什麼要使用 first? 因為該 query 撈出來後, 只會有一筆 model, 如果是用 get 的話, 會是一個 collection 裡有一個 model, 所以直接使用 first() 即可
<?php
$withdrawStat = Withdraw::whereBetween('created_at', [$startDate, $endDate])
->where('status', Withdraw::STATUS_SUCCESS)
->first([
DB::raw('SUM(amount) AS total_amount, SUM(fee) AS total_fee, COUNT(id) AS total_count')
]);
以下的 Laravel 程式碼中, keyBy 的用途是? 如果不使用 keyBy 的話, 正常來說一個 collection 裡頭有多個 model 會以默認 index, 0, 1, 2 …, keyBy 可以使用指定的 key 來給 model 分組, 在這個例子中, 就是以 model 下的 slug 欄位的值做分組
<?php
$wallets = auth()->user()->wallets()->get()->keyBy('slug');
以下的 Laravel 程式碼中, data_get 的用途是? $depositStats 結構像是這樣 $depositStats = ['BankCard::TYPE_FEE' => ['total_count', 'total_amount', 'total_count', 'total_amount']], data_get 可以取得一個 collection 裡頭的巢狀 array 值
<?php
return response()->json([
'data' => [
'fee_wallet_deposit_success_count' => data_get($depositStats, [BankCard::TYPE_FEE, 'total_count'], 0),
'fee_wallet_deposit_success_amount' => data_get($depositStats, [BankCard::TYPE_FEE, 'total_amount'], 0) / 100, // todo remove hard code
'withdraw_wallet_deposit_success_count' => data_get($depositStats, [BankCard::TYPE_WITHDRAW, 'total_count'], 0),
'withdraw_wallet_deposit_success_amount' => data_get($depositStats, [BankCard::TYPE_WITHDRAW, 'total_amount'], 0) / 100, // todo remove hard code
'withdraw_success_count' => $withdrawStat->total_count ?? 0,
'withdraw_success_amount' => ($withdrawStat->total_amount ?? 0) / 100, // todo remove hard code
'fee_wallet_balance' => data_get($wallets, [User::SLUG_FEE_WALLET, 'balanceFloat'], 0),
'withdraw_wallet_balance' => data_get($wallets, [User::SLUG_WITHDRAW_WALLET, 'balanceFloat'], 0),
],
]);
以下的 Laravel 程式碼代表什麼意思? 定義一個 unique rule, 並且將範圍限定在特定的 user 上, 代表不同 user 之間的訂單不需要 unique
<?php
$orderNumberUniqueRule = Rule::unique($withdrawTable, 'order_number')->where(function ($query) use ($user) {
$query->where('user_id', $user->getKey());
});
以下的 Laravel function 的作用是什麼?
  1. 將 request 裡的參數除了 sign 之外都調出來
  2. 排列這些 key
  3. 首先使用 http_build_query function 針對剛剛的參數來產生一組 url 加密的字串, 然後將這字串與 user 的 secret_key 欄位內的值相串, 然後使用 url 解密這一整個字串, 最後再使用 md5 處理取得 hash 值, 我們比對這個值跟帶進來的 sign 有沒有一樣, 如果不一樣就是不合法
  4. 唯有知道 secret_key 的雙方可以對內容加解密, 而經由這樣的加解密驗證, 確保 request 的內容再傳送過程中未被串改
    <?php
    private function signValid(Request $request, $secretKey)
    {
    $allParametersExceptSign = $request->except('sign');

    ksort($allParametersExceptSign);

    return strcasecmp(
    md5(urldecode(http_build_query($allParametersExceptSign)) . $secretKey),
    $request->sign
    ) === 0;
    }
以下的 Laravel function 的作用是什麼?
  1. 宣告 lock-key 以及持有時間
  2. 嘗試取得 lock-key, 如果不可得, 持續嘗試五秒
  3. 用 transaction 實作, 若有任何錯誤皆返回
  4. 如果無法取得 lock-key, 返回錯誤
  5. 回返錯誤訊息
  6. 如果 lock-key 還被持有中, 釋放 lock-key
    <?php
    public function lock(User $user, $action)
    {
    $lock = Cache::lock($user->lockKey(), 10);

    try
    {
    $lock->block(5);

    return DB::transaction($action);

    } catch (LockTimeoutException $e)
    {
    abort(Response::HTTP_CONFLICT, '请稍候再试');
    } finally
    {
    optional($lock)->release();
    }
    }
以下的 Laravel 程式碼中, balance 是扣款前還是扣款後? 扣款前, 因為 $transaction 還沒被執行完畢
<?php
$transaction = $user->withdrawFloat($request->input('amount'), [
'before_balance' => $user->balance,
]);
以下的 Laravel 程式碼是什麼意思呢? 將資料存入 mysql 中的 json 欄位
<?php
$deposit->user_bank_meta = (object)[
'subbranch' => $userBankCard->subbranch,
'province' => $userBankCard->province,
'city' => $userBankCard->city,
];
下面的 Laravel 程式碼是什麼意思? 將檔案存在 $deposit->getTable(), 檔名為 $deposit->system_order_number, 使用 filesystem.cloud, 可設為 s3
<?php
$request->file('payment_instrument')
->storeAs($deposit->getTable(), $deposit->system_order_number, config('filesystems.cloud'));
下面的 Laravel Requests 代表什麼意思? 使用 captcha 的 extension captcha_api 來驗證, 因為該驗證器一定需要一個 key, 如果在 validation 期間 key 為 null, 那直接就回 500 了, 所以這邊處理, 當沒有 captcha_key 時, 給一個隨機 10 碼, 這樣會驗不過(key 沒帶原本就應該驗不過), 但是不會 500
<?php
public function rules()
{
return [
'username' => 'required_without:email|string',
'email' => 'required_without:username|email',
'password' => 'required|string',
'captcha_key' => 'required',
'captcha' => 'required|captcha_api:'.($this->request->get('captcha_key') ?? Str::random(10)),
];
}
以下的 Laravel 程式碼中, where 內為什麼只有一個參數?

Laravel 中, 如果 request 中的 parameter 與資料庫中的欄位名稱相同, 就可以直接用這種方式 query

<?php
$bankCard = BankCard::where($request->only('card_number'))
->withTrashed()
->first() ?? BankCard::create($data);
以下的 Laravel 程式碼是什麼意思?
1. query 出 card_number 的 model
2. withTrashed 代表強制顯示已被 soft deleted 的 model
3. 取出第一筆
4. 若無結果, 則根據輸入的資料建立一張卡
5. 更新 bankCard
6. 若 bankCard 為 soft deleted, 解除它
<?php
$bankCard = BankCard::where($request->only('card_number'))
->withTrashed()
->first() ?? BankCard::create($data);

$bankCard->update($data);
$bankCard->restore();
在 Laravel 中, 如何從一個 collection 當中取得其中一個 model, 而該 model 中的 price 欄位的值是在這個 collection 的所有 model 之中最小或最大的?
<?php
$min = $data->where('price', $data->min('price'))->first();
// ['name' => 'test', 'price' => 10]
$max = $data->where('price', $data->max('price'))->first();
// ['name' => 'test', 'price' => 600]
以下的 Laravel example code 的意思是?
  • Example:

    <?php
    collection->push($model)
  • Answer:
    將 $model push 到該 collection 中

以下的 Laravel 程式碼代表什麼意思?
1. 帶入欄位的名稱需與資料庫的欄位一樣
2. 依序檢查指定的欄位
3. 若 request 中的該欄位是有值的, 並且該值與目前資料庫中的值是不同的, 這代表有變更產生
4. 將變更的 key 跟 value 放入空的 collection $updateAttributes 中
5. 如果這個 collection 含有 card_number, 這代表卡號變更了, 將 balance => 0 放入 updateAttributes
6. 如果 updateAttributes 是存在的, 開始更新
<?php
$updatedAttributes = collect();

foreach (['card_holder_name', 'card_number', 'bank_name', 'type', 'auto_withdraw'] as $attribute) {
if (!is_null($request->$attribute) && ($request->$attribute != $bankCard->$attribute)) {
$updatedAttributes = $updatedAttributes->merge([$attribute => $request->$attribute]);
}
}

if ($updatedAttributes->has('card_number')) {
$updatedAttributes = $updatedAttributes->merge(['balance' => 0]);
}

if ($updatedAttributes->isNotEmpty()) {
$bankCard->update($updatedAttributes->toArray());
}
Laravel 中, Model 的命名通常是單數還是複數?

單數

操作 Resource 的 controller, 通常命名的規則是?

該 Resource 對應的名稱, 如 IpController

Laravel 中的變數命名習慣是?

camel case

Laravel 中, blade view 的命名習慣是?

會以資料夾做區分, 像是 withdraws/index.blade.php、withdraws/store.blade.php

Laravel 中, 如何將 namespace, prefix, middleware 同時作用到複數的 route 上?
<?php
Route::group([
'namespace' => 'Worker',
'prefix' => 'worker',
'middleware' => 'check.worker.token',
], function () {
Route::apiResource('working-tasks', 'WorkingTaskController')->only('store', 'update');
Route::apiResource('bank-cards', 'BankCardController')->only('update');

Route::post('captcha-cracks', 'CrackCaptchaController');
});
Laravel 中, 可否在 route 的 group 內再使用一個 group?

可以的

Laravel 中, 巢狀內的 route group 的屬性會不會繼承外層的 group 的屬性?

會的, 像是

<?php
Route::group([
'middleware' => ['auth']
], function () {
Route::get('me', 'AuthController@me');

Route::group([
'namespace' => 'Admin',
'prefix' => 'admin',
'middleware' => ['check.role.admin', 'check.source.admin'],
], function () {
Route::post('users/{user}/reset-password', 'UsersController@resetPassword');
Route::post('users/{user}/reset-withdraw-password', 'UsersController@resetWithdrawPassword');
Route::post('users/{user}/reset-google2fa-secret', 'UsersController@resetGoogle2faSecret');
Route::post('users/{user}/reset-secret-key', 'UsersController@resetSecretKey');
Route::put('users/{user}/delete-group', 'UsersController@deleteGroup')
->where(['user' => '[0-9]+']);
});
});
以下的 Laravel 程式碼中, 邏輯是怎麼樣的?
如果環境是在 production 的話, 檢查來源 ip, 來源 ip 可能有很多個, 取最後一個代表 client, 若不存在則拒絕存取
<?php
public function handle($request, Closure $next)
{
if (app()->environment(['production'])) {
abort_if(!IPs::where('address', Arr::last($request->ips()))->exists(), Response::HTTP_UNAUTHORIZED,
'Invalid source');
}

return $next($request);
}
以下的兩段 Laravel 在 Resource 中的程式碼, 有什麼差異?
  1. <?php
    'withdraws' => $this->whenLoaded('withdraws', Withdraw::collection($this->withdraws))
  2. <?php
    'withdraws' => $this->whenLoaded('withdraws', function () {
    return Withdraw::collection($this->withdraws);
    })

    第一個 block 中, PHP 會先去執行作為參數帶入的 $this->withdraws, 再將結果帶入 whenLoaded, 這便符合了 whenLoaded 的 relation 載入條件, 所以依然會將當前 resource 下的 relation 顯示出來, 實際運行上因為會先執行 $this->withdraws, 因此也會造成效能上的浪費

第二個 block 中, closure 在被呼叫之前, PHP 並不會去解析它, 所以會先執行 whenLoaded 函式, 如果條件吻合, 才會執行 closure, 所以不會去執行 $this->withdraws, 自然 whenLoaded 的條件就不會吻合, resource 中也就不會多撈一層

Laravel 中, 如何將 array 存到資料庫?
  • 資料庫類別為 json

    <?php
    Schema::table('users', function (Blueprint $table) {
    $table->json('iAmarray')->nullable();
    });
  • 存到資料庫前, 先使用 json_encode

    <?php
    data = json_encode($iAmArray);
  • 在 model 加入

    <?php
    protected $casts = [
    'iAmArray' => 'array'
    ];
當我在 Route 當中使用 apiResource 如下, 自動帶入 controller 的 model binding 的變數名稱為?

sub_account

<?php
Route::apiResource('sub-accounts', 'SubAccountsController')->only(['store', 'update']);
Laravel 中, 當我使用 scheduler, 腳本內的 user 務必要使用?

與 webserver 同一個 user

以下的 Laravel 程式碼的邏輯是?
  • 程式碼:

    <?php
    $canSeeSecretKey = optional(auth()->user())->isAdmin() || $this->is(auth()->user());
  • Answer:

    • 如果 auth()->user() 的身份是 admin 的話
    • 如果被帶入 resource 中的 model 的身份跟 auth()->user() 是同一個人的話(代表本人)
以下的 Laravel 程式碼的作用是?
  • code:

    <?php
    public static function depositTypeText()
    {
    return collect((new ReflectionClass(__CLASS__))->getConstants())
    ->filter(
    function ($value, $key) {
    return Str::startsWith($key, 'TYPE_DEPOSIT');
    }
    )
    ->mapWithKeys(
    function ($value, $key) {
    return [$value => $key];
    }
    );
    }
  • Answer:

    • RefectionClass: 取得指定 class 中的資料

    • __class__: 代表當前 class

    • getConstants: 取得 constants

    • filter: 只取符合條件的 key

    • mapWithKeys: 取得符合條件的 key/value pair

      以下的 Laravel 程式碼的邏輯是?
  • Example:

    <?php
    if ($endedAt->diffInDays($startedAt) > 31) {
    $request->merge(
    [
    'ended_at' => (clone $startedAt)->addDays(31)->format('Y-m-d H:i:s'),
    ]
    );
    }
  • Answer:
    如果 $endedAt 跟 $startedAt 相差大於 31 天, 那就把範圍定在最多相差 31 天

PHPSTORM 學習筆記 PHP 學習筆記

留言

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×