diff --git a/.github/actions/setup-cakephp/action.yml b/.github/actions/setup-cakephp/action.yml deleted file mode 100644 index 741a7e9..0000000 --- a/.github/actions/setup-cakephp/action.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: 'Setup CakePHP' -description: 'Clone CakePHP and setup it' - -runs: - using: "composite" - steps: - - - name: Clone CakePHP - uses: actions/checkout@v3 - with: - repository: cakephp/cakephp - path: ./cakephp - ref: 2.10.14 - - - name: Make tmp folder writable - run: chmod -R 777 ./cakephp/app/tmp - shell: bash - - - name: Fix composer autoload - run: | - echo " /tmp/core.php - echo "require ROOT . '/vendors/autoload.php'; " >> /tmp/core.php - echo "spl_autoload_unregister(array('App', 'load')); " >> /tmp/core.php - echo "spl_autoload_register(array('App', 'load'), true, true); " >> /tmp/core.php - echo "?>" >> /tmp/core.php - cat ./cakephp/app/Config/core.php >> /tmp/core.php - cp /tmp/core.php ./cakephp/app/Config/core.php - shell: bash - - - name: Copy plugin files to plugins folder - run: | - mkdir -p ./cakephp/plugins/Filter - cp -R ./Controller ./cakephp/plugins/Filter/Controller - cp -R ./Model ./cakephp/plugins/Filter/Model - cp -R ./Test ./cakephp/plugins/Filter/Test - cp -R ./View ./cakephp/plugins/Filter/View - shell: bash diff --git a/.github/actions/setup-database/action.yml b/.github/actions/setup-database/action.yml deleted file mode 100644 index 128d752..0000000 --- a/.github/actions/setup-database/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: 'Setup Database' -description: 'Create test database and generate database configuration file' - -runs: - using: "composite" - steps: - - - name: Create test database - run: mysql -u root -e "CREATE DATABASE cakephp_test" - shell: bash - - - name: Generate database configuration file - run: echo " array( - 'datasource' => 'Database/Mysql', - 'host' => '127.0.0.1', - 'login' => 'root' - ) - ); - public \$default = array( - 'persistent' => false, - 'host' => '', - 'login' => '', - 'password' => '', - 'database' => 'cakephp_test', - 'prefix' => '' - ); - public \$test = array( - 'persistent' => false, - 'host' => '', - 'login' => '', - 'password' => '', - 'database' => 'cakephp_test', - 'prefix' => '' - ); - public function __construct() { - \$db = 'mysql'; - if (!empty(\$_SERVER['DB'])) { - \$db = \$_SERVER['DB']; - } - foreach (array('default', 'test') as \$source) { - \$config = array_merge(\$this->{\$source}, \$this->identities[\$db]); - if (is_array(\$config['database'])) { - \$config['database'] = \$config['database'][\$source]; - } - if (!empty(\$config['schema']) && is_array(\$config['schema'])) { - \$config['schema'] = \$config['schema'][\$source]; - } - \$this->{\$source} = \$config; - } - } - }" > ./cakephp/app/Config/database.php - shell: bash diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 954a2ed..ea044b9 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -22,18 +22,17 @@ jobs: mysql-version: '5.6' - name: Check out repository code uses: actions/checkout@v3 - - name: Setup CakePHP - uses: ./.github/actions/setup-cakephp - - name: Setup database - uses: ./.github/actions/setup-database - - name: Install PHPUnit + - name: Install Dependencies run: | - cd ./cakephp - composer require 'phpunit/phpunit=5.7' - cd ../ + composer remove --dev phpstan/phpstan cakephp/cakephp-codesniffer overtrue/phplint --no-update + composer require 'cakephp/cakephp=3.10.5' 'phpunit/phpunit=5.7.0' --with-all-dependencies + shell: bash + - name: Create test database + run: mysql -u root -e "CREATE DATABASE cakephp_test" + shell: bash - name: Unit Tests - run: ./cakephp/lib/Cake/Console/cake test Filter All --stderr -app ./cakephp/app + run: ./vendor/bin/phpunit ./tests PHP-Lint: runs-on: ubuntu-latest @@ -58,13 +57,24 @@ jobs: php-version: 7.4 - name: Check out repository code uses: actions/checkout@v3 - - name: Setup CakePHP - uses: ./.github/actions/setup-cakephp - - name: Install PHPUnit - run: composer require 'phpunit/phpunit=7.0' - - name: Install PHPStan - run: | - composer require --dev phpstan/phpstan - composer require --dev phpstan/phpstan-phpunit + - name: Install Dependencies + run: composer require 'cakephp/cakephp=3.10.5' 'phpunit/phpunit=7.0' phpstan/phpstan phpstan/phpstan-phpunit --with-all-dependencies - name: PHPStan - run: vendor/bin/phpstan analyse --level=8 ./cakephp/plugins/Filter + run: vendor/bin/phpstan analyse --level=8 ./src ./tests + + PHP-Code-Sniffer: + runs-on: ubuntu-latest + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 5.6 + - name: Check out repository code + uses: actions/checkout@v3 + - name: Install PHPCS + run: | + composer remove --dev phpstan/phpstan phpunit/phpunit overtrue/phplint --no-update + composer require cakephp/cakephp-codesniffer + vendor/bin/phpcs --config-set installed_paths /home/runner/work/cakephp-filter-plugin/cakephp-filter-plugin/vendor/cakephp/cakephp-codesniffer + - name: Run PHPCS + run: vendor/bin/phpcs --colors --parallel=16 -p --standard=CakePHP --extensions=php,ctp src/ tests/ diff --git a/.gitignore b/.gitignore index ca21e90..dd66903 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /.settings/ /.buildpath /.project +/.phplint-cache +/composer.lock +/vendor/ diff --git a/Controller/Component/FilterComponent.php b/Controller/Component/FilterComponent.php deleted file mode 100644 index 5b96c44..0000000 --- a/Controller/Component/FilterComponent.php +++ /dev/null @@ -1,417 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -App::import('Component', 'Session'); -App::import('Behavior', 'Filter.Filtered'); - -/** - * @property RequestHandlerComponent $RequestHandler - * @property SessionComponent $Session - */ -class FilterComponent extends Component -{ - /** - * @var string[] - */ - public $components = array('Session'); - - /** - * @var mixed[] - */ - public $settings = array(); - - /** - * @var mixed[] - */ - public $nopersist = array(); - - /** - * @var mixed[] - */ - public $formData = array(); - - /** - * @var mixed[] - */ - protected $_request_settings = array(); - - /** - * @param \ComponentCollection $collection - * @param mixed[] $settings - */ - public function __construct(ComponentCollection $collection, $settings = array()) - { - parent::__construct($collection, $settings); - $this->_request_settings = $settings; - } - - public function initialize(Controller $controller) - { - if (!isset($controller->filters)) - { - return; - } - - $this->__updatePersistence($controller, $this->_request_settings); - $this->settings[$controller->name] = $controller->filters; - - if (!isset($this->settings[$controller->name][$controller->request->action])) - { - return; - } - - $settings = $this->settings[$controller->name][$controller->request->action]; - - foreach ($settings as $model => $filter) - { - if (!isset($controller->{$model})) - { - trigger_error(__('Filter model not found: %s', $model)); - continue; - } - - $controller->$model->Behaviors->attach('Filter.Filtered', $filter); - } - } - - public function startup(Controller $controller) - { - if (!isset($this->settings[$controller->name][$controller->request->action])) - { - return; - } - - $settings = $this->settings[$controller->name][$controller->request->action]; - - if (!in_array('Filter.Filter', $controller->helpers)) - { - $controller->helpers[] = 'Filter.Filter'; - } - - $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', $controller->name, $controller->request->action); - $filterFormId = $controller->request->query('filterFormId'); - if ($controller->request->is('get') && !empty($filterFormId)) - { - $this->formData = $controller->request->query('data'); - } - elseif (!$controller->request->is('post') || !isset($controller->request->data['Filter']['filterFormId'])) - { - $persistedData = array(); - - if ($this->Session->check($sessionKey)) - { - $persistedData = $this->Session->read($sessionKey); - } - - if (empty($persistedData)) - { - return; - } - - $this->formData = $persistedData; - } - else - { - $this->formData = $controller->request->data; - if ($this->Session->started()) - { - $this->Session->write($sessionKey, $this->formData); - } - } - - foreach ($settings as $model => $options) - { - if (!isset($controller->{$model})) - { - trigger_error(__('Filter model not found: %s', $model)); - continue; - } - - $controller->$model->setFilterValues($this->formData); - } - } - - public function beforeRender(Controller $controller) - { - if (!isset($this->settings[$controller->name][$controller->request->action])) - { - return; - } - - $models = $this->settings[$controller->name][$controller->request->action]; - $viewFilterParams = array(); - - foreach ($models as $model => $fields) - { - if (!isset($controller->$model)) - { - trigger_error(__('Filter model not found: %s', $model)); - continue; - } - - foreach ($fields as $field => $settings) - { - if (!is_array($settings)) - { - $field = $settings; - $settings = array(); - } - - if (!isset($settings['required'])) - { - $settings['required'] = false; - } - - if (!isset($settings['type'])) - { - $settings['type'] = 'text'; - } - - $options = array(); - - $fieldName = $field; - $fieldModel = $model; - - if (strpos($field, '.') !== false) - { - list($fieldModel, $fieldName) = explode('.', $field); - } - - if (!empty($this->formData)) - { - if (isset($this->formData[$fieldModel][$fieldName])) - { - $options['value'] = $this->formData[$fieldModel][$fieldName]; - - if ($options['value']) - { - $options['class'] = 'filter-active'; - } - } - } - - if (isset($settings['inputOptions'])) - { - if (!is_array($settings['inputOptions'])) - { - $settings['inputOptions'] = array($settings['inputOptions']); - } - - $options = array_merge($options, $settings['inputOptions']); - } - - if (isset($settings['label'])) - { - $options['label'] = $settings['label']; - } - - switch ($settings['type']) - { - case 'select': - $options['type'] = 'select'; - - $selectOptions = array(); - /** @var \Model|bool $workingModel */ - $workingModel = ClassRegistry::init($fieldModel); - if (is_bool($workingModel)) { - throw new MissingModelException(array($fieldModel)); - } - - if (isset($settings['selectOptions'])) - { - $selectOptions = $settings['selectOptions']; - } - - if (isset($settings['selector'])) - { - if (!method_exists($workingModel, $settings['selector'])) - { - trigger_error - ( - __( - 'Selector method "%s" not found in model "%s" for field "%s"!', - $settings['selector'], - $fieldModel, - $fieldName - ) - ); - return; - } - - $selectorName = $settings['selector']; - $options['options'] = $workingModel->$selectorName($selectOptions); - } - else - { - if ($fieldModel == $model) - { - $options['options'] = $workingModel->find - ( - 'list', - array_merge - ( - $selectOptions, - array - ( - 'nofilter' => true, - 'fields' => array($fieldName, $fieldName), - ) - ) - ); - } - else - { - $options['options'] = $workingModel->find('list', array_merge($selectOptions, array('nofilter' => true))); - } - } - - if (!$settings['required']) - { - $options['empty'] = ''; - } - - if (isset($settings['multiple'])) - { - $options['multiple'] = $settings['multiple']; - } - - break; - - case 'checkbox': - $options['type'] = 'checkbox'; - - if (isset($options['value'])) - { - $options['checked'] = !!$options['value']; - unset($options['value']); - } - else if (isset($settings['default'])) - { - $options['checked'] = !!$settings['default']; - } - break; - - default: - $options['type'] = $settings['type']; - break; - } - - // if no value has been set, show the default one - if (!isset($options['value']) && - isset($settings['default']) && - $options['type'] != 'checkbox') - { - $options['value'] = $settings['default']; - } - - $viewFilterParams[] = array - ( - 'name' => sprintf('%s.%s', $fieldModel, $fieldName), - 'options' => $options - ); - } - } - - if (!empty($this->settings['add_filter_value_to_title']) && - array_search($controller->action, $this->settings['add_filter_value_to_title']) !== false) - { - $title = $controller->viewVars['title_for_layout']; - foreach ($viewFilterParams as $viewFilterParam) - { - if (!empty($viewFilterParam['options']['class']) && - $viewFilterParam['options']['class'] == 'filter-active') - { - $titleValue = $viewFilterParam['options']['value']; - if ($viewFilterParam['options']['type'] == 'select') - { - $titleValue = $viewFilterParam['options']['options'][$titleValue]; - } - $title .= ' - ' . $titleValue; - } - } - $controller->set('title_for_layout', $title); - } - $controller->set('viewFilterParams', $viewFilterParams); - } - - /** - * @param \Controller $controller - * @param mixed[] $settings - * @return void - */ - private function __updatePersistence($controller, $settings) - { - if ($this->Session->check('FilterPlugin.NoPersist')) - { - $this->nopersist = $this->Session->read('FilterPlugin.NoPersist'); - } - - if (isset($settings['nopersist'])) - { - $this->nopersist[$controller->name] = $settings['nopersist']; - if ($this->Session->started()) - { - $this->Session->write('FilterPlugin.NoPersist', $this->nopersist); - } - } - else if (isset($this->nopersist[$controller->name])) - { - unset($this->nopersist[$controller->name]); - if ($this->Session->started()) - { - $this->Session->write('FilterPlugin.NoPersist', $this->nopersist); - } - } - - if (!empty($this->nopersist)) - { - foreach ($this->nopersist as $nopersistController => $actions) - { - if (is_string($actions)) - { - $actions = array($actions); - } - else if ($actions === true) - { - $actions = array(); - } - - if (empty($actions) && $controller->name != $nopersistController) - { - if ($this->Session->check(sprintf('FilterPlugin.Filters.%s', $nopersistController))) - { - $this->Session->delete(sprintf('FilterPlugin.Filters.%s', $nopersistController)); - continue; - } - } - - foreach ($actions as $action) - { - if ($controller->name == $nopersistController && $action == $controller->request->action) - { - continue; - } - - if ($this->Session->check(sprintf('FilterPlugin.Filters.%s.%s', $nopersistController, $action))) - { - $this->Session->delete(sprintf('FilterPlugin.Filters.%s.%s', $nopersistController, $action)); - } - } - } - } - } - - public function shutdown(Controller $controller) - { - } -} diff --git a/Controller/FilterAppController.php b/Controller/FilterAppController.php deleted file mode 100644 index 50f98c9..0000000 --- a/Controller/FilterAppController.php +++ /dev/null @@ -1,17 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -class FilterAppController extends AppController -{ - -} diff --git a/Model/Behavior/FilteredBehavior.php b/Model/Behavior/FilteredBehavior.php deleted file mode 100644 index 8aa0984..0000000 --- a/Model/Behavior/FilteredBehavior.php +++ /dev/null @@ -1,493 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -App::uses('Sanitize', 'Utility'); - -class FilteredBehavior extends ModelBehavior -{ - /** - * Keeps current values after filter form post. - * - * @var mixed[] - */ - protected $_filterValues = array(); - - /** - * {@inheritDoc} - * - * @param \Model $Model Model using this behavior - * @param mixed[] $settings Configuration settings for $model - * @return void - */ - public function setup(Model $Model, $settings = array()) - { - foreach ($settings as $key => $value) - { - if (!is_array($value)) - { - $key = $value; - $value = array(); - } - - $this->settings[$Model->alias][$key] = array_merge - ( - array - ( - 'type' => 'text', - 'condition' => 'like', - 'required' => false, - 'selectOptions' => array() - ), - $value - ); - } - - $this->_filterValues[$Model->alias] = array(); - } - - /** - * {@inheritDoc} - * - * @param \Model $Model Model using this behavior - * @param mixed[] $query Data used to execute this query, i.e. conditions, order, etc. - * @return bool|mixed[] - */ - public function beforeFind(Model $Model, $query) - { - if (isset($query['nofilter']) && $query['nofilter'] === true) - { - return $query; - } - $callbackOptions = array(); - if (method_exists($Model, 'beforeDataFilter')) - { - $callbackOptions['values'] = $this->_filterValues[$Model->alias]; - $callbackOptions['settings'] = $this->settings[$Model->alias]; - - if (!$Model->beforeDataFilter($query, $callbackOptions)) - { - return $query; - } - } - - if (!isset($this->settings[$Model->alias])) - { - return $query; - } - - $settings = $this->settings[$Model->alias]; - $values = $this->_filterValues[$Model->alias]; - - foreach ($settings as $field => $options) - { - $this->addFieldToFilter($Model, $query, $settings, $values, $field, $options); - } - - if (method_exists($Model, 'afterDataFilter')) - { - $callbackOptions['values'] = $this->_filterValues[$Model->alias]; - $callbackOptions['settings'] = $this->settings[$Model->alias]; - - $result = $Model->afterDataFilter($query, $callbackOptions); - - if (is_array($result)) - { - $query = $result; - } - } - - return $query; - } - - /** - * @param \Model $Model - * @param mixed[] $query - * @param mixed[] $settings - * @param mixed[] $values - * @param string $field - * @param mixed[] $field_options - * @return void - */ - protected function addFieldToFilter($Model, &$query, $settings, $values, $field, $field_options) - { - $configurationModelName = $Model->alias; - $configurationFieldName = $field; - - if (strpos($field, '.') !== false) - { - list($configurationModelName, $configurationFieldName) = explode('.', $field); - } - - if (!isset($values[$configurationModelName][$configurationFieldName]) && isset($field_options['default'])) - { - $values[$configurationModelName][$configurationFieldName] = $field_options['default']; - } - - if ($field_options['required'] && !isset($values[$configurationModelName][$configurationFieldName])) - { - // TODO: implement a bit of a user friendly handling of this scenario.. - trigger_error(__('No value present for required field %s and default value not present', $field)); - return; - } - - if (!isset($values[$configurationModelName][$configurationFieldName]) || (empty($values[$configurationModelName][$configurationFieldName]) && $values[$configurationModelName][$configurationFieldName] != 0)) - { - // no value to filter with, just skip this field - return; - } - - // the value we get as condition and where it comes from is not the same as the - // model and field we're using to filter the data - $filterFieldName = $configurationFieldName; - $filterModelName = $configurationModelName; - $linkModelName = null; - $relationType = null; - - if ($configurationModelName != $Model->alias) - { - $relationTypes = array('hasMany', 'hasOne', 'belongsTo', 'hasAndBelongsToMany'); - - foreach ($relationTypes as $type) - { - if ($type == 'hasAndBelongsToMany') { - if (!empty($Model->{$configurationModelName})) { - $configurationModelAlias = $Model->{$configurationModelName}->alias; - if (!empty($Model->{$type}[$configurationModelAlias])) { - $linkModelName = $Model->{$type}[$configurationModelAlias]['with']; - } - } - } - if (isset($Model->{$type}) && isset($Model->{$type}[$configurationModelName])) - { - $filterModelName = 'Filter'.$configurationModelName; - $relationType = $type; - break; - } - } - } - - if (isset($field_options['filterField'])) - { - if (strpos($field_options['filterField'], '.') !== false) - { - list($filterModelName, $filterFieldName) = explode('.', $field_options['filterField']); - - if ($filterModelName != $Model->alias) - { - $filterModelName = 'Filter'.$filterModelName; - } - } - else - { - $filterModelName = $Model->alias; - $filterFieldName = $field_options['filterField']; - } - } - - $realFilterField = sprintf('%s.%s', $filterModelName, $filterFieldName); - - if (isset($Model->{$relationType}) && isset($Model->{$relationType}[$configurationModelName])) - { - $relatedModel = $Model->{$configurationModelName}; - $relatedModelAlias = 'Filter'.$relatedModel->alias; - - if (!Set::matches(sprintf('/joins[alias=%s]', $relatedModelAlias), $query)) - { - $joinStatements = $this->buildFilterJoin($Model, $relatedModel, $linkModelName); - foreach ($joinStatements as $joinStatement) - { - $query['joins'][] = $joinStatement; - } - } - } - - $this->buildFilterConditions - ( - $query, - $realFilterField, - $field_options, - $values[$configurationModelName][$configurationFieldName] - ); - } - - /** - * Build join conditions from Model to relatedModel. - * - * @param Model $Model - * @param Model $relatedModel - * @param string $linkModelName - * @return mixed[] Cake join array - */ - protected function buildFilterJoin(Model $Model, Model $relatedModel, $linkModelName) - { - $conditions = array(); - $relationTypes = array('hasMany', 'hasOne', 'belongsTo', 'hasAndBelongsToMany'); - - $relatedModelAlias = null; - $relationType = null; - $linkModelAlias = null; - - foreach ($relationTypes as $type) - { - if (isset($Model->{$type}) && isset($Model->{$type}[$relatedModel->alias])) - { - if (!empty($linkModelName)) - { - $linkModelAlias = $Model->{$linkModelName}->alias; - } - $relatedModelAlias = 'Filter'.$relatedModel->alias; - $relationType = $type; - break; - } - } - $linkConditions = array(); - if (isset($Model->{$relationType}[$relatedModel->alias]['foreignKey']) - && $Model->{$relationType}[$relatedModel->alias]['foreignKey']) - { - if ($relationType == 'belongsTo') - { - $conditions[] = sprintf - ( - '%s.%s = %s.%s', - $Model->alias, $Model->{$relationType}[$relatedModel->alias]['foreignKey'], - $relatedModelAlias, $relatedModel->primaryKey - ); - } - else if (in_array($relationType, array('hasMany', 'hasOne'))) - { - $conditions[] = sprintf - ( - '%s.%s = %s.%s', - $Model->alias, $Model->primaryKey, - $relatedModelAlias, $Model->{$relationType}[$relatedModel->alias]['foreignKey'] - ); - } - else if ($relationType == 'hasAndBelongsToMany') - { - $conditions[] = sprintf - ( - '%s.%s = %s.%s', - $Model->{$relationType}[$relatedModel->alias]['with'], $Model->{$relationType}[$relatedModel->alias]['associationForeignKey'], - $relatedModelAlias, $relatedModel->primaryKey - ); - - $linkConditions[] = sprintf - ( - '%s.%s = %s.%s', - $Model->alias, $Model->primaryKey, - $linkModelAlias, $Model->{$relationType}[$relatedModel->alias]['foreignKey'] - ); - } - } - - // merge any custom conditions from the relation, but change - // the alias to our $relatedModelAlias - if (isset($Model->{$relationType}[$relatedModel->alias]['conditions']) && - !empty($Model->{$relationType}[$relatedModel->alias]['conditions'])) - { - $customConditions = $Model->{$relationType}[$relatedModel->alias]['conditions']; - - if (!is_array($Model->{$relationType}[$relatedModel->alias]['conditions'])) - { - $customConditions = array($customConditions); - } - if ($relatedModelAlias !== null) { - $aliasPattern = sprintf('#(?alias); - $filterConditions = preg_replace($aliasPattern, $relatedModelAlias, $customConditions); - $conditions = array_merge($conditions, $filterConditions); - } - } - - $return = array - ( - array - ( - 'table' => $relatedModel->table, - 'alias' => $relatedModelAlias, - 'type' => 'LEFT', - 'conditions' => $conditions, - ) - ); - - if (!empty($linkModelName)) - { - $return = array - ( - array - ( - 'table' => $Model->{$linkModelName}->table, - 'alias' => $linkModelAlias, - 'type' => 'LEFT', - 'conditions' => $linkConditions, - ), - array - ( - 'table' => $relatedModel->table, - 'alias' => $relatedModelAlias, - 'type' => 'LEFT', - 'conditions' => $conditions, - ) - ); - } - return $return; - } - - /** - * Build query conditions and add them to $query. - * - * @param mixed[] $query Cake query array. - * @param string $field Filter field. - * @param mixed[] $options Configuration options for this field. - * @param mixed $value Field value. - * @return void - */ - protected function buildFilterConditions(array &$query, $field, $options, $value) - { - $conditionFieldFormats = array - ( - 'like' => '%s like', - 'ilike' => '%s ilike', - 'contains' => '%s like', - 'startswith' => '%s like', - 'endswith' => '%s like', - 'equal' => '%s', - 'equals' => '%s', - '=' => '%s', - ); - $conditionValueFormats = array - ( - 'like' => '%%%s%%', - 'ilike' => '%%%s%%', - 'contains' => '%%%s%%', - 'startswith' => '%s%%', - 'endswith' => '%%%s', - 'equal' => '%s', - 'equals' => '%s', - '=' => '%s', - ); - - switch ($options['type']) - { - case 'select': - if (is_string($value) && strlen(trim(strval($value))) == 0) - { - break; - } - - $query['conditions'][$field] = $value; - break; - case 'checkbox': - $query['conditions'][$field] = $value; - break; - default: - if (strlen(trim(strval($value))) == 0) - { - break; - } - - $condition = $options['condition']; - - switch ($condition) - { - case 'like': - case 'ilike': - case 'contains': - case 'startswith': - case 'endswith': - case 'equal': - case 'equals': - case '=': - $formattedField = sprintf($conditionFieldFormats[$condition], $field); - $formattedValue = sprintf($conditionValueFormats[$condition], $value); - - $query['conditions'][$formattedField] = $formattedValue; - break; - default: - { - $query['conditions'][$field.' '.$condition] = $value; - } - break; - } - - break; - } - } - - /** - * Makes a string SQL-safe. - * - * @param bool|string|null $string String to sanitize. - * @param string $connection Database connection being used. - * @return bool|string|null SQL safe string. - */ - private function __escape($string, $connection = 'default') - { - if (is_numeric($string) || $string === null || is_bool($string)) { - return $string; - } - /** @var \DboSource $db */ - $db = ConnectionManager::getDataSource($connection); - $string = $db->value($string, 'string'); - $start = 1; - if ($string[0] === 'N') { - $start = 2; - } - return substr(substr($string, $start), 0, -1); - } - - /** - * Makes an array SQL-safe. - * - * @param string|mixed[] $data Data to sanitize. - * @param string $connection DB connection being used. - * @return (bool|string|null)|mixed[] Sanitized data. - */ - private function __clean($data, $connection = 'default') - { - if (empty($data)) { - return $data; - } - if (is_array($data)) { - foreach ($data as $key => $val) { - $data[$key] = $this->__clean($val, $connection); - } - return $data; - } - return $this->__escape($data, $connection); - } - - /** - * Sets filter values. - * - * @param Model $Model Current model. - * @param mixed[] $values Filter values. - * @return void - */ - public function setFilterValues($Model, $values = array()) - { - $values = $this->__clean($values, $Model->useDbConfig); - $this->_filterValues[$Model->alias] = array_merge($this->_filterValues[$Model->alias], (array)$values); - } - - /** - * Gets filter values. - * - * @param Model $Model Current model. - * @return mixed[] - */ - public function getFilterValues($Model) - { - return $this->_filterValues; - } - -} diff --git a/Model/FilterAppModel.php b/Model/FilterAppModel.php deleted file mode 100644 index 0c66581..0000000 --- a/Model/FilterAppModel.php +++ /dev/null @@ -1,17 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -class FilterAppModel extends AppModel -{ - -} diff --git a/README.markdown b/README.markdown index ea82238..b5aefe6 100644 --- a/README.markdown +++ b/README.markdown @@ -9,7 +9,9 @@ session, but this can be turned off if undesirable. It also features callback methods for further search refinement where necessary. -**IMPORTANT**: These instructions are for CakePHP 2.0. If you're using CakePHP 1.3.x +**IMPORTANT**: These instructions are for CakePHP 3.0. +If you are using CakePHP 2.x then go to https://github.com/lecterror/cakephp-filter-plugin/tree/2.x. +If you're using CakePHP 1.3.x the correct path to unload the plugin is `app/plugins/filter/`. More importantly, **if you're using CakePHP 1.3.x you should use the 1.3.x version of this plugin**, not the latest version from GitHub. @@ -23,7 +25,7 @@ First, obtain the plugin. If you're using Git, run this while in your app folder git submodule update Or visit and download the -plugin manually to your `app/Plugin/Filter/` folder. +plugin manually to your `plugins/Filter/` folder. To use the plugin, you need to tell it which model to filter and which fields to use. For a quick tutorial, visit diff --git a/Test/Case/AllTest.php b/Test/Case/AllTest.php deleted file mode 100644 index fd2c232..0000000 --- a/Test/Case/AllTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ -App::uses('CakePlugin', 'Core'); - -class AllFilterTests extends CakeTestSuite -{ - /** - * @return \CakeTestSuite - */ - public static function suite() - { - $suite = new CakeTestSuite('All FilterPlugin tests'); - - $suite->addTestDirectoryRecursive(CakePlugin::path('Filter').'Test'.DS.'Case'); - - return $suite; - } -} diff --git a/Test/Case/Controller/Component/FilterComponentTest.php b/Test/Case/Controller/Component/FilterComponentTest.php deleted file mode 100644 index e4e9901..0000000 --- a/Test/Case/Controller/Component/FilterComponentTest.php +++ /dev/null @@ -1,722 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -App::uses('Router', 'Routing'); -App::uses('Component', 'Filter.Filter'); -App::uses('Document', 'Filter.Test/Case/MockObjects'); -App::uses('Document2', 'Filter.Test/Case/MockObjects'); -App::uses('Document3', 'Filter.Test/Case/MockObjects'); -App::uses('DocumentCategory', 'Filter.Test/Case/MockObjects'); -App::uses('DocumentTestsController', 'Filter.Test/Case/MockObjects'); -App::uses('Item', 'Filter.Test/Case/MockObjects'); -App::uses('Metadata', 'Filter.Test/Case/MockObjects'); - -class FilterComponentTest extends CakeTestCase -{ - /** - * @var string[] - */ - public $fixtures = array - ( - 'plugin.filter.document_category', - 'plugin.filter.document', - 'plugin.filter.item', - 'plugin.filter.metadata', - ); - - /** - * @var \DocumentTestsController - */ - public $Controller = null; - - public function startTest($method) - { - Router::connect('/', array('controller' => 'document_tests', 'action' => 'index')); - $request = new CakeRequest('/'); - $request->addParams(Router::parse('/')); - $this->Controller = new DocumentTestsController($request); - $this->Controller->uses = array('Document'); - - if (array_search($method, array('testPersistence')) !== false) - { - $this->Controller->components = array - ( - 'Session', - 'Filter.Filter' => array('nopersist' => true) - ); - } - else - { - $this->Controller->components = array - ( - 'Session', - 'Filter.Filter' - ); - } - - $this->Controller->constructClasses(); - $this->Controller->Session->destroy(); - $this->Controller->Components->trigger('initialize', array($this->Controller)); - } - - public function endTest($method) - { - $this->Controller->Session->destroy(); - unset($this->Controller); - } - - /** - * Test bailing out when no filters are present. - * - * @return void - */ - public function testNoFilters() - { - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->assertEmpty($this->Controller->Filter->settings); - $this->assertFalse($this->Controller->Document->Behaviors->enabled('Filtered')); - - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->assertFalse(in_array('Filter.Filter', $this->Controller->helpers)); - } - - /** - * Test bailing out when a filter model can't be found - * or when the current action has no filters. - * - * @return void - */ - public function testNoModelPresentOrNoActionFilters() - { - $testSettings = array - ( - 'index' => array - ( - 'DocumentArse' => array - ( - 'DocumentFeck.drink' => array('type' => 'irrelevant') - ) - ) - ); - - $this->expectException('PHPUnit_Framework_Error_Notice'); - $this->Controller->filters = $testSettings; - $this->Controller->Components->trigger('initialize', array($this->Controller)); - - $testSettings = array - ( - 'someotheraction' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ) - ) - ); - - - $this->Controller->filters = $testSettings; - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->assertFalse($this->Controller->Document->Behaviors->enabled('Filtered')); - - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ) - ), - ); - - $this->Controller->filters = $testSettings; - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->assertTrue($this->Controller->Document->Behaviors->enabled('Filtered')); - } - - /** - * Test basic filter settings. - * - * @return void - */ - public function testBasicFilters() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ) - ) - ); - $this->Controller->filters = $testSettings; - - $expected = array - ( - $this->Controller->name => $testSettings - ); - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->assertEquals($expected, $this->Controller->Filter->settings); - } - - /** - * Test running a component with no filter data. - * - * @return void - */ - public function testEmptyStartup() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->assertTrue(in_array('Filter.Filter', $this->Controller->helpers)); - } - - /** - * Test loading filter data from session (both full and empty). - * - * @return void - */ - public function testSessionStartupData() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ), - 'FakeNonexistant' => array - ( - 'drink' => array('type' => 'select') - ) - ) - ); - $this->Controller->filters = $testSettings; - - $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', $this->Controller->name, $this->Controller->action); - - $filterValues = array(); - $this->Controller->Session->write($sessionKey, $filterValues); - $this->expectException('PHPUnit_Framework_Error_Notice'); - $this->Controller->Components->trigger('initialize', array($this->Controller)); - - $this->expectException('PHPUnit_Framework_Error_Notice'); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $actualFilterValues = $this->Controller->Document->getFilterValues(); - $this->assertEquals - ( - $filterValues, - $actualFilterValues[$this->Controller->Document->alias] - ); - - $filterValues = array('Document' => array('title' => 'in')); - $this->Controller->Session->write($sessionKey, $filterValues); - - $this->Controller->Components->trigger('startup', array($this->Controller)); - $actualFilterValues = $this->Controller->Document->getFilterValues(); - $this->assertEquals - ( - $filterValues, - $actualFilterValues[$this->Controller->Document->alias] - ); - - $this->Controller->Session->delete($sessionKey); - } - - /** - * Test loading filter data from a post request. - * - * @return void - */ - public function testPostStartupData() - { - $_SERVER['REQUEST_METHOD'] = 'POST'; - - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ), - ) - ); - - $this->Controller->filters = $testSettings; - - $filterValues = array('Document' => array('title' => 'in'), 'Filter' => array('filterFormId' => 'Document')); - $this->Controller->data = $filterValues; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - - $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', $this->Controller->name, $this->Controller->action); - $sessionData = $this->Controller->Session->read($sessionKey); - $this->assertEquals($filterValues, $sessionData); - - $actualFilterValues = $this->Controller->Document->getFilterValues(); - $this->assertEquals - ( - $filterValues, - $actualFilterValues[$this->Controller->Document->alias] - ); - } - - /** - * Test exiting beforeRender when in an action with no settings. - * - * @return void - */ - public function testBeforeRenderAbort() - { - $testSettings = array - ( - 'veryMuchNotIndex' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $this->assertFalse(isset($this->Controller->viewVars['viewFilterParams'])); - } - - /** - * Test triggering an error when the plugin runs into a setting - * for filtering a model which cannot be found. - * - * @return void - */ - public function testNoModelFound() - { - $testSettings = array - ( - 'index' => array - ( - 'ThisModelDoesNotExist' => array - ( - 'ThisModelDoesNotExist.title' => array('type' => 'text') - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->expectException('PHPUnit_Framework_Error_Notice'); - $this->Controller->Components->trigger('initialize', array($this->Controller)); - - //$this->expectError(); - $this->Controller->Components->trigger('startup', array($this->Controller)); - - $this->expectException('PHPUnit_Framework_Error_Notice'); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - } - - /** - * Test the view variable generation for very basic filtering. - * Also tests model name detection and custom label. - * - * @return void - */ - public function testBasicViewInfo() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'title', - 'DocumentCategory.id' => array('type' => 'select', 'label' => 'Category'), - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $expected = array - ( - array('name' => 'Document.title', 'options' => array('type' => 'text')), - array - ( - 'name' => 'DocumentCategory.id', - 'options' => array - ( - 'type' => 'select', - 'options' => array - ( - 1 => 'Testing Doc', - 2 => 'Imaginary Spec', - 3 => 'Nonexistant data', - 4 => 'Illegal explosives DIY', - 5 => 'Father Ted', - ), - 'empty' => false, - 'label' => 'Category', - ) - ), - ); - - $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); - } - - /** - * Test passing additional inputOptions to the form - * helper, used to customize search form. - * - * @return void - */ - public function testAdditionalInputOptions() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'title' => array('inputOptions' => 'disabled'), - 'DocumentCategory.id' => array - ( - 'type' => 'select', - 'label' => 'Category', - 'inputOptions' => array('class' => 'important') - ), - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $expected = array - ( - array - ( - 'name' => 'Document.title', - 'options' => array - ( - 'type' => 'text', - 'disabled' - ) - ), - array - ( - 'name' => 'DocumentCategory.id', - 'options' => array - ( - 'type' => 'select', - 'options' => array - ( - 1 => 'Testing Doc', - 2 => 'Imaginary Spec', - 3 => 'Nonexistant data', - 4 => 'Illegal explosives DIY', - 5 => 'Father Ted', - ), - 'empty' => false, - 'label' => 'Category', - 'class' => 'important', - ) - ), - ); - - $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); - } - - /** - * Test data fetching for select input when custom selector - * and custom options are provided. - * - * @return void - */ - public function testCustomSelector() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'DocumentCategory.id' => array - ( - 'type' => 'select', - 'label' => 'Category', - 'selector' => 'customSelector', - 'selectOptions' => array('conditions' => array('DocumentCategory.description LIKE' => '%!%')), - ), - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $expected = array - ( - array - ( - 'name' => 'DocumentCategory.id', - 'options' => array - ( - 'type' => 'select', - 'options' => array - ( - 1 => 'Testing Doc', - 5 => 'Father Ted', - ), - 'empty' => false, - 'label' => 'Category', - ) - ), - ); - - $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); - } - - /** - * Test checkbox input filtering. - * - * @return void - */ - public function testCheckboxOptions() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.is_private' => array - ( - 'type' => 'checkbox', - 'label' => 'Private?', - 'default' => true, - ), - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $expected = array - ( - array - ( - 'name' => 'Document.is_private', - 'options' => array - ( - 'type' => 'checkbox', - 'checked' => true, - 'label' => 'Private?', - ) - ), - ); - - $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); - } - - /** - * Test basic filter settings. - * - * @return void - */ - public function testSelectMultiple() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'DocumentCategory.id' => array - ( - 'type' => 'select', - 'multiple' => true, - ) - ) - ) - ); - $this->Controller->filters = $testSettings; - - $expected = array - ( - $this->Controller->name => $testSettings - ); - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->assertEquals($expected, $this->Controller->Filter->settings); - } - - /** - * Test select input for the model filtered. - * - * @return void - */ - public function testSelectInputFromSameModel() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array - ( - 'type' => 'select', - ), - ) - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $expected = array - ( - array - ( - 'name' => 'Document.title', - 'options' => array - ( - 'type' => 'select', - 'options' => array - ( - 'Testing Doc' => 'Testing Doc', - 'Imaginary Spec' => 'Imaginary Spec', - 'Nonexistant data' => 'Nonexistant data', - 'Illegal explosives DIY' => 'Illegal explosives DIY', - 'Father Ted' => 'Father Ted', - 'Duplicate title' => 'Duplicate title', - ), - 'empty' => '', - ) - ), - ); - - $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); - } - - /** - * Test disabling persistence for single action - * and for the entire controller. - * - * @return void - */ - public function testPersistence() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'Document.title' => array('type' => 'text') - ), - ) - ); - $this->Controller->filters = $testSettings; - - $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', 'SomeOtherController', $this->Controller->action); - $filterValues = array('Document' => array('title' => 'in'), 'Filter' => array('filterFormId' => 'Document')); - $this->Controller->Session->write($sessionKey, $filterValues); - - $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', $this->Controller->name, $this->Controller->action); - $filterValues = array('Document' => array('title' => 'in'), 'Filter' => array('filterFormId' => 'Document')); - $this->Controller->Session->write($sessionKey, $filterValues); - - $this->Controller->Filter->nopersist = array(); - $this->Controller->Filter->nopersist[$this->Controller->name] = true; - $this->Controller->Filter->nopersist['SomeOtherController'] = true; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - - $expected = array($this->Controller->name => array($this->Controller->action => $filterValues)); - $this->assertEquals($expected, $this->Controller->Session->read('FilterPlugin.Filters')); - } - - /** - * Test whether filtering by belongsTo model text field - * works correctly. - * - * @return void - */ - public function testBelongsToFilteringByText() - { - $testSettings = array - ( - 'index' => array - ( - 'Document' => array - ( - 'DocumentCategory.title' => array('type' => 'text') - ), - ) - ); - $this->Controller->filters = $testSettings; - - $this->Controller->Components->trigger('initialize', array($this->Controller)); - $this->Controller->Components->trigger('startup', array($this->Controller)); - $this->Controller->Components->trigger('beforeRender', array($this->Controller)); - - $expected = array - ( - array - ( - 'name' => 'DocumentCategory.title', - 'options' => array - ( - 'type' => 'text', - ) - ), - ); - - $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); - } -} diff --git a/Test/Case/MockObjects/Document.php b/Test/Case/MockObjects/Document.php deleted file mode 100644 index f51ecfc..0000000 --- a/Test/Case/MockObjects/Document.php +++ /dev/null @@ -1,28 +0,0 @@ -returnValue; - } -} diff --git a/Test/Case/MockObjects/Document3.php b/Test/Case/MockObjects/Document3.php deleted file mode 100644 index d0d7eda..0000000 --- a/Test/Case/MockObjects/Document3.php +++ /dev/null @@ -1,48 +0,0 @@ -itemToUnset)) - { - return $query; - } - - if (isset($query['conditions'][$this->itemToUnset])) - { - unset($query['conditions'][$this->itemToUnset]); - } - - return $query; - } -} diff --git a/Test/Case/MockObjects/DocumentCategory.php b/Test/Case/MockObjects/DocumentCategory.php deleted file mode 100644 index c3b1033..0000000 --- a/Test/Case/MockObjects/DocumentCategory.php +++ /dev/null @@ -1,28 +0,0 @@ -find('list', $options); - } -} diff --git a/Test/Case/MockObjects/DocumentTestsController.php b/Test/Case/MockObjects/DocumentTestsController.php deleted file mode 100644 index ab122c0..0000000 --- a/Test/Case/MockObjects/DocumentTestsController.php +++ /dev/null @@ -1,39 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -App::import('Core', array('AppModel', 'Model')); -App::uses('Document', 'Filter.Test/Case/MockObjects'); -App::uses('Document2', 'Filter.Test/Case/MockObjects'); -App::uses('Document3', 'Filter.Test/Case/MockObjects'); -App::uses('DocumentCategory', 'Filter.Test/Case/MockObjects'); -App::uses('DocumentTestsController', 'Filter.Test/Case/MockObjects'); -App::uses('Item', 'Filter.Test/Case/MockObjects'); -App::uses('Metadata', 'Filter.Test/Case/MockObjects'); - -class FilteredBehaviorTest extends CakeTestCase -{ - /** - * @var string[] - */ - public $fixtures = array - ( - 'plugin.filter.document_category', - 'plugin.filter.document', - 'plugin.filter.item', - 'plugin.filter.metadata', - ); - - /** - * @var \Document|\Document2|\Document3 - */ - public $Document = null; - - public function startTest($model) - { - $Document = ClassRegistry::init('Document'); - $this->assertInstanceOf('Document', $Document); - $this->Document = $Document; - } - - public function endTest($model) - { - unset($this->Document); - } - - /** - * Detach and re-attach the behavior to reset the options. - * - * @param mixed[] $options Behavior options. - * @return void - */ - protected function _reattachBehavior($options = array()) - { - $this->Document->Behaviors->detach('Filtered'); - $this->Document->Behaviors->attach('Filter.Filtered', $options); - } - - /** - * Test attaching without options. - * - * @return void - */ - public function testBlankAttaching() - { - $this->Document->Behaviors->attach('Filter.Filtered'); - $this->assertTrue($this->Document->Behaviors->enabled('Filtered')); - } - - /** - * Test attaching with options. - * - * @return void - */ - public function testInitSettings() - { - $testOptions = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like'), - 'DocumentCategory.id' => array('type' => 'select', 'filterField' => 'document_category_id'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?') - ); - $this->_reattachBehavior($testOptions); - - $expected = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like', 'required' => false, 'selectOptions' => array()), - 'DocumentCategory.id' => array('type' => 'select', 'filterField' => 'document_category_id', 'condition' => 'like', 'required' => false, 'selectOptions' => array()), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?', 'condition' => 'like', 'required' => false, 'selectOptions' => array()) - ); - $Filtered = $this->Document->Behaviors->__get('Filtered'); - $this->assertInstanceOf('FilteredBehavior', $Filtered); - $this->assertEquals($expected, $Filtered->settings[$this->Document->alias]); - } - - /** - * Test init settings when only a single field is given, with no extra options. - * - * @return void - */ - public function testInitSettingsSingle() - { - $testOptions = array('Document.title'); - $this->_reattachBehavior($testOptions); - - $expected = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like', 'required' => false, 'selectOptions' => array()), - ); - $Filtered = $this->Document->Behaviors->__get('Filtered'); - $this->assertInstanceOf('FilteredBehavior', $Filtered); - $this->assertEquals($expected, $Filtered->settings[$this->Document->alias]); - } - - /** - * Test setting the filter values for future queries. - * - * @return void - */ - public function testSetFilterValues() - { - $testOptions = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'DocumentCategory.id' => array('type' => 'select', 'filterField' => 'document_category_id'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?') - ); - - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('title' => 'in', 'is_private' => 0), - 'DocumentCategory' => array('id' => 1) - ); - - $this->Document->setFilterValues($filterValues); - $actualFilterValues = $this->Document->getFilterValues(); - $this->assertEquals($filterValues, $actualFilterValues[$this->Document->alias]); - } - - /** - * Test detecting an error in options - when a field is 'required' but no value is given for it. - * - * @return void - */ - public function testLoadingRequiredFieldValueMissing() - { - $testOptions = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'DocumentCategory.id' => array('type' => 'select', 'filterField' => 'document_category_id'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?') - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('is_private' => 0), - 'DocumentCategory' => array('id' => 1) - ); - $this->Document->setFilterValues($filterValues); - - $this->expectException('PHPUnit_Framework_Error_Notice'); - $this->Document->find('first'); - } - - /** - * Test filtering with conditions from current model and belongsTo model. - * - * @return void - */ - public function testFilteringBelongsTo() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'DocumentCategory.id' => array('type' => 'select') - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('title' => 'in'), - 'DocumentCategory' => array('id' => 1) - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-06-28 10:39:23', 'updated' => '2010-06-29 11:22:48')), - array('Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44')) - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } - - /** - * @return void - */ - public function testFilteringBelongsToTextField() - { - $testOptions = array - ( - 'DocumentCategory.title' => array('type' => 'text') - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'DocumentCategory' => array('title' => 'spec') - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')) - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } - - /** - * Test filtering with conditions from current model and belongsTo model, - * same as testFilteringBelongsTo() except for a change in filterField format. - * - * @return void - */ - public function testFilteringBelongsToFilterFieldTest() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'DocumentCategory.id' => array('type' => 'select', 'filterField' => 'Document.document_category_id') - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('title' => 'in'), - 'DocumentCategory' => array('id' => 1) - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-06-28 10:39:23', 'updated' => '2010-06-29 11:22:48')), - array('Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44')) - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } - - /** - * Test various conditions for the type 'text' in filtering (less than, equal, like, etc..) - * - * @return void - */ - public function testFilteringBelongsToDifferentConditions() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => '='), - 'DocumentCategory.id' => array('type' => 'select') - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('title' => 'Illegal explosives DIY'), - 'DocumentCategory' => array('id' => '') - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1, 'created' => '2010-01-08 05:15:03', 'updated' => '2010-05-22 03:15:24')), - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - - $testOptions = array - ( - 'id' => array('type' => 'text', 'condition' => '>='), - 'created' => array('type' => 'text', 'condition' => '<=') - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('id' => 3, 'created' => '2010-03-01') - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1, 'created' => '2010-01-08 05:15:03', 'updated' => '2010-05-22 03:15:24')), - array('Document' => array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - array('Document' => array('id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - array('Document' => array('id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } - - /** - * Test filtering with conditions on current model, the belongsTo model - * and hasMany model (behavior adds an INNER JOIN in query). - * - * @return void - */ - public function testFilteringBelongsToAndHasMany() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'DocumentCategory.id' => array('type' => 'select'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?'), - 'Item.code' => array('type' => 'text'), - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Document' => array('title' => 'in', 'is_private' => 0), - 'DocumentCategory' => array('id' => 1), - 'Item' => array('code' => '04') - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - 'DocumentCategory' => array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'), - 'Metadata' => array('id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'), - 'Item' => array - ( - array('id' => 4, 'document_id' => 2, 'code' => 'The item #01'), - array('id' => 5, 'document_id' => 2, 'code' => 'The item #02'), - array('id' => 6, 'document_id' => 2, 'code' => 'The item #03'), - array('id' => 7, 'document_id' => 2, 'code' => 'The item #04') - ) - ) - ); - - $result = $this->Document->find('all'); - $this->assertEquals($expected, $result); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - 'DocumentCategory' => array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'), - 'Metadata' => array('id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'), - ) - ); - - $result = $this->Document->find('all', array('recursive' => 0)); - $this->assertEquals($expected, $result); - - $this->Document->unbindModel(array('hasMany' => array('Item')), false); - $this->Document->bindModel(array('hasMany' => array('Item')), false); - - $result = $this->Document->find('all', array('recursive' => 0)); - $this->assertEquals($expected, $result); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44') - ) - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } - - /** - * Test filtering with join which has some custom - * condition in the relation (both string and array). - * - * @return void - */ - public function testCustomJoinConditions() - { - $testOptions = array - ( - 'Metadata.weight' => array('type' => 'text', 'condition' => '>'), - ); - $this->_reattachBehavior($testOptions); - - $filterValues = array - ( - 'Metadata' => array('weight' => 3), - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array - ( - 'Document' => array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'), - 'Metadata' => array('id' => 5, 'document_id' => 5, 'weight' => 4, 'size' => 790, 'permissions' => 'rw-rw-r--'), - ) - ); - - $this->Document->recursive = -1; - $oldConditions = $this->Document->hasOne['Metadata']['conditions']; - $this->Document->hasOne['Metadata']['conditions'] = array('Metadata.size > 500'); - $this->Document->Behaviors->attach('Containable'); - - $result = $this->Document->find('all', array('contain' => array('Metadata'))); - $this->assertEquals($expected, $result); - - $this->Document->hasOne['Metadata']['conditions'] = 'Metadata.size > 500'; - $result = $this->Document->find('all', array('contain' => array('Metadata'))); - $this->assertEquals($expected, $result); - - $this->Document->hasOne['Metadata']['conditions'] = $oldConditions; - $this->Document->Behaviors->detach('Containable'); - } - - /** - * Test for any possible conflicts with Containable behavior. - * - * @return void - */ - public function testFilteringBelongsToAndHasManyWithContainable() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'DocumentCategory.id' => array('type' => 'select'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?'), - 'Item.code' => array('type' => 'text'), - ); - - $this->_reattachBehavior($testOptions); - $this->Document->Behaviors->attach('Containable'); - - $filterValues = array - ( - 'Document' => array('title' => 'in', 'is_private' => 0), - 'DocumentCategory' => array('id' => 1), - 'Item' => array('code' => '04') - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - 'DocumentCategory' => array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'), - 'Item' => array - ( - array('id' => 4, 'document_id' => 2, 'code' => 'The item #01'), - array('id' => 5, 'document_id' => 2, 'code' => 'The item #02'), - array('id' => 6, 'document_id' => 2, 'code' => 'The item #03'), - array('id' => 7, 'document_id' => 2, 'code' => 'The item #04') - ) - ) - ); - - $result = $this->Document->find('all', array('contain' => array('DocumentCategory', 'Item'))); - $this->assertEquals($expected, $result); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - 'DocumentCategory' => array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'), - ) - ); - - $result = $this->Document->find('all', array('contain' => array('DocumentCategory'))); - $this->assertEquals($expected, $result); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - ) - ); - - $result = $this->Document->find('all', array('contain' => array())); - $this->assertEquals($expected, $result); - - $this->Document->Behaviors->detach('Containable'); - } - - /** - * Test filtering by text input with hasOne relation. - * - * @return void - */ - public function testHasOneAndHasManyWithTextSearch() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'Item.code' => array('type' => 'text'), - 'Metadata.size' => array('type' => 'text', 'condition' => '='), - ); - - $filterValues = array - ( - 'Document' => array('title' => 'in'), - 'Item' => array('code' => '04'), - 'Metadata' => array('size' => 45), - ); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec'), - ) - ); - - $this->_reattachBehavior($testOptions); - $this->Document->setFilterValues($filterValues); - - $this->Document->recursive = -1; - $result = $this->Document->find('all', array('fields' => array('Document.id', 'Document.title'))); - $this->assertEquals($expected, $result); - } - - /** - * Test filtering with Containable and hasOne Model.field. - * - * @return void - */ - public function testHasOneWithContainable() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'Item.code' => array('type' => 'text'), - 'Metadata.size' => array('type' => 'text', 'condition' => '='), - ); - - $filterValues = array - ( - 'Document' => array('title' => 'in'), - 'Item' => array('code' => '04'), - 'Metadata' => array('size' => 45), - ); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - 'Metadata' => array('id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'), - 'Item' => array - ( - array('id' => 4, 'document_id' => 2, 'code' => 'The item #01'), - array('id' => 5, 'document_id' => 2, 'code' => 'The item #02'), - array('id' => 6, 'document_id' => 2, 'code' => 'The item #03'), - array('id' => 7, 'document_id' => 2, 'code' => 'The item #04') - ) - ) - ); - - // containable first, filtered second - $this->Document->Behaviors->attach('Containable'); - $this->_reattachBehavior($testOptions); - $this->Document->setFilterValues($filterValues); - $result = $this->Document->find('all', array('contain' => array('Metadata', 'Item'))); - $this->assertEquals($expected, $result); - $this->Document->Behaviors->detach('Containable'); - - // filtered first, containable second - $this->_reattachBehavior($testOptions); - $this->Document->setFilterValues($filterValues); - $this->Document->Behaviors->attach('Containable'); - $result = $this->Document->find('all', array('contain' => array('Metadata', 'Item'))); - $this->assertEquals($expected, $result); - $this->Document->Behaviors->detach('Containable'); - } - - /** - * Test filtering when a join is already present in the query, - * this should prevent duplicate joins and query errors. - * - * @return void - */ - public function testJoinAlreadyPresent() - { - $testOptions = array - ( - 'title' => array('type' => 'text', 'condition' => 'like', 'required' => true), - 'Item.code' => array('type' => 'text'), - 'Metadata.size' => array('type' => 'text', 'condition' => '='), - ); - - $filterValues = array - ( - 'Document' => array('title' => 'in'), - 'Item' => array('code' => '04'), - 'Metadata' => array('size' => 45), - ); - - $expected = array - ( - array - ( - 'Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - 'DocumentCategory' => array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'), - 'Metadata' => array('id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'), - 'Item' => array - ( - array('id' => 4, 'document_id' => 2, 'code' => 'The item #01'), - array('id' => 5, 'document_id' => 2, 'code' => 'The item #02'), - array('id' => 6, 'document_id' => 2, 'code' => 'The item #03'), - array('id' => 7, 'document_id' => 2, 'code' => 'The item #04') - ) - ) - ); - - $customJoin = array(); - $customJoin[] = array - ( - 'table' => 'items', - 'alias' => 'FilterItem', - 'type' => 'INNER', - 'conditions' => 'Document.id = FilterItem.document_id', - ); - - $this->_reattachBehavior($testOptions); - $this->Document->setFilterValues($filterValues); - $result = $this->Document->find('all', array('joins' => $customJoin, 'recursive' => 1)); - $this->assertEquals($expected, $result); - } - - /** - * Test the 'nofilter' query param. - * - * @return void - */ - public function testNofilterFindParam() - { - $testOptions = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like'), - 'DocumentCategory.id' => array('type' => 'select'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?', 'default' => 0) - ); - $this->_reattachBehavior($testOptions); - - - $filterValues = array - ( - 'DocumentCategory' => array('id' => 2), - 'Document' => array('title' => '') - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')) - ); - - $result = $this->Document->find('all', array('recursive' => -1, 'nofilter' => true)); - $this->assertNotEquals($expected, $result); - - $result = $this->Document->find('all', array('recursive' => -1, 'nofilter' => 'true')); - $this->assertEquals($expected, $result); - } - - /** - * Test bailing out if no settings exist for the current model. - * - * @return void - */ - public function testExitWhenNoSettings() - { - $this->Document->DocumentCategory->Behaviors->attach('Filter.Filtered'); - - $this->assertFalse(isset($this->Document->DocumentCategory->Behaviors->Filtered->settings[$this->Document->DocumentCategory->alias])); - - $filterValues = array - ( - 'DocumentCategory' => array('id' => 2) - ); - $this->Document->DocumentCategory->setFilterValues($filterValues); - - $expected = array - ( - array('DocumentCategory' => array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!')), - array('DocumentCategory' => array('id' => 2, 'title' => 'Imaginary Spec', 'description' => 'This doc does not exist')), - array('DocumentCategory' => array('id' => 3, 'title' => 'Nonexistant data', 'description' => 'This doc is probably empty')), - array('DocumentCategory' => array('id' => 4, 'title' => 'Illegal explosives DIY', 'description' => 'Viva la revolucion!')), - array('DocumentCategory' => array('id' => 5, 'title' => 'Father Ted', 'description' => 'Feck! Drink! Arse! Girls!')) - ); - - $result = $this->Document->DocumentCategory->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - - $this->Document->DocumentCategory->Behaviors->detach('Filtered'); - } - - /** - * Test beforeDataFilter() callback, used to cancel filtering if necessary. - * - * @return void - */ - public function testBeforeDataFilterCallbackCancel() - { - $Document = ClassRegistry::init('Document2'); - $this->assertInstanceOf('Document2', $Document); - $this->Document = $Document; - $testOptions = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like'), - 'DocumentCategory.id' => array('type' => 'select'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?') - ); - $this->_reattachBehavior($testOptions); - - - $filterValues = array - ( - 'DocumentCategory' => array('id' => 2) - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-06-28 10:39:23', 'updated' => '2010-06-29 11:22:48')), - array('Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44')), - array('Document' => array('id' => 3, 'title' => 'Nonexistant data', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-04-28 11:12:33', 'updated' => '2010-05-05 15:03:24')), - array('Document' => array('id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1, 'created' => '2010-01-08 05:15:03', 'updated' => '2010-05-22 03:15:24')), - array('Document' => array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - array('Document' => array('id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - array('Document' => array('id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } - - /** - * Test afterDataFilter() callback, used to modify the conditions after - * filter conditions have been applied. - * - * @return void - */ - public function testAfterDataFilterCallbackQueryChange() - { - $Document = ClassRegistry::init('Document3'); - $this->assertInstanceOf('Document3', $Document); - $this->Document = $Document; - $this->Document->itemToUnset = 'FilterDocumentCategory.id'; - - $testOptions = array - ( - 'Document.title' => array('type' => 'text', 'condition' => 'like'), - 'DocumentCategory.id' => array('type' => 'select'), - 'Document.is_private' => array('type' => 'checkbox', 'label' => 'Private?') - ); - $this->_reattachBehavior($testOptions); - - - $filterValues = array - ( - 'DocumentCategory' => array('id' => 2) - ); - $this->Document->setFilterValues($filterValues); - - $expected = array - ( - array('Document' => array('id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-06-28 10:39:23', 'updated' => '2010-06-29 11:22:48')), - array('Document' => array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44')), - array('Document' => array('id' => 3, 'title' => 'Nonexistant data', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-04-28 11:12:33', 'updated' => '2010-05-05 15:03:24')), - array('Document' => array('id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1, 'created' => '2010-01-08 05:15:03', 'updated' => '2010-05-22 03:15:24')), - array('Document' => array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - array('Document' => array('id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - array('Document' => array('id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15')), - ); - - $result = $this->Document->find('all', array('recursive' => -1)); - $this->assertEquals($expected, $result); - } -} diff --git a/Test/Fixture/DocumentCategoryFixture.php b/Test/Fixture/DocumentCategoryFixture.php deleted file mode 100644 index b667357..0000000 --- a/Test/Fixture/DocumentCategoryFixture.php +++ /dev/null @@ -1,39 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -class DocumentCategoryFixture extends CakeTestFixture -{ - public $name = 'DocumentCategory'; - - /** - * @var (bool|int|string)[][] - */ - public $fields = array - ( - 'id' => array('type' => 'integer', 'key' => 'primary'), - 'title' => array('type' => 'string', 'length' => 100, 'null' => false), - 'description' => array('type' => 'string', 'length' => 255) - ); - - /** - * @var (int|string)[][] - */ - public $records = array - ( - array('id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'), - array('id' => 2, 'title' => 'Imaginary Spec', 'description' => 'This doc does not exist'), - array('id' => 3, 'title' => 'Nonexistant data', 'description' => 'This doc is probably empty'), - array('id' => 4, 'title' => 'Illegal explosives DIY', 'description' => 'Viva la revolucion!'), - array('id' => 5, 'title' => 'Father Ted', 'description' => 'Feck! Drink! Arse! Girls!') - ); -} diff --git a/Test/Fixture/DocumentFixture.php b/Test/Fixture/DocumentFixture.php deleted file mode 100644 index 0332e8c..0000000 --- a/Test/Fixture/DocumentFixture.php +++ /dev/null @@ -1,45 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -class DocumentFixture extends CakeTestFixture -{ - public $name = 'Document'; - - /** - * @var (bool|int|string)[][] - */ - public $fields = array - ( - 'id' => array('type' => 'integer', 'key' => 'primary'), - 'title' => array('type' => 'string', 'length' => '255', 'null' => false), - 'document_category_id' => array('type' => 'integer', 'null' => false), - 'owner_id' => array('type' => 'integer', 'null' => false), - 'is_private' => array('type' => 'integer', 'length' => 1, 'null' => false), - 'created' => array('type' => 'datetime', 'null' => false), - 'updated' => array('type' => 'datetime', 'null' => true) - ); - - /** - * @var (int|string)[][] - */ - public $records = array - ( - array('id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-06-28 10:39:23', 'updated' => '2010-06-29 11:22:48'), - array('id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'), - array('id' => 3, 'title' => 'Nonexistant data', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-04-28 11:12:33', 'updated' => '2010-05-05 15:03:24'), - array('id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1, 'created' => '2010-01-08 05:15:03', 'updated' => '2010-05-22 03:15:24'), - array('id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'), - array('id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'), - array('id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'), - ); -} diff --git a/Test/Fixture/ItemFixture.php b/Test/Fixture/ItemFixture.php deleted file mode 100644 index c7f3424..0000000 --- a/Test/Fixture/ItemFixture.php +++ /dev/null @@ -1,44 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -class ItemFixture extends CakeTestFixture -{ - public $name = 'Item'; - - /** - * @var (bool|int|string)[][] - */ - public $fields = array - ( - 'id' => array('type' => 'integer', 'key' => 'primary'), - 'document_id' => array('type' => 'integer', 'null' => false), - 'code' => array('type' => 'string', 'length' => '20', 'null' => false) - ); - - /** - * @var (int|string)[][] - */ - public $records = array - ( - array('id' => 1, 'document_id' => 1, 'code' => 'The item #01'), - array('id' => 2, 'document_id' => 1, 'code' => 'The item #02'), - array('id' => 3, 'document_id' => 1, 'code' => 'The item #03'), - array('id' => 4, 'document_id' => 2, 'code' => 'The item #01'), - array('id' => 5, 'document_id' => 2, 'code' => 'The item #02'), - array('id' => 6, 'document_id' => 2, 'code' => 'The item #03'), - array('id' => 7, 'document_id' => 2, 'code' => 'The item #04'), - array('id' => 8, 'document_id' => 3, 'code' => 'The item #01'), - array('id' => 9, 'document_id' => 4, 'code' => 'The item #01'), - array('id' => 10, 'document_id' => 5, 'code' => 'The item #01') - ); -} diff --git a/Test/Fixture/MetadataFixture.php b/Test/Fixture/MetadataFixture.php deleted file mode 100644 index 4178a22..0000000 --- a/Test/Fixture/MetadataFixture.php +++ /dev/null @@ -1,41 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -class MetadataFixture extends CakeTestFixture -{ - public $name = 'Metadata'; - - /** - * @var (bool|int|string)[][] - */ - public $fields = array - ( - 'id' => array('type' => 'integer', 'key' => 'primary'), - 'document_id' => array('type' => 'integer', 'null' => false), - 'weight' => array('type' => 'integer', 'null' => false), - 'size' => array('type' => 'integer', 'null' => false), - 'permissions' => array('type' => 'string', 'length' => 10, 'null' => false), - ); - - /** - * @var (int|string)[][] - */ - public $records = array - ( - array('id' => 1, 'document_id' => 1, 'weight' => 5, 'size' => 256, 'permissions' => 'rw-r--r--'), - array('id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'), - array('id' => 3, 'document_id' => 3, 'weight' => 2, 'size' => 78, 'permissions' => 'rw-rw-r--'), - array('id' => 4, 'document_id' => 4, 'weight' => 1, 'size' => 412, 'permissions' => 'rw-r--r--'), - array('id' => 5, 'document_id' => 5, 'weight' => 4, 'size' => 790, 'permissions' => 'rw-rw-r--'), - ); -} diff --git a/View/Elements/filter_form_begin.ctp b/View/Elements/filter_form_begin.ctp deleted file mode 100644 index 7bba9dc..0000000 --- a/View/Elements/filter_form_begin.ctp +++ /dev/null @@ -1,34 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ -?> -
- Form->create( - false, - array( - 'url' => array( - 'plugin' => $this->request->params['plugin'], - 'controller' => $this->request->params['controller'], - 'action' => $this->request->params['action'], - ), - 'id' => $modelName.'Filter', - ) + $options - ); ?> - Form->inputDefaults(array('required' => false)); ?> -
- - Form->input('Filter.filterFormId', array('type' => 'hidden', 'value' => $modelName)); ?> diff --git a/View/Elements/filter_form_end.ctp b/View/Elements/filter_form_end.ctp deleted file mode 100644 index 4985522..0000000 --- a/View/Elements/filter_form_end.ctp +++ /dev/null @@ -1,17 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ -?> -
- Form->submit(__('Submit')); ?> - Form->end(); ?> -
diff --git a/View/Elements/filter_form_fields.ctp b/View/Elements/filter_form_fields.ctp deleted file mode 100644 index 0d1ba57..0000000 --- a/View/Elements/filter_form_fields.ctp +++ /dev/null @@ -1,27 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ - -if (isset($viewFilterParams)) -{ - foreach ($viewFilterParams as $field) - { - if(empty($includeFields) || in_array($field['name'], $includeFields)) - { - $fieldName = explode('.', $field['name']); - if (count($fieldName) === 2) { - $field['options']['name'] = sprintf('data[%s][%s]', $fieldName[0], $fieldName[1]); - } - echo $this->Form->input($field['name'], $field['options']); - } - } -} diff --git a/View/Helper/FilterHelper.php b/View/Helper/FilterHelper.php deleted file mode 100644 index fb3b1b7..0000000 --- a/View/Helper/FilterHelper.php +++ /dev/null @@ -1,114 +0,0 @@ - - - Multi-licensed under: - MPL - LGPL - GPL -*/ -App::uses('AppHelper', 'View/Helper'); - -class FilterHelper extends AppHelper -{ - /** - * @param string $modelName - * @param mixed[] $options - * @return string - */ - public function filterForm($modelName, $options) - { - $view =& $this->_View; - - $output = $view->element - ( - 'filter_form_begin', - array - ( - 'plugin' => 'Filter', - 'modelName' => $modelName, - 'options' => $options - ), - array('plugin' => 'Filter') - ); - - $output .= $view->element - ( - 'filter_form_fields', - array('plugin' => 'Filter'), - array('plugin' => 'Filter') - ); - - $output .= $view->element - ( - 'filter_form_end', - array('plugin' => 'Filter'), - array('plugin' => 'Filter') - ); - - return $output; - } - - /** - * @param string $modelName - * @param mixed[] $options - * @return string - */ - public function beginForm($modelName, $options) - { - $view =& $this->_View; - $output = $view->element - ( - 'filter_form_begin', - array - ( - 'plugin' => 'Filter', - 'modelName' => $modelName, - 'options' => $options - ), - array('plugin' => 'Filter') - ); - - return $output; - } - - /** - * @param string[] $fields - * @return string - */ - public function inputFields($fields = array()) - { - $view =& $this->_View; - $output = $view->element - ( - 'filter_form_fields', - array - ( - 'plugin' => 'Filter', - 'includeFields' => $fields - ), - array('plugin' => 'Filter') - ); - - return $output; - } - - /** - * @return string - */ - public function endForm() - { - $view = $this->_View; - $output = $view->element - ( - 'filter_form_end', - array(), - array('plugin' => 'Filter') - ); - - return $output; - } -} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b108016 --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "lecterror/cakephp-filter-plugin", + "description": "Filter is a CakePHP plugin which enables you to create filtering forms for your data in a very fast and simple way, without getting in the way of paging, sorting and other \\\"standard\\\" things when displaying data. It also remembers the filter conditions in a session, but this can be turned off if undesirable.", + "type": "cakephp-plugin", + "license": "GPL-3.0-or-later", + "minimum-stability": "stable", + "require": { + "cakephp/cakephp": "^3.10" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^5.1", + "overtrue/phplint": "^2.0|^3.4", + "phpstan/phpstan": "^0.1|^1.10", + "phpunit/phpunit": "^5.7.14|^6.0" + }, + "autoload": { + "psr-4": { + "Filter\\": "src/", + "Filter\\Test\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/phpstan.neon b/phpstan.neon index f391fb0..ac708a9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,10 @@ parameters: - phpstan-bootstrap.php scanDirectories: - ./ + fileExtensions: + - php + - ctp ignoreErrors: # False positive as __() function can accept more than 2 parameters. - - '/Function __ invoked with [0-9_]+ parameters, 1-2 required./' + - '/Cannot call method enableHydration\(\) on array\|Cake\\ORM\\Query./' + - '/Cannot call method join\(\) on array\|Cake\\ORM\\Query./' diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..c352744 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,26 @@ + + + + + tests/ + + + + + + + + + + + + + + + src/ + + + diff --git a/src/Controller/Component/FilterComponent.php b/src/Controller/Component/FilterComponent.php new file mode 100644 index 0000000..d09f43d --- /dev/null +++ b/src/Controller/Component/FilterComponent.php @@ -0,0 +1,402 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +/** + * @property \Filter\Controller\Component\RequestHandlerComponent $RequestHandler + * @property \Filter\Controller\Component\SessionComponent $Session + */ +class FilterComponent extends Component +{ + /** + * @var array + */ + public $components = ['Session']; + + /** + * @var array + */ + public $settings = []; + + /** + * @var array + */ + public $nopersist = []; + + /** + * @var array + */ + public $formData = []; + + /** + * @var array + */ + protected $_request_settings = []; + + /** + * {@inheritDoc} + * + * @param \Cake\Controller\ComponentRegistry $registry A ComponentRegistry this component can use to lazy load its components + * @param array $config Array of configuration settings. + */ + public function __construct(ComponentRegistry $registry, array $config = []) + { + parent::__construct($registry, $config); + $this->_request_settings = $config; + } + + /** + * Is called before the controller’s beforeFilter method, but after the controller’s initialize() method. + * + * @param \Cake\Event\Event $event Event object. + * @return void + */ + public function beforeFilter(Event $event) + { + $controller = $this->getController(); + if (!isset($controller->filters)) { + return; + } + + $this->__updatePersistence($this->_request_settings); + $controllerName = $controller->getName(); + $this->settings[$controllerName] = $controller->filters; + + $action = $controller->getRequest()->getParam('action'); + if (!isset($this->settings[$controllerName][$action])) { + return; + } + + $settings = $this->settings[$controllerName][$action]; + + foreach ($settings as $model => $filter) { + if (!isset($controller->{$model})) { + trigger_error(sprintf('Filter model not found: %s', $model)); + continue; + } + + $controller->$model->addBehavior('Filter.Filtered', $filter); + } + } + + /** + * Is called after the controller’s beforeFilter method but before the controller executes the current action handler. + * + * @param \Cake\Event\Event $event Event object. + * @return void + */ + public function startup(Event $event) + { + $controller = $this->getController(); + $controllerName = $controller->getName(); + $action = $controller->getRequest()->getParam('action'); + if (!isset($this->settings[$controllerName][$action])) { + return; + } + + $settings = $this->settings[$controllerName][$action]; + + if (!in_array('Filter.Filter', $controller->viewBuilder()->getHelpers())) { + $controller->viewBuilder()->setHelpers(['Filter.Filter']); + } + + $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', $controllerName, $action); + $Session = $controller->getRequest()->getSession(); + $filterFormId = $controller->request->getQuery('filterFormId'); + if ($controller->request->is('get') && !empty($filterFormId)) { + /** @var array $requestData */ + $requestData = $controller->request->getQuery('data', []); + $this->formData = $requestData; + } elseif (!$controller->request->is('post') || $controller->request->getData('Filter.filterFormId') === null) { + $persistedData = []; + + if ($Session->check($sessionKey)) { + $persistedData = $Session->read($sessionKey); + } + + if (empty($persistedData)) { + return; + } + + $this->formData = $persistedData; + } else { + /** @var array $requestData */ + $requestData = $controller->request->getData(); + $this->formData = $requestData; + if ($Session->started()) { + $Session->write($sessionKey, $this->formData); + } + } + foreach ($settings as $model => $options) { + if (!isset($controller->{$model})) { + trigger_error(__('Filter model not found: %s', $model)); + continue; + } + + $controller->$model->setFilterValues($this->formData); + } + } + + /** + * Is called after the controller executes the requested action’s logic, but before the controller renders views and layout. + * + * @param \Cake\Event\Event $event Event object. + * @return void + */ + public function beforeRender(Event $event) + { + $controller = $this->getController(); + $controllerName = $controller->getName(); + $action = $controller->getRequest()->getParam('action'); + if (!isset($this->settings[$controllerName][$action])) { + return; + } + + $models = $this->settings[$controllerName][$action]; + $viewFilterParams = []; + + foreach ($models as $model => $fields) { + if (!isset($controller->$model)) { + trigger_error(__('Filter model not found: %s', $model)); + continue; + } + + foreach ($fields as $field => $settings) { + if (!is_array($settings)) { + $field = $settings; + $settings = []; + } + + if (!isset($settings['required'])) { + $settings['required'] = false; + } + + if (!isset($settings['type'])) { + $settings['type'] = 'text'; + } + + $options = []; + + $fieldName = $field; + $fieldModel = $model; + $className = null; + if (isset($settings['className'])) { + $className = $settings['className']; + } + if (strpos($field, '.') !== false) { + list($fieldModel, $fieldName) = explode('.', $field); + } + + if (!empty($this->formData)) { + if (isset($this->formData[$fieldModel][$fieldName])) { + $options['value'] = $this->formData[$fieldModel][$fieldName]; + + if ($options['value']) { + $options['class'] = 'filter-active'; + } + } + } + + if (isset($settings['inputOptions'])) { + if (!is_array($settings['inputOptions'])) { + $settings['inputOptions'] = [$settings['inputOptions']]; + } + + $options = array_merge($options, $settings['inputOptions']); + } + + if (isset($settings['label'])) { + $options['label'] = $settings['label']; + } + + switch ($settings['type']) { + case 'select': + $options['type'] = 'select'; + + $selectOptions = []; + $TableLocator = $this->getController()->getTableLocator(); + if ($TableLocator->exists($fieldModel)) { + $workingModel = $TableLocator->get($fieldModel); + } else { + if ($className !== null) { + $workingModel = $TableLocator->get($fieldModel, [ + 'className' => $className, + ]); + } else { + $workingModel = $TableLocator->get($fieldModel); + } + } + + if (isset($settings['selectOptions'])) { + $selectOptions = $settings['selectOptions']; + } + + if (isset($settings['selector'])) { + if (!method_exists($workingModel, $settings['selector'])) { + trigger_error( + sprintf( + 'Selector method "%s" not found in model "%s" for field "%s"!', + $settings['selector'], + $fieldModel, + $fieldName + ) + ); + + return; + } + + $selectorName = $settings['selector']; + $options['options'] = $workingModel->$selectorName($selectOptions); + } else { + if ($fieldModel == $model) { + $listOptions = array_merge( + $selectOptions, + [ + 'nofilter' => true, + 'keyField' => $fieldName, + 'valueField' => $fieldName, + 'fields' => [$fieldName, $fieldName], + ] + ); + } else { + $listOptions = array_merge($selectOptions, ['nofilter' => true]); + } + $options['options'] = $workingModel->find('list', $listOptions) + ->toArray(); + } + + if (!$settings['required']) { + $options['empty'] = ''; + } + + if (isset($settings['multiple'])) { + $options['multiple'] = $settings['multiple']; + } + + break; + + case 'checkbox': + $options['type'] = 'checkbox'; + + if (isset($options['value'])) { + $options['checked'] = (bool)$options['value']; + unset($options['value']); + } elseif (isset($settings['default'])) { + $options['checked'] = (bool)$settings['default']; + } + break; + + default: + $options['type'] = $settings['type']; + break; + } + + // if no value has been set, show the default one + if ( + !isset($options['value']) && + isset($settings['default']) && + $options['type'] != 'checkbox' + ) { + $options['value'] = $settings['default']; + } + + $viewFilterParams[] = + [ + 'name' => sprintf('%s.%s', $fieldModel, $fieldName), + 'options' => $options, + ]; + } + } + + if ( + !empty($this->settings['add_filter_value_to_title']) && + array_search($action, $this->settings['add_filter_value_to_title']) !== false + ) { + $title = $controller->viewVars['title_for_layout']; + foreach ($viewFilterParams as $viewFilterParam) { + if ( + !empty($viewFilterParam['options']['class']) && + $viewFilterParam['options']['class'] == 'filter-active' + ) { + $titleValue = $viewFilterParam['options']['value']; + if ($viewFilterParam['options']['type'] == 'select') { + $titleValue = $viewFilterParam['options']['options'][$titleValue]; + } + $title .= ' - ' . $titleValue; + } + } + $controller->set('title_for_layout', $title); + } + $controller->set('viewFilterParams', $viewFilterParams); + } + + /** + * @param array $settings Settings. + * @return void + */ + private function __updatePersistence($settings) + { + $controller = $this->getController(); + $controllerName = $controller->getName(); + $Session = $controller->getRequest()->getSession(); + if ($Session->check('FilterPlugin.NoPersist')) { + $this->nopersist = $Session->read('FilterPlugin.NoPersist'); + } + + if (isset($settings['nopersist'])) { + $this->nopersist[$controllerName] = $settings['nopersist']; + if ($Session->started()) { + $Session->write('FilterPlugin.NoPersist', $this->nopersist); + } + } elseif (isset($this->nopersist[$controllerName])) { + unset($this->nopersist[$controllerName]); + if ($Session->started()) { + $Session->write('FilterPlugin.NoPersist', $this->nopersist); + } + } + + if (!empty($this->nopersist)) { + foreach ($this->nopersist as $nopersistController => $actions) { + if (is_string($actions)) { + $actions = [$actions]; + } elseif ($actions === true) { + $actions = []; + } + + if (empty($actions) && $controllerName != $nopersistController) { + if ($Session->check(sprintf('FilterPlugin.Filters.%s', $nopersistController))) { + $Session->delete(sprintf('FilterPlugin.Filters.%s', $nopersistController)); + continue; + } + } + + $action = $controller->getRequest()->getParam('action'); + foreach ($actions as $noPersistAction) { + if ($controllerName == $nopersistController && $noPersistAction == $action) { + continue; + } + $sessionKey = sprintf('FilterPlugin.Filters.%s.%s', $nopersistController, $noPersistAction); + if ($Session->check($sessionKey)) { + $Session->delete($sessionKey); + } + } + } + } + } +} diff --git a/src/Model/Behavior/FilteredBehavior.php b/src/Model/Behavior/FilteredBehavior.php new file mode 100644 index 0000000..ed3a639 --- /dev/null +++ b/src/Model/Behavior/FilteredBehavior.php @@ -0,0 +1,471 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class FilteredBehavior extends Behavior +{ + /** + * Keeps current values after filter form post. + * + * @var array + */ + protected $_filterValues = []; + + /** + * 2.x compartible settings (supports having dots in the keys, f.ex. 'Model.id'). + * + * @var array + */ + public $settings = []; + + /** + * {@inheritDoc} + * + * @param array $settings The configuration settings provided to this behavior. + * @return void + */ + public function initialize(array $settings) + { + foreach ($settings as $key => $value) { + if (!is_array($value)) { + $key = $value; + $value = []; + } + + $this->settings[$this->getTable()->getAlias()][$key] = array_merge( + [ + 'type' => 'text', + 'condition' => 'like', + 'required' => false, + 'selectOptions' => [], + ], + $value + ); + } + + $this->_filterValues[$this->getTable()->getAlias()] = []; + } + + /** + * {@inheritDoc} + * + * Callback method that listens to the `beforeFind` event in the bound + * table. It modifies the passed query by applying search filters. + * + * @param \Cake\Event\Event $event The beforeFind event that was fired. + * @param \Cake\ORM\Query $Query Query. + * @param \ArrayObject $options The options for the query. + * @return void + */ + public function beforeFind(Event $event, Query $Query, ArrayObject $options) + { + if (isset($Query->getOptions()['nofilter']) && $Query->getOptions()['nofilter'] === true) { + return; + } + $Table = $this->getTable(); + $alias = $Table->getAlias(); + if (method_exists($Table, 'beforeDataFilter')) { + $callbackOptions['values'] = $this->_filterValues[$alias]; + $callbackOptions['settings'] = $this->settings[$alias]; + + if (!$Table->beforeDataFilter($Query, $callbackOptions)) { + return; + } + } + + if (!isset($this->settings[$alias])) { + return; + } + + $settings = $this->settings[$alias]; + $values = $this->_filterValues[$alias]; + + foreach ($settings as $field => $options) { + $this->addFieldToFilter($Table, $Query, $values, $field, $options); + } + + if (method_exists($Table, 'afterDataFilter')) { + $callbackOptions['values'] = $this->_filterValues[$alias]; + $callbackOptions['settings'] = $this->settings[$alias]; + + $Table->afterDataFilter($Query, $callbackOptions); + } + } + + /** + * Adds field filters. + * + * @param \Cake\ORM\Table $Table Model table object. + * @param \Cake\ORM\Query $Query Query object. + * @param array $values Filter values. + * @param string $field Field name. + * @param array $fieldOptions Field options. + * @return void + */ + protected function addFieldToFilter(Table $Table, Query $Query, $values, $field, $fieldOptions) + { + $configurationModelName = $Table->getAlias(); + $configurationFieldName = $field; + + if (strpos($field, '.') !== false) { + list($configurationModelName, $configurationFieldName) = explode('.', $field); + } + + if (!isset($values[$configurationModelName][$configurationFieldName]) && isset($fieldOptions['default'])) { + $values[$configurationModelName][$configurationFieldName] = $fieldOptions['default']; + } + + if ($fieldOptions['required'] && !isset($values[$configurationModelName][$configurationFieldName])) { + trigger_error(sprintf('No value present for required field "%s" and default value not present', $field)); + + return; + } + + if ( + !isset($values[$configurationModelName][$configurationFieldName]) || + ( + empty($values[$configurationModelName][$configurationFieldName]) && + $values[$configurationModelName][$configurationFieldName] != 0 + ) + ) { + // no value to filter with, just skip this field + return; + } + + // the value we get as condition and where it comes from is not the same as the + // model and field we're using to filter the data + $filterFieldName = $configurationFieldName; + $filterModelName = $configurationModelName; + $linkModelName = null; + $relationType = null; + + if ($configurationModelName != $Table->getAlias()) { + if ($Table->hasAssociation($configurationModelName)) { + $relationType = $Table->getAssociation($configurationModelName)->type(); + if ($relationType == Association::MANY_TO_MANY) { + $linkModelName = $Table->{$configurationModelName}->junction()->getAlias(); + } + $filterModelName = 'Filter' . $configurationModelName; + } + } + + if (isset($fieldOptions['filterField'])) { + if (strpos($fieldOptions['filterField'], '.') !== false) { + list($filterModelName, $filterFieldName) = explode('.', $fieldOptions['filterField']); + + if ($filterModelName != $Table->getAlias()) { + $filterModelName = 'Filter' . $filterModelName; + } + } else { + $filterModelName = $Table->getAlias(); + $filterFieldName = $fieldOptions['filterField']; + } + } + + $realFilterField = sprintf('%s.%s', $filterModelName, $filterFieldName); + if ($Table->hasAssociation($configurationModelName)) { + $relatedModel = $Table->{$configurationModelName}->getTarget(); + if (!$this->__isAlreadyJoined($Query, $relatedModel)) { + $joinStatements = $this->buildFilterJoin($Table, $relatedModel, $linkModelName); + foreach ($joinStatements as $joinStatement) { + $Query->join($joinStatement); + } + } + } + + $this->buildFilterConditions( + $Query, + $realFilterField, + $fieldOptions, + $values[$configurationModelName][$configurationFieldName] + ); + } + + /** + * Checks whether the given query object already contains a given table join. + * + * @param \Cake\ORM\Query $Query Query object. + * @param \Cake\ORM\Table $Table Related model. + * @return bool + */ + private function __isAlreadyJoined(Query $Query, Table $Table) + { + $relatedModelAlias = 'Filter' . $Table->getAlias(); + $containedAliases = array_keys($Query->getContain()); + $joinAliases = $this->__extractJoinAliases($Query); + + return in_array($relatedModelAlias, $containedAliases) || in_array($relatedModelAlias, $joinAliases); + } + + /** + * Extract the JOIN clause aliases from the given query object. + * + * @param \Cake\ORM\Query $Query Query object. + * @return array + */ + private function __extractJoinAliases(Query $Query) + { + $aliases = []; + $joins = $Query->clause('join'); + foreach ($joins as $join) { + if (array_key_exists('alias', $join)) { + $aliases[] = $join['alias']; + } + } + + return $aliases; + } + + /** + * Build join conditions from Model to relatedModel. + * + * @param \Cake\ORM\Table $Table Model table object. + * @param \Cake\ORM\Table $RelatedTable Related model table object. + * @param string $linkModelName Linked model name (alias) in MANY_TO_MANY association. + * @return array Cake join array. + */ + protected function buildFilterJoin(Table $Table, Table $RelatedTable, $linkModelName) + { + $conditions = []; + $alias = $Table->getAlias(); + $primaryKey = $Table->getPrimaryKey(); + $relatedTableAlias = $RelatedTable->getAlias(); + $relatedModelAlias = null; + $relationType = null; + $association = null; + $foreignKey = null; + $associationPrimaryKey = null; + $associationConditions = null; + if (!$Table->hasAssociation($relatedTableAlias)) { + return []; + } + $relatedModelAlias = 'Filter' . $relatedTableAlias; + $association = $Table->getAssociation($relatedTableAlias); + $linkModelAlias = null; + if (!empty($linkModelName) && ($association instanceof BelongsToMany)) { + $linkModelAlias = $association->junction()->getAlias(); + } + $relationType = $association->type(); + $foreignKey = $association->getForeignKey(); + $associationConditions = $association->getConditions(); + $associationPrimaryKey = $RelatedTable->getPrimaryKey(); + $linkConditions = []; + if (!empty($foreignKey) && is_string($foreignKey)) { + if ($relationType == Association::MANY_TO_ONE && is_string($associationPrimaryKey)) { + $conditions[] = sprintf( + '%s.%s = %s.%s', + $alias, + $foreignKey, + $relatedModelAlias, + $associationPrimaryKey + ); + } elseif ( + in_array($relationType, [Association::ONE_TO_MANY, Association::ONE_TO_ONE]) && + is_string($primaryKey) + ) { + $conditions[] = sprintf( + '%s.%s = %s.%s', + $alias, + $primaryKey, + $relatedModelAlias, + $foreignKey + ); + } elseif ( + $relationType == Association::MANY_TO_MANY && + is_string($primaryKey) && + is_string($associationPrimaryKey) + ) { + $associationForeignKey = $RelatedTable->getAssociation($alias)->getForeignKey(); + if (is_string($associationForeignKey)) { + $conditions[] = sprintf( + '%s.%s = %s.%s', + $linkModelAlias, + $associationForeignKey, + $relatedModelAlias, + $associationPrimaryKey + ); + } + + $linkConditions[] = sprintf( + '%s.%s = %s.%s', + $alias, + $primaryKey, + $linkModelAlias, + $foreignKey + ); + } + } + + // merge any custom conditions from the relation, but change + // the alias to our $relatedModelAlias + if (!empty($associationConditions)) { + $customConditions = $associationConditions; + + if (!is_array($associationConditions)) { + $customConditions = [$customConditions]; + } + $formatAlias = sprintf('#(? $RelatedTable->getTable(), + 'alias' => $relatedModelAlias, + 'type' => 'LEFT', + 'conditions' => $conditions, + ], + ]; + + if (!empty($linkModelName) && ($association instanceof BelongsToMany)) { + $return = + [ + + [ + 'table' => $association->junction()->getTable(), + 'alias' => $linkModelAlias, + 'type' => 'LEFT', + 'conditions' => $linkConditions, + ], + + [ + 'table' => $RelatedTable->getTable(), + 'alias' => $relatedModelAlias, + 'type' => 'LEFT', + 'conditions' => $conditions, + ], + ]; + } + + return $return; + } + + /** + * Build query conditions and add them to $Query. + * + * @param \Cake\ORM\Query $Query Cake query array. + * @param string $field Filter field. + * @param array $options Configuration options for this field. + * @param mixed $value Field value. + * @return void + */ + protected function buildFilterConditions(Query $Query, $field, $options, $value) + { + $conditionFieldFormats = + [ + 'like' => '%s like', + 'ilike' => '%s ilike', + 'contains' => '%s like', + 'startswith' => '%s like', + 'endswith' => '%s like', + 'equal' => '%s', + 'equals' => '%s', + '=' => '%s', + ]; + $conditionValueFormats = + [ + 'like' => '%%%s%%', + 'ilike' => '%%%s%%', + 'contains' => '%%%s%%', + 'startswith' => '%s%%', + 'endswith' => '%%%s', + 'equal' => '%s', + 'equals' => '%s', + '=' => '%s', + ]; + + switch ($options['type']) { + case 'select': + if (is_string($value) && strlen(trim(strval($value))) == 0) { + break; + } + if (is_array($value)) { + $Query->andWhere([$field . ' IN' => $value]); + } else { + $Query->andWhere([$field => $value]); + } + break; + case 'checkbox': + if (is_array($value)) { + $Query->andWhere([$field . ' IN' => $value]); + } else { + $Query->andWhere([$field => $value]); + } + break; + default: + if (strlen(trim(strval($value))) == 0) { + break; + } + + $condition = $options['condition']; + + switch ($condition) { + case 'like': + case 'ilike': + case 'contains': + case 'startswith': + case 'endswith': + case 'equal': + case 'equals': + case '=': + $formattedField = sprintf($conditionFieldFormats[$condition], $field); + $formattedValue = sprintf($conditionValueFormats[$condition], $value); + $Query->andWhere([$formattedField => $formattedValue]); + break; + default: + $Query->andWhere([$field . ' ' . $condition => $value]); + break; + } + + break; + } + } + + /** + * Sets filter values. + * + * @param array $values Filter values. + * @return void + */ + public function setFilterValues($values = []) + { + $alias = $this->getTable()->getAlias(); + $this->_filterValues[$alias] = array_merge($this->_filterValues[$alias], (array)$values); + } + + /** + * Gets filter values. + * + * @return array + */ + public function getFilterValues() + { + return $this->_filterValues; + } +} diff --git a/src/View/Elements/filter_form_begin.ctp b/src/View/Elements/filter_form_begin.ctp new file mode 100644 index 0000000..83bf4f2 --- /dev/null +++ b/src/View/Elements/filter_form_begin.ctp @@ -0,0 +1,37 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + + @var array $options + @var string $modelName + @var \Cake\View\View $this + */ + +?> +
+ Form->create( + false, + [ + 'url' => [ + 'plugin' => $this->getRequest()->getParam('plugin'), + 'controller' => $this->getRequest()->getParam('controller'), + 'action' => $this->getRequest()->getParam('action'), + ], + 'id' => $modelName . 'Filter', + ] + $options + ); ?> +
+ + Form->control('Filter.filterFormId', ['type' => 'hidden', 'value' => $modelName]); ?> diff --git a/src/View/Elements/filter_form_end.ctp b/src/View/Elements/filter_form_end.ctp new file mode 100644 index 0000000..acb0762 --- /dev/null +++ b/src/View/Elements/filter_form_end.ctp @@ -0,0 +1,19 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + + @var \Cake\View\View $this + */ +?> +
+ Form->submit(__('Submit')); ?> + Form->end(); ?> +
diff --git a/src/View/Elements/filter_form_fields.ctp b/src/View/Elements/filter_form_fields.ctp new file mode 100644 index 0000000..19cd8fa --- /dev/null +++ b/src/View/Elements/filter_form_fields.ctp @@ -0,0 +1,29 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + + @var \Cake\View\View $this + */ + +if (isset($viewFilterParams)) { + foreach ($viewFilterParams as $field) { + if (empty($includeFields) || in_array($field['name'], $includeFields)) { + $fieldName = explode('.', $field['name']); + if (count($fieldName) === 2) { + $field['options']['name'] = sprintf('data[%s][%s]', $fieldName[0], $fieldName[1]); + } + if (!isset($field['required'])) { + $field['required'] = false; + } + echo $this->Form->control($field['name'], $field['options']); + } + } +} diff --git a/src/View/Helper/FilterHelper.php b/src/View/Helper/FilterHelper.php new file mode 100644 index 0000000..cab1dec --- /dev/null +++ b/src/View/Helper/FilterHelper.php @@ -0,0 +1,109 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class FilterHelper extends Helper +{ + /** + * @param string $modelName Model name. + * @param array $options Options. + * @return string + */ + public function filterForm($modelName, $options) + { + $view =& $this->_View; + + $output = $view->element( + 'filter_form_begin', + [ + 'plugin' => 'Filter', + 'modelName' => $modelName, + 'options' => $options, + ], + ['plugin' => 'Filter'] + ); + + $output .= $view->element( + 'filter_form_fields', + ['plugin' => 'Filter'], + ['plugin' => 'Filter'] + ); + + $output .= $view->element( + 'filter_form_end', + ['plugin' => 'Filter'], + ['plugin' => 'Filter'] + ); + + return $output; + } + + /** + * @param string $modelName Model name. + * @param array $options Options. + * @return string + */ + public function beginForm($modelName, $options) + { + $view =& $this->_View; + $output = $view->element( + 'filter_form_begin', + [ + 'plugin' => 'Filter', + 'modelName' => $modelName, + 'options' => $options, + ], + ['plugin' => 'Filter'] + ); + + return $output; + } + + /** + * @param array $fields Fileds to include. + * @return string + */ + public function inputFields($fields = []) + { + $view =& $this->_View; + $output = $view->element( + 'filter_form_fields', + [ + 'plugin' => 'Filter', + 'includeFields' => $fields, + ], + ['plugin' => 'Filter'] + ); + + return $output; + } + + /** + * @return string + */ + public function endForm() + { + $view = $this->_View; + $output = $view->element( + 'filter_form_end', + [], + ['plugin' => 'Filter'] + ); + + return $output; + } +} diff --git a/tests/Fixture/DocumentCategoriesFixture.php b/tests/Fixture/DocumentCategoriesFixture.php new file mode 100644 index 0000000..8293c63 --- /dev/null +++ b/tests/Fixture/DocumentCategoriesFixture.php @@ -0,0 +1,45 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class DocumentCategoriesFixture extends TestFixture +{ + /** + * @var mixed[] + */ + public $fields = + [ + 'id' => ['type' => 'integer'], + 'title' => ['type' => 'string', 'length' => 100, 'null' => false], + 'description' => ['type' => 'string', 'length' => 255], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * @var (int|string)[][] + */ + public $records = + [ + ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + ['id' => 2, 'title' => 'Imaginary Spec', 'description' => 'This doc does not exist'], + ['id' => 3, 'title' => 'Nonexistant data', 'description' => 'This doc is probably empty'], + ['id' => 4, 'title' => 'Illegal explosives DIY', 'description' => 'Viva la revolucion!'], + ['id' => 5, 'title' => 'Father Ted', 'description' => 'Feck! Drink! Arse! Girls!'], + ]; +} diff --git a/tests/Fixture/DocumentsFixture.php b/tests/Fixture/DocumentsFixture.php new file mode 100644 index 0000000..ef469a0 --- /dev/null +++ b/tests/Fixture/DocumentsFixture.php @@ -0,0 +1,51 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class DocumentsFixture extends TestFixture +{ + /** + * @var mixed[] + */ + public $fields = + [ + 'id' => ['type' => 'integer'], + 'title' => ['type' => 'string', 'length' => '255', 'null' => false], + 'document_category_id' => ['type' => 'integer', 'null' => false], + 'owner_id' => ['type' => 'integer', 'null' => false], + 'is_private' => ['type' => 'integer', 'length' => 1, 'null' => false], + 'created' => ['type' => 'datetime', 'null' => false], + 'updated' => ['type' => 'datetime', 'null' => true], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * @var (int|string)[][] + */ + public $records = + [ + ['id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-06-28 10:39:23', 'updated' => '2010-06-29 11:22:48'], + ['id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-03-28 12:19:13', 'updated' => '2010-04-29 11:23:44'], + ['id' => 3, 'title' => 'Nonexistant data', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, 'created' => '2010-04-28 11:12:33', 'updated' => '2010-05-05 15:03:24'], + ['id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1, 'created' => '2010-01-08 05:15:03', 'updated' => '2010-05-22 03:15:24'], + ['id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'], + ['id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'], + ['id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0, 'created' => '2009-01-13 05:15:03', 'updated' => '2010-12-05 03:24:15'], + ]; +} diff --git a/tests/Fixture/ItemsFixture.php b/tests/Fixture/ItemsFixture.php new file mode 100644 index 0000000..4e8a831 --- /dev/null +++ b/tests/Fixture/ItemsFixture.php @@ -0,0 +1,50 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class ItemsFixture extends TestFixture +{ + /** + * @var mixed[] + */ + public $fields = + [ + 'id' => ['type' => 'integer'], + 'document_id' => ['type' => 'integer', 'null' => false], + 'code' => ['type' => 'string', 'length' => '20', 'null' => false], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * @var (int|string)[][] + */ + public $records = + [ + ['id' => 1, 'document_id' => 1, 'code' => 'The item #01'], + ['id' => 2, 'document_id' => 1, 'code' => 'The item #02'], + ['id' => 3, 'document_id' => 1, 'code' => 'The item #03'], + ['id' => 4, 'document_id' => 2, 'code' => 'The item #01'], + ['id' => 5, 'document_id' => 2, 'code' => 'The item #02'], + ['id' => 6, 'document_id' => 2, 'code' => 'The item #03'], + ['id' => 7, 'document_id' => 2, 'code' => 'The item #04'], + ['id' => 8, 'document_id' => 3, 'code' => 'The item #01'], + ['id' => 9, 'document_id' => 4, 'code' => 'The item #01'], + ['id' => 10, 'document_id' => 5, 'code' => 'The item #01'], + ]; +} diff --git a/tests/Fixture/MetadataFixture.php b/tests/Fixture/MetadataFixture.php new file mode 100644 index 0000000..a5b2b33 --- /dev/null +++ b/tests/Fixture/MetadataFixture.php @@ -0,0 +1,47 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class MetadataFixture extends TestFixture +{ + /** + * @var mixed[] + */ + public $fields = + [ + 'id' => ['type' => 'integer'], + 'document_id' => ['type' => 'integer', 'null' => false], + 'weight' => ['type' => 'integer', 'null' => false], + 'size' => ['type' => 'integer', 'null' => false], + 'permissions' => ['type' => 'string', 'length' => 10, 'null' => false], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * @var (int|string)[][] + */ + public $records = + [ + ['id' => 1, 'document_id' => 1, 'weight' => 5, 'size' => 256, 'permissions' => 'rw-r--r--'], + ['id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'], + ['id' => 3, 'document_id' => 3, 'weight' => 2, 'size' => 78, 'permissions' => 'rw-rw-r--'], + ['id' => 4, 'document_id' => 4, 'weight' => 1, 'size' => 412, 'permissions' => 'rw-r--r--'], + ['id' => 5, 'document_id' => 5, 'weight' => 4, 'size' => 790, 'permissions' => 'rw-rw-r--'], + ]; +} diff --git a/tests/TestCase/Controller/Component/FilterComponentTest.php b/tests/TestCase/Controller/Component/FilterComponentTest.php new file mode 100644 index 0000000..17a165b --- /dev/null +++ b/tests/TestCase/Controller/Component/FilterComponentTest.php @@ -0,0 +1,732 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class FilterComponentTest extends TestCase +{ + /** + * @var string[] + */ + public $fixtures = + [ + 'plugin.Filter.DocumentCategories', + 'plugin.Filter.Documents', + 'plugin.Filter.Items', + 'plugin.Filter.Metadata', + ]; + + /** + * @var \Filter\Test\TestCase\MockObjects\DocumentTestsController + */ + public $Controller = null; + + public function setUp() + { + parent::setUp(); + $request = new ServerRequest([ + 'params' => [ + 'controller' => 'DocumentTests', + 'action' => 'index', + ], + ]); + $this->Controller = new DocumentTestsController($request); + } + + public function tearDown() + { + parent::tearDown(); + $this->Controller->getRequest()->getSession()->destroy(); + unset($this->Controller); + } + + /** + * Test bailing out when no filters are present. + * + * @return void + */ + public function testNoFilters() + { + $this->assertEmpty($this->Controller->Filter->settings); + $this->assertFalse($this->Controller->Document->hasBehavior('Filtered')); + + $this->assertFalse(in_array('Filter.Filter', $this->Controller->viewBuilder()->getHelpers())); + } + + /** + * @return void + */ + public function testNoActionFilters() + { + $testSettings = + [ + 'someotheraction' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + + $this->Controller->filters = $testSettings; + $this->Controller->dispatchEvent('Controller.initialize'); + $this->assertFalse($this->Controller->Document->hasBehavior('Filtered')); + + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + + $this->Controller->filters = $testSettings; + $this->Controller->dispatchEvent('Controller.initialize'); + $this->assertTrue($this->Controller->Document->hasBehavior('Filtered')); + } + + /** + * Test basic filter settings. + * + * @return void + */ + public function testBasicFilters() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $expected = + [ + $this->Controller->getName() => $testSettings, + ]; + $this->Controller->dispatchEvent('Controller.initialize'); + $this->assertEquals($expected, $this->Controller->Filter->settings); + } + + /** + * Test running a component with no filter data. + * + * @return void + */ + public function testEmptyStartup() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->assertTrue(in_array('Filter.Filter', $this->Controller->viewBuilder()->getHelpers())); + } + + /** + * @return void + */ + public function testSessionStartupDataFakeNonexistantModel() + { + $testSettings = + [ + 'index' => + [ + 'FakeNonexistant' => + [ + 'drink' => ['type' => 'select'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + $sessionKey = sprintf( + 'FilterPlugin.Filters.%s.%s', + $this->Controller->getName(), + $this->Controller->getRequest()->getParam('action') + ); + $filterValues = []; + $this->Controller->getRequest()->getSession()->write($sessionKey, $filterValues); + $this->expectException('PHPUnit\Framework\Error\Notice'); + $this->Controller->dispatchEvent('Controller.initialize'); + } + + /** + * Test loading filter data from session (both full and empty). + * + * @return void + */ + public function testSessionStartupData() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $sessionKey = sprintf( + 'FilterPlugin.Filters.%s.%s', + $this->Controller->getName(), + $this->Controller->getRequest()->getParam('action') + ); + + $filterValues = []; + $this->Controller->getRequest()->getSession()->write($sessionKey, $filterValues); + $this->Controller->dispatchEvent('Controller.initialize'); + + $this->Controller->dispatchEvent('Controller.startup'); + $actualFilterValues = $this->Controller->Document->getFilterValues(); + $this->assertEquals( + $filterValues, + $actualFilterValues[$this->Controller->Document->getAlias()] + ); + + $filterValues = ['Document' => ['title' => 'in']]; + $this->Controller->getRequest()->getSession()->write($sessionKey, $filterValues); + + $this->Controller->dispatchEvent('Controller.startup'); + $actualFilterValues = $this->Controller->Document->getFilterValues(); + $this->assertEquals( + $filterValues, + $actualFilterValues[$this->Controller->Document->getAlias()] + ); + + $this->Controller->getRequest()->getSession()->delete($sessionKey); + } + + /** + * Test loading filter data from a post request. + * + * @return void + */ + public function testPostStartupData() + { + $request = new ServerRequest([ + 'params' => [ + 'controller' => 'DocumentTests', + 'action' => 'index', + ], + 'environment' => [ + 'REQUEST_METHOD' => 'POST', + ], + ]); + $this->Controller = new DocumentTestsController($request); + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + + $this->Controller->filters = $testSettings; + + $filterValues = ['Document' => ['title' => 'in'], 'Filter' => ['filterFormId' => 'Document']]; + $this->Controller->request = $this->Controller->getRequest()->withParsedBody($filterValues); + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + + $sessionKey = sprintf( + 'FilterPlugin.Filters.%s.%s', + $this->Controller->getName(), + $this->Controller->getRequest()->getParam('action') + ); + $sessionData = $this->Controller->getRequest()->getSession()->read($sessionKey); + $this->assertEquals($filterValues, $sessionData); + + $actualFilterValues = $this->Controller->Document->getFilterValues(); + $this->assertEquals( + $filterValues, + $actualFilterValues[$this->Controller->Document->getAlias()] + ); + } + + /** + * Test exiting beforeRender when in an action with no settings. + * + * @return void + */ + public function testBeforeRenderAbort() + { + $testSettings = + [ + 'veryMuchNotIndex' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $this->assertFalse(isset($this->Controller->viewVars['viewFilterParams'])); + } + + /** + * Test triggering an error when the plugin runs into a setting + * for filtering a model which cannot be found. + * + * @return void + */ + public function testNoModelFound() + { + $testSettings = + [ + 'index' => + [ + 'ThisModelDoesNotExist' => + [ + 'ThisModelDoesNotExist.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + $this->expectException('PHPUnit\Framework\Error\Notice'); + $this->Controller->dispatchEvent('Controller.initialize'); + } + + /** + * Test the view variable generation for very basic filtering. + * Also tests model name detection and custom label. + * + * @return void + */ + public function testBasicViewInfo() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'title', + 'DocumentCategory.id' => [ + 'type' => 'select', + 'label' => 'Category', + 'className' => DocumentCategoriesTable::class, + ], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $expected = + [ + ['name' => 'Document.title', 'options' => ['type' => 'text']], + + [ + 'name' => 'DocumentCategory.id', + 'options' => + [ + 'type' => 'select', + 'options' => + [ + 1 => 'Testing Doc', + 2 => 'Imaginary Spec', + 3 => 'Nonexistant data', + 4 => 'Illegal explosives DIY', + 5 => 'Father Ted', + ], + 'empty' => false, + 'label' => 'Category', + ], + ], + ]; + + $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); + } + + /** + * Test passing additional inputOptions to the form + * helper, used to customize search form. + * + * @return void + */ + public function testAdditionalInputOptions() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'title' => ['inputOptions' => 'disabled'], + 'DocumentCategory.id' => + [ + 'type' => 'select', + 'label' => 'Category', + 'inputOptions' => ['class' => 'important'], + 'className' => DocumentCategoriesTable::class, + ], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $expected = + [ + + [ + 'name' => 'Document.title', + 'options' => + [ + 'type' => 'text', + 'disabled', + ], + ], + + [ + 'name' => 'DocumentCategory.id', + 'options' => + [ + 'type' => 'select', + 'options' => + [ + 1 => 'Testing Doc', + 2 => 'Imaginary Spec', + 3 => 'Nonexistant data', + 4 => 'Illegal explosives DIY', + 5 => 'Father Ted', + ], + 'empty' => false, + 'label' => 'Category', + 'class' => 'important', + ], + ], + ]; + + $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); + } + + /** + * Test data fetching for select input when custom selector + * and custom options are provided. + * + * @return void + */ + public function testCustomSelector() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'DocumentCategory.id' => + [ + 'type' => 'select', + 'label' => 'Category', + 'selector' => 'customSelector', + 'selectOptions' => [ + 'conditions' => ['DocumentCategory.description LIKE' => '%!%'], + ], + 'className' => DocumentCategoriesTable::class, + ], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $expected = + [ + + [ + 'name' => 'DocumentCategory.id', + 'options' => + [ + 'type' => 'select', + 'options' => + [ + 1 => 'Testing Doc', + 5 => 'Father Ted', + ], + 'empty' => false, + 'label' => 'Category', + ], + ], + ]; + + $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); + } + + /** + * Test checkbox input filtering. + * + * @return void + */ + public function testCheckboxOptions() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.is_private' => + [ + 'type' => 'checkbox', + 'label' => 'Private?', + 'default' => true, + ], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $expected = + [ + + [ + 'name' => 'Document.is_private', + 'options' => + [ + 'type' => 'checkbox', + 'checked' => true, + 'label' => 'Private?', + ], + ], + ]; + + $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); + } + + /** + * Test basic filter settings. + * + * @return void + */ + public function testSelectMultiple() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'DocumentCategory.id' => + [ + 'type' => 'select', + 'multiple' => true, + ], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $expected = + [ + $this->Controller->getName() => $testSettings, + ]; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->assertEquals($expected, $this->Controller->Filter->settings); + } + + /** + * Test select input for the model filtered. + * + * @return void + */ + public function testSelectInputFromSameModel() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => + [ + 'type' => 'select', + 'className' => DocumentsTable::class, + ], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $expected = + [ + + [ + 'name' => 'Document.title', + 'options' => + [ + 'type' => 'select', + 'options' => + [ + 'Testing Doc' => 'Testing Doc', + 'Imaginary Spec' => 'Imaginary Spec', + 'Nonexistant data' => 'Nonexistant data', + 'Illegal explosives DIY' => 'Illegal explosives DIY', + 'Father Ted' => 'Father Ted', + 'Duplicate title' => 'Duplicate title', + ], + 'empty' => '', + ], + ], + ]; + + $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); + } + + /** + * Test disabling persistence for single action + * and for the entire controller. + * + * @return void + */ + public function testPersistence() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'Document.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + $this->Controller->components()->unload('Filter'); + $this->Controller->loadComponent('Filter.Filter', ['nopersist' => true]); + + $sessionKey = sprintf( + 'FilterPlugin.Filters.%s.%s', + 'SomeOtherController', + $this->Controller->getRequest()->getParam('action') + ); + $filterValues = ['Document' => ['title' => 'in'], 'Filter' => ['filterFormId' => 'Document']]; + $this->Controller->getRequest()->getSession()->write($sessionKey, $filterValues); + + $sessionKey = sprintf( + 'FilterPlugin.Filters.%s.%s', + $this->Controller->getName(), + $this->Controller->getRequest()->getParam('action') + ); + $filterValues = ['Document' => ['title' => 'in'], 'Filter' => ['filterFormId' => 'Document']]; + $this->Controller->getRequest()->getSession()->write($sessionKey, $filterValues); + + $this->Controller->Filter->nopersist = []; + $this->Controller->Filter->nopersist[$this->Controller->getName()] = true; + $this->Controller->Filter->nopersist['SomeOtherController'] = true; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + + $expected = [ + $this->Controller->getName() => [ + $this->Controller->getRequest()->getParam('action') => $filterValues, + ], + ]; + $this->assertEquals($expected, $this->Controller->getRequest()->getSession()->read('FilterPlugin.Filters')); + } + + /** + * Test whether filtering by belongsTo model text field + * works correctly. + * + * @return void + */ + public function testBelongsToFilteringByText() + { + $testSettings = + [ + 'index' => + [ + 'Document' => + [ + 'DocumentCategory.title' => ['type' => 'text'], + ], + ], + ]; + $this->Controller->filters = $testSettings; + + $this->Controller->dispatchEvent('Controller.initialize'); + $this->Controller->dispatchEvent('Controller.startup'); + $this->Controller->dispatchEvent('Controller.beforeRender'); + + $expected = + [ + + [ + 'name' => 'DocumentCategory.title', + 'options' => + [ + 'type' => 'text', + ], + ], + ]; + + $this->assertEquals($expected, $this->Controller->viewVars['viewFilterParams']); + } +} diff --git a/tests/TestCase/MockObjects/DocumentCategoriesTable.php b/tests/TestCase/MockObjects/DocumentCategoriesTable.php new file mode 100644 index 0000000..5a38987 --- /dev/null +++ b/tests/TestCase/MockObjects/DocumentCategoriesTable.php @@ -0,0 +1,39 @@ +hasMany('Documents'); + } + + /** + * @param mixed[] $options + * @return mixed[]|int|null + */ + public function customSelector($options = []) + { + $options['nofilter'] = true; + + return $this->find('list', $options) + ->where([ + 'DocumentCategory.title LIKE' => '%T%', + ]) + ->toArray(); + } +} diff --git a/tests/TestCase/MockObjects/DocumentTestsController.php b/tests/TestCase/MockObjects/DocumentTestsController.php new file mode 100644 index 0000000..2cddf66 --- /dev/null +++ b/tests/TestCase/MockObjects/DocumentTestsController.php @@ -0,0 +1,53 @@ +getTableLocator()->get('Documents', [ + 'className' => DocumentsTable::class, + ]); + $this->Document = $Table; + $this->loadComponent('Filter.Filter'); + } + + /** + * @return void + */ + public function index() + { + } + + /** + * must override this or the tests never complete. + * + * @param string|mixed[] $url + * @param int|mixed[]|null|string $status + * @param bool $exit + * @return \Cake\Http\Response|null + */ + public function redirect($url, $status = null, $exit = true) + { + return null; + } +} diff --git a/tests/TestCase/MockObjects/Documents2Table.php b/tests/TestCase/MockObjects/Documents2Table.php new file mode 100644 index 0000000..b8eb41e --- /dev/null +++ b/tests/TestCase/MockObjects/Documents2Table.php @@ -0,0 +1,43 @@ +setAlias('Document'); + $this->setTable('documents'); + $this->belongsTo('DocumentCategories'); + $this->hasMany('Items'); + } + + /** + * @var bool + */ + public $returnValue = false; + + /** + * @param \Cake\ORM\Query $query Query. + * @param mixed[] $options + * @return mixed[]|bool + */ + public function beforeDataFilter($query, $options) + { + return $this->returnValue; + } +} diff --git a/tests/TestCase/MockObjects/Documents3Table.php b/tests/TestCase/MockObjects/Documents3Table.php new file mode 100644 index 0000000..b572d10 --- /dev/null +++ b/tests/TestCase/MockObjects/Documents3Table.php @@ -0,0 +1,56 @@ +setAlias('Document'); + $this->setTable('documents'); + $this->belongsTo('DocumentCategories'); + $this->hasMany('Items'); + } + + /** + * @var string|null + */ + public $itemToUnset = null; + + /** + * @param \Cake\ORM\Query $query Query. + * @param mixed[] $options + * @return \Cake\ORM\Query + */ + public function afterDataFilter($query, $options) + { + if (!is_string($this->itemToUnset)) { + return $query; + } + $query->clause('where')->iterateParts(function ($Comparison) { + /** @var \Cake\Database\Expression\Comparison $Comparison */ + $field = $Comparison->getField(); + if ($field == $this->itemToUnset) { + return null; + } + + return $Comparison; + }); + + return $query; + } +} diff --git a/tests/TestCase/MockObjects/DocumentsTable.php b/tests/TestCase/MockObjects/DocumentsTable.php new file mode 100644 index 0000000..15792f0 --- /dev/null +++ b/tests/TestCase/MockObjects/DocumentsTable.php @@ -0,0 +1,28 @@ +belongsTo('DocumentCategories'); + $this->hasMany('Items'); + $this->hasOne('Metadata'); + } +} diff --git a/tests/TestCase/MockObjects/ItemsTable.php b/tests/TestCase/MockObjects/ItemsTable.php new file mode 100644 index 0000000..29f8c98 --- /dev/null +++ b/tests/TestCase/MockObjects/ItemsTable.php @@ -0,0 +1,22 @@ +belongsTo('Documents'); + } +} diff --git a/tests/TestCase/MockObjects/MetadataTable.php b/tests/TestCase/MockObjects/MetadataTable.php new file mode 100644 index 0000000..be7ab50 --- /dev/null +++ b/tests/TestCase/MockObjects/MetadataTable.php @@ -0,0 +1,19 @@ +belongsTo('Documents'); + } +} diff --git a/tests/TestCase/Model/Behavior/FilteredBehaviorTest.php b/tests/TestCase/Model/Behavior/FilteredBehaviorTest.php new file mode 100644 index 0000000..d3eef19 --- /dev/null +++ b/tests/TestCase/Model/Behavior/FilteredBehaviorTest.php @@ -0,0 +1,976 @@ + + + Multi-licensed under: + MPL + LGPL + GPL + */ + +class FilteredBehaviorTest extends TestCase +{ + /** + * @var string[] + */ + public $fixtures = + [ + 'plugin.Filter.DocumentCategories', + 'plugin.Filter.Documents', + 'plugin.Filter.Items', + 'plugin.Filter.Metadata', + ]; + + /** + * @var \Filter\Test\TestCase\MockObjects\DocumentsTable|\Filter\Test\TestCase\MockObjects\Documents2Table|\Filter\Test\TestCase\MockObjects\Documents3Table + */ + public $Document = null; + + public function setUp() + { + parent::setUp(); + $Document = $this->getTableLocator()->get('Documents', ['className' => DocumentsTable::class]); + $this->assertInstanceOf(DocumentsTable::class, $Document); + $this->Document = $Document; + } + + public function tearDown() + { + parent::tearDown(); + unset($this->Document); + } + + /** + * Detach and re-attach the behavior to reset the options. + * + * @param mixed[] $options Behavior options. + * @return void + */ + protected function _reattachBehavior($options = []) + { + if ($this->Document->hasBehavior('Filtered')) { + $this->Document->removeBehavior('Filtered'); + } + $this->Document->addBehavior('Filter.Filtered', $options); + } + + /** + * Test attaching without options. + * + * @return void + */ + public function testBlankAttaching() + { + $this->Document->addBehavior('Filter.Filtered'); + $this->assertTrue($this->Document->hasBehavior('Filtered')); + } + + /** + * Test attaching with options. + * + * @return void + */ + public function testInitSettings() + { + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like'], + 'DocumentCategories.id' => ['type' => 'select', 'filterField' => 'document_category_id'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + ]; + $this->_reattachBehavior($testOptions); + + $expected = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like', 'required' => false, 'selectOptions' => []], + 'DocumentCategories.id' => ['type' => 'select', 'filterField' => 'document_category_id', 'condition' => 'like', 'required' => false, 'selectOptions' => []], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?', 'condition' => 'like', 'required' => false, 'selectOptions' => []], + ]; + $Filtered = $this->Document->getBehavior('Filtered'); + $this->assertInstanceOf(FilteredBehavior::class, $Filtered); + $this->assertEquals($expected, $Filtered->settings[$this->Document->getAlias()]); + } + + /** + * Test init settings when only a single field is given, with no extra options. + * + * @return void + */ + public function testInitSettingsSingle() + { + $testOptions = ['Documents.title']; + $this->_reattachBehavior($testOptions); + + $expected = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like', 'required' => false, 'selectOptions' => []], + ]; + $Filtered = $this->Document->getBehavior('Filtered'); + $this->assertInstanceOf(FilteredBehavior::class, $Filtered); + $this->assertEquals($expected, $Filtered->settings[$this->Document->getAlias()]); + } + + /** + * Test setting the filter values for future queries. + * + * @return void + */ + public function testSetFilterValues() + { + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'DocumentCategories.id' => ['type' => 'select', 'filterField' => 'document_category_id'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + ]; + + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['title' => 'in', 'is_private' => 0], + 'DocumentCategories' => ['id' => 1], + ]; + + $this->Document->setFilterValues($filterValues); + $actualFilterValues = $this->Document->getFilterValues(); + $this->assertEquals($filterValues, $actualFilterValues[$this->Document->getAlias()]); + } + + /** + * Test detecting an error in options - when a field is 'required' but no value is given for it. + * + * @return void + */ + public function testLoadingRequiredFieldValueMissing() + { + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'DocumentCategories.id' => ['type' => 'select', 'filterField' => 'document_category_id'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['is_private' => 0], + 'DocumentCategories' => ['id' => 1], + ]; + $this->Document->setFilterValues($filterValues); + + $this->expectException('PHPUnit\Framework\Error\Notice'); + $this->Document->find()->first(); + } + + /** + * Test filtering with conditions from current model and belongsTo model. + * + * @return void + */ + public function testFilteringBelongsTo() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'DocumentCategories.id' => ['type' => 'select'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['title' => 'in'], + 'DocumentCategories' => ['id' => 1], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * @return void + */ + public function testFilteringBelongsToTextField() + { + $testOptions = + [ + 'DocumentCategories.title' => ['type' => 'text'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'DocumentCategories' => ['title' => 'spec'], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain(['DocumentCategories']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test filtering with conditions from current model and belongsTo model, + * same as testFilteringBelongsTo() except for a change in filterField format. + * + * @return void + */ + public function testFilteringBelongsToFilterFieldTest() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'DocumentCategories.id' => ['type' => 'select', 'filterField' => 'Documents.document_category_id'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['title' => 'in'], + 'DocumentCategories' => ['id' => 1], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain(['DocumentCategories']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test various conditions for the type 'text' in filtering (less than, equal, like, etc..) + * + * @return void + */ + public function testFilteringBelongsToDifferentConditions() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => '='], + 'DocumentCategories.id' => ['type' => 'select'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['title' => 'Illegal explosives DIY'], + 'DocumentCategories' => ['id' => ''], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain(['DocumentCategories']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $testOptions = + [ + 'id' => ['type' => 'text', 'condition' => '>='], + 'created' => ['type' => 'text', 'condition' => '<='], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['id' => 3, 'created' => '2010-03-01'], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1], + ['id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0], + ['id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0], + ['id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain(['DocumentCategories']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test filtering with conditions on current model, the belongsTo model + * and hasMany model (behavior adds an INNER JOIN in query). + * + * @return void + */ + public function testFilteringBelongsToAndHasMany() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'DocumentCategories.id' => ['type' => 'select'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + 'Items.code' => ['type' => 'text'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['title' => 'in', 'is_private' => 0], + 'DocumentCategories' => ['id' => 1], + 'Items' => ['code' => '04'], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + 'document_category' => ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + 'metadata' => ['id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'], + 'items' => + [ + ['id' => 4, 'document_id' => 2, 'code' => 'The item #01'], + ['id' => 5, 'document_id' => 2, 'code' => 'The item #02'], + ['id' => 6, 'document_id' => 2, 'code' => 'The item #03'], + ['id' => 7, 'document_id' => 2, 'code' => 'The item #04'], + ], + ], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'DocumentCategories' => [ + 'fields' => ['id', 'title', 'description'], + ], + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + 'Items' => [ + 'fields' => ['id', 'document_id', 'code'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + 'document_category' => ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + 'metadata' => ['id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'], + ], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'DocumentCategories' => [ + 'fields' => ['id', 'title', 'description'], + ], + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $this->Document->associations()->remove('Item'); + $this->Document->hasMany('Item'); + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'DocumentCategories' => [ + 'fields' => ['id', 'title', 'description'], + ], + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $expected = + [ + + [ + 'id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0, + ], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test filtering with join which has some custom + * condition in the relation (both string and array). + * + * @return void + */ + public function testCustomJoinConditions() + { + $testOptions = + [ + 'Metadata.weight' => ['type' => 'text', 'condition' => '>'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Metadata' => ['weight' => 3], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + + [ + 'id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0, + 'metadata' => ['id' => 5, 'document_id' => 5, 'weight' => 4, 'size' => 790, 'permissions' => 'rw-rw-r--'], + ], + ]; + $Metadata = $this->Document->associations()->get('Metadata'); + $this->assertInstanceOf(Association::class, $Metadata); + $oldConditions = $Metadata->getConditions(); + $Metadata->setConditions(['Metadata.size > 500']); + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $Metadata->setConditions(['Metadata.size > 500']); + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $Metadata->setConditions($oldConditions); + } + + /** + * Test for any possible conflicts with Containable behavior. + * + * @return void + */ + public function testFilteringBelongsToAndHasManyWithContainable() + { + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'DocumentCategories.id' => ['type' => 'select'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + 'Items.code' => ['type' => 'text'], + ]; + + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'Documents' => ['title' => 'in', 'is_private' => 0], + 'DocumentCategories' => ['id' => 1], + 'Items' => ['code' => '04'], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + 'document_category' => ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + 'items' => + [ + ['id' => 4, 'document_id' => 2, 'code' => 'The item #01'], + ['id' => 5, 'document_id' => 2, 'code' => 'The item #02'], + ['id' => 6, 'document_id' => 2, 'code' => 'The item #03'], + ['id' => 7, 'document_id' => 2, 'code' => 'The item #04'], + ], + ], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'DocumentCategories' => [ + 'fields' => ['id', 'title', 'description'], + ], + 'Items' => [ + 'fields' => ['id', 'document_id', 'code'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + 'document_category' => ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + ], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'DocumentCategories' => [ + 'fields' => ['id', 'title', 'description'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + ], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test filtering by text input with hasOne relation. + * + * @return void + */ + public function testHasOneAndHasManyWithTextSearch() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'Items.code' => ['type' => 'text'], + 'Metadata.size' => ['type' => 'text', 'condition' => '='], + ]; + + $filterValues = + [ + 'Documents' => ['title' => 'in'], + 'Items' => ['code' => '04'], + 'Metadata' => ['size' => 45], + ]; + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + ], + ]; + + $this->_reattachBehavior($testOptions); + $this->Document->setFilterValues($filterValues); + + $result = $this->Document->find() + ->select(['id', 'title']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test filtering with Containable and hasOne Model.field. + * + * @return void + */ + public function testHasOneWithContainable() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'Items.code' => ['type' => 'text'], + 'Metadata.size' => ['type' => 'text', 'condition' => '='], + ]; + + $filterValues = + [ + 'Documents' => ['title' => 'in'], + 'Items' => ['code' => '04'], + 'Metadata' => ['size' => 45], + ]; + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + 'metadata' => ['id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'], + 'items' => + [ + ['id' => 4, 'document_id' => 2, 'code' => 'The item #01'], + ['id' => 5, 'document_id' => 2, 'code' => 'The item #02'], + ['id' => 6, 'document_id' => 2, 'code' => 'The item #03'], + ['id' => 7, 'document_id' => 2, 'code' => 'The item #04'], + ], + ], + ]; + + // containable first, filtered second + $this->_reattachBehavior($testOptions); + $this->Document->setFilterValues($filterValues); + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + 'Items' => [ + 'fields' => ['id', 'document_id', 'code'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + // filtered first, containable second + $this->_reattachBehavior($testOptions); + $this->Document->setFilterValues($filterValues); + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + 'Items' => [ + 'fields' => ['id', 'document_id', 'code'], + ], + ]) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test filtering when a join is already present in the query, + * this should prevent duplicate joins and query errors. + * + * @return void + */ + public function testJoinAlreadyPresent() + { + $testOptions = + [ + 'title' => ['type' => 'text', 'condition' => 'like', 'required' => true], + 'Items.code' => ['type' => 'text'], + 'Metadata.size' => ['type' => 'text', 'condition' => '='], + ]; + + $filterValues = + [ + 'Documents' => ['title' => 'in'], + 'Items' => ['code' => '04'], + 'Metadata' => ['size' => 45], + ]; + + $expected = + [ + + [ + 'id' => 2, + 'title' => 'Imaginary Spec', + 'document_category_id' => 1, + 'owner_id' => 1, + 'is_private' => 0, + 'document_category' => ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + 'metadata' => ['id' => 2, 'document_id' => 2, 'weight' => 0, 'size' => 45, 'permissions' => 'rw-------'], + 'items' => + [ + ['id' => 4, 'document_id' => 2, 'code' => 'The item #01'], + ['id' => 5, 'document_id' => 2, 'code' => 'The item #02'], + ['id' => 6, 'document_id' => 2, 'code' => 'The item #03'], + ['id' => 7, 'document_id' => 2, 'code' => 'The item #04'], + ], + ], + ]; + + $customJoin = []; + $customJoin[] = + [ + 'table' => 'items', + 'alias' => 'FilterItems', + 'type' => 'INNER', + 'conditions' => 'Documents.id = FilterItems.document_id', + ]; + + $this->_reattachBehavior($testOptions); + $this->Document->setFilterValues($filterValues); + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->contain([ + 'DocumentCategories' => [ + 'fields' => ['id', 'title', 'description'], + ], + 'Metadata' => [ + 'fields' => ['id', 'document_id', 'weight', 'size', 'permissions'], + ], + 'Items' => [ + 'fields' => ['id', 'document_id', 'code'], + ], + ]) + ->join($customJoin) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test the 'nofilter' query param. + * + * @return void + */ + public function testNofilterFindParam() + { + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like'], + 'DocumentCategories.id' => ['type' => 'select'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?', 'default' => 0], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'DocumentCategories' => ['id' => 2], + 'Documents' => ['title' => ''], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0], + ]; + + $result = $this->Document->find('all', ['nofilter' => true]) + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertNotEquals($expected, $result); + + $result = $this->Document->find('all', ['nofilter' => 'true']) + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test bailing out if no settings exist for the current model. + * + * @return void + */ + public function testExitWhenNoSettings() + { + $this->Document->DocumentCategories->addBehavior('Filter.Filtered'); + + $Filtered = $this->Document->DocumentCategories->behaviors()->get('Filtered'); + $this->assertFalse(isset($Filtered->settings[$this->Document->DocumentCategories->getAlias()])); + + $filterValues = + [ + 'DocumentCategories' => ['id' => 2], + ]; + $this->Document->DocumentCategories->setFilterValues($filterValues); + + $expected = + [ + ['id' => 1, 'title' => 'Testing Doc', 'description' => 'It\'s a bleeding test doc!'], + ['id' => 2, 'title' => 'Imaginary Spec', 'description' => 'This doc does not exist'], + ['id' => 3, 'title' => 'Nonexistant data', 'description' => 'This doc is probably empty'], + ['id' => 4, 'title' => 'Illegal explosives DIY', 'description' => 'Viva la revolucion!'], + ['id' => 5, 'title' => 'Father Ted', 'description' => 'Feck! Drink! Arse! Girls!'], + ]; + + $result = $this->Document->DocumentCategories->find('all', ['nofilter' => 'true']) + ->select(['id', 'title', 'description']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + + $this->Document->DocumentCategories->removeBehavior('Filtered'); + } + + /** + * Test beforeDataFilter() callback, used to cancel filtering if necessary. + * + * @return void + */ + public function testBeforeDataFilterCallbackCancel() + { + $Document = $this->getTableLocator()->get('Document2', ['className' => Documents2Table::class]); + $this->assertInstanceOf(Documents2Table::class, $Document); + $this->Document = $Document; + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like'], + 'DocumentCategories.id' => ['type' => 'select'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'DocumentCategories' => ['id' => 2], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 3, 'title' => 'Nonexistant data', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1], + ['id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0], + ['id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0], + ['id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0], + ]; + + $result = $this->Document->find() + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } + + /** + * Test afterDataFilter() callback, used to modify the conditions after + * filter conditions have been applied. + * + * @return void + */ + public function testAfterDataFilterCallbackQueryChange() + { + $Document = $this->getTableLocator()->get('Document3', ['className' => Documents3Table::class]); + $this->assertInstanceOf(Documents3Table::class, $Document); + $this->Document = $Document; + $this->Document->itemToUnset = 'FilterDocumentCategories.id'; + + $testOptions = + [ + 'Documents.title' => ['type' => 'text', 'condition' => 'like'], + 'DocumentCategories.id' => ['type' => 'select'], + 'Documents.is_private' => ['type' => 'checkbox', 'label' => 'Private?'], + ]; + $this->_reattachBehavior($testOptions); + + $filterValues = + [ + 'DocumentCategories' => ['id' => 2], + ]; + $this->Document->setFilterValues($filterValues); + + $expected = + [ + ['id' => 1, 'title' => 'Testing Doc', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 2, 'title' => 'Imaginary Spec', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 3, 'title' => 'Nonexistant data', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 0], + ['id' => 4, 'title' => 'Illegal explosives DIY', 'document_category_id' => 1, 'owner_id' => 1, 'is_private' => 1], + ['id' => 5, 'title' => 'Father Ted', 'document_category_id' => 2, 'owner_id' => 2, 'is_private' => 0], + ['id' => 6, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0], + ['id' => 7, 'title' => 'Duplicate title', 'document_category_id' => 5, 'owner_id' => 3, 'is_private' => 0], + ]; + + $result = $this->Document->find('all') + ->select(['id', 'title', 'document_category_id', 'owner_id', 'is_private']) + ->enableHydration(false) + ->toArray(); + $this->assertEquals($expected, $result); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3d134f6 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,12 @@ +