Commit 712f4dae by Qiang Xue

url wip

parent ab42ab2d
...@@ -300,7 +300,7 @@ foreach ($customers as $customer) { ...@@ -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 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. 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()]]: 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) { ...@@ -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 Sometimes, you may want to customize the relational queries on the fly. It can be
......
...@@ -98,24 +98,50 @@ class UrlManager extends Component ...@@ -98,24 +98,50 @@ class UrlManager extends Component
*/ */
protected function processRules() 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. * 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. * @return string the route (controllerID/actionID) and perhaps GET parameters in path format.
*/ */
public function parseUrl($request) public function parseUrl($request)
{ {
if (isset($_GET[$this->routeVar])) { }
return $_GET[$this->routeVar];
} else { public function createUrl($route, $params = array())
return ''; {
$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, '/');
} }
} }
...@@ -14,9 +14,15 @@ use yii\base\Object; ...@@ -14,9 +14,15 @@ use yii\base\Object;
/** /**
* UrlManager manages the URLs of Yii applications. * UrlManager manages the URLs of Yii applications.
* array( * array(
* 'pattern' => 'post/<id:\d+>', * 'pattern' => 'post/<page:\d+>',
* 'route' => 'post/view', * '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> * @author Qiang Xue <qiang.xue@gmail.com>
...@@ -29,22 +35,128 @@ class UrlRule extends Object ...@@ -29,22 +35,128 @@ class UrlRule extends Object
*/ */
public $pattern; public $pattern;
/** /**
* @var string the route to the controller action
*/
public $route;
/**
* @var array the default GET parameters (name=>value) that this rule provides. * @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 * When this rule is used to parse the incoming request, the values declared in this property
* will be injected into $_GET. * will be injected into $_GET.
*/ */
public $params = array(); public $defaults = array();
/**
* @var string the route to the controller action protected $paramRules = array();
*/ protected $routeRule;
public $route; 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 = ''; $route = '';
$params = array(); $params = array();
......
<?php
namespace yiiunit\framework\web;
class UrlManagerTest extends \yiiunit\TestCase
{
}
<?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),
),
),
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment