artabro/wire/modules/LanguageSupport/LanguageSupportFields.module

577 lines
16 KiB
Text
Raw Normal View History

2024-08-27 11:35:37 +02:00
<?php namespace ProcessWire;
/**
* Multi-language support fields module
*
* ProcessWire 3.x, Copyright 2022 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' => 101,
'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() {
parent::__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() {
$fields = $this->wire()->fields;
$this->addHookBefore('FieldtypeLanguageInterface::sleepValue', $this, 'fieldtypeSleepValue');
$this->addHookBefore('PageFinder::getQuery', $this, 'pageFinderGetQuery');
$this->addHookBefore('Fieldtype::formatValue', $this, 'hookFieldtypeFormatValue');
$languageNames = array();
$fieldNames = $fields->getAllNames('name');
foreach($this->wire()->languages as $language) {
$languageNames[] = $language->name;
}
// keep track of which fields are multilanguage
$this->multilangFields = $fields->findByType('FieldtypeLanguageInterface', array(
'inherit' => true,
'valueType' => 'name',
'indexType' => '',
));
// 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(isset($fieldNames[$altName])) {
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() {
$languages = $this->wire()->languages;
$languages->addHook('added', $this, 'hookLanguageAdded');
$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) {
/** @var Field $field */
$field = $event->arguments[1];
if($field->name === 'language') return;
$language = $this->wire()->user->language;
if(!$language || !$language->id || $language->isDefault()) return;
// exit quickly if we can determine now we don't need to continue
if(!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 value
if($field->type->isEmptyValue($field, $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) {
$fields = $this->wire()->fields;
$language = $event->arguments[0];
if($language->template->name != LanguageSupport::languageTemplateName) return;
foreach($this->multilangFields as $name) {
$field = $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) {
$fields = $this->wire()->fields;
$language = $event->arguments[0];
if($language->template->name != LanguageSupport::languageTemplateName) return;
foreach($this->multilangFields as $name) {
$field = $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 ID
try {
$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 ID
try {
$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 $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 fields
if(!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);
$this->wire($v);
$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);
$this->wire($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 value
if(!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) {
/** @var Field $field */
$field = $event->arguments(0);
/** @var InputfieldWrapper $inputfields */
$inputfields = $event->return;
/** @var InputfieldRadios $f */
$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->get('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);
if(!isset($this->multilangAlternateFields[$parentName])) return '';
if(!in_array($altFieldName, $this->multilangAlternateFields[$parentName])) return '';
if(!$returnLanguage) return $parentName;
$languageName = substr($altFieldName, $pos+1);
return $this->wire()->languages->get($languageName);
}
/**
* 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);
}
/**
* Get multi-language field names (fields that implement FieldtypeLanguageInterface)
*
* @return array
* @since 3.0.194
*
*/
public function getMultilangFieldNames() {
return $this->multilangFields;
}
/**
* 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) {
$name = (string) $name;
if(isset($this->multilangAlternateFields[$name])) {
// default language for an alternate field set
return $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->wire()->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, ", "));
}
}
}