1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-14 10:45:54 +02:00

Add support for PageFinder selectors where field name is the name of a Fieldtype module. During search, it translates to all fields having that Fieldtype. This enables you to perform a search on all text/textarea fields at once, for example, "FieldtypeTextarea%=something". You can also do "FieldtypeText.extends%=something", and that will include all fields that use or extend the given Fieldtype. Or you can do "FieldtypeText.fields%=something" and the resulting PageArray will contain a "fields" data property that contains an array of matched fields and the quantity of pages that matched each. You can also do "FieldtypeText.extends.fields%=something" and it will include both behaviors mentioned here. Finally, you can use OR expressions here too, like "FieldtypeText|FieldtypeTextarea%=something". Note that these searches are not particularly fast and may have limits on scale. The "FieldtypeName.fields" option is slower than without the "fields" option, BUT it enables it to scale further. In my testing so far, the %= operator actually performs quite a bit better here for text-based searches than the *= or ~= operators, likely because the resulting query probably really limits use of the fulltext indexes.

This commit is contained in:
Ryan Cramer
2018-02-09 09:58:49 -05:00
parent bafe3d4a12
commit 0630b3a3c3
3 changed files with 175 additions and 8 deletions

View File

@@ -159,6 +159,7 @@ class PageFinder extends Wire {
protected $extraOrSelectors = array(); // one from each field must match
protected $sortsAfter = array(); // apply these sorts after pages loaded
protected $reverseAfter = false; // reverse order after load?
protected $pageArrayData = array(); // any additional data that should be populated back to any resulting PageArray objects
// protected $extraSubSelectors = array(); // subselectors that are added in after getQuery()
// protected $extraJoins = array();
@@ -377,6 +378,7 @@ class PageFinder extends Wire {
$this->templates_id = null;
$this->checkAccess = true;
$this->getQueryNumChildren = 0;
$this->pageArrayData = array();
$this->setupStatusChecks($selectors, $options);
// move getTotal option to a class property, after setupStatusChecks
@@ -514,7 +516,7 @@ class PageFinder extends Wire {
$eq = null;
foreach($selectors as $selector) {
$field = $selector->field;
$field = $selector->field();
if($field === '_custom') {
$selectors->remove($selector);
@@ -548,8 +550,11 @@ class PageFinder extends Wire {
}
$selectors->remove($selector);
} else if(strpos($selector->field(), '.owner.') && !$this->wire('fields')->get('owner')) {
} else if(strpos($field, '.owner.') && !$this->wire('fields')->get('owner')) {
$selector->field = str_replace('.owner.', '__owner.', $selector->field());
} else if(stripos($field, 'Fieldtype') === 0) {
$this->preProcessFieldtypeSelector($selectors, $selector);
}
}
@@ -651,6 +656,117 @@ class PageFinder extends Wire {
$this->start = $start;
}
/**
* Pre-process a selector having field name that begins with "Fieldtype"
*
* @param Selectors $selectors
* @param Selector $selector
*
*/
protected function preProcessFieldtypeSelector(Selectors $selectors, Selector $selector) {
$foundFields = null;
$foundTypes = null;
$replaceFields = array();
$failFields = array();
/** @var Languages|null $languages */
$languages = $this->wire('languages');
$selectorCopy = null;
foreach($selector->fields() as $fieldName) {
$subfield = '';
$findPerField = false;
$findExtends = false;
if(strpos($fieldName, '.')) {
$parts = explode('.', $fieldName);
$fieldName = array_shift($parts);
foreach($parts as $k => $part) {
if($part === 'fields') {
$findPerField = true;
unset($parts[$k]);
} else if($part === 'extends') {
$findExtends = true;
unset($parts[$k]);
}
}
if(count($parts)) $subfield = implode('.', $parts);
}
$fieldtype = $this->wire('fieldtypes')->get($fieldName);
if(!$fieldtype) continue;
$fieldtypeLang = $languages ? $this->wire('fieldtypes')->get("{$fieldName}Language") : null;
foreach($this->wire('fields') as $f) {
if($findExtends) {
// allow any Fieldtype that is an instance of given one, or extends it
if(!wireInstanceOf($f->type, $fieldtype)
&& ($fieldtypeLang === null || !wireInstanceOf($f->type, $fieldtypeLang))) continue;
} else {
// only allow given Fieldtype
if($f->type !== $fieldtype && ($fieldtypeLang === null || $f->type !== $fieldtypeLang)) continue;
}
$fName = $subfield ? "$f->name.$subfield" : $f->name;
if($findPerField) {
if($selectorCopy === null) $selectorCopy = clone $selector;
$selectorCopy->field = $fName;
$selectors->replace($selector, $selectorCopy);
$count = $this->wire('pages')->count($selectors);
$selectors->replace($selectorCopy, $selector);
if($count) {
if($foundFields === null) {
$foundFields = isset($this->pageArrayData['fields']) ? $this->pageArrayData['fields'] : array();
}
// include only fields that we know will match
$replaceFields[$fName] = $fName;
if(isset($foundFields[$fName])) {
$foundFields[$fName] += $count;
} else {
$foundFields[$fName] = $count;
}
} else {
$failFields[$fName] = $fName;
}
} else {
// include all fields (faster)
$replaceFields[$fName] = $fName;
}
if($findExtends) {
if($foundTypes === null) {
$foundTypes = isset($this->pageArrayData['extends']) ? $this->pageDataArray['extends'] : array();
}
$fType = $f->type->className();
if(isset($foundTypes[$fType])) {
$foundTypes[$fType][] = $fName;
} else {
$foundTypes[$fType] = array($fName);
}
}
}
}
if(count($replaceFields)) {
$selector->fields = array_values($replaceFields);
} else if(count($failFields)) {
// forced non-match and prevent field-not-found error after this method
$selector->field = reset($failFields);
}
if(is_array($foundFields)) {
arsort($foundFields);
$this->pageArrayData['fields'] = $foundFields;
}
if(is_array($foundTypes)) {
$this->pageArrayData['extends'] = $foundTypes;
}
}
/**
* Pre-process the given selector to perform any necessary replacements
@@ -2456,5 +2572,19 @@ class PageFinder extends Wire {
return true;
}
/**
* Get data that should be populated back to any resulting PageArrays data() method
*
* @param PageArray|null $pageArray Optionally populate given PageArray
* @return array
*
*/
public function getPageArrayData(PageArray $pageArray = null) {
if($pageArray !== null && count($this->pageArrayData)) {
$pageArray->data($this->pageArrayData);
}
return $this->pageArrayData;
}
}

View File

@@ -335,6 +335,7 @@ class PagesLoader extends Wire {
$pages = $this->pages->newPageArray($loadOptions);
}
$pageFinder->getPageArrayData($pages);
$pages->setTotal($total);
$pages->setLimit($limit);
$pages->setStart($start);

View File

@@ -2101,19 +2101,55 @@ class WireArray extends Wire implements \IteratorAggregate, \ArrayAccess, \Count
* #pw-group-other-data-storage
* #pw-link [Introduction of data method](https://processwire.com/talk/topic/5098-new-wirearray-api-additions-on-dev/)
*
* @param string|null $key Name of data property you want to get or set. Omit to get all data properties.
* @param mixed|null $value Value of data property you want to set. Omit when getting properties.
* @param string|null|array|bool $key Name of data property you want to get or set, or:
* - Omit to get all data properties.
* - Specify associative array of [property => value] to set multiple properties.
* - Specify associative array and boolean TRUE for $value argument to replace all data with the new array given in $key.
* - Specify regular array of property names to return multiple properties.
* - Specify boolean FALSE to unset property name specified in $value argument.
* @param mixed|null|bool $value Value of data property you want to set. Omit when getting properties.
* - Specify boolean TRUE to replace all data with associative array of data given in $key argument.
* @return WireArray|mixed|array|null Returns one of the following, depending on specified arguments:
* - `mixed` when getting a single property: whatever you set is what you will get back.
* - `null` if the property you are trying to get does not exist in the data.
* - `$this` reference to this WireArray if you were setting a value.
* - `array` of all data if you specified no arguments.
* - `array` of all data if you specified no arguments or requested multiple keys.
*
*/
public function data($key = null, $value = null) {
if(is_null($key) && is_null($value)) return $this->extraData;
if(is_null($value)) return isset($this->extraData[$key]) ? $this->extraData[$key] : null;
$this->extraData[$key] = $value;
if($key === null && $value === null) {
// get all properties
return $this->extraData;
} else if(is_array($key)) {
// get or set multiple properties
if($value === true) {
// replace all data with data in given $key array
$this->extraData = $key;
} else {
// test if array is associative
if(ctype_digit(implode('0', array_keys($key)))) {
// regular, non-associative array, GET only requested properties
$a = array();
foreach($key as $k) {
$a[$k] = isset($this->extraData[$k]) ? $this->extraData[$k] : null;
}
return $a;
} else if(count($key)) {
// associative array, setting multiple values to extraData
$this->extraData = array_merge($this->extraData, $key);
}
}
} else if($key === false && is_string($value)) {
// unset a property
unset($this->extraData[$value]);
} else if($value === null) {
// get a property
return isset($this->extraData[$key]) ? $this->extraData[$key] : null;
} else {
// set a property
$this->extraData[$key] = $value;
}
return $this;
}