YIMP - Control Panel for Yii 2 on Bootstrap 4

I am sure that many developers who prefer frameworks to ready-made CMS have in stock a solution on Bootstrap or its analogues, which is used to create admin interfaces and other back-office interfaces. And I have it. It has been working successfully for many years, but is hopelessly outdated. It's time to rewrite.







While working on the new version, I tried to summarize all my experience on this topic, and as a result I got YIMP - a bike that I’m not ashamed to share: GitHub , LiveDemo , API Documentation .







YIMP is very simple. But behind this simplicity is a long thought, which I also want to share. So this article is not an instruction. Here we talk about architecture, dependency management, the MVC paradigm, and the user interface, of course.







So, YIMP is a dashboard. This is not a ready-made admin panel, not a CMS or even a CMF. Representation code must be written independently or use Gii (templates are included). YIMP provides a layout that defines where the controls should be located, as well as the interface through which the application transfers data to the layout. This is how it looks on desktops:













Layout adaptive. As the screen shrinks, the elements begin to disappear or move under the buttons in the navbar. As a result , the same page on the phone looks like this:







Better let it be under the spoiler






What do we see in the layout? The application title, breadcrumbs, three menus (left, right and top), widgets in the sidebars, page title. In my practice, this set of elements was enough to develop any interfaces - from admin pages of landing pages to corporate information systems. I tried to arrange them so that the space was used as efficiently as possible. What do you think?







The markup is written in pure Bootstrap, without extensions and customization. Wherever possible, classes from Bootstrap were used, so if you decide to use customization, then there should be no problems.







As I said, YIMP includes an interface through which the application transfers data to the layout. Let's see how this happens. Open the hood!







Layout



I believe that the developer should have full control over the layout, so when installing YIMP, I recommend copying his code to his application. In my opinion, this is much better than leaving a layout in the package and blocking a bunch of settings for it. Let's see the layout code:







77 lines of code. It’s not necessary to delve into!
<?php use dmitrybtn\yimp\widgets\Alert; use dmitrybtn\yimp\Yimp; use yii\bootstrap4\Html; $yimp = new Yimp(); $yimp->register($this); /** @var string $content Content came from view */ ?> <?php $this->beginPage() ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="<?= Yii::$app->charset ?>"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <?php echo Html::csrfMetaTags() ?> <title><?php echo Html::encode($yimp->nav->getTitle()) ?></title> <?php $this->head() ?> </head> <body> <?php $this->beginBody() ?> <?php echo $yimp->navbar() ?> <?php echo $yimp->beginSidebars() ?> <?php echo $yimp->beginLeftSidebar() ?> <?php echo $yimp->beginLeftSidebarMenu() ?> <?php echo $yimp->menuLeft([ 'options' => ['class' => 'nav-pills flex-column border rounded py-2'] ]) ?> <?php echo $yimp->endLeftSidebarMenu() ?> <?php if (isset($this->blocks[$yimp::SIDEBAR_LEFT])): ?> <?php echo $this->blocks[$yimp::SIDEBAR_LEFT] ?> <?php endif ?> <?php echo $yimp->endLeftSidebar() ?> <?php echo $yimp->beginRightSidebar() ?> <?php echo $yimp->beginRightSidebarMenu() ?> <?php echo $yimp->menuRight([ 'options' => ['class' => 'nav-pills flex-column border rounded py-2'] ]) ?> <?php echo $yimp->endRightSidebarMenu() ?> <?php if (isset($this->blocks[$yimp::SIDEBAR_RIGHT])): ?> <?php echo $this->blocks[$yimp::SIDEBAR_RIGHT] ?> <?php endif ?> <?php echo $yimp->endRightSidebar() ?> <?php echo $yimp->endSidebars() ?> <?php echo $yimp->beginContent() ?> <?php echo $yimp->headerDesktop() ?> <?php echo Alert::widget() ?> <?php echo $content ?> <?php echo $yimp->endContent() ?> <?php if (isset($this->blocks[$yimp::FOOTER])): ?> <?php echo $this->blocks[$yimp::FOOTER] ?> <?php endif ?> <?php $this->endBody() ?> </body> </html> <?php $this->endPage() ?>
      
      





As you can see, all YIMP markup is wrapped in methods. Most of these methods simply print the lines that it takes from this array . Yes, the KISS principle is our everything.







Please note that blocks are used in the layout. They are needed to display widgets defined in views (this is how form controls are rendered). Well, if the widget should hang on all pages, it is better to determine it directly in the layout.







So, the main area and widgets are defined in the views. And where are the headings, menus and bread crumbs determined? In my opinion, they are best defined in controllers. This is an important point, since such a decision contradicts the MVC paradigm. Let's look at this issue in more detail.







Controllers



So, let there be a ProfileController



that can display information about the profile of the current user and change the password. Logically, the profile/view



action will be called "My profile." It is also logical that in the main menu there should be an item “My profile”. Finally, “My profile” should be in breadcrumbs: “Home / My profile / Change password”. I think the desire to define a constant with the words "My profile" is quite justified. To do this in the view does not work. Selecting a separate layer for headings is cumbersome. Reasoning like this, I came to the controllers.







The next step was the decision to define in the controllers not only the action headers, but also the breadcrumbs and the menu. And do it so that YIMP can read them. And here we need an example. Let's look at a possible implementation of the ProfileController



class.







52 lines of simple code. Better to look carefully!
 class ProfileController extends \yii\web\Controller { public $nav; public function init() { parent::init(); $this->nav = new \dmitrybtn\yimp\Navigator; } public static function titleView() { return ' '; } public static function titlePassword() { return ' '; } public static function crumbsToView() { return [ ['label' => static::titleView(), 'url' => ['/profile/view']] ]; } public function actionView() { $this->nav->title = static::titleView(); $this->nav->menuRight = [ ['label' => ''], ['label' => static::titlePassword(), ['password']], ]; ... return $this->render('view'); } public function actionPassword() { $this->nav->title = static::titlePassword(); $this->nav->crumbs = static::crumbsToView(); ... return $this->render('password'); } }
      
      





Headers and breadcrumbs are defined using static methods, which means they can be used anywhere. For example, in the main menu of the application you can write:







 ['label' => ProfileController::titleView(), 'url' => ['/profile/view']],
      
      





Why exactly the methods? Because tomorrow you will be asked instead of the words "My profile" to display the login of the current user.







With bread crumbs the same. Suppose that our user has a list of pictures for which ImageController



is responsible. Then in the image/create



action you can write:







  $this->nav->crumbs = ProfileController::crumbsToView(),
      
      





and get bread crumbs like "Home / My profile / Add a picture." By the way, since the image/create



action is called “Add a picture”, the profile/view



action menu will need to be corrected:







  $this->nav->menuRight = [ ['label' => ''], ['label' => static::titlePassword(), ['password']], ['label' => ImageController::titleCreate(), 'url' => ['/image/create']] ];
      
      





I think the idea is understandable. In my opinion, this is a simple and effective solution for which you can move away from the MVC paradigm. Yes, the controller code is getting bigger, but there is a place for it in the controller - we don’t write business logic there, right? And yes, I would be very interested to know your opinion on this matter.







We are going further. As you might have guessed, the nav



property, defined as \dmitrybtn\yimp\Navigator



, is used to transfer headers, menus, and breadcrumbs from the controller to the layout. And this is another feature of YIMP.









How do the settings get into the layout? Very simple. During its initialization, YIMP checks for the nav



property of the current controller. If this property is readable and is a navigator ( instanceof \dmitrybtn\yimp\Navigator



), it is used to display the corresponding information. The navigator includes properties corresponding to layout elements, a complete list of which is easier to see in the API documentation .







In the application, it is recommended to create your own navigator and define menus in it that will not depend on the current action (top and left). After that, in all controllers, you need to create the nav



property and define it as a navigator (you can use your hands, you can inherit, or you can trait). If necessary, you can define several navigators.







This approach eliminates the direct relationship between YIMP and the controller. All interaction is carried out through one object, the implementation of which is controlled by the developer. That is, this is the same Dependency Inversion Principle from SOLID or Low Coupling from GRASP .







It would be ideologically correct to use interfaces, both for the controller and for the navigator. But here I decided to go the simplest way and not clutter up the system. After all, DIP does not talk about interfaces, but about abstractions. And in this case, abstraction is an agreement about the presence of a certain type of property in a particular class. What do you think?







The absence of a direct relationship between YIMP and the controller becomes important when modules appear in the system that do not know anything about YIMP. Or vice versa - modules written under YIMP are installed in a system that YIMP does not use.







In the first case, YIMP will not see nav



properties in the controller. There will be no error, but your menus will disappear from the screen, and the action ID will be used as the title. How to be Very simple - if YIMP cannot take the navigator from the controller, it will create it through the DI container using the alias yimp-nav



. Using this alias, you can register your own default navigator, for example by specifying in the application settings:







  'container' => [ 'definitions' => [ 'yimp-nav' => [ 'class' => '\your\own\Navigator', ] ] ],
      
      





In the second case, the navigator in the controller will be, but there will be no one to read it. In this case, it is recommended to write a wrapper for the views in the module, which will adapt the navigator from the current controller to the format accepted in Yii. That is, to display <h1>



in the main area, <title>



and breadcrumbs to pass through the parameters Yii::$app->view



, and to display the right menu in the form of buttons.







Conclusion



YIMP is now published without a version. I see no reason to publish the pre-release version - everything is too simple for that. I think it’s better to test a couple of weeks on a real project and immediately switch to version 1.0.0. So criticism, comments and help on GitHub are very welcome.







And I’m completing a module that implements access control. How to finish - I’ll write.







As you can see, there is nothing complicated here. I am sure many of you have something similar in stock. And I would be very interested to know how you solve the user interface task in your admin area.







Thanks everyone!








All Articles