Curso Programación Orientada a Objetos en PHP

Si ya tienes las bases de PHP (variables, arrays, funciones, ciclos) y quieres dar el siguiente paso, este es tu curso. La Programación Orientada a Objetos, o POO, es la forma en la que se organiza casi cualquier proyecto serio en PHP moderno, desde Laravel hasta WordPress en sus partes más avanzadas.

Si necesitas los fundamentos basicos puedes ir al Curso aprende PHP desde cero.

No necesitas nada distinto a lo que ya tenías para el curso anterior: tu editor de código y un entorno como XAMPP o Laragon para correr PHP. Lo único que cambia aquí es la forma de pensar el código: en vez de funciones sueltas, vamos a empezar a organizar todo en clases y objetos.

Este curso no cubre bases de datos ni seguridad a fondo, aunque vas a ver algún ejemplo de paso para no quedarnos en lo abstracto. Esos temas, igual que en el curso anterior, se cubren aparte.

Lo que vamos a ver

  • Lección 0: Qué es la POO y por qué importa
  • Lección 1: Tu primera clase y objeto
  • Lección 2: Propiedades y métodos
  • Lección 3: El constructor
  • Lección 4: Encapsulamiento (public, private, protected)
  • Lección 5: Getters y setters
  • Lección 6: Herencia
  • Lección 7: Sobrescritura de métodos y parent::
  • Lección 8: Polimorfismo
  • Lección 9: Clases abstractas
  • Lección 10: Interfaces
  • Lección 11: Traits
  • Lección 12: Propiedades y métodos estáticos
  • Lección 13: Constantes de clase
  • Lección 14: Métodos mágicos
  • Lección 15: Namespaces
  • Lección 16: Autoloading con Composer

Lección 0: Qué es la POO y por qué importa

La Programación Orientada a Objetos propone otra forma de organizar las cosas: en vez de pensar en funciones y datos por separado, agrupas ambos en algo llamado clase.

Una clase es como un molde que describe qué información tiene “algo” (sus propiedades) y qué puede hacer (sus métodos). A partir de ese molde, creas objetos, que son instancias concretas de esa clase.

Un ejemplo con el que seguro ya tienes intuición: piensa en la clase Auto. Todos los autos tienen propiedades en común (marca, color, velocidad actual) y comportamientos en común (acelerar, frenar).

Pero tu auto y el de tu vecino son objetos distintos, cada uno con sus propios valores en esas propiedades, aunque ambos vengan del mismo molde.

Antes de seguir, piensa en dos o tres “cosas” del mundo real que podrías modelar como clases (un libro, un empleado, un producto).

No hace falta que escribas código todavía, solo que empieces a pensar en propiedades y comportamientos.

Lección 1: Tu primera clase y objeto

Vamos a lo práctico. Una clase se define con la palabra class, y para crear un objeto a partir de ella usamos new.

<?php
class Auto {
    public $marca = "Sin definir";
}

$miAuto = new Auto();
echo $miAuto->marca; // Sin definir

Fíjate en el operador ->, que es como accedes a las propiedades y métodos de un objeto. Si vienes de arrays asociativos, es fácil confundirlo con [], así que tómate un momento para acostumbrarte a la sintaxis.

Puedes crear varios objetos a partir de la misma clase, y cada uno va a tener su propia copia independiente de las propiedades:

<?php
$auto1 = new Auto();
$auto2 = new Auto();

$auto1->marca = "Toyota";
$auto2->marca = "Nissan";

echo $auto1->marca; // Toyota
echo $auto2->marca; // Nissan, no se pisan entre sí

Esto es justo la idea central de la POO: la clase es el plano, el objeto es la construcción real hecha a partir de ese plano. Puedes construir tantas casas como quieras del mismo plano, y cada una es independiente de las demás.

Crea una clase Libro con una propiedad titulo, instancia dos objetos distintos y asígnales títulos diferentes.

Lección 2: Propiedades y métodos

Ya viste las propiedades (los datos que guarda un objeto). Ahora toca la otra mitad: los métodos, que son básicamente funciones que viven dentro de una clase y describen lo que un objeto puede hacer.

<?php
class Auto {
    public $marca;
    public $velocidad = 0;

    public function acelerar() {
        $this->velocidad += 10;
    }
}

$miAuto = new Auto();
$miAuto->marca = "Toyota";
$miAuto->acelerar();

echo $miAuto->velocidad; // 10

La palabra $this es clave aquí: dentro de un método, $this se refiere al objeto específico sobre el que se está llamando ese método. Si tienes dos autos y llamas acelerar() en cada uno, $this apunta a un auto distinto en cada llamada, por eso sus velocidades no se mezclan.

También puedes pasarle parámetros a un método, igual que con cualquier función:

<?php
class Auto {
    public $velocidad = 0;

    public function acelerar($cantidad) {
        $this->velocidad += $cantidad;
    }
}

$miAuto = new Auto();
$miAuto->acelerar(25);
echo $miAuto->velocidad; // 25

Agrega a tu clase Libro un método describir() que devuelva una oración usando la propiedad titulo.

Lección 3: El constructor

Hasta ahora, para darle valores iniciales a un objeto, tuvimos que asignarlos línea por línea después de crearlo. El constructor te permite hacer eso mismo en un solo paso, justo en el momento en que el objeto nace.

En PHP, el constructor es un método especial llamado __construct, que se ejecuta automáticamente cada vez que usas new.

<?php
class Auto {
    public $marca;
    public $velocidad;

    public function __construct($marca, $velocidad = 0) {
        $this->marca = $marca;
        $this->velocidad = $velocidad;
    }
}

$miAuto = new Auto("Toyota");
echo $miAuto->marca; // Toyota

Nota que el parámetro $velocidad tiene un valor por defecto, igual que vimos con funciones normales en el curso anterior, así que puedes omitirlo al crear el objeto si no lo necesitas distinto de cero.

Desde PHP 8 existe además una forma abreviada de escribir constructores, llamada “promoción de propiedades”, que te ahorra tener que declarar la propiedad arriba y volver a asignarla adentro:

<?php
class Auto {
    public function __construct(
        public string $marca,
        public int $velocidad = 0
    ) {}
}

$miAuto = new Auto("Toyota");
echo $miAuto->marca; // Toyota

Es exactamente lo mismo que el ejemplo anterior, solo más compacto. La vas a ver muchísimo en código PHP moderno.

Dale a tu clase Libro un constructor que reciba titulo y autor, y úsalo para crear un objeto en una sola línea.

Lección 4: Encapsulamiento (public, private, protected)

Hasta ahora todas nuestras propiedades fueron public, lo que significa que cualquier parte de tu código puede leerlas o modificarlas libremente desde fuera de la clase. Suena cómodo, pero en la práctica es peligroso: nada evita que alguien le ponga una velocidad negativa a tu auto, por ejemplo.

Aquí es donde entra el encapsulamiento, uno de los pilares de la POO. La idea es simple: controla quién puede acceder a qué. Para eso existen tres niveles de visibilidad:

  • public: accesible desde cualquier parte, dentro o fuera de la clase.
  • private: solo accesible desde dentro de la misma clase.
  • protected: accesible desde dentro de la clase y desde las clases que hereden de ella (esto lo vas a entender mejor en la lección de herencia).
<?php
class Auto {
    private $velocidad = 0;

    public function acelerar($cantidad) {
        $this->velocidad += $cantidad;
    }

    public function obtenerVelocidad() {
        return $this->velocidad;
    }
}

$miAuto = new Auto();
$miAuto->acelerar(20);
echo $miAuto->obtenerVelocidad(); // 20

// Esto ya no funciona, y PHP te lo va a marcar como error:
// echo $miAuto->velocidad;

La regla general que casi todo el mundo sigue: si no tienes una buena razón para que algo sea público, hazlo private y expón solo lo necesario a través de métodos. Esto se conecta directo con lo que viene en la siguiente lección.

Cambia la propiedad titulo de tu clase Libro a private, y crea un método obtenerTitulo() que la devuelva.

Lección 5: Getters y setters

Ya viste que hacer una propiedad private te obliga a usar métodos para leerla desde fuera. Esos métodos tienen nombre propio: los que devuelven un valor se llaman getters, y los que lo modifican se llaman setters. Es una convención tan común que casi todo desarrollador PHP los reconoce de inmediato por su nombre.

<?php
class Auto {
    private $velocidad = 0;

    public function getVelocidad() {
        return $this->velocidad;
    }

    public function setVelocidad($nuevaVelocidad) {
        if ($nuevaVelocidad < 0) {
            $nuevaVelocidad = 0; // evitamos velocidades negativas
        }
        $this->velocidad = $nuevaVelocidad;
    }
}

$miAuto = new Auto();
$miAuto->setVelocidad(-10);
echo $miAuto->getVelocidad(); // 0, el setter lo protegió

Aquí está la verdadera ventaja del encapsulamiento: el setter puede validar antes de guardar. Si la propiedad fuera pública, cualquiera podría meterle un valor negativo sin que tu clase pudiera hacer nada al respecto.

No sientas que necesitas un getter y un setter para absolutamente cada propiedad; a veces una propiedad solo se lee y nunca se modifica desde fuera, y ahí basta con el getter. Usa el criterio según lo que en verdad necesite tu clase.

Agrega un setter a tu clase Libro que no permita guardar un título vacío.

Lección 6: Herencia

La herencia te permite crear una clase nueva basada en otra ya existente, heredando sus propiedades y métodos sin tener que copiarlos. Es útil cuando tienes varias clases que comparten comportamiento en común, pero cada una también tiene algo propio.

<?php
class Vehiculo {
    protected $velocidad = 0;

    public function acelerar($cantidad) {
        $this->velocidad += $cantidad;
    }

    public function getVelocidad() {
        return $this->velocidad;
    }
}

class Auto extends Vehiculo {
    public $marca;
}

class Motocicleta extends Vehiculo {
    public $cilindraje;
}

$miAuto = new Auto();
$miAuto->acelerar(30);
echo $miAuto->getVelocidad(); // 30, heredado de Vehiculo

Fíjate que usamos protected en vez de private para $velocidad. Si hubiera sido private, las clases hijas (Auto, Motocicleta) no habrían podido acceder a ella directamente, ni siquiera heredando. Esa es justo la diferencia entre ambos niveles de visibilidad que mencionamos antes.

La herencia es poderosa, pero también fácil de abusar. Si te encuentras heredando solo para reutilizar un par de métodos sin que exista una relación real de “es un tipo de”, probablemente te convenga más usar composición (una clase que contiene un objeto de otra) o los traits que vas a ver más adelante.

Crea una clase base Persona con nombre y edad, y una clase Estudiante que herede de ella y agregue una propiedad escuela.

Lección 7: Sobrescritura de métodos y parent::

A veces una clase hija necesita comportarse distinto a su clase padre en algún método específico, aunque conserve el resto. A esto se le llama sobrescritura (override): defines en la clase hija un método con el mismo nombre que el de la clase padre, y el de la hija toma prioridad.

<?php
class Vehiculo {
    public function describir() {
        return "Soy un vehículo genérico";
    }
}

class Auto extends Vehiculo {
    public function describir() {
        return "Soy un auto";
    }
}

$miAuto = new Auto();
echo $miAuto->describir(); // Soy un auto

Muchas veces no quieres reemplazar el comportamiento del padre por completo, sino extenderlo. Para eso existe parent::, que te deja llamar al método original de la clase padre desde dentro del método sobrescrito:

<?php
class Vehiculo {
    public function describir() {
        return "Soy un vehículo genérico";
    }
}

class Auto extends Vehiculo {
    public function describir() {
        return parent::describir() . ", específicamente un auto";
    }
}

$miAuto = new Auto();
echo $miAuto->describir(); // Soy un vehículo genérico, específicamente un auto

Esto mismo aplica al constructor: si la clase hija define su propio __construct, el de la clase padre no se ejecuta automáticamente, tienes que llamarlo tú con parent::__construct() si todavía lo necesitas.

En tu clase Estudiante, sobrescribe un método describir() heredado de Persona para que incluya también la escuela, apoyándote en parent::describir().

Lección 8: Polimorfismo

El polimorfismo suena más complicado de lo que es. La idea es que distintos objetos, todos relacionados por herencia (o por una interfaz, que vemos más adelante), puedan responder de forma distinta a la misma llamada de método, y tu código pueda tratarlos de manera uniforme sin preocuparse de qué clase exacta es cada uno.

<?php
class Vehiculo {
    public function hacerSonido() {
        return "...";
    }
}

class Auto extends Vehiculo {
    public function hacerSonido() {
        return "Bip bip";
    }
}

class Motocicleta extends Vehiculo {
    public function hacerSonido() {
        return "Vrooom";
    }
}

$vehiculos = [new Auto(), new Motocicleta()];

foreach ($vehiculos as $vehiculo) {
    echo $vehiculo->hacerSonido() . "<br>";
}

Lo interesante aquí es que el foreach no necesita saber si está tratando con un Auto o una Motocicleta: simplemente llama a hacerSonido() en cada uno, y cada objeto sabe responder a su manera. Esto es tremendamente útil cuando tienes listas de objetos de distintos tipos pero relacionados, como en un sistema donde manejas distintos métodos de pago o distintos tipos de notificación.

Crea una tercera clase que herede de Vehiculo (por ejemplo Camion) con su propio hacerSonido(), y recorre un array con los tres tipos de vehículo en un solo foreach.

Lección 9: Clases abstractas

Hay veces que una clase padre existe solo para ser heredada, y no tiene sentido crear objetos directamente de ella. Por ejemplo, “Vehículo” es un concepto útil como base, pero en la vida real no existe “un vehículo” sin más, siempre es un auto, una moto, algo concreto. Para esos casos existen las clases abstractas.

<?php
abstract class Vehiculo {
    abstract public function hacerSonido();

    public function describir() {
        return "Soy un vehículo que hace: " . $this->hacerSonido();
    }
}

class Auto extends Vehiculo {
    public function hacerSonido() {
        return "Bip bip";
    }
}

// Esto daría error, no puedes instanciar una clase abstracta:
// $vehiculo = new Vehiculo();

$miAuto = new Auto();
echo $miAuto->describir(); // Soy un vehículo que hace: Bip bip

Un método declarado como abstract no tiene cuerpo en la clase padre, solo su firma. Es básicamente un contrato: “cualquier clase que herede de mí está obligada a implementar este método”. Si una clase hija no lo implementa, PHP te va a marcar error.

La diferencia con una clase normal es justamente esa obligación combinada con la imposibilidad de instanciarla directamente. Úsala cuando tengas una jerarquía de clases que comparten estructura pero cada una necesita su propia implementación de ciertos comportamientos.

Convierte tu clase Vehiculo (o Persona, si prefieres seguir con ese ejemplo) en abstracta, con al menos un método abstracto que las clases hijas estén obligadas a implementar.

Lección 10: Interfaces

Una interfaz es parecida a una clase abstracta en que define un contrato de métodos que las clases deben implementar, pero va un paso más allá: no tiene ninguna implementación, solo nombres de métodos, y una clase puede implementar varias interfaces al mismo tiempo (algo que no puedes hacer con herencia, ya que en PHP solo puedes heredar de una clase padre).

<?php
interface Facturable {
    public function calcularTotal();
}

class Producto implements Facturable {
    public $precio;
    public $cantidad;

    public function __construct($precio, $cantidad) {
        $this->precio = $precio;
        $this->cantidad = $cantidad;
    }

    public function calcularTotal() {
        return $this->precio * $this->cantidad;
    }
}

$producto = new Producto(50, 3);
echo $producto->calcularTotal(); // 150

¿Cuándo usar una interfaz y cuándo una clase abstracta? Como regla general: usa una interfaz cuando quieres garantizar que distintas clases, aunque no tengan relación de herencia entre sí, cumplan con cierto comportamiento (por ejemplo, que tanto un Producto como un Servicio sepan calcularTotal()). Usa una clase abstracta cuando además de ese contrato quieres compartir código común entre las clases hijas.

Es perfectamente válido combinar ambas cosas: una clase puede heredar de una clase abstracta y, al mismo tiempo, implementar una o varias interfaces.

Crea una interfaz Describible con un método describir(), e impleméntala en dos clases distintas que no tengan relación de herencia entre sí.

Lección 11: Traits

Ya vimos que en PHP una clase solo puede heredar de una clase padre. Pero hay situaciones donde quieres compartir el mismo bloque de código entre clases que no tienen ninguna relación jerárquica real, y ahí la herencia se queda corta. Para eso existen los traits: bloques de métodos reutilizables que puedes “mezclar” dentro de cualquier clase con la palabra use.

<?php
trait Saludable {
    public function saludar() {
        return "Hola, soy " . $this->nombre;
    }
}

class Persona {
    use Saludable;
    public $nombre;
}

class Robot {
    use Saludable;
    public $nombre = "R2D2";
}

$persona = new Persona();
$persona->nombre = "Agustín";
echo $persona->saludar(); // Hola, soy Agustín

$robot = new Robot();
echo $robot->saludar(); // Hola, soy R2D2

Persona y Robot no tienen ninguna relación de herencia entre sí, pero ambas ganan el método saludar() gracias al trait. Puedes incluso usar varios traits en una misma clase separándolos con coma dentro del mismo use.

Piensa en los traits como una forma de “copiar y pegar” código de forma controlada y reutilizable, sin los compromisos que trae la herencia real. Son especialmente útiles para funcionalidades transversales, como registrar logs o formatear fechas, que no tienen que ver con la jerarquía de tus clases.

Crea un trait Temporizable con un método marcarFecha() que guarde la fecha actual, y úsalo en dos clases distintas.

Lección 12: Propiedades y métodos estáticos

Todo lo que vimos hasta ahora pertenece a un objeto específico: cada instancia tiene su propia copia de las propiedades. Pero a veces necesitas algo que pertenezca a la clase en sí, compartido entre todos los objetos, sin necesidad de crear una instancia para usarlo. Para eso existe static.

<?php
class ContadorDeAutos {
    public static $total = 0;

    public function __construct() {
        self::$total++;
    }
}

new ContadorDeAutos();
new ContadorDeAutos();
new ContadorDeAutos();

echo ContadorDeAutos::$total; // 3

Nota dos cosas: primero, para acceder a una propiedad estática desde dentro de la clase usamos self::, no $this->. Segundo, para acceder desde fuera usamos NombreDeClase::$propiedad, sin necesidad de crear un objeto.

Los métodos también pueden ser estáticos, típicamente para funciones utilitarias que no dependen del estado de ningún objeto en particular:

<?php
class Matematicas {
    public static function esPar($numero) {
        return $numero % 2 === 0;
    }
}

var_dump(Matematicas::esPar(10)); // true, sin crear ningún objeto

No abuses de static: si usas propiedades estáticas para todo, terminas recreando el mismo problema de las variables globales que la POO justamente busca evitar. Resérvalo para casos donde en verdad tiene sentido que el dato o comportamiento pertenezca a la clase completa y no a una instancia particular.

Agrega a tu clase Libro un contador estático que lleve la cuenta de cuántos libros se han creado.

Lección 13: Constantes de clase

Además de propiedades y métodos, una clase puede tener constantes: valores que nunca cambian, definidos con const, y que igual que las propiedades estáticas, pertenecen a la clase y no a cada objeto individual.

<?php
class Auto {
    const RUEDAS = 4;
}

echo Auto::RUEDAS; // 4

Son útiles para valores que quieres dejar bien documentados dentro de la clase a la que pertenecen conceptualmente, en vez de tenerlos sueltos por ahí como variables globales o números “mágicos” sin contexto dentro del código.

<?php
class Pedido {
    const ESTADO_PENDIENTE = "pendiente";
    const ESTADO_ENVIADO = "enviado";
    const ESTADO_ENTREGADO = "entregado";

    public $estado = self::ESTADO_PENDIENTE;
}

$pedido = new Pedido();
if ($pedido->estado === Pedido::ESTADO_PENDIENTE) {
    echo "Tu pedido todavía no ha sido enviado.";
}

Comparado con simplemente escribir el texto "pendiente" directamente en el código, usar la constante Pedido::ESTADO_PENDIENTE te protege de errores de tipeo y deja mucho más claro, con solo leerlo, de dónde viene ese valor.

Agrega a tu clase Libro una constante que defina el número máximo de préstamos permitidos, y úsala dentro de algún método.

Lección 14: Métodos mágicos

PHP tiene un grupo de métodos especiales, todos empiezan con doble guion bajo (__), que se ejecutan automáticamente en ciertas situaciones sin que tú los llames directamente. Ya conociste uno: __construct. Aquí van otros que vas a encontrar seguido.

__toString se ejecuta cuando intentas usar un objeto como si fuera texto, por ejemplo dentro de un echo:

<?php
class Producto {
    public $nombre;
    public $precio;

    public function __construct($nombre, $precio) {
        $this->nombre = $nombre;
        $this->precio = $precio;
    }

    public function __toString() {
        return "{$this->nombre} (\${$this->precio})";
    }
}

$producto = new Producto("Teclado", 350);
echo $producto; // Teclado ($350), gracias a __toString

__get y __set se activan cuando intentas leer o escribir una propiedad que no existe o no es accesible directamente, y te dan la oportunidad de interceptar esa acción:

<?php
class Configuracion {
    private $datos = [];

    public function __set($nombre, $valor) {
        $this->datos[$nombre] = $valor;
    }

    public function __get($nombre) {
        return $this->datos[$nombre] ?? null;
    }
}

$config = new Configuracion();
$config->tema = "oscuro"; // dispara __set
echo $config->tema;        // dispara __get, imprime "oscuro"

No necesitas memorizar todos los métodos mágicos que existen (hay varios más, como __call o __clone), pero sí vale la pena reconocerlos cuando los veas en código de otras personas, porque a primera vista pueden parecer que “hacen magia” si no sabes que están ahí.

Agrega un método __toString() a tu clase Libro que devuelva una descripción legible combinando título y autor.

Lección 15: Namespaces

A medida que un proyecto crece y empiezas a usar código de terceros (librerías, paquetes de Composer), es cuestión de tiempo para que dos clases distintas terminen con el mismo nombre, por ejemplo dos clases llamadas Usuario que vienen de partes distintas de tu aplicación. Los namespaces existen para resolver justo ese problema, agrupando tus clases bajo un “espacio de nombres” que evita colisiones.

<?php
// archivo: Modelos/Usuario.php
namespace App\Modelos;

class Usuario {
    public $nombre;
}
<?php
// archivo: index.php
require "Modelos/Usuario.php";

use App\Modelos\Usuario;

$usuario = new Usuario();
$usuario->nombre = "Agustín";
echo $usuario->nombre;

El namespace se declara al inicio del archivo con namespace, y para usar una clase que vive en un namespace distinto al actual, la importas con use, indicando la ruta completa. Piensa en los namespaces como carpetas para tus clases: así como no puedes tener dos archivos con el mismo nombre en la misma carpeta, pero sí en carpetas distintas, tampoco puedes tener dos clases con el mismo nombre en el mismo namespace, pero sí en namespaces distintos.

Es común que el namespace refleje la estructura de carpetas de tu proyecto, aunque técnicamente PHP no te obliga a que coincidan, solo es una convención que casi todo el mundo sigue porque hace el código mucho más fácil de navegar.

Organiza tu clase Libro dentro de un namespace App\Modelos, y ajusta el archivo donde la usas para importarla correctamente con use.

Lección 16: Autoloading con Composer

Hasta ahora, cada vez que necesitábamos una clase de otro archivo, tuvimos que escribir manualmente un require. Funciona, pero en un proyecto con decenas o cientos de clases se vuelve poco práctico. Composer, el gestor de dependencias más usado en el mundo PHP, resuelve esto con un sistema llamado autoloading: cargar automáticamente la clase que necesitas, en el momento en que la necesitas, sin que tengas que hacer require a mano.

Para activarlo, tu proyecto necesita un archivo composer.json con la configuración de autoload, típicamente usando el estándar PSR-4:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

Esto le dice a Composer: “todo lo que esté bajo el namespace App, búscalo dentro de la carpeta src/, respetando esa misma estructura de carpetas”. Después de crear o modificar esta configuración, corres composer dump-autoload en la terminal para que Composer genere el archivo que hace la magia.

Una vez configurado, en vez de una fila de require al principio de tu script, solo necesitas esta línea:

<?php
require "vendor/autoload.php";

use App\Modelos\Libro;

$libro = new Libro("Cien años de soledad", "Gabriel García Márquez");

Composer va a encontrar la clase Libro automáticamente, siempre y cuando el namespace y la ubicación del archivo respeten la convención PSR-4 que configuraste. Este mismo mecanismo es el que usan por debajo todos los paquetes que instalas con composer require, así que entenderlo te va a ayudar a moverte con más soltura en cualquier proyecto PHP moderno.

Para cerrar

Con estas lecciones ya tienes las bases sólidas de la Programación Orientada a Objetos en PHP: clases, objetos, encapsulamiento, herencia, polimorfismo, interfaces, traits y cómo organizar todo con namespaces y Composer.

Es un cambio de mentalidad más que de sintaxis, así que si algo no termina de acomodarse en tu cabeza a la primera, es completamente normal, tómate tu tiempo repitiendo los ejercicios con tus propios ejemplos.

El siguiente paso natural es combinar todo esto con una base de datos usando PDO de forma orientada a objetos, y más adelante explorar patrones de diseño, que son básicamente soluciones probadas a problemas comunes que te vas a encontrar una y otra vez conforme tus proyectos crezcan.