Skip to content

Bug: in-model pager ignores the model event (with where clause, run via callback), reporting incorrect paging information #8412

Closed

Description

@dgvirtual

PHP Version

8.1

CodeIgniter4 Version

4.4.4

CodeIgniter4 Installation Method

Composer (using codeigniter4/appstarter)

Which operating systems have you tested for this bug?

Linux

Which server did you use?

cli-server (PHP built-in webserver)

Database

SQLite3

What happened?

I have a custom event in a model that applies where clause to every search implemented thus:

protected $beforeFind = ['restrictToUser'];

    protected function restrictToUser(array $data)
    {
        // in my case the user_id comes from session, hardcode here:
        $this->where($this->table . '.user_id', 3);

        return $data;
    }

When I use it with an in-model pager, the find results apply this event, but the pager does not, resulting in wrong page count and wrong total record number.

Steps to Reproduce

Start with empty project.
Rename env to .env, change these lines in it to the values:

app.baseURL = 'http://localhost:8080/'
database.default.database = ci4
database.default.DBDriver = SQLite3

Add a cli route in app/Config/Routes.php:

$routes->cli('home', 'Home::index');

Add migration for a table:

php spark make:migration CreateTestTable

Replace up() and down() methods with:

public function up()
    {
        $this->forge->addField([
            'id' => [
                'type'           => 'INT',
                'constraint'     => 10,
                'auto_increment' => true,
                'null'           => false,
            ],
            'user_id' => [
                'type'       => 'INT',
                'constraint' => 10,
                'null'       => false,
            ],
            'data' => [
                'type'       => 'VARCHAR',
                'constraint' => 255,
                'null'       => false,
            ],
            'created_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
            'updated_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
            'deleted_at' => [
                'type' => 'DATETIME',
                'null' => true,
            ],
        ]);

        $this->forge->addKey('id', true);
        $this->forge->createTable('test');
    }

    public function down()
    {
        $this->forge->dropTable('test');
    }

make a seeder:

php spark make:seeder test --suffix

Replace the seeder method run() with this:

public function run()
    {
        $builder = $this->db->table('test');
        $data = [];
        
        $i = 1;
        while ($i < 100) {

            $user_id = rand(2, 4);
            $data = [
                'user_id'        => $user_id,
                'data'           => 'Some text by user ' . $user_id . ', entry ' . $i,
                'created_at'     => '2024-01-09 12:51:33',
                'updated_at'     => '2024-01-09 12:51:33',
            ];

            $builder->insert($data);

            $i++;
        }
    }

Create model:

php spark make:model TestModel

in model, update the $beforeFind callback thus:

protected $beforeFind = ['restrictToUser'];

and add event method to model:

    protected function restrictToUser(array $data)
    {
        // in my case the user_id comes from session, hardcode here:
        $this->where($this->table . '.user_id', 3);

        return $data;
    }

Add test code to the Home controller, replace the index() method with:

    public function index()
    {
        $model = model(\App\Models\TestModel::class);
        echo PHP_EOL;

        echo 'Total number of entries in table (should be 99): ' . $model->countAllResults(false);
        echo PHP_EOL . PHP_EOL;

        $list = $model->findAll();
        //$list = $model->where('user_id', 3)->findAll();

        echo 'Number with user_id = 3, using findAll(), event restrictToUser applied (should be ~1/3 of 99): ' . count($list);
        echo PHP_EOL . PHP_EOL;

        echo 'Rerun find with paginate() (squeze all results in one page):' ;
        $list = $model->paginate(101);
        $pager = $model->pager;
        echo '... done.' . PHP_EOL . PHP_EOL;

        echo 'Real number of results, that honours event restrictToUser (should be ~1/3 of 99): ' . count($list);
        echo PHP_EOL . PHP_EOL;

        echo 'pager-reported number, that ignores event restrictToUser  (should be ~1/3 of 99, but is 99): ' . $pager->getTotal();
        echo PHP_EOL . PHP_EOL;

        echo 'Rerun find with  paginate() (10 per page):' ;
        $list = $model->paginate(10);
        $pager = $model->pager;
        echo '... done.' . PHP_EOL . PHP_EOL;

        echo 'Pager-reported number, that ignores event restrictToUser (should be ~1/3 of 99, but is 99): ' . $pager->getTotal();
        echo PHP_EOL . PHP_EOL;

        echo 'Pager-reported page number, that ignores event restrictToUser (should be ~3, but is 10) : ' . $pager->getPageCount();
        echo PHP_EOL . PHP_EOL;

    }

Run the test :

dg@tvenkinys:~/Programavimas/MOK/ci4bugDemo$ php spark migrate

CodeIgniter v4.4.4 Command Line Tool - Server Time: 2024-01-12 08:44:50 UTC+00:00

Running all new migrations...
        Running: (App) 2024-01-10-214032_App\Database\Migrations\CreateTestTable
Migrations complete.
dg@tvenkinys:~/Programavimas/MOK/ci4bugDemo$ php spark db:seed TestSeeder

CodeIgniter v4.4.4 Command Line Tool - Server Time: 2024-01-12 08:45:12 UTC+00:00

Seeded: App\Database\Seeds\TestSeeder

dg@tvenkinys:~/Programavimas/MOK/ci4bugDemo$ php public/index.php home

Total number of entries in table (should be 99): 99

Number with user_id = 3, using findAll(), event restrictToUser applied (should be ~1/3 of 99): 28

Rerun find with paginate() (squeze all results in one page):... done.

Real number of results, that honours event restrictToUser (should be ~1/3 of 99): 28

pager-reported number, that ignores event restrictToUser  (should be ~1/3 of 99, but is 99): 99

Rerun find with  paginate() (10 per page):... done.

Pager-reported number, that ignores event restrictToUser (should be ~1/3 of 99, but is 99): 99

Pager-reported page number, that ignores event restrictToUser (should be ~3, but is 10) : 10

Expected Output

I expect the in-model pager report the same number of entries as returned by the model paginate() method, applying the in-model specified event correctly.

Anything else?

No response

Activity

  1. added
    bugVerified issues on the current code behavior or pull requests that will fix them
    on Jan 12, 2024
  2. kenjis commented on Jan 12, 2024

    @kenjis
    Member

    You reset the query with countAllResults().

    echo 'total number of entries in table: ' . $model->countAllResults();

    Try countAllResults(false).

  3. dgvirtual commented on Jan 12, 2024

    @dgvirtual
    ContributorAuthor

    Try countAllResults(false).

    Tried; it changes nothing. Updated the test code above. And thanks for code colouring :)

  4. kenjis commented on Jan 12, 2024

    @kenjis
    Member

    Ah, countAllResults() does not take care of events, so the result is always wrong.

  5. dgvirtual commented on Jan 12, 2024

    @dgvirtual
    ContributorAuthor

    Ah, countAllResults() does not take care of events, so the result is always wrong.

    I assumed that, no problem here. It is the results at this line in output:

    pager-reported number, that ignores event restrictToUser ...

    and later that are the problem. Pager counts the number of results that the model returns wrong.

  6. dgvirtual commented on Jan 12, 2024

    @dgvirtual
    ContributorAuthor

    My theory of this error after looking in the code of BaseModel.php:

    The $beforeFind callbacks are fired in the findAll() method, so, at the very last element of the chain of building sql command.

    The method paginate() uses the the findAll() method internally, but only calls it AFTER in initiates the Pager library and adds all the data to it. So, the Pager is initiated before the callback runs within the findAll(), on line 1239:

    $this->pager = $pager->store($group, $page, $perPage, $this->countAllResults(false), $segment);

    How does that sound?

  7. kenjis commented on Jan 12, 2024

    @kenjis
    Member

    It seems that we cannot solve this issue easily.

    When we generate pagination, we need to:

    1. get all count (countAllResults(false))
    2. get paginated result set (findAll($limit, $offset)) (Note that we limit the result)

    Also:
    3. $beforeFind callbacks are NOT fired in countAllResults().
    4. findAll() resets the query.

    If we call findAll($limit, $offset) before countAllResults(false), countAllResults(false) does not work.
    Even if we stop findAll() to reset the query (there is no API to do so now), the countAllResults(false) result will be limited and wrong.

    So the query order is not wrong in the current code:

    public function paginate(?int $perPage = null, string $group = 'default', ?int $page = null, int $segment = 0)
    {
    // Since multiple models may use the Pager, the Pager must be shared.
    $pager = Services::pager();
    if ($segment !== 0) {
    $pager->setSegment($segment, $group);
    }
    $page = $page >= 1 ? $page : $pager->getCurrentPage($group);
    // Store it in the Pager library, so it can be paginated in the views.
    $this->pager = $pager->store($group, $page, $perPage, $this->countAllResults(false), $segment);
    $perPage = $this->pager->getPerPage($group);
    $offset = ($pager->getCurrentPage($group) - 1) * $perPage;
    return $this->findAll($perPage, $offset);
    }

  8. kenjis commented on Jan 13, 2024

    @kenjis
    Member

    I don't know how to fix this issue or if we should not fix it.

    Workaround is to override paginate() something like this?

        public function paginate(?int $perPage = null, string $group = 'default', ?int $page = null, int $segment = 0)
        {
            // Fire restrictToUser() manually. // Added
            $this->restrictToUser([]);         // Added
    
            // Since multiple models may use the Pager, the Pager must be shared.
            $pager = Services::pager();
    
            if ($segment !== 0) {
                $pager->setSegment($segment, $group);
            }
    
            $page = $page >= 1 ? $page : $pager->getCurrentPage($group);
            // Store it in the Pager library, so it can be paginated in the views.
            $this->pager = $pager->store($group, $page, $perPage, $this->countAllResults(false), $segment);
            $perPage     = $this->pager->getPerPage($group);
            $offset      = ($pager->getCurrentPage($group) - 1) * $perPage;
    
            // Disable model events for findAll(). // Added
            $this->allowCallbacks(false);          // Added
    
            return $this->findAll($perPage, $offset);
        }
  9. dgvirtual commented on Jan 13, 2024

    @dgvirtual
    ContributorAuthor

    I don't know how to fix this issue or if we should not fix it.

    Workaround is to override paginate() something like this?

    I see; I was looking at the code yesterday and also could not envisage a way to improve it (but thought maybe the more experienced people might). The suggested workaround would work I guess while I only have that single callback; either that or just add the needed where clause manually before the call to pagination (as I do now; it does generate a double WHERE but it works).

    I am leaving the issue open, but I am ok with it being closed as WONTFIX if an efficient solution cannot be found. Thanks for investigating, @kenjis!

  10. michalsn commented on Jan 13, 2024

    @michalsn
    Member

    I wouldn't say it's a bug. Documentation is clear, what methods are affected by the database events: https://codeigniter.com/user_guide/models/model.html#event-parameters

    Although this may be inconvenient, just like in the presented case. To make it work, we would have to modify the countAllResults() method, to also respect the beforeFind events. TBH, I'm not convinced that we should since these events are designed to work with different types of queries.

  11. removed
    bugVerified issues on the current code behavior or pull requests that will fix them
    on Jan 13, 2024
  12. kenjis commented on Jan 14, 2024

    @kenjis
    Member

    According to the current documentation, it is not a bug. The behavior is expected.

    My question is: should countAllResults() respect Model *Find Events?
    Because it is also a method to find. But I'm not sure we should or not.

  13. lonnieezell commented on Jan 14, 2024

    @lonnieezell
    Member

    The events only work on model-specific methods and countAllResults is from the query builder. I understand that this is a weird case but no I don't think so. The events were never intended to change the number of results, only modify the results that were found. Filtering should happen after results are returned.

  14. kenjis commented on Jan 14, 2024

    @kenjis
    Member

    @lonnieezell Model has countAllResults():

    public function countAllResults(bool $reset = true, bool $test = false)

    The events were never intended to change the number of results, only modify the results that were found. Filtering should happen after results are returned.

    Do you say the following Model callback is kind of a misuse?

        protected function restrictToUser(array $data)
        {
            // in my case the user_id comes from session, hardcode here:
            $this->where($this->table . '.user_id', 3);
    
            return $data;
        }
  15. lonnieezell commented on Jan 14, 2024

    @lonnieezell
    Member

    Ah, I forgot that was overridden for soft-deletes. Seems I misspoke.

    Would I say it's a misuse? No, just not what events were originally thought of. Everything always gets used in creative ways you didn't expect. Part of the fun - seeing how others use your stuff. :)

    Thinking of it another way, though, no I wouldn't expect a count() query to use the same event as a find() query, though.

  16. juanma386 commented on Aug 25, 2024

    @juanma386

    This is Controller function:

    <?php
    
    namespace App\Controllers;
    
    use App\Models\BusinessModel;
    use CodeIgniter\RESTful\ResourceController;
    use App\Models\{
        BusinessProfilesModel,
        ClientApplicationsModel,
    };
    use Config\{
        Database,
        Services,
    };
    
    class BusinessController extends ResourceController
    {
        protected $modelName = ClientApplicationsModel::class;
        protected $format = 'json';
    
        public function __construct()
        {
            // Load the model
            $this->db = Database::connect(); // Forzar la conexión a la base de datos
            $this->pager = Services::pager();
        }
        public function apps(){
            $model = model(\App\Models\ClientApplicationsModel::class);
            echo PHP_EOL;
    
            echo 'Total number of entries in table (should be 99): ' . $model->countAllResults(false);
            echo PHP_EOL . PHP_EOL;
    
            $list = $model->findAll();
            //$list = $model->where('user_id', 3)->findAll();
    
            echo 'Number with user_id = 3, using findAll(), event restrictToUser applied (should be ~1/3 of 99): ' . count($list);
            echo PHP_EOL . PHP_EOL;
    
            echo 'Rerun find with paginate() (squeze all results in one page):' ;
            $list = $model->paginate(101);
            $pager = $model->pager;
            echo '... done.' . PHP_EOL . PHP_EOL;
    
            echo 'Real number of results, that honours event restrictToUser (should be ~1/3 of 99): ' . count($list);
            echo PHP_EOL . PHP_EOL;
    
            echo 'pager-reported number, that ignores event restrictToUser  (should be ~1/3 of 99, but is 99): ' . $pager->getTotal();
            echo PHP_EOL . PHP_EOL;
    
            echo 'Rerun find with  paginate() (10 per page):' ;
            $list = $model->paginate(10);
            $pager = $model->pager;
            echo '... done.' . PHP_EOL . PHP_EOL;
    
            echo 'Pager-reported number, that ignores event restrictToUser (should be ~1/3 of 99, but is 99): ' . $pager->getTotal();
            echo PHP_EOL . PHP_EOL;
    
            echo 'Pager-reported page number, that ignores event restrictToUser (should be ~3, but is 10) : ' . $pager->getPageCount();
            echo PHP_EOL . PHP_EOL;
    
        }

    Testing

    # Displayed at 00:34:00am — PHP: 8.3.4 Displayed at 00:34:00am — PHP: 8.3.4 — CodeIgniter: 4.5.4 -- Environment: development
    
    ## TypeError
    CodeIgniter\Database\BaseResult::getResult(): Argument #1 ($type) must be of type string, null given, called in /var/www/business.hexome.cloud/html/system/Model.php on line 287
    SYSTEMPATH/Database/BaseResult.php at line 106
    
     99     /**
    100      * Retrieve the results of the query. Typically an array of
    101      * individual data rows, which can be either an 'array', an
    102      * 'object', or a custom class name.
    103      *
    104      * @param string $type The row type. Either 'array', 'object', or a class name to use
    105      */
    106     public function getResult(string $type = 'object'): array
    107     {
    108         if ($type === 'array') {
    109             return $this->getResultArray();
    110         }
    111 
    112         if ($type === 'object') {
    113             return $this->getResultObject();
    [Backtrace](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#backtrace) [Server](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#server) [Request](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#request) [Response](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#response) [Files](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#files) [Memory](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#memory)
    SYSTEMPATH/Model.php : 287   —  CodeIgniter\Database\BaseResult->getResult ()
    
    280 
    281         if ($this->tempUseSoftDeletes) {
    282             $builder->where($this->table . '.' . $this->deletedField, null);
    283         }
    284 
    285         $results = $builder->limit($limit, $offset)
    286             ->get()
    287             ->getResult($this->tempReturnType);
    288 
    289         if ($useCast) {
    290             foreach ($results as $i => $row) {
    291                 $results[$i] = $this->convertToReturnType($row, $returnType);
    292             }
    293 
    294             $this->tempReturnType = $returnType;
    SYSTEMPATH/BaseModel.php : 676   —  CodeIgniter\Model->doFindAll ()
    
    669 
    670             if (isset($eventData['returnData']) && $eventData['returnData'] === true) {
    671                 return $eventData['data'];
    672             }
    673         }
    674 
    675         $eventData = [
    676             'data'      => $this->doFindAll($limit, $offset),
    677             'limit'     => $limit,
    678             'offset'    => $offset,
    679             'method'    => 'findAll',
    680             'singleton' => false,
    681         ];
    682 
    683         if ($this->tempAllowCallbacks) {
    APPPATH/Controllers/BusinessController.php : 35   —  CodeIgniter\BaseModel->findAll ()
    
    28     public function apps(){
    29         $model = model(\App\Models\ClientApplicationsModel::class);
    30         echo PHP_EOL;
    31 
    32         echo 'Total number of entries in table (should be 99): ' . $model->countAllResults(false);
    33         echo PHP_EOL . PHP_EOL;
    34 
    35         $list = $model->findAll();
    36         //$list = $model->where('user_id', 3)->findAll();
    37 
    38         echo 'Number with user_id = 3, using findAll(), event restrictToUser applied (should be ~1/3 of 99): ' . count($list);
    39         echo PHP_EOL . PHP_EOL;
    40 
    41         echo 'Rerun find with paginate() (squeze all results in one page):' ;
    42         $list = $model->paginate(101);
    SYSTEMPATH/CodeIgniter.php : 933   —  App\Controllers\BusinessController->apps ()
    
    926         // This is a Web request or PHP CLI request
    927         $params = $this->router->params();
    928 
    929         // The controller method param types may not be string.
    930         // So cannot set `declare(strict_types=1)` in this file.
    931         $output = method_exists($class, '_remap')
    932             ? $class->_remap($this->method, ...$params)
    933             : $class->{$this->method}(...$params);
    934 
    935         $this->benchmark->stop('controller');
    936 
    937         return $output;
    938     }
    939 
    940     /**
    SYSTEMPATH/CodeIgniter.php : 509   —  CodeIgniter\CodeIgniter->runController ()
    
    502             if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
    503                 throw PageNotFoundException::forMethodNotFound($this->method);
    504             }
    505 
    506             // Is there a "post_controller_constructor" event?
    507             Events::trigger('post_controller_constructor');
    508 
    509             $returned = $this->runController($controller);
    510         } else {
    511             $this->benchmark->stop('controller_constructor');
    512             $this->benchmark->stop('controller');
    513         }
    514 
    515         // If $returned is a string, then the controller output something,
    516         // probably a view, instead of echoing it directly. Send it along
    SYSTEMPATH/CodeIgniter.php : 355   —  CodeIgniter\CodeIgniter->handleRequest ()
    
    348         $possibleResponse = $this->runRequiredBeforeFilters($filters);
    349 
    350         // If a ResponseInterface instance is returned then send it back to the client and stop
    351         if ($possibleResponse instanceof ResponseInterface) {
    352             $this->response = $possibleResponse;
    353         } else {
    354             try {
    355                 $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
    356             } catch (DeprecatedRedirectException|ResponsableInterface $e) {
    357                 $this->outputBufferingEnd();
    358                 if ($e instanceof DeprecatedRedirectException) {
    359                     $e = new RedirectException($e->getMessage(), $e->getCode(), $e);
    360                 }
    361 
    362                 $this->response = $e->getResponse();
    SYSTEMPATH/Boot.php : 325   —  CodeIgniter\CodeIgniter->run ()
    
    318 
    319     /**
    320      * Now that everything is set up, it's time to actually fire
    321      * up the engines and make this app do its thang.
    322      */
    323     protected static function runCodeIgniter(CodeIgniter $app): void
    324     {
    325         $app->run();
    326     }
    327 
    328     protected static function saveConfigCache(FactoriesCache $factoriesCache): void
    329     {
    330         $factoriesCache->save('config');
    331     }
    332 
    SYSTEMPATH/Boot.php : 67   —  CodeIgniter\Boot::runCodeIgniter ()
    
    60         if ($configCacheEnabled) {
    61             $factoriesCache = static::loadConfigCache();
    62         }
    63 
    64         static::autoloadHelpers();
    65 
    66         $app = static::initializeCodeIgniter();
    67         static::runCodeIgniter($app);
    68 
    69         if ($configCacheEnabled) {
    70             static::saveConfigCache($factoriesCache);
    71         }
    72 
    73         // Exits the application, setting the exit code for CLI-based
    74         // applications that might be watching.
    FCPATH/index.php : 56   —  CodeIgniter\Boot::bootWeb ()
    
    49 // ^^^ Change this line if you move your application folder
    50 
    51 $paths = new Config\Paths();
    52 
    53 // LOAD THE FRAMEWORK BOOTSTRAP FILE
    54 require $paths->systemDirectory . '/Boot.php';
    55 
    56 exit(CodeIgniter\Boot::bootWeb($paths));
    57— CodeIgniter: 4.5.4 -- Environment: development
    TypeError
    CodeIgniter\Database\BaseResult::getResult(): Argument #1 ($type) must be of type string, null given, called in /var/www/business.hexome.cloud/html/system/Model.php on line 287
    SYSTEMPATH/Database/BaseResult.php at line 106
    
     99     /**
    100      * Retrieve the results of the query. Typically an array of
    101      * individual data rows, which can be either an 'array', an
    102      * 'object', or a custom class name.
    103      *
    104      * @param string $type The row type. Either 'array', 'object', or a class name to use
    105      */
    106     public function getResult(string $type = 'object'): array
    107     {
    108         if ($type === 'array') {
    109             return $this->getResultArray();
    110         }
    111 
    112         if ($type === 'object') {
    113             return $this->getResultObject();
    [Backtrace](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#backtrace) [Server](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#server) [Request](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#request) [Response](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#response) [Files](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#files) [Memory](http://business.hexome.cloud/api/business/apps?page=1&limit=10&user_id=04ffb579-5f30-11ef-83a0-7071bcbe887a#memory)
    SYSTEMPATH/Model.php : 287   —  CodeIgniter\Database\BaseResult->getResult ()
    
    280 
    281         if ($this->tempUseSoftDeletes) {
    282             $builder->where($this->table . '.' . $this->deletedField, null);
    283         }
    284 
    285         $results = $builder->limit($limit, $offset)
    286             ->get()
    287             ->getResult($this->tempReturnType);
    288 
    289         if ($useCast) {
    290             foreach ($results as $i => $row) {
    291                 $results[$i] = $this->convertToReturnType($row, $returnType);
    292             }
    293 
    294             $this->tempReturnType = $returnType;
    SYSTEMPATH/BaseModel.php : 676   —  CodeIgniter\Model->doFindAll ()
    
    669 
    670             if (isset($eventData['returnData']) && $eventData['returnData'] === true) {
    671                 return $eventData['data'];
    672             }
    673         }
    674 
    675         $eventData = [
    676             'data'      => $this->doFindAll($limit, $offset),
    677             'limit'     => $limit,
    678             'offset'    => $offset,
    679             'method'    => 'findAll',
    680             'singleton' => false,
    681         ];
    682 
    683         if ($this->tempAllowCallbacks) {
    APPPATH/Controllers/BusinessController.php : 35   —  CodeIgniter\BaseModel->findAll ()
    
    28     public function apps(){
    29         $model = model(\App\Models\ClientApplicationsModel::class);
    30         echo PHP_EOL;
    31 
    32         echo 'Total number of entries in table (should be 99): ' . $model->countAllResults(false);
    33         echo PHP_EOL . PHP_EOL;
    34 
    35         $list = $model->findAll();
    36         //$list = $model->where('user_id', 3)->findAll();
    37 
    38         echo 'Number with user_id = 3, using findAll(), event restrictToUser applied (should be ~1/3 of 99): ' . count($list);
    39         echo PHP_EOL . PHP_EOL;
    40 
    41         echo 'Rerun find with paginate() (squeze all results in one page):' ;
    42         $list = $model->paginate(101);
    SYSTEMPATH/CodeIgniter.php : 933   —  App\Controllers\BusinessController->apps ()
    
    926         // This is a Web request or PHP CLI request
    927         $params = $this->router->params();
    928 
    929         // The controller method param types may not be string.
    930         // So cannot set `declare(strict_types=1)` in this file.
    931         $output = method_exists($class, '_remap')
    932             ? $class->_remap($this->method, ...$params)
    933             : $class->{$this->method}(...$params);
    934 
    935         $this->benchmark->stop('controller');
    936 
    937         return $output;
    938     }
    939 
    940     /**
    SYSTEMPATH/CodeIgniter.php : 509   —  CodeIgniter\CodeIgniter->runController ()
    
    502             if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
    503                 throw PageNotFoundException::forMethodNotFound($this->method);
    504             }
    505 
    506             // Is there a "post_controller_constructor" event?
    507             Events::trigger('post_controller_constructor');
    508 
    509             $returned = $this->runController($controller);
    510         } else {
    511             $this->benchmark->stop('controller_constructor');
    512             $this->benchmark->stop('controller');
    513         }
    514 
    515         // If $returned is a string, then the controller output something,
    516         // probably a view, instead of echoing it directly. Send it along
    SYSTEMPATH/CodeIgniter.php : 355   —  CodeIgniter\CodeIgniter->handleRequest ()
    
    348         $possibleResponse = $this->runRequiredBeforeFilters($filters);
    349 
    350         // If a ResponseInterface instance is returned then send it back to the client and stop
    351         if ($possibleResponse instanceof ResponseInterface) {
    352             $this->response = $possibleResponse;
    353         } else {
    354             try {
    355                 $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse);
    356             } catch (DeprecatedRedirectException|ResponsableInterface $e) {
    357                 $this->outputBufferingEnd();
    358                 if ($e instanceof DeprecatedRedirectException) {
    359                     $e = new RedirectException($e->getMessage(), $e->getCode(), $e);
    360                 }
    361 
    362                 $this->response = $e->getResponse();
    SYSTEMPATH/Boot.php : 325   —  CodeIgniter\CodeIgniter->run ()
    
    318 
    319     /**
    320      * Now that everything is set up, it's time to actually fire
    321      * up the engines and make this app do its thang.
    322      */
    323     protected static function runCodeIgniter(CodeIgniter $app): void
    324     {
    325         $app->run();
    326     }
    327 
    328     protected static function saveConfigCache(FactoriesCache $factoriesCache): void
    329     {
    330         $factoriesCache->save('config');
    331     }
    332 
    SYSTEMPATH/Boot.php : 67   —  CodeIgniter\Boot::runCodeIgniter ()
    
    60         if ($configCacheEnabled) {
    61             $factoriesCache = static::loadConfigCache();
    62         }
    63 
    64         static::autoloadHelpers();
    65 
    66         $app = static::initializeCodeIgniter();
    67         static::runCodeIgniter($app);
    68 
    69         if ($configCacheEnabled) {
    70             static::saveConfigCache($factoriesCache);
    71         }
    72 
    73         // Exits the application, setting the exit code for CLI-based
    74         // applications that might be watching.
    FCPATH/index.php : 56   —  CodeIgniter\Boot::bootWeb ()
    
    49 // ^^^ Change this line if you move your application folder
    50 
    51 $paths = new Config\Paths();
    52 
    53 // LOAD THE FRAMEWORK BOOTSTRAP FILE
    54 require $paths->systemDirectory . '/Boot.php';
    55 
    56 exit(CodeIgniter\Boot::bootWeb($paths));
    57
    
  17. juanma386 commented on Aug 25, 2024

    @juanma386

    To solve this error of deprecate documentation oficial of with bug on:
    Displayed at 00:34:00am — PHP: 8.3.4 Displayed at 00:34:00am — PHP: 8.3.4 — CodeIgniter: 4.5.4 -- Environment: development
    Simple solve error with CI please use this:

    /**
        * Retrieves paginated businesses for a specific user using raw SQL.
        *
        * @param int $page The current page number
        * @param int $limit The number of items per page
        * @param string $userId The user ID to filter the businesses
        * @return array Paginated business data and pagination details
        */
        public function getPaginatedResults(int $page, int $limit, string $userId): array
        {
            // Calculate the offset
            $offset = ($page - 1) * $limit;
    
            // SQL query to get paginated results
            $sql = "SELECT * FROM $this->table WHERE user_id = :user_id: ORDER BY created_at ASC LIMIT :limit: OFFSET :offset:";
    
            // Execute the query
            $query = $this->db->query($sql, [
                'user_id' => $userId,
                'limit' => $limit,
                'offset' => $offset
            ]);
    
            // Get the results
            $results = $query->getResult();
    
            // SQL query to count the total records
            $countSql = "SELECT COUNT(*) as total FROM $this->table WHERE user_id = :user_id:";
            $totalQuery = $this->db->query($countSql, ['user_id' => $userId]);
            $total = $totalQuery->getRow()->total;
    
            // Build the response with paginated data and pagination details
            return [
                'data' => $results,
                'pagination' => [
                    'total' => $total,
                    'per_page' => $limit,
                    'current_page' => $page,
                    'last_page' => ceil($total / $limit),
                    'next' => $page < ceil($total / $limit) ? $page + 1 : null,
                    'previous' => $page > 1 ? $page - 1 : null,
                ],
            ];
        }

    use to query please use warning with Validator to restrict access or sql insection to $userId;
    To testing in the dev enviroment is:

    public function apps()
        {
            $pager = service('pager');   
            $page = intval($this->request->getVar('page', FILTER_VALIDATE_INT)) ?: 1;
            $limit = intval($this->request->getVar('limit', FILTER_VALIDATE_INT)) ?: 10;
            $userId = $this->request->getVar('user_id');
    
            if (empty($userId)) {
                return $this->fail('User ID is required.', 400);
            }
    
            if ($page <= 0) {
                return $this->fail('Page number must be greater than 0.', 400);
            }
    
            if ($limit <= 0) {
                return $this->fail('Limit must be greater than 0.', 400);
            }
    
            try {
                $result = $this->model->getPaginatedResults($page, $limit, $userId);
                return $this->respond([
                    'status' => 200,
                    'error' => null,
                    'data' => $result['data'],
                    'pagination' => $result['pagination'],
                ]);
            } catch (\Exception $e) {
                return $this->fail('Error in querying the database: ' . $e->getMessage(), 500);
            }
        }
    {
      "status": 200,
      "error": null,
      "data": [
        {
          "client_id": "50512010-624f-11ef-b9bf-7071bcbe887a"
        },
        {
          "client_id": "5c1a2230-6254-11ef-b9bf-7071bcbe887a"
        },
        {
          "client_id": "620bd04f-6258-11ef-b9bf-7071bcbe887a"
        },
        {
          "client_id": "57f55e69-625c-11ef-b9bf-7071bcbe887a"
        }
      ],
      "pagination": {
        "total": "4",
        "per_page": 10,
        "current_page": 1,
        "last_page": 1,
        "next": null,
        "previous": null
      }
    }
  18. mavelo-llc commented on Jun 27, 2025

    @mavelo-llc

    Same issue?

    Figured using beforeFind to apply conditions should work, but pager ignores them.

    Example Table: Users has 5 rows; only one with status = '1'

    model('UserModel')->where('status', 1);
    $rows = model('UserModel')->paginate(2);
    $links = model('UserModel')->pager->links();

    Result: 1 row; 1 page link (correct)

    $rows = model('UserModel')->paginate(2);
    $links = model('UserModel')->pager->links();

    Result: 5 rows; 3 page links (correct)

    // Simplified for example
    class UserModel extends BaseModel {
        protected $beforeFind = ['onlyEnabled'];
    
        protected function onlyEnabled(array $data) {
            $this->where('status', 1);
            
            return $data;
        }

    After moving where to the model's onlyEnabled method...

    $rows = model('UserModel')->paginate(2);
    $links = model('UserModel')->pager->links();

    Result: 1 row; 3 page links (wrong)

    Was hoping to prevent code duplication without manually calling a 'filter' method before every find|findAll|first call, but seems there is no way to make pager work with before/after events the way we hoped. Our models are shared between multiple modules, so we don't really want to rewrite pager.

  19. michalsn commented on Jun 28, 2025

    @michalsn
    Member

    @mavelo-llc Please check this PR: #9617

  20. michalsn commented on Jul 15, 2025

    @michalsn
    Member

    We've decided to document the current behavior, as "fixing" it could introduce confusion in other situations - beforeFind event is specifically intended to run only before find* methods.

    If this behavior is desired in your app, feel free to follow the implementation shared in #9617 or the dedicated article, which includes a handy trait.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions