← Back to team overview

anewt-developers team mailing list archive

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