Press 'c' to toggle code style
Press 's' for speaker notes
“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 | |
Errors happen at start of execution | Errors can happen during middle of execution | |
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);
}
}
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];
}
}
Use rdlowrey/auryn...it's awesome. Or use the Symfony one if you have a Symfony app
// In bootstrap.php
$injector->alias('VariableMap', 'PSR7VariableMap');
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) === false) {
$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);
}
rdlowrey/auryn makes it easier to write tests.
function testSearchControllerWorks()
{
$varMap = new ArrayVariableMap(['searchTerms' => 'foo,bar']);
$injector = createTestInjector(
[VariableMap::class => $varMap],
[DataSource::class => EchoDataSource::class]
);
$result = $injector->execute([SearchController::class, 'search']);
$this->assertEquals(['foo', 'bar'], $result);
}
function testSearchControllerException()
{
$varMap = new ArrayVariableMap([]);
$dataSource = new EchoDataSource();
$controller = new SearchController();
$this->setExpectedException('ParamMissingException');
$controller->search($varMap, $dataSource);
}
function testSearchControllerException()
{
$varMap = new ArrayVariableMap([]);
$injector = createTestInjector(
[VariableMap::class => $varMap],
[DataSource::class => EchoDataSource::class]
);
$this->setExpectedException(ParamMissingException::class);
$injector->execute([SearchController::class, 'search']);
}
“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;
return $dataSource->searchForItems($searchOptions);
}
}
class SearchOptions
{
public $keywords;
public $limit;
public function __construct(array $keywords, int $limit = 1000)
{
foreach ($keywords as $keyword) {
if (is_string($keyword) === false) {
$message = "Type ".gettype($keyword)." not allowed";
throw new \InvalidArgumentException($message);
}
}
$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);
...
}
// In bootstrap.php
$injector->share(new TmpPath(__DIR__.'/var/temp'));
$injector->share(new ImageTmpPath(__DIR__.'/var/temp_images'));
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
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.”