Pull to refresh

Yii2: Делаем модуль для управления модулями

Reading time 8 min
Views 14K
Приветствую всех! На текущем проекте мы используем Yii2 и в процессе разработки понадобилась некая сущность как модуль.

В Yii2 уже реализована модульная система, но есть один минус в том что модуль не позволяет выводить один модуль в другом модуле, а использования виджетов тоже не подходит, т.к. это часть вида и не умеет обрабатывать действия, например входящий POST-запрос (хотя одно время мы использовали виджеты так с некими костылями).

Мы создадим модуль, который будет содержать вложенные модули, которые можно будет использовать в любом контроллере.

В итоге мы получим:

  • Модули выводятся в любом контроллере
  • Управление через БД состоянием модуля (включен/выключен) и его позицией
  • Обработка входящих запросов
  • Вывод только на определенных страницах нужные модули

За идею взято реализация модулей в CMS OpenCart.

Начнем с создания модуля dispatcher через gii и подключим его в конфиге web.php

'dispatcher' => [
     'class' => 'app\modules\dispatcher\Module',
],

В директории модуля app\modules\dispatcher создадим класс BasicModule, который наследуется от \yii\base\Module.

BasicModule.php
<?php

namespace app\modules\dispatcher;

use app\modules\dispatcher\components\Controller;
use app\modules\dispatcher\models\LayoutModule;

/**
 *
 * Class Module
 * @package app\modules\dispatcher\components
 *
 */
class BasicModule extends \yii\base\Module
{
    const POSITION_HEADER = 'header';
    const POSITION_FOOTER = 'footer';
    const POSITION_LEFT = 'left';
    const POSITION_RIGHT = 'right';

    /**
     * @var array of positions
     */
    static protected $positions = [
        self::POSITION_HEADER,
        self::POSITION_FOOTER,
        self::POSITION_LEFT,
        self::POSITION_RIGHT,
    ];

    /**
     * @var string controller name
     */
    public $defaultControllerName = 'DefaultController';

    /**
     * @var string dir of modules catalog
     */
    public $modulesDir = 'catalog';

    /**
     * @var string modules namespace
     */
    private $_modulesNamespace;

    /**
     * @var string absolute path to modules dir
     */
    public $modulePath;

    /**
     *
     * @throws \yii\base\InvalidParamException
     */
    public function init()
    {
        parent::init();

        $this->_setModuleVariables();

        $this->loadModules();
    }

    /**
     * Load modules from directory by path
     * @throws \yii\base\InvalidParamException
     */
    protected function loadModules()
    {
        $handle = opendir($this->modulePath);

        while (($dir = readdir($handle)) !== false) {
            if ($dir === '.' || $dir === '..') {
                continue;
            }

            $class = $this->_modulesNamespace . '\\' . $dir . '\\Module';

            if (class_exists($class)) {
                $this->modules = [
                    $dir => [
                        'class' => $class,
                    ],
                ];
            }
        }

        closedir($handle);
    }

    /**
     * @param $layout
     * @param array $positions
     * @return array
     * @throws \yii\base\InvalidConfigException
     */
    public function run($layout, array $positions = [])
    {
        $model = $this->findModel($layout, $positions);

        $data = [];

        foreach ($model as $item) {
            if ($controller = $this->findModuleController($item['module'])) {
                $data[$item['position']][] = \Yii::createObject($controller, [$item['module'], $this])->index();
            }
        }

        return $data;
    }

    /**
     * @param $layout_id
     * @param array $positions
     * @return array|\yii\db\ActiveRecord[]
     * @internal param $layout
     */
    public function findModel($layout_id, array $positions = [])
    {
        if (empty($positions)) {
            $positions = self::$positions;
        }

        return LayoutModule::find()
            ->where([
                'layout_id' => $layout_id,
                'position' => $positions,
                'status' => LayoutModule::STATUS_ACTIVE,
            ])->orderBy([
                'sort_order' => SORT_ASC
            ])->asArray()->all();
    }

    /**
     * @param $name
     * @return null|string
     */
    public function findModuleController($name)
    {
        $className = $this->_modulesNamespace . '\\' . $name . '\controllers\\' . $this->defaultControllerName;

        return is_subclass_of($className, Controller::class) ? $className : null;
    }

    /**
     * Set modules namespace and path
     */
    private function _setModuleVariables()
    {
        $class = new \ReflectionClass($this);
        $this->_modulesNamespace = $class->getNamespaceName() . '\\' . $this->modulesDir;
        $this->modulePath = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . $this->modulesDir;
    }
}


Унаследуем класс модуля app\modules\dispatcher\Module от BasicModule

Module.php
<?php

namespace app\modules\dispatcher;

/**
 * dispatcher module definition class
 */
class Module extends BasicModule
{
    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
    }
}


Создадим и выполним миграцию:

Миграция
    public $table = '{{%layout_module}}';

    public function safeUp()
    {
        $tableOptions = null;
        if ($this->db->driverName === 'mysql') {
            $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
        }

         $this->createTable($this->table, [
            'id' => $this->primaryKey(),
            'layout_id' => $this->integer()->notNull(), // id страницы для вывода нашего модуля
            'module' => $this->string(150)->notNull(),  //название модуля
            'status' => $this->boolean()->defaultValue(true),
            'position' => $this->string(30)->notNull(),
            'sort_order' => $this->integer()->defaultValue(1),
        ], $tableOptions);
    }

    public function safeDown()
    {
        $this->dropTable($this->table);
    }

Заполним созданную таблицу:

INSERT INTO `layout_module` VALUES ('1', '1', 'test', '1', 'header', '1');
INSERT INTO `layout_module` VALUES ('2', '1', 'test', '1', 'footer', '1');
INSERT INTO `layout_module` VALUES ('3', '1', 'test', '1', 'left', '1');

В корне нашего модуля dispatcher добавим директорию components. Создадим класс Controller который будет наследовать \yii\web\Controller. Переопределим в нем метод render().

Controller.php
<?php

namespace app\modules\dispatcher\components;

/**
 *
 * Class Controller
 * @package app\modules\dispatcher\components
 */
class Controller extends \yii\web\Controller
{
    /**
     * @param string $view
     * @param array $params
     * @return string
     * @throws \yii\base\InvalidParamException
     * @throws \yii\base\ViewNotFoundException
     * @throws \yii\base\InvalidCallException
     */
    public function render($view, $params = [])
    {
        $controller = str_replace('Controller', '', $this->module->defaultControllerName);

        $path = '@app/modules/dispatcher/' . $this->module->modulesDir . '/' . $this->id . '/views/' . $controller;

        return $this->getView()->render($path . '/' . 'index', $params, $this);
    }
}


В корне модуля dispatcher добавим директорию catalog — это родительская директория для наших модулей.

Дальше мы создаем наш первый модуль, который по своей структуре ничем не отличается от обычно модуля Yii2. Создаем директорию test, в ней создаем класс Module:

Module.php
<?php
namespace app\modules\dispatcher\catalog\test;

/**
 * test module definition class
 */
class Module extends \yii\base\Module
{
    /**
     * @inheritdoc
     */
    public $controllerNamespace = 'app\modules\dispatcher\catalog\test\controllers';
}


Создаем директорию controllers и в ней класс DefaultController который наследуем от нашего app\modules\dispatcher\components\Controller.

DefaultController.php
<?php

namespace app\modules\dispatcher\catalog\test\controllers;

use app\modules\dispatcher\components\Controller;

/**
 * Default controller for the `test` module
 */
class DefaultController extends Controller
{
    /**
     * Renders the index view for the module
     * @return string
     * @throws \yii\base\InvalidParamException
     * @throws \yii\base\ViewNotFoundException
     * @throws \yii\base\InvalidCallException
     */
    public function index()
    {
        return $this->render('index');
    }
}


Важно: чтоб работал наш модуль он всегда должен наследоваться от app\modules\dispatcher\components\Controller и содержать метод index

Создадим директории для представления views/default и файл нашего представления:

index.php
<div class="dispatcher-default-index">
    <p>
        You may customize this page by editing the following file:<br>
        <code><?= __FILE__ ?></code>
    </p>
</div>


Почти все готово, осталось только сделать вызов наших модулей. Для этого создадим компонент Dispatcher в app\modules\dispatcher\components:

Dispatcher.php
<?php

namespace app\modules\dispatcher\components;

use yii\base\Object;

class Dispatcher extends Object
{
    /**
     * @var \app\modules\dispatcher\Module
     */
    private $_module;

    public $module = 'dispatcher';

    /**
     * Dispatcher constructor.
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        parent::__construct($config);

        $this->_module = \Yii::$app->getModule($this->module);
    }

    /**
     * Get modules by layout
     *
     * @param $layout
     * @param array $positions
     * @return array
     * @throws \yii\base\InvalidConfigException
     */
    public function modules($layout, array $positions = [])
    {
        return $this->_module->run($layout, $positions);
    }
}


Теперь надо подключить наш компонент в web.php

        'dispatcher' => [
            'class' => 'app\modules\dispatcher\components\Dispatcher',
        ],

Не забываем что компонент надо добавить в массив components.

В любом контроллере, например SiteController, в методе actionIndex() добавим

	/* @var $modules Dispatcher */
	$modules = \Yii::$app->dispatcher->modules(1);

	return $this->render('index', compact('modules'));

Осталось только добавить в наше представление позиции для вывода модулей views/site/index.php:

index.php
<?php

/* @var $this yii\web\View */

$this->title = 'My Yii Application';

use app\modules\dispatcher\Module;

?>
<div class="site-index">

    <?php if (isset($modules[Module::POSITION_HEADER])) { ?>
        <div class="row">
            <?php foreach ($modules[Module::POSITION_HEADER] as $module) {
                echo $module;
            } ?>
        </div>
    <?php } ?>


    <div class="jumbotron">
        <h1>Congratulations!</h1>

        <p class="lead">You have successfully created your Yii-powered application.</p>

        <p><a class="btn btn-lg btn-success" href="http://www.yiiframework.com">Get started with Yii</a></p>
    </div>

    <div class="body-content">

        <div class="row">
            <div class="col-lg-4">
                <h2>Heading</h2>

                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
                    incididunt ut
                    labore et
                    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
                    nisi
                    ut aliquip
                    ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
                    cillum dolore eu
                    fugiat nulla pariatur.</p>

                <p><a class="btn btn-default" href="http://www.yiiframework.com/doc/">Yii
                        Documentation »</a></p>
            </div>
            <div class="col-lg-4">
                <h2>Heading</h2>

                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
                    incididunt ut
                    labore et
                    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
                    nisi
                    ut aliquip
                    ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
                    cillum dolore eu
                    fugiat nulla pariatur.</p>

                <p><a class="btn btn-default" href="http://www.yiiframework.com/forum/">Yii
                        Forum »</a>
                </p>
            </div>
            <div class="col-lg-4">
                <h2>Heading</h2>

                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
                    incididunt ut
                    labore et
                    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
                    nisi
                    ut aliquip
                    ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
                    cillum dolore eu
                    fugiat nulla pariatur.</p>

                <p><a class="btn btn-default" href="http://www.yiiframework.com/extensions/">Yii
                        Extensions »</a></p>
            </div>
        </div>
    </div>

    <?php if (isset($modules[Module::POSITION_FOOTER])) { ?>
        <div class="row">
            <?php foreach ($modules[Module::POSITION_FOOTER] as $module) {
                echo $module;
            } ?>
        </div>
    <?php } ?>
</div>


Рекомендую официальную документацию по модулям.

Весь код выложен на GitHub.
Tags:
Hubs:
+5
Comments 12
Comments Comments 12

Articles