Scalar Type Hints and Return Types
Scalar Type Hints
<?php
// All scalar types supported
defineAge(int $age): void
{
echo "Age: $age";
}
definePrice(float $price): void
{
echo "Price: $price";
}
defineName(string $name): void
{
echo "Name: $name";
}
defineActive(bool $active): void
{
echo "Active: " . ($active ? 'Yes' : 'No');
}
// Without strict_types (type coercion happens)
defineAge('42'); // Works! String becomes int
definePrice('29.99'); // Works! String becomes float
defineName(42); // Works! Int becomes string
// With strict_types (no coercion)
declare(strict_types=1);
declareAge('42'); // TypeError!
declarePrice(29); // TypeError!
declareName(42); // TypeError!
Return Types
<?php
// Explicit return types
function getProductPrice(int $id): float
{
return 29.99;
}
function getProductName(int $id): string
{
return 'Widget';
}
function isActive(int $id): bool
{
return true;
}
function getProductIds(): array
{
return [1, 2, 3];
}
// Nullable return type
function findProduct(int $id): ?Product
{
return $this->repository->getById($id); // Can return null
}
// Void return type (no return value)
function logMessage(string $message): void
{
error_log($message);
// No return statement needed
}
// Never return type (PHP 8.1+)
function throwError(string $message): never
{
throw new \Exception($message);
}
Strict Types Declaration
<?php
// Must be first statement
declare(strict_types=1);
function add(int $a, int $b): int
{
return $a + $b;
}
// Works
add(5, 3); // 8
add(5, 3); // 8
// TypeError (no implicit conversion)
add('5', '3'); // TypeError!
add(5.5, 3.2); // TypeError!
add(5, 3); // 8
// File-level strict mode
declare(strict_types=1);
// All functions in this file require strict types
function multiply(int $a, int $b): int {
return $a * $b;
}
Typed Properties
<?php
class Product
{
// Typed properties (PHP 7.4+)
public int $id;
public string $name;
public float $price;
public bool $active = true;
public ?string $description = null;
// Readonly properties (PHP 8.1+)
public readonly string $sku;
public readonly \DateTimeImmutable $createdAt;
public function __construct(
string $sku,
) {
$this->sku = $sku;
$this->createdAt = new \DateTimeImmutable();
}
}
$product = new Product('WDG-001');
$product->name = 'Widget'; // Works
$product->name = 123; // TypeError!
$product->sku = 'NEW-SKU'; // Error! readonly
Key Takeaway
Always use strict_types=1 to prevent type coercion bugs. Declare parameter types and return types for clarity and safety. Use ?Type for nullable returns.
Union Types and PHP 8 Features
Union Types (PHP 8.0+)
<?php
// Union type: accepts multiple types
function formatId(int|string $id): string
{
return (string)$id;
}
formatId(123); // '123'
formatId('abc'); // 'abc'
formatId(3.14); // TypeError!
// Nullable union type
function findUser(int|null $id): ?User
{
if ($id === null) {
return null;
}
return $this->userRepository->getById($id);
}
// Multiple union types
function processData(int|float|string $value): string
{
return match(true) {
is_int($value) => "Integer: $value",
is_float($value) => "Float: $value",
is_string($value) => "String: $value"
};
}
// Union types in class properties
class ApiResponse
{
public int|string $id;
public array|null $data;
public string|int $statusCode;
}
Intersection Types (PHP 8.1+)
<?php
// Intersection type: must implement ALL interfaces
function processData(Countable&Iterator $collection): void
{
foreach ($collection as $item) {
echo $item . "\n";
}
echo "Count: " . $collection->count() . "\n";
}
// Must be both Countable AND Iterator
data = new ArrayObject([1, 2, 3]);
processData($data); // Works!
// Only Countable - doesn't work
processData(new class implements Countable {
public function count(): int { return 3; }
}); // TypeError!
Named Arguments (PHP 8.0+)
<?php
function createUser(
string $email,
string $name,
string $role = 'customer',
bool $active = true,
?string $phone = null
) {
// ...
}
// Traditional: must pass all args in order
createUser('a@b.com', 'John', 'admin', true, null);
// Named: skip defaults, any order
createUser(
email: 'a@b.com',
name: 'John',
phone: '555-1234',
role: 'admin'
);
// Array unpacking
$config = [
'email' => 'a@b.com',
'name' => 'John',
'role' => 'admin'
];
createUser(...$config);
// Useful with many optional parameters
html_entity_decode(
$encoded,
flags: ENT_QUOTES | ENT_HTML5,
encoding: 'UTF-8'
);
Match Expression (PHP 8.0+)
<?php
// Match is like switch but:
// 1. Uses === comparison
// 2. Returns a value
// 3. No fallthrough
// 4. More concise
$status = 200;
$message = match($status) {
200 => 'OK',
301 => 'Moved Permanently',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown'
};
// Multiple conditions
$day = 'Monday';
$type = match($day) {
'Saturday', 'Sunday' => 'Weekend',
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday' => 'Weekday',
default => 'Unknown'
};
// With expressions
$price = 100;
$discount = match(true) {
$price >= 1000 => 0.20,
$price >= 500 => 0.15,
$price >= 100 => 0.10,
default => 0.05
};
Key Takeaway
PHP 8 union types allow flexible type declarations. Named arguments improve readability with many optional parameters. Match expression provides concise, type-safe value selection.
Constructor Promotion and Magento Patterns
Constructor Promotion (PHP 8.0+)
<?php
// Before PHP 8 - verbose
class Product
{
private string $name;
private float $price;
private string $sku;
private bool $active;
public function __construct(
string $name,
float $price,
string $sku,
bool $active = true
) {
$this->name = $name;
$this->price = $price;
$this->sku = $sku;
$this->active = $active;
}
}
// PHP 8+ - constructor promotion
class Product
{
public function __construct(
private string $name,
private float $price,
private string $sku,
private bool $active = true
) {
// Additional initialization logic here
}
}
// Mixed visibility
class ApiResponse
{
public function __construct(
public readonly int $code,
public readonly string $message,
private array $data = []
) {}
public function toArray(): array
{
return [
'code' => $this->code,
'message' => $this->message,
'data' => $this->data
];
}
}
Union Types with Constructor Promotion
<?php
class ProductSearchResult
{
public function __construct(
public readonly int|string $id,
public readonly string $name,
public readonly float|null $price,
public readonly array $attributes = []
) {}
}
// Using named arguments with promoted properties
$result = new ProductSearchResult(
id: 123,
name: 'Widget',
price: 29.99
);
$result2 = new ProductSearchResult(
id: 'custom-sku',
name: 'Custom Widget',
price: null // Price unknown
);
Magento DI with Constructor Promotion
<?php
namespace Vendor\Module\Service;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Event\ManagerInterface;
use Psr\Log\LoggerInterface;
class ProductPriceService
{
public function __construct(
private ProductRepositoryInterface $productRepository,
private ManagerInterface $eventManager,
private LoggerInterface $logger,
private float $defaultTaxRate = 0.08
) {}
public function getFinalPrice(int $productId): float
{
try {
$product = $this->productRepository->getById($productId);
$price = $product->getPrice();
$this->eventManager->dispatch('product_price_calculate', [
'price' => &$price,
'product' => $product
]);
return $price * (1 + $this->defaultTaxRate);
} catch (\Exception $e) {
$this->logger->error('Price calculation failed: ' . $e->getMessage());
return 0.0;
}
}
}
// Usage - DI resolves all dependencies
$service = $this->productPriceServiceFactory->create();
// Or with custom tax rate:
$service = new ProductPriceService(
productRepository: $productRepo,
eventManager: $eventManager,
logger: $logger,
defaultTaxRate: 0.10
);
Type Safety in Magento
<?php
// Magento uses typed properties and return types
namespace Magento\Catalog\Api\Data;
interface ProductInterface
{
public function getId(): ?int;
public function getSku(): ?string;
public function getName(): ?string;
public function getPrice(): float;
public function getStatus(): int;
public function getVisibility(): int;
public function getType(): string;
public function getCreatedAt(): ?string;
public function getUpdatedAt(): ?string;
}
// Concrete implementation with strict types
class Product implements ProductInterface
{
private int $id;
private string $sku;
private string $name;
private float $price;
private int $status;
private int $visibility;
private string $type;
public function __construct(
string $sku,
string $name,
float $price,
string $type = 'simple',
int $status = 1,
int $visibility = 4
) {
$this->sku = $sku;
$this->name = $name;
$this->price = $price;
$this->type = $type;
$this->status = $status;
$this->visibility = $visibility;
}
// Getter methods with return types
public function getId(): ?int { return $this->id ?? null; }
public function getSku(): string { return $this->sku; }
public function getName(): string { return $this->name; }
public function getPrice(): float { return $this->price; }
public function getStatus(): int { return $this->status; }
public function getVisibility(): int { return $this->visibility; }
public function getType(): string { return $this->type; }
}
Key Takeaway
Constructor promotion reduces boilerplate code. Combine with readonly for immutable properties. Named arguments make construction more readable. Magento's DI system leverages constructor type hints for automatic resolution.
Quiz
1. What is a union type in PHP 8?
2. What does constructor promotion do in PHP 8?
3. What is the difference between match and switch?
4. What does named arguments allow you to do?
5. What does readonly do to a property?
Flashcards
Question
What is a union type?
Click to reveal answer
Answer
A type that accepts multiple possible types. Example: function format(int|string $id) accepts both int and string values.
Question
What does constructor promotion do?
Click to reveal answer
Answer
Automatically creates and assigns properties from constructor parameters. public function __construct(private string $name) creates a property and assigns it.
Question
What is the difference between match and switch?
Click to reveal answer
Answer
match: uses ===, returns a value, no fallthrough, more concise. switch: uses ==, needs break, can fall through.
Question
What are named arguments?
Click to reveal answer
Answer
PHP 8 feature that lets you pass arguments by parameter name: createUser(email: 'a@b.com', "name": 'John'). Allows skipping defaults.
Question
What does readonly do?
Click to reveal answer
Answer
Makes a property settable only once (usually in constructor). Cannot be modified afterward. Useful for immutable data.
Question
What does strict_types=1 prevent?
Click to reveal answer
Answer
Implicit type coercion. Without it, '5' passed to int parameter becomes 5. With strict_types=1, it throws TypeError.
Question
What is the difference between ?Type and Type|null?
Click to reveal answer
Answer
They're equivalent in PHP 8.0+. ?int is shorthand for int|null. Both mean the value can be the type or null.
Question
What does the never return type mean?
Click to reveal answer
Answer
The function never returns normally - it always throws an exception or terminates. Example: function throwError(): never { throw new Exception(); }
Revision Notes
Key Takeaways
- 1. Always use strict_types=1 to prevent type coercion
- 2. Union types (int|string) accept multiple types
- 3. Constructor promotion reduces boilerplate code
- 4. Named arguments improve readability with many optional parameters
- 5. Match expression is more concise than switch
- 6. readonly properties are immutable after initialization
- 7. Magento uses typed properties and return types extensively
Interview Tips
- • Explain union types and when to use them
- • Describe constructor promotion and its benefits
- • Know the difference between match and switch
- • Understand named arguments and their use cases
- • Explain why strict_types=1 is important
Cheat Sheet
PHP 8 Type System Cheat Sheet
Union Types:
function format(int|string $id): string|null { /* ... */ }
Constructor Promotion:
public function __construct(private string $name, private float $price) {}
Match Expression:
$result = match($value) {
1 => 'one',
2 => 'two',
default => 'other'
};
Named Arguments:
createUser(email: 'a@b.com', "name": 'John', "active": false);
Readonly Properties:
public readonly string $sku; // Set once in constructor
Strict Types:
declare(strict_types=1); // First line in file