jii
Version:
Jii - Full-Stack JavaScript Framework
554 lines (486 loc) • 19.1 kB
JavaScript
/**
* @author Vladimir Kozhin <affka@affka.ru>
* @license MIT
*/
'use strict';
const Jii = require('../BaseJii');
const Action = require('./Action');
const Response = require('./Response');
const Controller = require('./Controller');
const AnonymousAction = require('../request/AnonymousAction');
const InvalidRouteException = require('../exceptions/InvalidRouteException');
const InvalidConfigException = require('../exceptions/InvalidConfigException');
const ActionEvent = require('./ActionEvent');
const ErrorHandler = require('./ErrorHandler');
const _trimStart = require('lodash/trimStart');
const _trim = require('lodash/trim');
const _lastIndexOf = require('lodash/lastIndexOf');
const _isFunction = require('lodash/isFunction');
const _indexOf = require('lodash/indexOf');
const _isUndefined = require('lodash/isUndefined');
const _isString = require('lodash/isString');
const _map = require('lodash/map');
const _each = require('lodash/each');
const _has = require('lodash/has');
const Context = require('./Context');
class Module extends Context {
preInit(id, moduleObject, config) {
/**
* The root directory that contains layout view files for this module.
* @type {string}
*/
this._layoutPath = null;
/**
* The root directory that contains view files for this module
* @type {string}
*/
this._viewPath = null;
/**
* Stored controller instances
* @type {object}
*/
this._controllers = {};
/**
* @type {string|null}
*/
this.errorRoute = null;
/**
* The layout that should be applied for views within this module. This refers to a view name
* relative to [[layoutPath]]. If this is not set, it means the layout value of the [[module|parent module]]
* will be taken. If this is false, layout will be disabled within this module.
* @type {string|boolean}
*/
this.layout = null;
/**
* The default route of this module. Defaults to 'default'.
* The route may consist of child module ID, controller ID, and/or action ID.
* For example, `help`, `post/create`, `admin/post/create`.
* If action ID is not given, it will take the default value as specified in defaultAction.
* @type {string}
*/
this.defaultRoute = 'default';
/**
* String the namespace that controller classes are in. If not set,
* it will use the "controllers" sub-namespace under the namespace of this module.
* For example, if the namespace of this module is "foo\bar", then the default
* controller namespace would be "foo\bar\controllers".
* @type {string}
*/
this.controllerNamespace = null;
/**
* Mapping from controller ID to controller configurations.
* Each name-value pair specifies the configuration of a single controller.
* A controller configuration can be either a string or an array.
* If the former, the string should be the fully qualified class name of the controller.
* If the latter, the array must contain a 'class' element which specifies
* the controller's fully qualified class name, and the rest of the name-value pairs
* in the array are used to initialize the corresponding controller properties. For example,
*
* ~~~
* {
* account: 'app.controllers.UserController',
* article: {
* className: 'app.controllers.PostController',
* pageTitle: 'something new'
* }
* }
* ~~~
* @type {object}
*/
this.controllerMap = {};
/**
* @type {{string: function}}
*/
this.inlineActions = {};
/**
* @type {string}
*/
this.id = id;
/**
* The parent module of this module. Null if this module does not have a parent.
* @type {Module}
*/
this.module = moduleObject;
/**
* @type {object}
*/
this._modules = {};
super.preInit(config);
}
init() {
// TODO
/*if (this.controllerNamespace === null) {
var index = _lastIndexOf(this.className(), '.');
this.controllerNamespace = this.className().substr(0, index);
}*/
}
/**
*
* @returns {Promise}
*/
start() {
return Promise.all(_map(this._components, component => {
return _isFunction(component.start) ? component.start() : null;
})).then(() => {
return Promise.all(_map(this._modules, module => {
return module.start();
}));
});
}
/**
*
* @returns {Promise}
*/
stop() {
var promises = [];
_each(this._components, component => {
if (_isFunction(component.stop)) {
promises.push(component.stop());
}
});
return Promise.all(promises);
}
getUniqueId() {
if (this.module) {
var id = this.module.getUniqueId() + '/' + this.id;
return _trimStart(id, '/');
}
return this.id;
}
/**
* Returns the directory that contains the controller classes according to [[controllerNamespace]].
* Note that in order for this method to return a value, you must define
* an alias for the root namespace of [[controllerNamespace]].
* @return {string} the directory that contains the controller classes.
*/
getControllerPath() {
return Jii.getAlias('@' + this.controllerNamespace.replace('.', '/'));
}
/**
* Returns the directory that contains the view files for this module.
* @return {string} the root directory of view files. Defaults to "[[basePath]]/view".
*/
getViewPath() {
if (this._viewPath === null) {
this._viewPath = Jii.app.getBasePath() + '/views';
}
return this._viewPath;
}
/**
* Sets the directory that contains the view files.
* @param {string} path the root directory of view files.
*/
setViewPath(path) {
this._viewPath = Jii.getAlias(path);
}
/**
* Returns the directory that contains layout view files for this module.
* @return {string} the root directory of layout files. Defaults to "[[viewPath]]/layouts".
*/
getLayoutPath() {
if (this._layoutPath === null) {
this._layoutPath = this.getViewPath() + '/layouts';
}
return this._layoutPath;
}
/**
* Sets the directory that contains the layout files.
* @param {string} path the root directory of layout files.
*/
setLayoutPath(path) {
this._layoutPath = Jii.getAlias(path);
}
/**
* Checks whether the child module of the specified ID exists.
* This method supports checking the existence of both child and grand child modules.
* @param {string} id module ID. For grand child modules, use ID path relative to this module (e.g. `admin/content`).
* @return {boolean} whether the named module exists. Both loaded and unloaded modules
* are considered.
*/
hasModule(id) {
var index = _indexOf(id, '.');
if (index !== -1) {
var moduleId = id.substr(0, index);
var childModuleId = id.substr(index + 1);
// Check sub-module
var moduleObject = this.getModule(moduleId);
return moduleObject !== null ? moduleObject.hasModule(childModuleId) : false;
}
return _has(this._modules[id]);
}
/**
* Retrieves the child module of the specified ID.
* This method supports retrieving both child modules and grand child modules.
* @param {string} id module ID (case-sensitive). To retrieve grand child modules,
* use ID path relative to this module (e.g. `admin/content`).
* @return {Module} the module instance, null if the module does not exist.
*/
getModule(id) {
// Get sub-module
var index = _indexOf(id, '.');
if (index !== -1) {
var moduleId = id.substr(0, index);
var childModuleId = id.substr(index + 1);
var moduleObject = this.getModule(moduleId);
return moduleObject !== null ? moduleObject.getModule(childModuleId) : null;
}
return this._modules[id] || null;
}
/**
* Adds a sub-module to this module.
* @param {string} id module ID
* @param {Module|array|null} moduleObject the sub-module to be added to this module. This can
* be one of the followings:
*
* - a [[Module]] object
* - a configuration array: when [[getModule()]] is called initially, the array
* will be used to instantiate the sub-module
* - null: the named sub-module will be removed from this module
*/
setModule(id, moduleObject) {
if (moduleObject === null) {
delete this._modules[id];
} else {
// Create module instance
if (!(moduleObject instanceof module.exports)) {
moduleObject = Jii.createObject(moduleObject, id, this);
}
// Add link
this._modules[id] = moduleObject;
}
}
/**
* Returns the sub-modules in this module.
* @return {Module[]} the modules (indexed by their IDs)
*/
getModules() {
return this._modules;
}
/**
* Registers sub-modules in the current module.
*
* Each sub-module should be specified as a name-value pair, where
* name refers to the ID of the module and value the module or a configuration
* array that can be used to create the module. In the latter case, [[Jii.createObject()]]
* will be used to create the module.
*
* If a new sub-module has the same ID as an existing one, the existing one will be overwritten silently.
*
* The following is an example for registering two sub-modules:
*
* ~~~
* [
* 'comment' => [
* 'class' => 'app\modules\comment\CommentModule',
* 'db' => 'db',
* ],
* 'booking' => ['class' => 'app\modules\booking\BookingModule'],
* ]
* ~~~
*
* @param {object} modules modules (id => module configuration or instances)
*/
setModules(modules) {
_each(modules, (moduleObject, id) => {
this.setModule(id, moduleObject);
});
}
/**
* Runs a controller action specified by a route.
* This method parses the specified route and creates the corresponding child module(s), controller and action
* instances. It then calls [[Controller::runAction()]] to run the action with the given parameters.
* If the route is empty, the method will use [[defaultRoute]].
* @param {string} route the route that specifies the action.
* @param {Context} context
* @return {Promise} the result of the action.
* @throws {InvalidRouteException} if the requested route cannot be resolved into an action successfully
*/
runAction(route, context) {
var routeParams = this._parseRoute(route);
return Promise.resolve().then(() => {
var fullRoute = routeParams.id + '/' + (routeParams.route || 'index');
if (_has(this.inlineActions, fullRoute)) {
var action = this.inlineActions[fullRoute] instanceof Action ? this.inlineActions[fullRoute] : new AnonymousAction(fullRoute, this, this.inlineActions[fullRoute]);
return this.beforeAction(action, context).then(result => {
if (result === false) {
return Promise.reject();
}
return Promise.resolve().then(() => {
return action.runWithParams(context);
}).then(data => {
if (!_isUndefined(data) && context.response instanceof Response) {
context.response.data = data;
context.response.send();
}
return this.beforeAction(action, context).then(() => {
return data;
});
});
});
}
var parts = this.createController(route);
if (parts !== null) {
/** @type {Controller} */
var controller = parts[0];
var actionId = parts[1];
return controller.runAction(actionId, context);
}
var id = this.getUniqueId();
var requestName = id ? id + '/' + route : route;
//throw new InvalidRouteException('Unable to resolve the request `' + requestName + '`.');
Jii.info('Unable to resolve the request `' + requestName + '`.');
}).catch(e => {
Jii.catchHandler(e);
var errorRoute = this._findErrorRoute();
if (errorRoute !== null && route !== errorRoute) {
context.setComponent('errorHandler', {
className: ErrorHandler,
error: e
});
this.runAction(errorRoute, context);
}
});
}
/**
*
* @param {string} route
* @return {boolean}
*/
existsRoute(route) {
var routeParams = this._parseRoute(route);
var id = routeParams.id;
route = routeParams.route;
if (_has(this.inlineActions, id + '/' + (route || 'index'))) {
return true;
}
if (_has(this.controllerMap, id)) {
return true;
}
var moduleObject = this.getModule(id);
if (moduleObject !== null) {
return moduleObject.existsRoute(route);
}
if (/^[a-z0-9-_]+$/.test(id)) {
var className = id.charAt(0).toUpperCase() + id.slice(1).replace(/-([a-z])/g, (m, v) => v.toUpperCase()) + 'Controller';
if (this.controllerMap[className]) {
return true;
}
}
return false;
}
/**
* Creates a controller instance based on the controller ID.
*
* The controller is created within this module. The method first attempts to
* create the controller based on the [[controllerMap]] of the module.
*
* @param {string} route the route consisting of module, controller and action IDs.
* @return {[]|null} If the controller is created successfully, it will be returned together
* with the requested action ID. Otherwise false will be returned.
* @throws {InvalidConfigException} if the controller class and its file do not match.
*/
createController(route) {
var routeParams = this._parseRoute(route);
var id = routeParams.id;
route = routeParams.route;
var controller = null;
if (_has(this.controllerMap, id)) {
controller = Jii.createObject(this.controllerMap[id], id, this);
return controller !== null ? [
controller,
route
] : null;
}
var moduleObject = this.getModule(id);
if (moduleObject !== null) {
return moduleObject.createController(route);
}
if (/^[a-z0-9-_]+$/.test(id)) {
var className = id.charAt(0).toUpperCase() + id.slice(1).replace(/-([a-z])/g, (m, v) => v.toUpperCase()) + 'Controller';
if (_has(this.controllerMap, className)) {
controller = Jii.createObject(this.controllerMap[className], id, this);
return controller !== null ? [
controller,
route
] : null;
}
}
return controller !== null ? [
controller,
route
] : null;
}
_parseRoute(route) {
if (route === '') {
route = this.defaultRoute;
}
route = _trim(route, '/');
var index = route.indexOf('/');
if (index !== -1) {
return {
id: route.substr(0, index),
route: route.substr(index + 1)
};
}
return {
id: route,
route: ''
};
}
/**
* This method is invoked right before an action of this module is to be executed (after all possible filters.)
* You may override this method to do last-minute preparation for the action.
* Make sure you call the parent implementation so that the relevant event is triggered.
* @param {Action} action the action to be executed.
* @param {Context} context
* @return {Promise}
*/
beforeAction(action, context) {
this.trigger(Module.EVENT_BEFORE_ACTION, new ActionEvent({
action: action,
context: context
}));
return Promise.resolve(true);
}
/**
* This method is invoked right after an action of this module has been executed.
* You may override this method to do some postprocessing for the action.
* Make sure you call the parent implementation so that the relevant event is triggered.
* @param {Action} action the action just executed.
* @param {Context} context
* @return {Promise}
*/
afterAction(action, context) {
this.trigger(Module.EVENT_AFTER_ACTION, new ActionEvent({
action: action,
context: context
}));
return Promise.resolve();
}
_findErrorRoute() {
var module = this.module;
var errorRoute = null;
if (_isString(this.errorRoute)) {
errorRoute = this.errorRoute;
} else if (this.errorRoute === null) {
while (module !== null && module.errorRoute === null) {
module = module.module;
}
if (module !== null && _isString(module.errorRoute)) {
errorRoute = module.errorRoute;
}
}
return errorRoute;
}
}
/**
* @event Module#afterAction
* @property {ActionEvent} event
*/
Module.EVENT_AFTER_ACTION = 'afterAction';
/**
* @event Module#beforeAction
* @property {ActionEvent} event
*/
Module.EVENT_BEFORE_ACTION = 'beforeAction';
module.exports = Module;