anewt-developers team mailing list archive
-
anewt-developers team
-
Mailing list archive
-
Message #00225
[Branch ~uws/anewt/anewt.uws] Rev 1763: [urldispatcher] Implement external commands using callbacks
------------------------------------------------------------
revno: 1763
committer: Wouter Bolsterlee <uws@xxxxxxxxx>
branch nick: anewt.uws
timestamp: Sat 2010-02-20 16:29:38 +0100
message:
[urldispatcher] Implement external commands using callbacks
The documentation is updated to reflect all changes and API
additions, so refer to the docs for a more elaborate
description.
Fixes bug #514472.
modified:
urldispatcher/urldispatcher.lib.php
urldispatcher/urldispatcher.test.php
--
lp:anewt
https://code.launchpad.net/~uws/anewt/anewt.uws
Your team Anewt developers is subscribed to branch lp:anewt.
To unsubscribe from this branch go to https://code.launchpad.net/~uws/anewt/anewt.uws/+edit-subscription.
=== modified file 'urldispatcher/urldispatcher.lib.php'
--- urldispatcher/urldispatcher.lib.php 2010-02-20 12:48:21 +0000
+++ urldispatcher/urldispatcher.lib.php 2010-02-20 15:29:38 +0000
@@ -73,20 +73,48 @@
* AnewtURLDispatcher is based around two main concepts:
* <strong>commands</strong> and <strong>routes</strong>.
*
+ *
* \section commands Commands
*
- * Commands are methods that do the actual work, like building an output page.
- * Commands are regular methods on your AnewtURLDispatcher subclass, and look
+ * Commands are methods that do the actual work, like building an output page,
+ * or outputting a RSS feed. There are multiple ways to define commands.
+ *
+ * Commands can be regular methods on your AnewtURLDispatcher subclass, and look
* like \c command_xyz(), where \c xyz is the name of the command. What you
* would normally create a separate PHP file for, e.g. <code>users.php</code>,
* you can now write as a simple method, e.g. <code>command_users()</code>.
- * Other related pages are just additional methods on the same class.
+ * Other related pages are just additional methods on the same class. This way,
* AnewtURLDispatcher allows you to group related functionality together in one
* file, e.g. a user page, an âedit userâ page, or a ânew userâ page. (Note that
* if you insist you can still <code>require_once('latest-news.php')</code> in
* your command method, though this is not how AnewtURLDispatcher is intended to
* be used).
*
+ * Alternatively, commands can be implemented outside your AnewtURLDispatcher
+ * subclass. In this case, commands are callbacks on other classes using the PHP
+ * conventions for callbacks: a two-item array like the one you can pass to
+ * functions like <code>call_user_func()</code> (in fact, this function is used
+ * internally). This approach is particularly useful for larger projects where
+ * you do not want to write the code for all pages of your application into the
+ * same dispatcher class, because this would lead to an unmaintainable mess.
+ * Instead, you can split all functionality related to a certain part of your
+ * application into its own class, e.g. article overview, view, and edit urls
+ * could point to <code>ArticleCommand::overview()</code>,
+ * <code>ArticleCommand::view()</code>, and <code>ArticleCommand::edit()</code>
+ * methods. Just create a method for each specific page, and define routes that
+ * call those methods. It is recommended to use static methods, but it is also
+ * possible to use an object instance whose methods will be called. See the PHP
+ * manual on the callable type conventions and <code>call_user_func()</code>.
+ *
+ * If you use external commands, the classes that implement these commands can
+ * be loaded lazily, i.e. only when they are needed. For example, this would
+ * mean that the <code>ArticleCommand</code> class is only loaded when an
+ * article page is requested, and not if the login page is requested.
+ * AnewtURLDispatcher will invoke the method include_command_class() if it
+ * encounters a class that cannot be resolved, so override that method and make
+ * it load the right classes if you want to use the lazy loading functionality.
+ *
+ *
* \section routes Routes
*
* Routes define how URLs map to commands. This is where you define your URL
@@ -105,7 +133,9 @@
* - Change some settings if you're not happy with the defaults
* - Add routes to map URLs to commands
* - Implement commands to actually make your application do something useful,
- * just like would do otherwise.
+ * just like would do otherwise. Commands can be implemented directly into the
+ * dispatcher subclass, or in external files (use callbacks when adding route
+ * in that case)
*
* Setting up clean URLs for the Apache web server can be done using this \c
* .htaccess snippet. This instructs Apache to invoke \c dispatch.php for all
@@ -115,9 +145,9 @@
* \code
* RewriteEngine On
* RewriteBase /the/base/url/of/your/application
- *
+ *
* RewriteRule ^$ dispatch.php [L,QSA]
- *
+ *
* RewriteCond %{REQUEST_FILENAME} !-f
* RewriteCond %{REQUEST_FILENAME} !-d
* RewriteRule ^(.*) dispatch.php [L,QSA]
@@ -299,6 +329,15 @@
* $this->add_route_url_parts('month_archive', array('news', ':year', ':month'));
* \endcode
*
+ * Alternatively, the same, but now using external commands:
+ *
+ * \code
+ * $this->constraints = array('year' => '#^\\d{4}$#', 'month' => '#^\\d{1,2}$#');
+ * $this->add_route_url_parts(array('NewsCommand', 'latest_news'), 'news');
+ * $this->add_route_url_parts(array('NewsCommand', 'month_archive'), 'news/:year/:month/');
+ * $this->add_route_url_parts(array('NewsCommand', 'month_archive'), array('news', ':year', ':month'));
+ * \endcode
+ *
* Constraints should be provided as an associative array that maps
* parameter names to regular expressions. The constraints in the \c
* constraints property apply to all routes using URL parts, but can be
@@ -315,6 +354,93 @@
* URLs anyway since it is used for the client-side fragment specifier.)
*/
+ /**
+ * Callback used to lazy-load a command class.
+ *
+ * Override this method to hook in application-specific logic to make an
+ * external class with dispatcher commands available to the dispatcher, and
+ * you only want load the class when needed (lazy loading). This is used
+ * for routes pointing to external commands (those defined outside the
+ * AnewtURLDispatcher itself) when the class is not made available yet.
+ *
+ * \param $class_name
+ * The name of the class this method should load.
+ */
+ protected function include_command_class($class_name)
+ {
+ /* Do nothing by default */
+ }
+
+ /**
+ * Validate a command.
+ *
+ * \param $command
+ * The command to validate
+ *
+ * \return
+ * The validate command
+ */
+ private function validate_command($command)
+ {
+ if (is_string($command))
+ $command = array($this, sprintf('command_%s', $command));
+
+ list ($is_valid, $name_for_error_message) = $this->is_valid_command($command);
+
+ if (!$is_valid)
+ throw new AnewtHTTPException(
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ 'Dispatcher command invalid or not available: %s.',
+ $name_for_error_message);
+
+ return $command;
+ }
+
+ /**
+ * Check whether a command is valid.
+ *
+ * \param $command
+ * The command to validate
+ *
+ * \return
+ * A 2-tuple with a 'valid' flag and an error message.
+ */
+ private function is_valid_command($command)
+ {
+ $is_valid = false;
+ $name_for_error_message = $command;
+
+ if (is_string($command))
+ $command = array($this, sprintf('command_%s', $command));
+
+ if (is_numeric_array($command) && count($command) == 2 && is_string($command[1]))
+ {
+ if (is_string($command[0]))
+ {
+ /* Static method: Foo::bar() */
+ if (class_exists($command[0]))
+ {
+ /* Class is already loaded, so check thoroughly */
+ $is_valid = is_callable($command, false, $name_for_error_message);
+ }
+ else
+ {
+ /* Class is not loaded yet (see include_command_class()), so
+ * we can only check syntax in this case. */
+ $is_valid = is_callable($command, true, $name_for_error_message);
+ }
+ }
+ elseif (is_object($command[0]))
+ {
+ /* Instance method: $foo->bar() */
+ $is_valid = method_exists($command[0], $command[1]);
+ $name_for_error_message = sprintf('%s::%s', get_class($command[0]), $command[1]);
+ }
+ }
+
+ return array($is_valid, $name_for_error_message);
+ }
+
/**
* Add a route based on a regular expression.
*
@@ -327,9 +453,7 @@
*/
public function add_route_regex($command, $regex)
{
- $method = sprintf('command_%s', $command);
- if (!method_exists($this, $method))
- throw new AnewtHTTPException(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Dispatcher method "%s" is not defined.', $method);
+ $command = $this->validate_command($command);
$this->routes[] = array(
ANEWT_URL_DISPATCHER_ROUTE_TYPE_REGEX,
@@ -353,9 +477,7 @@
*/
public function add_route_url_parts($command, $url_parts, $additional_constraints=null)
{
- $method = sprintf('command_%s', $command);
- if (!method_exists($this, $method))
- throw new AnewtHTTPException(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Dispatcher method "%s" is not defined.', $method);
+ $command = $this->validate_command($command);
if (is_null($additional_constraints))
$additional_constraints = array();
@@ -435,7 +557,7 @@
private function real_dispatch($url=null, $prefix=null)
{
/* Use the current URL if no explicit url was given */
-
+
if (is_null($url))
$url = AnewtRequest::relative_url();
@@ -495,7 +617,7 @@
/* Try to find a matching route and extract the parameters */
$found_route = false;
- $command_name = null;
+ $command = null;
$parameters = array();
$url_parts = strlen($url) > 0
? explode('/', $url)
@@ -504,7 +626,7 @@
foreach ($this->routes as $route)
{
$route_type = array_shift($route);
- $route_command_name = array_shift($route);
+ $route_command = array_shift($route);
$route_parameters = array();
@@ -524,7 +646,7 @@
array_shift($route_parameters);
$route_parameters = array_map('urldecode', $route_parameters);
- $command_name = $route_command_name;
+ $command = $route_command;
$parameters = $route_parameters;
$found_route = true;
break;
@@ -598,7 +720,7 @@
/* If this code is reached, we found a matching route with all
* the constraints on the URL parts satisfied. */
- $command_name = $route_command_name;
+ $command = $route_command;
$parameters = $route_parameters;
$found_route = true;
break;
@@ -618,8 +740,8 @@
$url_parts = explode('/', $url, 2);
if ($url_parts)
{
- $command_name = $url_parts[0];
- $found_route = method_exists($this, sprintf('command_%s', $command_name));
+ $command = array($this, sprintf('command_%s', $url_parts[0]));
+ list ($found_route, $error_message_to_ignore) = $this->is_valid_command($command);
}
}
@@ -627,27 +749,36 @@
/* As a last resort try the default handler, if one was set. */
$default_command = $this->default_command;
- if (!($found_route || is_null($default_command)))
+ if (!$found_route && !is_null($default_command))
{
- if (!method_exists($this, sprintf('command_%s', $default_command)))
- throw new AnewtHTTPException(HTTP_STATUS_INTERNAL_SERVER_ERROR,
- 'Default command "%s" cannot be found.', $default_command);
-
- $command_name = $default_command;
+ $command = $default_command;
+ $command = $this->validate_command($command);
$found_route = true;
}
+
/* If we still don't have a command, we give up. Too bad... not found */
if (!$found_route)
throw new AnewtHTTPException(HTTP_STATUS_NOT_FOUND);
- /* Run the command. We know the method exists since that is already
- * checked in the add_route_*() methods. */
-
- $method = sprintf('command_%s', $command_name);
+
+ /* Check the command for validity. In most cases we already know the
+ * command exists since that is already checked in the add_route_*()
+ * methods or in the code above, except for lazily loaded commands, so
+ * we try to load them and check for validity afterwards. */
+
+ if (is_array($command) && is_string($command[0]))
+ {
+ $this->include_command_class($command[0]);
+ $command = $this->validate_command($command);
+ }
+
+
+ /* Finally... run the command and the pre and post command hooks. */
+
$this->pre_command($parameters);
- $this->$method($parameters);
+ call_user_func($command, $parameters);
$this->post_command($parameters);
}
=== modified file 'urldispatcher/urldispatcher.test.php'
--- urldispatcher/urldispatcher.test.php 2009-08-02 20:09:24 +0000
+++ urldispatcher/urldispatcher.test.php 2010-02-20 15:29:38 +0000
@@ -26,7 +26,9 @@
$this->add_route_url_parts('test', '/parts/:first/:second/');
$this->add_route_url_parts('test', array('parts', ':first', ':second', ':third'), array('third' => '#^three$#'));
- $this->add_route_url_parts('test', '/');
+ $this->add_route_url_parts(array($this, 'command_test'), '/');
+
+ $this->add_route_url_parts(array('TestDispatcherCommand', 'external_test'), '/external');
}
function command_test($parameters)
@@ -35,6 +37,15 @@
}
}
+class TestDispatcherCommand
+{
+ static public function external_test($parameters)
+ {
+ echo 'external command', NL;
+ print_r($parameters);
+ }
+}
+
$d = new TestDispatcher();
$d->dispatch();