| @@ -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) | 
| @@ -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(); | 
| @@ -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)) { | |
| 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)) { | |
| 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 | } | 
| @@ -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 | } |