├── .gitignore ├── screenshot.jpg ├── .gitattributes ├── README.md └── FieldtypeSelectExtOption.module /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kixe/FieldtypeSelectExtOption/HEAD/screenshot.jpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | # Show correct language for ProcessWire .module 4 | *.module linguist-language=PHP 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FIELDTYPE SELECT EXTERNAL OPTION 2 | ================================ 3 | 4 | Fieldtype which generates the options for a Select Inputfield from *any* MYSQL table in *any* (accessible) database. Define the source table, columns (to pull value & label) and the preferred Inputfieldtype (Select, Radios, Checkboxes SelectMultiple or ASMSelect) in field settings. Access to all values in the corresponding row of the source table via API. 5 | 6 | ## Create a new field 7 | + Step 1: Create a new field select fieldtype **SelectExtOption**. 8 | + Step 2: Define options under tab **Details** 9 | + Step 3: Save. Done! 10 | 11 | --- 12 | 13 | ## Settings 14 |  15 | 16 | ### Inputfieldtype 17 | Fieldtype **SelectExtOption** is compatible with the following Inputfieldtypes. You maybe need to adapt InputfieldPage to make all the options available. 18 | 19 | + InputfieldSelect 20 | + InputfieldRadios 21 | + InputfieldCheckboxes 22 | + InputfieldSelectMultiple 23 | + InputfieldAsmSelect 24 | + InputfieldToggle 25 | 26 | *Note: 27 | - InputfieldToggle requires always a value. 28 | - InputfieldAsmSelect selections are sortable. 29 | - 3d party modules maybe supported too. 30 | [Read more ...](#developers-note)* 31 | 32 | ### Source Table 33 | Choose any datatable in the database including those which are not depending to Processwire. **required**. 34 | 35 | ### Option Value 36 | Select a column of the source datatable to get the value for the option tag. 37 | Only Integer types allowed. (*This Class extends FieldtypeMulti which stores values as int(11)*.) 38 | Default column or column if not selected is always the first column of the table. 39 | <option value="**Option Value**" > ... 40 | 41 | *note: Option will overwrite the preceding option with same value while generating the select. Unique values recommended.* 42 | 43 | ### Option Label 44 | Select a column of the source datatable to get the label for the option tag. 45 | <option>**Option Label**</option> 46 | All types allowed. 47 | Default label or label if not selected same as **Option Value**. 48 | 49 | ### Filter 50 | Small Filter to limit the options if needed. Adds a **WHERE** condition to the **SELECT** statement 51 | which pulls the options from the datatable. Function filter() is hookable. 52 | 53 | ### Order by Label 54 | Options are ordered by **Option Label**. Select to order by any other column. 55 | 56 | ### Order Descending 57 | Order is Ascending by default. Check to switch to **Descending** 58 | 59 | --- 60 | 61 | ## API 62 | 63 | ### Return field value 64 | #### `$page->[fieldname]` 65 | By default you get the value. A value from another column can be selected in the field settings. 66 | 67 | #### `$page->[fieldname]->[property]` 68 | All column values are populated as a property (columnname) except values of columns named with reserved words ('label', 'value', 'row', 'options' and 'data'). 69 | 70 | ``` 71 | 72 | /** 73 | * single values (InputfieldSelect) 74 | * @return SelectExtOption Object (extended WireData Object) 75 | * 76 | **/ 77 | 78 | $page->myfield->value 79 | $page->myfield->label 80 | $page->myfield->row // assoc array (columnname => value) of all values of the selected datatablerow 81 | $page->myfield->options // assoc array (value => label) of all selectable options 82 | $page->myfield->columnname-1 83 | $page->myfield->columnname-2 // ... 84 | 85 | /** 86 | * muliple values (InputfiedSelectMultiple, InputfieldAsmSelect) 87 | * @return WireArray Object with SelectExtOption elements for each single value like above 88 | * 89 | **/ 90 | 91 | // Usage Examples 92 | $page->myfield->last()->row['land'] // value of column 'land' of last selected item 93 | $page->myfield->first()->row['id'] 94 | $page->myfield->eq(3)->value // integer value of 4th item in array of selected items 95 | $page->myfield->each('pages_id') // array of value of column 'pages\_id' of each item 96 | 97 | ``` 98 | 99 | *note: to get the value of a column named by reserved word use the row property, like $page->myfield->row['data']* 100 | 101 | ### Set a field value via API 102 | 103 | ``` 104 | 105 | /* Inputfieldtype Select */ 106 | $page->of(false); 107 | $page->myfield->value = 3; 108 | $page->save('myfield'); 109 | 110 | 111 | /* Inputfieldtype SelectMultiple (add a single value) */ 112 | $v = new SelectExtOption; 113 | $v->value = 3; 114 | $page->of(false); 115 | $page->myfield->add($v); 116 | $page->save('myfield'); 117 | 118 | 119 | /* Inputfieldtype Select/SelectMultiple. Will replace existing values */ 120 | $page->of(false); 121 | $page->myfield = array(3,7,9); // Example Values 122 | $page->save('myfield'); 123 | 124 | ``` 125 | 126 | *note: trying to set a not existing value will be ignored* 127 | 128 | ### Public module functions options() and row() 129 | 130 | ``` 131 | 132 | // call the module 133 | $getdata = $modules->get('FieldtypeSelectExtOption'); 134 | 135 | // return array of all possible value/ label pairs version >= 1.1.6 136 | $getdata->options('selector'); // selector = field-name, field-id or field-instance 137 | 138 | // find value(s) of the first or only field of type SelectExtOption in current page 139 | $getdata->row(); 140 | 141 | // find value(s) of a specific field of type SelectExtOption in current page, useful if more than one of same type 142 | $getdata->row('myfield'); 143 | 144 | // find value(s) of a specific field of type SelectExtOption in a page found by selector string 145 | $getdata->row('myfield','selectorstring'); 146 | 147 | // find value(s) of the first or only field of type SelectExtOption in a page found by selector string 148 | $getdata->row(null,'selectorstring'); 149 | 150 | ``` 151 | 152 | Function row() will return a MultipleArray with the stored value(s) as key. 153 | 154 | #### Example 155 | 156 | _Field Settings_ 157 | 158 | + **Inputfieldtype** = 'InputfieldAsmSelect' 159 | + **Source Table** = 'pages' 160 | + **Option Value** = 'id' 161 | + **Option Label** = 'name' 162 | 163 | _Selected Values in Frontend_ 164 | 165 | + 'admin' 166 | + 'user' 167 | 168 | _Code_ 169 | 170 | ``` 171 | $getdata = $modules->get('FieldtypeSelectExtOption'); 172 | $getdata->row(); 173 | ``` 174 | 175 | _Output_ 176 | 177 | ``` 178 | 179 | array (size=2) 180 | 2 => 181 | array (size=10) 182 | 'id' => string '2' (length=1) 183 | 'parent_id' => string '1' (length=1) 184 | 'templates_id' => string '2' (length=1) 185 | 'name' => string 'admin' (length=5) 186 | 'status' => string '1035' (length=4) 187 | 'modified' => string '2015-01-29 06:37:43' (length=19) 188 | 'modified_users_id' => string '41' (length=2) 189 | 'created' => string '0000-00-00 00:00:00' (length=19) 190 | 'created_users_id' => string '2' (length=1) 191 | 'sort' => string '15' (length=2) 192 | 29 => 193 | array (size=10) 194 | 'id' => string '29' (length=2) 195 | 'parent_id' => string '28' (length=2) 196 | 'templates_id' => string '2' (length=1) 197 | 'name' => string 'users' (length=5) 198 | 'status' => string '29' (length=2) 199 | 'modified' => string '2011-04-05 00:39:08' (length=19) 200 | 'modified_users_id' => string '41' (length=2) 201 | 'created' => string '2011-03-19 19:15:29' (length=19) 202 | 'created_users_id' => string '2' (length=1) 203 | 'sort' => string '0' (length=1) 204 | 205 | ``` 206 | 207 | 208 | ## Developers Note 209 | **3d party Inputfieldtypes** are supported too, if they are subclasses of **InputfieldSelect**. Furthermore they should be added in settings of **InputfieldPage** module. No guarantees that these Inputfieldtypes will work as expected. 210 | Please test carefully. 211 | Working example: [InputfieldChosenSelect](http://modules.processwire.com/modules/inputfield-chosen-select/). 212 | 213 | [InputfieldSelectMultipleTransfer](http://modules.processwire.com/modules/inputfield-select-multiple-transfer/) will be supported after removing the '>' in the selector string of javascript file. 214 | (Line 3 and 19 InputfieldSelectMultipleTransfer.js) 215 | 216 | ## Links 217 | + [Support Board processwire.com](https://processwire.com/talk/topic/9320-fieldtype-select-external-option/) 218 | + [Project Page github.com](https://github.com/kixe/FieldtypeSelectExtOption) 219 | 220 | ## License 221 | [GNU-GPLv3](http://www.gnu.org/licenses/gpl-3.0.html) 222 | 223 | ## Author 224 | kixe (Christoph Thelen) 225 | -------------------------------------------------------------------------------- /FieldtypeSelectExtOption.module: -------------------------------------------------------------------------------- 1 | 'Select External Option', 83 | 'version' => 220, 84 | 'summary' => __('Fieldtype which generates the options for a Select Inputfield from *any* table in *any* database hosted *anywhere*. Define database, source table, columns (to pull value & label) and the preferred Inputfieldtype in field settings.'), 85 | 'author' => 'kixe', 86 | 'href' => 'http://modules.processwire.com/modules/fieldtype-select-ext-option/', 87 | 'license' => 'GNU-GPLv3', 88 | 'hreflicense' => 'http://www.gnu.org/licenses/gpl-3.0.html', 89 | 'icon' => 'database', 90 | 'requires' => 'ProcessWire>=3.0.148' 91 | ); 92 | } 93 | 94 | protected $database = null; 95 | 96 | protected $databases = []; 97 | 98 | protected $tables = []; 99 | 100 | /** 101 | * Max allowed number of options 102 | * 103 | */ 104 | const OPTIONSLIMIT = 500; 105 | 106 | 107 | /** 108 | * Setup Inputfield hooks 109 | * 110 | */ 111 | public function init() { 112 | // remove Inputfield setup field 'defaultValue' because it's provided by this class 113 | $this->addHookAfter('InputfieldSelect::getConfigInputfields', function($e) { 114 | if ($e->object->hasFieldtype instanceof FieldtypeSelectExtOption == false) return; 115 | $inputfields = $e->return; 116 | $e->return = $inputfields->remove('defaultValue'); 117 | }); 118 | 119 | // set default value 120 | $this->addHookBefore("Inputfield::render", function ($e) { 121 | $page = $e->object->hasPage; 122 | $field = $e->object->hasField; 123 | $fieldtype = $e->object->hasFieldtype; 124 | // quick exit 125 | if ($fieldtype instanceof FieldtypeSelectExtOption == false) return; 126 | 127 | if ($field->init_value !== null) { 128 | $field->defaultValue = is_array($field->init_value)? implode("\n", $field->init_value) : "$field->init_value"; 129 | } 130 | }); 131 | 132 | /** 133 | * hook for special case: 134 | * - field required 135 | * - InputfieldSelect 136 | * - value 0 is a selectable option 137 | * - 0 is selected 138 | * in this case InputfieldSelect::renderOptions() does not make a difference between 0 and null and provides the 'deselect' option tag. We remove it here 139 | * 140 | */ 141 | $this->addHookAfter("InputfieldSelect::render", function ($e) { 142 | $page = $e->object->hasPage; 143 | $field = $e->object->hasField; 144 | $fieldtype = $e->object->hasFieldtype; 145 | // quick exit 146 | if ($fieldtype instanceof FieldtypeSelectExtOption == false) return; 147 | // if ($field->type != $this) return; 148 | if ($field->required && $e->object->intValue === array(0)) { 149 | $string = $e->return; 150 | $e->return = str_replace("", '', $string); 151 | } 152 | }); 153 | } 154 | 155 | /** 156 | * Called when field of this type is initialized at boot or after lazy loaded 157 | * 158 | * #pw-internal 159 | * 160 | * @param Field $field 161 | * @since ProcessWire 3.0.194 162 | * @see Fieldtype core class 163 | * 164 | */ 165 | public function initField(Field $field) { 166 | $this->setDatabase($field); 167 | } 168 | 169 | /** 170 | * Create a new PDO instance from field settings. Set properties 171 | * $lastAccessField, $database, $tables 172 | * @see Fieldtype core class 173 | * 174 | * 175 | * @param Field $field 176 | * @return bool if WireDatabasePDO could be set 177 | * 178 | */ 179 | protected function setDatabase(Field $field) { 180 | 181 | if ($this->lastAccessField == $field && $this->database) return true; 182 | if (array_key_exists($field->id, $this->databases)) { 183 | if (!($this->databases[$field->id] instanceof WireDatabasePDO)) { 184 | $type = gettype($this->databases[$field->id]); 185 | if ($type == 'object') { 186 | $class = get_class($this->databases[$field->id]); 187 | $type = "instanceof $class"; 188 | } 189 | unset($this->databases[$field->id]); 190 | throw new WireException("Expected instance of WireDatabasePDO for FieldtypeSelectExtOption::databases[$field->id] $type given"); 191 | } else { 192 | $this->database = $this->databases[$field->id]; 193 | $this->setLastAccessField($field); 194 | return true; 195 | } 196 | } 197 | // internal 198 | if (!strlen($field->db_user.$field->db_pass.$field->db_name)) { 199 | $this->setLastAccessField($field); 200 | $this->database = $this->wire('database'); 201 | $this->databases[$field->id] = $this->database; 202 | $this->tables = $this->database->getTables(); 203 | return true; 204 | } 205 | // missing required spec to establish external db connection 206 | else if (strlen($field->db_user) == 0 || strlen($field->db_pass) == 0 || strlen($field->db_name) == 0) { 207 | $this->database = $this->wire('database'); 208 | unset($this->databases[$field->id]); 209 | if (!count($_POST)) $this->error('Setting an external database failed: Missing value/s'); 210 | return false; 211 | } 212 | // try to init external db connection 213 | else { 214 | try { 215 | $host = $field->db_host; 216 | $username = $field->db_user; 217 | $password = $field->db_pass; 218 | $name = $field->db_name; 219 | $socket = $field->db_socket; 220 | $charset = $this->wire('config')->dbCharset; // charset set in config, don't change here! 221 | $port = $field->db_port; // default port set in config 222 | 223 | if ($socket) { 224 | // if socket is provided ignore $host and $port and use $socket instead: 225 | $dsn = "mysql:unix_socket=$socket;dbname=$name;"; 226 | } else { 227 | $dsn = "mysql:dbname=$name;host=$host"; 228 | if($port) $dsn .= ";port=$port"; 229 | } 230 | if (!$this->checkAccess($host, $port)) { 231 | $field->db_host = 'localhost'; // we reset to localhost 232 | $field->save('db_host'); 233 | return false; 234 | } 235 | $driver_options = array( 236 | \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES '$charset'", 237 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 238 | \PDO::ATTR_TIMEOUT => 5, 239 | \PDO::ATTR_PERSISTENT => true // test to get better performance ... 240 | ); 241 | $database = new WireDatabasePDO($dsn, $username, $password, $driver_options); 242 | $database->setDebugMode($this->wire('config')->debug); 243 | // make first query, set tables 244 | $this->tables = $database->getTables(); 245 | $this->database = $database; 246 | $this->databases[$field->id] = $database; 247 | $this->setLastAccessField($field); 248 | return true; 249 | } catch (\Throwable $e) { 250 | if (!count($_POST)) { 251 | $this->error("Setting an external database for field '$field->name' failed: ".$e->getMessage()); 252 | return false; 253 | } 254 | } 255 | } 256 | if (!count($_POST)) { 257 | throw new WireException("Unepected error"); 258 | } 259 | } 260 | 261 | /** 262 | * Check accessibility of external host:port before trying to create a database connection 263 | * 264 | * @param string $host 265 | * @param string/int $port 266 | * @return bool 267 | * 268 | */ 269 | protected function checkAccess($host, $port) { 270 | if (!function_exists('socket_create')) return true; 271 | // make all errors, warnings, notices exception 272 | // set_error_handler(function($errno, $errstr) {throw new Exception($errstr, $errno);}); 273 | 274 | if (!filter_var($host, FILTER_VALIDATE_IP)) { 275 | $_host = gethostbyname($host); 276 | if (empty($_POST) && $_host == $host) $this->error("Unable to translate '$host' to valid IP Adress (IPv4)"); 277 | else if (empty($_POST) && !filter_var($_host, FILTER_VALIDATE_IP)) $this->error('Invalid IPv4 syntax for host'); 278 | else $host = $_host; 279 | } 280 | if (!empty($this->errors(array('array')))) return false; 281 | 282 | $socket = socket_create(AF_INET, SOCK_STREAM, 0); 283 | $message = ''; 284 | try { 285 | if (socket_connect($socket, $host, $port)) { 286 | socket_close($socket); 287 | return true; 288 | } 289 | } 290 | catch (Exception $e) { 291 | $message = "Error: ".$e->getMessage(); 292 | 293 | } 294 | if (empty($_POST)) $this->error("No response from [$host:$port]. $message"); 295 | restore_error_handler(); 296 | return false; 297 | } 298 | 299 | /** 300 | * prevent database queries before database is set 301 | * 302 | */ 303 | public function getLoadQueryAutojoin(Field $field, DatabaseQuerySelect $query) { 304 | if (!$this->database) return null; 305 | return parent::getLoadQueryAutojoin($field, $query); 306 | } 307 | 308 | /** 309 | * Get an associative array of all values of the row depending to the selected single value. 310 | * 311 | * @param object $field 312 | * @param int $value 313 | * @return array / empty if column holding the values is not defined 314 | * 315 | */ 316 | protected function getTableRow(Field $field, $value) { 317 | if ($value === null) return array(); 318 | if (!$this->database) return array(); 319 | $table = $field->option_table; 320 | if (!$table) return array(); 321 | $columns = $this->getDatabaseColumns($table); 322 | $valuecolumn = $field->option_value; 323 | if (!in_array($valuecolumn,$columns)) return array(); 324 | $table = $this->database->escapeTable($table); 325 | $value = (int)$value; 326 | $filter = $this->filter($field); 327 | $filter = ($filter)?" AND $filter":''; 328 | $sql = "SELECT * FROM `$table` WHERE $valuecolumn = '$value'$filter"; 329 | $query = $this->database->query($sql); 330 | if (!$query->rowCount()) return array(); 331 | return $query->fetch(\PDO::FETCH_ASSOC); // single return, unique value 332 | } 333 | 334 | /** 335 | * Access to all column values of the selected row/rows. 336 | * 337 | * @param string fieldname, default: first field in page of type SelectExtOption 338 | * @param selector null|string|bool to get a page, default: current page 339 | * @param value int|array to get a specific row, selector must be set to false 340 | * @return assoc array 341 | * 342 | */ 343 | public function row($name = null, $selector = null, $value = null) { 344 | $return = array(); 345 | $n = $name; 346 | if ($name) $name = ',name='.$name; 347 | 348 | // get a page value 349 | if ($value === null && $selector !== false) { 350 | $page = ($selector)? $this->wire('pages')->get($selector) : $this->wire('page'); 351 | if ($page instanceof NullPage) throw new WireException("Page not found. Selector string '$selector' doesn't match."); 352 | $field = $page->fields->get('type=FieldtypeSelectExtOption'.$name); 353 | //field does not belong to pages fieldgroup 354 | if (!$name && !$field) throw new WireException("Page '$page->name' doesn't contain any field of type 'SelectExtOption'!"); 355 | if (!$field) throw new WireException("Field '$n' doesn't belong to page '$page->name'."); 356 | $name = $field->name; 357 | $value = $page->$name; 358 | } else { 359 | $field = wire('fields')->get($n); 360 | if (is_numeric($value)) $value = (int) $value; 361 | else if (!is_array($value)) throw new WireException("3d argument 'value' must be of type numeric (int) or array."); 362 | } 363 | 364 | if (is_int($value)) { 365 | $return = $this->getTableRow($field, $value); 366 | } 367 | else if (is_array()) { 368 | foreach ($value as $key => $val) { 369 | $row = $this->getTableRow($field, $value); 370 | $return[$key] = $row; 371 | } 372 | } 373 | else if ($value instanceof SelectExtOptionArray) { 374 | foreach ($value as $key => $val) { 375 | $row = $this->getTableRow($field, $val->value); 376 | $return[$key] = $row; 377 | } 378 | } else if ($value instanceof SelectExtOption) { 379 | $return = $this->getTableRow($field, $value->value); 380 | } else { 381 | // ... 382 | } 383 | return $return; 384 | } 385 | 386 | public function getInputfield(Page $page, Field $field) { 387 | 388 | // get (set default) name of Inputfieldtype 389 | $class = ($field->input_type)? $field->input_type : 'InputfieldSelect'; 390 | // set Inputfield 391 | $inputfield = $this->modules->get($class); 392 | 393 | // get the options array 394 | $options = $this->options($field); 395 | // any selectable options provided? 396 | if (empty($options)) { 397 | $inputfield = $this->modules->get('InputfieldMarkup'); 398 | $inputfield->textformatters = array('TextformatterMarkdownExtra'); 399 | $inputfield->markupText = "Field: *$field->name* doesn't provide selectable options. Check field settings!"; 400 | return $inputfield; 401 | } 402 | 403 | foreach($options as $optval => &$label) $label = $this->label($label, $optval, $page, $field); 404 | if (!$field->option_order) { 405 | if ($field->option_asc) arsort($options, SORT_LOCALE_STRING); // sort descending by final label 406 | else asort($options, SORT_LOCALE_STRING); // sort ascending by final label 407 | } 408 | // else ksort($options); // DO NOT SORT, because already sorted by database query "order by" 409 | $inputfield->addOptions($options, true); 410 | 411 | /** 412 | * attributes needed? 413 | * 414 | foreach ($options as $optval => $label) { 415 | $inputfield->addOption($optval, $label, array()); 416 | } 417 | */ 418 | 419 | // InputfieldToggle requires a value, the property 'useDeselect' is not provided here 420 | // value must be a valid selectable option 421 | if ($class == 'InputfieldToggle') { 422 | if ($page->{$field->name} instanceof WireArray && $page->{$field->name}->value === null) { 423 | reset($options); 424 | $value = $field->init_value? $field->init_value : key($options); 425 | $page->setAndSave($field->name, $value); 426 | } 427 | } 428 | return $inputfield; 429 | } 430 | 431 | /** 432 | * Get an array of all columns in a given table of the database. 433 | * 434 | * @param string name of the table 435 | * @return array 436 | * 437 | */ 438 | protected function getDatabaseColumns($table) { 439 | if (!$this->database || !in_array($table, $this->tables)) return array(); 440 | $columns = array(); 441 | $table = $this->database->escapeTable($table); 442 | $sql = "SHOW COLUMNS FROM $table"; 443 | $query = $this->database->query($sql); 444 | if (!$query->rowCount()) return array(); 445 | $rows = $query->fetchAll(); 446 | foreach ($rows as $row) $columns[] = $row['Field']; 447 | return $columns; 448 | } 449 | 450 | /** 451 | * Return true if column in a given table of the database is of type (int). 452 | * 453 | * @param string datatable and column 454 | * @return null (if table and/or column doesn't exist) 455 | * @return bool (true if int) 456 | * 457 | */ 458 | protected function isIntColumn($table, $column) { 459 | $result = array(); 460 | $columns = $this->getDatabaseColumns($table); 461 | if (!$columns) return null; 462 | if (!in_array($column,$columns)) return null; 463 | $table = $this->database->escapeTable($table); 464 | $column = $this->database->escapeCol($column); 465 | $sql = "SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$table' AND COLUMN_NAME = '$column'"; 466 | $query = $this->database->query($sql); 467 | if (!$query->rowCount()) return null; 468 | foreach ($query->fetchAll() as $type) if(strpos($type[0],'int') !== false) return true; 469 | return false; 470 | } 471 | 472 | /** 473 | * Return array of all selectable options, key ≈ (int)optionvalue, value ≈ optionlabel 474 | * 475 | * @param string/int/object $field - Field, field->id, field->name 476 | * @param object $language - Language - default: user language 477 | * @return array 478 | * 479 | */ 480 | public function options($field = null, Language $language = null) { 481 | if (!$field instanceof Field) { 482 | $selector = ''; 483 | if (is_int($field)) $selector = ",id=$field"; 484 | elseif (is_string($field)) $selector = ",name=$field"; 485 | $_field = $field; 486 | $field = $this->wire('fields')->get('type=FieldtypeSelectExtOption'.$selector); 487 | if (!$field) throw new WireException("Field '$_field' doesn't exist or is not of type FieldtypeSelectExtOption."); 488 | } 489 | elseif ($field->type != 'FieldtypeSelectExtOption') throw new WireException("Expecting FieldtypeSelectExtOption."); 490 | if (!$this->database) return []; 491 | 492 | // set language 493 | $langID = null; 494 | if (!$language && 495 | $this->wire('modules')->isInstalled("LanguageSupport") && 496 | !$this->wire('user')->get('language')->isDefault()) { 497 | $language = $this->wire('user')->get('language'); 498 | } 499 | if ($language && !empty($field->{"option_label_$language->id"})) $langID = "_$language->id"; 500 | // create options array 501 | $options = $this->getExtOptions($field->option_table,$field->option_value,$field->{"option_label$langID"},$this->filter($field),$field->option_order,$field->option_asc,$field->option_label); 502 | return $options; 503 | } 504 | 505 | /** 506 | * Get options array from external field or datatable 507 | * 508 | * @return bool false if table and/or column doesn't exist 509 | * @return null if table doesn't contain any data 510 | * @return array(value => label) 511 | * 512 | */ 513 | protected function getExtOptions($table, $value = false, $label = false, $filter = false, $order = false, $dir = false, $defaultLabel = null) { 514 | 515 | // quick exit 516 | if (!$this->database) return false; 517 | 518 | // check if table exists and is accessible 519 | $table = $this->database->escapeTable($table); 520 | if (!in_array($table, $this->tables)) return false; 521 | 522 | // get columns array 523 | $columns = $this->getDatabaseColumns($table); 524 | 525 | // we need minimum one inttype column 526 | foreach ($columns as $column) { 527 | if ($this->isIntColumn($table,$column)) $intcolumn = $column; break; 528 | } 529 | if (!isset($intcolumn)) return false; 530 | 531 | // check if column isset and exists, default 1st column 532 | if ($label === false) $label = $columns[0]; 533 | else if (!in_array($label,$columns)) return false; 534 | 535 | // check if column isset and exist, default 1st column 536 | if($defaultLabel === null) $defaultLabel = $label; 537 | else if (!in_array($defaultLabel,$columns)) return false; 538 | 539 | // check if column isset and exists, default 1st column of type int 540 | if($value === false) $value = $intcolumn; 541 | else if (!in_array($value,$columns)) return false; 542 | 543 | $options = array(); 544 | // @see getConfigInputfields() 545 | if ($dir !== 'LIMIT 1') { 546 | $dir = ((bool)$dir === true)? 'DESC':'ASC'; 547 | } 548 | 549 | $order = ($order)?$order:$label; 550 | $order = $this->database->escapeCol($order); 551 | // validate/sanitize filterstring 552 | $filter = $filter? "WHERE $filter" : ''; 553 | // $statement = "SELECT * FROM `$table` $filter ORDER BY `$order` $dir LIMIT " . self::OPTIONSLIMIT; 554 | $statement = "SELECT * FROM `$table` $filter ORDER BY `$order` $dir"; 555 | $query = $this->database->prepare($statement); 556 | $query->execute(); 557 | if (!$query->rowCount()) return null; 558 | 559 | if ($query->rowCount() > self::OPTIONSLIMIT) { 560 | $this->error($this->className() . sprintf(': Maximum number (%1$d < %2$d) of selectable options exceeded. Use filter function in field settings to limit.', self::OPTIONSLIMIT, $query->rowCount())); 561 | return false; 562 | } 563 | $this->errors('clear all'); 564 | 565 | while ($row = $query->fetch(\PDO::FETCH_ASSOC)) $options[$row[$value]] = strlen((string) $row[$label])? $row[$label] : $row[$defaultLabel]; 566 | return $options; 567 | } 568 | 569 | /** 570 | * Hookable function called from Inputield provides option to edit option labels 571 | * 572 | * @param string $label (option label) 573 | * @param int $value (option value) 574 | * @param object $page 575 | * @param object $field 576 | * @return string 577 | * 578 | */ 579 | protected function ___label($label, $value, Page $page, Field $field) { 580 | return $label; 581 | } 582 | 583 | /** 584 | * Hookable function provides option to filter options array 585 | * 586 | * @param object $field 587 | * @return string SQL command filter part. example: "column = 'value'" 588 | * 589 | */ 590 | protected function ___filter(Field $field) { 591 | // all parts set? 592 | if (!$field->filter_column||!$field->filter_selector||$field->filter_value === null) return false; 593 | // valid operator? 594 | $operators = array(' LIKE ',' NOT LIKE '); 595 | // we use pipe to allow OR 596 | if (strpos($field->filter_value, '|')) { 597 | $values = explode('|', $field->filter_value); 598 | foreach ($values as &$v) { 599 | $v = $this->database->quote($v); 600 | } 601 | unset($v); 602 | } else $values = array($this->database->quote($field->filter_value)); 603 | 604 | if (!$this->database->isOperator($field->filter_selector) && !in_array($field->filter_selector,$operators)) return false; 605 | // escape column, compose filter string 606 | $return = ''; 607 | foreach ($values as $value) { 608 | $return .= '`'.$this->database->escapeCol($field->filter_column).'`'.$field->filter_selector.$value. ' OR '; 609 | } 610 | return '(' . rtrim($return, ' OR ') . ')'; 611 | } 612 | 613 | /** 614 | * Return an existing page (data) value if identical with runtime value 615 | * workaround to prevent Page::trackChange() triggered by PageComparison::isEqual() resulting in a message that page value has been changed even if nothing has changed due to strict object comparison (instance must be identical) 616 | * 617 | * @param object $page 618 | * @param object $field 619 | * @param int|array|object $value 620 | * @return int|array|object SelectExtOption|SelectExtOptionArray 621 | * 622 | * @see self::sanitizeValue(), PageComparison::isEqual(), WireData::set() 623 | * 624 | */ 625 | protected function getEqualValue(Page $page, Field $field, $value) { 626 | 627 | // receive current page value (SelectExtOption or SelectExtOptionArray) from data array 628 | $_value = isset($page->data[$field->name]) ? $page->data[$field->name] : null; 629 | if (!$_value) return $value; 630 | 631 | // $this->error('changed'); 632 | 633 | $int1 = null; 634 | $int2 = null; 635 | $array1 = []; 636 | $array2 = []; 637 | 638 | // $this->error('changed'); 639 | 640 | if ($value instanceof SelectExtOption) $int1 = $value->value; 641 | if ($value instanceof SelectExtOptionArray) $array1 = array_values($value->each('value')); 642 | 643 | if ($_value instanceof SelectExtOption) $int2 = $_value->value; 644 | if ($_value instanceof SelectExtOptionArray) $array2 = array_values($_value->each('value')); 645 | 646 | if (is_numeric($value)) $int1 = (int) $value; 647 | if (is_array($value)) { 648 | foreach ($value as $key => $item) { 649 | if ($item instanceof SelectExtOption) $array1[] = $item->value; 650 | else if (is_numeric($item)) $array1[] = (int) $item; 651 | } 652 | } 653 | 654 | $array1 = array_unique($array1); 655 | $array2 = array_unique($array2); 656 | 657 | // sortable? 658 | $module = $this->modules->get($field->input_type); 659 | $sortable = ($module instanceof InputfieldHasArrayValue && $module instanceof InputfieldHasSortableValue)? true : false; 660 | if (!$sortable) { 661 | sort($array1); 662 | sort($array2); 663 | } 664 | 665 | if ($array1 === $array2 && $int1 === $int2) return $_value; 666 | return $value; 667 | } 668 | 669 | public function getBlankValue(Page $page, Field $field) { 670 | $module = $this->modules->get($field->input_type); 671 | return ($module instanceof InputfieldHasArrayValue)? new SelectExtOptionArray() : new SelectExtOption(); 672 | } 673 | 674 | /** 675 | * Return the defaultValue property for this field (if set), or blank otherwise. 676 | * 677 | * Under no circumstances should this return NULL, because that is used by Page to determine if a field has been loaded. 678 | * 679 | * @param object $page 680 | * @param object $field 681 | * @return mixed 682 | * 683 | * @see Fieldtype::getDefaultValue(), Field::getDefaultValue(), InputfieldSelect::checkDefaultValue() 684 | * 685 | */ 686 | public function getDefaultValue(Page $page, Field $field) { 687 | if ($field->init_value !== null) { 688 | return $this->sanitizeValue($page, $field, $field->init_value); 689 | } 690 | return $this->getBlankValue($page, $field); 691 | } 692 | 693 | public function getSingleValue(Field $field, $value) { 694 | // if ($value instanceof SelectExtOption) return $value; 695 | if ($value === null) return new SelectExtOption(); 696 | $new = new SelectExtOption(); 697 | $new->value = (int) $value; 698 | $row = $this->getTableRow($field, $new->value); 699 | if (empty($row)) return new SelectExtOption(); 700 | foreach ($row as $key => $value) { 701 | // skip reserved words 702 | if (in_array($key, array('of','value','label','row','data','options','toString'))) continue; 703 | $new->set($key, $value); 704 | } 705 | if ($field->option_output) $new->toString = $field->option_output; 706 | $new->row = $row; 707 | $new->options = $this->options($field); 708 | $new->label = $new->options[$new->value]; 709 | return $new; 710 | } 711 | 712 | public function sanitizeValue(Page $page, Field $field, $value) { 713 | $value = $this->getEqualValue($page, $field, $value); 714 | if ($value instanceof SelectExtOption) return $value; 715 | if ($value instanceof SelectExtOptionArray) return $value; 716 | if (is_array($value)) { 717 | $return = new SelectExtOptionArray(); 718 | foreach ($value as $key => $val) { 719 | if (is_numeric($val)) $key = $val; 720 | if ($val instanceof SelectExtOption) $key = $val->value; 721 | $return->set((int) $key, $this->getSingleValue($field, $val)); 722 | } 723 | $return->data('options',$this->options($field)); 724 | return $return; 725 | } 726 | if (empty($value) || !is_numeric($value)) return $this->getBlankValue($page, $field); 727 | return $this->getSingleValue($field, $value); 728 | } 729 | 730 | public function wakeupValue(Page $page, Field $field, $value) { 731 | if (empty($value)) return $this->getBlankValue($page, $field); 732 | if (!is_array($value)) $value = array($value); 733 | // if input type has changed ... 734 | $module = $this->modules->get($field->input_type); 735 | if (!$module instanceof InputfieldHasArrayValue) return $this->getSingleValue($field, array_pop($value)); 736 | $return = new SelectExtOptionArray(); 737 | foreach ($value as $val) $return->set((int) $val, $this->getSingleValue($field, $val)); 738 | $return->data('options',$this->options($field)); 739 | return $return; 740 | } 741 | 742 | public function sleepValue(Page $page, Field $field, $value) { 743 | if ($value instanceof SelectExtOption) return ($value->value !== null)? array($value->value) : []; 744 | $return = array(); 745 | if ($value instanceof SelectExtOptionArray) { 746 | foreach ($value as $object) { 747 | if (!$object instanceof SelectExtOption) throw new WireException("Expecting an instance of SelectExtOption"); 748 | if ($object->value === null) continue; 749 | $return[] = "$object->value"; // typecast as string e.g. "0" 750 | } 751 | } 752 | return $return; 753 | } 754 | 755 | /** 756 | * Prep a value for front-end output 757 | * 758 | * This returns a cloned copy of $value with output formatting enabled. 759 | * 760 | * @param Page $page 761 | * @param Field $field 762 | * @param SelectExtOption|SelectExtOptionArray $value 763 | * @return SelectExtOption|SelectExtOptionArray 764 | * 765 | */ 766 | public function ___formatValue(Page $page, Field $field, $value) { 767 | $value = $this->sanitizeValue($page, $field, $value); 768 | if ($value instanceof SelectExtOption) { 769 | $_value = clone $value; 770 | } else { 771 | $_value = new SelectExtOptionArray(); 772 | foreach($value as $option) { 773 | $_option = clone $option; 774 | $_value->add($_option); 775 | } 776 | } 777 | $_value->of(true); 778 | return $_value; 779 | } 780 | 781 | /** 782 | * Given a value, return an portable version of it 783 | * 784 | * @param Page $page 785 | * @param Field $field 786 | * @param string|int|float|array|object|null $value 787 | * @param array $options Optional settings to shape the exported value, if needed: 788 | * - `human` (boolean): When true, Fieldtype may optionally emphasize human readability over importability 789 | * - `language` (null|int|object): Language or language id 790 | * @return int|string|array 791 | * 792 | */ 793 | public function ___exportValue(Page $page, Field $field, $value, array $options = array()) { 794 | $defaultOptions = array( 795 | 'human' => false, 796 | 'language' => null 797 | ); 798 | $options = array_merge($defaultOptions, $options); 799 | 800 | // default return int (single) or array of int (multiple) 801 | if (!$options['human'] || $value->toString == 'value') return $this->sleepValue($page, $field, $value); 802 | 803 | // get language 804 | $language = null; 805 | if ($this->wire('modules')->isInstalled("LanguageSupport") && $options['language']) { 806 | if (!$options['language'] instanceof Language) $language = $this->wire('languages')->get($options['language']); 807 | else $language = $options['language']; 808 | } 809 | 810 | // return label or array of labels (language sensitive) 811 | if ($language->id && $value->toString == 'label') { 812 | if ($value instanceof SelectExtOption) return $this->options($field, $language)[$value->value]; 813 | else { 814 | $return = []; 815 | foreach ($value as $v) $return[] = $this->options($field, $language)[$v->value]; 816 | return $return; 817 | } 818 | } 819 | 820 | // return human readable string or array of strings 821 | $value = $this->sanitizeValue($page, $field, $value); 822 | $value->of(true); 823 | if ($value instanceof SelectExtOption) return "$value"; 824 | else { 825 | $return = []; 826 | foreach ($value as $v) $return[] = "$v"; 827 | return $return; 828 | } 829 | } 830 | 831 | /** 832 | * get a saveable value (array of selectable option values [int]), by searching for matches of $search in label columns or any column 833 | * $page->set('myfield', $return) or $page->setAndSave('myfield', $return) 834 | * 835 | * @param Field $field 836 | * @param string|int|float|array $search 837 | * @param int $full 838 | * - 0: searching in label columns only (multilanguage if set) 839 | * - 1: full search in all columns 840 | * @return array 841 | * 842 | */ 843 | public function getSaveableValue(Field $field, $search, $full = 0) { 844 | 845 | // search for labels 846 | if (!$full) { 847 | $columns = [$field->option_label]; 848 | $otherLanguagePageIDs = $this->modules->isInstalled("LanguageSupport")? $this->modules->get("LanguageSupport")->otherLanguagePageIDs: null; 849 | if (!empty($otherLanguagePageIDs)) { 850 | foreach ($otherLanguagePageIDs as $lid) { 851 | if (empty($field->{"option_label_$lid"})) continue; 852 | $columns[] = $field->{"option_label_$lid"}; 853 | } 854 | } 855 | } 856 | 857 | // full search 858 | else $columns = $this->getDatabaseColumns($field->option_table); 859 | 860 | // compose statement 861 | $columnsString = '`' . implode('`,`', $columns) . '`'; 862 | $filter = $this->filter($field); 863 | $filter = ($filter)? "WHERE $filter AND ":'WHERE '; 864 | if (is_array($search)) $search = implode("' IN ($columnsString) OR '" , $search); 865 | $filter .= "'$search' IN ($columnsString)"; 866 | $statement = "SELECT * FROM `$field->option_table` $filter"; 867 | 868 | // execute statement 869 | $query = $this->database->prepare($statement); 870 | $query->execute(); 871 | if (!$query->rowCount()) return []; 872 | 873 | $result = []; 874 | while($row = $query->fetch(\PDO::FETCH_ASSOC)) { 875 | $result[] = (int) $row[$field->option_value]; 876 | } 877 | if (empty($result)) return []; 878 | return $result; 879 | } 880 | 881 | /** 882 | * Get the database query that matches a Fieldtype table’s data 883 | * translate label to related value (always integer) 884 | * 885 | */ 886 | public function getMatchQuery($query, $table, $subfield, $operator, $value) { 887 | if (!is_numeric($value)) $value = $this->getSaveableValue($query->field, $value, 0); 888 | if (empty($value)) $value = ''; 889 | return parent::getMatchQuery($query, $table, $subfield, $operator, $value); 890 | } 891 | 892 | /** 893 | * Return array with information about what properties and operators can be used with this field 894 | * corresponding to the selected Inputfieldtype (single, multiple) 895 | * 896 | */ 897 | public function ___getSelectorInfo(Field $field, array $data = array()) { 898 | $class = $field->input_type; 899 | $module = $this->modules->get($class); 900 | if($module instanceof InputfieldHasArrayValue) return parent::___getSelectorInfo($field, $data); 901 | else return Fieldtype::___getSelectorInfo($field, $data); 902 | } 903 | 904 | public function ___getConfigInputfields(Field $field) { 905 | 906 | $inputfields = parent::___getConfigInputfields($field); 907 | 908 | // usage 909 | $markup = file_exists(dirname(__FILE__) . '/README.md')?file_get_contents(dirname(__FILE__) . '/README.md'):null; 910 | if ($markup) { 911 | $f = $this->modules->get("InputfieldMarkup"); 912 | $f->label = $this->_('Usage'); 913 | // call textformatter before wrapping 914 | $this->modules->get('TextformatterMarkdownExtra')->format($markup); 915 | $f->markupText = "
%s
", $this->_('Setting an external database failed.'))); 948 | $fieldset->error = true; 949 | } 950 | $inputfields->add($fieldset); 951 | 952 | // DB Name 953 | $f = $this->modules->get("InputfieldText"); 954 | $f->label = $this->_("DB Name"); 955 | $f->attr('name', 'db_name'); 956 | $f->attr('value', $field->db_name); 957 | if ($fieldset->error) $f->set('wrapClass', 'InputfieldStateError uk-alert-danger'); 958 | $f->columnWidth = 20; 959 | $fieldset->append($f); 960 | 961 | // DB User 962 | $f = $this->modules->get("InputfieldText"); 963 | $f->label = $this->_("DB User"); 964 | $f->attr('name', 'db_user'); 965 | $f->attr('value', $field->db_user); 966 | if ($fieldset->error) $f->set('wrapClass', 'InputfieldStateError uk-alert-danger'); 967 | $f->columnWidth = 20; 968 | $fieldset->append($f); 969 | 970 | // DB Pass 971 | $f = $this->modules->get("InputfieldText"); 972 | $f->label = $this->_("DB Pass"); 973 | $f->attr('name', 'db_pass'); 974 | $f->attr('value', $field->db_pass); 975 | if ($fieldset->error) $f->set('wrapClass', 'InputfieldStateError uk-alert-danger'); 976 | $f->columnWidth = 20; 977 | $fieldset->append($f); 978 | 979 | // DB Host 980 | $f = $this->modules->get("InputfieldText"); 981 | $f->label = $this->_("DB Host"); 982 | $f->attr('name', 'db_host'); 983 | $host = (isset($field->db_host))?$field->db_host:'localhost'; 984 | $f->attr('value', $host); 985 | if ($fieldset->error) $f->set('wrapClass', 'InputfieldStateError uk-alert-danger'); 986 | $f->columnWidth = 20; 987 | $fieldset->append($f); 988 | 989 | /* 990 | // DB Socket 991 | $f = $this->modules->get("InputfieldText"); 992 | $f->label = $this->_("Socket"); 993 | $f->attr('name', 'db_socket'); 994 | $f->attr('value', $field->db_socket); 995 | if ($fieldset->error) $f->set('wrapClass', 'InputfieldStateError uk-alert-danger'); 996 | $f->columnWidth = 16.666; 997 | $fieldset->append($f); 998 | */ 999 | 1000 | // DB Port 1001 | $f = $this->modules->get("InputfieldText"); 1002 | $f->label = $this->_("DB Port"); 1003 | $f->attr('name', 'db_port'); 1004 | $field->db_port? $f->attr('value', $field->db_port) : $f->attr('value', $this->wire('config')->dbPort); 1005 | if ($fieldset->error) $f->set('wrapClass', 'InputfieldStateError uk-alert-danger'); 1006 | $f->columnWidth = 20; 1007 | $fieldset->append($f); 1008 | 1009 | // create options 1010 | $fieldset = $this->modules->get("InputfieldFieldset"); 1011 | $fieldset->label = $this->_('Create options from any database table'); 1012 | $fieldset->notes = $this->_("Save after selecting 'Source Table' to populate the appropriate select for 'Option Label' and 'Option Value'. Make a selection and save again."); 1013 | $inputfields->add($fieldset); 1014 | 1015 | // source table 1016 | $f = $this->modules->get("InputfieldSelect"); 1017 | $f->label = $this->_("Source Table"); 1018 | $f->attr('name', 'option_table'); 1019 | $f->required = true; 1020 | $f->attr('value', $field->option_table); 1021 | if (!$field->option_table) $f->addOption(null, 'no table selected!',array('selected'=>'selected')); 1022 | // we use $dir for limit to check if one single row exists 1023 | $table = $this->getExtOptions($field->option_table,false,false,false,false,'LIMIT 1'); 1024 | if ($table === null && !count($_POST)) $f->error('Table doesn\'t contain any data!'); 1025 | else if ($this->database) foreach ($this->tables as $table) $f->addOption($table, $table); 1026 | $f->description = $this->_("Choose a table in your database."); 1027 | $f->columnWidth = 33; 1028 | $fieldset->append($f); 1029 | 1030 | $columns = $this->getDatabaseColumns($field->option_table); 1031 | // option value column 1032 | $f = $this->modules->get("InputfieldSelect"); 1033 | $f->label = $this->_("Option Value"); 1034 | $f->attr('name', 'option_value'); 1035 | $f->attr('value', $field->option_value); 1036 | $f->description = $this->_("Choose an integer type column."); 1037 | if ($columns) foreach ($columns as $value) { 1038 | if ($this->isIntColumn($field->option_table,$value)) $f->addOption($value, $value); 1039 | } 1040 | if ($this->database && $field->option_table && !count($f->getOptions()) && !count($_POST)) $f->error("Only tables with columns of type 'integer' allowed!"); 1041 | if (!$field->option_value) $f->addOption(null, 'no column selected!',array('selected'=>'selected')); 1042 | $f->columnWidth = 34; 1043 | $fieldset->append($f); 1044 | 1045 | // multilanguage environment 1046 | $otherLanguagePageIDs = $this->modules->isInstalled("LanguageSupport")? $this->modules->get("LanguageSupport")->otherLanguagePageIDs:null; 1047 | // option label column (default language) 1048 | $f = $this->modules->get("InputfieldSelect"); 1049 | $appendLabelText = $otherLanguagePageIDs? " (default language)":''; 1050 | $f->label = $this->_("Option Label$appendLabelText"); 1051 | $f->attr('name', 'option_label'); 1052 | $f->attr('value', $field->option_label); 1053 | $f->description = $this->_("Choose from all columns."); 1054 | if (!$field->option_label) $f->addOption(null, 'no column selected!',array('selected'=>'selected')); 1055 | if ($columns) foreach ($columns as $label) $f->addOption($label, $label); 1056 | $f->columnWidth = 33; 1057 | $fieldset->append($f); 1058 | 1059 | // option label column (other languages) 1060 | if ($otherLanguagePageIDs) { 1061 | $columnWidth = floor(100/ count($otherLanguagePageIDs)); 1062 | 1063 | foreach ($otherLanguagePageIDs as $otherLanguagePageID) { 1064 | $langName = $this->wire('languages')->get($otherLanguagePageID)->name; 1065 | $f = $this->modules->get("InputfieldSelect"); 1066 | $f->label = sprintf($this->_("Option Label (%s)"),$langName); 1067 | $f->attr('name', "option_label_$otherLanguagePageID"); 1068 | $f->attr('value', $field->{"option_label_$otherLanguagePageID"}); 1069 | $f->description = $this->_("Choose from all columns."); 1070 | if (!$field->option_label) $f->addOption(null, 'no column selected!',array('selected'=>'selected')); 1071 | if ($columns) foreach ($columns as $label) $f->addOption($label, $label); 1072 | $f->columnWidth = $columnWidth; 1073 | $f->collapsed = Inputfield::collapsedBlank; 1074 | $fieldset->append($f); 1075 | } 1076 | } 1077 | 1078 | // option filter 1079 | $fieldset = $this->modules->get("InputfieldFieldset"); 1080 | $fieldset->label = $this->_('Filter'); 1081 | $fieldset->description = $this->_("Configure to filter the option list. Use pipes to create OR groups."); 1082 | $filter = $this->filter($field); 1083 | $fieldset->notes = $this->_("SELECT * FROM $field->option_table WHERE $filter"); 1084 | $fieldset->collapsed = Inputfield::collapsedBlank; 1085 | $fieldset->showIf = "option_value!=''"; 1086 | $inputfields->add($fieldset); 1087 | 1088 | // filter column 1089 | $f = $this->modules->get("InputfieldSelect"); 1090 | $f->label = $this->_("Column"); 1091 | $f->attr('name', 'filter_column'); 1092 | $f->attr('value', $field->filter_column); 1093 | if ($columns) foreach ($columns as $filtercol) $f->addOption($filtercol, $filtercol); 1094 | $f->columnWidth = 33; 1095 | $fieldset->append($f); 1096 | 1097 | // filter selector 1098 | $f = $this->modules->get("InputfieldSelect"); 1099 | $f->label = $this->_("Selector Operator"); 1100 | $f->attr('name', 'filter_selector'); 1101 | $f->attr('value', $field->filter_selector); 1102 | $selectors = array('=', '<', '>', '>=', '<=', '<>', '!=', ' LIKE ',' NOT LIKE '); 1103 | foreach ($selectors as $selector) $f->addOption($selector, $selector); 1104 | $f->columnWidth = 34; 1105 | $fieldset->append($f); 1106 | 1107 | // filter value 1108 | $f = $this->modules->get("InputfieldText"); 1109 | $f->label = $this->_("Value"); 1110 | $f->attr('name', 'filter_value'); 1111 | $f->attr('value', $field->filter_value); 1112 | $f->columnWidth = 33; 1113 | $fieldset->append($f); 1114 | 1115 | // option orderby 1116 | $f = $this->modules->get("InputfieldSelect"); 1117 | $f->label = $this->_("Order by"); 1118 | $f->description = $this->_("Default: Order by label."); 1119 | $f->notes = $this->_("Hook in function `___label(\$label, \$value, \$page, \$field)` to modify the label for your needs."); 1120 | $f->attr('name', 'option_order'); 1121 | $f->attr('value', $field->option_order); 1122 | if ($columns) foreach ($columns as $ordercol) { 1123 | if ($ordercol == $field->option_label) continue; 1124 | $f->addOption($ordercol, $ordercol); 1125 | } 1126 | $f->collapsed = Inputfield::collapsedBlank; 1127 | $f->showIf = "option_value!=''"; 1128 | // $f->columnWidth = 50; 1129 | $inputfields->append($f); 1130 | 1131 | // option order asc/ desc 1132 | $f = $this->modules->get("InputfieldRadios"); 1133 | $f->label = $this->_("Order Direction"); 1134 | $f->attr('name', 'option_asc'); 1135 | ($field->option_asc)?$f->attr('value', $field->option_asc):$f->attr('value', 0); 1136 | $f->addOption(0,$this->_("Ascending")); 1137 | $f->addOption(1,$this->_("Descending")); 1138 | $f->collapsed = $field->option_asc? Inputfield::collapsedBlank : Inputfield::collapsedYes; 1139 | $f->showIf = "option_value!=''"; 1140 | // $f->columnWidth = 50; 1141 | $inputfields->append($f); 1142 | 1143 | // option output 1144 | $f = $this->modules->get("InputfieldSelect"); 1145 | $f->label = $this->_("Output"); 1146 | $f->attr('name', 'option_output'); 1147 | $f->attr('value', $field->option_output? $field->option_output : 'value'); 1148 | $f->description = $this->_("Select a property for direct output `__toString()`."); 1149 | $labelLabel = $otherLanguagePageIDs? $this->_('label (language sensitive)') : 'label'; 1150 | $f->addOption($this->_('previously set'), array('value' => 'value', 'label' => $labelLabel)); 1151 | // disallow reserved properties 1152 | if ($columns) $columns = array_diff($columns, array('value','label','row','data','options','toString')); 1153 | $f->addOption($this->_('table columns'), array_combine($columns, $columns)); 1154 | $f->collapsed = Inputfield::collapsedYes; 1155 | $f->showIf = "option_value!=''"; 1156 | $f->required = true; 1157 | $inputfields->append($f); 1158 | 1159 | // initial selection field preview 1160 | $options = $this->options($field); 1161 | if (!empty($options) && $field->input_type && $f = $this->wire('modules')->get($field->input_type)) { 1162 | $f->optionColumns = $field->optionColumns? $field->optionColumns:null; 1163 | $f->attr('name', 'init_value'); 1164 | $f->label = $this->_('What options do you want pre-selected? (if any)'); 1165 | 1166 | $f->collapsed = $field->option_value && $field->option_label? Inputfield::collapsedBlank : Inputfield::collapsedYes; 1167 | $f->description = sprintf($this->_('This field also serves as a preview of your selected input type (%s) and options.'), $field->input_type); 1168 | // allow no pre-selection if value 0 is selectable 1169 | if (array_key_exists(0, $options) && $f instanceof InputfieldSelectMultiple === false) { 1170 | $label = $field->input_type == 'InputfieldRadios'? '*no pre-selection*':' '; 1171 | $f->addOption('',$label); 1172 | } 1173 | foreach ($options as $value => $label) { 1174 | $f->addOption($value, $label); 1175 | } 1176 | $f->attr('value', $field->init_value); 1177 | if (!$field->required) { 1178 | $f->notes = $this->_('Please note: your selections here do not become active unless a value is *always* required for this field. See the "required" option on the Input tab of your field settings.'); 1179 | } else { 1180 | $f->notes = $this->_('This feature is active since a value is always required.'); 1181 | } 1182 | $f->showIf = "option_value!='',option_label!=''"; 1183 | $inputfields->add($f); 1184 | } 1185 | // no selectable options provided 1186 | else if ($field->option_table && $field->option_lable && $field->option_value && !count($_POST) && empty($options)) { 1187 | $this->error("No selectable options provided. Check your 'Source' and 'Filter' settings."); 1188 | } 1189 | return $inputfields; 1190 | } 1191 | } 1192 | 1193 | /** 1194 | * Helper WireData Class to hold a SelectExtOption object 1195 | * 1196 | */ 1197 | class SelectExtOption extends WireData { 1198 | 1199 | /** 1200 | * Array where get/set properties are stored 1201 | * 1202 | */ 1203 | protected $data = array(); 1204 | 1205 | /** 1206 | * Output formatting on/off 1207 | * 1208 | * @var bool 1209 | * 1210 | */ 1211 | protected $of = false; 1212 | 1213 | public function __construct($value = null) { 1214 | parent::__construct(); 1215 | $this->set('value', null); 1216 | $this->set('label', null); 1217 | $this->set('toString', 'value'); 1218 | $this->set('row', array()); 1219 | $this->set('options', array()); 1220 | } 1221 | 1222 | public function set($key, $value) { 1223 | if (in_array($key,array('value','label','row','data','options'))) { 1224 | if ($key == 'data') throw new WireException("'data' property is reserved by WireData class. (array where get/set properties are stored)"); 1225 | // validation of label ? 1226 | if ($key == 'value' && !is_null($value) && !is_int($value)) throw new WireException("SelectExtOption object only accepts integer (int) as value property"); 1227 | if ($key == ('row'|'options') && !is_array($value)) throw new WireException("SelectExtOption object only accepts arrays as '$key' property"); 1228 | } 1229 | return parent::set($key, $value); 1230 | } 1231 | 1232 | public function get($key) { 1233 | return parent::get($key); 1234 | } 1235 | 1236 | /** 1237 | * Turn output formatting on or off, or get current value 1238 | * 1239 | * @param null|bool $of Omit to return current value, or specify true|false to set 1240 | * @return bool 1241 | * 1242 | */ 1243 | public function of($of = null) { 1244 | if(is_null($of)) return $this->of; 1245 | $this->of = $of ? true : false; 1246 | return $this->of; 1247 | } 1248 | 1249 | public function __toString() { 1250 | if ($this->of == false) return (string) $this->value; 1251 | // get label (language support: current user language) 1252 | if ($this->toString == 'label') return $this->options[$this->value]; 1253 | return (string) $this->{$this->toString}; 1254 | } 1255 | } 1256 | 1257 | /** 1258 | * Helper WireArray Class to hold a SelectExtOptionArray object 1259 | * 1260 | */ 1261 | class SelectExtOptionArray extends WireArray { 1262 | 1263 | /** 1264 | * Output formatting on or off 1265 | * 1266 | * @var bool 1267 | * 1268 | */ 1269 | protected $of = false; 1270 | 1271 | /** 1272 | * Get or set output formatting mode 1273 | * 1274 | * @param bool|null $of Omit to retrieve mode, or specify bool to set it 1275 | * @return bool Current mode. If also setting mode, returns previous mode. 1276 | * 1277 | */ 1278 | public function of($of = null) { 1279 | $_of = $this->of; 1280 | if(is_null($of)) return $_of; 1281 | $this->of = $of ? true : false; 1282 | foreach($this as $option) { 1283 | /** @var SelectExtOption $option */ 1284 | $option->of($this->of); 1285 | } 1286 | return $_of; // whatever previous value was 1287 | } 1288 | 1289 | public function isValidItem($item) { 1290 | return $item instanceof SelectExtOption; 1291 | } 1292 | 1293 | public function makeBlankItem() { 1294 | return $this->wire(new SelectExtOption()); 1295 | } 1296 | 1297 | /** 1298 | * Return string value of these options (pipe separated IDs or toString value of each selected option if output formatting enabled) 1299 | * 1300 | * @return string 1301 | * 1302 | */ 1303 | public function __toString() { 1304 | if ($this->of == false) return $this->implode('|', 'value'); 1305 | $formattedString = []; 1306 | foreach ($this->getAll() as $option) { 1307 | $formattedString[] = "$option"; 1308 | } 1309 | return implode('|', $formattedString); 1310 | } 1311 | } 1312 | --------------------------------------------------------------------------------