在以下项目中实施 S.O.L.I.D 域对象模型

Implementing a S.O.L.I.D Domain Object Model in the following project

我有以下示例,其中我倾向于使用几个 classes 来创建一个简单的网络应用程序。

文件层次结构如下所示。

> cupid 
    - libs 
        - request
        - router 
        - database
        - view 
    - bootstrap.php 
  - index.php 

index.php 只是调用了 bootstrap.php,后者又包含如下内容:

// bootstrap.php
namespace cupid
use request, router, database, view; 

spl_autoload_register(function($class){ /* autoload */ });

$request  = new view; 
$response = new response; 
$router   = new router; 
$database = new database; 

$router->get('/blog/{id}', function($id) use ($database, $view) {

    $article = $database->select("SELECT blog, content FROM foo WHERE id = ?",[$id]); 

    $view->layout('blogPage', ['article'=>$article]);
}); 

您可能已经看出来,我的问题是这一行:

$article = $database->select("SELECT blog, content FROM foo WHERE id = ?", [$id]); 

我不想使用,而是尝试使用“域对象模型”方法。

现在,考虑到我将添加另一个名为域的文件夹,blog.php

> cupid 
    - domain
       - Blog.php
    - libs 
        ...

并用属性映射 table 行、getter 和设置器填充 blog.php ..

namespace App\Domain; 

class Blog {

    private $id, $title, $content, $author; 

    public function getTitle(){
        return $this->title; 
    }           

    public function setTitle($title){
        $this->title = $title; 
    }

    ...
}

我的问题是:假设我对DOM的理解到目前为止是正确的,并且我有一个CRUD/ORMclass,或者用于查询数据库的 PDO 包装器;

"How can I tie together, i.e. the blog model with the PDO wrapper to fetch a blog inside my bootstrap file?"..

至于域对象,您基本上已经编写了一个,您的博客对象。要成为领域模型,class 必须提供一个表示以及问题中概念的任何功能 space。

这里更有趣的问题和您似乎正在努力解决的问题是如何持久化域模型。遵循单一责任原则的宗旨,您的博客 class 应该作为一个博客 post 并做博客 post 可以做的事情,而不是存储一个。为此,您将引入博客存储库 post 的概念,该存储库将处理存储和检索此类对象。下面是如何完成此操作的简单实现。

class BlogRepository  {
    public function __construct(\cupid\database $db){
        $this->db = $db;
    }

    public function findById($id){
        $blogData = $this->db->select("select * from blog where id = ?", [$id]);
        if ($blogData){
            return $this->createBlogFromArray($blogData);
        }
        return null;
    }
    public function findAllByTag($tag){...}
    public function save(Blog $blog) {...}
    private function createBlogFromArray(array $array){
        $blog = new Blog();
        $blog->setId($blogData["id"]);
        $blog->setTitle($blogData["title"]);
        $blog->setContent($blogData["content"]);
        $blog->setAuthor($blogData["author"]);
        return $blog;
    }
}

那么你的控制器应该看起来像这样。

$router->get('/blog/{id}', function($id) use ($blogRepository, $view) {
    $article = $blogRepository->findById($id);
    if ($article) {
        $view->layout('blogPage', ['article'=>$article]);
    } else {
        $view->setError("404");
    }
}); 

要真正可靠,上面的 class 应该是 BlogRepository 接口的数据库特定实现,以遵守 IoC。还应该向 BlogRepository 提供一个工厂,以根据从商店检索到的数据实际创建博客对象。

在我看来,这样做的一大好处是您可以在一个地方实施和维护所有与博客相关的数据库交互。

此方法的其他优点

  • 为您的域对象实施缓存很简单
  • 切换到不同的数据源(从平面文件、博主 api、文档数据库服务器、PostgresSQL 等)可以轻松完成。

您也可以使用类型感知 ORM 来更通用地解决同一问题。基本上这个 Repository class 只不过是单个 class 的 ORM。

这里重要的是您不是直接与数据库对话,而是让 sql 分散在您的代码中。这会造成维护噩梦,并将您的代码耦合到数据库的模式。

就我个人而言,我总是倾向于将数据库操作放在数据库 class 中,它完成了初始化 class、打开连接等所有繁重工作。它还有通用的查询包装器我传递了 SQL-statements,其中包含绑定变量的正常占位符,加上要绑定的变量数组(或者如果这更适合你的话,可以使用可变数量的参数方法)。如果你想单独绑定每个参数而不使用 $stmt->execute(array()); 你只需在你选择的数据结构中传递具有值的类型,多暗数组,字典,JSON,任何适合你的需要而且你会发现很容易合作。

模型 class 它自己(在您的情况下是博客)然后子class 数据库。然后你有几个选择。你想使用构造函数只创建新对象吗?你想让它只根据 ID 加载吗?或者两者兼而有之?类似于:

function __construct(id = null, title = null, ingress = null, body = null) {
    if(id){
        $row = $this->getRow("SELECT * FROM blog WHERE id = :id",id); // Get a single row from the result
        $this->title = $row->title;
        $this->ingress = $row->ingress;
        $this->body = $row->body;
        ... etc
    } else if(!empty(title,ingress,body)){
        $this->title = title;
        ... etc
    }
}

也许两者都不是?如果您愿意,可以跳过构造函数并使用 new(title, ingress, body)save()load(id) 方法。

当然,如果你只是配置一些 class 成员并让 Database-superclass 根据你发送或设置的内容进行查询构建,查询部分可以进一步推广作为成员变量。例如:

class Database {
    $columns = []; // Array for storing the column names, could also be a dictionary that also stores the values
    $idcolumn = "id"; // Generic id column name typically used, can be overridden in subclass
    ...
    // Function for loading the object in a generic way based on configured data
    function load($id){
        if(!$this->db) $this->connect(); // Make sure we are connected
        $query = "SELECT "; // Init the query to base string
        foreach($this->columns as $column){
            if($query !== "SELECT ") $query .= ", "; // See if we need comma before column name

            $query .= $column; // Add column name to query
        }
        $query .= " FROM " . $this->tablename . " WHERE " . $this->idcolumn . " = :" . $this->idcolumn . ";";
        $arg = ["col"=>$this->idcolumn,"value"=>$id,"type"=>PDO::PARAM_INT];
        $row = $this->getRow($query,[$arg]); // Do the query and get the row pass in the type of the variable along with the variable, in this case an integer based ID
        foreach($row as $column => $value){
            $this->$column = $value; // Assign the values from $row to $this
        }
    }
    ...
    function getRow($query,$args){
        $statement = $this->query($query,$args); // Use the main generic query to return the result as a PDOStatement
        $result = $statement->fetch(); // Get the first row
        return $result;
    }
    ...
    function query($query,$args){
        ...
        $stmt = $this->db->prepare($query);
        foreach($args as $arg){
            $stmt->bindParam(":".$arg["col"],$arg["value"],$arg["type"]);
        }
        $stmt->execute();
        return $stmt;
    }
    ...
}

现在,如您所见,load($id)getrow($query,$args)query($query,$args) 完全通用。 “getrow()”只是 query() 上获取第一行的包装器,您可能希望有几个不同的包装器来以不同的方式来解释您的语句结果。如果不能使它们通用,您甚至可能还想将特定于对象的包装器添加到您的模型中。现在模型,在你的情况下 Blog 可能看起来像:

class Blog extends Database {
    $title;
    $ingress;
    $body;
    ...
    function __construct($id = null){
        $this->columns = ["title","ingress","body","id",...];
        $this->idcolumn = "articleid"; // override parent id name
        ...
        if($id) $this->load($id);
    }
    ...
}

这样使用:$blog = new Blog(123); 加载特定的博客,或者 $blog = new Blog(); $blog->title = "title"; ... $blog->save(); 如果你想要一个新的。

"How can I tie together, i.e. the blog model with the PDO wrapper to fetch a blog inside my bootstrap file?"..

要将两者联系在一起,您可以使用 对象关系映射器 (ORM)。 ORM 库只是为了将 PHP 类 粘合到数据库行而构建的。周围有几个 ORM libraries for PHP。此外,大多数 ORM 都有一个内置的数据库抽象层,这意味着您可以轻松地切换数据库供应商。

使用ORM时的注意事项:
虽然引入 ORM 也会引入一些膨胀(和一些学习),但可能不值得为单个 Blog 对象投入时间。虽然,如果您的博客条目也有作者、一个或多个类别 and/or 相关文件,ORM 可能很快会帮助您 reading/writing 数据库。从您发布的代码来看,ORM 将在将来扩展应用程序时得到回报。


更新:使用 Doctrine 2 的例子

您可以查看官方 Doctrine 文档的 querying section 以查看您拥有的不同读取权限选项。重新考虑你给出的例子:

// current implementation    
$article = $database->select("SELECT blog, content FROM foo WHERE id = ?",[$id]);

// possible implementation using Doctrine
$article = $em->getRepository(Blog::class)->find($id);

然而,理想情况下,您定义自己的存储库以将您的业务逻辑与 Doctrines 分开 API,如下例所示:

use Doctrine\ORM\EntityRepository;

interface BlogRepositoryInterface {
    public function findById($id);
    public function findByAuthor($author);
}

class BlogRepsitory implements BlogRepositoryInterface {
    /** @var EntityRepository */
    private $repo;

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

    public function findById($id) {
        return $this->repo->find($id);
    }

    public function findByAuthor($author) {
        return $this->repo->findBy(['author' => $author]);
    }
}

我希望这个示例能够说明您可以多么轻松地将业务领域模型和逻辑与底层库分开,以及 ORM 可以发挥多么强大的作用。