1
0
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:
Ryan Cramer
2021-07-23 09:55:19 -04:00
parent 6368910434
commit e9754b1177
2 changed files with 235 additions and 72 deletions

View File

@@ -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);
}
} }

View File

@@ -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());
} }