Large applications without ORM?

And the problem is I find it hard to define a responsibility of a product entity object - is it to store and provide data about a product? If yes then what data? Plain data as coming from the database? Or also data calculated on the fly? Only from the same entity (e.g. database row) or related rows too? If an accessor method contains any arguments does it mean it violates SRP?

OK, let’s consider this approach - how do I strip it out? For example, I have getFinalPriceForCustomer(), which depends on an external value. One solution I came up with:

class PriceCalculator {
    public function __construct($customer_type) {
        // ...
    }
    
    public function calculate($net_price) {
        // ...
    }
}

class Product {
    public function getFinalPriceForCustomer($customer_type) {
        $c = new PriceCalculator($customer_type);
        return $c->calculate($this->net_price);
    }
}

The problem is the Product entity still remains littered with the same methods, except the implementation has been moved outside. I could get rid of getFinalPriceForCustomer() entirely but then maintainability of the calling code becomes harder. Imagine I have many instances of this throughout my code:

// there's just one way of calculating the final price, no dependencies
$price = $product->getFinalPrice();

Later I decide to implement different prices for different customers so I can’t just add a customer type argument into the method calls but I need to change them into something like this:

$c = new PriceCalculator($customer->type);
$price = $c->calculate($product->net_price);

And one more example, very common among data mapper implementations - what about getting related entities? For example:

$product->getOpinions();

This is a very convenient way of grabbing data from a related table, but doesn’t it violate SRP in the same way as $product->getFinalPrice($customer_type)? If we eager-load all related data with the mapper we may say it doesn’t since the opinions can be thought of as a collection of data belonging to the product. But if we lazy-load then getOptions() needs to fetch data from a database (with a mapper) so we would need to have this:

$product->getOpinions($mapper);

and in this way we add persistence as another responsibility to the entity, leading to an Active Record pattern, which violates SRP.

As you can see I find it hard to specify what a single responsibility is for an entity object and there seems to be no clear cut rule where one responsibility ends and another begins. This needs a very disciplined programmer not to cross the lines easily.