$value) { $instance->$type = $value; } return $instance; } /** * Create a new instance from a request. * Will look for a classic string term and use that * Otherwise we'll use the details from an advanced search form. */ public static function fromRequest(Request $request): self { if (!$request->has('search') && !$request->has('term')) { return static::fromString(''); } if ($request->has('term')) { return static::fromString($request->get('term')); } $instance = new SearchOptions(); $inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']); $parsedStandardTerms = static::parseStandardTermString($inputs['search'] ?? ''); $instance->searches = array_filter($parsedStandardTerms['terms']); $instance->exacts = array_filter($parsedStandardTerms['exacts']); array_push($instance->exacts, ...array_filter($inputs['exact'] ?? [])); $instance->tags = array_filter($inputs['tags'] ?? []); foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) { if (empty($filterVal)) { continue; } $instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal; } if (isset($inputs['types']) && count($inputs['types']) < 4) { $instance->filters['type'] = implode('|', $inputs['types']); } return $instance; } /** * Decode a search string into an array of terms. */ protected static function decode(string $searchString): array { $terms = [ 'searches' => [], 'exacts' => [], 'tags' => [], 'filters' => [], ]; $patterns = [ 'exacts' => '/"((?:\\\\.|[^"\\\\])*)"/', 'tags' => '/\[(.*?)\]/', 'filters' => '/\{(.*?)\}/', ]; // Parse special terms foreach ($patterns as $termType => $pattern) { $matches = []; preg_match_all($pattern, $searchString, $matches); if (count($matches) > 0) { $terms[$termType] = $matches[1]; $searchString = preg_replace($pattern, '', $searchString); } } // Unescape exacts and backslash escapes foreach ($terms['exacts'] as $index => $exact) { $terms['exacts'][$index] = static::decodeEscapes($exact); } // Parse standard terms $parsedStandardTerms = static::parseStandardTermString($searchString); array_push($terms['searches'], ...$parsedStandardTerms['terms']); array_push($terms['exacts'], ...$parsedStandardTerms['exacts']); // Split filter values out $splitFilters = []; foreach ($terms['filters'] as $filter) { $explodedFilter = explode(':', $filter, 2); $splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : ''; } $terms['filters'] = $splitFilters; // Filter down terms where required $terms['exacts'] = array_filter($terms['exacts']); $terms['searches'] = array_filter($terms['searches']); return $terms; } /** * Decode backslash escaping within the input string. */ protected static function decodeEscapes(string $input): string { $decoded = ""; $escaping = false; foreach (str_split($input) as $char) { if ($escaping) { $decoded .= $char; $escaping = false; } else if ($char === '\\') { $escaping = true; } else { $decoded .= $char; } } return $decoded; } /** * Parse a standard search term string into individual search terms and * convert any required terms to exact matches. This is done since some * characters will never be in the standard index, since we use them as * delimiters, and therefore we convert a term to be exact if it * contains one of those delimiter characters. * * @return array{terms: array, exacts: array} */ protected static function parseStandardTermString(string $termString): array { $terms = explode(' ', $termString); $indexDelimiters = SearchIndex::$delimiters; $parsed = [ 'terms' => [], 'exacts' => [], ]; foreach ($terms as $searchTerm) { if ($searchTerm === '') { continue; } $becomeExact = (strpbrk($searchTerm, $indexDelimiters) !== false); $parsed[$becomeExact ? 'exacts' : 'terms'][] = $searchTerm; } return $parsed; } /** * Set the value of a specific filter in the search options. */ public function setFilter(string $filterName, string $filterValue = ''): void { $this->filters[$filterName] = $filterValue; } /** * Encode this instance to a search string. */ public function toString(): string { $parts = $this->searches; foreach ($this->exacts as $term) { $escaped = str_replace('\\', '\\\\', $term); $escaped = str_replace('"', '\"', $escaped); $parts[] = '"' . $escaped . '"'; } foreach ($this->tags as $term) { $parts[] = "[{$term}]"; } foreach ($this->filters as $filterName => $filterVal) { $parts[] = '{' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}'; } return implode(' ', $parts); } }