HiveBrain v1.2.0
Get Started
← Back to all entries
patternphpMinor

Slim API with 3 layer architecture

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
layerarchitecturewithapislim

Problem

I'm new to Slim and the three layered approach I'm using below. So far I have the API, a Presentation layer (leaving out for now), a Business Logic Layer, and a Data Access Layer. The code's working, but I know it can be improved a lot. I'd really appreciate some feedback from you all.

A few things I'm questioning already:

  • Do I really need to create a new BLL object for each method in the API?



  • In the DAL I'm sure I shouldn't be connecting to the database in each method. What would be a better approach?



  • With the Slim routes, it seems like /calendars/:id is getting redundant. Instead of always passing the ID up to the BLL then to the DAL, perhaps a single session variable in the DAL would be cleaner?



Of course the code below has been trimmed to keep it short, but hopefully you get the idea.

Slim API

```
true
));

$app->log->setEnabled(true);

$app->group('/v1', function () use ($app) {
$app->get('/calendars/:id/events', 'getEvents');
$app->get('/calendars/:id/event/:eid', 'getEvent');
$app->get('/calendars/:id/users(/:type)', 'getUsers');
$app->get('/calendars/:id/categories', 'getCategories');
$app->get('/calendars/:id/locations', 'getLocations');
$app->get('/calendars/:id/holidays', 'getHolidays');
$app->post('/calendars/:id/categories', 'addCategory');
});

$app->run();

function getHolidays ($id) {
$bll = new BusinessLayer();
$result = json_encode($bll->getHolidaysBLL($id));
echo '{"holidays": ' . $result . '}';
}
function getUsers ($id, $type = '') {
$bll = new BusinessLayer();
$result = json_encode($bll->getUsersBLL($id, $type));
echo '{"user": ' . $result . '}';
}

function getEvents ($id) {
$bll = new BusinessLayer();
$result = json_encode($bll->getEventsBLL($id));
echo $result;
}

function getEvent ($id, $eid) {
$bll = new BusinessLayer();
$result = json_encode($bll->getEventBLL($id, $eid));
echo '{"event": '$result . "}";
}

function addCategory ($id) {
$request

Solution

I've seen this layering before, in fact I'm working on a .NET project with this exact layering (DataAccessLayer and BusinessLayer). The BusinessLayer object does absolutely nothing. I've seen this pattern in use many times and it introduces an additional class that adds no value. I would just use the DataAccessLayer class.

Besides, your BusinessLayer class doesn't actually contain any business logic.

The DataAccessLayer class looks mostly fine, except I would drop the "DAL" suffix on all the method names. The class name takes care of telling you its the "Data Access Layer".

The exception handling needs some reworking. In fact, if a PDOException gets thrown, I would just let it go. Catch it in your Slim controller methods and echo the error JSON. Don't do that in your DAL. And please please please please please don't swallow errors. Log them some place --- but not in the Data Access Layer.

function addCategory ($id) {
    $logger = new Logger(); // request();
        $category = json_decode($request->getBody());       
        $bll = new BusinessLayer();
        $result = json_encode($bll->addCategoryBLL($category->cat_name, $category->cat_color, $id));
        echo $result;
    }
    catch (PDOException $ex) {
        $logger->error($ex);
        echo '{"error": "' . $ex->getMessage() . '"}';
    }
}

class Logger {
    public function error(Exception $ex) {
        // log error message and stack trace
    }
}


Separation of Concerns

@mikehomme commented:


(The business layer will) handle a lot of things such as validation, authentication, usage limits, etc.

Just those three things should be three additional layers in your application. In fact, authentication and rate limiting (usage limits) are great candidates for Slim PHP middleware. Your controller functions shouldn't even get invoked in the first place if the user is not authenticated, plus you would be repeating all of the authentication logic in each controller function. Don't Repeat Yourself (DRY) can be adhered to if you insert authentication and rate limiting in the middleware tier of your application.

Now your routes become:

$app->post('/calendars/:id/categories', 'authenticateRequest', 'enforceUsageLimit', 'addCategory');


The middleware functions:

function authenticateUser() {
    // Get auth token from query string
    // Query DB for user

    if (!$valid) {
        $app->redirect(...);
    }
}

function enforceUsageLimit() {
    // Find user in DB
    // Increment request count

    if ($numberOfRequests > $limit) {
        $app->redirect(...);
    }
}


Validation

Validation deserves a little more explanation. First, I would recommend against using plain old Arrays to hold you data. There are good O/RM's out there for PHP so you can deal with concrete classes. Your Data Access Layer should be returning Domain Models. A Domain Model is a class that represents a row in one table of the database. It gives you the ability to bundle data with behavior in a modular fashion. The validator class should handle validating the whole domain model object and keeping track of the error messages.

Putting your validation rules in it's own class or classes allows you to use the validation rules in multiple contexts (or multiple controller functions). It becomes an extra if statement, but is necessary in every MVC style framework I've used (Ruby on Rails, Codeigniter, ASP.NET MVC).

First, the domain model: Category

Category Domain Model

class Category {
    private $name;
    private $color;
    private $id;

    public function Category($data = array()) {
        foreach ($data as $key => $value) {
            $this->$key = $value;
        }
    }

    public function getName() {
        return $this->name;
    }

    public function setName($value) {
        $this->name = $value;
    }

    public function getColor() {
        return $this->color;
    }

    public function setColor($value) {
        $this->color = $value;
    }

    public function getId() {
        return $this->id;
    }

    public function setId($value) {
        $this->id = $value;
    }
}


This class represents a row in the "categories" table in the database. It takes a hash of key value pairs to populate its properties in the constructor. It has little more than getters and setters right now, but if you need to add functionality that concerns categories, this is the class to add it (related reading: Domain Driven Design)

Now the validation classes:

```
class BaseValidator {
private $errors;

public function __construct() {
$this->errors = array();
}

public function addError($key, $message) {
if (empty($this->errors[$key])) {
$this->errors[$key] = array();
}

$this->errors[$key][] = ucfirst($key) . ' ' . $message;
}

public function getErrors() {
return $this->errors;
}

public function hasErrors() {
return !empty($this->errors);

Code Snippets

function addCategory ($id) {
    $logger = new Logger(); // <- You will have to implement this

    try {
        $request = \Slim\Slim::getInstance()->request();
        $category = json_decode($request->getBody());       
        $bll = new BusinessLayer();
        $result = json_encode($bll->addCategoryBLL($category->cat_name, $category->cat_color, $id));
        echo $result;
    }
    catch (PDOException $ex) {
        $logger->error($ex);
        echo '{"error": "' . $ex->getMessage() . '"}';
    }
}

class Logger {
    public function error(Exception $ex) {
        // log error message and stack trace
    }
}
$app->post('/calendars/:id/categories', 'authenticateRequest', 'enforceUsageLimit', 'addCategory');
function authenticateUser() {
    // Get auth token from query string
    // Query DB for user

    if (!$valid) {
        $app->redirect(...);
    }
}

function enforceUsageLimit() {
    // Find user in DB
    // Increment request count

    if ($numberOfRequests > $limit) {
        $app->redirect(...);
    }
}
class Category {
    private $name;
    private $color;
    private $id;

    public function Category($data = array()) {
        foreach ($data as $key => $value) {
            $this->$key = $value;
        }
    }

    public function getName() {
        return $this->name;
    }

    public function setName($value) {
        $this->name = $value;
    }

    public function getColor() {
        return $this->color;
    }

    public function setColor($value) {
        $this->color = $value;
    }

    public function getId() {
        return $this->id;
    }

    public function setId($value) {
        $this->id = $value;
    }
}
class BaseValidator {
    private $errors;

    public function __construct() {
        $this->errors = array();
    }

    public function addError($key, $message) {
        if (empty($this->errors[$key])) {
            $this->errors[$key] = array();
        }

        $this->errors[$key][] = ucfirst($key) . ' ' . $message;
    }

    public function getErrors() {
        return $this->errors;
    }

    public function hasErrors() {
        return !empty($this->errors);
    }
}

Context

StackExchange Code Review Q#93914, answer score: 7

Revisions (0)

No revisions yet.