Press 'c' to toggle code style
Press 's' for speaker notes
Single responsibility | ★★★★★ |
Open/closed | wat |
Liskov substitution | ★★★★ |
Interface segregation | ★ |
Dependency inversion | ★★★ |
If you don't recognise these.....then reading up on the others can be your homework.
Maintainer of Imagick and Gmagick extensions.
PHP RFCs:
Constructor behaviour of internal classes - PHP 7.0
Closure from callable - PHP 7.1
get_class() disallow null - in voting!
Consistent Callables - PHP 8.0?
Want help drafting an RFC - talk to me!
“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 {
static function factory(array $config) { ... }
}
Reasonable | Understandable | |
---|---|---|
Errors happen at boundaries of execution | Errors can happen during middle of execution |
class MySQLi {
function __construct($host, $username, $password,
$dbname, $port) {...}
}
//oops - forget to set password, dbname and port
$db = new MySQLi('localhost', 'db_user');
class S3Client {
static function factory(array $config) { ... }
}
// Spelling is hard
$config['passwrod'] = '12345';
S3Client::factory($config)
Reasonable | Understandable | |
---|---|---|
Simpler parts, more of them | Fewer parts, but more complex code |
function testFoo()
{
$mock = Mockery::mock(Request::class);
->shouldReceive('getUri')
->andReturn('/search')
->getMock();
foo($mockObject);
// If foo doesn't call '$mock->getUri()'
// the test will fail
}
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);
}
}
//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];
}
}
class SearchController
{
// function search(Request $request, DataSource $dataSource)
function search(VariableMap $variableMap, DataSource $dataSource)
{
$searchTerms = $variableMap->getVariable('searchTerms');
// $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 DataSource {
function searchForItems(array $searchOptions);
}
class EchoDataSource implements DataSource {
function searchForItems(array $searchOptions) {
return $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);
// limit added to $searchOptions
$searchOptions['limite'] = 50;
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');
// $searchOptions = [];
// $searchOptions['keywords'] = explode(',', $searchTerms);
// $searchOptions['limite'] = 50; // LIMIT added
$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);
...
}
function badCode($userId)
{
global $session;
$userId = $session->getUserId();
return User::find($userId);
}
function solidCode(Session $session, UserRepo $userRepo)
{
$userId = $session->getUserId();
return $userRepo->find($session->getUserId());
}
badCode(); // This is easy to call
$session = new FileSession("/var/php/sessions", $request);
$userRepo = new UserSqlRepo($pdo);
solidCode($logger, $userRepo); // This is harder to call
// Setup session once
$injector->alias(Session::class, FileSession::class);
$session = new FileSession("/var/log/appname", $request);
$injector->share($session);
// Setup a UserSqlRepo once
function createUserRepo(PDO $pdo) {
return new UserSqlRepo($dbConnection);
}
$injector->delegate(UserRepo::class, 'createUserRepo');
// This runs the code and passes the
// correct variables to the function.
$injector->execute('solidCode');
interface FileUploader {
function upload($localFilename, $storageName);
}
interface FileTransfer {
function upload($localFilename, $storageName);
function download($localFilename, $storageName);
}
interface FileTransfer {
function upload($localFilename, $storageName);
function download($localFilename, $storageName);
function fileExists($storageName);
}
And so it grows...
interface FileUploader {
function upload($localFilename, $storageName);
}
interface FileDownloader {
function download($localFilename, $storageName);
}
interface FileChecker {
function fileExists($storageName);
}
interface FileTransfer extends FileUploader, FileDownloader { }
class S3FileManager implements
FileUploader,
FileDownloader,
FileChecker
{
...
}
function foo(FileTransfer $fileTransfer) {
...
}
foo(new S3FileManager(...));
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.
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.”
Object-oriented programming is an exceptionally bad idea which could only have originated in California — Edsger W. Dijkstra