Índice
EL proyecto lo realizaremos en una 8 publicaciones apróximadamente, de las cuales la primera es la explicación del proyecto y partir de ahí ya empezaremos con el desarrollo real:
En la continuación de este tutorial vamos a ver en la segunda publicación la creación del proyecto backend ADMIN_APP utilizando Docker Compose para crear un proyecto con Laravel sobre Nginx y MySQL.
Índice
EL proyecto lo realizaremos en una 8 publicaciones apróximadamente, vamos a ver el punto 2 en esta publicación.
- Explicación del proyecto: Creación de una aplicación de microservicios con Laravel y RabbitMQ
- Creación del proyecto backend ADMIN-APP con Docker Compose : Laravel, Nginx y MySQL
- Desarrollamos la API REST de ADMIN-APP para empresas y clientes con Laravel
- CRUD controller y API controller para las empresas (cm_enterprise) (30/01/2022)
- CRUD controller y API controller para los clientes (cm_customer) (06/02/2022)
- Creación del proyecto order app con docker frontend pedidos (13/02/2022)
- RabbitMQ admin y order app , actualización de clientes (20/02/2022)
- Creamos la página de entrada de pedidos
- RabbitMQ Automatización y colas diferenciadas
- RabbitMQ actualizamos el saldo de clientes en ADMIN-APP desde ORDER-APP
- Nota: durante las publicaciones este índice podrá sufrir alguna modificación para adaptar el contenido.
Software que utilizaremos
En este listado incluimos todo el software utilizado en todo el proyecto, no solo en esta publicación:
- Linux (Ubuntu 20.04 LTS): sistema operativo Linux con la versión de Ubuntu 20.04 LTS
- Docker (docker): Docker version 20.10.7, esta guía se ha generado utilizando esta versión de Docker.
- Docker compose (docker-compose): docker-compose version 1.25.0, , esta guía se ha generado utilizando esta versión
- Laravel 8: framewok PHP de desarrollo de aplicaciones web, en este caso utilizaremos la versión 8.
- Mysql 5.7: versión utilizada en la imagen de docker-compose para la base de datos.
- Nginx: servidor web de aplicaciones
- PHP 7.4: utilizamos PHP en una versión superior a 7.4 siguiendo las recomendaciones de Laravel, por eso la imagen que utilicemos será de esta versión.
- Composer 2: gestor de paquetes a partir de archivos json utilizado por Laravel, utilizamos Composer 2 porque lo necesitamos para Laravel 8, en esta publicación indicamos como actualizarlo.
- Artisan: programa de línea de comandos de Laravel que nos ayuda en la creación de elementos como modelos, controladores, …, y también, nos proporciona otras utilidades como gestión de las migraciones o el listado de las rutas
- RabbitMQ / CloudAMQP: en este proyecto utlizaremos CloudAMQP (provides managed RabbitMQ servers in the cloud), que nos proporciona un servidor para RabbitMQ en la nube, y así nos facilita el desarrollo en la nube. Otra opción sería instalar una máquina Docker con RabbitMQ pero eso complicaría el objetivo de este tutorial
- Artisan CLI: Laravel incluye un interfaz de línea de comandos que se conoce como Artisan.
3. Desarrollamos la API REST de ADMIN-APP para empresas y clientes con Laravel
En el desarrollo de la API con Laravel no vamos a entrar en profundidad en la explicación del funcionamiento del framework, intentaremos dar las explicaciones básicas para que alguien que esté empezando o que tenga conocimientos de PHP pero no de Laravel puedan seguir el desarrollo.
Con Laravel utilizaremos el cliente de línea de comandos artisan:
- Artisan CLI: Laravel incluye un interfaz de línea de comandos que se conoce como Artisan (Artisan CLI), este nos proporcionará una serie de comandos de ayuda para el desarrollo de nuestros proyectos, como pueden ser la creación de las clases de los modelos, creación de controladores, creación de rutas,…
3.1. CRUD controller y API controller para las empresas (cm_enterprise)
Vamos a crear el controlador para nuestra API de empresas que se corresponderá a la tabla cm_enterprise, para ello vamos a generar las clases bases del controlador, modelo y migraciones con un único comando:
docker-compose exec admin-app-lpm php artisan make:model CmEnterprise --controller --migration --resource
Ahora creamos el controlador para la API y tambén, la gestión de la fuente de recursos (resource):
docker-compose exec admin-app-lpm php artisan make:controller API/CmEnterpriseController --api docker-compose exec admin-app-lpm php artisan make:resource CmEnterpriseResource
Con está parte ya hemos creado la estructura para la API de empresas, empecemos a programar.
3.1.1 Definimos la migración para la tabla cm_enterprise
En la definición del proyecto vimos la definición básica de la tabla cm_enterprise en el diagrama entidad relación del proyecto:
Como se puede ver en la imagen anterior los datos son sencillos y se entienden por si mismos: nombre de la empresa, país, estado, moneda, …, el esquema SQL sería el siguiente:
CREATE TABLE cm_enterprise ( identerprise INT NOT NULL AUTO_INCREMENT, enterprise VARCHAR(150), description VARCHAR(250), contact VARCHAR(250), estate VARCHAR(30), country VARCHAR(100), currency VARCHAR(100), elanguage VARCHAR(6), CONSTRAINT pk_enterprise PRIMARY KEY (identerprise) ) ENGINE=InnoDB COMMENT='Tabla para controlar las empresas que se usan en la aplicación, la aplicación se desarrolla en función de estos parámetros ya que habrá algunas tablas que serán comunes a las empresa y otras que no, por ejemplo, cada empresa tendrá sus propios clientes, pero tendrá los mismos idiomas, monedas y países de trabajo.'; GRANT ALL ON TABLE cm_enterprise TO xulescode;
Pero como queremos hacerlo con migraciones desde Laravel, el fichero generado para la creación de la tabla se encuentra en database/migrations/ , es en esta ubicación donde iremos creando las tablas de la base de datos y los cambios que se necesiten según vayan evolucionando.
En mi caso tenemos la clase 2022_01_17_131604_create_cm_enterprises_table.php donde definiremos la tabla, dentro de la clase se definen dos métodos:
- up: se ejecuta cuando lanzamos la migración ( php artisan migrate ).
- down: se ejecuta cuando retrocemos la migración ( php artisan rollback o php artisan rollback –step=4).
Vamos a ver la clase desarrollada y después explicamos algún concepto:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateCmEnterprisesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { /** * Tabla para controlar las empresas que se usan en la aplicación, * la aplicación se desarrolla en función de estos parámetros ya que * habrá algunas tablas que serán comunes a las empresa y otras que no, * por ejemplo, cada empresa tendrá sus propios clientes, * pero tendrá los mismos idiomas, monedas y países de trabajo. * identerprise INT NOT NULL AUTO_INCREMENT, * enterprise VARCHAR(150), * description VARCHAR(250), * contact VARCHAR(250), * estate VARCHAR(30), * country VARCHAR(100), * currency VARCHAR(100), * elanguage VARCHAR(6), * */ Schema::create('cm_enterprise', function (Blueprint $table) { $table->increments('identerprise'); $table->string('enterprise', 150)->nullable(false); $table->string('description', 250); $table->string('contact', 250); $table->string('estate', 30); $table->string('currency', 100); $table->string('country', 100); $table->string('elanguage', 6); $table->timestamps(); $table->engine = 'InnoDB'; // Specify the table storage engine (MySQL). }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('cm_enterprise'); } }
Para facilitar la comprensión incorporó como comentarios el contenido y el tipo de datos de la tabla que básicamente tiene un identificador único de tipo entero y campos de tipo de string:
- increments(‘identerprise’): creamos una clave primaria con el nombre que le indicamos, sino indicamos nada por defecto lo creará con el nombre del campo id.
- string(‘enterprise’, 150)->nullable(false): con el método string indicamos que se cree un campo en base de datos de tipo string,y a continuación, la limitación de longitud 150. Además, como queremos que no sea null lo indicamos con pasando false en el método nullable. Para crear otros tipos de campos lo puedes encontrar en el enlace Database migrations: Available column types
- engine(‘InnoDB’): en este caso le indició a MySQL el tipo de almacenamiento.
- timestamps(): creamos los campos created_at y updated_at para que se actualicen cada vez que creamos o actualizamos un registro respectivamente.
3.1.2 Definimos el modelo para la tabla de empresas
Definimos la clase del modelo para la empresa, la clase CmEnters priseModel la hemos creado automáticamente cuando creamos todos las clases con artisan para CmEnterprise. Los modelos se crean por defecto en app/Models como no hemos especificado ninguna carpeta más encontrarás aquí tu clase.
En la definición del modelo definimos los datos que utilizamos de la tabla cm_enterprise, tenemos que indicar la tabla, la clave primaria, las columnas listables, … También indicaremos si queremos crear la tabla con timestamps de creación y actualización cada vez que se actualice un campo, por defecto los crea, en nuestro caso como ejemplo os mostramos como decirle que los cree o no en función de si indicamos el parámetro timestamp a true o false.
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class CmEnterprise extends Model { use HasFactory; /** * Nombre de la tabla de la base de datos. * Database table name. */ protected $table = 'cm_enterprise'; /** * Indicamos si los ids son auto incrementales. * Indicates if the IDs are auto-incrementing. * @var bool si true id autoincrementeal, si false no. */ public $incrementing = true; /** * Por defecto Eloquent asume que existe una clave primaria llamada id, * si este no es nuesto caso lo tenemos que indicar en la variable $primaryKey. * Eloquent asumes tha a primary key called id exists by default, * if it isn't our case we have to write the name in the var class $primaryKey. */ protected $primaryKey = 'identerprise'; /** * Indicamos si el modelo tiene campos de tiempo de creación y actualización. * Indicates if the model should be timestamped. * @var bool */ public $timestamps = true; /** * Definimos los campos de la tabla directamente en la variable de tipo array $fillable. * We define the fields of the table in the var $fillable directly. */ protected $fillable = array( 'identerprise', 'enterprise', 'description', 'contact', 'estate', 'elanguage', 'currency', 'country'); }
Creo que se entiende perfectamente con los comentarios añadidos en la clase así que no añadiré nada más.
3.1.3 Definimos el recurso para empresas
Vamos a definir la respuesta de los servicios de nuestra API para empresas, así organizamos estructuralmente la devolución de registros, esto lo hacemos en la clase CmEnterpriseResource dentro de app/HTTP/Resources
namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class CmEnterpriseResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable */ public function toArray($request) { return [ 'enterprise' => $this->enterprise, 'description ' => $this->description, 'contact ' => $this->contact, 'estate ' => $this->estate, 'elanguage ' => $this->elanguage, 'country ' => $this->country, 'currency ' => $this->currency ]; } }
Como se puede ver a partir de los parámetros que se pasan en el $request organizamos la respuesta en formato Json.
3.1.4. Definimos la ruta para la API
La razón de definir las rutas de la API antes de desarrollar el controlador es simplemente para que podamos ir probando paso a paso según vayamos mostrando la implementación de cada método, recordad que esto es posible ya que en la creación de clases con artisan le hemos indicado que nos cree los métodos básicos en el controlador esto nos permite avanzar y crear las rutas de la API de forma general.
Para ello vamos a la carpeta routes, aquí encontraremos inicialmente 4 ficheros para la definición diferenciada de las rutas:
- api.php: definición de rutas para la API.
- channels.php : definición de rutas para los canales implementados.
- console.php : definiciíon de rutas para consola.
- web.php : definición de rutas para la web.
En nuestro caso abrimos api.php y completamos con el acceso a todas las rutas:
// Añadimos la ruta al controlador que utilizaremos para la ruta use App\Http\Controllers\API\CmEnterpriseController; //Añadimos el acceso a todas las rutas estándar CRUD Route::resource('enterprises', CmEnterpriseController::class);
3.1.5 Definimos el controlador CRUD para API
Ahora ya solo nos queda definir el controlador en este punto sería conveniente desarrollar los test con PHPUnit para el controlar y todos sus métodos, siguiendo los principios TDD estos los deberíamos desarrollar antes de la implementación del controlador. En este caso, para centrarnos en el desarrollo no publicaremos la parte de test.
Los métodos que por defecto hemos creado en la clase y que se corresponden a las operaciones CRUD son:
- index(): listado de todas las empresas.
- store(Request $request): creación de una nueva empresa.
- show($id): consulta de datos de una empresa.
- update(Request $request, $id): actualización de los datos de una empresa.
- destroy($id): eliminación de una empresa.
Veámoslos paso a paso, lo que si iremos proporcionando son datos para hacer las pruebas funcionales con Insomnia, empecemos:
3.1.5.1 Creación de registros: store(Request $request)
Para crear un nuevo registro lo primero que haremos es obtener del $request los datos para el nuevo registro, introduciremos con un Validator una serie de comprobaciones de integridad y si estas son correctas finalmente crearemos el registro con las utilidades que nos proporcionar el modelo de Laravel que hemos creado.
Antes de empezar añadimos las siguientes importaciones en la clase y que vamos a utilizar ya:
use App\Models\CmEnterprise; use App\Http\Resources\CmEnterpriseResource; use Validator;
Estamos listos ya para mostraros el desarrollo del método store:
/** * Store a newly created resource in storage. * Crea un nuevo registro en la base de datos. * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $inputEnterprise = $request->all(); $validator = Validator::make($inputEnterprise, [ 'enterprise' => ['required', 'unique:cm_enterprise', 'max:255'], 'description' => ['required'], 'contact' => ['required', 'max:25'], ]); if($validator->fails()){ $response = [ 'success' => false, 'message' => 'Validation Error.' ]; if(!empty($validator->errors())){ $response['data'] = $validator->errors(); } return response()->json($response, 404); } $enterprise = CmEnterprise::create($inputEnterprise); $message = 'Empresa creada correctamente.'; $response = [ 'success' => true, 'data' => new CmEnterpriseResource($enterprise), 'message' => $message, ]; return response()->json($response, 201); }
Veamos algunas cosas pormenorizadas para aquella gente que este empezando:
- $inputEnterprise = $request->all();
- Validator
- Si falla devolvemos una respuesta json acorde: response()->json($response, 404);
- Si todo está Ok seguimos para la creación.
- CmEnterprise::create($inputEnterprise)
- response()->json($response, 201): devolvemos la respuesta creada, indicando el valor correcto de respuesta para la creación de un registro: 201
Ahora utilizamos Insomnia como se puede ver en la imagen para construir una llamada y hacer un test funcional, también para ir añadiendo registros a la base de datos e ir haciendo pruebas:
3.1.5.2 Listado de empresas: index()
Una vez que ya tenemos registros vamos a crear el método que nos devolverá todos los registros de empresas de la base de datos.
Veamos la implementación del método y después lo explicamos:
/** * Display a listing of the resource. * Mostramos el listado de los regitros solicitados. * @return \Illuminate\Http\Response */ public function index() { $enterprises = CmEnterprise::all(); $message = 'Empresas obtenidos correctamente'; $response = [ 'success' => true, 'data' => CmEnterpriseResource::collection($enterprises), 'message' => $message, ]; return response()->json($response, 200); }
Esta parte es sencilla simplemente utilizamos el modelo para obtener todos los registros de empresa y después con los datos creamos la respuesta de la API:
- $enterprises = CmEnterprise::all() : obtenemos todos los registros de las empresas.
- ‘data’ => CmEnterpriseResource::collection($enterprises) : al construir la respuesta en response utilizamos CmEnterpriseResource para devolver la colección de empresas con el formato definido.
En mi caso ya he añadido múltiples registros de empresas a la base de datos comprobemos con Insomina si nos devuelve correctamente los datos:
3.1.5.3 Mostramos el registro seleccionado por id: show($id)
Para un id especificado en la ruta devolvemos sus datos, utilizaremos de nuevo las utilidades del modelo para devolver el registro , buscado por id, sino se encuentra una empresa devolvemos 404 (not found), veamos como:
/** * Display the specified resource. * Muestra el registro solicitado * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { $enterprise = CmEnterprise::find($id); if (is_null($enterprise)) { return response()->json( $response = [ 'success' => false, 'message' => 'No se ha encontrado la empresa.' ], 404); } $message = 'Empresa encontrada.'; $response = [ 'success' => true, 'data' => new CmEnterpriseResource($enterprise), 'message' => $message, ]; return response()->json($response, 200); }
Para el id pasado como parámetro en la consulta utilizamos el método find() que precisamente busca por id:
- $enterprise = CmEnterprise::find($id): utilzamos el método find que nos busca por id, otros métodos disponibles los puedes consultar en Eloquent Collections Available methods
- ‘data’ => new CmEnterpriseResource($enterprise): utilizamos CmResource para devolver siempre los registros de la forma definida.
- return response()->json($response, 200): devolvemos la respuesta en formato Json y con el código 200.
Probamos en Insomnia por ejemplo para el registro con identerprise igual a 4, veamos el resultado:
3.1.5.4 Creación de registros: update(Request $request, $id)
Al tener ya creados registros vamos a programar la actualización con el método update(Request $request, $id) , vemos el código y después lo comentamos:
/** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { $enterprise = CmEnterprise::find($id); if (is_null($enterprise)) { return response()->json( $response = [ 'success' => false, 'message' => 'Empresa no encontrado.' ], 404); } else { $inputEnterprise = $request->all(); $validator = Validator::make($inputEnterprise, [ 'enterprise' => ['required', 'unique:cm_enterprise', 'max:255'], 'description' => ['required'], 'contact' => ['required', 'max:25'], ]); if($validator->fails()){ $response = [ 'success' => false, 'dataIn' => $inputEnterprise, 'id-pasado' =>$id, 'message' => 'Validation Error.' ]; if(!empty($validator->errors())){ $response['data'] = $validator->errors(); } return response()->json($response, 404); } $enterprise->enterprise = $request->input('enterprise'); $enterprise->description = $request->input('description'); $enterprise->contact = $request->input('contact'); $enterprise->estate = $request->input('estate'); $enterprise->elanguage = $request->input('elanguage'); $enterprise->country = $request->input('country'); $enterprise->currency = $request->input('currency'); $enterprise->save(); $message = 'Empresa actualizada.'; $response = [ 'success' => true, 'data' => new CmEnterpriseResource($enterprise), 'message' => $message, ]; return response()->json($response, 200); } }
A diferencia del método store donde directamente introducíamos los datos pasados como parámetros directamente en el modelo para su creación, ahora recogemos los datos del request para validarlos y después actualizar el registro ya existente:
- $enterprise = CmEnterprise::find($id): buscamos la empresa a actualizar por el id que se pasa como paŕametro, si no existe damos un error 404.
- $validator = Validator::make($inputEnterprise, …): recogemos todos los parámetros del request directamente en la variable $inputEnterprise, si los campos a actualizar no cumplen las reglas de validación damos un error 404. En las reglas de validación utilizamos las siguintes prefedefinidas (también se pueden crear reglas personalizadas):
- required: campo obligatorio.
- unique:cm_enterprise: el campo con esta regla tiene que tener un valor único en la tabla.
- max:valor: el valor máximos de caractéres es el indicado en valor.
- Si todo va bien en la validación actualizamos, en este caso nos falta comprobar que los campos no obligatorios vienen con información ya que sino el código dará un error al no encontrar el parámetro.
- Propuesta de mejoras:
- Añadir reglas para los campos obligatorios de base de datos.
- Si los campos están vacíos mantener el valor que tienen actualmente.
- Si el valor de empresa es el que tiene el propio registro no tenerlo en cuenta en la validación.
- Propuesta de mejoras:
- $enterprise->save(): utilizamos los modelos que nos proporciona el modelo para actualizar el registro.
- $response: devolvemos los datos del registro actualizado más el código de respuesta 200.
3.1.5.5 Eliminar registro: destroy($id)
Finalmente nos queda ver como eliminar los registros, en este caso después de comprobar la existencia de la empresa a eliminar, estableceremos una comprobación más de que la empresa no tiene clientes definidos, ahora mismo como no tenemos definido el modelo para los clientes, dejamos la condición con $numCustomers fija a cero, esto lo podremos cambiar con una consulta sobre el modelo CmCustomer con la condición de la empresa que queremos eliminar.
El código que presentamos es el siguiente:
/** * Remove the specified resource from storage. * Eliminamos el registro especificado por id. * @param int $id * @return \Illuminate\Http\Response */ public function destroy($id) { $enterprise = CmEnterprise::find($id); if (is_null($enterprise)) { return response()->json( $response = [ 'success' => false, 'message' => 'Empresa no encontrada para eliminar.' ], 404); } else { $lenterprise = $enterprise->enterprise; $numCustomers = 0; // Necesitamos comprobar la integridad del borrado, si se utiliza en clientes no se puede borrar. if ($numCustomers > 0) { return response()->json( $response = [ 'success' => false, 'message' => 'La empresa '.$lenterprise.' no se puede borrar porque está siendo utilizado en clientes', ], 304); } else { $enterprise->delete(); $response = [ 'success' => true, 'message' => 'La empresa '.$lenterprise.' se ha borrado correctamente', ]; return response()->json($response, 204); } } }
Las comprobaciones son similares a las realizadas en otros métodos, en este caso simplemente una vez encontrado el registro utilizamos el método delete() del modelo para eliminarlo.
Hasta aquí llegamos con el desarrollo de la API Rest CRUD para empresas, hemos dejado varias mejoras en los métodos para que utilicéis como práctica.
En el siguiente punto desarrollaremos la API Rest CRUD para clientes, próximante …..