The Crud plugin allow high reusability of the default Create, Retrieve, Update and Delete (CRUD) actions in your controllers
Usually the code for CRUD is very simple, and always look the same - this plugin will add the actions to your controller so you don't have to reimplement them over and over
It only works with CakePHP > 2.1 - as it utilizes the new event system
- CakePHP 2.1
- PHP 5.3
git clone git://github.com/nodesagency/Platform-Crud-Plugin.git app/Plugin/Crud
git submodule add git://github.com/nodesagency/Platform-Crud-Plugin.git app/Plugin/Crud
Add the following to your app/Config/bootstrap.php
<?php
CakePlugin::load('Crud', array('bootstrap' => true, 'routes' => true));
?>
In your (app) controller load the Crud component and add required method
<?php
/**
* Application wide controller
*
* @abstract
* @package App.Controller
*/
abstract class AppController extends Controller {
/**
* List of global controller components
*
* @cakephp
* @var array
*/
public $components = array(
// Enable CRUD actions
'Crud.Crud' => array(
'actions' => array('index', 'add', 'edit', 'view', 'delete')
)
);
/**
* Dispatches the controller action. Checks that the action exists and isn't private.
*
* If Cake raises MissingActionException we attempt to execute Crud
*
* @param CakeRequest $request
* @return mixed The resulting response.
* @throws PrivateActionException When actions are not public or prefixed by _
* @throws MissingActionException When actions are not defined and scaffolding and CRUD is not enabled.
*/
public function invokeAction(CakeRequest $request) {
try {
return parent::invokeAction($request);
} catch (MissingActionException $e) {
// Check for any dispatch components
if (!empty($this->dispatchComponents)) {
// Iterate dispatchComponents
foreach ($this->dispatchComponents as $component => $enabled) {
// Skip them if they aren't enabled
if (empty($enabled)) {
continue;
}
// Skip if isActionMapped isn't defined in the Component
if (!method_exists($this->{$component}, 'isActionMapped')) {
continue;
}
// Skip if the action isn't mapped
if (!$this->{$component}->isActionMapped($request->params['action'])) {
continue;
}
// Skip if executeAction isn't defined in the Component
if (!method_exists($this->{$component}, 'executeAction')) {
continue;
}
// Execute the callback, should return CakeResponse object
return $this->{$component}->executeAction();
}
}
// No additional callbacks, re-throw the normal Cake exception
throw $e;
}
}
}
?>
We only use routes without prefix in these examples, but the Crud component works with any prefixes you may have. It just requires some additional configuration.
In the code example above, we pass in an actions array with all the controller actions we wish the Crud component to handle - you can easily omit some of the actions
<?php
class AppController extends Controller {
public $components = array(
// Enable CRUD actions
'Crud.Crud' => array(
// All actions but delete() will be implemented
'actions' => array('index', 'add', 'edit', 'view')
)
);
}
?>
In the above example, if /delete is called on any controller, cake will raise it's normal missing action error as if nothing has happened
You can enable and disable Crud actions on the fly
<?php
/**
* Application wide controller
*
* @abstract
* @package App.Controller
*/
abstract class AppController extends Controller {
/**
* List of global controller components
*
* @cakephp
* @var array
*/
public $components = array(
// Enable CRUD actions
'Crud.Crud' => array(
'actions' => array('index', 'add', 'edit', 'view', 'delete')
)
);
public function beforeFilter() {
// Will ignore delete action
$this->Crud->disableAction('delete');
// Will process delete action again
$this->Crud->enableAction('delete');
parent::beforeFilter();
}
}
?>
You can also change the default view used for a Crud action
<?php
class DemoController extends AppController {
public function beforeFilter() {
// Change the view for add crud action to be "public_add.ctp"
$this->Crud->mapActionView('add', 'public_add');
// Change the view for edit crud action to be "public_edit.ctp"
$this->Crud->mapActionView('edit', 'public_edit');
// Convenient shortcut to change both at once
$this->Crud->mapActionView(array('add' => 'public_add', 'edit' => 'public_edit'));
parent::beforeFilter();
}
}
?>
The Crud component always operates on the $modelClass of your controller, that's the first model in your $uses array
By default CrudComponent assumes your add and edit views is identical, and will render them both with a "form.ctp" file.
There is no view for delete action, it will always redirect
The CRUD plugin uses the new event system introduced in Cake 2.1
The subject object can be accessed through $event->subject in all event callbacks
Name | Value Type | Description |
---|---|---|
self | CrudComponent | A reference to the CRUD component |
controller | AppController | A reference to the controller handling the current request |
model | AppModel | A reference to the model Crud is working on |
request | CakeRequest | A reference to the CakeRequest for the current request |
action | string | The current controller action being requested |
All Crud events always return void, any modifications should be done to the CrudEventSubject object ($event->subject)
All Crud events take exactly one parameter, CakeEvent $event
The CRUD component emits the following events
Event | Subject modifiers | Description |
---|---|---|
Crud.init | N/A | Initialize method |
Crud.beforePaginate | $subject->controller->paginate | Modify any pagination settings |
Crud.afterPaginate | $subject->items | You can modify the pagination result if needed, passed as $items |
Crud.beforeRender | N/A | Invoked right before the view will be rendered |
Event | Subject modifiers | Description |
---|---|---|
Crud.init | N/A | Initialize method |
Crud.beforeSave | N/A | Access and modify the data from the $request object |
Crud.afterSave | $subject->id | $id is only available if the save was successful |
Crud.beforeRender | N/A | Invoked right before the view will be rendered |
Event | Subject modifiers | Description |
---|---|---|
Crud.init | N/A | Initialize method |
Crud.beforeSave | N/A | Access and modify the data from the $request object |
Crud.afterSave | $subject->id | $id is only available if the save was successful |
Crud.beforeFind | $subject->query | Modify the $query array, same as $queryParams in behavior beforeFind() |
Crud.recordNotFound | N/A | If beforeFind could not find a record |
Crud.afterFind | N/A | Modify the record found by find() and return it |
Crud.beforeRender | N/A | Invoked right before the view will be rendered |
Event | Subject modifiers | Description |
---|---|---|
Crud.init | N/A | Initialize method |
Crud.beforeFind | $subject->query | Modify the $query array, same as $queryParams in behavior beforeFind() |
Crud.recordNotFound | N/A | If beforeFind could not find a record |
Crud.afterFind | N/A | Modify the record found by find() and return it |
Crud.beforeRender | N/A | Invoked right before the view will be rendered |
Event | Subject modifiers | Description |
---|---|---|
Crud.init | N/A | Initialize method |
Crud.beforeFind | $subject->query | Modify the $query array, same as $queryParams in behavior beforeFind() |
Crud.recordNotFound | N/A | If beforeFind could not find a record |
Crud.beforeDelete | N/A | Stop the delete by redirecting away from the action |
Crud.afterDelete | N/A | Modify the record found by find() and return it |
I would recommend using the Event class if you need to subscribe to more than one event
Crud events must be inside app/Controller/Event ( app/Plugin/$plugin/Controller/Event for plugins)
Your Event class should look like this:
<?php
App::uses('CrudBaseEvent', 'Crud.Controller/Event');
class DemoEvent extends CrudBaseEvent {
public function beforeRender(CakeEvent $event) {
// Check about this is admin, and about this function should be process for this action
if ($event->subject->shouldProcess('only', array('admin_add'))) {
// We only wanna do something, if this is admin request, and only for "admin_add"
}
}
public function afterSave(CakeEvent $event) {
// In this test, we want afterSave to do one thing, for admin_add and another for admin_edit
// If admin_add redirect to index
if ($event->subject->shouldProcess('only', array('admin_add'))) {
if ($event->subject->success) {
$event->subject->controller->redirect(array('action' => 'index'));
}
}
// If admin_edit redirect to self
elseif ($event->subject->shouldProcess('only', array('admin_edit'))) {
if ($event->subject->success) {
$event->subject->controller->redirect(array('action' => 'edit', $id));
}
}
}
}
?>
and the controller
<?php
App::uses('DemoEvent', 'Controller/Event');
class DemoController extends AppController {
public function beforeFilter() {
parent::beforeFilter();
$this->getEventManager()->attach(new DemoEvent());
}
}
?>
<?php
class DemoController extends AppController {
public function beforeFilter() {
parent::beforeFilter();
$this->Crud->on('Crud.beforePaginate', function(CakeEvent $event) { debug($event->subject->query); });
}
}
?>
<?php
class DemoController extends AppController {
public function beforeFilter() {
parent::beforeFilter();
$this->Crud->on('Crud.beforePaginate', array($this, 'demoEvent'));
}
public function demoEvent(CakeEvent $event) {
$event->subject->query['conditions']['is_active'] = true;
}
}
?>
If you are used to bake or CakePHP scaffolding you might want to have some control over the data it is sent to the view for filling select boxes for associated models. Crud component can be configured to return the list of record for all related models or just those you want to in a per-action basis
By default all related model lists for main Crud component model instance will be fetched, but only for add
, edit
and corresponding
admin actions. For instance if your Post
model in associated to Tag
and Author
, then for the aforementioned actions you will have
in your view the authors
and tags
variable containing the result of calling find('list') on each model.
Should you need more fine grain control over the lists fetched, you can configure statically or use dynamic methods:
<?php
class DemoController extends AppController {
/**
* List of global controller components
*
* @cakephp
* @var array
*/
public $components = array(
// Enable CRUD actions
'Crud.Crud' => array(
'actions' => array('index', 'add', 'edit', 'view', 'delete'),
'relatedList' => array(
'add' => array('Author'), //Only set $authors variable in the view for action add and admin_add
'edit' => array('Tag', 'Cms.Page'), //Set $tags and $pages variable. Page model from plugin Cms will be used
// As admin_edit is not listed here it will use defaults from edit action
)
)
);
}
?>
You can also configure default to not repeat yourself too much:
<?php
class DemoController extends AppController {
/**
* List of global controller components
*
* @cakephp
* @var array
*/
public $components = array(
// Enable CRUD actions
'Crud.Crud' => array(
'actions' => array('index', 'add', 'edit', 'view', 'delete'),
'relatedList' => array(
'default' => array('Author'),
'add' => true, // add action is enabled and will fetch Author by default
'admin_change' => true, // admin_change action is enabled and will fetch Author by default
'edit' => array('Tag'), //edit action is enabled and will only fetch Tags
'admin_edit' => false // admin_edit action is disabled, no related models will be fetched
)
)
);
}
?>
If configuring statically is not your thing, or you want to dynamically fetch related models based on some conditions, then you can
call mapRelatedList
and enableRelatedList
function in CrudComponent:
<?php
class DemoController extends AppController {
public function beforeFilter() {
parent::beforeFilter();
$this->Crud->enableRelatedList(array('index', 'delete'));
$this->mapRelatedList(array('Author', 'Cms.Page'), 'default'); // By default all enabled actions should fetch Author and Page
}
public function delete() {
$this->mapRelatedList(array('Author'), 'default'); // Only fetch authors list
$this->Crud->executeAction('delete');
}
}
?>
If for any reason you need to alter the query or final results generated by fetching related models lists, you can use Crud.beforeListRelated
and
Crud.afterListRelated
events to inject your own logic.
Crud.beforeListRelated
wil receive the following parameters in the event subject, which can be altered on the fly before any result is fetched
* query: An array with options for find('list')
* model: Model instance, the model to be used for fiding the list or records
Crud.afterListRelated
wil receive the following parameters in the event subject, which can be altered on the fly after results were fetched
* items: result from calling find('list')
* viewVar: Variable name to be set on the view with items as value
* model: Model instance, the model to be used for fiding the list or records
<?php
class DemoController extends AppController {
//...
public function beforeFilter() {
parent::beforeFilter();
//Authors list should only have the 3 most recen items
$this->Crud->on('beforeListRelated', function($event) {
if ($event->subject->model instanceof Author) {
$event->subject->query['limit'] = 3;
$event->subject->query['order'] = array('Author.created' => 'DESC');
}
});
$this->Crud->on('afterListRelated', function($event) {
if ($event->subject->model instanceof Tag) {
$event->subject->items += array(0 => 'N/A');
$event->subject->viewVar = 'labels';
}
});
}
}
?>
- Crud used to have a Config/bootstrap.php file, its no longer have, please make sure to remove the bootstrap => true from CakePlugin::load('Crud')
- All event classes used to be in Lib/Crud/Event - they are now located in Controller/Event
- The files used to have a namespace \Crud\Event - thats no longer the case
- The classes used to extend from "Base" - they should now extend from CrudBaseEvent
You must now load the classes on your own.
- In all your Event class files that extends "CrudBaseEvent" must have "App::uses('CrudBaseEvent', 'Crud.Controller/Event');" before the class declaration
- In all controllers where you attach the Crud Event to the event manager, you must load the Event class with "App::uses('DemoEvent', 'Controller/Event');" or "App::uses('DemoEvent', 'Plugin.Controller/Event');"
- Make sure that app/Config/bootstrap.php that loads Crud plugin doesn't load the bootstrap file
- Move all Event classes from Lib/Crud/Event to Controller/Event (both for App and Plugin folders)
- Remove all "namespace Crud\Event" from the classes
- Load CrudBaseEvent in each Event class ( App::uses('CrudBaseEvent', 'Crud.Controller/Event'); )
- Make sure all Event classes no longer extends from Base but from CrudBaseEvent
- Find all places where you attach Crud Events to the your EventManger ($this->getEventManager()->attach(..))
- Make sure you load your Event class before your Controller Class declaration ( App::uses('DemoEvent', 'Plugin.Controller/Event'); )
- Make sure you don't use "new \Crud\Event$className" but the normal Event class name now (new DemoEvent();)
<?php
// app/Plugin/Demo/Lib/Crud/Event/Demo.php
namespace Crud\Event;
class Demo extends Base {
}
// app/Plugin/Demo/Controller/DemosController.php
class DemosController extends DemoAppController {
public function beforeFilter() {
parent::beforeFilter();
$this->getEventManager()->attach(new Crud\Event\Demo());
}
}
?>
<?php
// app/Plugin/Demo/Controller/Event/DemoEvent.php
App::uses('CrudBaseEvent', 'Crud.Controller/Event');
class DemoEvent extends CrudBaseEvent {
}
// app/Plugin/Demo/Controller/DemoAppController.php
App::uses('DemoEvent', 'Demo.Controller/Event');
class DemosController extends DemoAppController {
public function beforeFilter() {
parent::beforeFilter();
$this->getEventManager()->attach(new DemoEvent());
}
}
?>