Blame | Last modification | View Log | Download
<?php namespace ProcessWire;/*** Multi-language support fields module** ProcessWire 3.x, Copyright 2016 by Ryan Cramer* https://processwire.com** @method void languageAdded(Page $language) #pw-hooker* @method void languageDeleted(Page $language) #pw-hooker* @method void fieldLanguageAdded(Field $field, Page $language) #pw-hooker* @method void fieldLanguageRemoved(Field $field, Page $language) #pw-hooker**/class LanguageSupportFields extends WireData implements Module {/*** Return information about the module**/static public function getModuleInfo() {return array('title' => 'Languages Support - Fields','version' => 100,'summary' => 'Required to use multi-language fields.','author' => 'Ryan Cramer','autoload' => true,'singular' => true,'requires' => array('LanguageSupport'),'installs' => array('FieldtypePageTitleLanguage','FieldtypeTextareaLanguage','FieldtypeTextLanguage',));}/*** Cached names of fields that are dealing in multiple languages.**/protected $multilangFields = array();/*** Cached names of fields that have alternate language-specific versions** Indexed by original field name and value is an array of the language-alternate versions**/protected $multilangAlternateFields = array();/*** Construct and set our dynamic config vars**/public function __construct() {// load other required classes$dirname = dirname(__FILE__);require_once($dirname . '/LanguagesValueInterface.php');require_once($dirname . '/FieldtypeLanguageInterface.php');require_once($dirname . '/LanguagesPageFieldValue.php');}public function wired() {$this->addHookAfter('FieldtypeLanguageInterface::loadPageField', $this, 'fieldtypeLoadPageField');$this->addHookAfter('FieldtypeLanguageInterface::wakeupValue', $this, 'fieldtypeWakeupValue');$this->addHookAfter('FieldtypeLanguageInterface::getConfigInputfields', $this, 'fieldtypeGetConfigInputfields');parent::wired();}public function init() {// intentionally left blank}/*** Initialize the language support API vars**/public function LS_init() {$this->addHookBefore('FieldtypeLanguageInterface::sleepValue', $this, 'fieldtypeSleepValue');$this->addHookBefore('PageFinder::getQuery', $this, 'pageFinderGetQuery');$this->addHookBefore('Fieldtype::formatValue', $this, 'hookFieldtypeFormatValue');$languageNames = array();$fieldNames = array();foreach($this->wire('languages') as $language) {$languageNames[] = $language->name;}// keep track of which fields are multilanguageforeach($this->wire('fields') as $field) {if($field->type instanceof FieldtypeLanguageInterface) {$this->multilangFields[] = $field->name;}$fieldNames[] = $field->name;}// determine which fields have language alternates, i.e. 'title_es' is an alternate for 'title'foreach($fieldNames as $fieldName) {foreach($languageNames as $languageName) {$altName = $fieldName . '_' . $languageName;if(in_array($altName, $fieldNames)) {if(!isset($this->multilangAlternateFields[$fieldName])) $this->multilangAlternateFields[$fieldName] = array();$this->multilangAlternateFields[$fieldName][] = $altName;}}}}/*** Called by ProcessWire when the API and known $page is ready**/public function LS_ready() {$this->languages->addHook('added', $this, 'hookLanguageAdded');$this->languages->addHook('deleted', $this, 'hookLanguageDeleted');}/*** Hook into FieldtypeText::formatValue** Replace a value of one field with the value from another field that has the same name, but with the language name appended to it.* Example: title and title_es** @param HookEvent $event**/public function hookFieldtypeFormatValue(HookEvent $event) {$field = $event->arguments[1];if($field->name == 'language') return;$language = $this->wire('user')->get('language');if(!$language || !$language->id || $language->isDefault()) return;// exit quickly if we can determine now we don't need to continueif(!isset($this->multilangAlternateFields[$field->name])) return;/** @var Page $page */$page = $event->arguments[0];// determine name of language field, if present.// note that if the language name contains dashes or dots (- or .) the field name should contain underscores there instead$newName = $field->name . '_' . str_replace(array('-', '.'), '_', $language->name);$newField = $page->fieldgroup->getField($newName);if(!$newField) return;// unformatted so nothing can modify it first$value = $page->getUnformatted($newName);// if the page doesn't have a populated language-specific field, then exit// this will make it fallback to the default language valueif(empty($value)) return;if(is_object($value)) {if($value instanceof WireArray && !$value->count()) return;if($value instanceof NullPage) return;}// we have a new field: swap $field with $newField in the arguments$newValue = $page->get($newName); // get formatted version$arguments = $event->arguments;$arguments[1] = $newField;$arguments[2] = $newValue;$event->arguments = $arguments;}/*** Hook called when new language added** @param HookEvent $event**/public function hookLanguageAdded(HookEvent $event) {$language = $event->arguments[0];if($language->template->name != LanguageSupport::languageTemplateName) return;foreach($this->multilangFields as $name) {$field = $this->wire('fields')->get($name);if($field) $this->fieldLanguageAdded($field, $language);}$this->languageAdded($language);}/*** Hookable function called when a new language is added** @param Page|Language $language**/public function ___languageAdded(Page $language) {// hookable, intentionally blank}/*** Hook called when languag is deleted** @param HookEvent $event**/public function hookLanguageDeleted(HookEvent $event) {$language = $event->arguments[0];if($language->template->name != LanguageSupport::languageTemplateName) return;foreach($this->multilangFields as $name) {$field = $this->wire('fields')->get($name);if($field) $this->fieldLanguageRemoved($field, $language);}$this->languageDeleted($language);}/*** Hookable function called when a language is deleted** @param Page|Language $language**/public function ___languageDeleted(Page $language) {// hookable, intentionally blank}/*** Called when a new language is added to the system for each field that implements FieldtypeLanguageInterface** @param Field $field* @param Page|Language $language**/public function ___fieldLanguageAdded(Field $field, Page $language) {if($language->isDefault) return;if(!($field->type instanceof FieldtypeLanguageInterface)) return;$schema = $field->type->getDatabaseSchema($field);$database = $this->wire('database');$table = $database->escapeTable($field->table);foreach($schema as $name => $value) {if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue;// field in schema ends with the language IDtry {$database->exec("ALTER TABLE `{$table}` ADD `$name` $value");} catch(\Exception $e) {$this->error($e->getMessage(), Notice::log);}}foreach($schema['keys'] as $name => $value) {if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue;// index in schema ends with the language IDtry {$database->exec("ALTER TABLE `{$table}` ADD $value");} catch(\Exception $e) {$this->error($e->getMessage(), Notice::log);}}}/*** Called when a language is removed from the system for each field that implements FieldtypeLanguageInterface** @param Field $field* @param Page|Language $language**/protected function ___fieldLanguageRemoved(Field $field, Page $language) {if($language->isDefault) return;if(!($field->type instanceof FieldtypeLanguageInterface)) return;$schema = $field->type->getDatabaseSchema($field);$database = $this->wire('database');$table = $database->escapeTable($field->table);foreach($schema as $name => $value) {if(!preg_match('/[^\d]+' . $language->id . '$/', $name)) continue;try {$database->exec("ALTER TABLE `{$table}` DROP COLUMN `$name`");} catch(\Exception $e) {// just catch, no need for fatal errors here}}}/*** Hook into PageFinder::getQuery** Adjusts the selectors passed to the query so that find operations search in user's native language version, as well as the default version.** We may make this behavior configurable later on, as one may want to limit the search to 1 language only.** @param HookEvent $event**/public function pageFinderGetQuery(HookEvent $event) {$user = $this->wire('user');$language = $user->language;$database = $this->wire('database');if(!$language || !$language->id || $language->isDefault()) return;$arguments = $event->arguments;$selectors = $arguments[0];foreach($selectors as $selector) {$changed = false;$fields = $selector->field;$fields = is_array($fields) ? $fields : array($fields);foreach($fields as $key => $field) {$subfield = '';if(strpos($field, '.')) list($field, $subfield) = explode('.', $field);$field = $database->escapeCol($field);$subfield = $database->escapeCol($subfield);if(isset($this->multilangAlternateFields[$field])) {// account for multilang alternates like 'title_es' for 'title'$altName = $field . '_' . $database->escapeCol($user->language->name);if(in_array($altName, $this->multilangAlternateFields[$field])) {if($subfield) $altName .= ".$subfield";array_unshift($fields, $altName);$changed = true;}}// next we account for actual multilang fieldsif(!in_array($field, $this->multilangFields)) continue;if(!$subfield) $subfield = 'data';if($subfield === 'data') {array_unshift($fields, "$field.$subfield" . (int) $user->language->id);$changed = true;}}if($changed) $selector->field = $fields;}$arguments[0] = $selectors;$event->arguments = $arguments;}/*** Hook into FieldtypeLanguageInterface::loadPageField** Converts the value to a LanguagesPageFieldValue** @param HookEvent $event**/public function fieldtypeLoadPageField(HookEvent $event) {$page = $event->arguments[0];$field = $event->arguments[1];$value = $event->return;if($value instanceof LanguagesPageFieldValue) return;$v = new LanguagesPageFieldValue($page, $field, $value);$event->return = $v;}/*** Hook into FieldtypeLanguageInterface::wakeupValue** Converts the value to a LanguagesPageFieldValue** @param HookEvent $event**/public function fieldtypeWakeupValue(HookEvent $event) {$page = $event->arguments[0];$field = $event->arguments[1];$value = $event->return;if($value instanceof LanguagesPageFieldValue) {$value->setTrackChanges(true);$value->setField($field);// good} else if(is_array($value)) {$value = new LanguagesPageFieldValue($page, $field, $value);$value->setTrackChanges(true);$value->setField($field);$event->return = $value;}}/*** Hook into FieldtypeLanguageInterface::sleepValue** Converts a LanguagesPageFieldValue to an array** @param HookEvent $event**/public function fieldtypeSleepValue(HookEvent $event) {// $page = $event->arguments[0];// $field = $event->arguments[1];$value = $event->arguments[2];//$value = $event->return;$values = array();if(!$value instanceof LanguagesPageFieldValue) return;foreach($this->wire('languages') as $language) {if($language->isDefault()) $key = 'data';else $key = 'data' . $language->id;$values[$key] = $value->getLanguageValue($language->id);}/*if(!strlen($values['data'])) foreach($values as $k => $v) {// prevent the possibility of the default language having// a blank value while some other language has a populated valueif(!strlen($v)) continue;$values['data'] = $v;break;}*/// ensure that sleepValue is getting an array$event->setArgument(2, $values);// $event->return = $values;}public function fieldtypeGetConfigInputfields(HookEvent $event) {$field = $event->arguments(0);$inputfields = $event->return;$f = $this->wire('modules')->get('InputfieldRadios');$f->attr('name', 'langBlankInherit');$f->label = $this->_('Language Support / Blank Behavior');$f->description = $this->_("What should happen when this field's value is blank?");$f->notes = $this->_('Applies only to non-default language values on the front-end of your site.');$f->addOption(LanguagesPageFieldValue::langBlankInheritDefault, $this->_('Inherit value from default language'));$f->addOption(LanguagesPageFieldValue::langBlankInheritNone, $this->_('Remain blank'));$f->attr('value', (int) $field->langBlankInherit);$f->collapsed = Inputfield::collapsedBlank;$inputfields->add($f);}/*** Given a field name, return an array of alternate language field names** Returns a blank array if none found** @param string $fieldName* @return array**/public function getAlternateFields($fieldName) {if(isset($this->multilangAlternateFields[$fieldName])) return $this->multilangAlternateFields[$fieldName];return array();}/*** Given an alternate field name, return the parent (default-language) version of it** @param string $altFieldName* @param bool $returnLanguage Specify true if you want this function to return the language rather than the parent field name.* @return string|Language Returns blank string if none found**/public function getAlternateFieldParent($altFieldName, $returnLanguage = false) {$pos = strrpos($altFieldName, '_');if(!$pos) return '';$parentName = substr($altFieldName, 0, $pos);// $this->message($parentName);if(isset($this->multilangAlternateFields[$parentName]) && in_array($altFieldName, $this->multilangAlternateFields[$parentName])) {if(!$returnLanguage) return $parentName;$languageName = substr($altFieldName, $pos+1);return $this->wire('languages')->get($languageName);}return '';}/*** Get the language associated with the alternate field name** @param string $altFieldName* @return Page|false Language page associated with the field, or blank string or false if not found**/public function getAlternateFieldLanguage($altFieldName) {return $this->getAlternateFieldParent($altFieldName, true);}/*** Is the given field name a language alternate field?** If it is, the Language of the field is returned.* If it isn't, then boolean false is returned.** This method also accounts for default language.** @param string $name* @return bool|Language**/public function isAlternateField($name) {if(isset($this->multilangAlternateFields[$name])) {// default language for an alternate field setreturn $this->wire('languages')->getDefault();}if(!strpos($name, '_')) return false;$language = $this->getAlternateFieldParent($name, true);if($language && $language->id) return $language;return false;}/*** Install the module**/public function ___install() {$this->modules->get('FieldtypeTextLanguage');}/*** Uninstall the module**/public function ___uninstall() {// first check if there are any fields using the LanguageInterface$errors = '';foreach($this->wire('fields') as $field) {if($field->type instanceof FieldtypeLanguageInterface) $errors .= $field->name . ", ";}if($errors) throw new WireException("Can't uninstall because these fields use the language interface: " . rtrim($errors, ", "));}}