<?php
namespace XF\Filterer;
use XF\Mvc\Entity\Finder;
use function in_array, is_array, is_string;
abstract class AbstractFilterer
{
/**
* @var string
*/
protected $finderType;
/**
* @var Finder
*/
protected $finder;
/**
* @var array
*/
protected $setupData;
protected $finalized = false;
protected $applied = false;
/**
* @var array
*/
protected $lookupMap;
protected $rawFilters = [];
protected $linkParams = [];
protected $displayValues = [];
public function __construct(array $setupData = [])
{
$this->finderType = $this->getFinderType();
$this->finder = $this->setupFinder($setupData);
$this->setupData = $setupData;
$lookupTypes = $this->getLookupTypeList();
$this->lookupMap = array_fill_keys($lookupTypes, true);
}
/**
* The short name of the finder that will be used by this filterer.
*
* @return string
*/
abstract protected function getFinderType(): string;
/**
* Mapping of filter names to expected input types. Value should be a type that
* can be used by the input filterer.
*
* @return array
*/
abstract protected function getFilterTypeMap(): array;
/**
* A list of filter names whose values should be looked up before being displayed.
* This lookup is provided by the display template in the form of
* - key:$filterName for name look ups
* - val:$value for value look ups
*
* @return array
*/
abstract protected function getLookupTypeList(): array;
/**
* Applies the specified filter to the finder if needed. Returns true if applied and false otherwise.
*
*
* @param string $filterName
* @param mixed $value Value to apply to the filter. Output value will be used as the link param value.
* @param mixed $displayValue Value to display for this filter. If unmodified, uses the $value
*
* @return bool
*/
abstract protected function applyFilter(string $filterName, &$value, &$displayValue): bool;
/**
* Sets up the finder. Extensions are better done via initFinder.
*
* @param array $setupData
*
* @return Finder
*/
protected function setupFinder(array $setupData): Finder
{
$finder = $this->app()->finder($this->finderType);
$this->initFinder($finder, $setupData);
return $finder;
}
/**
* Ideal place to setup the default elements of the finder (such as order) and to apply
* any setup-based constraints or validations (such as template filtering requiring a style_id).
*
* @param Finder $finder
* @param array $setupData
*/
protected function initFinder(Finder $finder, array $setupData)
{
}
/**
* Gets default values for filters for the purposes of filling out a form. Most commonly,
* this will be a reasonable default for radio or checkbox options.
*
* @return array
*/
protected function getFormDefaults(): array
{
return [];
}
/**
* Adds the specified filters and immediately applies them. (No further changes possible.)
*
* @param array|\XF\Http\Request $input
* @param array|string|null $skip Any number of filters that should be disregarded
*
* @return Finder
*/
public function applyFilters($input, $skip = null): Finder
{
$this->addFilters($input, $skip);
$this->apply();
return $this->finder;
}
/**
* Adds specified filters from input. Input values that are null, 0, empty strings or empty arrays
* will be disregarded.
*
* @param array|\XF\Http\Request $input
* @param array|string|null $skip Any number of filters that should be disregarded
*/
public function addFilters($input, $skip = null)
{
if ($this->finalized)
{
throw new \LogicException("Filters have already been finalized, cannot change");
}
$typeMap = $this->getFilterTypeMap();
if ($input instanceof \XF\Http\Request)
{
$input = $input->filter($typeMap);
}
if (!is_array($input))
{
throw new \LogicException("Input must either be array or Request object");
}
$inputFilterer = $this->app()->inputFilterer();
if ($skip)
{
$skip = (array)$skip;
}
foreach ($typeMap AS $filterName => $inputType)
{
if (!isset($input[$filterName]))
{
continue;
}
if ($skip && in_array($filterName, $skip))
{
continue;
}
$inputValue = $inputFilterer->filter($input[$filterName], $inputType);
if (!$this->hasFilterableValue($filterName, $inputValue))
{
continue;
}
$this->rawFilters[$filterName] = $inputValue;
}
}
protected function hasFilterableValue(string $filterName, $inputValue): bool
{
$noValue = (
$inputValue === null
|| $inputValue === ''
|| $inputValue === 0
|| (is_array($inputValue) && !$inputValue)
);
return !$noValue;
}
public function removeFilter(string $filterName)
{
if ($this->finalized)
{
throw new \LogicException("Filters have already been finalized, cannot change");
}
unset($this->rawFilters[$filterName]);
}
protected function finalize()
{
if ($this->finalized)
{
return;
}
$this->finalized = true;
$this->onFinalize();
}
/**
* Run when the applicable filters have been locked in. Can be used to ensure that certain
* filters are always present or to manipulate the finder in special ways.
*/
protected function onFinalize()
{
}
/**
* Applies the specified filters.
*
* @return Finder
*/
public function apply(): Finder
{
if ($this->applied)
{
throw new \LogicException("Filters already applied");
}
$this->applied = true;
$this->finalize();
foreach ($this->rawFilters AS $filterName => $inputValue)
{
$displayValue = null;
if ($this->applyFilter($filterName, $inputValue, $displayValue))
{
$this->addLinkParam($filterName, $inputValue);
if ($displayValue === null)
{
$displayValue = $inputValue;
}
$this->addDisplayValue($filterName, $displayValue);
}
}
return $this->finder;
}
protected function addLinkParam(string $name, $value)
{
if ($value === true)
{
$value = 1;
}
else if ($value === false)
{
$value = 0;
}
$this->linkParams[$name] = $value;
}
protected function addDisplayValue(string $name, $value)
{
$isLookup = $this->lookupMap[$name] ?? false;
if ($isLookup)
{
if (is_string($value))
{
$value = "val:$value";
}
else if (is_array($value))
{
foreach ($value AS &$v)
{
$v = "val:$v";
}
}
}
$this->displayValues[$name] = $value;
}
public function getFinder(): Finder
{
if (!$this->applied)
{
throw new \LogicException("Can only get finder after filters are applied");
}
return $this->finder;
}
public function getRawFilters(): array
{
return $this->rawFilters;
}
public function getRawFilter(string $filterName)
{
return $this->rawFilters[$filterName] ?? null;
}
public function getFiltersForForm(): array
{
$this->finalize();
return array_replace($this->getFormDefaults(), $this->rawFilters);
}
public function getLinkParams(): array
{
return $this->linkParams;
}
public function getDisplayValues(): array
{
return $this->displayValues;
}
protected function app(): \XF\App
{
return \XF::app();
}
}