讓你的程式更美好 – Service後可以加入很多功能

今天我們繼續拆分程式碼!把商業邏輯的內容寫在 Service 檔案中,這過程中我們都沒有加入新功能,主要是把程式放適當的位置。

Service

Service檔案 必須手動新增,建立一個 AnimalService 專門來處理動物資源大大小小的商業邏輯。(這裡不包含外部API喔!只用來寫操作動物資源的邏輯!)

假設我今天想把 行政院農委會收容所認養公開資料 的API,接到我的送養平台,要另外寫一個 Service檔案喔!

我的另外一篇鐵人賽 後端前進PostgreSQL (鐵人賽連結) 系列,是在嘗試學習用 PostgreSQL 資料庫!整理、拆解這些農委會的認養公開資料以及PostgreSQL的介紹,有興趣的也可以看一下喔!

新建檔案

app\Services\AnimalService.php

Controller 減肥開始

尷尬尷尬,到目前為止程式碼都非常的簡單,要拆開來感覺有點多此一舉,但等到系統變龐大時,一定會感覺出來它的好處,請先相信我!

因為我們 AnimalController 程式碼最長的就是 index 方法,所以我們把篩選條件排序邏輯,拆分到 Service檔案中,雖然我們寫的篩選、排序邏輯很通用,沒有客製化邏輯,就只是欄位跟後面的數值去篩選及排序,如下所示,但真實情況下,可能會有一些客製化的邏輯!所以我們把這兩個部分拆開放在 AnimalService.php

app/Http/Controllers/AnimalController.php 原本撰寫查詢動物列表的程式碼

public function index(Request $request)
{
    // 設定預設值

    $marker = isset($request->marker) ? $request->marker : 1;
    $limit = isset($request->limit) ? $request->limit : 10;

    $query = Animal::query();

    // 篩選欄位條件 Day26 拆到Service
    if (isset($request->filters)) {
        $filters = explode(',', $request->filters);
        foreach ($filters as $key => $filter) {
            list($criteria, $value) = explode(':', $filter);
            $query->where($criteria, 'like', "%$value%");
        }
    }

    //排列順序 Day26 拆到Service
    if (isset($request->sort)) {
        $sorts = explode(',', $request->sort);
        foreach ($sorts as $key => $sort) {
            list($criteria, $value) = explode(':', $sort);
            if ($value == 'asc' || $value == 'desc') {
                $query->orderBy($criteria, $value);
            }
        }
    } else {
        $query->orderBy('id', 'asc');
    }

    $animals = $query->where('id', '>=', $marker)->paginate($limit);

    return response($animals, Response::HTTP_OK);
}

拆解步驟

打開 app/Services/AnimalService.php 先來拆篩選邏輯

第一動:為方法取一個好名字 filterAnimals 這邊命名方式的建議,我是把持著幾個簡單的原則,就是 駝峰格式動作+資源名稱簡易單字不隨便縮寫

第二動:先把篩選方法複製過來

<?php

namespace App\Services;

class AnimalService
{
    public function filterAnimals(Request $request)
    {

        // 篩選欄位條件
        if (isset($request->filters)) {
            $filters = explode(',', $request->filters);
            foreach ($filters as $key => $filter) {
                list($criteria, $value) = explode(':', $filter);
                $query->where($criteria, 'like', "%$value%");
            }
        }
    }
}

第三動:上面的程式碼需要修改,這樣還不能動,修改結果如下所示(可以上下比對一下)

<?php

namespace App\Services;

class AnimalService
{
    public function filterAnimals($filters, $query)
    {
        // 篩選欄位條件
        if (isset($filters)) {
            $filtersArray = explode(',', $filters);
            foreach ($filtersArray as $key => $filter) {
                list($criteria, $value) = explode(':', $filter);
                $query->where($criteria, $value);
            }
        }
        return $query;
    }
}

第四動:來到 AnimalController 依賴注入 AnimalService

//記得引入 AnimalService
use App\Services\AnimalService;

class AnimalController extends Controller
{
    // class 加入以下程式碼
    private $animalService;

    public function __construct(AnimalService $animalService)
    {
        $this->animalService = $animalService;
        $this->middleware('auth:api', ['except' => ['index','show']]);
    }

    //...以下略過
}

第五動:修改 AnimalControllerindex 方法的程式

public function index(Request $request)
{
    // 設定預設值

    $marker = isset($request->marker) ? $request->marker : 1;
    $limit = isset($request->limit) ? $request->limit : 10;

    $query = Animal::query();

    // 加入下面這一行
    $query = $this->animalService->filterAnimals($request->filters, $query);
    
    // 篩選欄位條件(刪除)
    // if (isset($request->filters)) {
    //     $filters = explode(',', $request->filters);
    //     foreach ($filters as $key => $filter) {
    //         list($criteria, $value) = explode(':', $filter);
    //         $query->where($criteria, 'like', "%$value%");
    //     }
    // }

    //排列順序
    if (isset($request->sort)) {
        $sorts = explode(',', $request->sort);
        foreach ($sorts as $key => $sort) {
            list($criteria, $value) = explode(':', $sort);
            if ($value == 'asc' || $value == 'desc') {
                $query->orderBy($criteria, $value);
            }
        }
    } else {
        $query->orderBy('id', 'asc');
    }

    $animals = $query->where('id', '>=', $marker)->paginate($limit);

    return response($animals, Response::HTTP_OK);
}

第六動:儲存測試一下! 成功通過測試程式!但別忘記我們撰寫的測試很少,尤其是測試查詢動物的程式碼,只有測試回傳的資料結構而已!所以建議手動送送看請求,確認篩選的部分是否正確喔!

測試成功

另外一個排序邏輯的拆分保留給您小試身手一下吧! 我最後會把這兩個檔案貼到今天文章的附件單元。


可以偷偷去看一下文章最下面的 Controller 是不是比較簡單明瞭了?!控制要分配給哪個Service檔案,邏輯細節的部分交由Service拆分的方法執行,可能目前程式碼沒有很多體會不太出來,但如果系統越來越大,會越有感覺喔!

題外話:如果要呼叫同一個檔案的其他方法,可以這麼做! 例如 在 AnimalService 檔案中 filterAnimals() 方法外,呼叫 filterAnimals() 的話 $this->filterAnimals(這裡給對應的參數就可以囉)

假設功能需求是這樣!

業主:我想要讓會員可以刊登動物資料,至少一定要有圖片、影片,而且要記錄動物有沒有結紮,這些操作希望都可以有紀錄,並且想要讓關注的人收到通知!

翻譯蒟蒻

讓會員刊登送養動物資料必須檢查資料是否符合我們的資料格式上傳圖片或影片,經由系統處理完成以後寄送一封Email給刊登資料會員,通知會員刊登成功,並且記錄一筆記錄到系統,請且通知每個關注追蹤這位會員的人,Email告知追蹤對象有刊登動物資訊。

分析這個需求

需求分析我們以動作討論為主,資料庫規劃這裡先不談

Controller 需要做到這些部分,會員操作 POST /api/animal 的動作 對應的 store 方法要完成下列動作

  1. 檢查資料格式
  2. 檔案圖片影片上傳到伺服器,執行剪裁、製造縮圖,或影片轉檔。(可能有隊列處理)
  3. 資料寫入資料庫
  4. 傳送Email給刊登會員 (通知刊登起訖日,注意事項…)
  5. 寫入新增動物資料的操作記錄
  6. 檢查追蹤這位會員的其他會員
  7. 發送通知,不管是Email 或是網站的通知 app的推播…
  8. 轉換輸出格式,回傳新建資料!

光是一個 POST /api/animal 動作,全部的邏輯都寫在controller 絕對是一個不好的決定。

最好可以把它拆開來一個動作一個方法甚至可以想辦法拆成可以重複利用的多個方法,可用在不同地方!

附件

附上拆分程式碼!

app/Services/AnimalService.php

<?php

namespace App\Services;

class AnimalService
{
    /**
     * 
     */
    public function filterAnimals($filters, $query)
    {

        // 篩選欄位條件
        if (isset($filters)) {
            $filtersArray = explode(',', $filters);
            foreach ($filtersArray as $key => $filter) {
                list($criteria, $value) = explode(':', $filter);
                $query->where($criteria, $value);
            }
        }

        return $query;
    }
    
    /**
     * 
     */
    public function sortAnimals($sorts, $query)
    {
        if (isset($sorts)) {
            $sortArray = explode(',', $sorts);
            foreach ($sortArray as $key => $sort) {
                list($criteria, $value) = explode(':', $sort);
                if ($value == 'asc' || $value == 'desc') {
                    $query->orderBy($criteria, $value);
                }
            }
        } else {
            $query->orderBy('id', 'asc');
        }

        return $query;
    }
}

app/Http/Controllers/AnimalController.php

<?php

namespace App\Http\Controllers;

use App\Animal;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Http\Resources\AnimalResource;
use Auth;
use App\Http\Requests\StoreAnimalRequest;
use App\Services\AnimalService;

class AnimalController extends Controller
{
    private $animalService;

    public function __construct(AnimalService $animalService)
    {
        $this->animalService = $animalService;
        $this->middleware('auth:api', ['except' => ['index','show']]);
    }

    /**
     * Display a listing of the resource.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request)
    {
        // 設定預設值

        $marker = isset($request->marker) ? $request->marker : 1;
        $limit = isset($request->limit) ? $request->limit : 10;

        $query = Animal::query();

        $query = $this->animalService->filterAnimals($request->filters, $query);
        
        $query = $this->animalService->sortAnimals($request->sort, $query);

        $animals = $query->where('id', '>=', $marker)->paginate($limit);

        return response($animals, Response::HTTP_OK);
    }

    // 以下略過

}

發佈留言