Content Index
The file upload or file loading process consists of transferring one or more files from a local device to a remote server. Broadly speaking, this process is divided into two fundamental steps:
- The user selects the file they wish to upload and sends it to the server using a form or an HTTP request.
- The server receives the file, validates it, and stores it in a specific location within the project.
Once we have our POST form in CodeIgniter 4, we are ready to implement the upload logic on the server.
In a web application, the file upload process can be implemented in two ways: via a native HTML form using the <input type="file"> field, or via plugins and libraries that send HTTP requests asynchronously (AJAX). Both approaches are perfectly valid; the choice depends on the user experience you want to offer.
The complete CodeIgniter 4 workflow for handling file uploads can be divided into the following blocks:
- Verify that the request contains the uploaded file using
$request->getFile(). - Verify that the file is valid and has not been moved yet, using
isValid()andhasMoved(). - Validate that the file meets your criteria: MIME type, maximum weight, allowed extension, etc. For example, if an image is expected, you should only accept
image/jpg,image/gif, orimage/png. - Move the file to a destination folder within the project using the
move()method. The most common options are thewriteable/folder (not accessible via HTTP, safer) or thepublic/folder (directly accessible from the browser). - Finally, depending on the outcome: if the upload was successful, we save the file reference (e.g., its name) in the database using the corresponding model; if there was an error, we display it to the user and halt the workflow.
Below I show you the private function _upload() that encapsulates all this logic:
private function _upload($id)
{
if($imageFile = $this->request->getFile('image')){
if($imageFile->isValid() && !$imageFile->hasMoved()){
// validations
$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");
}
}
}
}This function returns
nullif the upload was completed successfully, or the first error message encountered during the upload process. This allows us to consume it easily in any controller method using a simple conditional.
Below I show you how to consume the _upload() function within the create() and update() methods, which are part of the CRUD in CodeIgniter 4.
It is important to highlight the order of operations: first we insert or update the data in the database to obtain the record's $id, and then we execute the upload passing that identifier as an argument. This ensures that we always have a valid reference to associate the uploaded file with. Furthermore, observe the branching with conditionals: if the person was created successfully but the upload failed, we redirect to the edit form so the user can retry the upload without losing their data:
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', 'Person created successfully' );
}else{
return redirect()->to("/person/$id/edit")->withInput();
}
return redirect()->to('/person')->with('message', 'Person created successfully');
}
// we have issues with validations
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', 'Person updated successfully: ' . $person->name);
}
return redirect()->back()->withInput();
}
// we have issues with validations
//return redirect()->back()->withInput();
}As you can see, we call the
_upload()function once we already have a reference to the created or updated resource. In this case study, that resource is a person; but the pattern is exactly the same for any other entity in your application.
You can find the complete application code—including views and defined routes—in my GitHub repository.
Key functions of the UploadedFile object in CodeIgniter 4
The UploadedFile class in CodeIgniter 4 exposes several useful methods that allow you to precisely control how the uploaded file is named and manipulated. Depending on the purpose of the upload, you may need one or the other:
To get the original file name as sent by the user:
getName()If you need to modify that name before saving it—for example, converting it to lowercase to avoid case-sensitivity issues on Linux systems—you can combine PHP methods with the framework's without any problem:
strtolower($imagefile->getName())To generate a random and unique name, avoiding collisions between files sharing the same original name:
getRandomName()To get the file extension safely, without blindly trusting what the user sends:
guessExtension()It is worth noting that it is recommended to use guessExtension() instead of reading the extension directly from the original file name. This method determines it from the actual MIME type detected by the server, which adds an additional layer of security against maliciously renamed files (for example, a .php script renamed to .jpg).
How to display uploaded images in CodeIgniter 4: two approaches

Uploading a file is only half the work: in the vast majority of cases, you will also need to display it to the user. This is where CodeIgniter 4 requires a bit more thought. In Laravel, for example, there is the disk system (Storage) that abstracts all this management; in CodeIgniter 4, being a lighter and more minimalist framework, we do not have that ready-to-use abstraction layer. Therefore, below I explain the two most common approaches to solve this problem.
The problem with the writeable/ folder
Following the official documentation, CodeIgniter 4 suggests saving uploaded files inside the writeable/ folder, which is the only one with write permissions enabled by default. The drawback is that this folder is not accessible via HTTP from the browser, so you cannot point to it directly with an <img src="..."> tag. To solve this, you have two paths:
Approach 1: serve the file via PHP code on the server
This approach consists of creating a route and a method in the controller that reads the file from the writeable/ folder and serves it directly to the browser, setting the correct HTTP headers and using PHP's fpassthru() function. It is the safest option since the file is never directly accessible from the web:
function image($movieId = null, $image = null)
{
// opens the file in binary mode
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');
// sends the correct headers
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));
// dumps the image and stops the script
fpassthru($fp);
exit;
}Approach 2: move the files directly to the public/ folder
The second option—and the easiest to implement—is to move the uploaded files directly to the public/ folder, which is the public root of the project and is indeed accessible via HTTP. This allows you to build the file URL directly without needing any intermediate method:
$newName = $imageFile->getRandomName();
//$imageFile->move(WRITEPATH . 'uploads/avatar/', $newName);
$imageFile->move(ROOTPATH.'public/uploads', $newName);
$personModel = new PersonModel();
$personModel->update($id, [
'image' => $newName
]);However, an important security consideration must be kept in mind: by using the public/ folder, you are partially breaking the framework's protection scheme. This folder contains CodeIgniter's bootstrap index.php and, being accessible via HTTP, any executable file that a malicious user manages to upload could be invoked directly from the server.
For this reason, if you decide to use the public/ folder, make sure to apply strict validations on the actual MIME type of the file using guessExtension(), and consider adding rules in the web server—either in Apache's .htaccess or in Nginx's configuration—to block script execution within your uploads subfolder. The writeable/ folder, on the other hand, is inherently safer because it is outside the document root and cannot be accessed directly through a URL.
Next step: create a paginated listing of records in Bootstrap 5 with CodeIgniter 4.