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