Slides from my talk at the GTA-PHP Meetup Group about getting mixed HTML / PHP code into objects using SOLID principles.
Meetup page: http://www.meetup.com/GTA-PHP-User-Group-Toronto/events/230656470/
Code is on github: https://github.com/zymsys/solid
25. 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?
26. 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)
28. 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)
29. 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
30. 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
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
37. 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)
38. 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
39. 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
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 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
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 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
61. 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
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
71. 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
77. 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
78. 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
82. Imagine an interface to our
Sales class
interface SalesInterface
{
public function addProductToCart($cartId, $productId, $quantity);
public function modifyProductQuantityInCart($cartId, $productId, $quantity);
public function loadCartItems();
}
83. 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
{
}
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.
Loads more into ivars instead of directly into view
Strategies are also added at initialize.
Avoid these terms, but if they come up:
Parameters should be contravariant
Return types should be covariant
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.