$30 off During Our Annual Pro Sale. View Details »

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.

Anna Filina
PRO

February 26, 2020
Tweet

More Decks by Anna Filina

Other Decks in Programming

Transcript


  1. PHP Best Practices
    MONTREAL | FEB 26, 2020 @afilina

    View Slide

  2. Anna Filina
    ‣ Coding since 1997.
    ‣ PHP since 2003.
    ‣ Legacy archaeologist.
    ‣ Test automation.
    ‣ Talks and workshops.
    ‣ YouTube videos.

    View Slide

  3. View Slide

  4. What Is This About?
    ‣ Not talking about nice-to-haves.
    ‣ Not talking about style.
    ‣ Real bugs as justification for each point.

    View Slide

  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]);

    View Slide

  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

    View Slide

  7. private function getPaths()
    {
    return '[
    {"list_products":"/products"},
    {"view_cart":"/cart"},
    ]';
    }

    View Slide

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

    View Slide

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

    View Slide

  10. private $path1 = ['list_products' => '/products'];
    private $path2 = ['view_cart' => '/cart'];

    View Slide

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

    View Slide

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

    View Slide

  13. /**
    * @return array
    */
    private function getPaths(): array

    View Slide

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

    View Slide

  15. /**
    * @return array
    */
    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' ...

    View Slide

  16. if ($path === '') {
    throw new InvalidArgumentException('Is blank');
    }

    View Slide

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

    View Slide

  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;
    }
    }

    View Slide

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

    View Slide

  20. /**
    * @return array
    */
    private function getPaths(): array
    {
    return [
    new Path('/products'),
    new Path('/cart'),
    ];
    }

    View Slide

  21. public function setPrice(int $price)
    {
    $this->price = $price;
    }
    $this->setPrice(1.15);

    View Slide

  22. declare(strict_types=1);
    TypeError : Argument 1 passed to MyClass::setPrice() must be of the
    type int, float given

    View Slide

  23. Strict types.

    Strict types.
    Strict types.

    View Slide

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

    View Slide

  25. NPEs galore.

    View Slide

  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

    View Slide

  27. class Product
    {
    public string $name;
    }

    View Slide

  28. class ProductEntity
    {
    public $name;

    public $price;
    }
    final class Product
    {
    public string $name;
    public int $price;
    //...
    }

    View Slide

  29. return new Product(
    $product->name,
    $product->price
    );

    View Slide

  30. if ($product->getLastPrice() !== null) {
    return number_format($product->getLastPrice());
    }
    TypeError : number_format() expects parameter 1 to be
    float, null given

    View Slide

  31. public function getLastPrice()
    {
    return array_pop($this->prices);
    }

    View Slide

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

    View Slide

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

    View Slide

  34. interface ApiAware
    {
    public function setApi(Api $api);
    }
    if ($class instanceof ApiAware) {
    $class->setApi($api);
    }

    View Slide

  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);
    }
    }

    View Slide

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

    View Slide

  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);
    }
    }

    View Slide

  38. Dependency injection 

    is your friend.

    View Slide

  39. if (!empty($array)) {
    return $array[0];
    }
    Trying to access array offset on value of type bool

    View Slide

  40. if (empty($airplaneSeat)) {
    $this->book($airplaneSeat);
    }

    View Slide

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

    View Slide

  42. !== ""
    !== 0
    !== 0.0
    !== null
    === true
    count(array())

    View Slide

  43. 0.99 + 0.01 === 1

    View Slide

  44. IEEE 754

    floating point arithmetic.

    View Slide

  45. $amountInCents + 1

    View Slide

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

    View Slide

  47. How to test?

    View Slide

  48. $soapApi = new SoapApi($wsdl, $soapKey, $config);
    public function __construct(SoapApi $soap)
    {
    $this->soap = $soap;
    }

    View Slide

  49. class Order
    {
    public function getProducts()
    {
    return Product::find($this->productIds);
    }
    }
    Product::shouldReceive('find')
    ->once()
    ->andReturn([]);

    View Slide

  50. class MyController extends AbstractController
    {
    public function myAction()
    {
    $doctrine = $this->container->get('doctrine');
    }
    }

    View Slide

  51. Dependency injection 

    is your friend.

    View Slide


  52. THANKS!
    @afilina

    View Slide