|
1
|
|
|
<?php |
|
2
|
|
|
namespace WebStream\Delegate; |
|
3
|
|
|
|
|
4
|
|
|
use WebStream\DI\Injector; |
|
5
|
|
|
use WebStream\Container\Container; |
|
6
|
|
|
use WebStream\Util\Security; |
|
7
|
|
|
use WebStream\Util\CommonUtils; |
|
8
|
|
|
use WebStream\Exception\Extend\RouterException; |
|
9
|
|
|
|
|
10
|
|
|
/** |
|
11
|
|
|
* ルーティングクラス |
|
12
|
|
|
* @author Ryuichi TANAKA. |
|
13
|
|
|
* @since 2011/08/19 |
|
14
|
|
|
* @version 0.7 |
|
15
|
|
|
*/ |
|
16
|
|
|
class Router |
|
17
|
|
|
{ |
|
18
|
|
|
use Injector, CommonUtils; |
|
19
|
|
|
|
|
20
|
|
|
/** |
|
21
|
|
|
* @var array<string> ルーティングルール |
|
22
|
|
|
*/ |
|
23
|
|
|
private $rules; |
|
24
|
|
|
|
|
25
|
|
|
/** |
|
26
|
|
|
* @var Container リクエストコンテナ |
|
27
|
|
|
*/ |
|
28
|
|
|
private $request; |
|
29
|
|
|
|
|
30
|
|
|
/** |
|
31
|
|
|
* @var Container ルーティング結果 |
|
32
|
|
|
*/ |
|
33
|
|
|
private $routingContainer; |
|
34
|
|
|
|
|
35
|
|
|
/** |
|
36
|
|
|
* コンストラクタ |
|
37
|
|
|
* @param Request リクエストオブジェクト |
|
|
|
|
|
|
38
|
|
|
*/ |
|
39
|
|
|
public function __construct(array $rules, Container $request) |
|
40
|
|
|
{ |
|
41
|
|
|
$this->rules = $rules; |
|
42
|
|
|
$this->request = $request; |
|
43
|
|
|
$this->routingContainer = new Container(false); |
|
44
|
|
|
} |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* デストラクタ |
|
48
|
|
|
*/ |
|
49
|
|
|
public function __destruct() |
|
50
|
|
|
{ |
|
51
|
|
|
$this->logger->debug("Router is clear."); |
|
|
|
|
|
|
52
|
|
|
} |
|
53
|
|
|
|
|
54
|
|
|
/** |
|
55
|
|
|
* ルーティングを解決する |
|
56
|
|
|
*/ |
|
57
|
|
|
public function resolve() |
|
58
|
|
|
{ |
|
59
|
|
|
// ルーティングルールの検証 |
|
60
|
|
|
$this->validate(); |
|
61
|
|
|
// ルーティング結果の格納 |
|
62
|
|
|
$this->resolveRouting() ?: $this->resolveStaticFilePath(); |
|
63
|
|
|
} |
|
64
|
|
|
|
|
65
|
|
|
/** |
|
66
|
|
|
* ルーティング結果を返却する |
|
67
|
|
|
* @return Container ルーティング結果 |
|
68
|
|
|
*/ |
|
69
|
|
|
public function getRoutingResult() |
|
70
|
|
|
{ |
|
71
|
|
|
return $this->routingContainer; |
|
72
|
|
|
} |
|
73
|
|
|
|
|
74
|
|
|
/** |
|
75
|
|
|
* ルーティングルールを検証する |
|
76
|
|
|
*/ |
|
77
|
|
|
private function validate() |
|
78
|
|
|
{ |
|
79
|
|
|
// パス定義部分('/xxx')は禁止の定義がされた時点でエラーとする |
|
80
|
|
|
// CA部分('controller#action')はパスにアクセスされたときにチェックする |
|
81
|
|
|
foreach ($this->rules as $path => $ca) { |
|
82
|
|
|
// 静的ファイルへのパスがルーティングルールに定義された場合 |
|
83
|
|
|
// パス定義された時点で弾く |
|
84
|
|
|
if (preg_match('/\/(img|js|css|file)(?:$|\/)/', $path)) { |
|
85
|
|
|
throw new RouterException("Include the prohibit routing path: " . $path); |
|
86
|
|
|
} |
|
87
|
|
|
// 許可したルーティングパス定義に合っていなければ弾く |
|
88
|
|
|
if (!preg_match('/^\/{1}(?:$|:?[a-zA-Z]{1}[a-zA-Z0-9-_\/\.:]{0,}$)/', $path)) { |
|
89
|
|
|
throw new RouterException("Invalid path defintion: " . $path); |
|
90
|
|
|
} |
|
91
|
|
|
// ルールとURLがマッチした場合に動的にチェックを掛ける |
|
92
|
|
|
// パスがマッチしたときにアクション名をチェックし、その時点で弾く |
|
93
|
|
|
if ($this->request->pathInfo === $path) { |
|
|
|
|
|
|
94
|
|
|
// ルーティング定義(Controller#Action)が正しい場合 |
|
95
|
|
|
// _(アンダースコア)は許可するが、2回以上の連続の場合、末尾につく場合は許可しない |
|
96
|
|
|
// NG例:my__blog, my_blog_ |
|
97
|
|
|
if (!preg_match('/^(?:([a-z]{1}(?:_(?=[a-z])|[a-z0-9])+))#(?:([a-z]{1}(?:_(?=[a-z])|[a-z0-9])+))$/', $ca, $matches)) { |
|
98
|
|
|
// ルーティング定義(Controller#Action)が正しくない場合 |
|
99
|
|
|
throw new RouterException("Invalid controller#action definition: " . $ca); |
|
100
|
|
|
} |
|
101
|
|
|
} |
|
102
|
|
|
} |
|
103
|
|
|
} |
|
104
|
|
|
|
|
105
|
|
|
/** |
|
106
|
|
|
* ルーティングを解決する |
|
107
|
|
|
*/ |
|
108
|
|
|
private function resolveRouting() |
|
109
|
|
|
{ |
|
110
|
|
|
// ルーティングルールからController、Actionを取得 |
|
111
|
|
|
foreach ($this->rules as $path => $ca) { |
|
112
|
|
|
$route = []; |
|
|
|
|
|
|
113
|
|
|
$tokens = explode("/", ltrim($path, "/")); |
|
114
|
|
|
$placeholderedParams = []; |
|
115
|
|
|
$keyList = []; |
|
116
|
|
|
|
|
117
|
|
|
for ($i = 0; $i < count($tokens); $i++) { |
|
|
|
|
|
|
118
|
|
|
$token = $tokens[$i]; |
|
119
|
|
|
// PATH定義にプレースホルダがある場合は正規表現に置き換える |
|
120
|
|
|
if (preg_match('/:(.*?)(?:\/|$)/', $token, $matches)) { |
|
121
|
|
|
$keyList[] = $matches[1]; |
|
122
|
|
|
$token = preg_replace('/(:.*?)(?:\/|$)/', '(.+)', $token); |
|
123
|
|
|
} |
|
124
|
|
|
$tokens[$i] = $token; |
|
125
|
|
|
} |
|
126
|
|
|
// プレースホルダのパラメータをセット |
|
127
|
|
|
$expantionPath = $path; |
|
128
|
|
|
// PATH_INFOの階層数とルーティング定義の階層数が一致すればルーティングがマッチ |
|
129
|
|
|
if (($this->request->pathInfo !== $path) && |
|
|
|
|
|
|
130
|
|
|
count(explode('/', $path)) === count(explode('/', $this->request->pathInfo))) { |
|
131
|
|
|
// プレースホルダと実URLをひもづける |
|
132
|
|
|
$pathPattern = "/^\/" . implode("\/", $tokens) . "$/"; |
|
133
|
|
|
if (preg_match($pathPattern, $this->request->pathInfo, $matches)) { |
|
134
|
|
|
for ($j = 1; $j < count($matches); $j++) { |
|
|
|
|
|
|
135
|
|
|
$key = $keyList[$j - 1]; |
|
136
|
|
|
$placeholderedParams[$key] = Security::safetyIn($matches[$j]); |
|
137
|
|
|
// プレースホルダを一時展開する |
|
138
|
|
|
$expantionPath = preg_replace('/:[a-zA-Z_]{1}[a-zA-Z0-9_]{0,}/', $matches[$j], $expantionPath, 1); |
|
139
|
|
|
} |
|
140
|
|
|
} |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
// プレースホルダを展開済みのパス定義が完全一致したときはController、Actionを展開する |
|
144
|
|
|
if ($this->request->pathInfo === $expantionPath && |
|
145
|
|
|
preg_match('/^(?:([a-z]{1}(?:_(?=[a-z])|[a-z0-9])+))#(?:([a-z]{1}(?:_(?=[a-z])|[a-z0-9])+))$/', $ca, $matches)) { |
|
146
|
|
|
$this->setController($matches[1]); |
|
147
|
|
|
$this->setAction($matches[2]); |
|
148
|
|
|
$this->routingContainer->params = $placeholderedParams; |
|
|
|
|
|
|
149
|
|
|
$this->logger->info("Routed path: " . $matches[1] . "#" . $matches[2]); |
|
|
|
|
|
|
150
|
|
|
|
|
151
|
|
|
// ルーティングルールがマッチした場合は抜ける |
|
152
|
|
|
return true; |
|
153
|
|
|
} |
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
return false; |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* 静的ファイルパスを解決する |
|
161
|
|
|
*/ |
|
162
|
|
|
private function resolveStaticFilePath() |
|
163
|
|
|
{ |
|
164
|
|
|
$staticFile = $this->applicationInfo->applicationRoot . "/app/views/" . $this->applicationInfo->publicDir . $this->request->pathInfo; |
|
|
|
|
|
|
165
|
|
|
|
|
166
|
|
|
if (is_file($staticFile)) { |
|
167
|
|
|
$this->routingContainer->staticFile = $staticFile; |
|
|
|
|
|
|
168
|
|
|
} elseif (pathinfo($staticFile, PATHINFO_EXTENSION) == 'css') { |
|
169
|
|
|
// cssファイル指定かつ存在しない場合で、同ディレクトリ内にlessファイルがあればcssにコンパイルする |
|
170
|
|
|
$less = new \lessc(); |
|
|
|
|
|
|
171
|
|
|
$dirpath = dirname($staticFile); |
|
172
|
|
|
$filenameWitoutExt = pathinfo($staticFile, PATHINFO_FILENAME); |
|
173
|
|
|
$lessFilepath = $dirpath . "/" . $filenameWitoutExt . ".less"; |
|
174
|
|
|
// lessファイルも見つからない場合はエラー |
|
175
|
|
|
if (!file_exists($lessFilepath)) { |
|
176
|
|
|
$this->logger->error("The file of css has been specified, but not found even file of less:" . $lessFilepath); |
|
|
|
|
|
|
177
|
|
|
|
|
178
|
|
|
return; |
|
179
|
|
|
} |
|
180
|
|
|
if (@$less->checkedCompile($lessFilepath, $staticFile)) { |
|
181
|
|
|
if (is_file($staticFile)) { |
|
182
|
|
|
$this->routingContainer->staticFile = $staticFile; |
|
183
|
|
|
} else { |
|
184
|
|
|
$this->logger->error("Failed to file create, cause parmission denied: " . $dirpath); |
|
185
|
|
|
} |
|
186
|
|
|
} |
|
187
|
|
|
} |
|
188
|
|
|
} |
|
189
|
|
|
|
|
190
|
|
|
/** |
|
191
|
|
|
* コントローラを設定する |
|
192
|
|
|
* @param string コントローラ文字列 |
|
|
|
|
|
|
193
|
|
|
*/ |
|
194
|
|
|
private function setController($controller) |
|
195
|
|
|
{ |
|
196
|
|
|
if (isset($controller)) { |
|
197
|
|
|
$this->routingContainer->pageName = $this->snake2ucamel($controller); |
|
|
|
|
|
|
198
|
|
|
$this->routingContainer->controller = $this->snake2ucamel($controller) . "Controller"; |
|
|
|
|
|
|
199
|
|
|
} |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* アクションを設定する |
|
204
|
|
|
* @param string アクション文字列 |
|
|
|
|
|
|
205
|
|
|
*/ |
|
206
|
|
|
private function setAction($action) |
|
207
|
|
|
{ |
|
208
|
|
|
if (isset($action)) { |
|
209
|
|
|
$this->routingContainer->action = $this->snake2lcamel($action); |
|
|
|
|
|
|
210
|
|
|
} |
|
211
|
|
|
} |
|
212
|
|
|
} |
|
213
|
|
|
|