core/base/Module.php

Summary

Maintainability
A
25 mins
Test Coverage
<?php

namespace luya\base;

use luya\console\interfaces\ImportControllerInterface;
use luya\helpers\ObjectHelper;
use Yii;
use yii\base\Application;
use yii\base\InvalidConfigException;

/**
 * LUYA Module base class.
 *
 * The module class provides url rule defintions and other helper methods.
 *
 * In order to use a module within the CMS context it must extend from this base module class.
 *
 * @property array $urlRules Contains all urlRules for this module. You can either provide a full {{luya\web\UrlRule}} object configuration as array like this:
 * ```php
 * 'urlRules' => [
 *     ['pattern' => 'mymodule/detail/<id:\d+>', 'route' => 'mymodule/detail/user'],
 * ],
 * ```
 *
 * Or you can provide a key value pairing where key is the pattern and the value is the route:
 *
 * ```php
 * 'urlRules' => [
 *     'mymodule/detail/<id:\d+>' => 'mymodule/detail/user',
 * ],
 * ```
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.0
 */
abstract class Module extends \yii\base\Module
{
    /**
     * @var array Contains the apis for each module to provided them in the admin module. They represents
     * the name of the api and the value represents the class. Example value:
     *
     * ```php
     * [
     *     'api-admin-user' => 'admin\apis\UserController',
     *     'api-cms-navcontainer' => 'admin\apis\NavContainerController'
     * ]
     * ```
     */
    public $apis = [];

    /**
     * @var array An array with additional rules for a given api name. This allows you to extend and customize the {{yii\rest\UrlRule}} for
     * a given API. Example:
     *
     * ```php
     * 'apiRules' => [
     *     'api-admin-user' => ['extraPattern' => ['GET {id}/login-list' => 'logins']],
     *     'api-admin-group' => ['exception' => ['this-action']],
     * ],
     * ```
     *
     * You can define all properties from {{yii\rest\UrlRule}}.
     *
     * @since 1.0.10
     */
    public $apiRules = [];

    /**
     * @var array An array with Tag class names to inject into the tag parser on luya boot, where key is the identifier and value the create object conifg:
     *
     * ```php
     * [
     *     'link' => 'luya\cms\tags\LinkTag',
     *     'file' => ['class' => 'luya\admin\tags\FileTag'],
     * ]
     * ```
     *
     * As by default the yii2 configurable object you can also pass properties to your tag object in order to configure them.
     */
    public $tags = [];

    private $_urlRules = [];

    /**
     * UrlRules for this module. You can either provide a full {{luya\web\UrlRule}}
     * object configuration as array like this:
     *
     * ```php
     * 'urlRules' => [
     *     ['pattern' => 'mymodule/detail/<id:\d+>', 'route' => 'mymodule/detail/user'],
     * ],
     * ```
     *
     * Or you can provide a key value pairing where key is the pattern and the value is the route:
     *
     * ```php
     * 'urlRules' => [
     *     'mymodule/detail/<id:\d+>' => 'mymodule/detail/user',
     * ],
     * ```
     *
     * @var array $rules Contains all urlRules for this module. You can either provide a full {{luya\web\UrlRule}}
     * object configuration as array
     * @since 1.0.1
     */
    public function setUrlRules(array $rules)
    {
        $this->_urlRules = $rules;
    }

    /**
     * Getter method for urlRules.
     *
     * > Never use the getter method, use the $urlRules virtual property as it provides backwards compatibility.
     *
     * @return array
     * @since 1.0.1
     */
    public function getUrlRules()
    {
        return $this->_urlRules;
    }

    /**
     * @var array An array containing all components which should be registered for the current module. If
     * the component does not exists an Exception will be thrown.
     */
    public $requiredComponents = [];

    /**
     * @var bool Defines the location of the layout file whether in the @app namespace or a module:
     *
     * - true = looking for layout file in `@app/views/<ID>/layouts`.
     * - false = looking for layout file in `@module/views/layouts/`.
     *
     * This variable is only available if your not in a context call. A context call would be if the cms renders the module.
     */
    public $useAppLayoutPath = true;

    /**
     * @var bool Define the location of the view files inside the controller actions
     *
     * - true = the view path of the @app/views
     * - false = the view path of the @modulename/views
     *
     */
    public $useAppViewPath = false;

    /**
     * @var array mapping from action ID to view configurations.
     * Each name-value pair specifies the configuration of a specifed oder wildcard action to a single view folder.
     * The first match.
     *
     * For example:
     *
     * ```php
     * [
     *   'default/index' => '@app/views/mymodule/default',
     *   'login/info' => '@app/views/mymodule/login',
     *   'default/*' => '@app/views/mymodule/default',
     *   '*' => '@app/views/mymodule',
     * ]
     * ```
     *
     * > Keep in mind, the viewMap only works when `useAppViewPath` is `false`.
     *
     * @since 1.0.11
     */
    public $viewMap = [];

    /**
     * @var string if this/the module is included via another module (parent module), the parent module will write its
     * name inside the child modules $context variable. For example the cms includes the news module, the context variable
     * of news would have the value "cms".
     */
    public $context;

    /**
     * @var string The default name of the moduleLayout
     */
    public $moduleLayout = 'layout';

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        // verify all the components
        foreach ($this->requiredComponents as $component) {
            if (!Yii::$app->has($component)) {
                throw new InvalidConfigException(sprintf('The required component "%s" is not registered in the configuration file', $component));
            }
        }

        static::onLoad();
    }

    /**
     * The LUYA Bootstrap method will be invoken when the application starts.
     *
     * As LUYA modules will be loaded while bootstraping, this method will ALWAYS be invoken when
     * the application starts.
     *
     * Compared to the {{yii\base\BootstrapInterface}} the module or class must still be configured
     * to bootstrap in the configuration section, the {{luyaBootstrap}} will be invoken always.
     *
     * @param Application $app
     * @since 1.0.21
     */
    public function luyaBootstrap(Application $app)
    {
    }

    /**
     * Override the default implementation of Yii's getLayoutPath(). If the property `$useAppLayoutPath` is true,
     * the *@app* namespace views will be looked up for view files.
     * Else the layout path of the active theme will be used.
     *
     * @return string
     * @see \yii\base\Module::getLayoutPath()
     */
    public function getLayoutPath()
    {
        if (Yii::$app->themeManager->hasActiveTheme && $this->useAppLayoutPath) {
            $this->setLayoutPath(Yii::$app->themeManager->activeTheme->layoutPath);
        } elseif ($this->useAppLayoutPath) {
            $this->setLayoutPath('@app/views/'.$this->id.'/layouts');
        }

        return parent::getLayoutPath();
    }

    /**
     * Extract the current module from the route and return the new resolved route.
     *
     * @param string $route Route to resolve, e.g. `admin/default/index`
     * @return string The resolved route without the module id `default/index` when input was `admin/default/index`
     * and the current module id is `admin`.
     */
    public function resolveRoute($route)
    {
        $routeParts = explode('/', $route);
        foreach ($routeParts as $k => $v) {
            if (($k == 0 && $v == $this->id) || (empty($v))) {
                unset($routeParts[$k]);
            }
        }
        if (count($routeParts) == 0) {
            return $this->defaultRoute;
        }

        return implode('/', $routeParts);
    }

    /**
     * register a component to the application. id => definition. All components will be registered during bootstrap process.
     *
     * @return array
     */
    public function registerComponents()
    {
        return [];
    }

    /**
     * Define a last of importer class with an array or run code directily with the import() method.
     *
     * Can be either an array with classes:
     *
     * ```php
     * public function import(ImportControllerInterface $importer)
     * {
     *     return [
     *         'path\to\class\Import',
     *         MyImporterClass::className(),
     *     ];
     * }
     * ```
     *
     * Or a direct functional call which executes importer things:
     *
     * ```php
     * public function import(ImportControllerInterface $importer)
     * {
     *     foreach ($importer->getDirectoryFiles('blocks') as $block) {
     *         // do something with block file.
     *     }
     * }
     * ```
     *
     * @param \luya\console\interfaces\ImportControllerInterface $importer The importer controller class which will be invoke to the import method.
     * @return boolean|array If an array is returned it must contain object class to created extending from {{luya\console\Command}}.
     */
    public function import(ImportControllerInterface $importer)
    {
        return false;
    }

    /**
     * returns "luya\base" for example.
     *
     * @return string
     */
    public function getNamespace()
    {
        return implode('\\', array_slice(explode('\\', get_class($this)), 0, -1));
    }

    /**
     * Returns all controller files of this module from the `getControllerPath()` folder, where the key is the reusable
     * id of this controller and value the file on the server.
     *
     * @return array Returns an array where the key is the controller id and value the original file.
     */
    public function getControllerFiles()
    {
        return ObjectHelper::getControllers($this);
    }

    /**
     * Overrides the yii2 default behavior by not throwing an exception if no alias has been defined
     * for the controller namespace. Otherwise each module requires an alias for its first namepsace entry
     * which results into exception for external modules without an alias.
     * exception.
     *
     * @inheritdoc
     */
    public function getControllerPath()
    {
        return Yii::getAlias('@' . str_replace('\\', '/', $this->controllerNamespace), false);
    }

    // STATIC METHODS

    /**
     * Internal used to register the translations from the translation array or set alias paths.
     *
     * This is a static behavior, so we can call this call without the object context, for example when
     * the composer plugin registers blocks but the module is not registered with translations.
     *
     * @return void
     */
    public static function onLoad()
    {
    }

    /**
     * Register a Translation to the i18n component.
     *
     * In order to register Translations you can register them inside the {{luya\base\Module::onLoad()}} method.
     *
     * ```php
     * public static function onLoad()
     * {
     *     $this->registerTranslation('mymodule*', static::staticBasePath() . '/messages', [
     *         'mymodule' => 'mymodule.php',
     *         'mymodule/sub' => 'sub.php',
     *     ]);
     * }
     * ```
     *
     * @param string $prefix The prefix of which the messages are indicated
     * @param string $basePath The path to the messages folder where the messages are located.
     * @param array $fileMap The files mapping inside the messages folder.
     */
    public static function registerTranslation($prefix, $basePath, array $fileMap)
    {
        if (!isset(Yii::$app->i18n->translations[$prefix])) {
            Yii::$app->i18n->translations[$prefix] = [
                'class' => 'yii\i18n\PhpMessageSource',
                'basePath' => $basePath,
                'fileMap' => $fileMap,
            ];
        }
    }

    /**
     * Get base path from static view port.
     *
     * @return string
     */
    public static function staticBasePath()
    {
        $class = new \ReflectionClass(static::class);

        return dirname($class->getFileName());
    }

    /**
     * Base translation method which invokes the onLoad function.
     *
     * This makes it possible to register module translations without adding the module
     * to the components list. This is very important for luya extensions.
     *
     * @param string $category the message category.
     * @param string $message the message to be translated.
     * @param array $params the parameters that will be used to replace the corresponding placeholders in the message.
     * @param string $language the language code (e.g. `en-US`, `en`). If this is null, the current
     * [[\yii\base\Application::language|application language]] will be used.
     * @return string the translated message.
     */
    public static function baseT($category, $message, array $params = [], $language = null)
    {
        static::onLoad();
        return Yii::t($category, $message, $params, $language);
    }
}