Previous Article
Build a Custom Website Responsiveness Checker Tool (2026 Guide)
11 July 2018
CodeIgniter is a small, fast PHP framework with a gentle learning curve — a reasonable choice when Laravel feels like more than the job needs.
One thing to settle first: CodeIgniter 3 is end-of-life. It no longer receives feature work, and it requires a PHP version that is itself unsupported. Everything below targets CodeIgniter 4. If you are maintaining a CI3 application, the migration table at the end is the part you want.
CI3's "download a zip and unpack it" flow is gone. Use Composer:
composer create-project codeigniter4/appstarter myproject cd myproject
Requirements: PHP 8.1 or newer, with the intl and mbstring extensions enabled. If intl is missing you will get an obscure error on first run — enable it in php.ini before anything else.
Start the development server:
php spark serve # → http://localhost:8080
In CodeIgniter 3, the entire framework sat inside your web root. A server misconfiguration could expose application/config/database.php with your credentials in it.
CI4 puts only public/ in the web root. Everything else — app/, vendor/, writable/, .env — lives above it and is unreachable over HTTP.
Point your virtual host's document root at public/, not the project folder. Serving the project root reintroduces exactly the vulnerability CI4 was restructured to remove.
<VirtualHost *:80>
ServerName myproject.test
DocumentRoot /var/www/myproject/public
<Directory /var/www/myproject/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
CI3 used arrays in application/config/. CI4 uses PHP classes in app/Config/, plus a .env file for anything environment-specific.
cp env .env
# .env CI_ENVIRONMENT = development app.baseURL = 'http://localhost:8080/' database.default.hostname = localhost database.default.database = myproject database.default.username = root database.default.password = database.default.DBDriver = MySQLi
.env is git-ignored by default. Keep credentials there, never in app/Config/Database.php — that file is committed.
Set CI_ENVIRONMENT = production when you deploy. In development CI4 shows a detailed error page including stack traces and file paths; leaving that on in production hands attackers a map of your application.
<?php
// app/Controllers/Products.php
declare(strict_types=1);
namespace App\Controllers;
use App\Models\ProductModel;
class Products extends BaseController
{
public function index(): string
{
$model = new ProductModel();
return view('products/index', [
'title' => 'All products',
'products' => $model->orderBy('name', 'ASC')->findAll(20),
]);
}
public function show(int $id): string
{
$product = (new ProductModel())->find($id);
if ($product === null) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return view('products/show', ['product' => $product]);
}
}
Note the namespace and the typed return. CI4 is a modern PHP codebase — CI3's global functions and untyped everything are gone.
<?php
// app/Config/Routes.php
$routes->get('/', 'Home::index');
$routes->get('products', 'Products::index');
$routes->get('products/(:num)', 'Products::show/$1');
// Grouped, with a filter applied to all of them
$routes->group('admin', ['filter' => 'auth'], static function ($routes) {
$routes->get('dashboard', 'Admin\Dashboard::index');
$routes->post('products/create', 'Admin\Products::create');
});
Define routes explicitly and turn auto-routing off — it is disabled by default in CI4 for good reason. Auto-routing exposes every public controller method as a URL, including ones you never intended to be reachable.
<?php
// app/Models/ProductModel.php
declare(strict_types=1);
namespace App\Models;
use CodeIgniter\Model;
class ProductModel extends Model
{
protected $table = 'products';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useTimestamps = true;
// Only these columns can be mass-assigned — everything else is ignored.
protected $allowedFields = ['name', 'slug', 'price', 'description'];
protected $validationRules = [
'name' => 'required|max_length[150]',
'price' => 'required|decimal|greater_than[0]',
'slug' => 'required|alpha_dash|is_unique[products.slug,id,{id}]',
];
public function findPublished(int $limit = 20): array
{
return $this->where('status', 'published')
->orderBy('created_at', 'DESC')
->findAll($limit);
}
}
$allowedFields is a security control, not a convenience. Without it, a form posting is_admin=1 could write straight to that column — the mass assignment vulnerability. Anything not listed is silently discarded.
<?= $this->extend('layouts/main') ?>
<?= $this->section('content') ?>
<h1><?= esc($title) ?></h1>
<ul class="product-list">
<?php foreach ($products as $product): ?>
<li>
<a href="<?= site_url('products/' . $product['id']) ?>">
<?= esc($product['name']) ?>
</a>
<span><?= esc(number_format((float) $product['price'], 2)) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?= $this->endSection() ?>
Always wrap output in esc(). It is CI4's context-aware escaping helper — esc($value, 'attr') for attributes, 'js' inside script blocks, 'url' for URLs. Echoing a variable unescaped is how XSS gets in.
php spark make:migration CreateProductsTable
public function up(): void
{
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'name' => ['type' => 'VARCHAR', 'constraint' => 150],
'slug' => ['type' => 'VARCHAR', 'constraint' => 150],
'price' => ['type' => 'DECIMAL', 'constraint' => '10,2'],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('slug');
$this->forge->createTable('products');
}
public function down(): void
{
$this->forge->dropTable('products');
}
php spark migrate
Migrations mean your schema is version-controlled alongside your code, instead of living in a SQL file someone emails around.
| CodeIgniter 3 | CodeIgniter 4 |
|---|---|
| Download zip | composer create-project |
| Framework in web root | Only public/ in web root |
application/config/*.php arrays | app/Config/*.php classes + .env |
$this->load->model('X') | new XModel() |
$this->load->view('x', $data) | return view('x', $data) |
$this->input->post('x') | $this->request->getPost('x') |
$route['default_controller'] | $routes->get('/', 'Home::index') |
| No namespaces | PSR-4 throughout |
| Auto-routing on | Off by default |
This is not a drop-in upgrade — the directory layout, bootstrapping and conventions all changed. Realistically it is a port rather than an upgrade. For a large CI3 application, budget accordingly and move it module by module, starting with anything that handles authentication or user input.
public/..env, never in committed config.CI_ENVIRONMENT = production before deploying.$allowedFields to prevent mass assignment.esc().Advertisement
Written by
Amit VermaFounder / Senior Software Engineer
Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.
More articles by AmitYou might also like
We ship software that scales. Let's work together.