SoFloPHPIndependent reference

How PHP Classes Interfaces Traits and Visibility Rules Work

PHP classes and interfaces enforce contracts through visibility rules and type declarations; modern PHP favours composition with readonly classes and constructor promotion.

How PHP Classes Interfaces Traits and Visibility Rules Work

Most developers hit a wall around their third project. Inheritance alone cannot model their domain without turning the codebase into a fragile hierarchy. The answer is contract-driven design built on PHP classes, interfaces, composition and visibility. A modern PHP codebase under 8.2 uses PSR-4 for autoloading, Composer for dependencies, and the PHP-FIG standards for interface design. Interfaces define the contract. Readonly classes implement the value objects. Constructor property promotion eliminates boilerplate from DTOs. Traits appear only where composition cannot reach. Final classes lock the implementation.

Get the combination wrong and you pay in test maintenance and refactoring cost for years. The visibility rules enforce boundaries. Public methods are the contract. Protected methods exist only in classes genuinely designed for extension, and those are rare. Private methods are the default for internal logic. Every protected method you write is a commitment to support that method in every future subclass, and that commitment has a maintenance cost.

PSR-4 namespace directory mapping underpins every project you touch. The directory tree tells you the namespace structure before you read a single line of code. Composer autoloader classmap entries sit alongside that PSR-4 mapping, but the standard is what makes the layout predictable across frameworks and teams.

PHP Classes Interfaces Composition Visibility: Contract First, Implementation Second

An interface contract says what a unit does, never how. A class implementing that interface says how. Design from the contract outward and a change in the how never cascades into the calling code. The PHP-FIG standards encode this separation across the ecosystem. Ignoring it is the fastest way to a codebase where every class knows too much about every other.

Visibility modifiers public, protected, and private are the enforcement mechanism. Public is the contract surface. Protected is for subclasses that extend behaviour without breaking it. Private is the internal detail you change without telling anyone. A common failure mode is making a method protected by default, which leaks implementation to every future subclass. Start private. Open up only when a concrete extension demands it.

Final classes are increasingly the default. A final class cannot be extended, which forces composition over inheritance. When you see final class PaymentProcessor, the author is telling you to wrap it, not subclass it. That constraint prevents the deep inheritance chains that make method override signature rules a debugging nightmare.

Constructor Property Promotion Arrived in PHP 8.0

Before PHP 8.0, a value object needed a constructor parameter, a private property, and an assignment line for every field. Three lines per field, most of them boilerplate. Constructor property promotion collapses those three into one: public function __construct(private string $name, private Money $amount). The visibility, type, and property declaration happen in the parameter list.

Promotion works with any visibility modifier and with nullable types. You can mix promoted and non-promoted parameters in the same constructor. One sharp edge: a promoted parameter cannot be used in a class that extends another where the parent constructor defines the same property name. The engine throws a fatal error. Rename the property or refactor the hierarchy.

When to Skip Promotion

Use promotion for value objects and DTOs. Skip it on service classes where the constructor accepts a long list of dependencies you may need to decorate individually. A promoted parameter is compact but leaves no room for a docblock on the property. If a property needs a detailed description for static analysis, keep the traditional three-line form.

Readonly Properties Landed in PHP 8.1 and Readonly Classes in 8.2

A read-only property is writable once, in the constructor or at declaration, and then locked. A read-only class extends that to every typed property in the class, making the whole object immutable after construction. The keyword readonly on the class means you never have to repeat it on each property.

The constraint is strict. A read-only property cannot be unset, cannot be reassigned, and cannot be passed by reference to a function that modifies it. A read-only class cannot contain untyped properties and cannot be extended unless the child is also read-only. These rules prevent the two most common sources of accidental mutation in PHP: a setter called from deep in a call stack and an untyped dynamic property added at runtime.

Where Readonly Classes Belong

Reach for a read-only class when an object represents a value, a configuration snapshot, or a result. A Money object, a ShippingAddress, an OrderConfirmed event. Do not use it for entities managed by an ORM, where the persistence layer needs to hydrate properties after construction. For those, mark individual properties as read-only and leave the class extensible.

PHP Interface Segregation PSR and the PSR-4 Namespace Directory Mapping

PHP interface segregation is not a single PSR but a principle enforced by the PHP-FIG standards process. PSR-4 namespace directory mapping defines how a fully qualified class name like Acme\Invoice\Domain\LineItem resolves to the file src/Invoice/Domain/LineItem.php. The autoloader expects the namespace prefix to match a base directory and the sub-namespace to match subdirectories.

Composer autoloader classmap entries can supplement PSR-4 for files that do not follow the convention, such as a legacy lib/ directory. The rule: PSR-4 for new code, classmap for exceptions. A project that relies entirely on classmap has abandoned the standard and will break when Composer runs on a filesystem with different case sensitivity.

Self vs Static Resolution

Namespace resolution follows the mapping. When you write use Acme\Invoice\Domain\LineItem;, the autoloader calls the PSR-4 rule for the Acme prefix and finds the file. The keyword self resolves to the class where it is written. The keyword static uses late static binding to resolve to the class that was actually instantiated at runtime. That distinction is the difference between a base class that knows its own table name and one that lets a subclass override it.

PHP Readonly, Promotion, and Visibility Feature Matrix
FeaturePHP VersionScopeMutation RuleBest Fit
Constructor property promotion8.0Constructor parameterAssignable once in constructorDTOs, value objects
Readonly properties8.1Individual typed propertyAssignable once at declaration or in constructorEntities, configuration
Readonly classes8.2All typed properties in the classAll properties locked after constructionImmutable values, events
Final classesPHP 5+Whole classNo extension permittedSealed implementations

PHP Traits vs Composition: Reusable Fragments, Not Object Graphs

A trait is a compile-time copy-and-paste. The trait's code is inserted into the class at the engine level. There is no runtime dispatch, no separate instance, and no interface contract implied by the trait itself. The decision to use one comes at the point where two unrelated classes need the same behaviour and you cannot move it into a shared parent.

The trap is using a trait where a composed object would work. A trait that holds state, expects a particular constructor signature, or calls methods it does not define has created a hidden contract. That contract is not enforceable by the engine and breaks silently when a new class uses the trait. The test for correct use: can you replace the trait with a private composed object and a set of forwarding methods without changing the class's public API? If yes, use composition. If no, the trait is entangled with the host class and is a maintenance liability.

Composition Is the Default

Object composition over inheritance is the durable principle. A class that accepts a LoggerInterface in its constructor has an explicit, testable dependency. A class that uses a LoggingTrait has a hidden dependency on whatever that trait happens to call. The trait version is faster to write and slower to debug. Modern PHP codebases default to composition and use traits only for cross-cutting concerns like __invoke magic methods or standardised __construct behaviour that cannot be injected.

Method Override Signature, Covariance, Contravariance, and Late Static Binding

A method override signature must be compatible with the parent. PHP enforces this at runtime: an overriding method cannot add required parameters, cannot remove optional ones, and cannot change the return type to something wider. The rules loosen in one direction. Covariance allows a child method to return a more specific type than the parent declared. Contravariance allows a child method to accept a less specific parameter type. Both rules preserve the Liskov substitution principle: code written against the parent class must work with any child.

The practical failure happens when a refactor changes a parent method's signature and the IDE does not flag a distant subclass that overrides it. The subclass continues to compile but throws a fatal error the next time it is loaded. Declare every class final unless you have a documented reason to permit extension. This catches the error at the point of declaration rather than at the point of execution.

Late Static Binding in Practice

Late static binding answers the question of which class a static call comes from. The keyword static resolves to the class that was called at runtime, not the class where the code is written. This is essential for base classes that generate queries or look up table names: a static::$table in a parent class returns the child's table when called through the child. Using self would always return the parent's table, breaking every subclass that expects polymorphism.

Abstract Classes, Magic Methods, and Final Class Usage

An abstract class sits between an interface and a concrete class. It defines part of the implementation and leaves part for subclasses to fill. Use it when multiple classes share a partial implementation that is not complete enough to instantiate on its own. A common pattern is an abstract controller that handles authentication and leaves the action method for each page to implement.

Magic methods __construct and __invoke are the two you use daily. The constructor initialises state. The invoke method turns an object into a callable, which is the standard way to write single-action classes in PSR-15 middleware chains. Other magic methods like __get and __call exist but signal a design that has lost track of its own interface. A class that relies on magic methods for access is a class that cannot be statically analysed.

Why Final Classes Are the Default

Final classes are the default in new PHP 8.2 codebases because they reduce the cognitive surface of every class. Open a final class and you know the implementation you see is the only one that exists. Open a non-final class and you must check every subclass to understand the full behaviour. The cost of making a class final is zero at runtime. The benefit is removing an entire category of bug from the codebase.

Putting It Together: The Contract-Driven PHP Codebase

The failure case is the codebase that starts with inheritance and adds interfaces later. By the time the interfaces arrive, the class hierarchy is too deep to refactor safely, and the interfaces become a thin wrapper over a brittle structure. Start with the interface. Write the contract first. Implement it in a final class. Compose behaviour from objects that satisfy their own contracts. That sequence costs an extra five minutes per class and saves weeks of debugging over the life of the project.

Common Questions

What PHP version do I need for readonly classes?

PHP 8.2. The readonly class keyword marks all typed properties as immutable after construction. If you are on 8.1, you can mark individual properties as readonly but cannot apply it to the whole class.

How does constructor property promotion work?

It lets you declare and assign a property directly in the constructor parameter list. Write <code>public function __construct(private string $name)</code> instead of declaring the property, the parameter, and the assignment on three separate lines. Available from PHP 8.0.

What is the difference between self and static in PHP?

Self resolves to the class where the code is written. Static uses late static binding to resolve to the class that was actually called at runtime. Use static in base classes that need to reference the child class's properties or methods.

When should I use a trait instead of composition?

Use a trait only when the behaviour cannot be injected as a dependency. The test is whether you can replace the trait with a private composed object and forwarding methods without changing the public API. If you can, use composition.

How does PSR-4 namespace directory mapping work?

A namespace prefix maps to a base directory, and sub-namespaces map to subdirectories. The fully qualified class name <code>App\Domain\User</code> resolves to <code>src/Domain/User.php</code> when the <code>App</code> prefix maps to <code>src/</code>.

Are final classes really the default in modern PHP?

Yes. A final class cannot be extended, which forces composition over inheritance. The runtime cost is zero and the maintenance benefit is removing the need to check every subclass to understand behaviour. Only remove final when you have a documented reason to permit extension.

What is the interface segregation principle in PHP?

A class should not be forced to implement methods it does not use. Break large interfaces into smaller, role-specific ones. A class that needs only a read method should implement <code>Readable</code>, not a monolithic <code>RepositoryInterface</code> that also declares write and delete.