Skip to content
Browse files

Redesigned Operation API.

  • Loading branch information...
1 parent d4069e7 commit 98b6964ac8b9be0152653ac9f49ca5903be59ee2 @olvlvl committed
Showing with 1,039 additions and 957 deletions.
  1. +8 −1 CHANGELOG
  2. +8 −1 config/core.php
  3. +2 −2 config/routes.php
  4. +54 −0 operations/core__aloha.php
  5. +36 −0 operations/core__ping.php
  6. +64 −0 operations/delete.php
  7. +156 −0 operations/save.php
  8. +10 −76 wdcore.php
  9. +30 −712 wdmodule.php
  10. +671 −165 wdoperation.php
View
9 CHANGELOG
@@ -1,4 +1,11 @@
-2011-03-12 # 0.9.0-dev
+2011-03-14 # 0.10.0-dev
+
+[NEW] Redesigned Operation API.
+
+
+
+
+2011-03-10 # 0.9.0
* Modules accessor:
View
9 config/core.php
@@ -1,5 +1,7 @@
<?php
+$operations_path = $path . 'operations' . DIRECTORY_SEPARATOR;
+
return array
(
'autoload' => array
@@ -25,7 +27,12 @@
'WdRoute' => $path . 'wdroute.php',
'WdSession' => $path . 'wdsession.php',
'WdTranslator' => $path . 'wdi18n.php',
- 'WdUploaded' => $path . 'wduploaded.php'
+ 'WdUploaded' => $path . 'wduploaded.php',
+
+ 'delete_WdOperation' => $operations_path . 'delete.php',
+ 'save_WdOperation' => $operations_path . 'save.php',
+ 'core__aloha_WdOperation' => $operations_path . 'core__aloha.php',
+ 'core__ping_WdOperation' => $operations_path . 'core__ping.php'
),
'cache configs' => false,
View
4 config/routes.php
@@ -4,11 +4,11 @@
(
'/api/core/aloha' => array
(
- 'callback' => array('WdCore', 'operation_aloha')
+ 'class' => 'core__aloha_WdOperation'
),
'/api/core/ping' => array
(
- 'callback' => array('WdCore', 'operation_ping')
+ 'class' => 'core__ping_WdOperation'
)
);
View
54 operations/core__aloha.php
@@ -0,0 +1,54 @@
+<?php
+
+/*
+ * This file is part of the WdCore package.
+ *
+ * (c) Olivier Laviale <olivier.laviale@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Displays information about the core and its modules.
+ */
+class core__aloha_WdOperation extends WdOperation
+{
+ protected function validate()
+ {
+ return true;
+ }
+
+ protected function process()
+ {
+ global $core;
+
+ $enabled = array();
+ $disabled = array();
+
+ foreach ($core->modules->descriptors as $module_id => $descriptor)
+ {
+ if (!empty($descriptor[WdModule::T_DISABLED]))
+ {
+ $disabled[] = $module_id;
+
+ continue;
+ }
+
+ $enabled[] = $module_id;
+ }
+
+ sort($enabled);
+ sort($disabled);
+
+ header('Content-Type: text/plain; charset=utf-8');
+
+ $rc = 'WdCore version ' . WdCore::VERSION . ' is running here with:';
+ $rc .= PHP_EOL . PHP_EOL . implode(PHP_EOL, $enabled);
+ $rc .= PHP_EOL . PHP_EOL . 'Disabled modules:';
+ $rc .= PHP_EOL . PHP_EOL . implode(PHP_EOL, $disabled);
+ $rc .= PHP_EOL . PHP_EOL . strip_tags(implode(PHP_EOL, WdDebug::fetchMessages('debug')));
+
+ return $rc;
+ }
+}
View
36 operations/core__ping.php
@@ -0,0 +1,36 @@
+<?php
+
+/*
+ * This file is part of the WdCore package.
+ *
+ * (c) Olivier Laviale <olivier.laviale@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Keeps the user's session alive. Only already created sessions are kept alive, new sessions
+ * are *not* created.
+ */
+class core__ping_WdOperation extends WdOperation
+{
+ protected function validate()
+ {
+ return true;
+ }
+
+ protected function process()
+ {
+ global $core;
+
+ header('Content-Type: text/plain; charset=utf-8');
+
+ if (WdSession::exists())
+ {
+ $core->session;
+ }
+
+ return 'pong';
+ }
+}
View
64 operations/delete.php
@@ -0,0 +1,64 @@
+<?php
+
+/*
+ * This file is part of the WdCore package.
+ *
+ * (c) Olivier Laviale <olivier.laviale@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Deletes a record.
+ */
+class delete_WdOperation extends WdOperation
+{
+ /**
+ * Controls for the operation: permission(manage), record and ownership.
+ *
+ * @see WdOperation::__get_controls()
+ */
+ protected function __get_controls()
+ {
+ return array
+ (
+ self::CONTROL_PERMISSION => WdModule::PERMISSION_MANAGE,
+ self::CONTROL_RECORD => true,
+ self::CONTROL_OWNERSHIP => true
+ )
+
+ + parent::__get_controls();
+ }
+
+ protected function validate()
+ {
+ return true;
+ }
+
+ /**
+ * Delete the target record.
+ *
+ * @see WdOperation::process()
+ */
+ protected function process()
+ {
+ $key = $this->key;
+
+ if (!$this->module->model->delete($key))
+ {
+ wd_log_error('Unable to delete the record %key from %module.', array('%key' => $key, '%module' => (string) $this->module));
+
+ return;
+ }
+
+ if (isset($this->params['#location']))
+ {
+ $this->location = $this->params['#location'];
+ }
+
+ wd_log_done('The record %key has been delete from %module.', array('%key' => $key, '%module' => $this->module->title), 'delete');
+
+ return $key;
+ }
+}
View
156 operations/save.php
@@ -0,0 +1,156 @@
+<?php
+
+/*
+ * This file is part of the WdCore package.
+ *
+ * (c) Olivier Laviale <olivier.laviale@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * The "save" operation is used to create or update a record.
+ */
+class save_WdOperation extends WdOperation
+{
+ /**
+ * Change controls:
+ *
+ * CONTROL_PERMISSION => WdModule::PERMISSION_CREATE
+ * CONTROL_OWNERSHIP => true
+ * CONTROL_FORM => true
+ *
+ * @param WdOperation $operation
+ * @return array The controls of the operation.
+ */
+ protected function __get_controls()
+ {
+ return array
+ (
+ self::CONTROL_PERMISSION => WdModule::PERMISSION_CREATE,
+ self::CONTROL_RECORD => true,
+ self::CONTROL_OWNERSHIP => true,
+ self::CONTROL_FORM => true
+ )
+
+ + parent::__get_controls();
+ }
+
+ /**
+ * Overrides the getter to prevent exceptions when the operation key is empty.
+ *
+ * @see WdOperation::__get_record()
+ */
+ protected function __get_record()
+ {
+ return $this->key ? parent::__get_record() : null;
+ }
+
+ /**
+ * Overrides the method in order for the control to pass if the operation key is empty, which
+ * is the case when creating a new record.
+ *
+ * @see WdOperation::control_record()
+ */
+ protected function control_record()
+ {
+ return $this->key ? parent::control_record() : true;
+ }
+
+ /**
+ * Filters out the operation's parameters, which are not defined as fields by the
+ * primary model of the module, and take care of filtering or resolving properties values.
+ *
+ * Fields defined as 'boolean'
+ * ---------------------------
+ *
+ * The value of the property is filtered using the filter_var() function and the
+ * FILTER_VALIDATE_BOOLEAN filter. If the property in the operation params is empty, the
+ * property value is set the `false`.
+ *
+ * Fields defined as 'varchar'
+ * ---------------------------
+ *
+ * If the property is not empty in the operation params, the property value is trimed using the
+ * trim() function, ensuring that there is no leading or trailing white spaces.
+ *
+ * @see WdOperation::__get_properties()
+ * @return array The properties of the operation.
+ */
+ protected function __get_properties()
+ {
+ $schema = $this->module->model->get_extended_schema();
+ $fields = $schema['fields'];
+ $properties = array_intersect_key($this->params, $fields);
+
+ foreach ($fields as $identifier => $definition)
+ {
+ $type = $definition['type'];
+
+ if ($type == 'boolean')
+ {
+ if (empty($properties[$identifier]))
+ {
+ $properties[$identifier] = false;
+
+ continue;
+ }
+
+ $properties[$identifier] = filter_var($properties[$identifier], FILTER_VALIDATE_BOOLEAN);
+ }
+ else if ($type == 'varchar')
+ {
+ if (empty($properties[$identifier]) || !is_string($properties[$identifier]))
+ {
+ continue;
+ }
+
+ $properties[$identifier] = trim($properties[$identifier]);
+ }
+ }
+
+ return $properties;
+ }
+
+ /**
+ * The method simply returns true.
+ *
+ * @see WdOperation::validate()
+ */
+ protected function validate()
+ {
+ return true;
+ }
+
+ /**
+ * Creates or updates a record in the module's primary model.
+ *
+ * A record is created if the operation's key is empty, otherwise an existing record is
+ * updated.
+ *
+ * The method uses the `properties` property to get the properties used to create or update
+ * the record.
+ *
+ * @return array An array composed of the save mode ('update' or 'create') and the record's
+ * key.
+ * @throws WdException when saving the record fails.
+ */
+ protected function process()
+ {
+ $key = $this->key;
+ $record_key = $this->module->model->save($this->properties, $key);
+ $log_params = array('%key' => $key, '%module' => $this->module->title);
+
+ if (!$record_key)
+ {
+ throw new WdException($key ? 'Unable to update record %key in %module.' : 'Unable to create record in %module.', $log_params);
+ }
+
+ $this->location = $_SERVER['REQUEST_URI'];
+
+ wd_log_done($key ? 'The record %key in %module has been saved.' : 'A new record has been saved in %module.', $log_params, 'save');
+
+ return array('mode' => $key ? 'update' : 'create', 'key' => $record_key);
+ }
+}
View
86 wdcore.php
@@ -16,9 +16,12 @@
require_once 'helpers/debug.php';
require_once 'wdobject.php';
+/**
+ * @property WdDatabase $db The primary database connection.
+ */
class WdCore extends WdObject
{
- const VERSION = '0.9.0-dev';
+ const VERSION = '0.10.0-dev';
static public $config = array();
@@ -267,67 +270,10 @@ protected function run_operation($uri, array $params)
return;
}
- $operation->dispatch();
+ $operation->__invoke();
return $operation;
}
-
- /**
- * Display information about the core and its modules.
- *
- * This is the callback used by the "/api/core/aloha" operation.
- */
-
- static public function operation_aloha()
- {
- global $core;
-
- $enabled = array();
- $disabled = array();
-
- foreach ($core->modules->descriptors as $module_id => $descriptor)
- {
- if (!empty($descriptor[WdModule::T_DISABLED]))
- {
- $disabled[] = $module_id;
-
- continue;
- }
-
- $enabled[] = $module_id;
- }
-
- sort($enabled);
- sort($disabled);
-
- header('Content-Type: text/plain; charset=utf-8');
-
- $rc = 'WdCore version ' . self::VERSION . ' is running here with:';
- $rc .= PHP_EOL . PHP_EOL . implode(PHP_EOL, $enabled);
- $rc .= PHP_EOL . PHP_EOL . 'Disabled modules:';
- $rc .= PHP_EOL . PHP_EOL . implode(PHP_EOL, $disabled);
- $rc .= PHP_EOL . PHP_EOL . strip_tags(implode(PHP_EOL, WdDebug::fetchMessages('debug')));
-
- return $rc;
- }
-
- /**
- * Keeps the user's session alive. Only already created sessions are kept alive, new sessions
- * are *not* created.
- */
- static public function operation_ping()
- {
- global $core;
-
- header('Content-Type: text/plain; charset=utf-8');
-
- if (WdSession::exists())
- {
- $core->session;
- }
-
- return 'pong';
- }
}
if (!function_exists('class_alias'))
@@ -698,15 +644,11 @@ protected function index_module($id, $path)
$autoload = array();
- //COMPAT-20110108
+ $operations_dir = $path . 'operations' . DIRECTORY_SEPARATOR;
- $autoload_dir = $path . 'autoload' . DIRECTORY_SEPARATOR;
-
- if (is_dir($autoload_dir))
+ if (is_dir($operations_dir))
{
- $dh = opendir($autoload_dir);
-
- WdDebug::trigger('the autoload dir was a bad idea, we should remove it: \1', array($autoload_dir));
+ $dh = opendir($operations_dir);
while (($file = readdir($dh)) !== false)
{
@@ -715,21 +657,13 @@ protected function index_module($id, $path)
continue;
}
- $name = basename($file, '.php');
-
- if ($name[0] == '_')
- {
- $name = $flat_id . $name;
- }
-
- $autoload[$name] = $autoload_dir . $file;
+ $name = $flat_id . '__' . basename($file, '.php') . '_WdOperation';
+ $autoload[$name] = $operations_dir . $file;
}
closedir($dh);
}
- // /COMPAT
-
if (file_exists($path . 'module.php'))
{
$autoload[$flat_id . '_WdModule'] = $path . 'module.php';
View
742 wdmodule.php
@@ -9,6 +9,9 @@
* file that was distributed with this source code.
*/
+/**
+ * @property WdModel $model The primary model of the module.
+ */
class WdModule extends WdObject
{
const T_CATEGORY = 'category';
@@ -25,7 +28,7 @@ class WdModule extends WdObject
const T_TITLE = 'title';
/*
- * PERMISSIONS
+ * PERMISSIONS:
*
* NONE: Well, you can't do anything
*
@@ -40,7 +43,6 @@ class WdModule extends WdObject
* ADMINISTER: You have complete control over the module
*
*/
-
const PERMISSION_NONE = 0;
const PERMISSION_ACCESS = 1;
const PERMISSION_CREATE = 2;
@@ -48,6 +50,9 @@ class WdModule extends WdObject
const PERMISSION_MANAGE = 4;
const PERMISSION_ADMINISTER = 5;
+ const OPERATION_SAVE = 'save';
+ const OPERATION_DELETE = 'delete';
+
static public function is_extending($module_id, $extending_id)
{
global $core;
@@ -94,23 +99,44 @@ public function __toString()
return $this->id;
}
+ protected function __volatile_get_id()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Getter for the $flat_id magic property.
+ *
+ * @return string The _flat_ version of the module id.
+ */
protected function __get_flat_id()
{
return strtr($this->id, '.', '_');
}
/**
- * Getter for the primary model.
+ * Getter for the $model magic property.
*
* @return WdModel The _primary_ model for the module.
*/
-
protected function __get_model()
{
return $this->model();
}
/**
+ * Returns the module title, translated to the current language.
+ *
+ * @return string
+ */
+ protected function __get_title()
+ {
+ $default = isset($this->tags[WdModule::T_TITLE]) ? $this->tags[WdModule::T_TITLE] : 'Undefined';
+
+ return t($this->flat_id, array(), array('scope' => array('module', 'title'), 'default' => $default));
+ }
+
+ /**
* Check wheter or not the module is installed.
*
* @return mixed TRUE if the module is installed, FALSE if the module
@@ -149,7 +175,6 @@ public function is_installed()
* not installation process.
*
*/
-
public function install()
{
if (empty($this->tags[self::T_MODELS]))
@@ -193,7 +218,6 @@ public function install()
* @return mixed TRUE is the module has successfully been uninstalled. FALSE if the module
* (or parts of the module) failed to uninstall. NULL if there is no unistall process.
*/
-
public function uninstall()
{
if (empty($this->tags[self::T_MODELS]))
@@ -229,7 +253,6 @@ public function run()
/**
* @var array Used to cache loaded models.
*/
-
protected $models = array();
/**
@@ -240,7 +263,6 @@ public function run()
* @param $which
* @return WdModel The requested model.
*/
-
public function model($which='primary')
{
global $core;
@@ -440,710 +462,6 @@ protected function resolve_model_tags($tags, $which)
return $tags;
}
- /*
- * OPERATIONS
- */
-
- /**
- * Handles a specified operation.
- *
- * Before processing the operation, the function first checks if the operation is actually
- * implemented by the module, and if it's valid.
- *
- * Checking the operation's implementation
- * =======================================
- *
- * A callback method is required for any operation to be processed. The name of the method
- * must follow the pattern "operation_<name>", where "<name>" is the name of the operation. A
- * module is considered being capable of handling an operation if its associated method is
- * implemented.
- *
- * If the required method is not implemented by the module's class, an exception with code
- * 404 is thrown.
- *
- * Checking the validity of an operation
- * =====================================
- *
- * A control chain, often specific to the operation, must be passed for an operation to be
- * processed. The control chain is handled by the method handle_operation_control(), failures
- * often resulting in an exception being thrown.
- *
- * Processing the operation
- * ========================
- *
- * Once the controls passed successfully, the operation is processed by invoking the callback
- * method associated with the operation.
- *
- * @param WdOperation $operation The operation object to handle.
- *
- * @throws WdHTTPException
- */
-
- public function handle_operation(WdOperation $operation)
- {
- $name = $operation->name;
- $callback = 'operation_' . $name;
-
- if (!method_exists($this, $callback))
- {
- throw new WdHTTPException
- (
- 'Unknown operation %operation for the %module module.', array
- (
- '%operation' => $name, '%module' => $this->id
- ),
-
- 404
- );
- }
-
- if (!$this->handle_operation_control($operation))
- {
- return;
- }
-
- return $this->$callback($operation);
- }
-
- const CONTROL_AUTHENTICATION = 101;
- const CONTROL_PERMISSION = 102;
- const CONTROL_RECORD = 103;
- const CONTROL_OWNERSHIP = 104;
- const CONTROL_FORM = 105;
- const CONTROL_VALIDATOR = 106;
- const CONTROL_PROPERTIES = 107;
-
- /**
- * Returns the default controls for any operation.
- *
- * All controls are defined, but onyl the _validator_ control is requested.
- *
- * @param WdOperation $operation
- */
-
- protected function controls_for_operation(WdOperation $operation)
- {
- return array
- (
- self::CONTROL_AUTHENTICATION => false,
- self::CONTROL_PERMISSION => self::PERMISSION_NONE,
- self::CONTROL_RECORD => false,
- self::CONTROL_OWNERSHIP => false,
- self::CONTROL_FORM => false,
- self::CONTROL_VALIDATOR => true,
- self::CONTROL_PROPERTIES => false
- );
- }
-
- /**
- * Handles the operation control.
- *
- * Controls for the operation are retrieved by invoking the "controls_for_operation[_<name>]"
- * method. The control chain is processed by invoking the "control_operation[_<name>]" method.
- *
- * @param WdOperation $operation
- * @return boolean Wheter or not the controls were successfully passed.
- */
-
- protected function handle_operation_control(WdOperation $operation)
- {
- $operation_name = $operation->name;
-
- $fallback = 'controls_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- $controls = $this->$callback($operation) + $this->controls_for_operation($operation);
-
- if (!empty($controls[self::CONTROL_OWNERSHIP]))
- {
- $controls[self::CONTROL_RECORD] = true;
- }
-
- $fallback = 'control_operation';
- $callback = $fallback . '_' . $operation->name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- return $this->$callback($operation, $controls);
- }
-
- /**
- * Controls the operation.
- *
- * A number of controls to pass may be requested before an operation is processed. This
- * function tries the specified controls (or operation specific controls if they are defined).
- * If all the specified controls are passed, the operation control is considered sucessful.
- *
- * Controls are passed in the following order:
- *
- * 1. CONTROL_AUTHENTICATION
- *
- * Controls the authentication of the user. The"control_authentication_for_operation[_<name>]"
- * callback method is invoked for this control. An exception with the code 401 is thrown if
- * the control fails.
- *
- * 2. CONTROL_PERMISSION
- *
- * Controls the permission of the guest or user. The
- * "control_permission_for_operation[_<name>]" callback method is invoked for this control. An
- * exception with code 401 is thrown if the control fails.
- *
- * 3. CONTROL_RECORD
- *
- * Controls the existence of the record specified by the operation's key. The
- * "control_record_for_operation[_<name>]" callback method is invoked for this control. The
- * value returned by the callback method is set in the operation objet under the `record`
- * property. The callback method must throw an exception if the record could not be loaded or
- * the control of this record failed.
- *
- * 4. CONTROL_OWNERSHIP
- *
- * Controls the ownership of the user over the record found during the CONTROL_RECORD step. The
- * "control_ownership_for_operation[_<name>]" callback method is invoked for the control. An
- * exception with code 401 is thrown if the control fails.
- *
- * 5. CONTROL_FORM
- *
- * Controls the form associated with the operation by checking its existence and validity.
- * The "control_form_for_operation[_<name>]" callback method is invoked for this control.
- * Failing the control won't throw an exception, but a message will be logged to the debug log.
- *
- * 6. CONTROL_PROPERTIES
- *
- * Controls the operation's params and process them to create properties suitable for the
- * module's primary model. The "control_properties_for_operation[_<name>]" callback method is
- * invoked for this control. Failling the control won't throw an exception, but a message will
- * be logged to the debug log.
- *
- * 7. CONTROL_VALIDATOR
- *
- * Validate the operation using the "validate_operation[_<name>]" callback method. Failing the
- * control won't throw an exception, but a message will be logged to the debug log.
- *
- * @param WdOperation $operation The operation object.
- * @param array $controls The controls to pass for the operation to be processed.
- * @return boolean Wheter or not the controls where passed.
- */
-
- protected function control_operation(WdOperation $operation, array $controls)
- {
- $operation_name = $operation->name;
-
- if ($controls[self::CONTROL_AUTHENTICATION])
- {
- $fallback = 'control_authentication_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- if (!$this->$callback($operation))
- {
- throw new WdHTTPException
- (
- 'The %operation operation requires authentication.', array
- (
- '%operation' => $operation_name
- ),
-
- 401
- );
- }
- }
-
- if ($controls[self::CONTROL_PERMISSION])
- {
- $fallback = 'control_permission_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- $this->$callback($operation, $controls[self::CONTROL_PERMISSION]);
- }
-
- if ($controls[self::CONTROL_RECORD])
- {
- $fallback = 'control_record_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- $operation->record = $this->$callback($operation);
-
- /*
- if (!$record instanceof WdActiveRecord)
- {
- throw new WdHTTPException
- (
- "The requested record could not be loaded from the %module module: %key", array
- (
- '%key' => $operation->key,
- '%module' => $this->id
- ),
-
- 404
- );
- }
- */
- }
-
- if ($controls[self::CONTROL_OWNERSHIP])
- {
- $fallback = 'control_ownership_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- if (!$this->$callback($operation))
- {
- throw new WdHTTPException
- (
- "You don't have ownership of the record: %key", array
- (
- '%key' => $operation->key
- ),
-
- 401
- );
- }
- }
-
- if ($controls[self::CONTROL_FORM])
- {
- $fallback = 'control_form_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- if (!$this->$callback($operation))
- {
- wd_log('Control %control failed for operation %operation on module %module.', array('%control' => 'form', '%module' => $this->id, '%operation' => $operation_name));
-
- return false;
- }
- }
-
- if ($controls[self::CONTROL_PROPERTIES])
- {
- $fallback = 'control_properties_for_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- try
- {
- $operation->properties = $this->$callback($operation);
- }
- catch (Exception $e)
- {
- wd_log
- (
- "Control %control failed for operation %operation on module %module: :exception", array
- (
- '%control' => 'properties', '%module' => $this->id, '%operation' => $operation_name, ':exception' => $e->getMessage()
- )
- );
-
- return false;
- }
- }
-
- if ($controls[self::CONTROL_VALIDATOR])
- {
- $fallback = 'validate_operation';
- $callback = $fallback . '_' . $operation_name;
-
- if (!method_exists($this, $callback))
- {
- $callback = $fallback;
- }
-
- if (!$this->$callback($operation))
- {
- wd_log('Control failed on validator. Module: %module, operation: %operation', array('%module' => $this->id, '%operation' => $operation_name));
-
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Controls the authentication of the user for the operation.
- *
- * @param WdOperation $operation
- */
- protected function control_authentication_for_operation(WdOperation $operation)
- {
- global $core;
-
- return ($core->user_id != 0);
- }
-
- /**
- * Controls the permission of the user for the operation.
- *
- * @param WdOperation $operation The operation object.
- * @param mixed $permission The required permission.
- * @throws WdException if the user doesn't have the specified permission.
- */
- protected function control_permission_for_operation(WdOperation $operation, $permission)
- {
- global $core;
-
- if (!$core->user->has_permission($permission, $this))
- {
- throw new WdHTTPException
- (
- "You don't have permission to perform the %operation operation on the %module module.", array
- (
- '%operation' => $operation->name,
- '%module' => $this->id
- ),
-
- 401
- );
- }
-
- return true;
- }
-
- /**
- * Controls the properties for the operation.
- *
- * Currently, this generic method returns an empty array as properties.
- *
- * @param WdOperation $operation
- * @return array An empty array.
- */
-
- protected function control_properties_for_operation(WdOperation $operation)
- {
- return array();
- }
-
- /**
- * Control the existence of the record the operation is to be applied to.
- *
- * The operation's key is used to find the record in the module's primary model. The found
- * record is stored in the 'record' property of the operation object.
- *
- * @param $operation The operation object.
- * @throws WdException when the record cannot be found in the model.
- */
- protected function control_record_for_operation(WdOperation $operation)
- {
- return $this->model[$operation->key];
- }
-
- /**
- * Override the record control for the "save" operation in order for the control to pass even
- * if the operation's key is empty, which is the case when creating a new record.
- *
- * @param WdOperation $operation
- */
- protected function control_record_for_operation_save(WdOperation $operation)
- {
- return $operation->key ? $this->control_record_for_operation($operation) : null;
- }
-
- /**
- * Controls the ownership of the user over the operation's record.
- *
- * The control is failed if a record was found but the user has no ownership on that record.
- *
- * The control is sucessful if there is no record in the operation object, or there is a record
- * and the user has ownership on that record.
- *
- * @param WdOperation $operation
- * @return bool
- */
- protected function control_ownership_for_operation(WdOperation $operation)
- {
- global $core;
-
- $record = $operation->record;
-
- if ($record && !$core->user->has_ownership($this, $record))
- {
- return false;
- }
-
- return true;
- }
-
- /**
- * Control the form for the operation.
- *
- * The function assumes the form was saved in the user's session.
- *
- * If the function fails to retieve or validate the saved form it returns false. Otherwise
- * the retrieved form is set in the operation object under the `form` property and the function
- * returns true.
- *
- * One can override this method to modify operation parameters before the form gets validated,
- * or override the method to control unsaved forms.
- *
- * @param $operation
- * @return bool
- */
- protected function control_form_for_operation(WdOperation $operation)
- {
- $params = &$operation->params;
-
- if (empty($operation->form))
- {
- $operation->form = WdForm::load($params);
- }
-
- $form = $operation->form;
-
- if (!$form || !$form->validate($params))
- {
- return false;
- }
-
- return true;
- }
-
- /**
- * Default operations validator.
- *
- * @param WdOperation $operation
- * @throws WdException because a validator *must* be defined for each operation.
- */
- protected function validate_operation(WdOperation $operation)
- {
- throw new WdException
- (
- 'The %module module is missing a validator for the %operation operation', array
- (
- '%operation' => $operation->name,
- '%module' => $this->id
- )
- );
- }
-
- const OPERATION_SAVE = 'save';
-
- /**
- * Returns the controls for the "save" operation.
- *
- * @param WdOperation $operation
- * @return array The controls of the operation.
- */
- protected function controls_for_operation_save(WdOperation $operation)
- {
- return array
- (
- self::CONTROL_AUTHENTICATION => false,
- self::CONTROL_PERMISSION => self::PERMISSION_CREATE,
- self::CONTROL_OWNERSHIP => true,
- self::CONTROL_FORM => true,
- self::CONTROL_PROPERTIES => true,
- self::CONTROL_VALIDATOR => true
- );
- }
-
- /**
- * Filters out the operation's parameters, which are not defined as fields by the
- * primary model of the module, and take care of filtering or resolving properties values.
- *
- * Fields defined as 'boolean'
- * ---------------------------
- *
- * The value of the property is filtered using the filter_var() function and the
- * FILTER_VALIDATE_BOOLEAN filter. If the property in the operation params is empty, the
- * property value is set the `false`.
- *
- * Fields defined as 'varchar'
- * ---------------------------
- *
- * If the property is not empty in the operation params, the property value is trimed using the
- * trim() function, ensuring that there is no leading or trailing white spaces.
- *
- * @param WdOperation $operation
- * @return array The controled properties.
- */
- protected function control_properties_for_operation_save(WdOperation $operation)
- {
- $schema = $this->model->get_extended_schema();
- $fields = $schema['fields'];
- $properties = array_intersect_key($operation->params, $fields);
-
- foreach ($fields as $identifier => $definition)
- {
- $type = $definition['type'];
-
- if ($type == 'boolean')
- {
- if (empty($properties[$identifier]))
- {
- $properties[$identifier] = false;
-
- continue;
- }
-
- $properties[$identifier] = filter_var($properties[$identifier], FILTER_VALIDATE_BOOLEAN);
- }
- else if ($type == 'varchar')
- {
- if (empty($properties[$identifier]) || !is_string($properties[$identifier]))
- {
- continue;
- }
-
- $properties[$identifier] = trim($properties[$identifier]);
- }
- }
-
- return $properties;
- }
-
- /**
- * Saves a record to the primary model associated with the module.
- *
- * A record is either created or updated. A record is created if the operation's key is empty,
- * otherwise an existing record is updated.
- *
- * The method uses the operation's `properties` property, created by the
- * control_properties_for_operation() method, to save the record.
- *
- * @param WdOperation $operation An operation object.
- * @return array An array composed of the save mode ('update' or 'create') and the record's
- * key.
- * @throws WdException when saving the record failed.
- */
- protected function operation_save(WdOperation $operation)
- {
- $operation_key = $operation->key;
- $key = $this->model->save($operation->properties, $operation_key);
- $log_params = array('%key' => $operation_key, '%module' => $this->id);
-
- if (!$key)
- {
- throw new WdException($operation_key ? 'Unable to update record %key in %module.' : 'Unable to create record in %module.', $log_params);
- }
-
- $operation->location = $_SERVER['REQUEST_URI'];
-
- wd_log_done($operation_key ? 'The record %key in %module has been saved.' : 'A new record has been saved in %module.', $log_params, 'save');
-
- return array
- (
- 'mode' => $operation_key ? 'update' : 'create',
- 'key' => $key
- );
- }
-
- const OPERATION_DELETE = 'delete';
-
- /**
- * Returns controls for the "delete" operation.
- *
- * @param WdOperation $operation
- *
- * @return array The controls for the "delete" operation.
- */
- protected function controls_for_operation_delete(WdOperation $operation)
- {
- return array
- (
- self::CONTROL_PERMISSION => self::PERMISSION_MANAGE,
- self::CONTROL_RECORD => true,
- self::CONTROL_OWNERSHIP => true,
- self::CONTROL_FORM => false,
- self::CONTROL_VALIDATOR => true
- );
- }
-
- /**
- * Validates the "delete" operation.
- *
- * The operation is validated only if the operation key is defined.
- *
- * @param WdOperation $operation
- */
- protected function validate_operation_delete(WdOperation $operation)
- {
- return (!empty($operation->key) || !empty($operation->params[WdOperation::KEYS]));
- }
-
- /**
- * Performs the "delete" operation.
- *
- * @param WdOperation $operation
- * @throws WdException
- */
- protected function operation_delete(WdOperation $operation)
- {
- $params = &$operation->params;
-
- if (isset($params[WdOperation::KEYS]))
- {
- $keys = $params[WdOperation::KEYS];
-
- foreach ($keys as $key => $dummy)
- {
- if ($this->model->delete($key))
- {
- wd_log_done('The entry %key has been delete from %module.', array('%key' => $key, '%module' => $this->id));
-
- continue;
- }
-
- wd_log_error('Unable to delete the entry %key from %module.', array('%key' => $key, '%module' => $this->id));
- }
- }
- else if ($operation->key)
- {
- $key = $operation->key;
-
- if ($this->model->delete($key))
- {
- wd_log_done('The entry %key has been delete from %module.', array('%key' => $key, '%module' => $this->id));
-
- return true;
- }
- else
- {
- wd_log_error('Unable to delete the entry %key from %module.', array('%key' => $key, '%module' => $this->id));
-
- return;
- }
- }
- else
- {
- throw new WdException('Keys are missing for the delete operation.');
- }
- }
-
/**
* Get a block.
*
View
836 wdoperation.php
@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
-class WdOperation
+abstract class WdOperation extends WdObject
{
const DESTINATION = '#destination';
const NAME = '#operation';
@@ -18,52 +18,124 @@ class WdOperation
const RESTFUL_BASE = '/api/';
const RESTFUL_BASE_LENGHT = 5;
+ static protected $formats = array
+ (
+ 'json' => array('application/json', 'format_json'),
+ 'xml' => array('application/xml', 'format_xml')
+ );
+
/**
- * Encodes a module operation as a RESful URL:
- *
- * '/api/<destination>/(<key>/)?<method>?param1=...'
+ * Encodes a RESful operation.
*
- * @param mixed $destination
- * @param string $name
+ * @param string $pattern
* @param array $params
- * @param int|null $key
*/
-
- static public function encode($destination, $name, array $params=array(), $key=null)
+ static public function encode($pattern, array $params=array())
{
- $query = http_build_query($params, '', '&');
-
- return self::RESTFUL_BASE . $destination . '/' . ($key !== null ? $key . '/' : '') . $name . ($query ? '?' . $query : '');
- }
-
- static public function decode($uri, array $request)
- {
- $method = 'GET';
$destination = null;
- $operation = null;
-
- //DIRTY:COMPAT
+ $name = null;
+ $key = null;
- if (isset($request['!do']))
+ if (isset($params[self::DESTINATION]))
{
- $request['do'] = $request['!do'];
+ $destination = $params[self::DESTINATION];
+
+ unset($params[self::DESTINATION]);
}
- #
- # RESTful
- #
+ if (isset($params[self::NAME]))
+ {
+ $name = $params[self::NAME];
+
+ unset($params[self::NAME]);
+ }
- if (substr($uri, 0, 4) == '/do/')
+ if (isset($params[self::KEY]))
{
- if (WdDebug::$config['mode'] == 'test')
- {
- WdDebug::trigger('The URL for RESTful operation is now "/api/" instead of "/do/": %uri', array('%uri' => $uri));
- }
+ $key = $params[self::KEY];
- $uri = '/api/' + substr($uri, 4);
+ unset($params[self::KEY]);
}
- // /COMPAT
+ $qs = http_build_query($params, '', '&');
+
+ return self::RESTFUL_BASE . strtr
+ (
+ $pattern, array
+ (
+ '{destination}' => $destination,
+ '{name}' => $name,
+ '{key}' => $key
+ )
+ )
+
+ . ($qs ? '?' . $qs : '');
+ }
+
+ /**
+ * Decodes the specified request into a WdOperation instance.
+ *
+ * An operation can be defined as a route, in which case the path of the request starts with
+ * "/api/". An operation can also be defined using the request parameters, in which case
+ * the DESTINATION, NAME and optionaly KEY parameters are defined within the request
+ * parameters.
+ *
+ * When the operation is defined as a route, the method searches for a matching route.
+ *
+ * If a matching route is found, the captured parameters of the matching route are merged
+ * with the request parameters and the method tries to create a WdOperation instance using
+ * the route.
+ *
+ * If no matching route could be found, the method tries to extract the DESTINATION, NAME and
+ * optional KEY parameters from the URL using the `/api/:destination(/:key)/:name` pattern.
+ * If the URL matches this pattern, captured parameters are merged with the request
+ * parameters and the operation decoding continues as if the operation was defined using
+ * parameters instead of the REST API.
+ *
+ * Finally, the method searches for the DESTINATION, NAME and optional KEY aparameters within
+ * the request parameters to create the WdOperation instance.
+ *
+ * If no operation was found in the request, the method simply returns.
+ *
+ *
+ * Instancing using the matching route
+ * -----------------------------------
+ *
+ * The matching route must define either the class of the operation instance (by defining the
+ * `class` key) or a callback that would create the operation instance (by defining the
+ * `callback` key).
+ *
+ * If the route defines the instance class, it is used to create the instance. Otherwise, the
+ * callback is used to create the instance.
+ *
+ *
+ * Instancing using the request parameters
+ * ---------------------------------------
+ *
+ * The operation destination (specified by the DESTINATION parameter) is the id of the
+ * destination module. The class and the operation name (specified by the NAME
+ * parameter) are used to search for the corresponding operation class to create the instance:
+ *
+ * <module_class_fragment>__<operation_name>_WdOperation
+ *
+ * The inheritence of the module class is used the find a suitable class. For example,
+ * these are the classes tried for the "contents.articles" module and the "save" operation:
+ *
+ * contents_articles__save_WdOperation
+ * contents__save_WdOperation
+ * system_nodes__save_WdOperation
+ *
+ * An instance of the found class is created with the request arguments and returned. If the
+ * class could not be found to create the operation instance, an exception is raised.
+ *
+ * @param string $uri The request URI.
+ * @param array $params The request parameters.
+ * @throws WdException When there is an error in the operation request.
+ * @throws WdHTTPException When the specified operation doesn't exists.
+ */
+ static public function decode($uri, array $params=array())
+ {
+ global $core;
if (substr($uri, 0, self::RESTFUL_BASE_LENGHT) == self::RESTFUL_BASE)
{
@@ -72,20 +144,22 @@ static public function decode($uri, array $request)
$uri = substr($uri, 0, -strlen($_SERVER['QUERY_STRING']) - 1);
}
- if (substr($uri, -5, 5) == '.json')
+ foreach (self::$formats as $extension => $format)
{
- $_SERVER['HTTP_ACCEPT'] = 'application/json';
+ $extension = '.' . $extension;
+ $extension_length = strlen($extension);
- $uri = substr($uri, 0, -5);
- }
- else if (substr($uri, -4, 4) == '.xml')
- {
- $_SERVER['HTTP_ACCEPT'] = 'application/xml';
+ if (substr($uri, -$extension_length) == $extension)
+ {
+ $_SERVER['HTTP_ACCEPT'] = $format[0];
+
+ $uri = substr($uri, 0, -$extension_length);
- $uri = substr($uri, 0, -4);
+ break;
+ }
}
- $routes = WdConfig::get_constructed('restful_operations', array(__CLASS__, 'restful_operations_constructor'), 'routes');
+ $routes = WdConfig::get_constructed('api', array(__CLASS__, 'api_constructor'), 'routes');
foreach ($routes as $pattern => $route)
{
@@ -96,12 +170,46 @@ static public function decode($uri, array $request)
continue;
}
+ #
+ # We found a matching route. The arguments captured from the route are merged with
+ # the request parameters. The route must define either a class for the operation
+ # instance (defined using the `class` key) or a callback to create that instance
+ # (defined using the `callback` key).
+ #
+
if (is_array($match))
{
- $request += $match;
+ $params = $match + $params;
+ }
+
+ if (isset($route['callback']) && isset($route['class']))
+ {
+ throw new WdException('Ambiguous definition for operation route, both callback and class are defined.');
}
+ else if (isset($route['callback']))
+ {
+ $operation = call_user_func($route['callback'], $params);
- $operation = new WdOperation($route, $pattern, $request);
+ if (!$operation instanceof WdOperation)
+ {
+ throw new WdException('The operation route callback %callback failed to produce an operation object.', array('%callback' => implode('::', $route['callback'])));
+ }
+ }
+ else if (isset($route['class']))
+ {
+ $class = $route['class'];
+
+ if (!class_exists($class, true))
+ {
+ throw new WdException('Unable to create operation instance, the %class class is not defined.', array('%class' => $class));
+ }
+
+ $operation = new $class($route, $pattern, $params);
+ }
+ else
+ {
+ throw new WdException('The operation route must either define a class or a callback.');
+ }
$operation->terminus = true;
$operation->method = 'GET';
@@ -110,7 +218,8 @@ static public function decode($uri, array $request)
}
#
- #
+ # We could not find a matching route, we try to extract the DESTINATION, NAME and
+ # optional KEY from the URI.
#
preg_match('#^([a-z\.]+)/(([^/]+)/)?([a-zA-Z0-9_\-]+)$#', substr($uri, self::RESTFUL_BASE_LENGHT), $matches);
@@ -119,39 +228,50 @@ static public function decode($uri, array $request)
{
list(, $destination, , $operation_key, $name) = $matches;
- $name = strtr($name, '-', '_');
-
- $request[self::KEY] = $matches[2] ? $operation_key : null;
- }
- else
- {
- throw new WdException('Uknown operation: %operation', array('%operation' => substr($uri, 4)), array(404 => 'Unknow operation'));
+ $params[self::DESTINATION] = $destination;
+ $params[self::NAME] = strtr($name, '-', '_');
+ $params[self::KEY] = $matches[2] ? $operation_key : null;
}
}
- else if (isset($request[self::NAME]))
+
+ #
+ # The request is not a API operation, we try to extract the operation information from the
+ # request parameters. If the DESTINATION and NAME request parameters are empty, we simply
+ # return because there is no operation to process.
+ #
+
+ if (empty($params[self::DESTINATION]) && empty($params[self::NAME]))
+ {
+ return;
+ }
+ else if (empty($params[self::DESTINATION]))
+ {
+ throw new WdException('The destination for the %operation operation is missing', array('%operation' => $params[self::NAME]));
+ }
+ else if (empty($params[self::NAME]))
{
- $method = 'POST';
- $name = $request[self::NAME];
+ throw new WdException('The operation for the %destination destination is missing', array('%destination' => $params[self::DESTINATION]));
+ }
- if (empty($request[self::DESTINATION]))
- {
- throw new WdException('Missing destination for operation %operation', array('%operation' => $name));
- }
+ $name = $params[self::NAME];
+ $destination = $params[self::DESTINATION];
- $destination = $request[self::DESTINATION];
+ unset($params[self::DESTINATION]);
+ unset($params[self::NAME]);
- unset($request[self::DESTINATION]);
- unset($request[self::NAME]);
- }
+ $module = $core->modules[$destination];
+ $class = self::resolve_operation_class($name, $module);
- if (!$destination || !$name)
+ if (!$class)
{
- return false;
+ throw new WdHTTPException('Uknown operation %operation for the %module module.', array('%module' => (string) $module, '%operation' => $name), 404);
}
- $operation = new WdOperation($destination, $name, $request);
+ $operation = new $class($module, $name, $params);
+
+ $method = $_SERVER['REQUEST_METHOD'];
- if ($method == 'GET')
+ if (substr($uri, 0, self::RESTFUL_BASE_LENGHT) == self::RESTFUL_BASE || $method == 'GET')
{
$operation->terminus = true;
}
@@ -162,13 +282,11 @@ static public function decode($uri, array $request)
}
/**
- * Constructs the configuration "restful_operations" by filtering RESTful routes from the
- * "routes" config.
+ * Constructs the "api" configuration by filtering API routes from the "routes" fragments.
*
* @param array $fragments Configuration fragments.
*/
-
- static public function restful_operations_constructor(array $fragments)
+ static public function api_constructor(array $fragments)
{
$routes = array();
@@ -190,9 +308,29 @@ static public function restful_operations_constructor(array $fragments)
return $routes;
}
+ static private function resolve_operation_class($name, $target)
+ {
+ static $suffix = '_WdModule';
+ static $suffix_lenght = 9;
+
+ $class = get_class($target);
+
+ while ($class && substr($class, -$suffix_lenght) == $suffix)
+ {
+ $try = substr($class, 0, -$suffix_lenght) . "__{$name}_WdOperation";
+
+ if (class_exists($try, true))
+ {
+ return $try;
+ }
+
+ $class = get_parent_class($class);
+ }
+ }
+
public $name;
- public $destination;
public $key;
+ public $destination;
public $params = array();
public $response;
@@ -200,12 +338,127 @@ static public function restful_operations_constructor(array $fragments)
public $location;
public $method;
+ /**
+ * @var WdModule Target module for the operation.
+ *
+ * The property is set by the constructor.
+ */
+ protected $module;
+
+ /**
+ * @var array Controls to pass before validation.
+ */
+ private $controls;
+
+ const CONTROL_AUTHENTICATION = 101;
+ const CONTROL_PERMISSION = 102;
+ const CONTROL_RECORD = 103;
+ const CONTROL_OWNERSHIP = 104;
+ const CONTROL_FORM = 105;
+
+ /**
+ * Getter for the {@link $controls} property.
+ *
+ * @return array
+ */
+ protected function __get_controls()
+ {
+ return array
+ (
+ self::CONTROL_AUTHENTICATION => false,
+ self::CONTROL_PERMISSION => false,
+ self::CONTROL_RECORD => false,
+ self::CONTROL_OWNERSHIP => false,
+ self::CONTROL_FORM => false
+ );
+ }
+
+ /**
+ * @var WdActiveRecord The target active record object of the operation.
+ */
+ protected $record;
+
+ /**
+ * Getter for the {@link $record} property.
+ *
+ * @return WdActiveRecord
+ */
+ protected function __get_record()
+ {
+ return $this->module->model[$this->key];
+ }
+
+ /**
+ * The form object of the operation.
+ *
+ * @var WdForm
+ */
+ protected $form;
+
+ /**
+ * Getter for the {@link $form} property.
+ *
+ * The method tries to load the form from the operation parameters using the
+ * {@link WdForm::load()} method.
+ *
+ * One can override this method to provide the form using another method. Or simply define the
+ * {@link $form} property to circumvent the getter.
+ *
+ * @return WdForm|null
+ */
+ protected function __get_form()
+ {
+ return $this->form = WdForm::load($this->params);
+ }
+
+ /**
+ * @var array The properties for the operation.
+ */
+ protected $properties;
+
+ /**
+ * Getter for the {@link $properties} property.
+ *
+ * The getter should only be called during the {@link process()} method.
+ *
+ * @return array
+ */
+ protected function __get_properties()
+ {
+ return array();
+ }
+
+ /**
+ * @var output Format for the operation response.
+ */
+ protected $format;
+
+ /**
+ * Constructor.
+ *
+ * The {@link $controls}, {@link $record}, {@link $form} and {@link $properties} properties
+ * are unset in order for their getters to be called on the next access, while keeping their
+ * scope.
+ *
+ * @param WdModule|array $destination The destination of the operation, either a module or a
+ * route.
+ * @param string $name The name of the operation.
+ * @param array $params The parameters of the operation.
+ */
public function __construct($destination, $name, array $params=array())
{
+ unset($this->controls);
+ unset($this->record);
+ unset($this->form);
+ unset($this->properties);
+
$this->destination = $destination;
$this->name = $name;
$this->params = $params;
+ $this->target = $destination;
+ $this->module = $destination instanceof WdModule ? $destination : null;
+
if (isset($params[self::KEY]))
{
$this->key = $params[self::KEY];
@@ -213,18 +466,96 @@ public function __construct($destination, $name, array $params=array())
}
/**
- * Used to skip `location` and `terminus` handling while dispatching sub operations.
+ * @var int Count operations nesting.
*
- * @var int sub operation nesting.
+ * _Location_ and _terminus_ are disabled for sub operations.
*/
+ static private $nesting=0;
- static private $dispatch_nest=0;
-
- public function dispatch()
+ /**
+ * Handles the operation and prints or returns its result.
+ *
+ *
+ * The response object
+ * -------------------
+ *
+ * The operation result is saved in a _response_ object, which may contain meta data describing
+ * or accompanying the result. Operations can use the response object to provide additional
+ * information with the result they return. For example, the `WdOperation` class returns the
+ * success and error messages in the `log` property.
+ *
+ * Depending on the `Accept` header of the request, the response object can be formated as
+ * JSON or XML. If the `Accept` header is "application/json" the response is formated as JSON.
+ * If the `Accept` header is "application/xml" the response is formated as XML. If the
+ * `Accept` header is not of a supported type, only the result is printed, as a string.
+ *
+ * For API requests, the output format can also be defined by appending the correspondig
+ * extension to the request path:
+ *
+ * /api/system.nodes/12/online.json
+ *
+ *
+ * Control, validation and processing
+ * ----------------------------------
+ *
+ * Before the operation is actually processed with the {@link process()} method, it is
+ * controled and validated with the {@link control()} and {@link validate()} methods. If the
+ * control or validation fail the operation is not processed.
+ *
+ * The controls passed to the {@link control()} method are obtained through the
+ * {@link $controls} property or the {@link __get_controls()} getter if the property is not
+ * accessible.
+ *
+ *
+ * Events
+ * ------
+ *
+ * The `operation.<name>:before` event is fired before the operation is processed using the
+ * {@link process()} method with the operation destination as `target` key and the operation
+ * itself as `operation` key.
+ *
+ * The `operation.<name>` event is fired after the operation has been processed if its
+ * result is not `null` with the operation destination as `target` key, the operation itself
+ * as `operation` key and a reference to the operation result as `rc` key.
+ *
+ *
+ * Terminus and location
+ * ---------------------
+ *
+ * If the {@link $terminus} property is true after the operation has been processed, the
+ * script is ended. Remaining debug logs are added to the HTTP header as
+ * `X-Debug-<i>: <message>`. The {@link $terminus} property is always set to true when the
+ * request result is formated as JSON or XML.
+ *
+ * If the {@link $location} property is set after the operation has been processed, it is used
+ * to define the `Location` header, causing a redirection of the request. Also, the current
+ * request URL is set as `Referer`.
+ *
+ *
+ * Nested operations
+ * -----------------
+ *
+ * Operations can be nested, ie an operation can invoke another operation. A nesting counter
+ * is maintained and will disable handling of the {@link $terminus} and {@link $location}
+ * properties, until the original operation finishes.
+ *
+ *
+ * Failed operation
+ * ----------------
+ *
+ * If the result of the operation is `null`, the operation is considered as failed, in which
+ * case the result is not printed out and no event is fired. Still, the {@link $terminus} and
+ * {@link $location} properties are honored.
+ *
+ * Note that exceptions are not caught by the method.
+ *
+ * @return mixed The result of the operation.
+ */
+ public function __invoke()
{
global $core;
- self::$dispatch_nest++;
+ self::$nesting++;
$name = $this->name;
@@ -233,55 +564,41 @@ public function dispatch()
$name = substr($name, self::RESTFUL_BASE_LENGHT);
}
- #
- # reset results
- #
-
- $this->response = (object) array
- (
- 'rc' => null,
- 'log' => array()
- );
-
- #
- # We trigger the 'operation.<name>:before' event, listeners might use the event to
- # tweak the operation before the destination module processes the operation.
- #
+ $rc = null;
+ $this->response = (object) array('rc' => null, 'log' => array());
+ $module = $this->destination instanceof WdModule ? $this->destination : null;
- $destination = $this->destination;
- $module = is_array($destination) ? null : $module = $core->modules[$destination];
+ if ($this->control($this->controls) && $this->validate())
+ {
+ #
+ # The 'operation.<name>:before' event is fired before the operation is processed.
+ #
- WdEvent::fire
- (
- 'operation.' . $name . ':before', array
+ WdEvent::fire
(
- 'target' => $module,
- 'operation' => $this
- )
- );
+ 'operation.' . $name . ':before', array
+ (
+ 'target' => $module,
+ 'operation' => $this
+ )
+ );
- if (is_array($destination))
- {
- $rc = call_user_func($destination['callback'], $this);
+ $rc = $this->process();
}
else
{
- #
- # We ask the module to handle the operation. In return we get a response or `null` if the
- # operation failed.
- #
-
- $rc = $module->handle_operation($this);
+ wd_log('Operation control or validation failed.');
}
+ $this->response->rc = $rc;
+
#
- # If the operation succeed, we trigger a 'operation.<name>' event, listeners might use the
- # event for further processing. For example, a _comment_ module might delete the comments
- # related to an _article_ module from which an article has been deleted.
+ # If the operation succeed (its result is not null), the 'operation.<name>' event is fired.
+ # Listeners might use the event for further processing. For example, a _comment_ module
+ # might delete the comments related to an _article_ module from which an article was
+ # deleted.
#
- $this->response->rc = $rc;
-
if ($rc !== null)
{
WdEvent::fire
@@ -295,13 +612,12 @@ public function dispatch()
);
}
- if (--self::$dispatch_nest)
+ if (--self::$nesting)
{
return $this->response->rc;
}
$terminus = $this->terminus;
- $response = $this->response;
#
# The operation response can be requested as JSON or XML, in which case the script is
@@ -317,54 +633,31 @@ public function dispatch()
{
$accept = $_SERVER['HTTP_ACCEPT'];
- if ($accept == 'application/json' || $accept == 'application/xml')
+ foreach (self::$formats as $format)
{
- $logs = array('done', 'error');
+ list($format_mime, $format_callback) = $format;
- foreach ($logs as $type)
+ if ($format_mime != $accept)
{
- $response->log[$type] = WdDebug::fetchMessages($type);
+ continue;
}
- switch ($accept)
- {
- case 'application/json':
- {
- // https://addons.mozilla.org/en-US/firefox/addon/10869/
-
- $rc = json_encode($response);
- $rc_type = 'application/json';
- }
- break;
-
- case 'application/xml':
- {
- $rc = wd_array_to_xml($response, 'response');
- $rc_type = 'application/xml';
- }
- break;
- }
+ $logs = array('done', 'error');
- if ($rc !== null)
+ foreach ($logs as $type)
{
- header('Content-Type: ' . $rc_type);
- header('Content-Length: '. strlen($rc));
+ $this->response->log[$type] = WdDebug::fetchMessages($type);
}
- $terminus = true;
- }
- else if ($this->method == 'GET')
- {
- $rc = $this->response->rc;
- }
- }
+ $rc = $this->$format_callback();
- if ($this->location && !headers_sent())
- {
- header('Location: ' . $this->location);
- header('Referer: ' . $_SERVER['REQUEST_URI']);
+ header('Content-Type: ' . $format_mime);
+ header('Content-Length: '. strlen($rc));
- exit;
+ $terminus = true;
+
+ break;
+ }
}
#
@@ -400,30 +693,243 @@ public function dispatch()
exit;
}
+ if ($this->location && !headers_sent())
+ {
+ header('Location: ' . $this->location);
+ header('Referer: ' . $_SERVER['REQUEST_URI']);
+
+ exit;
+ }
+
return $this->response->rc;
}
- public function handle_booleans($booleans)
+ /**
+ * Controls the operation.
+ *
+ * A number of controls may be passed before an operation is validated and processed. Controls
+ * are defined as an array where the key is the control identifier, and the value defines
+ * whether the control is enabled. Controls are enabled by setting their value to true:
+ *
+ * array
+ * (
+ * self::CONTROL_AUTHENTICATION => true,
+ * self::CONTROL_RECORD => true,
+ * self::CONTROL_FORM => false
+ * );
+ *
+ * Instead of a boolean, the "permission" control is enabled by a permission string or a
+ * permission level.
+ *
+ * array
+ * (
+ * self::CONTROL_PERMISSION => WdModule::PERMISSION_MAINTAIN
+ * );
+ *
+ * The {@link $controls} property is used to get the controls or its magic getter
+ * {@link __get_controls()} if the property is not accessible.
+ *
+ * Controls are passed in the following order:
+ *
+ * 1. CONTROL_AUTHENTICATION
+ *
+ * Controls the authentication of the user. The {@link control_authentication()} method is
+ * invoked for this control. An exception with the code 401 is thrown when the control fails.
+ * The control is enabled by setting its value to true:
+ *
+ * array
+ * (
+ * self::CONTROL_AUTHENTICATION => true