Skip to content

Instantly share code, notes, and snippets.

@mikeschinkel
Last active October 18, 2018 23:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeschinkel/28f4780f7769140a2cba201b6a7185a3 to your computer and use it in GitHub Desktop.
Save mikeschinkel/28f4780f7769140a2cba201b6a7185a3 to your computer and use it in GitHub Desktop.
Rough First Draft of Proposal for DI Container built into PHP

Proposal for DI Container built into PHP

This is a work-in-progress

Introduction

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.

Justification

"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.

Rational: Typing and Linting.

So the reason for this proposal is to enable typing the properties of a DI container, and also to empower the potential for linting.

Proposal

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;
    }
}

Empty Container: {}

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

Declaring with default keyword

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(),
}

Literal Syntax for Instantiating a DI container

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(),
});

Ignore Unrecognized Properties

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');

Typing not required, and duck-typing can be used when strict

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(),
}

Special required value

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

The DI container is really just an object, but one that requires an additional property modifier that PHP currently provides.

A new eval modifier for properties

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
 *            )
 *    
 *    )
 */

Args then are just objects with automatic declaration of eval properties

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
 *            )
 *
 *    )
 */

Passing the value, not a eval property

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
 *    (
 *    )
 */

Accessing the closure of a property with the eval modifier

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
 *            )
 *    
 *    )
 */

DI containers should support linear nesting and merging

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().

<?php
namespace Example
class Query {
/**
* @var string
*/
public $name;
/**
* @var Logger
*/
public $logger;
/**
* @var DBConnection
*/
public $db;
/**
* @var Result
*/
public $result;
/**
* @param string $name
* @param args $args
* @return Query
*/
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;
}
/**
* @param Query $query
* @return Result
*/
public function run(string $query): Result
$this->result = $args->db->query($query);
if ($this->result->is_error()) {
$args->logger->log(sprintf(
'ERROR: Query %s failed: %s',
$this->name,
$this->result->getMessage()
));
}
return $this->result;
}
}
<?php
$query = new \Example\Query('MongoDB',{
db => \Example\MongoDB::connection(),
});
$result = $query->run('{city:"Atlanta"}');
<?php
$query = new \Example\Query('MySQL');
$result = $query->run('SHOW TABLES');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment