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:
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.
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;
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.
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.
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.
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í:
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
.
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.
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>
<button (click)="volver()" type="button" mat-flat-button color="primary">Volver</button>
</p>
</form>
Cuando el formulario sea enviado invocamos a updateMascota
.
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
</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)
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.
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.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.
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
Una pregunta externa, cuánto se puede cobrar por un sistema como este, para saber a futuro
Yo creo que 5 dólares aproximadamente, solo es un CRUD
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
Hola, claro. Aquí encuentras más información: https://parzibyte.me/blog/contrataciones-ayuda/
Saludos
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 :)