CodeIgniter4: CRUD Data for Blog Post Management
Published:
Implementing CRUD (Create, Read, Update, Delete) is an essential part of web application development as it enables dynamic interaction with data stored in the database. In this article, we practice managing blog post data through an admin panel, starting from displaying the list, adding, editing, and deleting data.
This practice continues the MyBlog project from the previous article. We will implement CRUD data to manage the posts table through an admin panel. Topics covered include:
- Creating the admin post (route group and controller)
- Displaying data (Read)
- Adding data (Create)
- Editing data (Update)
- Deleting data (Delete)
1. Creating the Admin Post
First, create a route group for admin. Open app/Config/Routes.php and add the following code:
$routes->group('admin', function($routes){
$routes->get('post', 'PostAdmin::index');
$routes->get('post/(:segment)/preview', 'PostAdmin::preview/$1');
$routes->add('post/new', 'PostAdmin::create');
$routes->add('post/(:segment)/edit', 'PostAdmin::edit/$1');
$routes->get('post/(:segment)/delete', 'PostAdmin::delete/$1');
});
The code above creates a route group for the admin section, meaning every route within the group must be prefixed with /admin. For example, to open the create form, the address is /admin/post/new.
Two methods are used in the routes:
get()— can only be accessed via HTTP GET method.add()— can be accessed using both GET and POST. Used on thecreateandeditroutes because besides displaying the form (GET), we also send data (POST).
Creating the PostAdmin Controller
Open the terminal and run:
php spark make:controller PostAdmin
Output if successful:
File created: APPPATH/Controllers/PostAdmin.php
Open app/Controllers/PostAdmin.php and fill it with the following complete code:
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
class PostAdmin extends BaseController
{
public function index()
{
$post = new PostModel();
$data['posts'] = $post->findAll();
echo view('admin/admin_post_list', $data);
}
//--------------------------------------------------------------
public function preview($id)
{
$post = new PostModel();
$data['post'] = $post->where('id', $id)->first();
if(!$data['post']){
throw PageNotFoundException::forPageNotFound();
}
echo view('post_detail', $data);
}
//--------------------------------------------------------------
public function create()
{
// perform validation
$validation = \Config\Services::validation();
$validation->setRules(['title' => 'required']);
$isDataValid = $validation->withRequest($this->request)->run();
// if data is valid, save to database
if($isDataValid){
$post = new PostModel();
$post->insert([
"title" => $this->request->getPost('title'),
"content" => $this->request->getPost('content'),
"status" => $this->request->getPost('status'),
"slug" => url_title($this->request->getPost('title'), '-', TRUE)
]);
return redirect('admin/post');
}
// display create form
echo view('admin/admin_post_create');
}
//--------------------------------------------------------------
public function edit($id)
{
// get the article to edit
$post = new PostModel();
$data['post'] = $post->where('id', $id)->first();
// validate the article data
$validation = \Config\Services::validation();
$validation->setRules([
'id' => 'required',
'title' => 'required'
]);
$isDataValid = $validation->withRequest($this->request)->run();
// if data is valid, save to database
if($isDataValid){
$post->update($id, [
"title" => $this->request->getPost('title'),
"content" => $this->request->getPost('content'),
"status" => $this->request->getPost('status')
]);
return redirect('admin/post');
}
// display edit form
echo view('admin/admin_post_update', $data);
}
//--------------------------------------------------------------
public function delete($id)
{
$post = new PostModel();
$post->delete($id);
return redirect('admin/post');
}
}
The PostAdmin controller has five methods:
index()— displays the list of articles.preview($id)— displays the article preview based on$id.create()— creates a new article.edit($id)— edits an article based on$id.delete($id)— deletes an article based on$id.
2. Displaying Data (Read)
In the PostAdmin controller, the read feature is found in the index() and preview() methods.
The index() method retrieves all data using findAll() and sends it to the admin_post_list view:
public function index()
{
$post = new PostModel();
$data['posts'] = $post->findAll();
echo view('admin/admin_post_list', $data);
}
The preview($id) method retrieves a single article by $id using where() and first(), then displays it in the post_detail view:
public function preview($id)
{
$post = new PostModel();
$data['post'] = $post->where('id', $id)->first();
if(!$data['post']){
throw PageNotFoundException::forPageNotFound();
}
echo view('post_detail', $data);
}
Admin Post List View
Create a new admin folder inside app/Views, then create the file admin_post_list.php:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyBlog</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="<?= base_url('css/bootstrap.min.css') ?>" />
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container">
<a class="navbar-brand" href="<?= base_url() ?>">MyBlog</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarNav" aria-controls="navbarNav"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse justify-content-between" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="<?= base_url('admin/post') ?>">Blog</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a href="<?= base_url('admin/post/new') ?>"
class="btn btn-primary mr-3">New Post</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= base_url('admin/setting') ?>">Setting</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= base_url('auth/logout') ?>">Logout</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="p-5 mb-4 bg-light rounded-3">
<div class="container py-5">
<h1 class="display-5 fw-bold">Blog > Admin</h1>
</div>
</div>
<div class="container">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Title</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php foreach($posts as $post): ?>
<tr>
<td><?= $post['id'] ?></td>
<td>
<strong><?= $post['title'] ?></strong><br>
<small class="text-muted"><?= $post['created_at'] ?></small>
</td>
<td>
<?php if($post['status'] === 'published'): ?>
<small class="text-success"><?= $post['status'] ?></small>
<?php else: ?>
<small class="text-muted"><?= $post['status'] ?></small>
<?php endif ?>
</td>
<td>
<a href="<?= base_url('admin/post/'.$post['id'].'/preview') ?>"
class="btn btn-sm btn-outline-secondary" target="_blank">Preview</a>
<a href="<?= base_url('admin/post/'.$post['id'].'/edit') ?>"
class="btn btn-sm btn-outline-secondary">Edit</a>
<a href="#"
data-href="<?= base_url('admin/post/'.$post['id'].'/delete') ?>"
onclick="confirmToDelete(this)"
class="btn btn-sm btn-outline-danger">Delete</a>
</td>
</tr>
<?php endforeach ?>
</tbody>
</table>
<!-- Delete Confirmation Modal -->
<div id="confirm-dialog" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<h2 class="h2">Are you sure?</h2>
<p>The data will be deleted and lost forever</p>
</div>
<div class="modal-footer">
<a href="#" role="button" id="delete-button"
class="btn btn-danger">Delete</a>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<script>
function confirmToDelete(el) {
document.getElementById("delete-button")
.setAttribute("href", el.dataset.href);
var myModal = new bootstrap.Modal(
document.getElementById('confirm-dialog'), {
keyboard: false
});
myModal.show();
}
</script>
</div>
<div class="container py-4">
<footer class="pt-3 mt-4 text-muted border-top">
<div class="container">
© <?= Date('Y') ?>
</div>
</footer>
</div>
<!-- jQuery and Bootstrap JS -->
<script src="<?= base_url('js/jquery.min.js') ?>"></script>
<script src="<?= base_url('js/bootstrap.bundle.min.js') ?>"></script>
</body>
</html>
The admin post list view displays all posts in a table with action buttons: Preview, Edit, and Delete. The preview display is the same as the post_detail view on the user side, only the URL is different: admin/post/{id}/preview.
3. Adding Data (Create)
The create() method handles two conditions: displaying the form (GET) and saving data (POST).
Validation process:
$validation = \Config\Services::validation();
$validation->setRules(['title' => 'required']);
$isDataValid = $validation->withRequest($this->request)->run();
- Load the validation library.
- Set validation rules: the
titlefield must be filled (required). - Run validation against the request data.
If the data is valid, save to database:
if($isDataValid){
$post = new PostModel();
$post->insert([
"title" => $this->request->getPost('title'),
"content" => $this->request->getPost('content'),
"status" => $this->request->getPost('status'),
"slug" => url_title($this->request->getPost('title'), '-', TRUE)
]);
return redirect('admin/post');
}
- Data is retrieved from form input using
$this->request->getPost(). - The
slugis automatically generated from the title using theurl_title()helper. - After the data is saved, the user is redirected to the post list page.
If the data is not yet valid or the page is opened for the first time, display the form:
echo view('admin/admin_post_create');
Create Form View
Create the file app/Views/admin/admin_post_create.php:
<div class="container">
<form action="" method="post" id="text-editor">
<div class="form-group mb-2">
<label for="title">Title</label>
<input type="text" name="title" class="form-control"
placeholder="Post title" required>
</div>
<div class="form-group mb-2">
<textarea name="content" class="form-control" cols="30" rows="10"
placeholder="Write a great post!"></textarea>
</div>
<div class="form-group">
<button type="submit" name="status" value="published"
class="btn btn-primary">Publish</button>
<button type="submit" name="status" value="draft"
class="btn btn-secondary">Save to Draft</button>
</div>
</form>
</div>
This form has two submit buttons:
- Publish — saves the article with
publishedstatus. - Save to Draft — saves the article with
draftstatus.
4. Editing Data (Update)
The edit($id) method handles retrieving old data, validation, and saving the updated data.
Retrieve the article to edit:
$post = new PostModel();
$data['post'] = $post->where('id', $id)->first();
Perform validation (id and title fields are required):
$validation = \Config\Services::validation();
$validation->setRules([
'id' => 'required',
'title' => 'required'
]);
$isDataValid = $validation->withRequest($this->request)->run();
If valid, update the data in the database:
if($isDataValid){
$post->update($id, [
"title" => $this->request->getPost('title'),
"content" => $this->request->getPost('content'),
"status" => $this->request->getPost('status')
]);
return redirect('admin/post');
}
// display update form
echo view('admin/admin_post_update', $data);
Update Form View
Create the file app/Views/admin/admin_post_update.php:
<div class="container">
<form action="" method="post" id="text-editor">
<input type="hidden" name="id" value="<?= $post['id'] ?>" />
<div class="form-group mb-2">
<label for="title">Title</label>
<input type="text" name="title" class="form-control"
placeholder="Post title" value="<?= $post['title'] ?>" required>
</div>
<div class="form-group mb-2">
<textarea name="content" class="form-control" cols="30" rows="10"
placeholder="Write a great post!"><?= $post['content'] ?></textarea>
</div>
<div class="form-group mb-2">
<button type="submit" name="status" value="published"
class="btn btn-primary">Publish</button>
<button type="submit" name="status" value="draft"
class="btn btn-secondary">Save to Draft</button>
</div>
</form>
</div>
Key differences from the create form:
- A hidden input
idis included to identify the article being edited. - The
titlefield andtextareaare pre-filled with old data from the database (value<?= $post['title'] ?>and<?= $post['content'] ?>).
5. Deleting Data (Delete)
The delete($id) method receives the $id parameter, deletes the data from the posts table, then redirects to the list page:
public function delete($id)
{
$post = new PostModel();
$post->delete($id);
return redirect('admin/post');
}
In the admin_post_list.php view, the delete button does not directly delete data. Instead, it triggers a confirmation through a Bootstrap modal:
<a href="#"
data-href="<?= base_url('admin/post/'.$post['id'].'/delete') ?>"
onclick="confirmToDelete(this)"
class="btn btn-sm btn-outline-danger">Delete</a>
Confirmation modal:
<div id="confirm-dialog" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<h2 class="h2">Are you sure?</h2>
<p>The data will be deleted and lost forever</p>
</div>
<div class="modal-footer">
<a href="#" role="button" id="delete-button"
class="btn btn-danger">Delete</a>
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
The confirmToDelete(el) JavaScript function:
function confirmToDelete(el) {
document.getElementById("delete-button")
.setAttribute("href", el.dataset.href);
var myModal = new bootstrap.Modal(
document.getElementById('confirm-dialog'), {
keyboard: false
});
myModal.show();
}
- Gets the
delete-buttonelement and sets itshrefattribute with thedata-hrefvalue from the clicked button. - Creates a Bootstrap modal instance for
confirm-dialogand disables modal closing via keyboard. - Shows the modal using the
show()method so the user can confirm the deletion.
6. Admin Views Directory Structure
After all views are created, the app/Views directory structure becomes:
app/Views/
admin/
admin_post_list.php ← List of all posts (Read)
admin_post_create.php ← Add post form (Create)
admin_post_update.php ← Edit post form (Update)
home.php
about.php
contact.php
faqs.php
post.php
post_detail.php ← Also used for admin preview
Admin Route to Controller and View mapping:
| Route | Method | Action |
|---|---|---|
/admin/post | PostAdmin::index | Display article list |
/admin/post/(:segment)/preview | PostAdmin::preview/$1 | Article preview |
/admin/post/new | PostAdmin::create | Add & save article form |
/admin/post/(:segment)/edit | PostAdmin::edit/$1 | Edit & save article form |
/admin/post/(:segment)/delete | PostAdmin::delete/$1 | Delete article |
7. Summary
This practice implements CRUD (Create, Read, Update, Delete) on an admin panel to manage blog post data using CodeIgniter 4:
- Route Group — all admin routes are grouped under the
admingroup for better organization. - Read — the
index()method displays the entire article list withfindAll(), andpreview($id)displays a single article detail withfirst(). - Create — the
create()method validates input, saves new data to the database, and automatically generates aslugfrom the title. - Update — the
edit($id)method retrieves old data, validates changes, then saves the updated data. - Delete — the
delete($id)method deletes data by ID, equipped with a Bootstrap confirmation modal before execution. - Validation — every create and update process includes validation using
\Config\Services::validation()to ensure incoming data meets the rules.
By mastering this CRUD workflow, the web application can manage data efficiently and responsively according to user needs.


