It doesn't really matter what kind of code you are developing, be it business logic or framework logic (or another kind of logic), the entire point here is how you deal with class dependencies.
The term business logic itself is very abstractive. You can represent a business logic with a single class (a.k.a domain object) or you can represent a business logic as a layer.
One basic thing to note: Database is just a storage engine
When you develop any application, you should keep in mind that database can be changed (or you could migrate to NoSQL solution in the future). And when/if you do, then $pdo
is no longer a dependency. If you had storage logic totally decoupled from business one, then it will be easy enough to replace a storage engine. Otherwise you end up rewriting a lot of things when changing it.
A properly designed architecture encourages to decouple storage logic from application logic. Those patterns are established as a best practice: Data Mapper or Table Gateway
namespace Storage\MySQL;
use PDO;
abstract class AbstractMapper
{
protected $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
}
class UserMapper extends AbstractMapper
{
private $table = 'cms_users';
public function fetchById($id)
{
$query = sprintf('SELECT * FROM `%s` WHERE `id` =:id', $this->table);
$stmt = $this->pdo->prepare($query);
$stmt->execute(array(
':id' => $id
));
return $stmt->fetch();
}
}
So in this case, for the current storage engine $pdo
is a core dependency and it's not a dependency of a framework or the application you're developing. The next problem you should solve here is how to automatize this process of passing $pdo
dependency to mappers. There's only one solution you can take advantage of - Factory pattern
$pdo = new PDO();
$mapperFactory = new App\Storage\MySQL\Factory($pdo);
$mapperFactory->build('UserMapper');
Now let's see obvious benefits:
First of all, it's readability - anyone seeing the code will get the clue, that Mapper is being used to abstract table access. Second you can easily replace a storage engine (if you plan to migrate in future and add several databases support)
$mongo = new Mongo();
$mapperFactory = new App\Storage\Mongo\Factory($mongo);
$mapperFactory->build('UserMapper');
Note: All mappers for different storage engines should implement one interface (API enforcement)
Model should not be one class
When it comes to web, we basically do the following:
- Rendering a form (that might look like Contact Page, Login form etc)
- Then submitting a form
- Then comparing against our validation rules
- On success we insert form data into a storage engine, on failure we display error messages
So when you implement a model as a class, then you end up writing validation and storage logic in the same class, therefore tight-coupling and breaking the SRP.
One of this common example, you can see in Yii Framework
A proper model should be a folder of classes containing application logic (see in ZF2 or SF2).
And finally, should you really use DiC?
Should you use a DI container when developing a code? Well, let's look at this classical code example:
class House
{
public function __construct($diContainer)
{
$this->door = $diContainer->getDoor();
$this->window = $diContainer->getWindow();
$this->floor = $diContainer->getFloor();
}
}
$house = new House($di)
In this case you tell that your House
depends on DiC
and not explicitly on doors, window and floor. But wait does our House
really depend on DiC? Surely it doesn't.
And when you start testing your class, you would have also to provide a prepared DiC (which is completely irrelevant)
Usually people use Di containers in order to avoid injection all the time. But from another side, it costs some RAM and some time to parse since most DI containers are configuration based.
Good architecture can be free of any Di container, since it takes advantage of Factories and the SRP.
So good application components should be free of any Service Locators and Di Containers.