SOLID In Practice
@jessicamauerhan | http://bit.ly/solid-oop
@jessicamauerhan
Software Engineer
@HelpScout
jessicamauerhan@gmail.com
jmauerhan.wordpress.com
@jessicamauerhan | http://bit.ly/solid-oop
What is SOLID?
● Five principles for Object
Oriented Programming
● Guidelines which can help
ensure system is easy to
maintain
● Primarily focused on
communication between
dependencies or
collaborators
Let ϕ(픁) be a property provable
about objects 픁 of type T.
Then ϕ(퐲) should be true for
objects 퐲 of type S where S is a
subtype of T
@jessicamauerhan | http://bit.ly/solid-oop
Class Rectangle
{
protected $length;
protected $height;
public function area()
{
return $this-length * $this-area();
}
public function setLength($length)
{
$this-length = $length;
}
public function setHeight($height)
{
$this-height = $height;
}
}
Class Square extends Rectangle
{
public function setLength($length)
{
$this-length = $length;
$this-height = $length;
}
public function setHeight($height)
{
$this-length = $height;
$this-height = $height;
}
}
http://bit.ly/solid-oop
@jessicamauerhan | http://bit.ly/solid-oop
S.O.L.I.D.
● Single Responsibility
● Open/Closed
● Liskov Substitution
● Interface Segregation
● Dependency Inversion
It's All About Change
@jessicamauerhan | http://bit.ly/solid-oop
User Registration
● Collect User Input
● Validate User Input
● Persist User Data
● Send Confirmation Email
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
}
}
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
}
private function emailIsRegistered(string $email): bool
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
$result = pg_query($db, SELECT id FROM users WHERE (email='{$email}'));
$rows = pg_num_rows($result);
return $rows  0;
}
}
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
//Validate user input
if ($this-emailIsRegistered($user-getEmail())) {
return new Response('register', ['error' = 'Your email address is already registered']);
}
}
private function emailIsRegistered(string $email): bool
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
$result = pg_query($db, SELECT id FROM users WHERE (email='{$email}'));
$rows = pg_num_rows($result);
return $rows  0;
}
}
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
//Validate user input
if ($this-emailIsRegistered($user-getEmail())) {
return new Response('register', ['error' = 'Your email address is already registered']);
}
//Persist User
$this-saveUser($user);
}
private function emailIsRegistered(string $email): bool { [...] }
private function saveUser(User $user)
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
pg_query($db, INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}'));
}
}
Class UserRegistration
{
public function register(Request $request): Response
{
[...]
}
private function emailIsRegistered(string $email): bool
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
$result = pg_query($db, SELECT id FROM users WHERE (email='{$email}'));
$rows = pg_num_rows($result);
return $rows  0;
}
private function saveUser(User $user)
{
$dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER'];
$db = pg_connect($dsn);
pg_query($db, INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}'));
}
}
Class UserRegistration
{
public function register(Request $request): Response
{
[...]
}
private function emailIsRegistered(string $email): bool
{
$rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows  0;
}
private function saveUser(User $user)
{
DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}'));
}
}
Class UserRegistration
{
public function register(Request $request): Response
{
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
//Validate user input
if ($this-emailIsRegistered($user-getEmail())) {
return new Response('register', ['error' = 'Your email address is already registered']);
}
//Persist User
$this-saveUser($user);
//Send Confirmation Email
$this-sendConfirmationEmail($user-getEmail());
return new Response('register', ['success' = 'You are registered! Please check your email!']);
}
private function sendConfirmationEmail(string $email)
{
$subject = Confirm Email;
$message = Please aclick here/a to confirm your email!;
$headers = From: mysite@email.com;
mail($email, $subject, $message, $headers);
}
Class UserRegistration
{
public function register(Request $request): Response {
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
//Validate user input
if ($this-emailIsRegistered($user-getEmail())) {
return new Response('register', ['error' = 'Your email address is already registered']);
}
//Persist User
$this-saveUser($user);
//Send Confirmation Email
$this-sendConfirmationEmail($user-getEmail());
return new Response('register', ['success' = 'You are registered! Please check your email!']);
}
private function sendConfirmationEmail(string $email) {
$subject = Confirm Email;
$message = Please aclick here/a to confirm your email!;
$headers = From: mysite@email.com;
mail($email, $subject, $message, $headers);
}
private function saveUser(User $user) {
DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}'));
}
private function emailIsRegistered(string $email): bool {
$rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows  0;
}
}
Class UserRegistration
{
public function register(Request $request): Response {
//Collect user input
if (!$request-has('email') || !$request-has('password')) {
return new Response('register', ['error' = 'Please provide an email and a password']);
}
$user = new User();
$user-email = $request-get('email');
$user-password = $request-get('password');
//Validate user input
if ($this-emailIsRegistered($user-getEmail())) {
return new Response('register', ['error' = 'Your email address is already registered']);
}
//Persist User
$this-saveUser($user);
//Send Confirmation Email
$this-sendConfirmationEmail($user-getEmail());
return new Response('register', ['success' = 'You are registered! Please check your email!']);
}
private function sendConfirmationEmail(string $email) {
$subject = Confirm Email;
$message = Please aclick here/a to confirm your email!;
$headers = From: mysite@email.com;
mail($email, $subject, $message, $headers);
}
private function saveUser(User $user) {
DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}'));
}
private function emailIsRegistered(string $email): bool {
$rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows  0;
}
}
@jessicamauerhan | http://bit.ly/solid-oop
Dependency Injection
● Collaborators are supplied to
class from outside - aka
injected into it
Service Location
● Collaborators are retrieved or
instantiated from inside the
class using them
Class UserRegistration
{
public function register(Request $request): Response { [...] }
private function sendConfirmationEmail(string $email) { [...] }
private function saveUser(User $user)
{
$sql = INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')
DB::query($sql);
}
private function emailIsRegistered(string $email): bool
{
$rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows  0;
}
}
Class UserRegistration
{
private $db;
public function __construct(Database $db)
{
$this-db = $db;
}
public function register(Request $request): Response { [...] }
private function sendConfirmationEmail(string $email) { [...] }
private function saveUser(User $user)
{
$sql = INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')
$this-db-query($sql);
}
private function emailIsRegistered(string $email): bool
{
$rows = $this-db-query(SELECT count(id) FROM users WHERE (email='{$email}'));
return $rows  0;
}
}
Class UserRegistration
{
private $db;
private $mailer;
public function __construct(Database $db, Mailer $mailer)
{
$this-db = $db;
$this-mailer = $mailer;
}
public function register(Request $request): Response { [...] }
private function sendConfirmationEmail(string $email)
{
//Convert our email to Mandrill's email
$mandrillEmail = [
'to' = ['email' = $email-getTo()],
'from_email' = $email-getFrom(),
'subject' = $email-getSubject(),
'text' = $email-getMessage()
];
$this-mandrill-send($mandrillEmail);
}
private function saveUser(User $user) { [...] }
private function emailIsRegistered(string $email): bool { [...] }
Dependency Inversion
Single Responsibility Principle
Class UserRegistration
{
private $factory;
public function __construct(UserFactory $factory)
{
$this-factory = $factory;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
}
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
Class UserRegistration
{
private $factory, $validator;
public function __construct(UserFactory $factory, UserValidator $validator)
{
$this-factory = $factory;
$this-validator = $validator;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
if ($this-validator-isValid($user) === false) {
$invalidProperties = $this-validator
-getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository)
{
$this-factory = $factory;
$this-validator = $validator;
$this-repository = $repository;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
if ($this-validator-isValid($user) === false) {
$invalidProperties = $this-validator
-getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this-repository-create($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
}
}
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserRepository
{
/**
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
Class UserRegistration
{
private $factory, $validator, $repository, $emailer;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository, ConfirmationEmailer $emailer)
{
$this-factory = $factory;
$this-validator = $validator;
$this-repository = $repository;
$this-emailer = $emailer;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
if ($this-validator-isValid($user) === false) {
$invalidProperties = $this-validator
-getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this-repository-create($user);
$this-emailer-sendConfirmationEmail($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
} catch (MailerException $mailerException) {
return UnableToSendConfirmationEmailResponse($user);
}
}
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserRepository
{
/**
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserRepository
{
/**
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
Class UserRegistration
{
private $factory;
public function __construct(UserFactory $factory)
{
$this-factory = $factory;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
}
}
Class UserRegistration
{
private $factory, $validator;
public function __construct(UserFactory $factory, UserValidator $validator)
{
$this-factory = $factory;
$this-validator = $validator;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
if ($this-validator-isValid($user) === false) {
$invalidProperties = $this-validator
-getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository)
{
$this-factory = $factory;
$this-validator = $validator;
$this-repository = $repository;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
if ($this-validator-isValid($user) === false) {
$invalidProperties = $this-validator
-getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this-repository-create($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
}
}
}
Class UserRegistration
{
private $factory, $validator, $repository, $emailer;
public function __construct(UserFactory $factory, UserValidator $validator,
UserRepository $repository, ConfirmationEmailer $emailer)
{
$this-factory = $factory;
$this-validator = $validator;
$this-repository = $repository;
$this-emailer = $emailer;
}
public function register(Request $request): Response
{
try {
$user = $this-factory-createUser($request);
} catch (UnableToParseUser $exception) {
return new InvalidRequestResponse($exception-getErrors());
}
if ($this-validator-isValid($user) === false) {
$invalidProperties = $this-validator
-getInvalidProperties($user);
return new InvalidUserResponse($invalidProperties);
}
try {
$this-repository-create($user);
$this-emailer-sendConfirmationEmail($user);
return UserCreatedResponse($user);
} catch (UnableToCreateUser $exception) {
return UnableToCreateUserResponse($user);
} catch (MailerException $mailerException) {
return UnableToSendConfirmationEmailResponse($user);
}
}
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createRegisteringUser(Request $request): User;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserRepository
{
/**
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createUser(Request $request): User;
}
interface UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createUser(Request $request): User;
}
class RegisterFormUserFactory implements UserFactory
{
/**
* @throws UnableToParseUser
*/
public function createUser(Request $request): User
{
if(!$request-has('email') || !$request-has('password')){
throw new UnableToParseUser();
}
$email = $request-get('email');
$password = $request-get('password');
return new User($email, $password);
}
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
interface UserValidator
{
public function isValid(User $user): bool;
public function getInvalidProperties(User $user):array;
}
class NewUserValidator implements UserValidator
{
/** @var UserRepository */
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this-userRepository = $userRepository;
}
public function isValid(User $user): bool
{
$emailExists = $this-userRepository-emailExists($user-getEmail());
if ($emailExists === true) {
return false;
}
return true;
}
}
interface UserRepository
{
/**
* @param User $user
* @return bool
*
* @throws UnableToCreateUser
*/
public function create(User $user): bool;
/**
* @param string $email
* @return bool
*
* @throws DataSourceError
*/
public function emailExists(string $email): bool;
}
class PdoUserRepository implements UserRepository
{
/** @var Pdo */
private $pdo;
public function __construct(Pdo $pdo) {
$this-pdo = $pdo;
}
public function create(User $user): bool {
try {
$statement = $this-pdo-prepare(INSERT INTO users(email, password) VALUES(:email, :password));
$statement-execute(['email' = $user-getEmail(), 'password' = $user-getPassword()]);
} catch (PDOException $PDOException) {
throw new UnableToCreateUser($user-getEmail());
}
return true;
}
public function emailExists(string $email): bool {
try {
$statement = $this-pdo-prepare(SELECT count(id) AS numRows FROM users WHERE email = :email);
$statement-execute([email = $email]);
$result = $statement-fetchColumn();
return ($result  0);
} catch (PDOException $PDOException) {
throw new DataSourceError();
}
}
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
class MandrillConfirmationEmailer implements ConfirmationEmailer
{
private $mandrill;
public function __construct(Mandrill $mandrill)
{
$this-mandrill = $mandrill;
}
public function send(User $user)
{
$message = Please aclick here/a to confirm your account.;
$from = app@example.com;
$subject = Confirm your account;
$email = [
'to' = [['EmailTemplate' = $user-getEmail()]],
'from_email' = $from,
'subject' = $subject,
'text' = $message
];
$this-mandrill-send($email);
}
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
class MandrillConfirmationEmailer implements ConfirmationEmailer
{
private $mandrill;
public function __construct(Mandrill $mandrill)
{
$this-mandrill = $mandrill;
}
public function send(User $user)
{
$message = Please aclick here/a to confirm your account.;
$from = app@example.com;
$subject = Confirm your account;
$email = [
'to' = [['EmailTemplate' = $user-getEmail()]],
'from_email' = $from,
'subject' = $subject,
'text' = $message
];
$this-mandrill-send($email);
}
}
interface ConfirmationEmailer
{
/**
* @throws MailerException
*/
public function send(string $to);
}
class MandrillConfirmationEmailer implements ConfirmationEmailer
{
private $mandrill;
public function __construct(Mandrill $mandrill)
{
$this-mandrill = $mandrill;
}
public function send(User $user)
{
$message = Please aclick here/a to confirm your account.;
$from = app@example.com;
$subject = Confirm your account;
$email = [
'to' = [['EmailTemplate' = $user-getEmail()]],
'from_email' = $from,
'subject' = $subject,
'text' = $message
];
$this-mandrill-send($email);
}
}
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this-to = $to;
$this-from = $from;
$this-subject = $subject;
$this-message = $message;
}
public function getTo(): string {
return $this-to;
}
public function getFrom(): string {
return $this-from;
}
public function getSubject(): string {
return $this-subject;
}
public function getMessage(): string {
return $this-message;
}
}
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this-to = $to;
$this-from = $from;
$this-subject = $subject;
$this-message = $message;
}
public function getTo(): string {
return $this-to;
}
public function getFrom(): string {
return $this-from;
}
public function getSubject(): string {
return $this-subject;
}
public function getMessage(): string {
return $this-message;
}
}
class ConfirmationEmailService implements ConfirmationEmailer
{
private $mailer;
private $from = 'mysite@example.com';
private $subject = 'Confirm Email';
private $message = 'Please click here to confirm your email';
public function __construct(Mailer $mailer) {
$this-mailer = $mailer;
}
public function send(string $to) {
$email = new Email($to, $this-from,
$this-subject, $this-message);
$this-mailer-send($email);
}
}
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this-to = $to;
$this-from = $from;
$this-subject = $subject;
$this-message = $message;
}
public function getTo(): string {
return $this-to;
}
public function getFrom(): string {
return $this-from;
}
public function getSubject(): string {
return $this-subject;
}
public function getMessage(): string {
return $this-message;
}
}
class ConfirmationEmailService implements ConfirmationEmailer
{
private $mailer;
private $from = 'mysite@example.com';
private $subject = 'Confirm Email';
private $message = 'Please click here to confirm your email';
public function __construct(Mailer $mailer) {
$this-mailer = $mailer;
}
public function send(string $to) {
$email = new Email($to, $this-from,
$this-subject, $this-message);
$this-mailer-send($email);
}
}
class MandrillMailer implements Mailer
{
private $mandrill;
public function __construct(Mandrill $mandrill) {
$this-mandrill = $mandrill;
}
public function send(Email $email) {
//Convert our email to Mandrill's email
$mandrillMail = ['to' = ['email' = $email-getTo()],
'from_email' = $email-getFrom(),
'subject' = $email-getSubject(),
'text' = $email-getMessage()];
$this-mandrill-send($mandrillEmail);
}
}
interface ConfirmationEmailer
{
public function send(string $to); /** @throws MailerException */
}
interface Mailer
{
public function send(Email $email);
}
class Email
{
private $to, $from, $subject, $message;
public function __construct(string $to, string $from,
string $subject, string $message) {
$this-to = $to;
$this-from = $from;
$this-subject = $subject;
$this-message = $message;
}
public function getTo(): string {
return $this-to;
}
public function getFrom(): string {
return $this-from;
}
public function getSubject(): string {
return $this-subject;
}
public function getMessage(): string {
return $this-message;
}
}
class ConfirmationEmailService implements ConfirmationEmailer
{
private $mailer;
private $from = 'mysite@example.com';
private $subject = 'Confirm Email';
private $message = 'Please click here to confirm your email';
public function __construct(Mailer $mailer) {
$this-mailer = $mailer;
}
public function send(string $to) {
$email = new Email($to, $this-from,
$this-subject, $this-message);
$this-mailer-send($email);
}
}
class SendGridMailer implements Mailer
{
private $sendgrid;
public function __construct(SendGrid $sendgrid) {
$this-sedngrid = $sendgrid;
}
public function send(Email $email) {
//Convert our email to SendGrid's email
$sgEmail = new SendGridEmail($email-getTo(),
$email-getFrom(),
$email-getSubject(),
$email-getMessage());
$this-sendgrid-queue($sgEmail);
}
}
class AccountOverdueListener
{
private $mailer;
public function __construct(Mailer $mailer)
{
$this-mailer = $mailer;
}
public function handle(OverdueEvent $event)
{
$account = $event-getAccount();
$email = new OverdueEmail($account-getEmailAddress(), $account-getAmountDue());
$this-mailer-send($email);
}
}
class AccountOverdueListener
{
private $mailer;
private $texter;
public function __construct(Mailer $mailer, Texter $texter)
{
$this-mailer = $mailer;
$this-texter = $texter;
}
public function handle(OverdueEvent $event)
{
$account = $event-getAccount();
$notifications = $account-getEnabledNotifications();
foreach ($notifications AS $notificationType) {
if ($notificationType === 'email') {
$email = new OverdueEmail($account-getEmailAddress(), $account-getAmountDue());
$this-mailer-send($email);
} elseif ($notificationType === 'text') {
$text = new OverdueText($account-getPhoneNumber(), $account-getAmountDue());
$this-texter-send($text);
}
}
}
}
Open/Closed Principle
class AccountOverdueEmailListener
{
public function handle(Mailer $mailer, OverdueEvent $event)
{
$account = $event-getAccount();
$email = new OverdueEmail($account-getEmailAddress(), $account-getAmountDue());
$mailer-send($email);
}
}
class AccountOverdueTextListener
{
public function handle(Texter $texter, OverdueEvent $event)
{
$account = $event-getAccount();
$text = new OverdueText($account-getPhoneNumber(), $account-getAmountDue());
$texter-send($text);
}
}
E-Commerce App
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this-discount = $discount;
}
public function calculateAmountOff(Item $item)
{
return ($this-discount / 100) * $item-getCost();
}
}
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this-discount = $discount;
}
public function calculateAmountOff(Item $item)
{
return ($this-discount / 100) * $item-getCost();
}
}
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function setEligibleType(string $type)
{
$this-type = $type;
}
public function calculateAmountOff(Item $item)
{
if ($this-hasEnoughItems()  $item-isType($this-type)) {
return ($this-discount / 100) * $item-getCost();
}
return 'Error: Not enough items';
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item-isType($this-type)) {
$found++;
}
if ($found == $this-countItemsRequired) {
return true;
}
}
return false;
}
}
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this-discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
return ($this-discount / 100) * $item-getCost();
}
}
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function setEligibleType(string $type)
{
$this-type = $type;
}
public function calculateAmountOff(Item $item): float
{
if ($this-hasEnoughItems()  $item-isType($this-type)) {
return ($this-discount / 100) * $item-getCost();
}
throw new NotEnoughItems();
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item-isType($this-type)) {
$found++;
}
if ($found == $this-countItemsRequired) {
return true;
}
}
return false;
}
}
Liskov Substitution Principle
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this-discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
return ($this-discount / 100) * $item-getCost();
}
}
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function setEligibleType(string $type)
{
$this-type = $type;
}
public function calculateAmountOff(Item $item): float
{
if ($this-hasEnoughItems()  $item-isType($this-type)) {
return ($this-discount / 100) * $item-getCost();
}
throw new NotEnoughItems();
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item-isType($this-type)) {
$found++;
}
if ($found == $this-countItemsRequired) {
return true;
}
}
return false;
}
}
Class PercentageDiscount
{
private $discount;
public function __construct(int $discount)
{
$this-discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
return ($this-discount / 100) * $item-getCost();
}
}
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function __construct($discount, $type)
{
$this-type = $type;
parent::__construct($discount);
}
public function calculateAmountOff(Item $item)
{
if ($this-hasEnoughItems()  $item-isType($this-type)) {
return ($this-discount / 100) * $item-getCost();
}
return 0;
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item-isType($this-type)) {
$found++;
}
if ($found == $this-countItemsRequired) {
return true;
}
}
return false;
}
Interface Discount
{
/** @throws DiscountException */
public function calculateAmountOff(Item $item): float;
}
Class DiscountException extends Exception
{
}
Class PercentageDiscount implements Discount
{
private $discount;
public function __construct(int $discount)
{
$this-discount = $discount;
}
public function calculateAmountOff(Item $item): float
{
return ($this-discount / 100) * $item-getCost();
}
}
Class BuyItemsToGetDiscount extends PercentageDiscount
{
private $type;
private $countItemsRequired = 2;
public function __construct($discount, $type)
{
$this-type = $type;
parent::__construct($discount);
}
public function calculateAmountOff(Item $item)
{
if ($this-hasEnoughItems()  $item-isType($this-type)) {
return ($this-discount / 100) * $item-getCost();
}
throw new DiscountException ();
}
public function hasEnoughItems(array $items): bool
{
$found = 0;
foreach ($items AS $item) {
if ($item-isType($this-type)) {
$found++;
}
if ($found == $this-countItemsRequired) {
return true;
}
}
return false;
}
Shopping Cart
Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
class Cart
{
private $items;
private $total;
public function addItem(Item $item)
{
$this-items[] = $item;
}
public function calculateTotal()
{
$subtotal = 0;
$shippingTotal = 0;
foreach ($this-items AS $item) {
$subtotal += $item-getPrice();
$shippingTotal += $item-getShippingCost();
}
return $subtotal + $shippingTotal;
}
}
Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
Class Cart { [...] }
Class ServiceManager
{
private $services;
public function addService(Item $item)
{
$this-services[] = $item;
}
public function getCompletedServices()
{
$completed = [];
foreach ($this-services AS $service) {
if ($service-isCompleted()) {
$completed[] = $service;
}
}
return $completed;
}
}
Interface Segregation
Interface Item
{
public function getName(): string;
public function getPrice(): float;
public function getShippingWeight(): float;
public function getShippingCost(): float;
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
Interface Buyable
{
public function getName(): string;
public function getPrice(): float;
}
Interface Product
{
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
Interface Service
{
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
Interface Buyable
{
public function getName(): string;
public function getPrice(): float;
}
Interface Product
{
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
Interface Service
{
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
Class BuyableItem implements Buyable
{
protected $name, $price;
public function __construct(string $name,
float $price)
{
$this-name = $name;
$this-price = $price;
}
public function getName(): string
{
return $this-name;
}
public function getPrice(): float
{
return $this-price;
}
}
Interface Buyable
{
public function getName(): string;
public function getPrice(): float;
}
Interface Product
{
public function getShippingWeight(): float;
public function getShippingCost(): float;
}
Interface Service
{
public function getWorkerAssigned(): Worker;
public function assignWorker(Worker $worker);
public function getServiceScheduledDate(): DateTime;
public function getServiceCompletedDate(): DateTime;
public function isCompleted(): bool;
}
Class BuyableItem implements Buyable { [...] }
Class BuyableProduct extends BuyableItem implements
Product
{
protected $shippingCost, $shippingWeight;
public function __construct(string $name,
float $price,
float $shippingCost,
float $shippingWeight)
{
$this-shippingCost = $shippingCost;
$this-shippingWeight = $shippingWeight;
parent::__construct($name, $price);
}
public function getShippingCost(): float {
return $this-getShippingCost();
}
public function getShippingWeight(): float {
return $this-shippingWeight;
}
}
S.O.L.I.D.
● Single Responsibility - Registration Class delegating to Collaborators
● Open/Closed - Adding new Notifiers without always editing Listener
● Liskov Substitution - Ensured Discount subtypes could be used as Discount
● Interface Segregation - Items use Buyable, Product and Service interfaces
● Dependency Inversion - Registration Controller, Emailer, Shopping Cart,
Notification Factory - all use interfaces, not implementations
@jessicamauerhan | http://bit.ly/solid-oop
SLD: Apply Often
 Preemptively
● Single Responsibility
● Liskov Substitution
● Dependency Inversion
OI: Apply As Needed
When Changes Occur
● Open/Closed
● Interface Segregation
@jessicamauerhan | http://bit.ly/solid-oop
Thank You!
SOLID in Practice
http://bit.ly/solid-oop
Feedback  Questions?
Welcome  Encouraged!
@jessicamauerhan
jessicamauerhan@gmail.com
jmauerhan.wordpress.com

SOLID in Practice

  • 1.
    SOLID In Practice @jessicamauerhan| http://bit.ly/solid-oop
  • 2.
  • 3.
    @jessicamauerhan | http://bit.ly/solid-oop Whatis SOLID? ● Five principles for Object Oriented Programming ● Guidelines which can help ensure system is easy to maintain ● Primarily focused on communication between dependencies or collaborators
  • 4.
    Let ϕ(픁) bea property provable about objects 픁 of type T. Then ϕ(퐲) should be true for objects 퐲 of type S where S is a subtype of T
  • 5.
    @jessicamauerhan | http://bit.ly/solid-oop ClassRectangle { protected $length; protected $height; public function area() { return $this-length * $this-area(); } public function setLength($length) { $this-length = $length; } public function setHeight($height) { $this-height = $height; } } Class Square extends Rectangle { public function setLength($length) { $this-length = $length; $this-height = $length; } public function setHeight($height) { $this-length = $height; $this-height = $height; } }
  • 6.
  • 7.
    @jessicamauerhan | http://bit.ly/solid-oop S.O.L.I.D. ●Single Responsibility ● Open/Closed ● Liskov Substitution ● Interface Segregation ● Dependency Inversion
  • 8.
  • 9.
    @jessicamauerhan | http://bit.ly/solid-oop UserRegistration ● Collect User Input ● Validate User Input ● Persist User Data ● Send Confirmation Email
  • 10.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); } }
  • 11.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); } private function emailIsRegistered(string $email): bool { $dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER']; $db = pg_connect($dsn); $result = pg_query($db, SELECT id FROM users WHERE (email='{$email}')); $rows = pg_num_rows($result); return $rows 0; } }
  • 12.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); //Validate user input if ($this-emailIsRegistered($user-getEmail())) { return new Response('register', ['error' = 'Your email address is already registered']); } } private function emailIsRegistered(string $email): bool { $dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER']; $db = pg_connect($dsn); $result = pg_query($db, SELECT id FROM users WHERE (email='{$email}')); $rows = pg_num_rows($result); return $rows 0; } }
  • 13.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); //Validate user input if ($this-emailIsRegistered($user-getEmail())) { return new Response('register', ['error' = 'Your email address is already registered']); } //Persist User $this-saveUser($user); } private function emailIsRegistered(string $email): bool { [...] } private function saveUser(User $user) { $dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER']; $db = pg_connect($dsn); pg_query($db, INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')); } }
  • 14.
    Class UserRegistration { public functionregister(Request $request): Response { [...] } private function emailIsRegistered(string $email): bool { $dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER']; $db = pg_connect($dsn); $result = pg_query($db, SELECT id FROM users WHERE (email='{$email}')); $rows = pg_num_rows($result); return $rows 0; } private function saveUser(User $user) { $dsn = 'host='.$_ENV['DB_HOST'].' dbname='.$_ENV['DB_DB'].' password='.$_ENV['DB_PASS'].' user='.$_ENV['DB_USER']; $db = pg_connect($dsn); pg_query($db, INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')); } }
  • 15.
    Class UserRegistration { public functionregister(Request $request): Response { [...] } private function emailIsRegistered(string $email): bool { $rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}')); return $rows 0; } private function saveUser(User $user) { DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')); } }
  • 16.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); //Validate user input if ($this-emailIsRegistered($user-getEmail())) { return new Response('register', ['error' = 'Your email address is already registered']); } //Persist User $this-saveUser($user); //Send Confirmation Email $this-sendConfirmationEmail($user-getEmail()); return new Response('register', ['success' = 'You are registered! Please check your email!']); } private function sendConfirmationEmail(string $email) { $subject = Confirm Email; $message = Please aclick here/a to confirm your email!; $headers = From: mysite@email.com; mail($email, $subject, $message, $headers); }
  • 17.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); //Validate user input if ($this-emailIsRegistered($user-getEmail())) { return new Response('register', ['error' = 'Your email address is already registered']); } //Persist User $this-saveUser($user); //Send Confirmation Email $this-sendConfirmationEmail($user-getEmail()); return new Response('register', ['success' = 'You are registered! Please check your email!']); } private function sendConfirmationEmail(string $email) { $subject = Confirm Email; $message = Please aclick here/a to confirm your email!; $headers = From: mysite@email.com; mail($email, $subject, $message, $headers); } private function saveUser(User $user) { DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')); } private function emailIsRegistered(string $email): bool { $rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}')); return $rows 0; } }
  • 18.
    Class UserRegistration { public functionregister(Request $request): Response { //Collect user input if (!$request-has('email') || !$request-has('password')) { return new Response('register', ['error' = 'Please provide an email and a password']); } $user = new User(); $user-email = $request-get('email'); $user-password = $request-get('password'); //Validate user input if ($this-emailIsRegistered($user-getEmail())) { return new Response('register', ['error' = 'Your email address is already registered']); } //Persist User $this-saveUser($user); //Send Confirmation Email $this-sendConfirmationEmail($user-getEmail()); return new Response('register', ['success' = 'You are registered! Please check your email!']); } private function sendConfirmationEmail(string $email) { $subject = Confirm Email; $message = Please aclick here/a to confirm your email!; $headers = From: mysite@email.com; mail($email, $subject, $message, $headers); } private function saveUser(User $user) { DB::query(INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}')); } private function emailIsRegistered(string $email): bool { $rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}')); return $rows 0; } }
  • 19.
    @jessicamauerhan | http://bit.ly/solid-oop DependencyInjection ● Collaborators are supplied to class from outside - aka injected into it Service Location ● Collaborators are retrieved or instantiated from inside the class using them
  • 20.
    Class UserRegistration { public functionregister(Request $request): Response { [...] } private function sendConfirmationEmail(string $email) { [...] } private function saveUser(User $user) { $sql = INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}') DB::query($sql); } private function emailIsRegistered(string $email): bool { $rows = DB::query(SELECT count(id) FROM users WHERE (email='{$email}')); return $rows 0; } }
  • 21.
    Class UserRegistration { private $db; publicfunction __construct(Database $db) { $this-db = $db; } public function register(Request $request): Response { [...] } private function sendConfirmationEmail(string $email) { [...] } private function saveUser(User $user) { $sql = INSERT INTO users(email, password) VALUES('{$user-email}', '{$user-password}') $this-db-query($sql); } private function emailIsRegistered(string $email): bool { $rows = $this-db-query(SELECT count(id) FROM users WHERE (email='{$email}')); return $rows 0; } }
  • 22.
    Class UserRegistration { private $db; private$mailer; public function __construct(Database $db, Mailer $mailer) { $this-db = $db; $this-mailer = $mailer; } public function register(Request $request): Response { [...] } private function sendConfirmationEmail(string $email) { //Convert our email to Mandrill's email $mandrillEmail = [ 'to' = ['email' = $email-getTo()], 'from_email' = $email-getFrom(), 'subject' = $email-getSubject(), 'text' = $email-getMessage() ]; $this-mandrill-send($mandrillEmail); } private function saveUser(User $user) { [...] } private function emailIsRegistered(string $email): bool { [...] }
  • 23.
  • 24.
  • 25.
    Class UserRegistration { private $factory; publicfunction __construct(UserFactory $factory) { $this-factory = $factory; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } } } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; }
  • 26.
    interface UserFactory { /** * @throwsUnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } Class UserRegistration { private $factory, $validator; public function __construct(UserFactory $factory, UserValidator $validator) { $this-factory = $factory; $this-validator = $validator; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } if ($this-validator-isValid($user) === false) { $invalidProperties = $this-validator -getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } } }
  • 27.
    Class UserRegistration { private $factory,$validator, $repository; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository) { $this-factory = $factory; $this-validator = $validator; $this-repository = $repository; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } if ($this-validator-isValid($user) === false) { $invalidProperties = $this-validator -getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this-repository-create($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } } } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } interface UserRepository { /** * @throws UnableToCreateUser */ public function create(User $user): bool; } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; }
  • 28.
    Class UserRegistration { private $factory,$validator, $repository, $emailer; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository, ConfirmationEmailer $emailer) { $this-factory = $factory; $this-validator = $validator; $this-repository = $repository; $this-emailer = $emailer; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } if ($this-validator-isValid($user) === false) { $invalidProperties = $this-validator -getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this-repository-create($user); $this-emailer-sendConfirmationEmail($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } catch (MailerException $mailerException) { return UnableToSendConfirmationEmailResponse($user); } } } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } interface UserRepository { /** * @throws UnableToCreateUser */ public function create(User $user): bool; } interface ConfirmationEmailer { /** * @throws MailerException */ public function send(string $to); } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } interface UserRepository { /** * @throws UnableToCreateUser */ public function create(User $user): bool; } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; }
  • 29.
    Class UserRegistration { private $factory; publicfunction __construct(UserFactory $factory) { $this-factory = $factory; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } } } Class UserRegistration { private $factory, $validator; public function __construct(UserFactory $factory, UserValidator $validator) { $this-factory = $factory; $this-validator = $validator; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } if ($this-validator-isValid($user) === false) { $invalidProperties = $this-validator -getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } } } Class UserRegistration { private $factory, $validator, $repository; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository) { $this-factory = $factory; $this-validator = $validator; $this-repository = $repository; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } if ($this-validator-isValid($user) === false) { $invalidProperties = $this-validator -getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this-repository-create($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } } } Class UserRegistration { private $factory, $validator, $repository, $emailer; public function __construct(UserFactory $factory, UserValidator $validator, UserRepository $repository, ConfirmationEmailer $emailer) { $this-factory = $factory; $this-validator = $validator; $this-repository = $repository; $this-emailer = $emailer; } public function register(Request $request): Response { try { $user = $this-factory-createUser($request); } catch (UnableToParseUser $exception) { return new InvalidRequestResponse($exception-getErrors()); } if ($this-validator-isValid($user) === false) { $invalidProperties = $this-validator -getInvalidProperties($user); return new InvalidUserResponse($invalidProperties); } try { $this-repository-create($user); $this-emailer-sendConfirmationEmail($user); return UserCreatedResponse($user); } catch (UnableToCreateUser $exception) { return UnableToCreateUserResponse($user); } catch (MailerException $mailerException) { return UnableToSendConfirmationEmailResponse($user); } } } interface UserFactory { /** * @throws UnableToParseUser */ public function createRegisteringUser(Request $request): User; } interface UserValidator { public function isValid(User $user): bool; public function getInvalidProperties(User $user):array; } interface UserRepository { /** * @throws UnableToCreateUser */ public function create(User $user): bool; } interface ConfirmationEmailer { /** * @throws MailerException */ public function send(string $to); }
  • 30.
    interface UserFactory { /** * @throwsUnableToParseUser */ public function createUser(Request $request): User; }
  • 31.
    interface UserFactory { /** * @throwsUnableToParseUser */ public function createUser(Request $request): User; } class RegisterFormUserFactory implements UserFactory { /** * @throws UnableToParseUser */ public function createUser(Request $request): User { if(!$request-has('email') || !$request-has('password')){ throw new UnableToParseUser(); } $email = $request-get('email'); $password = $request-get('password'); return new User($email, $password); } }
  • 32.
    interface UserValidator { public functionisValid(User $user): bool; public function getInvalidProperties(User $user):array; }
  • 33.
    interface UserValidator { public functionisValid(User $user): bool; public function getInvalidProperties(User $user):array; } class NewUserValidator implements UserValidator { /** @var UserRepository */ private $userRepository; public function __construct(UserRepository $userRepository) { $this-userRepository = $userRepository; } public function isValid(User $user): bool { $emailExists = $this-userRepository-emailExists($user-getEmail()); if ($emailExists === true) { return false; } return true; } }
  • 34.
    interface UserRepository { /** * @paramUser $user * @return bool * * @throws UnableToCreateUser */ public function create(User $user): bool; /** * @param string $email * @return bool * * @throws DataSourceError */ public function emailExists(string $email): bool; }
  • 35.
    class PdoUserRepository implementsUserRepository { /** @var Pdo */ private $pdo; public function __construct(Pdo $pdo) { $this-pdo = $pdo; } public function create(User $user): bool { try { $statement = $this-pdo-prepare(INSERT INTO users(email, password) VALUES(:email, :password)); $statement-execute(['email' = $user-getEmail(), 'password' = $user-getPassword()]); } catch (PDOException $PDOException) { throw new UnableToCreateUser($user-getEmail()); } return true; } public function emailExists(string $email): bool { try { $statement = $this-pdo-prepare(SELECT count(id) AS numRows FROM users WHERE email = :email); $statement-execute([email = $email]); $result = $statement-fetchColumn(); return ($result 0); } catch (PDOException $PDOException) { throw new DataSourceError(); } } }
  • 36.
    interface ConfirmationEmailer { /** * @throwsMailerException */ public function send(string $to); }
  • 37.
    interface ConfirmationEmailer { /** * @throwsMailerException */ public function send(string $to); } class MandrillConfirmationEmailer implements ConfirmationEmailer { private $mandrill; public function __construct(Mandrill $mandrill) { $this-mandrill = $mandrill; } public function send(User $user) { $message = Please aclick here/a to confirm your account.; $from = app@example.com; $subject = Confirm your account; $email = [ 'to' = [['EmailTemplate' = $user-getEmail()]], 'from_email' = $from, 'subject' = $subject, 'text' = $message ]; $this-mandrill-send($email); } }
  • 38.
    interface ConfirmationEmailer { /** * @throwsMailerException */ public function send(string $to); } class MandrillConfirmationEmailer implements ConfirmationEmailer { private $mandrill; public function __construct(Mandrill $mandrill) { $this-mandrill = $mandrill; } public function send(User $user) { $message = Please aclick here/a to confirm your account.; $from = app@example.com; $subject = Confirm your account; $email = [ 'to' = [['EmailTemplate' = $user-getEmail()]], 'from_email' = $from, 'subject' = $subject, 'text' = $message ]; $this-mandrill-send($email); } }
  • 39.
    interface ConfirmationEmailer { /** * @throwsMailerException */ public function send(string $to); } class MandrillConfirmationEmailer implements ConfirmationEmailer { private $mandrill; public function __construct(Mandrill $mandrill) { $this-mandrill = $mandrill; } public function send(User $user) { $message = Please aclick here/a to confirm your account.; $from = app@example.com; $subject = Confirm your account; $email = [ 'to' = [['EmailTemplate' = $user-getEmail()]], 'from_email' = $from, 'subject' = $subject, 'text' = $message ]; $this-mandrill-send($email); } }
  • 40.
    interface ConfirmationEmailer { public functionsend(string $to); /** @throws MailerException */ }
  • 41.
    interface ConfirmationEmailer { public functionsend(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); }
  • 42.
    interface ConfirmationEmailer { public functionsend(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this-to = $to; $this-from = $from; $this-subject = $subject; $this-message = $message; } public function getTo(): string { return $this-to; } public function getFrom(): string { return $this-from; } public function getSubject(): string { return $this-subject; } public function getMessage(): string { return $this-message; } }
  • 43.
    interface ConfirmationEmailer { public functionsend(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this-to = $to; $this-from = $from; $this-subject = $subject; $this-message = $message; } public function getTo(): string { return $this-to; } public function getFrom(): string { return $this-from; } public function getSubject(): string { return $this-subject; } public function getMessage(): string { return $this-message; } } class ConfirmationEmailService implements ConfirmationEmailer { private $mailer; private $from = 'mysite@example.com'; private $subject = 'Confirm Email'; private $message = 'Please click here to confirm your email'; public function __construct(Mailer $mailer) { $this-mailer = $mailer; } public function send(string $to) { $email = new Email($to, $this-from, $this-subject, $this-message); $this-mailer-send($email); } }
  • 44.
    interface ConfirmationEmailer { public functionsend(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this-to = $to; $this-from = $from; $this-subject = $subject; $this-message = $message; } public function getTo(): string { return $this-to; } public function getFrom(): string { return $this-from; } public function getSubject(): string { return $this-subject; } public function getMessage(): string { return $this-message; } } class ConfirmationEmailService implements ConfirmationEmailer { private $mailer; private $from = 'mysite@example.com'; private $subject = 'Confirm Email'; private $message = 'Please click here to confirm your email'; public function __construct(Mailer $mailer) { $this-mailer = $mailer; } public function send(string $to) { $email = new Email($to, $this-from, $this-subject, $this-message); $this-mailer-send($email); } } class MandrillMailer implements Mailer { private $mandrill; public function __construct(Mandrill $mandrill) { $this-mandrill = $mandrill; } public function send(Email $email) { //Convert our email to Mandrill's email $mandrillMail = ['to' = ['email' = $email-getTo()], 'from_email' = $email-getFrom(), 'subject' = $email-getSubject(), 'text' = $email-getMessage()]; $this-mandrill-send($mandrillEmail); } }
  • 45.
    interface ConfirmationEmailer { public functionsend(string $to); /** @throws MailerException */ } interface Mailer { public function send(Email $email); } class Email { private $to, $from, $subject, $message; public function __construct(string $to, string $from, string $subject, string $message) { $this-to = $to; $this-from = $from; $this-subject = $subject; $this-message = $message; } public function getTo(): string { return $this-to; } public function getFrom(): string { return $this-from; } public function getSubject(): string { return $this-subject; } public function getMessage(): string { return $this-message; } } class ConfirmationEmailService implements ConfirmationEmailer { private $mailer; private $from = 'mysite@example.com'; private $subject = 'Confirm Email'; private $message = 'Please click here to confirm your email'; public function __construct(Mailer $mailer) { $this-mailer = $mailer; } public function send(string $to) { $email = new Email($to, $this-from, $this-subject, $this-message); $this-mailer-send($email); } } class SendGridMailer implements Mailer { private $sendgrid; public function __construct(SendGrid $sendgrid) { $this-sedngrid = $sendgrid; } public function send(Email $email) { //Convert our email to SendGrid's email $sgEmail = new SendGridEmail($email-getTo(), $email-getFrom(), $email-getSubject(), $email-getMessage()); $this-sendgrid-queue($sgEmail); } }
  • 46.
    class AccountOverdueListener { private $mailer; publicfunction __construct(Mailer $mailer) { $this-mailer = $mailer; } public function handle(OverdueEvent $event) { $account = $event-getAccount(); $email = new OverdueEmail($account-getEmailAddress(), $account-getAmountDue()); $this-mailer-send($email); } }
  • 47.
    class AccountOverdueListener { private $mailer; private$texter; public function __construct(Mailer $mailer, Texter $texter) { $this-mailer = $mailer; $this-texter = $texter; } public function handle(OverdueEvent $event) { $account = $event-getAccount(); $notifications = $account-getEnabledNotifications(); foreach ($notifications AS $notificationType) { if ($notificationType === 'email') { $email = new OverdueEmail($account-getEmailAddress(), $account-getAmountDue()); $this-mailer-send($email); } elseif ($notificationType === 'text') { $text = new OverdueText($account-getPhoneNumber(), $account-getAmountDue()); $this-texter-send($text); } } } }
  • 48.
  • 49.
    class AccountOverdueEmailListener { public functionhandle(Mailer $mailer, OverdueEvent $event) { $account = $event-getAccount(); $email = new OverdueEmail($account-getEmailAddress(), $account-getAmountDue()); $mailer-send($email); } } class AccountOverdueTextListener { public function handle(Texter $texter, OverdueEvent $event) { $account = $event-getAccount(); $text = new OverdueText($account-getPhoneNumber(), $account-getAmountDue()); $texter-send($text); } }
  • 50.
  • 51.
    Class PercentageDiscount { private $discount; publicfunction __construct(int $discount) { $this-discount = $discount; } public function calculateAmountOff(Item $item) { return ($this-discount / 100) * $item-getCost(); } }
  • 52.
    Class PercentageDiscount { private $discount; publicfunction __construct(int $discount) { $this-discount = $discount; } public function calculateAmountOff(Item $item) { return ($this-discount / 100) * $item-getCost(); } } Class BuyItemsToGetDiscount extends PercentageDiscount { private $type; private $countItemsRequired = 2; public function setEligibleType(string $type) { $this-type = $type; } public function calculateAmountOff(Item $item) { if ($this-hasEnoughItems() $item-isType($this-type)) { return ($this-discount / 100) * $item-getCost(); } return 'Error: Not enough items'; } public function hasEnoughItems(array $items): bool { $found = 0; foreach ($items AS $item) { if ($item-isType($this-type)) { $found++; } if ($found == $this-countItemsRequired) { return true; } } return false; } }
  • 53.
    Class PercentageDiscount { private $discount; publicfunction __construct(int $discount) { $this-discount = $discount; } public function calculateAmountOff(Item $item): float { return ($this-discount / 100) * $item-getCost(); } } Class BuyItemsToGetDiscount extends PercentageDiscount { private $type; private $countItemsRequired = 2; public function setEligibleType(string $type) { $this-type = $type; } public function calculateAmountOff(Item $item): float { if ($this-hasEnoughItems() $item-isType($this-type)) { return ($this-discount / 100) * $item-getCost(); } throw new NotEnoughItems(); } public function hasEnoughItems(array $items): bool { $found = 0; foreach ($items AS $item) { if ($item-isType($this-type)) { $found++; } if ($found == $this-countItemsRequired) { return true; } } return false; } }
  • 54.
  • 55.
    Class PercentageDiscount { private $discount; publicfunction __construct(int $discount) { $this-discount = $discount; } public function calculateAmountOff(Item $item): float { return ($this-discount / 100) * $item-getCost(); } } Class BuyItemsToGetDiscount extends PercentageDiscount { private $type; private $countItemsRequired = 2; public function setEligibleType(string $type) { $this-type = $type; } public function calculateAmountOff(Item $item): float { if ($this-hasEnoughItems() $item-isType($this-type)) { return ($this-discount / 100) * $item-getCost(); } throw new NotEnoughItems(); } public function hasEnoughItems(array $items): bool { $found = 0; foreach ($items AS $item) { if ($item-isType($this-type)) { $found++; } if ($found == $this-countItemsRequired) { return true; } } return false; } }
  • 56.
    Class PercentageDiscount { private $discount; publicfunction __construct(int $discount) { $this-discount = $discount; } public function calculateAmountOff(Item $item): float { return ($this-discount / 100) * $item-getCost(); } } Class BuyItemsToGetDiscount extends PercentageDiscount { private $type; private $countItemsRequired = 2; public function __construct($discount, $type) { $this-type = $type; parent::__construct($discount); } public function calculateAmountOff(Item $item) { if ($this-hasEnoughItems() $item-isType($this-type)) { return ($this-discount / 100) * $item-getCost(); } return 0; } public function hasEnoughItems(array $items): bool { $found = 0; foreach ($items AS $item) { if ($item-isType($this-type)) { $found++; } if ($found == $this-countItemsRequired) { return true; } } return false; }
  • 57.
    Interface Discount { /** @throwsDiscountException */ public function calculateAmountOff(Item $item): float; } Class DiscountException extends Exception { } Class PercentageDiscount implements Discount { private $discount; public function __construct(int $discount) { $this-discount = $discount; } public function calculateAmountOff(Item $item): float { return ($this-discount / 100) * $item-getCost(); } } Class BuyItemsToGetDiscount extends PercentageDiscount { private $type; private $countItemsRequired = 2; public function __construct($discount, $type) { $this-type = $type; parent::__construct($discount); } public function calculateAmountOff(Item $item) { if ($this-hasEnoughItems() $item-isType($this-type)) { return ($this-discount / 100) * $item-getCost(); } throw new DiscountException (); } public function hasEnoughItems(array $items): bool { $found = 0; foreach ($items AS $item) { if ($item-isType($this-type)) { $found++; } if ($found == $this-countItemsRequired) { return true; } } return false; }
  • 58.
  • 59.
    Interface Item { public functiongetName(): string; public function getPrice(): float; public function getShippingWeight(): float; public function getShippingCost(): float; }
  • 60.
    Interface Item { public functiongetName(): string; public function getPrice(): float; public function getShippingWeight(): float; public function getShippingCost(): float; } class Cart { private $items; private $total; public function addItem(Item $item) { $this-items[] = $item; } public function calculateTotal() { $subtotal = 0; $shippingTotal = 0; foreach ($this-items AS $item) { $subtotal += $item-getPrice(); $shippingTotal += $item-getShippingCost(); } return $subtotal + $shippingTotal; } }
  • 61.
    Interface Item { public functiongetName(): string; public function getPrice(): float; public function getShippingWeight(): float; public function getShippingCost(): float; public function getWorkerAssigned(): Worker; public function assignWorker(Worker $worker); public function getServiceScheduledDate(): DateTime; public function getServiceCompletedDate(): DateTime; public function isCompleted(): bool; } Class Cart { [...] } Class ServiceManager { private $services; public function addService(Item $item) { $this-services[] = $item; } public function getCompletedServices() { $completed = []; foreach ($this-services AS $service) { if ($service-isCompleted()) { $completed[] = $service; } } return $completed; } }
  • 62.
  • 63.
    Interface Item { public functiongetName(): string; public function getPrice(): float; public function getShippingWeight(): float; public function getShippingCost(): float; public function getWorkerAssigned(): Worker; public function assignWorker(Worker $worker); public function getServiceScheduledDate(): DateTime; public function getServiceCompletedDate(): DateTime; public function isCompleted(): bool; } Interface Buyable { public function getName(): string; public function getPrice(): float; } Interface Product { public function getShippingWeight(): float; public function getShippingCost(): float; } Interface Service { public function getWorkerAssigned(): Worker; public function assignWorker(Worker $worker); public function getServiceScheduledDate(): DateTime; public function getServiceCompletedDate(): DateTime; public function isCompleted(): bool; }
  • 64.
    Interface Buyable { public functiongetName(): string; public function getPrice(): float; } Interface Product { public function getShippingWeight(): float; public function getShippingCost(): float; } Interface Service { public function getWorkerAssigned(): Worker; public function assignWorker(Worker $worker); public function getServiceScheduledDate(): DateTime; public function getServiceCompletedDate(): DateTime; public function isCompleted(): bool; } Class BuyableItem implements Buyable { protected $name, $price; public function __construct(string $name, float $price) { $this-name = $name; $this-price = $price; } public function getName(): string { return $this-name; } public function getPrice(): float { return $this-price; } }
  • 65.
    Interface Buyable { public functiongetName(): string; public function getPrice(): float; } Interface Product { public function getShippingWeight(): float; public function getShippingCost(): float; } Interface Service { public function getWorkerAssigned(): Worker; public function assignWorker(Worker $worker); public function getServiceScheduledDate(): DateTime; public function getServiceCompletedDate(): DateTime; public function isCompleted(): bool; } Class BuyableItem implements Buyable { [...] } Class BuyableProduct extends BuyableItem implements Product { protected $shippingCost, $shippingWeight; public function __construct(string $name, float $price, float $shippingCost, float $shippingWeight) { $this-shippingCost = $shippingCost; $this-shippingWeight = $shippingWeight; parent::__construct($name, $price); } public function getShippingCost(): float { return $this-getShippingCost(); } public function getShippingWeight(): float { return $this-shippingWeight; } }
  • 66.
    S.O.L.I.D. ● Single Responsibility- Registration Class delegating to Collaborators ● Open/Closed - Adding new Notifiers without always editing Listener ● Liskov Substitution - Ensured Discount subtypes could be used as Discount ● Interface Segregation - Items use Buyable, Product and Service interfaces ● Dependency Inversion - Registration Controller, Emailer, Shopping Cart, Notification Factory - all use interfaces, not implementations
  • 67.
    @jessicamauerhan | http://bit.ly/solid-oop SLD:Apply Often Preemptively ● Single Responsibility ● Liskov Substitution ● Dependency Inversion OI: Apply As Needed When Changes Occur ● Open/Closed ● Interface Segregation
  • 68.
    @jessicamauerhan | http://bit.ly/solid-oop ThankYou! SOLID in Practice http://bit.ly/solid-oop Feedback Questions? Welcome Encouraged! @jessicamauerhan jessicamauerhan@gmail.com jmauerhan.wordpress.com