Version: current

      1 <?php
      2 /**
      3  * Base class for filtering implementations,
      4  * which work together with {@link SearchContext}
      5  * to create or amend a query for {@link DataObject} instances.
      6  * See {@link SearchContext} for more information.
      7  *
      8  * @package sapphire
      9  * @subpackage search
     10  */
     11 abstract class SearchFilter extends Object {
     12
     13     /**
     14      * @var string Classname of the inspected {@link DataObject}
     15      */
     16     protected $model;
     17
     18     /**
     19      * @var string
     20      */
     21     protected $name;
     22
     23     /**
     24      * @var string
     25      */
     26     protected $fullName;
     27
     28     /**
     29      * @var mixed
     30      */
     31     protected $value;
     32
     33     /**
     34      * @var string Name of a has-one, has-many or many-many relation (not the classname).
     35      * Set in the constructor as part of the name in dot-notation, and used in
     36      * {@link applyRelation()}.
     37      */
     38     protected $relation;
     39
     40     /**
     41      * @param string $fullName Determines the name of the field, as well as the searched database
     42      *  column. Can contain a relation name in dot notation, which will automatically join
     43      *  the necessary tables (e.g. "Comments.Name" to join the "Comments" has-many relationship and
     44      *  search the "Name" column when applying this filter to a SiteTree class).
     45      * @param mixed $value
     46      */
     47     function __construct($fullName, $value = false) {
     48         $this->fullName = $fullName;
     49         // sets $this->name and $this->relation
     50         $this->addRelation($fullName);
     51         $this->value = $value;
     52     }
     53
     54     /**
     55      * Called by constructor to convert a string pathname into
     56      * a well defined relationship sequence.
     57      *
     58      * @param string $name
     59      */
     60     protected function addRelation($name) {
     61         if (strstr($name, '.')) {
     62             $parts = explode('.', $name);
     63             $this->name = array_pop($parts);
     64             $this->relation = $parts;
     65         } else {
     66             $this->name = $name;
     67         }
     68     }
     69
     70     /**
     71      * Set the root model class to be selected by this
     72      * search query.
     73      *
     74      * @param string $className
     75      */
     76     public function setModel($className) {
     77         $this->model = $className;
     78     }
     79
     80     /**
     81      * Set the current value to be filtered on.
     82      *
     83      * @param string $value
     84      */
     85     public function setValue($value) {
     86         $this->value = $value;
     87     }
     88
     89     /**
     90      * Accessor for the current value to be filtered on.
     91      * Caution: Data is not escaped.
     92      *
     93      * @return string
     94      */
     95     public function getValue() {
     96         return $this->value;
     97     }
     98
     99     /**
    100      * The original name of the field.
    101      *
    102      * @return string
    103      */
    104     public function getName() {
    105         return $this->name;
    106     }
    107
    108     /**
    109      * The full name passed to the constructor,
    110      * including any (optional) relations in dot notation.
    111      *
    112      * @return string
    113      */
    114     public function getFullName() {
    115         return $this->fullName;
    116     }
    117
    118     /**
    119      * Normalizes the field name to table mapping.
    120      *
    121      * @return string
    122      */
    123     function getDbName() {
    124         // Special handler for "NULL" relations
    125         if($this->name == "NULL") return $this->name;
    126
    127         // SRM: This code finds the table where the field named $this->name lives
    128         // Todo: move to somewhere more appropriate, such as DataMapper, the magical class-to-be?
    129         $candidateClass = $this->model;
    130         while($candidateClass != 'DataObject') {
    131             if(singleton($candidateClass)->hasOwnTableDatabaseField($this->name)) break;
    132             $candidateClass = get_parent_class($candidateClass);
    133         }
    134         if($candidateClass == 'DataObject') user_error("Couldn't find field $this->name in any of $this->model's tables.", E_USER_ERROR);
    135
    136         return "\"$candidateClass\".\"$this->name\"";
    137     }
    138
    139     /**
    140      * Return the value of the field as processed by the DBField class
    141      *
    142      * @return string
    143      */
    144     function getDbFormattedValue() {
    145         // SRM: This code finds the table where the field named $this->name lives
    146         // Todo: move to somewhere more appropriate, such as DataMapper, the magical class-to-be?
    147         $candidateClass = $this->model;
    148         $dbField = singleton($this->model)->dbObject($this->name);
    149         $dbField->setValue($this->value);
    150         return $dbField->RAW();
    151     }
    152
    153     /**
    154      * Traverse the relationship fields, and add the table
    155      * mappings to the query object state. This has to be called
    156      * in any overloaded {@link SearchFilter->apply()} methods manually.
    157      *
    158      * @todo try to make this implicitly triggered so it doesn't have to be manually called in child filters
    159      * @param SQLQuery $query
    160      * @return SQLQuery
    161      */
    162     function applyRelation($query) {
    163         if (is_array($this->relation)) {
    164             foreach($this->relation as $rel) {
    165                 $model = singleton($this->model);
    166                 if ($component = $model->has_one($rel)) {
    167                     if(!$query->isJoinedTo($component)) {
    168                         $foreignKey = $model->getReverseAssociation($component);
    169                         $query->leftJoin($component, "\"$component\".\"ID\" = \"{$this->model}\".\"{$foreignKey}ID\"");
    170
    171                         /**
    172                          * add join clause to the component's ancestry classes so that the search filter could search on its
    173                          * ancester fields.
    174                          */
    175                         $ancestry = ClassInfo::ancestry($component, true);
    176                         if(!empty($ancestry)){
    177                             $ancestry = array_reverse($ancestry);
    178                             foreach($ancestry as $ancestor){
    179                                 if($ancestor != $component){
    180                                     $query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
    181                                     $component=$ancestor;
    182                                 }
    183                             }
    184                         }
    185                     }
    186                     $this->model = $component;
    187                 } elseif ($component = $model->has_many($rel)) {
    188                     if(!$query->isJoinedTo($component)) {
    189                         $ancestry = $model->getClassAncestry();
    190                         $foreignKey = $model->getRemoteJoinField($rel);
    191                         $query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\"");
    192                         /**
    193                          * add join clause to the component's ancestry classes so that the search filter could search on its
    194                          * ancestor fields.
    195                          */
    196                         $ancestry = ClassInfo::ancestry($component, true);
    197                         if(!empty($ancestry)){
    198                             $ancestry = array_reverse($ancestry);
    199                             foreach($ancestry as $ancestor){
    200                                 if($ancestor != $component){
    201                                     $query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\"");
    202                                     $component=$ancestor;
    203                                 }
    204                             }
    205                         }
    206                     }
    207                     $this->model = $component;
    208                 } elseif ($component = $model->many_many($rel)) {
    209                     list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;
    210                     $parentBaseClass = ClassInfo::baseDataClass($parentClass);
    211                     $componentBaseClass = ClassInfo::baseDataClass($componentClass);
    212                     $query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\"");
    213                     $query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\"");
    214                     if(ClassInfo::hasTable($componentClass)) {
    215                         $query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\"");
    216                     }
    217                     $this->model = $componentClass;
    218
    219                 // Experimental support for user-defined relationships via a "(relName)Query" method
    220                 // This will likely be dropped in 2.4 for a system that makes use of Lazy Data Lists.
    221                 } elseif($model->hasMethod($rel.'Query')) {
    222                     // Get the query representing the join - it should have "$ID" in the filter
    223                     $newQuery = $model->{"{$rel}Query"}();
    224                     if($newQuery) {
    225                         // Get the table to join to
    226                         //DATABASE ABSTRACTION: I don't think we need this line anymore:
    227                         $newModel = str_replace('`','',array_shift($newQuery->from));
    228                         // Get the filter to use on the join
    229                         $ancestry = $model->getClassAncestry();
    230                         $newFilter = "(" . str_replace('$ID', "\"{$ancestry[0]}\".\"ID\"" , implode(") AND (", $newQuery->where) ) . ")";
    231                         $query->leftJoin($newModel, $newFilter);
    232                         $this->model = $newModel;
    233                     } else {
    234                         $this->name = "NULL";
    235                         return;
    236                     }
    237                 }
    238             }
    239         }
    240         return $query;
    241     }
    242
    243     /**
    244      * Apply filter criteria to a SQL query.
    245      *
    246      * @param SQLQuery $query
    247      * @return SQLQuery
    248      */
    249     abstract public function apply(SQLQuery $query);
    250
    251     /**
    252      * Determines if a field has a value,
    253      * and that the filter should be applied.
    254      * Relies on the field being populated with
    255      * {@link setValue()}
    256      *
    257      * @return boolean
    258      */
    259     public function isEmpty() {
    260         return false;
    261     }
    262
    263 }
    264 ?>

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -