@@ -27,8 +27,12 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 | } |
@@ -58,7 +58,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 | } |
@@ -368,7 +368,7 @@ discard block |
||
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 |
||
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) |
@@ -13,679 +13,679 @@ |
||
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 | } |
@@ -67,7 +67,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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(); |
@@ -50,7 +50,7 @@ |
||
50 | 50 | |
51 | 51 | /** |
52 | 52 | * @param $typeID |
53 | - * @return callable |
|
53 | + * @return Closure |
|
54 | 54 | */ |
55 | 55 | protected function getValuesClosure($typeID) |
56 | 56 | { |
@@ -11,93 +11,93 @@ discard block |
||
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 |
||
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 |
@@ -54,7 +54,7 @@ discard block |
||
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 |
||
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(); |
@@ -243,7 +243,7 @@ |
||
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) |
@@ -41,413 +41,413 @@ |
||
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 | } |
@@ -89,7 +89,7 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 { |
@@ -267,7 +267,7 @@ |
||
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 | { |
@@ -8,376 +8,376 @@ |
||
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 | } |
@@ -135,9 +135,9 @@ discard block |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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 |
||
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() : '', |
@@ -21,7 +21,7 @@ |
||
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 | { |
@@ -8,85 +8,85 @@ |
||
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 | } |
@@ -15,7 +15,7 @@ discard block |
||
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 |
||
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 |
||
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)); |
@@ -8,54 +8,54 @@ |
||
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 | } |
@@ -17,5 +17,5 @@ |
||
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 | } |
@@ -8,14 +8,14 @@ |
||
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 | } |
@@ -73,7 +73,7 @@ discard block |
||
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 |
||
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 |
||
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 | } |
@@ -16,138 +16,138 @@ |
||
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 | } |