20 ejercicios avanzados de PHP resueltos: POO, MySQL, seguridad y MVC

Estos 20 ejercicios avanzados de PHP resueltos están pensados para quienes ya dominan sintaxis, formularios, PDO y bases de datos y quieren practicar conceptos de aplicaciones reales.

Vas a trabajar con POO, seguridad, autenticación, roles, consultas preparadas, MVC y Composer.

1. Crear una clase Producto

<?php
class Producto
{
    public function __construct(
        private string $nombre,
        private float $precio
    ) {
    }

    public function getNombre(): string
    {
        return $this->nombre;
    }

    public function getPrecio(): float
    {
        return $this->precio;
    }
}
?>

2. Encapsular el stock de un producto

public function descontarStock(int $cantidad): void
{
    if ($cantidad <= 0 || $cantidad > $this->stock) {
        throw new InvalidArgumentException('Stock inválido');
    }

    $this->stock -= $cantidad;
}

3. Crear una interfaz de notificaciones

interface Notificable
{
    public function enviar(string $mensaje): void;
}

4. Implementar notificaciones por email

class EmailNotificacion implements Notificable
{
    public function enviar(string $mensaje): void
    {
        echo 'Email: ' . $mensaje;
    }
}

5. Crear un repositorio con PDO

class ProductoRepository
{
    public function __construct(
        private PDO $pdo
    ) {
    }

    public function todos(): array
    {
        return $this->pdo
            ->query('SELECT * FROM productos')
            ->fetchAll(PDO::FETCH_ASSOC);
    }
}

6. Buscar por ID con una consulta preparada

public function buscar(int $id): ?array
{
    $stmt = $this->pdo->prepare(
        'SELECT * FROM productos WHERE id = :id'
    );

    $stmt->execute(['id' => $id]);

    $resultado = $stmt->fetch(PDO::FETCH_ASSOC);

    return $resultado ?: null;
}

7. Implementar una transacción

$pdo->beginTransaction();

try {
    // operaciones relacionadas

    $pdo->commit();
} catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
}

8. Login seguro con password_verify()

if (
    $usuario &&
    password_verify($password, $usuario['password'])
) {
    session_regenerate_id(true);

    $_SESSION['usuario_id'] = $usuario['id'];
}

9. Middleware simple de autenticación

function requireLogin(): void
{
    if (!isset($_SESSION['usuario_id'])) {
        header('Location: /login.php');
        exit;
    }
}

10. Controlar acceso por rol

function requireAdmin(): void
{
    requireLogin();

    if ($_SESSION['rol'] !== 'admin') {
        http_response_code(403);
        exit('Acceso denegado');
    }
}

11. Generar un token CSRF

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

12. Validar el token CSRF

if (
    !isset($_POST['csrf_token']) ||
    !hash_equals(
        $_SESSION['csrf_token'],
        $_POST['csrf_token']
    )
) {
    http_response_code(403);
    exit;
}

13. Crear una capa de servicio

class ProductoService
{
    public function __construct(
        private ProductoRepository $repository
    ) {
    }

    public function listarActivos(): array
    {
        return array_filter(
            $this->repository->todos(),
            fn($producto) => $producto['activo']
        );
    }
}

14. Crear un controlador MVC

class ProductoController
{
    public function __construct(
        private ProductoService $service
    ) {
    }

    public function index(): void
    {
        $productos = $this->service->listarActivos();

        require '../Views/productos/index.php';
    }
}

15. Crear un router simple

$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

switch ($ruta) {
    case '/productos':
        $controller->index();
        break;

    default:
        http_response_code(404);
}

16. Usar namespaces

<?php

namespace App\Services;

class ProductoService
{
}
?>

17. Configurar autoload PSR-4

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

Después:

composer dump-autoload

18. Crear una búsqueda paginada con PDO

$buscar = trim($_GET['buscar'] ?? '');
$pagina = max(1, (int) ($_GET['pagina'] ?? 1));
$limite = 20;
$offset = ($pagina - 1) * $limite;

$stmt = $pdo->prepare(
    'SELECT *
     FROM productos
     WHERE nombre LIKE :buscar
     ORDER BY id DESC
     LIMIT :limite OFFSET :offset'
);

$stmt->bindValue(':buscar', '%' . $buscar . '%');
$stmt->bindValue(':limite', $limite, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();

19. Registrar errores en un archivo

try {
    // operación
} catch (Throwable $e) {
    error_log(
        $e->getMessage() . PHP_EOL,
        3,
        __DIR__ . '/app.log'
    );
}

20. Diseñar un mini sistema MVC completo

Objetivo: construir una aplicación de gestión de productos con:

  • login;
  • roles;
  • CRUD;
  • PDO;
  • consultas preparadas;
  • CSRF;
  • arquitectura MVC;
  • autoload PSR-4;
  • validación;
  • paginación y búsqueda.

Una estructura posible:

app/
├── Controllers/
├── Models/
├── Repositories/
├── Services/
└── Views/

config/
public/
vendor/
composer.json

¿Qué deberías poder hacer después?

Si podés resolver estos desafíos entendiendo cada parte, ya tenés una base muy sólida de PHP moderno y estás en condiciones de comenzar a trabajar con frameworks como Laravel con mucha más claridad.

Serie de ejercicios de PHP

También podés ver todos los contenidos prácticos en la sección Ejercicios resueltos.

¿Te sirvió esta guía? ☕

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Composer en PHP: dependencias, composer.json, vendor y autoloading PSR-4

Después de aprender programación orientada a objetos y MVC, el siguiente paso para trabajar con PHP moderno es conocer Composer, el gestor de dependencias más utilizado en el ecosistema PHP.

Composer permite instalar librerías, administrar versiones y cargar clases automáticamente mediante autoloading.

Además, es una herramienta fundamental para trabajar con frameworks como Laravel.

¿Qué es Composer?

Composer es un gestor de dependencias para PHP.

Permite declarar qué librerías necesita nuestro proyecto y descargarlas automáticamente.

Por ejemplo, en lugar de copiar manualmente una librería dentro del proyecto, podemos instalarla con:

composer require monolog/monolog

Composer se encargará de descargar la librería y sus dependencias.

¿Qué problema resuelve?

Sin Composer, un proyecto podría requerir:

  • descargar librerías manualmente;
  • copiar archivos;
  • resolver dependencias;
  • verificar versiones compatibles;
  • hacer múltiples require.

Composer automatiza gran parte de ese trabajo.

Comprobar si Composer está instalado

Desde una terminal podemos ejecutar:

composer --version

Si está instalado correctamente, veremos la versión disponible.

Crear un proyecto con Composer

Dentro de una carpeta podemos ejecutar:

composer init

Composer nos hará algunas preguntas y generará un archivo llamado:

composer.json

¿Qué es composer.json?

composer.json contiene la configuración del proyecto y sus dependencias.

Un ejemplo mínimo:

{
    "name": "clubprogramador/ejemplo",
    "require": {
        "php": "^8.2"
    }
}

Instalar una dependencia

Podemos instalar una librería con:

composer require monolog/monolog

Composer modificará composer.json y descargará los paquetes necesarios.

La carpeta vendor

Composer almacena las dependencias dentro de:

vendor/

Por ejemplo:

proyecto/
│
├── app/
├── public/
├── vendor/
├── composer.json
└── composer.lock

¿Qué es composer.lock?

composer.lock registra las versiones exactas de las dependencias instaladas.

Esto permite que distintas personas o servidores instalen exactamente las mismas versiones.

Por eso, en proyectos de aplicaciones, normalmente composer.lock se incluye en el control de versiones.

composer install vs composer update

composer install

composer install

Instala las versiones indicadas en composer.lock.

composer update

composer update

Busca versiones nuevas compatibles con las reglas de composer.json y actualiza composer.lock.

Por eso no conviene ejecutar composer update indiscriminadamente en producción.

Autoloading

Composer genera automáticamente un archivo:

vendor/autoload.php

Podemos cargarlo una sola vez:

<?php

require __DIR__ . '/vendor/autoload.php';

?>

A partir de ahí, Composer puede cargar clases automáticamente.

El problema de múltiples require

Sin autoloading podríamos terminar con algo así:

require 'app/Models/Producto.php';
require 'app/Models/Usuario.php';
require 'app/Services/MailService.php';
require 'app/Controllers/ProductoController.php';

A medida que el proyecto crece, esto se vuelve difícil de mantener.

PSR-4

PSR-4 es un estándar utilizado para asociar namespaces con carpetas.

Podemos configurar Composer así:

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

Esto significa que las clases cuyo namespace comienza con App\ se buscarán dentro de la carpeta app/.

Ejemplo con namespace

Archivo:

app/Models/Producto.php

Contenido:

<?php

namespace App\Models;

class Producto
{
    public function __construct(
        public string $nombre
    ) {
    }
}

?>

Regenerar el autoload

Después de modificar la configuración de autoload podemos ejecutar:

composer dump-autoload

Usar la clase

Ahora podemos escribir:

<?php

require __DIR__ . '/vendor/autoload.php';

use App\Models\Producto;

$producto = new Producto(
    'Teclado'
);

echo $producto->nombre;

?>

No necesitamos hacer un require específico para Producto.php.

Estructura de proyecto con PSR-4

proyecto/
│
├── app/
│   ├── Controllers/
│   │   └── ProductoController.php
│   │
│   ├── Models/
│   │   └── Producto.php
│   │
│   └── Services/
│       └── ProductoService.php
│
├── public/
│   └── index.php
│
├── vendor/
├── composer.json
└── composer.lock

Namespace para un controlador

<?php

namespace App\Controllers;

use App\Models\Producto;

class ProductoController
{
    public function index(): void
    {
        // lógica del controlador
    }
}

?>

Instalar paquetes de desarrollo

Algunas dependencias solo se necesitan durante el desarrollo.

Podemos instalarlas con:

composer require --dev paquete/nombre

Estas dependencias se guardan dentro de:

require-dev

Versiones en Composer

Composer permite indicar restricciones de versiones.

Por ejemplo:

"monolog/monolog": "^3.0"

El operador ^ permite instalar actualizaciones compatibles dentro de la misma versión mayor, según las reglas de versionado semántico aplicables al paquete.

Packagist

Packagist es el repositorio público principal de paquetes utilizados por Composer.

Cuando ejecutamos:

composer require proveedor/paquete

Composer normalmente busca ese paquete en Packagist.

No subir vendor al repositorio

En muchos proyectos PHP se agrega:

/vendor/

al archivo .gitignore.

Las dependencias pueden reconstruirse ejecutando:

composer install

Lo importante es conservar:

  • composer.json;
  • composer.lock.

Ejemplo de .gitignore

/vendor/
.env

También es habitual excluir archivos con configuraciones sensibles como .env.

Composer y MVC

Podemos mejorar el proyecto MVC de la guía anterior eliminando los require manuales.

Antes:

require 'Producto.php';
require 'ProductoController.php';

Después:

require __DIR__ . '/../vendor/autoload.php';

use App\Models\Producto;
use App\Controllers\ProductoController;

Composer no es solamente para frameworks

Aunque Laravel utiliza Composer intensivamente, también es muy útil en proyectos PHP propios.

Podemos utilizar paquetes para:

  • enviar emails;
  • generar PDFs;
  • trabajar con archivos Excel;
  • manejar logs;
  • consumir APIs;
  • crear tests;
  • procesar fechas;
  • trabajar con variables de entorno.

Buenas prácticas

  • versionar composer.json y composer.lock;
  • no modificar manualmente archivos dentro de vendor;
  • utilizar autoload PSR-4;
  • mantener namespaces consistentes con la estructura de carpetas;
  • revisar las dependencias antes de instalarlas;
  • actualizar paquetes de manera controlada;
  • no ejecutar composer update en producción sin comprender el impacto.

Errores comunes

  • olvidar ejecutar composer install después de clonar un proyecto;
  • eliminar composer.lock sin motivo;
  • editar archivos dentro de vendor;
  • configurar mal el namespace PSR-4;
  • olvidar ejecutar composer dump-autoload después de ciertos cambios;
  • confundir composer install con composer update;
  • subir credenciales o secretos al repositorio.

Ejercicio práctico

Tomá el proyecto MVC de la guía anterior y adaptalo para utilizar Composer.

El objetivo es:

  1. crear un archivo composer.json;
  2. configurar App\ con PSR-4;
  3. agregar namespaces a Modelos y Controladores;
  4. eliminar los require individuales;
  5. cargar solamente vendor/autoload.php;
  6. ejecutar composer dump-autoload;
  7. comprobar que el CRUD siga funcionando.

¿Qué sigue?

Con POO, MVC y Composer ya tenemos gran parte de la base conceptual necesaria para comprender un framework PHP moderno.

La próxima guía puede ser una introducción a Laravel desde cero:

  • qué es Laravel;
  • cómo instalarlo;
  • estructura del proyecto;
  • rutas;
  • controladores;
  • Blade;
  • modelos;
  • migraciones.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

MVC en PHP desde cero: Modelo, Vista y Controlador con un ejemplo completo

Hasta ahora construimos formularios, consultas con PDO, CRUD, autenticación, permisos, seguridad y programación orientada a objetos. El siguiente paso es aprender a organizar mejor una aplicación PHP.

Una de las arquitecturas más conocidas para separar responsabilidades es MVC: Modelo, Vista y Controlador.

En esta guía vamos a entender qué hace cada parte y construiremos un ejemplo sencillo con productos y MySQL.

¿Qué problema resuelve MVC?

En proyectos pequeños es común comenzar con archivos que mezclan:

  • HTML;
  • consultas SQL;
  • validaciones;
  • lógica de negocio;
  • redirecciones;
  • procesamiento de formularios.

Por ejemplo, un único archivo podría consultar productos, procesar un POST, validar datos y generar todo el HTML.

Eso puede funcionar al principio, pero a medida que la aplicación crece se vuelve difícil de mantener.

MVC intenta separar esas responsabilidades.

¿Qué significa MVC?

MVC significa:

  • Model: Modelo;
  • View: Vista;
  • Controller: Controlador.

Modelo

El Modelo se ocupa de trabajar con los datos y con la lógica relacionada con ellos.

Por ejemplo:

  • consultar productos;
  • buscar un producto por ID;
  • crear registros;
  • actualizarlos;
  • eliminarlos.

Vista

La Vista se ocupa de presentar la información al usuario.

Normalmente contiene:

  • HTML;
  • tablas;
  • formularios;
  • mensajes;
  • pequeñas expresiones PHP para mostrar datos.

La vista no debería contener consultas SQL.

Controlador

El Controlador coordina la aplicación.

Por ejemplo:

  1. recibe una petición;
  2. solicita información al modelo;
  3. procesa la lógica necesaria;
  4. envía datos a la vista.

Flujo básico de MVC

Usuario
   ↓
Controlador
   ↓
Modelo
   ↓
Base de datos
   ↓
Controlador
   ↓
Vista
   ↓
Usuario

No todas las aplicaciones MVC implementan exactamente el mismo flujo, pero esta idea ayuda a entender la separación de responsabilidades.

Estructura de carpetas

Podemos comenzar con una estructura simple:

proyecto/
│
├── app/
│   ├── Controllers/
│   │   └── ProductoController.php
│   │
│   ├── Models/
│   │   └── Producto.php
│   │
│   └── Views/
│       └── productos/
│           ├── index.php
│           └── create.php
│
├── config/
│   └── database.php
│
└── public/
    └── index.php

Base de datos de ejemplo

Vamos a utilizar una tabla productos:

CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0
);

Conexión a MySQL

En config/database.php podemos centralizar la conexión:

<?php

$host = 'localhost';
$db = 'tienda';
$user = 'root';
$password = '';

$dsn = "mysql:host=$host;dbname=$db;charset=utf8mb4";

$pdo = new PDO($dsn, $user, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);

?>

Crear el Modelo Producto

El modelo será responsable de consultar la tabla.

<?php

class Producto
{
    public function __construct(
        private PDO $pdo
    ) {
    }

    public function todos(): array
    {
        $stmt = $this->pdo->query(
            'SELECT id, nombre, precio, stock
             FROM productos
             ORDER BY id DESC'
        );

        return $stmt->fetchAll();
    }
}

?>

El modelo conoce PDO y la base de datos. La vista no necesita saber cómo se obtuvieron los productos.

Crear el Controlador

En app/Controllers/ProductoController.php:

<?php

class ProductoController
{
    public function __construct(
        private Producto $producto
    ) {
    }

    public function index(): void
    {
        $productos = $this->producto->todos();

        require __DIR__
            . '/../Views/productos/index.php';
    }
}

?>

El controlador solicita los datos al modelo y carga la vista.

Crear la Vista

En app/Views/productos/index.php:

<h1>Productos</h1>

<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>Nombre</th>
            <th>Precio</th>
            <th>Stock</th>
        </tr>
    </thead>

    <tbody>

    <?php foreach ($productos as $producto): ?>

        <tr>
            <td>
                <?= $producto['id'] ?>
            </td>

            <td>
                <?= htmlspecialchars(
                    $producto['nombre'],
                    ENT_QUOTES,
                    'UTF-8'
                ) ?>
            </td>

            <td>
                $<?= number_format(
                    $producto['precio'],
                    2,
                    ',',
                    '.'
                ) ?>
            </td>

            <td>
                <?= $producto['stock'] ?>
            </td>
        </tr>

    <?php endforeach; ?>

    </tbody>
</table>

Crear un punto de entrada

Podemos utilizar public/index.php como punto de entrada:

<?php

require __DIR__ . '/../config/database.php';
require __DIR__ . '/../app/Models/Producto.php';
require __DIR__ . '/../app/Controllers/ProductoController.php';

$modelo = new Producto($pdo);

$controller = new ProductoController($modelo);

$controller->index();

?>

¿Qué logramos?

Ahora cada responsabilidad está separada:

  • Producto.php trabaja con los datos;
  • ProductoController.php coordina la petición;
  • index.php muestra el HTML.

Agregar un formulario de creación

Podemos agregar un método al controlador:

public function create(): void
{
    require __DIR__
        . '/../Views/productos/create.php';
}

Vista para crear un producto

<h1>Nuevo producto</h1>

<form method="POST" action="?accion=store">

    <label>Nombre</label>
    <input type="text" name="nombre" required>

    <label>Precio</label>
    <input type="number" step="0.01" name="precio" required>

    <label>Stock</label>
    <input type="number" name="stock" required>

    <button type="submit">
        Guardar
    </button>

</form>

Agregar el método crear() al Modelo

public function crear(
    string $nombre,
    float $precio,
    int $stock
): bool {
    $sql = 'INSERT INTO productos
            (nombre, precio, stock)
            VALUES (:nombre, :precio, :stock)';

    $stmt = $this->pdo->prepare($sql);

    return $stmt->execute([
        'nombre' => $nombre,
        'precio' => $precio,
        'stock' => $stock
    ]);
}

Agregar store() al Controlador

public function store(): void
{
    $nombre = trim($_POST['nombre'] ?? '');
    $precio = filter_input(
        INPUT_POST,
        'precio',
        FILTER_VALIDATE_FLOAT
    );

    $stock = filter_input(
        INPUT_POST,
        'stock',
        FILTER_VALIDATE_INT
    );

    if (
        $nombre === '' ||
        $precio === false ||
        $stock === false
    ) {
        exit('Datos inválidos');
    }

    $this->producto->crear(
        $nombre,
        $precio,
        $stock
    );

    header('Location: index.php');
    exit;
}

Agregar un router simple

Podemos decidir qué método ejecutar mediante un parámetro:

$accion = $_GET['accion'] ?? 'index';

switch ($accion) {

    case 'create':
        $controller->create();
        break;

    case 'store':
        $controller->store();
        break;

    default:
        $controller->index();
}

Esto todavía es un router muy básico, pero sirve para entender la idea.

Modelo no significa necesariamente tabla

Es común pensar que cada modelo debe representar una tabla exactamente, pero el concepto es más amplio.

El modelo representa los datos y reglas de negocio de la aplicación.

En proyectos simples puede coincidir bastante con una tabla, pero en sistemas mayores puede incluir lógica más compleja.

La Vista no debería consultar la base

Evitemos algo como esto dentro de una vista:

// No recomendado dentro de una vista

$stmt = $pdo->query(
    'SELECT * FROM productos'
);

La vista debería recibir los datos ya preparados.

El Controlador tampoco debería hacer todo

Otro error frecuente es mover todo el código del archivo original hacia el controlador.

Un controlador gigantesco sigue siendo difícil de mantener.

La idea es que coordine responsabilidades, no que concentre toda la aplicación.

Separar la lógica de negocio

A medida que el proyecto crece, podemos tener otras capas o servicios.

Por ejemplo:

app/
├── Controllers/
├── Models/
├── Services/
└── Views/

Un servicio podría encargarse de reglas específicas que no pertenecen directamente a una vista o a una consulta SQL.

MVC y seguridad

La arquitectura no reemplaza las buenas prácticas vistas anteriormente.

Seguimos necesitando:

  • consultas preparadas;
  • validación de datos;
  • protección CSRF;
  • escape de salida;
  • autenticación;
  • autorización.

MVC no es una regla rígida

Existen diferentes formas de implementar MVC.

Dos frameworks pueden utilizar el patrón y tener estructuras distintas.

Lo importante para comenzar es comprender la separación de responsabilidades.

¿Cómo se relaciona esto con Laravel?

Laravel utiliza muchos de estos conceptos.

Por ejemplo:

  • los Models representan y trabajan con datos;
  • los Controllers reciben peticiones y coordinan acciones;
  • las vistas se construyen normalmente con Blade;
  • las rutas determinan qué controlador debe ejecutarse.

Laravel agrega muchas herramientas alrededor de esta arquitectura, pero entender MVC primero hace que sea mucho más fácil comprender el framework.

Errores comunes al aprender MVC

  • poner consultas SQL dentro de las vistas;
  • crear controladores que hacen absolutamente todo;
  • mezclar HTML dentro de los modelos;
  • crear una clase por cada pequeño fragmento de código sin necesidad;
  • pensar que MVC obliga a usar una estructura exacta;
  • creer que usar carpetas llamadas Models, Views y Controllers ya convierte automáticamente un proyecto en MVC.

Ejercicio práctico

Extendé el ejemplo de productos para implementar un CRUD completo utilizando MVC.

Debería incluir:

  1. listar productos;
  2. crear productos;
  3. editar productos;
  4. eliminar productos;
  5. validar formularios;
  6. usar consultas preparadas;
  7. proteger la eliminación mediante POST;
  8. separar correctamente Modelo, Vista y Controlador.

¿Qué sigue?

En la próxima guía vamos a aprender Composer y autoloading en PHP.

Veremos:

  • qué es Composer;
  • qué es composer.json;
  • cómo instalar paquetes;
  • qué es vendor;
  • autoloading;
  • PSR-4;
  • cómo dejar de utilizar múltiples require manuales.

Ese paso nos acercará todavía más al funcionamiento interno de proyectos PHP modernos y frameworks como Laravel.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal

Programación orientada a objetos en PHP: clases, objetos, herencia e interfaces

Hasta ahora trabajamos con PHP utilizando variables, funciones, formularios, PDO, sesiones, autenticación y seguridad. El siguiente paso es aprender programación orientada a objetos, una forma de organizar el código en clases y objetos.

Este enfoque es fundamental para trabajar en proyectos medianos o grandes y es la base de muchos frameworks modernos, incluido Laravel.

¿Qué es la programación orientada a objetos?

La programación orientada a objetos, también llamada POO u OOP, organiza el código alrededor de objetos que representan entidades del sistema.

Por ejemplo, en una aplicación podemos tener objetos como:

  • Usuario;
  • Producto;
  • Pedido;
  • Curso;
  • Factura.

Cada objeto puede tener datos y comportamientos propios.

¿Qué es una clase?

Una clase funciona como una plantilla para crear objetos.

<?php

class Usuario
{
}

?>

Crear un objeto

Para crear una instancia utilizamos new:

$usuario = new Usuario();

$usuario es ahora un objeto de la clase Usuario.

Propiedades

Las propiedades representan los datos del objeto.

class Usuario
{
    public string $nombre;
    public string $email;
}

Podemos asignar valores:

$usuario = new Usuario();

$usuario->nombre = 'Ana';
$usuario->email = 'ana@email.com';

Métodos

Los métodos representan comportamientos del objeto.

class Usuario
{
    public string $nombre;

    public function saludar(): string
    {
        return 'Hola, soy ' . $this->nombre;
    }
}

Para llamarlo:

$usuario = new Usuario();
$usuario->nombre = 'Ana';

echo $usuario->saludar();

¿Qué significa $this?

$this hace referencia al objeto actual.

Por ejemplo:

$this->nombre

significa “la propiedad nombre de este objeto”.

El constructor

El constructor se ejecuta automáticamente al crear un objeto.

class Usuario
{
    public string $nombre;
    public string $email;

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

Ahora podemos crear el objeto directamente con sus datos:

$usuario = new Usuario(
    'Ana',
    'ana@email.com'
);

Promoción de propiedades en el constructor

En versiones modernas de PHP podemos simplificar el código:

class Usuario
{
    public function __construct(
        public string $nombre,
        public string $email
    ) {
    }
}

Visibilidad: public, protected y private

Las propiedades y métodos pueden tener distintos niveles de acceso.

public

Puede utilizarse desde cualquier parte:

public string $nombre;

private

Solo puede utilizarse dentro de la misma clase:

private string $password;

protected

Puede utilizarse dentro de la clase y sus clases hijas:

protected string $rol;

Encapsulamiento

El encapsulamiento consiste en proteger el estado interno de un objeto y controlar cómo se accede a él.

class Cuenta
{
    private float $saldo = 0;

    public function depositar(float $importe): void
    {
        if ($importe <= 0) {
            return;
        }

        $this->saldo += $importe;
    }

    public function obtenerSaldo(): float
    {
        return $this->saldo;
    }
}

El saldo no puede modificarse directamente desde afuera.

Getters y setters

Los getters permiten leer valores y los setters modificarlos de forma controlada.

class Producto
{
    private float $precio;

    public function setPrecio(float $precio): void
    {
        if ($precio < 0) {
            throw new InvalidArgumentException(
                'El precio no puede ser negativo'
            );
        }

        $this->precio = $precio;
    }

    public function getPrecio(): float
    {
        return $this->precio;
    }
}

Herencia

Una clase puede heredar propiedades y métodos de otra utilizando extends.

class Usuario
{
    public function __construct(
        protected string $nombre
    ) {
    }

    public function getNombre(): string
    {
        return $this->nombre;
    }
}

class Administrador extends Usuario
{
    public function eliminarUsuario(): string
    {
        return 'Usuario eliminado';
    }
}

Sobrescribir métodos

Una clase hija puede redefinir un método de la clase padre.

class Usuario
{
    public function descripcion(): string
    {
        return 'Usuario del sistema';
    }
}

class Administrador extends Usuario
{
    public function descripcion(): string
    {
        return 'Administrador del sistema';
    }
}

parent::

Podemos llamar a un método de la clase padre utilizando parent::.

class Administrador extends Usuario
{
    public function descripcion(): string
    {
        return parent::descripcion()
            . ' con permisos administrativos';
    }
}

Clases abstractas

Una clase abstracta sirve como base para otras clases y no puede instanciarse directamente.

abstract class Empleado
{
    public function __construct(
        protected string $nombre
    ) {
    }

    abstract public function calcularSueldo(): float;
}

Una clase hija debe implementar el método abstracto:

class EmpleadoMensual extends Empleado
{
    public function __construct(
        string $nombre,
        private float $sueldo
    ) {
        parent::__construct($nombre);
    }

    public function calcularSueldo(): float
    {
        return $this->sueldo;
    }
}

Interfaces

Una interfaz define qué métodos debe implementar una clase.

interface Notificable
{
    public function enviar(string $mensaje): void;
}

Después podemos crear distintas implementaciones:

class NotificacionEmail implements Notificable
{
    public function enviar(string $mensaje): void
    {
        echo 'Enviando email: ' . $mensaje;
    }
}

Polimorfismo

Gracias a una interfaz podemos trabajar con diferentes implementaciones utilizando el mismo contrato.

function notificar(
    Notificable $servicio,
    string $mensaje
): void {
    $servicio->enviar($mensaje);
}

La función no necesita saber cómo se envía realmente la notificación.

Propiedades y métodos static

Un miembro static pertenece a la clase y no a una instancia concreta.

class Conversor
{
    public static function kilometrosAMillas(float $km): float
    {
        return $km * 0.621371;
    }
}

Se utiliza así:

$millas = Conversor::kilometrosAMillas(10);

Constantes de clase

class Pedido
{
    public const ESTADO_PENDIENTE = 'pendiente';
    public const ESTADO_PAGADO = 'pagado';
}

Podemos acceder mediante:

Pedido::ESTADO_PAGADO

Namespaces

Cuando un proyecto crece, los namespaces ayudan a organizar clases y evitar conflictos de nombres.

<?php

namespace App\Models;

class Usuario
{
}

?>

Luego podemos importar la clase:

use App\Models\Usuario;

$usuario = new Usuario();

Ejemplo práctico: clase Producto

class Producto
{
    public function __construct(
        private int $id,
        private string $nombre,
        private float $precio,
        private int $stock
    ) {
    }

    public function getNombre(): string
    {
        return $this->nombre;
    }

    public function getPrecio(): float
    {
        return $this->precio;
    }

    public function tieneStock(): bool
    {
        return $this->stock > 0;
    }

    public function descontarStock(int $cantidad): void
    {
        if ($cantidad <= 0 || $cantidad > $this->stock) {
            throw new InvalidArgumentException(
                'Cantidad inválida'
            );
        }

        $this->stock -= $cantidad;
    }
}

Ventajas de la programación orientada a objetos

  • organiza mejor proyectos grandes;
  • permite reutilizar código;
  • facilita separar responsabilidades;
  • mejora el mantenimiento;
  • permite crear componentes reemplazables;
  • facilita las pruebas automatizadas;
  • es la base conceptual de frameworks como Laravel.

No todo necesita ser una clase

La POO es una herramienta, no una obligación.

Crear clases sin una responsabilidad clara puede hacer que el código sea más complejo en lugar de mejorarlo.

Una buena clase debería representar una responsabilidad concreta y mantener relacionados sus datos y comportamientos.

Errores comunes

  • hacer todas las propiedades públicas;
  • crear clases que solamente agrupan funciones sin una responsabilidad clara;
  • abusar de la herencia;
  • crear jerarquías demasiado complejas;
  • utilizar métodos static para todo;
  • confundir una clase con un objeto;
  • no utilizar tipos de datos cuando ayudan a detectar errores;
  • crear clases gigantes que hacen demasiadas cosas.

Ejercicio práctico

Creá un pequeño sistema de biblioteca utilizando POO.

Definí:

  • una clase Libro con título, autor y disponibilidad;
  • una clase Usuario;
  • un método para prestar un libro;
  • un método para devolverlo;
  • encapsulamiento para impedir cambios inválidos;
  • una interfaz Notificable para informar cuando se realiza un préstamo.

¿Qué sigue?

En la próxima guía vamos a organizar un proyecto PHP utilizando una arquitectura más clara: MVC (Modelo, Vista y Controlador).

Veremos cómo separar:

  • acceso a datos;
  • lógica de negocio;
  • controladores;
  • vistas;
  • rutas.

Esto nos va a acercar mucho más a la estructura que utilizan frameworks como Laravel.

¿Te sirvió esta guía? ☕

Si este contenido te ayudó y querés apoyar a Club Programador para seguir publicando ejercicios, proyectos y guías gratuitas, podés colaborar mediante:

☕ Apoyar con Mercado Pago

🌎 Apoyar con PayPal