
In the third part we have improved the presentation by displaying each recipe on a separate page. Now we are going to improve it a bit more by adding the pagination of the recipe list.
First of all, add the slug
field to the list of updatable fields in the RecipeModel
class:
app/Models/RecipeModel.php
<?php namespace App\Models;
use CodeIgniter\Model;
use App\Entities\Recipe;
class RecipeModel extends Model
{
protected $table = 'recipe';
protected $returnType = Recipe::Class;
protected $allowedFields = [
'title',
'instructions',
'slug',
];
}
You can also delete the List of recipe by id in the recipe_list
view to keep only the List of recipe by slug. Delete the following lines:
<h3>List of recipe by id</h3>
<ul>
<?php foreach ($recipes as $recipe): ?>
<li><?= anchor('recipe/' . $recipe->id, $recipe->title) ?></li>
<?php endforeach; ?>
</ul>
Insert dummy data with the Seeder class
To test the pagination, it will take us a lot more data than what we inserted in the previous parts of this series of articles. To make our life easier, we are going to use the Seeder
class. This class allows us to create data from the command line (CLI). The RecipesSeeder
class is very simple, we get an instance of the Query Builder for each of the recipe
and ingredient
tables, we insert 500 new recipes with the title "Dummy recipe number X", and for each recipe we insert three ingredients A, B and C in a single step with the function insertBatch()
.
Create the RecipesSeeder
class int the app/Database/Seeds
directory:
app/Database/Seeds/RecipesSeeder.php
<?php namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class RecipesSeeder extends Seeder
{
public function run()
{
// Get an instance of the Query Builder for the RECIPE table
$qb_recipe = $this->db->table('recipe');
// Get an instance of the Query Builder for th INGREDIENT table
$qb_ingredient = $this->db->table('ingredient');
// Number of dummy recipe to create
$nb_recipe = 500;
// Loop 500 times
for ($recipe_no = 1; $recipe_no <= $nb_recipe; $recipe_no++)
{
// Define a dummy recipe
$recipe = [
'title' => "Dummy recipe {$recipe_no}",
'slug' => "dummy-recipe-no-{$recipe_no}",
'instructions' => <<<EOT
Add all the ingredients to a baking dish.
Bake at 350 °F (180 °C) for 45 minutes.
EOT
];
// Insert this recipe
$qb_recipe->insert($recipe);
// Get the ID of the inserted recipe
$recipe_id = $this->db->insertID();
log_message('debug', "Inserted ID: $recipe_id");
// Define 3 dummy ingredients associated to this recipe
$ingredients = [
[
'name' => "Ingredient A",
'quantity' => "200 g",
'recipe_id' => $recipe_id
],
[
'name' => "Ingredient B",
'quantity' => "50 g",
'recipe_id' => $recipe_id
],
[
'name' => "Ingredient C",
'quantity' => "25 g",
'recipe_id' => $recipe_id
]
];
// Insert these 3 ingredients
$qb_ingredient->insertBatch($ingredients);
}
}
}
Run a Seeder class
The command to run a "seeder" is:
php spark db:seed RecipesSeeder
This command must be executed at the location of the spark
script. Normally it is in the root directory of the CodeIgniter installation. That is, one level above the app
and system
directories. In my case, I am using MAMP on macOS and my application is installed in the ci4.test
directory. Also, I am not using the PHP installed globally on my computer. I am using version 7.4.2 which is installed in MAMP. So just open a Terminal window and type the following commands:
cd /Applications/MAMP/htdocs/ci4.test
/Applications/MAMP/bin/php/php7.4.2/bin/php spark db:seed RecipesSeeder
The execution is very fast, 2000 rows are inserted in less than three seconds (500 recipes + 1500 ingredients). You can now refresh the page http://ci4.test:8888 which will now display a list of 500 recipes, or more if you have kept the recipes inserted in the second part.
Pagination
Ok, a list of 500 items doesn't make sense. It's time to implement pagination!
The MyRecipes library
The easiest way to paginate the results is to use the paginate()
function of the Model
class instead of the findAll()
function. This function does a lot of the work for us. It will automatically read the page number variable passed in the URL (?page=1), will configure the Pager
class and will add the LIMIT
clause to the SQL query, and then return the final result obtained by findAll()
. To use pagination, the controller needs to access the model because we must use the instance of the Pager
that it used to prepare the pagination. We are therefore going to modify our MyRecipes
library to make the instances of the models accessible via public variables.
app/Libraries/MyRecipes.php
<?php namespace App\Libraries;
use App\Models\RecipeModel;
use App\Models\IngredientModel;
class MyRecipes
{
public $recipeModel;
public $ingredientModel;
public function __construct()
{
$this->recipeModel = new RecipeModel();
$this->ingredientModel = new IngredientModel();
}
/**
* Get the list of recipes
* @return array
*/
public function getListRecipes ()
{
// Only get id, slug and title fields
$this->recipeModel->select('id, slug, title');
$recipes = $this->recipeModel
->orderBy('id')
->paginate();
return $recipes;
}
/**
* Get a recipe by its id
* @param int $id
* @return object|NULL
*/
public function getRecipeById (int $id)
{
// Get the recipe by its id
$recipe = $this->recipeModel->find($id);
if ($recipe !== null)
{
$recipe->ingredients = $this->ingredientModel
->where( ['recipe_id' => $recipe->id] )
->orderBy('id')
->findAll();
}
return $recipe;
}
/**
* Get a recipe by its slug
* @param string $slug
* @return object|NULL
*/
public function getRecipeBySlug (string $slug)
{
// Get the recipe by its slug
$recipe = $this->recipeModel->where('slug', $slug)->first();
if ($recipe !== null)
{
$recipe->ingredients = $this->ingredientModel
->where( ['recipe_id' => $recipe->id] )
->orderBy('id')
->findAll();
}
return $recipe;
}
}
The only thing to change in the controller is to get the instance of the Pager
class used by the model and pass it to the view. This will allow us to display links to other pages.
app/Controllers/RecipesController.php
<?php namespace App\Controllers;
use App\Libraries\MyRecipes;
class RecipesController extends BaseController
{
/**
* List of recipes
* @return string
*/
public function index()
{
// Create an instance of our library
$myRecipes = new MyRecipes();
// Collect all the data used by the view in a $data array
$data = [
'page_title' => "My Recipes",
'page_subtitle' => "I present you my favorite recipes...",
'recipes' => $myRecipes->getListRecipes(),
// Pass the paginnation class instance to the view
'pager' => $myRecipes->recipeModel->pager,
];
/* Each of the items in the $data array will be accessible
* in the view by variables with the same name as the key:
* $page_title, $page_subtitle, $recipes and $pager
*/
return view('recipe_list', $data);
}
/**
* One recipe
* @param int $id
* @return string
*/
public function recipeById (int $id)
{
// Create an instance of our library
$myRecipes = new MyRecipes();
$data = [];
/* Get the recipe for the id received in parameter.
* If the recipe does not exist, throw a page not found exception (error 404)
*/
if ( ! $data['recipe'] = $myRecipes->getRecipeById($id))
{
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return view('recipe', $data);
}
/**
* One recipe
* @param string $slug
* @return string
*/
public function recipeBySlug (string $slug)
{
// Create an instance of our library
$myRecipes = new MyRecipes();
$data = [];
/* Get the recipe for the slug received in parameter.
* If the recipe does not exist, throw a page not found exception (error 404)
*/
if ( ! $data['recipe'] = $myRecipes->getRecipeBySlug($slug))
{
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return view('recipe', $data);
}
}
Configuration
The Pager
class is a CodeIgniter system class which allows us to do pagination and among other things to generate links to other pages. It needs a configuration file to define the available templates and the default number of items to display per page. Here we add a new template that we will use instead of the default CodeIgniter templates. The default templates are very basic and instead of writing CSS we will be using Bootstrap CSS classes.
app/Config/Pager.php
<?php namespace Config;
use CodeIgniter\Config\BaseConfig;
class Pager extends BaseConfig
{
// Pagination template aliases
public $templates = [
// Default templates provided with CodeIgniter
'default_full' => 'CodeIgniter\Pager\Views\default_full',
'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
// Custom template to use Bootstrap
'bootstrap' => 'App\Views\pagination_bootstrap',
];
// Default value for the number of item per page
public $perPage = 25;
}
Bootstrap template for the pagination
Our custom template is not much different from the original template. We add the CSS classes page-item
and page-link
, and we add links to the previous page and the next page with the help of the getPreviousPage()
and getNextPage()
functions.
app/Views/pagination_bootstrap.php
<?php
/**
* @var \CodeIgniter\Pager\PagerRenderer $pager Pagination class instance
* @var array $link Information for a link to a page (created by the foreach instruction)
*/
// Set how many page nunber to show on each sides of the current page number
$pager->setSurroundCount(2);
?>
<nav aria-label="<?= lang('Pager.pageNavigation') ?>">
<ul class="pagination justify-content-center">
<?php if ($pager->hasPrevious()) : ?>
<li class="page-item">
<a href="<?= $pager->getFirst() ?>" aria-label="<?= lang('Pager.first') ?>" class="page-link">
<span aria-hidden="true"><?= lang('Pager.first') ?></span>
</a>
</li>
<li>
<a href="<?= $pager->getPreviousPage() ?>" aria-label="<?= lang('Pager.previous') ?>" class="page-link">
<span aria-hidden="true"><?= lang('Pager.previous') ?></span>
</a>
</li>
<li class="page-item">
<a href="<?= $pager->getPrevious() ?>" class="page-link">
<span aria-hidden="true">«</span>
</a>
</li>
<?php endif ?>
<?php foreach ($pager->links() as $link) : ?>
<li class="<?= $link['active'] ? 'page-item active' : 'page-item' ?>">
<a href="<?= $link['uri'] ?>" class="page-link">
<?= $link['title'] ?>
</a>
</li>
<?php endforeach ?>
<?php if ($pager->hasNext()) : ?>
<li class="page-item">
<a href="<?= $pager->getNext() ?>" class="page-link">
<span aria-hidden="true">»</span>
</a>
</li>
<li>
<a href="<?= $pager->getNextPage() ?>" aria-label="<?= lang('Pager.next') ?>" class="page-link">
<span aria-hidden="true"><?= lang('Pager.next') ?></span>
</a>
</li>
<li class="page-item">
<a href="<?= $pager->getLast() ?>" aria-label="<?= lang('Pager.last') ?>" class="page-link">
<span aria-hidden="true"><?= lang('Pager.last') ?></span>
</a>
</li>
<?php endif ?>
</ul>
</nav>
The recipe_list view
Now all you have to do is display the links to the other pages with the $pager->links()
function. However, this uses the default CodeIgniter template. We are therefore going to call the function by specifying it to use our custom template that we named bootstrap in the configuration file. We could have given it any other name. The first parameter is used to define the pagination group in cases where there are several lists to paginate on the same page. As we only have one set of results to paginate, we pass 'default'
:
<?= $pager->links('default', 'bootstrap') ?>
app/Views/recipe_list.php
<?php
/**
* @var string $page_title The page title (automatically created by CI from the $data array)
* @var string $page_subtitle The page subtitle (automatically created by CI from the $data array)
* @var array $recipes List of recipes (automatically created by CI from the $data array)
* @var App\Entities\Recipe $recipe One recipe (created by the foreach instruction)
* @var \CodeIgniter\Pager\PagerRenderer $pager Pagination class instance
*/
?>
<!DOCTYPE html>
<html lang="en">
<head>
<title><?= esc($page_title) ?></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
crossorigin="anonymous">
<style type="text/css">
.title
{
padding: 3rem 1.5rem;
}
article
{
padding: 1.5rem 1.5rem;
}
</style>
</head>
<body>
<main role="main" class="container">
<div class="title">
<h1>
<?= esc($page_title) ?>
<small class="text-muted"><?= esc($page_subtitle) ?></small>
</h1>
</div>
<div class="container">
<h3>List of recipe</h3>
<ul>
<?php foreach ($recipes as $recipe): ?>
<li><?= anchor('recipe/' . $recipe->slug, $recipe->title) ?></li>
<?php endforeach; ?>
</ul>
<?= $pager->links('default', 'bootstrap') ?>
</div>
</main>
<footer>
<p class="text-center">© 2020 My recipe website</p>
</footer>
</body>
</html>
The recipe view
In the recipe view, I added a call to the nl2br()
function which means "new line to break". That is, it transforms the newline characters \n
into their HTML equivalent: <br>
. Add the function call at the line where the instructions are displayed:
app/Views/recipe.php
<h5>Instructions</h5>
<p><?= nl2br( esc($recipe->instructions) ) ?></p>
Fourth part finished!
You can now refresh the page http://ci4.test:8888 which will now display 25 recipes per page with links to go to the next and previous page, first and last page, page numbers and links to move forward or backward several pages. Now that we can see what the application looks like with a ton of recipes spread over several pages, it's obvious that it's going to need a search form. This is what we are going to do right now in the fifth part.