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

в 13:02, , рубрики: cms, php, yii, yii2, yii2 framework, модуль, Разработка веб-сайтов, разработка сайтов

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

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

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

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

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

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

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

'dispatcher' => [
     'class' => 'appmodulesdispatcherModule',
],

В директории модуля appmodulesdispatcher создадим класс BasicModule, который наследуется от yiibaseModule.

BasicModule.php

<?php

namespace appmodulesdispatcher;

use appmodulesdispatchercomponentsController;
use appmodulesdispatchermodelsLayoutModule;

/**
 *
 * Class Module
 * @package appmodulesdispatchercomponents
 *
 */
class BasicModule extends yiibaseModule
{
    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 yiibaseInvalidParamException
     */
    public function init()
    {
        parent::init();

        $this->_setModuleVariables();

        $this->loadModules();
    }

    /**
     * Load modules from directory by path
     * @throws yiibaseInvalidParamException
     */
    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 yiibaseInvalidConfigException
     */
    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|yiidbActiveRecord[]
     * @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;
    }
}

Унаследуем класс модуля appmodulesdispatcherModule от BasicModule

Module.php
<?php

namespace appmodulesdispatcher;

/**
 * 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 который будет наследовать yiiwebController. Переопределим в нем метод render().

Controller.php

<?php

namespace appmodulesdispatchercomponents;

/**
 *
 * Class Controller
 * @package appmodulesdispatchercomponents
 */
class Controller extends yiiwebController
{
    /**
     * @param string $view
     * @param array $params
     * @return string
     * @throws yiibaseInvalidParamException
     * @throws yiibaseViewNotFoundException
     * @throws yiibaseInvalidCallException
     */
    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 appmodulesdispatchercatalogtest;

/**
 * test module definition class
 */
class Module extends yiibaseModule
{
    /**
     * @inheritdoc
     */
    public $controllerNamespace = 'appmodulesdispatchercatalogtestcontrollers';
}

Создаем директорию controllers и в ней класс DefaultController который наследуем от нашего appmodulesdispatchercomponentsController.

DefaultController.php

<?php

namespace appmodulesdispatchercatalogtestcontrollers;

use appmodulesdispatchercomponentsController;

/**
 * Default controller for the `test` module
 */
class DefaultController extends Controller
{
    /**
     * Renders the index view for the module
     * @return string
     * @throws yiibaseInvalidParamException
     * @throws yiibaseViewNotFoundException
     * @throws yiibaseInvalidCallException
     */
    public function index()
    {
        return $this->render('index');
    }
}

Важно: чтоб работал наш модуль он всегда должен наследоваться от appmodulesdispatchercomponentsController и содержать метод 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 в appmodulesdispatchercomponents:

Dispatcher.php

<?php

namespace appmodulesdispatchercomponents;

use yiibaseObject;

class Dispatcher extends Object
{
    /**
     * @var appmodulesdispatcherModule
     */
    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 yiibaseInvalidConfigException
     */
    public function modules($layout, array $positions = [])
    {
        return $this->_module->run($layout, $positions);
    }
}

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

        'dispatcher' => [
            'class' => 'appmodulesdispatchercomponentsDispatcher',
        ],

Не забываем что компонент надо добавить в массив 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 yiiwebView */

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

use appmodulesdispatcherModule;

?>
<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.

Автор: yu-hritsaiy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js