Pattern misuse and unit test gotchas
| <?php | |
| // --- Bad inheritance | |
| abstract class Media | |
| { | |
| abstract protected function tableSource() : string; | |
| public function getByID(int $id) : array | |
| { | |
| $tableSource = $this->tableSource(); | |
| return $this->db->query("select * from $tableSource where id = ?", $id); | |
| } | |
| public function download(int $id) : string | |
| { | |
| $provider = $this instanceof Music ? new S3Provider() : new ExternalProvider(); | |
| return $provider->getDownloadURL($id); | |
| } | |
| } | |
| class Music extends Media | |
| { | |
| protected function tableSource() : string | |
| { | |
| return 'music'; | |
| } | |
| } | |
| class Video extends Media | |
| { | |
| protected function tableSource() : string | |
| { | |
| return 'videos'; | |
| } | |
| } | |
| // --- composition | |
| interface Media | |
| { | |
| public function getByID(int $id) : array; | |
| public function download(int $id) : string; | |
| } | |
| trait MediaDownloadTrait | |
| { | |
| private $provider; | |
| public function download(int $id) : string | |
| { | |
| return $this->provider->getDownloadURL($id); | |
| } | |
| } | |
| class Music implements Media | |
| { | |
| use MediaDownloadTrait; | |
| public function __construct(Provider $provider = null) | |
| { | |
| $this->provider = $provider ?? new S3Provider(); | |
| } | |
| public function getByID(int $id) : array | |
| { | |
| return $this->db->query("select * from music where id = ?", $id); | |
| } | |
| } | |
| class Video implements Media | |
| { | |
| use MediaDownloadTrait; | |
| public function __construct(Provider $provider = null) | |
| { | |
| $this->provider = $provider ?? new ExternalProvider(); | |
| } | |
| public function getByID(int $id) : array | |
| { | |
| return $this->db->query("select * from videos where id = ?", $id); | |
| } | |
| } |
| <?php | |
| class Item | |
| { | |
| public function __construct($name, $upc, $price) | |
| { | |
| $this->name = $name; | |
| $this->upc = $upc; | |
| $this->price = $price; | |
| } | |
| } | |
| class ItemCollection implements \Iterator | |
| { | |
| private $items = []; | |
| private $position = 0; | |
| public function __construct(Item ...$items) | |
| { | |
| $this->items = $items; | |
| } | |
| public function addItem(Item $item) | |
| { | |
| $this->items[] = $item; | |
| } | |
| public function rewind() { | |
| $this->position = 0; | |
| } | |
| public function current() { | |
| return $this->items[$this->position]; | |
| } | |
| public function key() { | |
| return $this->position; | |
| } | |
| public function next() { | |
| ++$this->position; | |
| } | |
| public function valid() { | |
| return isset($this->items[$this->position]); | |
| } | |
| } | |
| function display(ItemCollection $items) | |
| { | |
| foreach ($items as $item) { | |
| echo $item->name; | |
| } | |
| } | |
| $item = new Item('A', '80090192308', 9.99); | |
| $item2 = new Item('B', '80912980741', 10.99); | |
| $itemCollection = new ItemCollection($item, $item2); | |
| display($items); | |
| function displayEasier(Item ...$items) | |
| { | |
| foreach ($items as $item) { | |
| echo $item->name; | |
| } | |
| } | |
| displayEasier($item, $item2); |
| <?php | |
| class Email | |
| { | |
| public $email; | |
| public function __construct(string $email) | |
| { | |
| $this->email = $email; | |
| } | |
| public function isValid() : boolean | |
| { | |
| return SomeRFCRequirement::satisfies($this->email); | |
| } | |
| } | |
| class Emailer | |
| { | |
| public function sendEmail(Email $email, string $body) | |
| { | |
| if (!$email->isValid()) { | |
| throw \RuntimeException('Invalid email'); | |
| } | |
| } | |
| } | |
| class EmailRepo | |
| { | |
| public function saveEmail(Email $email) : int | |
| { | |
| return $this->db->execute('insert into emails (email) values (?)', $email->email); | |
| } | |
| public function getEmail($id) : Email | |
| { | |
| return new Email($this->db->query('select email from emails where id = ?', $id)); | |
| } | |
| } | |
| $email = new Email('somebademail'); | |
| $emailRepo = new EmailRepo(); | |
| $id = $emailRepo->saveEmail($email); | |
| // Sometime later (possibly after user has left the site) | |
| $email = $emailRepo->getEmail($id); | |
| $emailer = new Emailer(); | |
| $emailer->sendEmail($email, 'Some cool email text'); | |
| // OH NO invalid email what do we do now? |
| <?php | |
| // Observer pattern overuse | |
| class ItemObserver implements SplObserver | |
| { | |
| public function update(SplSubject $subject) { | |
| foreach ($subject->items() as $item) { | |
| if ($item['quantity'] < 0) { | |
| // Now we have to know this observer's behavior in the subject in order to handle this? | |
| throw new \RuntimeException('Expected positive quantity.'); | |
| } | |
| } | |
| } | |
| } | |
| class Cart implements SplSubject | |
| { | |
| private $items = []; | |
| public function __construct() | |
| { | |
| $this->bus = []; | |
| } | |
| public function attach(SplObserver $observer) : void | |
| { | |
| $this->bus[] = $observer; | |
| } | |
| public function detach(SplObserver $observer) : void | |
| { | |
| } | |
| public function addItem(array $item) : void | |
| { | |
| $this->items[] = $item; | |
| $this->notify(); | |
| } | |
| public function notify() : void | |
| { | |
| foreach ($this->bus as $observer) { | |
| $observer->update($this); | |
| } | |
| } | |
| public function items() : array | |
| { | |
| return $items; | |
| } | |
| } | |
| $cart = new Cart(); | |
| $cart->addItem(['quantity' => 1, 'name' => 'some product']); |
| <?php | |
| // Provider misuse | |
| interface SearchProvider | |
| { | |
| public function search(string $term) : array; | |
| } | |
| class Provider | |
| { | |
| private $providerList = [ | |
| 'products' => [ | |
| 'active' => 'ProductsElasticSearchProvider', | |
| 'fallback' => 'ProductsMySQLProvider', | |
| ], | |
| 'discounts' => [ | |
| 'active' => 'DiscountMySQLProvider', | |
| 'fallback' => 'DiscountMySQLProvider', | |
| ], | |
| 'coupons' => [ | |
| 'active' => 'CouponMySQLProvider', | |
| 'fallback' => 'CouponMySQLProvider', | |
| ], | |
| 'giftcard' => [ | |
| 'active' => 'GiftcardMySQLProvider', | |
| 'fallback' => 'GiftcardMySQLProvider', | |
| ], | |
| ]; | |
| public static function getProvider(string $type) | |
| { | |
| $classPath = static::$providerList[$type]['active']; | |
| return new $classPath(); | |
| } | |
| } | |
| class ProductMySQLProvider implements SearchProvider | |
| { | |
| public function search(string $term) : array | |
| { | |
| return $this->db->query('select * from products where name = ?', $term); | |
| } | |
| } | |
| class ProductController | |
| { | |
| public function __construct(Request $request, Response $response) | |
| { | |
| $this->request = $request; | |
| $this->response = $response; | |
| } | |
| public function search() | |
| { | |
| $this->response->body($this->getProvider('product')->search($this->request->param('term'))); | |
| } | |
| } |
| <?php | |
| use PHPUnit\Framework\TestCase; | |
| class ExpensiveOperation | |
| { | |
| public function doIt() | |
| { | |
| // This is just a simulation of something that takes a while | |
| sleep(1); | |
| return $this->getNow(); | |
| } | |
| protected function getNow() | |
| { | |
| return date('c'); | |
| } | |
| } | |
| class ExpensiveOperationTest extends TestCase | |
| { | |
| // Brittle tests with dates | |
| public function testDoIt() | |
| { | |
| $now = date('c'); | |
| $op = new ExpensiveOperation(); | |
| $result = $op->doIt(); | |
| $this->assertEquals($now, $result); | |
| // This will fail periodically depending on the timing of the test | |
| } | |
| public function testDoItNiavelyBetter() | |
| { | |
| $now = date('c'); | |
| $mock = $this->getMock(ExpsensiveOperation::class, ['getNow']); | |
| $mock->expects($this->any())->method('getNow') | |
| ->will($this->returnValue($now)); | |
| $result = $mock->doIt(); | |
| $this->assertEquals($now, $result); | |
| // Passes, but we have a mock :( | |
| } | |
| public function testDoItBest() | |
| { | |
| $now = new \DateTimeImmutable(); | |
| $op = new ExpensiveOperationBetter($now); | |
| $result = $op->doIt(); | |
| $this->assertEquals($now->format('c'), $result); | |
| // Passes, no mocks! | |
| } | |
| } | |
| class ExpensiveOperationBetter extends ExpensiveOperation | |
| { | |
| private $now; | |
| public function __construct(\DateTimeImmutable $now = null) | |
| { | |
| if ($now === null) { | |
| $now = new \DateTimeImmutable(); | |
| } | |
| $this->now = $now; | |
| } | |
| protected function getNow() | |
| { | |
| return $this->now->format('c'); | |
| } | |
| } |
| <?php | |
| use PHPUnit\Framework\TestCase; | |
| use PHPUnit\DbUnit\TestCaseTrait; | |
| class DataModel | |
| { | |
| public function getEntries() : array | |
| { | |
| return $this->db->query("select * from some_table"); | |
| } | |
| public function getFilteredEntries(string $name) : array | |
| { | |
| return $this->db->query("select * from some_table where name = ?", $name); | |
| } | |
| } | |
| class DataModelFixture | |
| { | |
| public $fixture = [ | |
| 'some_table' => [ | |
| ['id' => 1, 'name' => 'a'], | |
| ['id' => 2, 'name' => 'b'], | |
| ['id' => 3, 'name' => 'b'], | |
| ] | |
| ]; | |
| } | |
| class DataModelTest extends TestCase | |
| { | |
| use TestCaseTrait; | |
| public function setUp() | |
| { | |
| $this->fixture = DataModelFixture::$fixture; | |
| } | |
| public function testGetEntries() | |
| { | |
| $model = new DataModel(); | |
| $results = $model->getEntries(); | |
| $this->assertCount(3, $results); | |
| } | |
| public function testGetFilteredEntries() | |
| { | |
| $model = new DataModel(); | |
| $results = $model->getFilteredEntries('b'); | |
| $this->assertCount(2, $results); | |
| } | |
| } |
| <?php | |
| use PHPUnit\Framework\TestCase; | |
| function numberGenerator($length) : \Generator | |
| { | |
| foreach (range(1, $length) as $number) { | |
| yield $number; | |
| } | |
| } | |
| class NumberGeneratorTest extends TestCase | |
| { | |
| public function testNumberGeneratorOops() | |
| { | |
| $results = numberGenerator(3); | |
| $this->assertEquals([1, 2, 3], $results); | |
| // this will fail | |
| } | |
| public function testNumberGeneratorFix() | |
| { | |
| $results = iterator_to_array(numberGenerator(3)); | |
| // or | |
| foreach (numberGenerator(3) as $number) { | |
| $results[] = $number; | |
| } | |
| $this->assertEquals([1, 2, 3], $results); | |
| // Passes | |
| } | |
| } |