DEV Community

Cover image for Composition vs. Inheritance in PHP Frameworks
István Döbrentei
István Döbrentei

Posted on • Edited on

Composition vs. Inheritance in PHP Frameworks

I chose this title for my post because understanding the difference between these concepts, composition and inheritance, is essential for writing clean, maintainable code. Many developers dive into popular frameworks and use their powerful features without fully grasping the principles behind them.
Yet, in our fast-paced and constantly evolving world, maintainability and reliability are crucial. In this post, I’ll briefly introduce both concepts and demonstrate how they are applied in well-known PHP frameworks like Slim or Laravel by exploring real code examples.

Understanding Inheritance

Let’s begin by defining inheritance. In the object-oriented world, a class represents a logical unit that models real-world concepts. When you create an instance of a class, you get an object. Objects have properties (data) and methods (behavior), and they can also relate to other objects.

One such relationship is inheritance, where a class (called a child or subclass) inherits properties and methods from another class (the parent or superclass). This allows the child class to reuse or override functionality defined in the parent, supporting extensibility and reducing code duplication.

In PHP, inheritance is only possible between classes and interfaces. A class can inherit from only one other class—this is known as single inheritance. While PHP doesn’t support multiple inheritance directly, it offers a feature called traits to help simulate it. However, I’ll focus solely on classical inheritance here.

Inheritance plays an important role in promoting the Don't Repeat Yourself (DRY) principle, making your codebase more maintainable and easier to extend.

What Is Composition?

Now, let’s explore what composition means. Think about the word itself—composition. What comes to mind? That’s right: creating something by putting smaller pieces together.

In programming, composition is the practice of building a class by using instances of other classes. Instead of inheriting behavior from a parent class, you're assembling functionality from separate, focused components. Each of these components (or "pieces") serves a clear, specific purpose, and together, they form a more complex system.

You might wonder: Why is this so important? The answer lies in one of the biggest challenges in software development—maintainability.

In our fast-paced, ever-changing world, codebases need to be adaptable. Composition supports this by reducing tight coupling and making it easier to modify or replace individual parts without causing ripple effects elsewhere. It also enhances testability. You can isolate individual components and write targeted tests for each one.

And as we all know, a high-quality codebase is impossible without good test coverage. With composition, you gain confidence in your system’s behavior because every business use case can be verified through well-structured tests.

Slim Framework

Let’s take a closer look at how inheritance and composition are used in the Slim Framework.

A good example of inheritance is the Logger class under the Slim namespace. It extends an abstract class called AbstractLogger, which in turn implements the LoggerInterface from the PSR-3 standard. Abstract classes in PHP cannot be instantiated directly, so this setup provides a reusable foundation for custom logging logic while adhering to a standardized interface.

This design allows developers to plug in their own logger implementations or use the default one in a consistent and predictable way, all while staying compatible with other PSR-3-compliant tools.

Another example of inheritance in Slim is in its HTTP-related exception classes, such as HttpException, HttpSpecializedException, and HttpForbiddenException. These classes share common behavior through inheritance but typically don’t contain complex logic. Instead, they override specific methods, implement a standard interface, or provide simple property access via getters and setters. This keeps the exception hierarchy clear and focused while still taking advantage of inheritance for code reuse.

Another interesting case is the FastRouteDispatcher class in Slim, which extends the GroupCountBased class from the FastRoute library. Although the GroupCountBased class is marked as @final in its docblock, it isn't technically marked with the final keyword in code. This annotation is a convention, signaling that the class should not be extended, even if PHP itself doesn’t enforce it.

In such cases, one could argue that a more flexible design using composition or a decorator pattern might be preferable. However, the Slim team may have chosen inheritance here for pragmatic reasons—such as tight integration with FastRoute, performance, or compatibility with previous versions.

This also serves as a practical example of polymorphism. Both GroupCountBased and Slim’s FastRouteDispatcher extend from the same base and can respond to shared method calls in different ways. This allows Slim to plug into FastRoute's routing system while still customizing behavior as needed.

Image description

A Composition Example in the Slim Framework

Now let’s look at how composition is used in the Slim Framework. When a class uses other classes to fulfill its responsibilities, we refer to it as a "has-a" relationship—one of the clearest signs of composition.

Slim handles these relationships through dependency injection (DI). Interestingly, Slim doesn't include a DI container of its own. Instead, it's designed to work with any PSR-11-compatible container, such as PHP-DI, Pimple, Laminas ServiceManager, or the Symfony DependencyInjection component.

This means Slim is built around a standard interface, not a concrete implementation. That’s a powerful design decision. It decouples the framework from any specific container and allows developers to plug in the tool that best fits their needs.

It’s a great example of flexible, future-proof design. Have you ever thought about structuring your own applications this way? If so, you’re well on your way to building maintainable and scalable software.

Laravel

In Laravel, inheritance plays a key role in the framework’s core architecture. A clear example is the Application class, which extends the Container class. This relationship allows Laravel’s application instance to inherit all the powerful features of the service container—such as dependency resolution and binding—while adding its own framework-specific logic.

A strong example of interface inheritance in Laravel is the ContainerContract interface. This contract extends the PSR-11 ContainerInterface, which defines the basic behavior of a dependency injection (DI) container. Laravel builds upon this standard by expanding the interface to meet the needs of a full-featured framework, turning it into the foundation of Laravel's entire DI system.

This design reflects Laravel’s contract-based architecture—a principle where components depend on interfaces, not concrete implementations. The Container class implements the ContainerContract, and Laravel components rely on the contract, making the system more flexible and testable.

Composition in Laravel

Laravel is full of examples that showcase the power of composition, which is used extensively throughout its components.

Take the PasswordBroker class, for instance. Rather than extending another class, it uses composition to handle password reset functionality. Dependencies like the UserProvider and other related services are injected via the constructor, allowing the class to collaborate with other components without tightly coupling to them.

This design allows Laravel to be flexible and testable. For instance, you can easily swap out the UserProvider, mock dependencies for unit testing, or extend behavior without modifying existing classes.

This is the true strength of composition—it promotes loose coupling and makes your codebase easier to maintain, extend, and reuse.

Conclusion

Understanding the differences between inheritance and composition—and knowing when to apply each—is a key step toward writing clean, maintainable code. When used appropriately, these principles help developers build flexible, testable, and scalable applications. And that ultimately leads to more satisfied teams, happier customers, and a healthier codebase.

Top comments (0)