Реализация объектной модели домена S.O.L.I.D в следующем проекте

У меня есть следующий пример, в котором я склонен использовать пару классов для создания простого веб-приложения.

Иерархия файлов выглядит так.

> 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]); 

Который я не хочу использовать, и вместо этого попробуйте подход "Object Object Model".

Теперь, учитывая, что я добавлю другую папку под названием domain, blog.php

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

И заполните blog.php строками таблицы сопоставления свойств, а также getter и seters..

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/ORM или оболочка PDO для запроса базы данных;

"Как я могу связать вместе, т.е. модель блога с оболочкой PDO для извлечения блога внутри моего загрузочного файла?".

Ответы

Ответ 1

Что касается объекта домена, в котором вы в основном уже написали, ваш объект блога. Чтобы квалифицироваться как модель домена, все классы должны представлять собой представление вместе с любой функциональностью концепции внутри вашего проблемного пространства.

Более интересная проблема здесь и та, с которой вы, похоже, боретесь, - это сохранение модели домена. Сохраняя принцип принципа единой ответственности, ваш класс в блоге должен иметь дело с записью в блоге и делать то, что может делать запись в блоге, а не хранить его. Для этого вы бы представили концепцию репозитория сообщений в блоге, в котором будут храниться и извлекаться объекты такого типа. Ниже приведена простая реализация того, как это можно сделать.

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");
    }
}); 

Чтобы действительно быть SOLID, вышеуказанный класс должен быть специфичной для базы данных интерфейсом BlogRepository для привязки к IoC. A factory также должен быть предоставлен в BlogRepository для фактического создания объектов блога из данных, полученных из хранилища.

По моему мнению, одна из больших преимуществ этого - у вас есть одно место, где вы можете внедрять и поддерживать все связанные с блогом взаимодействия с базой данных.

Другие преимущества этого метода

  • Реализация кэширования для ваших объектов домена будет тривиальной
  • Переход на другой источник данных (из плоских файлов, blogger api, Document Database Server, PostgresSQL и т.д.) может быть легко выполнен.

В качестве альтернативы вы можете использовать ORM, поддерживающий тип, для более общего решения этой проблемы. В основном этот класс репозитория является не чем иным, как ORM для одного класса.

Важно то, что вы не говорите напрямую с базой данных и оставляете sql разбросанным по всему вашему коду. Это создает кошмар для обслуживания и связывает ваш код с схемой вашей базы данных.

Ответ 2

Лично я всегда придерживаюсь операций с базой данных в классе базы данных, который делает все тяжелый подъем инициализации класса, открывая соединение и т.д. Он также имеет общие обертки запросов, к которым я передаю SQL-инструкции, которые содержат нормальные заполнители для связанных переменных, а также массив переменных, подлежащих привязке (или переменное число параметров, если это вам подходит). Если вы хотите связать каждый параметр отдельно, а не использовать $stmt->execute(array());, вы просто передаете типы со значением в структуре данных по вашему выбору, многомерным массивом, словарем, JSON, тем, что подходит вашим потребностям, и вы найдете легко работать с.

Класс модели он сам (блог в вашем случае), затем подклассы базы данных. Тогда у вас есть несколько вариантов. Вы хотите использовать конструктор для создания только новых объектов? Вы хотите, чтобы он загружался только на основе идентификаторов? Или смесь обоих? Что-то вроде:

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 {
    $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();, если вы хотите новый.

Ответ 3

"Как я могу связать вместе, т.е. модель блога с оболочкой PDO для извлечения блога внутри моего загрузочного файла?".

Чтобы связать их вместе, вы можете использовать объектно-реляционный картограф (ORM). Библиотеки ORM создаются только для приклеивания ваших классов PHP к строкам базы данных. Есть пара библиотек ORM для PHP. Кроме того, большинство ORM имеют встроенный уровень абстракции базы данных, что означает, что вы можете просто переключить поставщика базы данных без каких-либо проблем.

Соображения при использовании ORM:
При введении ORM также вводится некоторое раздувание (и некоторое обучение), возможно, не стоит инвестировать время просто для одного объекта Blog. Хотя, если в ваших блогах также есть автор, одна или несколько категорий и/или связанных файлов, ORM может вскоре помочь вам в чтении/записи базы данных. Судя по вашему опубликованному коду, ORM окупится при расширении приложения в будущем.


Обновление: пример с использованием Doctrine 2

Вы можете посмотреть раздел запроса официальной документации Doctrine, чтобы просмотреть различные параметры, доступные для доступа к чтению. Пересмотрите пример, который вы указали:

// 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 могут вступить в игру.