Skip to content

Automated CRUD / Scaffolding for CakePHP 2.1+ - keep your controllers real DRY !

Notifications You must be signed in to change notification settings

thenyel/Platform-Crud-Plugin

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status

Introduction

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

Installation

Requirements

  • CakePHP 2.1
  • PHP 5.3

Cloning and loading

With a simple git clone

git clone git://github.com/nodesagency/Platform-Crud-Plugin.git app/Plugin/Crud

As a git submodule

git submodule add git://github.com/nodesagency/Platform-Crud-Plugin.git app/Plugin/Crud

Loading and installation

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;
		}
	}
}
?>

Configuration

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();
	}
}
?>

Convention

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

Event system

The CRUD plugin uses the new event system introduced in Cake 2.1

Global accessible subject properties

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

Crud actions and their events

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

index()

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

add()

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

edit()

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

view()

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

delete()

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

Subscribing to an event

I would recommend using the Event class if you need to subscribe to more than one event

Full event class

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());
	}
}
?>

A lamba / Closure

<?php
class DemoController extends AppController {
	public function beforeFilter() {
		parent::beforeFilter();
		$this->Crud->on('Crud.beforePaginate', function(CakeEvent $event) { debug($event->subject->query); });
	}
}
?>

A method in your controller

<?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;
	}
}
?>

Filling Related Models select boxes

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');
	}

}
?>

Related models' list events

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

Example

<?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';
			}
		});
	}

}
?>

Migration from legacy PSR-0 autoloader to more Cake feel

Changes

  • 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

New

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');"

Step by step

  • 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();)

Examples

Before

<?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());
	}
}
?>

After

<?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());
	}
}
?>

About

Automated CRUD / Scaffolding for CakePHP 2.1+ - keep your controllers real DRY !

Resources

Stars

Watchers

Forks

Packages

No packages published