Browse Source

Data filter enhancements and docs

- Added docs.
- Moved to "data" namespace.
- Moved messages to "yii" category.
- Added Russian messages translation.
tags/2.0.13
Alexander Makarov 7 years ago committed by GitHub
parent
commit
376006a2d5
  1. 62
      docs/guide/output-data-providers.md
  2. 30
      docs/guide/output-data-widgets.md
  3. 8
      docs/guide/rest-quick-start.md
  4. 10
      docs/guide/rest-resources.md
  5. 6
      framework/data/ActiveDataFilter.php
  6. 26
      framework/data/DataFilter.php
  7. 21
      framework/i18n/I18N.php
  8. 32
      framework/messages/de/yii-rest.php
  9. 6
      framework/messages/de/yii.php
  10. 33
      framework/messages/hy/yii-rest.php
  11. 6
      framework/messages/hy/yii.php
  12. 14
      framework/messages/ru/yii.php
  13. 3
      framework/rest/IndexAction.php
  14. 4
      tests/framework/data/ActiveDataFilterTest.php
  15. 6
      tests/framework/data/DataFilterTest.php

62
docs/guide/output-data-providers.md

@ -318,9 +318,9 @@ class CsvDataProvider extends BaseDataProvider
}
return $keys;
} else {
return array_keys($models);
}
return array_keys($models);
}
/**
@ -339,3 +339,61 @@ class CsvDataProvider extends BaseDataProvider
}
}
```
## Filtering Data Providers using Data Filters <span id="filtering-data-providers-using-data-filters"></span>
While you can build conditions for active data provider manually as described in
[Filtering Data](output-data-widgets.md#filtering-data) and [Separate Filter Form](output-data-widgets.md#separate-filter-form)
sections of data widgets guide, Yii has data filters that are very useful if you need flexible filter condtions.
Data filters could be used as follows:
```php
$filter = new ActiveDataFilter([
'searchModel' => 'app\models\PostSearch'
]);
$filterCondition = null;
// You may load filters from any source. For example,
// if you prefer JSON in request body,
// use Yii::$app->request->getBodyParams() below:
if ($filter->load(\Yii::$app->request->get())) {
$filterCondition = $filter->build();
if ($filterCondition === false) {
// Serializer would get errors out of it
return $filter;
}
}
$query = Post::find();
if ($filterCondition !== null) {
$query->andWhere($filterCondition);
}
return new ActiveDataProvider([
'query' => $query,
]);
```
`PostSearch` model serves the purpose of defining which properties and values are allowed for filtering:
```php
use yii\base\Model;
class PostSearch extends Model
{
public $id;
public $title;
public function rules()
{
return [
['id', 'integer'],
['title', 'string', 'min' => 2, 'max' => 200],
];
}
}
```
Data filters are quite flexible. You may customize how conditions are built and which operators are allowed.
For details check API docs on [[\yii\data\DataFilter]].

30
docs/guide/output-data-widgets.md

@ -7,7 +7,7 @@ While the [DetailView](#detail-view) widget can be used to display data for a si
providing features like pagination, sorting and filtering.
DetailView <a name="detail-view"></a>
DetailView <span id="detail-view"></span>
----------
The [[yii\widgets\DetailView|DetailView]] widget displays the details of a single data [[yii\widgets\DetailView::$model|model]].
@ -59,7 +59,7 @@ echo DetailView::widget([
]);
```
ListView <a name="list-view"></a>
ListView <span id="list-view"></span>
--------
The [[yii\widgets\ListView|ListView]] widget is used to display data from a [data provider](output-data-providers.md).
@ -124,7 +124,7 @@ echo ListView::widget([
These are then also available as variables in the view.
GridView <a name="grid-view"></a>
GridView <span id="grid-view"></span>
--------
Data grid or [[yii\grid\GridView|GridView]] is one of the most powerful Yii widgets. It is extremely useful if you need to quickly build the admin
@ -155,7 +155,7 @@ The above code first creates a data provider and then uses GridView to display e
the data provider. The displayed table is equipped with sorting and pagination functionality out of the box.
### Grid columns
### Grid columns <span id="grid-columns"></span>
The columns of the grid table are configured in terms of [[yii\grid\Column]] classes, which are
configured in the [[yii\grid\GridView::columns|columns]] property of GridView configuration.
@ -186,7 +186,7 @@ Note that if the [[yii\grid\GridView::columns|columns]] part of the configuratio
Yii tries to show all possible columns of the data provider's model.
### Column classes
### Column classes <span id="column-classes"></span>
Grid columns could be customized by using different column classes:
@ -257,7 +257,7 @@ For configuring data columns there is also a shortcut format which is described
API documentation for [[yii\grid\GridView::columns|columns]].
#### Action column
#### Action column <span id="action-column"></span>
[[yii\grid\ActionColumn|Action column]] displays action buttons such as update or delete for each row.
@ -314,7 +314,7 @@ Available properties you can configure are:
]
```
#### Checkbox column
#### Checkbox column <span id="checkbox-column"></span>
[[yii\grid\CheckboxColumn|Checkbox column]] displays a column of checkboxes.
@ -340,7 +340,7 @@ var keys = $('#grid').yiiGridView('getSelectedRows');
// keys is an array consisting of the keys associated with the selected rows
```
#### Serial column
#### Serial column <span id="serial-column"></span>
[[yii\grid\SerialColumn|Serial column]] renders row numbers starting with `1` and going forward.
@ -355,13 +355,13 @@ echo GridView::widget([
```
### Sorting data
### Sorting data <span id="sorting-data"></span>
> Note: This section is under development.
>
> - https://github.com/yiisoft/yii2/issues/1576
### Filtering data
### Filtering data <span id="filtering-data"></span>
For filtering data, the GridView needs a [model](structure-models.md) that represents the search criteria which is
usually taken from the filter fields in the GridView table.
@ -448,7 +448,7 @@ echo GridView::widget([
]);
```
### Separate filter form
### Separate filter form <span id="separate-filter-form"></span>
Most of the time using GridView header filters is enough, but in case you need a separate filter form,
you can easily add it as well. You can create partial view `_search.php` with the following contents:
@ -526,7 +526,7 @@ And add the representative fields to the filter form:
<?= $form->field($model, 'creationTo') ?>
```
### Working with model relations
### Working with model relations <span id="working-with-model-relations"></span>
When displaying active records in a GridView you might encounter the case where you display values of related
columns such as the post author's name instead of just his `id`.
@ -619,7 +619,7 @@ $query->andFilterWhere(['LIKE', 'author.name', $this->getAttribute('author.name'
> Info: For more information on `joinWith` and the queries performed in the background, check the
> [active record docs on joining with relations](db-active-record.md#joining-with-relations).
#### Using SQL views for filtering, sorting and displaying data
#### Using SQL views for filtering, sorting and displaying data <span id="using-sql-views"></span>
There is also another approach that can be faster and more useful - SQL views. For example, if we need to show the gridview
with users and their profiles, we can do so in this way:
@ -688,7 +688,7 @@ All attributes will be working out of the box. Note that this approach has sever
`isDeleted` or others that will influence the UI, you will need to duplicate them in this class too.
### Multiple GridViews on one page
### Multiple GridViews on one page <span id="multiple-gridviews"></span>
You can use more than one GridView on a single page but some additional configuration is needed so that
they do not interfere with each other.
@ -721,7 +721,7 @@ echo GridView::widget([
]);
```
### Using GridView with Pjax
### Using GridView with Pjax <span id="using-gridview-with-pjax"></span>
The [[yii\widgets\Pjax|Pjax]] widget allows you to update a certain section of a
page instead of reloading the entire page. You can use it to update only the

8
docs/guide/rest-quick-start.md

@ -8,6 +8,7 @@ In particular, Yii supports the following features about RESTful APIs:
* Response format negotiation (supporting JSON and XML by default);
* Customizable object serialization with support for selectable output fields;
* Proper formatting of collection data and validation errors;
* Collection pagination, filtering and sorting;
* Support for [HATEOAS](http://en.wikipedia.org/wiki/HATEOAS);
* Efficient routing with proper HTTP verb check;
* Built-in support for the `OPTIONS` and `HEAD` verbs;
@ -185,7 +186,12 @@ For example, the URL `http://localhost/users?fields=id,email` will only return t
> Info: You may have noticed that the result of `http://localhost/users` includes some sensitive fields,
> such as `password_hash`, `auth_key`. You certainly do not want these to appear in your API result.
> You can and should filter out these fields as described in the [Resources](rest-resources.md) section.
> You can and should remove these fields from result as described in the [Resources](rest-resources.md) section.
Addionally, you can sort collections like `http://localhost/users?sort=email` or
`http://localhost/users?sort=-email`. Filtering collections like `http://localhost/users?filter[id]=10` or
`http://localhost/users?filter[email][like]=gmail.com` could be implemented using
data filters. See [Resources](rest-resources.md#filtering-collections) section for details.
## Summary <span id="summary"></span>

10
docs/guide/rest-resources.md

@ -244,4 +244,14 @@ will also include the pagination information by the following HTTP headers:
* `X-Pagination-Per-Page`: The number of resources in each page;
* `Link`: A set of navigational links allowing client to traverse the resources page by page.
Since collection in REST APIs is a data provider, it shares all data provider features i.e. pagination and sorting.
An example may be found in the [Quick Start](rest-quick-start.md#trying-it-out) section.
### Filtering collections <span id="filtering-collections"></span>
Since version 2.0.13 Yii provides a facility to filter collections. An example can be found in the
[Quick Start](rest-quick-start.md#trying-it-out) guide. In case you're implementing an endpoint yourself,
filtering could be done as described in
[Filtering Data Providers using Data Filters](output-data-providers.md#filtering-data-providers-using-data-filters)
section of Data Providers guide.

6
framework/rest/ActiveDataFilter.php → framework/data/ActiveDataFilter.php

@ -5,7 +5,7 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
namespace yii\data;
/**
* ActiveDataFilter allows composing a filtering condition in a format suitable for [[\yii\db\QueryInterface::where()]].
@ -96,7 +96,7 @@ class ActiveDataFilter extends DataFilter
} else {
$callback = [$this, 'buildAttributeCondition'];
}
$parts[] = call_user_func($callback, $key, $value);
$parts[] = $callback($key, $value);
}
if (!empty($parts)) {
@ -168,7 +168,7 @@ class ActiveDataFilter extends DataFilter
} else {
$callback = $method;
}
$parts[] = call_user_func($callback, $operator, $value, $attribute);
$parts[] = $callback($operator, $value, $attribute);
} else {
$parts[] = $this->buildOperatorCondition($operator, $value, $attribute);
}

26
framework/rest/DataFilter.php → framework/data/DataFilter.php

@ -5,7 +5,7 @@
* @license http://www.yiiframework.com/license/
*/
namespace yii\rest;
namespace yii\data;
use Yii;
use yii\base\InvalidConfigException;
@ -61,7 +61,7 @@ use yii\validators\StringValidator;
* You may populate it from request data via [[load()]] method:
*
* ```php
* use yii\rest\DataFilter;
* use yii\data\DataFilter;
*
* $dataFilter = new DataFilter();
* $dataFilter->load(Yii::$app->request->getBodyParams());
@ -388,12 +388,12 @@ class DataFilter extends Model
protected function defaultErrorMessages()
{
return [
'invalidFilter' => Yii::t('yii-rest', 'The format of {filter} is invalid.'),
'operatorRequireMultipleOperands' => Yii::t('yii-rest', "Operator '{operator}' requires multiple operands."),
'unknownAttribute' => Yii::t('yii-rest', "Unknown filter attribute '{attribute}'"),
'invalidAttributeValueFormat' => Yii::t('yii-rest', "Condition for '{attribute}' should be either a value or valid operator specification."),
'operatorRequireAttribute' => Yii::t('yii-rest', "Operator '{operator}' must be used with a search attribute."),
'unsupportedOperatorType' => Yii::t('yii-rest', "'{attribute}' does not support operator '{operator}'."),
'invalidFilter' => Yii::t('yii', 'The format of {filter} is invalid.'),
'operatorRequireMultipleOperands' => Yii::t('yii', "Operator '{operator}' requires multiple operands."),
'unknownAttribute' => Yii::t('yii', "Unknown filter attribute '{attribute}'"),
'invalidAttributeValueFormat' => Yii::t('yii', "Condition for '{attribute}' should be either a value or valid operator specification."),
'operatorRequireAttribute' => Yii::t('yii', "Operator '{operator}' must be used with a search attribute."),
'unsupportedOperatorType' => Yii::t('yii', "'{attribute}' does not support operator '{operator}'."),
];
}
@ -409,7 +409,7 @@ class DataFilter extends Model
if (isset($messages[$messageKey])) {
$message = $messages[$messageKey];
} else {
$message = Yii::t('yii-rest', 'The format of {filter} is invalid.');
$message = Yii::t('yii', 'The format of {filter} is invalid.');
}
$params = array_merge(
@ -756,9 +756,9 @@ class DataFilter extends Model
{
if ($name === $this->filterAttributeName) {
return $this->getFilter();
} else {
return parent::__get($name);
}
return parent::__get($name);
}
/**
@ -780,9 +780,9 @@ class DataFilter extends Model
{
if ($name === $this->filterAttributeName) {
return $this->getFilter() !== null;
} else {
return parent::__isset($name);
}
return parent::__isset($name);
}
/**

21
framework/i18n/I18N.php

@ -55,21 +55,12 @@ class I18N extends Component
public function init()
{
parent::init();
if (!isset($this->translations['yii*'])) {
if (!isset($this->translations['yii'])) {
$this->translations['yii'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US',
'basePath' => '@yii/messages',
];
}
if (!isset($this->translations['yii-*']) && !isset($this->translations['yii-rest'])) {
$this->translations['yii-rest'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US',
'basePath' => '@yii/messages',
];
}
if (!isset($this->translations['yii']) && !isset($this->translations['yii*'])) {
$this->translations['yii'] = [
'class' => 'yii\i18n\PhpMessageSource',
'sourceLanguage' => 'en-US',
'basePath' => '@yii/messages',
];
}
if (!isset($this->translations['app']) && !isset($this->translations['app*'])) {

32
framework/messages/de/yii-rest.php

@ -1,32 +0,0 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* Message translations.
*
* This file is automatically generated by 'yii message/extract' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* Message string can be used with plural forms format. Check i18n section
* of the guide for details.
*
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
'\'{attribute}\' does not support operator \'{operator}\'.' => '\'{attribute}\' unterstützt den Operator \'{operator}\' nicht.',
'Condition for \'{attribute}\' should be either a value or valid operator specification.' => 'Die Bedingung für \'{attribute}\' muss entweder ein Wert oder ein Operatorvergleich sein.',
'Operator \'{operator}\' must be used with a search attribute.' => 'Der Operator \'{operator}\' muss zusammen mit einem Such-Attribut verwendet werden.',
'Operator \'{operator}\' requires multiple operands.' => 'Der Operator \'{operator}\' erwartet mehrere Operanden.',
'The format of {filter} is invalid.' => 'Das Format von {filter} ist ungültig.',
'Unknown filter attribute \'{attribute}\'' => 'Unbekanntes Filter-Attribut \'{attribute}\'',
];

6
framework/messages/de/yii.php

@ -135,4 +135,10 @@ return [
'{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} Petabyte',
'{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} TebiByte',
'{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} Terabyte',
'\'{attribute}\' does not support operator \'{operator}\'.' => '\'{attribute}\' unterstützt den Operator \'{operator}\' nicht.',
'Condition for \'{attribute}\' should be either a value or valid operator specification.' => 'Die Bedingung für \'{attribute}\' muss entweder ein Wert oder ein Operatorvergleich sein.',
'Operator \'{operator}\' must be used with a search attribute.' => 'Der Operator \'{operator}\' muss zusammen mit einem Such-Attribut verwendet werden.',
'Operator \'{operator}\' requires multiple operands.' => 'Der Operator \'{operator}\' erwartet mehrere Operanden.',
'The format of {filter} is invalid.' => 'Das Format von {filter} ist ungültig.',
'Unknown filter attribute \'{attribute}\'' => 'Unbekanntes Filter-Attribut \'{attribute}\'',
];

33
framework/messages/hy/yii-rest.php

@ -1,33 +0,0 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
/**
* Message translations for Armenian (հայերեն) language.
* @author Gevorg Mansuryan <gevorgmansuryan@gmail.com>
*
* This file is automatically generated by 'yii message/extract' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* Message string can be used with plural forms format. Check i18n section
* of the guide for details.
*
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
'\'{attribute}\' does not support operator \'{operator}\'.' => '«{attribute}»-ը չի սպասարկում «{operator}» օպերատորը։',
'Condition for \'{attribute}\' should be either a value or valid operator specification.' => '«{attribute}»-ի համար պետք է լինի արժեք կամ գործող օպերատորի հստակեցում:',
'Operator \'{operator}\' must be used with a search attribute.' => '«{operator}» օպերատորը պետք է օգտագործվի որոնման ատրիբուտի հետ միասին:',
'Operator \'{operator}\' requires multiple operands.' => '«{operator}» օպերատորը պահանջում բազմակի օպերանդներ։',
'The format of {filter} is invalid.' => '{filter}-ի ֆորմատը անվավեր է։',
'Unknown filter attribute \'{attribute}\'' => 'Անհայտ ֆիլտրի ատրիբուտ՝ «{attribute}»։',
];

6
framework/messages/hy/yii.php

@ -136,4 +136,10 @@ return [
'{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} {n, plural, =1{պետաբայթ} other{պետաբայթ}}',
'{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} {n, plural, =1{տեբիբայթ} other{տեբիբայթ}}',
'{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} {n, plural, =1{տերաբայթ} other{տերաբայթ}}',
'\'{attribute}\' does not support operator \'{operator}\'.' => '«{attribute}»-ը չի սպասարկում «{operator}» օպերատորը։',
'Condition for \'{attribute}\' should be either a value or valid operator specification.' => '«{attribute}»-ի համար պետք է լինի արժեք կամ գործող օպերատորի հստակեցում:',
'Operator \'{operator}\' must be used with a search attribute.' => '«{operator}» օպերատորը պետք է օգտագործվի որոնման ատրիբուտի հետ միասին:',
'Operator \'{operator}\' requires multiple operands.' => '«{operator}» օպերատորը պահանջում բազմակի օպերանդներ։',
'The format of {filter} is invalid.' => '{filter}-ի ֆորմատը անվավեր է։',
'Unknown filter attribute \'{attribute}\'' => 'Անհայտ ֆիլտրի ատրիբուտ՝ «{attribute}»։',
];

14
framework/messages/ru/yii.php

@ -23,11 +23,14 @@
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
'Powered by {yii}' => 'Работает на {yii}',
'Unknown alias: -{name}' => 'Неизвестный псевдоним: -{name}',
'Yii Framework' => 'Yii Framework',
'(not set)' => '(не задано)',
'\'{attribute}\' does not support operator \'{operator}\'.' => '"{attribute}" не поддерживает оператор "{operator}".',
'Condition for \'{attribute}\' should be either a value or valid operator specification.' => 'Условие для "{attribute}" должно быть или значением или верной спецификацией оператора.',
'Operator \'{operator}\' must be used with a search attribute.' => 'Оператор "{operator}" должен использоваться через атрибут поиска.',
'Operator \'{operator}\' requires multiple operands.' => 'Оператор "{operator}" требует несколько операндов.',
'The format of {filter} is invalid.' => 'Формат фильтра {filter} не верен.',
'Unknown filter attribute \'{attribute}\'' => 'Неизвестный атрибут фильтра "{attribute}"',
' and ' => ' и ',
'(not set)' => '(не задано)',
'An internal server error occurred.' => 'Возникла внутренняя ошибка сервера.',
'Are you sure you want to delete this item?' => 'Вы уверены, что хотите удалить этот элемент?',
'Delete' => 'Удалить',
@ -45,6 +48,7 @@ return [
'Page not found.' => 'Страница не найдена.',
'Please fix the following errors:' => 'Исправьте следующие ошибки:',
'Please upload a file.' => 'Загрузите файл.',
'Powered by {yii}' => 'Работает на {yii}',
'Showing <b>{begin, number}-{end, number}</b> of <b>{totalCount, number}</b> {totalCount, plural, one{item} other{items}}.' => 'Показаны записи <b>{begin, number}-{end, number}</b> из <b>{totalCount, number}</b>.',
'The combination {values} of {attributes} has already been taken.' => 'Комбинация {values} параметров {attributes} уже существует.',
'The file "{file}" is not an image.' => 'Файл «{file}» не является изображением.',
@ -59,10 +63,12 @@ return [
'The verification code is incorrect.' => 'Неправильный проверочный код.',
'Total <b>{count, number}</b> {count, plural, one{item} other{items}}.' => 'Всего <b>{count, number}</b> {count, plural, one{запись} few{записи} many{записей} other{записи}}.',
'Unable to verify your data submission.' => 'Не удалось проверить переданные данные.',
'Unknown alias: -{name}' => 'Неизвестный псевдоним: -{name}',
'Unknown option: --{name}' => 'Неизвестная опция: --{name}',
'Update' => 'Редактировать',
'View' => 'Просмотр',
'Yes' => 'Да',
'Yii Framework' => 'Yii Framework',
'You are not allowed to perform this action.' => 'Вам не разрешено производить данное действие.',
'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Вы не можете загружать более {limit, number} {limit, plural, one{файла} few{файлов} many{файлов} other{файла}}.',
'in {delta, plural, =1{a day} other{# days}}' => 'через {delta, plural, =1{день} one{# день} few{# дня} many{# дней} other{# дня}}',

3
framework/rest/IndexAction.php

@ -9,6 +9,7 @@ namespace yii\rest;
use Yii;
use yii\data\ActiveDataProvider;
use yii\data\DataFilter;
/**
* IndexAction implements the API endpoint for listing multiple models.
@ -51,7 +52,7 @@ class IndexAction extends Action
*
* ```php
* [
* 'class' => 'yii\rest\ActiveDataFilter',
* 'class' => 'yii\data\ActiveDataFilter',
* 'searchModel' => function () {
* return (new \yii\base\DynamicModel(['id' => null, 'name' => null, 'price' => null]))
* ->addRule('id', 'integer')

4
tests/framework/rest/ActiveDataFilterTest.php → tests/framework/data/ActiveDataFilterTest.php

@ -1,9 +1,9 @@
<?php
namespace yiiunit\framework\rest;
namespace yiiunit\framework\data;
use yii\base\DynamicModel;
use yii\rest\ActiveDataFilter;
use yii\data\ActiveDataFilter;
use yiiunit\TestCase;
class ActiveDataFilterTest extends TestCase

6
tests/framework/rest/DataFilterTest.php → tests/framework/data/DataFilterTest.php

@ -1,14 +1,14 @@
<?php
namespace yiiunit\framework\rest;
namespace yiiunit\framework\data;
use yii\base\DynamicModel;
use yii\rest\DataFilter;
use yii\data\DataFilter;
use yiiunit\data\base\Singer;
use yiiunit\TestCase;
/**
* @group rest
* @group data
*/
class DataFilterTest extends TestCase
{
Loading…
Cancel
Save