Actualizando la versión de PHP al proyecto Pokedex.
Tags: php, development, webdev, programming
Published: 2023-05-26 06:05:58

Hace ya tiempo, hice una publicación en la cual desarrolle un Pokedex con simple y llano PHP, solo usando ciertas librerías que sirvieron como herramientas para agregar funcionalidad al proyecto. 

Como contexto general, es como un mini framework donde solo tiene funcionalidad de crear rutas, hacer requests y responses y crear controllers.

> El repositorio del proyecto lo puedes consultar aquí:
> [https://github.com/krsrk/pokedex-vanilla-php](https://github.com/krsrk/pokedex-vanilla-php)

Nos vamos a dar a la tarea de actualizar la versión de PHP a la mas actual, que es la 8.2(actualmente la versión que maneja el pokedex es la 7.2). 

Después de actualizar las librerías y la versión de PHP, refactorizaremos código basados en las nuevas características de la versión 8.2 de PHP.

## Pre requisitos

- Tener **PHP 8.2** instalado en CLI
- Tener **GIT** instalado en CLI
- Tener la versión mas actual de **composer** instalado en CLI

## Manos a la obra

Los primeros pasos que debemos hacer, es actualizar el **composer.json** con las versiones de las librerías mas actuales y la versión o versiones de PHP que vamos a manejar.

Entonces actualizamos nuestro **composer.json** con lo siguiente:

```
"require": {
        "php": "^8.1||^8.2",
        "twig/twig": "^v3.6.0",
        "illuminate/database": "^v10.10.1",
        "vlucas/phpdotenv": "^v5.5.0"
    },
```

Después ejecutamos el siguiente comando:

```
composer install
```
Con estos pasos ya tendríamos actualizadas las librerías que se usaron, así como también la versión de PHP requerida para el proyecto.

> Si quieres revisar estos cambios puedes ver el Pull Request en el siguiente link:
> https://github.com/krsrk/pokedex-vanilla-php/pull/2

## Actualizando código


### Instalando nuestra suite de pruebas

Para este paso necesitaremos la librería de **PEST**, que es una librería o super set de **PHPUnit** que nos ayuda hacer pruebas unitarias y pruebas de integración. Con esto crearemos las pruebas unitarias del actual código(src/Utils) para poder validar y probar los cambios el código actual.

Instalamos la librería:

```
composer require pestphp/pest --dev --with-all-dependencies
```

Despues ejecutamos el siguiente comando:

```
./vendor/bin/pest --init
```

> El comando anterior nos inicializa la configuración de PEST para empezar a crear nuestros tests.

Ahora, probamos la instalación de PEST con el siguiente comando:

```
./vendor/bin/pest
```

### Creando nuestra primer prueba

Ya teniendo PEST instalado, vamos a hacer cambios en la clase Response. Estos cambios aplicarían lo siguiente:

- Implementar el contrato de la clase(crear la interface de la clase Response)
- Agregar tipado de datos en métodos, propiedades y parámetros(esto se puede hacer desde la versión 8.1).
- Refactorización de procesos internos de la clase.

Con estos requerimientos definidos, creamos nuestra prueba:

```
<?php
//tests/Unit/ResponseTest

test('it can create a response object', function (){
    $response = new \Utils\Response();

    expect($response)->toBeObject();
});
```

### Actualizando nuestra clase Response

Como anteriormente mencionamos, el primer requerimiento es hacer el contrato de la clase o interface:

```
<?php

namespace Utils;

interface ResponseInterface
{
    public function setClosure(): void;

    public function send(Request $request): void;

    public function getClosure(): mixed;

    public function json(mixed $dataResponse, int $codeResponse = 200): void;
}
```

Implementamos la interface en nuestra clase Response:

```
class Response implements ResponseInterface
{
  ...
```

Cambiamos la definición de nuestros métodos:

```
// Antes
public function send($request)
{

// Después
public function send(Request $request): void
{
  ...
```

Cambiamos también el constructor de la clase:

```
// Antes
protected $closure;

public function __construct($closure = null)
{
        if(!is_null($closure)) {
            $this->setClosure($closure);
        }
}

// Después
public function __construct(protected mixed $closure = null)
{
        if(!is_null($this->closure)) {
            $this->setClosure();
        }
}
```

Cuando inicializamos la propiedad "closure" en el método "setClosure", se le asignan varios valores de "closure types" que son de tipo string; actualmente están escritos sin ninguna asignación directo en el código, o lo que se conoce en el ambiente dev como valores **"hardcodeados"**, para arreglar esto usaremos Enums con los valores de los closure types:

Creamos la clase Enum para implementar los valores:

```
<?php

namespace Utils\ValueObjects;

enum ClosureTypes: string
{
    case CLOSURE_STRING = 'string';

    case CLOSURE_ARRAY = 'array';

    case CLOSURE_CONTROLLER = 'controller';

    case CLOSURE_FUNCTION = 'function';
}
```

En nuestro metodo "setClosure" cambiamos esos valores:

```
...

if (is_array($closure)) {
    // Antes: $closureType = 'array'

    // Después
    $closureType = ClosureTypes::CLOSURE_ARRAY->value;
}

if (is_callable($closure)) {
    // Antes: $closureType = 'closure'

    // Después
    $closureType = ClosureTypes::CLOSURE_FUNCTION->value;
}
```

Modificamos nuestro archivo de test con lo siguiente:

```
test('it can create a response object with closure', function (){
    $response = new \Utils\Response('Hello World');

    expect($response)->toBeObject();
});

test('it can create a response object with string closure', function (){
    $response = new \Utils\Response('Hello World');
    $closure = $response->getClosure();

    expect($closure['closure_type'])->toBe(\Utils\ValueObjects\ClosureTypes::CLOSURE_STRING->value);
});

test('it can create a response object with array closure', function (){
    $response = new \Utils\Response(['pokemon' => 'Bulbasaur']);
    $closure = $response->getClosure();

    expect($closure['closure_type'])->toBe(\Utils\ValueObjects\ClosureTypes::CLOSURE_ARRAY->value);
});

test('it can create a response object with function closure', function (){
    $response = new \Utils\Response(fn() => 'Hello World');
    $closure = $response->getClosure();

    expect($closure['closure_type'])->toBe(\Utils\ValueObjects\ClosureTypes::CLOSURE_FUNCTION->value);
});

test('it can create a response object with controller closure', function (){
    $response = new \Utils\Response('PokemonController@index');
    $closure = $response->getClosure();

    expect($closure['closure_type'])->toBe(\Utils\ValueObjects\ClosureTypes::CLOSURE_CONTROLLER->value);
});

test('it expects "unknown" if there is an unknown closure', function (){
    $response = new \Utils\Response(1);
    $closure = $response->getClosure();

    expect($closure['closure_type'])->toBe('unknown');
});
```

Ejecutamos nuestro test y deberíamos ver algo como lo siguiente:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2k1i9u7ru5peyw9a9not.png)

### Refactorizando el método send()

Una vez que tengamos nuestros test validados, debemos refactorizar el código de nuestra clase.

Para efectos prácticos lo vamos hacer con el método send(), que es el que se encargará de ejecutar el response dependiendo su tipo de closure(string, array, json, function, controller).

El código original del método es el siguiente:

```
public function send($request)
{
        if ($this->closure['closure_type'] == 'string') {
            echo $this->closure['closure'];
        } elseif ($this->closure['closure_type'] == 'array') {
            echo json_encode($this->closure['closure']);
        } elseif ($this->closure['closure_type'] == 'closure' || $this->closure['closure_type'] == 'controller') {
            $this->_executeClosure($request);
        } else {
            header("HTTP/1.0 404 Not Found");
            exit('<h3>404 - Not Found</h3>');
        }
}
```
Para refactorizar las sentencias **"if"** implementaremos un tipo factory para poder instanciar una clase dependiendo algún parámetro de funcionalidad o comportamiento.

Entonces, si por ejemplo tengo un **closure_type = 'string'** el Factory va instanciar la clase ClosureTypeString y va a ejecutar su método execute().

De esta forma quedaría el código refactorizado del método send():

```
if (is_null(ClosureTypes::tryFrom($this->closure['closure_type']))) {
    header("HTTP/1.0 404 Not Found");
    exit('<h3>404 - Not Found</h3>');
}

ClosureType::from($this->closure['closure_type'], $this->closure['closure'])->execute($request->getParams());
```

Agregamos el test de este método ya refactorizado:

```
test('it can send a string response', function (){
    $_SERVER['REQUEST_URI'] = '/orders/products/1';
    $response = new \Utils\Response('Hello World');
    $response->send(request());
})->expectNotToPerformAssertions();

test('it can send an array response', function (){
    $_SERVER['REQUEST_URI'] = '/orders/products/1';
    $response = new \Utils\Response(['message' => 'Hello World']);
    $response->send(request());
})->expectNotToPerformAssertions();

...

```

Ejecutamos las pruebas y vemos que todas pasaron:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qc1g8fu98kxb8s5y9vv5.png)

Ahora, iniciamos el servidor y ejecutamos nuestro proyecto con alguna ruta que venga registrada en el **routes/web.php**, deberíamos de ver lo siguiente:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tp47hb20iplkva91p1ig.png)


Ejecutamos todas nuestras pruebas con el siguiente comando:

```
 ./vendor/bin/pest
```

Y deberíamos de ver algo como esto:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o04sdfjh4uy4ey37ssbm.png)

Con estos pasos, podríamos decir, que tenemos cubiertos los cambios al integrar las pruebas al código. Es muy importante, que en cambios mayores al código se tengan integradas este tipo de pruebas, ya que estas son las que nos pueden salvar de varios dolores de cabeza y ahorrar tiempo a la hora de hacer algún deploy.

Los cambios los puedes consultar en el siguiente PR:

https://github.com/krsrk/pokedex-vanilla-php/pull/5