簡體   English   中英

PHP中正確的存儲庫模式設計?

[英]Proper Repository Pattern Design in PHP?

前言:我正在嘗試在具有關系數據庫的 MVC 架構中使用存儲庫模式。

我最近開始在 PHP 中學習 TDD,並且我意識到我的數據庫與我的應用程序的其余部分耦合得太緊密了。 我已經閱讀了有關存儲庫並使用IoC 容器將其“注入”到我的控制器中的內容。 很酷的東西。 但是現在有一些關於存儲庫設計的實際問題。 考慮以下示例。

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

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

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

問題 #1:字段太多

所有這些查找方法都使用全選字段 ( SELECT * ) 方法。 然而,在我的應用程序中,我總是試圖限制我獲得的字段數量,因為這通常會增加開銷並減慢速度。 對於使用這種模式的人,您如何處理?

問題#2:方法太多

雖然這個類現在看起來不錯,但我知道在現實世界的應用程序中我需要更多的方法。 例如:

  • findAllByNameAndStatus
  • 查找所有國家
  • findAllWithEmailAddressSet
  • 按年齡和性別查找全部
  • findAllByAgeAndGenderOrderByAge
  • 等等。

如您所見,可能的方法列表可能非常非常長。 然后,如果您添加上面的字段選擇問題,問題就會惡化。 過去,我通常只是將所有這些邏輯都放在我的控制器中:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

使用我的存儲庫方法,我不想以這樣的方式結束:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

問題 #3:無法匹配接口

我看到了使用存儲庫接口的好處,所以我可以換掉我的實現(用於測試目的或其他目的)。 我對接口的理解是它們定義了一個實現必須遵循的契約。 這很好,直到您開始向存儲庫添加其他方法,例如findAllInCountry() 現在我需要更新我的接口以也有這個方法,否則其他實現可能沒有它,這可能會破壞我的應用程序。 這感覺很瘋狂……尾巴搖着狗的情況。

規格模式?

這讓我相信存儲庫應該只有固定數量的方法(如save()remove()find()findAll()等)。 但是,我如何運行特定的查找? 我聽說過Specification Pattern ,但在我看來,這只會減少一整套記錄(通過IsSatisfiedBy() ),如果您從數據庫中提取,這顯然存在重大性能問題。

幫助?

顯然,在使用存儲庫時,我需要重新考慮一些事情。 任何人都可以啟發如何最好地處理?

我想我會嘗試回答我自己的問題。 以下只是解決我最初問題中問題 1-3 的一種方法。

免責聲明:在描述模式或技術時,我可能不會總是使用正確的術語。 對不起。

目標:

  • 創建用於查看和編輯Users的基本控制器的完整示例。
  • 所有代碼都必須是完全可測試和可模擬的。
  • 控制器應該不知道數據存儲在哪里(意味着它可以更改)。
  • 顯示 SQL 實現的示例(最常見)。
  • 為了獲得最佳性能,控制器應該只接收他們需要的數據——沒有額外的字段。
  • 實現應該利用某種類型的數據映射器來簡化開發。
  • 實現應該能夠執行復雜的數據查找。

解決方案

我將持久存儲(數據庫)交互分為兩類: R (讀取)和CUD (創建、更新、刪除)。 我的經驗是,讀取確實是導致應用程序變慢的原因。 雖然數據操作 (CUD) 實際上更慢,但它發生的頻率要低得多,因此不太值得關注。

CUD (創建、更新、刪除)很容易。 這將涉及使用實際模型,然后將其傳遞到我的Repositories以進行持久化。 請注意,我的存儲庫仍將提供 Read 方法,但僅用於創建對象,而不是顯示。 稍后再談。

R (讀取)並不那么容易。 這里沒有模型,只有值對象 如果您願意,請使用數組。 這些對象可能代表單個模型或多個模型的混合,實際上是任何東西。 這些本身並不是很有趣,但它們是如何生成的。 我正在使用我所說的Query Objects

代碼:

用戶模型

讓我們從我們的基本用戶模型開始。 請注意,根本沒有 ORM 擴展或數據庫內容。 只是純粹的模特榮耀。 添加您的 getter、setter、驗證等。

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

存儲庫接口

在創建我的用戶存儲庫之前,我想創建我的存儲庫界面。 這將定義存儲庫必須遵循的“合同”才能被我的控制器使用。 請記住,我的控制器不會知道數據實際存儲在哪里。

請注意,我的存儲庫將只包含這三種方法。 save()方法負責創建和更新用戶,僅取決於用戶對象是否設置了 id。

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

SQL 存儲庫實現

現在創建我的接口實現。 如前所述,我的示例將使用 SQL 數據庫。 請注意使用數據映射器來防止必須編寫重復的 SQL 查詢。

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

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

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

查詢對象接口

現在通過我們的存儲庫處理CUD (創建、更新、刪除),我們可以專注於R (讀取)。 查詢對象只是某種類型的數據查找邏輯的封裝。 它們不是查詢構建器。 通過像我們的存儲庫一樣抽象它,我們可以更改它的實現並更容易地測試它。 查詢對象的一個​​例子可能是AllUsersQueryAllActiveUsersQuery ,甚至是MostCommonUserFirstNames

您可能會想“我不能在我的存儲庫中為這些查詢創建方法嗎?” 是的,但這就是我不這樣做的原因:

  • 我的存儲庫用於處理模型對象。 在現實世界的應用程序中,如果我要列出所有用戶,為什么還需要獲取password字段?
  • 存儲庫通常是特定於模型的,但查詢通常涉及多個模型。 那么你把你的方法放在哪個存儲庫中?
  • 這使我的存儲庫非常簡單——而不是一個臃腫的方法類。
  • 所有查詢現在都組織到自己的類中。
  • 真的,在這一點上,存儲庫的存在只是為了抽象我的數據庫層。

對於我的示例,我將創建一個查詢對象來查找“AllUsers”。 這是界面:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

查詢對象實現

這是我們可以再次使用數據映射器來幫助加快開發的地方。 請注意,我允許對返回的數據集進行一項調整 - 字段。 這大約是我想要操縱執行的查詢的程度。 請記住,我的查詢對象不是查詢構建器。 他們只是執行特定的查詢。 但是,因為我知道我可能會在許多不同的情況下經常使用這個,所以我給自己指定字段的能力。 我從不想返回我不需要的字段!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

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

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

在繼續討論控制器之前,我想展示另一個示例來說明它的強大功能。 也許我有一個報告引擎,需要為AllOverdueAccounts創建一個報告。 這對我的數據映射器來說可能很棘手,在這種情況下我可能想編寫一些實際的SQL 沒問題,這是這個查詢對象的樣子:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

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

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

這很好地將我對該報告的所有邏輯保留在一個類中,並且易於測試。 我可以隨心所欲地模擬它,甚至可以完全使用不同的實現。

控制器

現在是有趣的部分 - 將所有部分組合在一起。 請注意,我正在使用依賴注入。 通常依賴項被注入到構造函數中,但我實際上更喜歡將它們直接注入到我的控制器方法(路由)中。 這最小化了控制器的對象圖,實際上我發現它更清晰。 請注意,如果您不喜歡這種方法,請使用傳統的構造函數方法。

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

最后的想法:

這里要注意的重要事項是,當我修改(創建、更新或刪除)實體時,我正在處理真實的模型對象,並通過我的存儲庫執行持久性。

但是,當我顯示(選擇數據並將其發送到視圖)時,我沒有使用模型對象,而是使用普通的舊值對象。 我只選擇我需要的字段,它的設計目的是讓我可以最大限度地提高我的數據查找性能。

我的存儲庫保持非常干凈,而是將這種“混亂”組織到我的模型查詢中。

我使用數據映射器來幫助開發,因為為常見任務編寫重復的 SQL 是荒謬的。 但是,您絕對可以在需要的地方編寫 SQL(復雜的查詢、報告等)。 當你這樣做時,它很好地隱藏在一個正確命名的類中。

我很想聽聽你對我的方法的看法!


2015 年 7 月更新:

我在評論中被問到我在哪里結束了這一切。 嗯,實際上並沒有那么遠。 說實話,我仍然不太喜歡存儲庫。 我發現它們對於基本查找來說太過分了(特別是如果您已經在使用 ORM),並且在處理更復雜的查詢時會很混亂。

我通常使用 ActiveRecord 樣式的 ORM,因此大多數情況下,我只會在整個應用程序中直接引用這些模型。 但是,在我有更復雜查詢的情況下,我將使用查詢對象來使這些更可重用。 我還應該注意,我總是將我的模型注入到我的方法中,使它們更容易在我的測試中模擬。

根據我的經驗,以下是您的問題的一些答案:

問:我們如何處理帶回不需要的字段?

答:根據我的經驗,這實際上歸結為處理完整實體與臨時查詢。

一個完整的實體類似於一個User對象。 它具有屬性和方法等。它是您代碼庫中的一等公民。

即席查詢會返回一些數據,但除此之外我們一無所知。 當數據在應用程序中傳遞時,它是在沒有上下文的情況下完成的。 User嗎? 附加了一些Order信息的User 我們真的不知道。

我更喜歡使用完整的實體。

您經常會帶回不會使用的數據是對的,但您可以通過多種方式解決此問題:

  1. 積極緩存實體,因此您只需從數據庫中支付一次讀取費用。
  2. 花更多時間為您的實體建模,以便它們之間有很好的區別。 (考慮將一個大實體拆分為兩個較小的實體等)
  3. 考慮擁有多個版本的實體。 您可以有一個User作為后端,也可以有一個UserSmall用於 AJAX 調用。 一個可能有 10 個屬性,一個可能有 3 個屬性。

使用臨時查詢的缺點:

  1. 您最終會在許多查詢中獲得基本相同的數據。 例如,對於User ,您最終會為許多調用編寫基本相同的select * 一個調用將獲得 10 個字段中的 8 個,一個將獲得 10 個中的 5 個,一個將獲得 10 個中的 7 個。為什么不將所有調用替換為一個調用,該調用獲得 10 個中的 10 個? 這很糟糕的原因是重構/測試/模擬是謀殺。
  2. 隨着時間的推移,很難對您的代碼進行高層次的推理。 而不是像“為什么User這么慢?”這樣的陳述。 您最終會跟蹤一次性查詢,因此錯誤修復往往很小且本地化。
  3. 替換底層技術真的很難。 如果您現在將所有內容都存儲在 MySQL 中並希望遷移到 MongoDB,那么替換 100 個臨時調用比替換少數實體要困難得多。

問:我的存儲庫中有太多方法。

答:除了合並電話之外,我還沒有真正看到任何解決方法。 存儲庫中的方法調用實際上映射到應用程序中的功能。 功能越多,特定數據調用就越多。 您可以推遲功能並嘗試將類似的調用合並為一個。

一天結束時的復雜性必須存在於某個地方。 使用存儲庫模式,我們將它推送到存儲庫界面,而不是制作一堆存儲過程。

有時我不得不告訴自己,“好吧,它必須給某個地方!沒有靈丹妙葯。”

我使用以下接口:

  • Repository - 加載、插入、更新和刪除實體
  • Selector - 根據過濾器在存儲庫中查找實體
  • Filter - 封裝過濾邏輯

我的Repository與數據庫無關; 事實上,它沒有指定任何持久性; 它可以是任何東西:SQL 數據庫、xml 文件、遠程服務、來自外太空的外星人等。為了搜索功能, Repository構建了一個可以過濾、 LIMIT 、排序和計數的Selector 最后,選擇器從持久化中獲取一個或多個Entities

下面是一些示例代碼:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

然后,一種實現:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

想法是泛型Selector使用Filter而實現SqlSelector使用SqlFilter SqlSelectorFilterAdapter使通用Filter適應具體的SqlFilter

客戶端代碼創建Filter對象(即通用過濾器),但在選擇器的具體實現中,這些過濾器在 SQL 過濾器中進行了轉換。

其他選擇器實施方式中,像InMemorySelector ,從變換FilterInMemoryFilter使用其特定InMemorySelectorFilterAdapter ; 因此,每個選擇器實現都帶有自己的過濾器適配器。

使用此策略,我的客戶端代碼(在業務層中)不關心特定的存儲庫或選擇器實現。

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

PS這是我的真實代碼的簡化

我會在這方面補充一點,因為我目前正試圖自己掌握所有這些。

#1 和 2

這是您的 ORM 完成繁重工作的理想場所。 如果您正在使用實現某種 ORM 的模型,您可以使用它的方法來處理這些事情。 如果需要,創建自己的 orderBy 函數來實現 Eloquent 方法。 以 Eloquent 為例:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

您似乎正在尋找的是 ORM。 沒有理由您的存儲庫不能基於一個。 這將需要用戶擴展雄辯,但我個人不認為這是一個問題。

但是,如果您確實想避免使用 ORM,則您必須“自己動手”才能獲得所需的內容。

#3

接口不應該是硬性要求。 有些東西可以實現一個接口並添加到它。 它不能做的是無法實現該接口所需的功能。 您還可以擴展像類這樣的接口以保持 DRY。

也就是說,我才剛剛開始掌握,但這些認識對我有所幫助。

我只能評論我們(在我的公司)處理這個問題的方式。 首先,性能對我們來說不是太大的問題,但擁有干凈/正確的代碼才是。

首先,我們定義模型,例如使用 ORM 創建UserEntity對象的UserModel 當從模型加載UserEntity所有字段都會加載。 對於引用外部實體的字段,我們使用適當的外部模型來創建相應的實體。 對於這些實體,數據將按需加載。 現在你的第一反應可能是......?......!!! 讓我給你舉個例子:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

在我們的例子中, $db是一個能夠加載實體的 ORM。 該模型指示 ORM 加載一組特定類型的實體。 ORM 包含一個映射,並使用它將該實體的所有字段注入到該實體中。 然而,對於外部字段,只加載這些對象的 id。 在這種情況下, OrderModel創建OrderEntity s,其中僅包含引用訂單的 id。 PersistentEntity::getFieldOrderEntity調用時,實體會指示它的模型將所有字段延遲加載到OrderEntity 與一個 UserEntity 關聯的所有OrderEntity都被視為一個結果集,並將立即加載。

這里的神奇之處在於我們的模型和 ORM 將所有數據注入到實體中,而實體僅為PersistentEntity提供的通用getField方法提供包裝函數。 總而言之,我們總是加載所有字段,但在必要時加載引用外部實體的字段。 只是加載一堆字段並不是真正的性能問題。 然而,加載所有可能的外部實體會導致巨大的性能下降。

現在根據 where 子句加載一組特定的用戶。 我們提供了一個面向對象的類包,允許您指定可以粘合在一起的簡單表達式。 在示例代碼中,我將其命名為GetOptions 它是選擇查詢的所有可能選項的包裝器。 它包含 where 子句、group by 子句和其他所有內容的集合。 我們的 where 子句相當復雜,但您顯然可以輕松制作一個更簡單的版本。

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

該系統的最簡單版本是將查詢的 WHERE 部分作為字符串直接傳遞給模型。

對於這個相當復雜的回復,我很抱歉。 我試圖盡可能快速和清晰地總結我們的框架。 如果您有任何其他問題,請隨時問他們,我會更新我的答案。

編輯:此外,如果您真的不想立即加載某些字段,則可以在 ORM 映射中指定延遲加載選項。 因為所有字段最終都是通過getField方法加載的,所以您可以在調用該方法時最后一分鍾加載一些字段。 這在 PHP 中不是一個很大的問題,但我不會推薦用於其他系統。

這些是我見過的一些不同的解決方案。 它們各有利弊,但由您來決定。

問題 #1:字段太多

這是一個重要的方面,尤其是當您考慮僅索引掃描時 我看到了處理這個問題的兩種解決方案。 您可以更新您的函數以接受一個可選的數組參數,該參數將包含要返回的列列表。 如果此參數為空,您將返回查詢中的所有列。 這可能有點奇怪; 根據您可以檢索對象或數組的參數。 您還可以復制所有函數,以便您有兩個不同的函數來運行相同的查詢,但一個返回一個列數組,另一個返回一個對象。

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

問題#2:方法太多

一年前我曾與Propel ORM進行過短暫的合作,這是基於我從那次經歷中所記得的。 Propel 可以選擇根據現有數據庫模式生成其類結構。 它為每個表創建兩個對象。 第一個對象是一長串訪問功能,類似於您當前列出的; findByAttribute($attribute_value) 下一個對象繼承自第一個對象。 您可以更新此子對象以構建更復雜的 getter 函數。

另一種解決方案是使用__call()將未定義的函數映射到可操作的東西。 您的__call方法將能夠將 findById 和 findByName 解析為不同的查詢。

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

我希望這至少有一些幫助。

我認為在這種情況下, graphQL是一個很好的候選者,它可以在不增加數據存儲庫復雜性的情況下提供大規模查詢語言。

但是,如果您暫時不想使用 graphQL,還有另一種解決方案。 通過使用DTO ,其中對象用於在進程之間傳輸數據,在這種情況下是在服務/控制器和存儲庫之間。

上面已經提供了一個優雅的答案,但是我將嘗試舉另一個例子,我認為它更簡單,可以作為新項目的起點。

如代碼所示,我們只需要 4 個方法來進行 CRUD 操作。 find方法將用於通過傳遞對象參數來列出和讀取。 后端服務可以基於 URL 查詢字符串或基於特定參數構建定義的查詢對象。

如果需要,查詢對象( SomeQueryDto )也可以實現特定的接口。 並且很容易在不增加復雜性的情況下進行擴展。

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

用法示例:

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);

我建議https://packagist.org/packages/prettus/l5-repository作為供應商在 Laravel5 中實現 Repositories/Criterias 等......:D

我同意@ryan1234,你應該在代碼中傳遞完整的對象,並且應該使用通用查詢方法來獲取這些對象。

Model::where(['attr1' => 'val1'])->get();

對於外部/端點使用,我非常喜歡 GraphQL 方法。

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

問題 #3:無法匹配接口

我看到了使用存儲庫接口的好處,所以我可以換掉我的實現(用於測試目的或其他目的)。 我對接口的理解是它們定義了一個實現必須遵循的契約。 這很好,直到您開始向存儲庫添加其他方法,例如 findAllInCountry()。 現在我需要更新我的接口以也有這個方法,否則其他實現可能沒有它,這可能會破壞我的應用程序。 這感覺很瘋狂……尾巴搖着狗的情況。

我的直覺告訴我,這可能需要一個接口來實現查詢優化方法和通用方法。 性能敏感的查詢應該有針對性的方法,而不頻繁或輕量級的查詢由通用處理程序處理,可能是控制器做更多雜耍的費用。

通用方法將允許實現任何查詢,因此將防止在過渡期間破壞性更改。 目標方法允許您在有意義的時候優化調用,並且它可以應用於多個服務提供商。

這種方法類似於執行特定優化任務的硬件實現,而軟件實現則是輕量級的或靈活的實現。

   class Criteria {}
   class Select {}
   class Count {}
   class Delete {}
   class Update {}
   class FieldFilter {}
   class InArrayFilter {}
   // ...

   $crit = new Criteria();  
   $filter = new FieldFilter();
   $filter->set($criteria, $entity, $property, $value);
   $select = new Select($criteria);
   $count = new Count($criteria);
   $count->getRowCount();
   $select->fetchOne(); // fetchAll();

所以我認為

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM