@@ -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 | } |