mdeweerd/yii-relatedsearchbehavior

Ideas on re-using (fields of) 'core entities' in all related entities and views

Opened this issue · 1 comments

Hey,

This is not really something very specific to your great extension but I think this still can be a good place to ask :)
(if you find it inappropiate or just don't want to discuss it here, close it without comment!)

What do you think is a good or best practice to re-use fields of 'core entities' in all sorts of gridviews?

I make use of the EColumns extension and have a lot entities in my application that relate in some way (direct or 'through') to, for example, the following 'core entities':

  • customer
  • product

Both of these are just an example and have quite some fields (>20), including quite some 'BELONGS_TO' fields.
Examples of entitities that have to show information of both of above 'core entities':

  • order items
  • reminders
  • notes

Depending on the goal of the user they configure the to be shown fields in the gridView through the EColumns widget.
Sometimes they need the productname and a barcode, other times only a stock or location, etc.
But because of the variety of processes all fields should be available everywhere :)

Repeating searchable/showable fields in the gridviews or relatedSearchBehavior in all dependent objects is 'not the way to go'.

My first idea for this is a function that could be created in a base 'active record' class that will return the searchable attributes,
that can also be used in:

  • a function that will return the default datacolumn configuration for those attributes.
  • a function that returns an array to be passed to relatedSearchBehavior to add searchable attributes prefixed with the relation name

How do/would you cope with this?

Currently relatedsearchbehavior allows us to reuse field definitions "recursively". If you have 'invoice' with related record 'client' that has a related record 'addressinfo' that has a field 'street', then you can define 'street' on invoice as 'client.street' and 'street' on 'client' as 'addressinfo.street'.
The 'relatedsearchbehavior' resolves that kind of to 'client.address.street' which is further resolved so that it correctly adds joins, etc. to the query.

What you are asking is quite similar, but you want it to be more implicit for certain fields. Sure, it can be added to 'relatedsearchbehavior' and I think it is appropriate.

One could add a function similar to 'attributeNames' which would be 'globalNames'. This function would return all the fields (native or related) of the 'CActiveRecord' that it is defined in that should be 'global'.

But this list can also be added as a configuration of the 'relatedsearchbehavior'.

The method using the a function "globalNames" has the advantage that it does not require adding the 'relatedsearchbehavior' on 'CActiveRecord's where adding the behavior is an unnecessary overhead.

"globalNames" could also be a global configuration specified as an array of 'field'=>'CLASS' the advantage is that is has to be read only once, but the disadvantage is that the information is not contained in the class exposing its fields.

'relatedsearchbehavior' would have to find all global fields by traversing the tree of 'HAS_ONE' or 'BELONGS_TO' relations, while avoiding infinite recursion (every class can be traversed only once) and warn or ignore relations for classes that appear multiple times in the tree, except for direct relations.

There might be a need for a flag 'useGlobalSearch' in a global configuration, and also as a local configuration (to be able to disable it for certain 'CActiveRecords' for performance reasons).
However, by triggering the recursive search only when the field is not found by the current methods (real attribute name, or related attribute name). If it is not found in the search for a global field, it would trigger an exception. So I think that eventually the global search is only triggered if there is a need for it or if there is a mistake that will trigger an exception.

Appropriate caching of the search for global fields is also recommended.

Here is a class that I haven't published anywhere else doing a similar recursive search, but to do something more complex.
: recursively saving the records.
This Adapter of a CActiveRecords allows me to set the fields inside related records of the class and then save them during the call of 'save()' in the main record. Less lines than RelatedSearchBehavior, but not sure it is less complex. I am only using it in one 'CActiveRecord', but its serving me well and can help when extending 'RelatedSearchBehavior'

<?php
/********************************************************************/
/* Copying and use prohibited without prior written authorisation.  */
/********************************************************************/
/* @author Mario De Weerd                                           */
/********************************************************************/

class _RelationAR extends CActiveRecord {
    /** List of relations not to save recursively */
    protected $skipRelations=[];
    public function setAttribute($name,$value) {
        //echo Yii::trace("***Assign attr. ***".$name."**",'vardump');
        if($relation=$this->getActiveRelation($name)) {
            /* TODO: when this is not a new record, update the related records if appropriate */
            if($relation instanceof CHasManyRelation) { // HasMany || ManyMany
                //echo Yii::trace("***Assign attr. HasMany ***".$name."**".CVarDumper::dumpAsString($relation),'vardump');
                //CVarDumper::dump($relation);
                foreach($value as $recordValues) {
                    if($recordValues instanceof $relation->className) {
                        // Already an active record, use it.
                        $record = $recordValues;
                    } else {
                        // Attributes, create the record
                        /** @var CActiveRecord $record */
                        $record = new $relation->className;
                        $record->attributes = $recordValues;
                        //echo Yii::trace("***recordValues***".$name."**".CVarDumper::dumpAsString($recordValues),'vardump');
                        //echo Yii::trace("***Assign attributes***".$name."**".CVarDumper::dumpAsString($record),'vardump');
                    }
                    //echo Yii::trace("***Assign attributes***".$name."**".CVarDumper::dumpAsString($record->attributes),'vardump');
                    $this->addRelatedRecord($name,$record,true);
                }
            } else {  // HasOne || BelongsTo
                if(!$value instanceof $relation->className) {
                    $record = new $relation->className;
                    $record->attributes = $value;
                } else {
                    $record=$value;
                }
                //echo Yii::trace("***Assign attributes***".$name."**".CVarDumper::dumpAsString($record->attributes),'vardump');
                $this->addRelatedRecord(
                        $name,
                        $record,
                        false /* False for HasOne and BelongsTo relations because no index is needed */
                );
                //    CVarDumper::dump( $record);
            }
            return true;
        } else {
            return parent::setAttribute($name,$value);
        }
    }

    protected function beforeSave() {
        $result=parent::beforeSave();
        if($result) {
            foreach(array_keys($this->relations()) as $name) {
                if(in_array($name,$this->skipRelations)) continue;
                //echo Yii::trace("***********HasMany*************".CVarDumper::dumpAsString($this),'vardump');
                if(($relation=$this->getActiveRelation($name))) {
                    $records=$this->getRelated($name);
                    //$records=$this->{$name};
                    if($records instanceof CActiveRecord) {
                        $records=array($records);
                    }
                    //echo Yii::trace("***********Relation***$name*".CVarDumper::dumpAsString($relation),'vardump');
                    if($relation instanceof CBelongsToRelation) {
                    	// The related record's key is stored in this active record.
                        // Store the related record first and then get the key.
                        if(is_array($records)&&count($records)>1) {
                            throw new CException(Yii::t('app',"Too many related records for {attribute} of {classname}",
                                    array('{attribute}'=>$name,'{classname}'=>get_class($this))));
                        }
                        if(isset($records[0])) {
                            $record=$records[0];
                            //echo Yii::trace("***********Record**********".CVarDumper::dumpAsString($record->attributes),'vardump');
                            if($record instanceof CActiveRecord) {
                                /** @var CActiveRecord $records */
                                if(!$record->save()) {
                                	$this->addErrors($record->errors);
                                	//throw new CException("Related record not saved".CHtml::errorSummary($record));
                                	$result=false;
                                }
                                //echo Yii::trace("***********Records saved **********".CVarDumper::dumpAsString($record->getPrimaryKey()),'vardump');
                                /** @var CActiveRelation $relation */
                                $this->{$relation->foreignKey} = $record->getPrimaryKey();
                            }
                        }
                    }
                }
            }
        }
        //echo Yii::trace("****** BEFORE SAVE END*****".CVarDumper::dumpAsString($this->attributes),'vardump');

        return $result;
    }

    protected function afterSave() {
        parent::afterSave();
        foreach(array_keys($this->relations()) as $name) {
            //echo Yii::trace("***********HasMany*************".CVarDumper::dumpAsString($name),'vardump');
            //echo Yii::trace("***********HasRel**********".CVarDumper::dumpAsString($this->hasRelated($name)),'vardump');
            if($this->hasRelated($name)&&$relation=$this->getActiveRelation($name)) {
                if(in_array($name,$this->skipRelations)) continue;

                //echo Yii::trace("***********HasRel2**$name********".CVarDumper::dumpAsString($relation),'vardump');
                $records=$this->getRelated($name);
                if($records instanceof CActiveRecord) {
                    // Prepare for foreach
                    $records=array($records);
                }
                //echo Yii::trace("***********Count**$name********".CVarDumper::dumpAsString(count($records)),'vardump');

                if($relation instanceof CHasManyRelation
                        ||$relation instanceof CHasOneRelation) {
                    if(count($records)>1 && $relation instanceof CHasOneRelation) {
                        throw new CException(
                                Yii::t(
                                    'app',
                                    'More than 1 record for "has one" relation {relation} of {class}',
                                    array('{relation}'=>$name,'{class}'=>get_class($this))
                                )
                        );
                    }
                    foreach($records as /** @var CActiveRecord $record */ $record) {
                        //echo Yii::trace("************Record*************".CVarDumper::dumpAsString($record),'vardump');
                        // Store the current primary key in the related record.
                        // Save the related record.
                        if($record instanceof CActiveRecord) {
                            $record->{$relation->foreignKey}=$this->getPrimaryKey();
                            /** @var CActiveRecord $records */
                            if(!$record->save()) {
                            	$this->addErrors($record->getErrors());
                            }
                        } else {
                            if($record!=null) {
                                throw new CException(Yii::t('app',"Too many related records for {attribute} of {classname}",
                                        array('{attribute}'=>$name,'{classname}'=>get_class($this))));
                            }
                        }
                    }
                }
                //echo Yii::trace("***********BelongsTo*************".CVarDumper::dumpAsString($name),'vardump');
                // The ID of this record is required before adding the related
                // record.
            } else {
                // Recorded in beforeSave.
            }
        }
    }
}