Issue #1953606 by jaydub: Added Use natural sort for url segments.
[project/facetapi_pretty_paths.git] / plugins / facetapi / url_processor_pretty_paths.inc
1 <?php
2
3 /**
4 * @file
5 * A custom URL processor for pretty paths.
6 */
7
8 /**
9 * Extension of FacetapiUrlProcessor.
10 */
11 class FacetapiUrlProcessorPrettyPaths extends FacetapiUrlProcessorStandard {
12
13 /**
14 * An array of filter params.
15 *
16 * @var array.
17 */
18 protected $filterParams = array();
19
20 /**
21 * An array of pretty path segments.
22 *
23 * @var array.
24 */
25 protected $pathSegments = array();
26
27 /**
28 * The adapter that uses this FacetapiUrlProcessor.
29 *
30 * @var FacetapiAdapter
31 */
32 protected $adapter = array();
33
34 /**
35 * Options that apply for this FacetapiUrlProcessor.
36 */
37 protected $options = array();
38
39 /**
40 * The full path string, as we override $_GET['q].
41 */
42 protected $fullPath = NULL;
43
44 /**
45 * The base path for constructing pretty facet paths.
46 */
47 protected $basePath = NULL;
48
49 /**
50 * Constructor, sets adapter.
51 *
52 * @param FacetapiAdapter $adapter
53 * The adapter that uses this FacetapiUrlProcessor.
54 */
55 function __construct(FacetapiAdapter $adapter) {
56 $this->adapter = $adapter;
57 $this->options = variable_get('facetapi_pretty_paths_searcher_' . $this->adapter->getSearcher() . '_options');
58 }
59
60 /**
61 * Pulls facet params from the $_GET variable.
62 *
63 * Overrides FacetapiUrlProcessorStandard::fetchParams().
64 */
65 public function fetchParams() {
66
67 // Skip pretty paths logic for admin pages, as
68 // for pretty paths, we have to manipulate $_GET['q].
69 // @todo: Fix this workaround?
70 if (strpos($_GET['q'], 'admin/') === 0) {
71 return $_GET;
72 }
73
74 $this->fullPath = $_GET['q'];
75 $this->filterParams = array();
76 $facets = $this->adapter->getEnabledFacets();
77
78 $args = explode('/', $_GET['q']);
79
80 // Traverse all segments "<alias>/<value>" from right to left (array_pop).
81 while (($value = array_pop($args)) !== NULL) {
82 if (!($alias = array_pop($args))) {
83 // Restore last url part, if this was an incomplete segment.
84 array_push($args, $value);
85 break;
86 }
87 $found = FALSE;
88 $alias = $this->decodePathSegmentAlias($alias);
89
90 // Look for matching facet.
91 foreach ($facets as $facet_alias => $facet) {
92 $pretty_paths_alias = $this->getFacetPrettyPathsAlias($facet);
93
94 // Add to params if alias from url matches alias from facet settings.
95 if ($pretty_paths_alias == $alias) {
96 $found = TRUE;
97 $value = $this->decodePathSegmentValue($value, $facet);
98
99 $this->filterParams[] = rawurlencode($facet['field']) . ':' . $value;
100
101 // "Copy" to prevent $segment['facet'] from being overridden.
102 $my_facet = $facet;
103 $segment = $this->getPathSegment($my_facet, $value);
104 $this->pathSegments[$segment['key']] = $segment;
105 }
106 }
107 // When no more facet path segments have been found,
108 // we assume the rest of the url as the search basePath.
109 if (!$found) {
110 array_push($args, $alias, $value);
111 break;
112 }
113 }
114
115 // Set the rest of the url as the search basePath.
116 $this->basePath = implode('/', $args);
117
118 // Since we traversed all segments from right to left we need to reverse
119 // them here. Reason is because the order matters for some facets.
120 $this->pathSegments = array_reverse($this->pathSegments);
121 $this->filterParams = array_reverse($this->filterParams);
122
123 // Return params as expected.
124 $params = $_GET;
125 $params['q'] = $this->basePath;
126 $params[$this->filterKey] = $this->filterParams;
127 return $params;
128 }
129
130 /**
131 * Remove the filter query part from params and return it.
132 *
133 * Overrides FacetapiUrlProcessorStandard::getQueryString().
134 */
135 public function getQueryString(array $facet, array $values, $active) {
136 $params = $this->params;
137 unset($params[$this->filterKey]);
138 return $params;
139 }
140
141 /**
142 * Pretty paths will be generated as "search/url/segment1/segment2/".
143 *
144 * By default, a segment will look like:
145 * "<alias>/<value>".
146 *
147 * Overrides FacetapiUrlProcessorStandard::getFacetPath().
148 */
149 public function getFacetPath(array $facet, array $values, $active) {
150 $segments = $this->pathSegments;
151 $active_items = $this->adapter->getActiveItems($facet);
152
153 // Adds to segments if inactive, removes if active.
154 foreach ($values as $value) {
155 $segment = $this->getPathSegment($facet, $value);
156 if ($active && isset($active_items[$value])) {
157 unset($segments[$segment['key']]);
158 }
159 elseif (!$active) {
160 $segments[$segment['key']] = $segment;
161 }
162 }
163
164 $path = $this->constructPath($segments);
165 return $path;
166 }
167
168 /* ### Generic helpers ### */
169
170 /**
171 * Construct a path from for a given array of filter segments.
172 *
173 * @param array $segments
174 * @return string
175 */
176 public function constructPath(array $segments) {
177 if (!empty($this->options['sort_path_segments'])) {
178 // Sort to avoid multiple urls with duplicate content.
179 uksort($segments, 'strnatcmp');
180 }
181 $path = $this->getBasePath();
182 // Add all path segments.
183 foreach ($segments as $key => $segment) {
184 $this->encodePathSegment($segment, $segment['facet']);
185 $path .= '/' . $segment['alias'] . '/' . $segment['value'];
186 }
187 return $path;
188 }
189
190 /**
191 * Generate a path segment for a given facet + value.
192 */
193 protected function getPathSegment(&$facet, $value) {
194 $pretty_paths_alias = $this->getFacetPrettyPathsAlias($facet);
195 return array(
196 'alias' => $pretty_paths_alias,
197 'value' => $value,
198 'facet' => $facet,
199 'key' => $pretty_paths_alias . "_" . $value,
200 );
201 }
202
203 protected function encodePathSegment(array &$segment, array $facet) {
204 facetapi_pretty_paths_coder_callback('encodePathSegment',
205 array(
206 'segment' => &$segment,
207 'facet' => $facet,
208 'adapter' => $this->adapter,
209 )
210 );
211 }
212
213 protected function decodePathSegmentAlias($alias) {
214 return facetapi_pretty_paths_coder_callback('decodePathSegmentAlias',
215 array(
216 'alias' => $alias
217 )
218 );
219 }
220
221 protected function decodePathSegmentValue($value, array $facet) {
222 return facetapi_pretty_paths_coder_callback('decodePathSegmentValue',
223 array(
224 'facet' => $facet,
225 'adapter' => $this->adapter,
226 'value' => $value,
227 )
228 );
229 }
230
231 /**
232 * Returns the pretty_paths_alias for a given facet.
233 *
234 * If there is no custom pretty_paths_alias settings, in will default
235 * to rawurlencode($facet['field alias']).
236 */
237 public function getFacetPrettyPathsAlias(array $facet) {
238 $facet_settings = $this->adapter->getFacetSettingsGlobal($facet);
239 // Path alias defaults to facet_name.
240 return !empty($facet_settings->settings['pretty_paths_alias']) ?
241 $facet_settings->settings['pretty_paths_alias'] : $facet['field alias'];
242 }
243
244 /**
245 * Implements FacetapiUrlProcessorPrettyPaths::setBreadcrumb().
246 */
247 public function setBreadcrumb() {
248 $breadcrumb = drupal_get_breadcrumb();
249
250 // $facets to use later to get the segment
251 $facets = $this->adapter->getEnabledFacets();
252
253 // Gets search keys and active items form the adapter.
254 $keys = $this->adapter->getSearchKeys();
255 $active_items = $this->adapter->getAllActiveItems();
256
257 $item = menu_get_item();
258 $last_load_func = is_array($item['load_functions']) ? end($item['load_functions']) : NULL;
259 if (!empty($item['title']) && ((!$keys && $active_items) || ($keys && $last_load_func != 'menu_tail_load'))) {
260 $last = end($breadcrumb);
261 $this_page = l($item['title'], $item['href'], $item['localized_options']);
262 if ($last != $this_page) {
263 $breadcrumb[] = $this_page;
264 }
265 }
266
267 // Initializes base breadcrumb query.
268 $query = $this->params;
269 unset($query[$this->filterKey]);
270
271 // Adds the current search to the query.
272 if ($keys) {
273 // The last item should be text, not a link.
274 $breadcrumb[] = $active_items ? l($keys, $this->getBasePath(), array('query' => $query)) : check_plain($keys);
275 }
276
277 // Use this to track the active facet trail.
278 $segments = array();
279
280 // Keep track of what the last item is.
281 $last = end($active_items);
282
283 foreach ($active_items as $item) {
284 // Add this breadcrumb segment to the complete segments list.
285 // Do not use aliases, use the real facet
286 $segments[] = $this->getPathSegment($facets[$item['facets'][0]], $item['value']);
287
288 // Replaces with the mapped value.
289 $value = $this->adapter->getMappedValue($item['facets'][0], $item['value']);
290
291 // The last item should be text, not a link.
292 if ($last == $item) {
293 $breadcrumb[] = !empty($value['#html']) ? $value['#markup'] : check_plain($value['#markup']);
294 }
295 else {
296 $base = $this->adapter->getSearchPath();
297 $path = $this->constructPath($segments);
298 $breadcrumb[] = l($value['#markup'], $path, array('html' => !empty($value['#html'])));
299 }
300 }
301
302 // Sets the breadcrumb trail with h keys and filters.
303 drupal_set_breadcrumb($breadcrumb);
304 }
305
306 public function getFullPath() {
307 return $this->fullPath;
308 }
309
310 public function getBasePath() {
311 $basePath = $this->basePath;
312 // Mimic search api base path logic which allows to set a custom base path.
313 // @see SearchApiFacetapiAdapter::getSearchPath().
314 if (module_exists('search_api_facetapi') && $this->adapter instanceof SearchApiFacetapiAdapter) {
315 $search = $this->adapter->getCurrentSearch();
316 if ($search && $search[0]->getOption('search_api_base_path')) {
317 $basePath = $search[0]->getOption('search_api_base_path');
318 }
319 }
320 return $basePath;
321 }
322
323 }