Slide 1

Slide 1 text

Fantastic Bugs and How to Avoid Them LONGHORN PHP | OCT 2021 @afilina

Slide 2

Slide 2 text

Zend Framework 1 has undefined variables.

Slide 3

Slide 3 text

Anna Filina • Coding since 1997 (VB4) • PHP since 2003 • Legacy archaeology • Test automation • Public speaking • Mentorship • YouTube videos

Slide 4

Slide 4 text

• Real-world "silly" bugs. • How they came to be. • How you can avoid making mistakes. • Also, I need to rant about bad code. What is this talk about?

Slide 5

Slide 5 text

Bugs as justification for following best practices.

Slide 6

Slide 6 text

$paths = $this->getPaths(); $urls = array_map(function ($path) { return self::BASE_URI . $path; }, $paths); array_map(): Expected parameter 2 to be an array, string given

Slide 7

Slide 7 text

private function getPaths() { return '[ {"list_products":"/products"}, {"view_cart":"/cart"}, ]'; } You have to read every method to know what will happen.

Slide 8

Slide 8 text

private function getPaths(): array { return [ $this->path1, $this->path2, ]; } We get earlier warnings, and IDEs can highlight problems in your code.

Slide 9

Slide 9 text

$paths = $this->getPaths(); $urls = array_map(function ($path) { return self::BASE_URI . $path; }, $paths); Array to string conversion

Slide 10

Slide 10 text

private $path1 = ['list_products' => '/products']; private $path2 = ['view_cart' => '/cart']; We assumed that the paths were strings.

Slide 11

Slide 11 text

private $path1 = '/products'; private $path2 = '/cart'; We change paths to strings.

Slide 12

Slide 12 text

$urls = array_map(function (string $path) { return self::BASE_URI . $path; }, $paths); Useful if closure is very long, but the warning is still not early enough.

Slide 13

Slide 13 text

/** * @return array */ private function getPaths(): array Type hints to describe array in more detail, then use a static analysis tool.

Slide 14

Slide 14 text

composer require --dev vimeo/psalm vendor/bin/psalm --init Detected level 7 as a suitable initial default vendor/bin/psalm src

Slide 15

Slide 15 text

/** * @return array */ private function getPaths(): array { return [ $this->path1, $this->path2, ]; } ERROR: MixedReturnTypeCoercion - src/TypeMismatch.php:65:16 - The type 'array{0: mixed, 1: mixed}' is more general than the declared return type 'array' ...

Slide 16

Slide 16 text

private $path1 = '/products'; private $path2 = '/cart'; It's possible for these values to be set to something other than strings.

Slide 17

Slide 17 text

/** * @var string */ private string $path1 = '/products'; In older PHP versions, we use annotations. In PHP 7.4+, we can specify the type.

Slide 18

Slide 18 text

/** * @return array */ private function getPaths(): array { return [ $this->path1, $this->path2, ]; } Annotations potentially lie, unless you can prove them with static analysis.

Slide 19

Slide 19 text

if ($path === '') { throw new InvalidArgumentException('Is blank'); } The problem with this is you have to remember to do it everywhere.

Slide 20

Slide 20 text

$urls = array_map(function (string $path) { return self::BASE_URI . $path; }, $paths); It would be nice if instead of expecting a simple string, we could say that we expect something that is not blank.

Slide 21

Slide 21 text

$urls = array_map(function (Path $path) { return self::BASE_URI . $path; }, $paths); We can declare a custom type and just assume that it's valid.

Slide 22

Slide 22 text

final class Path { public function __construct( private string $path ) { Assert::that($path) ->notBlank(); } public function getValue(): string { return $this->path; } } Class is final; value comes through constructor and gets validated; class has no setters.

Slide 23

Slide 23 text

composer require beberlei/assert Look for assertion libraries to help you with the validation.

Slide 24

Slide 24 text

/** * @return array */ private function getPaths(): array { return [ new Path('/products'), new Path('/cart'), ]; } We now swap our strings for Path objects and update the type hint.

Slide 25

Slide 25 text

array_map(function (Path $path) { return self::BASE_URI . $path->getValue(); }, $paths); When we see a Path, we can assume that it's ever blank.

Slide 26

Slide 26 text

Float coercion.

Slide 27

Slide 27 text

public function setPrice(int $price) { $this->price = $price; } $this->setPrice(1.15); 1.15 float will be coerced to int, rounding it down to 1 without any warnings.

Slide 28

Slide 28 text

Slide 29

Slide 29 text

Strict types. Strict types. Strict types.

Slide 30

Slide 30 text

private string $stringPath; private Path $voPath; Since PHP 7.4, you can have property types, which will also prevent you from accessing a value that wasn't yet assigned.

Slide 31

Slide 31 text

NPEs everywhere.

Slide 32

Slide 32 text

class Product { public $name; } //... $this->findByName($this->product->name); TypeError : Argument 1 passed to MyClass::findByName() must be of the type string, null given

Slide 33

Slide 33 text

class Product { public string $name; } The solution is to simply declare the type.

Slide 34

Slide 34 text

class ProductEntity { public $name; public $price; } final class Product { private string $name; private int $price; //... } ORM models/entities can be converted to stricter objects before you let your app interact with them.

Slide 35

Slide 35 text

return new ProductName( $product->name ); We can even have smaller objects to represent a subset of fields, or even just one field.

Slide 36

Slide 36 text

if ($product->getLastPrice() !== null) { return number_format($product->getLastPrice()); } TypeError : number_format() expects parameter 1 to be float, null given We just checked that it was not null, yet it still crashes due to a null.

Slide 37

Slide 37 text

public function getLastPrice() { return array_pop($this->prices); } There is no guarantee that a method will return the same value each time.

Slide 38

Slide 38 text

$lastPrice = $product->getLastPrice(); if ($lastPrice !== null) { return number_format($lastPrice); } Don't assume anything and assign the value to a local variable first.

Slide 39

Slide 39 text

@$array[$foo->a()]; public function a() { trigger_error('my error', E_USER_ERROR); } $array[$foo->a()] ?? 'something else'; In an attempt to suppress missing index notices, we can suppress bigger problems. Avoid @.

Slide 40

Slide 40 text

interface ApiAware { public function setApi(Api $api); } if ($class instanceof ApiAware) { $class->setApi($api); } *Aware interfaces are a common way to inject services.

Slide 41

Slide 41 text

final class MyClass implements ApiAware { private $api; public function setApi(Api $api): void { $this->api = $api; } public function sendApiRequest() { $product = new Product(); $this->api->sendRequest($product); } } If setApi was not called before sendRequest for any reason, this will crash.

Slide 42

Slide 42 text

Error : Call to a member function sendRequest() on null

Slide 43

Slide 43 text

final class MyClass { public function __construct( private Api $api ) {} public function sendApiRequest() { $product = new Product(); $this->api->sendRequest($product); } } Ditch those interfaces and use proper dependency injection.

Slide 44

Slide 44 text

Dependency injection is your friend.

Slide 45

Slide 45 text

if (!empty($array)) { return $array[0]; } Trying to access array offset on value of type bool empty doesn't just operate on arrays.

Slide 46

Slide 46 text

empty(""); empty(0); empty(0.0); empty("0"); empty(null); empty(false); empty(array()); All these things are considered empty.

Slide 47

Slide 47 text

!== "" !== 0 !== 0.0 !== null === true !== [] Check specifically for what you need instead. Otherwise, types can get coerced and break the logic.

Slide 48

Slide 48 text

0.99 + 0.01 === 1 This is false.

Slide 49

Slide 49 text

IEEE 754 floating point arithmetic. In simple terms, if you use floats, numbers may not add up.

Slide 50

Slide 50 text

$amountInCents + 1 Operate on integers when possible. Divide at the presentation layer.

Slide 51

Slide 51 text

Interfaces vs concrete classes.

Slide 52

Slide 52 text

/** @var PaymentGatewayInterface */ $gateway = $this->getSelectedGateway(); $gateway->preauthorizePayment(); Even though the gateway is the right type, the method might not exist.

Slide 53

Slide 53 text

PaymentGatewayInterface StripeGateway capturePayment PaypalGateway capturePayment preauthorizePayment capturePayment This happens when you implements additional methods, so it but only in some classes. Try to stick to the interface methods in your implementations.

Slide 54

Slide 54 text

@afilina Questions?