Реализация объектной модели предметной области 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]); 

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

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

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

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

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

Author: samayo, 2015-07-17

3 answers

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

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

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

Чтобы действительно быть НАДЕЖНЫМ, вышеуказанный класс должен быть конкретной реализацией базы данных интерфейса BlogRepository, чтобы соответствовать IoC. Фабрика также должна вероятно, будет передан в BlogRepository для фактического создания объектов блога из данных, полученных из магазина.

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

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

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

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

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

 14
Author: Orangepill, 2015-07-23 07:01:46

Лично я всегда склонен помещать операции с базой данных в класс базы данных, который выполняет всю тяжелую работу по инициализации класса, открытию соединения и т. Д. В нем также есть общие оболочки запросов, в которые я передаю SQL-операторы, содержащие обычные заполнители для связанных переменных, плюс массив переменных, которые должны быть связаны (или подход с переменным числом параметров, если это вам больше подходит). Если вы хотите связать каждый параметр по отдельности и не использовать $stmt->execute(array()); Вы просто передаете типы со значением в структуре данных по вашему выбору, массив multi dim, словарь, 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();, если вы хотите новый.

 4
Author: Canis, 2015-07-24 12:09:46

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

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

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


Обновление: Пример использования доктрины 2

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

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

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

Однако в идеале вы определяете свой собственный репозиторий, чтобы отделить свою бизнес-логику от 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 могут быть задействованы.

 3
Author: Fabian Keller, 2017-05-23 11:53:57