@Ocramius @asgrim @RoaveTeam Imma start calling you Gandalphp.
— Lee Davis (@leedavis81) 9 July 2015I'm an Extremist.
I became an Extremist.
I maintain too many projects that allow too much.
Everyone wants a shiny new feature.
This talk is about aggressive practices.
These practices work well for long-lived projects.
Every OSS project should probably adopt them.
If you don't like these practices, don't apply them ...
... but please be considerate about what you release
(Sorry!)
Those who don't admit it, are the dumbest!
Defensive driving = assuming that other drivers will make mistakes
Avoid Mistakes
Fool Proofing
ninety percent of everything is crap.
Theodore SturgeonGreatly reduces bugs and cognitive load
PSR-7 got this right!
Profile!
Profile!
Profile!
> O(log(n))
class DbConnection { public function __construct(...) { // ... } public function setLogger(Logger $logger = null) { $this->logger = $logger; } }
class DbConnection { public function __construct(..., Logger $logger) { // ... } // look ma, no setters! }
$dbConnection = new DbConnection(..., new FakeLogger());
class Spammer { public function spam($email, $template, $optOutLink = false) { // yes, this is a really bad spammer } }
class Spammer { public function sendIllegalSpam($email, $template) { // without opt-out link } public function sendApparentlyLegalSpam($email, $template) { // with opt-out link } }
class BankAccount { // ... public function setLastRefresh(DateTime $lastRefresh) { $this->lastRefresh = $lastRefresh; } }
(examples are purposely simplified/silly!)
$currentTime = new DateTime(); $bankAccount1->setLastRefresh($currentTime); $bankAccount2->setLastRefresh($currentTime);
// ... stuff
$currentTime->setTimestamp($aTimestamp);
class BankAccount { // ... public function setLastRefresh(DateTime $lastRefresh) { $this->lastRefresh = clone $lastRefresh; } }
class BankAccount { // ... public function setLastRefresh(DateTimeImmutable $lastRefresh) { $this->lastRefresh = $lastRefresh; } }
class MoneyTransfer { public function __construct(Money $amount, DateTime $transferDate) { $this->amount = $amount; $this->transferDate = $transferDate; } }
class MoneyTransfer { public function __construct(Money $amount, DateTime $transferDate) { $this->amount = $amount; $this->transferDate = clone $transferDate; } }
BY DEFAULT!
public function addMoney(Money $money) { $this->money[] = $money; } public function markBillPaidWithMoney(Bill $bill, Money $money) { $this->bills[] = $bill->paid($money); }
$bankAccount->addMoney($money); $bankAccount->markBillPaidWithMoney($bill, $money);
These methods are part of a single interaction, but are exposed as separate public APIs
private function addMoney(Money $money) { $this->money[] = $money; } private function markBillPaidWithMoney(Bill $bill, Money $money) { $this->bills[] = $bill->paid($money); } public function payBill(Bill $bill, Money $money) { $this->addMoney($money); $this->markBillPaidWithMoney($bill, $money); }
$bankAccount->payBill($bill, $money);
Single public API endpoint wrapping around state change
$userId = $controller->request()->get('userId'); $userRoles = $controller->request()->get('userRoles');
Subsequent method calls are assuming that the request will be the same
(apart from being foolish API)
$request = $controller->request(); $userId = $request->get('userId'); $userRoles = $request->get('userRoles');
No assumption on what request() does: we don't trust it
class Train { public function __construct(array $wagons) { $this->wagons = (function (Wagon ...$wagons) { return $wagons; })(...$wagons); } }
Yes, it's horrible, but it works
(on PHP 7)
Just use it!
class PrisonerTransferRequest { /** * @param mixed $accessLevel * - false if none * - true if guards are required * - null if to be decided * - 10 if special cargo is needed * - 20 if high security is needed */ public function approve($securityLevel) { // ... } }
class PrisonerTransferRequest { public function approve(PrisonerSecurityLevel $securityLevel) { // ... } }
register(new EmailAddress('ocramius@gmail.com'));
No need to re-validate inside register()!
$thing ->doFoo() ->doBar();
$thing = $thing->doFoo(); $thing = $thing->doBar();
$thing->doFoo(); $thing->doBar();
Only ONE way!
$thing->doFoo(); $thing->doBar();
(There's a blogpost for that!)
If not, then do not use extends.
You don't even need extends.
(There's a blogpost for that!)
Do you REALLY need to re-use that code?
DRY or just hidden coupling?
class LoginRequest { public function __clone() { $this->time = clone $this->time; } }
Cloning requires encapsulated state understanding
... which is a contradiction!
class Human { public function __clone() { throw new \DomainException( 'Why would you even clone me?!' . 'It\'s currently illegal!' ); } }
Do you know the lifecycle of every dependency?
Is your object even supposed to be stored somewhere?
class MyAwesomeThing { public function __sleep() { throw new BadMethodCallException( 'NO! MY THING! CANNOT HAZ!' ); } }
YES, ALL OF THEM!
Bring your CRAP score <= 2
It will make you cry.
You will implore for less features.
You will love having less features.
interface AuthService { /** * @return bool * * @throws InvalidArgumentException * @throws IncompatibleCredentialsException * @throws UnavailableBackendException */ public function authenticate(Credentials $credentials); }
class MyLogin { public function __construct(AuthService $auth) { $this->auth = $auth; } public function login($username, $password) { if ($this->auth->authenticate(new BasicCredentials($username, $password)) { // ... return true; } return false; } }
What can happen here?
A boolean true is returned
A boolean false is returned
A InvalidArgumentException is thrown
A IncompatibleCredentialsException is thrown
A UnavailableBackendException is thrown
Such little code, 5 tests already!
Await strict
Return strict