Slides are at docs.basereality.com
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 - PHP 7.2
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);
}
}
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];
}
}
interface DataSource {
function searchForItems(array $searchOptions);
}
class EchoDataSource implements DataSource {
function searchForItems(array $searchOptions) {
return $searchOptions;
}
}
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.”
“many smallclient-specificinterfacestypes are better than one biggeneral-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)
{
//@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 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());
}
// This is easy to call
badCode();
$session = new FileSession("/var/php/sessions", $request);
$userRepo = new UserSqlRepo($pdo);
// This is harder to call
solidCode($logger, $userRepo);
Don't have time to do this properly
https://github.com/danack/example
https://github.com/Danack/example/blob/master/readme_auryn_slim.md
“many small types are better than one big type, and naming your types makes them easier to reason about”
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);
...
}
$emailSender->send(
$user->getName(),
$user->getEmailAddress()
);
$emailSender->send(
null,
$signupFormData->getEmailAddress()
);
interface EmailRecipient {
public function getName(): ?string;
public function getEmailAddress(): string;
}
class UserEmailRecipient implements EmailRecipient {
private $user;
function __construct(User $user) {
$this->user = $user;
}
public function getName(): ?string {
return $this->user->getName();
}
public function getEmailAddress(): string {
return $this->user->getEmailAddress();
}
}
class SignupEmailRecipient implements EmailRecipient {
private $formData;
function __construct(SignupFormData $signupFormData) {
$this->formData = $signupFormData;
}
public function getName(): ?string {
return null;
}
public function getEmailAddress(): string {
return $this->formData->getEmailAddress();
}
};
$emailSender->send(
new UserEmailRecipient($user)
);
$emailSender->send(
new SignupEmailRecipient($signupFormData)
);
// Gets a list of articles
function getArticles(
VarMap $varMap,
ArticlesRepo $articlesRepo
) {
$limit = v::intVal()->max(20)
->validate($varMap->get('limit'));
$after = v::oneOf(
v::nullType(),
v::intVal()->max(10000)
)->validate($varMap->get('after'));
return $articlesRepo->getArticles($limit, $after);
}
// Gets a list of articles
function getArticles(
VarMap $varMap,
ArticlesRepo $articlesRepo
) {
$getArticlesParams = GetArticlesParams::create($varMap);
return $articlesRepo->getArticles($getArticlesParams);
}
danack/params on packagist and github.
interface Cache {
function get($key);
function set($key, $value);
function setTtl($key, $value, $ttl);
function delete($key);
function deleteItems(array $keys);
function clear();
function clearKeys(array $keys);
function clearPattern($regex);
function setNx($key, callable $fn);
function setNxTtl($key, $ttl, $value);
}
interface CacheBasic {
function get($key);
function set($key, $value);
}
interface CacheDelete {
function delete($key);
}
interface CacheClear {
function clear();
}
interface BasicAndDeleteCache extends CacheBasic, CacheDelete { }
class RedisCache implements
CacheBasic,
CacheDelete
{ ... }
function foo(BasicAndDeleteCache $fileTransfer) {...}
class RedisCache extends RedisCache
implements BasicAndDeleteCache {
// empty - just used for declaring interface
}
foo(new RedisCache(...));
Union types RFC if/when passed will make this easier to do.
interface DB {
function autocommit(bool $mode) : bool;
function change_user($user, $password, $database) : bool;
function character_set_name() : string;
function close(void) : bool;
function commit(int $flags = 0 , string $name = null ) : bool;
// and all the others from MySQLi
}
class MySQLDB extends MySQLi implements DB {
...
}
interface PurchaseFeeRepo
{
public function findPurchaseThatNeedsFeeFetching(): ?Purchase;
public function saveFee(Purchase $purchase, FeeData $feeData);
public function saveFeeProcessingFailed(Purchase $purchase);
}
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
https://joind.in/talk/729af
Twitter: @MrDanack
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