❌ Let's talk about code ✔
Alexander Makarov, Yii framework
Who's Alexander? 🤠
Writing code is easy! 👌
Especially PHP code! 🤘
Right? 😟
😭
The plan 📃
1. Composition and inheritance.
2. State.
3. Dependencies and injection.
4. Methods.
5. Exceptions.
6. Object types.
7. Services and PHP long running servers.
8. Tests.
9. Principles.
10. Layers and abstraction.
❌ What breaks our code? ☠
● Inheritance.
● Coupling.
● State.
● Assumptions.
● Testing.
● Architecture.
● ...
😍 Composition and inheritance 🤨
😍 Composition vs inheritance 🤕
abstract class Notifier
{
private string $template;
private array $parameters;
public function __construct(string $template, array $parameters = [])
{
// ...
}
protected function renderTemplate(): string
{
// ...
}
abstract public function send(): void;
}
final class EmailNotifier extends Notifier
{
private string $to;
private string $subject;
public function __construct(string $to, string $subject, string $template, array
$parameters = [])
{
// ...
}
public function send(): void
{
mail($this->to, $this->subject, $this->renderTemplate());
}
}
abstract Notifier - compose message, send message
○ final EmailNotifier.
○ final SmsNotifier.
○ final DebugNotifier.
Problems?
Notifier is coupled with concrete implementations.
We need more!
○ Sms notifier and Email notifier should log.
○ Sms notifier and Email notifier should not allow to send message twice.
○ We should send to multiple channels with fallback.
○ We should not send emails on April 1st.
○ 😱
Let's try without inheritance? 😎
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
interface Message
{
public function getSubject(): string;
public function getText(): string;
}
final class Address
{
public function getEmail(): string { … }
public function getPhone(): string { … }
}
interface Message
{
public function getSubject(): string;
public function getText(): string;
}
final class Address
{
public function getEmail(): string { … }
public function getPhone(): string { … }
}
interface Sender
{
public function send(Message $message, Address $to): void;
}
class EmailSender implements Sender
class SmsSender implements Sender
class Logger implements Sender
class DuplicatePreventer implements Sender
$sender = new DuplicatePreventer(new Logger(new SmsSender()));
$notifier = new Notifier($sender);
$notifier->send($message, $to);
So… inheritance is bad?
● Yes. Prefer to avoid it.
● Use it where appropriate: exceptions and other
hierarchies.
State 🤯
final class DataProvider
{
public function __construct(Query $query, Connection $connection) { ... }
public function all()
{
return $this->connection->findAll($this->query);
}
public function one()
{
$limitedQuery = $this->query->limit(1);
return $this->connection->findOne($limitedQuery);
}
}
$query = (new Query())
->select()
->from('post');
$dataProvider = new DataProvider($query);
var_dump($dataProvider->one());
var_dump($dataProvider->all());
interface QueryInterface
{
public function select(string $columns): self;
public function from(string $table): self;
public function limit(int $limit): self;
}
🤯
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
public function one()
{
$limitedQuery = $this->query->limit(1);
return
$this->connection->findOne($limitedQuery);
}
How to fix it?
public function limit(int $limit): self
{
$new = clone $this;
$new->limit = $limit;
return $new;
}
public function limit(int $limit): self
{
$new = clone $this;
$new->limit = $limit;
return $new;
}
State
● Avoid it if possible.
● Encapsulate it.
● Absolutely avoid for fluent interfaces.
● Fluent interface = immutability.
Dependencies and injection 🤓
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
final class Notifier
{
private Sender $sender;
public function __construct(Sender $sender) {
// ...
}
public function send(Message $message, Address $to): void
{
$this->sender->send($message, $to);
}
}
Is it possible to do it wrong?
final class Notifier
{
private ContainerInterface $container;
public function __construct(ContainerInterface $container) {
// ...
}
public function send(Message $message, Address $to): void
{
$sender = $this->container->get(Sender::class);
$sender->send($message, $to);
}
}
public function __construct(ContainerInterface $container) {
// ...
}
public function send(Message $message, Address $to): void
{
$sender = $this->container->get(Sender::class);
Dependency Injection
● DI is simple.
● You can do DI without container.
● Prefer composition over inheritance.
● Request what you need, not what you want to
ask to get what you need.
● Avoid passing container around.
Is it possible to do moar wrong?
final class Notifier
{
public function send(Message $message, Address $to): void
{
$sender = App::$container->get(Sender::class);
$sender->send($message, $to);
}
}
final class Notifier
{
public function send(Message $message, Address $to): void
{
$sender = App::$container->get(Sender::class);
$sender->send($message, $to);
}
}
Methods 😎
Method structure
1. Precondition checks.
2. Failure scenarios.
3. Main path.
4. Postcondition checks.
5. return.
Precondition checks
● Check if arguments are valid.
● Throw InvalidArgumentException.
● Don't try to "fix" things.
Fixing things
● Auth via Facebook. Same nickname present.
Attach Facebook ID to the account.
● Only letters are allowed in a nickname.
"samdark123" entered. Fix it by removing all
the numbers.
Failure scenarios
● RuntimeException.
● Avoid trying to "fix" things.
Main path
● Just what method does.
Postcondition checks
● Check if what was done is done right.
● Throw RuntimeException if not.
How do I know method is OK?
Cyclomatic complexity
Number of independent execution paths in a
control graph.
function doIt(int $number) {
if ($number === 42) {
$result = calculateSpecialResult();
} else {
$result = calculateResult(42);
}
if ($result === null) {
throw new RuntimeException('Unable to get result');
}
return $result;
}
V(G) = E - N + 2 = 7 - 6 + 2 = 3
V(G) = P + 1 = 2 + 1 = 3
● V(G) - cyclomatic complexity.
● E - edges.
● N - nodes.
● P - control structure points:
○ if → 1.
○ compound if → number of
sub-conditions.
○ iteration → 1.
○ switch → 1 per branch.
Keep cyclomatic
complexity low.
Return/throw early
function login(array $data)
{
if (isset($data['username'], $data['password'])) {
$user = $this->findUser($data['username']);
if ($user !== null) {
if ($user->isPasswordValid($data['password'])) {
$this->loginUser();
$this->refresh();
} else {
throw new InvalidArgumentException('Password is not valid.');
}
} else {
throw new InvalidArgumentException('User not found.');
}
} else {
throw new InvalidArgumentException('Both username and password are required.');
}
}
function login(array $data)
{
if (!isset($data['username'], $data['password'])) {
throw new InvalidArgumentException('Both username and password are required.');
}
$user = $this->findUser($data['username']);
if ($user === null) {
throw new InvalidArgumentException('User not found.');
}
if (!$user->isPasswordValid($data['password'])) {
throw new InvalidArgumentException('Password is not valid.');
}
$this->loginUser();
$this->refresh();
}
function login(array $data)
{
$this->assertUsernameAndPasswordPresent($data);
$user = $this->findUser($data['username']);
$this->assertUserFound($user);
$this->assertPasswordValid($user, $data['password']);
$this->loginUser();
$this->refresh();
}
Boolean flags
If a method has a boolean flag:
1. Single responsibility principle is violated.
2. You need multiple methods instead.
Visibility scopes
Private should be your default.
🔞 Exceptions 🚳
Exceptions
● Two types:
○ Ones we can handle.
■ Custom type, domain exceptions.
○ Ones we should not handle.
■ Prefer built-in types:
● RunitmeException.
● InvalidArgumentException.
Exception messages
● Should be explanative.
● Use proper English sentences.
Friendly exceptions
● https://github.com/yiisoft/friendly-exception
● getName().
● getSolution().
Object types 🦆 🦜 🦉
Services and non-services
● Services are doing something.
● Non-services hold data and return/mutate it.
Domain objects
● Extract domain objects to validate less.
○ Money, Point, Email.
● Composite values are object as well.
● Should not have dependencies injected. Only values.
● Use static named constructors: fromString, fromInt etc.
● Use private constructor for validation.
● Do not bloat these.
● Immutable.
DTO
● Can go crazy here but keep it simple.
● Public properties are fine :)
● Collect errors instead of throwing
exceptions.
● Use mass-assign.
Services 🛠
Services / Components
● Created once.
● Usually immutable.
● Use proper DI.
● Usually stored in DI container and/or accessible
via service locator.
Rules for a service
● Preferably stateless.
● No method injection or property injection.
● No optional dependencies.
● Explicit dependencies (proper DI).
● Action-relevant data should be passed to action
method.
● Light constructors. Postpone initialization.
● Do not make assumptions. Throw exceptions right
away.
PHP long running servers 🚀
How it works
// initialization
while ($request = $psr7->acceptRequest()) {
$response = $application->handle($request);
$psr7->respond($response);
gc_collect_cycles();
}
Rules
● Stateless services / State reset.
● Taking care about memory.
● Killing what's not used.
🪓🔨Tests ⛏🏹
How to write tests
● Arrange, act, assert.
● No dependencies.
● One test - one AAA.
● Prefer black box tests.
○ Test public API.
○ Hands off non-public API!
○ Don't check internal state!
○ Don't test methods, tests behavior.
Principles 📙
Should it be immutable?
● Service = immutable (if possible).
● Entity = mutable.
● Other objects = immutable.
● Fluent interface = immutable (don't do it with
mutable objects).
How to mutate object?
● DTO:
○ Directly.
● Value objects:
○ Immutable chaining.
● Entities:
○ Record changes as events.
● Other objects:
○ Immutable chaining.
Getting/setting info
Command-query separation
● Doing both is confusing.
● In some cases it is necessary.
Query method
class ShoppingCart
{
public function getTotal(): int {
}
}
Rules for queries
● Return a single type.
● Do not return internal state.
● Be specific. Avoid too flexible methods.
● Add abstraction for queries crossing system
boundaries.
● Do not use command methods in query
methods.
Command method
class ShoppingCart
{
public function addItem(Item $item): void {
if ($this->getTotal() + $item->cost > self::MAX_TOTAL) {
throw new MaxTotalReached('Leave something for others!');
}
$this->items[] = $item;
}
}
Rules for commands
● Name in imperative form.
● Limit the scope, postpone with events and
queues.
● Throw exceptions.
● Add abstraction for commands crossing system
boundaries.
● Use queries to get info, commands to process it.
Models
Read/write models
● Similar to query / command.
● Search in Elastic / Insert to MySQL.
● Read models should be use-case specific.
● Read models should be close to data source.
🥞 Layers and abstraction 🍔
Don't over-abstract
● Some objects are meant to be concrete:
○ Controllers.
○ Application services.
○ Entities.
○ Value objects.
FIN 🎬
1. Composition and inheritance.
2. State.
3. Dependencies and injection.
4. Methods.
5. Exceptions.
6. Object types.
7. Services and PHP long running servers.
8. Tests.
9. Principles.
10. Layers and abstraction.
There's always more 👀
Keep learning! 💪
Think! 💡
❓ Questions time!
● https://www.yiiframework.com/
● https://twitter.com/sam_dark
● https://rmcreative.ru/
● PHP Russia Videos
● Further learning:
○ Matthias Noback https://matthiasnoback.nl/
○ Cyclomatic complexity by Anna Filina
https://www.youtube.com/watch?v=UdjEb6t9-e4
○ Anthony Ferrara videos: https://www.youtube.com/user/ircmaxell/videos

Alexander Makarov "Let’s talk about code"

  • 1.
    ❌ Let's talkabout code ✔ Alexander Makarov, Yii framework
  • 2.
  • 3.
    Writing code iseasy! 👌
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
    1. Composition andinheritance. 2. State. 3. Dependencies and injection. 4. Methods. 5. Exceptions. 6. Object types. 7. Services and PHP long running servers. 8. Tests. 9. Principles. 10. Layers and abstraction.
  • 9.
    ❌ What breaksour code? ☠
  • 10.
    ● Inheritance. ● Coupling. ●State. ● Assumptions. ● Testing. ● Architecture. ● ...
  • 11.
    😍 Composition andinheritance 🤨
  • 12.
    😍 Composition vsinheritance 🤕
  • 13.
    abstract class Notifier { privatestring $template; private array $parameters; public function __construct(string $template, array $parameters = []) { // ... } protected function renderTemplate(): string { // ... } abstract public function send(): void; }
  • 14.
    final class EmailNotifierextends Notifier { private string $to; private string $subject; public function __construct(string $to, string $subject, string $template, array $parameters = []) { // ... } public function send(): void { mail($this->to, $this->subject, $this->renderTemplate()); } }
  • 15.
    abstract Notifier -compose message, send message ○ final EmailNotifier. ○ final SmsNotifier. ○ final DebugNotifier. Problems? Notifier is coupled with concrete implementations. We need more! ○ Sms notifier and Email notifier should log. ○ Sms notifier and Email notifier should not allow to send message twice. ○ We should send to multiple channels with fallback. ○ We should not send emails on April 1st. ○ 😱
  • 16.
    Let's try withoutinheritance? 😎
  • 17.
    final class Notifier { privateSender $sender; public function __construct(Sender $sender) { // ... } public function send(Message $message, Address $to): void { $this->sender->send($message, $to); } }
  • 18.
    final class Notifier { privateSender $sender; public function __construct(Sender $sender) { // ... } public function send(Message $message, Address $to): void { $this->sender->send($message, $to); } }
  • 19.
    interface Message { public functiongetSubject(): string; public function getText(): string; } final class Address { public function getEmail(): string { … } public function getPhone(): string { … } }
  • 20.
    interface Message { public functiongetSubject(): string; public function getText(): string; } final class Address { public function getEmail(): string { … } public function getPhone(): string { … } }
  • 21.
    interface Sender { public functionsend(Message $message, Address $to): void; }
  • 22.
    class EmailSender implementsSender class SmsSender implements Sender class Logger implements Sender class DuplicatePreventer implements Sender $sender = new DuplicatePreventer(new Logger(new SmsSender())); $notifier = new Notifier($sender); $notifier->send($message, $to);
  • 23.
    So… inheritance isbad? ● Yes. Prefer to avoid it. ● Use it where appropriate: exceptions and other hierarchies.
  • 24.
  • 25.
    final class DataProvider { publicfunction __construct(Query $query, Connection $connection) { ... } public function all() { return $this->connection->findAll($this->query); } public function one() { $limitedQuery = $this->query->limit(1); return $this->connection->findOne($limitedQuery); } }
  • 26.
    $query = (newQuery()) ->select() ->from('post'); $dataProvider = new DataProvider($query); var_dump($dataProvider->one()); var_dump($dataProvider->all());
  • 27.
    interface QueryInterface { public functionselect(string $columns): self; public function from(string $table): self; public function limit(int $limit): self; }
  • 28.
  • 30.
    public function limit(int$limit): self { $this->limit = $limit; return $this; }
  • 31.
    public function one() { $limitedQuery= $this->query->limit(1); return $this->connection->findOne($limitedQuery); }
  • 32.
  • 33.
    public function limit(int$limit): self { $new = clone $this; $new->limit = $limit; return $new; }
  • 34.
    public function limit(int$limit): self { $new = clone $this; $new->limit = $limit; return $new; }
  • 35.
    State ● Avoid itif possible. ● Encapsulate it. ● Absolutely avoid for fluent interfaces. ● Fluent interface = immutability.
  • 36.
  • 37.
    final class Notifier { privateSender $sender; public function __construct(Sender $sender) { // ... } public function send(Message $message, Address $to): void { $this->sender->send($message, $to); } }
  • 38.
    final class Notifier { privateSender $sender; public function __construct(Sender $sender) { // ... } public function send(Message $message, Address $to): void { $this->sender->send($message, $to); } }
  • 39.
    Is it possibleto do it wrong?
  • 40.
    final class Notifier { privateContainerInterface $container; public function __construct(ContainerInterface $container) { // ... } public function send(Message $message, Address $to): void { $sender = $this->container->get(Sender::class); $sender->send($message, $to); } }
  • 41.
    public function __construct(ContainerInterface$container) { // ... } public function send(Message $message, Address $to): void { $sender = $this->container->get(Sender::class);
  • 42.
    Dependency Injection ● DIis simple. ● You can do DI without container. ● Prefer composition over inheritance. ● Request what you need, not what you want to ask to get what you need. ● Avoid passing container around.
  • 43.
    Is it possibleto do moar wrong?
  • 45.
    final class Notifier { publicfunction send(Message $message, Address $to): void { $sender = App::$container->get(Sender::class); $sender->send($message, $to); } }
  • 46.
    final class Notifier { publicfunction send(Message $message, Address $to): void { $sender = App::$container->get(Sender::class); $sender->send($message, $to); } }
  • 47.
  • 48.
    Method structure 1. Preconditionchecks. 2. Failure scenarios. 3. Main path. 4. Postcondition checks. 5. return.
  • 49.
    Precondition checks ● Checkif arguments are valid. ● Throw InvalidArgumentException. ● Don't try to "fix" things.
  • 50.
    Fixing things ● Authvia Facebook. Same nickname present. Attach Facebook ID to the account. ● Only letters are allowed in a nickname. "samdark123" entered. Fix it by removing all the numbers.
  • 52.
    Failure scenarios ● RuntimeException. ●Avoid trying to "fix" things.
  • 53.
    Main path ● Justwhat method does.
  • 54.
    Postcondition checks ● Checkif what was done is done right. ● Throw RuntimeException if not.
  • 55.
    How do Iknow method is OK?
  • 56.
    Cyclomatic complexity Number ofindependent execution paths in a control graph.
  • 58.
    function doIt(int $number){ if ($number === 42) { $result = calculateSpecialResult(); } else { $result = calculateResult(42); } if ($result === null) { throw new RuntimeException('Unable to get result'); } return $result; }
  • 59.
    V(G) = E- N + 2 = 7 - 6 + 2 = 3 V(G) = P + 1 = 2 + 1 = 3 ● V(G) - cyclomatic complexity. ● E - edges. ● N - nodes. ● P - control structure points: ○ if → 1. ○ compound if → number of sub-conditions. ○ iteration → 1. ○ switch → 1 per branch. Keep cyclomatic complexity low.
  • 60.
  • 61.
    function login(array $data) { if(isset($data['username'], $data['password'])) { $user = $this->findUser($data['username']); if ($user !== null) { if ($user->isPasswordValid($data['password'])) { $this->loginUser(); $this->refresh(); } else { throw new InvalidArgumentException('Password is not valid.'); } } else { throw new InvalidArgumentException('User not found.'); } } else { throw new InvalidArgumentException('Both username and password are required.'); } }
  • 62.
    function login(array $data) { if(!isset($data['username'], $data['password'])) { throw new InvalidArgumentException('Both username and password are required.'); } $user = $this->findUser($data['username']); if ($user === null) { throw new InvalidArgumentException('User not found.'); } if (!$user->isPasswordValid($data['password'])) { throw new InvalidArgumentException('Password is not valid.'); } $this->loginUser(); $this->refresh(); }
  • 63.
    function login(array $data) { $this->assertUsernameAndPasswordPresent($data); $user= $this->findUser($data['username']); $this->assertUserFound($user); $this->assertPasswordValid($user, $data['password']); $this->loginUser(); $this->refresh(); }
  • 64.
  • 65.
    If a methodhas a boolean flag: 1. Single responsibility principle is violated. 2. You need multiple methods instead.
  • 66.
  • 67.
  • 68.
    Exceptions ● Two types: ○Ones we can handle. ■ Custom type, domain exceptions. ○ Ones we should not handle. ■ Prefer built-in types: ● RunitmeException. ● InvalidArgumentException.
  • 69.
    Exception messages ● Shouldbe explanative. ● Use proper English sentences.
  • 70.
  • 71.
  • 72.
    Services and non-services ●Services are doing something. ● Non-services hold data and return/mutate it.
  • 73.
    Domain objects ● Extractdomain objects to validate less. ○ Money, Point, Email. ● Composite values are object as well. ● Should not have dependencies injected. Only values. ● Use static named constructors: fromString, fromInt etc. ● Use private constructor for validation. ● Do not bloat these. ● Immutable.
  • 74.
    DTO ● Can gocrazy here but keep it simple. ● Public properties are fine :) ● Collect errors instead of throwing exceptions. ● Use mass-assign.
  • 75.
  • 76.
    Services / Components ●Created once. ● Usually immutable. ● Use proper DI. ● Usually stored in DI container and/or accessible via service locator.
  • 77.
    Rules for aservice ● Preferably stateless. ● No method injection or property injection. ● No optional dependencies. ● Explicit dependencies (proper DI). ● Action-relevant data should be passed to action method. ● Light constructors. Postpone initialization. ● Do not make assumptions. Throw exceptions right away.
  • 78.
    PHP long runningservers 🚀
  • 79.
    How it works //initialization while ($request = $psr7->acceptRequest()) { $response = $application->handle($request); $psr7->respond($response); gc_collect_cycles(); }
  • 80.
    Rules ● Stateless services/ State reset. ● Taking care about memory. ● Killing what's not used.
  • 81.
  • 82.
    How to writetests ● Arrange, act, assert. ● No dependencies. ● One test - one AAA. ● Prefer black box tests. ○ Test public API. ○ Hands off non-public API! ○ Don't check internal state! ○ Don't test methods, tests behavior.
  • 83.
  • 84.
    Should it beimmutable? ● Service = immutable (if possible). ● Entity = mutable. ● Other objects = immutable. ● Fluent interface = immutable (don't do it with mutable objects).
  • 85.
    How to mutateobject? ● DTO: ○ Directly. ● Value objects: ○ Immutable chaining. ● Entities: ○ Record changes as events. ● Other objects: ○ Immutable chaining.
  • 86.
  • 87.
    Command-query separation ● Doingboth is confusing. ● In some cases it is necessary.
  • 88.
    Query method class ShoppingCart { publicfunction getTotal(): int { } }
  • 89.
    Rules for queries ●Return a single type. ● Do not return internal state. ● Be specific. Avoid too flexible methods. ● Add abstraction for queries crossing system boundaries. ● Do not use command methods in query methods.
  • 90.
    Command method class ShoppingCart { publicfunction addItem(Item $item): void { if ($this->getTotal() + $item->cost > self::MAX_TOTAL) { throw new MaxTotalReached('Leave something for others!'); } $this->items[] = $item; } }
  • 91.
    Rules for commands ●Name in imperative form. ● Limit the scope, postpone with events and queues. ● Throw exceptions. ● Add abstraction for commands crossing system boundaries. ● Use queries to get info, commands to process it.
  • 92.
  • 93.
    Read/write models ● Similarto query / command. ● Search in Elastic / Insert to MySQL. ● Read models should be use-case specific. ● Read models should be close to data source.
  • 94.
    🥞 Layers andabstraction 🍔
  • 98.
    Don't over-abstract ● Someobjects are meant to be concrete: ○ Controllers. ○ Application services. ○ Entities. ○ Value objects.
  • 99.
  • 100.
    1. Composition andinheritance. 2. State. 3. Dependencies and injection. 4. Methods. 5. Exceptions. 6. Object types. 7. Services and PHP long running servers. 8. Tests. 9. Principles. 10. Layers and abstraction.
  • 101.
  • 102.
  • 103.
  • 104.
    ❓ Questions time! ●https://www.yiiframework.com/ ● https://twitter.com/sam_dark ● https://rmcreative.ru/ ● PHP Russia Videos ● Further learning: ○ Matthias Noback https://matthiasnoback.nl/ ○ Cyclomatic complexity by Anna Filina https://www.youtube.com/watch?v=UdjEb6t9-e4 ○ Anthony Ferrara videos: https://www.youtube.com/user/ircmaxell/videos