No, the kind of code your end-user would write probably should not require knowledge of the BankManager’s dependencies.
Dependency injection almost always comes hand-in-hand with a dependency injection container. The container would manage object creation, configuration, lifetime, and dependencies. And you could even use it as the new entry point of your application.
<?php
namespace BankModule;
class ServiceContainer
{
// This replaces singletons, globals, statics, etc.
// Any instance that needs to be reused throughout your application will be saved here.
private $services = [];
public function getBankManager()
{
// This "if not set" guard looks very similar to the singleton pattern, and indeed it is.
// It's still necessary to restrict some objects to just a single instance.
// The crucial difference is that there's no statics, no global state.
if (!isset($this->services['bankManager'])) {
$this->services['bankManager'] = new BankManager($this->getAccountManager());
}
return $this->services['bankManager'];
}
public function getAccountManager()
{
if (!isset($this->services['accountManager'])) {
$this->services['accountManager'] = new AccountManager();
}
return $this->services['accountManager'];
}
}
And your end-users would use it this way:
$bankServiceContainer = new BankModule\ServiceContainer();
$bankManager = $bankServiceContainer->getBankManager();
// ...
The latter is more adaptable to change, which is a good thing, because software requirements change frequently. Good software architecture yields a long term payoff. Over the years, as you add, remove, and change features, good architecture makes those changes easy. Bad architecture increases the likelihood of hacks, bugs, inconsistencies, and all-around spaghetti-ness.
For me, good design means that when I make a change, it’s as if the entire program was crafted in anticipation of it. I can solve a task with just a few choice function calls that slot in perfectly, leaving not the slightest ripple on the placid surface of the code.
In the former example, your ShoppingCart class is hardcoded to use BankManager. It can’t swap in any alternative implementation. Also, your ShoppingCart has to know how to create a BankManager, which requires knowledge of, and access to, all of BankManager’s dependencies. That’s something ShoppingCart doesn’t need to concern itself with.
Instead, we resolve the dependencies in the container:
class ServiceContainer
{
// ...
public function getShoppingCart()
{
// No "if not set" guard this time.
// Remember, the container manages object lifetime.
// It's responsible for knowing whether you need to reuse an instance,
// or whether you need to create a fresh instance with each request.
return new ShoppingCart($this->getBankManager());
}
}