Filtering ListView by related models and additional parameters

Hello everyone!

I have the following ERD:

6716

Снимок экрана 2015-09-24 в 17.10.27.png

In Tour model following relations are defined:




class Tour extends \yii\db\ActiveRecord

{

    ...


    /**

     * @return \yii\db\ActiveQuery

     */

    public function getStartLocation()

    {

        return $this->hasOne(Location::className(), ['id' => 'start_location_id']);

    }


    /**

     * @return \yii\db\ActiveQuery

     */

    public function getTourLocations()

    {

        return $this->hasMany(TourLocation::className(), ['tour_id' => 'id']);

    }


    /**

     * @return \yii\db\ActiveQuery

     */

    public function getLocations()

    {

        return $this->hasMany(Location::className(), ['id' => 'location_id'])->viaTable('tour_location', ['tour_id' => 'id']);

    }


    /**

     * @return \yii\db\ActiveQuery

     */

    public function getTourSights()

    {

        return $this->hasMany(TourSight::className(), ['tour_id' => 'id']);

    }


    /**

     * @return \yii\db\ActiveQuery

     */

    public function getSights()

    {

        return $this->hasMany(Sight::className(), ['id' => 'sightseeing_id'])->viaTable('tour_sightseeing', ['tour_id' => 'id']);

    }


    ...

}



My task is to show the list of Tours in Listview depending on the Location or Sightseeing provided in GET request.

First of all I tried to use search model (SearchTour model is default generated by Gii for now) and this approach worked quite well for the request by tour.start_location_id only:




class TourController extends Controller

{

    ...

    

    /**

     * Displays all Tours for single location

     * @param string $slug

     * @return mixed

     */

    public function actionByLocation($slug)

    {

        $location = Location::findBySlug($slug);


        $searchModel = new SearchTour(['start_location_id' => $location->id]);

        $toursDataProvider = $searchModel->search(Yii::$app->request->queryParams);


        return $this->render('by-location', [

            'searchModel' => $searchModel,

            'dataProvider' => $toursDataProvider,

            'location' => $location,

        ]);

    }

...

}



However I can’t figure out the proper and optimal way for displaying using actionByLocation not only the tours by start_location_id, but also if the provided parameter is among the locations linked by tour_location many-to-many junction table.

Moreover in the next step I will be implementing smart filter that may provide one or more sightseeings and ListView output will also need to be adopted for this additional condition.

I started thinking about SqlDataProvider and dynamic SQL building but decided to advice with community first as the chances are high that similar task was already solved by someone of you folks.

Could anyone kindly advise how to better meet above requirements keeping the code clean and with optimal performance?

Thanks a lot in advance!

SearchModel generated by gii is meant to be a container of search parameters, and is also expected to be customized to satisfy the needs of the application’s business logic.

  1. You can add an extra attribute that you may want to use in the search:



class SearchTour extends Tour

{

    ...

    public $location_id;

    ...



  1. And then customize the "search" method with the newly added attribute:



    ...

    $query = Tour::find()->joinWith(['tourLocation'])->...


    $query->andFilterWhere(['tour_location.id' => $this->location_id]);

    ...



  1. And you can customize your form for searching using the customized SearchTour model.

So, it is quite easy to include a location name in your search parameters. Just add ‘location_name’ attribute to the search model and customize the search method in the same way.

softark, thank you very much for the prompt and detailed answer.

Could you please add some clarity on how I can customize the following two pieces of logic in SearchTour->search() method:

  1. I need to look for location_id in both sources: tour.start_location_id and tour_location.location_id.

I know that normally conditions are added by andFilterWhere, but how can I add the ‘and (tour.start_location_id = :location_id or tour_location.location_id = :location_id’ clause to the query?

  1. Similar task but for multiple Sightseeings provided I would like to add ‘and (tour_sightseeing.sightseeing_id = :sightseeing1 or tour_sightseeing.sightseeing_id = :sightseeing2 etc…)’.

Is it somehow possible?

I have figured out the proper solution in SearchTour->search() method myself:




        if (!is_null($this->location_id)) {

            $query->andFilterWhere(['or', ['start_location_id' => $this->location_id], ['tour_location.location_id' => $this->location_id]]);

        }


        if (!is_null($this->sightseeing_id)) {

            if(is_array($this->sightseeing_id)) {

                $condition = ['or'];

                foreach ($this->sightseeing_id as $sightseeing_id) {

                    $condition[] = $sightseeing_id;

                }

                $query->andFilterWhere($condition);

            }

            else {

                $query->andFilterWhere(['tour_sightseeing' => $this->sightseeing_id]);

            }

        }



Thanks a lot for good direction!