Issues (910)

framework/base/Controller.php (3 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\base;
9
10
use Yii;
11
use yii\di\Instance;
12
use yii\di\NotInstantiableException;
13
14
/**
15
 * Controller is the base class for classes containing controller logic.
16
 *
17
 * For more details and usage information on Controller, see the [guide article on controllers](guide:structure-controllers).
18
 *
19
 * @property-read Module[] $modules All ancestor modules that this controller is located within.
20
 * @property-read string $route The route (module ID, controller ID and action ID) of the current request.
21
 * @property-read string $uniqueId The controller ID that is prefixed with the module ID (if any).
22
 * @property View|\yii\web\View $view The view object that can be used to render views or view files.
23
 * @property string $viewPath The directory containing the view files for this controller.
24
 *
25
 * @author Qiang Xue <[email protected]>
26
 * @since 2.0
27
 */
28
class Controller extends Component implements ViewContextInterface
29
{
30
    /**
31
     * @event ActionEvent an event raised right before executing a controller action.
32
     * You may set [[ActionEvent::isValid]] to be false to cancel the action execution.
33
     */
34
    const EVENT_BEFORE_ACTION = 'beforeAction';
35
    /**
36
     * @event ActionEvent an event raised right after executing a controller action.
37
     */
38
    const EVENT_AFTER_ACTION = 'afterAction';
39
40
    /**
41
     * @var string the ID of this controller.
42
     */
43
    public $id;
44
    /**
45
     * @var Module the module that this controller belongs to.
46
     */
47
    public $module;
48
    /**
49
     * @var string the ID of the action that is used when the action ID is not specified
50
     * in the request. Defaults to 'index'.
51
     */
52
    public $defaultAction = 'index';
53
    /**
54
     * @var string|null|false the name of the layout to be applied to this controller's views.
55
     * This property mainly affects the behavior of [[render()]].
56
     * Defaults to null, meaning the actual layout value should inherit that from [[module]]'s layout value.
57
     * If false, no layout will be applied.
58
     */
59
    public $layout;
60
    /**
61
     * @var Action|null the action that is currently being executed. This property will be set
62
     * by [[run()]] when it is called by [[Application]] to run an action.
63
     */
64
    public $action;
65
    /**
66
     * @var Request|array|string The request.
67
     * @since 2.0.36
68
     */
69
    public $request = 'request';
70
    /**
71
     * @var Response|array|string The response.
72
     * @since 2.0.36
73
     */
74
    public $response = 'response';
75
76
    /**
77
     * @var View|null the view object that can be used to render views or view files.
78
     */
79
    private $_view;
80
    /**
81
     * @var string|null the root directory that contains view files for this controller.
82
     */
83
    private $_viewPath;
84
85
86
    /**
87
     * @param string $id the ID of this controller.
88
     * @param Module $module the module that this controller belongs to.
89
     * @param array $config name-value pairs that will be used to initialize the object properties.
90
     */
91 493
    public function __construct($id, $module, $config = [])
92
    {
93 493
        $this->id = $id;
94 493
        $this->module = $module;
95 493
        parent::__construct($config);
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     * @since 2.0.36
101
     */
102 493
    public function init()
103
    {
104 493
        parent::init();
105 493
        $this->request = Instance::ensure($this->request, Request::className());
106 493
        $this->response = Instance::ensure($this->response, Response::className());
107
    }
108
109
    /**
110
     * Declares external actions for the controller.
111
     *
112
     * This method is meant to be overwritten to declare external actions for the controller.
113
     * It should return an array, with array keys being action IDs, and array values the corresponding
114
     * action class names or action configuration arrays. For example,
115
     *
116
     * ```php
117
     * return [
118
     *     'action1' => 'app\components\Action1',
119
     *     'action2' => [
120
     *         'class' => 'app\components\Action2',
121
     *         'property1' => 'value1',
122
     *         'property2' => 'value2',
123
     *     ],
124
     * ];
125
     * ```
126
     *
127
     * [[\Yii::createObject()]] will be used later to create the requested action
128
     * using the configuration provided here.
129
     * @return array
130
     */
131 331
    public function actions()
132
    {
133 331
        return [];
134
    }
135
136
    /**
137
     * Runs an action within this controller with the specified action ID and parameters.
138
     * If the action ID is empty, the method will use [[defaultAction]].
139
     * @param string $id the ID of the action to be executed.
140
     * @param array $params the parameters (name-value pairs) to be passed to the action.
141
     * @return mixed the result of the action.
142
     * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
143
     * @see createAction()
144
     */
145 308
    public function runAction($id, $params = [])
146
    {
147 308
        $action = $this->createAction($id);
148 308
        if ($action === null) {
149
            throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id);
150
        }
151
152 308
        Yii::debug('Route to run: ' . $action->getUniqueId(), __METHOD__);
153
154 308
        if (Yii::$app->requestedAction === null) {
155 308
            Yii::$app->requestedAction = $action;
156
        }
157
158 308
        $oldAction = $this->action;
159 308
        $this->action = $action;
160
161 308
        $modules = [];
162 308
        $runAction = true;
163
164
        // call beforeAction on modules
165 308
        foreach ($this->getModules() as $module) {
166 308
            if ($module->beforeAction($action)) {
167 308
                array_unshift($modules, $module);
168
            } else {
169
                $runAction = false;
170
                break;
171
            }
172
        }
173
174 308
        $result = null;
175
176 308
        if ($runAction && $this->beforeAction($action)) {
177
            // run the action
178 302
            $result = $action->runWithParams($params);
179
180 296
            $result = $this->afterAction($action, $result);
181
182
            // call afterAction on modules
183 296
            foreach ($modules as $module) {
184
                /* @var $module Module */
185 296
                $result = $module->afterAction($action, $result);
186
            }
187
        }
188
189 296
        if ($oldAction !== null) {
190 7
            $this->action = $oldAction;
191
        }
192
193 296
        return $result;
194
    }
195
196
    /**
197
     * Runs a request specified in terms of a route.
198
     * The route can be either an ID of an action within this controller or a complete route consisting
199
     * of module IDs, controller ID and action ID. If the route starts with a slash '/', the parsing of
200
     * the route will start from the application; otherwise, it will start from the parent module of this controller.
201
     * @param string $route the route to be handled, e.g., 'view', 'comment/view', '/admin/comment/view'.
202
     * @param array $params the parameters to be passed to the action.
203
     * @return mixed the result of the action.
204
     * @see runAction()
205
     */
206 286
    public function run($route, $params = [])
207
    {
208 286
        $pos = strpos($route, '/');
209 286
        if ($pos === false) {
210 285
            return $this->runAction($route, $params);
211 1
        } elseif ($pos > 0) {
212 1
            return $this->module->runAction($route, $params);
213
        }
214
215
        return Yii::$app->runAction(ltrim($route, '/'), $params);
216
    }
217
218
    /**
219
     * Binds the parameters to the action.
220
     * This method is invoked by [[Action]] when it begins to run with the given parameters.
221
     * @param Action $action the action to be bound with parameters.
222
     * @param array $params the parameters to be bound to the action.
223
     * @return array the valid parameters that the action can run with.
224
     */
225 3
    public function bindActionParams($action, $params)
226
    {
227 3
        return [];
228
    }
229
230
    /**
231
     * Creates an action based on the given action ID.
232
     * The method first checks if the action ID has been declared in [[actions()]]. If so,
233
     * it will use the configuration declared there to create the action object.
234
     * If not, it will look for a controller method whose name is in the format of `actionXyz`
235
     * where `xyz` is the action ID. If found, an [[InlineAction]] representing that
236
     * method will be created and returned.
237
     * @param string $id the action ID.
238
     * @return Action|null the newly created action instance. Null if the ID doesn't resolve into any action.
239
     */
240 346
    public function createAction($id)
241
    {
242 346
        if ($id === '') {
243 3
            $id = $this->defaultAction;
244
        }
245
246 346
        $actionMap = $this->actions();
247 346
        if (isset($actionMap[$id])) {
248 15
            return Yii::createObject($actionMap[$id], [$id, $this]);
249
        }
250
251 331
        if (preg_match('/^(?:[a-z0-9_]+-)*[a-z0-9_]+$/', $id)) {
252 331
            $methodName = 'action' . str_replace(' ', '', ucwords(str_replace('-', ' ', $id)));
253 331
            if (method_exists($this, $methodName)) {
254 330
                $method = new \ReflectionMethod($this, $methodName);
255 330
                if ($method->isPublic() && $method->getName() === $methodName) {
256 330
                    return new InlineAction($id, $this, $methodName);
257
                }
258
            }
259
        }
260
261 19
        return null;
262
    }
263
264
    /**
265
     * This method is invoked right before an action is executed.
266
     *
267
     * The method will trigger the [[EVENT_BEFORE_ACTION]] event. The return value of the method
268
     * will determine whether the action should continue to run.
269
     *
270
     * In case the action should not run, the request should be handled inside of the `beforeAction` code
271
     * by either providing the necessary output or redirecting the request. Otherwise the response will be empty.
272
     *
273
     * If you override this method, your code should look like the following:
274
     *
275
     * ```php
276
     * public function beforeAction($action)
277
     * {
278
     *     // your custom code here, if you want the code to run before action filters,
279
     *     // which are triggered on the [[EVENT_BEFORE_ACTION]] event, e.g. PageCache or AccessControl
280
     *
281
     *     if (!parent::beforeAction($action)) {
282
     *         return false;
283
     *     }
284
     *
285
     *     // other custom code here
286
     *
287
     *     return true; // or false to not run the action
288
     * }
289
     * ```
290
     *
291
     * @param Action $action the action to be executed.
292
     * @return bool whether the action should continue to run.
293
     */
294 308
    public function beforeAction($action)
295
    {
296 308
        $event = new ActionEvent($action);
297 308
        $this->trigger(self::EVENT_BEFORE_ACTION, $event);
298 302
        return $event->isValid;
299
    }
300
301
    /**
302
     * This method is invoked right after an action is executed.
303
     *
304
     * The method will trigger the [[EVENT_AFTER_ACTION]] event. The return value of the method
305
     * will be used as the action return value.
306
     *
307
     * If you override this method, your code should look like the following:
308
     *
309
     * ```php
310
     * public function afterAction($action, $result)
311
     * {
312
     *     $result = parent::afterAction($action, $result);
313
     *     // your custom code here
314
     *     return $result;
315
     * }
316
     * ```
317
     *
318
     * @param Action $action the action just executed.
319
     * @param mixed $result the action return result.
320
     * @return mixed the processed action result.
321
     */
322 296
    public function afterAction($action, $result)
323
    {
324 296
        $event = new ActionEvent($action);
325 296
        $event->result = $result;
326 296
        $this->trigger(self::EVENT_AFTER_ACTION, $event);
327 296
        return $event->result;
328
    }
329
330
    /**
331
     * Returns all ancestor modules of this controller.
332
     * The first module in the array is the outermost one (i.e., the application instance),
333
     * while the last is the innermost one.
334
     * @return Module[] all ancestor modules that this controller is located within.
335
     */
336 308
    public function getModules()
337
    {
338 308
        $modules = [$this->module];
339 308
        $module = $this->module;
340 308
        while ($module->module !== null) {
341
            array_unshift($modules, $module->module);
342
            $module = $module->module;
343
        }
344
345 308
        return $modules;
346
    }
347
348
    /**
349
     * Returns the unique ID of the controller.
350
     * @return string the controller ID that is prefixed with the module ID (if any).
351
     */
352 341
    public function getUniqueId()
353
    {
354 341
        return $this->module instanceof Application ? $this->id : $this->module->getUniqueId() . '/' . $this->id;
355
    }
356
357
    /**
358
     * Returns the route of the current request.
359
     * @return string the route (module ID, controller ID and action ID) of the current request.
360
     */
361 5
    public function getRoute()
362
    {
363 5
        return $this->action !== null ? $this->action->getUniqueId() : $this->getUniqueId();
364
    }
365
366
    /**
367
     * Renders a view and applies layout if available.
368
     *
369
     * The view to be rendered can be specified in one of the following formats:
370
     *
371
     * - [path alias](guide:concept-aliases) (e.g. "@app/views/site/index");
372
     * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes.
373
     *   The actual view file will be looked for under the [[Application::viewPath|view path]] of the application.
374
     * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash.
375
     *   The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]].
376
     * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]].
377
     *
378
     * To determine which layout should be applied, the following two steps are conducted:
379
     *
380
     * 1. In the first step, it determines the layout name and the context module:
381
     *
382
     * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module;
383
     * - If [[layout]] is null, search through all ancestor modules of this controller and find the first
384
     *   module whose [[Module::layout|layout]] is not null. The layout and the corresponding module
385
     *   are used as the layout name and the context module, respectively. If such a module is not found
386
     *   or the corresponding layout is not a string, it will return false, meaning no applicable layout.
387
     *
388
     * 2. In the second step, it determines the actual layout file according to the previously found layout name
389
     *    and context module. The layout name can be:
390
     *
391
     * - a [path alias](guide:concept-aliases) (e.g. "@app/views/layouts/main");
392
     * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be
393
     *   looked for under the [[Application::layoutPath|layout path]] of the application;
394
     * - a relative path (e.g. "main"): the actual layout file will be looked for under the
395
     *   [[Module::layoutPath|layout path]] of the context module.
396
     *
397
     * If the layout name does not contain a file extension, it will use the default one `.php`.
398
     *
399
     * @param string $view the view name.
400
     * @param array $params the parameters (name-value pairs) that should be made available in the view.
401
     * These parameters will not be available in the layout.
402
     * @return string the rendering result.
403
     * @throws InvalidArgumentException if the view file or the layout file does not exist.
404
     */
405 8
    public function render($view, $params = [])
406
    {
407 8
        $content = $this->getView()->render($view, $params, $this);
408 7
        return $this->renderContent($content);
409
    }
410
411
    /**
412
     * Renders a static string by applying a layout.
413
     * @param string $content the static string being rendered
414
     * @return string the rendering result of the layout with the given static string as the `$content` variable.
415
     * If the layout is disabled, the string will be returned back.
416
     * @since 2.0.1
417
     */
418 7
    public function renderContent($content)
419
    {
420 7
        $layoutFile = $this->findLayoutFile($this->getView());
421 7
        if ($layoutFile !== false) {
422 2
            return $this->getView()->renderFile($layoutFile, ['content' => $content], $this);
423
        }
424
425 5
        return $content;
426
    }
427
428
    /**
429
     * Renders a view without applying layout.
430
     * This method differs from [[render()]] in that it does not apply any layout.
431
     * @param string $view the view name. Please refer to [[render()]] on how to specify a view name.
432
     * @param array $params the parameters (name-value pairs) that should be made available in the view.
433
     * @return string the rendering result.
434
     * @throws InvalidArgumentException if the view file does not exist.
435
     */
436
    public function renderPartial($view, $params = [])
437
    {
438
        return $this->getView()->render($view, $params, $this);
439
    }
440
441
    /**
442
     * Renders a view file.
443
     * @param string $file the view file to be rendered. This can be either a file path or a [path alias](guide:concept-aliases).
444
     * @param array $params the parameters (name-value pairs) that should be made available in the view.
445
     * @return string the rendering result.
446
     * @throws InvalidArgumentException if the view file does not exist.
447
     */
448 84
    public function renderFile($file, $params = [])
449
    {
450 84
        return $this->getView()->renderFile($file, $params, $this);
451
    }
452
453
    /**
454
     * Returns the view object that can be used to render views or view files.
455
     * The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use
456
     * this view object to implement the actual view rendering.
457
     * If not set, it will default to the "view" application component.
458
     * @return View|\yii\web\View the view object that can be used to render views or view files.
459
     */
460 92
    public function getView()
461
    {
462 92
        if ($this->_view === null) {
463 92
            $this->_view = Yii::$app->getView();
464
        }
465
466 92
        return $this->_view;
467
    }
468
469
    /**
470
     * Sets the view object to be used by this controller.
471
     * @param View|\yii\web\View $view the view object that can be used to render views or view files.
472
     */
473
    public function setView($view)
474
    {
475
        $this->_view = $view;
476
    }
477
478
    /**
479
     * Returns the directory containing view files for this controller.
480
     * The default implementation returns the directory named as controller [[id]] under the [[module]]'s
481
     * [[viewPath]] directory.
482
     * @return string the directory containing the view files for this controller.
483
     */
484 1
    public function getViewPath()
485
    {
486 1
        if ($this->_viewPath === null) {
487 1
            $this->_viewPath = $this->module->getViewPath() . DIRECTORY_SEPARATOR . $this->id;
488
        }
489
490 1
        return $this->_viewPath;
491
    }
492
493
    /**
494
     * Sets the directory that contains the view files.
495
     * @param string $path the root directory of view files.
496
     * @throws InvalidArgumentException if the directory is invalid
497
     * @since 2.0.7
498
     */
499
    public function setViewPath($path)
500
    {
501
        $this->_viewPath = Yii::getAlias($path);
502
    }
503
504
    /**
505
     * Finds the applicable layout file.
506
     * @param View $view the view object to render the layout file.
507
     * @return string|bool the layout file path, or false if layout is not needed.
508
     * Please refer to [[render()]] on how to specify this parameter.
509
     * @throws InvalidArgumentException if an invalid path alias is used to specify the layout.
510
     */
511 7
    public function findLayoutFile($view)
512
    {
513 7
        $module = $this->module;
514 7
        $layout = null;
515 7
        if (is_string($this->layout)) {
516 2
            $layout = $this->layout;
517 5
        } elseif ($this->layout === null) {
518
            while ($module !== null && $module->layout === null) {
519
                $module = $module->module;
520
            }
521
            if ($module !== null && is_string($module->layout)) {
522
                $layout = $module->layout;
523
            }
524
        }
525
526 7
        if ($layout === null) {
527 5
            return false;
528
        }
529
530 2
        if (strncmp($layout, '@', 1) === 0) {
531 1
            $file = Yii::getAlias($layout);
532 1
        } elseif (strncmp($layout, '/', 1) === 0) {
533
            $file = Yii::$app->getLayoutPath() . DIRECTORY_SEPARATOR . substr($layout, 1);
534
        } else {
535 1
            $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $layout;
0 ignored issues
show
The method getLayoutPath() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

535
            $file = $module->/** @scrutinizer ignore-call */ getLayoutPath() . DIRECTORY_SEPARATOR . $layout;

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
536
        }
537
538 2
        if (pathinfo($file, PATHINFO_EXTENSION) !== '') {
0 ignored issues
show
It seems like $file can also be of type false; however, parameter $path of pathinfo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

538
        if (pathinfo(/** @scrutinizer ignore-type */ $file, PATHINFO_EXTENSION) !== '') {
Loading history...
539 1
            return $file;
540
        }
541 1
        $path = $file . '.' . $view->defaultExtension;
0 ignored issues
show
Are you sure $file of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

541
        $path = /** @scrutinizer ignore-type */ $file . '.' . $view->defaultExtension;
Loading history...
542 1
        if ($view->defaultExtension !== 'php' && !is_file($path)) {
543
            $path = $file . '.php';
544
        }
545
546 1
        return $path;
547
    }
548
549
    /**
550
     * Fills parameters based on types and names in action method signature.
551
     * @param \ReflectionType $type The reflected type of the action parameter.
552
     * @param string $name The name of the parameter.
553
     * @param array &$args The array of arguments for the action, this function may append items to it.
554
     * @param array &$requestedParams The array with requested params, this function may write specific keys to it.
555
     * @throws ErrorException when we cannot load a required service.
556
     * @throws InvalidConfigException Thrown when there is an error in the DI configuration.
557
     * @throws NotInstantiableException Thrown when a definition cannot be resolved to a concrete class
558
     * (for example an interface type hint) without a proper definition in the container.
559
     * @since 2.0.36
560
     */
561 11
    final protected function bindInjectedParams(\ReflectionType $type, $name, &$args, &$requestedParams)
562
    {
563
        // Since it is not a builtin type it must be DI injection.
564 11
        $typeName = $type->getName();
565 11
        if (($component = $this->module->get($name, false)) instanceof $typeName) {
566 8
            $args[] = $component;
567 8
            $requestedParams[$name] = 'Component: ' . get_class($component) . " \$$name";
568 11
        } elseif ($this->module->has($typeName) && ($service = $this->module->get($typeName)) instanceof $typeName) {
569 2
            $args[] = $service;
570 2
            $requestedParams[$name] = 'Module ' . get_class($this->module) . " DI: $typeName \$$name";
571 9
        } elseif (\Yii::$container->has($typeName) && ($service = \Yii::$container->get($typeName)) instanceof $typeName) {
572 2
            $args[] = $service;
573 2
            $requestedParams[$name] = "Container DI: $typeName \$$name";
574 6
        } elseif ($type->allowsNull()) {
575 4
            $args[] = null;
576 4
            $requestedParams[$name] = "Unavailable service: $name";
577
        } else {
578 2
            throw new Exception('Could not load required service: ' . $name);
579
        }
580
    }
581
}
582