Custom concrete5 User Reports

concrete5's built-in user reporting functionality (found in the Dashboard -> Users and Groups) is pretty powerful. You can search by email address, group, user name, date created and any custom user attribute you've setup for inclusion in advanced search. You can sort by these custom attributes as well. Eventually, the users section of the dashboard will also get the saved search and column reordering functionality found in the file manager.

concrete5 user search

This works great for many administrators, but sometimes you need to be able to output these reports programmatically, in order to include them on the front-end of a website, within a custom administrative area, or just group information together in different ways. When that's the case, you need to get a little deeper and actually start writing some concrete5 code. Fortunately, we try and provide a nice API for doing just that.


Let's say we wanted a custom report within a user's website for track visitors. This would be a custom single page that we'd lock down with permissions, so only administrators could get to it. You'd create a "custom_user_report.php" single page, so it could be found at (Head over here if you need more information about creating and working with single pages.)

Our user report would need to list users who visited a particular page, group them by the most visits, and sort them by that visit amount in descending order. The search form for this could look like:

Search Code

After choosing a particular page, and the start/end dates for our report, we'd submit the form, and get a list of users to that page over that period of time, along with the total number of visits.

Search Results

The UserList Object

concrete5 provides a UserList class for filtering and sorting a result set of users. It's easy to get an array of UserInfo objects back from this class. In this example, we actually need more than just a class that filters and displays users. We need a class that can group and sort users based on how many times they've visited a particular page. That's why we define a custom UserVisitedPageList class that extends our UserList class, and put it in the models/ directory in the root of the website. It can then be loaded with


Here are the contents of the class:

page = $c; $this->autoSortColumns[] = 'totalVisits'; } public function setBaseQuery() { $this->setQuery('select u.uID, count(ps.pstID) as totalVisits From Users u left join PageStatistics ps on ps.uID = u.uID'); $this->filter('cID', $this->page->getCollectionID()); $this->filter('ps.uID', 0, '>'); $this->groupBy('uID'); $this->sortBy('totalVisits', 'desc'); } public function get($itemsToGet = 100, $offset = 0) { $userInfos = array(); $this->createQuery(); $r = DatabaseItemList::get( $itemsToGet, intval($offset)); foreach($r as $row) { $ui = UserInfo::getByID($row['uID']); $ui->totalVisits = $row['totalVisits']; $ui->timestamp = $row['timestamp']; $ui->pageID = $row['cID']; $userInfos[] = $ui; } return $userInfos; } } Let's break this class down, component by component: 1. The class extends UserList, which in turn extends DatabaseItemList. Understanding the [DatabaseItemList][7] class will help this make quite a bit more sense. 2. The __construct() method, automatically run when the class is instantiated, sets the page that we're limiting results to as the protected $page variable, and adds to the $autoSortColumns array. This array is defined in the DatabaseItemList class, and determines which columns in the classes query can be sorted through a $_GET request. 3. The setBaseQuery command completely overrides the regular UserList setBaseQuery command. It groups and retrieves user visit on a page by page basis, filtering by logged in users, and logically grouping and sorting. 4. Finally, the get() function is overridden. This is a function that is automatically run by DatabaseItemList::getPage(), and run any time you want to run the query and get the results. Here, we rewrite the regular UserList class so that it still returns an array of UserInfo objects, but we set some additional object properties on these objects. This is key: these properties are not found in the regular UserInfo object. Instead, we set them for this class, and when we pass this array of result objects into the page, they'll be available and we can show them in our data grid. ## The Search Form In the screenshot above, you can see that we don't run the search function when the page is first loaded. Instead, we force the user to submit the form. That form submits to the "search_users" action. Here is the search form in our custom single page:

        <td class="field">
            <?=$form->submit('search', t('Search'))?>


This is a pretty basic form. We use the concrete5 DateTime, Page Selector and Form helpers to make it easier to output HTML and JavaScript.

The form submits to the "search_users" action, which is the function we define in our page's controller.

The Page Controller

Here is the search_users object in our controller, which references the custom UserVisitedPageList class:

public function search_users() {
    $page = Page::getByID($_REQUEST['pageID']);
    $ul = new UserVisitedPageList($page);
    if ($_GET['dt_from']) {
        $dateStart = date('Y-m-d', strtotime($_GET['dt_from']));
        if ($type == 'tests') {
            $dateStart = strtotime($dateStart);
        $ul->filter('timestamp', $dateStart, '>=');
    if ($_GET['dt_to']) {
        $dateEnd = strtotime($_GET['dt_to']);
        $dateEnd = date('Y-m-d 23:59:59', $dateEnd);
        if ($type == 'tests') {
            $dateEnd = strtotime($dateEnd);
        $ul->filter('timestamp', $dateEnd, '<=');

    if ($_REQUEST['num_results']) {
    $this->set('userList', $ul);
    $this->set('results', $ul->getPage());

When the page is submitted, our custom UserList class, "UserVisitedPageList," is loaded from the models directory. Then, the page selector form helper parses the ID of the submitted page, and sends it into a page object. The PageStatistics database table is filtered by timestamp for any "from" or "to" dates that we pass into the request. Finally, the number of visits shown on a page is filtered based on the "num_results" GET variable.

The Results (Back to the View)

Here is how we display the results. Take note of the $userList and $results variables: they are set by the controller at the end of the search_results function:

<? if (count($results) > 0) { ?>

    <table id="search-results" border="0" cellpadding="0" cellspacing="0">
        <th><input id="cb-all" type="checkbox" /></th>
        <th class="<?=$userList->getSearchResultsClass('uEmail')?>"><a href="<?=$userList->getSortByURL('uEmail', 'asc')?>"><?=t('Email')?></a></th>
        <th class="<?=$userList->getSearchResultsClass('ak_first_name')?>"><a href="<?=$userList->getSortByURL('ak_first_name', 'asc')?>"><?=t('First Name')?></a></th>
        <th class="<?=$userList->getSearchResultsClass('ak_last_name')?>"><a href="<?=$userList->getSortByURL('ak_last_name', 'asc')?>"><?=t('Last Name')?></a></th>
        <th class="<?=$userList->getSearchResultsClass('uLastOnline')?>"><a href="<?=$userList->getSortByURL('uLastOnline', 'desc')?>"><?=t('Last Visit')?></a></th>
        <th class="<?=$userList->getSearchResultsClass('totalVisits')?>"><a href="<?=$userList->getSortByURL('totalVisits', 'desc')?>"><?=t('Visits')?></a></th>
    $nav = Loader::helper('navigation');
    foreach($results as $ui) { 
            <td><?=$form->checkbox('uID[]', $ui->getUserID())?></td>
            <td><a href="<?=$this->action('view_user', $ui->getUserID())?>"><?=$ui->getUserEmail()?></a></td>
            <td><?=($ui->getLastOnline() >0 ?date('m/d/Y g:i a',$ui->getLastOnline()):'Never')?></td>

    <? $userList->displayPaging(); ?>

<? } else {?>
    No results found.
<? } ?>

This is the standard way to create a data grid in concrete5:

  1. Create table headers that reference ItemList::getSearchResultsClass so that you can easily tell when table columns are being sorted.
  2. Pair this with ItemList::getSortByURL(), which makes it so that you can easily sort a result set in a particular order, by a particular column, without having to gather up all request variables and write all the sorting code yourself.
  3. ItemList::displayPaging() takes care of the paging, based on what page of results you're currently on.
  4. Finally, take note of "totalVisits." Again, this isn't a property that's usually available to the UserInfo object. Instead, we have set it in our custom UserList class.

That's it!

Hopefully this example has shown that it's not that hard to extend the UserList class to write a custom report in concrete5. Pair this with some controllers and single pages, and you can make full-fledged user management applications easily, with components you're already used to using.

Loading Conversation
label('pageID', 'Filter by Page')?> selectPage('pageID', '')?> label('dt_from', 'Between')?> date('dt_from', '')?> label('dt_to', 'And')?> date('dt_to', '')?> label('num_results', '# Results')?> select('num_results', $numResults)?>