1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace yrc\rest; |
4
|
|
|
|
5
|
|
|
use yii\rest\Controller as RestController; |
6
|
|
|
use yii\filters\Cors; |
7
|
|
|
use yii\filters\AccessControl; |
8
|
|
|
use yii\filters\RateLimiter; |
9
|
|
|
use yii\filters\VerbFilter; |
10
|
|
|
use yii\filters\ContentNegotiator; |
11
|
|
|
use yii\web\HttpException; |
12
|
|
|
use yii\web\ForbiddenHttpException; |
13
|
|
|
use yrc\web\Response; |
14
|
|
|
|
15
|
|
|
use Yii; |
16
|
|
|
|
17
|
|
|
use ReflectionClass; |
18
|
|
|
use ReflectionMethod; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Implements Restful API controller interfaces |
22
|
|
|
* @class Controller |
23
|
|
|
*/ |
24
|
|
|
class Controller extends RestController |
25
|
|
|
{ |
26
|
|
|
/** |
27
|
|
|
* Allowed HTTP verbs |
28
|
|
|
* @var array $httpVerbs |
29
|
|
|
*/ |
30
|
|
|
private $httpVerbs = ['post', 'get', 'delete', 'put', 'patch', 'options', 'head']; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Global access filter |
34
|
|
|
*/ |
35
|
|
|
public function beforeAction($action) |
36
|
|
|
{ |
37
|
|
|
$parent = parent::beforeAction($action); |
38
|
|
|
|
39
|
|
|
// Check the global access control header |
40
|
|
|
if (!Yii::$app->yrc->checkAccessHeader(Yii::$app->request)) { |
41
|
|
|
throw new HttpException(401); |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
return $parent; |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* RestController automatically applies HTTP verb filtering and CORS headers |
49
|
|
|
* @return array |
50
|
|
|
*/ |
51
|
|
|
public function behaviors() |
52
|
|
|
{ |
53
|
|
|
$behaviors = parent::behaviors(); |
54
|
|
|
|
55
|
|
|
$authenticator = false; |
56
|
|
|
|
57
|
|
|
if (isset($behaviors['authenticator'])) { |
58
|
|
|
$authenticator = $behaviors['authenticator']; |
59
|
|
|
unset($behaviors['authenticator']); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
$behaviors['contentNegotiator'] = [ |
63
|
|
|
'class' => ContentNegotiator::className(), |
64
|
|
|
'formats' => [ |
65
|
|
|
'application/json' => Response::FORMAT_JSON, |
66
|
|
|
'application/vnd.25519+json' => Response::FORMAT_JSON25519, |
67
|
|
|
'application/xml' => Response::FORMAT_XML, |
68
|
|
|
] |
69
|
|
|
]; |
70
|
|
|
|
71
|
|
|
$behaviors['corsFilter'] = [ |
72
|
|
|
'class' => Cors::className(), |
73
|
|
|
'cors' => [ |
74
|
|
|
'Origin' => ['*'], |
75
|
|
|
'Access-Control-Request-Method' => $this->getHttpVerbMethodsFromClass($this->actions()[$this->action->id]), |
76
|
|
|
'Access-Control-Request-Headers' => ['*'], |
77
|
|
|
'Access-Control-Expose-Headers' => [ |
78
|
|
|
'Access-Control-Allow-Origin', |
79
|
|
|
'X-Pagination-Per-Page', |
80
|
|
|
'X-Pagination-Total-Count', |
81
|
|
|
'X-Pagination-Current-Page', |
82
|
|
|
'X-Pagination-Page-Count', |
83
|
|
|
'Allow', |
84
|
|
|
'X-Rate-Limit-Limit', |
85
|
|
|
'X-Rate-Limit-Remaining', |
86
|
|
|
'X-Rate-Limit-Reset' |
87
|
|
|
], |
88
|
|
|
] |
89
|
|
|
]; |
90
|
|
|
|
91
|
|
|
$behaviors['verbs'] = [ |
92
|
|
|
'class' => VerbFilter::className(), |
93
|
|
|
'actions' => $this->getVerbFilterActionMap() |
94
|
|
|
]; |
95
|
|
|
|
96
|
|
|
// Move authenticator after verbs and cors |
97
|
|
|
if ($authenticator != false) { |
98
|
|
|
$behaviors['authenticator'] = $authenticator; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
$behaviors['rateLimiter'] = [ |
102
|
|
|
'class' => RateLimiter::className(), |
103
|
|
|
'enableRateLimitHeaders' => true |
104
|
|
|
]; |
105
|
|
|
|
106
|
|
|
$access = $this->getAccessControl(); |
107
|
|
|
|
108
|
|
|
if ($access !== null) { |
109
|
|
|
$behaviors['access'] = $access; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
// Manually add the ACAO header because Yii2 is terrible at doing it |
113
|
|
|
header("Access-Control-Allow-Origin: " . \implode(',', $behaviors['corsFilter']['cors']['Origin'])); |
114
|
|
|
return $behaviors; |
|
|
|
|
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Pulls the ACL list from the action |
119
|
|
|
* @return array |
120
|
|
|
*/ |
121
|
|
|
private function getAccessControl() |
122
|
|
|
{ |
123
|
|
|
$access = [ |
124
|
|
|
'class' => AccessControl::className(), |
125
|
|
|
'denyCallback' => function($rule, $action) { |
|
|
|
|
126
|
|
|
throw new ForbiddenHttpException(Yii::t('yrc', 'You do not have permission to access this resource')); |
127
|
|
|
} |
128
|
|
|
]; |
129
|
|
|
|
130
|
|
|
$acl = $this->action->acl; |
131
|
|
|
if ($acl === null) { |
132
|
|
|
return null; |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
foreach ($acl as $verb => $perms) { |
136
|
|
|
$access['rules'][] = [ |
137
|
|
|
'allow' => true, |
138
|
|
|
'verbs' => [$verb], |
139
|
|
|
'roles' => $perms |
140
|
|
|
]; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// Allow HTTP Options |
144
|
|
|
$access['rules'][] = [ |
145
|
|
|
'allow' => true, |
146
|
|
|
'verbs' => ['OPTIONS'] |
147
|
|
|
]; |
148
|
|
|
|
149
|
|
|
return $access; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Retrieves the HTTP verb list |
154
|
|
|
* @param string $class |
155
|
|
|
* @return array |
156
|
|
|
*/ |
157
|
|
|
private function getHttpVerbMethodsFromClass($class) |
158
|
|
|
{ |
159
|
|
|
$result = []; |
160
|
|
|
|
161
|
|
|
if (is_array($class)) { |
162
|
|
|
$class = $class['class']; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
// Fetch the public methods for the class then filter them out by the http verbs |
166
|
|
|
$reflection = new ReflectionClass($class); |
167
|
|
|
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); |
168
|
|
|
foreach ($methods as $method) { |
169
|
|
|
if (\in_array($method->name, $this->httpVerbs)) { |
170
|
|
|
$result[] = $method->name; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
return $result; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Convers self::actions() for automatic verb filtering |
179
|
|
|
* @return array |
180
|
|
|
*/ |
181
|
|
|
private function getVerbFilterActionMap() |
182
|
|
|
{ |
183
|
|
|
$actions = $this->actions(); |
184
|
|
|
|
185
|
|
|
// Only apply this filtering for ActionMapped Controllers |
186
|
|
|
if (empty($actions)) { |
187
|
|
|
return []; |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
$actionMap = []; |
191
|
|
|
|
192
|
|
|
// Iterate over all the actions, and automatically determine the methods implemented |
193
|
|
|
foreach ($actions as $actionName => $params) { |
194
|
|
|
static $class = null; |
195
|
|
|
if (is_array($params)) { |
196
|
|
|
$class = $params['class']; |
197
|
|
|
} else { |
198
|
|
|
$class = $params; |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
$actionMap[$actionName] = $this->getHttpVerbMethodsFromClass($class); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
return $actionMap; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.