"New" is Not Your Enemy!
Stephan Hochdörfer // October 13, 2016
Good evening everybody. Thanks for joining me on my first nomadphp session ever.
Before telling you what not to do with your DI container allow me to introduce myself.
Hi, I am Stephan Hochdörfer
@shochdoerfer
I work for bitExpert AG
We are a technology company focusing on web and mobile projects for our clients.
My current role is called Head of Technology.
Technical consultant
I do support our development teams and help them if they run into technical problems.
Knowledge transfer
I organize tutorials and workshops and organize and host our yearly company conference.
unKonf host & organiser
Besides organising our internal conference once a year I also organize and host our public unconference
called "the unKonf".
I speak at conferences (a lot)
I speak a lot at conferences. Actually that is not true any more. I did fewer conferences this year than
previous years.
Co-Org. of PHP UG FFM
Co-Org. of PHP UG MRN
So that is me. Lets dig into some code...
10 years ago we did this...
< ?php
class ProductController extends Controller
{
public function index ()
{
$service = new ProductService ();
$this -> render (
'index' ,
array (
'products' => $service -> readAllProducts ()
)
);
}
}
I feel we PHP developers tend to live an extreme life.
We actually that is not true, 10 years ago code looked more like this...
Actually it looked more like...
< ?php
class ProductController extends Controller
{
public function index ()
{
global $CONFIG ;
$db = new Database ( $CONFIG [ 'dsn' ]);
$db -> connect ();
$products = $db -> query ( 'SELECT *
from products;' );
$this -> render (
'index' ,
array (
'products' => $products
)
);
}
}
No service layer, hardcoded db queries in our controller classes. In every method we instanciated the
database layer because we thought this is how to do it. Big pile of mess.
Tightly coupled code
Code was tightly coupled. Thight coupling might be a good goal to achieve in real life, but in software
development this is a no go.
No component re-use
Tightly coupled code means we cannot easily reuse existing components which leads to copy-and-paste
development which is not what we want.
No isolation, not testable!
Tightly coupled code also means that we cannot test components in complete isolation. We can only execute
integration tests, in the worst case integration tests that hit our production database.
...and then came this guy
Back in 2010 I gave my first presentation at a conference about how "Real world DI" and at the same time
most of the "version 2" frameworks (Zend, Symfony, ...) adopted the idea to avoid tight coupling by
introducing
DI / IoC.
And today?
< ?php
class ProductController extends Controller
{
public function __construct (
ProductService $productService ,
ProductCommentsService $productCommentsService ,
ProductRatingService $productRatingService ,
UserService $userService ,
NotificationService $notificationService ,
DiscountService $discountService ,
EmailService $emailService ,
PaymentFactory $paymentFactory ,
Order $order ,
Comment $comment ,
Rating $rating ,
PriceFormatterHelper $formatter
){
// [...]
}
}
Too many dependencies! We simply inject everything. Looks like we PHP developers are scared of
using the new keyword in our code these days. The only good thing about this code: Using constructor
injection!
Constructor Injection FTW!
< ?php
class ProductController extends Controller
{
public function __construct (
ProductService $productService ,
UserService $userService
){
// [...]
}
}
Clearly communicates the intent. The controller depends on a ProductService implementation as well as
UserService implementation. I know exactly what to provide to be able to instanciate an use the class.
Property Injection is evil!
« Repeat after me: field injection is a dumb idea... *sigh* » - @olivergierke
Property Injection is evil!
< ?php
class ProductController extends Controller
{
/**
* @Inject
* @var ProductService
*/
private $productService ;
/**
* @Inject
* @var UserService
*/
private $userService ;
// [...]
}
Property Injection (or Field injection) does not clearly communicate which dependencies are needed. I cannot
"see" from the outside which dependencies are needed. I do always neeed as DI container to resolve the
dependencies. This is so wrong.
But public properties...
< ?php
class ProductController extends Controller
{
/**
* @Inject
* @var ProductService
*/
public $productService ;
/**
* @Inject
* @var UserService
*/
public $userService ;
// [...]
}
Even worse. Sure you can "see" the properties than thus guess which dependencies might be needed but there is
no type check. I could easily pass an UserService instance to the $productService variable.
Avoid Setter Injection...
< ?php
class ProductController extends Controller
{
public function setProductService ( ProductService $service )
{
// [...]
}
public function setUserService ( UserService $service )
{
// [...]
}
}
Setter injection would fix the type check issue but still you have no idea which dependencies need to be set
upfront to make your code execute properly.
...but optional dependencies?
The concept of optional dependencies is a lie!
Either you need a dependency or not. Loggers do not count, will give proof later.
Just because you do not use a class that depends on a different lib does not make it a optional dependency.
Also avoid Interface Injection
< ?php
interface ProductServiceAware
{
public function setProductService ( ProductService $service );
}
< ?php
class ProductController extends Controller
implements ProductServiceAware
{
public function setProductService ( ProductService $service )
{
// [...]
}
}
Quite similar to Propery Injection. Does not clearly shows intent that this is a required dependency. Similar
to Property Injection you will need a DI container with the "knowledge" that the setter method needs to be
called
before(!) any other method.
ContainerAware is a lie, too!
< ?php
interface ContainerAware
{
public function setContainer ( ContainerInterface $container );
}
< ?php
class ProductController extends Controller
implements ContainerAware
{
protected $productService ;
public function setContainer ( ContainerInterface $container )
{
$this -> productService =
$container -> get ( 'productService' );
}
}
There is so much wrong with in these few lines of code. Never in inject the DI container
into your application classes.Never use the DI container as a registry: we depend on a string
"productService".
We do not what type it really is. We simply trust the container to deliver what we need. We
hide the dependencies we really need from the outer world, which makes it hard to test.
But I need lazy-loading...
If you "need" lazy-loading because of too many dependencies you actually have a different problem to solve.
Lazy-loading does make sense in certain cases but not when it comes to "dynamically" instanciate the
dependencies needed.
To new or not to new...
< ?php
class OrderController extends Controller
{
public function __construct (
BasketService $basketService ,
OrderService $orderService ,
Order $order
){
$products = $basketService -> getProducts ();
foreach ( $products as $product ) {
$order -> addProduct ( $product );
}
// [...]
$orderService -> order ( $order );
}
}
To new or not to new...
< ?php
class OrderController extends Controller
{
public function __construct (
BasketService $basketService ,
OrderService $orderService
){
$order = new Order ();
$products = $basketService -> getProducts ();
foreach ( $products as $product ) {
$order -> addProduct ( $product );
}
// [...]
$orderService -> order ( $order );
}
}
It is ok to use new to instanciate domain objects or helper/util classes. You should simply not instanciate
"dependencies" aka services with new in your class.
To new or not to new...
< ?php
class FileTemplateStore
{
public function parseTemplate ( $file , array $variables = [])
{
$content = file_get_contents ( $file );
// replace placeholders with content from $variables array...
// [...]
return SimpleMessage:: parseFromString ( $content );
}
}
No need to replace SimpleMessage with my own custom implementation, it is just a helper for turning an email
template into a mail message object.
What is a dependency?
Sometimes coding feels like being trapped in an forrest, you cannot see the sky. All you see is trees and
all those trees look like the same, even though in fact there are different types of trees arround. Similar
to coding. Not every object is the same.
What is a dependency?
Simply ask yourself: Do I really need to replace this dependency with something
different?
What is a dependency?
Spoiler alert: Sometimes dependencies are not what they seem to be.
Is a logger a dependency?
Is a logger a dependency?
< ?php
class OrderController extends Controller
{
protected $logger ;
public function __construct ()
{
$this -> logger = new Logger ( '/tmp/application.log' );
// [...]
}
}
{
"require" : {
"psr/log" : "^1.0" ,
"monolog/monolog" : "^1.21"
}
}
Harcoding the logger in every class does not help, especially when you have to pass the log file path
over and over again. Library problem: Does not help when you depend on a PSR-3 but use a concrete PSR-3
implementation.
What is this PSR-3 thing?
« The main goal is to allow libraries to receive a Psr\Log\LoggerInterface object and write
logs to it in a simple and universal way. » - PHP-FIG
No vendor lock-in for loggers
In other words: no vendor lock-in for logger implementations. When composing your app of multiple packages
you simply do not want to configure 10 different logger packages, do you?
How to solve this problem?
< ?php
namespace Psr\Log;
/**
* Describes a logger-aware instance.
*/
interface LoggerAwareInterface
{
/**
* Sets a logger instance on the object.
*
* @param LoggerInterface $logger
* @return null
*/
public function setLogger ( LoggerInterface $logger );
}
PSR-3 offer us this little nifty interface (plus a trait) to implement.
How to solve this problem?
< ?php
$controller = new OrderController ();
$controller -> processOrder ();
PHP Fatal error: Uncaught Error: Call to a member function debug()
on null in / home/ shochdoerfer/ Workspace/ demo/ src/ Demo/ Http/ Controller/
OrderController.php: 29
Sure, your magic DI container will most probably take care of this out of the box. But still, once you
need to do the method calls on your own you need know which methods to call upfront.
Fix: Rely on NullLogger
< ?php
class OrderController extends Controller
implements \Psr\Log\LoggerAwareInterface
{
protected $logger ;
public function __construct ()
{
$this -> logger = new \Psr\Log\NullLogger ();
// [...]
}
}
Downside: In every construct() method you need to create a NullLogger instance. Feels wrong to me!
Fix: Constructor injection
< ?php
class OrderController extends Controller
{
protected $logger ;
public function __construct ( \Psr\Log\LoggerInterface $logger )
{
$this -> logger = $logger ;
// [...]
}
}
Fix: Constructor injection
< ?php
class FileTemplateStore
{
public function parseTemplate ( $file , array $variables = [])
{
$content = file_get_contents ( $file );
// replace placeholders with content from $variables array...
// [...]
return SimpleMessage:: parseFromString ( $content ,
$this -> logger);
}
}
Passing loggers around like wild...
Logger is infrastructure
The Simple Logging Facade for PSR-3 loggers
bitexpert/slf4psrlog
$> composer.phar require bitexpert/ slf4psrlog
< ?php
class FileTemplateStore
{
protected $logger ;
public function __construct ()
{
$this -> logger = \b itExpert\Slf4PsrLog\LoggerFactory:: getLogger (
__CLASS__);
}
public function parseTemplate ( $file , array $variables = [])
{
$this -> logger-> debug ( 'Parsing template...' );
// [...]
}
}
If not custom logger instance was provided (e.g. no set-up was run) the LoggerFactory will simply
return a NullLogger instance so that your code will not break!
The glue code...
< ?php
\bitExpert\Slf4PsrLog\LoggerFactory:: registerFactoryCallback (
function ( $channel ) {
if (! \Monolog\Registry:: hasLogger ( $channel ))
{
\Monolog\Registry:: addLogger (
new \Monolog\Logger ( $channel )
);
}
return \Monolog\Registry:: getInstance ( $channel );
}
);
In your application code you need to configure the LoggerFactory. This is not needed in our library! You
can however configure the logger when executing your unit tests and have a concrete logger implementation
in your require-dev section of composer.json.
What is a dependency?
Remember: Sometimes dependencies are not what they seem to be.
Back on track: What is a dependency and more specifically what is not a dependency?
Is a factory a dependency?
Not really, a factory is simply a container responsible for creating a dependency.
Injecting factory gone wrong
< ?php
class OrderController extends Controller
{
public function __construct (
PaymentProcessorFactory $factory
){
$processor = $factory -> getInstance ();
// [...]
}
}
Why should your code need to care that there is a factory which creates a PaymentProcessor instance? Your
code
should only rely on a PaymentProcessor instance.
Explicit dependencies
< ?php
class OrderController extends Controller
{
public function __construct (
PaymentProcessor $paymentProcessor
){
// [...]
}
}
bitexpert/disco
« [...] a container-interop compatible, annotation-based dependency injection container. » -
Disco
container-interop?
« container-interop tries to identify and standardize features in container objects [...] to
achieve interoperability. » - container-interop
Annotations?
Actually most ppl would not ask why but more like "Are you serious"? Yes I am. Not the biggest fan of
Annotations because still Annotations are no 1st class citizens of PHP core but for what I needed them
they were the perfect fit.
Why another DI container?
« [...] to make DI in PHP great again. » - Me
Problems Disco solves
Configuration as code (to improve refactoring)
Write clean configuration without much boilerplate code
Write readable configuration code
Configuration example
< ?php
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
/** @Configuration */
class Config
{
/** @Bean */
public function productService ()
: ProductService
{
$db = new Database ( 'mysql://user:secret @localhost /mydb' );
return new ProductService ( $db );
}
}
Syntactically a valid PHP class.
Configuration example
< ?php
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
/** @Configuration */
class Config
{
/** @Bean */
protected function database () :
Database
{
return new Database ( 'mysql://user:secret @localhost /mydb' );
}
/** @Bean */
public function productService ()
: ProductService
{
return new ProductService ( $this -> database ());
}
}
Configuration example
< ?php
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
/** @Configuration */
class Config
{
/** @Bean({"singleton"=false, "lazy"=false,
"scope"="request"}) */
public function productService ()
: ProductService
{
return new ProductService ();
}
}
Configuration example
< ?php
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
/** @Configuration */
class BeanConfiguration
{
/** @Bean({"alias"="\My\Custom\Namespace\ProductService"}) */
public function productService ()
: ProductService
{
return new ProductService ();
}
}
Aliases do also work. Needed in case your service names contain letters that are not valid for a
method name. Method names take predecene over aliases.
Disco in action
< ?php
$beanFactory = new AnnotationBeanFactory ( Config:: class );
BeanFactoryRegistry:: register ( $beanFactory );
$productService = $this -> beanFactory-> get ( 'productService' );
$productService = $this -> beanFactory-> get (
'\My\Custom\Namespace\ProductService'
);
Using primitives
< ?php
use bitExpert\Disco\Annotations\Bean;
use bitExpert\Disco\Annotations\Configuration;
/** @Configuration */
class BeanConfiguration
{
/**
* @Bean
*/
public function disco () : string
{
return 'Disco rocks!' ;
}
}
Disco supports all primitives which are supported by PHP: array, callable, bool, float, int, string
Structure the configuration
< ?php
use bitExpert\Disco\Annotations\Bean;
trait ProductModule
{
/** @Bean */
public function productService ()
: ProductService
{
$db = new Database ( 'mysql://user:secret @localhost /mydb' );
return new ProductService ( $db );
}
}
< ?php
use bitExpert\Disco\Annotations\Configuration;
/** @Configuration */
class BeanConfiguration
{
use ProductModule;
}
Thanks to traits you are able to overwrite method with your own implementations and you will not run into
issues with clashing method names and such.
...and some more stuff
Custom PostProcessors
Parameterized Beans
Sessionaware Beans
...
Thank you! Questions?
Stephan Hochdörfer
Head of Technology , bitExpert AG (Mannheim,
Germany)
S.Hochdoerfer@bitExpert.de
@shochdoerfer
#PHP, #DevOps, #Automation
#phpugffm, #phpugmrn, #unKonf