From e9754b11774618c3c186f93b6041ebf8ae31d2f5 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 23 Jul 2021 09:55:19 -0400 Subject: [PATCH] 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. --- .../FieldtypeOptions/FieldtypeOptions.module | 64 ++++- .../SelectableOptionManager.php | 243 +++++++++++++----- 2 files changed, 235 insertions(+), 72 deletions(-) diff --git a/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module b/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module index 37391b86..198e2e41 100644 --- a/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module +++ b/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module @@ -3,7 +3,7 @@ /** * ProcessWire Select Options Fieldtype * - * ProcessWire 3.x, Copyright 2020 by Ryan Cramer + * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * * @property SelectableOptionManager $manager @@ -16,7 +16,7 @@ class FieldtypeOptions extends FieldtypeMulti implements Module { return array( 'title' => __('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) { - + if($subfield == 'count') return parent::getMatchQuery($query, $table, $subfield, $operator, $value); if($subfield == 'data' && (ctype_digit("$value") || empty($value))) { // this is fine (presumed to be an option_id) @@ -338,22 +338,34 @@ class FieldtypeOptions extends FieldtypeMulti implements Module { if(!count($options)) { if(!$subfield || !SelectableOption::isProperty($subfield)) { - // if empty subfield or not a subfield we recognize, just assume title - $subfield = 'title'; + $s = rtrim($subfield, '0123456789'); + 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); } - $option = $options->first(); - if($operator != '=' && $operator != '!=') { // for fulltext operations... // since we are now just matching IDs of already found options - $operator = '='; + $operator = '='; } - + $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 == '!=') { @@ -423,11 +435,13 @@ class FieldtypeOptions extends FieldtypeMulti implements Module { 'name' => 'title', 'input' => 'text', 'operators' => array('%=', '=', '!=', '^=', '$=', '*=', '~='), + 'label' => $this->_('Title'), ), 'value' => array( 'name' => 'value', 'input' => 'text', 'operators' => array('%=', '=', '!=', '^=', '$=', '*=', '~='), + 'label' => $this->_('Value text'), ), 'id' => array( 'name' => 'id', @@ -435,6 +449,25 @@ class FieldtypeOptions extends FieldtypeMulti implements Module { '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); @@ -621,4 +654,15 @@ class FieldtypeOptions extends FieldtypeMulti implements Module { 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); + } + } diff --git a/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionManager.php b/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionManager.php index 68e0adfb..ae36ba16 100644 --- a/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionManager.php +++ b/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionManager.php @@ -6,7 +6,7 @@ * Handles management of the fieldtype_options table and related field_[name] table * to assist FieldtypeOptions module. * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * */ @@ -215,7 +215,7 @@ class SelectableOptionManager extends Wire { /** @var DatabaseQuerySelect $query */ $query = $this->wire(new DatabaseQuerySelect()); - $query->select('*'); + $query->select(self::optionsTable . '.*'); $query->from(self::optionsTable); $query->where("fields_id=:fields_id"); $query->bindValue(':fields_id', $field->id); @@ -755,77 +755,187 @@ class SelectableOptionManager extends Wire { * */ public function updateLanguages(HookEvent $event = null) { - if($event) {} // ignore - if(!$this->useLanguages) return; - $database = $this->wire('database'); - $table = self::optionsTable; - $languages = $this->wire('languages'); - $maxLen = $database->getMaxIndexLength(); - if(strtolower($this->wire('config')->dbCharset) == 'utf8mb4') $maxLen -= 20; - - // check for added languages - foreach($languages as $language) { - 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()); - } + if(!$this->useLanguages || !$this->wire()->languages) return; + + if($event) { + $language = $event->arguments(0); /** @var Language $language */ + $updateType = $event->arguments(1); /** @var string $updateType one of 'added' or 'deleted' */ + } else { + $language = null; + $updateType = ''; } - // check for deleted languages - $query = $database->prepare("SHOW COLUMNS FROM $table LIKE 'title%'"); - $query->execute(); - while($row = $query->fetch(\PDO::FETCH_ASSOC)) { - $name = $row['Field']; - if($name === 'title') continue; - $id = (int) str_replace('title', '', $name); - $language = $languages->get($id); - if($language && $language->id) continue; - $titleCol = "title$id"; - $valueCol = "value$id"; - $this->message("FieldtypeOptions: Delete language $id", Notice::debug); + if($updateType === 'deleted') { + $sqls = $this->checkLanguagesDeleted($language); + } else if($updateType === 'added') { + $sqls = $this->checkLanguagesAdded($language); + } else { + $sqls = array_merge($this->checkLanguagesAdded(), $this->checkLanguagesDeleted()); + } + + $database = $this->wire()->database; + + foreach($sqls as $sql) { try { - $database->exec("ALTER TABLE $table DROP INDEX $titleCol"); - $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"); + $database->exec($sql); } 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() { - $database = $this->wire('database'); + $database = $this->wire()->database; $maxLen = $database->getMaxIndexLength(); - $query = $database->prepare("SHOW TABLES LIKE '" . self::optionsTable . "'"); - $query->execute(); - if($query->rowCount() == 0) { - $engine = $this->wire('config')->dbEngine; - $charset = $this->wire('config')->dbCharset; + if(!$database->tableExists(self::optionsTable)) { + $config = $this->wire()->config; + $engine = $config->dbEngine; + $charset = $config->dbCharset; + $table = self::optionsTable; if(strtolower($charset) == 'utf8mb4') $maxLen -= 20; $sql = - "CREATE TABLE " . self::optionsTable . " (" . + "CREATE TABLE $table (" . "fields_id INT UNSIGNED NOT NULL, " . "option_id INT UNSIGNED NOT NULL, " . "`title` TEXT, " . @@ -834,18 +944,27 @@ class SelectableOptionManager extends Wire { "PRIMARY KEY (fields_id, option_id), " . "UNIQUE title (title($maxLen), fields_id), " . "INDEX `value` (`value`($maxLen), fields_id), " . - "INDEX sort (sort, fields_id), " . - "FULLTEXT title_value (`title`, `value`)" . + "INDEX sort (sort, fields_id) " . ") ENGINE=$engine DEFAULT CHARSET=$charset"; $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(); } + /** + * Uninstall + * + */ public function uninstall() { try { - $this->wire('database')->exec("DROP TABLE " . self::optionsTable); + $this->wire()->database->exec("DROP TABLE " . self::optionsTable); } catch(\Exception $e) { $this->warning($e->getMessage()); }