====== Práctica 2a ====== **3 pts** Vamos a seguir desarrollando nuestra API de películas. La idea es construir una arquitectura por capas similar a la siguiente: {{ :clase:daw:dws:1eval:practicas:capas.png?400 |}} 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. En esta práctica vamos a usar una arquitectura por capas, aunque simplificada al máximo posible. Existen diferentes arquitecturas, aunque, en general, casi todas se basan en la separación de componentes por capas. Hoy en día, las más usadas son las llamadas //arquitecturas limpias//, como la //arquitectura hexagonal//. 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 qué usar interfaces? Por ejemplo, en principio, en esta práctica vamos a usar datos estáticos, es decir, nuestra clase //MoviesStaticDAO// devolverá datos que tenga dentro de su código. Más adelante, cambiaremos a datos en archivos JSON (incluso en temas posteriores usaremos datos que están almacenados en bbdd). Aprovechando el polimorfismo de clases, en el código de nuestros servicios llamaremos a la interfaz genérica de DAO, con lo que al cambiar la implementación que usemos, no necesitaremos tocar casi nada de nuestro código (en una de las ampliaciones de esta práctica veremos como tocar el mínimo código al hacer esos cambios mediante el patrón //factory//). ===== Controladores ===== 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. ===== DTO ===== 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 [[https://www.php.net/manual/es/class.jsonserializable.php|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. ===== Servicios ===== 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//. En PHP, al contrario que Java, por ejemplo, no podemos indicar el tipo de datos que tendrá nuestro //array//. Lo suyo sería indicar al método //all()// que tiene que devolver un conjunto de //MovieDTO//. Aunque se puede hacer creando más clases, por simplificar vamos a dejarlo como está. 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. ===== Ampliaciones ===== **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.