Índice

EL proyecto lo realizaremos en una 8 publicaciones apróximadamente, vamos con el apartado 3.2 en esta publicación.

  1. Explicación del proyecto: Creación de una aplicación de microservicios con Laravel y RabbitMQ
  2. Creación del proyecto backend ADMIN-APP con Docker Compose : Laravel, Nginx y MySQL
  3. Desarrollamos la API REST de ADMIN-APP para empresas y clientes con Laravel
    1. CRUD controller y API controller para las empresas (cm_enterprise) (31/01/2022)
      1. Definimos la migración para la tabla de empresas
      2. Definimos el modelo para la tabla de empresas
      3. Definimos el recurso para empresas
      4. Definimos la ruta para la API
      5. Definimos el controlador CRUD para API
    2. CRUD controller y API controller para los clientes (cm_customer) (07/02/2022)
      1. Definimos la migración para la tabla de clientes
      2. Creamos el modelo para la tabla de clientes
      3. Detallamos el recurso para clientes
      4. Añadimos la ruta para la API
      5. Definimos el controlador CRUD para la Api Rest
        1. Creación de registros: store(Request $request)
        2. Listado de clientes: index()
        3. Consulta de cliente: show($id)
        4. Actualización de cliente: update($id)
        5. Eliminamos el cliente: delete($id)
  4. Creación del proyecto order app con docker frontend pedidos (12/02/2022)
  5. RabbitMQ admin y order app , actualización de clientes
  6. Creamos la página de entrada de pedidos
  7. RabbitMQ Automatización y colas diferenciadas
  8. 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:

Learning Project Mini - Diagrama ER - ADMIN APP
Learning Project Mini – Diagrama ER – ADMIN APP

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:

ADMIN APP - API - Creación de un cliente (store)
ADMIN APP – API – Creación de un cliente (store)
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:

ADMIN APP - API - Listado de clientes (index)
ADMIN APP – API – Listado de clientes (index)

Si queremos devolver los datos de empresa modificamos CmCustomerResource de la siguiente manera:

  1. 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:

ADMIN APP - API - Listado de clientes con los datos de empresa
ADMIN APP – API – Listado de clientes con los datos de empresa
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:

ADMIN APP - API - Consulta un cliente (show)
ADMIN APP – API – Consulta un cliente (show)

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:

ADMIN APP - API - Actualización de un cliente (update)
ADMIN APP – API – Actualización de un cliente (update)

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);
    }
ADMIN APP - API - Eliminamos un cliente (delete)
ADMIN APP – API – Eliminamos un cliente (delete)