Resumen: en este post te mostraré cómo consumir una base de datos de MySQL con Angular, usando PHP como lenguaje de servidor y AJAX (con el módulo HttpClient de Angular) para enviar los datos.

Para los estilos vamos a usar Angular Material. Puedes ver el código en GitHub y una demostración con su explicación en el siguiente vídeo:

CRUD de Angular con PHP y MySQL

Si eres nuevo en esto, dejo unos enlaces de interés. Vamos a usar PHP así que es necesario configurar CORS. Para la base de datos vamos a usar MySQL, si quieres puedes comenzar aprendiendo PHP y MySQL.

Después vamos a crear la app de Angular, todos los componentes son generados con la CLI. Más tarde se agrega Angular Material, se crea un servicio que usa el módulo HttpClient, un momento más tarde se configura el router y en algunos componentes utilizamos formularios.

No te preocupes, iremos poco a poco.

Base de datos

La base de datos puede llamarse como quieras, solo recuerda cambiar el nombre en el archivo bd.php listado abajo.

El esquema de la única tabla es el siguiente, puedes pegarlo directamente en la consola o usando phpmyadmin:

CREATE TABLE `mascotas` (
  `id` bigint(20) UNSIGNED NOT NULL primary key AUTO_INCREMENT,
  `nombre` varchar(255) NOT NULL,
  `raza` varchar(255) NOT NULL,
  `edad` tinyint(4) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Lado del servidor

El lado del servidor es sencillo y simple. Vamos a dividir cada operación en un archivo e incluir el de la conexión a la base de datos en cada uno de ellos.

Dentro de cada archivo se ejecuta la sentencia SQL para el insert, update, delete y select.

Quedan así:

<?php
$contraseña = "";
$usuario = "root";
$nombre_base_de_datos = "mascotas";
try {
    return new PDO('mysql:host=localhost;dbname=' . $nombre_base_de_datos, $usuario, $contraseña);
} catch (Exception $e) {
    echo "Ocurrió algo con la base de datos: " . $e->getMessage();
}
<?php
header("Access-Control-Allow-Origin: http://localhost:4200");
header("Access-Control-Allow-Methods: DELETE");
$metodo = $_SERVER["REQUEST_METHOD"];
if ($metodo != "DELETE" && $metodo != "OPTIONS") {
    exit("Solo se permite método DELETE");
}

if (empty($_GET["idMascota"])) {
    exit("No hay id de mascota para eliminar");
}
$idMascota = $_GET["idMascota"];
$bd = include_once "bd.php";
$sentencia = $bd->prepare("DELETE FROM mascotas WHERE id = ?");
$resultado = $sentencia->execute([$idMascota]);
echo json_encode($resultado);
<?php
header("Access-Control-Allow-Origin: http://localhost:4200");
if (empty($_GET["idMascota"])) {
    exit("No hay id de mascota");
}
$idMascota = $_GET["idMascota"];
$bd = include_once "bd.php";
$sentencia = $bd->prepare("select id, nombre, raza, edad from mascotas where id = ?");
$sentencia->execute([$idMascota]);
$mascota = $sentencia->fetchObject();
echo json_encode($mascota);
<?php
header("Access-Control-Allow-Origin: http://localhost:4200");
$bd = include_once "bd.php";
$sentencia = $bd->query("select id, nombre, raza, edad from mascotas");
$mascotas = $sentencia->fetchAll(PDO::FETCH_OBJ);
echo json_encode($mascotas);
<?php
header("Access-Control-Allow-Origin: http://localhost:4200");
header("Access-Control-Allow-Headers: *");
$jsonMascota = json_decode(file_get_contents("php://input"));
if (!$jsonMascota) {
    exit("No hay datos");
}
$bd = include_once "bd.php";
$sentencia = $bd->prepare("insert into mascotas(nombre, raza, edad) values (?,?,?)");
$resultado = $sentencia->execute([$jsonMascota->nombre, $jsonMascota->raza, $jsonMascota->edad]);
echo json_encode([
    "resultado" => $resultado,
]);
<?php
header("Access-Control-Allow-Origin: http://localhost:4200");
header("Access-Control-Allow-Methods: PUT");
header("Access-Control-Allow-Headers: *");
if ($_SERVER["REQUEST_METHOD"] != "PUT") {
    exit("Solo acepto peticiones PUT");
}
$jsonMascota = json_decode(file_get_contents("php://input"));
if (!$jsonMascota) {
    exit("No hay datos");
}
$bd = include_once "bd.php";
$sentencia = $bd->prepare("UPDATE mascotas SET nombre = ?, raza = ?, edad = ? WHERE id = ?");
$resultado = $sentencia->execute([$jsonMascota->nombre, $jsonMascota->raza, $jsonMascota->edad, $jsonMascota->id]);
echo json_encode($resultado);

Como ves en el inicio de cada uno de ellos configuro CORS, después decodifico o codifico los datos con JSON y hago las consultas en cada archivo.

Eso es todo lo que haremos del lado del servidor.

El servicio para conectar Angular con PHP

Ahora tengo un servicio que se encargará de llamar a todos esos archivos del servidor. Queda así:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Mascota } from "./mascota"
import { environment } from "../environments/environment"
@Injectable({
  providedIn: 'root'
})
export class MascotasService {
  baseUrl = environment.baseUrl

  constructor(private http: HttpClient) { }

  getMascotas() {
    return this.http.get(`${this.baseUrl}/getAll.php`);
  }

  getMascota(id: string | number) {
    return this.http.get(`${this.baseUrl}/get.php?idMascota=${id}`);
  }

  addMascota(mascota: Mascota) {
    return this.http.post(`${this.baseUrl}/post.php`, mascota);
  }

  deleteMascota(mascota: Mascota) {
    return this.http.delete(`${this.baseUrl}/delete.php?idMascota=${mascota.id}`);
  }

  updateMascota(mascota: Mascota) {
    return this.http.put(`${this.baseUrl}/update.php`, mascota);
  }
}

En el servicio utilizo HttpClient, y la clase Mascota que es la siguiente:

export class Mascota {
    constructor(
        public nombre: string,
        public raza: string,
        public edad: number,
        public id?: number,
    ) { }

}

El campo id es opcional pues no todos los objetos tendrán id, solo los que ya han sido guardados previamente.

El enrutador

Como estamos creando una SPA con Angular usamos el enrutador de angular; queda configurado así:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AgregarMascotaComponent } from './agregar-mascota/agregar-mascota.component';
import { ListarMascotasComponent } from './listar-mascotas/listar-mascotas.component';
import { EditarMascotaComponent } from './editar-mascota/editar-mascota.component';
import { AcercaDeComponent } from './acerca-de/acerca-de.component';

const routes: Routes = [
  { path: "acerca-de", component: AcercaDeComponent },
  { path: "mascotas", component: ListarMascotasComponent },
  { path: "mascotas/agregar", component: AgregarMascotaComponent },
  { path: "mascotas/editar/:id", component: EditarMascotaComponent },
  { path: "", redirectTo: "/mascotas", pathMatch: "full" },// Cuando es la raíz
  { path: "**", redirectTo: "/mascotas" }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Ahora vamos a ver cada componente.

Insertar dato: create

Formulario basado en plantilla para registrar un nuevo dato con PHP y MySQL desde Angular

Comencemos viendo el componente para insertar datos. Primero diseñamos el formulario con Angular Material y se ve así:

<h2>Agregar mascota <mat-icon>pets</mat-icon></h2>
<form (ngSubmit)="onSubmit()" #formMascota="ngForm">
    <p>

        <mat-form-field>
            <input matInput name="nombre" [(ngModel)]="mascotaModel.nombre"
                type="text"
                placeholder="Nombre">
        </mat-form-field>
    </p>
    <p>
        <mat-form-field>
            <input matInput name="raza" [(ngModel)]="mascotaModel.raza"
                type="text"
                placeholder="Raza">
        </mat-form-field>
    </p>
    <p>
        <mat-form-field>
            <input matInput name="edad" [(ngModel)]="mascotaModel.edad"
                type="number"
                placeholder="Edad">
        </mat-form-field>
    </p>
    <p>
        <button type="submit" mat-flat-button color="accent">Guardar</button>
    </p>
</form>

Dentro de la lógica simplemente esperamos a que el formulario sea enviado, invocamos al servicio previamente visto y mostramos un Snackbar indicando que se ha registrado correctamente.

Para registrar el valor invocamos a addMascota.

import { Component, OnInit } from '@angular/core';
import { Mascota } from '../mascota';
import { MascotasService } from "../mascotas.service"
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';


@Component({
  selector: 'app-agregar-mascota',
  templateUrl: './agregar-mascota.component.html',
  styleUrls: ['./agregar-mascota.component.css']
})
export class AgregarMascotaComponent implements OnInit {

  constructor(private mascotasService: MascotasService,
    private snackBar: MatSnackBar,
    private router: Router,
  ) { }

  ngOnInit() {
  }
  mascotaModel = new Mascota("", "", undefined)

  onSubmit() {
    this.mascotasService.addMascota(this.mascotaModel).subscribe(() => {
      this.snackBar.open('Mascota guardada', undefined, {
        duration: 1500,
      });
      this.router.navigate(['/mascotas']);
    })
  }

}

Después navegamos a la vista de para listar todas las mascotas y se ve así:

Listar datos en tabla de Angular

Ahora veamos la operación select. Para ello invocamos al archivo getAll de PHP y traemos los datos en formato de arreglo. Comencemos viendo la vista HTML:

<h2>Listado de mascotas</h2>
<table mat-table [dataSource]="mascotas" class="mat-elevation-z8" style="width:
    100%;">
    <ng-container matColumnDef="nombre">
        <th mat-header-cell *matHeaderCellDef>Nombre</th>
        <td mat-cell *matCellDef="let mascota"> {{mascota.nombre}} </td>
    </ng-container>
    <ng-container matColumnDef="raza">
        <th mat-header-cell *matHeaderCellDef>Raza</th>
        <td mat-cell *matCellDef="let mascota"> {{mascota.raza}} </td>
    </ng-container>
    <ng-container matColumnDef="edad">
        <th mat-header-cell *matHeaderCellDef>Edad</th>
        <td mat-cell *matCellDef="let mascota"> {{mascota.edad}} </td>
    </ng-container>
    <ng-container matColumnDef="editar">
        <th mat-header-cell *matHeaderCellDef>Editar</th>
        <td mat-cell *matCellDef="let mascota">
            <a mat-icon-button [routerLink]="['/mascotas/editar', mascota.id]">
                <mat-icon color="primary">edit</mat-icon>
            </a>
        </td>
    </ng-container>
    <ng-container matColumnDef="eliminar">
        <th mat-header-cell *matHeaderCellDef>Eliminar</th>
        <td mat-cell *matCellDef="let mascota">
            <button (click)="eliminarMascota(mascota)" mat-icon-button>
                <mat-icon color="warn">delete</mat-icon>
            </button>
        </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="['nombre', 'raza', 'edad', 'editar',
        'eliminar']"></tr>
    <tr mat-row *matRowDef="let fila; columns: ['nombre', 'raza', 'edad',
        'editar', 'eliminar'];"></tr>
</table>

Es una tabla de Angular Material. Depende del arreglo mascotas y define algunos botones, uno para editar que navega a la ruta de edición (definida en el router) y otro para eliminar que invoca al método eliminarMascota.

La lógica en el lenguaje TypeScript es la siguiente:

import { Component, OnInit } from '@angular/core';
import { MascotasService } from "../mascotas.service"
import { Mascota } from "../mascota"
import { MatDialog } from '@angular/material/dialog';
import { DialogoConfirmacionComponent } from "../dialogo-confirmacion/dialogo-confirmacion.component"
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
  selector: 'app-listar-mascotas',
  templateUrl: './listar-mascotas.component.html',
  styleUrls: ['./listar-mascotas.component.css']
})
export class ListarMascotasComponent implements OnInit {
  private mascotas: Mascota[] = [
    new Mascota("Maggie", "Chihuahua", 20)
  ];

  constructor(private mascotasService: MascotasService, private dialogo: MatDialog, private snackBar: MatSnackBar) { }

  eliminarMascota(mascota: Mascota) {
    this.dialogo
      .open(DialogoConfirmacionComponent, {
        data: `¿Realmente quieres eliminar a ${mascota.nombre}?`
      })
      .afterClosed()
      .subscribe((confirmado: Boolean) => {
        if (!confirmado) return;
        this.mascotasService
          .deleteMascota(mascota)
          .subscribe(() => {
            this.obtenerMascotas();
            this.snackBar.open('Mascota eliminada', undefined, {
              duration: 1500,
            });
          });
      })
  }

  ngOnInit() {
    this.obtenerMascotas();
  }

  obtenerMascotas() {
    return this.mascotasService
      .getMascotas()
      .subscribe((mascotas: Mascota[]) => this.mascotas = mascotas);
  }

}

Definimos un arreglo que va a ser el de la tabla; el mismo será modificado más tarde al invocar a obtenerMascotas que consume el servicio de Angular y asigna el valor (línea 42)

En el método ngOnInit obtenemos los datos. También tenemos la función que elimina un valor, que igualmente consume al servicio en el método deleteMascota.

Eliminar valor de MySQL con Angular y PHP

Veamos la operación delete, tanto de HTTP como de MySQL. Para ello vamos a usar un diálogo de confirmación que ya expliqué en otro post, igualmente podrías usar un confirm nativo de JavaScript.

En caso de que se confirme, se ejecuta lo siguiente:

eliminarMascota(mascota: Mascota) {
  this.dialogo
    .open(DialogoConfirmacionComponent, {
      data: `¿Realmente quieres eliminar a ${mascota.nombre}?`
    })
    .afterClosed()
    .subscribe((confirmado: Boolean) => {
      if (!confirmado) return;
      this.mascotasService
        .deleteMascota(mascota)
        .subscribe(() => {
          this.obtenerMascotas();
          this.snackBar.open('Mascota eliminada', undefined, {
            duration: 1500,
          });
        });
    })
}

Eso invocará al método deleteMascota del servicio que a su vez invocará al archivo delete.php que hará una sentencia DELETE de SQL.

Editar valor y rellenar formulario

La última operación que veremos es la de editar un valor, para ello usaremos los parámetros del router de Angular para recuperar el id y después obtener el valor en MySQL para más tarde rellenar el formulario.

El código es el siguiente:

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { MascotasService } from "../mascotas.service"
import { Mascota } from '../mascota';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
  selector: 'app-editar-mascota',
  templateUrl: './editar-mascota.component.html',
  styleUrls: ['./editar-mascota.component.css']
})
export class EditarMascotaComponent implements OnInit {

  private mascota: Mascota = new Mascota("", "", 0);

  constructor(private route: ActivatedRoute,
    private router: Router, private mascotasService: MascotasService,
    private snackBar: MatSnackBar) { }

  ngOnInit() {
    let idMascota = this.route.snapshot.paramMap.get("id");
    this.mascotasService.getMascota(idMascota).subscribe((mascota: Mascota) => this.mascota = mascota)
  }

  volver() {
    this.router.navigate(['/mascotas']);
  }

  onSubmit() {
    this.mascotasService.updateMascota(this.mascota).subscribe(() => {
      this.snackBar.open('Mascota actualizada', undefined, {
        duration: 1500,
      });
      this.volver();
    });
  }

}

Lo importante aquí es el ngOnInit que es en donde obtenemos el parámetro de la ruta y a partir del mismo obtenemos el valor por id llamando al método getMascota.

En cuanto tenemos el valor lo asignamos y así se refleja en la vista, en donde ya está definido el formulario:

<h2>Editar mascota <mat-icon>pets</mat-icon></h2>
<form (ngSubmit)="onSubmit()" #formMascota="ngForm">
    <p>
        <mat-form-field>
            <input matInput name="nombre" [(ngModel)]="mascota.nombre"
                type="text"
                placeholder="Nombre">
        </mat-form-field>
    </p>
    <p>
        <mat-form-field>
            <input matInput name="raza" [(ngModel)]="mascota.raza"
                type="text"
                placeholder="Raza">
        </mat-form-field>
    </p>
    <p>
        <mat-form-field>
            <input matInput name="edad" [(ngModel)]="mascota.edad"
                type="number"
                placeholder="Edad">
        </mat-form-field>
    </p>
    <p>
        <button type="submit" mat-flat-button color="accent">Guardar</button>
        &nbsp;
        <button (click)="volver()" type="button" mat-flat-button color="primary">Volver</button>
    </p>
</form>

Cuando el formulario sea enviado invocamos a updateMascota.

El menú de navegación

Todas las vistas que mostré están dentro de un contenedor tipo navigation drawer. La vista padre queda así:

<div class="contenedor-padre">
  <mat-toolbar class="barra" color="warn">
    <mat-toolbar-row>
      <button (click)="cajon.toggle()" mat-icon-button>
        <mat-icon>menu</mat-icon>
      </button>
      <span>
        CRUD Angular con PHP y MySQL&nbsp;
      </span>
      <span>
        <a mat-stroked-button href="https://parzibyte.me/blog">By Parzibyte</a>
      </span>
    </mat-toolbar-row>
  </mat-toolbar>
  <mat-sidenav-container class="contenido">
    <mat-sidenav style="min-width: 300px;" #cajon mode="side">
      <mat-nav-list>
        <a mat-list-item routerLink="/mascotas/agregar">
          <mat-icon color="accent">add</mat-icon>
          Agregar mascota
        </a>
        <a mat-list-item routerLink="/mascotas">
          <mat-icon color="primary">pets</mat-icon>
          Ver mascotas
        </a>
      </mat-nav-list>
      <mat-accordion>
        <mat-expansion-panel>
          <mat-expansion-panel-header>
            <mat-panel-title>
              Más
            </mat-panel-title>
          </mat-expansion-panel-header>

          <mat-nav-list>
            <a mat-list-item routerLink="/acerca-de">
              Acerca de
            </a>
            <a mat-list-item href='https://parzibyte.me/blog/contrataciones-ayuda/'>
              Ayuda
            </a>
          </mat-nav-list>
        </mat-expansion-panel>
      </mat-accordion>
    </mat-sidenav>
    <mat-sidenav-content class="padding-10">
      <router-outlet></router-outlet>
    </mat-sidenav-content>
  </mat-sidenav-container>
</div>

Con el siguiente resultado (el menú de navegación se puede ocultar y se ve a la izquierda)

Conclusión y notas finales

Hemos terminado de implementar una aplicación web con Angular usando los estilos de Material Design y PHP como lenguaje de servidor, además de MySQL como manejador de base de datos.

Con esto podemos darnos una idea de cómo conectar el framework Angular con casi cualquier lenguaje de servidor. Espero traer más ejemplos en el futuro, mientras tanto te invito a suscribirte o a ver más artículos sobre Angular.

Por cierto, para la URL base estoy usando variables del entorno, así cambio la URL del servidor dependiendo de si estoy o no en producción.

Nota: prueba la app aquí, o mira el código en GitHub.

Bonus: cómo instalar

Como ves, en el repositorio existe la carpeta server. Esa carpeta server debe estar en tu servidor con PHP y Apache (o cuaquier otro servidor web que soporte PHP)

En modo desarrollo, utiliza CORS. Después, cuando hayas terminado, compila con ng build --prod y copia todo lo generado dentro de dist/crud-angular-php-mysql a la carpeta del servidor (en donde están los archivos de PHP), pues ya no necesitas servirlo con Node en producción, sino con PHP.

No olvides configurar la ruta del servidor dentro de los archivos que están en el directorio environments.

Si quieres algo rápido, simplemente descarga la versión ya compilada, colócala en tu directorio público, configura credenciales de base de datos en bd.php, crea la tabla y listo.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

  • ¡Buenas! Tan sólo agradecerte este ejemplo, me ha ahorrado bastantes horas de desarrollo y me ha ido de fábula, dado que nunca había visto conectados PHP con Angular2.

    ¡Muchas gracias y suerte con futuros proyectos!

  • *Is the syntax still working fine without errors today 05/12/2022?
    *¿Aún sigue funcionando bien sin errores la sintaxis hoy en día 12/05/2022?

    *in case no give me suggestions or something updated to 2022
    *en caso de que no, denme sugerencias o algo actualizado a 2022

  • me aparece Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays

  • Hola me podrías ayudar en una cuestiona que tengo, he probado tu proyecto y tengo un problema para encontrar los archivos php ¿ Me podrías hechar una mano?. Un saludo

  • Buenas como estas? tengo un problema con el codigo, lo estoy intentando replicar pero siempre me sale este error Access to XMLHttpRequest at 'http://localhost/mascotas_angular/getAll.php' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Podrias apoyarme para ver porque ese error?

    • Como el error lo indica, es CORS. Debes configurar los encabezados en PHP; pero recuerda que solo es para el modo desarrollo, para producción no es necesario.
      Saludos :)

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

2 horas hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

6 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

6 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

6 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

6 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

7 días hace

Esta web usa cookies.