Hoy te voy a mostrar cómo conectar dos tecnologías: PHP y React. Vamos a ver cómo traer y enviar datos a PHP (para guardarlos en MySQL) desde React también conocido como React JS.
Al final podremos decir que estamos conectando React con PHP y MySQL. Lo que vamos a hacer será las 4 operaciones fundamentales (Crear, actualizar, eliminar y obtener) datos de MySQL para pasarlos a PHP y luego a React a través de una API.
Como resultado vamos a tener una aplicación web creada totalmente con React que consume archivos de PHP a través de AJAX con JSON. Por cierto, será una SPA o Single Page Application.
Creando aplicación de React
He creado el frontend de React con Create React App. Básicamente fue instalar NPM con Node y ejecutar:
npx create-react-app react-cliente-php
cd react-cliente-php
npm start
En este caso mi app se llama react-cliente-php
. Una vez que creé la app, agregué componentes, etcétera y trabajé sobre ese código.
Si tú ya tienes código existente o un proyecto en marcha, puedes tomar esto como ejemplo. Al final ambos usamos la misma librería: React JS.
Nota: recuerda que cada que quieras editar la app web debes iniciar el servidor con npm start
.
Lado del servidor
En el backend he implementado código PHP puro. Eres libre de modificarlo, mejorarlo, etcétera. Son simples archivos que sirven como API para las 4 operaciones que React va a requerir. He usado MySQL / MariaDB para la gestión de la base de datos.
Por cierto, esta app web va a gestionar videojuegos. El esquema de la base de datos queda así:
CREATE TABLE IF NOT EXISTS videojuegos(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255) NOT NULL,
precio DECIMAL(9,2) NOT NULL,
calificacion TINYINT NOT NULL
);
Comencemos viendo el archivo de las funciones, básicamente tiene todos los métodos que vamos a usar y queda así:
<?php
function eliminarVideojuego($id)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("DELETE FROM videojuegos WHERE id = ?");
return $sentencia->execute([$id]);
}
function actualizarVideojuego($videojuego)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("UPDATE videojuegos SET nombre = ?, precio = ?, calificacion = ? WHERE id = ?");
return $sentencia->execute([$videojuego->nombre, $videojuego->precio, $videojuego->calificacion, $videojuego->id]);
}
function obtenerVideojuegoPorId($id)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("SELECT id, nombre, precio, calificacion FROM videojuegos WHERE id = ?");
$sentencia->execute([$id]);
return $sentencia->fetchObject();
}
function obtenerVideojuegos()
{
$bd = obtenerConexion();
$sentencia = $bd->query("SELECT id, nombre, precio, calificacion FROM videojuegos");
return $sentencia->fetchAll();
}
function guardarVideojuego($videojuego)
{
$bd = obtenerConexion();
$sentencia = $bd->prepare("INSERT INTO videojuegos(nombre, precio, calificacion) VALUES (?, ?, ?)");
return $sentencia->execute([$videojuego->nombre, $videojuego->precio, $videojuego->calificacion]);
}
function obtenerVariableDelEntorno($key)
{
if (defined("_ENV_CACHE")) {
$vars = _ENV_CACHE;
} else {
$file = "env.php";
if (!file_exists($file)) {
throw new Exception("El archivo de las variables de entorno ($file) no existe. Favor de crearlo");
}
$vars = parse_ini_file($file);
define("_ENV_CACHE", $vars);
}
if (isset($vars[$key])) {
return $vars[$key];
} else {
throw new Exception("La clave especificada (" . $key . ") no existe en el archivo de las variables de entorno");
}
}
function obtenerConexion()
{
$password = obtenerVariableDelEntorno("MYSQL_PASSWORD");
$user = obtenerVariableDelEntorno("MYSQL_USER");
$dbName = obtenerVariableDelEntorno("MYSQL_DATABASE_NAME");
$database = new PDO('mysql:host=localhost;dbname=' . $dbName, $user, $password);
$database->query("set names utf8;");
$database->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
$database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$database->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
return $database;
}
En la función obtenerConexion
estamos usando PDO para obtener una conexión a la base de datos de MySQL.
Fíjate en que las credenciales de acceso son obtenidas de un archivo con variables del entorno, así que debes crear el archivo env.php
tomando como ejemplo env.ejemplo.php
:
; <?php exit; ?>
MYSQL_DATABASE_NAME = "videojuegos_react"
MYSQL_USER = "root"
MYSQL_PASSWORD = ""
Las demás funciones que se encuentran en el archivo están relacionadas a la gestión de videojuegos. Ahora exponemos cada una de esas funciones en archivos separados de PHP que ahora sí vamos a invocar desde React.
Por ejemplo, para obtener todos los videojuegos simplemente invocamos a una función desde el archivo obtener_videojuegos.php
:
<?php
include_once "cors.php";
include_once "funciones.php";
$videojuegos = obtenerVideojuegos();
echo json_encode($videojuegos);
De este modo hacemos comunicación usando JSON y consultamos los datos desde React con fetch. Cada función está expuesta con su respectivo código. No colocaré todo el código aquí, pero puedes verlo completo en el repositorio que dejaré más adelante.
Habilitar CORS en PHP para aceptar peticiones desde React
En cada archivo del servidor he incluido también el archivo para habilitar CORS en PHP. Debido a que nuestra web app (en modo desarrollo) de React estará en localhost:3000
y la API de PHP estará en localhost:80
debemos habilitar CORS en ésta última.
De este modo podemos compartir recursos y básicamente realizar llamadas AJAX desde React a PHP. Cuando prepares tu app para producción puedes dejar de incluir este archivo, pero mientras tanto queda así:
<?php
$dominioPermitido = "http://localhost:3000";
header("Access-Control-Allow-Origin: $dominioPermitido");
header("Access-Control-Allow-Headers: content-type");
header("Access-Control-Allow-Methods: OPTIONS,GET,PUT,POST,DELETE");
Este archivo modifica los encabezados, así que asegúrate de incluirlo al inicio de todos los archivos en donde quieras habilitar la comunicación entre ambas tecnologías.
Aplicación web con React
Veamos la aplicación principal. En este caso estamos usando el Router de React, e indicamos los componentes para cada ruta a la que navega el usuario.
import Nav from "./Nav";
import AgregarVideojuego from "./AgregarVideojuego";
import VerVideojuegos from "./VerVideojuegos";
import EditarVideojuego from "./EditarVideojuego";
import {
Switch,
Route,
} from "react-router-dom";
function App() {
return (
<div>
<Nav></Nav>
<div className="container is-fullhd">
<div className="columns">
<Switch>
<Route path="/videojuegos/agregar">
<AgregarVideojuego></AgregarVideojuego>
</Route>
<Route path="/videojuegos/editar/:id">
<EditarVideojuego></EditarVideojuego>
</Route>
<Route path="/videojuegos/ver">
<VerVideojuegos></VerVideojuegos>
</Route>
<Route path="/">
<VerVideojuegos></VerVideojuegos>
</Route>
</Switch>
</div>
</div>
</div>
);
}
export default App;
Como puedes ver, básicamente tenemos 3 componentes: uno para agregar videojuego, otro para editar y otro para mostrar. Ahora veamos también el menú de navegación:
import React from 'react';
import logo from "./img/parzibyte_logo.png";
import { NavLink } from "react-router-dom";
class Nav extends React.Component {
render() {
return (
<nav className="navbar is-warning" role="navigation" aria-label="main navigation">
<div className="navbar-brand">
<a className="navbar-item" href="https://parzibyte.me/l/fW8zGd">
<img alt="" src={logo} style={{ maxHeight: "80px" }} />
</a>
<button className="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
</div>
<div id="navbarBasicExample" className="navbar-menu">
<div className="navbar-start">
<NavLink activeClassName="is-active" className="navbar-item" to="/videojuegos/ver">Ver videojuegos</NavLink>
<NavLink activeClassName="is-active" className="navbar-item" to="/videojuegos/agregar">Agregar videojuego</NavLink>
</div>
<div className="navbar-end">
<div className="navbar-item">
<div className="buttons">
<a target="_blank" rel="noreferrer" href="https://parzibyte.me/l/fW8zGd" className="button is-primary">
<strong>Soporte y ayuda</strong>
</a>
</div>
</div>
</div>
</div>
</nav>
);
}
}
export default Nav;
Este Nav solo es el menú de navegación que lleva a los distintos módulos del sistema. Por cierto, estoy usando el framework CSS Bulma. Ahora que he explicado el diseño general y que se va a mostrar un componente de acuerdo a la ruta, veamos cada componente.
Agregar nuevo videojuego
Veamos la operación para insertar un nuevo valor en la base de datos desde React. Aquí vamos a tener un formulario y vamos a usar el estado interno también conocido como internal state. Primero veamos el formulario que se renderiza:
render() {
return (
<div className="column is-one-third">
<h1 className="is-size-3">Agregar videojuego</h1>
<ToastContainer></ToastContainer>
<form className="field" onSubmit={this.manejarEnvioDeFormulario}>
<div className="form-group">
<label className="label" htmlFor="nombre">Nombre:</label>
<input autoFocus required placeholder="Nombre" type="text" id="nombre" onChange={this.manejarCambio} value={this.state.videojuego.nombre} className="input" />
</div>
<div className="form-group">
<label className="label" htmlFor="precio">Precio:</label>
<input required placeholder="Precio" type="number" id="precio" onChange={this.manejarCambio} value={this.state.videojuego.precio} className="input" />
</div>
<div className="form-group">
<label className="label" htmlFor="calificacion">Calificación:</label>
<input required placeholder="Calificación" type="number" id="calificacion" onChange={this.manejarCambio} value={this.state.videojuego.calificacion} className="input" />
</div>
<div className="form-group">
<button className="button is-success mt-2">Guardar</button>
<Link to="/videojuegos/ver" className="button is-primary mt-2">Volver</Link>
</div>
</form>
</div>
);
}
El formulario está relacionado con la variable videojuego
del estado interno de la app. Ya sea al nombre, precio o calificación. Y en el envío del formulario (submit
) se invoca a manejarEnvioDeFormulario
que queda así:
async manejarEnvioDeFormulario(evento) {
evento.preventDefault();
// Codificar nuestro videojuego como JSON
const cargaUtil = JSON.stringify(this.state.videojuego);
// ¡Y enviarlo!
const respuesta = await fetch(`${Constantes.RUTA_API}/guardar_videojuego.php`, {
method: "POST",
body: cargaUtil,
});
const exitoso = await respuesta.json();
if (exitoso) {
toast('Videojuego guardado 🎮', {
position: "top-left",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
this.setState({
videojuego: {
nombre: "",
precio: "",
calificacion: "",
}
});
} else {
toast.error("Error guardando. Intenta de nuevo");
}
}
Este método es muy importante pues aquí tomamos la variable del estado interno y la enviamos a nuestro servidor con PHP. Aquí es en donde la magia sucede. Aquí estamos conectando React con PHP.
En la línea 6 creamos la carga útil que básicamente es codificar como JSON el objeto del videojuego. Después de eso esperamos a que la petición se termine y evaluamos la respuesta para mostrar una notificación dependiendo de la misma.
También reiniciamos el formulario, es decir, limpiamos los campos.
Listar videojuegos en tabla
La siguiente operación que vamos a ver es la operación HTTP GET u operación Read de este CRUD. PHP nos va a devolver un arreglo con todos los videojuegos, así que nosotros debemos obtenerlos con Javascript y React para mostrarlos.
El constructor de este componente de react queda así:
constructor(props) {
super(props);
this.state = {
videojuegos: [],
};
}
Como puedes ver solo tenemos a videojuegos
, que es un arreglo vacío. Cuando el componente se haya acabado de montar vamos a obtener todos los videojuegos desde la API de PHP:
async componentDidMount() {
const respuesta = await fetch(`${Constantes.RUTA_API}/obtener_videojuegos.php`);
const videojuegos = await respuesta.json();
this.setState({
videojuegos: videojuegos,
});
}
Con this.setState
refrescamos el estado con el arreglo que nos haya devuelto PHP. De este modo se van a asignar los videojuegos traídos desde MySQL. Finalmente veamos el método render
que renderiza una tabla:
render() {
return (
<div>
<div className="column">
<h1 className="is-size-3">Ver videojuegos</h1>
<ToastContainer></ToastContainer>
</div>
<div className="table-container">
<table className="table is-fullwidth is-bordered">
<thead>
<tr>
<th>Nombre</th>
<th>Precio</th>
<th>Calificación</th>
<th>Editar</th>
<th>Eliminar</th>
</tr>
</thead>
<tbody>
{this.state.videojuegos.map(videojuego => {
return <FilaDeTablaDeVideojuego key={videojuego.id} videojuego={videojuego}></FilaDeTablaDeVideojuego>;
})}
</tbody>
</table>
</div>
</div>
);
}
Aquí hay otra cosa interesante y es que dentro del tbody
estamos renderizando otro componente llamado FilaDeTablaDeVideojuego
por cada videojuego presente. Este componente también es creado por nosotros.
En seguida vamos a ver ese componente, por ahora fíjate en que le estamos pasando el objeto del videojuego
y estamos indicando su clave.
Lo que hacemos con map es transformar el arreglo de videojuegos en componentes JSX que React va a renderizar. Básicamente es como transformar un objeto a HTML.
Fila de tabla
Decidí separar el concepto de la fila de la tabla para hacer el código más legible y separar los componentes de React. La fila de la tabla va a tener un enlace (para editar un videojuego) y un botón para eliminar un videojuego. El diseño queda así:
render() {
if (this.state.eliminado) {
return null;
}
return (
<tr>
<td>{this.props.videojuego.nombre}</td>
<td>{this.props.videojuego.precio}</td>
<td>{this.props.videojuego.calificacion}</td>
<td>
<Link to={`/videojuegos/editar/${this.props.videojuego.id}`} className="button is-info">Editar</Link>
</td>
<td>
<button onClick={this.eliminar} className="button is-danger">Eliminar</button>
</td>
</tr>
);
}
En caso de que el videojuego esté eliminado no vamos a devolver nada, es decir, vamos a devolver null
. Esto es controlado con la bandera eliminado
.
Este componente es un hijo del componente de la tabla. Nosotros no podemos (me parece que sí se puede con Redux) decirle al padre que se ha eliminado un dato y que por favor refresque la tabla, así que cada que un videojuego se elimine usando la API, se va a marcar esta bandera y el componente va a desaparecer de la tabla.
Todo esto es para mostrar al usuario que el valor se está eliminando en tiempo real, pero sin refrescar toda la tabla. Simplemente desaparecemos la fila que ya fue eliminada en la base de datos consumiendo la API.
Eliminar videojuego
Al hacer clic en la fila de la tabla se va a invocar al siguiente método que muestra una alerta con Sweet Alert 2 y en caso que el usuario confirme la operación se hace una petición DELETE a PHP para finalmente desaparecer la fila de la tabla e indicarlo con un toast:
async eliminar() {
const resultado = await Swal.fire({
title: 'Confirmación',
text: `¿Eliminar "${this.props.videojuego.nombre}"?`,
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3298dc',
cancelButtonColor: '#f14668',
cancelButtonText: 'No',
confirmButtonText: 'Sí, eliminar'
});
// Si no confirma, detenemos la función
if (!resultado.value) {
return;
}
const respuesta = await fetch(`${Constantes.RUTA_API}/eliminar_videojuego.php?id=${this.props.videojuego.id}`, {
method: "DELETE",
});
const exitoso = await respuesta.json();
if (exitoso) {
toast('Videojuego eliminado ', {
position: "top-left",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
this.setState({
eliminado: true,
});
} else {
toast.error("Error eliminando. Intenta de nuevo");
}
}
La bandera se está estableciendo en la línea 31. De este modo la fila se desaparece a sí misma, y como cada componente está aislado, esto no afecta a los demás al menos que el padre refresque la tabla.
Editar videojuego
Anteriormente en la fila de la tabla te mostré que se crea un enlace para editar cada videojuego. Ese enlace lleva a otra ruta (obviamente) e invoca a otro componente.
Cuando el componente se monta, obtenemos el id de la ruta e invocamos a la API para traer los detalles de ese videojuego y rellenar el formulario:
async componentDidMount() {
// Obtener ID de URL
const idVideojuego = this.props.match.params.id;
// Llamar a la API para obtener los detalles
const respuesta = await fetch(`${Constantes.RUTA_API}/obtener_videojuego.php?id=${idVideojuego}`);
const videojuego = await respuesta.json();
// "refrescar" el formulario
this.setState({
videojuego: videojuego,
});
}
Una vez rellenado el formulario simplemente manejamos el envío del mismo tal como lo hicimos cuando agregamos un nuevo videojuego, pero ahora hacemos una petición PUT a otro archivo y también enviamos el ID del videojuego:
async manejarEnvioDeFormulario(evento) {
evento.preventDefault();
// Codificar nuestro videojuego como JSON
const cargaUtil = JSON.stringify(this.state.videojuego);
// ¡Y enviarlo!
const respuesta = await fetch(`${Constantes.RUTA_API}/actualizar_videojuego.php`, {
method: "PUT",
body: cargaUtil,
});
const exitoso = await respuesta.json();
if (exitoso) {
toast('Videojuego guardado 🎮', {
position: "top-left",
autoClose: 2000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
} else {
toast.error("Error guardando. Intenta de nuevo");
}
}
Con esto hemos terminado las operaciones fundamentales de este sistema.
Descargando el código
Como te diste cuenta no expuse el código completo aquí, pues me llevaría bastante tiempo y el post se haría muy largo. Pero no te preocupes, si quieres explorar todo el código, descargarlo, probarlo, etcétera te dejo el repositorio del cliente y el repositorio del servidor en Github.
Recuerda que en el caso del código de React primero debes instalar las dependencias con npm install
y luego ejecutar el servidor con npm start
.
Para el caso del código PHP simplemente debes configurar la base de datos y colocar el código en tu servidor web Apache. Si estás en Windows con XAMPP puede que sea en C:\xampp\htdocs
y si es con Linux podría ser en /var/www/public_html
.
También fíjate en el archivo Constantes.js de la app de react, ahí se configura la ruta del servidor que puede cambiar dependiendo de dónde vayas a montar la app.
Preparando para producción
Como te has dado cuenta, hemos desarrollado el lado del servidor y del cliente por separado, pero esto no significa que no podamos ponerlos juntos. Una vez que el frontend esté listo, ejecuta:
npm run build
Ese comando va a minificar y optimizar los archivos de la app web de React. También va a colocar todos esos archivos que te menciono en una carpeta llamada build
, y dentro de ella tendrás varios archivos (un index.html
, logo, etcétera) además de una carpeta llamada static
.
Copia todo el contenido de build
en donde residen los archivos de la API de PHP, y luego sube todo el proyecto al servidor web para desplegarlo. Por ejemplo, mi directorio ya listo para producción queda así como en la siguiente imagen:
Ahora ya tienes solo archivos JavaScript, HTML y PHP. Puedes colocarlos en el servidor web o en localhost y acceder a ellos. Eso sí, cuando hagas cambios tendrás que iniciar el servidor de Node y luego ejecutar npm run build
.
Conclusión
Muy pronto estaré trayendo más contenido de este tipo. Mientras tanto puede que te interese ver cómo conectar Angular con PHP o leer más sobre PHP en mi blog.
Gracias tu aporte es valioso