Event
{Event}  Deep-dive on can't-miss topics like data modeling, schema design, security and more at MongoDB.local NYC! Learn more >

Connect CodeIgniter and MongoDB: A Step-by-Step Tutorial

CodeIgniter is a modern PHP framework that’s easy to learn and use. It’s a powerful tool for building web applications. MongoDB Atlas is a cloud-based database service that provides a simple, flexible, and reliable way to store, manage, and query data. In this tutorial, we'll learn how to connect CodeIgniter and MongoDB Atlas to build a full-featured CRUD (Create, Read, Update, Delete) application. The source code for this project is available on GitHub.

What are we building?

In this tutorial, we'll build a reading journal application. The application will allow users to track their reading progress. Users will be able to add books, update their progress, and see a list of all books they've been reading. Here's a demonstration of the finished application:


Finished reading journal application demonstration.

Prerequisites

You can follow this tutorial step by step and build your application from scratch. No preexisting knowledge of CodeIgniter or MongoDB is required. However, you will need a couple of things:

  • A MongoDB Atlas account with a free cluster.
  • PHP 7.3 or newer.
  • Composer—a package manager for PHP.
  • The MongoDB PHP Driver.

The following sections will guide you through the required setup. If you want the final code instead, you can find it on GitHub.

MongoDB Atlas database

For this tutorial, we'll be using MongoDB Atlas to store our data. Since Atlas is cloud-based and offers a forever-free cluster, we won't need to worry about installing and maintaining a local copy of MongoDB. Follow the Get Started with Atlas guide to create a free account and set up a cluster.

CodeIgniter4 and project setup

We'll build our application with CodeIgniter4 which, at the moment of writing, is the latest version of CodeIgniter. Since CodeIgniter4 is a PHP framework, we need to make sure we've got PHP version 7.3 or newer installed on our machine. To verify your PHP version, run the following command in your terminal emulator:

php --version

To set up a new CodeIgniter4 project, we'll use Composer. Composer is a package manager for PHP. It allows you to manage your dependencies and install them with one command. To install Composer, follow the official installation instructions.

Once you've installed Composer, run the following command to create a new CodeIngiter4 project:

composer create-project codeigniter4/appstarter reading-journal-codeigniter-mongodb
cd reading-journal-codeigniter-mongodb

Set up the MongoDB PHP Driver

To connect our CodeIgniter application to MongoDB, we'll need to install the MongoDB PHP Driver. The driver consists of two components—the extension and the library.

First, we'll install the extension and load it in the php.ini file:

pecl install mongodb
echo "extension=mongodb.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`

Next, we'll install the library with Composer:

composer require mongodb/mongodb

Set up the environment variables

Finally, we'll create an .env file in the root directory of our project (note the leading dot). This file will contain server-specific settings, such as the MongoDB Atlas connection string and the database name. You can choose any name you want for your database but make sure your connection string and credentials are correct.

reading-journal-codeigniter-mongodb/.env

CI_ENVIRONMENT = development
ATLAS_URI = 'mongodb+srv://<username>:<password>@sandbox.pv0l7.mongodb.net/?retryWrites=true&w=majority'
DATABASE = 'codeigniter'

Now, we can run the application with PHP's built-in server:

php spark serve

Open your browser and navigate to http://localhost:8000/. You see the CodeIgniter4 getting started page:


CodeIgniter getting started page

Great job! We've finished our setup and are ready to start building our “Reading Journal” application!

Connect CodeIgniter to MongoDB Atlas

First, we'll need to connect our CodeIgniter application to MongoDB Atlas. We already installed the MongoDB PHP Driver with Composer. We'll create a DatabaseConnector library class that will connect to the database using the driver. The class will also expose a database object for the rest of the application. Create a new DatabaseConnector.php file in the app/Libraries directory and add the following code:

reading-journal-codeigniter-mongodb/app/Libraries/DatabaseConnector.php

<?php
 
namespace App\Libraries;

class DatabaseConnector {
    private $client;
    private $database;

    function __construct() {
        $uri = getenv('ATLAS_URI');
        $database = getenv('DATABASE');

        if (empty($uri) || empty($database)) {
            show_error('You need to declare ATLAS_URI and DATABASE in your .env file!');
        }

        try {
            $this->client = new \MongoDB\Client($uri);
        } catch(MongoDB\Driver\Exception\MongoConnectionException $ex) {
            show_error('Couldn\'t connect to database: ' . $ex->getMessage(), 500);
        }

        try {
            $this->database = $this->client->selectDatabase($database);
        } catch(MongoDB\Driver\Exception\RuntimeException $ex) {
            show_error('Error while fetching database with name: ' . $database . $ex->getMessage(), 500);
        }
    }

    function getDatabase() {
        return $this->database;
    }
}

Create a model from CRUD operations

Next, we'll create a books model that will handle the CRUD operations for our books. Create a new BooksModel.php file in the app/Models directory and add the following code:

reading-journal-codeigniter-mongodb/app/Models/BooksModel.php

<?php

namespace App\Models;

use App\Libraries\DatabaseConnector;

class BooksModel {
    private $collection;

    function __construct() {
        $connection = new DatabaseConnector();
        $database = $connection->getDatabase();
        $this->collection = $database->books;
    }

    function getBooks($limit = 10) {
        try {
            $cursor = $this->collection->find([], ['limit' => $limit]);
            $books = $cursor->toArray();

            return $books;
        } catch(\MongoDB\Exception\RuntimeException $ex) {
            show_error('Error while fetching books: ' . $ex->getMessage(), 500);
        }
    }

    function getBook($id) {
        try {
            $book = $this->collection->findOne(['_id' => new \MongoDB\BSON\ObjectId($id)]);

            return $book;
        } catch(\MongoDB\Exception\RuntimeException $ex) {
            show_error('Error while fetching book with ID: ' . $id . $ex->getMessage(), 500);
        }
    }

    function insertBook($title, $author, $pages) {
        try {
            $insertOneResult = $this->collection->insertOne([
                'title' => $title,
                'author' => $author,
                'pages' => $pages,
                'pagesRead' => 0,
            ]);

            if($insertOneResult->getInsertedCount() == 1) {
                return true;
            }

            return false;
        } catch(\MongoDB\Exception\RuntimeException $ex) {
            show_error('Error while creating a book: ' . $ex->getMessage(), 500);
        }
    }

    function updateBook($id, $title, $author, $pagesRead) {
        try {
            $result = $this->collection->updateOne(
                ['_id' => new \MongoDB\BSON\ObjectId($id)],
                ['$set' => [
                    'title' => $title,
                    'author' => $author,
                    'pagesRead' => $pagesRead,
                ]]
            );

            if($result->getModifiedCount()) {
                return true;
            }

            return false;
        } catch(\MongoDB\Exception\RuntimeException $ex) {
            show_error('Error while updating a book with ID: ' . $id . $ex->getMessage(), 500);
        }
    }

    function deleteBook($id) {
        try {
            $result = $this->collection->deleteOne(['_id' => new \MongoDB\BSON\ObjectId($id)]);

            if($result->getDeletedCount() == 1) {
                return true;
            }

            return false;
        } catch(\MongoDB\Exception\RuntimeException $ex) {
            show_error('Error while deleting a book with ID: ' . $id . $ex->getMessage(), 500);
        }
    }
}

In the constructor of the BooksModel class, we're instantiating a new DatabaseConnector object to connect to the database. We're using the getDatabase() method to access the database object and the books collection. We're using the books collection throughout the model to perform CRUD operations.


Note: If you have multiple models in your application, you should reuse the database connection instead of creating a new connection for each model. An easy way to do that is by converting the DatabaseConnector class to a singleton.


In the BooksModel class methods, we're using collection methods from the MongoDB PHP driver API to fetch, insert, update, and delete books:

  • The find() method fetches a $limit number of documents from the books collection.
  • The findOne() method fetches a single document from the books collection based on the _id field.
  • The insertOne() method inserts a single document into the books collection.
  • The updateOne() method updates a single document in the books collection based on the _id field.
  • The deleteOne() method deletes a single document from the books collection based on the _id field.

Build a controller and a view

Following the MVC (model-view-controller) pattern, next we need to create views for our applications pages:

  • The list.php view will display a list of books.
  • The create.php view will allow users to create a new book.
  • The details.php view will display a single book.
  • The edit.php view will allow users to edit a book.

To complement the pages, we'll also create header.php and footer.php templates that will be included on each page. These templates will include the navigation menu, the footer copyright, and the Bootstrap library for styling.

The final piece of the MVC pattern will be the Books controller class. The controller will fetch data using the BooksModel and pass it to the views. It will also be responsible for validating the user input and submitting form data to the model.

Let's get started!

The header.php and footer.php templates will be respectively the beginning and the end of each page. First, create the header.php template in app/Views/templates with the following content:

reading-journal-codeigniter-mongodb/app/Views/templates/header.php

<!doctype html>
<html lang="en">
    <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

        <!-- Load Bootstrap CSS -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

        <title>Reading Journal: MongoDB & CodeIgniter CRUD Tutorial</title>
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <div class="container-fluid">
                <a class="navbar-brand" href="/">Reading Journal</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link" href="/books">Books List</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/books/create">Add a Book</a>
                    </li>
                </ul>
            </div>
        </nav>
        <div class="container mt-5">

Then, add the footer.php template to the same directory:

reading-journal-codeigniter-mongodb/app/Views/templates/footer.php

 </div>

    <footer class="text-center">
        <small>&copy; 2022</small>
    </footer>

    <!-- Bootstrap Bundle with Popper -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>

Application pages views

First, we'll create the list.php view in the app/Views/books directory.

reading-journal-codeigniter-mongodb/app/Views/books/list.php

<?php if (! empty($books) && is_array($books)): ?>

    <table class="table table-striped table-bordered mt-4">
        <thead>
            <tr>
                <th>Title</th>
                <th>Author</th>
                <th>Progress</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($books as $book): ?>
                <tr>
                    <td>
                        <a href="<?= '/books/' . $book['_id'] ?>">
                            <?= esc($book['title']) ?>
                        </a>
                    </td>
                    <td><?= esc($book['author']) ?></td>
                    <td><?= round(esc($book['pagesRead']) / esc($book['pages']) * 100, 2) ?>%</td>
                    <td>
                        <a class="btn btn-primary" href="<?= '/books/edit/' . $book['_id'] ?>">Edit</a>
                        <a class="btn btn-danger" href="<?= '/books/delete/' . $book['_id'] ?>">Delete</a>
                    </td>
                </tr>
            <?php endforeach ?>
    </table>

<?php else: ?>
    <h2>You don't have any books yet!</h3>
<?php endif ?>

<a href="books/create">
    <button class="btn btn-primary mt-4">Add a book</button>
</a>

The $books variable will contain the list of books passed from the controller. The foreach loop will iterate over the list of books and display each one in a table row.

Next, let's add the create.php view.

reading-journal-codeigniter-mongodb/app/Views/books/create.php.

<h2><?= esc($title) ?></h2>

<?= session()->getFlashdata('error') ?>
<?= service('validation')->listErrors() ?>

<form action="/books/create" method="post">
    <?= csrf_field() ?>

    <div class="mt-3">
        <label class="form-label" for="title">Title</label>
        <input class="form-control" type="input" name="title" />
    </div>

    <div class="mt-3">
        <label class="form-label" for="author">Author</label>
        <input class="form-control" type="input" name="author" />
    </div>

    <div class="mt-3">
        <label class="form-label" for="pages">Pages</label>
        <input class="form-control" type="number" name="pages" />
    </div>

    <input class="btn btn-success mt-4" type="submit" name="submit" value="Add Book" />
</form>

For the create.php view, we'll need to add the csrf_field() helper function to prevent cross-site request forgery. To load this helper, we'll add the following line to the app/Config/Filters.php file in the $methods array:

reading-journal-codeigniter-mongodb/app/Config/Filters.php

 public $methods = [
        'post' => ['csrf'], // add this line
    ];

The next view we'll create is the edit.php view.

reading-journal-codeigniter-mongodb/app/Views/books/edit.php

<h2><?= esc($title) ?></h2>

<?= session()->getFlashdata('error') ?>
<?= service('validation')->listErrors() ?>

<form action="<?= '/books/edit/' . $book['_id'] ?>" method="post" autocomplete="off">
    <?= csrf_field() ?>

    <div class="form-floating mb-3">
        <input value="<?= $book['title'] ?>" class="form-control" type="text" name="title" placeholder="Title" required>
        <label for="title">Title</label>
    </div>

    <div class="form-floating mb-3">
        <input value="<?= $book['author'] ?>" class="form-control" type="text" name="author" placeholder="Author" required>
        <label for="author">Author</label>
    </div>

    <div class="form-floating mb-3">
        <input value="<?= $book['pagesRead'] ?>" class="form-control" type="number" name="pagesRead" value="0" required>
        <label for="pagesRead">Read Pages</label>
    </div>

    <button class="btn btn-primary" name="submit">Edit Book</button>
</form>

Finally, we'll add the details.php view.

reading-journal-codeigniter-mongodb/app/Views/books/details.php

<h2><?= esc($book['title']) ?></h2>
<div class="mt-4">
    <p>Author: <?= esc($book['author']) ?></p>
    <p>Pages: <?= esc($book['pages']) ?></p>
    <p>Pages read: <?= esc($book['pagesRead']) ?></p>

    <p id="reading-progress">Your reading progress:</p>
    <div class="progress">
        <div
            class="progress-bar"
            role="progressbar"
            style="width: <?= $book['progress'] ?>%"
            aria-valuenow="<?= $book['progress'] ?>"
            aria-valuemin="0"
            aria-valuemax="100"
            aria-labelledby="reading-progress"
        ><?= $book['progress'] ?>%</div>
    </div>

    <a href=".." class="btn btn-link mt-3">Go back</a>
</div>

We're all done with the views! Next up, let's implement the books controller that will manage the communication between the views and the model.

Books controller

Create the Books.php class in the app/Controllers directory.

reading-journal-codeigniter-mongodb/app/Controllers/Books.php

<?php

namespace App\Controllers;

helper('inflector');

use App\Models\BooksModel;

class Books extends BaseController
{
    public function index()
    {
        $model = model(BooksModel::class);

        $data = [
            'books' => $model->getBooks(),
        ];

        echo view('templates/header', $data);
        echo view('books/list', $data);
        echo view('templates/footer', $data);
    }

    public function details($segment = null)
    {
        $model = model(BooksModel::class);

        $data['book'] = $model->getBook($segment);

        if (empty($data['book'])) {
            throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find book with ID: ' . $segment);
        }
    
        $data['title'] = $data['book']['title'];
        $data['book']['progress'] = round($data['book']['pagesRead'] / $data['book']['pages'] * 100, 2);
    
        echo view('templates/header', $data);
        echo view('books/details', $data);
        echo view('templates/footer', $data);
    }

    public function create()
    {
        $model = model(BooksModel::class);

        if ($this->request->getMethod() === 'post' && $this->validate([
            'title' => 'required|min_length[1]|max_length[255]',
            'author' => 'required|min_length[1]|max_length[255]',
            'pages' => 'required|is_natural_no_zero',
        ])) {
            $model->insertBook(
                $this->request->getPost('title'),
                $this->request->getPost('author'),
                $this->request->getPost('pages'),
            );

            return redirect()->to('books');
        } else {
            echo view('templates/header');
            echo view('books/create', ['title' => 'Add a new book']);
            echo view('templates/footer');
        }
    }

    public function edit($segment = null)
    {
        $model = model(BooksModel::class);

        $data['book'] = $model->getBook($segment);

        if (empty($data['book'])) {
            throw new \CodeIgniter\Exceptions\PageNotFoundException('Cannot find book with ID: ' . $segment);
        }

        $data['title'] = $data['book']['title'];

        if ($this->request->getMethod() === 'post' && $this->validate([
            'title' => 'required|min_length[1]|max_length[255]',
            'author' => 'required|min_length[1]|max_length[255]',
            'pagesRead' => 'required|is_natural',
        ])) {
            $model->updateBook(
                $data['book']['_id'],
                $this->request->getPost('title'),
                $this->request->getPost('author'),
                $this->request->getPost('pagesRead'),
            );

            return redirect()->to('books');
        } else {
            echo view('templates/header', $data);
            echo view('books/edit', $data);
            echo view('templates/footer', $data);
        }
    }

    public function delete($segment = null) {
        if (!empty($segment) && $this->request->getMethod() == 'get') {
            $model = model(BooksModel::class);
            $model->deleteBook($segment);
        }

        return redirect()->to('books');
    }
}

For every action in the controller, we're using the BooksModel to query the database or modify the data. We're also validating the form data using the CodeIgniter built-in validation helpers.

Routes

Finally, we need to set up the routes. Open the app/Config/Routes.php file and replace its contents with the following:

reading-journal-codeigniter-mongodb/app/Config/Routes.php

<?php

namespace Config;

// Create a new instance of our RouteCollection class.
$routes = Services::routes();

// Load the system's routing file first, so that the app and ENVIRONMENT
// can override as needed.
if (file_exists(SYSTEMPATH . 'Config/Routes.php')) {
    require SYSTEMPATH . 'Config/Routes.php';
}

/*
 * --------------------------------------------------------------------
 * Router Setup
 * --------------------------------------------------------------------
 */
$routes->setDefaultNamespace('App\Controllers');
$routes->setDefaultController('Books');
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
$routes->setAutoRoute(false);

/*
 * --------------------------------------------------------------------
 * Route Definitions
 * --------------------------------------------------------------------
 */

$routes->match(['get', 'post'], 'books/create', 'Books::create');
$routes->match(['get', 'post'], 'books/edit/(:segment)', 'Books::edit/$1');
$routes->get('books/delete/(:segment)', 'Books::delete/$1');

$routes->get('books/(:segment)', 'Books::details/$1');
$routes->get('books', 'Books::index');
$routes->addRedirect('/', 'books');

/*
 * --------------------------------------------------------------------
 * Additional Routing
 * --------------------------------------------------------------------
 *
 * There will often be times that you need additional routing and you
 * need it to be able to override any defaults in this file. Environment
 * based routes is one such time. require() additional route files here
 * to make that happen.
 *
 * You will have access to the $routes object within that file without
 * needing to reload it.
 */
if (file_exists(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) {
    require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php';
}

Save the file, and you're ready to go!

Testing the application

Refresh your browser window and play around with the application. You can add a new book, edit an existing book, and delete a book.


Finished reading journal application demonstration.

Conclusion

In this step-by-step tutorial, we learned how to create a CRUD application using CodeIgniter4 and MongoDB. CodeIgniter4 is a modern PHP framework. Combining it with MongoDB's document model allows you to create modern data-driven web applications with ease. We learned how to use the MongoDB PHP driver to query and modify data from our database.

We also saw how easy it is to use MongoDB Atlas, the data platform by MongoDB. If you haven’t already, you can create a free account and your first forever-free cluster today!