Skip to main content
Being Idea Innovations
CodeIgniter 4 Tutorial for Beginners (and Why CodeIgniter 3 Is Done)
Back to Blog

CodeIgniter 4 Tutorial for Beginners (and Why CodeIgniter 3 Is Done)

13 July 20185 min readCodeIgniter
Share:

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.

How MVC fits together

Request flow through CodeIgniter: browser to router, controller, model, database, and back through the view Browser GET /products/8 Router Routes.php Controller coordinates Model data + rules Database View HTML only Solid = request · dashed = response. The controller never writes SQL; the view never queries.
The separation is the point: models own data access, views own presentation, controllers coordinate and own neither.
  • Model — data structures and the rules around them. Queries live here, not in controllers.
  • View — presentation. A full page or a fragment such as a header. No business logic.
  • Controller — receives the request, asks the model for data, hands it to a view. Should stay thin.

Installing CodeIgniter 4

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

The public/ directory — CI4's biggest improvement

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>

Configuration

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.

A controller

<?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.

Routes

<?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.

A model

<?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.

A view

<?= $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.

Migrations

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.

Migrating from CodeIgniter 3

CodeIgniter 3CodeIgniter 4
Download zipcomposer create-project
Framework in web rootOnly public/ in web root
application/config/*.php arraysapp/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 namespacesPSR-4 throughout
Auto-routing onOff 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.

Summary

  • CodeIgniter 3 is end-of-life; build new work on CI4.
  • Install with Composer, and point the web root at public/.
  • Credentials in .env, never in committed config.
  • Set CI_ENVIRONMENT = production before deploying.
  • Define routes explicitly; leave auto-routing off.
  • Use $allowedFields to prevent mass assignment.
  • Escape every output with esc().
  • Manage schema with migrations.
Share:

Written by

Amit Verma

Founder / 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 Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers