Created
May 30, 2018 14:17
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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) {} | |
} |
Thanks for sharing.
Running example (it's blue)
https://gist.github.com/GDmac/91f986ba5d656d36baae0a9254ef38c5/104e55b5e67d98f407303efd87a2590325c8af5b
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I do like the mix of a developer, PHP, and 18yo Whiskey.