Browse Source

url wip

tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
712f4dae0d
  1. 4
      docs/api/db/ActiveRecord.md
  2. 38
      framework/web/UrlManager.php
  3. 130
      framework/web/UrlRule.php
  4. 7
      tests/unit/framework/web/UrlManagerTest.php
  5. 441
      tests/unit/framework/web/UrlRuleTest.php

4
docs/api/db/ActiveRecord.md

@ -300,7 +300,7 @@ foreach ($customers as $customer) {
~~~
How many SQL queries will be performed in the above code, assuming there are more than 100 customers in
the database? 101! The first SQL query brings back 100 customers. Then for each customer, another SQL query
the database? 101! The first SQL query brings back 100 customers. Then for each customer, a SQL query
is performed to bring back the customer's orders.
To solve the above performance problem, you can use the so-called *eager loading* by calling [[ActiveQuery::with()]]:
@ -318,7 +318,7 @@ foreach ($customers as $customer) {
}
~~~
As you can see, only two SQL queries were needed for the same task.
As you can see, only two SQL queries are needed for the same task.
Sometimes, you may want to customize the relational queries on the fly. It can be

38
framework/web/UrlManager.php

@ -98,24 +98,50 @@ class UrlManager extends Component
*/
protected function processRules()
{
foreach ($this->rules as $i => $rule) {
if (!isset($rule['class'])) {
$rule['class'] = 'yii\web\UrlRule';
}
$this->rules[$i] = \Yii::createObject($rule);
}
}
/**
* Parses the user request.
* @param HttpRequest $request the request application component
* @param Request $request the request application component
* @return string the route (controllerID/actionID) and perhaps GET parameters in path format.
*/
public function parseUrl($request)
{
if (isset($_GET[$this->routeVar])) {
return $_GET[$this->routeVar];
} else {
return '';
}
public function createUrl($route, $params = array())
{
$anchor = isset($params['#']) ? '#' . $params['#'] : '';
unset($anchor['#']);
/** @var $rule UrlRule */
foreach ($this->rules as $rule) {
if (($url = $rule->createUrl($route, $params)) !== false) {
return $this->getBaseUrl() . $url . $anchor;
}
}
public function createUrl($route, $params = array(), $ampersand = '&')
if ($params !== array()) {
$route .= '?' . http_build_query($params);
}
return $this->getBaseUrl() . '/' . $route . $anchor;
}
private $_baseUrl;
public function getBaseUrl()
{
return $this->_baseUrl;
}
public function setBaseUrl($value)
{
$this->_baseUrl = trim($value, '/');
}
}

130
framework/web/UrlRule.php

@ -14,9 +14,15 @@ use yii\base\Object;
/**
* UrlManager manages the URLs of Yii applications.
* array(
* 'pattern' => 'post/<id:\d+>',
* 'pattern' => 'post/<page:\d+>',
* 'route' => 'post/view',
* 'params' => array('id' => 1),
* 'defaults' => array('page' => 1),
* )
*
* array(
* 'pattern' => 'about',
* 'route' => 'site/page',
* 'defaults' => array('view' => 'about'),
* )
*
* @author Qiang Xue <qiang.xue@gmail.com>
@ -29,22 +35,128 @@ class UrlRule extends Object
*/
public $pattern;
/**
* @var string the route to the controller action
*/
public $route;
/**
* @var array the default GET parameters (name=>value) that this rule provides.
* When this rule is used to parse the incoming request, the values declared in this property
* will be injected into $_GET.
*/
public $params = array();
/**
* @var string the route to the controller action
*/
public $route;
public $defaults = array();
protected $paramRules = array();
protected $routeRule;
protected $template;
protected $routeParams = array();
public function createUrl($route, $params, $ampersand)
public function init()
{
$this->pattern = trim($this->pattern, '/');
if ($this->pattern === '') {
$this->template = '';
$this->pattern = '#^$#u';
return;
} else {
$this->pattern = '/' . $this->pattern . '/';
}
$this->route = trim($this->route, '/');
if (strpos($this->route, '<') !== false && preg_match_all('/<(\w+)>/', $this->route, $matches)) {
foreach ($matches[1] as $name) {
$this->routeParams[$name] = "<$name>";
}
}
$tr = $tr2 = array();
if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
foreach ($matches as $match) {
$name = $match[1][0];
$pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
if (isset($this->defaults[$name])) {
$length = strlen($match[0][0]);
$offset = $match[0][1];
if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') {
$tr["<$name>"] = "(?P<$name>(?:/$pattern)?)";
} else {
$tr["<$name>"] = "(?P<$name>(?:$pattern)?)";
}
} else {
$tr["<$name>"] = "(?P<$name>$pattern)";
}
if (isset($this->routeParams[$name])) {
$tr2["<$name>"] = "(?P<$name>$pattern)";
} else {
$this->paramRules[$name] = $pattern === '[^\/]+' ? '' : "#^$pattern$#";
}
}
}
$this->template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern);
$this->pattern = '#^' . strtr($this->template, $tr) . '$#u';
if ($this->routeParams !== array()) {
$this->routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
}
}
public function parseUrl($pathInfo)
{
}
public function createUrl($route, $params)
{
$tr = array();
// match the route part first
if ($route !== $this->route) {
if ($this->routeRule !== null && preg_match($this->routeRule, $route, $matches)) {
foreach ($this->routeParams as $key => $name) {
$tr[$name] = $matches[$key];
}
} else {
return false;
}
}
// match default params
// if a default param is not in the route pattern, its value must also be matched
foreach ($this->defaults as $name => $value) {
if (!isset($params[$name])) {
return false;
} elseif (strcmp($params[$name], $value) === 0) { // strcmp will do string conversion automatically
unset($params[$name]);
if (isset($this->paramRules[$name])) {
$tr["<$name>"] = '';
$tr["/<$name>/"] = '/';
}
} elseif (!isset($this->paramRules[$name])) {
return false;
}
}
// match params in the pattern
foreach ($this->paramRules as $name => $rule) {
if (isset($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
$tr["<$name>"] = urlencode($params[$name]);
unset($params[$name]);
} elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
return false;
}
}
$url = trim(strtr($this->template, $tr), '/');
if (strpos($url, '//') !== false) {
$url = preg_replace('#/+#', '/', $url);
}
if ($params !== array()) {
$url .= '?' . http_build_query($params);
}
return $url;
}
public function parse($path)
public function parse($pathInfo)
{
$route = '';
$params = array();

7
tests/unit/framework/web/UrlManagerTest.php

@ -0,0 +1,7 @@
<?php
namespace yiiunit\framework\web;
class UrlManagerTest extends \yiiunit\TestCase
{
}

441
tests/unit/framework/web/UrlRuleTest.php

@ -0,0 +1,441 @@
<?php
namespace yiiunit\framework\web;
use yii\web\UrlRule;
class UrlRuleTest extends \yiiunit\TestCase
{
public function testCreateUrl()
{
$suites = $this->getTestsForCreateUrl();
foreach ($suites as $i => $suite) {
list ($name, $config, $tests) = $suite;
$rule = new UrlRule($config);
foreach ($tests as $j => $test) {
list ($route, $params, $expected) = $test;
$url = $rule->createUrl($route, $params);
$this->assertEquals($expected, $url, "Test#$i-$j: $name");
}
}
}
public function testParseUrl()
{
$suites = $this->getTestsForParseUrl();
foreach ($suites as $i => $suite) {
list ($name, $config, $tests) = $suite;
$rule = new UrlRule($config);
foreach ($tests as $j => $test) {
$pathInfo = $test[0];
$route = $test[1];
$params = isset($test[2]) ? $test[2] : array();
$result = $rule->parseUrl($pathInfo);
if ($route === false) {
$this->assertFalse($result, "Test#$i-$j: $name");
} else {
$this->assertEquals(array($route, $params), $result, "Test#$i-$j: $name");
}
}
}
}
protected function getTestsForCreateUrl()
{
// structure of each test
// message for the test
// config for the URL rule
// list of inputs and outputs
// route
// params
// expected output
return array(
array(
'empty pattern',
array(
'pattern' => '',
'route' => 'post/index',
),
array(
array('post/index', array(), ''),
array('comment/index', array(), false),
array('post/index', array('page' => 1), '?page=1'),
),
),
array(
'without param',
array(
'pattern' => 'posts',
'route' => 'post/index',
),
array(
array('post/index', array(), 'posts'),
array('comment/index', array(), false),
array('post/index', array('page' => 1), 'posts?page=1'),
),
),
array(
'with param',
array(
'pattern' => 'post/<page>',
'route' => 'post/index',
),
array(
array('post/index', array(), false),
array('comment/index', array(), false),
array('post/index', array('page' => 1), 'post/1'),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'),
),
),
array(
'with param requirement',
array(
'pattern' => 'post/<page:\d+>',
'route' => 'post/index',
),
array(
array('post/index', array('page' => 'abc'), false),
array('post/index', array('page' => 1), 'post/1'),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1?tag=a'),
),
),
array(
'with multiple params',
array(
'pattern' => 'post/<page:\d+>-<tag>',
'route' => 'post/index',
),
array(
array('post/index', array('page' => '1abc'), false),
array('post/index', array('page' => 1), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/1-a'),
),
),
array(
'with optional param',
array(
'pattern' => 'post/<page:\d+>/<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'),
array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2/a'),
),
),
array(
'with optional param not in pattern',
array(
'pattern' => 'post/<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 2, 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'),
),
),
array(
'multiple optional params',
array(
'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>',
'route' => 'post/index',
'defaults' => array('page' => 1, 'sort' => 'yes'),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'YES'), false),
array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'yes'), 'post/a'),
array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'yes'), 'post/2/a'),
array('post/index', array('page' => 2, 'tag' => 'a', 'sort' => 'no'), 'post/2/a/no'),
array('post/index', array('page' => 1, 'tag' => 'a', 'sort' => 'no'), 'post/a/no'),
),
),
array(
'optional param and required param separated by dashes',
array(
'pattern' => 'post/<page:\d+>-<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-a'),
array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-a'),
),
),
array(
'optional param at the end',
array(
'pattern' => 'post/<tag>/<page:\d+>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/a'),
array('post/index', array('page' => 2, 'tag' => 'a'), 'post/a/2'),
),
),
array(
'consecutive optional params',
array(
'pattern' => 'post/<page:\d+>/<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1, 'tag' => 'a'),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post'),
array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2'),
array('post/index', array('page' => 1, 'tag' => 'b'), 'post/b'),
array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2/b'),
),
),
array(
'consecutive optional params separated by dash',
array(
'pattern' => 'post/<page:\d+>-<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1, 'tag' => 'a'),
),
array(
array('post/index', array('page' => 1), false),
array('post/index', array('page' => '1abc', 'tag' => 'a'), false),
array('post/index', array('page' => 1, 'tag' => 'a'), 'post/-'),
array('post/index', array('page' => 1, 'tag' => 'b'), 'post/-b'),
array('post/index', array('page' => 2, 'tag' => 'a'), 'post/2-'),
array('post/index', array('page' => 2, 'tag' => 'b'), 'post/2-b'),
),
),
array(
'route has parameters',
array(
'pattern' => '<controller>/<action>',
'route' => '<controller>/<action>',
'defaults' => array(),
),
array(
array('post/index', array('page' => 1), 'post/index?page=1'),
array('module/post/index', array(), false),
),
),
array(
'route has parameters with regex',
array(
'pattern' => '<controller:post|comment>/<action>',
'route' => '<controller>/<action>',
'defaults' => array(),
),
array(
array('post/index', array('page' => 1), 'post/index?page=1'),
array('comment/index', array('page' => 1), 'comment/index?page=1'),
array('test/index', array('page' => 1), false),
array('post', array(), false),
array('module/post/index', array(), false),
array('post/index', array('controller' => 'comment'), 'post/index?controller=comment'),
),
),
/* this is not supported
array(
'route has default parameter',
array(
'pattern' => '<controller:post|comment>/<action>',
'route' => '<controller>/<action>',
'defaults' => array('action' => 'index'),
),
array(
array('post/view', array('page' => 1), 'post/view?page=1'),
array('comment/view', array('page' => 1), 'comment/view?page=1'),
array('test/view', array('page' => 1), false),
array('post/index', array('page' => 1), 'post?page=1'),
),
),
*/
);
}
protected function getTestsForParseUrl()
{
// structure of each test
// message for the test
// config for the URL rule
// list of inputs and outputs
// pathInfo
// expected route, or false if the rule doesn't apply
// expected params, or not set if empty
return array(
array(
'empty pattern',
array(
'pattern' => '',
'route' => 'post/index',
),
array(
array('', 'post/index'),
array('a', false),
),
),
array(
'without param',
array(
'pattern' => 'posts',
'route' => 'post/index',
),
array(
array('posts', 'post/index'),
array('a', false),
),
),
array(
'with param',
array(
'pattern' => 'post/<page>',
'route' => 'post/index',
),
array(
array('post/1', 'post/index', array('page' => '1')),
array('post/a', 'post/index', array('page' => 'a')),
array('post', false),
array('posts', false),
),
),
array(
'with param requirement',
array(
'pattern' => 'post/<page:\d+>',
'route' => 'post/index',
),
array(
array('post/1', 'post/index', array('page' => '1')),
array('post/a', false),
array('post/1/a', false),
),
),
array(
'with multiple params',
array(
'pattern' => 'post/<page:\d+>-<tag>',
'route' => 'post/index',
),
array(
array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/a', false),
array('post/1', false),
array('post/1/a', false),
),
),
array(
'with optional param',
array(
'pattern' => 'post/<page:\d+>/<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/1/a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a')),
array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/1', 'post/index', array('page' => '1', 'tag' => '1')),
),
),
array(
'with optional param not in pattern',
array(
'pattern' => 'post/<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/1', 'post/index', array('page' => '1', 'tag' => '1')),
array('post', false),
),
),
array(
'multiple optional params',
array(
'pattern' => 'post/<page:\d+>/<tag>/<sort:yes|no>',
'route' => 'post/index',
'defaults' => array('page' => 1, 'sort' => 'yes'),
),
array(
array('post/1/a/yes', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')),
array('post/2/a/no', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'no')),
array('post/2/a', 'post/index', array('page' => '2', 'tag' => 'a', 'sort' => 'yes')),
array('post/a/no', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'no')),
array('post/a', 'post/index', array('page' => '1', 'tag' => 'a', 'sort' => 'yes')),
array('post', false),
),
),
array(
'optional param and required param separated by dashes',
array(
'pattern' => 'post/<page:\d+>-<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/1-a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/2-a', 'post/index', array('page' => '2', 'tag' => 'a')),
array('post/-a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/a', false),
array('post-a', false),
),
),
array(
'optional param at the end',
array(
'pattern' => 'post/<tag>/<page:\d+>',
'route' => 'post/index',
'defaults' => array('page' => 1),
),
array(
array('post/a/1', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/a/2', 'post/index', array('page' => '2', 'tag' => 'a')),
array('post/a', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/2', 'post/index', array('page' => '1', 'tag' => '2')),
array('post', false),
),
),
array(
'consecutive optional params',
array(
'pattern' => 'post/<page:\d+>/<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1, 'tag' => 'a'),
),
array(
array('post/2/b', 'post/index', array('page' => '2', 'tag' => 'b')),
array('post/2', 'post/index', array('page' => '2', 'tag' => 'a')),
array('post', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post/b', 'post/index', array('page' => '1', 'tag' => 'b')),
array('post//b', false),
),
),
array(
'consecutive optional params separated by dash',
array(
'pattern' => 'post/<page:\d+>-<tag>',
'route' => 'post/index',
'defaults' => array('page' => 1, 'tag' => 'a'),
),
array(
array('post/2-b', 'post/index', array('page' => '2', 'tag' => 'b')),
array('post/2-', 'post/index', array('page' => '2', 'tag' => 'a')),
array('post/-b', 'post/index', array('page' => '1', 'tag' => 'b')),
array('post/-', 'post/index', array('page' => '1', 'tag' => 'a')),
array('post', false),
),
),
);
}
}
Loading…
Cancel
Save