A built-in DI container allows for explicitly declaring and typing properties inline for an object to be used for DI that would by default make closures for each assigned value and would call the closure automatically when the object's properties are dereferenced.
"Everyone" knows that the better way to program is using dependency injection. But many (most?) PHP developers don't do it? Why? IMO people don't always use DI because it can makes the code hard to read and too complex, and because there are not good built-in support for dependency injection containers.
What do I mean about built-in support? We have Pimple in userland, and many others, right? Why do we need built-in support?
Currently the same people who advocate for DI are also frequently those who also sing the praises of type hinting. But if you use a DI container, you loose type hinting. Damned if you do, damned if you don't.
So the reason for this proposal is to enable typing the properties of a DI container, and also to empower the potential for linting.
A new type would be defined, which I have chosen to name args
to keep it short. The args
type would allow declaration of an anonymous class, but one with properties specific to DI:
class Query {
// class properties declared here...
public function __construct(string $name, args $args={}): Query
$this->name = $name;
default $args = {
Logger logger => new Logger(),
DBConnection db => MySQLDB::getConnection(),
}
$this->logger = $args->logger;
$this->db = $args->db;
}
}
An empty DI container would be denoted with an empty pair of braces ({}
), which is what the following example uses to initialize the $args
parameter to enable the constructor above to be called without specifying a DI container and thus accept the default specified in the class:
public function __construct(string $name, args $args={}): Query
Inside a function/method the default
keyword would declare the default values for named DI container and would use braces ({}
) and fat arrows (=>
) as syntax markers for the declaration.
default $args = {
Logger logger => new Logger(),
DBConnection db => MySQLDB::getConnection(),
}
The literal syntax for instantiating a DI container would also use braces ({}
) and fat arrows (=>
) as syntax markers for the declaration:
$query = new \Example\Query('MongoDB',{
db => \Example\MongoDB::connection(),
});
A very important aspect of this proposal is that a DI container should ignore unrecognized properties just like HTML ignores unrecognized elements. This so that the DI container can be passed to called functions/methods that expect additional properties:
public function __construct(string $name, args $args={}): Query
// ...
default $args = {
// ...
DBConnection db => $this->getConnection($args),
}
// ...
}
public function getConnection(args $args={}): DBConnection {
default $args = {
string host => 'localhost',
string user => null,
string pass => null,
}
return new DBConnection(new mysqli($args->host,$args->user,$args->pass));
}
//-------------------
$query = new \Example\Query('MySQL',{
user => 'mikeschinkel',
pass => '12345',
});
$result = $query->run('SHOW TABLES');
Ideally, as in all of PHP, typing would not be required, only preferable. Further, duck typing should be enforcable for statically analyzable code that does not specify type when declare(strict_types=1)
is set:
default $args = {
logger => new Logger(),
db => MySQLDB::getConnection(),
}
Assuming the developer wants to document that a particular DI container property is required to be passed they would indicate so with a required
keyword in place of a value:
default $args = {
db => required,
}
The DI container is really just an object, but one that requires an additional property modifier that PHP currently provides.
Part of this proposal implies the addition of a eval
modifier for properties of a class (see the DropdownField::$option
property declaration.) Any default
assignment or any literal passed in as a parameter type hinted with args
would automatically apply the eval
modifier implicitly, but when defined via the class
keyword eval
would need to be defined explicitly:
class DropdownField extends Field {
public string $name;
public eval Option[] $options;
public function __construct(string $name,args $args={}) {
$this->name = $name;
default $args = {
options => required
}
$this->options = $args->options;
}
}
class Option {
public $id;
public $name;
public function __construct($id, $name) {
$this->id = $id;
$this->name = $name;
}
}
Then here is how these would be used. Notice that $options
is evaluated at the time of access, not at the time of assignment:
$options = [];
$ddField = new DropdownField('category',{
options => $options,
})
print_r( $ddField->options );
/* Prints:
*
* Array
* (
* )
*/
$options[] = new Option( 'mktg','Marketing');
print_r( $ddField->options );
/* Prints:
*
* Array
* (
* [0] => Option Object
* (
* [id] => mktg
* [name] => Marketing
* )
*
* )
*/
So here is the equivalent that can be done today using an Evalable
class and an the __invoke()
magic method, an anonymous function, and an anonymous class. But hopefully you can see that this is nightmarishly complex and no PHP developer in their right mind would be responsible creating an architecture depending on this level of complexity:
<?php
class DropdownField {
public string $name;
public Options[] $options;
public function __construct(string $name,$args=[]) {
$this->name = $name;
$args = array_merge( array(
'options' => new Evalable( [] ),
), $args );
$this->options = $args['options'];
}
}
class Evalable {
public $value;
public function __construct($value) {
$this->value = $value;
}
public function __invoke() {
return is_callable( $this->value )
? call_user_func( $this->value )
: $this->value;
}
}
$options = [];
$ddfield = new DropdownField('category',array(
'options' => new Evalable( function() use(&$options) {return new class {
public Options[] $options;
public function __construct($options) {
$this->options = $options;
}
}}),
));
$eval_options = $ddfield->options;
print_r( $eval_options() );
/* Prints:
*
* Array
* (
* )
*/
$options[] = new Option( 'mktg','Marketing');
print_r( $eval_options() );
/* Prints:
*
* Array
* (
* [0] => Option Object
* (
* [id] => mktg
* [name] => Marketing
* )
*
* )
*/
Let's say for a moment we really wanted to pass the value and not an eval type, just use the valueof
keyword:
$options = [];
$ddField = new DropdownField('category',{
options => valueof $options,
})
print_r( $ddField->options );
/* Prints:
*
* Array
* (
* )
*/
$options[] = new Option( 'mktg','Marketing');
print_r( $ddField->options );
/* Prints:
*
* Array
* (
* )
*/
Since an eval
property should always return its value whether it's scalar object or array then there is a need to be able to explicitly access the closure/callable. For this we would introduce the function get_arg_closure($arg)
which can return the closure for an arg:
$options = [];
$ddField = new DropdownField('category',{
options => $options,
})
$options_closure = get_arg_closure( $ddField->options );
print_r( $options_closure->call() ); // prints empty array
/* Prints:
*
* Array
* (
* )
*/
$options[] = new Option( 'mktg','Marketing');
print_r( $options_closure->call() );
/* Prints:
*
* Array
* (
* [0] => Option Object
* (
* [id] => mktg
* [name] => Marketing
* )
*
* )
*/
By linear nesting I mean DI containers should support the ability to pass in a list of properties that are logically nested but that are not nested from the perspective of merging. Let's illustrate with a modified version of a previous example. Not have the thin arrow (->
) syntax is used when passing args
to \Example\Query
that are then further passed as a subset to $this->getConnection()
using $args->db
:
public function __construct(string $name, args $args={}): Query
// ...
default $args = {
// ...
DBConnection db => $this->getConnection($args->db),
}
// ...
}
public function getConnection(args $args={}): DBConnection {
default $args = {
string host => 'localhost',
string user => null,
string pass => null,
}
return new DBConnection(new mysqli($args->host,$args->user,$args->pass));
}
//-------------------
$query = new \Example\Query('MySQL',{
logger => new MyLogger(),
db->user => 'mikeschinkel',
db->pass => '12345',
});
$result = $query->run('SHOW TABLES');
##Why not Just Named Parameters?
Given my PHP coding experience the past 10+ years I find the need to pass $args
as an array down through a call stack. Named parameters would require exploded and collecting at each level.
Having (only) named parameters would be like having call_user_func()
but no call_user_func_array()
.
See https://gist.github.com/timoschinkel/7c4d1ca1f71010f6ebe3f16b2f145a8c#gistcomment-2736717