Índice de contenido
El proceso de upload o carga de archivos consiste en transferir uno o varios ficheros desde un dispositivo local hacia un servidor remoto. A grandes rasgos, este proceso se divide en dos pasos fundamentales:
- El usuario selecciona el archivo que desea subir y lo envía al servidor mediante un formulario o una petición HTTP.
- El servidor recibe el archivo, lo valida y lo almacena en una ubicación específica dentro del proyecto.
Una vez que tenemos nuestro formulario de tipo POST en CodeIgniter 4, estamos listos para implementar la lógica del upload en el servidor.
En una aplicación web, el proceso de carga de archivos se puede implementar de dos maneras: mediante un formulario HTML nativo usando el campo <input type="file">, o mediante plugins y librerías que envían peticiones HTTP de forma asíncrona (AJAX). Ambos enfoques son perfectamente válidos; la elección depende de la experiencia de usuario que quieras ofrecer.
El flujo completo de CodeIgniter 4 para manejar el upload de archivos se puede dividir en los siguientes bloques:
- Verificar que la petición contiene el archivo cargado mediante
$request->getFile(). - Verificar que el archivo es válido y que aún no ha sido movido, usando
isValid()yhasMoved(). - Validar que el archivo cumpla con tus criterios: tipo MIME, peso máximo, extensión permitida, etc. Por ejemplo, si se espera una imagen, solo deberías aceptar
image/jpg,image/gifoimage/png. - Mover el archivo a una carpeta de destino dentro del proyecto mediante el método
move(). Las opciones más comunes son la carpetawriteable/(no accesible por HTTP, más segura) o la carpetapublic/(accesible directamente desde el navegador). - Finalmente, dependiendo del resultado: si el upload fue exitoso, guardamos la referencia al archivo (por ejemplo, su nombre) en la base de datos mediante el modelo correspondiente; si hubo un error, lo mostramos al usuario y detenemos el flujo.
A continuación te muestro la función privada _upload() que encapsula toda esta lógica:
private function _upload($id)
{
if($imageFile = $this->request->getFile('image')){
if($imageFile->isValid() && !$imageFile->hasMoved()){
// validaciones
$validated = $this->validate([
'image' => [
'uploaded[image]',
'mime_in[image,image/jpg,image/gif,image/png]'
]
]);
if($validated){
echo "ok";
$newName = $imageFile->getRandomName();
//$imageFile->move(WRITEPATH . 'uploads/avatar/', $newName);
$imageFile->move(ROOTPATH.'public/uploads', $newName);
$personModel = new PersonModel();
$personModel->update($id, [
'image' => $newName
]);
return null;
}else {
return $this->validator->getError("image");
}
}
}
}Esta función retorna
nullsi la carga se realizó correctamente, o el primer mensaje de error encontrado durante el proceso de upload. Esto nos permite consumirla de forma sencilla en cualquier método del controlador mediante un simple condicional.
A continuación te muestro cómo consumir la función _upload() dentro de los métodos create() y update(), que forman parte del CRUD en CodeIgniter 4.
Es importante destacar el orden de las operaciones: primero insertamos o actualizamos los datos en la base de datos para obtener el $id del registro, y después ejecutamos el upload pasando ese identificador como argumento. Esto garantiza que siempre tengamos una referencia válida a la que asociar el archivo cargado. Además, observa la bifurcación con los condicionales: si la persona se creó correctamente pero el upload falló, redirigimos al formulario de edición para que el usuario pueda reintentar la carga sin perder sus datos:
public function create()
{
$person = new PersonModel();
if ($this->validate('person')) {
$id = $person->insert(
[
'name' => $this->request->getPost('name'),
'surname' => $this->request->getPost('surname'),
'age' => $this->request->getPost('age'),
'description' => $this->request->getPost('description'),
]
);
$res = $this->_upload($id);
if($res == null) {
return redirect()->to('/person')->with('message', 'Persona creada exitosamente' );
}else{
return redirect()->to("/person/$id/edit")->withInput();
}
return redirect()->to('/person')->with('message', 'Persona creada exitosamente');
}
// tenemos problemas con las validaciones
return redirect()->back()->withInput();
}
public function update($id)
{
$personModel = new PersonModel();
$person = $personModel->asObject()->find($id);
if ($person == null) {
throw PageNotFoundException::forPageNotFound();
}
if ($this->validate('person')) {
$personModel->update(
$id,
[
'name' => $this->request->getPost('name'),
'surname' => $this->request->getPost('surname'),
'age' => $this->request->getPost('age'),
'description' => $this->request->getPost('description'),
]
);
$res = $this->_upload($id);
if($res == null) {
return redirect()->to('/person')->with('message', 'Persona actualizada exitosamente: ' . $person->name);
}
return redirect()->back()->withInput();
}
// tenemos problemas con las validaciones
//return redirect()->back()->withInput();
}Como puedes ver, llamamos a la función
_upload()una vez que ya tenemos referencia al recurso creado o actualizado. En este caso de estudio, ese recurso es una persona; pero el patrón es exactamente el mismo para cualquier otra entidad de tu aplicación.
Puedes encontrar el código completo de la aplicación —incluyendo las vistas y las rutas definidas— en mi repositorio de GitHub.
Funciones clave del objeto UploadedFile en CodeIgniter 4
La clase UploadedFile de CodeIgniter 4 expone varios métodos útiles que te permiten controlar con precisión cómo se nombra y manipula el archivo cargado. Dependiendo del propósito de la carga, puedes necesitar uno u otro:
Para obtener el nombre original del archivo tal como fue enviado por el usuario:
getName()Si necesitas modificar ese nombre antes de guardarlo —por ejemplo, convertirlo a minúsculas para evitar problemas de case-sensitivity en sistemas Linux—, puedes combinar los métodos de PHP con los del framework sin ningún problema:
strtolower($imagefile->getName())Para generar un nombre aleatorio y único, evitando colisiones entre archivos que compartan el mismo nombre original:
getRandomName()Para obtener la extensión del archivo de forma segura, sin confiar ciegamente en lo que el usuario envía:
guessExtension()Vale la pena destacar que se recomienda usar guessExtension() en lugar de leer la extensión directamente del nombre original del archivo. Este método la determina a partir del tipo MIME real detectado por el servidor, lo que añade una capa adicional de seguridad contra archivos renombrados maliciosamente (por ejemplo, un script .php renombrado a .jpg).
Cómo mostrar imágenes cargadas en CodeIgniter 4: dos enfoques

Cargar un archivo es solo la mitad del trabajo: en la gran mayoría de casos, también necesitarás mostrarlo al usuario. Aquí es donde CodeIgniter 4 requiere algo más de reflexión. En Laravel, por ejemplo, existe el sistema de discos (Storage) que abstrae toda esta gestión; en CodeIgniter 4, al ser un framework más ligero y minimalista, no disponemos de esa capa de abstracción lista para usar. Por eso, a continuación te explico los dos enfoques más comunes para resolver este problema.
El problema de la carpeta writeable/
Siguiendo la documentación oficial, CodeIgniter 4 sugiere guardar los archivos cargados dentro de la carpeta writeable/, que es la única con permisos de escritura habilitados por defecto. El inconveniente es que esta carpeta no es accesible mediante HTTP desde el navegador, por lo que no puedes apuntar a ella directamente con una etiqueta <img src="...">. Para resolverlo, tienes dos caminos:
Enfoque 1: servir el archivo mediante código PHP en el servidor
Este enfoque consiste en crear una ruta y un método en el controlador que lea el archivo desde la carpeta writeable/ y lo sirva directamente al navegador, estableciendo los headers HTTP correctos y usando la función fpassthru() de PHP. Es la opción más segura ya que el archivo nunca es accesible de forma directa desde la web:
function image($movieId = null, $image = null)
{
// abre el archivo en modo binario
if (!$movieId) { // $movieId== null
$movieId = $this->request->getGet('movie_id');
}
if (!$image) { // $image== null
$image = $this->request->getGet('image');
}
if ($movieId == '' || $image == '') {
throw PageNotFoundException::forPageNotFound();
}
$name = WRITEPATH . 'uploads/movies/' . $movieId . '/' . $image;
if (!file_exists($name)) {
throw PageNotFoundException::forPageNotFound();
}
$fp = fopen($name, 'rb');
// envía las cabeceras correctas
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));
// vuelca la imagen y detiene el script
fpassthru($fp);
exit;
}Enfoque 2: mover los archivos directamente a la carpeta public/
La segunda opción —y la más sencilla de implementar— es mover los archivos cargados directamente a la carpeta public/, que es la raíz pública del proyecto y sí es accesible mediante HTTP. Esto te permite construir la URL del archivo de forma directa sin necesidad de ningún método intermedio:
$newName = $imageFile->getRandomName();
//$imageFile->move(WRITEPATH . 'uploads/avatar/', $newName);
$imageFile->move(ROOTPATH.'public/uploads', $newName);
$personModel = new PersonModel();
$personModel->update($id, [
'image' => $newName
]);Sin embargo, hay que tener en cuenta una importante consideración de seguridad: al utilizar la carpeta public/, estás rompiendo parcialmente el esquema de protección del framework. Esta carpeta contiene el index.php de arranque de CodeIgniter y, al ser accesible por HTTP, cualquier archivo ejecutable que un usuario malintencionado logre subir podría ser invocado directamente desde el servidor.
Por este motivo, si decides usar la carpeta public/, asegúrate de aplicar validaciones estrictas sobre el tipo MIME real del archivo usando guessExtension(), y considera agregar reglas en el servidor web —ya sea en el .htaccess de Apache o en la configuración de Nginx— para bloquear la ejecución de scripts dentro de tu subcarpeta de uploads. La carpeta writeable/, en cambio, es intrínsecamente más segura porque está fuera del document root y no puede ser accedida de forma directa a través de una URL.
Siguiente paso: crear un listado paginado de registros en Bootstrap 5 con CodeIgniter 4.