PHP Best Practices

PHP Best Practices

Am I making a mistake? Is this the right decision in the long term? Join me as I show PHP code of over 10 years. I'll explain what worked well, what didn't and why. You can then make informed decisions for the robustness and maintainability of your application. On the menu: SOLID principles, testability, dependency injection, exception handling, use of null and false, ORMs, etc.

B3b2139e4f2c0eca4efe2379fcebc1c5?s=128

Anna Filina

February 26, 2020
Tweet

Transcript

  1. <animés par la passion> PHP Best Practices MONTREAL | FEB

    26, 2020 @afilina
  2. Anna Filina ‣ Coding since 1997. ‣ PHP since 2003.

    ‣ Legacy archaeologist. ‣ Test automation. ‣ Talks and workshops. ‣ YouTube videos.
  3. None
  4. What Is This About? ‣ Not talking about nice-to-haves. ‣

    Not talking about style. ‣ Real bugs as justification for each point.
  5. $sql = 'SELECT * FROM users WHERE email="'.$email.'"'; $this->pdo->query($sql); //

    abc" OR 1=1 OR email="abc // SELECT * FROM users WHERE email="abc" OR 1=1 OR email="abc"; $sql = 'SELECT * FROM users WHERE email = :email'; $statement = $this->pdo->prepare($sql); $statement->execute([':email' => $email]);
  6. $paths = $this->getPaths(); $urls = array_map(function ($path) { return self::URL_ROOT

    . $path; }, $paths); array_map(): Expected parameter 2 to be an array, string given
  7. private function getPaths() { return '[ {"list_products":"/products"}, {"view_cart":"/cart"}, ]'; }

  8. private function getPaths(): array { return [ $this->path1, $this->path2, ];

    }
  9. $paths = $this->getPaths(); $urls = array_map(function ($path) { return self::URL_ROOT

    . $path; }, $paths); Array to string conversion
  10. private $path1 = ['list_products' => '/products']; private $path2 = ['view_cart'

    => '/cart'];
  11. private $path1 = '/products'; private $path2 = '/cart';

  12. $urls = array_map(function (string $path) { return self::URL_ROOT . $path;

    }, $paths);
  13. /** * @return array<int, string> */ private function getPaths(): array

  14. composer require --dev vimeo/psalm vendor/bin/psalm --init Detected level 7 as

    a suitable initial default vendor/bin/psalm src
  15. /** * @return array<int, string> */ private function getPaths(): array

    ERROR: MixedReturnTypeCoercion - src/TypeMismatch.php:65:16 - The type 'array{0: mixed, 1: mixed}' is more general than the declared return type 'array<int, string>' ...
  16. if ($path === '') { throw new InvalidArgumentException('Is blank'); }

  17. function (Path $path) { return self::URL_ROOT . $path; }, $paths);

  18. final class Path { private string $path; public function __construct(string

    $path) { Assert::that($path) ->notBlank(); $this->path = $path; } public function __toString(): string { return $this->path; } }
  19. array_map(function (Path $path) { return self::URL_ROOT . $path; }, $paths);

  20. /** * @return array<int, Path> */ private function getPaths(): array

    { return [ new Path('/products'), new Path('/cart'), ]; }
  21. public function setPrice(int $price) { $this->price = $price; } $this->setPrice(1.15);

  22. <?php declare(strict_types=1); TypeError : Argument 1 passed to MyClass::setPrice() must

    be of the type int, float given
  23. Strict types.
 Strict types. Strict types.

  24. private string $stringPath; private Path $voPath;

  25. NPEs galore.

  26. class Product { public $name; } //... $this->findByName($this->product->name); 
 TypeError

    : Argument 1 passed to MyClass::findByName() must be of the type string, null given
  27. class Product { public string $name; }

  28. class ProductEntity { public $name;
 public $price; } final class

    Product { public string $name; public int $price; //... }
  29. return new Product( $product->name, $product->price );

  30. if ($product->getLastPrice() !== null) { return number_format($product->getLastPrice()); } TypeError :

    number_format() expects parameter 1 to be float, null given
  31. public function getLastPrice() { return array_pop($this->prices); }

  32. $lastPrice = $product->getLastPrice(); if ($lastPrice !== null) { return number_format($lastPrice);

    }
  33. @$array[$foo->a()]; public function a() { trigger_error('my error', E_USER_ERROR); } $array[$foo->a()]

    ?? 'something else';
  34. interface ApiAware { public function setApi(Api $api); } if ($class

    instanceof ApiAware) { $class->setApi($api); }
  35. 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); } }
  36. Error : Call to a member function sendRequest() on null

  37. final class MyClass { private Api $api; public function __construct(Api

    $api) { $this->api = $api; } public function sendApiRequest() { $product = new Product(); $this->api->sendRequest($product); } }
  38. Dependency injection 
 is your friend.

  39. if (!empty($array)) { return $array[0]; } Trying to access array

    offset on value of type bool
  40. if (empty($airplaneSeat)) { $this->book($airplaneSeat); }

  41. empty(""); empty(0); empty(0.0); empty("0"); empty(null); empty(false); empty(array());

  42. !== "" !== 0 !== 0.0 !== null === true

    count(array())
  43. 0.99 + 0.01 === 1

  44. IEEE 754
 floating point arithmetic.

  45. $amountInCents + 1

  46. /** @var PaymentGatewayInterface */ $gateway = $this->getSelectedGateway(); $gateway->preauthorizePayment();

  47. How to test?

  48. $soapApi = new SoapApi($wsdl, $soapKey, $config); public function __construct(SoapApi $soap)

    { $this->soap = $soap; }
  49. class Order { public function getProducts() { return Product::find($this->productIds); }

    } Product::shouldReceive('find') ->once() ->andReturn([]);
  50. class MyController extends AbstractController { public function myAction() { $doctrine

    = $this->container->get('doctrine'); } }
  51. Dependency injection 
 is your friend.

  52. <animés par la passion> THANKS! @afilina