Índice
EL proyecto lo realizaremos en una 8 publicaciones apróximadamente, vamos con el apartado 3.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) (31/01/2022)
- CRUD controller y API controller para los clientes (cm_customer) (07/02/2022)
- Creación del proyecto order app con docker frontend pedidos (12/02/2022)
- RabbitMQ admin y order app , actualización de clientes
- 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
Continúamos con el desarrollo de la API en este caso con el End Point para los clientes, en esta publicación no repetiré las explicaciones realizadas, en la anterior publicación y solo iremos comentando código nuevo que vayamos utilizando.
3.2. CRUD controller y API controller para los clientes (cm_customer)
Vamos a crear el controlador para nuestra API de clientes que se corresponderá a la tabla cm_customer, 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 CmCustomer --controller --migration --resource
Si todo fue bien la respuesta de la consola será la siguiente:
Model created successfully. Created Migration: 2022_01_24_125026_create_cm_customers_table Controller created successfully.
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/CmCustomerController --api docker-compose exec admin-app-lpm php artisan make:resource CmCustomerResource
Con está parte ya hemos creado la estructura para la API de clientes, empecemos a programar.
3.2.1. Definimos la migración para la tabla cm_customer de clientes
La tabla cm_customer la definimos en el diagrama entidad relación con empresas, donde un cliente pertenece a una empresa:
El SQL de la tabla cm_customer que vamos a ver a continuación nos sirve de definición de la tabla en la migración:
CREATE TABLE cm_customer ( idcustomer INT NOT NULL AUTO_INCREMENT, identerprise INT, customer VARCHAR(150) NOT NULL, contact VARCHAR(250), customerstate VARCHAR(30), sale DECIMAL(10,3), paymentmethod VARCHAR(50), country VARCHAR(100), currency VARCHAR(100), elanguage VARCHAR(6), address VARCHAR(500), CONSTRAINT pk_cm_customer PRIMARY KEY (idcustomer), CONSTRAINT fk_cm_customer_identerprise FOREIGN KEY (identerprise) REFERENCES cb_enterprise (identerprise) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT un_cm_customer_cb_enterprise UNIQUE (identerprise, customer) ) ENGINE=InnoDB COMMENT='Tabla donde se almacenarán los clientes de las diferentes empresas, se entiende cliente como aquel que compra a una empresa.'; GRANT ALL ON TABLE cm_customer TO xulescode;
No tiene nada especial, diferentes campo de tipo string y numérico, algunos de ellos obligatorios, destacamos:
- Clave primaria: idcustomer de tipo entero y auto incremental
- Clave foránea sobre empresas: CONSTRAINT fk_cm_customer_identerprise FOREIGN KEY (identerprise), esto lo tenemos que definir en la migración.
- Clave única customer por empresa: es decir un cliente no puede tener el mismo nombre dentro de una empresa.
Aprovechamos está definición para ir viendo cosas nuevas para los que estáis empezando con Laravel como por ejemplo la definición de claves foráneas:
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateCmCustomersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { /** * Tabla donde se almacenarán los clientes de las diferentes empresas, * se entiende cliente como aquel que compra a una empresa. * idcustomer INT NOT NULL AUTO_INCREMENT, * identerprise INT, * customer VARCHAR(150) NOT NULL, * contact VARCHAR(250), * customerstate VARCHAR(30), * sale DECIMAL(10,3), * paymentmethod VARCHAR(50), * country VARCHAR(100), * currency VARCHAR(100), * elanguage VARCHAR(6), * address VARCHAR(500), * CONSTRAINT pk_cm_customer PRIMARY KEY (idcustomer), * CONSTRAINT fk_cm_customer_identerprise FOREIGN KEY (identerprise) * REFERENCES cb_enterprise (identerprise) MATCH SIMPLE * ON UPDATE NO ACTION ON DELETE NO ACTION, * CONSTRAINT un_cm_customer_cb_enterprise UNIQUE (identerprise, customer) */ Schema::create('cm_customer', function (Blueprint $table) { $table->increments('idcustomer'); $table->unsignedInteger('identerprise'); $table->foreign('identerprise')->references('identerprise')->on('cm_enterprise'); $table->string('customer', 150); $table->string('customerstate', 30); $table->string('contact', 250); $table->string('paymentmethod', 50); $table->string('currency', 100); $table->string('country', 100); $table->string('elanguage', 6); $table->string('address', 50); $table->timestamps(); $table->engine = 'InnoDB'; // Specify the table storage engine (MySQL). }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('cm_customer'); } }
Lo único nuevo que añadimos en está definición es la creación de una clave foránea:
- $table->foreign(‘identerprise’)->references(‘identerprise’)->on(‘cm_enterprise’): por una parte primero definimos el campo de la tabla identerprise, y después, indicamos la referecia del campo en la otra tabla (references(‘identerprise’)) y la tabla referenciada: on(‘cm_enterprise’).
Una vez completado el código ya podemos ejecutar la migración tal y como comentamos en la publicación anterior:
$ docker-compose exec admin-app-lpm php artisan migrate Migrating: 2022_01_24_125026_create_cm_customers_table Migrated: 2022_01_24_125026_create_cm_customers_table (30.91ms)
$ docker-compose exec admin-app-lpm php artisan migrate Migrating: 2022_01_24_125026_create_cm_customers_table Migrated: 2022_01_24_125026_create_cm_customers_table (30.91ms)
Si queréis comprobar el resultado podéis acceder al contenedor admin-lpm-db y consultar las tabla y su descripción, veamos cómo:
$ docker exec -it cx-lpm-customerdb-admin-db-001 bash root@b5548d1dfd2a:/# mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 219 Server version: 8.0.26 MySQL Community Server - GPL Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> use customerminidb; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A
Ahora mostramos la definición de la tabla con describe:
mysql> describe cm_customer; +---------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------------+--------------+------+-----+---------+----------------+ | idcustomer | int unsigned | NO | PRI | NULL | auto_increment | | identerprise | int unsigned | NO | MUL | NULL | | | customer | varchar(150) | NO | | NULL | | | customerstate | varchar(30) | NO | | NULL | | | contact | varchar(250) | NO | | NULL | | | paymentmethod | varchar(50) | NO | | NULL | | | currency | varchar(100) | NO | | NULL | | | country | varchar(100) | NO | | NULL | | | elanguage | varchar(6) | NO | | NULL | | | address | varchar(50) | NO | | NULL | | +---------------+--------------+------+-----+---------+----------------+ 10 rows in set (0.00 sec)
3.2.2. Creamos el modelo para la tabla de clientes
Creamos el modelo para la tabla cm_customer (clientes), la definición la haremos en CmCustomer dentro de la carpeta de los modelos app/Models, los campos que definimos en la migración los llevamos a la tabla:
namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class CmCustomer extends Model { use HasFactory; /** * Nombre de la tabla de la base de datos. * Database table name. */ protected $table = 'cm_customer'; /** * 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 = 'idcustomer'; /** * 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( 'idcustomer', 'identerprise', 'customer', 'contact', 'customerstate', 'paymentmethod', 'country', 'currency', 'elanguage', 'address'); }
Aquí no vamos a entrar en detallles ya que la definición ya la explicamos en la publicación anterior para empresas y es lo mismo: definición del modelo de empresas.
3.2.3. Detallamos el recurso para clientes (Resource)
Vamos a definir la respuesta de los servicios de nuestra API para clientes, así organizamos estructuralmente la devolución de registros, esto lo hacemos en la clase CmCustomerResource dentro de app/HTTP/Resources
namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class CmCustomerResource extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'customer' => $this->customer, 'customerstate' => $this->customerstate, 'contact' => $this->contact, 'sale' => $this->sale, 'identerprise' => $this->identerprise, 'paymentmethod' => $this->paymentmethod, 'elanguage' => $this->elanguage, 'currency' => $this->currency, 'country' => $this->country, 'address' => $this->address, ]; } }
3.2.4. Añadimos la ruta para la API
Siguiendo el mismo orden que en la publicación anterior definimos la ruta completa para la API dentro de la carpeta routes, en el fichero api.php de definición de rutas para la API, y completamos con el acceso a todas las rutas CRUD:
// Añadimos la ruta al controlador que utilizaremos para la ruta use App\Http\Controllers\API\CmCustomerController; //Añadimos el acceso a todas las rutas estándar CRUD Route::resource('customers', CmCustomerController::class);
3.2.5. Definimos el controlador CRUD para la Api Rest
Haremos la creación de los métodos en el controlador que creamos para la API Rest App\Http\Controllers\API\CmCustomerController , los métodos que por defecto hemos creado en la clase y que se corresponden a las operaciones CRUD son:
- index(): listado de todos clientes
- store(Request $request): creación de un nuevo cliente.
- show($id): consulta de datos de un cliente.
- update(Request $request, $id): actualización de los datos de un cliente.
- destroy($id): eliminación de un cliente.
Veámoslos paso a paso, lo que si iremos proporcionando son datos para hacer las pruebas funcionales con Insomnia, empecemos:
3.2.5.1 Creación de registros: store(Request $request)
En la creación del nuevo cliente añadimos las validaciones para customer, contact e identerprise, cómo ya explicamos en la publcación anterior, además comprobamos que el identerprise que se pasa como parámetro se corresponde a una empresa dada de alta:
/** * Store a newly created resource in storage. * Creamos un nuevo registro en la base de datos. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { $inputCustomer = $request->all(); $validator = Validator::make($inputCustomer, [ 'customer' => ['required', 'unique:cm_customer', 'max:255'], 'contact' => ['required', 'max:15'], 'identerprise' => ['required'], ]); if($validator->fails()){ $response = [ 'success' => false, 'message' => 'Validation Error.' ]; if(!empty($validator->errors())){ $response['data'] = $validator->errors(); } return response()->json($response, 404); } $enterprise = CmEnterprise::find($inputCustomer['identerprise']); if (!isset($enterprise)) { $response = [ 'success' => false, 'message' => 'Enterprise not exists.' ]; return response()->json($response, 404); } $customer = CmCustomer::create($inputCustomer); $message = 'Cliente creado correctamente.'; $response = [ 'success' => true, 'data' => new CmCustomerResource($customer), 'message' => $message, ]; return response()->json($response, 201); }
El resto es lo que ya vimos si pasamos las validaciones creamos el cliente: $customer = CmCustomer::create($inputCustomer); y devolvemos la respuesta para la creación de registro exitosa.
Veamos el test funcional con Insomnia:
3.2.5.2. Listado de clientes: index()
Para el listado de clientes además de los datos de cada cliente, podríamos devolver también los datos desglosados de la empresa.
/** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { $customers = CmCustomer::all(); $message = 'Clientes obtenidos correctamente'; $response = [ 'success' => true, 'data' => CmCustomerResource::collection($customers), 'message' => $message, ]; return response()->json($response, 200); }
Cómo ya lo habíamos visto no tiene ninguna complejidad, el resultado con Insomnia sería:
Si queremos devolver los datos de empresa modificamos CmCustomerResource de la siguiente manera:
- Importamos las clases que vamos a necesitar:
use App\Models\CmEnterprise; use App\Http\Resources\CmEnterpriseResource;
2. Ahora actualizamos el código de empresa, sustituimos:
'identerprise' => $this->identerprise,
Por:
'identerprise' => new CmEnterpriseResource(CmEnterprise::find($this->identerprise)),
Y listo ahora el resultado devuelto por la API será el siguiente:
3.2.5.3. Consulta de cliente: show($id)
Implementamos ahora el método de lectura para ello codificamos la función show($id) que creamos en la estructura por defecto, todo lo programado ya lo hemos explicado por lo que no tendrás dificultad en entenderlo:
/** * Display the specified resource. * Mostramos el recurso solicitado. * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { $customer = CmCustomer::find($id); if (is_null($customer)) { return response()->json( $response = [ 'success' => false, 'message' => 'No se ha encontrado el cliente.' ], 404); } $message = 'Cliente encontrado.'; $response = [ 'success' => true, 'data' => new CmCustomerResource($customer), 'message' => $message, ]; return response()->json($response, 200); }
Si hacemos la consulta desde Insomnia vemos como ahora el resultado consultado incluye el desglose de empresa:
3.2.5.4. Actualización de cliente: update($id)
Implementamos ahora el método de lectura para ello codificamos la función show($id) que creamos en la estructura por defecto, todo lo programado ya lo hemos explicado por lo que no tendrás dificultad en entenderlo:
/** * Update the specified resource in storage. * Actualizamos los datos del recurso especificado. * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { $customer = CmCustomer::find($id); if (is_null($customer)) { return response()->json( $response = [ 'success' => false, 'message' => 'Cliente no encontrado.' ], 404); } else { $inputCustomer = $request->all(); $validator = Validator::make($inputCustomer, [ 'customer' => ['required', Rule::unique('cm_customer')->ignore($customer)], 'contact' => ['required', 'max:250'], 'identerprise' => ['required'], ]); if($validator->fails()) { $response = [ 'success' => false, 'dataIn' => $inputCustomer, 'id-pasado' =>$id, 'message' => 'Validation Error.' ]; if(!empty($validator->errors())){ $response['data'] = $validator->errors(); } return response()->json($response, 404); } $customer->identerprise = $request->input('identerprise'); $customer->customer = $request->input('customer'); $customer->contact = $request->input('contact'); $customer->customerstate = $request->input('customerstate'); $customer->paymentmethod = $request->input('paymentmethod'); $customer->elanguage = $request->input('elanguage'); $customer->country = $request->input('country'); $customer->currency = $request->input('currency'); $customer->address = $request->input('address'); $customer->save(); $message = 'Cliente actualizado.'; $response = [ 'success' => true, 'data' => new CmCustomerResource($customer), 'message' => $message, ]; return response()->json($response, 200); }
En la actualización de cliente hemos especificado en la validación que si actualizamos el dato no tenga en cuenta el nombre de customer del registro actual para ello utilizamos:
- ‘customer’ => [‘required’, Rule::unique(‘cm_customer’)->ignore($customer)] : si dejamos la validación con la configuración básica que hicimos en store: ‘unique:cm_customer’, cada vez que actualicemos un registro y no cambiemos el valor de customer nos dará un error de validación ya que la comprobación la hace siempre, con la condición para que ignore el registro a actualizar funciona correctamente.
- En esta parte os animo a que hagáis pruebas de funcionamiento y que comprobéis los resultados.
Vamos a mostrar con Insomnia como actualizamos un registro:
3.2.5.5. Eliminamos el cliente: delete($id)
El método de eliminación de cliente consiste en comprobar que existe, podremos añadir comprobaciones extra en el futuro, por ejemplo, establecer un campo nuevo que indique si tiene pedidos hasOrders si los tiene no se podrá eliminar, y finalmente si se cumplen las condiciones eliminar el cliente.
Al igual que en la eliminación de empresa, el código será similar:
/** * Remove the specified resource from storage. * Eliminamos el recurso específicado del almacenamiento. * @param int $id * @return \Illuminate\Http\Response */ public function destroy($idcustomer) { $customer = CmCustomer::find($idcustomer); $lcustomer = $customer->customer; // No podremos borrar el cliente si tiene pedidos asignados (lo veremos en el futuro) $customer->delete(); $response = [ 'success' => true, 'message' => 'El cliente '.$lcustomer.' se ha borrado correctamente', ]; return response()->json($response, 200); }