Version: current

      1 <?php
      2 /**
      3 * Manages searching of properties on one or more {@link DataObject}
      4 * types, based on a given set of input parameters.
      5 * SearchContext is intentionally decoupled from any controller-logic,
      6 * it just receives a set of search parameters and an object class it acts on.
      7 *
      8 * The default output of a SearchContext is either a {@link SQLQuery} object
      9 * for further refinement, or a {@link DataObjectSet} that can be used to display
     10 * search results, e.g. in a {@link TableListField} instance.
     11 *
     12 * In case you need multiple contexts, consider namespacing your request parameters
     13 * by using {@link FieldSet->namespace()} on the $fields constructor parameter.
     14 *
     15 * Each DataObject subclass can have multiple search contexts for different cases,
     16 * e.g. for a limited frontend search and a fully featured backend search.
     17 * By default, you can use {@link DataObject->getDefaultSearchContext()} which is automatically
     18 * scaffolded. It uses {@link DataObject::$searchable_fields} to determine which fields
     19 * to include.
     20 *
     21 * @see http://doc.silverstripe.com/doku.php?id=searchcontext
     22 *
     23 * @package sapphire
     24 * @subpackage search
     25 */
     26 class SearchContext extends Object {
     27
     28     /**
     29      * DataObject subclass to which search parameters relate to.
     30      * Also determines as which object each result is provided.
     31      *
     32      * @var string
     33      */
     34     protected $modelClass;
     35
     36     /**
     37      * FormFields mapping to {@link DataObject::$db} properties
     38      * which are supposed to be searchable.
     39      *
     40      * @var FieldSet
     41      */
     42     protected $fields;
     43
     44     /**
     45      * Array of {@link SearchFilter} subclasses.
     46      *
     47      * @var array
     48      */
     49     protected $filters;
     50
     51     /**
     52      * The logical connective used to join WHERE clauses. Defaults to AND.
     53      * @var string
     54      */
     55     public $connective = 'AND';
     56
     57     /**
     58      * A key value pair of values that should be searched for.
     59      * The keys should match the field names specified in {@link self::$fields}.
     60      * Usually these values come from a submitted searchform
     61      * in the form of a $_REQUEST object.
     62      * CAUTION: All values should be treated as insecure client input.
     63      *
     64      * @param string $modelClass The base {@link DataObject} class that search properties related to.
     65      *                      Also used to generate a set of result objects based on this class.
     66      * @param FieldSet $fields Optional. FormFields mapping to {@link DataObject::$db} properties
     67      *                      which are to be searched. Derived from modelclass using
     68      *                      {@link DataObject::scaffoldSearchFields()} if left blank.
     69      * @param array $filters Optional. Derived from modelclass if left blank
     70      */
     71     function __construct($modelClass, $fields = null, $filters = null) {
     72         $this->modelClass = $modelClass;
     73         $this->fields = ($fields) ? $fields : new FieldSet();
     74         $this->filters = ($filters) ? $filters : array();
     75
     76         parent::__construct();
     77     }
     78
     79     /**
     80      * Returns scaffolded search fields for UI.
     81      *
     82      * @return FieldSet
     83      */
     84     public function getSearchFields() {
     85         return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
     86         // $this->fields is causing weirdness, so we ignore for now, using the default scaffolding
     87         //return singleton($this->modelClass)->scaffoldSearchFields();
     88     }
     89
     90     /**
     91      * @todo move to SQLQuery
     92      * @todo fix hack
     93      */
     94     protected function applyBaseTableFields() {
     95         $classes = ClassInfo::dataClassesFor($this->modelClass);
     96         $fields = array("\"".ClassInfo::baseDataClass($this->modelClass).'".*');
     97         if($this->modelClass != $classes[0]) $fields[] = '"'.$classes[0].'".*';
     98         //$fields = array_keys($model->db());
     99         $fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"';
    100         return $fields;
    101     }
    102
    103     /**
    104      * Returns a SQL object representing the search context for the given
    105      * list of query parameters.
    106      *
    107      * @param array $searchParams Map of search criteria, mostly taked from $_REQUEST.
    108      *  If a filter is applied to a relationship in dot notation,
    109      *  the parameter name should have the dots replaced with double underscores,
    110      *  for example "Comments__Name" instead of the filter name "Comments.Name".
    111      * @param string|array $sort Database column to sort on.
    112      *  Falls back to {@link DataObject::$default_sort} if not provided.
    113      * @param string|array $limit
    114      * @param SQLQuery $existingQuery
    115      * @return SQLQuery
    116      */
    117     public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) {
    118         $model = singleton($this->modelClass);
    119
    120         if($existingQuery) {
    121             $query = $existingQuery;
    122         } else {
    123             $query = $model->extendedSQL();
    124         }
    125
    126         $SQL_limit = Convert::raw2sql($limit);
    127         $query->limit($SQL_limit);
    128
    129         $SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort');
    130         $query->orderby($SQL_sort);
    131
    132         // hack to work with $searchParems when it's an Object
    133         $searchParamArray = array();
    134         if (is_object($searchParams)) {
    135             $searchParamArray = $searchParams->getVars();
    136         } else {
    137             $searchParamArray = $searchParams;
    138         }
    139
    140         foreach($searchParamArray as $key => $value) {
    141             $key = str_replace('__', '.', $key);
    142             if($filter = $this->getFilter($key)) {
    143                 $filter->setModel($this->modelClass);
    144                 $filter->setValue($value);
    145                 if(! $filter->isEmpty()) {
    146                     $filter->apply($query);
    147                 }
    148             }
    149         }
    150
    151         $query->connective = $this->connective;
    152         $query->distinct = true;
    153
    154         $model->extend('augmentSQL', $query);
    155
    156         return $query;
    157     }
    158
    159     /**
    160      * Returns a result set from the given search parameters.
    161      *
    162      * @todo rearrange start and limit params to reflect DataObject
    163      *
    164      * @param array $searchParams
    165      * @param string|array $sort
    166      * @param string|array $limit
    167      * @return DataObjectSet
    168      */
    169     public function getResults($searchParams, $sort = false, $limit = false) {
    170         $searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
    171
    172         $query = $this->getQuery($searchParams, $sort, $limit);
    173
    174         // use if a raw SQL query is needed
    175         $results = new DataObjectSet();
    176         foreach($query->execute() as $row) {
    177             $className = $row['RecordClassName'];
    178             $results->push(new $className($row));
    179         }
    180         return $results;
    181         //
    182         //return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit);
    183     }
    184
    185     /**
    186      * Callback map function to filter fields with empty values from
    187      * being included in the search expression.
    188      *
    189      * @param unknown_type $value
    190      * @return boolean
    191      */
    192     function clearEmptySearchFields($value) {
    193         return ($value != '');
    194     }
    195
    196     /**
    197      * Accessor for the filter attached to a named field.
    198      *
    199      * @param string $name
    200      * @return SearchFilter
    201      */
    202     public function getFilter($name) {
    203         if (isset($this->filters[$name])) {
    204             return $this->filters[$name];
    205         } else {
    206             return null;
    207         }
    208     }
    209
    210     /**
    211      * Get the map of filters in the current search context.
    212      *
    213      * @return array
    214      */
    215     public function getFilters() {
    216         return $this->filters;
    217     }
    218
    219     /**
    220      * Overwrite the current search context filter map.
    221      *
    222      * @param array $filters
    223      */
    224     public function setFilters($filters) {
    225         $this->filters = $filters;
    226     }
    227
    228     /**
    229      * Adds a instance of {@link SearchFilter}.
    230      *
    231      * @param SearchFilter $filter
    232      */
    233     public function addFilter($filter) {
    234         $this->filters[$filter->getFullName()] = $filter;
    235     }
    236
    237     /**
    238      * Removes a filter by name.
    239      *
    240      * @param string $name
    241      */
    242     public function removeFilterByName($name) {
    243         unset($this->filters[$name]);
    244     }
    245
    246     /**
    247      * Get the list of searchable fields in the current search context.
    248      *
    249      * @return FieldSet
    250      */
    251     public function getFields() {
    252         return $this->fields;
    253     }
    254
    255     /**
    256      * Apply a list of searchable fields to the current search context.
    257      *
    258      * @param FieldSet $fields
    259      */
    260     public function setFields($fields) {
    261         $this->fields = $fields;
    262     }
    263
    264     /**
    265      * Adds a new {@link FormField} instance.
    266      *
    267      * @param FormField $field
    268      */
    269     public function addField($field) {
    270         $this->fields->push($field);
    271     }
    272
    273     /**
    274      * Removes an existing formfield instance by its name.
    275      *
    276      * @param string $fieldName
    277      */
    278     public function removeFieldByName($fieldName) {
    279         $this->fields->removeByName($fieldName);
    280     }
    281
    282 }
    283 ?>
    284

[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. -