php - resueltos - Implementación de un modelo de objetos de dominio SOLID en el siguiente proyecto
modelo de dominio online (3)
En cuanto a un objeto de dominio, básicamente ya has escrito uno, tu objeto de blog. Para calificar como un modelo de dominio, todo lo que una clase debe hacer es proporcionar una representación junto con cualquiera de las funciones de un concepto dentro de su espacio problemático.
El problema más interesante aquí y con el que parece estar luchando es cómo persistir en un modelo de dominio. Manteniendo el principio de la responsabilidad única, su clase de Blog debería tratar de ser una publicación de blog y hacer las cosas que una publicación de blog puede hacer, no almacenar una. Para eso, introducirías el concepto de un repositorio de publicaciones de blog que se ocuparía de almacenar y recuperar objetos de este tipo. A continuación se muestra una implementación simple de cómo se puede hacer esto.
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;
}
}
Entonces tu controlador debería verse algo así.
$router->get(''/blog/{id}'', function($id) use ($blogRepository, $view) {
$article = $blogRepository->findById($id);
if ($article) {
$view->layout(''blogPage'', [''article''=>$article]);
} else {
$view->setError("404");
}
});
Para ser realmente SOLID la clase anterior debe ser una implementación específica de la base de datos de una interfaz BlogRepository para adherirse a IoC. Probablemente también deba suministrarse una fábrica a BlogRepository para que realmente cree los objetos del blog a partir de los datos recuperados de la tienda.
En mi opinión, uno de los grandes beneficios de hacer esto es tener un solo lugar donde pueda implementar y mantener todas las interacciones relacionadas con su blog con la base de datos.
Otras ventajas de este método
- Implementar el almacenamiento en caché para los objetos de tu dominio sería trivial
- Cambiar a una fuente de datos diferente (de archivos planos, api de blogger, Document Database Server, PostgresSQL, etc.) podría hacerse fácilmente.
Alternativamente, puede usar un ORM tipo enterado para una solución más general a este mismo problema. Básicamente, esta clase de repositorio no es más que un ORM para una sola clase.
Lo importante aquí es que no estás hablando directamente con la base de datos y dejando sql disperso en todo tu código. Esto crea una pesadilla de mantenimiento y acopla su código al esquema de su base de datos.
Tengo el siguiente ejemplo en el que tiendo a usar un par de clases, para crear una aplicación web simple.
La jerarquía de archivos parece así.
> cupid
- libs
- request
- router
- database
- view
- bootstrap.php
- index.php
El index.php
simplemente llama a bootstrap.php
que a su vez contiene algo como esto:
// 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]);
});
Como probablemente puedas decir, mi problema es esta línea:
$article = $database->select("SELECT blog, content FROM foo WHERE id = ?", [$id]);
Que no quiero usar, y en su lugar intento un enfoque de "Modelo de objetos de dominio".
Ahora, dado que agregaré otra carpeta llamada domain, con blog.php
> cupid
- domain
- Blog.php
- libs
...
Y rellene blog.php
con propiedades mapeando filas de tablas, y getter y setters ...
namespace App/Domain;
class Blog {
private $id, $title, $content, $author;
public function getTitle(){
return $this->title;
}
public function setTitle($title){
$this->title = $title;
}
...
}
Mi pregunta es : suponiendo que mi comprensión de DOM es correcta hasta ahora, y que tengo una clase CRUD / ORM, o un contenedor PDO para consultar la base de datos;
"¿Cómo puedo vincular, es decir, el modelo de blog con el contenedor PDO para buscar un blog dentro de mi archivo de arranque?" ..
"¿Cómo puedo vincular, es decir, el modelo de blog con el contenedor PDO para buscar un blog dentro de mi archivo de arranque?" ..
Para unir los dos, podría usar un mapeador relacional de objetos (ORM) . Las bibliotecas ORM están diseñadas solo para pegar sus clases de PHP a filas de bases de datos. Hay un par de bibliotecas ORM para PHP . Además, la mayoría de los ORM tienen una capa de abstracción de base de datos integrada, lo que significa que simplemente puede cambiar el proveedor de la base de datos sin ningún tipo de molestia.
Consideraciones al usar un ORM:
Si bien la introducción de un ORM también introduce algo de hinchazón (y algo de aprendizaje), puede que no valga la pena invertir el tiempo simplemente en un solo objeto de Blog
. Aunque, si las entradas de su blog también tienen un autor, una o varias categorías y / o archivos asociados, un ORM pronto puede ayudarlo a leer / escribir la base de datos. A juzgar por el código publicado, un ORM dará sus frutos al extender la aplicación en el futuro.
Actualización: Ejemplo usando Doctrine 2
Puede echar un vistazo a la sección de consultas de la documentación oficial de Doctrine para ver las diferentes opciones que tiene para el acceso de lectura. Reconsidere el ejemplo que dio:
// current implementation
$article = $database->select("SELECT blog, content FROM foo WHERE id = ?",[$id]);
// possible implementation using Doctrine
$article = $em->getRepository(Blog::class)->find($id);
Sin embargo, lo ideal es que defina su propio repositorio para separar su lógica empresarial de la API de Doctrines, como se muestra en el siguiente ejemplo:
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]);
}
}
Espero que el ejemplo muestre la facilidad con la que puede separar los modelos y la lógica de su dominio empresarial de la biblioteca subyacente y de qué tan poderosos ORM pueden entrar en juego.
Personalmente, siempre tiendo a pegar las operaciones de la base de datos en una clase de base de datos que hace todo el trabajo pesado de inicializar la clase, abrir la conexión, etc. También tiene contenedores genéricos de consulta a los que paso las sentencias SQL que contienen los marcadores de posición normales para las variables vinculadas, más una matriz de las variables que se vincularán (o el número variable de parámetros de aproximación si eso le conviene más). Si desea vincular cada parámetro individualmente y no usar $stmt->execute(array());
Solo debe pasar los tipos con el valor en una estructura de datos de su elección, matriz multidim, diccionario, JSON, lo que se ajuste a sus necesidades y le resulte fácil trabajar con ella.
La clase de modelo en sí misma (Blog en su caso) luego subclasifica la Base de datos. Entonces tienes que tomar algunas decisiones. ¿Desea usar el constructor para crear solo objetos nuevos? ¿Desea que solo se cargue en función de los ID? ¿O una mezcla de ambos? Algo como:
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
}
}
Quizás ninguno? Puede omitir el constructor y utilizar los métodos new(title, ingress, body)
, save()
y load(id)
si así lo prefiere.
Por supuesto, la parte de la consulta se puede generalizar aún más si solo configura algunos miembros de la clase y permite que la superclase de la base de datos haga la creación de consultas en función de lo que envíe o establezca como variables de miembros. Por ejemplo:
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;
}
...
}
Ahora, cuando veas la load($id)
, getrow($query,$args)
y query($query,$args)
es completamente genérico. ''getrow () ''es solo un contenedor en la query()
que obtiene la primera fila, es posible que desee tener varias envolturas diferentes para interpretar el resultado de su declaración de diferentes maneras. También es posible que desee agregar envoltorios específicos de objetos a sus modelos si no se pueden hacer genéricos. Ahora el modelo, en tu caso, Blog
podría verse así:
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);
}
...
}
Úselo como tal: $blog = new Blog(123);
para cargar un blog específico, o $blog = new Blog(); $blog->title = "title"; ... $blog->save();
$blog = new Blog(); $blog->title = "title"; ... $blog->save();
si quieres una nueva