PHP code fixes

User
Assistant
Here are the **minimal fixes** you need:

### 1) Imports (add at the top)

use PagerfantaPagerfanta;
use PagerfantaAdapterFixedAdapter;
use ConcreteCoreSearchPaginationPagination;

### 2) Fix the wrong helper call in `tracklog()`
Replace:

['page' => $page, 'perPage' => $perPage, 'offset' => $offset] = $this->getPagination();

with:

['page' => $page, 'perPage' => $perPage, 'offset' => $offset] = $this->getReqData();

### 3) Fix the `$total` query when `$keywords` is present (you forgot to execute)
Replace this block:

if($keywords){
    $total = (int) $db->createQueryBuilder()
        ->select('COUNT(*)')
        ->from(self::T_CFG)
        ->where('name LIKE :keywords OR feedSlug LIKE :keywords OR url LIKE :keywords')
        ->setParameter('keywords', "%$keywords%");
} else {
    $total = (int) $db->createQueryBuilder()
        ->select('COUNT(*)')
        ->from(self::T_CFG)
        ->execute()
        ->fetchOne();
}

with:

if ($keywords) {
    $total = (int) $db->createQueryBuilder()
        ->select('COUNT(*)')
        ->from(self::T_CFG)
        ->where('name LIKE :keywords OR feedSlug LIKE :keywords OR url LIKE :keywords')
        ->setParameter('keywords', "%$keywords%")
        ->execute()
        ->fetchOne();
} else {
    $total = (int) $db->createQueryBuilder()
        ->select('COUNT(*)')
        ->from(self::T_CFG)
        ->execute()
        ->fetchOne();
}

### 4) Replace the `PaginationFactory` usage (it expects a List provider, not an array)
**Remove**:

$factory = new PaginationFactory($this->app->make(Request::class));
$paginator = $factory->createPaginationObject($feeds);
// $pagination = $paginator->renderDefaultView();
// $this->set('pagination', $pagination);
// $this->set('paginator', $paginator);

**Add** (after you’ve got `$total`, `$feeds`, `$page`, `$perPage`):

$adapter = new FixedAdapter($total, $feeds);
$pagerfanta = new Pagerfanta($adapter);
$pagerfanta->setMaxPerPage($perPage);
$pagerfanta->setCurrentPage($page);

$pagination = new Pagination($this->request, $pagerfanta);
$this->set('pagination', $pagination);

Do the **same** in `tracklog()` after fetching `$total` and `$logs`:

$adapter = new FixedAdapter($total, $logs);
$pagerfanta = new Pagerfanta($adapter);
$pagerfanta->setMaxPerPage($perPage);
$pagerfanta->setCurrentPage($page);

$pagination = new Pagination($this->request, $pagerfanta);
$this->set('pagination', $pagination);

That’s it.
– The error came from passing an **array** to `PaginationFactory`, which expects a **List that implements Concrete’s pagination provider**.
– Switching to `Pagerfanta + FixedAdapter` creates a proper pagination object for arbitrary arrays.
– Also fixed the missing `execute()->fetchOne()` and the accidental call to `getPagination()`.

User
ConcreteCoreSearchPaginationPagination::__construct(): Argument #1 ($itemList) must be of type ConcreteCoreSearchItemListItemList, ConcreteCoreHttpRequest given, called in /var/www/html/packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php on line 82
Assistant
You’re instantiating Concrete’s **Pagination** with the wrong first argument. That class expects an **ItemList**, not a Request (hence the error). Since you’re paginating plain arrays (not a Concrete ItemList), the simplest fix is: **don’t use `ConcreteCoreSearchPaginationPagination` at all**—render pagination via Pagerfanta’s own view.

Here are the minimal changes:

### 1) Update `use` statements

use PagerfantaPagerfanta;
use PagerfantaAdapterFixedAdapter;
use PagerfantaViewDefaultView; // or TwitterBootstrap4View if you prefer
use ConcreteCoreSupportFacadeUrl;

### 2) After you build `$feeds` and have `$total`, `$page`, `$perPage`, replace your pagination code with this:

$adapter = new FixedAdapter($total, $feeds);
$pagerfanta = new Pagerfanta($adapter);
$pagerfanta->setMaxPerPage($perPage);
$pagerfanta->setCurrentPage($page);

// Build page links that preserve current query (keywords, limit)
$routeGenerator = function (int $p) use ($perPage, $keywords) {
    $params = ['page' => $p, 'limit' => $perPage];
    if ($keywords) {
        $params['keywords'] = $keywords;
    }
    return (string) Url::to('/dashboard/shopping_feed') . '?' . http_build_query($params);
};

// Render HTML for the pager
$view = new DefaultView(); // or new TwitterBootstrap4View()
$pagerHtml = $view->render($pagerfanta, $routeGenerator, ['proximity' => 2]);

$this->set('feeds', $feeds);
$this->set('total', $total);
$this->set('page', $page);
$this->set('limit', $perPage);
$this->set('keywords', $keywords);
$this->set('pagination', $pagerHtml); // <-- pass HTML to your view

### 3) Do the same in `tracklog()` (swap `$feeds` → `$logs`, and URL path if your action is `tracklog`):

$adapter = new FixedAdapter($total, $logs);
$pagerfanta = new Pagerfanta($adapter);
$pagerfanta->setMaxPerPage($perPage);
$pagerfanta->setCurrentPage($page);

$routeGenerator = function (int $p) use ($perPage) {
    $params = ['page' => $p, 'limit' => $perPage];
    return (string) Url::to('/dashboard/shopping_feed/tracklog') . '?' . http_build_query($params);
};

$view = new DefaultView();
$pagerHtml = $view->render($pagerfanta, $routeGenerator, ['proximity' => 2]);

$this->set('pagination', $pagerHtml);

### 4) In your view (single page template)
Just echo the HTML:

<?php if (!empty($pagination)) { echo $pagination; } ?>

#### Why this works
– `ConcreteCoreSearchPaginationPagination` is meant for Concrete **ItemList** objects (`PageList`, `UserList`, etc.).
– You’re paginating arrays from custom SQL, so **Pagerfanta + FixedAdapter** is perfect, and using its own view avoids needing an ItemList.

If you later convert your queries into a proper `ItemList` subclass, you can go back to `$list->getPagination()` and Concrete’s pagination helper.

User
NO Iwant to use pagination default so convert feed query to list
Assistant
Got it — let’s switch to **Concrete’s default pagination** by using `DatabaseItemList` classes. Below are drop-in list classes plus the controller changes you need.

# 1) Create list classes

## `src/Search/ShoppingFeed/FeedList.php`

<?php
namespace ConcretePackageShoppingFeedManagerSearchShoppingFeed;

use ConcreteCoreSearchItemListDatabaseItemList;
use DoctrineDBALQueryQueryBuilder;

class FeedList extends ItemList
{
    protected $autoSortColumns = ['id', 'name', 'feedSlug', 'url', 'created_at', 'updated_at'];
    protected $itemsPerPage = 10;

    private const T_CFG = 'ShoppingFeedConfiguration';

    /** Build the base SELECT */
    public function createQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('c.*')
           ->from(self::T_CFG, 'c');
        $this->query = $qb;
    }

    /** For count(*) */
    public function createCountQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('COUNT(*)')
           ->from(self::T_CFG, 'c');
        return $qb;
    }

    /** Final tweaks before execution (sorting is handled by parent via sortBy/sortBySearchKey) */
    public function finalizeQuery(QueryBuilder $qb)
    {
        // noop
    }

    /** Map a DB row to an item (array is fine) */
    public function getResult($row)
    {
        return $row; // return as associative array
    }

    /** Convenience filter */
    public function filterByKeywords(string $keywords): void
    {
        $kw = '%' . $keywords . '%';
        $this->query->andWhere('c.name LIKE :kw OR c.feedSlug LIKE :kw OR c.url LIKE :kw')
                    ->setParameter('kw', $kw);
    }
}

## `src/Search/ShoppingFeed/LogList.php`

<?php
namespace ConcretePackageShoppingFeedManagerSearchShoppingFeed;

use ConcreteCoreSearchItemListDatabaseItemList;
use DoctrineDBALQueryQueryBuilder;

class LogList extends ItemList
{
    protected $autoSortColumns = ['l.id', 'l.updated_at', 'l.status', 'c.name', 'c.feedSlug'];
    protected $itemsPerPage = 10;

    private const T_CFG = 'ShoppingFeedConfiguration';
    private const T_LOG = 'ShoppingFeedLog';

    public function createQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('l.id', 'l.feed_id', 'l.status', 'l.updated_at', 'l.error_message', 'c.name AS feed_name', 'c.feedSlug')
           ->from(self::T_LOG, 'l')
           ->leftJoin('l', self::T_CFG, 'c', 'c.id = l.feed_id');
        $this->query = $qb;
    }

    public function createCountQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('COUNT(*)')
           ->from(self::T_LOG, 'l');
        return $qb;
    }

    public function finalizeQuery(QueryBuilder $qb)
    {
        // noop
    }

    public function getResult($row)
    {
        return $row;
    }

    /** Optional keyword filter (search in log status/error and feed name/slug) */
    public function filterByKeywords(string $keywords): void
    {
        $kw = '%' . $keywords . '%';
        $this->query
            ->andWhere('l.status LIKE :kw OR l.error_message LIKE :kw OR c.name LIKE :kw OR c.feedSlug LIKE :kw')
            ->setParameter('kw', $kw);
    }
}

> Make sure your package’s `composer.json` (or `controller.php` autoloader) maps `ConcretePackageShoppingFeedManager` to `packages/shopping_feed_manager/src/`.

# 2) Update your controller to use the lists

<?php
namespace ConcretePackageShoppingFeedManagerControllerSinglePageDashboard;

use ConcreteCorePageControllerDashboardPageController;
use ConcreteCoreHttpRequest;
use ConcreteCoreDatabaseConnectionConnection;
use ConcreteCoreHttpResponseFactoryInterface;
use DoctrineDBALException;

// ADD:
use ConcretePackageShoppingFeedManagerSearchShoppingFeedFeedList;
use ConcretePackageShoppingFeedManagerSearchShoppingFeedLogList;

class ShoppingFeed extends DashboardPageController
{
    private function getReqData(): array
    {
        $req = $this->request;
        $perPage = (int) $req->query->get('limit', 10);
        $perPage = max(1, min($perPage, 100));
        $page = max(1, (int) $req->query->get('page', 1));
        $offset = ($page - 1) * $perPage;
        return compact('page', 'perPage', 'offset');
    }

    public function view()
    {
        ['page' => $page, 'perPage' => $perPage] = $this->getReqData();
        $keywords = (string) $this->request->query->get('keywords', '');

        try {
            $list = new FeedList($this->app);
            $list->sortBy('id', 'desc');                  // default sort
            if ($keywords !== '') {
                $list->filterByKeywords($keywords);
            }
            $list->setItemsPerPage($perPage);

            // Concrete default pagination
            $pagination = $list->getPagination();         // reads ?page from Request
            $feeds = $pagination->getCurrentPageResults();
            $total = $pagination->getTotalResults();

            $this->set('feeds', $feeds);
            $this->set('total', $total);
            $this->set('page', $page);
            $this->set('limit', $perPage);
            $this->set('keywords', $keywords);
            $this->set('pagination', $pagination);        // pass Pagination object to view (->renderDefaultView())
        } catch (Exception $e) {
            $this->error->add($e->getMessage());
        }
    }

    public function tracklog()
    {
        ['page' => $page, 'perPage' => $perPage] = $this->getReqData();
        $keywords = (string) $this->request->query->get('keywords', '');

        try {
            $list = new LogList($this->app);
            $list->sortBy('l.updated_at', 'desc');
            if ($keywords !== '') {
                $list->filterByKeywords($keywords);
            }
            $list->setItemsPerPage($perPage);

            $pagination = $list->getPagination();
            $logs = $pagination->getCurrentPageResults();
            $total = $pagination->getTotalResults();

            $this->set('logs', $logs);
            $this->set('total', $total);
            $this->set('page', $page);
            $this->set('limit', $perPage);
            $this->set('keywords', $keywords);
            $this->set('pagination', $pagination);
        } catch (Exception $e) {
            $this->error->add($e->getMessage());
        }
    }

    // deleteFeed() stays the same
}

# 3) In your view files

Concrete’s pagination object can render itself:

<?php if (isset($pagination)) : ?>
    <div class="ccm-pagination">
        <?php echo $pagination->renderDefaultView(); ?>
    </div>
<?php endif; ?>

(Keep using `$feeds` / `$logs` arrays for rows.)

## Notes

– `DatabaseItemList` gives you `getPagination()` that integrates with Concrete’s default pager.
– Sorting: use `$list->sortBy('column', 'asc|desc')` for columns included in `$autoSortColumns`.
– Keyword filter: done via `filterByKeywords()`. Extend as needed.
– If your tables are actually snake_case, just change the constants in the list classes.

This converts your custom queries to **proper ItemLists**, so the default Concrete pagination “just works.”

User
Fatal error: Class ConcretePackageShoppingFeedManagerSearchShoppingFeedFeedList contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (ConcreteCoreSearchItemListItemList::getTotalResults) in /var/www/html/packages/shopping_feed_manager/src/Search/ShoppingFeed/FeedList.php on line 7
Assistant
Ah 👍 — `ConcreteCoreSearchItemListItemList` (and its subclass `DatabaseItemList`) requires you to implement **`getTotalResults()`**. I didn’t include it in my first draft. That’s why you’re seeing the fatal error.

Here’s the corrected **`FeedList`** and **`LogList`** with `getTotalResults()` implemented:

## `FeedList.php`

<?php
namespace ConcretePackageShoppingFeedManagerSearchShoppingFeed;

use ConcreteCoreSearchItemListDatabaseItemList;
use DoctrineDBALQueryQueryBuilder;

class FeedList extends ItemList
{
    protected $autoSortColumns = ['id', 'name', 'feedSlug', 'url', 'created_at', 'updated_at'];
    protected $itemsPerPage = 10;

    private const T_CFG = 'ShoppingFeedConfiguration';

    public function createQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('c.*')
           ->from(self::T_CFG, 'c');
        $this->query = $qb;
    }

    public function createCountQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('COUNT(*)')
           ->from(self::T_CFG, 'c');
        return $qb;
    }

    public function finalizeQuery(QueryBuilder $qb) {}

    public function getResult($row)
    {
        return $row; // just return the associative array
    }

    public function filterByKeywords(string $keywords): void
    {
        $kw = '%' . $keywords . '%';
        $this->query->andWhere('c.name LIKE :kw OR c.feedSlug LIKE :kw OR c.url LIKE :kw')
                    ->setParameter('kw', $kw);
    }

    /** REQUIRED: total results */
    public function getTotalResults()
    {
        $qb = $this->createCountQuery();
        return (int) $qb->execute()->fetchOne();
    }
}

## `LogList.php`

<?php
namespace ConcretePackageShoppingFeedManagerSearchShoppingFeed;

use ConcreteCoreSearchItemListDatabaseItemList;
use DoctrineDBALQueryQueryBuilder;

class LogList extends ItemList
{
    protected $autoSortColumns = ['l.id', 'l.updated_at', 'l.status', 'c.name', 'c.feedSlug'];
    protected $itemsPerPage = 10;

    private const T_CFG = 'ShoppingFeedConfiguration';
    private const T_LOG = 'ShoppingFeedLog';

    public function createQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('l.id', 'l.feed_id', 'l.status', 'l.updated_at', 'l.error_message', 'c.name AS feed_name', 'c.feedSlug')
           ->from(self::T_LOG, 'l')
           ->leftJoin('l', self::T_CFG, 'c', 'c.id = l.feed_id');
        $this->query = $qb;
    }

    public function createCountQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('COUNT(*)')
           ->from(self::T_LOG, 'l');
        return $qb;
    }

    public function finalizeQuery(QueryBuilder $qb) {}

    public function getResult($row)
    {
        return $row;
    }

    public function filterByKeywords(string $keywords): void
    {
        $kw = '%' . $keywords . '%';
        $this->query
            ->andWhere('l.status LIKE :kw OR l.error_message LIKE :kw OR c.name LIKE :kw OR c.feedSlug LIKE :kw')
            ->setParameter('kw', $kw);
    }

    /** REQUIRED: total results */
    public function getTotalResults()
    {
        $qb = $this->createCountQuery();
        return (int) $qb->execute()->fetchOne();
    }
}

✅ With `getTotalResults()` implemented, both lists will now satisfy the abstract contract of `ItemList`.
Now `$list->getPagination()` will return a proper Concrete pagination object you can render with:

<?php if (isset($pagination)) { echo $pagination->renderDefaultView(); } ?>

Do you want me to also show you how to **wire the CSRF token + deleteFeed into the list results view** (so you can delete feeds directly with Concrete’s dashboard UI helpers)?

User
ConcreteCoreSearchItemListDatabaseItemList::__construct(): Argument #1 ($req) must be of type ?ConcreteCoreSearchStickyRequest, ConcreteCoreApplicationApplication given, called in /var/www/html/packages/shopping_feed_manager/controllers/single_page/dashboard/shopping_feed.php on line 42
Assistant
You’re passing the **Application** into the list constructor, but `DatabaseItemList` expects a **`StickyRequest`** (or `null`). Fix the constructor arg and (optionally) set up the DB connection in your list classes.

Here are the exact changes:

# 1) Controller: pass `StickyRequest` (not `$this->app`)
Add import at the top:

use ConcreteCoreSearchStickyRequest;

Then change both places you create the lists:

**`view()`**

$list = new FeedList($this->app->make(StickyRequest::class)); // was new FeedList($this->app)

**`tracklog()`**

$list = new LogList($this->app->make(StickyRequest::class)); // was new LogList($this->app)

# 2) List classes: constructor + consistent filters on COUNT
(So they work regardless of Concrete version and your count reflects keyword filters.)

## FeedList.php (diff-only)

use ConcreteCoreSearchStickyRequest;
use ConcreteCoreSupportFacadeDatabase;

class FeedList extends ItemList
{
    private const T_CFG = 'ShoppingFeedConfiguration';
    private ?string $keywords = null;

    public function __construct(StickyRequest $req = null)
    {
        parent::__construct($req);
        $this->connection = Database::connection(); // ensure DB set
    }

    public function createQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('c.*')->from(self::T_CFG, 'c');

        if ($this->keywords !== null) {
            $qb->andWhere('c.name LIKE :kw OR c.feedSlug LIKE :kw OR c.url LIKE :kw')
               ->setParameter('kw', '%' . $this->keywords . '%');
        }

        $this->query = $qb;
    }

    public function createCountQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('COUNT(*)')->from(self::T_CFG, 'c');

        if ($this->keywords !== null) {
            $qb->andWhere('c.name LIKE :kw OR c.feedSlug LIKE :kw OR c.url LIKE :kw')
               ->setParameter('kw', '%' . $this->keywords . '%');
        }
        return $qb;
    }

    public function filterByKeywords(string $keywords): void
    {
        $this->keywords = $keywords;
        // if query already built, also apply immediately:
        if ($this->query) {
            $this->query->andWhere('c.name LIKE :kw OR c.feedSlug LIKE :kw OR c.url LIKE :kw')
                        ->setParameter('kw', '%' . $this->keywords . '%');
        }
    }

    public function getTotalResults()
    {
        return (int) $this->createCountQuery()->execute()->fetchOne();
    }
}

## LogList.php (diff-only)

use ConcreteCoreSearchStickyRequest;
use ConcreteCoreSupportFacadeDatabase;

class LogList extends ItemList
{
    private const T_CFG = 'ShoppingFeedConfiguration';
    private const T_LOG = 'ShoppingFeedLog';
    private ?string $keywords = null;

    public function __construct(StickyRequest $req = null)
    {
        parent::__construct($req);
        $this->connection = Database::connection();
    }

    public function createQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('l.id','l.feed_id','l.status','l.updated_at','l.error_message','c.name AS feed_name','c.feedSlug')
           ->from(self::T_LOG, 'l')
           ->leftJoin('l', self::T_CFG, 'c', 'c.id = l.feed_id');

        if ($this->keywords !== null) {
            $qb->andWhere('l.status LIKE :kw OR l.error_message LIKE :kw OR c.name LIKE :kw OR c.feedSlug LIKE :kw')
               ->setParameter('kw', '%' . $this->keywords . '%');
        }

        $this->query = $qb;
    }

    public function createCountQuery()
    {
        $qb = $this->connection->createQueryBuilder();
        $qb->select('COUNT(*)')->from(self::T_LOG, 'l');

        if ($this->keywords !== null) {
            $qb->andWhere('l.status LIKE :kw OR l.error_message LIKE :kw OR c.name LIKE :kw OR c.feedSlug LIKE :kw')
               ->setParameter('kw', '%' . $this->keywords . '%');
        }
        return $qb;
    }

    public function filterByKeywords(string $keywords): void
    {
        $this->keywords = $keywords;
        if ($this->query) {
            $this->query->andWhere('l.status LIKE :kw OR l.error_message LIKE :kw OR c.name LIKE :kw OR c.feedSlug LIKE :kw')
                        ->setParameter('kw', '%' . $this->keywords . '%');
        }
    }

    public function getTotalResults()
    {
        return (int) $this->createCountQuery()->execute()->fetchOne();
    }
}

# 3) Keep the rest as-is
– `$pagination = $list->getPagination();`
– `$items = $pagination->getCurrentPageResults();`
– Render with `$pagination->renderDefaultView();`

This addresses the constructor type error and ensures pagination/count work with filters and default Concrete pagination.

User
use bootstrap 5 view like other concrete page s paginaiton
Assistant
You’ve got two clean options to render **Bootstrap 5** pagination that matches Concrete CMS v9+.

## Option A — Use Concrete’s built-in pagination view (recommended)
Concrete’s `Pagination` already outputs BS5-compatible markup.

In your single page view (e.g., `shopping_feed.php`):

<?php if (isset($pagination) && $pagination->haveToPaginate()) : ?>
    <nav aria-label="<?= t('Pagination') ?>">
        <?= $pagination->renderDefaultView(); ?>
        <?php // or: echo $pagination->renderView('dashboard'); ?>
    </nav>
<?php endif; ?>

> `renderDefaultView()` in v9+ uses Bootstrap 5 styles (same look as other dashboard lists).

## Option B — Force the Pagerfanta Bootstrap 5 view explicitly
If you want to be explicit (or tweak proximity), render via Pagerfanta’s BS5 view:

**Add import at the top of your view file:**

<?php
use PagerfantaViewTwitterBootstrap5View;
?>

**Then render:**

<?php if (isset($pagination) && $pagination->haveToPaginate()) : ?>
    <?php
    $view = new TwitterBootstrap5View();
    echo $view->render(
        $pagination->getPagerfanta(),      // Concrete wraps a Pagerfanta instance
        $pagination->getRouteGenerator(),  // Concrete’s route generator (preserves query params)
        ['proximity' => 2]                 // optional
    );
    ?>
<?php endif; ?>

Both approaches give you Bootstrap 5 pagination consistent with other Concrete pages. Option A is simplest and “most Concrete,” Option B gives you a bit more control (proximity, custom view rendering) while still using the same URLs and state.

Leave a Comment

Your email address will not be published. Required fields are marked *