objects namespaces and application architecture

Final Classes and Methods

final tells PHP that a class or method must not be extended or overridden.

This is a design decision. Use final when inheritance is not part of the contract and changing behaviour in a child class would make the code harder to reason about. Do not use final because it feels stricter by default; use it to protect clear boundaries.

Final Classes Cannot Be Extended

A final class can be used, but another class cannot inherit from it.

PHP example
<?php

declare(strict_types=1);

final class OrderReference
{
    public function __construct(private int $id)
    {
        if ($id < 1) {
            throw new InvalidArgumentException('Order ID must be positive.');
        }
    }

    public function value(): string
    {
        return 'ORD-' . str_pad((string) $this->id, 6, '0', STR_PAD_LEFT);
    }
}

$reference = new OrderReference(42);

echo $reference->value() . PHP_EOL;

// Prints:
// ORD-000042

This class is a small value-style object. There is no useful reason for another class to extend it.

Final Methods Cannot Be Overridden

A class can allow inheritance while locking a specific method.

PHP example
<?php

declare(strict_types=1);

class BaseReport
{
    final public function fileExtension(): string
    {
        return 'pdf';
    }

    public function title(): string
    {
        return 'Report';
    }
}

class SalesReport extends BaseReport
{
    public function title(): string
    {
        return 'Sales Report';
    }
}

$report = new SalesReport();

echo $report->title() . '.' . $report->fileExtension() . PHP_EOL;

// Prints:
// Sales Report.pdf

The child can customise the title, but it cannot change the file extension rule.

Final Helps Preserve Invariants

If a class protects important rules, allowing inheritance may let a child class break those rules.

PHP example
<?php

declare(strict_types=1);

final class EmailAddress
{
    public function __construct(private string $value)
    {
        if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException('Email address is invalid.');
        }
    }

    public function value(): string
    {
        return strtolower($this->value);
    }
}

$email = new EmailAddress('NIA@example.com');

echo $email->value() . PHP_EOL;

// Prints:
// nia@example.com

Making this class final keeps the validation and normalisation behaviour predictable.

Final Is Not A Replacement For Interfaces

If other code needs to depend on a replaceable behaviour, depend on an interface rather than extending a base class.

PHP example
<?php

declare(strict_types=1);

interface ReferenceFormatter
{
    public function format(int $id): string;
}

final class OrderReferenceFormatter implements ReferenceFormatter
{
    public function format(int $id): string
    {
        return 'ORD-' . str_pad((string) $id, 6, '0', STR_PAD_LEFT);
    }
}

$formatter = new OrderReferenceFormatter();

echo $formatter->format(7) . PHP_EOL;

// Prints:
// ORD-000007

The class can be final while still satisfying an interface that callers can depend on.

Testing Final Classes

Final classes are fine when tests exercise behaviour through public methods. Problems usually appear when tests rely on subclassing or mocking concrete classes instead of depending on interfaces.

PHP example
<?php

declare(strict_types=1);

final class PercentageDiscount
{
    public function __construct(private int $percent)
    {
        if ($percent < 0 || $percent > 100) {
            throw new InvalidArgumentException('Discount percent is invalid.');
        }
    }

    public function applyTo(int $pennies): int
    {
        return (int) round($pennies * (100 - $this->percent) / 100);
    }
}

$discount = new PercentageDiscount(20);

echo $discount->applyTo(1000) . PHP_EOL;

// Prints:
// 800

You can test the result directly without extending the class.

What To Remember

Use final classes when inheritance is not supported by the design. Use final methods when a child class may extend some behaviour but must not replace a specific rule. If callers need replaceability, provide an interface instead of relying on subclassing.

Practice

Task: Make Email Address Final

Create a final value-style class for an email address.

Requirements

  • Use declare(strict_types=1);.
  • Create a final class named EmailAddress.
  • Accept the address through the constructor.
  • Validate the address with FILTER_VALIDATE_EMAIL.
  • Return the normalised lowercase value from a public method.
  • Print one valid address.
  • Show one invalid address by catching the exception.
  • Include the expected output as comments in the same PHP code block.

The class should be final because its validation and normalisation rules should not be changed by subclasses.

Show solution
PHP example
<?php

declare(strict_types=1);

final class EmailAddress
{
    public function __construct(private string $value)
    {
        $value = trim($value);

        if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
            throw new InvalidArgumentException('Email address is invalid.');
        }

        $this->value = strtolower($value);
    }

    public function value(): string
    {
        return $this->value;
    }
}

$email = new EmailAddress(' NIA@example.com ');

echo $email->value() . PHP_EOL;

try {
    new EmailAddress('not-an-email');
} catch (InvalidArgumentException $exception) {
    echo $exception->getMessage() . PHP_EOL;
}

// Prints:
// nia@example.com
// Email address is invalid.

The class is final because callers should not be able to change the validation rules through inheritance. If another part of the application needs replaceable behaviour, it should depend on an interface around the service using this value.