Module.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\base;
  8. use Yii;
  9. /**
  10. * Module is the base class for module and application classes.
  11. *
  12. * A module represents a sub-application which contains MVC elements by itself, such as
  13. * models, views, controllers, etc.
  14. *
  15. * A module may consist of [[modules|sub-modules]].
  16. *
  17. * [[components|Components]] may be registered with the module so that they are globally
  18. * accessible within the module.
  19. *
  20. * @property array $aliases List of path aliases to be defined. The array keys are alias names (must start
  21. * with '@') and the array values are the corresponding paths or aliases. See [[setAliases()]] for an example.
  22. * This property is write-only.
  23. * @property string $basePath The root directory of the module.
  24. * @property array $components The components (indexed by their IDs).
  25. * @property string $controllerPath The directory that contains the controller classes.
  26. * @property string $layoutPath The root directory of layout files. Defaults to "[[viewPath]]/layouts".
  27. * @property array $modules The modules (indexed by their IDs).
  28. * @property string $uniqueId The unique ID of the module. This property is read-only.
  29. * @property string $viewPath The root directory of view files. Defaults to "[[basePath]]/view".
  30. *
  31. * @author Qiang Xue <[email protected]>
  32. * @since 2.0
  33. */
  34. class Module extends Component
  35. {
  36. /**
  37. * @var array custom module parameters (name => value).
  38. */
  39. public $params = [];
  40. /**
  41. * @var array the IDs of the components or modules that should be preloaded right after initialization.
  42. */
  43. public $preload = [];
  44. /**
  45. * @var string an ID that uniquely identifies this module among other modules which have the same [[module|parent]].
  46. */
  47. public $id;
  48. /**
  49. * @var Module the parent module of this module. Null if this module does not have a parent.
  50. */
  51. public $module;
  52. /**
  53. * @var string|boolean the layout that should be applied for views within this module. This refers to a view name
  54. * relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]]
  55. * will be taken. If this is false, layout will be disabled within this module.
  56. */
  57. public $layout;
  58. /**
  59. * @var array mapping from controller ID to controller configurations.
  60. * Each name-value pair specifies the configuration of a single controller.
  61. * A controller configuration can be either a string or an array.
  62. * If the former, the string should be the fully qualified class name of the controller.
  63. * If the latter, the array must contain a 'class' element which specifies
  64. * the controller's fully qualified class name, and the rest of the name-value pairs
  65. * in the array are used to initialize the corresponding controller properties. For example,
  66. *
  67. * ~~~
  68. * [
  69. * 'account' => 'app\controllers\UserController',
  70. * 'article' => [
  71. * 'class' => 'app\controllers\PostController',
  72. * 'pageTitle' => 'something new',
  73. * ],
  74. * ]
  75. * ~~~
  76. */
  77. public $controllerMap = [];
  78. /**
  79. * @var string the namespace that controller classes are in. If not set,
  80. * it will use the "controllers" sub-namespace under the namespace of this module.
  81. * For example, if the namespace of this module is "foo\bar", then the default
  82. * controller namespace would be "foo\bar\controllers".
  83. */
  84. public $controllerNamespace;
  85. /**
  86. * @return string the default route of this module. Defaults to 'default'.
  87. * The route may consist of child module ID, controller ID, and/or action ID.
  88. * For example, `help`, `post/create`, `admin/post/create`.
  89. * If action ID is not given, it will take the default value as specified in
  90. * [[Controller::defaultAction]].
  91. */
  92. public $defaultRoute = 'default';
  93. /**
  94. * @var string the root directory of the module.
  95. */
  96. private $_basePath;
  97. /**
  98. * @var string the root directory that contains view files for this module
  99. */
  100. private $_viewPath;
  101. /**
  102. * @var string the root directory that contains layout view files for this module.
  103. */
  104. private $_layoutPath;
  105. /**
  106. * @var string the directory containing controller classes in the module.
  107. */
  108. private $_controllerPath;
  109. /**
  110. * @var array child modules of this module
  111. */
  112. private $_modules = [];
  113. /**
  114. * @var array components registered under this module
  115. */
  116. private $_components = [];
  117. /**
  118. * Constructor.
  119. * @param string $id the ID of this module
  120. * @param Module $parent the parent module (if any)
  121. * @param array $config name-value pairs that will be used to initialize the object properties
  122. */
  123. public function __construct($id, $parent = null, $config = [])
  124. {
  125. $this->id = $id;
  126. $this->module = $parent;
  127. parent::__construct($config);
  128. }
  129. /**
  130. * Getter magic method.
  131. * This method is overridden to support accessing components
  132. * like reading module properties.
  133. * @param string $name component or property name
  134. * @return mixed the named property value
  135. */
  136. public function __get($name)
  137. {
  138. if ($this->hasComponent($name)) {
  139. return $this->getComponent($name);
  140. } else {
  141. return parent::__get($name);
  142. }
  143. }
  144. /**
  145. * Checks if a property value is null.
  146. * This method overrides the parent implementation by checking
  147. * if the named component is loaded.
  148. * @param string $name the property name or the event name
  149. * @return boolean whether the property value is null
  150. */
  151. public function __isset($name)
  152. {
  153. if ($this->hasComponent($name)) {
  154. return $this->getComponent($name) !== null;
  155. } else {
  156. return parent::__isset($name);
  157. }
  158. }
  159. /**
  160. * Initializes the module.
  161. * This method is called after the module is created and initialized with property values
  162. * given in configuration. The default implementation will call [[preloadComponents()]] to
  163. * load components that are declared in [[preload]].
  164. *
  165. * If you override this method, please make sure you call the parent implementation.
  166. */
  167. public function init()
  168. {
  169. if ($this->controllerNamespace === null) {
  170. $class = get_class($this);
  171. if (($pos = strrpos($class, '\\')) !== false) {
  172. $this->controllerNamespace = substr($class, 0, $pos) . '\\controllers';
  173. }
  174. }
  175. $this->preloadComponents();
  176. }
  177. /**
  178. * Returns an ID that uniquely identifies this module among all modules within the current application.
  179. * Note that if the module is an application, an empty string will be returned.
  180. * @return string the unique ID of the module.
  181. */
  182. public function getUniqueId()
  183. {
  184. return $this->module ? ltrim($this->module->getUniqueId() . '/' . $this->id, '/') : $this->id;
  185. }
  186. /**
  187. * Returns the root directory of the module.
  188. * It defaults to the directory containing the module class file.
  189. * @return string the root directory of the module.
  190. */
  191. public function getBasePath()
  192. {
  193. if ($this->_basePath === null) {
  194. $class = new \ReflectionClass($this);
  195. $this->_basePath = dirname($class->getFileName());
  196. }
  197. return $this->_basePath;
  198. }
  199. /**
  200. * Sets the root directory of the module.
  201. * This method can only be invoked at the beginning of the constructor.
  202. * @param string $path the root directory of the module. This can be either a directory name or a path alias.
  203. * @throws InvalidParamException if the directory does not exist.
  204. */
  205. public function setBasePath($path)
  206. {
  207. $path = Yii::getAlias($path);
  208. $p = realpath($path);
  209. if ($p !== false && is_dir($p)) {
  210. $this->_basePath = $p;
  211. } else {
  212. throw new InvalidParamException("The directory does not exist: $path");
  213. }
  214. }
  215. /**
  216. * Returns the directory that contains the controller classes.
  217. * Defaults to "[[basePath]]/controllers".
  218. * @return string the directory that contains the controller classes.
  219. */
  220. public function getControllerPath()
  221. {
  222. if ($this->_controllerPath !== null) {
  223. return $this->_controllerPath;
  224. } else {
  225. return $this->_controllerPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'controllers';
  226. }
  227. }
  228. /**
  229. * Sets the directory that contains the controller classes.
  230. * @param string $path the directory that contains the controller classes.
  231. * This can be either a directory name or a path alias.
  232. * @throws InvalidParamException if the directory is invalid
  233. */
  234. public function setControllerPath($path)
  235. {
  236. $this->_controllerPath = Yii::getAlias($path);
  237. }
  238. /**
  239. * Returns the directory that contains the view files for this module.
  240. * @return string the root directory of view files. Defaults to "[[basePath]]/view".
  241. */
  242. public function getViewPath()
  243. {
  244. if ($this->_viewPath !== null) {
  245. return $this->_viewPath;
  246. } else {
  247. return $this->_viewPath = $this->getBasePath() . DIRECTORY_SEPARATOR . 'views';
  248. }
  249. }
  250. /**
  251. * Sets the directory that contains the view files.
  252. * @param string $path the root directory of view files.
  253. * @throws InvalidParamException if the directory is invalid
  254. */
  255. public function setViewPath($path)
  256. {
  257. $this->_viewPath = Yii::getAlias($path);
  258. }
  259. /**
  260. * Returns the directory that contains layout view files for this module.
  261. * @return string the root directory of layout files. Defaults to "[[viewPath]]/layouts".
  262. */
  263. public function getLayoutPath()
  264. {
  265. if ($this->_layoutPath !== null) {
  266. return $this->_layoutPath;
  267. } else {
  268. return $this->_layoutPath = $this->getViewPath() . DIRECTORY_SEPARATOR . 'layouts';
  269. }
  270. }
  271. /**
  272. * Sets the directory that contains the layout files.
  273. * @param string $path the root directory of layout files.
  274. * @throws InvalidParamException if the directory is invalid
  275. */
  276. public function setLayoutPath($path)
  277. {
  278. $this->_layoutPath = Yii::getAlias($path);
  279. }
  280. /**
  281. * Defines path aliases.
  282. * This method calls [[Yii::setAlias()]] to register the path aliases.
  283. * This method is provided so that you can define path aliases when configuring a module.
  284. * @property array list of path aliases to be defined. The array keys are alias names
  285. * (must start with '@') and the array values are the corresponding paths or aliases.
  286. * See [[setAliases()]] for an example.
  287. * @param array $aliases list of path aliases to be defined. The array keys are alias names
  288. * (must start with '@') and the array values are the corresponding paths or aliases.
  289. * For example,
  290. *
  291. * ~~~
  292. * [
  293. * '@models' => '@app/models', // an existing alias
  294. * '@backend' => __DIR__ . '/../backend', // a directory
  295. * ]
  296. * ~~~
  297. */
  298. public function setAliases($aliases)
  299. {
  300. foreach ($aliases as $name => $alias) {
  301. Yii::setAlias($name, $alias);
  302. }
  303. }
  304. /**
  305. * Checks whether the child module of the specified ID exists.
  306. * This method supports checking the existence of both child and grand child modules.
  307. * @param string $id module ID. For grand child modules, use ID path relative to this module (e.g. `admin/content`).
  308. * @return boolean whether the named module exists. Both loaded and unloaded modules
  309. * are considered.
  310. */
  311. public function hasModule($id)
  312. {
  313. if (($pos = strpos($id, '/')) !== false) {
  314. // sub-module
  315. $module = $this->getModule(substr($id, 0, $pos));
  316. return $module === null ? false : $module->hasModule(substr($id, $pos + 1));
  317. } else {
  318. return isset($this->_modules[$id]);
  319. }
  320. }
  321. /**
  322. * Retrieves the child module of the specified ID.
  323. * This method supports retrieving both child modules and grand child modules.
  324. * @param string $id module ID (case-sensitive). To retrieve grand child modules,
  325. * use ID path relative to this module (e.g. `admin/content`).
  326. * @param boolean $load whether to load the module if it is not yet loaded.
  327. * @return Module|null the module instance, null if the module does not exist.
  328. * @see hasModule()
  329. */
  330. public function getModule($id, $load = true)
  331. {
  332. if (($pos = strpos($id, '/')) !== false) {
  333. // sub-module
  334. $module = $this->getModule(substr($id, 0, $pos));
  335. return $module === null ? null : $module->getModule(substr($id, $pos + 1), $load);
  336. }
  337. if (isset($this->_modules[$id])) {
  338. if ($this->_modules[$id] instanceof Module) {
  339. return $this->_modules[$id];
  340. } elseif ($load) {
  341. Yii::trace("Loading module: $id", __METHOD__);
  342. return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this);
  343. }
  344. }
  345. return null;
  346. }
  347. /**
  348. * Adds a sub-module to this module.
  349. * @param string $id module ID
  350. * @param Module|array|null $module the sub-module to be added to this module. This can
  351. * be one of the followings:
  352. *
  353. * - a [[Module]] object
  354. * - a configuration array: when [[getModule()]] is called initially, the array
  355. * will be used to instantiate the sub-module
  356. * - null: the named sub-module will be removed from this module
  357. */
  358. public function setModule($id, $module)
  359. {
  360. if ($module === null) {
  361. unset($this->_modules[$id]);
  362. } else {
  363. $this->_modules[$id] = $module;
  364. }
  365. }
  366. /**
  367. * Returns the sub-modules in this module.
  368. * @param boolean $loadedOnly whether to return the loaded sub-modules only. If this is set false,
  369. * then all sub-modules registered in this module will be returned, whether they are loaded or not.
  370. * Loaded modules will be returned as objects, while unloaded modules as configuration arrays.
  371. * @return array the modules (indexed by their IDs)
  372. */
  373. public function getModules($loadedOnly = false)
  374. {
  375. if ($loadedOnly) {
  376. $modules = [];
  377. foreach ($this->_modules as $module) {
  378. if ($module instanceof Module) {
  379. $modules[] = $module;
  380. }
  381. }
  382. return $modules;
  383. } else {
  384. return $this->_modules;
  385. }
  386. }
  387. /**
  388. * Registers sub-modules in the current module.
  389. *
  390. * Each sub-module should be specified as a name-value pair, where
  391. * name refers to the ID of the module and value the module or a configuration
  392. * array that can be used to create the module. In the latter case, [[Yii::createObject()]]
  393. * will be used to create the module.
  394. *
  395. * If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently.
  396. *
  397. * The following is an example for registering two sub-modules:
  398. *
  399. * ~~~
  400. * [
  401. * 'comment' => [
  402. * 'class' => 'app\modules\comment\CommentModule',
  403. * 'db' => 'db',
  404. * ],
  405. * 'booking' => ['class' => 'app\modules\booking\BookingModule'],
  406. * ]
  407. * ~~~
  408. *
  409. * @param array $modules modules (id => module configuration or instances)
  410. */
  411. public function setModules($modules)
  412. {
  413. foreach ($modules as $id => $module) {
  414. $this->_modules[$id] = $module;
  415. }
  416. }
  417. /**
  418. * Checks whether the named component exists.
  419. * @param string $id component ID
  420. * @return boolean whether the named component exists. Both loaded and unloaded components
  421. * are considered.
  422. */
  423. public function hasComponent($id)
  424. {
  425. return isset($this->_components[$id]);
  426. }
  427. /**
  428. * Retrieves the named component.
  429. * @param string $id component ID (case-sensitive)
  430. * @param boolean $load whether to load the component if it is not yet loaded.
  431. * @return Component|null the component instance, null if the component does not exist.
  432. * @see hasComponent()
  433. */
  434. public function getComponent($id, $load = true)
  435. {
  436. if (isset($this->_components[$id])) {
  437. if ($this->_components[$id] instanceof Object) {
  438. return $this->_components[$id];
  439. } elseif ($load) {
  440. return $this->_components[$id] = Yii::createObject($this->_components[$id]);
  441. }
  442. }
  443. return null;
  444. }
  445. /**
  446. * Registers a component with this module.
  447. * @param string $id component ID
  448. * @param Component|array|null $component the component to be registered with the module. This can
  449. * be one of the followings:
  450. *
  451. * - a [[Component]] object
  452. * - a configuration array: when [[getComponent()]] is called initially for this component, the array
  453. * will be used to instantiate the component via [[Yii::createObject()]].
  454. * - null: the named component will be removed from the module
  455. */
  456. public function setComponent($id, $component)
  457. {
  458. if ($component === null) {
  459. unset($this->_components[$id]);
  460. } else {
  461. $this->_components[$id] = $component;
  462. }
  463. }
  464. /**
  465. * Returns the registered components.
  466. * @param boolean $loadedOnly whether to return the loaded components only. If this is set false,
  467. * then all components specified in the configuration will be returned, whether they are loaded or not.
  468. * Loaded components will be returned as objects, while unloaded components as configuration arrays.
  469. * @return array the components (indexed by their IDs)
  470. */
  471. public function getComponents($loadedOnly = false)
  472. {
  473. if ($loadedOnly) {
  474. $components = [];
  475. foreach ($this->_components as $component) {
  476. if ($component instanceof Component) {
  477. $components[] = $component;
  478. }
  479. }
  480. return $components;
  481. } else {
  482. return $this->_components;
  483. }
  484. }
  485. /**
  486. * Registers a set of components in this module.
  487. *
  488. * Each component should be specified as a name-value pair, where
  489. * name refers to the ID of the component and value the component or a configuration
  490. * array that can be used to create the component. In the latter case, [[Yii::createObject()]]
  491. * will be used to create the component.
  492. *
  493. * If a new component has the same ID as an existing one, the existing one will be overwritten silently.
  494. *
  495. * The following is an example for setting two components:
  496. *
  497. * ~~~
  498. * [
  499. * 'db' => [
  500. * 'class' => 'yii\db\Connection',
  501. * 'dsn' => 'sqlite:path/to/file.db',
  502. * ],
  503. * 'cache' => [
  504. * 'class' => 'yii\caching\DbCache',
  505. * 'db' => 'db',
  506. * ],
  507. * ]
  508. * ~~~
  509. *
  510. * @param array $components components (id => component configuration or instance)
  511. */
  512. public function setComponents($components)
  513. {
  514. foreach ($components as $id => $component) {
  515. if (!is_object($component) && isset($this->_components[$id]['class']) && !isset($component['class'])) {
  516. // set default component class
  517. $component['class'] = $this->_components[$id]['class'];
  518. }
  519. $this->_components[$id] = $component;
  520. }
  521. }
  522. /**
  523. * Loads components that are declared in [[preload]].
  524. * @throws InvalidConfigException if a component or module to be preloaded is unknown
  525. */
  526. public function preloadComponents()
  527. {
  528. foreach ($this->preload as $id) {
  529. if ($this->hasComponent($id)) {
  530. $this->getComponent($id);
  531. } elseif ($this->hasModule($id)) {
  532. $this->getModule($id);
  533. } else {
  534. throw new InvalidConfigException("Unknown component or module: $id");
  535. }
  536. }
  537. }
  538. /**
  539. * Runs a controller action specified by a route.
  540. * This method parses the specified route and creates the corresponding child module(s), controller and action
  541. * instances. It then calls [[Controller::runAction()]] to run the action with the given parameters.
  542. * If the route is empty, the method will use [[defaultRoute]].
  543. * @param string $route the route that specifies the action.
  544. * @param array $params the parameters to be passed to the action
  545. * @return mixed the result of the action.
  546. * @throws InvalidRouteException if the requested route cannot be resolved into an action successfully
  547. */
  548. public function runAction($route, $params = [])
  549. {
  550. $parts = $this->createController($route);
  551. if (is_array($parts)) {
  552. /** @var Controller $controller */
  553. list($controller, $actionID) = $parts;
  554. $oldController = Yii::$app->controller;
  555. Yii::$app->controller = $controller;
  556. $result = $controller->runAction($actionID, $params);
  557. Yii::$app->controller = $oldController;
  558. return $result;
  559. } else {
  560. $id = $this->getUniqueId();
  561. throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
  562. }
  563. }
  564. /**
  565. * Creates a controller instance based on the controller ID.
  566. *
  567. * The controller is created within this module. The method first attempts to
  568. * create the controller based on the [[controllerMap]] of the module. If not available,
  569. * it will look for the controller class under the [[controllerPath]] and create an
  570. * instance of it.
  571. *
  572. * @param string $route the route consisting of module, controller and action IDs.
  573. * @return array|boolean If the controller is created successfully, it will be returned together
  574. * with the requested action ID. Otherwise false will be returned.
  575. * @throws InvalidConfigException if the controller class and its file do not match.
  576. */
  577. public function createController($route)
  578. {
  579. if ($route === '') {
  580. $route = $this->defaultRoute;
  581. }
  582. if (strpos($route, '/') !== false) {
  583. list ($id, $route) = explode('/', $route, 2);
  584. } else {
  585. $id = $route;
  586. $route = '';
  587. }
  588. $module = $this->getModule($id);
  589. if ($module !== null) {
  590. return $module->createController($route);
  591. }
  592. if (isset($this->controllerMap[$id])) {
  593. $controller = Yii::createObject($this->controllerMap[$id], $id, $this);
  594. } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) {
  595. $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $id))) . 'Controller';
  596. $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php';
  597. if (!is_file($classFile)) {
  598. return false;
  599. }
  600. $className = ltrim($this->controllerNamespace . '\\' . $className, '\\');
  601. Yii::$classMap[$className] = $classFile;
  602. if (is_subclass_of($className, 'yii\base\Controller')) {
  603. $controller = new $className($id, $this);
  604. } elseif (YII_DEBUG) {
  605. throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller.");
  606. }
  607. }
  608. return isset($controller) ? [$controller, $route] : false;
  609. }
  610. /**
  611. * This method is invoked right before an action of this module is to be executed (after all possible filters.)
  612. * You may override this method to do last-minute preparation for the action.
  613. * Make sure you call the parent implementation so that the relevant event is triggered.
  614. * @param Action $action the action to be executed.
  615. * @return boolean whether the action should continue to be executed.
  616. */
  617. public function beforeAction($action)
  618. {
  619. return true;
  620. }
  621. /**
  622. * This method is invoked right after an action of this module has been executed.
  623. * You may override this method to do some postprocessing for the action.
  624. * Make sure you call the parent implementation so that the relevant event is triggered.
  625. * @param Action $action the action just executed.
  626. * @param mixed $result the action return result.
  627. */
  628. public function afterAction($action, &$result)
  629. {
  630. }
  631. }