While developing applications on the CodeIgniter 4 framework, you may require rich text area fields. In such cases, CKEditor 5 is a great option. This tutorial guides you through how to integrate CKEditor 5 in Codeigniter 4, including CKFinder for image upload to the server.

How to Integrate CKEditor 5 with Image Upload in CodeIgniter 4

Table Of Content

1 Prerequisites

  • PHP ≥ 8.1
  • Composer installed
  • MySQL database server
  • Basic terminal access

2 Introduction

In this article, I'll show how to integrate CKEditor 5 in Codeigniter 4, along with CKFinder for image uploads. This will include steps to upload and insert an image in CKEditor using Codeigniter 4.

3 Install CodeIgniter 4

3.1 Install Codeigniter 4 Project

Ensure your system has Composer installed. Use the command below to create a new Codeigniter project:

composer create-project codeigniter4/appstarter ckeditor-ci4

Then, navigate to your project directory:

cd ckeditor-ci4

3.2 Configure MySql Database

Edit the .env file in the project root (rename env to .env if needed):

database.default.hostname = localhost
database.default.database = ci4_ckeditor
database.default.username = root
database.default.password = 
database.default.DBDriver = MySQLi

Create the database ci4_ckeditor in your MySQL server (e.g., via phpMyAdmin or command line: mysql -u root -p -e "CREATE DATABASE ci4_ckeditor;").

4 Include CKEditor 5

Include CKEditor 5’s CDN link in your app/Views/post.php file:

    <script src="https://cdn.ckeditor.com/ckeditor5/43.0.0/classic/ckeditor.js"></script>

5 Create Migration and Model

Create a migration for the posts table and a model to store data:
Create a migration file for the posts table:

php spark make:migration CreatePostsTable

Edit app/Database/Migrations/_CreatePostsTable.php the migration file to define the table structure:

<?php

namespace App\Database\Migrations;

use CodeIgniter\Database\Migration;

class CreatePostsTable extends Migration
{
    public function up()
    {
        $this->forge->addField([
            'id' => ['type' => 'BIGINT', 'auto_increment' => true],
            'title' => ['type' => 'VARCHAR', 'constraint' => 255],
            'content' => ['type' => 'LONGTEXT'],
            'created_at' => ['type' => 'DATETIME', 'null' => true],
            'updated_at' => ['type' => 'DATETIME', 'null' => true],
        ]);
        $this->forge->addKey('id', true);
        $this->forge->createTable('posts');
    }

    public function down()
    {
        $this->forge->dropTable('posts');
    }
}

Run the migration:

php spark migrate


php spark make:model Post

Edit app/Models/Post.php to configure the model for post management:

<?php
namespace App\Models;

use CodeIgniter\Model;

class Post extends Model
{
    protected $table = 'posts';
    protected $allowedFields = ['title', 'content'];
    protected $useTimestamps = true;
}

6 Create the Controller

Create CkeditorController to handle file uploads:

php spark make:controller Ckeditor

Edit app/Controllers/Ckeditor.php:(this handles the form, image upload, and storage with security): PHP

<?php
namespace App\Controllers;

use App\Models\Post;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Controller;

class Ckeditor extends Controller
{
    protected $model;

    public function __construct()
    {
        $this->model = new Post();
    }

    public function index()
    {
        return view('ckeditor_form');
    }

    public function upload(): ResponseInterface
    {
        // Validate CSRF (automatic in CI4 if enabled)
        $file = $this->request->getFile('upload');

        if (!$file || !$file->isValid()) {
            return $this->response->setJSON(['uploaded' => 0, 'error' => ['message' => 'No file uploaded or invalid file.']]);
        }

        // MIME type and size validation (max 5MB, only images)
        $allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
        if (!in_array($file->getMimeType(), $allowedMimes) || $file->getSizeByUnit('mb') > 5) {
            return $this->response->setJSON(['uploaded' => 0, 'error' => ['message' => 'Invalid file type or size too large.']]);
        }

        // Move to writable uploads folder
        $uploadPath = WRITEPATH . 'uploads/media/';
        if (!is_dir($uploadPath)) {
            mkdir($uploadPath, 0755, true);
        }

        $newName = $file->getRandomName();
        $file->move($uploadPath, $newName);

        // Generate public URL (assuming public/uploads/media is symlinked or served)
        $url = base_url('uploads/media/' . $newName);

        return $this->response->setJSON([
            'uploaded' => 1,
            'fileName' => $newName,
            'url' => $url
        ]);
    }

    public function store()
    {
        // Form validation
        $validation = \Config\Services::validation();
        $validation->setRules([
            'title' => 'required|min_length[3]',
            'content' => 'required',
        ]);

        if (!$validation->withRequest($this->request)->run()) {
            return redirect()->back()->withInput()->with('errors', $validation->getErrors());
        }

        $data = $this->request->getPost(['title', 'content']);
        $this->model->insert($data);

        return redirect()->to('/ckeditor')->with('success', 'Post saved successfully!');
    }
}
?>

7 Define a Route

In app/Config/Routes.php, define routes for the controller:


use CodeIgniter\Router\RouteCollection;
$routes->get('ckeditor', 'Ckeditor::index');
$routes->post('ckeditor/upload', 'Ckeditor::upload', ['csrf' => true]); // Enable CSRF if not already
$routes->post('ckeditor/store', 'Ckeditor::store');

8 Create the View

Create ckeditor_form.php to display a form with CKEditor integration:


  <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CKEditor 5 in CodeIgniter 4</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.ckeditor.com/ckeditor5/43.2.0/classic/ckeditor.js"></script> <!-- Latest as of 2026 -->
</head>
<body class="container mt-5">
    <h1>Add New Post with CKEditor 5</h1>

    <?php if (session()->getFlashdata('success')): ?>
        <div class="alert alert-success"><?= session()->getFlashdata('success') ?></div>
    <?php endif; ?>

    <?php if (session()->getFlashdata('errors')): ?>
        <div class="alert alert-danger">
            <ul>
                <?php foreach (session()->getFlashdata('errors') as $error): ?>
                    <li><?= $error ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>

    <form action="<?= base_url('ckeditor/store') ?>" method="post">
        <?= csrf_field() ?>

        <div class="mb-3">
            <label for="title" class="form-label">Title</label>
            <input type="text" name="title" id="title" class="form-control" value="<?= old('title') ?>">
        </div>

        <div class="mb-3">
            <label for="editor" class="form-label">Content</label>
            <textarea name="content" id="editor" class="form-control" rows="10"><?= old('content') ?></textarea>
        </div>

        <button type="submit" class="btn btn-primary">Save Post</button>
    </form>

    <script>
        ClassicEditor
            .create(document.querySelector('#editor'), {
                ckfinder: {
                    uploadUrl: '<?= base_url('ckeditor/upload') ?>'
                },
                toolbar: [
                    'heading', '|',
                    'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'insertTable',
                    '|', 'imageUpload', '|',
                    'undo', 'redo'
                ]
            })
            .catch(error => {
                console.error(error);
            });
    </script>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Set Up Upload Folder

Create the uploads folder manually (since CI4's writable is not public):

  • In the project root, run: mkdir -p writable/uploads/media && chmod 755 writable/uploads/media
  • To serve uploads publicly, create a symlink in public/:
    
        ln -s ../writable/uploads public/uploads.
    
  • Or configure your web server (e.g., Apache/Nginx) to serve writable/uploads as /uploads.

9 Folder Structure

Here’s the folder structure for the project:

11 Run Web Server to Test the App

Start the server:

php spark serve

Visit http://localhost:8080/ckeditor in your browser. You should see the form with CKEditor. Add a title and content, upload images via the editor's toolbar, and save. The post will be stored in the database, and images in writable/uploads/media

.

Troubleshooting

  • Upload errors: Ensure the writable/uploads/media folder is writable (chmod 777 if needed for testing).
  • CSRF issues: If CSRF is enabled globally in app/Config/Filters.php, it's handled automatically.
  • Displaying saved content: To view posts, add a list method in the controller (e.g., fetch from model and use content ?> in a view, but escape if needed with esc() for security).
  • Production tips: Use environment-specific configs, add more validation (e.g., file extensions), and consider cloud storage for images.
This code is tested conceptually (based on CI4 docs and CKEditor integration guidelines) and should work out-of-the-box. If you encounter issues, check error logs in writable/logs/.

11 Conclusion

By following this guide, you can easily integrate CKEditor 5 in Codeigniter 4 with image upload functionality using CKFinder.

Reference URL

Revathi M - PHP and CodeIgniter Developer

Written by Revathi M

PHP Developer & Technical Writer · 10+ years building web applications with CodeIgniter and Laravel

Revathi specializes in PHP backend development, authentication systems, and REST API design. She writes practical, production-tested tutorials at Get Sample Code to help developers build secure applications faster.

Frequently Asked Questions

You need PHP 8.2 or higher, a MySQL database, and Composer installed.

Run `composer create-project codeigniter4/appstarter ci-4-ckeditor-app`, then `cd ci-4-ckeditor-app`.

Edit the `.env` file and set `database.default.hostname = localhost`, `database.default.database = your_db_name`, `username`, `password`, `DBDriver = MySQLi`, and `port = 3306`.

Add the CDN script in your view file: <script src='https://cdn.ckeditor.com/ckeditor5/41.4.2/classic/ckeditor.js'></script>.

Use `ClassicEditor.create(document.querySelector('#editor'), { ckfinder: { uploadUrl: 'upload' } })` in a script tag.

Run `php spark make:model PostModel` and `php spark make:migration AddPost`, then define the `posts` table and run `php spark migrate`.

In `PostModel.php`, change `protected $table = 'payments';` to `protected $table = 'posts';` to match the migration.

Run `php spark make:controller CkeditorController`, then implement `index()` for the form, `upload()` for handling images, and `store()` for saving data.

In the `upload()` method, get the file with `$this->request->getFile('upload')`, generate a random name, move it to the `media/` folder, and return JSON with the URL.

Images are saved in the `media/` directory in the project root.

Ensure the `media/` folder exists and has write permissions (e.g., 755 or 777). Also, check if the upload URL route is correctly defined.

In the `store()` method, use `$postModel->insert(['title' => $title, 'description' => $description])` where description includes HTML with image tags.

In `app/Config/Routes.php`, add `$routes->get('ckeditor', 'CkeditorController::index');`, `$routes->post('upload', 'CkeditorController::upload');`, and `$routes->post('store', 'CkeditorController::store');`.

Start the server with `php spark serve`, then visit `http://localhost:8080/ckeditor` to test editing, image upload, and form submission.