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

3

15

I have the following example in which I tend to use a couple of classes, to create a simple web app.

The file hierarchy seems like this.

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

The index.php just calls the bootstrap.php which in turn contains something like this:

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

As you can probably tell, my problem is this line:

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

Which I don't want to use, and instead try a " Domain Object Model " approach.

Now, given that I will add another folder called domain, with blog.php

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

And fill blog.php with properties mapping table rows, and getter and setters ..

namespace App\Domain; 

class Blog {

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

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

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

    ...
}

My question is: Assuming my understanding of DOM is so far correct, and that I have a CRUD/ORM class, or a PDO wrapper to query the database;

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

Martingale answered 17/7, 2015 at 17:43 Comment(4)
Props for using slim ;) Best framework ever.Hiawatha
@JohnHunt error... I was building Fastpress when I asked that. :)Martingale
Fair enough, looks very similar to slim 3's routing...whatever that is.Hiawatha
@JohnHunt Yeah, it's my little awesome framework. The local version is way a head than the one in github. But yeah, it's similar to slimMartingale
P
14

As far as a Domain Object you basically already have written one, your blog object. To qualify as a domain model all a class must to is to provide a representation along with any of the functionality of a concept within your problem space.

The more interesting problem here and the one you appear to be struggling with is how to persist a domain model. Keeping with the tenet of the single responsibility principle your Blog class should deal with being a blog post and doing the things that a blog post can do, not storing one. For that you would introduce the concept of a repository of blog posts that would deal with storing and retrieving objects of this type. Below is a simple implementation of how this can be done.

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

Then your controller should look something like this.

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

To truly be SOLID the above class should be a database specific implementation of a BlogRepository interface to adhere to IoC. A factory should also probably be supplied to BlogRepository to actually create the blog objects from data retrieved from the store.

In my opinion one of the great benefits of doing this is you have a single place where you can implement and maintain all of your blog related interactions with the database.

Other Advantages to this method

  • Implementing caching for your domain objects would be trivial
  • Switching to a different data source (from flat files, blogger api, Document Database Server,PostgresSQL etc.) could be done easily.

You can alternatively use a type aware ORM for a more general solution to this same problem. Basically this Repository class is nothing more than a ORM for a single class.

The important thing here is that you are not talking directly to the database and leaving sql scattered throughout your code. This creates a maintenance nightmare and couples your code to the schema of your database.

Peltier answered 17/7, 2015 at 19:43 Comment(3)
Thanks, but as I said in the chatroom, this implementation seems bloated and contrived and may certainly violate the SOLID principles also. It would be nice to see a better implementation, if there is any.Martingale
I don't see why you feel the implementation is bloated... you are doing the same thing here as you would if you created the object in your dispatcher function on the route.. but in the event of a schema change I could make all my changes in one place.Peltier
I also can't see why anyone would say this approach violates SOLID principles. This is the exact approach many developers, wise to SOLID, would take.Langobard
E
4

Personally I always tend to stick the database operations in a database class which does all the heavy lifting of initialising the class, opening the connection etc. It also has generic query-wrappers to which I pass the SQL-statements which contains the normal placeholders for the bound variables, plus an array of the variables to be bound (or the variable number of parameters approach if thats suits you better). If you want to bind each param individually and not use the $stmt->execute(array()); You just pass in the types with the value in a data structure of your choosing, multi dim array, dictionary, JSON, whatever suits your needs and you find easy to work with.

The model class it self (Blog in your case) then subclasses the Database. Then you have a few choices to make. Do you want to use the constructor to create only new objects? Do you want it to only load based on IDs? Or a mix of both? Something like:

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
    }
}

Maybe neither? You can skip the constructor and use the new(title, ingress, body), save() and a load(id) methods if thats your preference.

Of course, the query part can be generalised even further if you just configure some class members and let the Database-superclass do the query building based on what you send in or set as member-variables. For example:

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;
    }
    ...
}

Now as you see the load($id), getrow($query,$args) and query($query,$args) is completely generic. ´getrow()´is just a wrapper on query() that gets the first row, you may want to have several different wrappers that to or interpret your statement result in different ways. You may also even want to add object specific wrappers to your models if they cannot be made generic. Now the model, in your case Blog could look like:

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

Use it as so: $blog = new Blog(123); to load a specific blog, or $blog = new Blog(); $blog->title = "title"; ... $blog->save(); if you want a new.

Egg answered 24/7, 2015 at 8:40 Comment(4)
Unless you blog is a type of database adapter it shouldn't extend database. This violates the Single Responsibility tenet of S.O.L.I.D.Peltier
@Peltier Hm... That design-pattern was new to me ;)Egg
You want a high level overview of the concepts check this outPeltier
Guess I have been doing a lot of it for years, but not known the names or the principles specification. I have never went to the extremes though, so no lasagna-code, but I dont think much spaghetti either :) Thanks for the link, it was a good view. Gives the mind something to ponder.Egg
E
3

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

To tie the two together, you could use an object-relational mapper (ORM). ORM libraries are built just for glueing your PHP classes to database rows. There are a couple of ORM libraries for PHP around. Also, most ORMs have a built in database abstraction layer, which means that you can simply switch the database vendor without any hassle.

Considerations when using an ORM:
While introducing a ORM also introduces some bloat (and some learning), it may not be worthwhile investing the time for simply a single Blog object. Although, if your blog entries also have an author, one or multiple categories and/or associated files, an ORM may soon help you reading/writing the database. Judging from your posted code, an ORM will pay off when extending the application in the future.


Update: Example using Doctrine 2

You may have a look at the querying section of the official Doctrine documentation to see the different options you have for read access. Reconsider the example you gave:

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

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

However, ideally you define your own repository to separate your business logic from Doctrines API like the following example illustrates:

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

I hope the example illustrates how easily you can separate your business domain models and logic from the underlying library and how powerful ORMs can come into play.

Eclat answered 26/7, 2015 at 17:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.