Completed
Push — master ( 686b54...ec5bd5 )
by Mark
06:38
created
tasks/BuildVFI.php 2 patches
Braces   +22 added lines, -9 removed lines patch added patch discarded remove patch
@@ -27,8 +27,12 @@  discard block
 block discarded – undo
27 27
 			$count  = $list->count();
28 28
 			for ($i = $n; $i < $count; $i += 10) {
29 29
 				$chunk = $list->limit(10, $i);
30
-				if (Controller::curr() instanceof TaskRunner) echo "Processing VFI #$i...\n";
31
-				foreach ($chunk as $rec) $rec->rebuildVFI();
30
+				if (Controller::curr() instanceof TaskRunner) {
31
+					echo "Processing VFI #$i...\n";
32
+				}
33
+				foreach ($chunk as $rec) {
34
+					$rec->rebuildVFI();
35
+				}
32 36
 			}
33 37
 			VirtualFieldIndex::build($c);
34 38
 
@@ -58,7 +62,9 @@  discard block
 block discarded – undo
58 62
 
59 63
 		if (isset($_GET['class']) && isset($_GET['id'])) {
60 64
 			$item = DataObject::get($_GET['class'])->byID($_GET['id']);
61
-			if (!$item || !$item->exists()) die('not found: ' . $_GET['id']);
65
+			if (!$item || !$item->exists()) {
66
+				die('not found: ' . $_GET['id']);
67
+			}
62 68
 			$item->rebuildVFI();
63 69
 			echo "done";
64 70
 			return;
@@ -66,7 +72,9 @@  discard block
 block discarded – undo
66 72
 
67 73
 		if (isset($_GET['link'])) {
68 74
 			$item = SiteTree::get_by_link($_GET['link']);
69
-			if (!$item || !$item->exists()) die('not found: ' . $_GET['link']);
75
+			if (!$item || !$item->exists()) {
76
+				die('not found: ' . $_GET['link']);
77
+			}
70 78
 			$item->rebuildVFI();
71 79
 			echo "done";
72 80
 			return;
@@ -74,8 +82,7 @@  discard block
 block discarded – undo
74 82
 
75 83
 		if (isset($_GET['start'])) {
76 84
 			$this->runFrom($_GET['class'], $_GET['start'], $_GET['field']);
77
-		}
78
-		else {
85
+		} else {
79 86
 			foreach(array('framework','sapphire') as $dirname) {
80 87
 				$script = sprintf("%s%s$dirname%scli-script.php", BASE_PATH, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
81 88
 				if(file_exists($script)) {
@@ -85,7 +92,9 @@  discard block
 block discarded – undo
85 92
 
86 93
 			$classes = VirtualFieldIndex::get_classes_with_vfi();
87 94
 			foreach ($classes as $class) {
88
-				if (isset($_GET['class']) && $class != $_GET['class']) continue;
95
+				if (isset($_GET['class']) && $class != $_GET['class']) {
96
+					continue;
97
+				}
89 98
 				$singleton = singleton($class);
90 99
 				$query = $singleton->get($class);
91 100
 				$dtaQuery = $query->dataQuery();
@@ -100,9 +109,13 @@  discard block
 block discarded – undo
100 109
 				for ($offset = $startFrom; $offset < $total; $offset += $this->stat('recordsPerRequest')) {
101 110
 					echo "$offset..";
102 111
 					$cmd = "php $script dev/tasks/$self class=$class start=$offset field=$field";
103
-					if($verbose) echo "\n  Running '$cmd'\n";
112
+					if($verbose) {
113
+						echo "\n  Running '$cmd'\n";
114
+					}
104 115
 					$res = $verbose ? passthru($cmd) : `$cmd`;
105
-					if($verbose) echo "  ".preg_replace('/\r\n|\n/', '$0  ', $res)."\n";
116
+					if($verbose) {
117
+						echo "  ".preg_replace('/\r\n|\n/', '$0  ', $res)."\n";
118
+					}
106 119
 				}
107 120
 			}
108 121
 		}
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -58,7 +58,7 @@  discard block
 block discarded – undo
58 58
 
59 59
 		if (isset($_GET['class']) && isset($_GET['id'])) {
60 60
 			$item = DataObject::get($_GET['class'])->byID($_GET['id']);
61
-			if (!$item || !$item->exists()) die('not found: ' . $_GET['id']);
61
+			if (!$item || !$item->exists()) die('not found: '.$_GET['id']);
62 62
 			$item->rebuildVFI();
63 63
 			echo "done";
64 64
 			return;
@@ -66,7 +66,7 @@  discard block
 block discarded – undo
66 66
 
67 67
 		if (isset($_GET['link'])) {
68 68
 			$item = SiteTree::get_by_link($_GET['link']);
69
-			if (!$item || !$item->exists()) die('not found: ' . $_GET['link']);
69
+			if (!$item || !$item->exists()) die('not found: '.$_GET['link']);
70 70
 			$item->rebuildVFI();
71 71
 			echo "done";
72 72
 			return;
@@ -76,9 +76,9 @@  discard block
 block discarded – undo
76 76
 			$this->runFrom($_GET['class'], $_GET['start'], $_GET['field']);
77 77
 		}
78 78
 		else {
79
-			foreach(array('framework','sapphire') as $dirname) {
79
+			foreach (array('framework', 'sapphire') as $dirname) {
80 80
 				$script = sprintf("%s%s$dirname%scli-script.php", BASE_PATH, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR);
81
-				if(file_exists($script)) {
81
+				if (file_exists($script)) {
82 82
 					break;
83 83
 				}
84 84
 			}
@@ -90,7 +90,7 @@  discard block
 block discarded – undo
90 90
 				$query = $singleton->get($class);
91 91
 				$dtaQuery = $query->dataQuery();
92 92
 				$sqlQuery = $dtaQuery->getFinalisedQuery();
93
-				$singleton->extend('augmentSQL',$sqlQuery,$dtaQuery);
93
+				$singleton->extend('augmentSQL', $sqlQuery, $dtaQuery);
94 94
 				$total = $query->count();
95 95
 				$startFrom = isset($_GET['startfrom']) ? $_GET['startfrom'] : 0;
96 96
 				$field = isset($_GET['field']) ? $_GET['field'] : '';
@@ -100,9 +100,9 @@  discard block
 block discarded – undo
100 100
 				for ($offset = $startFrom; $offset < $total; $offset += $this->stat('recordsPerRequest')) {
101 101
 					echo "$offset..";
102 102
 					$cmd = "php $script dev/tasks/$self class=$class start=$offset field=$field";
103
-					if($verbose) echo "\n  Running '$cmd'\n";
103
+					if ($verbose) echo "\n  Running '$cmd'\n";
104 104
 					$res = $verbose ? passthru($cmd) : `$cmd`;
105
-					if($verbose) echo "  ".preg_replace('/\r\n|\n/', '$0  ', $res)."\n";
105
+					if ($verbose) echo "  ".preg_replace('/\r\n|\n/', '$0  ', $res)."\n";
106 106
 				}
107 107
 			}
108 108
 		}
Please login to merge, or discard this patch.
code/helpers/FacetHelper.php 3 patches
Doc Comments   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -368,7 +368,7 @@  discard block
 block discarded – undo
368 368
 
369 369
     /**
370 370
      * NOTE: this will break if applied to something that's not a SiteTree subclass.
371
-     * @param DataList|PaginatedList $matches
371
+     * @param SS_List $matches
372 372
      * @param array $facet
373 373
      * @param int $typeID
374 374
      */
@@ -408,7 +408,7 @@  discard block
 block discarded – undo
408 408
 
409 409
     /**
410 410
      * Builds facets from all attributes present in the data set.
411
-     * @param DataList|PaginatedList $matches
411
+     * @param SS_List $matches
412 412
      * @return array
413 413
      */
414 414
     protected function buildAllAttributeFacets($matches)
Please login to merge, or discard this patch.
Indentation   +670 added lines, -670 removed lines patch added patch discarded remove patch
@@ -13,679 +13,679 @@
 block discarded – undo
13 13
  */
14 14
 class FacetHelper extends Object
15 15
 {
16
-    /** @var bool - if this is turned on it will use an algorithm that doesn't require traversing the data set if possible */
17
-    private static $faster_faceting = false;
18
-
19
-    /** @var bool - should the facets (link and checkbox only) be sorted - this can mess with things like category lists */
20
-    private static $sort_facet_values = true;
21
-
22
-    /** @var string - I don't know why you'd want to override this, but you could if you wanted */
23
-    private static $attribute_facet_regex = '/^ATT(\d+)$/';
24
-
25
-    /** @var bool - For checkbox facets, is the initial state all checked or all unchecked? */
26
-    private static $default_checkbox_state = true;
27
-
28
-
29
-    /**
30
-     * @return FacetHelper
31
-     */
32
-    public static function inst()
33
-    {
34
-        return Injector::inst()->get('FacetHelper');
35
-    }
36
-
37
-
38
-    /**
39
-     * Performs some quick pre-processing on filters from any source
40
-     *
41
-     * @param array $filters
42
-     * @return array
43
-     */
44
-    public function scrubFilters($filters)
45
-    {
46
-        if (!is_array($filters)) {
47
-            $filters = array();
48
-        }
49
-
50
-        foreach ($filters as $k => $v) {
51
-            if (empty($v)) {
52
-                unset($filters[$k]);
53
-            }
54
-            // this allows you to send an array as a comma-separated list, which is easier on the query string length
55
-            if (is_string($v) && strpos($v, 'LIST~') === 0) {
56
-                $filters[$k] = explode(',', substr($v, 5));
57
-            }
58
-        }
59
-
60
-        return $filters;
61
-    }
62
-
63
-
64
-    /**
65
-     * @param DataList $list
66
-     * @param array    $filters
67
-     * @param DataObject|string $sing - just a singleton object we can get information off of
68
-     * @return DataList
69
-     */
70
-    public function addFiltersToDataList($list, array $filters, $sing=null)
71
-    {
72
-        if (!$sing) {
73
-            $sing = singleton($list->dataClass());
74
-        }
75
-        if (is_string($sing)) {
76
-            $sing = singleton($sing);
77
-        }
78
-
79
-        if (!empty($filters)) {
80
-            foreach ($filters as $filterField => $filterVal) {
81
-                if ($sing->hasExtension('HasStaticAttributes') && preg_match(self::config()->attribute_facet_regex, $filterField, $matches)) {
82
-                    //					$sav = $sing->StaticAttributeValues();
16
+	/** @var bool - if this is turned on it will use an algorithm that doesn't require traversing the data set if possible */
17
+	private static $faster_faceting = false;
18
+
19
+	/** @var bool - should the facets (link and checkbox only) be sorted - this can mess with things like category lists */
20
+	private static $sort_facet_values = true;
21
+
22
+	/** @var string - I don't know why you'd want to override this, but you could if you wanted */
23
+	private static $attribute_facet_regex = '/^ATT(\d+)$/';
24
+
25
+	/** @var bool - For checkbox facets, is the initial state all checked or all unchecked? */
26
+	private static $default_checkbox_state = true;
27
+
28
+
29
+	/**
30
+	 * @return FacetHelper
31
+	 */
32
+	public static function inst()
33
+	{
34
+		return Injector::inst()->get('FacetHelper');
35
+	}
36
+
37
+
38
+	/**
39
+	 * Performs some quick pre-processing on filters from any source
40
+	 *
41
+	 * @param array $filters
42
+	 * @return array
43
+	 */
44
+	public function scrubFilters($filters)
45
+	{
46
+		if (!is_array($filters)) {
47
+			$filters = array();
48
+		}
49
+
50
+		foreach ($filters as $k => $v) {
51
+			if (empty($v)) {
52
+				unset($filters[$k]);
53
+			}
54
+			// this allows you to send an array as a comma-separated list, which is easier on the query string length
55
+			if (is_string($v) && strpos($v, 'LIST~') === 0) {
56
+				$filters[$k] = explode(',', substr($v, 5));
57
+			}
58
+		}
59
+
60
+		return $filters;
61
+	}
62
+
63
+
64
+	/**
65
+	 * @param DataList $list
66
+	 * @param array    $filters
67
+	 * @param DataObject|string $sing - just a singleton object we can get information off of
68
+	 * @return DataList
69
+	 */
70
+	public function addFiltersToDataList($list, array $filters, $sing=null)
71
+	{
72
+		if (!$sing) {
73
+			$sing = singleton($list->dataClass());
74
+		}
75
+		if (is_string($sing)) {
76
+			$sing = singleton($sing);
77
+		}
78
+
79
+		if (!empty($filters)) {
80
+			foreach ($filters as $filterField => $filterVal) {
81
+				if ($sing->hasExtension('HasStaticAttributes') && preg_match(self::config()->attribute_facet_regex, $filterField, $matches)) {
82
+					//					$sav = $sing->StaticAttributeValues();
83 83
 //					Debug::log("sav = {$sav->getJoinTable()}, {$sav->getLocalKey()}, {$sav->getForeignKey()}");
84 84
 //					$list = $list
85 85
 //						->innerJoin($sav->getJoinTable(), "\"{$sing->baseTable()}\".\"ID\" = \"{$sav->getJoinTable()}\".\"{$sav->getLocalKey()}\"")
86 86
 //						->filter("\"{$sav->getJoinTable()}\".\"{$sav->getForeignKey()}\"", $filterVal)
87 87
 //					;
88
-                    // TODO: This logic should be something like the above, but I don't know
89
-                    // how to get the join table from a singleton (which returns an UnsavedRelationList
90
-                    // instead of a ManyManyList). I've got a deadline to meet, though, so this
91
-                    // will catch the majority of cases as long as the extension is applied to the
92
-                    // Product class instead of a subclass.
93
-                    $list = $list
94
-                        ->innerJoin('Product_StaticAttributeTypes', "\"SiteTree\".\"ID\" = \"Product_StaticAttributeTypes\".\"ProductID\"")
95
-                        ->innerJoin('ProductAttributeValue', "\"Product_StaticAttributeTypes\".\"ProductAttributeTypeID\" = \"ProductAttributeValue\".\"TypeID\"")
96
-                        ->innerJoin('Product_StaticAttributeValues', "\"SiteTree\".\"ID\" = \"Product_StaticAttributeValues\".\"ProductID\" AND \"ProductAttributeValue\".\"ID\" = \"Product_StaticAttributeValues\".\"ProductAttributeValueID\"")
97
-                        ->filter("Product_StaticAttributeValues.ProductAttributeValueID", $filterVal);
98
-                } else {
99
-                    $list = $list->filter($this->processFilterField($sing, $filterField, $filterVal));
100
-                }
101
-            }
102
-        }
103
-
104
-        return $list;
105
-    }
106
-
107
-
108
-    /**
109
-     * @param DataObject $rec           This would normally just be a singleton but we don't want to have to create it over and over
110
-     * @param string     $filterField
111
-     * @param mixed      $filterVal
112
-     * @return array - returns the new filter added
113
-     */
114
-    public function processFilterField($rec, $filterField, $filterVal)
115
-    {
116
-        // First check for VFI fields
117
-        if ($rec->hasExtension('VirtualFieldIndex') && ($spec = $rec->getVFISpec($filterField))) {
118
-            if ($spec['Type'] == VirtualFieldIndex::TYPE_LIST) {
119
-                // Lists have to be handled a little differently
120
-                $f = $rec->getVFIFieldName($filterField) . ':PartialMatch';
121
-                if (is_array($filterVal)) {
122
-                    foreach ($filterVal as &$val) {
123
-                        $val = '|' . $val . '|';
124
-                    }
125
-                    return array($f => $filterVal);
126
-                } else {
127
-                    return array($f => '|' . $filterVal . '|');
128
-                }
129
-            } else {
130
-                // Simples are simple
131
-                $filterField = $rec->getVFIFieldName($filterField);
132
-            }
133
-        }
134
-
135
-        // Next check for regular db fields
136
-        if ($rec->dbObject($filterField)) {
137
-            // Is it a range value?
138
-            if (is_string($filterVal) && preg_match('/^RANGE\~(.+)\~(.+)$/', $filterVal, $m)) {
139
-                $filterField .= ':Between';
140
-                $filterVal = array_slice($m, 1, 2);
141
-            }
142
-
143
-            return array($filterField => $filterVal);
144
-        }
145
-
146
-        return array();
147
-    }
148
-
149
-
150
-    /**
151
-     * Processes the facet spec and removes any shorthand (field => label).
152
-     * @param array $facetSpec
153
-     * @return array
154
-     */
155
-    public function expandFacetSpec(array $facetSpec)
156
-    {
157
-        if (is_null($facetSpec)) {
158
-            return array();
159
-        }
160
-        $facets = array();
161
-
162
-        foreach ($facetSpec as $field => $label) {
163
-            if (is_array($label)) {
164
-                $facets[$field] = $label;
165
-            } else {
166
-                $facets[$field] = array('Label' => $label);
167
-            }
168
-
169
-            if (empty($facets[$field]['Source'])) {
170
-                $facets[$field]['Source'] = $field;
171
-            }
172
-            if (empty($facets[$field]['Type'])) {
173
-                $facets[$field]['Type']  = ShopSearch::FACET_TYPE_LINK;
174
-            }
175
-
176
-            if (empty($facets[$field]['Values'])) {
177
-                $facets[$field]['Values'] = array();
178
-            } else {
179
-                $vals = $facets[$field]['Values'];
180
-                if (is_string($vals)) {
181
-                    $vals = eval('return ' . $vals . ';');
182
-                }
183
-                $facets[$field]['Values'] = array();
184
-                foreach ($vals as $val => $lbl) {
185
-                    $facets[$field]['Values'][$val] = new ArrayData(array(
186
-                        'Label'     => $lbl,
187
-                        'Value'     => $val,
188
-                        'Count'     => 0,
189
-                    ));
190
-                }
191
-            }
192
-        }
193
-
194
-        return $facets;
195
-    }
196
-
197
-
198
-    /**
199
-     * This is super-slow. I'm assuming if you're using facets you
200
-     * probably also ought to be using Solr or something else. Or
201
-     * maybe you have unlimited time and can refactor this feature
202
-     * and submit a pull request...
203
-     *
204
-     * TODO: If this is going to be used for categories we're going
205
-     * to have to really clean it up and speed it up.
206
-     * Suggestion:
207
-     *  - option to turn off counts
208
-     *  - switch order of nested array so we don't go through results unless needed
209
-     *  - if not doing counts, min/max and link facets can be handled w/ queries
210
-     *  - separate that bit out into a new function
211
-     * NOTE: This is partially done with the "faster_faceting" config
212
-     * option but more could be done, particularly by covering link facets as well.
213
-     *
214
-     * Output - list of ArrayData in the format:
215
-     *   Label - name of the facet
216
-     *   Source - field name of the facet
217
-     *   Type - one of the ShopSearch::FACET_TYPE_XXXX constants
218
-     *   Values - SS_List of possible values for this facet
219
-     *
220
-     * @param SS_List $matches
221
-     * @param array $facetSpec
222
-     * @param bool $autoFacetAttributes [optional]
223
-     * @return ArrayList
224
-     */
225
-    public function buildFacets(SS_List $matches, array $facetSpec, $autoFacetAttributes=false)
226
-    {
227
-        $facets = $this->expandFacetSpec($facetSpec);
228
-        if (!$autoFacetAttributes && (empty($facets) || !$matches || !$matches->count())) {
229
-            return new ArrayList();
230
-        }
231
-        $fasterMethod = (bool)$this->config()->faster_faceting;
232
-
233
-        // fill them in
234
-        foreach ($facets as $field => &$facet) {
235
-            if (preg_match(self::config()->attribute_facet_regex, $field, $m)) {
236
-                $this->buildAttributeFacet($matches, $facet, $m[1]);
237
-                continue;
238
-            }
239
-
240
-            // NOTE: using this method range and checkbox facets don't get counts
241
-            if ($fasterMethod && $facet['Type'] != ShopSearch::FACET_TYPE_LINK) {
242
-                if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
243
-                    if (isset($facet['RangeMin'])) {
244
-                        $facet['MinValue'] = $facet['RangeMin'];
245
-                    }
246
-                    if (isset($facet['RangeMax'])) {
247
-                        $facet['MaxValue'] = $facet['RangeMax'];
248
-                    }
249
-                }
250
-
251
-                continue;
252
-            }
253
-
254
-            foreach ($matches as $rec) {
255
-                // If it's a range facet, set up the min/max
256
-                if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
257
-                    if (isset($facet['RangeMin'])) {
258
-                        $facet['MinValue'] = $facet['RangeMin'];
259
-                    }
260
-                    if (isset($facet['RangeMax'])) {
261
-                        $facet['MaxValue'] = $facet['RangeMax'];
262
-                    }
263
-                }
264
-
265
-                // If the field is accessible via normal methods, including
266
-                // a user-defined getter, prefer that
267
-                $fieldValue = $rec->relObject($field);
268
-                if (is_null($fieldValue) && $rec->hasMethod($meth = "get{$field}")) {
269
-                    $fieldValue = $rec->$meth();
270
-                }
271
-
272
-                // If not, look for a VFI field
273
-                if (!$fieldValue && $rec->hasExtension('VirtualFieldIndex')) {
274
-                    $fieldValue = $rec->getVFI($field);
275
-                }
276
-
277
-                // If we found something, process it
278
-                if (!empty($fieldValue)) {
279
-                    // normalize so that it's iterable
280
-                    if (!is_array($fieldValue) && !$fieldValue instanceof SS_List) {
281
-                        $fieldValue = array($fieldValue);
282
-                    }
283
-
284
-                    foreach ($fieldValue as $obj) {
285
-                        if (empty($obj)) {
286
-                            continue;
287
-                        }
288
-
289
-                        // figure out the right label
290
-                        if (is_object($obj) && $obj->hasMethod('Nice')) {
291
-                            $lbl = $obj->Nice();
292
-                        } elseif (is_object($obj) && !empty($obj->Title)) {
293
-                            $lbl = $obj->Title;
294
-                        } elseif (
295
-                            is_numeric($obj) &&
296
-                            !empty($facet['LabelFormat']) &&
297
-                            $facet['LabelFormat'] === 'Currency' &&
298
-                            $facet['Type'] !== ShopSearch::FACET_TYPE_RANGE // this one handles it via javascript
299
-                        ) {
300
-                            $tmp = Currency::create($field);
301
-                            $tmp->setValue($obj);
302
-                            $lbl = $tmp->Nice();
303
-                        } else {
304
-                            $lbl = (string)$obj;
305
-                        }
306
-
307
-                        // figure out the value for sorting
308
-                        if (is_object($obj) && $obj->hasMethod('getAmount')) {
309
-                            $val = $obj->getAmount();
310
-                        } elseif (is_object($obj) && !empty($obj->ID)) {
311
-                            $val = $obj->ID;
312
-                        } else {
313
-                            $val = (string)$obj;
314
-                        }
315
-
316
-                        // if it's a range facet, calculate the min and max
317
-                        if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
318
-                            if (!isset($facet['MinValue']) || $val < $facet['MinValue']) {
319
-                                $facet['MinValue'] = $val;
320
-                                $facet['MinLabel'] = $lbl;
321
-                            }
322
-                            if (!isset($facet['RangeMin']) || $val < $facet['RangeMin']) {
323
-                                $facet['RangeMin'] = $val;
324
-                            }
325
-                            if (!isset($facet['MaxValue']) || $val > $facet['MaxValue']) {
326
-                                $facet['MaxValue'] = $val;
327
-                                $facet['MaxLabel'] = $lbl;
328
-                            }
329
-                            if (!isset($facet['RangeMax']) || $val > $facet['RangeMax']) {
330
-                                $facet['RangeMax'] = $val;
331
-                            }
332
-                        }
333
-
334
-                        // Tally the value in the facets
335
-                        if (!isset($facet['Values'][$val])) {
336
-                            $facet['Values'][$val] = new ArrayData(array(
337
-                                'Label'     => $lbl,
338
-                                'Value'     => $val,
339
-                                'Count'     => 1,
340
-                            ));
341
-                        } elseif ($facet['Values'][$val]) {
342
-                            $facet['Values'][$val]->Count++;
343
-                        }
344
-                    }
345
-                }
346
-            }
347
-        }
348
-
349
-        // if we're auto-building the facets based on attributes,
350
-        if ($autoFacetAttributes) {
351
-            $facets = array_merge($this->buildAllAttributeFacets($matches), $facets);
352
-        }
353
-
354
-        // convert values to arraylist
355
-        $out = new ArrayList();
356
-        $sortValues = self::config()->sort_facet_values;
357
-        foreach ($facets as $f) {
358
-            if ($sortValues) {
359
-                ksort($f['Values']);
360
-            }
361
-            $f['Values'] = new ArrayList($f['Values']);
362
-            $out->push(new ArrayData($f));
363
-        }
364
-
365
-        return $out;
366
-    }
367
-
368
-
369
-    /**
370
-     * NOTE: this will break if applied to something that's not a SiteTree subclass.
371
-     * @param DataList|PaginatedList $matches
372
-     * @param array $facet
373
-     * @param int $typeID
374
-     */
375
-    protected function buildAttributeFacet($matches, array &$facet, $typeID)
376
-    {
377
-        $q = $matches instanceof PaginatedList ? $matches->getList()->dataQuery()->query() : $matches->dataQuery()->query();
378
-
379
-        if (empty($facet['Label'])) {
380
-            $type = ProductAttributeType::get()->byID($typeID);
381
-            $facet['Label'] = $type->Label;
382
-        }
383
-
384
-        $baseTable = $q->getFrom();
385
-        if (is_array($baseTable)) {
386
-            $baseTable = reset($baseTable);
387
-        }
388
-
389
-        $q = $q->setSelect(array())
390
-            ->selectField('"ProductAttributeValue"."ID"', 'Value')
391
-            ->selectField('"ProductAttributeValue"."Value"', 'Label')
392
-            ->selectField('count(distinct '.$baseTable.'."ID")', 'Count')
393
-            ->selectField('"ProductAttributeValue"."Sort"')
394
-            ->addInnerJoin('Product_StaticAttributeValues', $baseTable.'."ID" = "Product_StaticAttributeValues"."ProductID"')
395
-            ->addInnerJoin('ProductAttributeValue', '"Product_StaticAttributeValues"."ProductAttributeValueID" = "ProductAttributeValue"."ID"')
396
-            ->addWhere(sprintf("\"ProductAttributeValue\".\"TypeID\" = '%d'", $typeID))
397
-            ->setOrderBy('"ProductAttributeValue"."Sort"', 'ASC')
398
-            ->setGroupBy('"ProductAttributeValue"."ID"')
399
-            ->execute()
400
-        ;
401
-
402
-        $facet['Values'] = array();
403
-        foreach ($q as $row) {
404
-            $facet['Values'][ $row['Value'] ] = new ArrayData($row);
405
-        }
406
-    }
407
-
408
-
409
-    /**
410
-     * Builds facets from all attributes present in the data set.
411
-     * @param DataList|PaginatedList $matches
412
-     * @return array
413
-     */
414
-    protected function buildAllAttributeFacets($matches)
415
-    {
416
-        $q = $matches instanceof PaginatedList ? $matches->getList()->dataQuery()->query() : $matches->dataQuery()->query();
417
-
418
-        // this is the easiest way to get SiteTree vs SiteTree_Live
419
-        $baseTable = $q->getFrom();
420
-        if (is_array($baseTable)) {
421
-            $baseTable = reset($baseTable);
422
-        }
423
-
424
-        $q = $q->setSelect(array())
425
-            ->selectField('"ProductAttributeType"."ID"', 'TypeID')
426
-            ->selectField('"ProductAttributeType"."Label"', 'TypeLabel')
427
-            ->selectField('"ProductAttributeValue"."ID"', 'Value')
428
-            ->selectField('"ProductAttributeValue"."Value"', 'Label')
429
-            ->selectField('count(distinct '.$baseTable.'."ID")', 'Count')
430
-            ->selectField('"ProductAttributeValue"."Sort"')
431
-            ->addInnerJoin('Product_StaticAttributeTypes', $baseTable.'."ID" = "Product_StaticAttributeTypes"."ProductID"')
432
-            ->addInnerJoin('ProductAttributeType', '"Product_StaticAttributeTypes"."ProductAttributeTypeID" = "ProductAttributeType"."ID"')
433
-            ->addInnerJoin('Product_StaticAttributeValues', $baseTable.'."ID" = "Product_StaticAttributeValues"."ProductID"')
434
-            ->addInnerJoin('ProductAttributeValue', '"Product_StaticAttributeValues"."ProductAttributeValueID" = "ProductAttributeValue"."ID"'
435
-                . ' AND "ProductAttributeValue"."TypeID" = "ProductAttributeType"."ID"')
436
-            ->setOrderBy(array(
437
-                '"ProductAttributeType"."Label"' => 'ASC',
438
-                '"ProductAttributeValue"."Sort"' => 'ASC',
439
-            ))
440
-            ->setGroupBy(array('"ProductAttributeValue"."ID"', '"ProductAttributeType"."ID"'))
441
-            ->execute()
442
-        ;
443
-
444
-
445
-        $curType  = 0;
446
-        $facets   = array();
447
-        $curFacet = null;
448
-        foreach ($q as $row) {
449
-            if ($curType != $row['TypeID']) {
450
-                if ($curType > 0) {
451
-                    $facets['ATT'.$curType] = $curFacet;
452
-                }
453
-                $curType = $row['TypeID'];
454
-                $curFacet = array(
455
-                    'Label'  => $row['TypeLabel'],
456
-                    'Source' => 'ATT'.$curType,
457
-                    'Type'   => ShopSearch::FACET_TYPE_LINK,
458
-                    'Values' => array(),
459
-                );
460
-            }
461
-
462
-            unset($row['TypeID']);
463
-            unset($row['TypeLabel']);
464
-            $curFacet['Values'][ $row['Value'] ] = new ArrayData($row);
465
-        }
466
-
467
-        if ($curType > 0) {
468
-            $facets['ATT'.$curType] = $curFacet;
469
-        }
470
-        return $facets;
471
-    }
472
-
473
-
474
-    /**
475
-     * Inserts a "Link" field into the values for each facet which can be
476
-     * used to get a filtered search based on that facets
477
-     *
478
-     * @param ArrayList $facets
479
-     * @param array     $baseParams
480
-     * @param string    $baseLink
481
-     * @return ArrayList
482
-     */
483
-    public function insertFacetLinks(ArrayList $facets, array $baseParams, $baseLink)
484
-    {
485
-        $qs_f   = Config::inst()->get('ShopSearch', 'qs_filters');
486
-        $qs_t   = Config::inst()->get('ShopSearch', 'qs_title');
487
-
488
-        foreach ($facets as $facet) {
489
-            switch ($facet->Type) {
490
-                case ShopSearch::FACET_TYPE_RANGE:
491
-                    $params = array_merge($baseParams, array());
492
-                    if (!isset($params[$qs_f])) {
493
-                        $params[$qs_f] = array();
494
-                    }
495
-                    $params[$qs_f][$facet->Source] = 'RANGEFACETVALUE';
496
-                    $params[$qs_t] = $facet->Label . ': RANGEFACETLABEL';
497
-                    $facet->Link = $baseLink . '?' . http_build_query($params);
498
-                break;
499
-
500
-                case ShopSearch::FACET_TYPE_CHECKBOX;
501
-                    $facet->LinkDetails = json_encode(array(
502
-                        'filter'    => $qs_f,
503
-                        'source'    => $facet->Source,
504
-                        'leaves'    => $facet->FilterOnlyLeaves,
505
-                    ));
506
-
507
-                    // fall through on purpose
508
-
509
-                default:
510
-                    foreach ($facet->Values as $value) {
511
-                        // make a copy of the existing params
512
-                        $params = array_merge($baseParams, array());
513
-
514
-                        // add the filter for this value
515
-                        if (!isset($params[$qs_f])) {
516
-                            $params[$qs_f] = array();
517
-                        }
518
-                        if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
519
-                            unset($params[$qs_f][$facet->Source]); // this will be figured out via javascript
520
-                            $params[$qs_t] = ($value->Active ? 'Remove ' : '') . $facet->Label . ': ' . $value->Label;
521
-                        } else {
522
-                            $params[$qs_f][$facet->Source] = $value->Value;
523
-                            $params[$qs_t] = $facet->Label . ': ' . $value->Label;
524
-                        }
525
-
526
-                        // build a new link
527
-                        $value->Link = $baseLink . '?' . http_build_query($params);
528
-                    }
529
-            }
530
-        }
531
-
532
-        return $facets;
533
-    }
534
-
535
-
536
-    /**
537
-     * @param ArrayList $children
538
-     * @return array
539
-     */
540
-    protected function getRecursiveChildValues(ArrayList $children)
541
-    {
542
-        $out = array();
543
-
544
-        foreach ($children as $child) {
545
-            $out[$child->Value] = $child->Value;
546
-            if (!empty($child->Children)) {
547
-                $out += $this->getRecursiveChildValues($child->Children);
548
-            }
549
-        }
550
-
551
-        return $out;
552
-    }
553
-
554
-
555
-    /**
556
-     * For checkbox and range facets, this updates the state (checked and min/max)
557
-     * based on current filter values.
558
-     *
559
-     * @param ArrayList $facets
560
-     * @param array     $filters
561
-     * @return ArrayList
562
-     */
563
-    public function updateFacetState(ArrayList $facets, array $filters)
564
-    {
565
-        foreach ($facets as $facet) {
566
-            if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
567
-                if (empty($filters[$facet->Source])) {
568
-                    // If the filter is not being used at all, we count
569
-                    // all values as active.
570
-                    foreach ($facet->Values as $value) {
571
-                        $value->Active = (bool)FacetHelper::config()->default_checkbox_state;
572
-                    }
573
-                } else {
574
-                    $filterVals = $filters[$facet->Source];
575
-                    if (!is_array($filterVals)) {
576
-                        $filterVals = array($filterVals);
577
-                    }
578
-                    $this->updateCheckboxFacetState(
579
-                        !empty($facet->NestedValues) ? $facet->NestedValues : $facet->Values,
580
-                        $filterVals,
581
-                        !empty($facet->FilterOnlyLeaves));
582
-                }
583
-            } elseif ($facet->Type == ShopSearch::FACET_TYPE_RANGE) {
584
-                if (!empty($filters[$facet->Source]) && preg_match('/^RANGE\~(.+)\~(.+)$/', $filters[$facet->Source], $m)) {
585
-                    $facet->MinValue = $m[1];
586
-                    $facet->MaxValue = $m[2];
587
-                }
588
-            }
589
-        }
590
-
591
-        return $facets;
592
-    }
593
-
594
-
595
-    /**
596
-     * For checkboxes, updates the state based on filters. Handles hierarchies and FilterOnlyLeaves
597
-     * @param ArrayList $values
598
-     * @param array     $filterVals
599
-     * @param bool      $filterOnlyLeaves [optional]
600
-     * @return bool - true if any of the children are true, false if all children are false
601
-     */
602
-    protected function updateCheckboxFacetState(ArrayList $values, array $filterVals, $filterOnlyLeaves=false)
603
-    {
604
-        $out = false;
605
-
606
-        foreach ($values as $value) {
607
-            if ($filterOnlyLeaves && !empty($value->Children)) {
608
-                if (in_array($value->Value, $filterVals)) {
609
-                    // This wouldn't be normal, but even if it's not a leaf, we want to handle
610
-                    // the case where a filter might be set for this node. It should still show up correctly.
611
-                    $value->Active = true;
612
-                    foreach ($value->Children as $c) {
613
-                        $c->Active = true;
614
-                    }
615
-                    // TODO: handle more than one level of recursion here
616
-                } else {
617
-                    $value->Active = $this->updateCheckboxFacetState($value->Children, $filterVals, $filterOnlyLeaves);
618
-                }
619
-            } else {
620
-                $value->Active = in_array($value->Value, $filterVals);
621
-            }
622
-
623
-            if ($value->Active) {
624
-                $out = true;
625
-            }
626
-        }
627
-
628
-        return $out;
629
-    }
630
-
631
-
632
-    /**
633
-     * If there are any facets (link or checkbox) that have a HierarchyDivider field
634
-     * in the spec, transform them into a hierarchy so they can be displayed as such.
635
-     *
636
-     * @param ArrayList $facets
637
-     * @return ArrayList
638
-     */
639
-    public function transformHierarchies(ArrayList $facets)
640
-    {
641
-        foreach ($facets as $facet) {
642
-            if (!empty($facet->HierarchyDivider)) {
643
-                $out = new ArrayList();
644
-                $parentStack = array();
645
-
646
-                foreach ($facet->Values as $value) {
647
-                    if (empty($value->Label)) {
648
-                        continue;
649
-                    }
650
-                    $value->FullLabel = $value->Label;
651
-
652
-                    // Look for the most recent parent that matches the beginning of this one
653
-                    while (count($parentStack) > 0) {
654
-                        $curParent = $parentStack[ count($parentStack)-1 ];
655
-                        if (strpos($value->Label, $curParent->FullLabel) === 0) {
656
-                            if (!isset($curParent->Children)) {
657
-                                $curParent->Children = new ArrayList();
658
-                            }
659
-
660
-                            // Modify the name so we only show the last component
661
-                            $value->FullLabel = $value->Label;
662
-                            $p = strrpos($value->Label, $facet->HierarchyDivider);
663
-                            if ($p > -1) {
664
-                                $value->Label = trim(substr($value->Label, $p + 1));
665
-                            }
666
-
667
-                            $curParent->Children->push($value);
668
-                            break;
669
-                        } else {
670
-                            array_pop($parentStack);
671
-                        }
672
-                    }
673
-
674
-                    // If we went all the way back to the root without a match, this is
675
-                    // a new parent item
676
-                    if (count($parentStack) == 0) {
677
-                        $out->push($value);
678
-                    }
679
-
680
-                    // Each item could be a potential parent. If it's not it will get popped
681
-                    // immediately on the next iteration
682
-                    $parentStack[] = $value;
683
-                }
684
-
685
-                $facet->NestedValues = $out;
686
-            }
687
-        }
688
-
689
-        return $facets;
690
-    }
88
+					// TODO: This logic should be something like the above, but I don't know
89
+					// how to get the join table from a singleton (which returns an UnsavedRelationList
90
+					// instead of a ManyManyList). I've got a deadline to meet, though, so this
91
+					// will catch the majority of cases as long as the extension is applied to the
92
+					// Product class instead of a subclass.
93
+					$list = $list
94
+						->innerJoin('Product_StaticAttributeTypes', "\"SiteTree\".\"ID\" = \"Product_StaticAttributeTypes\".\"ProductID\"")
95
+						->innerJoin('ProductAttributeValue', "\"Product_StaticAttributeTypes\".\"ProductAttributeTypeID\" = \"ProductAttributeValue\".\"TypeID\"")
96
+						->innerJoin('Product_StaticAttributeValues', "\"SiteTree\".\"ID\" = \"Product_StaticAttributeValues\".\"ProductID\" AND \"ProductAttributeValue\".\"ID\" = \"Product_StaticAttributeValues\".\"ProductAttributeValueID\"")
97
+						->filter("Product_StaticAttributeValues.ProductAttributeValueID", $filterVal);
98
+				} else {
99
+					$list = $list->filter($this->processFilterField($sing, $filterField, $filterVal));
100
+				}
101
+			}
102
+		}
103
+
104
+		return $list;
105
+	}
106
+
107
+
108
+	/**
109
+	 * @param DataObject $rec           This would normally just be a singleton but we don't want to have to create it over and over
110
+	 * @param string     $filterField
111
+	 * @param mixed      $filterVal
112
+	 * @return array - returns the new filter added
113
+	 */
114
+	public function processFilterField($rec, $filterField, $filterVal)
115
+	{
116
+		// First check for VFI fields
117
+		if ($rec->hasExtension('VirtualFieldIndex') && ($spec = $rec->getVFISpec($filterField))) {
118
+			if ($spec['Type'] == VirtualFieldIndex::TYPE_LIST) {
119
+				// Lists have to be handled a little differently
120
+				$f = $rec->getVFIFieldName($filterField) . ':PartialMatch';
121
+				if (is_array($filterVal)) {
122
+					foreach ($filterVal as &$val) {
123
+						$val = '|' . $val . '|';
124
+					}
125
+					return array($f => $filterVal);
126
+				} else {
127
+					return array($f => '|' . $filterVal . '|');
128
+				}
129
+			} else {
130
+				// Simples are simple
131
+				$filterField = $rec->getVFIFieldName($filterField);
132
+			}
133
+		}
134
+
135
+		// Next check for regular db fields
136
+		if ($rec->dbObject($filterField)) {
137
+			// Is it a range value?
138
+			if (is_string($filterVal) && preg_match('/^RANGE\~(.+)\~(.+)$/', $filterVal, $m)) {
139
+				$filterField .= ':Between';
140
+				$filterVal = array_slice($m, 1, 2);
141
+			}
142
+
143
+			return array($filterField => $filterVal);
144
+		}
145
+
146
+		return array();
147
+	}
148
+
149
+
150
+	/**
151
+	 * Processes the facet spec and removes any shorthand (field => label).
152
+	 * @param array $facetSpec
153
+	 * @return array
154
+	 */
155
+	public function expandFacetSpec(array $facetSpec)
156
+	{
157
+		if (is_null($facetSpec)) {
158
+			return array();
159
+		}
160
+		$facets = array();
161
+
162
+		foreach ($facetSpec as $field => $label) {
163
+			if (is_array($label)) {
164
+				$facets[$field] = $label;
165
+			} else {
166
+				$facets[$field] = array('Label' => $label);
167
+			}
168
+
169
+			if (empty($facets[$field]['Source'])) {
170
+				$facets[$field]['Source'] = $field;
171
+			}
172
+			if (empty($facets[$field]['Type'])) {
173
+				$facets[$field]['Type']  = ShopSearch::FACET_TYPE_LINK;
174
+			}
175
+
176
+			if (empty($facets[$field]['Values'])) {
177
+				$facets[$field]['Values'] = array();
178
+			} else {
179
+				$vals = $facets[$field]['Values'];
180
+				if (is_string($vals)) {
181
+					$vals = eval('return ' . $vals . ';');
182
+				}
183
+				$facets[$field]['Values'] = array();
184
+				foreach ($vals as $val => $lbl) {
185
+					$facets[$field]['Values'][$val] = new ArrayData(array(
186
+						'Label'     => $lbl,
187
+						'Value'     => $val,
188
+						'Count'     => 0,
189
+					));
190
+				}
191
+			}
192
+		}
193
+
194
+		return $facets;
195
+	}
196
+
197
+
198
+	/**
199
+	 * This is super-slow. I'm assuming if you're using facets you
200
+	 * probably also ought to be using Solr or something else. Or
201
+	 * maybe you have unlimited time and can refactor this feature
202
+	 * and submit a pull request...
203
+	 *
204
+	 * TODO: If this is going to be used for categories we're going
205
+	 * to have to really clean it up and speed it up.
206
+	 * Suggestion:
207
+	 *  - option to turn off counts
208
+	 *  - switch order of nested array so we don't go through results unless needed
209
+	 *  - if not doing counts, min/max and link facets can be handled w/ queries
210
+	 *  - separate that bit out into a new function
211
+	 * NOTE: This is partially done with the "faster_faceting" config
212
+	 * option but more could be done, particularly by covering link facets as well.
213
+	 *
214
+	 * Output - list of ArrayData in the format:
215
+	 *   Label - name of the facet
216
+	 *   Source - field name of the facet
217
+	 *   Type - one of the ShopSearch::FACET_TYPE_XXXX constants
218
+	 *   Values - SS_List of possible values for this facet
219
+	 *
220
+	 * @param SS_List $matches
221
+	 * @param array $facetSpec
222
+	 * @param bool $autoFacetAttributes [optional]
223
+	 * @return ArrayList
224
+	 */
225
+	public function buildFacets(SS_List $matches, array $facetSpec, $autoFacetAttributes=false)
226
+	{
227
+		$facets = $this->expandFacetSpec($facetSpec);
228
+		if (!$autoFacetAttributes && (empty($facets) || !$matches || !$matches->count())) {
229
+			return new ArrayList();
230
+		}
231
+		$fasterMethod = (bool)$this->config()->faster_faceting;
232
+
233
+		// fill them in
234
+		foreach ($facets as $field => &$facet) {
235
+			if (preg_match(self::config()->attribute_facet_regex, $field, $m)) {
236
+				$this->buildAttributeFacet($matches, $facet, $m[1]);
237
+				continue;
238
+			}
239
+
240
+			// NOTE: using this method range and checkbox facets don't get counts
241
+			if ($fasterMethod && $facet['Type'] != ShopSearch::FACET_TYPE_LINK) {
242
+				if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
243
+					if (isset($facet['RangeMin'])) {
244
+						$facet['MinValue'] = $facet['RangeMin'];
245
+					}
246
+					if (isset($facet['RangeMax'])) {
247
+						$facet['MaxValue'] = $facet['RangeMax'];
248
+					}
249
+				}
250
+
251
+				continue;
252
+			}
253
+
254
+			foreach ($matches as $rec) {
255
+				// If it's a range facet, set up the min/max
256
+				if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
257
+					if (isset($facet['RangeMin'])) {
258
+						$facet['MinValue'] = $facet['RangeMin'];
259
+					}
260
+					if (isset($facet['RangeMax'])) {
261
+						$facet['MaxValue'] = $facet['RangeMax'];
262
+					}
263
+				}
264
+
265
+				// If the field is accessible via normal methods, including
266
+				// a user-defined getter, prefer that
267
+				$fieldValue = $rec->relObject($field);
268
+				if (is_null($fieldValue) && $rec->hasMethod($meth = "get{$field}")) {
269
+					$fieldValue = $rec->$meth();
270
+				}
271
+
272
+				// If not, look for a VFI field
273
+				if (!$fieldValue && $rec->hasExtension('VirtualFieldIndex')) {
274
+					$fieldValue = $rec->getVFI($field);
275
+				}
276
+
277
+				// If we found something, process it
278
+				if (!empty($fieldValue)) {
279
+					// normalize so that it's iterable
280
+					if (!is_array($fieldValue) && !$fieldValue instanceof SS_List) {
281
+						$fieldValue = array($fieldValue);
282
+					}
283
+
284
+					foreach ($fieldValue as $obj) {
285
+						if (empty($obj)) {
286
+							continue;
287
+						}
288
+
289
+						// figure out the right label
290
+						if (is_object($obj) && $obj->hasMethod('Nice')) {
291
+							$lbl = $obj->Nice();
292
+						} elseif (is_object($obj) && !empty($obj->Title)) {
293
+							$lbl = $obj->Title;
294
+						} elseif (
295
+							is_numeric($obj) &&
296
+							!empty($facet['LabelFormat']) &&
297
+							$facet['LabelFormat'] === 'Currency' &&
298
+							$facet['Type'] !== ShopSearch::FACET_TYPE_RANGE // this one handles it via javascript
299
+						) {
300
+							$tmp = Currency::create($field);
301
+							$tmp->setValue($obj);
302
+							$lbl = $tmp->Nice();
303
+						} else {
304
+							$lbl = (string)$obj;
305
+						}
306
+
307
+						// figure out the value for sorting
308
+						if (is_object($obj) && $obj->hasMethod('getAmount')) {
309
+							$val = $obj->getAmount();
310
+						} elseif (is_object($obj) && !empty($obj->ID)) {
311
+							$val = $obj->ID;
312
+						} else {
313
+							$val = (string)$obj;
314
+						}
315
+
316
+						// if it's a range facet, calculate the min and max
317
+						if ($facet['Type'] == ShopSearch::FACET_TYPE_RANGE) {
318
+							if (!isset($facet['MinValue']) || $val < $facet['MinValue']) {
319
+								$facet['MinValue'] = $val;
320
+								$facet['MinLabel'] = $lbl;
321
+							}
322
+							if (!isset($facet['RangeMin']) || $val < $facet['RangeMin']) {
323
+								$facet['RangeMin'] = $val;
324
+							}
325
+							if (!isset($facet['MaxValue']) || $val > $facet['MaxValue']) {
326
+								$facet['MaxValue'] = $val;
327
+								$facet['MaxLabel'] = $lbl;
328
+							}
329
+							if (!isset($facet['RangeMax']) || $val > $facet['RangeMax']) {
330
+								$facet['RangeMax'] = $val;
331
+							}
332
+						}
333
+
334
+						// Tally the value in the facets
335
+						if (!isset($facet['Values'][$val])) {
336
+							$facet['Values'][$val] = new ArrayData(array(
337
+								'Label'     => $lbl,
338
+								'Value'     => $val,
339
+								'Count'     => 1,
340
+							));
341
+						} elseif ($facet['Values'][$val]) {
342
+							$facet['Values'][$val]->Count++;
343
+						}
344
+					}
345
+				}
346
+			}
347
+		}
348
+
349
+		// if we're auto-building the facets based on attributes,
350
+		if ($autoFacetAttributes) {
351
+			$facets = array_merge($this->buildAllAttributeFacets($matches), $facets);
352
+		}
353
+
354
+		// convert values to arraylist
355
+		$out = new ArrayList();
356
+		$sortValues = self::config()->sort_facet_values;
357
+		foreach ($facets as $f) {
358
+			if ($sortValues) {
359
+				ksort($f['Values']);
360
+			}
361
+			$f['Values'] = new ArrayList($f['Values']);
362
+			$out->push(new ArrayData($f));
363
+		}
364
+
365
+		return $out;
366
+	}
367
+
368
+
369
+	/**
370
+	 * NOTE: this will break if applied to something that's not a SiteTree subclass.
371
+	 * @param DataList|PaginatedList $matches
372
+	 * @param array $facet
373
+	 * @param int $typeID
374
+	 */
375
+	protected function buildAttributeFacet($matches, array &$facet, $typeID)
376
+	{
377
+		$q = $matches instanceof PaginatedList ? $matches->getList()->dataQuery()->query() : $matches->dataQuery()->query();
378
+
379
+		if (empty($facet['Label'])) {
380
+			$type = ProductAttributeType::get()->byID($typeID);
381
+			$facet['Label'] = $type->Label;
382
+		}
383
+
384
+		$baseTable = $q->getFrom();
385
+		if (is_array($baseTable)) {
386
+			$baseTable = reset($baseTable);
387
+		}
388
+
389
+		$q = $q->setSelect(array())
390
+			->selectField('"ProductAttributeValue"."ID"', 'Value')
391
+			->selectField('"ProductAttributeValue"."Value"', 'Label')
392
+			->selectField('count(distinct '.$baseTable.'."ID")', 'Count')
393
+			->selectField('"ProductAttributeValue"."Sort"')
394
+			->addInnerJoin('Product_StaticAttributeValues', $baseTable.'."ID" = "Product_StaticAttributeValues"."ProductID"')
395
+			->addInnerJoin('ProductAttributeValue', '"Product_StaticAttributeValues"."ProductAttributeValueID" = "ProductAttributeValue"."ID"')
396
+			->addWhere(sprintf("\"ProductAttributeValue\".\"TypeID\" = '%d'", $typeID))
397
+			->setOrderBy('"ProductAttributeValue"."Sort"', 'ASC')
398
+			->setGroupBy('"ProductAttributeValue"."ID"')
399
+			->execute()
400
+		;
401
+
402
+		$facet['Values'] = array();
403
+		foreach ($q as $row) {
404
+			$facet['Values'][ $row['Value'] ] = new ArrayData($row);
405
+		}
406
+	}
407
+
408
+
409
+	/**
410
+	 * Builds facets from all attributes present in the data set.
411
+	 * @param DataList|PaginatedList $matches
412
+	 * @return array
413
+	 */
414
+	protected function buildAllAttributeFacets($matches)
415
+	{
416
+		$q = $matches instanceof PaginatedList ? $matches->getList()->dataQuery()->query() : $matches->dataQuery()->query();
417
+
418
+		// this is the easiest way to get SiteTree vs SiteTree_Live
419
+		$baseTable = $q->getFrom();
420
+		if (is_array($baseTable)) {
421
+			$baseTable = reset($baseTable);
422
+		}
423
+
424
+		$q = $q->setSelect(array())
425
+			->selectField('"ProductAttributeType"."ID"', 'TypeID')
426
+			->selectField('"ProductAttributeType"."Label"', 'TypeLabel')
427
+			->selectField('"ProductAttributeValue"."ID"', 'Value')
428
+			->selectField('"ProductAttributeValue"."Value"', 'Label')
429
+			->selectField('count(distinct '.$baseTable.'."ID")', 'Count')
430
+			->selectField('"ProductAttributeValue"."Sort"')
431
+			->addInnerJoin('Product_StaticAttributeTypes', $baseTable.'."ID" = "Product_StaticAttributeTypes"."ProductID"')
432
+			->addInnerJoin('ProductAttributeType', '"Product_StaticAttributeTypes"."ProductAttributeTypeID" = "ProductAttributeType"."ID"')
433
+			->addInnerJoin('Product_StaticAttributeValues', $baseTable.'."ID" = "Product_StaticAttributeValues"."ProductID"')
434
+			->addInnerJoin('ProductAttributeValue', '"Product_StaticAttributeValues"."ProductAttributeValueID" = "ProductAttributeValue"."ID"'
435
+				. ' AND "ProductAttributeValue"."TypeID" = "ProductAttributeType"."ID"')
436
+			->setOrderBy(array(
437
+				'"ProductAttributeType"."Label"' => 'ASC',
438
+				'"ProductAttributeValue"."Sort"' => 'ASC',
439
+			))
440
+			->setGroupBy(array('"ProductAttributeValue"."ID"', '"ProductAttributeType"."ID"'))
441
+			->execute()
442
+		;
443
+
444
+
445
+		$curType  = 0;
446
+		$facets   = array();
447
+		$curFacet = null;
448
+		foreach ($q as $row) {
449
+			if ($curType != $row['TypeID']) {
450
+				if ($curType > 0) {
451
+					$facets['ATT'.$curType] = $curFacet;
452
+				}
453
+				$curType = $row['TypeID'];
454
+				$curFacet = array(
455
+					'Label'  => $row['TypeLabel'],
456
+					'Source' => 'ATT'.$curType,
457
+					'Type'   => ShopSearch::FACET_TYPE_LINK,
458
+					'Values' => array(),
459
+				);
460
+			}
461
+
462
+			unset($row['TypeID']);
463
+			unset($row['TypeLabel']);
464
+			$curFacet['Values'][ $row['Value'] ] = new ArrayData($row);
465
+		}
466
+
467
+		if ($curType > 0) {
468
+			$facets['ATT'.$curType] = $curFacet;
469
+		}
470
+		return $facets;
471
+	}
472
+
473
+
474
+	/**
475
+	 * Inserts a "Link" field into the values for each facet which can be
476
+	 * used to get a filtered search based on that facets
477
+	 *
478
+	 * @param ArrayList $facets
479
+	 * @param array     $baseParams
480
+	 * @param string    $baseLink
481
+	 * @return ArrayList
482
+	 */
483
+	public function insertFacetLinks(ArrayList $facets, array $baseParams, $baseLink)
484
+	{
485
+		$qs_f   = Config::inst()->get('ShopSearch', 'qs_filters');
486
+		$qs_t   = Config::inst()->get('ShopSearch', 'qs_title');
487
+
488
+		foreach ($facets as $facet) {
489
+			switch ($facet->Type) {
490
+				case ShopSearch::FACET_TYPE_RANGE:
491
+					$params = array_merge($baseParams, array());
492
+					if (!isset($params[$qs_f])) {
493
+						$params[$qs_f] = array();
494
+					}
495
+					$params[$qs_f][$facet->Source] = 'RANGEFACETVALUE';
496
+					$params[$qs_t] = $facet->Label . ': RANGEFACETLABEL';
497
+					$facet->Link = $baseLink . '?' . http_build_query($params);
498
+				break;
499
+
500
+				case ShopSearch::FACET_TYPE_CHECKBOX;
501
+					$facet->LinkDetails = json_encode(array(
502
+						'filter'    => $qs_f,
503
+						'source'    => $facet->Source,
504
+						'leaves'    => $facet->FilterOnlyLeaves,
505
+					));
506
+
507
+					// fall through on purpose
508
+
509
+				default:
510
+					foreach ($facet->Values as $value) {
511
+						// make a copy of the existing params
512
+						$params = array_merge($baseParams, array());
513
+
514
+						// add the filter for this value
515
+						if (!isset($params[$qs_f])) {
516
+							$params[$qs_f] = array();
517
+						}
518
+						if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
519
+							unset($params[$qs_f][$facet->Source]); // this will be figured out via javascript
520
+							$params[$qs_t] = ($value->Active ? 'Remove ' : '') . $facet->Label . ': ' . $value->Label;
521
+						} else {
522
+							$params[$qs_f][$facet->Source] = $value->Value;
523
+							$params[$qs_t] = $facet->Label . ': ' . $value->Label;
524
+						}
525
+
526
+						// build a new link
527
+						$value->Link = $baseLink . '?' . http_build_query($params);
528
+					}
529
+			}
530
+		}
531
+
532
+		return $facets;
533
+	}
534
+
535
+
536
+	/**
537
+	 * @param ArrayList $children
538
+	 * @return array
539
+	 */
540
+	protected function getRecursiveChildValues(ArrayList $children)
541
+	{
542
+		$out = array();
543
+
544
+		foreach ($children as $child) {
545
+			$out[$child->Value] = $child->Value;
546
+			if (!empty($child->Children)) {
547
+				$out += $this->getRecursiveChildValues($child->Children);
548
+			}
549
+		}
550
+
551
+		return $out;
552
+	}
553
+
554
+
555
+	/**
556
+	 * For checkbox and range facets, this updates the state (checked and min/max)
557
+	 * based on current filter values.
558
+	 *
559
+	 * @param ArrayList $facets
560
+	 * @param array     $filters
561
+	 * @return ArrayList
562
+	 */
563
+	public function updateFacetState(ArrayList $facets, array $filters)
564
+	{
565
+		foreach ($facets as $facet) {
566
+			if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
567
+				if (empty($filters[$facet->Source])) {
568
+					// If the filter is not being used at all, we count
569
+					// all values as active.
570
+					foreach ($facet->Values as $value) {
571
+						$value->Active = (bool)FacetHelper::config()->default_checkbox_state;
572
+					}
573
+				} else {
574
+					$filterVals = $filters[$facet->Source];
575
+					if (!is_array($filterVals)) {
576
+						$filterVals = array($filterVals);
577
+					}
578
+					$this->updateCheckboxFacetState(
579
+						!empty($facet->NestedValues) ? $facet->NestedValues : $facet->Values,
580
+						$filterVals,
581
+						!empty($facet->FilterOnlyLeaves));
582
+				}
583
+			} elseif ($facet->Type == ShopSearch::FACET_TYPE_RANGE) {
584
+				if (!empty($filters[$facet->Source]) && preg_match('/^RANGE\~(.+)\~(.+)$/', $filters[$facet->Source], $m)) {
585
+					$facet->MinValue = $m[1];
586
+					$facet->MaxValue = $m[2];
587
+				}
588
+			}
589
+		}
590
+
591
+		return $facets;
592
+	}
593
+
594
+
595
+	/**
596
+	 * For checkboxes, updates the state based on filters. Handles hierarchies and FilterOnlyLeaves
597
+	 * @param ArrayList $values
598
+	 * @param array     $filterVals
599
+	 * @param bool      $filterOnlyLeaves [optional]
600
+	 * @return bool - true if any of the children are true, false if all children are false
601
+	 */
602
+	protected function updateCheckboxFacetState(ArrayList $values, array $filterVals, $filterOnlyLeaves=false)
603
+	{
604
+		$out = false;
605
+
606
+		foreach ($values as $value) {
607
+			if ($filterOnlyLeaves && !empty($value->Children)) {
608
+				if (in_array($value->Value, $filterVals)) {
609
+					// This wouldn't be normal, but even if it's not a leaf, we want to handle
610
+					// the case where a filter might be set for this node. It should still show up correctly.
611
+					$value->Active = true;
612
+					foreach ($value->Children as $c) {
613
+						$c->Active = true;
614
+					}
615
+					// TODO: handle more than one level of recursion here
616
+				} else {
617
+					$value->Active = $this->updateCheckboxFacetState($value->Children, $filterVals, $filterOnlyLeaves);
618
+				}
619
+			} else {
620
+				$value->Active = in_array($value->Value, $filterVals);
621
+			}
622
+
623
+			if ($value->Active) {
624
+				$out = true;
625
+			}
626
+		}
627
+
628
+		return $out;
629
+	}
630
+
631
+
632
+	/**
633
+	 * If there are any facets (link or checkbox) that have a HierarchyDivider field
634
+	 * in the spec, transform them into a hierarchy so they can be displayed as such.
635
+	 *
636
+	 * @param ArrayList $facets
637
+	 * @return ArrayList
638
+	 */
639
+	public function transformHierarchies(ArrayList $facets)
640
+	{
641
+		foreach ($facets as $facet) {
642
+			if (!empty($facet->HierarchyDivider)) {
643
+				$out = new ArrayList();
644
+				$parentStack = array();
645
+
646
+				foreach ($facet->Values as $value) {
647
+					if (empty($value->Label)) {
648
+						continue;
649
+					}
650
+					$value->FullLabel = $value->Label;
651
+
652
+					// Look for the most recent parent that matches the beginning of this one
653
+					while (count($parentStack) > 0) {
654
+						$curParent = $parentStack[ count($parentStack)-1 ];
655
+						if (strpos($value->Label, $curParent->FullLabel) === 0) {
656
+							if (!isset($curParent->Children)) {
657
+								$curParent->Children = new ArrayList();
658
+							}
659
+
660
+							// Modify the name so we only show the last component
661
+							$value->FullLabel = $value->Label;
662
+							$p = strrpos($value->Label, $facet->HierarchyDivider);
663
+							if ($p > -1) {
664
+								$value->Label = trim(substr($value->Label, $p + 1));
665
+							}
666
+
667
+							$curParent->Children->push($value);
668
+							break;
669
+						} else {
670
+							array_pop($parentStack);
671
+						}
672
+					}
673
+
674
+					// If we went all the way back to the root without a match, this is
675
+					// a new parent item
676
+					if (count($parentStack) == 0) {
677
+						$out->push($value);
678
+					}
679
+
680
+					// Each item could be a potential parent. If it's not it will get popped
681
+					// immediately on the next iteration
682
+					$parentStack[] = $value;
683
+				}
684
+
685
+				$facet->NestedValues = $out;
686
+			}
687
+		}
688
+
689
+		return $facets;
690
+	}
691 691
 }
Please login to merge, or discard this patch.
Spacing   +16 added lines, -16 removed lines patch added patch discarded remove patch
@@ -67,7 +67,7 @@  discard block
 block discarded – undo
67 67
      * @param DataObject|string $sing - just a singleton object we can get information off of
68 68
      * @return DataList
69 69
      */
70
-    public function addFiltersToDataList($list, array $filters, $sing=null)
70
+    public function addFiltersToDataList($list, array $filters, $sing = null)
71 71
     {
72 72
         if (!$sing) {
73 73
             $sing = singleton($list->dataClass());
@@ -117,14 +117,14 @@  discard block
 block discarded – undo
117 117
         if ($rec->hasExtension('VirtualFieldIndex') && ($spec = $rec->getVFISpec($filterField))) {
118 118
             if ($spec['Type'] == VirtualFieldIndex::TYPE_LIST) {
119 119
                 // Lists have to be handled a little differently
120
-                $f = $rec->getVFIFieldName($filterField) . ':PartialMatch';
120
+                $f = $rec->getVFIFieldName($filterField).':PartialMatch';
121 121
                 if (is_array($filterVal)) {
122 122
                     foreach ($filterVal as &$val) {
123
-                        $val = '|' . $val . '|';
123
+                        $val = '|'.$val.'|';
124 124
                     }
125 125
                     return array($f => $filterVal);
126 126
                 } else {
127
-                    return array($f => '|' . $filterVal . '|');
127
+                    return array($f => '|'.$filterVal.'|');
128 128
                 }
129 129
             } else {
130 130
                 // Simples are simple
@@ -170,7 +170,7 @@  discard block
 block discarded – undo
170 170
                 $facets[$field]['Source'] = $field;
171 171
             }
172 172
             if (empty($facets[$field]['Type'])) {
173
-                $facets[$field]['Type']  = ShopSearch::FACET_TYPE_LINK;
173
+                $facets[$field]['Type'] = ShopSearch::FACET_TYPE_LINK;
174 174
             }
175 175
 
176 176
             if (empty($facets[$field]['Values'])) {
@@ -178,7 +178,7 @@  discard block
 block discarded – undo
178 178
             } else {
179 179
                 $vals = $facets[$field]['Values'];
180 180
                 if (is_string($vals)) {
181
-                    $vals = eval('return ' . $vals . ';');
181
+                    $vals = eval('return '.$vals.';');
182 182
                 }
183 183
                 $facets[$field]['Values'] = array();
184 184
                 foreach ($vals as $val => $lbl) {
@@ -222,7 +222,7 @@  discard block
 block discarded – undo
222 222
      * @param bool $autoFacetAttributes [optional]
223 223
      * @return ArrayList
224 224
      */
225
-    public function buildFacets(SS_List $matches, array $facetSpec, $autoFacetAttributes=false)
225
+    public function buildFacets(SS_List $matches, array $facetSpec, $autoFacetAttributes = false)
226 226
     {
227 227
         $facets = $this->expandFacetSpec($facetSpec);
228 228
         if (!$autoFacetAttributes && (empty($facets) || !$matches || !$matches->count())) {
@@ -401,7 +401,7 @@  discard block
 block discarded – undo
401 401
 
402 402
         $facet['Values'] = array();
403 403
         foreach ($q as $row) {
404
-            $facet['Values'][ $row['Value'] ] = new ArrayData($row);
404
+            $facet['Values'][$row['Value']] = new ArrayData($row);
405 405
         }
406 406
     }
407 407
 
@@ -461,7 +461,7 @@  discard block
 block discarded – undo
461 461
 
462 462
             unset($row['TypeID']);
463 463
             unset($row['TypeLabel']);
464
-            $curFacet['Values'][ $row['Value'] ] = new ArrayData($row);
464
+            $curFacet['Values'][$row['Value']] = new ArrayData($row);
465 465
         }
466 466
 
467 467
         if ($curType > 0) {
@@ -493,8 +493,8 @@  discard block
 block discarded – undo
493 493
                         $params[$qs_f] = array();
494 494
                     }
495 495
                     $params[$qs_f][$facet->Source] = 'RANGEFACETVALUE';
496
-                    $params[$qs_t] = $facet->Label . ': RANGEFACETLABEL';
497
-                    $facet->Link = $baseLink . '?' . http_build_query($params);
496
+                    $params[$qs_t] = $facet->Label.': RANGEFACETLABEL';
497
+                    $facet->Link = $baseLink.'?'.http_build_query($params);
498 498
                 break;
499 499
 
500 500
                 case ShopSearch::FACET_TYPE_CHECKBOX;
@@ -517,14 +517,14 @@  discard block
 block discarded – undo
517 517
                         }
518 518
                         if ($facet->Type == ShopSearch::FACET_TYPE_CHECKBOX) {
519 519
                             unset($params[$qs_f][$facet->Source]); // this will be figured out via javascript
520
-                            $params[$qs_t] = ($value->Active ? 'Remove ' : '') . $facet->Label . ': ' . $value->Label;
520
+                            $params[$qs_t] = ($value->Active ? 'Remove ' : '').$facet->Label.': '.$value->Label;
521 521
                         } else {
522 522
                             $params[$qs_f][$facet->Source] = $value->Value;
523
-                            $params[$qs_t] = $facet->Label . ': ' . $value->Label;
523
+                            $params[$qs_t] = $facet->Label.': '.$value->Label;
524 524
                         }
525 525
 
526 526
                         // build a new link
527
-                        $value->Link = $baseLink . '?' . http_build_query($params);
527
+                        $value->Link = $baseLink.'?'.http_build_query($params);
528 528
                     }
529 529
             }
530 530
         }
@@ -599,7 +599,7 @@  discard block
 block discarded – undo
599 599
      * @param bool      $filterOnlyLeaves [optional]
600 600
      * @return bool - true if any of the children are true, false if all children are false
601 601
      */
602
-    protected function updateCheckboxFacetState(ArrayList $values, array $filterVals, $filterOnlyLeaves=false)
602
+    protected function updateCheckboxFacetState(ArrayList $values, array $filterVals, $filterOnlyLeaves = false)
603 603
     {
604 604
         $out = false;
605 605
 
@@ -651,7 +651,7 @@  discard block
 block discarded – undo
651 651
 
652 652
                     // Look for the most recent parent that matches the beginning of this one
653 653
                     while (count($parentStack) > 0) {
654
-                        $curParent = $parentStack[ count($parentStack)-1 ];
654
+                        $curParent = $parentStack[count($parentStack) - 1];
655 655
                         if (strpos($value->Label, $curParent->FullLabel) === 0) {
656 656
                             if (!isset($curParent->Children)) {
657 657
                                 $curParent->Children = new ArrayList();
Please login to merge, or discard this patch.
code/helpers/HasStaticAttributes.php 3 patches
Doc Comments   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -50,7 +50,7 @@
 block discarded – undo
50 50
 
51 51
     /**
52 52
      * @param $typeID
53
-     * @return callable
53
+     * @return Closure
54 54
      */
55 55
     protected function getValuesClosure($typeID)
56 56
     {
Please login to merge, or discard this patch.
Indentation   +177 added lines, -177 removed lines patch added patch discarded remove patch
@@ -11,93 +11,93 @@  discard block
 block discarded – undo
11 11
  */
12 12
 class HasStaticAttributes extends DataExtension
13 13
 {
14
-    private static $many_many = array(
15
-        'StaticAttributeTypes'  => 'ProductAttributeType',
16
-        'StaticAttributeValues' => 'ProductAttributeValue',
17
-    );
18
-
19
-
20
-    /**
21
-     * Adds variations specific fields to the CMS.
22
-     */
23
-    public function updateCMSFields(FieldList $fields)
24
-    {
25
-        $fields->addFieldsToTab('Root.Attributes', array(
26
-            HeaderField::create('Applicable Attribute Types'),
27
-            CheckboxSetField::create('StaticAttributeTypes', 'Static Attribute Types', ProductAttributeType::get()->map("ID", "Title")),
28
-            LiteralField::create('staticattributehelp', '<p>Select any attributes that apply to this product and click Save.</p>'),
29
-            HeaderField::create('Attributes'),
30
-        ));
31
-
32
-        foreach ($this->owner->StaticAttributeTypes() as $type) {
33
-            $source = $this->getValuesClosure($type->ID);
34
-
35
-            $newValFields = FieldList::create(array(
36
-                TextField::create('Value', 'Label'),
37
-                HiddenField::create('TypeID', '', $type->ID),
38
-            ));
39
-
40
-            $newValReq = RequiredFields::create('Value');
41
-
42
-            $valuesField = HasStaticAttributes_CheckboxSetField::create('StaticAttributeValues-'.$type->ID, $type->Title, $source());
43
-            $valuesField->setValue($this->owner->StaticAttributeValues()->filter('TypeID', $type->ID)->getIDList());
44
-            $valuesField->useAddNew('ProductAttributeValue', $source, $newValFields, $newValReq);
45
-
46
-            $fields->addFieldToTab('Root.Attributes', $valuesField);
47
-        }
48
-    }
49
-
50
-
51
-    /**
52
-     * @param $typeID
53
-     * @return callable
54
-     */
55
-    protected function getValuesClosure($typeID)
56
-    {
57
-        return function () use ($typeID) {
58
-            return ProductAttributeValue::get()->filter('TypeID', $typeID)->map('ID', 'Value')->toArray();
59
-        };
60
-    }
61
-
62
-
63
-    /**
64
-     * @return ArrayList
65
-     */
66
-    public function StaticAttributes()
67
-    {
68
-        $list = array();
69
-
70
-        foreach ($this->owner->StaticAttributeTypes() as $type) {
71
-            $type->ActiveValues = new ArrayList();
72
-            $list[$type->ID] = $type;
73
-        }
74
-
75
-        foreach ($this->owner->StaticAttributeValues() as $val) {
76
-            if (!isset($list[$val->TypeID])) {
77
-                continue;
78
-            }
79
-            $list[$val->TypeID]->ActiveValues->push($val);
80
-        }
81
-
82
-        return new ArrayList($list);
83
-    }
84
-
85
-
86
-    /**
87
-     * Add the default attribute types if any
88
-     */
89
-    public function onBeforeWrite()
90
-    {
91
-        if (empty($this->owner->ID)) {
92
-            $defaultAttributes = Config::inst()->get($this->owner->ClassName, 'default_attributes');
93
-            if (!empty($defaultAttributes)) {
94
-                $types = $this->owner->StaticAttributeTypes();
95
-                foreach ($defaultAttributes as $typeID) {
96
-                    $types->add($typeID);
97
-                }
98
-            }
99
-        }
100
-    }
14
+	private static $many_many = array(
15
+		'StaticAttributeTypes'  => 'ProductAttributeType',
16
+		'StaticAttributeValues' => 'ProductAttributeValue',
17
+	);
18
+
19
+
20
+	/**
21
+	 * Adds variations specific fields to the CMS.
22
+	 */
23
+	public function updateCMSFields(FieldList $fields)
24
+	{
25
+		$fields->addFieldsToTab('Root.Attributes', array(
26
+			HeaderField::create('Applicable Attribute Types'),
27
+			CheckboxSetField::create('StaticAttributeTypes', 'Static Attribute Types', ProductAttributeType::get()->map("ID", "Title")),
28
+			LiteralField::create('staticattributehelp', '<p>Select any attributes that apply to this product and click Save.</p>'),
29
+			HeaderField::create('Attributes'),
30
+		));
31
+
32
+		foreach ($this->owner->StaticAttributeTypes() as $type) {
33
+			$source = $this->getValuesClosure($type->ID);
34
+
35
+			$newValFields = FieldList::create(array(
36
+				TextField::create('Value', 'Label'),
37
+				HiddenField::create('TypeID', '', $type->ID),
38
+			));
39
+
40
+			$newValReq = RequiredFields::create('Value');
41
+
42
+			$valuesField = HasStaticAttributes_CheckboxSetField::create('StaticAttributeValues-'.$type->ID, $type->Title, $source());
43
+			$valuesField->setValue($this->owner->StaticAttributeValues()->filter('TypeID', $type->ID)->getIDList());
44
+			$valuesField->useAddNew('ProductAttributeValue', $source, $newValFields, $newValReq);
45
+
46
+			$fields->addFieldToTab('Root.Attributes', $valuesField);
47
+		}
48
+	}
49
+
50
+
51
+	/**
52
+	 * @param $typeID
53
+	 * @return callable
54
+	 */
55
+	protected function getValuesClosure($typeID)
56
+	{
57
+		return function () use ($typeID) {
58
+			return ProductAttributeValue::get()->filter('TypeID', $typeID)->map('ID', 'Value')->toArray();
59
+		};
60
+	}
61
+
62
+
63
+	/**
64
+	 * @return ArrayList
65
+	 */
66
+	public function StaticAttributes()
67
+	{
68
+		$list = array();
69
+
70
+		foreach ($this->owner->StaticAttributeTypes() as $type) {
71
+			$type->ActiveValues = new ArrayList();
72
+			$list[$type->ID] = $type;
73
+		}
74
+
75
+		foreach ($this->owner->StaticAttributeValues() as $val) {
76
+			if (!isset($list[$val->TypeID])) {
77
+				continue;
78
+			}
79
+			$list[$val->TypeID]->ActiveValues->push($val);
80
+		}
81
+
82
+		return new ArrayList($list);
83
+	}
84
+
85
+
86
+	/**
87
+	 * Add the default attribute types if any
88
+	 */
89
+	public function onBeforeWrite()
90
+	{
91
+		if (empty($this->owner->ID)) {
92
+			$defaultAttributes = Config::inst()->get($this->owner->ClassName, 'default_attributes');
93
+			if (!empty($defaultAttributes)) {
94
+				$types = $this->owner->StaticAttributeTypes();
95
+				foreach ($defaultAttributes as $typeID) {
96
+					$types->add($typeID);
97
+				}
98
+			}
99
+		}
100
+	}
101 101
 }
102 102
 
103 103
 
@@ -110,96 +110,96 @@  discard block
 block discarded – undo
110 110
  */
111 111
 class HasStaticAttributes_CheckboxSetField extends CheckboxSetField
112 112
 {
113
-    /**
114
-     * @return int
115
-     */
116
-    public function getAttributeTypeID()
117
-    {
118
-        $parts = explode('-', $this->name);
119
-        return count($parts) > 1 ? $parts[1] : 0;
120
-    }
121
-
122
-
123
-    /**
124
-     * @return string
125
-     */
126
-    public function getFieldName()
127
-    {
128
-        $parts = explode('-', $this->name);
129
-        return count($parts) > 0 ? $parts[0] : '';
130
-    }
131
-
132
-
133
-    /**
134
-     * Save the current value of this CheckboxSetField into a DataObject.
135
-     * If the field it is saving to is a has_many or many_many relationship,
136
-     * it is saved by setByIDList(), otherwise it creates a comma separated
137
-     * list for a standard DB text/varchar field.
138
-     *
139
-     * @param DataObjectInterface $record The record to save into
140
-     */
141
-    public function saveInto(DataObjectInterface $record)
142
-    {
143
-        $fieldname  = $this->getFieldName();
144
-        if (empty($fieldname)) {
145
-            return;
146
-        }
147
-        $typeID     = $this->getAttributeTypeID();
148
-        if (empty($typeID)) {
149
-            return;
150
-        }
151
-        $relation   = $record->$fieldname();
152
-        if (!$relation) {
153
-            return;
154
-        }
155
-        $relation   = $relation->filter('TypeID', $typeID);
156
-
157
-        // make a list of id's that should be there
158
-        $idList = array();
159
-        if (!empty($this->value) && is_array($this->value)) {
160
-            foreach ($this->value as $id => $bool) {
161
-                if ($bool) {
162
-                    $idList[$id] = $id;
163
-                }
164
-            }
165
-        }
166
-
167
-        // look at the existing elements and add/subtract
168
-        $toDelete = array();
169
-        foreach ($relation as $rec) {
170
-            if (isset($idList[$rec->ID])) {
171
-                // don't try to add it twice
172
-                unset($idList[$rec->ID]);
173
-            } else {
174
-                $toDelete[] = $rec->ID;
175
-            }
176
-        }
177
-
178
-        // add
179
-        foreach ($idList as $id) {
180
-            $relation->add($id);
181
-        }
182
-
183
-        // remove
184
-        foreach ($toDelete as $id) {
185
-            $relation->removeByID($id);
186
-        }
187
-    }
188
-
189
-
190
-    /**
191
-     * Load a value into this CheckboxSetField
192
-     */
193
-    public function setValue($value, $obj = null)
194
-    {
195
-        if (!empty($value) && !is_array($value) && !empty($this->value)) {
196
-            $this->value[] = $value;
197
-        } else {
198
-            parent::setValue($value, $obj);
199
-        }
200
-
201
-        return $this;
202
-    }
113
+	/**
114
+	 * @return int
115
+	 */
116
+	public function getAttributeTypeID()
117
+	{
118
+		$parts = explode('-', $this->name);
119
+		return count($parts) > 1 ? $parts[1] : 0;
120
+	}
121
+
122
+
123
+	/**
124
+	 * @return string
125
+	 */
126
+	public function getFieldName()
127
+	{
128
+		$parts = explode('-', $this->name);
129
+		return count($parts) > 0 ? $parts[0] : '';
130
+	}
131
+
132
+
133
+	/**
134
+	 * Save the current value of this CheckboxSetField into a DataObject.
135
+	 * If the field it is saving to is a has_many or many_many relationship,
136
+	 * it is saved by setByIDList(), otherwise it creates a comma separated
137
+	 * list for a standard DB text/varchar field.
138
+	 *
139
+	 * @param DataObjectInterface $record The record to save into
140
+	 */
141
+	public function saveInto(DataObjectInterface $record)
142
+	{
143
+		$fieldname  = $this->getFieldName();
144
+		if (empty($fieldname)) {
145
+			return;
146
+		}
147
+		$typeID     = $this->getAttributeTypeID();
148
+		if (empty($typeID)) {
149
+			return;
150
+		}
151
+		$relation   = $record->$fieldname();
152
+		if (!$relation) {
153
+			return;
154
+		}
155
+		$relation   = $relation->filter('TypeID', $typeID);
156
+
157
+		// make a list of id's that should be there
158
+		$idList = array();
159
+		if (!empty($this->value) && is_array($this->value)) {
160
+			foreach ($this->value as $id => $bool) {
161
+				if ($bool) {
162
+					$idList[$id] = $id;
163
+				}
164
+			}
165
+		}
166
+
167
+		// look at the existing elements and add/subtract
168
+		$toDelete = array();
169
+		foreach ($relation as $rec) {
170
+			if (isset($idList[$rec->ID])) {
171
+				// don't try to add it twice
172
+				unset($idList[$rec->ID]);
173
+			} else {
174
+				$toDelete[] = $rec->ID;
175
+			}
176
+		}
177
+
178
+		// add
179
+		foreach ($idList as $id) {
180
+			$relation->add($id);
181
+		}
182
+
183
+		// remove
184
+		foreach ($toDelete as $id) {
185
+			$relation->removeByID($id);
186
+		}
187
+	}
188
+
189
+
190
+	/**
191
+	 * Load a value into this CheckboxSetField
192
+	 */
193
+	public function setValue($value, $obj = null)
194
+	{
195
+		if (!empty($value) && !is_array($value) && !empty($this->value)) {
196
+			$this->value[] = $value;
197
+		} else {
198
+			parent::setValue($value, $obj);
199
+		}
200
+
201
+		return $this;
202
+	}
203 203
 }
204 204
 
205 205
 
Please login to merge, or discard this patch.
Spacing   +5 added lines, -5 removed lines patch added patch discarded remove patch
@@ -54,7 +54,7 @@  discard block
 block discarded – undo
54 54
      */
55 55
     protected function getValuesClosure($typeID)
56 56
     {
57
-        return function () use ($typeID) {
57
+        return function() use ($typeID) {
58 58
             return ProductAttributeValue::get()->filter('TypeID', $typeID)->map('ID', 'Value')->toArray();
59 59
         };
60 60
     }
@@ -140,19 +140,19 @@  discard block
 block discarded – undo
140 140
      */
141 141
     public function saveInto(DataObjectInterface $record)
142 142
     {
143
-        $fieldname  = $this->getFieldName();
143
+        $fieldname = $this->getFieldName();
144 144
         if (empty($fieldname)) {
145 145
             return;
146 146
         }
147
-        $typeID     = $this->getAttributeTypeID();
147
+        $typeID = $this->getAttributeTypeID();
148 148
         if (empty($typeID)) {
149 149
             return;
150 150
         }
151
-        $relation   = $record->$fieldname();
151
+        $relation = $record->$fieldname();
152 152
         if (!$relation) {
153 153
             return;
154 154
         }
155
-        $relation   = $relation->filter('TypeID', $typeID);
155
+        $relation = $relation->filter('TypeID', $typeID);
156 156
 
157 157
         // make a list of id's that should be there
158 158
         $idList = array();
Please login to merge, or discard this patch.
code/helpers/VirtualFieldIndex.php 3 patches
Doc Comments   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -243,7 +243,7 @@
 block discarded – undo
243 243
     }
244 244
 
245 245
     /**
246
-     * @param $name
246
+     * @param string $name
247 247
      * @return string
248 248
      */
249 249
     public function getVFIFieldName($name)
Please login to merge, or discard this patch.
Indentation   +408 added lines, -408 removed lines patch added patch discarded remove patch
@@ -41,413 +41,413 @@
 block discarded – undo
41 41
  */
42 42
 class VirtualFieldIndex extends DataExtension
43 43
 {
44
-    const TYPE_LIST     = 'list';
45
-    const TYPE_SIMPLE   = 'simple';
46
-    const DEPENDS_ALL   = 'all';
47
-    const DEPENDS_NONE  = 'none';
48
-
49
-    /** @var array - central config for all models */
50
-    private static $vfi_spec = array();
51
-
52
-    /** @var bool - if you set this to true it will write to both live and stage using DB::query and save some time, possibly */
53
-    private static $fast_writes_enabled = false;
54
-
55
-    /** @var bool - used to prevent an infinite loop in onBeforeWrite */
56
-    protected $isRebuilding = false;
57
-
58
-    public static $disable_building = false;
59
-
60
-    /**
61
-     * @return array
62
-     */
63
-    public static function get_classes_with_vfi()
64
-    {
65
-        $vfi_def = Config::inst()->get('VirtualFieldIndex', 'vfi_spec');
66
-        if (!$vfi_def || !is_array($vfi_def)) {
67
-            return array();
68
-        }
69
-        return array_keys($vfi_def);
70
-    }
71
-
72
-    /**
73
-     * Define extra db fields and indexes.
74
-     * @param $class
75
-     * @param $extension
76
-     * @param $args
77
-     * @return array
78
-     */
79
-    public static function get_extra_config($class, $extension, $args)
80
-    {
81
-        $vfi_def = self::get_vfi_spec($class);
82
-        if (!$vfi_def || !is_array($vfi_def)) {
83
-            return array();
84
-        }
85
-
86
-        $out = array(
87
-            'db'        => array(),
88
-            'indexes'   => array(),
89
-        );
90
-
91
-        foreach ($vfi_def as $field => $spec) {
92
-            $fn = 'VFI_' . $field;
93
-            $out['db'][$fn] = isset($spec['DBField']) ? $spec['DBField'] : 'Varchar(255)';
94
-            $out['indexes'][$fn] = true;
95
-        }
96
-
97
-        return $out;
98
-    }
99
-
100
-
101
-    /**
102
-     * Return a normalized version of the vfi definition for a given class
103
-     * @param string $class
104
-     * @return array
105
-     */
106
-    public static function get_vfi_spec($class)
107
-    {
108
-        $vfi_master = Config::inst()->get('VirtualFieldIndex', 'vfi_spec');
109
-        if (!$vfi_master || !is_array($vfi_master)) {
110
-            return array();
111
-        }
112
-
113
-        // merge in all the vfi's from ancestors as well
114
-        $vfi_def = array();
115
-        foreach (ClassInfo::ancestry($class) as $c) {
116
-            if (!empty($vfi_master[$c])) {
117
-                // we want newer classes to override parent classes so we do it this way
118
-                $vfi_def = $vfi_master[$c] + $vfi_def;
119
-            }
120
-        }
121
-        if (empty($vfi_def)) {
122
-            return array();
123
-        }
124
-
125
-        // convert shorthand to longhand
126
-        foreach ($vfi_def as $k => $v) {
127
-            if (is_numeric($k)) {
128
-                $vfi_def[$v] = $v;
129
-                unset($vfi_def[$k]);
130
-            } elseif (is_string($v)) {
131
-                $vfi_def[$k] = array(
132
-                    'Type'      => self::TYPE_SIMPLE,
133
-                    'DependsOn' => self::DEPENDS_ALL,
134
-                    'Source'    => $v,
135
-                );
136
-            } elseif (is_array($v) && !isset($vfi_def[$k]['Source'])) {
137
-                $vfi_def[$k] = array(
138
-                    'Type'      => self::TYPE_LIST,
139
-                    'DependsOn' => self::DEPENDS_ALL,
140
-                    'Source'    => $v,
141
-                );
142
-            } else {
143
-                if (!isset($v['Type'])) {
144
-                    $vfi_def[$k]['Type'] = is_array($v['Source']) ? self::TYPE_LIST : self::TYPE_SIMPLE;
145
-                }
146
-                if (!isset($v['DependsOn'])) {
147
-                    $vfi_def[$k]['DependsOn'] = self::DEPENDS_ALL;
148
-                }
149
-            }
150
-        }
151
-
152
-        return $vfi_def;
153
-    }
154
-
155
-    /**
156
-     * Rebuilds any vfi fields on one class (or all). Doing it in chunks means a few more
157
-     * queries but it means we can handle larger datasets without storing everything in memory.
158
-     *
159
-     * @param string $class [optional] - if not given all indexes will be rebuilt
160
-     */
161
-    public static function build($class='')
162
-    {
163
-        if ($class) {
164
-            $list   = DataObject::get($class);
165
-            $count  = $list->count();
166
-            for ($i = 0; $i < $count; $i += 10) {
167
-                $chunk = $list->limit(10, $i);
44
+	const TYPE_LIST     = 'list';
45
+	const TYPE_SIMPLE   = 'simple';
46
+	const DEPENDS_ALL   = 'all';
47
+	const DEPENDS_NONE  = 'none';
48
+
49
+	/** @var array - central config for all models */
50
+	private static $vfi_spec = array();
51
+
52
+	/** @var bool - if you set this to true it will write to both live and stage using DB::query and save some time, possibly */
53
+	private static $fast_writes_enabled = false;
54
+
55
+	/** @var bool - used to prevent an infinite loop in onBeforeWrite */
56
+	protected $isRebuilding = false;
57
+
58
+	public static $disable_building = false;
59
+
60
+	/**
61
+	 * @return array
62
+	 */
63
+	public static function get_classes_with_vfi()
64
+	{
65
+		$vfi_def = Config::inst()->get('VirtualFieldIndex', 'vfi_spec');
66
+		if (!$vfi_def || !is_array($vfi_def)) {
67
+			return array();
68
+		}
69
+		return array_keys($vfi_def);
70
+	}
71
+
72
+	/**
73
+	 * Define extra db fields and indexes.
74
+	 * @param $class
75
+	 * @param $extension
76
+	 * @param $args
77
+	 * @return array
78
+	 */
79
+	public static function get_extra_config($class, $extension, $args)
80
+	{
81
+		$vfi_def = self::get_vfi_spec($class);
82
+		if (!$vfi_def || !is_array($vfi_def)) {
83
+			return array();
84
+		}
85
+
86
+		$out = array(
87
+			'db'        => array(),
88
+			'indexes'   => array(),
89
+		);
90
+
91
+		foreach ($vfi_def as $field => $spec) {
92
+			$fn = 'VFI_' . $field;
93
+			$out['db'][$fn] = isset($spec['DBField']) ? $spec['DBField'] : 'Varchar(255)';
94
+			$out['indexes'][$fn] = true;
95
+		}
96
+
97
+		return $out;
98
+	}
99
+
100
+
101
+	/**
102
+	 * Return a normalized version of the vfi definition for a given class
103
+	 * @param string $class
104
+	 * @return array
105
+	 */
106
+	public static function get_vfi_spec($class)
107
+	{
108
+		$vfi_master = Config::inst()->get('VirtualFieldIndex', 'vfi_spec');
109
+		if (!$vfi_master || !is_array($vfi_master)) {
110
+			return array();
111
+		}
112
+
113
+		// merge in all the vfi's from ancestors as well
114
+		$vfi_def = array();
115
+		foreach (ClassInfo::ancestry($class) as $c) {
116
+			if (!empty($vfi_master[$c])) {
117
+				// we want newer classes to override parent classes so we do it this way
118
+				$vfi_def = $vfi_master[$c] + $vfi_def;
119
+			}
120
+		}
121
+		if (empty($vfi_def)) {
122
+			return array();
123
+		}
124
+
125
+		// convert shorthand to longhand
126
+		foreach ($vfi_def as $k => $v) {
127
+			if (is_numeric($k)) {
128
+				$vfi_def[$v] = $v;
129
+				unset($vfi_def[$k]);
130
+			} elseif (is_string($v)) {
131
+				$vfi_def[$k] = array(
132
+					'Type'      => self::TYPE_SIMPLE,
133
+					'DependsOn' => self::DEPENDS_ALL,
134
+					'Source'    => $v,
135
+				);
136
+			} elseif (is_array($v) && !isset($vfi_def[$k]['Source'])) {
137
+				$vfi_def[$k] = array(
138
+					'Type'      => self::TYPE_LIST,
139
+					'DependsOn' => self::DEPENDS_ALL,
140
+					'Source'    => $v,
141
+				);
142
+			} else {
143
+				if (!isset($v['Type'])) {
144
+					$vfi_def[$k]['Type'] = is_array($v['Source']) ? self::TYPE_LIST : self::TYPE_SIMPLE;
145
+				}
146
+				if (!isset($v['DependsOn'])) {
147
+					$vfi_def[$k]['DependsOn'] = self::DEPENDS_ALL;
148
+				}
149
+			}
150
+		}
151
+
152
+		return $vfi_def;
153
+	}
154
+
155
+	/**
156
+	 * Rebuilds any vfi fields on one class (or all). Doing it in chunks means a few more
157
+	 * queries but it means we can handle larger datasets without storing everything in memory.
158
+	 *
159
+	 * @param string $class [optional] - if not given all indexes will be rebuilt
160
+	 */
161
+	public static function build($class='')
162
+	{
163
+		if ($class) {
164
+			$list   = DataObject::get($class);
165
+			$count  = $list->count();
166
+			for ($i = 0; $i < $count; $i += 10) {
167
+				$chunk = $list->limit(10, $i);
168 168
 //				if (Controller::curr() instanceof TaskRunner) echo "Processing VFI #$i...\n";
169
-                foreach ($chunk as $rec) {
170
-                    $rec->rebuildVFI();
171
-                }
172
-            }
173
-        } else {
174
-            foreach (self::get_classes_with_vfi() as $c) {
175
-                self::build($c);
176
-            }
177
-        }
178
-    }
179
-
180
-    /**
181
-     * Rebuild all vfi fields.
182
-     */
183
-    public function rebuildVFI($field = '')
184
-    {
185
-        if ($field) {
186
-            $this->isRebuilding = true;
187
-            $spec   = $this->getVFISpec($field);
188
-            $fn     = $this->getVFIFieldName($field);
189
-            $val    = $this->getVFI($field, true);
190
-
191
-            if ($spec['Type'] == self::TYPE_LIST) {
192
-                if (is_object($val)) {
193
-                    $val = $val->toArray();
194
-                }    // this would be an ArrayList or DataList
195
-                if (!is_array($val)) {
196
-                    $val = array($val);
197
-                }        // this would be a scalar value
198
-                $val = self::encode_list($val);
199
-            } else {
200
-                if (is_array($val)) {
201
-                    $val = (string)$val[0];
202
-                }    // if they give us an array, just take the first value
203
-                if (is_object($val)) {
204
-                    $val = (string)$val->first();
205
-                }  // if a SS_List, take the first as well
206
-            }
207
-
208
-            if (Config::inst()->get('VirtualFieldIndex', 'fast_writes_enabled')) {
209
-                // NOTE: this is usually going to be bad practice, but if you
210
-                // have a lot of products and a lot of on...Write handlers that
211
-                // can get tedious really quick. This is just here as an option.
212
-                $table = '';
213
-                foreach ($this->owner->getClassAncestry() as $ancestor) {
214
-                    if (DataObject::has_own_table($ancestor)) {
215
-                        $sing = singleton($ancestor);
216
-                        if ($sing->hasOwnTableDatabaseField($fn)) {
217
-                            $table = $ancestor;
218
-                            break;
219
-                        }
220
-                    }
221
-                }
222
-
223
-                if (!empty($table)) {
224
-                    DB::query($sql = sprintf("UPDATE %s SET %s = '%s' WHERE ID = '%d'", $table, $fn, Convert::raw2sql($val), $this->owner->ID));
225
-                    DB::query(sprintf("UPDATE %s_Live SET %s = '%s' WHERE ID = '%d'", $table, $fn, Convert::raw2sql($val), $this->owner->ID));
226
-                    $this->owner->setField($fn, $val);
227
-                } else {
228
-                    // if we couldn't figure out the right table, fall back to the old fashioned way
229
-                    $this->owner->setField($fn, $val);
230
-                    $this->owner->write();
231
-                }
232
-            } else {
233
-                $this->owner->setField($fn, $val);
234
-                $this->owner->write();
235
-            }
236
-            $this->isRebuilding = false;
237
-        } else {
238
-            // rebuild all fields if they didn't specify
239
-            foreach ($this->getVFISpec() as $field => $spec) {
240
-                $this->rebuildVFI($field);
241
-            }
242
-        }
243
-    }
244
-
245
-    /**
246
-     * @param $name
247
-     * @return string
248
-     */
249
-    public function getVFIFieldName($name)
250
-    {
251
-        return 'VFI_' . $name;
252
-    }
253
-
254
-
255
-    /**
256
-     * @param string $field [optional]
257
-     * @return array|false
258
-     */
259
-    public function getVFISpec($field = '')
260
-    {
261
-        $spec = self::get_vfi_spec($this->owner->class);
262
-        if ($field) {
263
-            return empty($spec[$field]) ? false : $spec[$field];
264
-        } else {
265
-            return $spec;
266
-        }
267
-    }
268
-
269
-
270
-    /**
271
-     * @param string $field
272
-     * @param bool   $fromSource [optional] - if true, it will regenerate the data from the source fields
273
-     * @param bool   $forceIDs [optional] - if true, it will return an ID even if the norm is to return a DataObject
274
-     * @return string|array|SS_List
275
-     */
276
-    public function getVFI($field, $fromSource=false, $forceIDs=false)
277
-    {
278
-        $spec = $this->getVFISpec($field);
279
-        if (!$spec) {
280
-            return null;
281
-        }
282
-        if ($fromSource) {
283
-            if (is_array($spec['Source'])) {
284
-                $out = array();
285
-                foreach ($spec['Source'] as $src) {
286
-                    $myOut = self::get_value($src, $this->owner);
287
-                    if (is_array($myOut)) {
288
-                        $out = array_merge($out, $myOut);
289
-                    } elseif (is_object($myOut) && $myOut instanceof SS_List) {
290
-                        $out = array_merge($out, $myOut->toArray());
291
-                    } else {
292
-                        $out[] = $myOut;
293
-                    }
294
-                }
295
-                return $out;
296
-            } else {
297
-                return self::get_value($spec['Source'], $this->owner);
298
-            }
299
-        } else {
300
-            $val = $this->owner->getField($this->getVFIFieldName($field));
301
-            if ($spec['Type'] == self::TYPE_LIST) {
302
-                return self::decode_list($val, $forceIDs);
303
-            } else {
304
-                return $val;
305
-            }
306
-        }
307
-    }
308
-
309
-
310
-    /**
311
-     * Template version
312
-     * @param string $field
313
-     * @return string|array|SS_List
314
-     */
315
-    public function VFI($field)
316
-    {
317
-        return $this->getVFI($field);
318
-    }
319
-
320
-
321
-    /**
322
-     * @param array $list
323
-     * @return string
324
-     */
325
-    protected static function encode_list(array $list)
326
-    {
327
-        // If we've got objects, encode them a little differently
328
-        if (count($list) > 0 && is_object($list[0])) {
329
-            $ids = array();
330
-            foreach ($list as $rec) {
331
-                $ids[] = $rec->ID;
332
-            }
333
-            $val = '>' . $list[0]->ClassName . '|' . implode('|', $ids) . '|';
334
-        } else {
335
-            $val = '|' . implode('|', $list) . '|';
336
-        }
337
-
338
-        return $val;
339
-    }
340
-
341
-
342
-    /**
343
-     * @param string $val
344
-     * @param bool   $forceIDs [optional] - if true encoded objects will not be returned as objects but as id's
345
-     * @return array
346
-     */
347
-    protected static function decode_list($val, $forceIDs=false)
348
-    {
349
-        if ($val[0] == '>') {
350
-            $firstBar = strpos($val, '|');
351
-            if ($firstBar < 3) {
352
-                return array();
353
-            }
354
-            $className = substr($val, 1, $firstBar-1);
355
-            $ids = explode('|', trim(substr($val, $firstBar), '|'));
356
-            return $forceIDs ? $ids : DataObject::get($className)->filter('ID', $ids)->toArray();
357
-        } else {
358
-            return explode('|', trim($val, '|'));
359
-        }
360
-    }
361
-
362
-
363
-    /**
364
-     * This is largely borrowed from DataObject::relField, but
365
-     * adapted to work with many-many and has-many fields.
366
-     * @param string $fieldName
367
-     * @param DataObject $rec
368
-     * @return mixed
369
-     */
370
-    protected static function get_value($fieldName, DataObject $rec)
371
-    {
372
-        $component = $rec;
373
-
374
-        // We're dealing with relations here so we traverse the dot syntax
375
-        if (strpos($fieldName, '.') !== false) {
376
-            $relations = explode('.', $fieldName);
377
-            $fieldName = array_pop($relations);
378
-            foreach ($relations as $relation) {
379
-                // Inspect $component for element $relation
380
-                if ($component->hasMethod($relation)) {
381
-                    // Check nested method
382
-                    $component = $component->$relation();
383
-                } elseif ($component instanceof SS_List) {
384
-                    // Select adjacent relation from DataList
385
-                    $component = $component->relation($relation);
386
-                } elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
387
-                    // Select db object
388
-                    $component = $dbObject;
389
-                } else {
390
-                    user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
391
-                }
392
-            }
393
-        }
394
-
395
-        // Bail if the component is null
396
-        if (!$component) {
397
-            return null;
398
-        } elseif ($component instanceof SS_List) {
399
-            return $component->column($fieldName);
400
-        } elseif ($component->hasMethod($fieldName)) {
401
-            return $component->$fieldName();
402
-        } else {
403
-            return $component->$fieldName;
404
-        }
405
-    }
406
-
407
-    /**
408
-     * Trigger rebuild if needed
409
-     */
410
-    public function onBeforeWrite()
411
-    {
412
-        if ($this->isRebuilding || self::$disable_building) {
413
-            return;
414
-        }
415
-
416
-        $queueFields = interface_exists('QueuedJob') ? array() : false;
417
-
418
-        foreach ($this->getVFISpec() as $field => $spec) {
419
-            $rebuild = false;
420
-
421
-            if ($spec['DependsOn'] == self::DEPENDS_NONE) {
422
-                continue;
423
-            } elseif ($spec['DependsOn'] == self::DEPENDS_ALL) {
424
-                $rebuild = true;
425
-            } elseif (is_array($spec['DependsOn'])) {
426
-                foreach ($spec['DependsOn'] as $f) {
427
-                    if ($this->owner->isChanged($f)) {
428
-                        $rebuild = true;
429
-                        break;
430
-                    }
431
-                }
432
-            } else {
433
-                if ($this->owner->isChanged($spec['DependsOn'])) {
434
-                    $rebuild = true;
435
-                }
436
-            }
437
-
438
-            if ($rebuild) {
439
-                if ($queueFields === false) {
440
-                    $this->rebuildVFI($field);
441
-                } else {
442
-                    $queueFields[] = $field;
443
-                }
444
-            }
445
-        }
446
-
447
-        // if the queued-jobs module is present, then queue up the rebuild
448
-        if ($queueFields) {
449
-            $job = new VirtualFieldIndexQueuedJob($this->owner, $queueFields);
450
-            $job->triggerProcessing();
451
-        }
452
-    }
169
+				foreach ($chunk as $rec) {
170
+					$rec->rebuildVFI();
171
+				}
172
+			}
173
+		} else {
174
+			foreach (self::get_classes_with_vfi() as $c) {
175
+				self::build($c);
176
+			}
177
+		}
178
+	}
179
+
180
+	/**
181
+	 * Rebuild all vfi fields.
182
+	 */
183
+	public function rebuildVFI($field = '')
184
+	{
185
+		if ($field) {
186
+			$this->isRebuilding = true;
187
+			$spec   = $this->getVFISpec($field);
188
+			$fn     = $this->getVFIFieldName($field);
189
+			$val    = $this->getVFI($field, true);
190
+
191
+			if ($spec['Type'] == self::TYPE_LIST) {
192
+				if (is_object($val)) {
193
+					$val = $val->toArray();
194
+				}    // this would be an ArrayList or DataList
195
+				if (!is_array($val)) {
196
+					$val = array($val);
197
+				}        // this would be a scalar value
198
+				$val = self::encode_list($val);
199
+			} else {
200
+				if (is_array($val)) {
201
+					$val = (string)$val[0];
202
+				}    // if they give us an array, just take the first value
203
+				if (is_object($val)) {
204
+					$val = (string)$val->first();
205
+				}  // if a SS_List, take the first as well
206
+			}
207
+
208
+			if (Config::inst()->get('VirtualFieldIndex', 'fast_writes_enabled')) {
209
+				// NOTE: this is usually going to be bad practice, but if you
210
+				// have a lot of products and a lot of on...Write handlers that
211
+				// can get tedious really quick. This is just here as an option.
212
+				$table = '';
213
+				foreach ($this->owner->getClassAncestry() as $ancestor) {
214
+					if (DataObject::has_own_table($ancestor)) {
215
+						$sing = singleton($ancestor);
216
+						if ($sing->hasOwnTableDatabaseField($fn)) {
217
+							$table = $ancestor;
218
+							break;
219
+						}
220
+					}
221
+				}
222
+
223
+				if (!empty($table)) {
224
+					DB::query($sql = sprintf("UPDATE %s SET %s = '%s' WHERE ID = '%d'", $table, $fn, Convert::raw2sql($val), $this->owner->ID));
225
+					DB::query(sprintf("UPDATE %s_Live SET %s = '%s' WHERE ID = '%d'", $table, $fn, Convert::raw2sql($val), $this->owner->ID));
226
+					$this->owner->setField($fn, $val);
227
+				} else {
228
+					// if we couldn't figure out the right table, fall back to the old fashioned way
229
+					$this->owner->setField($fn, $val);
230
+					$this->owner->write();
231
+				}
232
+			} else {
233
+				$this->owner->setField($fn, $val);
234
+				$this->owner->write();
235
+			}
236
+			$this->isRebuilding = false;
237
+		} else {
238
+			// rebuild all fields if they didn't specify
239
+			foreach ($this->getVFISpec() as $field => $spec) {
240
+				$this->rebuildVFI($field);
241
+			}
242
+		}
243
+	}
244
+
245
+	/**
246
+	 * @param $name
247
+	 * @return string
248
+	 */
249
+	public function getVFIFieldName($name)
250
+	{
251
+		return 'VFI_' . $name;
252
+	}
253
+
254
+
255
+	/**
256
+	 * @param string $field [optional]
257
+	 * @return array|false
258
+	 */
259
+	public function getVFISpec($field = '')
260
+	{
261
+		$spec = self::get_vfi_spec($this->owner->class);
262
+		if ($field) {
263
+			return empty($spec[$field]) ? false : $spec[$field];
264
+		} else {
265
+			return $spec;
266
+		}
267
+	}
268
+
269
+
270
+	/**
271
+	 * @param string $field
272
+	 * @param bool   $fromSource [optional] - if true, it will regenerate the data from the source fields
273
+	 * @param bool   $forceIDs [optional] - if true, it will return an ID even if the norm is to return a DataObject
274
+	 * @return string|array|SS_List
275
+	 */
276
+	public function getVFI($field, $fromSource=false, $forceIDs=false)
277
+	{
278
+		$spec = $this->getVFISpec($field);
279
+		if (!$spec) {
280
+			return null;
281
+		}
282
+		if ($fromSource) {
283
+			if (is_array($spec['Source'])) {
284
+				$out = array();
285
+				foreach ($spec['Source'] as $src) {
286
+					$myOut = self::get_value($src, $this->owner);
287
+					if (is_array($myOut)) {
288
+						$out = array_merge($out, $myOut);
289
+					} elseif (is_object($myOut) && $myOut instanceof SS_List) {
290
+						$out = array_merge($out, $myOut->toArray());
291
+					} else {
292
+						$out[] = $myOut;
293
+					}
294
+				}
295
+				return $out;
296
+			} else {
297
+				return self::get_value($spec['Source'], $this->owner);
298
+			}
299
+		} else {
300
+			$val = $this->owner->getField($this->getVFIFieldName($field));
301
+			if ($spec['Type'] == self::TYPE_LIST) {
302
+				return self::decode_list($val, $forceIDs);
303
+			} else {
304
+				return $val;
305
+			}
306
+		}
307
+	}
308
+
309
+
310
+	/**
311
+	 * Template version
312
+	 * @param string $field
313
+	 * @return string|array|SS_List
314
+	 */
315
+	public function VFI($field)
316
+	{
317
+		return $this->getVFI($field);
318
+	}
319
+
320
+
321
+	/**
322
+	 * @param array $list
323
+	 * @return string
324
+	 */
325
+	protected static function encode_list(array $list)
326
+	{
327
+		// If we've got objects, encode them a little differently
328
+		if (count($list) > 0 && is_object($list[0])) {
329
+			$ids = array();
330
+			foreach ($list as $rec) {
331
+				$ids[] = $rec->ID;
332
+			}
333
+			$val = '>' . $list[0]->ClassName . '|' . implode('|', $ids) . '|';
334
+		} else {
335
+			$val = '|' . implode('|', $list) . '|';
336
+		}
337
+
338
+		return $val;
339
+	}
340
+
341
+
342
+	/**
343
+	 * @param string $val
344
+	 * @param bool   $forceIDs [optional] - if true encoded objects will not be returned as objects but as id's
345
+	 * @return array
346
+	 */
347
+	protected static function decode_list($val, $forceIDs=false)
348
+	{
349
+		if ($val[0] == '>') {
350
+			$firstBar = strpos($val, '|');
351
+			if ($firstBar < 3) {
352
+				return array();
353
+			}
354
+			$className = substr($val, 1, $firstBar-1);
355
+			$ids = explode('|', trim(substr($val, $firstBar), '|'));
356
+			return $forceIDs ? $ids : DataObject::get($className)->filter('ID', $ids)->toArray();
357
+		} else {
358
+			return explode('|', trim($val, '|'));
359
+		}
360
+	}
361
+
362
+
363
+	/**
364
+	 * This is largely borrowed from DataObject::relField, but
365
+	 * adapted to work with many-many and has-many fields.
366
+	 * @param string $fieldName
367
+	 * @param DataObject $rec
368
+	 * @return mixed
369
+	 */
370
+	protected static function get_value($fieldName, DataObject $rec)
371
+	{
372
+		$component = $rec;
373
+
374
+		// We're dealing with relations here so we traverse the dot syntax
375
+		if (strpos($fieldName, '.') !== false) {
376
+			$relations = explode('.', $fieldName);
377
+			$fieldName = array_pop($relations);
378
+			foreach ($relations as $relation) {
379
+				// Inspect $component for element $relation
380
+				if ($component->hasMethod($relation)) {
381
+					// Check nested method
382
+					$component = $component->$relation();
383
+				} elseif ($component instanceof SS_List) {
384
+					// Select adjacent relation from DataList
385
+					$component = $component->relation($relation);
386
+				} elseif ($component instanceof DataObject && ($dbObject = $component->dbObject($relation))) {
387
+					// Select db object
388
+					$component = $dbObject;
389
+				} else {
390
+					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
391
+				}
392
+			}
393
+		}
394
+
395
+		// Bail if the component is null
396
+		if (!$component) {
397
+			return null;
398
+		} elseif ($component instanceof SS_List) {
399
+			return $component->column($fieldName);
400
+		} elseif ($component->hasMethod($fieldName)) {
401
+			return $component->$fieldName();
402
+		} else {
403
+			return $component->$fieldName;
404
+		}
405
+	}
406
+
407
+	/**
408
+	 * Trigger rebuild if needed
409
+	 */
410
+	public function onBeforeWrite()
411
+	{
412
+		if ($this->isRebuilding || self::$disable_building) {
413
+			return;
414
+		}
415
+
416
+		$queueFields = interface_exists('QueuedJob') ? array() : false;
417
+
418
+		foreach ($this->getVFISpec() as $field => $spec) {
419
+			$rebuild = false;
420
+
421
+			if ($spec['DependsOn'] == self::DEPENDS_NONE) {
422
+				continue;
423
+			} elseif ($spec['DependsOn'] == self::DEPENDS_ALL) {
424
+				$rebuild = true;
425
+			} elseif (is_array($spec['DependsOn'])) {
426
+				foreach ($spec['DependsOn'] as $f) {
427
+					if ($this->owner->isChanged($f)) {
428
+						$rebuild = true;
429
+						break;
430
+					}
431
+				}
432
+			} else {
433
+				if ($this->owner->isChanged($spec['DependsOn'])) {
434
+					$rebuild = true;
435
+				}
436
+			}
437
+
438
+			if ($rebuild) {
439
+				if ($queueFields === false) {
440
+					$this->rebuildVFI($field);
441
+				} else {
442
+					$queueFields[] = $field;
443
+				}
444
+			}
445
+		}
446
+
447
+		// if the queued-jobs module is present, then queue up the rebuild
448
+		if ($queueFields) {
449
+			$job = new VirtualFieldIndexQueuedJob($this->owner, $queueFields);
450
+			$job->triggerProcessing();
451
+		}
452
+	}
453 453
 }
Please login to merge, or discard this patch.
Spacing   +8 added lines, -8 removed lines patch added patch discarded remove patch
@@ -89,7 +89,7 @@  discard block
 block discarded – undo
89 89
         );
90 90
 
91 91
         foreach ($vfi_def as $field => $spec) {
92
-            $fn = 'VFI_' . $field;
92
+            $fn = 'VFI_'.$field;
93 93
             $out['db'][$fn] = isset($spec['DBField']) ? $spec['DBField'] : 'Varchar(255)';
94 94
             $out['indexes'][$fn] = true;
95 95
         }
@@ -158,7 +158,7 @@  discard block
 block discarded – undo
158 158
      *
159 159
      * @param string $class [optional] - if not given all indexes will be rebuilt
160 160
      */
161
-    public static function build($class='')
161
+    public static function build($class = '')
162 162
     {
163 163
         if ($class) {
164 164
             $list   = DataObject::get($class);
@@ -248,7 +248,7 @@  discard block
 block discarded – undo
248 248
      */
249 249
     public function getVFIFieldName($name)
250 250
     {
251
-        return 'VFI_' . $name;
251
+        return 'VFI_'.$name;
252 252
     }
253 253
 
254 254
 
@@ -273,7 +273,7 @@  discard block
 block discarded – undo
273 273
      * @param bool   $forceIDs [optional] - if true, it will return an ID even if the norm is to return a DataObject
274 274
      * @return string|array|SS_List
275 275
      */
276
-    public function getVFI($field, $fromSource=false, $forceIDs=false)
276
+    public function getVFI($field, $fromSource = false, $forceIDs = false)
277 277
     {
278 278
         $spec = $this->getVFISpec($field);
279 279
         if (!$spec) {
@@ -330,9 +330,9 @@  discard block
 block discarded – undo
330 330
             foreach ($list as $rec) {
331 331
                 $ids[] = $rec->ID;
332 332
             }
333
-            $val = '>' . $list[0]->ClassName . '|' . implode('|', $ids) . '|';
333
+            $val = '>'.$list[0]->ClassName.'|'.implode('|', $ids).'|';
334 334
         } else {
335
-            $val = '|' . implode('|', $list) . '|';
335
+            $val = '|'.implode('|', $list).'|';
336 336
         }
337 337
 
338 338
         return $val;
@@ -344,14 +344,14 @@  discard block
 block discarded – undo
344 344
      * @param bool   $forceIDs [optional] - if true encoded objects will not be returned as objects but as id's
345 345
      * @return array
346 346
      */
347
-    protected static function decode_list($val, $forceIDs=false)
347
+    protected static function decode_list($val, $forceIDs = false)
348 348
     {
349 349
         if ($val[0] == '>') {
350 350
             $firstBar = strpos($val, '|');
351 351
             if ($firstBar < 3) {
352 352
                 return array();
353 353
             }
354
-            $className = substr($val, 1, $firstBar-1);
354
+            $className = substr($val, 1, $firstBar - 1);
355 355
             $ids = explode('|', trim(substr($val, $firstBar), '|'));
356 356
             return $forceIDs ? $ids : DataObject::get($className)->filter('ID', $ids)->toArray();
357 357
         } else {
Please login to merge, or discard this patch.
code/ShopSearch.php 3 patches
Doc Comments   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -267,7 +267,7 @@
 block discarded – undo
267 267
 
268 268
     /**
269 269
      * @param string $str
270
-     * @return SS_Query
270
+     * @return SQLQuery
271 271
      */
272 272
     public function getSuggestQuery($str='')
273 273
     {
Please login to merge, or discard this patch.
Indentation   +350 added lines, -350 removed lines patch added patch discarded remove patch
@@ -8,376 +8,376 @@
 block discarded – undo
8 8
  */
9 9
 class ShopSearch extends Object
10 10
 {
11
-    const FACET_TYPE_LINK       = 'link';
12
-    const FACET_TYPE_CHECKBOX   = 'checkbox';
13
-    const FACET_TYPE_RANGE      = 'range';
11
+	const FACET_TYPE_LINK       = 'link';
12
+	const FACET_TYPE_CHECKBOX   = 'checkbox';
13
+	const FACET_TYPE_RANGE      = 'range';
14 14
 
15
-    /** @var string - class name of adapter class to use */
16
-    private static $adapter_class = 'ShopSearchSimple';
15
+	/** @var string - class name of adapter class to use */
16
+	private static $adapter_class = 'ShopSearchSimple';
17 17
 
18
-    /** @var array - these classes will be added to the index - e.g. Category, Page, etc. */
19
-    private static $searchable = array();
18
+	/** @var array - these classes will be added to the index - e.g. Category, Page, etc. */
19
+	private static $searchable = array();
20 20
 
21
-    /** @var bool - if true, all buyable models will be added to the index automatically  */
22
-    private static $buyables_are_searchable = true;
21
+	/** @var bool - if true, all buyable models will be added to the index automatically  */
22
+	private static $buyables_are_searchable = true;
23 23
 
24
-    /** @var int - size of paging in the search */
25
-    private static $page_size = 10;
24
+	/** @var int - size of paging in the search */
25
+	private static $page_size = 10;
26 26
 
27
-    /** @var bool */
28
-    private static $suggest_enabled = true;
27
+	/** @var bool */
28
+	private static $suggest_enabled = true;
29 29
 
30
-    /** @var int - how many suggestions to provide */
31
-    private static $suggest_limit = 5;
30
+	/** @var int - how many suggestions to provide */
31
+	private static $suggest_limit = 5;
32 32
 
33
-    /** @var bool */
34
-    private static $search_as_you_type_enabled = true;
33
+	/** @var bool */
34
+	private static $search_as_you_type_enabled = true;
35 35
 
36
-    /** @var int - how may sayt (search-as-you-type) entries to provide */
37
-    private static $sayt_limit = 5;
36
+	/** @var int - how may sayt (search-as-you-type) entries to provide */
37
+	private static $sayt_limit = 5;
38 38
 
39
-    /** @var bool - automatically create facets for static attributes */
40
-    private static $auto_facet_attributes = false;
39
+	/** @var bool - automatically create facets for static attributes */
40
+	private static $auto_facet_attributes = false;
41 41
 
42
-    /** @var string - optionally, a different template to run ajax results through (sans-Page.ss) */
43
-    private static $ajax_results_template = '';
42
+	/** @var string - optionally, a different template to run ajax results through (sans-Page.ss) */
43
+	private static $ajax_results_template = '';
44 44
 
45
-    /** @var string - these allow you to use different querystring params in you need to */
46
-    private static $qs_query         = 'q';
47
-    private static $qs_filters       = 'f';
48
-    private static $qs_parent_search = '__ps';
49
-    private static $qs_title         = '__t';
50
-    private static $qs_source        = '__src'; // used to log searches from search-as-you-type
51
-    private static $qs_sort          = 'sort';
45
+	/** @var string - these allow you to use different querystring params in you need to */
46
+	private static $qs_query         = 'q';
47
+	private static $qs_filters       = 'f';
48
+	private static $qs_parent_search = '__ps';
49
+	private static $qs_title         = '__t';
50
+	private static $qs_source        = '__src'; // used to log searches from search-as-you-type
51
+	private static $qs_sort          = 'sort';
52 52
 
53
-    /** @var array - I'm leaving this particularly bare b/c with config merging it's a pain to remove items */
54
-    private static $sort_options = array(
55
-        'score desc'            => 'Relevance',
53
+	/** @var array - I'm leaving this particularly bare b/c with config merging it's a pain to remove items */
54
+	private static $sort_options = array(
55
+		'score desc'            => 'Relevance',
56 56
 //		'SiteTree_Title asc'    => 'Alphabetical (A-Z)',
57 57
 //		'SiteTree_Title dsc'    => 'Alphabetical (Z-A)',
58
-    );
59
-
60
-    /**
61
-     * @var array - default search facets (price, category, etc)
62
-     *   Key    field name - e.g. Price - can be a VirtualFieldIndex field
63
-     *   Value  facet label - e.g. Search By Category - if the value is a relation or returns an array or
64
-     *          list all values will be faceted individually
65
-     *          NOTE: this can also be another array with keys: Label, Type, and Values (for checkbox only)
66
-     */
67
-    private static $facets = array();
68
-
69
-    /** @var array - field definition for Solr only */
70
-    private static $solr_fulltext_fields = array();
71
-
72
-    /** @var array - field definition for Solr only */
73
-    private static $solr_filter_fields = array();
74
-
75
-    /** @var string - if present, will create a copy of SiteTree_Title that's suited for alpha sorting */
76
-    private static $solr_title_sort_field = '';
77
-
78
-    /**
79
-     * @var string - If present, everything matching the following regex will be removed from
80
-     *               keyword search queries before passing to the search adapter.
81
-     */
82
-    private static $keyword_filter_regex = '/[^a-zA-Z0-9\s\-]/';
83
-
84
-
85
-    /**
86
-     * @return array
87
-     */
88
-    public static function get_searchable_classes()
89
-    {
90
-        // First get any explicitly declared searchable classes
91
-        $searchable = Config::inst()->get('ShopSearch', 'searchable');
92
-        if (is_string($searchable) && strlen($searchable) > 0) {
93
-            $searchable = array($searchable);
94
-        } elseif (!is_array($searchable)) {
95
-            $searchable = array();
96
-        }
97
-
98
-        // Add in buyables automatically if asked
99
-        if (Config::inst()->get('ShopSearch', 'buyables_are_searchable')) {
100
-            $buyables = SS_ClassLoader::instance()->getManifest()->getImplementorsOf('Buyable');
101
-            if (is_array($buyables) && count($buyables) > 0) {
102
-                foreach ($buyables as $c) {
103
-                    $searchable[] = $c;
104
-                }
105
-            }
106
-        }
107
-
108
-        return array_unique($searchable);
109
-    }
110
-
111
-    /**
112
-     * Returns an array of categories suitable for a dropdown menu
113
-     * TODO: cache this
114
-     *
115
-     * @param int $parentID [optional]
116
-     * @param string $prefix [optional]
117
-     * @param int $maxDepth [optional]
118
-     * @return array
119
-     * @static
120
-     */
121
-    public static function get_category_hierarchy($parentID = 0, $prefix = '', $maxDepth = 999)
122
-    {
123
-        $out = array();
124
-        $cats = ProductCategory::get()
125
-            ->filter(array(
126
-                'ParentID'      => $parentID,
127
-                'ShowInMenus'   => 1,
128
-            ))
129
-            ->sort('Sort');
130
-
131
-        // If there is a single parent category (usually "Products" or something), we
132
-        // probably don't want that in the hierarchy.
133
-        if ($parentID == 0 && $cats->count() == 1) {
134
-            return self::get_category_hierarchy($cats->first()->ID, $prefix, $maxDepth);
135
-        }
136
-
137
-        foreach ($cats as $cat) {
138
-            $out[$cat->ID] = $prefix . $cat->Title;
139
-            if ($maxDepth > 1) {
140
-                $out += self::get_category_hierarchy($cat->ID, $prefix . $cat->Title . ' > ', $maxDepth - 1);
141
-            }
142
-        }
143
-
144
-        return $out;
145
-    }
146
-
147
-    /**
148
-     * @return ShopSearchAdapter
149
-     */
150
-    public static function adapter()
151
-    {
152
-        $adapterClass = Config::inst()->get('ShopSearch', 'adapter_class');
153
-        return Injector::inst()->get($adapterClass);
154
-    }
155
-
156
-    /**
157
-     * @return ShopSearch
158
-     */
159
-    public static function inst()
160
-    {
161
-        return Injector::inst()->get('ShopSearch');
162
-    }
163
-
164
-    /**
165
-     * The result will contain at least the following:
166
-     *      Matches - SS_List of results
167
-     *      TotalMatches - total # of results, unlimited
168
-     *      Query - query string
169
-     * Also saves a log record.
170
-     *
171
-     * @param array $vars
172
-     * @param bool $logSearch [optional]
173
-     * @param bool $useFacets [optional]
174
-     * @param int $start [optional]
175
-     * @param int $limit [optional]
176
-     * @return ArrayData
177
-     */
178
-    public function search(array $vars, $logSearch=true, $useFacets=true, $start=-1, $limit=-1)
179
-    {
180
-        $qs_q   = $this->config()->get('qs_query');
181
-        $qs_f   = $this->config()->get('qs_filters');
182
-        $qs_ps  = $this->config()->get('qs_parent_search');
183
-        $qs_t   = $this->config()->get('qs_title');
184
-        $qs_sort= $this->config()->get('qs_sort');
185
-        if ($limit < 0) {
186
-            $limit  = $this->config()->get('page_size');
187
-        }
188
-        if ($start < 0) {
189
-            $start  = !empty($vars['start']) ? (int)$vars['start'] : 0;
190
-        } // as far as i can see, fulltextsearch hard codes 'start'
191
-        $facets = $useFacets ? $this->config()->get('facets') : array();
192
-        if (!is_array($facets)) {
193
-            $facets = array();
194
-        }
195
-        if (empty($limit)) {
196
-            $limit = -1;
197
-        }
198
-
199
-        // figure out and scrub the sort
200
-        $sortOptions = $this->config()->get('sort_options');
201
-        $sort        = !empty($vars[$qs_sort]) ? $vars[$qs_sort] : '';
202
-        if (!isset($sortOptions[$sort])) {
203
-            $sort    = current(array_keys($sortOptions));
204
-        }
205
-
206
-        // figure out and scrub the filters
207
-        $filters  = !empty($vars[$qs_f]) ? FacetHelper::inst()->scrubFilters($vars[$qs_f]) : array();
208
-
209
-        // do the search
210
-        $keywords = !empty($vars[$qs_q]) ? $vars[$qs_q] : '';
211
-        if ($keywordRegex = $this->config()->get('keyword_filter_regex')) {
212
-            $keywords = preg_replace($keywordRegex, '', $keywords);
213
-        }
214
-        $results  = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort);
215
-
216
-        // massage the results a bit
217
-        if (!empty($keywords) && !$results->hasValue('Query')) {
218
-            $results->Query = $keywords;
219
-        }
220
-        if (!empty($filters) && !$results->hasValue('Filters')) {
221
-            $results->Filters = new ArrayData($filters);
222
-        }
223
-        if (!$results->hasValue('Sort')) {
224
-            $results->Sort = $sort;
225
-        }
226
-        if (!$results->hasValue('TotalMatches')) {
227
-            $results->TotalMatches = $results->Matches->hasMethod('getTotalItems')
228
-                ? $results->Matches->getTotalItems()
229
-                : $results->Matches->count();
230
-        }
231
-
232
-        // for some types of facets, update the state
233
-        if ($results->hasValue('Facets')) {
234
-            FacetHelper::inst()->transformHierarchies($results->Facets);
235
-            FacetHelper::inst()->updateFacetState($results->Facets, $filters);
236
-        }
237
-
238
-        // make a hash of the search so we can know if we've already logged it this session
239
-        $loggedFilters = !empty($filters) ? json_encode($filters) : null;
240
-        $loggedQuery   = strtolower($results->Query);
58
+	);
59
+
60
+	/**
61
+	 * @var array - default search facets (price, category, etc)
62
+	 *   Key    field name - e.g. Price - can be a VirtualFieldIndex field
63
+	 *   Value  facet label - e.g. Search By Category - if the value is a relation or returns an array or
64
+	 *          list all values will be faceted individually
65
+	 *          NOTE: this can also be another array with keys: Label, Type, and Values (for checkbox only)
66
+	 */
67
+	private static $facets = array();
68
+
69
+	/** @var array - field definition for Solr only */
70
+	private static $solr_fulltext_fields = array();
71
+
72
+	/** @var array - field definition for Solr only */
73
+	private static $solr_filter_fields = array();
74
+
75
+	/** @var string - if present, will create a copy of SiteTree_Title that's suited for alpha sorting */
76
+	private static $solr_title_sort_field = '';
77
+
78
+	/**
79
+	 * @var string - If present, everything matching the following regex will be removed from
80
+	 *               keyword search queries before passing to the search adapter.
81
+	 */
82
+	private static $keyword_filter_regex = '/[^a-zA-Z0-9\s\-]/';
83
+
84
+
85
+	/**
86
+	 * @return array
87
+	 */
88
+	public static function get_searchable_classes()
89
+	{
90
+		// First get any explicitly declared searchable classes
91
+		$searchable = Config::inst()->get('ShopSearch', 'searchable');
92
+		if (is_string($searchable) && strlen($searchable) > 0) {
93
+			$searchable = array($searchable);
94
+		} elseif (!is_array($searchable)) {
95
+			$searchable = array();
96
+		}
97
+
98
+		// Add in buyables automatically if asked
99
+		if (Config::inst()->get('ShopSearch', 'buyables_are_searchable')) {
100
+			$buyables = SS_ClassLoader::instance()->getManifest()->getImplementorsOf('Buyable');
101
+			if (is_array($buyables) && count($buyables) > 0) {
102
+				foreach ($buyables as $c) {
103
+					$searchable[] = $c;
104
+				}
105
+			}
106
+		}
107
+
108
+		return array_unique($searchable);
109
+	}
110
+
111
+	/**
112
+	 * Returns an array of categories suitable for a dropdown menu
113
+	 * TODO: cache this
114
+	 *
115
+	 * @param int $parentID [optional]
116
+	 * @param string $prefix [optional]
117
+	 * @param int $maxDepth [optional]
118
+	 * @return array
119
+	 * @static
120
+	 */
121
+	public static function get_category_hierarchy($parentID = 0, $prefix = '', $maxDepth = 999)
122
+	{
123
+		$out = array();
124
+		$cats = ProductCategory::get()
125
+			->filter(array(
126
+				'ParentID'      => $parentID,
127
+				'ShowInMenus'   => 1,
128
+			))
129
+			->sort('Sort');
130
+
131
+		// If there is a single parent category (usually "Products" or something), we
132
+		// probably don't want that in the hierarchy.
133
+		if ($parentID == 0 && $cats->count() == 1) {
134
+			return self::get_category_hierarchy($cats->first()->ID, $prefix, $maxDepth);
135
+		}
136
+
137
+		foreach ($cats as $cat) {
138
+			$out[$cat->ID] = $prefix . $cat->Title;
139
+			if ($maxDepth > 1) {
140
+				$out += self::get_category_hierarchy($cat->ID, $prefix . $cat->Title . ' > ', $maxDepth - 1);
141
+			}
142
+		}
143
+
144
+		return $out;
145
+	}
146
+
147
+	/**
148
+	 * @return ShopSearchAdapter
149
+	 */
150
+	public static function adapter()
151
+	{
152
+		$adapterClass = Config::inst()->get('ShopSearch', 'adapter_class');
153
+		return Injector::inst()->get($adapterClass);
154
+	}
155
+
156
+	/**
157
+	 * @return ShopSearch
158
+	 */
159
+	public static function inst()
160
+	{
161
+		return Injector::inst()->get('ShopSearch');
162
+	}
163
+
164
+	/**
165
+	 * The result will contain at least the following:
166
+	 *      Matches - SS_List of results
167
+	 *      TotalMatches - total # of results, unlimited
168
+	 *      Query - query string
169
+	 * Also saves a log record.
170
+	 *
171
+	 * @param array $vars
172
+	 * @param bool $logSearch [optional]
173
+	 * @param bool $useFacets [optional]
174
+	 * @param int $start [optional]
175
+	 * @param int $limit [optional]
176
+	 * @return ArrayData
177
+	 */
178
+	public function search(array $vars, $logSearch=true, $useFacets=true, $start=-1, $limit=-1)
179
+	{
180
+		$qs_q   = $this->config()->get('qs_query');
181
+		$qs_f   = $this->config()->get('qs_filters');
182
+		$qs_ps  = $this->config()->get('qs_parent_search');
183
+		$qs_t   = $this->config()->get('qs_title');
184
+		$qs_sort= $this->config()->get('qs_sort');
185
+		if ($limit < 0) {
186
+			$limit  = $this->config()->get('page_size');
187
+		}
188
+		if ($start < 0) {
189
+			$start  = !empty($vars['start']) ? (int)$vars['start'] : 0;
190
+		} // as far as i can see, fulltextsearch hard codes 'start'
191
+		$facets = $useFacets ? $this->config()->get('facets') : array();
192
+		if (!is_array($facets)) {
193
+			$facets = array();
194
+		}
195
+		if (empty($limit)) {
196
+			$limit = -1;
197
+		}
198
+
199
+		// figure out and scrub the sort
200
+		$sortOptions = $this->config()->get('sort_options');
201
+		$sort        = !empty($vars[$qs_sort]) ? $vars[$qs_sort] : '';
202
+		if (!isset($sortOptions[$sort])) {
203
+			$sort    = current(array_keys($sortOptions));
204
+		}
205
+
206
+		// figure out and scrub the filters
207
+		$filters  = !empty($vars[$qs_f]) ? FacetHelper::inst()->scrubFilters($vars[$qs_f]) : array();
208
+
209
+		// do the search
210
+		$keywords = !empty($vars[$qs_q]) ? $vars[$qs_q] : '';
211
+		if ($keywordRegex = $this->config()->get('keyword_filter_regex')) {
212
+			$keywords = preg_replace($keywordRegex, '', $keywords);
213
+		}
214
+		$results  = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort);
215
+
216
+		// massage the results a bit
217
+		if (!empty($keywords) && !$results->hasValue('Query')) {
218
+			$results->Query = $keywords;
219
+		}
220
+		if (!empty($filters) && !$results->hasValue('Filters')) {
221
+			$results->Filters = new ArrayData($filters);
222
+		}
223
+		if (!$results->hasValue('Sort')) {
224
+			$results->Sort = $sort;
225
+		}
226
+		if (!$results->hasValue('TotalMatches')) {
227
+			$results->TotalMatches = $results->Matches->hasMethod('getTotalItems')
228
+				? $results->Matches->getTotalItems()
229
+				: $results->Matches->count();
230
+		}
231
+
232
+		// for some types of facets, update the state
233
+		if ($results->hasValue('Facets')) {
234
+			FacetHelper::inst()->transformHierarchies($results->Facets);
235
+			FacetHelper::inst()->updateFacetState($results->Facets, $filters);
236
+		}
237
+
238
+		// make a hash of the search so we can know if we've already logged it this session
239
+		$loggedFilters = !empty($filters) ? json_encode($filters) : null;
240
+		$loggedQuery   = strtolower($results->Query);
241 241
 //		$searchHash    = md5($loggedFilters . $loggedQuery);
242 242
 //		$sessSearches  = Session::get('loggedSearches');
243 243
 //		if (!is_array($sessSearches)) $sessSearches = array();
244 244
 //		Debug::dump($searchHash, $sessSearches);
245 245
 
246
-        // save the log record
247
-        if ($start == 0 && $logSearch && (!empty($keywords) || !empty($filters))) { // && !in_array($searchHash, $sessSearches)) {
248
-            $log = SearchLog::create(array(
249
-                'Query'         => $loggedQuery,
250
-                'Title'         => !empty($vars[$qs_t]) ? $vars[$qs_t] : '',
251
-                'Link'          => Controller::curr()->getRequest()->getURL(true), // I'm not 100% happy with this, but can't think of a better way
252
-                'NumResults'    => $results->TotalMatches,
253
-                'MemberID'      => Member::currentUserID(),
254
-                'Filters'       => $loggedFilters,
255
-                'ParentSearchID'=> !empty($vars[$qs_ps]) ? $vars[$qs_ps] : 0,
256
-            ));
257
-            $log->write();
258
-            $results->SearchLogID = $log->ID;
259
-            $results->SearchBreadcrumbs = $log->getBreadcrumbs();
246
+		// save the log record
247
+		if ($start == 0 && $logSearch && (!empty($keywords) || !empty($filters))) { // && !in_array($searchHash, $sessSearches)) {
248
+			$log = SearchLog::create(array(
249
+				'Query'         => $loggedQuery,
250
+				'Title'         => !empty($vars[$qs_t]) ? $vars[$qs_t] : '',
251
+				'Link'          => Controller::curr()->getRequest()->getURL(true), // I'm not 100% happy with this, but can't think of a better way
252
+				'NumResults'    => $results->TotalMatches,
253
+				'MemberID'      => Member::currentUserID(),
254
+				'Filters'       => $loggedFilters,
255
+				'ParentSearchID'=> !empty($vars[$qs_ps]) ? $vars[$qs_ps] : 0,
256
+			));
257
+			$log->write();
258
+			$results->SearchLogID = $log->ID;
259
+			$results->SearchBreadcrumbs = $log->getBreadcrumbs();
260 260
 
261 261
 //			$sessSearches[] = $searchHash;
262 262
 //			Session::set('loggedSearches', $sessSearches);
263
-        }
264
-
265
-        return $results;
266
-    }
267
-
268
-    /**
269
-     * @param string $str
270
-     * @return SS_Query
271
-     */
272
-    public function getSuggestQuery($str='')
273
-    {
274
-        $hasResults = 'CASE WHEN max("SearchLog"."NumResults") > 0 THEN 1 ELSE 0 END';
275
-        $searchCount = 'count(distinct "SearchLog"."ID")';
276
-        $q = new SQLQuery();
277
-        $q = $q->setSelect('"SearchLog"."Query"')
278
-            // TODO: what to do with filter?
279
-            ->selectField($searchCount, 'SearchCount')
280
-            ->selectField('max("SearchLog"."Created")', 'LastSearch')
281
-            ->selectField('max("SearchLog"."NumResults")', 'NumResults')
282
-            ->selectField($hasResults, 'HasResults')
283
-            ->setFrom('"SearchLog"')
284
-            ->setGroupBy('"SearchLog"."Query"')
285
-            ->setOrderBy(array(
286
-                "$hasResults DESC",
287
-                "$searchCount DESC"
288
-            ))
289
-            ->setLimit(Config::inst()->get('ShopSearch', 'suggest_limit'))
290
-        ;
291
-
292
-        if (strlen($str) > 0) {
293
-            $q = $q->addWhere(sprintf('"SearchLog"."Query" LIKE \'%%%s%%\'', Convert::raw2sql($str)));
294
-        }
295
-
296
-        return $q;
297
-    }
298
-
299
-
300
-    /**
301
-     * @param string $str
302
-     * @return array
303
-     */
304
-    public function suggest($str='')
305
-    {
306
-        $adapter = self::adapter();
307
-        if ($adapter->hasMethod('suggest')) {
308
-            return $adapter->suggest($str);
309
-        } else {
310
-            return $this->getSuggestQuery($str)->execute()->column('Query');
311
-        }
312
-    }
313
-
314
-
315
-    /**
316
-     * Returns an array that can be made into json and passed to the controller
317
-     * containing both term suggestions and a few product matches.
318
-     *
319
-     * @param array $searchVars
320
-     * @return array
321
-     */
322
-    public function suggestWithResults(array $searchVars)
323
-    {
324
-        $qs_q       = $this->config()->get('qs_query');
325
-        $qs_f       = $this->config()->get('qs_filters');
326
-        $keywords   = !empty($searchVars[$qs_q]) ? $searchVars[$qs_q] : '';
327
-        $filters    = !empty($searchVars[$qs_f]) ? $searchVars[$qs_f] : array();
328
-
329
-        $adapter = self::adapter();
330
-
331
-        // get suggestions and product list from the adapter
332
-        if ($adapter->hasMethod('suggestWithResults')) {
333
-            $results = $adapter->suggestWithResults($keywords, $filters);
334
-        } else {
335
-            $limit      = (int)ShopSearch::config()->sayt_limit;
336
-            $search     = self::adapter()->searchFromVars($keywords, $filters, array(), 0, $limit, 'Popularity DESC');
337
-            //$search     = ShopSearch::inst()->search($searchVars, false, false, 0, $limit);
338
-
339
-            $results = array(
340
-                'products'      => $search->Matches,
341
-                'suggestions'   => $this->suggest($keywords),
342
-            );
343
-        }
344
-
345
-        // the adapter just gave us a list of products, which we need to process a little further
346
-        if (!empty($results['products'])) {
347
-            // this gets encoded into the product links
348
-            $searchVars['total'] = $results['products']->hasMethod('getTotalItems')
349
-                ? $results['products']->getTotalItems()
350
-                : $results['products']->count();
351
-
352
-            $products   = array();
353
-            foreach ($results['products'] as $prod) {
354
-                if (!$prod || !$prod->exists()) {
355
-                    continue;
356
-                }
357
-                $img = $prod->hasMethod('ProductImage') ? $prod->ProductImage() : $prod->Image();
358
-                $thumb = ($img && $img->exists()) ? $img->getThumbnail() : null;
359
-
360
-                $json = array(
361
-                    'link'  => $prod->Link() . '?' . ShopSearch::config()->qs_source . '=' . urlencode(base64_encode(json_encode($searchVars))),
362
-                    'title' => $prod->Title,
363
-                    'desc'  => $prod->obj('Content')->Summary(),
364
-                    'thumb' => $thumb ? $thumb->Link() : '',
365
-                    'price' => $prod->obj('Price')->Nice(),
366
-                );
367
-
368
-                if ($prod->hasExtension('HasPromotionalPricing') && $prod->hasValidPromotion()) {
369
-                    $json['original_price'] = $prod->getOriginalPrice()->Nice();
370
-                }
371
-
372
-                $products[] = $json;
373
-            }
374
-
375
-            // replace the list of product objects with json
376
-            $results['products'] = $products;
377
-        }
378
-
379
-        $this->extend('updateSuggestWithResults', $results, $keywords, $filters);
380
-
381
-        return $results;
382
-    }
263
+		}
264
+
265
+		return $results;
266
+	}
267
+
268
+	/**
269
+	 * @param string $str
270
+	 * @return SS_Query
271
+	 */
272
+	public function getSuggestQuery($str='')
273
+	{
274
+		$hasResults = 'CASE WHEN max("SearchLog"."NumResults") > 0 THEN 1 ELSE 0 END';
275
+		$searchCount = 'count(distinct "SearchLog"."ID")';
276
+		$q = new SQLQuery();
277
+		$q = $q->setSelect('"SearchLog"."Query"')
278
+			// TODO: what to do with filter?
279
+			->selectField($searchCount, 'SearchCount')
280
+			->selectField('max("SearchLog"."Created")', 'LastSearch')
281
+			->selectField('max("SearchLog"."NumResults")', 'NumResults')
282
+			->selectField($hasResults, 'HasResults')
283
+			->setFrom('"SearchLog"')
284
+			->setGroupBy('"SearchLog"."Query"')
285
+			->setOrderBy(array(
286
+				"$hasResults DESC",
287
+				"$searchCount DESC"
288
+			))
289
+			->setLimit(Config::inst()->get('ShopSearch', 'suggest_limit'))
290
+		;
291
+
292
+		if (strlen($str) > 0) {
293
+			$q = $q->addWhere(sprintf('"SearchLog"."Query" LIKE \'%%%s%%\'', Convert::raw2sql($str)));
294
+		}
295
+
296
+		return $q;
297
+	}
298
+
299
+
300
+	/**
301
+	 * @param string $str
302
+	 * @return array
303
+	 */
304
+	public function suggest($str='')
305
+	{
306
+		$adapter = self::adapter();
307
+		if ($adapter->hasMethod('suggest')) {
308
+			return $adapter->suggest($str);
309
+		} else {
310
+			return $this->getSuggestQuery($str)->execute()->column('Query');
311
+		}
312
+	}
313
+
314
+
315
+	/**
316
+	 * Returns an array that can be made into json and passed to the controller
317
+	 * containing both term suggestions and a few product matches.
318
+	 *
319
+	 * @param array $searchVars
320
+	 * @return array
321
+	 */
322
+	public function suggestWithResults(array $searchVars)
323
+	{
324
+		$qs_q       = $this->config()->get('qs_query');
325
+		$qs_f       = $this->config()->get('qs_filters');
326
+		$keywords   = !empty($searchVars[$qs_q]) ? $searchVars[$qs_q] : '';
327
+		$filters    = !empty($searchVars[$qs_f]) ? $searchVars[$qs_f] : array();
328
+
329
+		$adapter = self::adapter();
330
+
331
+		// get suggestions and product list from the adapter
332
+		if ($adapter->hasMethod('suggestWithResults')) {
333
+			$results = $adapter->suggestWithResults($keywords, $filters);
334
+		} else {
335
+			$limit      = (int)ShopSearch::config()->sayt_limit;
336
+			$search     = self::adapter()->searchFromVars($keywords, $filters, array(), 0, $limit, 'Popularity DESC');
337
+			//$search     = ShopSearch::inst()->search($searchVars, false, false, 0, $limit);
338
+
339
+			$results = array(
340
+				'products'      => $search->Matches,
341
+				'suggestions'   => $this->suggest($keywords),
342
+			);
343
+		}
344
+
345
+		// the adapter just gave us a list of products, which we need to process a little further
346
+		if (!empty($results['products'])) {
347
+			// this gets encoded into the product links
348
+			$searchVars['total'] = $results['products']->hasMethod('getTotalItems')
349
+				? $results['products']->getTotalItems()
350
+				: $results['products']->count();
351
+
352
+			$products   = array();
353
+			foreach ($results['products'] as $prod) {
354
+				if (!$prod || !$prod->exists()) {
355
+					continue;
356
+				}
357
+				$img = $prod->hasMethod('ProductImage') ? $prod->ProductImage() : $prod->Image();
358
+				$thumb = ($img && $img->exists()) ? $img->getThumbnail() : null;
359
+
360
+				$json = array(
361
+					'link'  => $prod->Link() . '?' . ShopSearch::config()->qs_source . '=' . urlencode(base64_encode(json_encode($searchVars))),
362
+					'title' => $prod->Title,
363
+					'desc'  => $prod->obj('Content')->Summary(),
364
+					'thumb' => $thumb ? $thumb->Link() : '',
365
+					'price' => $prod->obj('Price')->Nice(),
366
+				);
367
+
368
+				if ($prod->hasExtension('HasPromotionalPricing') && $prod->hasValidPromotion()) {
369
+					$json['original_price'] = $prod->getOriginalPrice()->Nice();
370
+				}
371
+
372
+				$products[] = $json;
373
+			}
374
+
375
+			// replace the list of product objects with json
376
+			$results['products'] = $products;
377
+		}
378
+
379
+		$this->extend('updateSuggestWithResults', $results, $keywords, $filters);
380
+
381
+		return $results;
382
+	}
383 383
 }
Please login to merge, or discard this patch.
Spacing   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -135,9 +135,9 @@  discard block
 block discarded – undo
135 135
         }
136 136
 
137 137
         foreach ($cats as $cat) {
138
-            $out[$cat->ID] = $prefix . $cat->Title;
138
+            $out[$cat->ID] = $prefix.$cat->Title;
139 139
             if ($maxDepth > 1) {
140
-                $out += self::get_category_hierarchy($cat->ID, $prefix . $cat->Title . ' > ', $maxDepth - 1);
140
+                $out += self::get_category_hierarchy($cat->ID, $prefix.$cat->Title.' > ', $maxDepth - 1);
141 141
             }
142 142
         }
143 143
 
@@ -175,13 +175,13 @@  discard block
 block discarded – undo
175 175
      * @param int $limit [optional]
176 176
      * @return ArrayData
177 177
      */
178
-    public function search(array $vars, $logSearch=true, $useFacets=true, $start=-1, $limit=-1)
178
+    public function search(array $vars, $logSearch = true, $useFacets = true, $start = -1, $limit = -1)
179 179
     {
180 180
         $qs_q   = $this->config()->get('qs_query');
181 181
         $qs_f   = $this->config()->get('qs_filters');
182 182
         $qs_ps  = $this->config()->get('qs_parent_search');
183 183
         $qs_t   = $this->config()->get('qs_title');
184
-        $qs_sort= $this->config()->get('qs_sort');
184
+        $qs_sort = $this->config()->get('qs_sort');
185 185
         if ($limit < 0) {
186 186
             $limit  = $this->config()->get('page_size');
187 187
         }
@@ -211,7 +211,7 @@  discard block
 block discarded – undo
211 211
         if ($keywordRegex = $this->config()->get('keyword_filter_regex')) {
212 212
             $keywords = preg_replace($keywordRegex, '', $keywords);
213 213
         }
214
-        $results  = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort);
214
+        $results = self::adapter()->searchFromVars($keywords, $filters, $facets, $start, $limit, $sort);
215 215
 
216 216
         // massage the results a bit
217 217
         if (!empty($keywords) && !$results->hasValue('Query')) {
@@ -269,7 +269,7 @@  discard block
 block discarded – undo
269 269
      * @param string $str
270 270
      * @return SS_Query
271 271
      */
272
-    public function getSuggestQuery($str='')
272
+    public function getSuggestQuery($str = '')
273 273
     {
274 274
         $hasResults = 'CASE WHEN max("SearchLog"."NumResults") > 0 THEN 1 ELSE 0 END';
275 275
         $searchCount = 'count(distinct "SearchLog"."ID")';
@@ -301,7 +301,7 @@  discard block
 block discarded – undo
301 301
      * @param string $str
302 302
      * @return array
303 303
      */
304
-    public function suggest($str='')
304
+    public function suggest($str = '')
305 305
     {
306 306
         $adapter = self::adapter();
307 307
         if ($adapter->hasMethod('suggest')) {
@@ -349,7 +349,7 @@  discard block
 block discarded – undo
349 349
                 ? $results['products']->getTotalItems()
350 350
                 : $results['products']->count();
351 351
 
352
-            $products   = array();
352
+            $products = array();
353 353
             foreach ($results['products'] as $prod) {
354 354
                 if (!$prod || !$prod->exists()) {
355 355
                     continue;
@@ -358,7 +358,7 @@  discard block
 block discarded – undo
358 358
                 $thumb = ($img && $img->exists()) ? $img->getThumbnail() : null;
359 359
 
360 360
                 $json = array(
361
-                    'link'  => $prod->Link() . '?' . ShopSearch::config()->qs_source . '=' . urlencode(base64_encode(json_encode($searchVars))),
361
+                    'link'  => $prod->Link().'?'.ShopSearch::config()->qs_source.'='.urlencode(base64_encode(json_encode($searchVars))),
362 362
                     'title' => $prod->Title,
363 363
                     'desc'  => $prod->obj('Content')->Summary(),
364 364
                     'thumb' => $thumb ? $thumb->Link() : '',
Please login to merge, or discard this patch.
code/ShopSearchControllerExtension.php 3 patches
Doc Comments   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -21,7 +21,7 @@
 block discarded – undo
21 21
 
22 22
     /**
23 23
      * @param SS_HTTPRequest $req
24
-     * @return string
24
+     * @return SS_HTTPResponse
25 25
      */
26 26
     public function search_suggest(SS_HTTPRequest $req)
27 27
     {
Please login to merge, or discard this patch.
Indentation   +70 added lines, -70 removed lines patch added patch discarded remove patch
@@ -8,85 +8,85 @@
 block discarded – undo
8 8
  */
9 9
 class ShopSearchControllerExtension extends Extension
10 10
 {
11
-    private static $allowed_actions = array('SearchForm', 'results', 'search_suggest');
11
+	private static $allowed_actions = array('SearchForm', 'results', 'search_suggest');
12 12
 
13
-    /**
14
-     * @return ShopSearchForm
15
-     */
16
-    public function SearchForm()
17
-    {
18
-        return ShopSearchForm::create($this->owner, 'SearchForm', $this->owner->Link() . 'search-suggest');
19
-    }
13
+	/**
14
+	 * @return ShopSearchForm
15
+	 */
16
+	public function SearchForm()
17
+	{
18
+		return ShopSearchForm::create($this->owner, 'SearchForm', $this->owner->Link() . 'search-suggest');
19
+	}
20 20
 
21 21
 
22
-    /**
23
-     * @param SS_HTTPRequest $req
24
-     * @return string
25
-     */
26
-    public function search_suggest(SS_HTTPRequest $req)
27
-    {
28
-        /** @var SS_HTTPResponse $response */
29
-        $response    = $this->owner->getResponse();
30
-        $callback    = $req->requestVar('callback');
22
+	/**
23
+	 * @param SS_HTTPRequest $req
24
+	 * @return string
25
+	 */
26
+	public function search_suggest(SS_HTTPRequest $req)
27
+	{
28
+		/** @var SS_HTTPResponse $response */
29
+		$response    = $this->owner->getResponse();
30
+		$callback    = $req->requestVar('callback');
31 31
 
32
-        // convert the search results into usable json for search-as-you-type
33
-        if (ShopSearch::config()->search_as_you_type_enabled) {
34
-            $searchVars = $req->requestVars();
35
-            $searchVars[ ShopSearch::config()->qs_query ] = $searchVars['term'];
36
-            unset($searchVars['term']);
37
-            $results = ShopSearch::inst()->suggestWithResults($searchVars);
38
-        } else {
39
-            $results = array(
40
-                'suggestions'   => ShopSearch::inst()->suggest($req->requestVar('term')),
41
-            );
42
-        }
32
+		// convert the search results into usable json for search-as-you-type
33
+		if (ShopSearch::config()->search_as_you_type_enabled) {
34
+			$searchVars = $req->requestVars();
35
+			$searchVars[ ShopSearch::config()->qs_query ] = $searchVars['term'];
36
+			unset($searchVars['term']);
37
+			$results = ShopSearch::inst()->suggestWithResults($searchVars);
38
+		} else {
39
+			$results = array(
40
+				'suggestions'   => ShopSearch::inst()->suggest($req->requestVar('term')),
41
+			);
42
+		}
43 43
 
44
-        if ($callback) {
45
-            $response->addHeader('Content-type', 'application/javascript');
46
-            $response->setBody($callback . '(' . json_encode($results) . ');');
47
-        } else {
48
-            $response->addHeader('Content-type', 'application/json');
49
-            $response->setBody(json_encode($results));
50
-        }
51
-        return $response;
52
-    }
44
+		if ($callback) {
45
+			$response->addHeader('Content-type', 'application/javascript');
46
+			$response->setBody($callback . '(' . json_encode($results) . ');');
47
+		} else {
48
+			$response->addHeader('Content-type', 'application/json');
49
+			$response->setBody(json_encode($results));
50
+		}
51
+		return $response;
52
+	}
53 53
 
54 54
 
55
-    /**
56
-     * If there is a search encoded in the link, go ahead and log it.
57
-     * This happens when you click through on a search suggestion
58
-     */
59
-    public function onAfterInit()
60
-    {
61
-        $req = $this->owner->getRequest();
62
-        $src = $req->requestVar(Config::inst()->get('ShopSearch', 'qs_source'));
63
-        if ($src) {
64
-            $qs_q   = Config::inst()->get('ShopSearch', 'qs_query');
65
-            $qs_f   = Config::inst()->get('ShopSearch', 'qs_filters');
66
-            $vars   = json_decode(base64_decode($src), true);
55
+	/**
56
+	 * If there is a search encoded in the link, go ahead and log it.
57
+	 * This happens when you click through on a search suggestion
58
+	 */
59
+	public function onAfterInit()
60
+	{
61
+		$req = $this->owner->getRequest();
62
+		$src = $req->requestVar(Config::inst()->get('ShopSearch', 'qs_source'));
63
+		if ($src) {
64
+			$qs_q   = Config::inst()->get('ShopSearch', 'qs_query');
65
+			$qs_f   = Config::inst()->get('ShopSearch', 'qs_filters');
66
+			$vars   = json_decode(base64_decode($src), true);
67 67
 
68
-            // log the search
69
-            $log = SearchLog::create(array(
70
-                'Query'         => strtolower($vars[$qs_q]),
71
-                'Link'          => $req->getURL(false), // These searches will never have child searches, but this will allow us to know what they clicked
72
-                'NumResults'    => $vars['total'],
73
-                'MemberID'      => Member::currentUserID(),
74
-                'Filters'       => !empty($vars[$qs_f]) ? json_encode($vars[$qs_f]) : null,
75
-            ));
76
-            $log->write();
68
+			// log the search
69
+			$log = SearchLog::create(array(
70
+				'Query'         => strtolower($vars[$qs_q]),
71
+				'Link'          => $req->getURL(false), // These searches will never have child searches, but this will allow us to know what they clicked
72
+				'NumResults'    => $vars['total'],
73
+				'MemberID'      => Member::currentUserID(),
74
+				'Filters'       => !empty($vars[$qs_f]) ? json_encode($vars[$qs_f]) : null,
75
+			));
76
+			$log->write();
77 77
 
78
-            // redirect to the clean page
79
-            $this->owner->redirect($req->getURL(false));
80
-        }
81
-    }
78
+			// redirect to the clean page
79
+			$this->owner->redirect($req->getURL(false));
80
+		}
81
+	}
82 82
 
83 83
 
84
-    /**
85
-     * @param ArrayData $results
86
-     * @param array     $data
87
-     * @return string
88
-     */
89
-    protected function generateLongTitle(ArrayData $results, array $data)
90
-    {
91
-    }
84
+	/**
85
+	 * @param ArrayData $results
86
+	 * @param array     $data
87
+	 * @return string
88
+	 */
89
+	protected function generateLongTitle(ArrayData $results, array $data)
90
+	{
91
+	}
92 92
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -15,7 +15,7 @@  discard block
 block discarded – undo
15 15
      */
16 16
     public function SearchForm()
17 17
     {
18
-        return ShopSearchForm::create($this->owner, 'SearchForm', $this->owner->Link() . 'search-suggest');
18
+        return ShopSearchForm::create($this->owner, 'SearchForm', $this->owner->Link().'search-suggest');
19 19
     }
20 20
 
21 21
 
@@ -32,7 +32,7 @@  discard block
 block discarded – undo
32 32
         // convert the search results into usable json for search-as-you-type
33 33
         if (ShopSearch::config()->search_as_you_type_enabled) {
34 34
             $searchVars = $req->requestVars();
35
-            $searchVars[ ShopSearch::config()->qs_query ] = $searchVars['term'];
35
+            $searchVars[ShopSearch::config()->qs_query] = $searchVars['term'];
36 36
             unset($searchVars['term']);
37 37
             $results = ShopSearch::inst()->suggestWithResults($searchVars);
38 38
         } else {
@@ -43,7 +43,7 @@  discard block
 block discarded – undo
43 43
 
44 44
         if ($callback) {
45 45
             $response->addHeader('Content-type', 'application/javascript');
46
-            $response->setBody($callback . '(' . json_encode($results) . ');');
46
+            $response->setBody($callback.'('.json_encode($results).');');
47 47
         } else {
48 48
             $response->addHeader('Content-type', 'application/json');
49 49
             $response->setBody(json_encode($results));
Please login to merge, or discard this patch.
code/SearchLog.php 1 patch
Indentation   +50 added lines, -50 removed lines patch added patch discarded remove patch
@@ -8,54 +8,54 @@
 block discarded – undo
8 8
  */
9 9
 class SearchLog extends DataObject
10 10
 {
11
-    private static $db = array(
12
-        'Query'         => 'Varchar(255)',
13
-        'Title'         => 'Varchar(255)', // title for breadcrumbs. any new facets added will be reflected here
14
-        'Link'          => 'Varchar(255)',
15
-        'Filters'       => 'Text', // json
16
-        'NumResults'    => 'Int',
17
-    );
18
-
19
-    private static $has_one = array(
20
-        'Member'        => 'Member',
21
-        'ParentSearch'  => 'SearchLog', // used in constructing a search breadcrumb
22
-    );
23
-
24
-
25
-    /**
26
-     * Generate the title if needed
27
-     */
28
-    protected function onBeforeWrite()
29
-    {
30
-        parent::onBeforeWrite();
31
-        if (!$this->Title) {
32
-            $this->Title = empty($this->Query) ? "Search" : "Search: {$this->Query}";
33
-        }
34
-    }
35
-
36
-
37
-    /**
38
-     * @return ArrayList
39
-     */
40
-    public function getBreadcrumbs()
41
-    {
42
-        $out    = new ArrayList();
43
-        $cur    = $this;
44
-
45
-        while ($cur && $cur->exists()) {
46
-            $out->unshift($cur);
47
-            $cur = $cur->ParentSearchID > 0 ? $cur->ParentSearch() : null;
48
-        }
49
-
50
-        return $out;
51
-    }
52
-
53
-
54
-    /**
55
-     * @return array
56
-     */
57
-    public function getFiltersArray()
58
-    {
59
-        return $this->Filters ? json_decode($this->Filters, true) : array();
60
-    }
11
+	private static $db = array(
12
+		'Query'         => 'Varchar(255)',
13
+		'Title'         => 'Varchar(255)', // title for breadcrumbs. any new facets added will be reflected here
14
+		'Link'          => 'Varchar(255)',
15
+		'Filters'       => 'Text', // json
16
+		'NumResults'    => 'Int',
17
+	);
18
+
19
+	private static $has_one = array(
20
+		'Member'        => 'Member',
21
+		'ParentSearch'  => 'SearchLog', // used in constructing a search breadcrumb
22
+	);
23
+
24
+
25
+	/**
26
+	 * Generate the title if needed
27
+	 */
28
+	protected function onBeforeWrite()
29
+	{
30
+		parent::onBeforeWrite();
31
+		if (!$this->Title) {
32
+			$this->Title = empty($this->Query) ? "Search" : "Search: {$this->Query}";
33
+		}
34
+	}
35
+
36
+
37
+	/**
38
+	 * @return ArrayList
39
+	 */
40
+	public function getBreadcrumbs()
41
+	{
42
+		$out    = new ArrayList();
43
+		$cur    = $this;
44
+
45
+		while ($cur && $cur->exists()) {
46
+			$out->unshift($cur);
47
+			$cur = $cur->ParentSearchID > 0 ? $cur->ParentSearch() : null;
48
+		}
49
+
50
+		return $out;
51
+	}
52
+
53
+
54
+	/**
55
+	 * @return array
56
+	 */
57
+	public function getFiltersArray()
58
+	{
59
+		return $this->Filters ? json_decode($this->Filters, true) : array();
60
+	}
61 61
 }
Please login to merge, or discard this patch.
code/ShopSearchAdapter.php 2 patches
Spacing   +1 added lines, -1 removed lines patch added patch discarded remove patch
@@ -17,5 +17,5 @@
 block discarded – undo
17 17
      * @param string $sort [optional]
18 18
      * @return ArrayData - must contain at least 'Matches' with an list of data objects that match the search
19 19
      */
20
-    public function searchFromVars($keywords, array $filters=array(), array $facetSpec=array(), $start=-1, $limit=-1, $sort='');
20
+    public function searchFromVars($keywords, array $filters = array(), array $facetSpec = array(), $start = -1, $limit = -1, $sort = '');
21 21
 }
Please login to merge, or discard this patch.
Indentation   +10 added lines, -10 removed lines patch added patch discarded remove patch
@@ -8,14 +8,14 @@
 block discarded – undo
8 8
  */
9 9
 interface ShopSearchAdapter
10 10
 {
11
-    /**
12
-     * @param string $keywords
13
-     * @param array $filters [optional]
14
-     * @param array $facetSpec [optional]
15
-     * @param int $start [optional]
16
-     * @param int $limit [optional]
17
-     * @param string $sort [optional]
18
-     * @return ArrayData - must contain at least 'Matches' with an list of data objects that match the search
19
-     */
20
-    public function searchFromVars($keywords, array $filters=array(), array $facetSpec=array(), $start=-1, $limit=-1, $sort='');
11
+	/**
12
+	 * @param string $keywords
13
+	 * @param array $filters [optional]
14
+	 * @param array $facetSpec [optional]
15
+	 * @param int $start [optional]
16
+	 * @param int $limit [optional]
17
+	 * @param string $sort [optional]
18
+	 * @return ArrayData - must contain at least 'Matches' with an list of data objects that match the search
19
+	 */
20
+	public function searchFromVars($keywords, array $filters=array(), array $facetSpec=array(), $start=-1, $limit=-1, $sort='');
21 21
 }
Please login to merge, or discard this patch.
code/helpers/FacetedCategory.php 2 patches
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -73,7 +73,7 @@  discard block
 block discarded – undo
73 73
      */
74 74
     protected function getFilters()
75 75
     {
76
-        $qs_f       = Config::inst()->get('ShopSearch', 'qs_filters');
76
+        $qs_f = Config::inst()->get('ShopSearch', 'qs_filters');
77 77
         if (!$qs_f) {
78 78
             return array();
79 79
         }
@@ -90,7 +90,7 @@  discard block
 block discarded – undo
90 90
      * @param bool $recursive
91 91
      * @return mixed
92 92
      */
93
-    public function FilteredProducts($recursive=true)
93
+    public function FilteredProducts($recursive = true)
94 94
     {
95 95
         if (!isset($this->_filteredProducts)) {
96 96
             $fn = Config::inst()->get('FacetedCategory', 'products_method');
@@ -124,7 +124,7 @@  discard block
 block discarded – undo
124 124
      */
125 125
     public function Facets()
126 126
     {
127
-        $spec       = $this->getFacetSpec();
127
+        $spec = $this->getFacetSpec();
128 128
         if (empty($spec)) {
129 129
             return new ArrayList;
130 130
         }
Please login to merge, or discard this patch.
Indentation   +134 added lines, -134 removed lines patch added patch discarded remove patch
@@ -16,138 +16,138 @@
 block discarded – undo
16 16
  */
17 17
 class FacetedCategory extends SiteTreeExtension
18 18
 {
19
-    private static $db = array(
20
-        'DisabledFacets' => 'Text', // This will be a comma-delimited list of facets that aren't used for a given category
21
-    );
22
-
23
-    /** @var array - facet definition - see ShopSearch and/or docs/en/Facets.md for format */
24
-    private static $facets = array();
25
-
26
-    /** @var bool - if true there will be a tab in the cms to disable some or all defined facets */
27
-    private static $show_disabled_facets_tab = true;
28
-
29
-    /** @var string - which method should we use to get the child products for FilteredProducts */
30
-    private static $products_method = 'ProductsShowable';
31
-
32
-    /** @var bool - automatically create facets for static attributes */
33
-    private static $auto_facet_attributes = false;
34
-
35
-
36
-    /**
37
-     * @param FieldList $fields
38
-     */
39
-    public function updateCMSFields(FieldList $fields)
40
-    {
41
-        if (Config::inst()->get('FacetedCategory', 'show_disabled_facets_tab')) {
42
-            $spec = FacetHelper::inst()->expandFacetSpec($this->getFacetSpec());
43
-            $facets = array();
44
-            foreach ($spec as $f => $v) {
45
-                $facets[$f] = $v['Label'];
46
-            }
47
-            $fields->addFieldToTab('Root.Facets', CheckboxSetField::create('DisabledFacets', "Don't show the following facets for this category:", $facets));
48
-        }
49
-    }
50
-
51
-
52
-    /**
53
-     * @return Controller
54
-     */
55
-    protected function getController()
56
-    {
57
-        return ($this->owner instanceof Controller) ? $this->owner : Controller::curr();
58
-    }
59
-
60
-
61
-    /**
62
-     * @return array
63
-     */
64
-    protected function getFacetSpec()
65
-    {
66
-        $spec = Config::inst()->get('FacetedCategory', 'facets');
67
-        return (empty($spec) || !is_array($spec)) ? array() : $spec;
68
-    }
69
-
70
-
71
-    /**
72
-     * @return array
73
-     */
74
-    protected function getFilters()
75
-    {
76
-        $qs_f       = Config::inst()->get('ShopSearch', 'qs_filters');
77
-        if (!$qs_f) {
78
-            return array();
79
-        }
80
-        $request    = $this->getController()->getRequest();
81
-        $filters    = $request->requestVar($qs_f);
82
-        if (empty($filters) || !is_array($filters)) {
83
-            return array();
84
-        }
85
-        return FacetHelper::inst()->scrubFilters($filters);
86
-    }
87
-
88
-
89
-    /**
90
-     * @param bool $recursive
91
-     * @return mixed
92
-     */
93
-    public function FilteredProducts($recursive=true)
94
-    {
95
-        if (!isset($this->_filteredProducts)) {
96
-            $fn = Config::inst()->get('FacetedCategory', 'products_method');
97
-            if (empty($fn)) {
98
-                $fn = 'ProductsShowable';
99
-            }
100
-            $this->_filteredProducts = $this->owner->$fn($recursive);
101
-            $this->_filteredProducts = FacetHelper::inst()->addFiltersToDataList($this->_filteredProducts, $this->getFilters());
102
-        }
103
-
104
-        return $this->_filteredProducts;
105
-    }
106
-
107
-    protected $_filteredProducts;
108
-
109
-
110
-    /**
111
-     * @return array
112
-     */
113
-    public function getDisabledFacetsArray()
114
-    {
115
-        if (empty($this->owner->DisabledFacets)) {
116
-            return array();
117
-        }
118
-        return explode(',', $this->owner->DisabledFacets);
119
-    }
120
-
121
-
122
-    /**
123
-     * @return ArrayList
124
-     */
125
-    public function Facets()
126
-    {
127
-        $spec       = $this->getFacetSpec();
128
-        if (empty($spec)) {
129
-            return new ArrayList;
130
-        }
131
-
132
-        // remove any disabled facets
133
-        foreach ($this->getDisabledFacetsArray() as $disabled) {
134
-            if (isset($spec[$disabled])) {
135
-                unset($spec[$disabled]);
136
-            }
137
-        }
138
-
139
-        $request    = $this->getController()->getRequest();
140
-        $baseLink   = $request->getURL(false);
141
-        $filters    = $this->getFilters();
142
-        $baseParams = array_merge($request->requestVars(), array());
143
-        unset($baseParams['url']);
144
-
145
-        $products   = $this->owner->hasMethod('ProductsForFaceting') ? $this->owner->ProductsForFaceting() : $this->FilteredProducts();
146
-        $facets     = FacetHelper::inst()->buildFacets($products, $spec, (bool)Config::inst()->get('FacetedCategory', 'auto_facet_attributes'));
147
-        $facets     = FacetHelper::inst()->transformHierarchies($facets);
148
-        $facets     = FacetHelper::inst()->updateFacetState($facets, $filters);
149
-        $facets     = FacetHelper::inst()->insertFacetLinks($facets, $baseParams, $baseLink);
150
-
151
-        return $facets;
152
-    }
19
+	private static $db = array(
20
+		'DisabledFacets' => 'Text', // This will be a comma-delimited list of facets that aren't used for a given category
21
+	);
22
+
23
+	/** @var array - facet definition - see ShopSearch and/or docs/en/Facets.md for format */
24
+	private static $facets = array();
25
+
26
+	/** @var bool - if true there will be a tab in the cms to disable some or all defined facets */
27
+	private static $show_disabled_facets_tab = true;
28
+
29
+	/** @var string - which method should we use to get the child products for FilteredProducts */
30
+	private static $products_method = 'ProductsShowable';
31
+
32
+	/** @var bool - automatically create facets for static attributes */
33
+	private static $auto_facet_attributes = false;
34
+
35
+
36
+	/**
37
+	 * @param FieldList $fields
38
+	 */
39
+	public function updateCMSFields(FieldList $fields)
40
+	{
41
+		if (Config::inst()->get('FacetedCategory', 'show_disabled_facets_tab')) {
42
+			$spec = FacetHelper::inst()->expandFacetSpec($this->getFacetSpec());
43
+			$facets = array();
44
+			foreach ($spec as $f => $v) {
45
+				$facets[$f] = $v['Label'];
46
+			}
47
+			$fields->addFieldToTab('Root.Facets', CheckboxSetField::create('DisabledFacets', "Don't show the following facets for this category:", $facets));
48
+		}
49
+	}
50
+
51
+
52
+	/**
53
+	 * @return Controller
54
+	 */
55
+	protected function getController()
56
+	{
57
+		return ($this->owner instanceof Controller) ? $this->owner : Controller::curr();
58
+	}
59
+
60
+
61
+	/**
62
+	 * @return array
63
+	 */
64
+	protected function getFacetSpec()
65
+	{
66
+		$spec = Config::inst()->get('FacetedCategory', 'facets');
67
+		return (empty($spec) || !is_array($spec)) ? array() : $spec;
68
+	}
69
+
70
+
71
+	/**
72
+	 * @return array
73
+	 */
74
+	protected function getFilters()
75
+	{
76
+		$qs_f       = Config::inst()->get('ShopSearch', 'qs_filters');
77
+		if (!$qs_f) {
78
+			return array();
79
+		}
80
+		$request    = $this->getController()->getRequest();
81
+		$filters    = $request->requestVar($qs_f);
82
+		if (empty($filters) || !is_array($filters)) {
83
+			return array();
84
+		}
85
+		return FacetHelper::inst()->scrubFilters($filters);
86
+	}
87
+
88
+
89
+	/**
90
+	 * @param bool $recursive
91
+	 * @return mixed
92
+	 */
93
+	public function FilteredProducts($recursive=true)
94
+	{
95
+		if (!isset($this->_filteredProducts)) {
96
+			$fn = Config::inst()->get('FacetedCategory', 'products_method');
97
+			if (empty($fn)) {
98
+				$fn = 'ProductsShowable';
99
+			}
100
+			$this->_filteredProducts = $this->owner->$fn($recursive);
101
+			$this->_filteredProducts = FacetHelper::inst()->addFiltersToDataList($this->_filteredProducts, $this->getFilters());
102
+		}
103
+
104
+		return $this->_filteredProducts;
105
+	}
106
+
107
+	protected $_filteredProducts;
108
+
109
+
110
+	/**
111
+	 * @return array
112
+	 */
113
+	public function getDisabledFacetsArray()
114
+	{
115
+		if (empty($this->owner->DisabledFacets)) {
116
+			return array();
117
+		}
118
+		return explode(',', $this->owner->DisabledFacets);
119
+	}
120
+
121
+
122
+	/**
123
+	 * @return ArrayList
124
+	 */
125
+	public function Facets()
126
+	{
127
+		$spec       = $this->getFacetSpec();
128
+		if (empty($spec)) {
129
+			return new ArrayList;
130
+		}
131
+
132
+		// remove any disabled facets
133
+		foreach ($this->getDisabledFacetsArray() as $disabled) {
134
+			if (isset($spec[$disabled])) {
135
+				unset($spec[$disabled]);
136
+			}
137
+		}
138
+
139
+		$request    = $this->getController()->getRequest();
140
+		$baseLink   = $request->getURL(false);
141
+		$filters    = $this->getFilters();
142
+		$baseParams = array_merge($request->requestVars(), array());
143
+		unset($baseParams['url']);
144
+
145
+		$products   = $this->owner->hasMethod('ProductsForFaceting') ? $this->owner->ProductsForFaceting() : $this->FilteredProducts();
146
+		$facets     = FacetHelper::inst()->buildFacets($products, $spec, (bool)Config::inst()->get('FacetedCategory', 'auto_facet_attributes'));
147
+		$facets     = FacetHelper::inst()->transformHierarchies($facets);
148
+		$facets     = FacetHelper::inst()->updateFacetState($facets, $filters);
149
+		$facets     = FacetHelper::inst()->insertFacetLinks($facets, $baseParams, $baseLink);
150
+
151
+		return $facets;
152
+	}
153 153
 }
Please login to merge, or discard this patch.