Zero to SOLID
in 45 Minutes
by Vic Metcalfe
@v_metcalfe
I made an
e-Commerce
site in PHP!
Children or those feint of
heart are warned to leave
the room…
<?php
session_start();
$connection = new PDO('mysql:host=localhost;dbname=solid', 'root', '');
if (!isset($_SESSION['cart'])) {
$connection->exec("INSERT INTO cart () VALUES ()");
$_SESSION['cart'] = $connection->lastInsertId();
}
if (isset($_POST['addproduct'])) {
$sql = "INSERT INTO cartitem (cart, product, quantity)
VALUES (:cart, :product, :quantity)
ON DUPLICATE KEY UPDATE quantity = quantity + :quantity";
$parameters = [
'cart' => $_SESSION['cart'],
'product' => $_POST['addproduct'],
'quantity' => $_POST['quantity'],
];
$statement = $connection->prepare($sql);
$statement->execute($parameters);
}
if (isset($_POST['update'])) {
$sql = "UPDATE cartitem SET quantity=:quantity
WHERE cart=:cart and product=:product";
$parameters = [
'cart' => $_SESSION['cart'],
'product' => $_POST['update'],
'quantity' => $_POST['quantity'],
];
$statement = $connection->prepare($sql);
$statement->execute($parameters);
}
https://github.com/zymsys/solid/blob/00/cart.php
$statement = $connection->prepare("SELECT * FROM cartitem
WHERE cart=:cart AND quantity <> 0");
$statement->execute(['cart' => $_SESSION['cart']]);
$cartItems = $statement->fetchAll();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GTA-PHP Gift Shop</title>
<link rel="stylesheet" href="site.css">
</head>
<body>
<div class="container">
<h1>GTA-PHP Gift Shop</h1>
<p>Buy our junk to keep our organizers up to date
with the latest gadgets.</p>
<table class="table">
<tr>
<th>Product Name</th>
<th>You Pay</th>
<th>Group Gets</th>
<th><!-- Column for add to cart button --></th>
</tr>
<?php
$products = [];
$result = $connection->query("SELECT * FROM product");
foreach ($result as $product) {
$products[$product['id']] = $product;
?>
<tr>
<td><?php echo $product['name']; ?></td>
https://github.com/zymsys/solid/blob/00/cart.php
<td><?php
$price = $product['price'];
echo number_format($price / 100, 2);
?></td>
<td>
<?php
echo number_format(
($product['price'] - $product['cost']) / 100, 2
);
?>
</td>
<td>
<form method="post">
<input type="number" name="quantity"
value="1" style="width: 3em">
<input type="hidden" name="addproduct"
value="<?php echo $product['id']; ?>">
<input class="btn btn-default btn-xs"
type="submit" value="Add to Cart">
</form>
</td>
</tr>
<?php
}
?>
</table>
<?php if (count($cartItems) > 0): ?>
<?php
$total = 0;
$taxable = 0;
$provinceCode = isset($_GET['province']) ?
$_GET['province'] : 'ON'; //Default to GTA-PHP's home
$provinces = [];
https://github.com/zymsys/solid/blob/00/cart.php
$result = $connection->query("SELECT * FROM province
ORDER BY name");
foreach ($result as $row) {
$provinces[$row['code']] = $row;
if ($row['code'] === $provinceCode) {
$province = $row;
}
}
?>
<h2>Your Cart:</h2>
<table class="table">
<?php foreach ($cartItems as $cartItem): ?>
<?php $product = $products[$cartItem['product']]; ?>
<tr>
<td>
<?php echo $product['name']; ?>
</td>
<td>
<form method="post">
Quantity:
<input type="hidden" name="update"
value="<?php echo $product['id']; ?>">
<input type="number" name="quantity" style="width: 3em"
value="<?php echo $cartItem['quantity']; ?>">
<button type="submit">Update</button>
</form>
</td>
<td>
<?php
echo number_format(
$cartItem['quantity'] * $product['price'] / 100, 2
);
$itemTotal = $cartItem['quantity'] * $product['price'];
https://github.com/zymsys/solid/blob/00/cart.php
$total += $itemTotal;
$taxable += $product['taxes'] ? $itemTotal : 0;
?>
</td>
</tr>
<?php endforeach; ?>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">Subtotal:</td>
<td><?php echo number_format($total / 100, 2); ?></td>
</tr>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">
<?php echo $province['name']; ?> taxes at
<?php echo $province['taxrate'] ?>%:</td>
<td>
<?php
$taxes = $taxable * $province['taxrate'] / 100;
$total += $taxes;
echo number_format($taxes / 100, 2);
?>
</td>
</tr>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">Total:</td>
<td><?php echo number_format($total / 100, 2); ?></td>
</tr>
</table>
<form method="get">
Calculate taxes for purchase from:
<select name="province">
https://github.com/zymsys/solid/blob/00/cart.php
<?php foreach ($provinces as $province): ?>
<?php
$selected = $provinceCode === $province['code'] ? 'selected' : '';
?>
<option value="<?php echo $province['code']; ?>"
<?php echo $selected; ?>>
<?php echo $province['name']; ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-default btn-xs">
Recalculate</button>
</form>
<form action="checkout.php" method="post">
<?php foreach ($cartItems as $itemNumber => $cartItem): ?>
<?php
$product = $products[$cartItem['product']];
?>
<input type="hidden" name="item<?php echo $itemNumber; ?>"
value="<?php echo $product['name'] . '|' .
number_format($product['price'] / 100, 2); ?>">
<?php endforeach; ?>
<input type="hidden" name="item<?php echo count($cartItems); ?>"
value="<?php echo 'Tax|' . number_format($taxes / 100, 2); ?>">
<button type="submit" class="btn btn-primary" style="float: right">
Checkout
</button>
</form>
<?php endif; ?>
</div>
</body>
</html>
https://github.com/zymsys/solid/blob/00/cart.php
Is there anything
wrong with this?
Step 1:
Separate PHP from
HTML
<?php
function initialize()
{
global $connection;
session_start();
$connection = new PDO('mysql:host=localhost;dbname=solid', 'root', '');
if (!isset($_SESSION['cart'])) {
$connection->exec("INSERT INTO cart () VALUES ()");
$_SESSION['cart'] = $connection->lastInsertId();
}
}
function handleAdd()
{
global $connection;
if (!isset($_POST['addproduct'])) {
return;
}
$sql = "INSERT INTO cartitem (cart, product, quantity)
VALUES (:cart, :product, :quantity)
ON DUPLICATE KEY UPDATE quantity = quantity + :quantity";
$parameters = [
'cart' => $_SESSION['cart'],
'product' => $_POST['addproduct'],
'quantity' => $_POST['quantity'],
];
$statement = $connection->prepare($sql);
$statement->execute($parameters);
}
https://github.com/zymsys/solid/blob/01/cart.php
function handleUpdate()
{
global $connection;
if (!isset($_POST['update'])) {
return;
}
$sql = "UPDATE cartitem SET quantity=:quantity
WHERE cart=:cart and product=:product";
$parameters = [
'cart' => $_SESSION['cart'],
'product' => $_POST['update'],
'quantity' => $_POST['quantity'],
];
$statement = $connection->prepare($sql);
$statement->execute($parameters);
}
function loadCartItems()
{
global $connection, $viewData;
$viewData = [];
$statement = $connection->prepare("SELECT * FROM cartitem
WHERE cart=:cart AND quantity <> 0");
$statement->execute(['cart' => $_SESSION['cart']]);
return $statement->fetchAll();
}
https://github.com/zymsys/solid/blob/01/cart.php
function loadProducts()
{
global $connection;
$products = [];
$result = $connection->query("SELECT * FROM product");
foreach ($result as $product) {
$products[$product['id']] = $product;
}
return $products;
}
function loadProvinces()
{
global $connection;
$provinces = [];
$result = $connection->query("SELECT * FROM province ORDER BY name");
foreach ($result as $row) {
$provinces[$row['code']] = $row;
}
return $provinces;
}
https://github.com/zymsys/solid/blob/01/cart.php
function calculateCartSubtotal($cartItems, $products)
{
$subtotal = 0;
foreach ($cartItems as $cartItem) {
$product = $products[$cartItem['product']];
$subtotal += $cartItem['quantity'] * $product['price'];
}
return $subtotal;
}
function calculateCartTaxes($cartItems, $products, $taxrate)
{
$taxable = 0;
foreach ($cartItems as $cartItem) {
$product = $products[$cartItem['product']];
$taxable += $product['taxes'] ?
$cartItem['quantity'] * $product['price'] : 0;
}
return $taxable * $taxrate / 100;
}
https://github.com/zymsys/solid/blob/01/cart.php
function buildViewData()
{
$viewData = [
'cartItems' => loadCartItems(),
'products' => loadProducts(),
'provinces' => loadProvinces(),
'provinceCode' => isset($_GET['province']) ?
$_GET['province'] : 'ON', //Default to GTA-PHP's home
];
foreach ($viewData['provinces'] as $province) {
if ($province['code'] === $viewData['provinceCode']) {
$viewData['province'] = $province;
}
}
$viewData['subtotal'] = calculateCartSubtotal($viewData['cartItems'],
$viewData['products']);
$viewData['taxes'] = calculateCartTaxes($viewData['cartItems'],
$viewData['products'], $viewData['province']['taxrate']);
$viewData['total'] = $viewData['subtotal'] + $viewData['taxes'];
return $viewData;
}
initialize();
handleAdd();
handleUpdate();
$viewData = buildViewData();
?>
https://github.com/zymsys/solid/blob/01/cart.php
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GTA-PHP Gift Shop</title>
<link rel="stylesheet" href="site.css">
</head>
<body>
<div class="container">
<h1>GTA-PHP Gift Shop</h1>
<p>Buy our junk to keep our organizers up to date
with the latest gadgets.</p>
<table class="table">
<tr>
<th>Product Name</th>
<th>You Pay</th>
<th>Group Gets</th>
<th><!-- Column for add to cart button --></th>
</tr>
<?php foreach ($viewData['products'] as $product): ?>
<tr>
<td><?php echo $product['name']; ?></td>
<td><?php
$price = $product['price'];
echo number_format($price / 100, 2);
?></td>
<td><?php
echo number_format(
($product['price'] - $product['cost']) / 100, 2
);
?></td>
https://github.com/zymsys/solid/blob/01/cart.php
<td>
<form method="post">
<input type="number" name="quantity"
value="1" style="width: 3em">
<input type="hidden" name="addproduct"
value="<?php echo $product['id']; ?>">
<input class="btn btn-default btn-xs"
type="submit" value="Add to Cart">
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
<?php if (count($viewData['cartItems']) > 0): ?>
<h2>Your Cart:</h2>
<table class="table">
<?php foreach ($viewData['cartItems'] as $cartItem): ?>
<?php $product = $viewData['products'][$cartItem['product']]; ?>
<tr>
<td>
<?php echo $product['name']; ?>
</td>
<td>
<form method="post">
Quantity:
<input type="hidden" name="update"
value="<?php echo $product['id']; ?>">
<input type="number" name="quantity" style="width: 3em"
value="<?php echo $cartItem['quantity']; ?>">
<button type="submit">Update</button>
</form>
</td>
https://github.com/zymsys/solid/blob/01/cart.php
<td>
<?php
echo number_format(
$cartItem['quantity'] * $product['price'] / 100, 2
);
?>
</td>
</tr>
<?php endforeach; ?>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">Subtotal:</td>
<td><?php echo number_format($viewData['subtotal'] / 100, 2); ?></td>
</tr>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">
<?php echo $viewData['province']['name']; ?> taxes at
<?php echo $viewData['province']['taxrate'] ?>%:</td>
<td>
<?php
echo number_format($viewData['taxes'] / 100, 2);
?>
</td>
</tr>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">Total:</td>
<td><?php echo number_format($viewData['total'] / 100, 2); ?></td>
</tr>
</table>
https://github.com/zymsys/solid/blob/01/cart.php
<form method="get">
Calculate taxes for purchase from:
<select name="province">
<?php foreach ($viewData['provinces'] as $province): ?>
<?php
$selected = $viewData['provinceCode'] ===
$province['code'] ? 'selected' : '';
?>
<option value="<?php echo $province['code']; ?>"
<?php echo $selected; ?>>
<?php echo $province['name']; ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-default btn-xs">Recalculate</button>
</form>
<form action="checkout.php" method="post">
<?php foreach ($viewData['cartItems'] as $itemNumber => $cartItem): ?>
<?php
$product = $viewData['products'][$cartItem['product']];
?>
<input type="hidden" name="item<?php echo $itemNumber; ?>"
value="<?php
echo $product['name'] . '|' .
number_format($product['price'] / 100, 2);
?>">
<?php endforeach; ?>
https://github.com/zymsys/solid/blob/01/cart.php
<input type="hidden"
name="item<?php echo count($viewData['cartItems']); ?>"
value="<?php echo 'Tax|' .
number_format($viewData['taxes'] / 100, 2); ?>">
<button type="submit" class="btn btn-primary" style="float: right">
Checkout</button>
</form>
<?php endif; ?>
</div>
</body>
</html>
https://github.com/zymsys/solid/blob/01/cart.php
Any room for
improvement now?
Objects
A very brief introduction
Objects help us to
organize our code
function initialize()
function handleAdd()
function handleUpdate()
function loadCartItems()
function loadProducts()
function loadProvinces()
function calculateCartSubtotal($cartItems, $products)
function calculateCartTaxes($cartItems, $products, $taxrate)
function buildViewData()
How might we group these functions?
function initialize()
function handleAdd()
function handleUpdate()
function loadCartItems()
function loadProducts()
function loadProvinces()
function calculateCartSubtotal($cartItems, $products)
function calculateCartTaxes($cartItems, $products, $taxrate)
function buildViewData()
Invisible stuff
that happens on
page load
Loads stuff into
our HTML (view)
Objects help us to
encapsulate code
function initialize()
function handleAdd()
function handleUpdate()
function loadCartItems()
function loadProducts()
function loadProvinces()
function calculateCartSubtotal($cartItems, $products)
function calculateCartTaxes($cartItems, $products, $taxrate)
function buildViewData()
Invisible stuff
that happens on
page load
Loads stuff into
our HTML (view)
function initialize()
function handleAdd()
function handleUpdate()
function loadCartItems()
function loadProducts()
function loadProvinces()
function calculateCartSubtotal($cartItems, $products)
function calculateCartTaxes($cartItems, $products, $taxrate)
function buildViewData()
private
private
public
private
private
public
private
private
private
class Initializer
{
private $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function initialize()
{
session_start();
if (!isset($_SESSION['cart'])) {
$this->connection->exec("INSERT INTO cart () VALUES ()");
$_SESSION['cart'] = $this->connection->lastInsertId();
}
$this->handleAdd();
$this->handleUpdate();
}
private function handleAdd() { … }
private function handleUpdate() { … }
}
https://github.com/zymsys/solid/blob/02/cart.php
class ViewData {
private $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
private function loadCartItems() { … }
private function loadProducts(){ … }
private function loadProvinces(){ … }
private function calculateCartSubtotal($cartItems, $products) { … }
private function calculateCartTaxes($cartItems, $products, $taxrate) { … }
public function buildViewData() { … }
}
https://github.com/zymsys/solid/blob/02/cart.php
public function buildViewData()
{
$viewData = [
'cartItems' => $this->loadCartItems(),
'products' => $this->loadProducts(),
'provinces' => $this->loadProvinces(),
'provinceCode' => isset($_GET['province']) ?
$_GET['province'] : 'ON', //Default to GTA-PHP's home
];
foreach ($viewData['provinces'] as $province) {
if ($province['code'] === $viewData['provinceCode']) {
$viewData['province'] = $province;
}
}
$viewData['subtotal'] = $this->calculateCartSubtotal($viewData['cartItems'],
$viewData['products']);
$viewData['taxes'] = $this->calculateCartTaxes($viewData['cartItems'],
$viewData['products'], $viewData['province']['taxrate']);
$viewData['total'] = $viewData['subtotal'] + $viewData['taxes'];
return $viewData;
}
https://github.com/zymsys/solid/blob/02/cart.php
SOLID
Fewer Gremlins in your Code
SRP
Single Responsibility Principal
Responsibility?
Business Responsibility
not code
function initialize()
function handleAdd()
function handleUpdate()
function loadCartItems()
function loadProducts()
function loadProvinces()
function calculateCartSubtotal($cartItems, $products)
function calculateCartTaxes($cartItems, $products, $taxrate)
function buildViewData()
Invisible stuff
that happens on
page load
Loads stuff into
our HTML (view)
function initialize()
function handleAdd()
function handleUpdate()
function loadCartItems()
function loadProducts()
function loadProvinces()
function calculateCartSubtotal($cartItems, $products)
function calculateCartTaxes($cartItems, $products, $taxrate)
function buildViewData()
Sales
Accounting
Inventory
Application / IT
class Application
{
private $connection;
public function __construct()
{
$this->connection = new PDO('mysql:host=localhost;dbname=solid', 'root', '');
$this->inventory = new Inventory($this->connection);
$this->sales = new Sales($this->connection);
$this->accounting = new Accounting($this->connection);
}
public function initialize()
{
session_start();
if (!isset($_SESSION['cart'])) {
$this->connection->exec("INSERT INTO cart VALUES ()");
$_SESSION['cart'] = $this->connection->lastInsertId();
}
$this->handlePost();
}
https://github.com/zymsys/solid/blob/03/cart.php
public function buildViewData()
{
$viewData = [
'cartItems' => $this->sales->loadCartItems(),
'products' => $this->inventory->loadProducts(),
'provinces' => $this->accounting->loadProvinces(),
'provinceCode' => isset($_GET['province']) ?
$_GET['province'] : 'ON', //Default to GTA-PHP's home
];
foreach ($viewData['provinces'] as $province) {
if ($province['code'] === $viewData['provinceCode']) {
$viewData['province'] = $province;
}
}
$viewData['subtotal'] = $this->accounting->
calculateCartSubtotal($viewData['cartItems'], $viewData['products']);
$viewData['taxes'] = $this->accounting->
calculateCartTaxes($viewData['cartItems'],
$viewData['products'], $viewData['province']['taxrate']);
$viewData['total'] = $viewData['subtotal'] + $viewData['taxes'];
return $viewData;
}
//Class Application Continued…
https://github.com/zymsys/solid/blob/03/cart.php
private function handlePost()
{
if (isset($_POST['addproduct'])) {
$this->sales->addProductToCart(
$_SESSION['cart'],
$_POST['addproduct'],
$_POST['quantity']
);
}
if (isset($_POST['update'])) {
$this->sales->modifyProductQuantityInCart(
$_SESSION['cart'],
$_POST['update'],
$_POST['quantity']
);
}
}
}
//Class Application Continued…
https://github.com/zymsys/solid/blob/03/cart.php
class Inventory {
private $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function loadProducts()
{
$products = [];
$result = $this->connection->query("SELECT * FROM product");
foreach ($result as $product) {
$products[$product['id']] = $product;
}
return $products;
}
}
https://github.com/zymsys/solid/blob/03/cart.php
class Sales {
private $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function addProductToCart($cartId, $productId, $quantity)
{
$sql = "INSERT INTO cartitem (cart, product, quantity)
VALUES (:cart, :product, :quantity)
ON DUPLICATE KEY UPDATE quantity = quantity + :quantity";
$parameters = [
'cart' => $cartId,
'product' => $productId,
'quantity' => $quantity,
];
$statement = $this->connection->prepare($sql);
$statement->execute($parameters);
}
https://github.com/zymsys/solid/blob/03/cart.php
public function modifyProductQuantityInCart($cartId, $productId, $quantity)
{
$sql = "UPDATE cartitem SET quantity=:quantity
WHERE cart=:cart and product=:product";
$parameters = [
'cart' => $cartId,
'product' => $productId,
'quantity' => $quantity,
];
$statement = $this->connection->prepare($sql);
$statement->execute($parameters);
}
public function loadCartItems()
{
$statement = $this->connection->prepare("SELECT * FROM cartitem
WHERE cart=:cart AND quantity <> 0");
$statement->execute(['cart' => $_SESSION['cart']]);
return $statement->fetchAll();
}
}
//Class Sales Continued…
https://github.com/zymsys/solid/blob/03/cart.php
class Accounting {
private $connection;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function loadProvinces()
{
$provinces = [];
$result = $this->connection->query("SELECT * FROM province ORDER BY name");
foreach ($result as $row) {
$provinces[$row['code']] = $row;
}
return $provinces;
}
public function calculateCartSubtotal($cartItems, $products)
{
$subtotal = 0;
foreach ($cartItems as $cartItem) {
$product = $products[$cartItem['product']];
$subtotal += $cartItem['quantity'] * $product['price'];
}
return $subtotal;
}
https://github.com/zymsys/solid/blob/03/cart.php
public function calculateCartTaxes($cartItems, $products, $taxrate)
{
$taxable = 0;
foreach ($cartItems as $cartItem) {
$product = $products[$cartItem['product']];
$taxable += $product['taxes'] ?
$cartItem['quantity'] * $product['price'] : 0;
}
return $taxable * $taxrate / 100;
}
}
//Class Accounting Continued…
https://github.com/zymsys/solid/blob/03/cart.php
OCP
Open/Closed Principle
Open and Closed?
Open to Extension
Closed to Modification
New requirement:
10% off orders over $100
Where does the code
go?
First we need to
understand inheritance
and polymorphism
Classes can extend
other classes
class AccountingStrategy {
private $description;
public function __construct($description)
{
$this->description = $description;
}
public function getAdjustment($cartItems)
{
return false;
}
public function getDescription()
{
return $this->description;
}
}
https://github.com/zymsys/solid/blob/04/cart.php
class TaxAccountingStrategy extends AccountingStrategy {
private $products;
private $taxRate;
public function __construct($products, $province)
{
parent::__construct($province['name'] . ' taxes at ' .
$province['taxrate'] . '%:');
$this->products = $products;
$this->taxRate = $province['taxrate'];
}
public function getAdjustment($cartItems)
{
$taxable = 0;
foreach ($cartItems as $cartItem) {
$product = $this->products[$cartItem['product']];
$taxable += $product['taxes'] ?
$cartItem['quantity'] * $product['price'] : 0;
}
return $taxable * $this->taxRate / 100;
}
}
https://github.com/zymsys/solid/blob/04/cart.php
TaxAccountingStrategy is
an AccountingStrategy.
public function initialize()
{
session_start();
if (!isset($_SESSION['cart'])) {
$this->connection->exec("INSERT INTO cart VALUES ()");
$_SESSION['cart'] = $this->connection->lastInsertId();
}
$this->handlePost();
$this->products = $this->inventory->loadProducts();
$provinceRepository = new ProvinceRepository($this->connection,
isset($_GET['province']) ? $_GET['province'] : 'ON');
$this->provinces = $provinceRepository->loadProvinces();
$this->selectedProvince = $provinceRepository->getSelectedProvince();
$this->accounting->addStrategy(
new TaxAccountingStrategy(
$this->products,
$provinceRepository->getSelectedProvince()
)
);
}
public function initialize()
{
session_start();
if (!isset($_SESSION['cart'])) {
$this->connection->exec("INSERT INTO cart VALUES ()");
$_SESSION['cart'] = $this->connection->lastInsertId();
}
$this->handlePost();
}
https://github.com/zymsys/solid/blob/04/cart.php
public function buildViewData()
{
$viewData = [
'cartItems' => $this->sales->loadCartItems(),
'products' => $this->inventory->loadProducts(),
'provinces' => $this->accounting->loadProvinces(),
'provinceCode' => isset($_GET['province']) ?
$_GET['province'] : 'ON', //Default to GTA-PHP's home
];
public function buildViewData()
{
$cartItems = $this->sales->loadCartItems();
$viewData = [
'cartItems' => $cartItems,
'products' => $this->products,
'provinces' => $this->provinces,
'adjustments' => $this->accounting->applyAdjustments($cartItems),
'provinceCode' => $this->selectedProvince['code'],
];
Done in
initialize() now
Used Twice
New!
Start of buildViewData()
https://github.com/zymsys/solid/blob/04/cart.php
End of buildViewData()
$viewData['subtotal'] = $this->accounting->
calculateCartSubtotal($viewData['cartItems'], $viewData['products']);
$viewData['taxes'] = $this->accounting->
calculateCartTaxes($viewData['cartItems'],
$viewData['products'], $viewData['province']['taxrate']);
$viewData['total'] = $viewData['subtotal'] + $viewData['taxes'];
return $viewData;
}
$viewData['subtotal'] = $this->accounting->
calculateCartSubtotal($viewData['cartItems'], $viewData['products']);
$viewData['total'] = $viewData['subtotal'] +
$this->accounting->getAppliedAdjustmentsTotal();
return $viewData;
}
Taxes are handled by adjustments
and removed as a specific item in
the view’s data.
https://github.com/zymsys/solid/blob/04/cart.php
// loadProvinces used to live in the Accounting class
class ProvinceRepository
{
private $connection;
private $provinces = null;
private $selectedProvince;
private $selectedProvinceCode;
public function __construct(PDO $connection, $selectedProvinceCode)
{
$this->connection = $connection;
$this->selectedProvinceCode = $selectedProvinceCode;
}
public function loadProvinces() { … } // Now sets $selectedProvince
public function getProvinces()
{
return is_null($this->provinces) ? $this->loadProvinces() : $this->provinces;
}
public function getSelectedProvince()
{
return $this->selectedProvince;
}
}
https://github.com/zymsys/solid/blob/04/cart.php
Remove calculateCartTaxes and add
AccountingStrategy
class Accounting {
private $connection;
private $strategies = [];
private $appliedAdjustments = 0;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function calculateCartSubtotal($cartItems, $products) { … } // No change
public function addStrategy(AccountingStrategy $strategy)
{
$this->strategies[] = $strategy;
}
https://github.com/zymsys/solid/blob/04/cart.php
public function applyAdjustments($cartItems)
{
$adjustments = [];
foreach ($this->strategies as $strategy) {
$adjustment = $strategy->getAdjustment($cartItems);
if ($adjustment) {
$this->appliedAdjustments += $adjustment;
$adjustments[] = [
'description' => $strategy->getDescription(),
'adjustment' => $adjustment,
];
}
}
return $adjustments;
}
public function getAppliedAdjustmentsTotal()
{
return $this->appliedAdjustments;
}
}
//Class Accounting Continued…
https://github.com/zymsys/solid/blob/04/cart.php
<?php foreach ($viewData['adjustments'] as $adjustment): ?>
<tr>
<td><!-- Name --></td>
<td style="text-align: right">
<?php echo $adjustment['description']; ?>
</td>
<td>
<?php
echo number_format($adjustment['adjustment'] / 100, 2);
?>
</td>
</tr>
<?php endforeach; ?>
<?php
for ($adjustmentIndex = 0;
$adjustmentIndex < count($viewData['adjustments']);
$adjustmentIndex += 1):
$adjustment = $viewData['adjustments'][$adjustmentIndex];
?>
<input type="hidden"
name="item<?php echo $adjustmentIndex; ?>"
value="<?php echo $adjustment['description'] . '|' .
number_format($adjustment['adjustment'] / 100, 2); ?>">
<?php endfor; ?>
Now we can add
DiscountAccountingStrategy
without modifying Accounting or
the view
class DiscountAccountingStrategy extends AccountingStrategy {
private $products;
public function __construct($products)
{
parent::__construct("Discount for orders over $100");
$this->products = $products;
}
public function getAdjustment($cartItems)
{
$total = array_reduce($cartItems, function ($carry, $item) {
$product = $this->products[$item['product']];
return $carry + $item['quantity'] * $product['price'];
}, 0);
return $total > 10000 ? ($total / -10) : false;
}
}
https://github.com/zymsys/solid/blob/04/cart.php
In Application::initialize()
$this->accounting->addStrategy(
new DiscountAccountingStrategy($this->products)
);
https://github.com/zymsys/solid/blob/04/cart.php
LSP
Liskov Substitution Principal
If a class is a thing, it
should act like that thing.
must
Square is a
Rectangle?
Ways to break LSP
• Throw a new exception
• Add requirements to parameters
• requiring a more specific type
• Return an unexpected type
• returning a less specific type
• Do anything that would be unexpected if used as a
stand-in for an ancestor
DIP
Dependency Inversion Principle
Yeah smarty-pants, the D
does come before the I
Classes can implement
interfaces, and can
depend on interfaces
Our Dependencies
Dependency Inversion
We can scrap the
AccountingStrategy class and add…
interface AccountingStrategyInterface
{
public function getDescription();
public function getAdjustment($cartItems);
}
https://github.com/zymsys/solid/blob/06/cart.php
Because Application creates concrete
classes there’s little to be gained by
adding other interfaces
public function __construct()
{
$this->connection = new PDO('mysql:host=localhost;dbname=solid', 'root', '');
$this->inventory = new Inventory($this->connection);
$this->sales = new Sales($this->connection);
$this->accounting = new Accounting($this->connection);
}
Dependency Injection Containers would
solve this, but are a topic for another talk.
https://github.com/zymsys/solid/blob/06/cart.php
ISP
Interface Segregation Principal
If we follow SRP and
interfaces describe classes,
what’s left to segregate?
Code Responsibilities
Imagine an interface to our
Sales class
interface SalesInterface
{
public function addProductToCart($cartId, $productId, $quantity);
public function modifyProductQuantityInCart($cartId, $productId, $quantity);
public function loadCartItems();
}
Imagine an interface to our
Sales class
interface SalesWriterInterface
{
public function addProductToCart($cartId, $productId, $quantity);
public function modifyProductQuantityInCart($cartId, $productId, $quantity);
public function loadCartItems();
}
interface SalesReaderInterface
{
}
Last words
Resist Overengineering
Simplify don’t complicate
Pragmatic not Dogmatic
Questions?

Zero to SOLID

  • 1.
    Zero to SOLID in45 Minutes by Vic Metcalfe @v_metcalfe
  • 2.
  • 3.
    Children or thosefeint of heart are warned to leave the room…
  • 4.
    <?php session_start(); $connection = newPDO('mysql:host=localhost;dbname=solid', 'root', ''); if (!isset($_SESSION['cart'])) { $connection->exec("INSERT INTO cart () VALUES ()"); $_SESSION['cart'] = $connection->lastInsertId(); } if (isset($_POST['addproduct'])) { $sql = "INSERT INTO cartitem (cart, product, quantity) VALUES (:cart, :product, :quantity) ON DUPLICATE KEY UPDATE quantity = quantity + :quantity"; $parameters = [ 'cart' => $_SESSION['cart'], 'product' => $_POST['addproduct'], 'quantity' => $_POST['quantity'], ]; $statement = $connection->prepare($sql); $statement->execute($parameters); } if (isset($_POST['update'])) { $sql = "UPDATE cartitem SET quantity=:quantity WHERE cart=:cart and product=:product"; $parameters = [ 'cart' => $_SESSION['cart'], 'product' => $_POST['update'], 'quantity' => $_POST['quantity'], ]; $statement = $connection->prepare($sql); $statement->execute($parameters); } https://github.com/zymsys/solid/blob/00/cart.php
  • 5.
    $statement = $connection->prepare("SELECT* FROM cartitem WHERE cart=:cart AND quantity <> 0"); $statement->execute(['cart' => $_SESSION['cart']]); $cartItems = $statement->fetchAll(); ?> <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>GTA-PHP Gift Shop</title> <link rel="stylesheet" href="site.css"> </head> <body> <div class="container"> <h1>GTA-PHP Gift Shop</h1> <p>Buy our junk to keep our organizers up to date with the latest gadgets.</p> <table class="table"> <tr> <th>Product Name</th> <th>You Pay</th> <th>Group Gets</th> <th><!-- Column for add to cart button --></th> </tr> <?php $products = []; $result = $connection->query("SELECT * FROM product"); foreach ($result as $product) { $products[$product['id']] = $product; ?> <tr> <td><?php echo $product['name']; ?></td> https://github.com/zymsys/solid/blob/00/cart.php
  • 6.
    <td><?php $price = $product['price']; echonumber_format($price / 100, 2); ?></td> <td> <?php echo number_format( ($product['price'] - $product['cost']) / 100, 2 ); ?> </td> <td> <form method="post"> <input type="number" name="quantity" value="1" style="width: 3em"> <input type="hidden" name="addproduct" value="<?php echo $product['id']; ?>"> <input class="btn btn-default btn-xs" type="submit" value="Add to Cart"> </form> </td> </tr> <?php } ?> </table> <?php if (count($cartItems) > 0): ?> <?php $total = 0; $taxable = 0; $provinceCode = isset($_GET['province']) ? $_GET['province'] : 'ON'; //Default to GTA-PHP's home $provinces = []; https://github.com/zymsys/solid/blob/00/cart.php
  • 7.
    $result = $connection->query("SELECT* FROM province ORDER BY name"); foreach ($result as $row) { $provinces[$row['code']] = $row; if ($row['code'] === $provinceCode) { $province = $row; } } ?> <h2>Your Cart:</h2> <table class="table"> <?php foreach ($cartItems as $cartItem): ?> <?php $product = $products[$cartItem['product']]; ?> <tr> <td> <?php echo $product['name']; ?> </td> <td> <form method="post"> Quantity: <input type="hidden" name="update" value="<?php echo $product['id']; ?>"> <input type="number" name="quantity" style="width: 3em" value="<?php echo $cartItem['quantity']; ?>"> <button type="submit">Update</button> </form> </td> <td> <?php echo number_format( $cartItem['quantity'] * $product['price'] / 100, 2 ); $itemTotal = $cartItem['quantity'] * $product['price']; https://github.com/zymsys/solid/blob/00/cart.php
  • 8.
    $total += $itemTotal; $taxable+= $product['taxes'] ? $itemTotal : 0; ?> </td> </tr> <?php endforeach; ?> <tr> <td><!-- Name --></td> <td style="text-align: right">Subtotal:</td> <td><?php echo number_format($total / 100, 2); ?></td> </tr> <tr> <td><!-- Name --></td> <td style="text-align: right"> <?php echo $province['name']; ?> taxes at <?php echo $province['taxrate'] ?>%:</td> <td> <?php $taxes = $taxable * $province['taxrate'] / 100; $total += $taxes; echo number_format($taxes / 100, 2); ?> </td> </tr> <tr> <td><!-- Name --></td> <td style="text-align: right">Total:</td> <td><?php echo number_format($total / 100, 2); ?></td> </tr> </table> <form method="get"> Calculate taxes for purchase from: <select name="province"> https://github.com/zymsys/solid/blob/00/cart.php
  • 9.
    <?php foreach ($provincesas $province): ?> <?php $selected = $provinceCode === $province['code'] ? 'selected' : ''; ?> <option value="<?php echo $province['code']; ?>" <?php echo $selected; ?>> <?php echo $province['name']; ?> </option> <?php endforeach; ?> </select> <button type="submit" class="btn btn-default btn-xs"> Recalculate</button> </form> <form action="checkout.php" method="post"> <?php foreach ($cartItems as $itemNumber => $cartItem): ?> <?php $product = $products[$cartItem['product']]; ?> <input type="hidden" name="item<?php echo $itemNumber; ?>" value="<?php echo $product['name'] . '|' . number_format($product['price'] / 100, 2); ?>"> <?php endforeach; ?> <input type="hidden" name="item<?php echo count($cartItems); ?>" value="<?php echo 'Tax|' . number_format($taxes / 100, 2); ?>"> <button type="submit" class="btn btn-primary" style="float: right"> Checkout </button> </form> <?php endif; ?> </div> </body> </html> https://github.com/zymsys/solid/blob/00/cart.php
  • 10.
  • 11.
  • 12.
    <?php function initialize() { global $connection; session_start(); $connection= new PDO('mysql:host=localhost;dbname=solid', 'root', ''); if (!isset($_SESSION['cart'])) { $connection->exec("INSERT INTO cart () VALUES ()"); $_SESSION['cart'] = $connection->lastInsertId(); } } function handleAdd() { global $connection; if (!isset($_POST['addproduct'])) { return; } $sql = "INSERT INTO cartitem (cart, product, quantity) VALUES (:cart, :product, :quantity) ON DUPLICATE KEY UPDATE quantity = quantity + :quantity"; $parameters = [ 'cart' => $_SESSION['cart'], 'product' => $_POST['addproduct'], 'quantity' => $_POST['quantity'], ]; $statement = $connection->prepare($sql); $statement->execute($parameters); } https://github.com/zymsys/solid/blob/01/cart.php
  • 13.
    function handleUpdate() { global $connection; if(!isset($_POST['update'])) { return; } $sql = "UPDATE cartitem SET quantity=:quantity WHERE cart=:cart and product=:product"; $parameters = [ 'cart' => $_SESSION['cart'], 'product' => $_POST['update'], 'quantity' => $_POST['quantity'], ]; $statement = $connection->prepare($sql); $statement->execute($parameters); } function loadCartItems() { global $connection, $viewData; $viewData = []; $statement = $connection->prepare("SELECT * FROM cartitem WHERE cart=:cart AND quantity <> 0"); $statement->execute(['cart' => $_SESSION['cart']]); return $statement->fetchAll(); } https://github.com/zymsys/solid/blob/01/cart.php
  • 14.
    function loadProducts() { global $connection; $products= []; $result = $connection->query("SELECT * FROM product"); foreach ($result as $product) { $products[$product['id']] = $product; } return $products; } function loadProvinces() { global $connection; $provinces = []; $result = $connection->query("SELECT * FROM province ORDER BY name"); foreach ($result as $row) { $provinces[$row['code']] = $row; } return $provinces; } https://github.com/zymsys/solid/blob/01/cart.php
  • 15.
    function calculateCartSubtotal($cartItems, $products) { $subtotal= 0; foreach ($cartItems as $cartItem) { $product = $products[$cartItem['product']]; $subtotal += $cartItem['quantity'] * $product['price']; } return $subtotal; } function calculateCartTaxes($cartItems, $products, $taxrate) { $taxable = 0; foreach ($cartItems as $cartItem) { $product = $products[$cartItem['product']]; $taxable += $product['taxes'] ? $cartItem['quantity'] * $product['price'] : 0; } return $taxable * $taxrate / 100; } https://github.com/zymsys/solid/blob/01/cart.php
  • 16.
    function buildViewData() { $viewData =[ 'cartItems' => loadCartItems(), 'products' => loadProducts(), 'provinces' => loadProvinces(), 'provinceCode' => isset($_GET['province']) ? $_GET['province'] : 'ON', //Default to GTA-PHP's home ]; foreach ($viewData['provinces'] as $province) { if ($province['code'] === $viewData['provinceCode']) { $viewData['province'] = $province; } } $viewData['subtotal'] = calculateCartSubtotal($viewData['cartItems'], $viewData['products']); $viewData['taxes'] = calculateCartTaxes($viewData['cartItems'], $viewData['products'], $viewData['province']['taxrate']); $viewData['total'] = $viewData['subtotal'] + $viewData['taxes']; return $viewData; } initialize(); handleAdd(); handleUpdate(); $viewData = buildViewData(); ?> https://github.com/zymsys/solid/blob/01/cart.php
  • 17.
    <!doctype html> <html lang="en"> <head> <metacharset="UTF-8"> <title>GTA-PHP Gift Shop</title> <link rel="stylesheet" href="site.css"> </head> <body> <div class="container"> <h1>GTA-PHP Gift Shop</h1> <p>Buy our junk to keep our organizers up to date with the latest gadgets.</p> <table class="table"> <tr> <th>Product Name</th> <th>You Pay</th> <th>Group Gets</th> <th><!-- Column for add to cart button --></th> </tr> <?php foreach ($viewData['products'] as $product): ?> <tr> <td><?php echo $product['name']; ?></td> <td><?php $price = $product['price']; echo number_format($price / 100, 2); ?></td> <td><?php echo number_format( ($product['price'] - $product['cost']) / 100, 2 ); ?></td> https://github.com/zymsys/solid/blob/01/cart.php
  • 18.
    <td> <form method="post"> <input type="number"name="quantity" value="1" style="width: 3em"> <input type="hidden" name="addproduct" value="<?php echo $product['id']; ?>"> <input class="btn btn-default btn-xs" type="submit" value="Add to Cart"> </form> </td> </tr> <?php endforeach; ?> </table> <?php if (count($viewData['cartItems']) > 0): ?> <h2>Your Cart:</h2> <table class="table"> <?php foreach ($viewData['cartItems'] as $cartItem): ?> <?php $product = $viewData['products'][$cartItem['product']]; ?> <tr> <td> <?php echo $product['name']; ?> </td> <td> <form method="post"> Quantity: <input type="hidden" name="update" value="<?php echo $product['id']; ?>"> <input type="number" name="quantity" style="width: 3em" value="<?php echo $cartItem['quantity']; ?>"> <button type="submit">Update</button> </form> </td> https://github.com/zymsys/solid/blob/01/cart.php
  • 19.
    <td> <?php echo number_format( $cartItem['quantity'] *$product['price'] / 100, 2 ); ?> </td> </tr> <?php endforeach; ?> <tr> <td><!-- Name --></td> <td style="text-align: right">Subtotal:</td> <td><?php echo number_format($viewData['subtotal'] / 100, 2); ?></td> </tr> <tr> <td><!-- Name --></td> <td style="text-align: right"> <?php echo $viewData['province']['name']; ?> taxes at <?php echo $viewData['province']['taxrate'] ?>%:</td> <td> <?php echo number_format($viewData['taxes'] / 100, 2); ?> </td> </tr> <tr> <td><!-- Name --></td> <td style="text-align: right">Total:</td> <td><?php echo number_format($viewData['total'] / 100, 2); ?></td> </tr> </table> https://github.com/zymsys/solid/blob/01/cart.php
  • 20.
    <form method="get"> Calculate taxesfor purchase from: <select name="province"> <?php foreach ($viewData['provinces'] as $province): ?> <?php $selected = $viewData['provinceCode'] === $province['code'] ? 'selected' : ''; ?> <option value="<?php echo $province['code']; ?>" <?php echo $selected; ?>> <?php echo $province['name']; ?> </option> <?php endforeach; ?> </select> <button type="submit" class="btn btn-default btn-xs">Recalculate</button> </form> <form action="checkout.php" method="post"> <?php foreach ($viewData['cartItems'] as $itemNumber => $cartItem): ?> <?php $product = $viewData['products'][$cartItem['product']]; ?> <input type="hidden" name="item<?php echo $itemNumber; ?>" value="<?php echo $product['name'] . '|' . number_format($product['price'] / 100, 2); ?>"> <?php endforeach; ?> https://github.com/zymsys/solid/blob/01/cart.php
  • 21.
    <input type="hidden" name="item<?php echocount($viewData['cartItems']); ?>" value="<?php echo 'Tax|' . number_format($viewData['taxes'] / 100, 2); ?>"> <button type="submit" class="btn btn-primary" style="float: right"> Checkout</button> </form> <?php endif; ?> </div> </body> </html> https://github.com/zymsys/solid/blob/01/cart.php
  • 22.
  • 23.
  • 24.
    Objects help usto organize our code
  • 25.
    function initialize() function handleAdd() functionhandleUpdate() function loadCartItems() function loadProducts() function loadProvinces() function calculateCartSubtotal($cartItems, $products) function calculateCartTaxes($cartItems, $products, $taxrate) function buildViewData() How might we group these functions?
  • 26.
    function initialize() function handleAdd() functionhandleUpdate() function loadCartItems() function loadProducts() function loadProvinces() function calculateCartSubtotal($cartItems, $products) function calculateCartTaxes($cartItems, $products, $taxrate) function buildViewData() Invisible stuff that happens on page load Loads stuff into our HTML (view)
  • 27.
    Objects help usto encapsulate code
  • 28.
    function initialize() function handleAdd() functionhandleUpdate() function loadCartItems() function loadProducts() function loadProvinces() function calculateCartSubtotal($cartItems, $products) function calculateCartTaxes($cartItems, $products, $taxrate) function buildViewData() Invisible stuff that happens on page load Loads stuff into our HTML (view)
  • 29.
    function initialize() function handleAdd() functionhandleUpdate() function loadCartItems() function loadProducts() function loadProvinces() function calculateCartSubtotal($cartItems, $products) function calculateCartTaxes($cartItems, $products, $taxrate) function buildViewData() private private public private private public private private private
  • 30.
    class Initializer { private $connection; publicfunction __construct(PDO $connection) { $this->connection = $connection; } public function initialize() { session_start(); if (!isset($_SESSION['cart'])) { $this->connection->exec("INSERT INTO cart () VALUES ()"); $_SESSION['cart'] = $this->connection->lastInsertId(); } $this->handleAdd(); $this->handleUpdate(); } private function handleAdd() { … } private function handleUpdate() { … } } https://github.com/zymsys/solid/blob/02/cart.php
  • 31.
    class ViewData { private$connection; public function __construct(PDO $connection) { $this->connection = $connection; } private function loadCartItems() { … } private function loadProducts(){ … } private function loadProvinces(){ … } private function calculateCartSubtotal($cartItems, $products) { … } private function calculateCartTaxes($cartItems, $products, $taxrate) { … } public function buildViewData() { … } } https://github.com/zymsys/solid/blob/02/cart.php
  • 32.
    public function buildViewData() { $viewData= [ 'cartItems' => $this->loadCartItems(), 'products' => $this->loadProducts(), 'provinces' => $this->loadProvinces(), 'provinceCode' => isset($_GET['province']) ? $_GET['province'] : 'ON', //Default to GTA-PHP's home ]; foreach ($viewData['provinces'] as $province) { if ($province['code'] === $viewData['provinceCode']) { $viewData['province'] = $province; } } $viewData['subtotal'] = $this->calculateCartSubtotal($viewData['cartItems'], $viewData['products']); $viewData['taxes'] = $this->calculateCartTaxes($viewData['cartItems'], $viewData['products'], $viewData['province']['taxrate']); $viewData['total'] = $viewData['subtotal'] + $viewData['taxes']; return $viewData; } https://github.com/zymsys/solid/blob/02/cart.php
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
    function initialize() function handleAdd() functionhandleUpdate() function loadCartItems() function loadProducts() function loadProvinces() function calculateCartSubtotal($cartItems, $products) function calculateCartTaxes($cartItems, $products, $taxrate) function buildViewData() Invisible stuff that happens on page load Loads stuff into our HTML (view)
  • 38.
    function initialize() function handleAdd() functionhandleUpdate() function loadCartItems() function loadProducts() function loadProvinces() function calculateCartSubtotal($cartItems, $products) function calculateCartTaxes($cartItems, $products, $taxrate) function buildViewData() Sales Accounting Inventory Application / IT
  • 39.
    class Application { private $connection; publicfunction __construct() { $this->connection = new PDO('mysql:host=localhost;dbname=solid', 'root', ''); $this->inventory = new Inventory($this->connection); $this->sales = new Sales($this->connection); $this->accounting = new Accounting($this->connection); } public function initialize() { session_start(); if (!isset($_SESSION['cart'])) { $this->connection->exec("INSERT INTO cart VALUES ()"); $_SESSION['cart'] = $this->connection->lastInsertId(); } $this->handlePost(); } https://github.com/zymsys/solid/blob/03/cart.php
  • 40.
    public function buildViewData() { $viewData= [ 'cartItems' => $this->sales->loadCartItems(), 'products' => $this->inventory->loadProducts(), 'provinces' => $this->accounting->loadProvinces(), 'provinceCode' => isset($_GET['province']) ? $_GET['province'] : 'ON', //Default to GTA-PHP's home ]; foreach ($viewData['provinces'] as $province) { if ($province['code'] === $viewData['provinceCode']) { $viewData['province'] = $province; } } $viewData['subtotal'] = $this->accounting-> calculateCartSubtotal($viewData['cartItems'], $viewData['products']); $viewData['taxes'] = $this->accounting-> calculateCartTaxes($viewData['cartItems'], $viewData['products'], $viewData['province']['taxrate']); $viewData['total'] = $viewData['subtotal'] + $viewData['taxes']; return $viewData; } //Class Application Continued… https://github.com/zymsys/solid/blob/03/cart.php
  • 41.
    private function handlePost() { if(isset($_POST['addproduct'])) { $this->sales->addProductToCart( $_SESSION['cart'], $_POST['addproduct'], $_POST['quantity'] ); } if (isset($_POST['update'])) { $this->sales->modifyProductQuantityInCart( $_SESSION['cart'], $_POST['update'], $_POST['quantity'] ); } } } //Class Application Continued… https://github.com/zymsys/solid/blob/03/cart.php
  • 42.
    class Inventory { private$connection; public function __construct(PDO $connection) { $this->connection = $connection; } public function loadProducts() { $products = []; $result = $this->connection->query("SELECT * FROM product"); foreach ($result as $product) { $products[$product['id']] = $product; } return $products; } } https://github.com/zymsys/solid/blob/03/cart.php
  • 43.
    class Sales { private$connection; public function __construct(PDO $connection) { $this->connection = $connection; } public function addProductToCart($cartId, $productId, $quantity) { $sql = "INSERT INTO cartitem (cart, product, quantity) VALUES (:cart, :product, :quantity) ON DUPLICATE KEY UPDATE quantity = quantity + :quantity"; $parameters = [ 'cart' => $cartId, 'product' => $productId, 'quantity' => $quantity, ]; $statement = $this->connection->prepare($sql); $statement->execute($parameters); } https://github.com/zymsys/solid/blob/03/cart.php
  • 44.
    public function modifyProductQuantityInCart($cartId,$productId, $quantity) { $sql = "UPDATE cartitem SET quantity=:quantity WHERE cart=:cart and product=:product"; $parameters = [ 'cart' => $cartId, 'product' => $productId, 'quantity' => $quantity, ]; $statement = $this->connection->prepare($sql); $statement->execute($parameters); } public function loadCartItems() { $statement = $this->connection->prepare("SELECT * FROM cartitem WHERE cart=:cart AND quantity <> 0"); $statement->execute(['cart' => $_SESSION['cart']]); return $statement->fetchAll(); } } //Class Sales Continued… https://github.com/zymsys/solid/blob/03/cart.php
  • 45.
    class Accounting { private$connection; public function __construct(PDO $connection) { $this->connection = $connection; } public function loadProvinces() { $provinces = []; $result = $this->connection->query("SELECT * FROM province ORDER BY name"); foreach ($result as $row) { $provinces[$row['code']] = $row; } return $provinces; } public function calculateCartSubtotal($cartItems, $products) { $subtotal = 0; foreach ($cartItems as $cartItem) { $product = $products[$cartItem['product']]; $subtotal += $cartItem['quantity'] * $product['price']; } return $subtotal; } https://github.com/zymsys/solid/blob/03/cart.php
  • 46.
    public function calculateCartTaxes($cartItems,$products, $taxrate) { $taxable = 0; foreach ($cartItems as $cartItem) { $product = $products[$cartItem['product']]; $taxable += $product['taxes'] ? $cartItem['quantity'] * $product['price'] : 0; } return $taxable * $taxrate / 100; } } //Class Accounting Continued… https://github.com/zymsys/solid/blob/03/cart.php
  • 47.
  • 48.
  • 49.
    Open to Extension Closedto Modification
  • 50.
    New requirement: 10% offorders over $100
  • 51.
  • 52.
    First we needto understand inheritance and polymorphism
  • 53.
  • 54.
    class AccountingStrategy { private$description; public function __construct($description) { $this->description = $description; } public function getAdjustment($cartItems) { return false; } public function getDescription() { return $this->description; } } https://github.com/zymsys/solid/blob/04/cart.php
  • 55.
    class TaxAccountingStrategy extendsAccountingStrategy { private $products; private $taxRate; public function __construct($products, $province) { parent::__construct($province['name'] . ' taxes at ' . $province['taxrate'] . '%:'); $this->products = $products; $this->taxRate = $province['taxrate']; } public function getAdjustment($cartItems) { $taxable = 0; foreach ($cartItems as $cartItem) { $product = $this->products[$cartItem['product']]; $taxable += $product['taxes'] ? $cartItem['quantity'] * $product['price'] : 0; } return $taxable * $this->taxRate / 100; } } https://github.com/zymsys/solid/blob/04/cart.php
  • 56.
  • 57.
    public function initialize() { session_start(); if(!isset($_SESSION['cart'])) { $this->connection->exec("INSERT INTO cart VALUES ()"); $_SESSION['cart'] = $this->connection->lastInsertId(); } $this->handlePost(); $this->products = $this->inventory->loadProducts(); $provinceRepository = new ProvinceRepository($this->connection, isset($_GET['province']) ? $_GET['province'] : 'ON'); $this->provinces = $provinceRepository->loadProvinces(); $this->selectedProvince = $provinceRepository->getSelectedProvince(); $this->accounting->addStrategy( new TaxAccountingStrategy( $this->products, $provinceRepository->getSelectedProvince() ) ); } public function initialize() { session_start(); if (!isset($_SESSION['cart'])) { $this->connection->exec("INSERT INTO cart VALUES ()"); $_SESSION['cart'] = $this->connection->lastInsertId(); } $this->handlePost(); } https://github.com/zymsys/solid/blob/04/cart.php
  • 58.
    public function buildViewData() { $viewData= [ 'cartItems' => $this->sales->loadCartItems(), 'products' => $this->inventory->loadProducts(), 'provinces' => $this->accounting->loadProvinces(), 'provinceCode' => isset($_GET['province']) ? $_GET['province'] : 'ON', //Default to GTA-PHP's home ]; public function buildViewData() { $cartItems = $this->sales->loadCartItems(); $viewData = [ 'cartItems' => $cartItems, 'products' => $this->products, 'provinces' => $this->provinces, 'adjustments' => $this->accounting->applyAdjustments($cartItems), 'provinceCode' => $this->selectedProvince['code'], ]; Done in initialize() now Used Twice New! Start of buildViewData() https://github.com/zymsys/solid/blob/04/cart.php
  • 59.
    End of buildViewData() $viewData['subtotal']= $this->accounting-> calculateCartSubtotal($viewData['cartItems'], $viewData['products']); $viewData['taxes'] = $this->accounting-> calculateCartTaxes($viewData['cartItems'], $viewData['products'], $viewData['province']['taxrate']); $viewData['total'] = $viewData['subtotal'] + $viewData['taxes']; return $viewData; } $viewData['subtotal'] = $this->accounting-> calculateCartSubtotal($viewData['cartItems'], $viewData['products']); $viewData['total'] = $viewData['subtotal'] + $this->accounting->getAppliedAdjustmentsTotal(); return $viewData; } Taxes are handled by adjustments and removed as a specific item in the view’s data. https://github.com/zymsys/solid/blob/04/cart.php
  • 60.
    // loadProvinces usedto live in the Accounting class class ProvinceRepository { private $connection; private $provinces = null; private $selectedProvince; private $selectedProvinceCode; public function __construct(PDO $connection, $selectedProvinceCode) { $this->connection = $connection; $this->selectedProvinceCode = $selectedProvinceCode; } public function loadProvinces() { … } // Now sets $selectedProvince public function getProvinces() { return is_null($this->provinces) ? $this->loadProvinces() : $this->provinces; } public function getSelectedProvince() { return $this->selectedProvince; } } https://github.com/zymsys/solid/blob/04/cart.php
  • 61.
    Remove calculateCartTaxes andadd AccountingStrategy class Accounting { private $connection; private $strategies = []; private $appliedAdjustments = 0; public function __construct(PDO $connection) { $this->connection = $connection; } public function calculateCartSubtotal($cartItems, $products) { … } // No change public function addStrategy(AccountingStrategy $strategy) { $this->strategies[] = $strategy; } https://github.com/zymsys/solid/blob/04/cart.php
  • 62.
    public function applyAdjustments($cartItems) { $adjustments= []; foreach ($this->strategies as $strategy) { $adjustment = $strategy->getAdjustment($cartItems); if ($adjustment) { $this->appliedAdjustments += $adjustment; $adjustments[] = [ 'description' => $strategy->getDescription(), 'adjustment' => $adjustment, ]; } } return $adjustments; } public function getAppliedAdjustmentsTotal() { return $this->appliedAdjustments; } } //Class Accounting Continued… https://github.com/zymsys/solid/blob/04/cart.php
  • 63.
    <?php foreach ($viewData['adjustments']as $adjustment): ?> <tr> <td><!-- Name --></td> <td style="text-align: right"> <?php echo $adjustment['description']; ?> </td> <td> <?php echo number_format($adjustment['adjustment'] / 100, 2); ?> </td> </tr> <?php endforeach; ?>
  • 64.
    <?php for ($adjustmentIndex =0; $adjustmentIndex < count($viewData['adjustments']); $adjustmentIndex += 1): $adjustment = $viewData['adjustments'][$adjustmentIndex]; ?> <input type="hidden" name="item<?php echo $adjustmentIndex; ?>" value="<?php echo $adjustment['description'] . '|' . number_format($adjustment['adjustment'] / 100, 2); ?>"> <?php endfor; ?>
  • 65.
    Now we canadd DiscountAccountingStrategy without modifying Accounting or the view
  • 66.
    class DiscountAccountingStrategy extendsAccountingStrategy { private $products; public function __construct($products) { parent::__construct("Discount for orders over $100"); $this->products = $products; } public function getAdjustment($cartItems) { $total = array_reduce($cartItems, function ($carry, $item) { $product = $this->products[$item['product']]; return $carry + $item['quantity'] * $product['price']; }, 0); return $total > 10000 ? ($total / -10) : false; } } https://github.com/zymsys/solid/blob/04/cart.php
  • 67.
  • 68.
  • 69.
    If a classis a thing, it should act like that thing. must
  • 70.
  • 71.
    Ways to breakLSP • Throw a new exception • Add requirements to parameters • requiring a more specific type • Return an unexpected type • returning a less specific type • Do anything that would be unexpected if used as a stand-in for an ancestor
  • 72.
  • 73.
    Yeah smarty-pants, theD does come before the I
  • 74.
    Classes can implement interfaces,and can depend on interfaces
  • 75.
  • 76.
  • 77.
    We can scrapthe AccountingStrategy class and add… interface AccountingStrategyInterface { public function getDescription(); public function getAdjustment($cartItems); } https://github.com/zymsys/solid/blob/06/cart.php
  • 78.
    Because Application createsconcrete classes there’s little to be gained by adding other interfaces public function __construct() { $this->connection = new PDO('mysql:host=localhost;dbname=solid', 'root', ''); $this->inventory = new Inventory($this->connection); $this->sales = new Sales($this->connection); $this->accounting = new Accounting($this->connection); } Dependency Injection Containers would solve this, but are a topic for another talk. https://github.com/zymsys/solid/blob/06/cart.php
  • 79.
  • 80.
    If we followSRP and interfaces describe classes, what’s left to segregate?
  • 81.
  • 82.
    Imagine an interfaceto our Sales class interface SalesInterface { public function addProductToCart($cartId, $productId, $quantity); public function modifyProductQuantityInCart($cartId, $productId, $quantity); public function loadCartItems(); }
  • 83.
    Imagine an interfaceto our Sales class interface SalesWriterInterface { public function addProductToCart($cartId, $productId, $quantity); public function modifyProductQuantityInCart($cartId, $productId, $quantity); public function loadCartItems(); } interface SalesReaderInterface { }
  • 84.
    Last words Resist Overengineering Simplifydon’t complicate Pragmatic not Dogmatic Questions?

Editor's Notes

  • #30 Why would I build handcuffs into my code that let me do less with it? Talk about the contract we have with code outside our class and how private methods make refactoring easier.
  • #58 Loads more into ivars instead of directly into view Strategies are also added at initialize.
  • #72 Avoid these terms, but if they come up: Parameters should be contravariant Return types should be covariant
  • #82 Most ISP definitions go so far as to say that no class should be forced to depend on methods they do not use. Personally I think this goes too far.