Using Model::paginate() for SQL Join in CodeIgniter 4
From Model, in CodeIgniter 4, we can paginate existing query that we use in current table defined in $table property, eg in model, like the following:
<?php namespace App\Models;
use CodeIgniter\Model;
class ProductModel extends Model
{
/**
* define properties table, returnType, allowedFields, validationRules, etc here
*/
// ...
public function getPaginatedProductData(string $keyword = ''): array
{
if ($keyword)
{
$this->builder()
->groupStart()
->like('product_code', $keyword)
->orLike('product_name', $keyword)
->groupEnd();
}
return [
'products' => $this->paginate(),
'pager' => $this->pager,
];
}
// ...
}
That’s for paginate rows in same table, how about in SQL Join? We can! For example, we have a use case to get product and price from the following table relation

that can be grabbed with join:
SELECT
`product`.`*`,
`price`.`price`
FROM
`product`
JOIN
`price`
ON
`product`.`id` = `price`.`product_id`
WHERE
`price`.`date` = DATE_FORMAT(NOW(),'%Y-%m-%d');
If we want an object representation with entity class, we can create an entity for that:
<?php namespace App\Entities;
use CodeIgniter\Entity;
class ProductWithPrice extends Entity
{
protected $attributes = [
'id' => null,
'product_code' => null,
'product_name' => null,
'price' => null,
];
}
Now, in the model, we can query the join and then paginate:
<?php namespace App\Models;
use App\Entities\ProductWithPrice;
use CodeIgniter\Model;
class ProductModel extends Model
{
// ...
public function getPaginatedProductWithPriceData()
{
$this->builder()
->select(["{$this->table}.*", 'price.price'])
->join('price', "{$this->table}.id = price.product_id")
->where("price.date = DATE_FORMAT(NOW(),'%Y-%m-%d')");
return [
'productWithPrices' => $this->asObject(ProductWithPrice::class)
->paginate(),
'pager' => $this->pager,
];
}
// ...
}
That’s it, the paginate() function now will paginate the query join we have with the object entity for the result row.
How to Avoid –stderr When Running phpunit for Functional/Integration Testing
When you do a Functional/Integration test with session and/or header relation. It will force you to use --stderr when running phpunit, or it will got error, eg: you’re testing that on logout when session exists as user, page will be redirected to login page with status code 302, and it got the following error:
$ vendor/bin/phpunit test/Integration/LogoutPageTest.php PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Logout Page (AppTest\Integration\LogoutPage) ✘ Open logout page as auser redirect to login page ┐ ├ Failed asserting that 500 matches expected 302. │ ╵ /Users/samsonasik/www/mezzio-authentication-with-authorization/test/Integration/LogoutPageTest.php:36 ┴ Time: 155 ms, Memory: 10.00 MB FAILURES! Tests: 1, Assertions: 1, Failures: 1.
You can use --stderr option on running it:
$ vendor/bin/phpunit test/Integration/LogoutPageTest.php --stderr PHPUnit 8.5.2 by Sebastian Bergmann and contributors. Logout Page (AppTest\Integration\LogoutPage) √ Open logout page as auser redirect to login page Time: 150 ms, Memory: 8.00 MB OK (1 test, 2 assertions)
or define stderr=true in phpunit.xml configuration:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
testdox="true"
stderr="true">
<!-- testsuites, filter, etc config -->
</phpunit>
Marking all test to be using stderr is a workaround, as not all tests actually require that, eg: unit test doesn’t need that. To avoid it, we can define @runTestsInSeparateProcesses and @preserveGlobalState disabled in the controller class that require that, so, the test class will be like the following:
<?php
declare(strict_types=1);
namespace AppTest\Integration;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\Uri;
use Mezzio\Authentication\UserInterface;
use PHPUnit\Framework\TestCase;
/**
* @runTestsInSeparateProcesses
* @preserveGlobalState disabled
*/
class LogoutPageTest extends TestCase
{
private $app;
protected function setUp(): void
{
$this->app = AppFactory::create();
}
public function testOpenLogoutPageAsAuserRedirectToLoginPage()
{
$sessionData = [
'username' => 'samsonasik',
'roles' => [
'user',
],
];
$_SESSION[UserInterface::class] = $sessionData;
$uri = new Uri('/logout');
$serverRequest = new ServerRequest([], [], $uri);
$response = $this->app->handle($serverRequest);
$this->assertEquals(302, $response->getStatusCode());
$this->assertEquals('/login', $response->getHeaderLine('Location'));
}
}
That’s it!
4 comments