@JRCSalter I think it's easiest to give you some examples from Laravel while correcting some misconceptions.
Facades
The idea of not needing to remember long class names doesn't make sense, because you still include the class name when you resolve it e.g. App::resolve('Core\Database');
You've misunderstood what they mean. They're talking about facades in general, not resolving dependencies. I'll demonstrate what they mean with the Cache facade. Here's how it's used:
Cache::put('key', $value, 10);
You don't need to know the default cache driver, how the cache repositories are constructed, or what actual classes are being used under the hood. Hence, you don't need to remember long class names. You just use the facade.
You could also build the cache repository object yourself and use it, but that takes some learning and lines of code. And what if one day your team decides to move from the default file-based driver to something like Redis? How do you build a Redis repository? I don't know because I don't need to. With facades you don't need to touch your code at all. You change a couple of lines of config and the framework does the rest.
You don't need to use facades. But they can make your life a lot easier.
Service container
That's still going to be a problem with or without containers though, isn't it? The container is defined in the bootstrap.php file, and if you saved it as a variable, you could then use it in other files.
Most of the time you don't deal with the service container directly. You simply type hint what you need and the framework automatically resolves and injects the dependencies. Let's say your app has an SmsSender service. Once you've bound it to the service container, you can use it anywhere in your codebase:
class SomeController extends Controller {
public function send(Request $request, SmsSender $smsSender) {
// The $smsSender is automatically injected and you can use it here.
// You can also inject it in the constructor.
}
}
A service container also makes it easy to bind services as singletons, i.e. single instances that are reused. You suggested loading the config and creating a new DB connection object where needed. Imagine your web route has 10 middlewares, a controller and some service classes. With your method, you might have to open several separate DB connections on every request. With a service container you can reuse the same connection everywhere. (Not that you have do any of this yourself in Laravel)
Example of service binding
Here's a simplified real life example that demonstrates how the service container makes you write less code, and some other benefits. One of our projects is a fairly big web app with around 500 routes, ~150 controllers, and many jobs and commands. We have a unified way of reporting issues to the dev team. There's an ErrorReporter interface with a single report() method, something like this:
interface ErrorReporter {
public function report($someParameters);
}
Then we have an EmailErrorReporter class that implements that interface:
class EmailErrorReporter implements ErrorReporter {
public function __construct($someEmailParameters) {
...
}
public function report($someParameters) {
...
}
}
We'd bind that to the service container as the ErrorReporter service:
$this->app->singleton(ErrorReporter::class, function($app) {
return new EmailErrorReporter(...);
});
... and then use it anywhere as needed:
public function __construct(ErrorReporter $reporter) {
...
}
The reporter is used in maybe 50 classes. None of them have to know how the reporter is implemented. When we decided to switch from email reports to Slack notifications, we only had to create a new SlackErrorReporter class and bind that as the ErrorReporter service. We didn't have to touch any of the classes actually using the service.
Now imagine if we had manually constructed the EmailErrorReporter object everywhere instead of doing it once in the binding. It would've been annoying.
Inversion of control
I want to emphasize why it's almost always better to pass dependencies down to your classes rather than load them from the classes themselves. Your code actually shows an example this. It's this part:
$db = new Database( $config[ 'database' ] );
Configuration is passed to the constructor instead of the object loading it from some hard-coded location. It could also do that, but then it would be a crappy class because it would only work if you stored your config in a specific way in a specific place in the filesystem. You couldn't use the class in a separate script or project with a different structure without modifying it.
It's much better to pass dependencies down to classes. A top-down approach makes low-level classes independent and agnostic of the environment. If the classes load files from hard-coded locations themselves, then it's like they have these tentacles that hold on to parts of the environment that they depend on. And once you have hundreds or thousands of files like that, the whole thing becomes an entangled mess where you can't change simple things without breaking the app in 200 different places. I've dealt with codebases like that and it's not fun.