PHP’s type system evolved gradually from 5.x. PHP 7.x strengthened it significantly – parameter typing, return types, nullable types, and union types on the horizon. PHP 7.4 adds typed properties. A good moment to summarise the state of typing and show best practices worth adopting today.
What we have in PHP 7.x today
<?php
declare(strict_types=1);
class OrderCalculator
{
public function calculateTotal(array $items, float $taxRate): float
{
$subtotal = array_sum(array_column($items, 'price'));
return $subtotal * (1 + $taxRate);
}
public function applyDiscount(float $price, ?float $discount): float
{
if ($discount === null) {
return $price;
}
return $price - $discount;
}
public function logOrder(int $orderId): void
{
// write to log
}
}
declare(strict_types=1) – use it always
Without strict mode PHP silently coerces types – string “5” passed to an int parameter gets converted to 5 with no error. With strict mode you get a TypeError:
<?php
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
add(2, 3); // 5 - ok
add(2.5, 3); // TypeError: Argument #1 must be of type int, float given
add("2", 3); // TypeError: Argument #1 must be of type int, string given
declare(strict_types=1) must be the first statement in the file, before namespace and use. It only affects function calls in that file.
Typing class properties today – via docblock and constructor
<?php
declare(strict_types=1);
class Product
{
/** @var int */
private $id;
/** @var string */
private $name;
/** @var float */
private $price;
/** @var string|null */
private $description;
public function __construct(
int $id,
string $name,
float $price,
?string $description = null
) {
$this->id = $id;
$this->name = $name;
$this->price = $price;
$this->description = $description;
}
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
public function getPrice(): float { return $this->price; }
public function getDescription(): ?string { return $this->description; }
}
Returning interface types instead of concrete classes
<?php
declare(strict_types=1);
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
class ProductService
{
public function __construct(
private ProductRepositoryInterface $productRepository
) {}
// Return the interface, not Model\Product
public function getById(int $id): ProductInterface
{
return $this->productRepository->getById($id);
}
}
Typing arrays – limitations and workarounds
<?php
class ProductCollection
{
/** @var ProductInterface[] */
private array $items = [];
public function add(ProductInterface $product): void
{
$this->items[] = $product;
}
/** @return ProductInterface[] */
public function all(): array
{
return $this->items;
}
public function count(): int
{
return count($this->items);
}
}
PHPStorm understands @var ProductInterface[] and suggests methods when iterating. This does not enforce types at runtime, but greatly aids static analysis with PHPStan or Psalm.
PHPStan – static analysis as a safety net
composer require --dev phpstan/phpstan # Level 5 (scale 0-8, higher = stricter) vendor/bin/phpstan analyse src --level=5
PHPStan catches type errors and calls to non-existent methods without running the code. For Magento 2 use the bitExpert/phpstan-magento package for platform-specific rules.
Summary
PHP typing is not a formality – it catches errors at write time instead of in production. declare(strict_types=1) costs one line and eliminates an entire class of silent coercions. In Magento 2, where code flows through DI, plugins and repositories – more types means easier problem diagnosis.
