Getting Started
cakephp-bouncer adds an approval workflow to any CakePHP Table: instead of saving directly, user proposals are stored as drafts in a bouncer_records table; an admin or moderator reviews them with diff output and approves, rejects, or supersedes.
The original record stays untouched until an admin approves the proposal.
Requirements
- PHP 8.2+
- CakePHP 5.1+
See the version map for older Cake/PHP combinations.
Installation
composer require dereuromark/cakephp-bouncer
bin/cake plugin load Bouncer
bin/cake migrations migrate -p BouncerThe migration creates the bouncer_records table.
Enable on a Table
Add the behavior to any table whose changes should require approval:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
parent::initialize($config);
$this->addBehavior('Bouncer.Bouncer', [
'userField' => 'user_id', // identifies the proposer
]);
}
}That alone activates approval for add and edit. The plugin intercepts beforeSave / beforeDelete, serializes the proposed entity into a bouncer_record, and short-circuits the actual write.
Wire up the controller
The controller needs to do two extra things vs. a plain CRUD controller:
- Pass the proposing user via the
bouncerUserIdsave option (so the draft remembers who proposed it). - On edit, load any existing pending draft for that user/record, so the user keeps editing their own proposal instead of stacking duplicates.
public function edit($id = null)
{
$article = $this->Articles->get($id);
$userId = $this->Authentication->getIdentity()->getIdentifier();
$draft = $this->Articles->getBehavior('Bouncer')->loadDraft($id, $userId);
if ($draft) {
$article = $this->Articles->patchEntity($article, $draft->getData());
$this->Flash->info('You are editing your pending draft');
}
if ($this->request->is(['patch', 'post', 'put'])) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
$this->Articles->save($article, ['bouncerUserId' => $userId]);
if ($this->Articles->getBehavior('Bouncer')->wasBounced()) {
$this->Flash->success('Your changes are pending approval');
return $this->redirect(['action' => 'index']);
}
}
$this->set(compact('article'));
}wasBounced() returns true when the save was diverted into a draft — use it to flash a different message and skip the "saved successfully" redirect.
Connect the admin routes
In config/routes.php:
$routes->prefix('Admin', function (RouteBuilder $routes) {
$routes->plugin('Bouncer', function (RouteBuilder $routes) {
$routes->connect('/pending', ['controller' => 'Bouncer', 'action' => 'index']);
$routes->fallbacks();
});
});Navigate to /admin/bouncer/bouncer to see the queue, side-by-side diffs, and approve / reject buttons.
Where next
- Configuration — behavior options and the
Bouncer.*app config keys - Usage — controller patterns including draft re-edit, bypass, and per-save options
- View Helper — render proposals in your own templates
- Features — admin UI, approval workflow internals, 3-way merge, advanced patterns, AuditStash integration