I could be waaaaaay off, but...
It seems that a model is necessary to use the library, you just don't care which one, right? Further, the library doesn't care how the model gets its data, or where the data comes from, it just wants an object that meets the criteria of the interface.
If the library doesn't need the connection object, then make that the responsibility of the model.
interface MyFooModelInterface
{
public function getItem($id);
public function addItem($item);
...
}
And here is a simple class example:
class MyLib
{
protected $model;
public function __construct(MyFooModelInterface $model)
{
$this->model = $model;
}
public function getItemById($id)
{
return $this->model->getItem($id);
}
}
And a Model example:
class MyModel implements MyFooModelInterface
{
protected $conn;
public function __construct(\PDO $conn)
{
$this->conn = $conn;
}
public function getItem($id)
{
...
}
public function addItem($item)
{
...
}
}
And here is an example of using it:
// the model is responsible for handling the data retrieval, not the library
$model = new Model($conn);
$lib = new MyLib($model);
return $lib->getItem($id);
Now, if I decide that I want a file-based solution, I can easily swap the model. To test the model, the connection obj is injected, so that's easy. To test the library, the model is also injected, so, again, that's easy.
In this case, the model is only responsible for getting the data, while the library is responsible for handling and processing the data. Neither cares what or how the other one does it.
EDIT
the library with a factory method to create the model if none was given...
class MyLib
{
protected $model;
public function __construct(MyFooModelInterface $model = null, $conn = null)
{
// if we have a model, use it
if (!is_null($model)) {
$this->model = $model;
}
// if we don't have a model, but have a connection obj, make a model
if (is_null($model) && !is_null($conn)) {
$this->model = $this->factoryModel($conn);
}
// if we don't have either, quit and go home
if (is_null($model) && is_null($conn)) {
throw new Exception('need moar inputs');
}
}
protected factoryModel($conn)
{
$this->model = new MyModel($conn);
}
public function getItemById($id)
{
return $this->model->getItem($id);
}
}
Indeed, you could have been using PDO directly. The only real thing it does is to rethrow PDOException
as RunTimeException
, a practice which should be avoided at all costs.
This being said, PDO doesn't keep you from having to create special adapters for a given database, since different databases have different syntax and functionality which cannot possibly be handled by PDO. It works for simple stuff like PDOStatement::rowCount()
, but PDO won't help you if, for example, you need to LIMIT/OFFSET
the number of results in MySQL and Microsoft SQL, forcing you to write LIMIT/OFFSET
query for one and WHERE rowNumber BETWEEN ... AND ...
for another.
Moreover, the presence of PDO doesn't mean you shouldn't have a database layer either: your business layer shouldn't call PDO directly.
Example
If a website needs to display the number of users, business layer will call database layer IDatabase
similarly to this:
$countUsers = $this->data->countAllUsers();
Then, you'll have an implementation for Microsoft SQL Server:
class SqlServerDatabase implements IDatabase
{
...
public function countAllUsers()
{
$query = 'select count(1) from [Community].[User]';
$statement = $this->connection->prepare($query);
$statement->execute();
return $statement->fetchColumn();
}
...
}
and another one for MySQL:
class SqlServerDatabase implements IDatabase
{
...
public function countAllUsers()
{
$query = 'select count(*) from `user`';
$statement = $this->connection->prepare($query);
$statement->execute();
return $statement->fetchColumn();
}
...
}
If you have a lot of table-counting, you may want to refactor that to reduce code duplication. Without PDO, you won't be able to do that. With PDO, you can:
abstract class DatabaseCommon
{
protected function countAllRows(TableName $tableName, $preferOneToAsterisk = false)
{
$query = $preferOneToAsterisk ?
'select count(1) from ' . $tableName->sanitize() :
'select count(*) from ' . $tableName->sanitize();
$statement = $this->connection->prepare($query);
$statement->execute();
return $statement->fetchColumn();
}
}
class SqlServerDatabase extends DatabaseCommon implements IDatabase
{
public function countAllUsers()
{
return $this->countAllRows(
new MicrosoftSqlTableName('Community', 'User'),
true
);
}
}
class SqlServerDatabase extends DatabaseCommon implements IDatabase
{
public function countAllUsers()
{
return $this->countAllRows(new MySqlTableName('user'));
}
}
Best Answer
It feels like Layer 3, the Database Request layer needs this. First, to be honest, the layering could use fixing. If you have "layers" and the question "which layer does this go in?" is not easily answered then you either don't have enough layers, or don't have the right layers.
What you have:
Database layer
Database connection layer
Database Request Layer
(this is your problem) Database Model Layer
(and so is this) Model Layer
Layer 3, the Database Request Layer is where your Query Builder belongs. Nothing outside of that layer should be doing ad hoc queries. Really, Layer 3 is more commonly referred to as the Data Access Layer (DAL) or Repository Layer. This is the layer that knows about the database. It should even know about the factories required to generated your entity objects.
Think of the DAL or Repository as a coordinating layer between two sub layers: The Database Request and Entity Factory layers.
What you really need:
Your entity classes should have absolutely no knowledge about persistence. It's the Repository/DAL that coordinates between multiple layers underneath - including your query builder.
Nothing outside the Repository/DAL should have anything to do with the query builder. This is a utility for making the construction of SQL queries easier. Instead, you can provide a Data Transfer Object (DTO) that will contain all the possible criteria values if you need to perform a search. The Repository/DAL should have a method that accepts this criteria DTO as an argument, and then uses the DTO to build a query using the Query Builder.
From that, something else has to translate the query builder object into a request to the database. You can create another class called a Query Executor which translates the Query Builder and creates a DB request, or just pass the query builder directly to the DB request object. Yeah you "break the separation of layers" but how much coupling have you really introduced? It's up to you.
Once your application reaches the complexity where you have a question like this, you need to invest your time in learning and configuring a good Object Relational Mapper (ORM).
Yes, but generally only your controllers and service classes will need access to repository objects.
A little be "Yes" and a little bit "No".
If you use Dependency Injection then the DI container or a factory object that churns out controllers will instantiate the repository.
If you introduce interfaces for your repository, you can have both:
The constructor for the controller accepts an
IFooRepository
as an argument. Ifnull
it defaults to theFooRepository
concrete implementation. Now, you can actually unit test your controllers by providing a mock or stub of theIFooRepository
interface.