From 08453431bb1751ff1f12f0e113b24ffda9257406 Mon Sep 17 00:00:00 2001 From: yarik Date: Tue, 8 Nov 2016 23:14:59 +0200 Subject: [PATCH] first commit --- Module.php | 12 ++++++++++++ behaviors/LanguageBehavior.php | 282 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ components/LanguageRequest.php | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ components/LanguageUrlManager.php | 35 +++++++++++++++++++++++++++++++++++ composer.json | 15 +++++++++++++++ migrations/m160829_104745_create_table_language.php | 35 +++++++++++++++++++++++++++++++++++ migrations/m160829_105345_add_default_languages.php | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ migrations/m160901_140639_add_ukrainian_language.php | 39 +++++++++++++++++++++++++++++++++++++++ migrations/m160927_124151_add_status_column.php | 19 +++++++++++++++++++ models/Language.php | 177 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.txt | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ widgets/LanguageForm.php | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ widgets/LanguagePicker.php | 29 +++++++++++++++++++++++++++++ widgets/views/language_form_frame.php | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ widgets/views/view.php | 35 +++++++++++++++++++++++++++++++++++ 15 files changed, 1046 insertions(+), 0 deletions(-) create mode 100755 Module.php create mode 100755 behaviors/LanguageBehavior.php create mode 100755 components/LanguageRequest.php create mode 100755 components/LanguageUrlManager.php create mode 100644 composer.json create mode 100755 migrations/m160829_104745_create_table_language.php create mode 100755 migrations/m160829_105345_add_default_languages.php create mode 100755 migrations/m160901_140639_add_ukrainian_language.php create mode 100755 migrations/m160927_124151_add_status_column.php create mode 100755 models/Language.php create mode 100755 readme.txt create mode 100755 widgets/LanguageForm.php create mode 100755 widgets/LanguagePicker.php create mode 100755 widgets/views/language_form_frame.php create mode 100755 widgets/views/view.php diff --git a/Module.php b/Module.php new file mode 100755 index 0000000..96903e3 --- /dev/null +++ b/Module.php @@ -0,0 +1,12 @@ + 'beforeSave', + ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave', + ActiveRecord::EVENT_AFTER_INSERT => 'afterSave', + ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave', + ]; + } + + /** + * Get $owner primary key to link language model + * @return string + */ + public function getOwnerKey():string + { + if(!empty( $this->ownerKey )) { + return $this->ownerKey; + } else { + return $this->owner->primaryKey()[ 0 ]; + } + } + + /** + * Set which attribute to use as $owner primary key to link language model + * + * @param string $value + */ + public function setOwnerKey(string $value) + { + $this->ownerKey = $value; + } + + /** + * Get language model attribute that is used as foreign key to $owner + * @return string + */ + public function getLangKey():string + { + if(!empty( $this->langKey )) { + return $this->langKey; + } else { + $owner = $this->owner; + return $owner::getTableSchema()->name . '_id'; + } + } + + /** + * Set which attribute to use as language model foreign key to $owner + * + * @param $value + */ + public function setLangKey(string $value) + { + $this->langKey = $value; + } + + /** + * Additional checks to attach this behavior + * + * @param ActiveRecord $owner + * + * @throws InvalidConfigException + */ + public function attach($owner) + { + if(empty( $this->objectLang )) { + $this->objectLang = $owner::className() . 'Lang'; + } elseif(!is_string($this->objectLang)) { + throw new InvalidConfigException('Object lang must be fully classified namespaced classname'); + } + try { + $this->objectLang = \Yii::createObject($this->objectLang); + } catch(\ReflectionException $exception) { + throw new InvalidConfigException('Object lang must be fully classified namespaced classname'); + } + if(( !$owner instanceof ActiveRecord ) || ( !$this->objectLang instanceof ActiveRecord )) { + throw new InvalidConfigException('Object lang must be fully classified namespaced classname'); + } + parent::attach($owner); + } + + /** + * Get query to get all language models for $owner indexed by language_id + * @return ActiveQuery + */ + public function getLangs() + { + $objectLang = $this->objectLang; + $owner = $this->owner; + return $owner->hasMany($objectLang::className(), [ $this->getLangKey() => $this->getOwnerKey() ]) + ->indexBy('language_id'); + } + + /** + * Get query to get language model for $owner for language_id, default to + * Language::getCurrent() + * + * @param int $language_id + * + * @return ActiveQuery + */ + public function getLang(int $language_id = NULL) + { + if(empty( $language_id )) { + $language_id = Language::getCurrent()->id; + } + $objectLang = $this->objectLang; + $table_name = $objectLang::getTableSchema()->name; + $owner = $this->owner; + return $owner->hasOne($objectLang::className(), [ $this->getLangKey() => $this->getOwnerKey() ]) + ->where([ $table_name . '.language_id' => $language_id ]); + } + + /** + * Generate language models for $owner for active languages. If $owner not new and language + * models already inserted, models will be filled with them. + * @return void + */ + public function generateLangs() + { + $owner = $this->owner; + $languages = Language::find() + ->where([ 'status' => true ]) + ->orderBy([ 'id' => SORT_ASC ]) + ->asArray() + ->column(); + $objectLang = $this->objectLang; + $owner_key = $this->getOwnerKey(); + $langs = []; + if(!$owner->isNewRecord) { + $langs = $this->getLangs() + ->andFilterWhere([ 'language_id' => $languages ]) + ->orderBy([ 'language_id' => SORT_ASC ]) + ->all(); + } + foreach($languages as $language) { + if(!array_key_exists($language, $langs)) { + $langs[ $language ] = \Yii::createObject([ + 'class' => $objectLang::className(), + 'language_id' => $language, + $this->getLangKey() => ( $owner->isNewRecord ? NULL : $owner->$owner_key ), + ]); + } + } + $this->modelLangs = $langs; + } + + /** + * Load language models with post data. + * + * @param Request $request + */ + public function loadLangs(Request $request) + { + foreach($request->post($this->objectLang->formName(), []) as $lang => $value) { + if(!empty( $this->modelLangs[ $lang ] )) { + $this->modelLangs[ $lang ]->attributes = $value; + $this->modelLangs[ $lang ]->language_id = $lang; + } + } + } + + /** + * Link language models with $owner by setting language model language key to owner key of + * owner + * @return bool If $owner is new record then return false else true + */ + public function linkLangs() + { + $owner = $this->owner; + // if($owner->isNewRecord) { + // return false; + // } + $lang_key = $this->getLangKey(); + $owner_key = $this->getOwnerKey(); + $modelLangs = $this->modelLangs; + foreach($modelLangs as $model_lang) { + $model_lang->$lang_key = $owner->$owner_key; + } + return true; + } + + /** + * Try to save all language models to the db. Validation function is run for all models. + * @return bool Whether all models are valid + */ + public function saveLangs() + { + $success = true; + $modelLangs = $this->modelLangs; + foreach($modelLangs as $model_lang) { + if($model_lang->save() === false) { + $success = false; + } + } + return $success; + } + + public function beforeSave($event) + { + /** + * @var ActiveRecord $owner + */ + $owner = $this->owner; + $db = $owner::getDb(); + $this->transaction = $db->beginTransaction(); + if($owner->hasAttribute('remote_id') && empty( $owner->remote_id )) { + $owner->remote_id = strval(microtime(true) * 10000); + } + } + + public function afterSave($event) + { + if(!empty( $this->modelLangs )) { + if($this->linkLangs() && $this->saveLangs()) { + $this->transaction->commit(); + $this->transactionStatus = true; + } else { + $this->transaction->rollBack(); + $this->transactionStatus = false; + } + } else { + $this->transaction->commit(); + $this->transactionStatus = true; + } + } + + /** + * @return bool + */ + public function getTransactionStatus():bool + { + return $this->transactionStatus; + } + } \ No newline at end of file diff --git a/components/LanguageRequest.php b/components/LanguageRequest.php new file mode 100755 index 0000000..9e8fe02 --- /dev/null +++ b/components/LanguageRequest.php @@ -0,0 +1,75 @@ +languageUrl === NULL) { + $this->languageUrl = $this->getUrl(); + + $url_list = explode('/', $this->languageUrl); + + $language_url = isset( $url_list[ 1 ] ) ? $url_list[ 1 ] : NULL; + Language::setCurrent($language_url); + + if($language_url !== NULL && $language_url === Language::getCurrent()->url && strpos($this->languageUrl, Language::getCurrent()->url) === 1) { + $this->languageUrl = substr($this->languageUrl, strlen(Language::getCurrent()->url) + 1); + } + } + + return $this->languageUrl; + } + + protected function resolvePathInfo() + { + $pathInfo = $this->getLanguageUrl(); + + if(( $pos = strpos($pathInfo, '?') ) !== false) { + $pathInfo = substr($pathInfo, 0, $pos); + } + + $pathInfo = urldecode($pathInfo); + + if(!preg_match('%^(?: + [\x09\x0A\x0D\x20-\x7E] + | [\xC2-\xDF][\x80-\xBF] + | \xE0[\xA0-\xBF][\x80-\xBF] + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} + | \xED[\x80-\x9F][\x80-\xBF] + | \xF0[\x90-\xBF][\x80-\xBF]{2} + | [\xF1-\xF3][\x80-\xBF]{3} + | \xF4[\x80-\x8F][\x80-\xBF]{2} + )*$%xs', $pathInfo) + ) { + $pathInfo = utf8_encode($pathInfo); + } + + $scriptUrl = $this->getScriptUrl(); + $baseUrl = $this->getBaseUrl(); + + if(strpos($pathInfo, $scriptUrl) === 0) { + $pathInfo = substr($pathInfo, strlen($scriptUrl)); + } elseif($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) { + $pathInfo = substr($pathInfo, strlen($baseUrl)); + } elseif(isset( $_SERVER[ 'PHP_SELF' ] ) && strpos($_SERVER[ 'PHP_SELF' ], $scriptUrl) === 0) { + $pathInfo = substr($_SERVER[ 'PHP_SELF' ], strlen($scriptUrl)); + } else { + throw new InvalidConfigException('Unable to determine the path info of the current request.'); + } + + if($pathInfo === '/') { + $pathInfo = substr($pathInfo, 1); + } + + return (string) $pathInfo; + } + } \ No newline at end of file diff --git a/components/LanguageUrlManager.php b/components/LanguageUrlManager.php new file mode 100755 index 0000000..9d1e6f0 --- /dev/null +++ b/components/LanguageUrlManager.php @@ -0,0 +1,35 @@ +url; + } else { + return '/' . $language->url . $url; + } + } + } \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..69d6345 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name": "artweb/artbox-language", + "description": "Yii2 light-weight CMS", + "license": "BSD-3-Clause", + "require": { + "php": ">=7.0", + "yiisoft/yii2": "*", + "developeruz/yii2-db-rbac": "*" + }, + "autoload": { + "psr-4": { + "artweb\\artbox\\language\\": "" + } + } +} \ No newline at end of file diff --git a/migrations/m160829_104745_create_table_language.php b/migrations/m160829_104745_create_table_language.php new file mode 100755 index 0000000..051d84e --- /dev/null +++ b/migrations/m160829_104745_create_table_language.php @@ -0,0 +1,35 @@ +createTable( + '{{%language}}', + [ + 'id' => $this->primaryKey(), + 'url' => $this->string() + ->notNull(), + 'local' => $this->string() + ->notNull(), + 'name' => $this->string() + ->notNull(), + 'default' => $this->boolean() + ->notNull() + ->defaultValue(false), + 'created_at' => $this->integer() + ->notNull(), + 'updated_at' => $this->integer() + ->notNull(), + ] + ); + } + + public function down() + { + $this->dropTable('{{%language}}'); + } + } diff --git a/migrations/m160829_105345_add_default_languages.php b/migrations/m160829_105345_add_default_languages.php new file mode 100755 index 0000000..d6d3a67 --- /dev/null +++ b/migrations/m160829_105345_add_default_languages.php @@ -0,0 +1,56 @@ +batchInsert( + '{{%language}}', + [ + 'id', + 'url', + 'local', + 'name', + 'default', + 'created_at', + 'updated_at', + ], + [ + [ + 1, + 'en', + 'en-EN', + 'English', + 0, + time(), + time(), + ], + [ + 2, + 'ru', + 'ru-RU', + 'Русский', + 1, + time(), + time(), + ], + ] + ); + } + + public function down() + { + $this->delete( + '{{%language}}', + [ + 'id' => [ + 1, + 2, + ], + ] + ); + } + } diff --git a/migrations/m160901_140639_add_ukrainian_language.php b/migrations/m160901_140639_add_ukrainian_language.php new file mode 100755 index 0000000..e3c40a5 --- /dev/null +++ b/migrations/m160901_140639_add_ukrainian_language.php @@ -0,0 +1,39 @@ +batchInsert( + '{{%language}}', + [ + 'id', + 'url', + 'local', + 'name', + 'default', + 'created_at', + 'updated_at', + ], + [ + [ + 3, + 'ua', + 'ua-UA', + 'Українська', + 0, + time(), + time(), + ], + ] + ); + } + + public function down() + { + $this->delete('{{%language}}', [ 'id' => [ 3 ] ]); + } + } diff --git a/migrations/m160927_124151_add_status_column.php b/migrations/m160927_124151_add_status_column.php new file mode 100755 index 0000000..ca47fb7 --- /dev/null +++ b/migrations/m160927_124151_add_status_column.php @@ -0,0 +1,19 @@ +addColumn('language', 'status', $this->boolean() + ->notNull() + ->defaultValue(false)); + } + + public function down() + { + $this->dropColumn('language', 'status'); + } + } diff --git a/models/Language.php b/models/Language.php new file mode 100755 index 0000000..640f48e --- /dev/null +++ b/models/Language.php @@ -0,0 +1,177 @@ + [ + 'class' => 'yii\behaviors\TimestampBehavior', + 'attributes' => [ + ActiveRecord::EVENT_BEFORE_INSERT => [ + 'created_at', + 'updated_at', + ], + ActiveRecord::EVENT_BEFORE_UPDATE => [ + 'updated_at', + ], + ], + ], + ]; + } + + /** + * @inheritdoc + */ + public function rules() + { + return [ + [ + [ + 'url', + 'local', + 'name', + 'created_at', + 'updated_at', + ], + 'required', + ], + [ + [ 'default' ], + 'boolean', + ], + [ + [ + 'created_at', + 'updated_at', + ], + 'integer', + ], + [ + [ + 'url', + 'local', + 'name', + ], + 'string', + 'max' => 255, + ], + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => Yii::t('app', 'Language ID'), + 'url' => Yii::t('app', 'Url'), + 'local' => Yii::t('app', 'Local'), + 'name' => Yii::t('app', 'Name'), + 'default' => Yii::t('app', 'Default'), + 'created_at' => Yii::t('app', 'Date Create'), + 'updated_at' => Yii::t('app', 'Date Update'), + ]; + } + + /** + * Get current language + * + * @return null|Language + */ + public static function getCurrent() + { + if (self::$current === null) { + self::$current = self::getDefaultLanguage(); + } + return self::$current; + } + + /** + * Set current language by Url param + * + * @param null|string $url Language url param + */ + public static function setCurrent($url = null) + { + $language = self::getLanguageByUrl($url); + self::$current = ( $language === null ) ? self::getDefaultLanguage() : $language; + Yii::$app->language = self::$current->local; + } + + /** + * Get default language + * + * @return null|Language + */ + public static function getDefaultLanguage() + { + /** + * @var null|Language $language + */ + $language = self::find() + ->where([ 'default' => true ]) + ->one(); + return $language; + } + + /** + * Get language by Url param + * + * @param null|string $url Language url param + * + * @return null|Language + */ + public static function getLanguageByUrl($url = null) + { + if ($url === null) { + return null; + } else { + /** + * @var null|Language $language + */ + $language = self::find() + ->where([ 'url' => $url ]) + ->one(); + if ($language === null) { + return null; + } else { + return $language; + } + } + } + } diff --git a/readme.txt b/readme.txt new file mode 100755 index 0000000..b1682b6 --- /dev/null +++ b/readme.txt @@ -0,0 +1,90 @@ +Как включить мультиязычность на сайте: +1. Запускаем миграцию: php yii migrate --migrationPath=common/modules/language/migrations +2. Добавляем в файл конфигурации: +'urlManager' => [ + 'enablePrettyUrl' => true, + 'showScriptName' => false, + 'class'=>'artweb\artbox\language\components\LanguageUrlManager', + 'rules'=>[ + '/' => 'site/index', + '//*'=>'/', + ] +], +3. Добавляем в файл конфигурации: +'request' => [ + 'class' => 'artweb\artbox\language\components\LanguageRequest' +], +4. Добавляем в файл конфигурации: +'language'=>'ru-RU', +'i18n' => [ + 'translations' => [ + '*' => [ + 'class' => 'yii\i18n\PhpMessageSource', + 'basePath' => '@frontend/messages', + 'sourceLanguage' => 'en', + 'fileMap' => [ + ], + ], + ], +], +5. Переводы писать в файл frontend\messages\{language}\app.php, где {language} - нужный язык, например ru. +6. Для вывода на странице сообщения с переводом используем функцию: Yii::t('app', {message}, $params = [], $language = null), + где {message} - нужное сообщение, $params - массив параметров, $language - нужный язык (по умолчанию используется текущий язык). +7. В наличие также виджет переключения языка: LanguagePicker::widget() + + +Как использовать мультиязычность для Active Record: +1. Создаем для таблицы {table} таблицу с языками {table_lang}. +2. Создаем для класса {Table} класс с языками {TableLang}. +3. Подключаеи для класса {Table} поведение LanguageBehavior: +public function behaviors() { + return [ + 'language' => [ + 'class' => LanguageBehavior::className(), + 'objectLang' => {TableLang}::className() // optional, default to {TableLang}::className() + 'ownerKey' => {Table}->id //optional, default to {Table}->primaryKey()[0] + 'langKey' => {TableLang}->table_id //optional, default to {Table}->tableName().'_id' + ], + ]; +} +3.1. PHPDoc для {Table}: + * * From language behavior * + * @property {TableLang} $lang + * @property {TableLang}[] $langs + * @property {TableLang} $objectLang + * @property string $ownerKey + * @property string $langKey + * @property {TableLang}[] $modelLangs + * @property bool $transactionStatus + * @method string getOwnerKey() + * @method void setOwnerKey(string $value) + * @method string getLangKey() + * @method void setLangKey(string $value) + * @method ActiveQuery getLangs() + * @method ActiveQuery getLang( integer $language_id ) + * @method {TableLang}[] generateLangs() + * @method void loadLangs(Request $request) + * @method bool linkLangs() + * @method bool saveLangs() + * @method bool getTransactionStatus() + * * End language behavior * +3.2. Убрать language behavior с наследуемых таблиц от {Table} ({TableSearch}...) +4. Доступные полезные методы: + {Table}->getLangs() - получить все текущие {TableLang} для {Table} проиндексированные по language_id + {Table}->getLang($language_id = NULL) - получить {TableLang} для определенного языка (default: текущий язык) для {Table} + {Table}->generateLangs() - получить массив {TableLang} под каждый язык, включая существующие записи, для {Table} + {Table}->loadLangs($request) - заполнить массив {TableLang} данными с POST + {Table}->linkLangs() - связать каждый элемент массива {TableLang} с текущей {Table} + {Table}->saveLangs() - провалидировать и сохранить каждый элемент массива {TableLang} +5. Добавить поля в форму (к примеру через Bootstrap Tabs). + В наличии: + LanguageForm::widget([ + 'modelLangs' => {TableLang}[], + 'formView' => string, + 'form' => ActiveForm, + ]); +6. Обрабатывать данные в контроллере. + 1. После создания/поиска {Table} создаем/находим языковые модели {Table}->generateLangs() + 2. При POST запросе загружаем данные в языковые модели {Table}->loadLangs(Request $request) + 3. После сохранения, если транзанкция успешна, то свойство {Table}->transactionStatus будет true, иначе возникла ошибка в какой то модели. +7. Получать данные на публичной части сайта через {Table}->lang. diff --git a/widgets/LanguageForm.php b/widgets/LanguageForm.php new file mode 100755 index 0000000..874b255 --- /dev/null +++ b/widgets/LanguageForm.php @@ -0,0 +1,78 @@ +formView === NULL) { + throw new InvalidConfigException('Form view must be set'); + } + if(empty( $this->modelLangs ) || !is_array($this->modelLangs)) { + throw new InvalidConfigException('Language models must be passed'); + } + if(empty( $this->getForm() )) { + throw new InvalidConfigException('Form model must be set'); + } + $this->languages = Language::find() + ->where([ 'status' => true ]) + ->orderBy([ 'default' => SORT_DESC ]) + ->indexBy('id') + ->all(); + } + + public function run() + { + return $this->render('language_form_frame', [ + 'languages' => $this->languages, + 'form_view' => $this->formView, + 'modelLangs' => $this->modelLangs, + 'form' => $this->getForm(), + 'idPrefix' => $this->idPrefix, + ]); + } + + public function getForm(): ActiveForm + { + return $this->form; + } + + public function setForm(ActiveForm $value) + { + $this->form = $value; + } + } \ No newline at end of file diff --git a/widgets/LanguagePicker.php b/widgets/LanguagePicker.php new file mode 100755 index 0000000..eefa288 --- /dev/null +++ b/widgets/LanguagePicker.php @@ -0,0 +1,29 @@ +render('view', [ + 'current' => Language::getCurrent(), + 'languages' => Language::find() + ->where([ + '!=', + 'id', + Language::getCurrent()->id, + ]) + ->all(), + ]); + } + } \ No newline at end of file diff --git a/widgets/views/language_form_frame.php b/widgets/views/language_form_frame.php new file mode 100755 index 0000000..d3b96e5 --- /dev/null +++ b/widgets/views/language_form_frame.php @@ -0,0 +1,69 @@ + +
+ 1) { + ?> + +
+ $model_lang) { + if(!array_key_exists($lang, $languages)) { + continue; + } + echo Html::tag('div', $this->render($form_view, [ + 'model_lang' => $model_lang, + 'language' => $languages[ $lang ], + 'form' => $form, + ]), [ + 'class' => 'tab-pane' . ( $first ? ' active' : '' ), + 'id' => $idPrefix . '_' . $lang, + ]); + $first = false; + } + ?> +
+ id ] )) { + echo $this->render($form_view, [ + 'model_lang' => $modelLangs[ $language->id ], + 'language' => $language, + 'form' => $form, + ]); + } + } + ?> +
diff --git a/widgets/views/view.php b/widgets/views/view.php new file mode 100755 index 0000000..3d0b450 --- /dev/null +++ b/widgets/views/view.php @@ -0,0 +1,35 @@ + +
+ + name; + ?> + + +
    + +
  • + getRequest(); + echo Html::a($language->name, '/' . $language->url . $request->getLanguageUrl()); + ?> +
  • + +
+
-- libgit2 0.21.4