Skip to content

Instantly share code, notes, and snippets.

@mathiasverraes
Created May 30, 2018 14:17
Show Gist options
  • Save mathiasverraes/968a2c3ce2a05cca5cb47867b97f21e9 to your computer and use it in GitHub Desktop.
Save mathiasverraes/968a2c3ce2a05cca5cb47867b97f21e9 to your computer and use it in GitHub Desktop.
We don't need no DIC libs / we don't need no deps control
<?php
// Context: I'm trying to argue that DI (and DIC) are great, and DIC libs suck.
// Happy to be proven wrong!
final class Router {
private $dependencies;
public function __construct (Dependencies $dependencies) {
$this->dependencies = $dependencies;
// You might say that this is Service Locator, but it's not. This router is toplevel,
// and toplevel must have access to dependencies. After that it can all just bubble nicely using proper DI.
}
public function route(Command $cmd) {
switch(true) {
case $cmd instanceof RubPlumbus:
$this->dependencies->PlumbusRubber()->handle($cmd);
break;
case $cmd instanceof SpitOnPlumbus:
// $this->dependencies->SomeOtherService()->handle($cmd);
//.. etc
// So what it's just a dumb list of case and instanceof instead of clever autowiring. This takes no time to
// write and no time to understand and nobody will ever need to stackoverflow it to get it.
// This is all lazy by default, Dependencies only makes stuff we ask for and we only ask for stuff when we need
// it.
// For content based routing:
case $cmd instanceof PaintPlumbus:
if($cmd->isBlue()) {
// do specific blue stuff
} else {
// ...
}
break;
}
}
}
abstract class Dependencies {
private $environment;
function __construct (Environment $environment) {
// No dependency on a global mutable env, this is a value object.
$this->environment = $environment;
}
public function PlumbusRubber (): PlumbusRubber {
// Public, because this service is toplevel
return new PlumbusRubber($this->PlumbusRepository());
}
// Only the methods above this line are public, because I expose as few of them as possible to the outside. I'm not
// just letting you get any dep from anywhere. Symfony has this feature, but it's public by default there, which
// annoys me. I'm easily annoyed by public things that shouldn't be public.
protected function PlumbusRepository (): PlumbusRepository {
return $this->MySQLPlumbusRepository();
}
protected function MySQLPlumbusRepository (): MySQLPlumbusRepository {
// NOTE: Private! Never exposed outside
return new MySQLPlumbusRepository($this->DBConnection());
}
protected function DBConnection (): DBConnection {
// The others deps didn't sharing because they're stateless and fast and often only used once in a request.
// Here there is some side effect on instantiation so it makes sense to reuse
$environment = $this->environment;
return
$this->share(
__METHOD__, // used as key
function () use ($environment) {
return new DBConnection(
$environment->db_host(),
$environment->db_username()
/* ... */
);
}
);
}
private $shared = [];
protected function share ($key, callable $factory) {
// I sure wish PHP had generics :`-(
if ( ! array_key_exists($key, $this->shared))
{
$this->shared[$key] = $factory();
}
return $this->shared[$key];
}
}
final class ProductionDependencies extends Dependencies {
}
final class StagingDependencies extends Dependencies {
// A typical use case would be to have a mock Mailer so we can't send email from tests
}
final class TestingDependencies extends Dependencies {
// Overriding stuff we want to differently in unit tests
protected function PlumbusRepository (): PlumbusRepository {
return $this->InMemoryPlumbusRepository();
// Or use your favourite mocking lib if you must
}
}
// Benefits:
// * No XML! No YAML! No frameworks! Just plain old POPOs and tautologies!
// * No black magic autowiring
// * All in glorious typehinting
// * I can even write unit tests for the Dependencies class itself.
// * There is no naming convention you have to learn like "app.thing.some_underscord_thing". Methods are named
// after the interface or the class of the thing they return. Such easy, much wow.
// * I don't need IDE plugins to navigate the xml shit and detect missing services because it's all POPO!
// * If you think any of this involves work: it's less work than any DIC lib I've used, because it's the same amount
// of information as you'd have to write anyway with whatever configuration format they use. The only exception is
// autowiring, but you'll pay that back with itnerest in debugging time anyway.
// ignore below, just making all the GLORIOUS TYPEHINTING WORK FOR ME LIKE BOSS
interface Command {};
class RubPlumbus implements Command {}
class SpitOnPlumbus implements Command {}
class PaintPlumbus implements Command {
public function isBlue(): bool {return true;}
}
class PlumbusRubber {
function __construct (PlumbusRepository $plumbusRepository) {}
public function handle ($cmd) {}
};
interface Environment {
public function db_host () : string;
public function db_username () : string;
}
interface PlumbusRepository {}
final class MySQLPlumbusRepository implements PlumbusRepository
{
function __construct (DBConnection $dbConnection) {}
}
class DBConnection {
function __construct (string $host, string $username) {}
}
@hollodotme
Copy link

Thanks for sharing.

@GDmac
Copy link

GDmac commented Mar 27, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment