Press 'c' to toggle code style
Press 's' for speaker notes
Maintainer of Imagick and Gmagick extensions.
PHP RFCs:
Closure from callable for PHP 7.1
Consistent Callables for PHP 8.0
Constructor behaviour of internal classes passed PHP 7.0
Codez:
Auryn - the best D.I. library https://github.com/rdlowrey/auryn
Tier - D.I. based framework https://github.com/danack/tier
Jig - D.I. based templating phpjig.com
“many client-specific interfaces are better than one general-purpose interface.”
Reasonable | Understandable | |
---|---|---|
Obvious from reading the code | Documentation / colleague can explain how it works |
class MySQLi {
function __construct($host, $username, $password,
$dbname, $port) {...}
}
class S3Client {
public function factory(array $config) {}
}
Reasonable | Understandable | |
---|---|---|
Errors happen at boundaries of execution | Errors can happen during middle of execution |
//oops - forget to set password and dbname
$db = new MySQLi('localhost', 'db_user');
$config['passwrod'] = '12345';
S3Client::factory($config)
Reasonable | Understandable | |
---|---|---|
Simpler parts, more of them | Fewer parts, but more complex code |
use Psr\Http\Message\ServerRequestInterface
as Request;
class SearchController
{
function search(Request $request, ...)
{
...
}
}
interface ServerRequestInterface extends RequestInterface
{
public function getServerParams();
public function getCookieParams();
public function withCookieParams(array $cookies);
public function getQueryParams();
public function withQueryParams(array $query);
public function getUploadedFiles();
public function withUploadedFiles(array $uploadedFiles);
public function getParsedBody();
public function withParsedBody($data);
public function getAttributes();
public function getAttribute($name, $default = null);
public function withAttribute($name, $value);
public function withoutAttribute($name);
}
interface RequestInterface extends MessageInterface
{
public function getRequestTarget();
public function withRequestTarget($requestTarget);
public function getMethod();
public function withMethod($method);
public function getUri();
public function withUri(UriInterface $uri,
$preserveHost = false);
}
interface MessageInterface
{
public function getProtocolVersion();
public function withProtocolVersion($version);
public function getHeaders();
public function hasHeader($name);
public function getHeader($name);
public function getHeaderLine($name);
public function withHeader($name, $value);
public function withAddedHeader($name, $value);
public function withoutHeader($name);
public function getBody();
public function withBody(StreamInterface $body);
}
function testSearchController()
{
$request = \Mockery::mock(Request::class)
->shouldReceive(...)
->andReturn(...)
->getMock();
???
???
}
class SearchController
{
function search(Request $request, DataSource $dataSource)
{
$queryParams = $request->getQueryParams();
if (!array_key_exists('searchTerms', $queryParams)) {
throw new ParamsMissingException("...");
}
$searchTerms = $queryParams['searchTerms'];
$searchOptions = [];
$searchOptions['keywords'] = explode(',', $searchTerms);
return $dataSource->searchForItems($searchOptions);
}
}
$queryParams = $request->getQueryParams();
if (!array_key_exists('searchTerms', $queryParams)) {
throw new ParamsMissingException("...");
}
$searchTerms = $queryParams['searchTerms'];
interface VariableMap
{
/**
* @throws ParamMissingException
*/
public function getVariable(string $variableName) : string;
}
class PSR7VariableMap implements VariableMap {
/** @var ServerRequestInterface */
private $serverRequest;
public function __construct(ServerRequestInterface $serverRequest) {
$this->serverRequest = $serverRequest;
}
public function getVariable(string $variableName) : string {
$queryParams = $this->serverRequest->getQueryParams();
if (array_key_exists($variableName, $queryParams) === false) {
$message = "Parameter [$variableName] is not available";
throw new ParamMissingException($message);
}
return $queryParams[$variableName];
}
}
class SearchController
{
function search(VariableMap $variableMap, DataSource $dataSource)
{
$searchTerms = $variableMap->getVariable('searchTerms');
$searchOptions = [];
$searchOptions['keywords'] = explode(',', $searchTerms);
return $dataSource->searchForItems($searchOptions);
}
}
class ArrayVariableMap implements VariableMap {
public function __construct(array $variables) {
$this->variables = $variables;
}
public function getVariable(string $variableName) : string {
if (!array_key_exists($variableName, $this->variables)) {
$message = "Parameter [$variableName] is not available";
throw new ParamMissingException($message);
}
return $this->variables[$variableName];
}
}
function testSearchControllerWorks()
{
$varMap = new ArrayVariableMap(
['searchTerms' => 'foo,bar']
);
// EchoDataSource just returns the keywords searched
$dataSource = new EchoDataSource();
$controller = new SearchController();
$result = $controller->search($varMap, $dataSource);
$this->assertEquals(['foo', 'bar'], $result);
}
function testSearchControllerException()
{
$varMap = new ArrayVariableMap([]);
$dataSource = new EchoDataSource();
$controller = new SearchController();
$this->setExpectedException('ParamMissingException');
$controller->search($varMap, $dataSource);
}
“many client-specificinterfacestypes are better than one general-purposeinterfacetype.”
class SearchController
{
function search(DataSource $dataSource, VariableMap $variableMap)
{
$searchTerms = $variableMap->getVariable('searchTerms');
$searchOptions = [];
$searchOptions['keywords'] = explode(',', $searchTerms);
$searchOptions['limite'] = 50; // LIMIT added
return $dataSource->searchForItems($searchOptions);
}
}
class SearchOptions
{
public $keywords;
public $limit;
public function __construct(array $keywords, int $limit = 1000)
{
//@TODO - check $keywords are all strings
$this->keywords = $keywords;
$this->limit = $limit;
}
}
class SearchController
{
function search(VariableMap $variableMap, DataSource $dataSource)
{
$searchTermsString = $variableMap->getVariable('searchTerms');
$searchTermsArray = explode(',', $searchTermsString);
$searchOptions = new SearchOptions($searchTermsArray, 50);
return $dataSource->searchForItems($searchOptions);
}
}
function writeTempFile(string $tmpPath) { ... file_put_contents($tmpPath.'/foo.txt', $data); ... }
function writeImageTempFile(string $tmpPath) { ... file_put_contents($tmpPath.'/foo.png', $imageData); ... }
class TmpPath
{
private $path;
public function __construct(string $tmpPath)
{
$this->path = $tmpPath;
}
public function getPath()
{
return $this->path;
}
}
function writeTempFile(TmpPath $tmpPath)
{
...
file_put_contents($tmpPath->getPath().'/foo.txt', $data);
...
}
function writeImageTempFile(ImageTmpPath $tmpPath)
{
...
file_put_contents($tmpPath->getPath().'/foo.png', $imageData);
...
}
class ArrayVariableMap implements VariableMap
{
public function __construct(array $variables)
{
$this->variables = $variables;
}
public function getVariable(string $variableName) : string
{
if (array_key_exists($variableName, $this->variables) === false) {
$message = "Parameter [$variableName] is not available";
throw new ParamMissingException($message);
}
return $this->variables[$variableName];
}
}
“The purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise. The intellectual effort needed to ... understand a program need not grow more than proportional to program length.” - Edsger W. Dijkstra
a.k.a. think of your code being better because it is simpler rather than being boring.
$foo = new Foo();
$foo->bar();
$injector->make('Foo');
$foo->bar();
$injector->execute('Foo::bar');
class Zot {
function __construct(Fot $fot) { ... }
function bar() {...}
}
class Fot {
function __construct(Pik $pik) { ... }
}
class Pik {
function __construct() { ... }
}
$injector->execute('Zot::bar');
function foo(DataSource $dataSource) {
return $dataSource->search('bar');
}
$injector->alias('DataSource', 'EchoDataSource');
$result = $injector->execute('foo');
class Foo {
public function bar(TmpPath $tmpPath) {
...
}
}
$injector->share(new TmpPath(__DIR__.'/var/temp'));
$injector->execute('Foo::bar');
function testSearchControllerWorks()
{
$varMap = new ArrayVariableMap(['searchTerms' => 'foo,bar']);
$injector->alias('VariableMap', 'ArrayVariableMap');
$injector->share($varMap);
$injector->alias('DataSource', 'EchoDataSource');
$injector->share(new EchoDataSource);
$result = $injector->execute('SearchController::search');
$this->assertEquals(['foo', 'bar'], $result);
}
function createESCredentials() {
$username = getenv('elasticsearch_username');
$password = getenv('elasticsearch_password');
return new ESCredentials($username, $password);
}
$injector->delegate('ESCredentials', 'createESCredentials');
$injector->make('ESCredentials');
class ElasticSearchDataSource implements DataSource {
public function __construct(ElasticSearchClient $esClient) {
...
}
...
}
class ElasticSearchClient {
public function __construct(ESCredentials $credentials) {
...
}
}
class ESCredentials {
public function __construct($username, $password) {
...
}
}
$injector->alias('DataSource', 'ElasticSearchDataSource');
$injector->delegate(
'ElasticSearchCredentials',
'createESCredentials'
);
// This needs a data-source
$injector->execute('SearchController::search');
Tier - app 'framework' based around DI
'Framework' - https://github.com/danack/tier
Example app - https://github.com/danack/tierjigskeleton
When people give talks on the "S.O.L.I.D." design principles one of the letters that doesn't get enough attention is the "i" - the "interface segregation principle". This talk seeks to redress that imbalance by going into a bit more in-depth into:
“For years there has been a theory that millions of monkeys typing at random on millions of typewriters would reproduce the entire works of Shakespeare. The Internet has proven this theory to be untrue.”