mirror of
https://github.com/processwire/processwire.git
synced 2025-08-14 02:34:24 +02:00
Improvements to FieldtypeOptions plus some refactoring in SelectableOptionsManager. This also removes the existing combined fulltext index 'value_title' and replaces it with separate fulltext indexes for value and title, since the previous one did not appear to ever be used. These table schema changes are applied at Modules > Refresh.
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
/**
|
/**
|
||||||
* ProcessWire Select Options Fieldtype
|
* ProcessWire Select Options Fieldtype
|
||||||
*
|
*
|
||||||
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||||
* https://processwire.com
|
* https://processwire.com
|
||||||
*
|
*
|
||||||
* @property SelectableOptionManager $manager
|
* @property SelectableOptionManager $manager
|
||||||
@@ -16,7 +16,7 @@ class FieldtypeOptions extends FieldtypeMulti implements Module {
|
|||||||
return array(
|
return array(
|
||||||
'title' => __('Select Options', __FILE__),
|
'title' => __('Select Options', __FILE__),
|
||||||
'summary' => __('Field that stores single and multi select options.', __FILE__),
|
'summary' => __('Field that stores single and multi select options.', __FILE__),
|
||||||
'version' => 1,
|
'version' => 2,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +319,7 @@ class FieldtypeOptions extends FieldtypeMulti implements Module {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function getMatchQuery($query, $table, $subfield, $operator, $value) {
|
public function getMatchQuery($query, $table, $subfield, $operator, $value) {
|
||||||
|
|
||||||
if($subfield == 'count') return parent::getMatchQuery($query, $table, $subfield, $operator, $value);
|
if($subfield == 'count') return parent::getMatchQuery($query, $table, $subfield, $operator, $value);
|
||||||
if($subfield == 'data' && (ctype_digit("$value") || empty($value))) {
|
if($subfield == 'data' && (ctype_digit("$value") || empty($value))) {
|
||||||
// this is fine (presumed to be an option_id)
|
// this is fine (presumed to be an option_id)
|
||||||
@@ -338,22 +338,34 @@ class FieldtypeOptions extends FieldtypeMulti implements Module {
|
|||||||
|
|
||||||
if(!count($options)) {
|
if(!count($options)) {
|
||||||
if(!$subfield || !SelectableOption::isProperty($subfield)) {
|
if(!$subfield || !SelectableOption::isProperty($subfield)) {
|
||||||
// if empty subfield or not a subfield we recognize, just assume title
|
$s = rtrim($subfield, '0123456789');
|
||||||
$subfield = 'title';
|
if($subfield && $this->manager->useLanguages() && SelectableOption::isProperty($s)) {
|
||||||
|
// property and language id, i.e. title1234 or value1235
|
||||||
|
} else {
|
||||||
|
// if empty subfield or not a subfield we recognize, just assume title
|
||||||
|
$subfield = 'title';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$options = $this->manager->findOptionsByProperty($query->field, $subfield, $operator, $value);
|
$options = $this->manager->findOptionsByProperty($query->field, $subfield, $operator, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$option = $options->first();
|
|
||||||
|
|
||||||
if($operator != '=' && $operator != '!=') {
|
if($operator != '=' && $operator != '!=') {
|
||||||
// for fulltext operations...
|
// for fulltext operations...
|
||||||
// since we are now just matching IDs of already found options
|
// since we are now just matching IDs of already found options
|
||||||
$operator = '=';
|
$operator = '=';
|
||||||
}
|
}
|
||||||
|
|
||||||
$subfield = 'data';
|
$subfield = 'data';
|
||||||
$value = $option ? $option->id : null;
|
|
||||||
|
if(count($options) < 2) {
|
||||||
|
$option = $options->first();
|
||||||
|
$value = $option ? $option->id : '';
|
||||||
|
} else {
|
||||||
|
$value = array();
|
||||||
|
foreach($options as $option) {
|
||||||
|
$value[] = $option->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($operator == '!=') {
|
if($operator == '!=') {
|
||||||
@@ -423,11 +435,13 @@ class FieldtypeOptions extends FieldtypeMulti implements Module {
|
|||||||
'name' => 'title',
|
'name' => 'title',
|
||||||
'input' => 'text',
|
'input' => 'text',
|
||||||
'operators' => array('%=', '=', '!=', '^=', '$=', '*=', '~='),
|
'operators' => array('%=', '=', '!=', '^=', '$=', '*=', '~='),
|
||||||
|
'label' => $this->_('Title'),
|
||||||
),
|
),
|
||||||
'value' => array(
|
'value' => array(
|
||||||
'name' => 'value',
|
'name' => 'value',
|
||||||
'input' => 'text',
|
'input' => 'text',
|
||||||
'operators' => array('%=', '=', '!=', '^=', '$=', '*=', '~='),
|
'operators' => array('%=', '=', '!=', '^=', '$=', '*=', '~='),
|
||||||
|
'label' => $this->_('Value text'),
|
||||||
),
|
),
|
||||||
'id' => array(
|
'id' => array(
|
||||||
'name' => 'id',
|
'name' => 'id',
|
||||||
@@ -435,6 +449,25 @@ class FieldtypeOptions extends FieldtypeMulti implements Module {
|
|||||||
'operators' => array('=', '!=', '>', '>=' ,'<', '<='),
|
'operators' => array('=', '!=', '>', '>=' ,'<', '<='),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$languages = $this->wire()->languages;
|
||||||
|
if($languages) {
|
||||||
|
foreach($languages as $language) {
|
||||||
|
if($language->isDefault()) continue;
|
||||||
|
|
||||||
|
$subfield = $subfields['title'];
|
||||||
|
$subfield['name'] .= $language->id;
|
||||||
|
$subfield['label'] .= " ($language->name)";
|
||||||
|
$subfields["title$language"] = $subfield;
|
||||||
|
|
||||||
|
$subfield = $subfields['value'];
|
||||||
|
$subfield['name'] .= $language->id;
|
||||||
|
$subfield['label'] .= " ($language->name)";
|
||||||
|
$subfields["value$language"] = $subfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($subfields);
|
||||||
|
}
|
||||||
|
|
||||||
$info['subfields'] = array_merge($info['subfields'], $subfields);
|
$info['subfields'] = array_merge($info['subfields'], $subfields);
|
||||||
|
|
||||||
@@ -621,4 +654,15 @@ class FieldtypeOptions extends FieldtypeMulti implements Module {
|
|||||||
return $this->manager->deleteOptions($this->_getField($field), $options);
|
return $this->manager->deleteOptions($this->_getField($field), $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade module version
|
||||||
|
*
|
||||||
|
* @param string $fromVersion
|
||||||
|
* @param string $toVersion
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function upgrade($fromVersion, $toVersion) {
|
||||||
|
$this->manager->upgrade($fromVersion, $toVersion);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
* Handles management of the fieldtype_options table and related field_[name] table
|
* Handles management of the fieldtype_options table and related field_[name] table
|
||||||
* to assist FieldtypeOptions module.
|
* to assist FieldtypeOptions module.
|
||||||
*
|
*
|
||||||
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
|
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||||||
* https://processwire.com
|
* https://processwire.com
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -215,7 +215,7 @@ class SelectableOptionManager extends Wire {
|
|||||||
|
|
||||||
/** @var DatabaseQuerySelect $query */
|
/** @var DatabaseQuerySelect $query */
|
||||||
$query = $this->wire(new DatabaseQuerySelect());
|
$query = $this->wire(new DatabaseQuerySelect());
|
||||||
$query->select('*');
|
$query->select(self::optionsTable . '.*');
|
||||||
$query->from(self::optionsTable);
|
$query->from(self::optionsTable);
|
||||||
$query->where("fields_id=:fields_id");
|
$query->where("fields_id=:fields_id");
|
||||||
$query->bindValue(':fields_id', $field->id);
|
$query->bindValue(':fields_id', $field->id);
|
||||||
@@ -755,77 +755,187 @@ class SelectableOptionManager extends Wire {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function updateLanguages(HookEvent $event = null) {
|
public function updateLanguages(HookEvent $event = null) {
|
||||||
if($event) {} // ignore
|
|
||||||
if(!$this->useLanguages) return;
|
|
||||||
|
|
||||||
$database = $this->wire('database');
|
if(!$this->useLanguages || !$this->wire()->languages) return;
|
||||||
$table = self::optionsTable;
|
|
||||||
$languages = $this->wire('languages');
|
if($event) {
|
||||||
$maxLen = $database->getMaxIndexLength();
|
$language = $event->arguments(0); /** @var Language $language */
|
||||||
if(strtolower($this->wire('config')->dbCharset) == 'utf8mb4') $maxLen -= 20;
|
$updateType = $event->arguments(1); /** @var string $updateType one of 'added' or 'deleted' */
|
||||||
|
} else {
|
||||||
// check for added languages
|
$language = null;
|
||||||
foreach($languages as $language) {
|
$updateType = '';
|
||||||
if($language->isDefault()) continue;
|
|
||||||
$titleCol = "title" . (int) $language->id;
|
|
||||||
$valueCol = "value" . (int) $language->id;
|
|
||||||
$query = $database->prepare("SHOW COLUMNS FROM $table LIKE '$valueCol'");
|
|
||||||
$query->execute();
|
|
||||||
if($query->rowCount() > 0) continue;
|
|
||||||
$this->message("FieldtypeOptions: Add language $language->name (id=$language)", Notice::debug);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$database->exec("ALTER TABLE $table ADD $titleCol TEXT");
|
|
||||||
$database->exec("ALTER TABLE $table ADD UNIQUE $titleCol ($titleCol($maxLen), fields_id)");
|
|
||||||
} catch(\Exception $e) {
|
|
||||||
$this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$database->exec("ALTER TABLE $table ADD $valueCol VARCHAR($maxLen)");
|
|
||||||
$database->exec("ALTER TABLE $table ADD INDEX $valueCol ($valueCol($maxLen), fields_id)");
|
|
||||||
$database->exec("ALTER TABLE $table ADD FULLTEXT {$titleCol}_$valueCol ($titleCol, $valueCol)");
|
|
||||||
} catch(\Exception $e) {
|
|
||||||
$this->error($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for deleted languages
|
if($updateType === 'deleted') {
|
||||||
$query = $database->prepare("SHOW COLUMNS FROM $table LIKE 'title%'");
|
$sqls = $this->checkLanguagesDeleted($language);
|
||||||
$query->execute();
|
} else if($updateType === 'added') {
|
||||||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
$sqls = $this->checkLanguagesAdded($language);
|
||||||
$name = $row['Field'];
|
} else {
|
||||||
if($name === 'title') continue;
|
$sqls = array_merge($this->checkLanguagesAdded(), $this->checkLanguagesDeleted());
|
||||||
$id = (int) str_replace('title', '', $name);
|
}
|
||||||
$language = $languages->get($id);
|
|
||||||
if($language && $language->id) continue;
|
$database = $this->wire()->database;
|
||||||
$titleCol = "title$id";
|
|
||||||
$valueCol = "value$id";
|
foreach($sqls as $sql) {
|
||||||
$this->message("FieldtypeOptions: Delete language $id", Notice::debug);
|
|
||||||
try {
|
try {
|
||||||
$database->exec("ALTER TABLE $table DROP INDEX $titleCol");
|
$database->exec($sql);
|
||||||
$database->exec("ALTER TABLE $table DROP INDEX $valueCol");
|
|
||||||
$database->exec("ALTER TABLE $table DROP INDEX {$titleCol}_$valueCol");
|
|
||||||
$database->exec("ALTER TABLE $table DROP $titleCol");
|
|
||||||
$database->exec("ALTER TABLE $table DROP $valueCol");
|
|
||||||
} catch(\Exception $e) {
|
} catch(\Exception $e) {
|
||||||
$this->error($e->getMessage());
|
$this->error("$sql -- " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for added languages
|
||||||
|
*
|
||||||
|
* @param Language|null $languageAdded
|
||||||
|
* @return array SQL statements to add language when appropriate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function checkLanguagesAdded($languageAdded = null) {
|
||||||
|
|
||||||
|
$database = $this->wire()->database;
|
||||||
|
$table = self::optionsTable;
|
||||||
|
$maxLen = $database->getMaxIndexLength();
|
||||||
|
$sqls = array();
|
||||||
|
$languages = $languageAdded ? array($languageAdded) : $this->wire()->languages;
|
||||||
|
|
||||||
|
if(strtolower($this->wire()->config->dbCharset) == 'utf8mb4') $maxLen -= 20;
|
||||||
|
|
||||||
|
foreach($languages as $language) {
|
||||||
|
/** @var Language $language */
|
||||||
|
if($language->isDefault()) continue;
|
||||||
|
$titleCol = "title" . (int) $language->id;
|
||||||
|
$valueCol = "value" . (int) $language->id;
|
||||||
|
if($database->columnExists($table, $valueCol)) continue;
|
||||||
|
$this->message("FieldtypeOptions: Add language $language->name (id=$language)", Notice::debug);
|
||||||
|
$sqls[] = "ALTER TABLE $table ADD $titleCol TEXT";
|
||||||
|
$sqls[] = "ALTER TABLE $table ADD UNIQUE $titleCol ($titleCol($maxLen), fields_id)";
|
||||||
|
$sqls[] = "ALTER TABLE $table ADD $valueCol VARCHAR($maxLen)";
|
||||||
|
$sqls[] = "ALTER TABLE $table ADD INDEX $valueCol ($valueCol($maxLen), fields_id)";
|
||||||
|
$sqls[] = "CREATE FULLTEXT INDEX {$titleCol}_ft ON $table($titleCol)";
|
||||||
|
$sqls[] = "CREATE FULLTEXT INDEX {$valueCol}_ft ON $table($valueCol)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sqls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for deleted languages
|
||||||
|
*
|
||||||
|
* @param Language|null $languageDeleted
|
||||||
|
* @return array SQL statements to delete language when appropriate
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function checkLanguagesDeleted($languageDeleted = null) {
|
||||||
|
|
||||||
|
$database = $this->wire()->database;
|
||||||
|
$table = self::optionsTable;
|
||||||
|
$languages = $this->wire()->languages;
|
||||||
|
$indexes = $database->getIndexes($table, true);
|
||||||
|
|
||||||
|
$query = $database->prepare("SHOW COLUMNS FROM $table LIKE 'title%'");
|
||||||
|
$query->execute();
|
||||||
|
$rows = array();
|
||||||
|
$sqls = array();
|
||||||
|
|
||||||
|
while($row = $query->fetch(\PDO::FETCH_ASSOC)) $rows[] = $row;
|
||||||
|
$query->closeCursor();
|
||||||
|
|
||||||
|
foreach($rows as $row) {
|
||||||
|
$name = $row['Field'];
|
||||||
|
if($name === 'title') continue;
|
||||||
|
$id = (int) str_replace('title', '', $name);
|
||||||
|
if($languageDeleted) {
|
||||||
|
// language specified and if it matches column name then allow it
|
||||||
|
if($languageDeleted->id !== $id) continue;
|
||||||
|
} else {
|
||||||
|
// check if language exists and if yes then skip it
|
||||||
|
$language = $languages->get($id);
|
||||||
|
if($language && $language->id) continue;
|
||||||
|
}
|
||||||
|
$this->message("FieldtypeOptions: Delete language $id", Notice::debug);
|
||||||
|
|
||||||
|
$titleCol = "title$id";
|
||||||
|
$valueCol = "value$id";
|
||||||
|
|
||||||
|
// Drop unique index: title+fields_id
|
||||||
|
if(isset($indexes[$titleCol])) $sqls[] = "ALTER TABLE $table DROP INDEX $titleCol";
|
||||||
|
if(isset($indexes[$valueCol])) $sqls[] = "ALTER TABLE $table DROP INDEX $valueCol";
|
||||||
|
|
||||||
|
// Drop fulltext index
|
||||||
|
if(isset($indexes[$titleCol . '_ft'])) $sqls[] = "ALTER TABLE $table DROP INDEX {$titleCol}_ft";
|
||||||
|
if(isset($indexes[$valueCol . '_ft'])) $sqls[] = "ALTER TABLE $table DROP INDEX {$valueCol}_ft";
|
||||||
|
|
||||||
|
// Drop older style combined index if present
|
||||||
|
if(isset($indexes["{$titleCol}_$valueCol"])) $sqls[] = "ALTER TABLE $table DROP INDEX {$titleCol}_$valueCol";
|
||||||
|
|
||||||
|
// drop column
|
||||||
|
$sqls[] = "ALTER TABLE $table DROP $titleCol";
|
||||||
|
$sqls[] = "ALTER TABLE $table DROP $valueCol";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sqls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade fieldtype_options table
|
||||||
|
*
|
||||||
|
* @param string $fromVersion
|
||||||
|
* @param string $toVersion
|
||||||
|
* @throws WireException
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function upgrade($fromVersion, $toVersion) {
|
||||||
|
|
||||||
|
if($fromVersion && $toVersion) {} // ignore
|
||||||
|
|
||||||
|
$database = $this->wire()->database;
|
||||||
|
$table = self::optionsTable;
|
||||||
|
|
||||||
|
if(!$database->tableExists($table)) return;
|
||||||
|
|
||||||
|
$indexes = $database->getIndexes($table, true);
|
||||||
|
|
||||||
|
if(isset($indexes['title_value'])) {
|
||||||
|
// removed combined title+value indexes created prior to 3.0.182
|
||||||
|
// and replace with separate fulltext indexes for title and value
|
||||||
|
foreach($indexes as $name => $info) {
|
||||||
|
if(strpos($name, 'title') !== 0) continue;
|
||||||
|
if(!strpos($name, '_value')) continue;
|
||||||
|
// i.e. title_value or title123_value123
|
||||||
|
$database->exec("ALTER TABLE $table DROP INDEX `$name`");
|
||||||
|
$this->message("Dropped index $table.$name", Notice::debug);
|
||||||
|
foreach($info['columns'] as $col) {
|
||||||
|
try {
|
||||||
|
$sql = "CREATE FULLTEXT INDEX {$col}_ft ON $table($col)";
|
||||||
|
$database->exec($sql);
|
||||||
|
$this->message("Added fulltext index for $table.$col", Notice::debug);
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
$this->error("$sql -- " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install
|
||||||
|
*
|
||||||
|
*/
|
||||||
public function install() {
|
public function install() {
|
||||||
|
|
||||||
$database = $this->wire('database');
|
$database = $this->wire()->database;
|
||||||
$maxLen = $database->getMaxIndexLength();
|
$maxLen = $database->getMaxIndexLength();
|
||||||
$query = $database->prepare("SHOW TABLES LIKE '" . self::optionsTable . "'");
|
|
||||||
$query->execute();
|
|
||||||
|
|
||||||
if($query->rowCount() == 0) {
|
if(!$database->tableExists(self::optionsTable)) {
|
||||||
$engine = $this->wire('config')->dbEngine;
|
$config = $this->wire()->config;
|
||||||
$charset = $this->wire('config')->dbCharset;
|
$engine = $config->dbEngine;
|
||||||
|
$charset = $config->dbCharset;
|
||||||
|
$table = self::optionsTable;
|
||||||
if(strtolower($charset) == 'utf8mb4') $maxLen -= 20;
|
if(strtolower($charset) == 'utf8mb4') $maxLen -= 20;
|
||||||
$sql =
|
$sql =
|
||||||
"CREATE TABLE " . self::optionsTable . " (" .
|
"CREATE TABLE $table (" .
|
||||||
"fields_id INT UNSIGNED NOT NULL, " .
|
"fields_id INT UNSIGNED NOT NULL, " .
|
||||||
"option_id INT UNSIGNED NOT NULL, " .
|
"option_id INT UNSIGNED NOT NULL, " .
|
||||||
"`title` TEXT, " .
|
"`title` TEXT, " .
|
||||||
@@ -834,18 +944,27 @@ class SelectableOptionManager extends Wire {
|
|||||||
"PRIMARY KEY (fields_id, option_id), " .
|
"PRIMARY KEY (fields_id, option_id), " .
|
||||||
"UNIQUE title (title($maxLen), fields_id), " .
|
"UNIQUE title (title($maxLen), fields_id), " .
|
||||||
"INDEX `value` (`value`($maxLen), fields_id), " .
|
"INDEX `value` (`value`($maxLen), fields_id), " .
|
||||||
"INDEX sort (sort, fields_id), " .
|
"INDEX sort (sort, fields_id) " .
|
||||||
"FULLTEXT title_value (`title`, `value`)" .
|
|
||||||
") ENGINE=$engine DEFAULT CHARSET=$charset";
|
") ENGINE=$engine DEFAULT CHARSET=$charset";
|
||||||
$database->exec($sql);
|
$database->exec($sql);
|
||||||
|
try {
|
||||||
|
$database->exec("CREATE FULLTEXT INDEX title_ft ON $table(`title`)");
|
||||||
|
$database->exec("CREATE FULLTEXT INDEX value_ft ON $table(`value`)");
|
||||||
|
} catch(\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($this->useLanguages) $this->updateLanguages();
|
if($this->useLanguages) $this->updateLanguages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstall
|
||||||
|
*
|
||||||
|
*/
|
||||||
public function uninstall() {
|
public function uninstall() {
|
||||||
try {
|
try {
|
||||||
$this->wire('database')->exec("DROP TABLE " . self::optionsTable);
|
$this->wire()->database->exec("DROP TABLE " . self::optionsTable);
|
||||||
} catch(\Exception $e) {
|
} catch(\Exception $e) {
|
||||||
$this->warning($e->getMessage());
|
$this->warning($e->getMessage());
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user