# Ejemplo 2: Web con apache+php+mysql

Vamos a ver un **ejemplo completo y funcional** de un entorno con:

* **Apache + PHP** (para servir HTML/PHP)
* **MySQL** (para consultas)
* **phpMyAdmin** (opcional, para administrar la base de datos)
* Volúmenes (para que podamos subir el código y se sincronice automáticamente)

Todo en un solo `docker-compose.yaml`&#x20;

***

## ✅ 1. Preparación entorno y  `docker-compose.yaml`

Empezamos por estructurar el proyecto, podemos usar VSCode para ello:

```
apache-test/
 ├── docker-compose.yaml
 └── www/
        ├── index.php        → Listado de clientes
        ├── style.css        → Estilos de CSS
        ├── add.php          → Formulario + alta de cliente
        ├── delete.php       → Eliminación de cliente
        └── db.php           → Conexión a la base de datos

```

Ten en cuenta que en el `docker-compose.yaml` crearemos un volumen que será la web e irá montado junto con la carpeta `www/`:

```yaml
volumes:
  - ./www:/var/www/html
```

* `./www` → carpeta en tu PC
* `/var/www/html` → carpeta dentro del contenedor Apache

**Todo lo que modifiques en `www/` aparece inmediatamente en Docker.**

Edita el archivo docker-compose.yaml:

```yaml
version: '3.8'

services:

  web:
    image: php:8.2-apache
    container_name: web
    volumes:
      - ./www:/var/www/html
    ports:
      - "8080:80"
    depends_on:
      - db
    environment:
      - MYSQL_HOST=db
      - MYSQL_USER=root
      - MYSQL_PASSWORD=secret
      - MYSQL_DATABASE=demo
    # Para que PHP pueda conectarse a MySQL
    command: >
      bash -c "docker-php-ext-install mysqli &&
               apache2-foreground"

  db:
    image: mysql:8.0
    container_name: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: demo
    ports:
      - "3306:3306"
    volumes:
      - dbdata:/var/lib/mysql

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: phpmyadmin
    depends_on:
      - db
    ports:
      - "8081:80"
    environment:
      PMA_HOST=db
      MYSQL_ROOT_PASSWORD=secret

volumes:
  dbdata:
```

***

## 📝 2. Archivos de HTML+PHP

El proyecto es para testear el CRUD de los dockers, el proyecto lo tienes a continuación, sino lo puedes extraer del siguiente github, por si lo quieres descargar más fácilmente:

```bash
git clone https://github.com/aviladotgibert/DecorationClients-webtest.git
```

<details>

<summary>Sino tienes aquí los archivos para copiar:</summary>

Vamos primero con nuestra conexión de base de datos:

{% code title="db.php" %}

```php
<?php
$host = "db";
$user = "root";
$pass = "secret";
$name = "demo";

$conn = new mysqli($host, $user, $pass, $name);

if ($conn->connect_error) {
    die("Error de conexión: " . $conn->connect_error);
}
?>
```

{% endcode %}

Vamos con el `index.php` que será nuestra web de testing:

{% code title="index.php" %}

```php
<?php include 'db.php'; ?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Clientes — DecoNatural</title>
    <style>
        <?php include 'style.css'; ?>
    </style>
</head>
<body>

<div class="container">

<h1>DecoNatural · Clientes</h1>
<p style="text-align:center;">
    <a class="button" href="add.php">➕ Añadir cliente</a>
</p>

<?php
$conn->query("CREATE TABLE IF NOT EXISTS clientes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100),
    email VARCHAR(120)
)");

$result = $conn->query("SELECT * FROM clientes ORDER BY id DESC");

if ($result->num_rows > 0): ?>
    <table>
        <tr>
            <th>Nombre</th>
            <th>Email</th>
            <th>Acción</th>
        </tr>

        <?php while ($row = $result->fetch_assoc()): ?>
            <tr>
                <td><?= htmlspecialchars($row['nombre']) ?></td>
                <td><?= htmlspecialchars($row['email']) ?></td>
                <td>
                    <a class="delete" href="delete.php?id=<?= $row['id'] ?>"
                       onclick="return confirm('¿Eliminar este cliente?')">Eliminar</a>
                </td>
            </tr>
        <?php endwhile; ?>
    </table>
<?php else: ?>
    <p style="text-align:center; color:#4a5a3c;">No hay clientes registrados aún.</p>
<?php endif; ?>

</div>

</body>
</html>

```

{% endcode %}

La hoja de estilos, `style.css` es la siguiente:

{% code title="style.css" %}

```css
body {
    font-family: 'Segoe UI', sans-serif;
    margin: 0;
    background: #f2efe7 url('https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=1500&q=60') no-repeat center/cover fixed;
    backdrop-filter: blur(2px);
}

.container {
    max-width: 700px;
    margin: 60px auto;
    background: rgba(255,255,255,0.88);
    padding: 25px;
    border-radius: 12px;
    box-shadow: 0 8px 25px rgba(0,0,0,0.15);
    border: 1px solid #dcd7c9;
}

h1, h2 {
    text-align: center;
    color: #4a5a3c;
    letter-spacing: 1px;
}

.button {
    background: #6f8f53;
    color: white;
    padding: 8px 14px;
    text-decoration: none;
    border-radius: 6px;
    font-weight: bold;
}

.button:hover {
    background: #516c3a;
}

table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 15px;
}

th {
    background: #6f8f53;
    color: white;
    padding: 10px;
    border-radius: 4px 4px 0 0;
}

td {
    padding: 10px;
    border-bottom: 1px solid #ccc;
}

input {
    width: 95%;
    padding: 10px;
    border-radius: 6px;
    border: 1px solid #bbb;
    margin-bottom: 12px;
}

button {
    padding: 10px 14px;
    background: #6f8f53;
    color: white;
    border: none;
    border-radius: 6px;
    font-size: 15px;
}

button:hover {
    background: #516c3a;
}

.delete {
    color: #b52626;
    font-weight: bold;
    text-decoration: none;
}

.delete:hover {
    text-decoration: underline;
}

```

{% endcode %}

Ahora vamos con `add.php` que es nuestra&#x20;

{% code title="add.php" %}

```php
<?php
include 'db.php';

$msg = "";

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nombre = $conn->real_escape_string($_POST['nombre']);
    $email  = $conn->real_escape_string($_POST['email']);

    if (!empty($nombre)) {
        $conn->query("INSERT INTO clientes (nombre, email) VALUES ('$nombre', '$email')");
        header("Location: index.php");
        exit();
    } else {
        $msg = "El nombre es obligatorio.";
    }
}
?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Añadir Cliente — DecoNatural</title>
    <style>
        <?php include 'style.css'; ?>
    </style>
</head>
<body>

<div class="container">

<h1>+ Añadir Cliente</h1>

<?php if ($msg): ?>
    <p style="color:red; text-align:center;"><?= $msg ?></p>
<?php endif; ?>

<form method="POST">
    <label>Nombre</label><br>
    <input type="text" name="nombre" required><br>

    <label>Email</label><br>
    <input type="email" name="email"><br>

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

<p style="text-align:center;">
    <a class="button" href="index.php">⬅ Volver</a>
</p>

</div>
</body>
</html>

```

{% endcode %}

Y por último terminamos con el `delete.php`

{% code title="delete.php" %}

```php
<?php
include 'db.php';

if (isset($_GET['id'])) {
    $id = intval($_GET['id']);
    $conn->query("DELETE FROM clientes WHERE id = $id");
}

header("Location: index.php");
exit();
```

{% endcode %}

</details>

***

## ▶ 3. Levantar contenedores y acceder:

Abrimos terminal en la carpeta donde está el compose y ejecutamos:

```bash
docker-compose up -d
```

Para acceder:

| Servicio      | URL                                             |
| ------------- | ----------------------------------------------- |
| Web con PHP   | [http://localhost:8080](http://localhost:8080/) |
| Test de MySQL | <http://localhost:8080/test.php>                |
| phpMyAdmin    | [http://localhost:8081](http://localhost:8081/) |
