📜 ⬆️ ⬇️

How to deploy multiple versions of sites on the same YII instance

In this article we will describe how we organized the work of our sites in one project on the yii framework. In the first part, we give a little theory of in what cases this may be needed and what is needed for that. And in the second part we proceed to the technical implementation.

Part 1


Introduction

Many companies support the work of several sites to promote their products in different markets. So do we. We have websites for Russian, American, European and other markets, separate websites for mobile devices, and websites of partner programs, which are also different for different countries. In development, we use the yii framework, to which we transferred our main site Alawar.ru last year, and this year also Alawar.com, Alawar.pl and sites of iOS devices. One of the features of the deployment of our sites on yii is that they all work on the same instance of this wonderful framework.

There are no problems in solving this problem; we will consider one specific implementation.

Site Differences

To understand how to arrange multiple sites on one platform, you need to figure out what they have in common and how they differ. Just want to note that by different sites we mean the difference not only in localization, which is ruled by elementary Yii::t('messageFile', 'messageCode') in views and different messageFile for different locales.
')
A different site may be the following:

Let us examine in detail each item.

Filling

By content, we mean different content for different sites. Since sites differ for different countries, and therefore they sell goods in different markets, the goods offered for sale in one market sometimes need to be removed from sale in another. Different sites may have different special offers and different discounts for their visitors.

In the end, even static pages of information can be different. Thus, we should be able to resolve these discrepancies by one instance of the framework.

Functionality of individual frontend units / modules

For example, on the site Alawar.com there is a functional Unlimited - subscriptions to all games for the time chosen by the user for the corresponding cost. Alawar.pl also provides this functionality to users, but subscription options offer different from the English site. On the Russian site, the work Unlimited is not provided at all.

Themes / Skins

At this point, different variants of tasks are possible:
  1. Different themes of sites (for example, a mobile site has a separate theme from the main one).
  2. Same theme sites, but different design options (for example, a festive skin on May 9 on Alawar.ru or a skin on July 4 on Alawar.com).

Learning how to create themes for the Yii engine is not a tricky business. The division of topics structurally in the framework addresses the issue of separation of design on sites.

Language / localization

And only in the last place is localization, which was mentioned above.

Site Similarities

And now let's take a look at what all sites can have in common.

The most important similarity and the only important and sufficient reason why it is worth doing different sites on the same platform is the same backend. If your sites have different backends, then you probably should not mix them into one project. Yes, we choose different products for sale on different sites. But all the products are in the same database and are available in the same admin panel. We draw different skins for different sites, but they can all be bolted to the same topic. Especially if the backend is not limited to mysql-admin.

In our case, the backend means:

Imagine that for each new site would have to raise all these technologies from scratch.

Assigned tasks

Summarizing the comparison of similarities and differences between our sites, we will determine what tasks the separation of sites should solve:
  1. Display of various and / or unique content on different sites while maintaining the overall backend.
  2. Ability to customize the functionality of different modules of sites separately from each other.
  3. Support for different themes or different skin themes, different text localizations.

Part 2


Config to determine the site

In this article we will show the division of the application into two sites: Russian and English. In reality, this way you can divide as many sites as you need.

Determine in what situation what site to show, we can on the following parameters:

Create a config file protected/config/sites.php with the following content:
 <?php return array( 'mywebsite.ru' => array( 'host' => array( 'mywebsite.ru', 'www.mywebsite.ru', ), 'userAgent' => false, ), 'mywebsite.com' => array( 'host' => array( 'mywebsite.com', 'www.mywebsite.com', ), 'userAgent' => false, ), ); 


In this example, we will analyze the definition of the site by domain only. In our case, the definition for the user agent is not used, so we specify 'userAgent' => false . This key is set for the case when you need to show different versions of sites for the same domain.

Separate configs for individual sites

Let's make for each site a separate configuration file with its unique settings. And we give these files the names corresponding to the keys in the above array. That is, create files:
protected/config/mywebsite.ru.php
protected/config/mywebsite.com.php

Here is the contents of the protected/config/mywebsite.ru.php :
 <?php return CMap::mergeArray( array( 'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'Application name', 'theme' => 'mywebsite', 'language' => 'ru', 'modules'=>array( ), 'controllerMap'=>array( 'site'=>'application.sites.common.controllers.SiteController', 'promo'=>array( 'class' => 'application.sites.mywebsite-ru.controllers.PromoController', 'viewPrefix' => '/mywebsite-ru/promo/', ), 'support'=>array( 'class' => 'application.sites.common.controllers.SupportController', 'viewPrefix' => '/mywebsite-ru/support/', ), ), 'components'=>array( 'urlManager'=>array( 'urlFormat'=>'path', 'showScriptName' => false, 'urlSuffix' => '/', 'rules' => array( '' => 'site/index', 'promo/' => 'promo/index', 'support/' => 'support/index', ), ), 'errorHandler'=>array( 'errorAction'=>'site/error', ), ), 'params'=>array( 'adminEmail'=>'admin@mywebsite.ru', 'arCustomParams' => array( 'customBannerPath' => '/promo/mywebsite.ru/promo-banner.png' ), ), ), require_once(dirname(__FILE__).'/main.php') ); 


As you can see, we transferred all site-dependent settings to separate files, which are configs of specific sites. Site name, subject, locale are defined in this config.

In order to implement a special site content or to process special requests, we will make separate controllers containing custom logic. We will determine the ways in which common for all sites and site-specific controllers will be available. The controllers used by this site are listed in the controllerMap parameter:
  'controllerMap'=>array( 'site'=>'application.sites.common.controllers.SiteController', 'promo'=>array( 'class' => 'application.sites.mywebsite-ru.controllers.PromoController', 'viewPrefix' => '/mywebsite-ru/promo/', ), 'support'=>array( 'class' => 'application.sites.common.controllers.SupportController', 'viewPrefix' => '/mywebsite-ru/support/', ), ), 

Here we see that mywebsite.ru uses a SiteController controller, common to both sites. The display path for this controller will be standard, it is not required to specify it additionally.
PromoController , on the contrary, refers only to this site, since may contain data on various promotions applicable only on a specific site. SupportController used common to both sites, but custom maps are used for this controller.

To achieve correct processing of the viewPrefix property in all frontend controllers without defining this property with further overriding by the render function in each, create a class protected/components/AController.php inherited from the CController class:
 <?php /** * Controller is the customized base controller class. * All controller classes for this application should extend from this base class. */ class AController extends CController { /** * @var string the default layout for the controller view. Defaults to '//layouts/column1', * meaning using a single column layout. See 'protected/views/layouts/column1.php'. */ public $layout='//layouts/main'; public $baseHref=false; public $viewPrefix=''; /** * @var array context menu items. This property will be assigned to {@link CMenu::items}. */ public $menu=array(); /** * @var array the breadcrumbs of the current page. The value of this property will * be assigned to {@link CBreadcrumbs::links}. Please refer to {@link CBreadcrumbs::links} * for more details on how to specify this property. */ public $breadcrumbs=array(); /** * @var string page description */ public $pageDescription = ''; public function render($view,$data=null,$return=false) { return parent::render($this->viewPrefix.$view,$data,$return); } } 

All frontend controllers in our application will inherit from this class. In it, we defined the viewPrefix property and, with due regard for it, redefined the render($view,$data=null,$return=false) method render($view,$data=null,$return=false) , which will allow us to structurally separate the controller mappings by the given prefix.

This is a flexible system that allows you to take out only those displays / controllers that differ so much that it is worth using separate files for them. Since all our sites use one backend, we should only share front-end logic, i.e. controllers and views, and models, forms, components, extensions and everything else are always common to all sites. Also, dispatching urls will differ on different sites. Therefore, we place the urlManager in a separate config. Well, all the other settings common to all sites are stored, as usual, in the main.php file, which is current with CMap::mergeArray

SiteDispatcher

Create all controllers and displays that will be used by our sites. We create the following files and folders required for them:
protected/sites/common/controllers/SiteController.php
protected/sites/common/controllers/SupportController.php
protected/sites/mywebsite-ru/controllers/PromoController.php
protected/sites/mywebsite-com/controllers/PromoController.php
public/themes/mywebsite/views/site/index.php
public/themes/mywebsite/views/mywebsite-ru/promo/index.php
public/themes/mywebsite/views/mywebsite-ru/support/index.php
public/themes/mywebsite/views/mywebsite-com/promo/index.php
public/themes/mywebsite/views/mywebsite-com/support/index.php

Each controller created must be inherited from the AController class. Thus, we break the standard structure YII a bit, but there is nothing to worry about. We have maintained the logical structure of the project.

To teach yii to run the necessary controllers using the config files we created, we will create a separate component class, protected/omponents/SiteDispatcher , whose task is to select the necessary config file for a specific situation:
 <?php class SiteDispatcher { //      public static function setCurrentSiteConfig( $configName ) { @session_start(); $_SESSION['CURRENT_SITE_CONFIG'] = array( 'host' => $_SERVER['HTTP_HOST'], 'configName' => $configName ); @session_write_close(); @session_destroy(); } //       public static function getCurrentSiteConfig() { @session_start(); $res = isset( $_SESSION['CURRENT_SITE_CONFIG'] ) ? $_SESSION['CURRENT_SITE_CONFIG'] : false; @session_write_close(); @session_destroy(); return $res; } public static function getConfigPath() { $arSites = self::getAvailableConfigs(); /*      ,     ,   ,   -          */ if ( ($arCurrent = self::getCurrentSiteConfig()) && $arCurrent['host'] == $_SERVER['HTTP_HOST'] && isset($arSites[$arCurrent['configName']]) ) { return 'protected/config/' . $arCurrent['configName'] . '.php'; } foreach ( $arSites as $configName => $arSiteConfig ) { $res = true; $res &= in_array( $_SERVER['HTTP_HOST'], $arSiteConfig['host'] ); if ( $res && $arSiteConfig['userAgent'] && isset( $_SERVER['HTTP_USER_AGENT'] ) ) { $m = false; $res &= preg_match( $arSiteConfig['userAgent'], $_SERVER['HTTP_USER_AGENT'], $m); } if ( $res ) { return 'protected/config/' . $configName . '.php'; } } error_log('Can\'t determine config to site: ' . var_export( array( 'host' => $_SERVER['HTTP_HOST'], 'userAgent' => $_SERVER['HTTP_HOST'], ), 1)); throw new Exception('Can\'t determine config to site'); } /** * * @static * @return mixed */ protected static function getAvailableConfigs() { return require( dirname(dirname(__FILE__)) . '/config/sites.php' ); } } 

Now we are able to determine which config to launch in which case. The getConfigPath() method returns the path to the selected configuration. At the same time, the selected config is saved to the session for ease of further access.

To invoke this method, at the beginning of the index.php file, replace the string

 $config=dirname(__FILE__).'/../protected/config/main.php'; 
on
 require dirname(__FILE__).'/../protected/components/SiteDispatcher.php'; $config=dirname(__FILE__).'/../'.SiteDispatcher::getConfigPath(); 


Thus, by separating configs, some controllers and mappings for different sites, we solved the first two tasks set in the first part.

Split skins

Using your own skins for different sites is decided by specifying the locale in the site config. As a rule, skin is separate css and js files of themes and images. In our project we use some global css and js files that define the structural layout of the theme common to all sites. As well as individual local css and js, which determine the design of this structure. All these styles and scripts are collected as a result of one file with our own minifiers and are connected in the layout as follows:
 <link rel="stylesheet" type="text/css" href="<?php echo Yii::app()->theme->baseUrl . '/css/'. Yii::app()->getLanguage() .'/main.minified.css'; ?>" /> <script type="text/javascript" src="<?php echo Yii::app()->theme->baseUrl . '/js/' . Yii::app()->getLanguage() . '/main_minified.js'; ?>"></script> 


Since Yii::app()->getLanguage() returns the locale defined in a separate config file of a specific site, by creating folders of the same name in the theme structure, we can save our css and js files there. It remains only to modify the minifier, which collects all the files into one, so that it supports working with the locale. But this is a topic for a separate article.

Separately, I would like to mention the issue of placing meters on our sites. There are many different scripts, google analytics, yandex metrika, addthis and others. Different sites use different counters. To get rid of the crutches with the Yii::app()->getLanguage() switch in the views to correctly display the desired counter on a specific site, we decided to put the counters into localization files.

Create files:
protected/messages/ru/scripts.php
protected/messages/en/scripts.php

Add the following code to them:
 <?php return array ( 'GoogleAnalitics' => ' <!-- GoogleAnalitics begin --> <script type="text/javascript"> //    RU </script> <!-- GoogleAnalitics end --> ', ); 

Now it is enough to call echo Yii::t('scripts', 'GoogleAnalitics') in the mapping files to display the necessary code for the desired site.

Conclusion

We will be happy to hear comments on our method of sharing sites, as well as other ways that you use yourself.

Thanks for attention!

Source: https://habr.com/ru/post/157877/


All Articles