ActiveDataFilter usage

This is one of the most undocumented feature of the framework, and i just can’t seem to understand how to use it properly.

Currently i have a simple usage like:


$filter = new ActiveDataFilter([
    'searchModel' => EventTagSearch::class,
]);

$filterCondition = null;
if ($filter->load(request()->get())) {
    $filterCondition = $filter->build();
}

where the search model looks like:

class EventTagSearch extends Model
{
    /**
     * @var string
     */
    public string $name = '';

    /**
     * @var string
     */
    public string $description = '';

    /**
     * @var string
     */
    public string $status_id = '';

	/**
	 * @var array 
	 */
    public array $category_id = [];

    /**
     * @return array
     */
    public function rules()
    {
        return [
            [['name', 'description'], 'string', 'min' => 1],
	        [['status_id'], 'integer', 'min' => 1],
	        [['category_id'], 'each', 'rule' => ['integer', 'min' => 1]],
        ];
    }
}

So if I query the controller like:
?filter[category_id][in][]=6&&filter[category_id][in][]=9
First error that i get is a type error:
Typed property EventTagSearch::$category_id must be array, string used
Why is that? I am providing an array in the query string after all, right?
Is it because the added in condition? Surely, even like that, it is still an array, isn’t it? I just cannot figure the reason out.

So next thing, let’s forget type safety for a moment and transform the attribute into:

/**
	 * @var array 
	 */
    public $category_id = [];

Now i get these errors:

Array
(
    [filter] => Array
        (
            [0] => Category Id is invalid.
            [1] => Category Id is invalid.
        )

)

Okay, this must be because of the validation rules, because now i am sure the in part from the array plays a role. So i can write a custom validation rule for this.
But as a general rule, attributes that accept operators should be specially handled, is that correct?
In the docs, at https://www.yiiframework.com/doc/api/2.0/yii-data-datafilter we can see an example of validation like:

public function rules()
    {
        return [
            [['id', 'name'], 'trim'],
            ['id', 'integer'],
            ['name', 'string'],
        ];
    }

But this will only work for when the filter is something like:
?filter[name]=john
but when the filter will be set to:
?filter[name][like]=john
it will fail.
Is this expected behavior? Why isn’t there a warning related to the way this works?

Continuing with this, let’s say i also modify my search model rules so that category_id is safe now so anything can be passed to it.
So now accessing ?filter[category_id][in][]=6&&filter[category_id][in][]=9 produces a filter like:

Array
(
    [0] => IN
    [1] => category_id
    [2] => Array
        (
            [0] => 6
            [1] => 9
        )

)

Which is perfect valid.
BUT, what do we do if we need to use this filter to query a related table?
There is absolutely no documentation showing us how to do this, we just don’t know what is the correct way to do this.
If we specify attributeMap like:

'attributeMap' => [
	'category_id'	=> 'categories.id'
],

It somehow works but not entirely, the validation complaints that categories.id is not a valid attribute, because of the dot, obviously.
So we ended up doing something stupid like:

if (is_array($filterCondition) && isset($filterCondition['category_id'])) {
        	$filterCondition['categories.id'] = $filterCondition['category_id'];
        	unset($filterCondition['category_id']);
        }

and in this way, joining the relation table will work.
Is this how it suppose to work? What is the correct way to query a related table using this feature?

@samdark - can you please shed some light related to the correct usage of this feature?
Thank you!

Bump :frowning:
Anyone any experience with this feature?

Too many questions at once :slight_smile:

Would you please dump data that you’re passing to load()?

Yes. That’s expected to fail if the validator is for a string but you pass an array to the model attribute.
I’m fine about adding a warning but I’m not sure what to write there. Any suggestios?

https://www.yiiframework.com/doc/guide/2.0/en/output-data-widgets#working-with-model-relations should be the same for filters.

Sorry, i haven’t posted in years, so i want to catch-up, lol :slight_smile:

I pasted above the example. Right now i don’t have it at hand because i just moved forward with this, sorry for that.

I think in the end this is not bad, we need to make sure the validation rules keep this working properly, so i think we can ignore this for now.

It does yes, but if you then check the errors on the filter class, you can see it does contain some errors related to the attributes. However, even with these errors, the build method works just fine and generates correct filters, so i don’t know what to make of it.

To sum this up, this is how it looks like right now for me:

public function prepareDataProvider(): ActiveDataProvider
{
    $filter = new ActiveDataFilter([
        'searchModel' => EventTagSearch::class,
        'attributeMap' => [
            'name'          => 't.name',
            'slug'          => 't.slug',
            'description'   => 't.description',
            'status_id'     => 't.status_id',
            'category_id'   => 'categories.id',
        ],
    ]);

    $filterCondition = null;
    if ($filter->load(request()->get())) {
        $filterCondition = $filter->build();
    }

    $query = EventTag::find()->alias('t');
    if (!user()->can('ADMIN')) {
        $query->andWhere(['t.status_id' => Status::ACTIVE]);
    }
    $query->joinWith('categories as categories', true);

    if ($filterCondition !== null) {
        $query->andWhere($filterCondition);
    }

    return new ActiveDataProvider([
        'query' => $query,
    ]);
}

And it all works just fine with the above exception i pointed out above, in that if i do $filter->getErrors(), i do get some errors, but the filters are created just fine.
I don’t know if i explained this properly, hopefully i did :slight_smile:

Here is an update, if you need to use relations, you need to extend your search model from the actual model you search into, not from yii\base\Model as specified in docs, which actually makes sense if you think about it, but given the docs didn’t say this, i never thought about it.

So in my case:

class EventTagSearch extends EventTag
{
	/**
	 * @return array
	 */
	public function rules()
	{
		return [
			[['name', 'slug', 'description', 'status_id', 'categories.id'], 'safe'],
		];
	}

	/**
	 * @return array
	 */
	public function scenarios()
	{
		return Model::scenarios();
	}

	/**
	 * @return array
	 */
	public function attributes()
	{
		return array_merge(parent::attributes(), ['categories.id']);
	}
}

This is because otherwise Yii will not recognize the categories.id attribute.
And now the controller code will be just:

public function prepareDataProvider(): ActiveDataProvider
{
    $filter = new ActiveDataFilter([
        'searchModel' => EventTagSearch::class,
        'attributeMap' => [
            'name'          => 't.name',
            'slug'          => 't.slug',
            'description'   => 't.description',
            'status_id'     => 't.status_id',
        ],
    ]);
	
    $filterCondition = null;
    if ($filter->load(request()->get())) {
        $filterCondition = $filter->build();
    }
    
    $query = EventTag::find()->alias('t');
    if (!user()->can('ADMIN')) {
        $query->andWhere(['t.status_id' => Status::ACTIVE]);
    }
    $query->joinWith('categories as categories', true);

    if ($filterCondition !== null) {
        $query->andWhere($filterCondition);
    }
    
    return new ActiveDataProvider([
        'query' => $query,
    ]);
}

Where we still need the attributeMap definition otherwise we can’t properly join.

@samdark - can you please confirm this is the right way to go about this?

The only issue with the above:

$filter = new ActiveDataFilter([
            'searchModel' => EventTagSearch::class,
            'attributeMap' => [
                'name'          => 't.name',
                'slug'          => 't.slug',
                'description'   => 't.description',
                'status_id'     => 't.status_id',
            ],
        ]);
		
        $filterCondition = null;
        if ($filter->load(request()->get())) {
            $filterCondition = $filter->build();
        }
        
        dd($filter->getErrors());

is that Model::getErrors say: 'Unknown filter attribute \"t.name\"' so in order to avoid this, i have to list t.name in rules and attributes of the search model.

Is this correct/expected?

Yes.

Yes, that is correct.

1 Like

Thank you, issues solved then :wink: