concrete-dashboard-crud · git:20260913.db96265 · 2026-09-13 · sha256 8ea13881a2ef1bf4
concrete-dashboard-crud git:20260913.db96265B
Immutable. This exact content is served forever at /api/v1/blob/8ea13881a2ef1bf4.
---
name: concrete-dashboard-crud
description: This skill provides instructions and best practices for implementing a CRUD (Create, Read, Update, Delete) interface for Doctrine entities within the Concrete CMS Dashboard, following standard core patterns.
---
# Concrete CMS Dashboard CRUD Implementation
This skill provides instructions and best practices for implementing a CRUD (Create, Read, Update, Delete) interface for Doctrine entities within the Concrete CMS Dashboard, following the standard core patterns.
## Architecture Overview
A standard CRUD interface in Concrete CMS Dashboard consists of several layers:
1. **Doctrine Entity**: The data model.
2. **Search Infrastructure**:
* **ItemList**: Extends `\Concrete\Core\Search\ItemList\Database\ItemList`. Handles querying the database.
* **SearchProvider**: Extends `\Concrete\Core\Search\Provider\AbstractSearchProvider`. Provides the search configuration.
* **ColumnSet**: Extends `\Concrete\Core\Search\ColumnSet\ColumnSet`. Defines table columns.
* **Result**: Extends `\Concrete\Core\Search\Result\Result`. Handles result rendering.
3. **Dashboard Page Controller**: Extends `DashboardPageController`. Manages actions (view, add, edit, submit, delete).
4. **UI Elements**:
* **Single Page View**: The main template.
* **Search Header**: An element for the search bar.
* **Header Menu**: An element for the "Add" button and pagination settings.
## 1. Search Infrastructure
### ItemList
Always extend `\Concrete\Core\Search\ItemList\Database\ItemList`. Do not use `EntityItemList` as it is rarely used in core and less flexible.
```php
namespace YourNamespace\Search\ItemList;
use Concrete\Core\Search\ItemList\Database\ItemList;
use Concrete\Core\Search\Pagination\PaginationProviderInterface;
use Pagerfanta\Doctrine\DBAL\QueryAdapter;
class YourEntityList extends ItemList implements PaginationProviderInterface
{
protected function createQuery()
{
$this->query->select('e.id')->from('YourEntityTable', 'e');
return $this->query;
}
public function getResult($queryRow)
{
$em = \ORM::entityManager();
return $em->find('YourNamespace\Entity\YourEntity', $queryRow['id']);
}
public function getPaginationAdapter()
{
return new QueryAdapter($this->deliverQueryObject());
}
public function filterByKeywords($keywords)
{
$this->query->andWhere($this->query->expr()->like('e.name', $this->query->createNamedParameter('%' . $keywords . '%')));
}
}
```
### SearchProvider
```php
namespace YourNamespace\Search\SearchProvider;
use Concrete\Core\Search\Provider\AbstractSearchProvider;
use YourNamespace\Search\ItemList\YourEntityList;
use YourNamespace\Search\ColumnSet\YourEntityColumnSet;
class YourEntitySearchProvider extends AbstractSearchProvider
{
public function getCustomAttributeKeys() { return []; }
public function getBaseColumnSet() { return new YourEntityColumnSet(); }
public function getCurrentColumnSet() { return new YourEntityColumnSet(); }
public function getDefaultColumnSet() { return new YourEntityColumnSet(); }
public function getItemList()
{
return new YourEntityList();
}
}
```
## 2. Dashboard Controller
The controller should manage the search state and pass necessary variables to elements.
```php
public function view()
{
$provider = $this->app->make(YourEntitySearchProvider::class);
$list = $provider->getItemList();
if ($this->request->query->has('keywords')) {
$list->filterByKeywords($this->request->query->get('keywords'));
}
$itemsPerPage = $this->request->query->get('itemsPerPage', 10);
$list->setItemsPerPage($itemsPerPage);
$result = $provider->createSearchResultObject($provider->getDefaultColumnSet(), $list);
$headerMenu = $this->app->make(ElementManager::class)->get('your_package/search/menu', 'your_package_handle');
$headerMenu->set('result', $result);
$headerMenu->set('itemsPerPage', $itemsPerPage);
$headerMenu->set('itemsPerPageOptions', $provider->getItemsPerPageOptions());
$headerMenu->set('urlHelper', $this->app->make('helper/url'));
$headerSearch = $this->app->make(ElementManager::class)->get('your_package/search/search', 'your_package_handle');
$headerSearch->set('headerSearchAction', $this->action('view'));
$this->set('result', $result);
$this->set('headerMenu', $headerMenu);
$this->set('headerSearch', $headerSearch);
}
```
## 3. UI Implementation
### Delete Confirmation
To implement a standard Concrete CMS delete confirmation dialog, use `ConcreteAlert.confirm()` in your search results table.
#### Controller
In your `view()` method, ensure the deletion action URL is available.
```php
$this->set('deleteAction', $this->action('delete'));
```
#### View Template
Add a hidden form or a `data-modal` attribute containing the confirmation form for each row.
```php
<tr data-details-url="<?= $view->action('edit', $item->getItem()->getId()) ?>">
<?php foreach ($item->getColumns() as $column) { ?>
<td><?= $column->getColumnValue() ?></td>
<?php } ?>
<td class="text-right">
<?php
$deleteForm = '<form method="post" action="' . $view->action('delete', $item->getItem()->getId()) . '">'
. app('helper/validation/token')->output('delete', true)
. t('Are you sure you want to delete this item?')
. '</form>';
?>
<button type="button" class="btn btn-danger btn-xs"
data-modal="<?= h($deleteForm) ?>"
onclick="ccm_deleteItem(this)">
<?= t('Delete') ?>
</button>
</td>
</tr>
<script>
var ccm_deleteItem = function(elem) {
var modal = elem.getAttribute('data-modal');
ConcreteAlert.confirm(modal, function() {
var submitButton = document.querySelector('.ui-dialog button[data-dialog-action]');
submitButton.disabled = true;
var modalForm = document.querySelector('#ccm-popup-confirmation form');
modalForm.submit();
}, 'btn-danger', '<?= t('Delete') ?>');
};
</script>
```
### Search Element (`search.php`)
Use `app('helper/form')` to get the form helper.
```php
<?php $form = app('helper/form'); ?>
<div class="ccm-header-search-form">
<form method="get" action="<?= $headerSearchAction ?>">
<div class="input-group">
<?= $form->text('keywords', ['placeholder' => t('Search')]) ?>
<button class="btn btn-outline-secondary" type="submit"><i class="fas fa-search"></i></button>
</div>
</form>
</div>
```
### View Template
Use full-width content and standard table classes.
```php
<div class="ccm-dashboard-content-full">
<table class="ccm-search-results-table">
<thead>
<tr>
<?php foreach ($result->getColumns() as $column) { ?>
<th class="<?= $column->getColumnStyleClass() ?>">
<a href="<?= h($column->getColumnSortURL()) ?>"><?= $column->getColumnTitle() ?></a>
</th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($result->getItems() as $item) { ?>
<tr data-details-url="<?= $view->action('edit', $item->getItem()->getId()) ?>">
<?php foreach ($item->getColumns() as $column) { ?>
<td><?= $column->getColumnValue() ?></td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<div class="ccm-search-results-pagination">
<?= $result->getPagination()->renderView('dashboard') ?>
</div>
</div>
```
## Best Practices
* **CSRF Protection**: Always use `$this->token->validate('action_name')` in `submit` and `delete` methods.
* **Deletion Safety**: Before deleting an entity, check for references in other tables and add a descriptive error if deletion is blocked.
* **Redirect after Post**: Always return a redirect after successful `submit` or `delete`.
* **Coding Standards**: Run `concrete/bin/concrete c5:phpcs fix {path}` on your PHP classes.
* **Element Manager**: Use `ElementManager` to load header search and menu elements instead of creating new `Element` objects manually.