Your problem is that you are putting information in your layers that should not be there. Your Data Access layer does not need to know HOW it will be connected, they just need to know that there is a properly configured driver to get data from.
In the same way, you are giving your controllers responsibilities they doesn't need: creating models.
Why this is bad?
Imagine that for some reason, you change the dependencies of your model classes. Like if you now have to store data from the model in two different databases at the same time for redundancy.
How would you do this? Change all of your controllers and alter the way you instantiate models inside them?
That's a lot of work! And bug prone.
So, how can I solve this?
I like to do this using Dependency Injection Principle and the Factory Pattern.
Why a factory?
To decouple the creation of my Model from who uses it. My default model factory creates Models that requires only one database connection to work:
namespace MyApplication\Model\Factory;
use MyApplication\Storage\StorageInterface;
interface FactoryInterface {
public function create($modelClassName);
}
class WithStorage implements FactoryInterface {
const MODEL_NAMESPACE = '\\MyApplication\\Model\\';
private $storage;
public function __construct(StorageInterface $storage) {
$this->storage = $storage;
}
public function create($modelClassName) {
$refl = new \ReflectionClass(self::MODEL_NAMESPACE . $modelClassName);
try {
return $refl->newInstance($this->storage);
} catch (\ReflectionException $e) {
throw new RuntimeException("Model {$modelClassName} not found");
}
}
}
Now, for your model classes that requires a storage, you can create a structure like this:
namespace MyApplication\Storage;
interface StorageInterface {
public function insert($container, array $data);
public function update($container, array $data, $condition);
// etc..
}
class PdoStorage implements StorageInterface {
private $dbh;
public function __construct(\PDO $dbh) {
$this->dbh = $dbh;
}
public function insert($container, array $data) {
// impl. omitted
}
public function update($container, array $data, $condition) {
// impl. omitted
}
}
So, if you have the following class:
namespace MyApplication\Model;
use MyApplication\Storage\StorageInterface;
// Entity impl. will be omitted for brevity.
use MyApplication\Entity\EntityInterface;
abstract class AbstractApplicationModel {
private $containerName;
private $storage;
protected function __construct($name, StorageInterface $storage) {
$this->containerName = (string) $name;
$this->storage = $storage;
}
public function save(EntityInterface $entity) {
// impl. omitted
}
public function delete(EntityInterface $entity) {
// impl. omitted
}
}
class UserModel extends AbstractApplicationModel {
public function __construct(StorageInterface $storage) {
parent::__construct('users', $storage);
}
}
With this, we solve our problem with the coupling within the model. Ok.
So, with all this, how can I get a Model component that is ready to store my data from my controller?
Directly:
namespace MyApplication\Controller;
use MyApplication\Model\UserModel;
use MyApplication\Storage\PDOStorage;
use MyApplication\Entity\User;
class UserController {
public function onCreate() {
$model = new UserModel(new Storage(new \PDO(...))); // Here's our problem
$entity = User::createFromArray([
'name' => 'John',
'surname' => 'Doe',
]);
try {
$model->save($entity);
} catch (Exception $e) {
echo 'Oops, something is wrong: ' . $e;
}
}
}
If you have just one model to deal in your whole application, you are ready to go. But you have several of them, then you will have problems.
What if I no longer want to use PDO as my storage driver and work with MySQLi? What if I don't want to use a RDBMS anymore, instead I want to store my data in plain text files?
This way, you'd have to change the implementation of all controllers (which violates the OCP). Lots of repetitive work to do. I hate this!
Oh wait! We have the freaking factory to create models for us! So, let's use it!
namespace MyApplication\Controller;
use MyApplication\Model\Factory\FactoryInterface;
use MyApplication\Entity\User;
abstract class AbstractController {
private $modelFactory;
public function __construct(ModelFactory $factory) {
$this->modelFactory = $factory;
}
public function getModelFactory() {
return $this->modelFactory;
}
}
class UserController {
public function onCreate() {
$model = $this->getModelFactory()->create('UserModel'); // now it's better
$entity = User::createFromArray([
'name' => 'John',
'surname' => 'Doe',
]);
try {
$model->save($entity);
} catch (Exception $e) {
echo 'Oops, something is wrong: ' . $e;
}
}
}
Now, to have all this working together, you have to do some setup on your bootstrap/front controller:
use MyApplication\Storage\PDOStorage;
use MyApplication\Model\Factory\WithStorage as ModelFactory;
$defaultPDODriver = new \PDO(...);
$defaultStorage = new PdoStorage($defaultPDODriver);
$defaultModelFactory = new ModelFactory($defaultStorage);
$controller = new UserController($defaultModelFactory);
Now, what do I do if I want to change my storage engine to plain text files?
$defaultStorage = new PlainFileStorage('/path/to/file'); // just this
Now, what do I do if I want to change my storage engine to one custom implementation that have 2 distinct database to save the same data?
$master = new PdoStorage(new \PDO(...));
$slave = new PdoStorage(new \PDO(.......));
$defaultStorage = new RedundancyStorage($master, $slave);
See? Now the way you store information is irrelevant to your models.
The same way, if you have some crazy business logic that changes the way your models do things based on the setup, you can also change your model factory:
$defaultModelFactory = new SomeCrazyModelFactory(...);
Your controllers are not even aware that your models changed (of course, you'd have to respect the same interface to be allowed to do this interchangeably).
That's a possible way, it's more or less how I do, but there are a few other possibilities.