Final class y final method en PHP: guía completa con ejemplos reales y código profesional
Aquí me gustaría explicar un concepto interesante en la programación orientada a objetos en PHP: hablo de final. Lo primero es entender qué es y para qué sirve, y luego veremos un ejemplo práctico de su uso.
Final class
La palabra final se utiliza para declarar que un método o clase no puede ser sobreescrito por una clase hija: en otras palabras, hablamos de la última implementación de un método o clase. Su objetivo es proteger un comportamiento específico para garantizar la estabilidad y coherencia del código.
Su estructura es la siguiente:
final class ClassName
{
// ...
final public function methodName()
{
// ...
}
}
Si hablamos de un ejemplo real y práctico, podemos imaginar a un generador de UUID (Identificador Universalmente Único) que no debería ser modificado por ninguna clase hija para asegurar que siempre generen identificadores únicos de manera consistente:
final class UuidGenerator
{
public function generate(): string
{
return bin2hex(random_bytes(16));
}
}
Con esto protegemos la forma de generar los UUIDs, asegurando que cualquier clase que herede de UuidGenerator no pueda alterar este comportamiento crítico.
Con bin2hex y random_bytes, garantizamos que los UUIDs generados sean únicos y seguros, consiguiendo así la integridad de los identificadores en nuestro sistema.
Como puedes imaginar podemos usar otras formas de generar un UUID, podemos por ejemplo trabajar con uniqid(), random_bytes directamente, random_int(), o incluso usar librerías externas especializadas en este tema. Pero es precisamente lo que queremos evitar al usar final: que alguien pueda cambiar la forma en la que se generan los UUIDs, comprometiendo su unicidad y seguridad.
Podemos usar esta clase de la siguiente manera:
$generator = new UuidGenerator();
$uuid = $generator->generate();
echo $uuid; // b1c501f00b0b88bf8d5d801c3ff5865b
Para comprobar esta teoría, puedes intentar crear otra clase que herede de UuidGenerator, con este simple hecho verás un error fatal. En otras palabras, si no puedes heredar, tampoco podrás sobreescribir sus métodos:
class Example extends UuidGenerator
{
// ...
}
// Fatal error: Class Example cannot extend final class UuidGenerator
Final method
Con este concepto tenemos un pequeño extra de seguridad, podemos heredar de una clase, pero no podemos sobreescribir los métodos que hemos declarado como final.
class Logger
{
final protected function timestamp()
{
return date('Y-m-d H:i:s');
}
final protected function format(array $data): array
{
return [
'timestamp' => $this->timestamp(),
'data' => $data,
];
}
final public function log(string $event, array $data = []): void
{
$data = $this->format($data);
$this->save($event, $data);
}
protected function save(string $event, array $data): void
{
$line = $event . ' ' . json_encode($data) . PHP_EOL;
file_put_contents('app.log', $line, FILE_APPEND);
}
}
Podemos heredar de Logger, pero no podremos sobreescribir los métodos timestamp, format y log, ya que están declarados como final. En todo caso, podemos sobreescribir el método save, que es el que realmente maneja la forma en la que se guardan los logs.
Este es el criterio que puedes tener, por ejemplo, quiero garantizar que timestamp, format y log no sean alterados porque son importantes para el buen funcionamiento de mi clase.
Puedo usar esta clase directamente de la siguiente manera:
$logger = new Logger();
$logger->log('user.registered', ['id' => 123, 'email' => '[email protected]']);
$logger->log('user.logged_in', ['id' => 123]);
$logger->log('user.updated', ['id' => 123, 'email' => '[email protected]']);
// Contenido de app.log:
// user.registered {"timestamp":"2025-12-05 05:04:10","data":{"id":123,"email":"[email protected]"}}
// user.logged_in {"timestamp":"2025-12-05 05:04:10","data":{"id":123}}
// user.updated {"timestamp":"2025-12-05 05:04:10","data":{"id":123,"email":"[email protected]"}}
Pero podemos extender la clase Logger para personalizar la forma en la que se guardan los logs, por ejemplo, añadiendo más información como el servicio, la versión y el entorno:
class ServiceLogger extends Logger
{
public function __construct(
private string $service = 'User Registration',
private string $version = '1.0',
private string $environment = 'local',
) {
// ...
}
protected function save(string $event, array $data): void
{
$enhanced = [
'service' => $this->service,
'version' => $this->version,
'environment' => $this->environment,
...$data,
];
parent::save($event, $enhanced);
}
}
Podemos usar esta nueva clase ServiceLogger de la siguiente manera:
$logger = new ServiceLogger('User Service', '2.1', 'production');
$logger->log('user.registered', ['id' => 123, 'email' => '[email protected]']);
El contenido de app.log ahora será:
user.registered {
"service":"User Service",
"version":"2.1",
"environment":"production",
"timestamp":"2025-12-05 05:28:11",
"data":{
"id":123,
"email":
"[email protected]"
}
}
Por eso mi variable de llama $enhanced, porque he mejorado la información y estructura sin alterar el comportamiento de los métodos timestamp, format y log, que siguen siendo los mismos y no pueden ser modificados.