3 pts
Vamos a seguir desarrollando nuestra API de películas. La idea es construir una arquitectura por capas similar a la siguiente:
Nuestra capa de presentación contendrán los controladores, que serán los encargados de tratar las peticiones y las respuestas. En la capa de negocio estarán los servicios que se encargarán de conectar con la capa de persistencia para tratar los datos. Por último, la capa de persistencia será la encargada de conectar con nuestros datos.
Vamos a añadir varias carpetas y archivos a nuestra estructura original:
/api /routes api.php /src /controllers //Controladores de nuestra API MoviesController.php //Controlador de películas /DAO //Objetos de acceso a datos /impl //Implementaciones de las interfaces DAO MoviesStaticDAO.php //Implementación con datos estáticos IMoviesDAO.php //Interfaz IMoviesDAO /data //Archivos de datos (en nuestro caso, en formato JSON) /DTO //Objetos de transferencia de datos MovieDTO.php //DTO de películas /services //Servicios de nuestra aplicación /impl //Implementaciones de las interfaces Services MoviesService.php //Implementación de la interfaz IMoviesService IMoviesService.php //Interfaz IMoviesService App.php helpers.php /vendor composer.json composer.lock index.php /html /api .htaccess index.php
La idea es crear controladores que se encargarán de llamar al servicio correspondiente según la petición. Este servicio llamará al DAO (Data Acces Object) que requiera para tratar los datos. Los DAO serán los encargados de trabajar con los datos que tengamos almacenados (de forma estática, en ficheros, en bbdd…). Para transportar los datos entre las capas, usamos los DTO (Data Transfer Object).
Por ahora, vamos a crear solo un controlador para las películas. En esta clase, crearemos 2 métodos y su constructor. Los métodos serán all(), que devolverá todas las películas, y find(), que devolverá el detalle de una película por su id.
Si te acuerdas, en la primera práctica modificamos el archivo composer.json para autocargar nuestras clases:
{ "name": "cesar/api", "autoload": { "psr-4": { "App\\": "src/" }, "files": ["src/helpers.php"] }, "authors": [ { "name": "César Guijarro", "email": "cguijarro@fpmislata.com" } ], "require": { "bramus/router": "^1.6", "larapack/dd": "^1.1" } }
La sección psr-4 la podemos utilizar para indicar al autoload del Composer en que carpeta tiene que buscar nuestros archivos de clases según su Namespace. En nuestro caso, le decimos que todos los Namespaces que empiecen con App tiene que buscarlos en la carpeta src.
Además, tenemos que hacer algunos cambios en nuestro archivo de rutas. En la primera práctica usábamos funciones para devolver el resultado según la ruta. Con la librería bramus/router también podemos indicarle a la ruta que cargue una acción de un controlador. Vamos a probar su funcionamiento:
$router = new \Bramus\Router\Router(); $router->setNamespace('\App'); /** * Definimos nuestras rutas */ $router->get('/', function() { echo "Bienvenido a la API de películas"; }); $router->get('/peliculas', 'controllers\MoviesController@all'); $router->get('/peliculas/(\d+)', 'controllers\MoviesController@find'); $router->run();
Como ves, estamos indicando que cuando accedamos a la ruta miweb.com/api/peliculas ejecute el método all() del controlador MoviesController y cuando accedamos a miweb.com/api/peliculas/ # ejecute el método find().
Vamos a crear ahora nuestra clase MoviesController con los métodos anteriores (por ahora, nos olvidamos del constructor y de los servicios):
namespace App\controllers; class MoviesController { public function all(){ echo "Listado de todas las películas"; } public function find($id){ echo "Detalle de la película con id $id"; } }
Si pruebas ahora, debería mostrarte la frase correspondiente en cada una de las rutas.
Vamos a crear ahora nuestro DTO de películas para poder transportar datos con el mismo formato entre las capas. Esta será una clase muy sencilla con una particularidad: implementará la interfaz JsonSerializable de PHP para poder hacer la transformación en formato JSON de forma sencilla:
namespace App\DTO; use JsonSerializable; class MovieDTO implements JsonSerializable{ /** * @param $id int * @param $titulo string * @param $anyo int * @param $duracion int */ function __construct(private int $id, private string $titulo, private int $anyo, private int $duracion) { $this->id = $id; $this->titulo = $titulo; $this->anyo = $anyo; $this->duracion = $duracion; } /** * @return int */ public function id(): int { return $this->id; } /** * @return string */ public function titulo(): string { return $this->titulo; } /** * @return int */ public function anyo(): int { return $this->anyo; } /** * @return int */ public function duracion(): int { return $this->duracion; } /** * Specify data which should be serialized to JSON * Serializes the object to a value that can be serialized natively by json_encode(). * * @return mixed Returns data which can be serialized by json_encode(), which is a value of any type other than a resource . */ function jsonSerialize(): mixed { return [ 'id' => $this->id, 'titulo' => $this->titulo, 'anyo' => $this->anyo, 'duracion' => $this->duracion ]; } }
Como ves, solo tiene los atributos de las películas y sus getters (por comodidad, en lugar de llamar a los métodos getTitulo(), por ejemplo, le llamamos titulo() directamente, aunque es un getter normal y corriente). En el método jsonSerialize() indicamos que formato queremos que tenga nuestro DTO cuando se transforme a JSON.
El siguiente paso será incorporar los servicios. Lo primero será crear una interfaz por cada entidad (películas, actores…) que queramos manejar. Por ahora, vamos a crear solo un servicio para las películas:
namespace App\services; use App\DTO\MovieDTO; interface IMoviesService { public function all(): array; public function find($id): MovieDTO; }
Nuestra interfaz tendrá dos métodos que todos los servicios tienen que implementar: all(), que devolverá un array y fin(), que devolverá un objeto de tipo MovieDTO.
Vamos a crear ahora la implementación de nuestra interfaz de IMoviesService. Por ahora, devolveremos los datos desde el código (sin usar ningún DAO) para comprobar su funcionamiento. Para eso, crearemos una propiedad privada $movies con el listado de las películas:
namespace App\services\impl; use App\services\IMoviesService; use App\DTO\MovieDTO; class MoviesService implements IMoviesService { private $movies = [ array( "id" => 1, "titulo" => "El padrino", "anyo" => 1972, "duracion" => 175 ) , array( "id" => 2, "titulo" => "El padrino 2", "anyo" => 1974, "duracion" => 200 ) , array( "id" => 3, "titulo" => "Senderos de gloria", "anyo" => 1957, "duracion" => 86 ) , array( "id" => 4, "titulo" => "Primera plana", "anyo" => 1974, "duracion" => 105 ) ]; public function all(): array { $result = array(); foreach ($this->movies as $movie) { array_push($result, new MovieDTO( $movie['id'], $movie['titulo'], $movie['anyo'], $movie['duracion'] ) ); } return $result; } /** * * @param mixed $id * * @return \App\DTO\MovieDTO */ function find($id): MovieDTO { //@TODO } }
En la función all(), recorremos el array $movies y vamos añadiendo al array $result nuevos objetos DTO que vamos creando por cada película. Por último, devolvemos el resultado.
Vamos a comprobar que todo funciona como toca. Tenemos que cambiar el código de nuestro controlador para indicar que utilice el servicio recién creado. Para eso, creamos un constructor donde le indicamos el tipo de servicio que vamos a utilizar:
namespace App\controllers; use App\services\impl\MoviesService; use App\services\IMoviesService; class MoviesController { private IMoviesService $service; function __construct() { $this->service = new MoviesService(); } public function all(){ echo json_encode($this->service->all()); } public function find($id){ echo "Detalle de la película con id $id"; } }
Comprueba que ahora, cuando accedes a la ruta miweb.com/api/peliculas te devuelve el listado de todas las películas.
Ampliación 1 (1 pt)
Implementa el método find() de la clase MoviesService para que devuelva los datos de la película con el id pasado. Haz que el controlador utilice ese método del servicio.
Ampliación 2 (1 pt)
Haz que el método find() lance una nueva excepción con el mensaje No se ha encontrado la película si el id no existe.