Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
GetSideFacets
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 5
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 handleRequest
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
42
 getFacetResults
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 formatFacets
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getCheckboxFacetCount
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * "Get Side Facets" AJAX handler
5 *
6 * PHP version 8
7 *
8 * Copyright (C) The National Library of Finland 2018-2023.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  AJAX
25 * @author   Ere Maijala <ere.maijala@helsinki.fi>
26 * @author   Juha Luoma <juha.luoma@helsinki.fi>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/development Wiki
29 */
30
31namespace VuFind\AjaxHandler;
32
33use Laminas\Mvc\Controller\Plugin\Params;
34use Laminas\View\Renderer\RendererInterface;
35use VuFind\Recommend\PluginManager as RecommendPluginManager;
36use VuFind\Recommend\SideFacets;
37use VuFind\Search\Base\Results;
38use VuFind\Search\RecommendListener;
39use VuFind\Search\SearchRunner;
40use VuFind\Session\Settings as SessionSettings;
41
42use function is_callable;
43
44/**
45 * "Get Side Facets" AJAX handler
46 *
47 * @category VuFind
48 * @package  AJAX
49 * @author   Ere Maijala <ere.maijala@helsinki.fi>
50 * @author   Juha Luoma <juha.luoma@helsinki.fi>
51 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
52 * @link     https://vufind.org/wiki/development Wiki
53 */
54class GetSideFacets extends \VuFind\AjaxHandler\AbstractBase implements \Laminas\Log\LoggerAwareInterface
55{
56    use \VuFind\Log\LoggerAwareTrait;
57
58    /**
59     * Recommend plugin manager
60     *
61     * @var RecommendPluginManager
62     */
63    protected $recommendPluginManager;
64
65    /**
66     * Search runner
67     *
68     * @var SearchRunner
69     */
70    protected $searchRunner;
71
72    /**
73     * View renderer
74     *
75     * @var RendererInterface
76     */
77    protected $renderer;
78
79    /**
80     * Constructor
81     *
82     * @param SessionSettings        $ss       Session settings
83     * @param RecommendPluginManager $rpm      Recommend plugin manager
84     * @param SearchRunner           $sr       Search runner
85     * @param RendererInterface      $renderer View renderer
86     */
87    public function __construct(
88        SessionSettings $ss,
89        \VuFind\Recommend\PluginManager $rpm,
90        SearchRunner $sr,
91        RendererInterface $renderer
92    ) {
93        $this->sessionSettings = $ss;
94        $this->recommendPluginManager = $rpm;
95        $this->searchRunner = $sr;
96        $this->renderer = $renderer;
97    }
98
99    /**
100     * Handle a request.
101     *
102     * @param Params $params Parameter helper from controller
103     *
104     * @return array [response data, HTTP status code]
105     */
106    public function handleRequest(Params $params)
107    {
108        $this->disableSessionWrites();  // avoid session write timing bug
109
110        // Allow both GET and POST variables:
111        $request = $params->fromQuery() + $params->fromPost();
112
113        $configIndex = $request['configIndex'] ?? 0;
114        $configLocation = $request['location'] ?? 'side';
115        $results = $this->getFacetResults($request, $configIndex, $configLocation);
116        if ($results instanceof \VuFind\Search\EmptySet\Results) {
117            $this->logError('Faceting request failed');
118            return $this->formatResponse('', self::STATUS_HTTP_ERROR);
119        }
120
121        // Set appropriate query suppression / extra field behavior:
122        $queryHelper = $results->getUrlQuery();
123        $queryHelper->setSuppressQuery((bool)($request['querySuppressed'] ?? false));
124        $extraFields = array_filter(explode(',', $request['extraFields'] ?? ''));
125        foreach ($extraFields as $field) {
126            if (isset($request[$field])) {
127                $queryHelper->setDefaultParameter($field, $request[$field]);
128            }
129        }
130
131        $recommend = $results->getRecommendations($configLocation)[0] ?? null;
132        if (null === $recommend) {
133            return $this->formatResponse(
134                'Invalid config requested',
135                self::STATUS_HTTP_BAD_REQUEST
136            );
137        }
138
139        $context = [
140            'recommend' => $recommend,
141            'params' => $results->getParams(),
142            'searchClassId' => $request['searchClassId'] ?? DEFAULT_SEARCH_BACKEND,
143        ];
144        if (isset($request['enabledFacets'])) {
145            // Render requested facets separately
146            $facets = $this->formatFacets(
147                $context,
148                $recommend,
149                $request['enabledFacets'],
150                $results
151            );
152            return $this->formatResponse(compact('facets'));
153        }
154
155        // Render full sidefacets
156        $html = $this->renderer->render(
157            'Recommend/SideFacets.phtml',
158            $context
159        );
160        return $this->formatResponse(compact('html'));
161    }
162
163    /**
164     * Perform search and return the results
165     *
166     * @param array  $request Request params
167     * @param string $index   Index of SideFacetsDeferred in configuration
168     * @param string $loc     Location where SideFacetsDeferred is configured
169     *
170     * @return Results
171     */
172    protected function getFacetResults(array $request, $index, $loc)
173    {
174        $setupCallback = function ($runner, $params, $searchId) use ($index, $loc) {
175            $listener = new RecommendListener(
176                $this->recommendPluginManager,
177                $searchId
178            );
179            $config = [];
180            $rawConfig = $params->getOptions()
181                ->getRecommendationSettings($params->getSearchHandler());
182            $settings = explode(':', $rawConfig[$loc][$index] ?? '');
183            if ($settings[0] === 'SideFacetsDeferred') {
184                $settings[0] = 'SideFacets';
185                $config[$loc][] = implode(':', $settings);
186            }
187            $listener->setConfig($config);
188            $listener->attach($runner->getEventManager()->getSharedManager());
189
190            $params->setLimit(0);
191            if (is_callable([$params, 'setHierarchicalFacetLimit'])) {
192                $params->setHierarchicalFacetLimit(-1);
193            }
194            $options = $params->getOptions();
195            $options->disableHighlighting();
196            $options->spellcheckEnabled(false);
197        };
198
199        $runner = $this->searchRunner;
200        return $runner->run(
201            $request,
202            $request['searchClassId'] ?? DEFAULT_SEARCH_BACKEND,
203            $setupCallback
204        );
205    }
206
207    /**
208     * Format facets according to their type
209     *
210     * @param array      $context   View rendering context
211     * @param SideFacets $recommend Recommendation module
212     * @param array      $facets    Facets to process
213     * @param Results    $results   Search results
214     *
215     * @return array
216     */
217    protected function formatFacets(
218        $context,
219        SideFacets $recommend,
220        $facets,
221        Results $results
222    ) {
223        $response = [];
224        $facetSet = $recommend->getFacetSet();
225        foreach ($facets as $facet) {
226            if (strpos($facet, ':')) {
227                $response[$facet]['checkboxCount']
228                    = $this->getCheckboxFacetCount($facet, $results);
229            } else {
230                $context['facet'] = $facet;
231                $context['cluster'] = $facetSet[$facet] ?? [
232                    'list' => [],
233                ];
234                $context['collapsedFacets'] = [];
235                $response[$facet]['html'] = $this->renderer->render(
236                    'Recommend/SideFacets/facet.phtml',
237                    $context
238                );
239            }
240        }
241        return $response;
242    }
243
244    /**
245     * Get the result count for a checkbox facet
246     *
247     * @param string  $facet   Facet
248     * @param Results $results Search results
249     *
250     * @return int|null
251     */
252    protected function getCheckboxFacetCount($facet, Results $results)
253    {
254        // There's currently no good way to return counts for checkbox filters.
255        return null;
256    }
257}