PHP Lazy loading objects and dependency injection

Ivan Pintar picture Ivan Pintar · Mar 29, 2012 · Viewed 10.4k times · Source

I have recently started reading about dependency injection and it has made me rethink some of my designs.

The problem i have is something like this: Let's say i have two classes: Car and Passenger; For those two classes i have some data mappers to work with the database: CarDataMapper and PassengerDataMapper

I want to be able to do something like this in code:

$car = CarDataMapper->getCarById(23); // returns the car object
foreach($car->getPassengers() as $passenger){ // returns all passengers of that car
    $passenger->doSomething();
}

Before I knew anything about DI, I would build my classes like this:

class Car {
    private $_id;
    private $_passengers = null;

    public function getPassengers(){
        if($this->_passengers === null){
            $passengerDataMapper = new PassengerDataMapper;
            $passengers = $passengerDataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        }
        return $this->_passengers;  
    }

}

I would also have similar code in the Passenger->getCar() method to fetch the car the passenger is in.

I now understand that this creates dependencies (well, I understood it before too, but I wasn't aware that this is "wrong") between the Car and the Passenger objects and the data mapper objects.

While trying to think of the solution for this two options came to mind, but I don't really like any of them:

1: Doing something like this:

$car = $carDataMapper->getCarById(23);
$passengers = $passengerDataMapper->getPassengersByCarId($car->getId());
$car->setPassengers($passengers);

foreach($car->getPassengers() as $passenger){
    $passenger->doSomething();
}

But what if passengers have objects that they need injected, and what if the nesting goes to ten or twenty levels... I would wind up instantiating nearly every object in the start of my application, which would in turn query the entire database during the process. If i have to send the passenger to another object which has to do something with the objects that the passenger holds, I do not want to immediately instantiate these objects too.

2: Injecting the data mappers into the car and passenger objects and having something like this:

class Car {
    private $_id;
    private $_passengers = null;
    private $_dataMapper = null;

    public function __construct($dataMapper){
        $this->setDataMapper($dataMapper);
    }

    public function getPassengers(){
        if($this->_passengers === null && $this->_dataMapper instanceof PassengerDataMapper){
            $passengers = $this->_dataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        }
        return $this->_passengers;  
    }
}

I dont like this any better, because it's not like the Car is really unaware of the data mapper, and without the data mapper, the Car could behave unpredictably (not returning passengers, when it actually has them)

So my first question is: Am I taking a completely wrong approach here, because, the more I look at it, the more it looks like I'm building an ORM, instead of a business layer?

The second question is: is there a way of actually decoupling the objects and the data mappers in a way that would allow me to use the objects as described in the very first code block?

Third question: I've seen some answers for other languages (some version of C, I think) resolving this issue with something like this described here: What is the proper way to inject a data access dependency for lazy loading? As I haven't had time to play with other languages, this makes no sense to me, so I'd be grateful if someone would explain the examples in the link in PHP-ish.

I have also looked at some DI frameworks, and read about DI Containers and Inversion of Control, but from what I understood they are used to define and inject dependencies for 'non dynamic' classes, where for instance, the Car would depend on the Engine, but it would not need the engine to be loaded dynamically from the db, it would simply be instantiated and injected into the Car.

Sorry for the lengthy post and thanks in advance.

Answer

aletzo picture aletzo · Mar 29, 2012

Maybe off-topic, but I think that it will help you a bit:

I think that you try to achieve the perfect solution. But no matter what you come up with, in a couple of years, you will be more experienced and you'll definitely be able to improve your design.

Over the past years with my colleagues we had developed many ORMs / Business Models, but for almost every new project we were starting from scratch, since everyone was more experienced, everyone had learned from the previous mistakes and everyone had come across with new patterns and ideas. All that added an extra month or so in development, which increased the cost of the final product.

No matter how good the tools are, the key problem is that the final product must be as good as possible, at the minimum cost. The client won't care and won't pay for things that can't see or understand.

Unless, of course, you code for research or for fun.

TL;DR: Your future self will always outsmart your current self, so do not overthink about it. Just pick carefully a working solution, master it and stick with it until it won't solve your problems :D

To answer your questions:

  1. Your code is perfectly fine, but the more you will try to make it "clever" or "abstract" or "dependency-free", the more you will lean towards an ORM.

  2. What you want in the first code block is pretty feasible. Take a look at how the Doctrine ORM works, or this very simple ORM approach I did a few months ago for a weekend project:

https://github.com/aletzo/dweet/blob/master/app/models