La visibilidad es un concepto clave en programación orientada a objetos. Directamente nos permite controlar el acceso a las propiedades y métodos.
Principalmente existen tres niveles de visibilidad: public, protected y private.
- Public: significa que la propiedad o método podría ser accedida desde cualquier parte del código.
- Protected: aquí conseguimos un nivel intermedio de acceso. La propiedad o método podrá ser usada en la clase a la que pertenece y en las clases que hereden de ella.
- Private: este es el nivel más restrictivo. La propiedad o método sólo podrá ser accedida desde la clase en la que se define. Es decir, las clases hijas no podrán acceder a ellas.
A esto también se le conoce como modificadores de acceso y son en ensencia reglas que definen cómo se puede acceder a las propiedades y métodos. Es una forma de encapsulación que nos permite proteger los datos y la funcionalidad de una clase.
Encapsular significa exponer únicamente lo necesario y ocultar el resto.
Todo esto con el objetivo de mantener la integridad de los datos y a evitar que se modifiquen de forma incorrecta o inesperada.
Aunque tendré un apartado especial para la herencia de clases, te lo explico brevemente aquí como contexto para entender mejor.
Herencia de clases básica
Herencia significa transmisión de algo a los descendientes. En programación, la herencia de clases sería un mecanismo que permite crear una nueva clase hija basada en una clase existente conocida como clase padre.
En este caso, la clase hija heredaría las propiedades y los métodos de su padre, de esta manera podemos reutilizar el código y básicamente extender la funcionalidad.
class ParentClass
{
public $name;
public function greet()
{
return "Hello, {$this->name}!";
}
}
class ChildClass extends ParentClass
{
public function greet()
{
return "Hi there, {$this->name}!";
}
}
$parent = new ParentClass();
$parent->name = 'Graaan';
echo $parent->greet(); // Imprime "Hello, Graaan!"
$child = new ChildClass();
$child->name = 'Italo';
echo $child->greet(); // Imprime "Hi there, Italo!"
En este ejemplo, ChildClass hereda de ParentClass, significa que ChildClass tiene acceso a la propiedad $name y al método greet(). Pero también hice algo interesante, ChildClass sobreescribe el método greet().
Es como si mi padre me enseñara a saludar de una manera, pero yo decidiera hacerlo a mi manera a partir de lo que aprendí de él.
Debemos tener en cuenta algunas cosas importantes sobre la herencia de clases:
- Para extender una clase usamos la palabra clave
extends. - Sólo podemos heredar de una clase padre, a esta también se le puede llamar como clase base.
- Podemos sobreescribir lo que heredamos (métodos y propiedades) en la clase hija.
- Para sobrescribir un método o propiedad, simplemente lo definimos de nuevo en la clase hija con el mismo nombre.
- Si queremos llamar al método o propiedad del padre desde la clase hija, podemos usar la palabra clave
parent::, por ejemplo:parent::greet().
// ...
class ChildClass extends ParentClass
{
public function greet()
{
$baseMessage = parent::greet();
return "$baseMessage Welcome!";
}
}
$child = new ChildClass();
$child->name = 'Italo';
echo $child->greet(); // Imprime "Hello, Italo! Welcome!"
Esta es la idea básica de la herencia, es una especie de jerarquía profesional que podemos ver incluso en Laravel con sus controladores, modelos y demás.
Public
Acabamos de ver su funcionamiento en el ejemplo anterior. Tenemos la propiedad $name y el método greet() como públicos, y por eso podemos acceder a ellos desde cualquier parte del código. Es el nivel de acceso más permisivo y por defecto todas las propiedades y métodos.
class Example
{
var $name;
function greet()
{
return "Hello, {$this->name}!";
}
}
Tanto $name como greet() son públicos. Podemos crear un objeto de la clase Example y acceder a ellos sin problemas:
$example = new Example();
$example->name = 'Italo';
echo $example->greet(); // Imprime "Hello, Italo!"
Pero no se recomienda trabajar así, esto significa falta de control. El resultado sería un código menos seguro y más difícil de mantener. Y si realmente necesitas que una propiedad o método sea público, es mejor especificarlo explícitamente:
class Example
{
public $name;
public function greet() {}
}
Piensa siempre en la legibilidad del código para un mejor mantenimiento a largo plazo.
Protected
Este caso es de nivel intermedio. Significa que la propiedad o método puede ser accedida desde la clase donde se define y desde las clases que heredan de ella.
class ParentClass
{
protected $name;
public function greet()
{
return "Hello, {$this->name}!";
}
}
class ChildClass extends ParentClass
{
public function greet()
{
return "Hi there, {$this->name}!";
}
}
En este ejemplo, $name es una propiedad protegida. Podemos acceder a ella desde ParentClass y ChildClass, pero no desde fuera de estas clases. Si intentemos acceder a $name como antes desde un objeto de ParentClass o ChildClass, tendremos un error:
// ...
$child->name = 'Graaan';
Inmediatamente dirá: Fatal error: Uncaught Error: Cannot access protected property ChildClass::$name. Y si creas un objeto de ParentClass y tratas de acceder a $name, obtendrás el mismo error: Fatal error: Uncaught Error: Cannot access protected property ParentClass::$name.
En este caso debemos cambiar de estrategia y usar métodos públicos para acceder y modificar la propiedad protegida:
class ParentClass
{
protected string $name;
public function setName(string $name): void
{
$this->name = $name;
}
public function greet()
{
return "Hello, {$this->name}!";
}
}
class ChildClass extends ParentClass {}
$child = new ChildClass();
$child->setName('Graaan');
echo $child->greet(); // Imprime "Hello, Graaan!"
Y para que el ejemplo sea más profesional, he añadido tipos de datos y valores de retorno en los métodos. Esto es en esencia lo que hace el modificador protected. He protegido a $name para que no pueda ser accedida directamente desde fuera de las clases, pero aún así puedo usarla dentro de las clases y sus descendientes.
Con eso mantenemos un buen nivel de encapsulación y control.
Private
Como mencioné antes, este es el nivel más restrictivo. Las propiedades y métodos privados sólo pueden ser accedidos desde la clase en la que se definen. Ni siquiera las clases hijas pueden acceder a ellos.
Por ejemplo, desde una clase hija, cuando una propiedad es protegida podemos acceder y modificar su valor, pero si es privada no podremos hacerlo:
class ParentClass
{
protected string $name;
public function setName(string $name): void
{
$this->name = $name;
}
public function greet()
{
return "Hello, {$this->name}!";
}
}
class ChildClass extends ParentClass
{
public function greet()
{
$this->name = strtoupper($this->name);
return "Hi there, {$this->name}!";
}
}
En este caso, $name es una propiedad protegida, por lo que podemos acceder a ella desde ChildClass y modificar su valor. Esto es perfectamente válido y no dará ningún error, pero es un problema de diseño. Si leemos bien, podemos ver que existe la intención de modificar el valor unicamente desde la clase padre con el método setName().
Al crear un objeto de ChildClass y llamar a setName() y greet(), obteneremos el siguiente resultado:
$child = new ChildClass();
$child->setName('Graaan');
echo $child->greet(); // Imprime "Hi there, GRAAAN!"
Vamos a evitarlo cambiando la propiedad a privada:
class ParentClass
{
private string $name;
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function greet()
{
return "Hello, {$this->name}!";
}
}
class ChildClass extends ParentClass
{
public function greet()
{
return "Hi there, {$this->getName()}!";
}
}
$child = new ChildClass();
$child->setName('Graaan');
echo $child->greet(); // Imprime "Hi there, Graaan!"
Ahora, si intentamos acceder a $name desde ChildClass, obtendremos un error. Hemos ocultado la propiedad para que sólo pueda ser accedida desde ParentClass. Por ello creamos métodos intermedios para acceder y modificar su valor.
Estos métodos se conocen como getters y setters. Su función es precisamente esa, obtener y establecer valores en propiedades privadas para mantener la encapsulación y el control. Podemos decir, que si deseas obtener o modificar el valor de alguna propiedad, debes hacerlo según las reglas definidas en los métodos públicos.
Proyecto resumen
class Logger
{
private string $file = 'app.log';
public function log(string $message): void
{
$format = $this->format($message);
$this->write($format);
}
protected function format(string $message): string
{
return $message;
}
private function write(string $message): void
{
$line = $message . PHP_EOL;
file_put_contents($this->file, $line, FILE_APPEND);
}
}
Acá tenemos una clase Logger que tiene tres métodos con diferentes niveles de visibilidad.
log(): es un método público que puede ser llamado desde cualquier parte del código. Su función es registrar un mensaje.format(): es un método protegido que puede ser accedido desde la claseLoggery desde cualquier clase que herede de ella. Su función es formatear el mensaje antes de escribirlo en el archivo.write(): es un método privado que sólo puede ser accedido desde la claseLogger. Su función es escribir el mensaje formateado en el archivo de log.- Finalmente, la propiedad
$filees privada, lo que significa que sólo puede ser accedida desde dentro de la claseLogger. Esto protege la ruta del archivo de log para evitar modificaciones externas.
Podemos crear un objeto de Logger y llamar al método log() para registrar un mensaje:
$logger = new Logger();
$logger->log('Example from Logger class');
Ahora, que tal si queremos extender la funcionalidad de Logger.
class CustomLogger extends Logger
{
protected function format(string $message): string
{
$date = date('Y-m-d H:i:s');
return strtoupper("[$date] $message");
}
}
$customLogger = new CustomLogger();
$customLogger->log('Example from CustomLogger class');
Como puedes ver, CustomLogger hereda de Logger y sobreescribe el método format(). Ahora, cuando llamamos a log() en un objeto de CustomLogger, el mensaje será formateado con la fecha y convertido a mayúsculas antes de ser escrito en el archivo de log.
El resultado en app.log sería algo así:
Example from Logger class
[2025-10-29 17:27:22] EXAMPLE FROM CUSTOMLOGGER CLASS
Con esto te muestro que antes de crear código, es importante pensar en los niveles de visibilidad adecuados para cada propiedad y método, con el fin de mantener un nivel lógico de encapsulación y control.