Commit ea25df85 authored by catch's avatar catch

Issue #1499596 by plach, fago: Introduce a basic entity form controller.

parent 6e7a13a7
......@@ -159,6 +159,8 @@ function drupal_get_form($form_id) {
* - build_info: Internal. An associative array of information stored by Form
* API that is necessary to build and rebuild the form from cache when the
* original context may no longer be available:
* - callback: The actual callback to be used to retrieve the form array. If
* none is provided $form_id is used instead. Can be any callable type.
* - args: A list of arguments to pass to the form constructor.
* - files: An optional array defining include files that need to be loaded
* for building the form. Each array entry may be the path to a file or
......@@ -266,11 +268,11 @@ function drupal_get_form($form_id) {
* ones used by Form API internals) for this kind of storage. The
* recommended way to ensure that the chosen key doesn't conflict with ones
* used by the Form API or other modules is to use the module name as the
* key name or a prefix for the key name. For example, the Node module uses
* $form_state['node'] in node editing forms to store information about the
* node being edited, and this information stays available across successive
* clicks of the "Preview" button as well as when the "Save" button is
* finally clicked.
* key name or a prefix for the key name. For example, the entity form
* controller classes use $form_state['entity'] in entity forms to store
* information about the entity being edited, and this information stays
* available across successive clicks of the "Preview" button (if available)
* as well as when the "Save" button is finally clicked.
* - buttons: A list containing copies of all submit and button elements in
* the form.
* - complete_form: A reference to the $form variable containing the complete
......@@ -747,9 +749,13 @@ function drupal_retrieve_form($form_id, &$form_state) {
// the constructor function itself.
$args = $form_state['build_info']['args'];
// We first check to see if there's a function named after the $form_id.
// If an explicit form builder callback is defined we just use it, otherwise
// we look for a function named after the $form_id.
$callback = !empty($form_state['build_info']['callback']) ? $form_state['build_info']['callback'] : $form_id;
// We first check to see if there is a valid form builder callback defined.
// If there is, we simply pass the arguments on to it to get the form.
if (!function_exists($form_id)) {
if (!is_callable($callback)) {
// In cases where many form_ids need to share a central constructor function,
// such as the node editing form, modules can implement hook_forms(). It
// maps one or more form_ids to the correct constructor functions.
......@@ -808,7 +814,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
// If $callback was returned by a hook_forms() implementation, call it.
// Otherwise, call the function named after the form id.
$form = call_user_func_array(isset($callback) ? $callback : $form_id, $args);
$form = call_user_func_array($callback, $args);
$form['#form_id'] = $form_id;
return $form;
......@@ -1469,7 +1475,7 @@ function form_execute_handlers($type, &$form, &$form_state) {
$batch['has_form_submits'] = TRUE;
}
else {
$function($form, $form_state);
call_user_func_array($function, array(&$form, &$form_state));
}
$return = TRUE;
}
......@@ -1812,7 +1818,7 @@ function form_builder($form_id, &$element, &$form_state) {
// checkboxes and files.
if (isset($element['#process']) && !$element['#processed']) {
foreach ($element['#process'] as $process) {
$element = $process($element, $form_state, $form_state['complete_form']);
$element = call_user_func_array($process, array(&$element, &$form_state, &$form_state['complete_form']));
}
$element['#processed'] = TRUE;
}
......
......@@ -596,7 +596,7 @@ function block_custom_block_save($edit, $delta) {
* Implements hook_form_FORM_ID_alter() for user_profile_form().
*/
function block_form_user_profile_form_alter(&$form, &$form_state) {
$account = $form['#user'];
$account = $form_state['controller']->getEntity($form_state);
$rids = array_keys($account->roles);
$result = db_query("SELECT DISTINCT b.* FROM {block} b LEFT JOIN {block_role} r ON b.module = r.module AND b.delta = r.delta WHERE b.status = 1 AND b.custom <> 0 AND (r.rid IN (:rids) OR r.rid IS NULL) ORDER BY b.weight, b.module", array(':rids' => $rids));
......
......@@ -423,7 +423,7 @@ function book_get_books() {
* @see book_pick_book_nojs_submit()
*/
function book_form_node_form_alter(&$form, &$form_state, $form_id) {
$node = $form['#node'];
$node = $form_state['controller']->getEntity($form_state);
$access = user_access('administer book outlines');
if (!$access) {
if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) {
......@@ -462,7 +462,8 @@ function book_form_node_form_alter(&$form, &$form_state, $form_id) {
* @see book_form_node_form_alter()
*/
function book_pick_book_nojs_submit($form, &$form_state) {
$form_state['node']->book = $form_state['values']['book'];
$node = $form_state['controller']->getEntity($form_state);
$node->book = $form_state['values']['book'];
$form_state['rebuild'] = TRUE;
}
......
......@@ -278,7 +278,7 @@ function comment_confirm_delete_page($cid) {
* @see confirm_form()
*/
function comment_confirm_delete($form, &$form_state, Comment $comment) {
$form['#comment'] = $comment;
$form_state['comment'] = $comment;
// Always provide entity id in the same form key as in the entity edit form.
$form['cid'] = array('#type' => 'value', '#value' => $comment->cid);
return confirm_form(
......@@ -295,7 +295,7 @@ function comment_confirm_delete($form, &$form_state, Comment $comment) {
* Form submission handler for comment_confirm_delete().
*/
function comment_confirm_delete_submit($form, &$form_state) {
$comment = $form['#comment'];
$comment = $form_state['comment'];
// Delete the comment and its replies.
comment_delete($comment->cid);
drupal_set_message(t('The comment and all its replies have been deleted.'));
......
This diff is collapsed.
......@@ -43,8 +43,7 @@ function comment_reply(Node $node, $pid = NULL) {
// The user is previewing a comment prior to submitting it.
if ($op == t('Preview')) {
if (user_access('post comments')) {
$comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid));
$build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
$build['comment_form'] = comment_add($node, $pid);
}
else {
drupal_set_message(t('You are not authorized to post comments.'), 'error');
......@@ -92,8 +91,7 @@ function comment_reply(Node $node, $pid = NULL) {
drupal_goto("node/$node->nid");
}
elseif (user_access('post comments')) {
$comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid));
$build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
$build['comment_form'] = comment_add($node, $pid);
}
else {
drupal_set_message(t('You are not authorized to post comments.'), 'error');
......
......@@ -221,7 +221,7 @@ function contact_form_user_profile_form_alter(&$form, &$form_state) {
'#weight' => 5,
'#collapsible' => TRUE,
);
$account = $form['#user'];
$account = $form_state['controller']->getEntity($form_state);
$form['contact']['contact'] = array(
'#type' => 'checkbox',
'#title' => t('Personal contact form'),
......
......@@ -27,6 +27,13 @@
* The class has to implement the
* Drupal\entity\EntityStorageControllerInterface interface. Leave blank
* to use the Drupal\entity\DatabaseStorageController implementation.
* - form controller class: An associative array where the keys are the names
* of the different form operations (such as creation, editing or deletion)
* and the values are the names of the controller classes. To facilitate
* supporting the case where an entity form varies only slightly between
* different operations, the name of the operation is passed also to the
* constructor of the form controller class. This way, one class can be used
* for multiple entity forms.
* - base table: (used by Drupal\entity\DatabaseStorageController) The
* name of the entity type's base table.
* - static cache: (used by Drupal\entity\DatabaseStorageController)
......@@ -139,6 +146,9 @@ function hook_entity_info() {
'label' => t('Node'),
'entity class' => 'Drupal\node\Node',
'controller class' => 'Drupal\node\NodeStorageController',
'form controller class' => array(
'default' => 'Drupal\node\NodeFormController',
),
'base table' => 'node',
'revision table' => 'node_revision',
'uri callback' => 'node_uri',
......
......@@ -5,6 +5,8 @@
* Entity API for handling entities like nodes or users.
*/
use \InvalidArgumentException;
use Drupal\entity\EntityFieldQuery;
use Drupal\entity\EntityMalformedException;
use Drupal\entity\EntityStorageException;
......@@ -73,6 +75,9 @@ function entity_get_info($entity_type = NULL) {
'fieldable' => FALSE,
'entity class' => 'Drupal\entity\Entity',
'controller class' => 'Drupal\entity\DatabaseStorageController',
'form controller class' => array(
'default' => 'Drupal\entity\EntityFormController',
),
'static cache' => TRUE,
'field cache' => TRUE,
'bundles' => array(),
......@@ -418,16 +423,123 @@ function entity_page_label(EntityInterface $entity, $langcode = NULL) {
}
/**
* Attaches field API validation to entity forms.
* Returns an entity form controller for the given operation.
*
* Since there might be different scenarios in which an entity is edited,
* multiple form controllers suitable to the different operations may be defined.
* If no controller is found for the default operation, the base class will be
* used. If a non-existing non-default operation is specified an exception will
* be thrown.
*
* @see hook_entity_info()
*
* @param $entity_type
* The type of the entity.
* @param $operation
* (optional) The name of an operation, such as creation, editing or deletion,
* identifying the controlled form. Defaults to 'default' which is the usual
* create/edit form.
*
* @return Drupal\entity\EntityFormControllerInterface
* An entity form controller instance.
*/
function entity_form_controller($entity_type, $operation = 'default') {
$info = entity_get_info($entity_type);
// Check whether there is a form controller class for the specified operation.
if (!empty($info['form controller class'][$operation])) {
$class = $info['form controller class'][$operation];
}
// If no controller is specified default to the base implementation.
elseif (empty($info['form controller class']) && $operation == 'default') {
$class = 'Drupal\entity\EntityFormController';
}
// If a non-existing operation has been specified stop.
else {
throw new InvalidArgumentException("Missing form controller for '$entity_type', operation '$operation'");
}
return new $class($operation);
}
/**
* Returns the form id for the given entity and operation.
*
* @param EntityInterface $entity
* The entity to be created or edited.
* @param $operation
* (optional) The operation for the form to be processed.
*
* @return
* A string representing the entity form id.
*/
function entity_form_id(EntityInterface $entity, $operation = 'default') {
$entity_type = $entity->entityType();
$bundle = $entity->bundle();
$form_id = $entity_type;
if ($bundle != $entity_type) {
$form_id = $bundle . '_' . $form_id;
}
if ($operation != 'default') {
$form_id = $form_id . '_' . $operation;
}
return $form_id . '_form';
}
/**
* Returns the default form state for the given entity and operation.
*
* @param EntityInterface $entity
* The entity to be created or edited.
* @param $operation
* (optional) The operation identifying the form to be processed.
*
* @return
* A $form_state array already filled the entity form controller.
*/
function entity_form_state_defaults(EntityInterface $entity, $operation = 'default') {
$form_state = array();
$controller = entity_form_controller($entity->entityType(), $operation);
$form_state['build_info']['callback'] = array($controller, 'build');
$form_state['build_info']['base_form_id'] = $entity->entityType() . '_form';
$form_state['build_info']['args'] = array($entity);
return $form_state;
}
/**
* Retrieves, populates, and processes an entity form.
*
* @param EntityInterface $entity
* The entity to be created or edited.
* @param $operation
* (optional) The operation identifying the form to be submitted.
* @param $form_state
* (optional) A keyed array containing the current state of the form.
*
* @return
* A $form_state array already filled with the entity form controller.
*/
function entity_form_submit(EntityInterface $entity, $operation = 'default', &$form_state = array()) {
$form_state += entity_form_state_defaults($entity, $operation);
$form_id = entity_form_id($entity, $operation);
drupal_form_submit($form_id, $form_state);
}
/**
* Returns the built and processed entity form for the given entity.
*
* @param EntityInterface $entity
* The entity to be created or edited.
* @param $operation
* (optional) The operation identifying the form variation to be returned.
*
* @return
* The processed form for the given entity and operation.
*/
function entity_form_field_validate($entity_type, $form, &$form_state) {
// All field attach API functions act on an entity object, but during form
// validation, we don't have one. $form_state contains the entity as it was
// prior to processing the current form submission, and we must not update it
// until we have fully validated the submitted input. Therefore, for
// validation, act on a pseudo entity created out of the form values.
$pseudo_entity = entity_create($entity_type, $form_state['values']);
field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state);
function entity_get_form(EntityInterface $entity, $operation = 'default') {
$form_state = entity_form_state_defaults($entity, $operation);
$form_id = entity_form_id($entity, $operation);
return drupal_build_form($form_id, $form_state);
}
/**
......
......@@ -278,5 +278,4 @@ public function getRevisionId() {
public function isCurrentRevision() {
return $this->isCurrentRevision;
}
}
<?php
/**
* @file
* Definition of Drupal\entity\EntityFormController.
*/
namespace Drupal\entity;
/**
* Base class for entity form controllers.
*/
class EntityFormController implements EntityFormControllerInterface {
/**
* The name of the current operation.
*
* Subclasses may use this to implement different behaviors depending on its
* value.
*
* @var string
*/
protected $operation;
/**
* Implements Drupal\entity\EntityFormControllerInterface::__construct().
*/
public function __construct($operation) {
$this->operation = $operation;
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::build().
*/
public function build(array $form, array &$form_state, EntityInterface $entity) {
// During the initial form build, add the entity to the form state for use
// during form building and processing. During a rebuild, use what is in the
// form state.
if (!$this->getEntity($form_state)) {
$this->init($form_state, $entity);
}
// Retrieve the form array using the possibly updated entity in form state.
$entity = $this->getEntity($form_state);
$form = $this->form($form, $form_state, $entity);
// Retrieve and add the form actions array.
$actions = $this->actionsElement($form, $form_state);
if (!empty($actions)) {
$form['actions'] = $actions;
}
return $form;
}
/**
* Initialize the form state and the entity before the first form build.
*/
protected function init(array &$form_state, EntityInterface $entity) {
// Add the controller to the form state so it can be easily accessed by
// module-provided form handlers there.
$form_state['controller'] = $this;
$this->setEntity($entity, $form_state);
$this->prepareEntity($entity);
}
/**
* Returns the actual form array to be built.
*
* @see Drupal\entity\EntityFormController::build()
*/
public function form(array $form, array &$form_state, EntityInterface $entity) {
// @todo Exploit the Property API to generate the default widgets for the
// entity properties.
$info = $entity->entityInfo();
if (!empty($info['fieldable'])) {
field_attach_form($entity->entityType(), $entity, $form, $form_state, $this->getFormLangcode($form_state));
}
return $form;
}
/**
* Returns the action form element for the current entity form.
*/
protected function actionsElement(array $form, array &$form_state) {
$element = $this->actions($form, $form_state);
// We cannot delete an entity that has not been created yet.
if ($this->getEntity($form_state)->isNew()) {
unset($element['delete']);
}
elseif (isset($element['delete'])) {
// Move the delete action as last one, unless weights are explicitly
// provided.
$delete = $element['delete'];
unset($element['delete']);
$element['delete'] = $delete;
}
$count = 0;
foreach (element_children($element) as $action) {
$element[$action] += array(
'#type' => 'submit',
'#weight' => ++$count * 5,
);
}
if (!empty($element)) {
$element['#type'] = 'actions';
}
return $element;
}
/**
* Returns an array of supported actions for the current entity form.
*/
protected function actions(array $form, array &$form_state) {
return array(
// @todo Rename the action key from submit to save.
'submit' => array(
'#value' => t('Save'),
'#validate' => array(
array($this, 'validate'),
),
'#submit' => array(
array($this, 'submit'),
array($this, 'save'),
),
),
'delete' => array(
'#value' => t('Delete'),
// No need to validate the form when deleting the entity.
'#submit' => array(
array($this, 'delete'),
),
),
// @todo Consider introducing a 'preview' action here, since it is used by
// many entity types.
);
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::validate().
*/
public function validate(array $form, array &$form_state) {
// @todo Exploit the Property API to validate the values submitted for the
// entity properties.
$entity = $this->buildEntity($form, $form_state);
$info = $entity->entityInfo();
if (!empty($info['fieldable'])) {
field_attach_form_validate($entity->entityType(), $entity, $form, $form_state);
}
// @todo Remove this.
// Execute legacy global validation handlers.
unset($form_state['validate_handlers']);
form_execute_handlers('validate', $form, $form_state);
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::submit().
*
* This is the default entity object builder function. It is called before any
* other submit handler to build the new entity object to be passed to the
* following submit handlers. At this point of the form workflow the entity is
* validated and the form state can be updated, this way the subsequently
* invoked handlers can retrieve a regular entity object to act on.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
*/
public function submit(array $form, array &$form_state) {
$entity = $this->buildEntity($form, $form_state);
$this->setEntity($entity, $form_state);
return $entity;
}
/**
* Form submission handler for the 'save' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
*/
public function save(array $form, array &$form_state) {
// @todo Perform common save operations.
}
/**
* Form submission handler for the 'delete' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
*/
public function delete(array $form, array &$form_state) {
// @todo Perform common delete operations.
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::getFormLangcode().
*/
public function getFormLangcode($form_state) {
// @todo Introduce a new form language type (see hook_language_types_info())
// to be used as the default active form language, should it be missing, so
// that entity forms can be used to submit multilingual values.
$language = $this->getEntity($form_state)->language();
return !empty($language->langcode) ? $language->langcode : NULL;
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::buildEntity().
*/
public function buildEntity(array $form, array &$form_state) {
$entity = clone $this->getEntity($form_state);
// @todo Move entity_form_submit_build_entity() here.
// @todo Exploit the Property API to process the submitted entity property.
entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state);
return $entity;
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::getEntity().
*/
public function getEntity(array $form_state) {
return isset($form_state['entity']) ? $form_state['entity'] : NULL;
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::setEntity().
*/
public function setEntity(EntityInterface $entity, array &$form_state) {
$form_state['entity'] = $entity;
}
/**
* Prepares the entity object before the form is built first.
*/
protected function prepareEntity(EntityInterface $entity) {
// @todo Perform common prepare operations and add a hook.
}
/**
* Implements Drupal\entity\EntityFormControllerInterface::getOperation().
*/
public function getOperation() {
return $this->operation;
}
}
<?php
/**
* @file
* Definition of Drupal\entity\EntityFormControllerInterface.
*/
namespace Drupal\entity;
/**
* Defines a common interface for entity form controller classes.
*/
interface EntityFormControllerInterface {
/**
* Constructs the object.
*
* @param string $operation
* The name of the current operation.
*/
public function __construct($operation);
/**
* Builds an entity form.
*
* This is the entity form builder which is invoked via drupal_build_form()
* to retrieve the form.
*
* @param array $form
* A nested array form elements comprising the form.
* @param array $form_state
* An associative array containing the current state of the form.
* @param string $entity_type
* The type of the entity being edited.
* @param \Drupal\entity\EntityInterface $entity
* The entity being edited.
*
* @return array
* The array containing the complete form.
*/
public function build(array $form, array &$form_state, EntityInterface $entity);
/**
* Returns the code identifying the active form language.
*
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return string
* The form language code.
*/
public function getFormLangcode($form_state);
/**
* Returns the operation identifying the form controller.
*
* @return string
* The name of the operation.
*/
public function getOperation();
/**
* Returns the form entity.
*
* The form entity which has been used for populating form element defaults.
*
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return \Drupal\entity\EntityInterface
* The current form entity.
*/
public function getEntity(array $form_state);
/**
* Sets the form entity.
*
* Sets the form entity which will be used for populating form element
* defaults. Usually, the form entity gets updated by
* \Drupal\entity\EntityFormControllerInterface::submit(), however this may
* be used to completely exchange the form entity, e.g. when preparing the
* rebuild of a multi-step form.
*
* @param \Drupal\entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form_state
* An associative array containing the current state of the form.
*/
public function setEntity(EntityInterface $entity, array &$form_state);
/**
* Builds an updated entity object based upon the submitted form values.
*
* For building the updated entity object the form's entity is cloned and
* the submitted form values are copied to entity properties. The form's
* entity remains unchanged.
*
* @see \Drupal\entity\EntityFormControllerInterface::getEntity()
*
* @param array $form
* A nested array form elements comprising the form.
* @param array $form_state
* An associative array containing the current state of the form.
*
* @return \Drupal\entity\EntityInterface
* An updated copy of the form's entity object.
*/
public function buildEntity(array $form, array &$form_state);
/**
* Validates the submitted form values of the entity form.
*
* @param array $form
* A nested array form elements comprising the form.
* @param array $form_state
* An associative array containing the current state of the form.