Prettus Repository (儲存庫模式)

Prettus\Repository

介紹

Prettus\Repository 是一個在 Laravel 中實現 Repository Pattern(儲存庫模式)的套件。它的目的是解耦應用程式的商業邏輯與資料存取層,提升代碼的可維護性與可測試性,同時簡化資料查詢、篩選、排序等操作。

使用目的

  • 資料存取抽象化:將資料存取邏輯與業務邏輯分離,方便維護與測試。
  • 簡化資料查詢:提供簡潔 API 實現 CRUD 操作。
  • 支援資料轉換:與 TransformableTransformableTrait 結合使用,可統一輸出格式。
  • 可擴展性高:支援複雜查詢與關聯操作,並可擴展 repository 層。

優點

  • 清晰結構:控制器聚焦業務邏輯,資料存取交由 Repository 管理。
  • 提升測試性:易於替換資料來源並模擬資料存取,利於單元測試。
  • 支援多種資料源:例如資料庫、API、外部服務等。
  • 簡化操作:內建常見的查詢功能如分頁、排序、篩選等。
  • 集中邏輯:統一資料操作邏輯位置,有利於維護與擴展。

缺點

  • 學習曲線偏高:對初學者來說結構較為複雜。
  • 效能損耗:增加額外抽象層,可能帶來輕微效能開銷。
  • 過度設計風險:在小型專案中可能顯得冗餘。

設計方向

  • 使用 Criteria 解耦跨模型的共用查詢條件(如 status = activetenant_id 等)。
  • 使用 Presenter 將資料轉換為前端格式,避免 controller 手動轉換資料。
  • Repository 聚焦單一模型的資料存取與邏輯封裝。

使用情境

  • 中大型應用:資料層結構複雜,需集中管理存取邏輯。
  • 多資料來源:如多資料庫或 API 整合情境。
  • 需大量單元測試:抽象化資料層有助於測試。

電子商務範例

定義資料模型

// Product.php
class Product extends Model
{
    protected $fillable = ['name', 'price', 'stock'];

    public function orders()
    {
        return $this->belongsToMany(Order::class);
    }
}

定義 Repository Interface 與實作

// ProductRepositoryInterface.php
namespace App\Repositories;

use Prettus\Repository\Contracts\RepositoryInterface;

interface ProductRepositoryInterface extends RepositoryInterface
{
    public function getProductsByCategory($categoryId);
}

// ProductRepository.php
namespace App\Repositories;

use Prettus\Repository\Eloquent\BaseRepository;
use App\Models\Product;

class ProductRepository extends BaseRepository implements ProductRepositoryInterface
{
    public function model()
    {
        return Product::class;
    }

    public function getProductsByCategory($categoryId)
    {
        return $this->model->where('category_id', $categoryId)->get();
    }
}

綁定至服務容器

// AppServiceProvider.php
public function register()
{
    $this->app->bind(ProductRepositoryInterface::class, ProductRepository::class);
}

控制器中使用 Repository

// ProductController.php
namespace App\Http\Controllers;

use App\Repositories\ProductRepositoryInterface;

class ProductController extends Controller
{
    protected $productRepository;

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

    public function show($categoryId)
    {
        $products = $this->productRepository->getProductsByCategory($categoryId);
        return response()->json($products);
    }
}

資料轉換(可選)

// Product.php
use Prettus\Repository\Contracts\Transformable;
use Prettus\Repository\Traits\TransformableTrait;

class Product extends Model implements Transformable
{
    use TransformableTrait;
}

// ProductTransformer.php
namespace App\Transformers;

use League\Fractal\TransformerAbstract;
use App\Models\Product;

class ProductTransformer extends TransformerAbstract
{
    public function transform(Product $product)
    {
        return [
            'id' => $product->id,
            'name' => $product->name,
            'price' => $product->price,
            'stock' => $product->stock,
        ];
    }
}

// ProductRepository 中使用
use App\Transformers\ProductTransformer;

public function getProductsByCategory($categoryId)
{
    $products = $this->model->where('category_id', $categoryId)->get();
    return fractal($products, new ProductTransformer())->toArray();
}

查詢功能與其他技巧

Scopes(模型範圍查詢)

// 模型中定義
public function scopeActive($query)
{
    return $query->where('status', 'active');
}

public function scopeInCategory($query, $categoryId)
{
    return $query->where('category_id', $categoryId);
}

// 使用方式
$products = $this->model->active()->inCategory($categoryId)->get();

Criteria(封裝查詢條件)

use Prettus\Repository\Contracts\CriteriaInterface;
use Prettus\Repository\Contracts\RepositoryInterface;

class ActiveCriteria implements CriteriaInterface
{
    public function apply($model, RepositoryInterface $repository)
    {
        return $model->where('status', 'active');
    }
}

$this->repository->pushCriteria(new ActiveCriteria());
$products = $this->repository->all();

分頁

public function getPagedProducts($categoryId, $perPage = 15)
{
    return $this->model->where('category_id', $categoryId)->paginate($perPage);
}

排序

$products = $this->model->orderBy('price', 'asc')->get();
$products = $this->model->orderBy('created_at', 'desc')->get();
$products = $this->model->orderBy($request->sortBy, $request->order)->get();

指定欄位查詢

$products = $this->model->select('id', 'name', 'price')->get();

限制返回筆數

$products = $this->model->limit(10)->get();

條件查詢

$products = $this->model->where('category_id', $categoryId)
                        ->where('price', '>', 100)
                        ->get();

$products = $this->model->where([
    ['status', '=', 'active'],
    ['stock', '>', 0]
])->get();

關聯查詢(Eager Loading)

$products = $this->model->with('category')->get();

原始查詢

$products = DB::select('SELECT * FROM products WHERE price > ?', [100]);

資料轉換與映射

use App\Transformers\ProductTransformer;
use Prettus\Repository\Facades\Fractal;

$products = $this->model->all();
$transformedProducts = Fractal::collection($products, new ProductTransformer())->toArray();

自定義查詢方法

public function getDiscountedProducts()
{
    return $this->model->where('discount', '>', 0)->get();
}

Scope 與 Criteria 比較

特性 Scope Criteria
使用範圍 模型內,適用簡單查詢 repository 層,適用複雜查詢
封裝方式 寫在 Eloquent 模型方法中 封裝成獨立類別,可重複使用
用途 簡單條件(如 active 複雜查詢邏輯,跨模型共用
可重用性 侷限於單一模型 可跨 repository 與模型重用
查詢方式 使用 wherescope... 方法組合查詢 使用 pushCriteria 將條件加入查詢中
適用場景 單純欄位查詢 多欄位條件或複合邏輯查詢