====== Práctica 3 ====== ** 6 pts** En esta práctica vamos a añadir conexión con una base de datos, y empezaremos a crear un pequeño [[https://es.wikipedia.org/wiki/Asignaci%C3%B3n_objeto-relacional|ORM]]. Lo primero será crea una nueva carpeta en //src// llamada //db//, donde estarán nuestros archivos necesarios para conectarnos con la bbdd y ejecutar sentencias SQL. Dentro de esa carpeta, tendremos una interfaz llamada //IPDOConnection// con el método //connect()//, que devolverá un objeto //PDO//, la carpeta donde implementaremos dicha interfaz y otra carpeta llamada //orm// donde crearemos nuestro pequeño ORM: /src ... /db /impl MysqlPDO.php /orm DB.php QueryBuilder.php IPDOConnection.php ... ===== MysqlPDO ===== Por ahora, solo crearemos una implementación de //IPDOConnection// llamada //MysqlPDO//, que conectará con una bbdd //mariadb// (también puede ser //mysql//). El método //connect()// conectará con la bbdd y devolverá una excepción con el código 500 si no puede conectarse por cualquier motivo. ===== ORM ===== ==== DB ==== Vamos a empezar por crear una clase (//DB//) que nos permita ejecutar sentencias SQL de forma sencilla. Por ahora, crearemos 3 métodos (más adelante iremos ampliando los métodos): //execute()//, que recibirá un //string// con el SQL que queramos ejecutar y los parámetros de la SQL (vamos a utilizar sentencias preparadas de la librería //PDO//). El método devolverá el resultado de una sentencia //SELECT//: private static function execute(string $sql, ?array $params = null):array { $pdo = DBFactory::getConnection()::connect(); $ps = $pdo->prepare($sql); $ps->execute($params); return $ps->fetchAll(\PDO::FETCH_ASSOC); } Nuestro segundo método será //select()//, que usaremos para ejecutar sentencias SELECT que devuelvan varios resultados (los devolveremos en formato //array// de objetos de la clase [[https://www.php.net/manual/es/reserved.classes.php|stdClass]]. Para ello, simplemente deberemos convertir el //array// que nos devuelva el método //execute()// anterior mediante //(object)//: public static function select(string $sql, ?array $params = null):array { $result = array(); $data = self::execute($sql, $params); foreach ($data as $record) { $result[] = ((object) $record); } return $result; } Nuestro último método (//selectOne()//), lo usaremos para devolver un objeto (en lugar de un //array// de objetos como en el método anterior). Nos será útil para ejecutar sentencias SELECT que van a devolvernos un solo resultado: public static function selectOne(string $sql, ?array $params = null): \stdClass { $data = self::execute($sql, $params); if(count($data) > 0) { return (object) $data[0]; } throw new \Exception("Recurso no encontrado", 404); } Para probar el funcionamiento de la clase //DB//, simplemente tenemos que crear un nuevo //DAO// (MoviesDBDAO) y usar sus métodos en //read()// y //findById// (Recuerda que tendrás que cambiar el //DAO// que vamos a utilizar en el //Factory// correspondiente): class MoviesDBDAO implements IMoviesDAO { static function create(MovieDTO $movie): bool { return false; } static function read(): array { $result = array(); $sql = "SELECT * FROM peliculas"; $db_data = DB::select($sql); foreach ($db_data as $movie) { $result[] = new MovieDTO( $movie->id, $movie->titulo, $movie->anyo, $movie->duracion ); } return $result; } static function findById(int $id): MovieDTO { $params = [ "id" => $id ]; $sql = "SELECT * FROM peliculas WHERE id = :id LIMIT 1"; $db_data = DB::selectOne($sql, $params); $result = new MovieDTO( $db_data->id, $db_data->titulo, $db_data->anyo, $db_data->duracion ); return $result; } static function update(int $id, MovieDTO $movie): bool { return false; } static function delete(int $id): bool { return false; } } ==== QueryBuilder ==== Vamos a añadir un poco más de abstracción a nuestras operaciones con la bbdd. Para ello, crearemos la clase //QueryBuilder//, que se encargará de montar las sentencias SQL simples por nosotros. Esta clase tendrá 4 propiedades: //$fields//, que serán los campos de la tabla que queremos recuperar en una sentencia SELECT, //$where// que tendrá la condición de una //SELECT//, //$params//, con los parámetros que vamos a usar en las sentencias preparadas y //$sql//, para mostrar la sentencia que vamos a ejecutar (esto último nos servirá para depurar la aplicación mientras la vamos montando). Además, tendrá una última propiedad (//$table//) que le pasaremos al constructor donde almacenaremos la tabla sobre la que hacer la consulta SQL: lass QueryBuilder { private string $fields = '*'; private string $where = ""; private ?array $params = null; private string $sql; function __construct(private string $table) { $this->table = $table; } Si te fijas, la propiedad //$fields// tiene valor por defecto *, lo que quiere decir que, a no ser que el usuario indique lo contrario, devolveremos todos los campos de la tabla. Para usar nuestra clase, crearemos un nuevo método **en la clase //DB//** llamado //table()// donde le pasaremos el nombre de la tabla sobre la que queremos ejecutar nuestras consultas y nos devolverá un nuevo objeto de la clase //QueryBuilder//: public static function table(string $table):QueryBuilder { return new QueryBuilder($table); } Vamos a crear ahora dos métodos para indicar los campos que queremos devolver (//select()//) y la condición de la sentencia SQL (//where()//). El primer método es muy sencillo, ya que solo modificará la propiedad //$fields// de nuestra clase: public function select(?string $fields = null) { $this->fields = (is_null($fields))? '*': $fields; return $this; } Lo interesante aquí es lo que devuelve el método: //$this// (el propio objeto). Esto nos será útil para poder concatenar métodos y así ir montando nuestras sentencias SQL poco a poco: DB::Table->select('titulo', 'duracion')->where('anyo', '>', 1980).... El método //where()// recibirá 3 parámetros: el campo de la tabla, la condición y el valor: public function where(string $field, string $condition, ?string $value) { if (is_null($value)) { $value = $condition; $condition = '='; } $this->where = "WHERE $field $condition :$field"; $this->params[":$field"] = $value; return $this; } Lo hemos preparado para que si se le pasan solo dos parámetros, la condición por defecto sea //=//. Los siguientes métodos serán //get()//, que devolverá un //array// de resultados de una sentencia SELECT, y //getOne()//, que utilizaremos para los casos donde solo queremos devolver un resultado. Estos métodos montarán la SQL y ejecutarán los métodos correspondientes de la clase //DB// (//select()// o //selectOne()//): public function get():array { $this->sql = "SELECT $this->fields FROM $this->table $this->where"; return DB::select($this->sql, $this->params); } public function getOne():stdClass { $this->sql = "SELECT $this->fields FROM $this->table $this->where LIMIT 1"; return DB::selectOne($this->sql, $this->params); } Vamos a implementar otro método que nos será útil cuando queramos buscar en una tabla por su clave primaria: public function find(int $id) { $this->where('id', '=', $id); return $this->getOne(); } Por último, creamos el método //toSQL()// que nos servirá para mostrar la sentencia que queramos ejecutar (lo podemos usar como ayuda para depurar nuestra aplicación): private function toSql() { dd($this->sql); } Listo, ahora si cambiamos los métodos //read()// y //findById()// en //MoviesDBDAO// usando la nueva clase, debería mostrarnos los resultados correspondientes: static function read(): array { $result = array(); $db_data = DB::table('peliculas')->select('*')->get(); foreach ($db_data as $movie) { $result[] = new MovieDTO( $movie->id, $movie->titulo, $movie->anyo, $movie->duracion ); } return $result; } static function findById(int $id): MovieDTO { $db_data = DB::table('peliculas')->find($id); $result = new MovieDTO( $db_data->id, $db_data->titulo, $db_data->anyo, $db_data->duracion ); return $result; } ===== Inserciones ===== Para permitir las inserciones en nuestra bbdd, vamos a crear un para de métodos más en la clase //DB//: public static function insert(string $sql, array $params): int { return self::executeNoResult($sql, $params); } private static function executeNoResult(string $sql, array $params):int { $pdo = DBFactory::getConnection()::connect(); try { $ps = $pdo->prepare($sql); return $ps->execute($params); } catch (\Throwable $th){ //throw $th; throw new \Exception("Error al insertar el recurso", 400); } } Los métodos son muy sencillo. El segundo (//executeNoResult()//) lo usamos para ejecutar sentencias SQL que no devuelven resultados, y el primero (//insert()//), ejecutará la sentencia INSERT que le pasemos con los parámetros correspondientes. Nuestra clase //DB// ya está lista para ejecutar sentencias INSERT (puedes probarla en el método //create()// de //MoviesDBDAO// para asegurarte que funciona). Como antes, vamos a simplificar el uso de sentencias INSERT creando el correspondiente método en la clase //QueryBuilder//: public function insert(array $data):int { $fieldsParams = ""; foreach ($data as $key => $value) { $fieldsParams .= ":$key,"; $this->params[":$key"] = $value; } $fieldsParams = rtrim($fieldsParams, ','); $fieldsName = implode(",", array_keys($data)); $this->sql = "INSERT INTO $this->table($fieldsName) VALUES ($fieldsParams)"; return DB::insert($this->sql, $this->params); } La única //complicación// en este método es montar la sentencia para usar sentencias con parámetros (... VALUES (:id, :titulo....), aunque el código es sencillo de entender. Una función que nos es muy útil es [[https://www.php.net/manual/es/function.implode.php|implode]], que nos convierte un //array// en un //string// con la separación entre elementos que queramos (en nuestro caso, una coma). Para usar nuestro método, simplemente tendremos que pasarle un //array// asociativo (con las claves del array igual al nombre de los campos de la bbdd) con los valores a insertar: class MoviesDBDAO { .... static function create(MovieDTO $movie): bool { return DB::table('peliculas')->insert(['titulo' => $movie->titulo(), 'anyo' => $movie->anyo(), 'duracion' => $movie->duracion()]); } .... Solo nos queda añadir la ruta correspondiente en nuestro archivo de rutas y un nuevo método en el controlador para poder insertar registros en nuestra bbdd: $router->post('/peliculas', 'controllers\MoviesController@insert'); public function insert() { try { $data = json_decode(file_get_contents('php://input'), true); $movie = new MovieDTO(null, $data['titulo'], $data['anyo'], $data['duracion']); MoviesFactory::getService()::insert($movie); HTTPResponse::json(201, "Recurso creado"); } catch (\Exception $e) { HTTPResponse::json($e->getCode(), $e->getMessage()); } } ===== Ampliaciones ===== **Ampliación 1 (2 pts)** Crea los métodos necesarios para poder hacer actualizaciones y borrados en la bbdd mediante las clases //DB// y //QueryBuilder//. **Ampliación 2 (1 pt)** Averigua qué hacer para poder tener los parámetros de conexión a nuestra bbdd en un archivo //.env// de configuración. **Ampliación 3 (1 pt)** Piensa alguna manera para poder devolver un mensaje de error con código 400 cuando la petición del usuario a la hora de insertar o actualizar un recurso no sea correcta (campos de la tabla que faltan, mal escritos...). Ten en cuenta la reusabilidad del código que implementes.