How to create a relational search form

This is my problem. I have three models namely Equipment, properties and maintenance. Equipment has many properties and maintenance while properties and maintenance belongs to equipment. By my main problem is that I want to have a page where users can search for equipment using a property or maintenance detail from the search form.

This is my Equipment model relations

public function relations()

{


	// NOTE: you may need to adjust the relation name and the related


	// class name for the relations automatically generated below.


	return array(


		


		


		'maintenance' => array(self::HAS_MANY, 'Maintenance', 'equipment_id'),


		'property' => array(self::HAS_MANY, 'property', 'equipment_id'),


	);


}

Property model relation

public function relations()

{


	// NOTE: you may need to adjust the relation name and the related


	// class name for the relations automatically generated below.


	return array(


		'equipment' => array(self::BELONGS_TO, 'Equipment', 'equipment_id'),


	);


}

Maintenance model relations

public function relations()

{


	// NOTE: you may need to adjust the relation name and the related


	// class name for the relations automatically generated below.


	return array(


		'equipment' => array(self::BELONGS_TO, 'Equipment', 'equipment_id'),


		


	);


}

I have created the search from on a new page

<?php

/* @var $this SiteController */

/* @var $model ContactForm */

/* @var $form CActiveForm */

$this->pageTitle=Yii::app()->name . ’ - Search’;

$this->breadcrumbs=array(

'Search',

);

?>

<div id="equipment_search">

<h4>Search Equipment</h4>

<?php echo CHtml::beginForm(array(‘search’), ‘get’); ?>

<?php echo CHtml::label(‘Title’, ‘title’); ?>

<?php echo CHtml::textField(‘title’); ?><br/><br/>

<h4>Properties</h4>

<?php echo CHtml::label(‘Property Title’, ‘title’); ?>

<?php echo CHtml::textField(‘title’); ?><br/><br/>

<?php echo CHtml::label(‘Property Value’, ‘value’); ?>

<?php echo CHtml::textField(‘value’); ?><br/><br/>

<?php echo CHtml::submitButton(‘Go!’); ?>

<?php echo CHtml::endForm(); ?>

</div><!-- form -->

</div>

My problem now is how do i query the database to fetch equipment where property is the property in the search property title and value input box.

Thanks

Check out the RelatedSearchBehavior extension.

With the extension, define ‘related attributes’ which are virtual attributes that look as if they are attributes of the model itself. (Defining the attributes also requires you to declare them in ‘rules()’.

Then in your search for, use these virtual attributes as you do for the other attributes.

At first, you may need to spend some time to understand (the use of) the extension, but you will be rewarded by the gain in time afterwards.

There is a demo with the extension that can help understand the use.

I checked out the extension,but i found out that it works with a HAS ONE relation displaying related on a grid view. My models are related with a HAS MANY which means that I can have many properties for an equipment and that makes it difficult displaying all the properties in a grid view. My example goes like this. I can have cars(equipment) like Toyota in different locations(Properties). How do I search from the equipment page for a toyota in a specific location.

The solution is to find the table with the most entries.

Usually there is one table that you can start from. In the demo the starting table is the invoice line, which is the starting point for every combination.

In your case there would be exactly one car (with a VIN number) which is in a given location which has certain equipments, so the car table is the one in which you search.

A car can have only one location, only one equipment or one possible list of equipments.

Thanks for your help. But please can you help me write a code that query a database say select from Equipment_id from propertytable where property title equals property title from the search from.

Hi

I just see that you did something like me: one property table where each equipment has many properties and I guess that you want to see these properties as columns…

I have done that too, but the complexity of the solution goes beyond what I can give on a forum.

I’ll give indications about what you can do, just for the search:

My base model sets up the RelatedSearchBehavior, the next code adds the properties. All used properties are defined in a EntityPropertyEnum.

In your case you would have a ‘const title;’ in EntityPropertyEnum.




	public function behaviors() {

    	$behaviors=parent::behaviors();

    	$properties=(new ReflectionClass('EntityPropertyEnum'))->getConstants();

    	foreach($properties as $property) {

        	$behaviors['relatedsearch']['relations'][$property]=$property."property.value";

    	}

    	return $behaviors;

	}



For the attributes, I did not implement a loop, I have listed the attributes explicitally. I could do it with a loop too.




    public function rules() {

         return array(

               ...

               array(EntityPropertyEnum::TITLE,'safe','on'=>'search'),

         );

   }



The next issue that you will encounter is that you can use a relation only once in a search request, so a solution is to have one relation for each property, or, to have a generic method to add the search condition.

So I have a generic method to add the search condition, my ‘search()’ looks like this:


    public function search() {

        $criteria = new CDbCriteria;

        $t=$this->getTableAlias(false,false);

        $ds=$this->getDbConnection()->getSchema();


        $columns=$this->getMetaData()->columns;

        $relations=$this->relations;

        foreach($this->getSafeAttributeNames() as $attribute) {

            $value=$this->{$attribute};

            if("$value" !== "") {

                if(isset($columns[$attribute])) {

                    $criteria->compare($ds->quoteColumnName("$t.$attribute"),$value,true);

                } else if(!isset($relations[$attribute])) {

                    /** Not a related search item -> suppose property */

                    $this->compareProperty($attribute, $value,true);

                }

            }

        }


        return $this->relatedSearch(

                $criteria

                //,array('sort'=>array('defaultOrder'=>$this->getTableAlias(true,false).'.entity_id DESC'))

        );

    }



You can see that the ‘compareProperty’ method is called for properties that do not have a relation.

The compare property implements a compare method, for the properties that are in the related table. This table, like yours, has many properties for one field.

It first finds the compare operator.

It then checks if a relation for the property exists - this relation is the property name followed by ‘property’. If yes, it uses the relation.

If not, it uses the general ‘_properties’ relation which can be used only once in the search and which in my case should not be used except for exceptional properties.


    /**

 	* Scope to constrain the entity to have a specific property value "like" given value.

 	*

 	* @param string $property

 	* @param string $value

 	*/

    public function compareProperty($property,$value, $partialMatch=false, $operator='AND', $escape=true)

    {

        if(is_array($value))

        {

            throw new CException('Array not supported for compareProperty');

        }

        else

            $value="$value";


        if(preg_match('/^(?:\s*(<>|<=|>=|<|>|=))?(.*)$/',$value,$matches))

        {

            $value=$matches[2];

            $op=$matches[1];

        }

        else

            $op='';


        if($value==='')

            return;




        $alias=':pr'.$this->aliasId();

        if($partialMatch)

        {

            $like='LIKE';

            if($op==='<>') $like='NOT LIKE';

            if($escape)

                $value='%'.strtr($value,array('%'=>'\%', '_'=>'\_', '\\'=>'\\\\')).'%';

            $op="$like";

        }

        elseif($op==='') {

            $op='=';

        }


        $relationAlias='po'.$this->aliasId();


        // My relations table has the name of the property in 'property_identifier' and the property in 'value'.

        $property_iden=$this->getDbConnection()->getSchema()->quoteColumnName("$relationAlias.property_identifier");

        $property_value=$this->getDbConnection()->getSchema()->quoteColumnName("$relationAlias.value");


        $relations=$this->relations();

        $relation=$property."property";

        if(isset($relations[$relation])) {

            $this->getDbCriteria()->mergeWith(

                    array(

                            'with'=>array(

                                    $relation=>array(

                                        'select'=>array(),

                                        'alias'=>$relationAlias,

                                        'condition'=>"$property_value $op $alias",

                                        'together'=>'true',

                                        'params'=>array($alias=>$value),

                                    ),

                            ),

                    )

            );

        } else {


            $this->getDbCriteria()->mergeWith(

                array(

                        'with'=>array(

                                '_properties'=>array(

                                        'select'=>array(),

                                        'alias'=>$relationAlias,

                                        'condition'=>"$property_iden=\"$property\" AND $property_value $op $alias",

                                        'together'=>'true',

                                        'params'=>array($alias=>$value),

                                ),

                        ),

                )

            );

        }

        return $this;

    }



The above is "advanced" and requires RelatedSearchBehavior in order for the properties in the properties table to be known.

If the properties are to be listed as rows in the table, things are easier.

If you want to see the properties as rows in a table, the above is simplified: you may not need to have one relation for each property, the search() function is simplier.

Further, to stick with your example, I remove the loops that make things generic.




	public function behaviors() {

    	$behaviors=parent::behaviors();  // Defines relatedsearch, ... - see extension


        $behaviors['relatedsearch']['relations']['title']="titleproperty.value";  // titleproperty is the relation for title.

    	return $behaviors;

	}



The above supposes that the title can be found through the ‘titleproperty’ in the column ‘value’.

My default search() implementation does ‘introspection’ to find the columns and add the conditions.

The related search method takes care of the comparison of virtual fields.


 

    public function rules() {

         return array(...

             array('title','safe','on'=>'search'), // Needed for filtering

        );

    }


    public function search() {

        $criteria = new CDbCriteria;

        $t=$this->getTableAlias(false,false);

        $ds=$this->getDbConnection()->getSchema();


        $columns=$this->getMetaData()->columns;

        foreach($this->getSafeAttributeNames() as $attribute) {

            $value=$this->{$attribute};

            if("$value" !== "") {

                if(isset($columns[$attribute])) {

                    $criteria->compare($ds->quoteColumnName("$t.$attribute"),$value,!$columns[$attribute]->isForeignKey);

                }

            }

        }

        return $this->relatedSearch(

                $criteria

                //,array('sort'=>array('defaultOrder'=>$this->getTableAlias(true,false).'.entity_id DESC'))

        );

    }

The above is "advanced" and requires RelatedSearchBehavior in order for the properties in the properties table to be known.

If the properties are to be listed as rows in the table, things are easier.

Thanks so much. I will try it and give feed back.

http://www.yiiframework.com/wiki/281/searching-and-sorting-by-related-model-in-cgridview/ Try this link.

Thanks for your help. I used the process and it worked for me dislaying the related category and i can search using them but I can’t search by the attributes of the main model. It keeps trowing exceptional error. Below is my code.

Subcategory Model

<?php

/**

  • This is the model class for table "{{subcategory}}".

  • The followings are the available columns in table ‘{{subcategory}}’:

  • @property integer $id

  • @property string $title

  • @property string $description

  • @property integer $category_id

*/

class Subcategory extends CActiveRecord

{

public &#036;category_search;


/**


 * @return string the associated database table name


 */


public function tableName()


{


	return '{{subcategory}}';


}





/**


 * @return array validation rules for model attributes.


 */


public function rules()


{


	// NOTE: you should only define rules for those attributes that


	// will receive user inputs.


	return array(


		array('title', 'required'),


		array('category_id', 'numerical', 'integerOnly'=&gt;true),


		array('title', 'length', 'max'=&gt;256),


		array('description', 'length', 'max'=&gt;2000),


		// The following rule is used by search().


		// @todo Please remove those attributes that should not be searched.


		array('id, title, description, category_id, category_search', 'safe', 'on'=&gt;'search'),


	);


}





/**


 * @return array relational rules.


 */


public function relations()


{


	// NOTE: you may need to adjust the relation name and the related


	// class name for the relations automatically generated below.


	return array(


		'category' =&gt; array(self::BELONGS_TO, 'Category', 'category_id'),


		


		


	);


}





/**


 * @return array customized attribute labels (name=&gt;label)


 */


public function attributeLabels()


{


	return array(


		'id' =&gt; 'ID',


		'title' =&gt; 'Title',


		'description' =&gt; 'Description',


		'category_id' =&gt; 'Category',


	);


}





/**


 * Retrieves a list of models based on the current search/filter conditions.


 *


 * Typical usecase:


 * - Initialize the model fields with values from filter form.


 * - Execute this method to get CActiveDataProvider instance which will filter


 * models according to data in model fields.


 * - Pass data provider to CGridView, CListView or any similar widget.


 *


 * @return CActiveDataProvider the data provider that can return the models


 * based on the search/filter conditions.


 */


public function search()


{


	// @todo Please modify the following code to remove attributes that should not be searched.





	&#036;criteria=new CDbCriteria;


	&#036;criteria-&gt;with = array( 'category' );





	&#036;criteria-&gt;compare('id',&#036;this-&gt;id);


	&#036;criteria-&gt;compare('title',&#036;this-&gt;title,true);


	&#036;criteria-&gt;compare('description',&#036;this-&gt;description,true);


	&#036;criteria-&gt;compare('category_id',&#036;this-&gt;category_id);


	&#036;criteria-&gt;compare( 'category.title', &#036;this-&gt;category_search, true );





	return new CActiveDataProvider( 'Subcategory', array(


'criteria'=&gt;&#036;criteria,


'sort'=&gt;array(


    'attributes'=&gt;array(


        'category_search'=&gt;array(


            'asc'=&gt;'category.title',


            'desc'=&gt;'category.title DESC',


        ),


        '*',


    ),


),

));

}





/**


 * Returns the static model of the specified AR class.


 * Please note that you should have this exact method in all your CActiveRecord descendants&#33;


 * @param string &#036;className active record class name.


 * @return Subcategory the static model class


 */


public static function model(&#036;className=__CLASS__)


{


	return parent::model(&#036;className);


}

}

Subcategory Admin Page

<?php

/* @var $this SubcategoryController */

/* @var $model Subcategory */

$this->breadcrumbs=array(

'Subcategories'=&gt;array('index'),


'Manage',

);

$this->menu=array(

array('label'=&gt;'List Subcategory', 'url'=&gt;array('index')),


array('label'=&gt;'Create Subcategory', 'url'=&gt;array('create')),

);

Yii::app()->clientScript->registerScript(‘search’, "

$(’.search-button’).click(function(){

&#036;('.search-form').toggle();


return false;

});

$(’.search-form form’).submit(function(){

&#036;('#subcategory-grid').yiiGridView('update', {


	data: &#036;(this).serialize()


});


return false;

});

");

?>

<h1>Manage Subcategories</h1>

<p>

You may optionally enter a comparison operator (<b>&lt;</b>, <b>&lt;=</b>, <b>&gt;</b>, <b>&gt;=</b>, <b>&lt;&gt;</b>

or <b>=</b>) at the beginning of each of your search values to specify how the comparison should be done.

</p>

<?php echo CHtml::link(‘Advanced Search’,’#’,array(‘class’=>‘search-button’)); ?>

<div class="search-form" style="display:none">

<?php $this->renderPartial(’_search’,array(

'model'=&gt;&#036;model,

)); ?>

</div><!-- search-form -->

<?php $this->widget(‘zii.widgets.grid.CGridView’, array(

'id'=&gt;'subcategory-grid',


'dataProvider'=&gt;&#036;model-&gt;search(),


'filter'=&gt;&#036;model,


'columns'=&gt;array(


	


	'title',


	'description',


	// array(

// ‘header’=>‘Category’,

// ‘value’=>‘CHtml::encode($data->category->title)’,//This will use the relationship and get all the details of the paticular user from users table

// ),

	 // array( 'name'=&gt;'category_search', 'value'=&gt;'&#036;data-&gt;category-&gt;title' ),


    //post strictly required user but in any case that model has not required user(or another relation) you should replace with it


    array( 'name'=&gt;'category_search', 'value'=&gt;'&#036;data-&gt;category ? &#036;data-&gt;category-&gt;title: &quot;-&quot;' ),


	


	array(


		'class'=&gt;'CButtonColumn',


	),


),

)); ?>

Searching by title and description pop up an error. How do I fix this.

At least title appears in multiple tables, so you need to disambiguate.




        $t=$this->getTableAlias(false,false);

        $ds=$this->getDbConnection()->getSchema();


                    

     	$attribute='title';

     	$criteria->compare($ds->quoteColumnName("$t.$attribute"),...);




Please am i putting this in the model?

You put this kind of code in the search() function - in replacement of the compare() call.