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 ?>