Completed
Push — master ( 4d9765...9b41d4 )
by Pascal
02:01
created

SearchResult::highlight()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 16
rs 9.4285
cc 2
eloc 9
nc 2
nop 1
1
<?php namespace PKleindienst\BlogSearch\Components;
2
3
use Cms\Classes\ComponentBase;
4
use Cms\Classes\Page;
5
use Input;
6
use RainLab\Blog\Models\Category as BlogCategory;
7
use RainLab\Blog\Models\Post as BlogPost;
8
use Redirect;
9
use System\Models\Parameters;
10
11
/**
12
 * Search Result component
13
 * @see RainLab\Blog\Components\Posts
14
 * @package PKleindienst\BlogSearch\Components
15
 */
16
class SearchResult extends ComponentBase
17
{
18
    /**
19
     * Parameter to use for the search
20
     * @var string
21
     */
22
    public $searchParam;
23
24
    /**
25
     * The search term
26
     * @var string
27
     */
28
    public $searchTerm;
29
30
    /**
31
     * A collection of posts to display
32
     * @var Collection
33
     */
34
    public $posts;
35
36
    /**
37
     * Parameter to use for the page number
38
     * @var string
39
     */
40
    public $pageParam;
41
42
    /**
43
     * Message to display when there are no messages.
44
     * @var string
45
     */
46
    public $noPostsMessage;
47
48
    /**
49
     * Reference to the page name for linking to posts.
50
     * @var string
51
     */
52
    public $postPage;
53
54
    /**
55
     * Reference to the page name for linking to categories.
56
     * @var string
57
     */
58
    public $categoryPage;
59
60
    /**
61
     * @return array
62
     */
63
    public function componentDetails()
64
    {
65
        return [
66
            'name'        => 'Search Result',
67
            'description' => 'Displays a list of blog posts that match the search term on the page.'
68
        ];
69
    }
70
71
    /**
72
     * @see RainLab\Blog\Components\Posts::defineProperties()
73
     * @return array
74
     */
75
    public function defineProperties()
76
    {
77
        // check build to add fallback to not supported inspector types if needed
78
        $hasNewInspector = Parameters::get('system::core.build') >= 306;
79
80
        return [
81
            'searchTerm' => [
82
                'title'       => 'Search Term',
83
                'description' => 'The value to determine what the user is searching for.',
84
                'type'        => 'string',
85
                'default'     => '{{ :search }}',
86
            ],
87
            'pageNumber' => [
88
                'title'       => 'rainlab.blog::lang.settings.posts_pagination',
89
                'description' => 'rainlab.blog::lang.settings.posts_pagination_description',
90
                'type'        => 'string',
91
                'default'     => '{{ :page }}',
92
            ],
93
            'disableUrlMapping' => [
94
                'title'       => 'Disable URL Mapping',
95
                'description' => 'If the url Mapping is disabled the search form uses the default GET Parameter q '
96
                                    . '(e.g. example.com/search?search=Foo instead of example.com/search/Foo)',
97
                'type'        => 'checkbox',
98
                'default'     => false,
99
                'showExternalParam' => false
100
            ],
101
            'hightlight' => [
102
                'title'       => 'Hightlight Matches',
103
                'type'        => 'checkbox',
104
                'default'     => false,
105
                'showExternalParam' => false
106
            ],
107
            'postsPerPage' => [
108
                'title'             => 'rainlab.blog::lang.settings.posts_per_page',
109
                'type'              => 'string',
110
                'validationPattern' => '^[0-9]+$',
111
                'validationMessage' => 'rainlab.blog::lang.settings.posts_per_page_validation',
112
                'default'           => '10',
113
            ],
114
            'noPostsMessage' => [
115
                'title'        => 'rainlab.blog::lang.settings.posts_no_posts',
116
                'description'  => 'rainlab.blog::lang.settings.posts_no_posts_description',
117
                'type'         => 'string',
118
                'default'      => 'No posts found',
119
                'showExternalParam' => false
120
            ],
121
            'sortOrder' => [
122
                'title'       => 'rainlab.blog::lang.settings.posts_order',
123
                'description' => 'rainlab.blog::lang.settings.posts_order_description',
124
                'type'        => 'dropdown',
125
                'default'     => 'published_at desc'
126
            ],
127
            'includeCategories' => [
128
                'title'       => 'Include Categories',
129
                'description' => 'Only Posts with selected categories are included in the search result',
130
                'type'        => $hasNewInspector ? 'set' : 'dropdown',
131
                'group'       => 'Categories'
132
            ],
133
            'excludeCategories' => [
134
                'title'       => 'Exclude Categories',
135
                'description' => 'Posts with selected categories are excluded from the search result',
136
                'type'        => $hasNewInspector ? 'set' : 'dropdown',
137
                'group'       => 'Categories'
138
            ],
139
            'categoryPage' => [
140
                'title'       => 'rainlab.blog::lang.settings.posts_category',
141
                'description' => 'rainlab.blog::lang.settings.posts_category_description',
142
                'type'        => 'dropdown',
143
                'default'     => 'blog/category',
144
                'group'       => 'Links',
145
            ],
146
            'postPage' => [
147
                'title'       => 'rainlab.blog::lang.settings.posts_post',
148
                'description' => 'rainlab.blog::lang.settings.posts_post_description',
149
                'type'        => 'dropdown',
150
                'default'     => 'blog/post',
151
                'group'       => 'Links',
152
            ],
153
        ];
154
    }
155
156
    /**
157
     * @return array
158
     */
159
    public function getIncludeCategoriesOptions()
160
    {
161
        return BlogCategory::lists('name', 'id');
162
    }
163
164
    /**
165
     * @return array
166
     */
167
    public function getExcludeCategoriesOptions()
168
    {
169
        return BlogCategory::lists('name', 'id');
170
    }
171
172
    /**
173
     * @see RainLab\Blog\Components\Posts::getCategoryPageOptions()
174
     * @return mixed
175
     */
176
    public function getCategoryPageOptions()
177
    {
178
        return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
179
    }
180
181
    /**
182
     * @see RainLab\Blog\Components\Posts::getPostPageOptions()
183
     * @return mixed
184
     */
185
    public function getPostPageOptions()
186
    {
187
        return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName');
188
    }
189
190
    /**
191
     * @see RainLab\Blog\Components\Posts::getSortOrderOptions()
192
     * @return mixed
193
     */
194
    public function getSortOrderOptions()
195
    {
196
        return BlogPost::$allowedSortingOptions;
197
    }
198
199
    /**
200
     * @see RainLab\Blog\Components\Posts::onRun()
201
     * @return mixed
202
     */
203
    public function onRun()
204
    {
205
        $this->prepareVars();
206
207
        // map get request to :search param
208
        $searchTerm = Input::get('search');
209
        if (!$this->property('disableUrlMapping') && \Request::isMethod('get') && $searchTerm) {
210
            // add ?cats[] query string
211
            $cats = Input::get('cat');
212
            $query = http_build_query(['cat' => $cats]);
213
            $query = preg_replace('/%5B[0-9]+%5D/simU', '%5B%5D', $query);
214
            $query = !empty($query) ? '?' . $query : '';
215
216
            return Redirect::to(
217
                $this->currentPageUrl([
218
                    $this->searchParam => urlencode($searchTerm)
219
                ])
220
                . $query
221
            );
222
        }
223
224
        // load posts
225
        $this->posts = $this->page[ 'posts' ] = $this->listPosts();
226
227
        /*
228
         * If the page number is not valid, redirect
229
         */
230
        if ($pageNumberParam = $this->paramName('pageNumber')) {
231
            $currentPage = $this->property('pageNumber');
232
233
            if ($currentPage > ($lastPage = $this->posts->lastPage()) && $currentPage > 1) {
234
                return Redirect::to($this->currentPageUrl([$pageNumberParam => $lastPage]));
235
            }
236
        }
237
    }
238
239
    /**
240
     * @see RainLab\Blog\Components\Posts::prepareVars()
241
     */
242
    protected function prepareVars()
243
    {
244
        $this->pageParam = $this->page[ 'pageParam' ] = $this->paramName('pageNumber');
245
        $this->searchParam = $this->page[ 'searchParam' ] = $this->paramName('searchTerm');
246
        $this->searchTerm = $this->page[ 'searchTerm' ] = urldecode($this->property('searchTerm'));
247
        $this->noPostsMessage = $this->page[ 'noPostsMessage' ] = $this->property('noPostsMessage');
248
249
        if ($this->property('disableUrlMapping')) {
250
            $this->searchTerm = $this->page[ 'searchTerm' ] = urldecode(Input::get('search'));
251
        }
252
253
        /*
254
         * Page links
255
         */
256
        $this->postPage = $this->page[ 'postPage' ] = $this->property('postPage');
257
        $this->categoryPage = $this->page[ 'categoryPage' ] = $this->property('categoryPage');
258
    }
259
260
    /**
261
     * @see RainLab\Blog\Components\Posts::prepareVars()
262
     * @return mixed
263
     */
264
    protected function listPosts()
265
    {
266
        // Filter posts
267
        $posts = BlogPost::with([
268
            'categories' => function ($q) {
269
                if (!is_null($this->property('excludeCategories'))) {
270
                    $q->whereNotIn('id', $this->property('excludeCategories'));
271
                }
272
                if (!is_null($this->property('includeCategories'))) {
273
                    $q->whereIn('id', $this->property('includeCategories'));
274
                }
275
            }
276
        ])
277
            ->where(function ($q) {
278
                $q->where('title', 'LIKE', "%{$this->searchTerm}%")
279
                    ->orWhere('content', 'LIKE', "%{$this->searchTerm}%")
280
                    ->orWhere('excerpt', 'LIKE', "%{$this->searchTerm}%");
281
            });
282
283
        // filter categories
284
        $cat = Input::get('cat');
285
        if ($cat) {
286
            $cat = is_array($cat) ? $cat : [$cat];
287
            $posts->filterCategories($cat);
288
        }
289
290
        // get posts in excluded category
291
        $blockedPosts = $this->getPostIdsByCategories($this->property('excludeCategories'));
292
        if (!empty($blockedPosts)) {
293
            $posts = $posts->whereNotIn('id', $blockedPosts);
294
        }
295
296
        // get only posts from included categories
297
        $allowedPosts = $this->getPostIdsByCategories($this->property('includeCategories'));
298
        if (!empty($allowedPosts)) {
299
            $posts = $posts->whereIn('id', $allowedPosts);
300
        }
301
302
        // List all the posts that match search terms, eager load their categories
303
        $posts = $posts->listFrontEnd([
304
            'page'    => $this->property('pageNumber'),
305
            'sort'    => $this->property('sortOrder'),
306
            'perPage' => $this->property('postsPerPage'),
307
        ]);
308
309
        /*
310
         * Add a "url" helper attribute for linking to each post and category
311
         */
312
        $posts->each(function ($post) {
313
            $post->setUrl($this->postPage, $this->controller);
314
315
            $post->categories->each(function ($category) {
316
                $category->setUrl($this->categoryPage, $this->controller);
317
            });
318
319
            // apply highlight of search result
320
            $this->highlight($post);
321
        });
322
323
        return $posts;
324
    }
325
326
    /**
327
     * Get the posts ids of posts with belong to specific categories
328
     * @param mixed $ids
329
     * @return array
330
     */
331
    protected function getPostIdsByCategories($ids = null)
332
    {
333
        if (is_null($ids) || !is_array($ids)) {
334
            return [];
335
        }
336
337
        $posts = [];
338
        $categories = BlogCategory::with(['posts' => function ($q) {
339
            $q->select('post_id');
340
        }])
341
            ->whereIn('id', $ids)
342
            ->get();
343
344
        $categories->each(function ($item) use (&$posts) {
345
            $item->posts->each(function ($item) use (&$posts) {
346
                $posts[] = $item->post_id;
347
            });
348
        });
349
350
        return $posts;
351
    }
352
353
    /**
354
     * @param \RainLab\Blog\Models\Post $post
355
     */
356
    protected function highlight(BlogPost $post)
357
    {
358
        if ($this->property('hightlight')) {
359
            $searchTerm = preg_quote($this->searchTerm, '|');
360
361
            // apply highlight
362
            $post->title = preg_replace('|(' . $searchTerm . ')|i', '<mark>$1</mark>', $post->title);
363
            $post->excerpt = preg_replace('|(' . $searchTerm . ')|i', '<mark>$1</mark>', $post->excerpt);
364
365
            $post->content_html = preg_replace(
366
                '~(?![^<>]*>)(' . $searchTerm . ')~ism',
367
                '<mark>$1</mark>',
368
                $post->content_html
369
            );
370
        }
371
    }
372
}
373