Skip to content
intermediate Phase 6 · PHP Advanced

PHP Type Declarations: Scalar Types, Union Types, and Constructor Promotion

Master scalar type hints, return types, union types (PHP 8.0), named arguments, and constructor promotion.

45m
0 problems
Topic Progress 0%

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?

Question 1 options

2. What does constructor promotion do in PHP 8?

Question 2 options

3. What is the difference between match and switch?

Question 3 options

4. What does named arguments allow you to do?

Question 4 options

5. What does readonly do to a property?

Question 5 options

Flashcards

Question

What is a union type?

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?

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?

Answer

match: uses ===, returns a value, no fallthrough, more concise. switch: uses ==, needs break, can fall through.

Question

What are named arguments?

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?

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?

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?

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?

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