How to use final in PHP?
If someone would ask me about my favorite keyword in PHP, I would certainly answer: final. It doesn’t mean I write this modifier in each class or method. It not only shows the intention but also provides a mechanism to protect the code. At least from the assumption.
The final keyword can be used both on the class level and on the method level. It prevents future extension of functionalities in a non-effective way. If a class is marked as final, then we can’t inherit from it. If a method is marked as final, we can’t override it.
The theory sounds good. Let’s go to details.
The intention behind final
In object-oriented programming, you can extend the behavior of the class by inheritance. It’s simple and convenient.
Do you remember the example classes Figure and its children: Square, Rectangle, Polygon, etc.? It’s a classic example of inheritance and polymorphism in work. In this case, you probably don’t want to finalize Figure class.
The final keyword added to the Figure class puts the extra restriction on the subject, so inheritance is no longer possible. So why does this mechanism even exist? Who would like to limit the usability of own solutions?
But finalizing is not the same as limiting.
Imagine the hypothetical class ArticleProvider whose only one public method getAll()
is responsible for returning collection of articles, e.g. from my blog. Moreover, let’s say, that this class fulfills the interface called ArticleProviderInterface.
If you want to operate over my articles, you have two possible ways. You can extend the class ArticleProvider using inheritance or you can use the class, e.g. using composition. Both ways lead to a proper solution, but because I’m the author of the class, I would like to encourage you to choose the safer option – composition. It prevents creating a chain of inheritance and preserves the encapsulation of behavior and state in class.
Since there is no special reason why someone would need to extend ArticleProvider, I decided to mark them as final. It means the class is ready to use as one of the implementations of the interface ArticleProviderInterface. Other developers may utilize only the public API of this class.
Final as the synonym of “ready”
An interface is a contract between developers and people who will be using the class so they can be sure that the behavior of methods specified by interface will not change.
Final, however, is a contract between developers to make sure, that the implementation of the specific class will not change in the future.
The class marked as final means, that the specific implementation of the interface is final. In other words, it’s complete in the sense, that there is no need to add any other functionalities there.
It looks like a good moment for an example. I’m going to create a simple mailer, so let’s define an interface for it.
interface MailerInterface
{
public function send(string $email, Message $message): bool;
}
I need at least two implementations of this interface: the first one should actually send an email. For this example, we can use a PHP’s builtin mail
function. The second one should write messages to file – I’ll use it in tests.
final class BasicMailer implements MailerInterface
{
public function send(string $email, Message $message): bool
{
return mail($email, $message->getSubject(), $message->getBody());
}
}
I think, that above class is functionally complete and fulfill the interface, so I decided to mark it as final. The second implementation is as follows:
final class FileMailer implements MailerInterface
{
public function __construct(string $filePath)
{
$this->file = $filePath;
}
public function send(string $email, Message $message): bool
{
$payload = json_encode([
'email' => $email,
'subject' => $message->getSubject(),
'body' => $message->getBody()
]);
return file_put_contents($this->file, $payload) !== false;
}
}
As previous, I marked this implementation as final. Nobody should expect more from this simple class – it does exactly what it should.
This example is suited to be used with the final keyword, but most of the classes we write aren’t. It’s important to know when we want to use this feature. I write want because there are no “should” or “should not”. You, as the developer and author, decide how your code is flexible. I’m going to write about this later.
How to introduce change if a class is final?
First, answer this question: “Does the change you would like to introduce isn’t the other implementation of specific interface?”
Consider this scenario: Our platform serves users' avatars from our own FTP server. Due to cost-reduction, business would like to migrate to the Amazon S3. Avatars are accessible by AvatarService
which fulfill the interface AvatarProviderInterface
.
interface AvatarProviderInterface
{
public function getForUserId(int $id): string;
}
Although it’s not explicitly marked, AvatarService is final in that sense, that it provides access to avatars from FTP server. Instead of changing its implementation to S3, we should create another implementation of AvatarProviderInterface, e.g. S3AvatarService
.
We finished with 2 completely different implementations of the same interface. Both are in some sense final – we don’t expect anyone to extend them. Moreover, in any moment, we can switch to the previous implementation. We may also deprecate it, if is no longer needed.
I recommend asking yourself if the change isn’t the other implementation each time when we need to change something in the class, even for a non-final class.
If it’s a bug that needs to be fixed in existing implementation, don’t worry. Go to your tests and spend a few minutes writing a missing scenario that covers this issue. Then, make tests green again.
Tests with final class
Tests are always connected to specific implementations. There is nothing wrong to mock a filesystem, an external API or a queue to check if specific implementation works properly.
I often hear an opinion that trying to cover each line of methods is sometimes like concreting the implementation. Whatever you change in the code, the covering test has started to fail.
Instead of identifying another implementation, developers are trying to change existing and completed (final) classes. Of course, there are exceptions. Nevertheless, it’s normal, that if you change the implementation, you have to also change the way how you test it.
How to mock final class?
One of the arguments against using the final keyword in classes is that we’re unable to create a mock of this class in order to test the dependent service.
It might sound strange, but this behavior is completely O.K.
Actually, you should mock an interface (generally speaking, abstraction) rather than the implementation of it. If you follow this rule, the problem with mocking final class will disappear. Unless you use methods or properties from outside the interface. I wrote why we shouldn’t do that in other article.
It shows also another concern: not everything should be marked as final.
When to use a final modifier?
There is no better than Marco Pivetta’s answer:
Make your classes always final, if they implement an interface, and no other public methods are defined
For any other situation, I would consider rethinking an approach a bit and check if a class doesn’t break any of SOLID principles. It may turn out, that making the class as final is ineffective, e.g. for classes like DTOs, controllers – a class without interface – or entities, (e.g. Doctrine’s entities) where final breaks proxy mechanism.
Summary
Marking class as final is a contract between developers, that the implementation of the class will not change in the future. Interface, on the other side, is a contract which ensures that class has specific behavior.
The final modifier may apply to both classes or methods. It prevents further modifications by inheritance, encouraging developers to use composition instead.
I recommend creating classes with the intention to “finalize” them. It’s not an obligation and you may remove this modifier anytime. It’s easier to transit from final to non-final class rather than the opposite direction.
Give yourself time to check if you’re comfortable with this approach. You’re going towards a better, cleaner code.
So, at the end I have one thought: If abstract means, that implementation is not ready, shouldn’t the final be a mark of its completeness? I’ll be happy to hear your opinions.
Featured photo by Angelina Litvin on Unsplash.