Yii Framework — поддержка нескольких языков в URL

Одной из причин, по которой для реализации программного комплекса я выбрал полноценный веб-фреймворк, стала необходимость поддержки трех языков: русского, украинского и английского. В системах управления содержимым (content management system), таких как Joomla, WordPress или Drupal, для поддержки нескольких языков необходимы сторонние дополнения. В то же время, в современных веб-фреймворках, использующих архитектуру Model — View — Controller, модуль поддержки языков встроен в систему и доступен без каких-либо манипуляций со стороны разработчика.

Поддержка нескольких языков интерфейса присутствует и в выбранном для разработки программного комплекса Yii Framework. С переводом представлений проблем нет; в Yii существуют для него два базовых механизма:

  • при помощи сообщений;
  • путем создания специфичных для языка файлов представлений.

Определенные трудности возникают при настройке преобразования URL. В отличие от некоторых других веб-фреймворков (например, Django), Yii «из коробки» не поддерживает работу с URL на сайтах с несколькими языками. Для того, чтобы устранить этот недостаток, надо рассмотреть, как происходит обработка URL в Yii.

В процессе обработки запроса из URL извлекается информация о:

  • контроллере;
  • действии (action) контроллера;
  • параметрах действия.

Например, гипотетическому URL вида /book/5 (вместо 5 может быть другое целое число — идентификатор книги) может соответствовать контроллер BookController (идентификатор book), его метод viewAction (идентификатор view) и параметр действия $id = 5 .

<?php
class BookController extends Controller {

	public function viewAction($id) {
		// отобразить информацию о книге 
		// с заданным идентификатором
	}
}

Обратное преобразование используется, например, в отображениях. По идентификаторам действия и контроллера и параметрам действия создается соответствующий URL (например, с помощью метода CController::createUrl).

Соответствие между URL и действиями осуществляет модуль Yii urlManager. Информация о преобразованиях URL хранится в главном файле конфигурации Yii (config/main.php) виде набора правил, каждое из которых состоит из двух частей:

  • Шаблон — выражение для URL, определяющий область применения правила. По своему синтаксису шаблоны напоминают регулярные выражения с именованными группами.
  • Действие, соответствующее URL. Действие состоит из двух частей: идентификаторов контроллера и действия в этом контроллере, разделенных косой чертой /.

Например, приведенное выше правило для URL вида /book/5 в файле конфигурации записывается как

'book/<id:\d+>'=>'book/view'

Наиболее очевидный способ добавления поддержки нескольких языков — усложнение используемых правил:

'<lang:(ru|uk|en)>/book/<id:\d+>'=>'book/view'
<?php
class BookController extends Controller {
	public function viewAction($id, $lang) {
		// установить язык интерфейса
		setupLanguage($lang);
		// отобразить информацию о книге 
		// с заданным идентификатором
	}
}

Однако этот способ не очень продуктивен:

  • Во все действия приходится добавлять новый параметр $lang и вызов метода setupLanguage.
  • Параметр lang нужно добавлять и во все методы создания URL.

Значительно более эффективный способ поддержки нескольких языков — использование специального класса правил. Модуль urlManager позволяет заменить класс для всех правил работы с URL с используемого по умолчанию CUrlRule на произвольный другой класс с двумя методами:

  • parseUrl — преобразование из URL в идентификатор действия и параметры;
  • createUrl — обратное преобразование.

При преобразовании URL в действие и обратно модуль Yii urlManager пытается применить соответствующую функцию каждого из правил, объявленных в файле конфигурации. Для того чтобы показать, что правило не применимо для заданного набора аргументов, указанные выше функции должны возвращать значение false. Для того, чтобы заменить класс всех правил, достаточно указать в файле конфигурации значение для поля urlRuleClass в модуле urlManager.

<?php
return array(
	// код настройки для других модулей

	'urlManager'=>array(
		'urlFormat'=>'path',
		'showScriptName'=>false,
		'urlRuleClass'=>'CustomUrlRule',

		'rules'=>array(
			'book/<id:\d+>'=>'book/view'
			// другие правила
		)
	)
);

При этом класс правил (в примере выше — CustomUrlRule) должен находиться в известном для Yii месте; имеет смысл поместить его в директорию components/.

Как и обычные правила CUrlRule, правила с новым классом инициализируются с использованием двух переменных: шаблона и идентификатора действия. Дополнительные параметры для создания правил можно передавать, если заменить идентификатор действия на массив:

'book/<id:\d+>'=>array('book/view', 'foo'=>'bar', 'foo2'=>'bar2')

В случае использования правил для поддержки нескольких языков класс правил (LangUrlRule) должен содержать три фрагмента.

Разбор URL

Метод parseUrl нового класса должен извлекать из предоставленного адреса информацию и устанавливать язык для отображения данных (переменную Yii::app()->language ). Для сайта программного комплекса язык определяется по первому фрагменту URL (en — для английского языка, ru — для русского, uk — для украинского). Если сведения о языке отсутствуют в URL, запрос должен перенаправляться на адрес с явно заданным языком. Например, если запрашивается адрес /book/1, после перенаправления URL станет равным /ru/book/1.

<?php
class LangUrlRule extends CUrlRule {
	
	const LANG_REGEX = '(ru|uk|en)';
	
	public function parseUrl($manager, $request, $pathInfo, $rawPathInfo) {
		// Язык должен находиться в начале пути $pathInfo
		// и отделяться слэшем '/'
		if (preg_match('%^'.self::LANG_REGEX.'(/.*)?$%', 
					$pathInfo, $matches)) {

			// Установить язык исходя из URL
			Yii::app()->language = $matches[1];
			$pathInfo = (count($matches) > 2) 
					? substr($matches[2], 1) : '';
			return parent::parseUrl($manager, $request, 
					$pathInfo, $rawPathInfo);
		} else if (parent::parseUrl($manager, $request, 
				$pathInfo, $rawPathInfo) !== false) {

			// Установить язык по умолчанию
			$lang = 'ru';
			$url = Yii::app()->baseUrl . '/' . $lang . '/' . $pathInfo;
			if (strlen($request->queryString) > 0) {
				$url .= '?' . $request->queryString;
			}
			$request->redirect($url);
			return false;
		}
	}
}

В приведенном коде при отсутствии языка в URL выбирается фиксированный язык (русский); более продвинутый способ — использование предпочитаемых языков, передаваемых браузером (Yii хранит его в переменной Yii::app()->request->preferredLanguages), однако может оказаться, что эти языки не поддерживаются.

Создание URL

Метод createUrl должен добавлять к началу адреса информацию о языке.

<?php
class LangUrlRule extends CUrlRule {
	
	public function createUrl($manager, $route, $params, $ampersand) {
		$lang = isset($params['lang']) 
				? $params['lang'] : Yii::app()->language;
		$lang = substr($lang, 0, 2); // Обрезать информацию о стране
		unset($params['lang']);
		
		$url = parent::createUrl($manager, $route, $params, $ampersand);
		if ($url === false) return false;
		return $lang.'/'.$url;
	}
}

Обратная совместимость

Должны поддерживаться нейтральные запросы, не зависящие от языка. Например, если на сайте используется капча, не имеет никакого смысла изменять ее URL в зависимости от языка отображения данных (более того, встроенный в Yii модуль капчи от этого перестает адекватно работать). Таким образом, должна быть возможность объявить правило вроде

'captcha'=>array('site/captcha', 'neutral'=>true)
<?php
class LangUrlRule extends CUrlRule {
	
	/**
	 * Если значение булевой переменно, то правило ведет себя 
	 * в точности как CUrlRule.
	 * 
	 * @var boolean
	 */
	public $neutral = false;
	
	public function __construct($route, $pattern) {
		parent::__construct($route, $pattern);
		
		if (is_array($route) && isset($route['neutral'])) {
			$this->neutral = $route['neutral'];
		}
	}
	
	public function createUrl($manager, $route, $params, $ampersand) {
		if ($this->neutral) {
			return parent::createUrl($manager, $route, 
					$params, $ampersand);
		}
		
		// код из фрагмента выше
	}
	
	public function parseUrl($manager, $request, $pathInfo, $rawPathInfo) {
		if ($this->neutral) {
			return parent::parseUrl($manager, $request, 
					$pathInfo, $rawPathInfo);
		}
		
		// код из фрагмента выше
	}
}

Полный код правила.

Итак, с помощью простых приемов можно научить Yii Framework работать с URL для сайтов с поддержкой нескольких языков. Насколько я понимаю, с минимальными изменениями тот же прием можно подходит и для Yii 2.x.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *