Browse files

Add scope to `lithium\net\http\Router` and `lithium\net\http\Media`.

- Routes can be scoped using the following notation:
  Routed::scope(); // Returns the current scope
  Router::scope('name'); // Use a new scope
  Router::scope('name'), function(){/* executed inside the scope */});
  Router::attach('name'), array(), array()); //Attach a mount point to a scope
  Router::attached() // Returns all attachments
  Router::attached('name', array()); // Returns the attached mount point configuration
- The Media class can be scoped using the following notation: (i.e. usefull for CDN or others media location).
  Media::scope(); // Returns the current scope
  Media::scope('name'); // Use a new scope
  Media::scope('name'), function(){/* executed inside the scope */});
  Media::attach('name'), array()); // Attach a mount point to a scope
  Media::attached(); // Returns all attachements
  Media::attached('name'); // Returns the attached mount point configuration
- Modifiying the Media::webroot() signature (BC Break)
- The paths of assets paths now called 'paths' for consistency see `Media::_asset` (BC Break)
- Include #535 it's not a good practice to use `'http:method'` at route level but it may be better to not let it buggy.
  • Loading branch information...
1 parent 99c7aef commit 9963274477fda88bfc58710a626c6df824e85e46 @jails committed Feb 16, 2013
View
4 console/command/Route.php
@@ -84,8 +84,8 @@ public function run() {
*
* @return void
*/
- public function all() {
- $routes = Router::get();
+ public function all($scope = true) {
+ $routes = Router::get(null, true);
$columns = array(array('Template', 'Params'), array('--------', '------'));
foreach ($routes As $route) {
View
104 core/Configuration.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ *
+ * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+use lithium\core\Environment;
+
+/**
+ * The `Configuration` class allow to store `Environement` based configurations.
+ * @see lithium\core\Environment
+ */
+class Configuration extends \lithium\core\Object {
+
+ /**
+ * Can provide configurations based on the environment,
+ * i.e. `'development'`, `'production'` or `'test'`
+ *
+ * @var array of configurations, indexed by name.
+ */
+ public $_configurations = array();
+
+ /**
+ * A closure called by `_config()` which allows to automatically
+ * assign or auto-generate additional configuration data, once a configuration is first
+ * accessed. This allows configuration data to be lazy-loaded from adapters or other data
+ * sources.
+ *
+ * @param string $name Name of the configuration which is being accessed. This is the key
+ * name containing the specific set of configuration passed into `config()`.
+ * @param array $config Configuration assigned to `$name`. If this configuration
+ * is segregated by environment, then this will contain the configuration for
+ * the current environment.
+ * @return array Returns the final array of settings for the given named configuration.
+ */
+ public $initConfig = null;
+
+ /**
+ * Sets configurations for a particular adaptable implementation, or returns the current
+ * configuration settings.
+ *
+ * @param string $name Name of the scope.
+ * @param array $config Configuration to set.
+ */
+ public function set($name = null, $config = null) {
+ if ($config && is_array($config)) {
+ $this->_configurations[$name] = $config;
+ return;
+ }
+ if ($config === false) {
+ unset($this->_configurations[$name]);
+ }
+ }
+
+ /**
+ * Gets an array of settings for the given named configuration in the current
+ * environment.
+ *
+ * @see lithium\core\Environment
+ * @param string $name Name of the configuration.
+ * @return array Settings of the named configuration.
+ */
+ public function get($name = null) {
+ if ($name === null) {
+ $result = array();
+ $this->_configurations = array_filter($this->_configurations);
+
+ foreach ($this->_configurations as $key => $value) {
+ $result[$key] = $this->get($key);
+ }
+ return $result;
+ }
+
+ $settings = &$this->_configurations;
+
+ if (!isset($settings[$name])) {
+ return null;
+ }
+
+ if (isset($settings[$name][0])) {
+ return $settings[$name][0];
+ }
+ $env = Environment::get();
+
+ $config = isset($settings[$name][$env]) ? $settings[$name][$env] : $settings[$name];
+
+ $method = is_callable($this->initConfig) ? $this->initConfig : null;
+ $settings[$name][0] = $method ? $method($name, $config) : $config;
+ return $settings[$name][0];
+ }
+
+ /**
+ * Clears all configurations.
+ */
+ public function reset() {
+ $this->_configurations = array();
+ }
+}
+
+?>
View
212 net/http/Media.php
@@ -33,6 +33,22 @@
class Media extends \lithium\core\StaticObject {
/**
+ * Contain the configuration of scopes.
+ *
+ * @var array of scopes
+ */
+ protected static $_scopes = null;
+
+ /**
+ * Stores the name of the scope in use.
+ * If set to `false`, no scope is used.
+ *
+ * @see lithium\net\http\Media::scope()
+ * @var string
+ */
+ protected static $_scope = false;
+
+ /**
* Maps file extensions to content-types. Used to set response types and determine request
* types. Can be modified with `Media::type()`.
*
@@ -66,7 +82,9 @@ class Media extends \lithium\core\StaticObject {
*
* @var array
*/
- protected static $_classes = array();
+ protected static $_classes = array(
+ 'configuration' => 'lithium\core\Configuration'
+ );
/**
* Returns the list of registered media types. New types can be set with the `type()` method.
@@ -355,7 +373,7 @@ public static function match($request, array $config) {
* applicable.
* - `'filter'`: An array of key/value pairs representing simple string replacements to
* be done on a path once it is generated.
- * - `'path'`: An array of key/value pairs where the keys are `String::insert()`
+ * - `'paths'`: An array of key/value pairs where the keys are `String::insert()`
* compatible paths, and the values are array lists of keys to be inserted into the
* path string.
* @return array If `$type` is empty, an associative array of all registered types and all
@@ -364,7 +382,7 @@ public static function match($request, array $config) {
* are both non-empty, returns `null`.
*/
public static function assets($type = null, $options = array()) {
- $defaults = array('suffix' => null, 'filter' => null, 'path' => array());
+ $defaults = array('suffix' => null, 'filter' => null, 'paths' => array());
if (!$type) {
return static::_assets();
@@ -401,7 +419,7 @@ public static function assets($type = null, $options = array()) {
* `false`.
* - `'filter'`: An array of key/value pairs representing simple string replacements to
* be done on a path once it is generated.
- * - `'path'`: An array of paths to search for the asset in. The paths should use
+ * - `'paths'`: An array of paths to search for the asset in. The paths should use
* `String::insert()` formatting. See `Media::$_assets` for more.
* - `suffix`: The suffix to attach to the path, generally a file extension.
* - `'timestamp'`: Appends the last modified time of the file to the path if `true`.
@@ -419,16 +437,38 @@ public static function asset($path, $type, array $options = array()) {
'base' => null,
'timestamp' => false,
'filter' => null,
- 'path' => array(),
+ 'paths' => array(),
'suffix' => null,
'check' => false,
'library' => true
);
- if (!$base = static::_assets($type)) {
+ if (!$paths = static::_assets($type)) {
$type = 'generic';
- $base = static::_assets('generic');
+ $paths = static::_assets('generic');
}
- $options += ($base + $defaults);
+
+ $base = isset($options['base']) ? rtrim($options['base'], '/') : '';
+ $options += array('scope' => static::scope());
+ $name = $options['scope'];
+
+ if ($name && $config = static::attached($name)) {
+ $defaults = array_merge($defaults, $config);
+
+ if (preg_match('/^((?:[a-z0-9-]+:)?\/\/)([^\/]*)/i', $base, $match)) {
+ $options = array_merge($defaults, array(
+ 'base' => rtrim($base . '/' . $defaults['prefix'], '/')
+ ));
+ } else {
+ $host = '';
+ if ($defaults['absolute']) {
+ $host = $defaults['scheme'] . $defaults['host'];
+ }
+ $base = $host ? ($base ? '/' . ltrim($base, '/') : '') : $base;
+ $options['base'] = rtrim($host . $base . '/' . $defaults['prefix'], '/');
+ }
+ }
+
+ $options += ($paths + $defaults);
$params = compact('path', 'type', 'options');
return static::_filter(__FUNCTION__, $params, function($self, $params) {
@@ -441,7 +481,7 @@ public static function asset($path, $type, array $options = array()) {
return $path;
}
$config = Libraries::get($library);
- $paths = $options['path'];
+ $paths = $options['paths'];
$config['default'] ? end($paths) : reset($paths);
$options['library'] = basename($config['path']);
@@ -511,11 +551,16 @@ public static function filterAssetPath($asset, $path, array $config, array $opti
*
* @param string|boolean $library The name of the library for which to find the path, or `true`
* for the default library.
- * @return string Returns the physical path to the web assets directory for a library. For
- * example, the `/webroot` directory of the default library would be
- * `LITHIUM_APP_PATH . '/webroot'`.
+ * @param string|boolean $scope The name of the to use to find the path.
+ * @return string Returns the physical path to the web assets directory.
*/
- public static function webroot($library = true) {
+ public static function webroot($scope = null, $library = true) {
+ if ($scope) {
+ if ($config = static::attached($scope)) {
+ return $config['path'];
+ }
+ return null;
+ }
if (!$config = Libraries::get($library)) {
return null;
}
@@ -531,32 +576,35 @@ public static function webroot($library = true) {
* Returns the physical path to an asset in the `/webroot` directory of an application or
* plugin.
*
- * @param string $path The path to a web asset, relative to the root path for its type. For
- * example, for a JavaScript file in `/webroot/js/subpath/file.js`, the correct
- * value for `$path` would be `'subpath/file.js'`.
+ * @param string $paths The path to a web asset, relative to the root path for its type. For
+ * example, for a JavaScript file in `/webroot/js/subpath/file.js`, the correct
+ * value for `$path` would be `'subpath/file.js'`.
* @param string $type A valid asset type, i.e. `'js'`, `'cs'`, `'image'`, or another type
- * registered with `Media::assets()`, or `'generic'`.
+ * registered with `Media::assets()`, or `'generic'`.
* @param array $options The options used to calculate the path to the file.
* @return string Returns the physical filesystem path to an asset in the `/webroot` directory.
*/
public static function path($path, $type, array $options = array()) {
$defaults = array(
'base' => null,
- 'path' => array(),
+ 'paths' => array(),
'suffix' => null,
- 'library' => true
+ 'library' => true,
+ 'scope' => false
);
if (!$base = static::_assets($type)) {
$type = 'generic';
$base = static::_assets('generic');
}
$options += ($base + $defaults);
- $config = Libraries::get($options['library']);
- $root = static::webroot($options['library']);
- $paths = $options['path'];
+ $paths = $options['paths'];
- $config['default'] ? end($paths) : reset($paths);
- $options['library'] = basename($config['path']);
+ if ($options['scope']) {
+ $root = static::webroot($options['scope']);
+ } else {
+ $root = static::webroot(null, $options['library']);
+ Libraries::get(true, 'name') === $options['library'] ? end($paths) : reset($paths);
+ }
if ($qOffset = strpos($path, '?')) {
$path = substr($path, 0, $qOffset);
@@ -723,12 +771,13 @@ public static function decode($type, $data, array $options = array()) {
/**
* Resets the `Media` class to its default state. Mainly used for ensuring a consistent state
* during testing.
- *
- * @return void
*/
public static function reset() {
- foreach (get_class_vars(__CLASS__) as $name => $value) {
- static::${$name} = array();
+ static::$_handlers = array();
+ static::$_types = array();
+ static::$_scope = false;
+ if (isset(static::$_scopes)) {
+ static::$_scopes->reset();
}
}
@@ -871,19 +920,19 @@ public static function handlers($type = null) {
*/
protected static function _assets($type = null) {
$assets = static::$_assets + array(
- 'js' => array('suffix' => '.js', 'filter' => null, 'path' => array(
+ 'js' => array('suffix' => '.js', 'filter' => null, 'paths' => array(
'{:base}/{:library}/js/{:path}' => array('base', 'library', 'path'),
'{:base}/js/{:path}' => array('base', 'path')
)),
- 'css' => array('suffix' => '.css', 'filter' => null, 'path' => array(
+ 'css' => array('suffix' => '.css', 'filter' => null, 'paths' => array(
'{:base}/{:library}/css/{:path}' => array('base', 'library', 'path'),
'{:base}/css/{:path}' => array('base', 'path')
)),
- 'image' => array('suffix' => null, 'filter' => null, 'path' => array(
+ 'image' => array('suffix' => null, 'filter' => null, 'paths' => array(
'{:base}/{:library}/img/{:path}' => array('base', 'library', 'path'),
'{:base}/img/{:path}' => array('base', 'path')
)),
- 'generic' => array('suffix' => null, 'filter' => null, 'path' => array(
+ 'generic' => array('suffix' => null, 'filter' => null, 'paths' => array(
'{:base}/{:library}/{:path}' => array('base', 'library', 'path'),
'{:base}/{:path}' => array('base', 'path')
))
@@ -893,6 +942,105 @@ protected static function _assets($type = null) {
}
return $assets;
}
+
+ /**
+ * Scope getter/setter.
+ *
+ * Special use case: If `$closure` is not null executing the closure inside
+ * the specified scope.
+ *
+ * @param string $name Name of the scope to use.
+ * @param array $closure A closure to execute inside the scope.
+ * @return mixed Returns the previous scope if if `$name` is not null and `$closure` is null,
+ * returns the default used scope if `$name` is null, otherwise returns `null`.
+ */
+ public static function scope($name = null, Closure $closure = null) {
+ if ($name === null) {
+ return static::$_scope;
+ }
+
+ if ($closure === null) {
+ $former = static::$_scope;
+ static::$_scope = $name;
+ return $former;
+ }
+
+ $former = static::$_scope;
+ static::$_scope = $name;
+ call_user_func($closure);
+ static::$_scope = $former;
+ }
+
+ /**
+ * Attach a scope to a mount point.
+ *
+ * Example:
+ * {{{
+ * Media::attach('app', array(
+ * 'path' => '/var/www/website/app/webroot/extradir',
+ * 'prefix' => 'extradir'
+ * ));
+ * }}}
+ *
+ * {{{
+ * Media::attach('cdn', array(
+ * 'absolute' => true,
+ * 'path' => null,
+ * 'host' => 'http://my.cdn.com',
+ * 'prefix' => 'project1/assets'
+ * ));
+ * }}}
+ *
+ */
+ public static function attach($name, $config = null) {
+ if (!isset(static::$_scopes)) {
+ static::_initScopes();
+ }
+ if ($name === false) {
+ $name = '__defaultScope__';
+ }
+ if (is_array($config) || $config === false) {
+ static::$_scopes->set($name, $config);
+ }
+ }
+
+ /**
+ * Returns an attached mount point configuration.
+ */
+ public static function attached($name = null) {
+ if (!isset(static::$_scopes)) {
+ static::_initScopes();
+ }
+ if ($name === false) {
+ $name = '__defaultScope__';
+ }
+ return static::$_scopes->get($name);
+ }
+
+ /**
+ * Initialize `static::$_scopes` with a `lithium\core\Configuration` instance.
+ */
+ protected static function _initScopes() {
+ $configuration = static::$_classes['configuration'];
+ static::$_scopes = new $configuration();
+ $self = get_called_class();
+ static::$_scopes->initConfig = function($name, $config) use ($self) {
+ $defaults = array(
+ 'absolute' => false,
+ 'host' => 'localhost',
+ 'scheme' => 'http://',
+ 'prefix' => '',
+ 'path' => null,
+ 'timestamp' => false,
+ 'filter' => null,
+ 'suffix' => null,
+ 'check' => false
+ );
+ $config += $defaults;
+ $config['prefix'] = trim($config['prefix'], '/');
+ return $config;
+ };
+ }
}
?>
View
29 net/http/Route.php
@@ -181,6 +181,7 @@ public function __construct(array $config = array()) {
'persist' => array(),
'handler' => null,
'continue' => false,
+ 'modifiers' => array(),
'formatters' => array(),
'unicode' => true
);
@@ -229,14 +230,17 @@ public function parse($request, array $options = array()) {
return false;
}
}
- if (isset($match['args'])) {
- $match['args'] = explode('/', $match['args']);
+ foreach ($this->_config['modifiers'] as $key => $modifier) {
+ if (isset($match[$key])) {
+ $match[$key] = $modifier($match[$key]);
+ }
}
- $result = array_filter(array_intersect_key($match, $this->_keys), function($val) {
- return !is_scalar($val) || strlen($val);
- });
- if (isset($this->_keys['args'])) {
- $result += array('args' => array());
+
+ $result = array_intersect_key($match + array('args' => array()), $this->_keys);
+ foreach ($result as $key => $value) {
+ if ($value === '') {
+ unset($result[$key]);
+ }
}
$result += $this->_params + $this->_defaults;
$request->params = $result + (array) $request->params;
@@ -258,7 +262,7 @@ public function parse($request, array $options = array()) {
* @return mixed
*/
public function match(array $options = array(), $context = null) {
- $defaults = array('action' => 'index');
+ $defaults = array('action' => 'index', 'http:method' => 'GET');
$query = null;
if (!$this->_config['continue']) {
@@ -270,6 +274,12 @@ public function match(array $options = array(), $context = null) {
unset($options['?']);
}
}
+ if (isset($this->_meta['http:method']) &&
+ $options['http:method'] != $this->_meta['http:method']
+ ) {
+ return false;
+ }
+ unset($options['http:method']);
if (!$options = $this->_matchKeys($options)) {
return false;
@@ -400,7 +410,6 @@ public function export() {
* @return void
*/
public function compile() {
- $this->_match = $this->_params;
foreach ($this->_params as $key => $value) {
if (!strpos($key, ':')) {
@@ -410,6 +419,8 @@ public function compile() {
$this->_meta[$key] = $value;
}
+ $this->_match = $this->_params;
+
if ($this->_template === '/' || $this->_template === '') {
$this->_pattern = '@^/*$@';
return;
View
549 net/http/Router.php
@@ -10,6 +10,7 @@
use lithium\util\Inflector;
use lithium\net\http\RoutingException;
+use Closure;
/**
* The two primary responsibilities of the `Router` class are to generate URLs from parameter lists,
@@ -45,6 +46,23 @@
class Router extends \lithium\core\StaticObject {
/**
+ * Contain the configuration of scopes.
+ *
+ * @var array of scopes
+ */
+ protected static $_scopes = null;
+
+ /**
+ * Stores the name of the scope to use for building urls.
+ * If is set to `true`, the scope of the user's request will be used.
+ * saved
+ *
+ * @see lithium\net\http\Router::scope()
+ * @var string
+ */
+ protected static $_scope = false;
+
+ /**
* An array of loaded `Route` objects used to match Request objects against.
*
* @see lithium\net\http\Route
@@ -53,6 +71,14 @@ class Router extends \lithium\core\StaticObject {
protected static $_configurations = array();
/**
+ * Array of closures used to format route parameters when parsing URLs.
+ *
+ * @see lithium\net\http\Router::modifiers()
+ * @var array
+ */
+ protected static $_modifiers = array();
+
+ /**
* An array of named closures matching up to corresponding route parameter values. Used to
* format those values.
*
@@ -67,7 +93,8 @@ class Router extends \lithium\core\StaticObject {
* @var array
*/
protected static $_classes = array(
- 'route' => 'lithium\net\http\Route'
+ 'route' => 'lithium\net\http\Route',
+ 'configuration' => 'lithium\core\Configuration'
);
/**
@@ -110,8 +137,13 @@ public static function config($config = array()) {
* @return array Array of routes
*/
public static function connect($template, $params = array(), $options = array()) {
+ if (is_array($options) && isset($options['scope'])) {
+ $name = $options['scope'];
+ } else {
+ $name = static::$_scope;
+ }
if (is_object($template)) {
- return (static::$_configurations[] = $template);
+ return (static::$_configurations[$name][] = $template);
}
if (is_string($params)) {
$params = static::_parseString($params, false);
@@ -121,15 +153,15 @@ public static function connect($template, $params = array(), $options = array())
$params = $tmp + $params;
}
$params = static::_parseController($params);
-
if (is_callable($options)) {
$options = array('handler' => $options);
}
$config = compact('template', 'params') + $options + array(
'formatters' => static::formatters(),
+ 'modifiers' => static::modifiers(),
'unicode' => static::$_unicode
);
- return (static::$_configurations[] = static::_instance('route', $config));
+ return (static::$_configurations[$name][] = static::_instance('route', $config));
}
/**
@@ -148,6 +180,51 @@ public static function process($request) {
/**
* Used to get or set an array of named formatter closures, which are used to format route
+ * parameters when parsing URLs. For example, the following would match a `posts/index` url
+ * to a `PostsController::indexAction()` method.
+ *
+ * {{{
+ * use litthium\util\Inflector;
+ *
+ * Router::modifiers(array(
+ * 'controller' => function($value) {
+ * return Inflector::camelize($value);
+ * },
+ * 'action' => function($value) {
+ * return Inflector::camelize($value) . 'Action';
+ * }
+ * ));
+ * }}}
+ *
+ * _Note_: Because modifiers are copied to `Route` objects on an individual basis, make sure
+ * you append your custom modifiers _before_ connecting new routes.
+ *
+ * @param array $modifiers An array of named formatter closures to append to (or overwrite) the
+ * existing list.
+ * @return array Returns the formatters array.
+ */
+ public static function modifiers(array $modifiers = array()) {
+ if (!static::$_modifiers) {
+ static::$_modifiers = array(
+ 'args' => function($value) {
+ return explode('/', $value);
+ },
+ 'controller' => function($value) {
+ return Inflector::camelize($value);
+ },
+ 'action' => function($value) {
+ return Inflector::camelize($value, false);
+ }
+ );
+ }
+ if ($modifiers) {
+ static::$_modifiers = array_filter($modifiers + static::$_modifiers);
+ }
+ return static::$_modifiers;
+ }
+
+ /**
+ * Used to get or set an array of named formatter closures, which are used to format route
* parameters when generating URLs. For example, for controller/action parameters to be dashed
* instead of underscored or camelBacked, you could do the following:
*
@@ -174,6 +251,10 @@ public static function formatters(array $formatters = array()) {
return is_array($value) ? join('/', $value) : $value;
},
'controller' => function($value) {
+ if (strpos($value, '\\')) {
+ $value = explode('\\', $value);
+ $value = end($value);
+ }
return Inflector::underscore($value);
}
);
@@ -188,6 +269,9 @@ public static function formatters(array $formatters = array()) {
* Accepts an instance of `lithium\action\Request` (or a subclass) and matches it against each
* route, in the order that the routes are connected.
*
+ * If a route match the request, `lithium\net\http\Router::_scope` will be updated according
+ * the scope membership of the route
+
* @see lithium\action\Request
* @see lithium\net\http\Router::connect()
* @param object $request A request object containing URL and environment data.
@@ -196,23 +280,46 @@ public static function formatters(array $formatters = array()) {
* typically include `'controller'` and `'action'` keys.
*/
public static function parse($request) {
- $orig = $request->params;
- $url = $request->url;
-
- foreach (static::$_configurations as $route) {
- if (!$match = $route->parse($request, compact('url'))) {
- continue;
- }
- $request = $match;
+ foreach (static::$_configurations as $name => $value) {
+ $orig = $request->params;
+ $url = $request->url;
+ $name = is_int($name) ? false : $name;
+ if (((!$config = static::attached($name)) ||
+ static::_matchScope($name, $request)) &&
+ isset(static::$_configurations[$name])) {
+ if ($config['prefix']) {
+ $url = preg_replace('@^/' . trim($config['prefix'], '/') . '@', '/', $url);
+ }
- if ($route->canContinue() && isset($request->params['args'])) {
- $url = '/' . join('/', $request->params['args']);
- unset($request->params['args']);
- continue;
+ foreach (static::$_configurations[$name] as $route) {
+ if (!$match = $route->parse($request, compact('url'))) {
+ continue;
+ }
+ $request = $match;
+ if ($route->canContinue() && isset($request->params['args'])) {
+ $url = '/' . join('/', $request->params['args']);
+ unset($request->params['args']);
+ continue;
+ }
+
+ if (isset($request->params['controller'])) {
+ $controller = $request->params['controller'];
+ if (isset($config['namespace']) && strpos($controller, '\\') === false) {
+ $controller = $config['namespace'] . '\\' . $controller;
+ $request->params['controller'] = $controller . 'Controller';
+ }
+ if (isset($config['library'])) {
+ $request->params['library'] = $config['library'];
+ }
+ }
+
+ static::attach($name, null, isset($request->params) ? $request->params : array());
+ static::scope($name);
+ return $request;
+ }
}
- return $request;
+ $request->params = $orig;
}
- $request->params = $orig;
}
/**
@@ -273,36 +380,76 @@ public static function parse($request) {
* prefixed with the base URL of the application.
*/
public static function match($url = array(), $context = null, array $options = array()) {
+ $defaults = array(
+ 'scheme' => null,
+ 'host' => null,
+ 'absolute' => false,
+ 'base' => $context ? rtrim($context->env('base'), '/') : ''
+ );
+
+ if ($context) {
+ $defaults['host'] = $context->host;
+ $defaults['scheme'] = $context->scheme . ($context->scheme ? '://' : '//');
+ }
+
+ $options += array('scope' => static::scope());
+ $vars = array();
+ $scope = $options['scope'];
+ if (is_array($scope)) {
+ list($tmp, $vars) = each($scope);
+ if (!is_array($vars)) {
+ $vars = $scope;
+ $scope = static::scope();
+ } else {
+ $scope = $tmp;
+ }
+ }
+ if ($scope && $config = static::attached($scope, $vars)) {
+ $config['host'] = $config['host'] ? : $defaults['host'];
+ if ($config['scheme'] === false) {
+ $config['scheme'] = '//';
+ } else {
+ $config['scheme'] .= ( $config['scheme'] ? '://' : $defaults['scheme']);
+ }
+ $config['scheme'] = $config['scheme'] ? : 'http://';
+ $prefix = $config['prefix'] ? '/' . $config['prefix'] : '';
+ $config['base'] = '/' . ltrim($defaults['base'] . $prefix, '/');
+ $defaults = $config + $defaults;
+ }
+
+ $options += $defaults;
if (is_string($url = static::_prepareParams($url, $context, $options))) {
return $url;
}
- $defaults = array('action' => 'index');
- $url += $defaults;
+
+ $base = $options['base'];
+ $url += array('action' => 'index');
$stack = array();
- $base = isset($context) ? $context->env('base') : '';
$suffix = isset($url['#']) ? "#{$url['#']}" : null;
unset($url['#']);
- foreach (static::$_configurations as $route) {
- if (!$match = $route->match($url, $context)) {
- continue;
- }
- if ($route->canContinue()) {
- $stack[] = $match;
- $export = $route->export();
- $keys = $export['match'] + $export['keys'] + $export['defaults'];
- unset($keys['args']);
- $url = array_diff_key($url, $keys);
- continue;
- }
- if ($stack) {
- $stack[] = $match;
- $match = static::_compileStack($stack);
+ if (isset(static::$_configurations[$scope])) {
+ foreach (static::$_configurations[$scope] as $route) {
+ if (!$match = $route->match($url, $context)) {
+ continue;
+ }
+ if ($route->canContinue()) {
+ $stack[] = $match;
+ $export = $route->export();
+ $keys = $export['match'] + $export['keys'] + $export['defaults'];
+ unset($keys['args']);
+ $url = array_diff_key($url, $keys);
+ continue;
+ }
+ if ($stack) {
+ $stack[] = $match;
+ $match = static::_compileStack($stack);
+ }
+ $path = rtrim("{$base}{$match}{$suffix}", '/') ? : '/';
+ $path = ($options) ? static::_prefix($path, $context, $options) : $path;
+ return $path ? : '/';
}
- $path = rtrim("{$base}{$match}{$suffix}", '/') ?: '/';
- $path = ($options) ? static::_prefix($path, $context, $options) : $path;
- return $path ?: '/';
}
$url = static::_formatError($url);
throw new RoutingException("No parameter match found for URL `{$url}`.");
@@ -351,11 +498,14 @@ protected static function _prepareParams($url, $context, array $options) {
return $url;
}
}
- if (is_string($url = static::_parseString($url, $context))) {
+ if (is_string($url = static::_parseString($url, $context, $options))) {
return static::_prefix($url, $context, $options);
}
}
- if (isset($url[0]) && is_array($params = static::_parseString($url[0], $context))) {
+ if (
+ isset($url[0])
+ && is_array($params = static::_parseString($url[0], $context, $options))
+ ) {
unset($url[0]);
$url = $params + $url;
}
@@ -374,13 +524,7 @@ protected static function _prepareParams($url, $context, array $options) {
*/
protected static function _prefix($path, $context = null, array $options = array()) {
$defaults = array('scheme' => null, 'host' => null, 'absolute' => false);
-
- if ($context) {
- $defaults['host'] = $context->env('HTTP_HOST');
- $defaults['scheme'] = $context->env('HTTPS') ? 'https://' : 'http://';
- }
$options += $defaults;
-
return ($options['absolute']) ? "{$options['scheme']}{$options['host']}{$path}" : $path;
}
@@ -397,7 +541,7 @@ protected static function _prefix($path, $context = null, array $options = array
* @param array $url The parameters that define the URL to be matched.
* @param object $context Typically an instance of `lithium\action\Request`, which contains a
* `$persist` property, which is an array of keys to be persisted in URLs between
- * requests.
+ * requests.
* @return array Returns the modified URL array.
*/
protected static function _persist($url, $context) {
@@ -417,23 +561,47 @@ protected static function _persist($url, $context) {
/**
* Returns a route from the loaded configurations, by name.
*
- * @param string $route Name of the route to request.
- * @return lithium\net\http\Route
+ * @param integer $route Route number.
+ * @param string $scope Name of the scope to get routes from. If `null`
+ * `lithium\net\http\Router::$_scope` will be used
+ *
+ * @return mixed if $route is an integer, return the `lithium\net\http\Route`
+ * instance or `null` if not found.
+ * if `$route === null` and `$scope === null`, will return all the routes
+ * for all scopes.
+ * if `$route === null` and `$scope === true`, return the array of all
+ * `lithium\net\http\Route` instances for the default scope.
+ * if `$route === null` and `$scope !== null`, will return all the routes
+ * for for the specified scopes.
*/
- public static function get($route = null) {
- if ($route === null) {
+ public static function get($route = null, $scope = null) {
+ if ($route === null && $scope === null) {
return static::$_configurations;
}
- return isset(static::$_configurations[$route]) ? static::$_configurations[$route] : null;
+
+ if ($scope === true) {
+ $scope = static::$_scope;
+ }
+
+ if ($route === null && $scope !== null) {
+ if (isset(static::$_configurations[$scope])) {
+ return static::$_configurations[$scope];
+ }
+ return array();
+ }
+
+ return isset(static::$_configurations[$scope][$route]) ? static::$_configurations[$scope][$route] : null;
}
/**
* Resets the `Router` to its default state, unloading all routes.
- *
- * @return void
*/
public static function reset() {
static::$_configurations = array();
+ static::$_scope = false;
+ if (isset(static::$_scopes)) {
+ static::$_scopes->reset();
+ }
}
/**
@@ -443,15 +611,278 @@ public static function reset() {
* @param boolean $context
* @return array
*/
- protected static function _parseString($path, $context) {
- if (!preg_match('/^[A-Za-z0-9_]+::[A-Za-z0-9_]+$/', $path)) {
- $base = $context ? $context->env('base') : '';
+ protected static function _parseString($path, $context, array $options = array()) {
+ if (!preg_match('/^[A-Za-z0-9_\\\\]+::[A-Za-z0-9_]+$/', $path)) {
+ $base = rtrim($options['base'], '/');
+ if ((!$path || $path[0] != '/') && $context && isset($context->controller)) {
+ $formatters = static::formatters();
+ $base .= '/' . $formatters['controller']($context->controller);
+ }
$path = trim($path, '/');
- return $context !== false ? "{$base}/{$path}" : null;
+ return "{$base}/{$path}";
}
list($controller, $action) = explode('::', $path, 2);
return compact('controller', 'action');
}
+
+ /**
+ * Scope getter/setter.
+ *
+ * Special use case: If `$closure` is not null executing the closure inside
+ * the specified scope.
+ *
+ * @param string $name Name of the scope to use.
+ * @param array $closure A closure to execute inside the scope.
+ * @return mixed Returns the previous scope if if `$name` is not null and `$closure` is null,
+ * returns the default used scope if `$name` is null, otherwise returns `null`.
+ */
+ public static function scope($name = null, Closure $closure = null) {
+ if ($name === null) {
+ return static::$_scope;
+ }
+
+ if ($closure === null) {
+ $former = static::$_scope;
+ static::$_scope = $name;
+ return $former;
+ }
+
+ $former = static::$_scope;
+ static::$_scope = $name;
+ call_user_func($closure);
+ static::$_scope = $former;
+ }
+
+ /**
+ * Attach a scope to a mount point.
+ *
+ * Example 1:
+ * {{{
+ * Router::attach('app', array(
+ * 'absolute' => true,
+ * 'host' => 'localhost',
+ * 'scheme' => 'http://',
+ * 'prefix' => 'web/tests'
+ * ));
+ * }}}
+ *
+ * Example 2:
+ * {{{
+ * Router::attach('app', array(
+ * 'absolute' => true,
+ * 'host' => '{:subdomain:[a-z]+}.{:hostname}.{:tld}',
+ * 'scheme' => '{:scheme:https://}',
+ * 'prefix' => ''
+ * ));
+ * }}}
+ *
+ * Attach the variable to populate for the app scope.
+ * {{{
+ * Router::attach('app', null, array(
+ * 'subdomain' => 'www',
+ * 'hostname' => 'lithify',
+ * 'tld' => 'me'
+ * ));
+ * }}}
+ *
+ * @param string Name of the scope.
+ * @param mixed Settings of the mount point or `null` for setting only variables to populate.
+ * @param array Variables to populate for the scope.
+ */
+ public static function attach($name, $config = null, array $vars = array()) {
+ if (!isset(static::$_scopes)) {
+ static::_initScopes();
+ }
+
+ if ($config === null) {
+ if ($vars && ($config = static::$_scopes->get($name))) {
+ $config['values'] = $vars;
+ static::$_scopes->set($name, $config);
+ }
+ return;
+ }
+
+ if ($name === false) {
+ $name = '__defaultScope__';
+ }
+ if (is_array($config) || $config === false) {
+ static::$_scopes->set($name, $config);
+ }
+ }
+
+ /**
+ * Returns an attached mount point configuration.
+ *
+ * Example:
+ * {{{
+ * Router::attach('app', array(
+ * 'absolute' => true,
+ * 'host' => '{:subdomain:[a-z]+}.{:hostname}.{:tld}',
+ * 'scheme' => '{:scheme:https://}',
+ * 'prefix' => ''
+ * ));
+ * }}}
+ *
+ * {{{
+ * $result = Router::attached('app', array(
+ * 'subdomain' => 'app',
+ * 'hostname' => 'blog',
+ * 'tld' => 'co.uk'
+ * ));
+ * }}}
+ *
+ * Will give the following array in `$result`:
+ *
+ * array(
+ * 'absolute' => true,
+ * 'host' => 'blog.mysite.co.uk',
+ * 'scheme' => 'http://',
+ * 'prefix' => ''
+ * ));
+ *
+ * @param string Name of the scope.
+ * @param array Optionnal variables which override the default setted variables with
+ * `lithium\net\http\Router::attach()`for population step.
+ * @return mixed The settings array of the scope or an array of settings array
+ * if `$name === null`.
+ */
+ public static function attached($name = null, array $vars = array()) {
+ if (!isset(static::$_scopes)) {
+ static::_initScopes();
+ }
+
+ if ($name === false) {
+ $name = '__defaultScope__';
+ }
+
+ $config = static::$_scopes->get($name);
+ if (!$config || $name === null) {
+ return $config;
+ }
+ $vars += $config['values'];
+ $match = '@\{:([^:}]+):?((?:[^{]+(?:\{[0-9,]+\})?)*?)\}@S';
+ $fields = array('scheme', 'host');
+ foreach ($fields as $field) {
+ if (preg_match_all($match, $config[$field], $m)) {
+ $tokens = $m[0];
+ $names = $m[1];
+ $regexs = $m[2];
+ foreach ($names as $i => $name) {
+ if (isset($vars[$name])) {
+ if (($regex = $regexs[$i]) && !preg_match("@^{$regex}\$@", $vars[$name])) {
+ continue;
+ }
+ $config[$field] = str_replace($tokens[$i], $vars[$name], $config[$field]);
+ }
+ }
+ }
+ }
+ return $config;
+ }
+
+ /**
+ * Initialize `static::$_scopes` with a `lithium\core\Configuration` instance.
+ */
+ protected static function _initScopes() {
+ $configuration = static::$_classes['configuration'];
+ static::$_scopes = new $configuration();
+ $self = get_called_class();
+ static::$_scopes->initConfig = function($name, $config) use ($self) {
+ $defaults = array(
+ 'absolute' => false,
+ 'host' => null,
+ 'scheme' => null,
+ 'prefix' => '',
+ 'pattern' => '',
+ 'library' => $name,
+ 'values' => array()
+ );
+
+ $config += $defaults;
+
+ if (!$config['pattern']) {
+ $config = $self::invokeMethod('_compileScope', array($config));
+ }
+ return $config;
+ };
+ }
+
+ /**
+ * Compiles the scope into regular expression patterns for matching against request URLs
+ *
+ * @param array $config Array of settings.
+ * @return array Returns the complied settings.
+ */
+ protected static function _compileScope(array $config) {
+ $defaults = array(
+ 'absolute' => false,
+ 'host' => null,
+ 'scheme' => null,
+ 'prefix' => '',
+ 'pattern' => '',
+ 'params' => array()
+ );
+
+ $config += $defaults;
+
+ $config['prefix'] = trim($config['prefix'], '/');
+ $prefix = '/' . ($config['prefix'] ? $config['prefix'] . '/' : '');
+
+ if (!$config['absolute']) {
+ $config['pattern'] = "@^{$prefix}@";
+ } else {
+ $fields = array('scheme', 'host');
+ foreach ($fields as $field) {
+ $dots = '/(?!\{[^\}]*)\.(?![^\{]*\})/';
+ $pattern[$field] = preg_replace($dots, '\.', $config[$field]);
+ $match = '@\{:([^:}]+):?((?:[^{]+(?:\{[0-9,]+\})?)*?)\}@S';
+ if (preg_match_all($match, $pattern[$field], $m)) {
+ $tokens = $m[0];
+ $names = $m[1];
+ $regexs = $m[2];
+ foreach ($names as $i => $name) {
+ $regex = $regexs[$i] ? : '[^/]+?';
+ $pattern[$field] = str_replace(
+ $tokens[$i],
+ "(?P<{$name}>{$regex})",
+ $pattern[$field]
+ );
+ $config['params'][] = $name;
+ }
+ }
+ }
+ $pattern['host'] = $pattern['host'] ? : 'localhost';
+ $pattern['scheme'] = $pattern['scheme'] . ($pattern['scheme'] ? '://' : '(.*?)//');
+ $config['pattern'] = "@^{$pattern['scheme']}{$pattern['host']}{$prefix}@";
+ }
+ return $config;
+ }
+
+ /**
+ * Check if a scope match a request
+ *
+ * @param string $name Name of an url scope
+ * @param string $request A `lithium\action\Request` instance to match on
+ * @return boolean
+ */
+ protected static function _matchScope($name, $request) {
+ $scheme = $request->scheme . ($request->scheme ? '://' : '//');
+ $host = $request->host;
+ $url = '/' . trim($request->url, '/') . '/';
+
+ if (($config = static::attached($name)) && $config['absolute']) {
+ preg_match($config['pattern'], $scheme . $host . $url, $match);
+ } else {
+ preg_match($config['pattern'], $url, $match);
+ }
+
+ if ($match) {
+ $result = array_intersect_key($match, array_flip($config['params']));
+ $request->params += $result;
+ return $result ? : true;
+ }
+ return false;
+ }
}
?>
View
5 template/Helper.php
@@ -146,6 +146,11 @@ protected function _options(array $defaults, array $scope) {
protected function _render($method, $string, $params, array $options = array()) {
$strings = $this->_strings;
+ if (isset($params['options']['scope'])) {
+ $options['scope'] = $params['options']['scope'];
+ unset($params['options']['scope']);
+ }
+
if ($this->_context) {
foreach ($params as $key => $value) {
$handler = isset($options['handlers'][$key]) ? $options['handlers'][$key] : $key;
View
13 tests/cases/action/DispatcherTest.php
@@ -26,9 +26,12 @@ public function setUp() {
public function tearDown() {
Router::reset();
-
- foreach ($this->_routes as $route) {
- Router::connect($route);
+ foreach ($this->_routes as $scope => $routes) {
+ Router::scope($scope, function() use ($routes) {
+ foreach ($routes as $route) {
+ Router::connect($route);
+ }
+ });
}
}
@@ -201,6 +204,10 @@ public function testAutoHandler() {
$this->assertEqual(array('Location: /redirect'), $result->headers());
}
+ public function methods() {
+ return array('testPluginControllerLookupFail');
+ }
+
public static function process($request) {
if ($request->url === '/auto') {
return new Response(array('location' => '/redirect'));
View
7 tests/cases/console/command/RouteTest.php
@@ -98,17 +98,16 @@ public function testEnvironment() {
* routes are connected to the router.
*/
public function testRouteLoading() {
- $this->assertEmpty(Router::get());
-
+ $this->assertEmpty(Router::get(null, true));
$command = new Route(array('routes' => $this->_config['routes']));
- $this->assertCount(4, Router::get());
+ $this->assertCount(4, Router::get(null, true));
Router::reset();
$request = new Request();
$request->params['env'] = 'production';
$command = new Route(compact('request') + array('routes' => $this->_config['routes']));
- $this->assertCount(2, Router::get());
+ $this->assertEqual(2, count(Router::get(null, true)));
}
/**
View
313 tests/cases/net/http/MediaTest.php
@@ -102,20 +102,20 @@ public function testAssetTypeHandling() {
$result = Media::assets('css');
$expected = '.css';
$this->assertEqual($expected, $result['suffix']);
- $this->assertTrue(isset($result['path']['{:base}/{:library}/css/{:path}']));
+ $this->assertTrue(isset($result['paths']['{:base}/{:library}/css/{:path}']));
$result = Media::assets('my');
$this->assertNull($result);
- $result = Media::assets('my', array('suffix' => '.my', 'path' => array(
+ $result = Media::assets('my', array('suffix' => '.my', 'paths' => array(
'{:base}/my/{:path}' => array('base', 'path')
)));
$this->assertNull($result);
$result = Media::assets('my');
$expected = '.my';
$this->assertEqual($expected, $result['suffix']);
- $this->assertTrue(isset($result['path']['{:base}/my/{:path}']));
+ $this->assertTrue(isset($result['paths']['{:base}/my/{:path}']));
$this->assertNull($result['filter']);
Media::assets('my', array('filter' => array('/my/' => '/your/')));
@@ -229,7 +229,7 @@ public function testAssetPathGeneration() {
}
public function testCustomAssetPathGeneration() {
- Media::assets('my', array('suffix' => '.my', 'path' => array(
+ Media::assets('my', array('suffix' => '.my', 'paths' => array(
'{:base}/my/{:path}' => array('base', 'path')
)));
@@ -542,19 +542,19 @@ public function testCustomWebroot() {
Libraries::add('defaultStyleApp', array('path' => LITHIUM_APP_PATH, 'bootstrap' => false));
$this->assertEqual(
realpath(LITHIUM_APP_PATH . '/webroot'),
- realpath(Media::webroot('defaultStyleApp'))
+ realpath(Media::webroot(null, 'defaultStyleApp'))
);
Libraries::add('customWebRootApp', array(
'path' => LITHIUM_APP_PATH,
'webroot' => LITHIUM_APP_PATH,
'bootstrap' => false
));
- $this->assertEqual(LITHIUM_APP_PATH, Media::webroot('customWebRootApp'));
+ $this->assertEqual(LITHIUM_APP_PATH, Media::webroot(null, 'customWebRootApp'));
Libraries::remove('defaultStyleApp');
Libraries::remove('customWebRootApp');
- $this->assertNull(Media::webroot('defaultStyleApp'));
+ $this->assertNull(Media::webroot(null, 'defaultStyleApp'));
}
/**
@@ -618,7 +618,7 @@ public function testGetLibraryWebroot() {
$this->assertNull(Media::webroot('foobar'));
Libraries::add('foobar', array('path' => __DIR__, 'webroot' => __DIR__));
- $this->assertEqual(__DIR__, Media::webroot('foobar'));
+ $this->assertEqual(__DIR__, Media::webroot(null, 'foobar'));
Libraries::remove('foobar');
$resources = Libraries::get(true, 'resources');
@@ -630,7 +630,7 @@ public function testGetLibraryWebroot() {
}
Libraries::add('media_test', array('path' => "{$resources}/media_test"));
- $this->assertFileExists(Media::webroot('media_test'));
+ $this->assertTrue(is_dir(Media::webroot(null, 'media_test')));
Libraries::remove('media_test');
rmdir($webroot);
}
@@ -713,6 +713,301 @@ public function testEmptyAssetPaths() {
$this->assertEqual('/js/.js', Media::asset('', 'js'));
$this->assertEqual('/', Media::asset('', 'generic'));
}
+
+ public function testLocation() {
+ $webroot = Libraries::get(true, 'resources') . '/tmp/tests/webroot';
+ mkdir($webroot);
+ $webroot = realpath($webroot);
+ $this->assertNotEmpty($webroot);
+ Media::attach('tests', array(
+ 'absolute' => true,
+ 'host' => 'www.hostname.com',
+ 'scheme' => 'http://',
+ 'prefix' => '/web/assets/tests',
+ 'path' => $webroot
+ ));
+ Media::attach('app', array(
+ 'absolute' => false,
+ 'prefix' => '/web/assets/app',
+ 'path' => $webroot
+ ));
+
+ $expected = array(
+ 'absolute' => false,
+ 'host' => 'localhost',
+ 'scheme' => 'http://',
+ 'prefix' => 'web/assets/app',
+ 'path' => $webroot,
+ 'timestamp' => false,
+ 'filter' => null,
+ 'suffix' => null,
+ 'check' => false
+ );
+ $result = Media::attached('app');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(
+ 'absolute' => true,
+ 'host' => 'www.hostname.com',
+ 'scheme' => 'http://',
+ 'prefix' => 'web/assets/tests',
+ 'path' => $webroot,
+ 'timestamp' => false,
+ 'filter' => null,
+ 'suffix' => null,
+ 'check' => false
+ );
+ $result = Media::attached('tests');
+ $this->assertEqual($expected, $result);
+ $this->_cleanUp();
+ }
+
+ public function testAssetWithAbsoluteLocation() {
+ Media::attach('appcdn', array(
+ 'absolute' => true,
+ 'scheme' => 'http://',
+ 'host' => 'my.cdn.com',
+ 'prefix' => '/assets',
+ 'path' => null
+ ));
+
+ $result = Media::asset('style','css');
+ $expected = '/css/style.css';
+ $this->assertEqual($expected, $result);
+
+ $result = Media::asset('style','css', array('scope' => 'appcdn'));
+ $expected = 'http://my.cdn.com/assets/css/style.css';
+ $this->assertEqual($expected, $result);
+
+ Media::scope('appcdn');
+
+ $result = Media::asset('style', 'css', array('scope' => false));
+ $expected = '/css/style.css';
+ $this->assertEqual($expected, $result);
+
+ $result = Media::asset('style', 'css');
+ $expected = 'http://my.cdn.com/assets/css/style.css';
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Create environment prefix location using `lihtium\net\http\Media::location`
+ * Check if `lihtium\net\http\Media::asset` return the correct URL
+ * for the production environement
+ */
+ public function testEnvironmentAsset1() {
+ Media::attach('appcdn', array(
+ 'production' => array(
+ 'absolute' => true,
+ 'path' => null,
+ 'scheme' => 'http://',
+ 'host' => 'my.cdnapp.com',
+ 'prefix' => '/assets',
+ ),
+ 'test' => array(
+ 'absolute' => true,
+ 'path' => null,
+ 'scheme' => 'http://',
+ 'host' => 'my.cdntest.com',
+ 'prefix' => '/assets',
+ )));
+
+ $env = Environment::get();
+
+ Environment::set('production');
+ $result = Media::asset('style', 'css', array('scope' => 'appcdn'));
+ $expected = 'http://my.cdnapp.com/assets/css/style.css';
+ }
+
+ /**
+ * Create environment prefix location using `lihtium\net\http\Media::location`
+ * Check if `lihtium\net\http\Media::asset` return the correct URL
+ * for the test environement
+ */
+ public function testEnvironmentAsset2() {
+ Media::attach('appcdn', array(
+ 'production' => array(
+ 'absolute' => true,
+ 'path' => null,
+ 'scheme' => 'http://',
+ 'host' => 'my.cdnapp.com',
+ 'prefix' => 'assets',
+ ),
+ 'test' => array(
+ 'absolute' => true,
+ 'path' => null,
+ 'scheme' => 'http://',
+ 'host' => 'my.cdntest.com',
+ 'prefix' => 'assets',
+ )));
+
+ $env = Environment::get();
+ Environment::set('test');
+ $result = Media::asset('style', 'css', array('scope' => 'appcdn'));
+ $expected = 'http://my.cdntest.com/assets/css/style.css';
+ $this->assertEqual($expected, $result);
+ Environment::is($env);
+ }
+
+ public function testAssetPathGenerationWithLocation() {
+ $resources = Libraries::get(true, 'resources') . '/tmp/tests';
+ $this->skipIf(!is_writable($resources), "Cannot write test app to resources directory.");
+ $paths = array("{$resources}/media_test/css", "{$resources}/media_test/js");
+
+ foreach ($paths as $path) {
+ if (!is_dir($path)) {
+ mkdir($path, 0777, true);
+ }
+ }
+ touch("{$paths[0]}/debug.css");
+
+ Media::attach('media_test', array(
+ 'prefix' => '',
+ 'path' => "{$resources}/media_test")
+ );
+
+ $result = Media::asset('debug', 'css', array('check' => true, 'scope' => 'media_test'));
+
+ $this->assertEqual('/css/debug.css', $result);
+
+ Media::attach('media_test', array(
+ 'prefix' => 'media_test',
+ 'path' => "{$resources}/media_test")
+ );
+
+ $result = Media::asset('debug', 'css', array('check' => true, 'scope' => 'media_test'));
+ $this->assertEqual('/media_test/css/debug.css', $result);
+
+ $result = Media::asset('debug', 'css', array(
+ 'timestamp' => true, 'scope' => 'media_test'
+ ));
+ $this->assertPattern('%^/media_test/css/debug\.css\?\d+$%', $result);
+
+ $result = Media::asset('/css/debug.css?type=test', 'css', array(
+ 'check' => true, 'base' => 'foo', 'scope' => 'media_test'
+ ));
+
+ $this->assertEqual('foo/media_test/css/debug.css?type=test', $result);
+
+ $result = Media::asset('/css/debug.css?type=test', 'css', array(
+ 'check' => true, 'base' => 'http://www.hostname.com/foo', 'scope' => 'media_test'
+ ));
+
+ $this->assertEqual('http://www.hostname.com/foo/media_test/css/debug.css?type=test', $result);
+
+ $result = Media::asset('/css/debug.css?type=test', 'css', array(
+ 'check' => true, 'base' => 'foo', 'timestamp' => true, 'scope' => 'media_test'
+ ));
+ $this->assertPattern('%^foo/media_test/css/debug\.css\?type=test&\d+$%', $result);
+
+ $result = Media::asset('this.file.should.not.exist.css', 'css', array('check' => true));
+ $this->assertFalse($result);
+
+ Media::attach('media_test', array(
+ 'prefix' => 'media_test',
+ 'path' => "{$resources}/media_test")
+ );
+
+ $result = Media::asset('debug', 'css', array('check' => true, 'scope' => 'media_test'));
+ $this->assertEqual('/media_test/css/debug.css', $result);
+
+ $result = Media::asset('debug', 'css', array(
+ 'timestamp' => true, 'scope' => 'media_test'
+ ));
+ $this->assertPattern('%^/media_test/css/debug\.css\?\d+$%', $result);
+
+ $result = Media::asset('/css/debug.css?type=test', 'css', array(
+ 'check' => true, 'base' => 'foo', 'scope' => 'media_test'
+ ));
+
+ $this->assertEqual('foo/media_test/css/debug.css?type=test', $result);
+
+ $result = Media::asset('/css/debug.css?type=test', 'css', array(
+ 'check' => true, 'base' => 'foo', 'timestamp' => true, 'scope' => 'media_test'
+ ));
+ $this->assertPattern('%^foo/media_test/css/debug\.css\?type=test&\d+$%', $result);
+
+ $result = Media::asset('this.file.should.not.exist.css', 'css', array('check' => true));
+ $this->assertFalse($result);
+
+ unlink("{$paths[0]}/debug.css");
+
+ foreach (array_merge($paths, array(dirname($paths[0]))) as $path) {
+ rmdir($path);
+ }
+ }
+
+ public function testEmptyHostAndSchemeOptionLocation() {
+ Media::attach('app', array('absolute' => true));
+
+ Media::scope('app');
+ $result = Media::asset('/js/path/file', 'js', array('base' => '/app/base'));
+ $expected = 'http://localhost/app/base/js/path/file.js';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testDeleteLocation() {
+ $result = Media::asset('/js/path/file', 'js', array('base' => '/app/base'));
+ $expected = '/app/base/js/path/file.js';
+ $this->assertEqual($expected, $result);
+
+ Media::attach('foo_blog', array(
+ 'prefix' => 'assets/plugin/blog'
+ ));
+
+ $result = Media::asset('/js/path/file', 'js', array(
+ 'scope' => 'foo_blog', 'base' => '/app/base'
+ ));
+ $expected = '/app/base/assets/plugin/blog/js/path/file.js';
+ $this->assertEqual($expected, $result);
+
+ Media::attach('foo_blog', false);
+ $this->assertEqual(array(), Media::attached('foo_blog'));
+ }
+
+ public function testListAttached() {
+ Media::attach('media1', array('prefix' => 'media1', 'absolute' => true));
+ Media::attach('media2', array('prefix' => 'media2', 'check' => true));
+ Media::attach('media3', array('prefix' => 'media3'));
+
+ $expected = array (
+ 'media1' => array (
+ 'prefix' => 'media1',
+ 'absolute' => true,
+ 'host' => 'localhost',
+ 'scheme' => 'http://',
+ 'path' => null,
+ 'timestamp' => false,
+ 'filter' => null,
+ 'suffix' => null,
+ 'check' => false
+ ),
+ 'media2' => array (
+ 'prefix' => 'media2',
+ 'absolute' => false,
+ 'host' => 'localhost',
+ 'scheme' => 'http://',
+ 'path' => null,
+ 'timestamp' => false,
+ 'filter' => null,
+ 'suffix' => null,
+ 'check' => true
+ ),
+ 'media3' => array (
+ 'prefix' => 'media3',
+ 'absolute' => false,
+ 'host' => 'localhost',
+ 'scheme' => 'http://',
+ 'path' => null,
+ 'timestamp' => false,
+ 'filter' => null,
+ 'suffix' => null,
+ 'check' => false
+ )
+ );
+
+ $this->assertEqual($expected, Media::attached());
+ }
}
?>
View
130 tests/cases/net/http/RouteTest.php
@@ -212,9 +212,31 @@ public function testRouteParsingWithOptionalParamsAndType() {
$this->assertEqual($expected, $result->params);
}
+ public function testRouteParsingWithParamsEqualsToZero() {
+ $route = new Route(array(
+ 'template' => '/{:controller}/{:action}/{:value}/{:id}.{:type}',
+ 'params' => array('id' => null)
+ ));
+ $request = new Request();
+ $default = array('controller' => 'posts');
+
+ $request->url = '/posts/action/0/123.json';
+ $result = $route->parse($request);
+ $expected = array(
+ 'action' => 'action',
+ 'value' => 0,
+ 'id' => '123',
+ 'type' => 'json'
+ ) + $default;
+ $this->assertEqual($expected, $result->params);
+ }
+
public function testRouteMatchingWithEmptyTrailingParams() {
$route = new Route(array(
'template' => '/{:controller}/{:action}/{:args}',
+ 'modifiers' => array('args' => function($value) {
+ return explode('/', $value);
+ }),
'formatters' => array('args' => function($value) {
return is_array($value) ? join('/', $value) : $value;
})
@@ -549,6 +571,107 @@ public function testMatchingEmptyRoute() {
}
/**
+ * Test route matching for routes with specified request method (http:method)
+ */
+ public function testMatchWithRequestMethod() {
+ $parameters = array('controller' => 'resource', 'action' => 'create');
+
+ $route = new Route(array(
+ 'template' => '/resource',
+ 'params' => $parameters + array('http:method' => 'POST')
+ ));
+
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'POST'
+ ));
+ $this->assertEqual('/resource', $result);
+
+ $result = $route->match(array('controller' => 'resource', 'action' => 'create'));
+ $this->assertEqual(false, $result);
+ }
+
+ /**
+ * Test route matching for routes with specified request method (http:method) and a param
+ */
+ public function testMatchWithRequestMethodWithParam() {
+ $parameters = array('controller' => 'resource', 'action' => 'create');
+
+ $route = new Route(array(
+ 'template' => '/{:param}',
+ 'params' => $parameters + array('http:method' => 'POST')
+ ));
+
+ $result = $route->match(array(
+ 'controller' => 'resource',
+ 'action' => 'create',
+ 'param' => 'value',
+ 'http:method' => 'POST'
+ ));
+ $this->assertEqual('/value', $result);
+
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'param' => 'value'
+ ));
+ $this->assertEqual(false, $result);
+ }
+
+ /**
+ * Test route matching for routes with no request method (http:method)
+ */
+ public function testMatchWithNoRequestMethod() {
+ $parameters = array('controller' => 'resource', 'action' => 'create');
+
+ $route = new Route(array(
+ 'template' => '/resource',
+ 'params' => $parameters
+ ));
+
+ $result = $route->match(array('controller' => 'resource', 'action' => 'create'));
+ $this->assertEqual('/resource', $result);
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'GET'
+ ));
+ $this->assertEqual('/resource', $result);
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'POST'
+ ));
+ $this->assertEqual('/resource', $result);
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'PUT'
+ ));
+ $this->assertEqual('/resource', $result);
+ }
+
+ /**
+ * Test route matching for routes with request method (http:method) GET
+ */
+ public function testMatchWithRequestMethodGet() {
+ $parameters = array('controller' => 'resource', 'action' => 'create');
+
+ $route = new Route(array(
+ 'template' => '/resource',
+ 'params' => $parameters + array('http:method' => 'GET')
+ ));
+
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'GET')
+ );
+ $this->assertEqual('/resource', $result);
+
+ $result = $route->match(array('controller' => 'resource', 'action' => 'create'));
+ $this->assertEqual('/resource', $result);
+
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'POST')
+ );
+ $this->assertEqual(false, $result);
+ $result = $route->match(array(
+ 'controller' => 'resource', 'action' => 'create', 'http:method' => 'PUT')
+ );
+ $this->assertEqual(false, $result);
+ }
+
+ /**
* Tests that routes with optional trailing elements have unnecessary slashes trimmed.
*/
public function testTrimmingEmptyPathElements() {
@@ -565,7 +688,12 @@ public function testTrimmingEmptyPathElements() {
}
public function testUrlEncodedArgs() {
- $route = new Route(array('template' => '/{:controller}/{:action}/{:args}'));
+ $route = new Route(array(
+ 'template' => '/{:controller}/{:action}/{:args}',
+ 'modifiers' => array('args' => function($value) {
+ return explode('/', $value);
+ })
+ ));
$request = new Request();
$request->url = '/posts/index/Food%20%26%20Dining';
$result = $route->parse($request);
View
1,090 tests/cases/net/http/RouterTest.php
@@ -26,14 +26,17 @@ public function setUp() {
public function tearDown() {
Router::reset();
-
- foreach ($this->_routes as $route) {
- Router::connect($route);
+ foreach ($this->_routes as $scope => $routes) {
+ Router::scope($scope, function() use ($routes) {
+ foreach ($routes as $route) {
+ Router::connect($route);
+ }
+ });
}
}
public function testBasicRouteConnection() {
- $result = Router::connect('/hello', array('controller' => 'posts', 'action' => 'index'));
+ $result = Router::connect('/hello', array('controller' => 'Posts', 'action' => 'index'));
$expected = array(
'template' => '/hello',
'pattern' => '@^/hello$@u',
@@ -108,7 +111,7 @@ public function testConnectingWithDefaultParams() {
* Tests basic options for connecting routes.
*/
public function testBasicRouteMatching() {
- Router