Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-6187: 'Create faceted search' article added #2125

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions code_samples/search/faceted_search/assets/styles/app.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
.ibexa-blog-posts-list {
display: flex;
align-items: flex-start;

&__aside {
flex-basis: 300px;
margin-right: 20px;
background-color: #f9f9fa;
padding: 24px;
border-radius: 8px;
}

&__content {
flex-basis: calc(100% - 320px);
}
}

.ibexa-blog-post {
&__body {
display: flex;
align-items: flex-start;
margin-top: 30px;
}

.ezimage-field {
margin: 0 !important;
}

&__body {
padding: 30px;
margin: 0 30px;
}

&__aside {
background-color: #f9f9fa;
border-radius: 8px;
padding: 24px;
margin-left: 30px;
min-width: 300px;
width: 100%;
}

&__header {
margin: 0 60px;
}

&__image {
img {
border: 1px solid #ccc;
border-radius: 20px;
width: 100%;
}
}

.ibexa-blog-post-meta {
margin: 20px 0;

&__label {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
}

&__categories {
margin: 5px 0;
}

&__category {
background: #eee;
border: 1px solid #ccc;
border-radius: 3px;
display: inline-block;
font-size: 0.9em;
font-weight: normal;
padding: 3px;
text-decoration: none;
}
}

.ibexa-blog-post-line {
display: flex;
align-items: flex-start;

&__image {
flex-basis: 200px;

.ezimage-field {
margin: 10px 10px 10px 0;
padding: 0;
}

img {
border: 1px solid #ccc;
}
}

&__categories {
margin: 5px 0;
}

&__category {
background: #dddddd;
border: 1px solid #666;
border-radius: 3px;
display: inline-block;
font-size: 0.8em;
font-weight: normal;
padding: 3px;
text-decoration: none;
}
}
6 changes: 6 additions & 0 deletions code_samples/search/faceted_search/config/packages/ibexa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
site:
image_variations:
cover:
reference: original
filters:
geometry/scalewidth: [ 1024 ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
ibexa:
system:
site:
content_view:
full:
blog:
match:
Identifier\ContentType: ['blog']
controller: 'App\Controller\BlogController'
template: '@ibexadesign/full/blog.html.twig'
blog_post:
match:
Identifier\ContentType: ['blog_post']
template: '@ibexadesign/full/blog_post.html.twig'
category:
match:
Identifier\ContentType: ['tag']
controller: 'App\Controller\BlogController'
template: '@ibexadesign/full/blog.html.twig'
line:
blog_post:
match:
Identifier\ContentType: ['blog_post']
template: '@ibexadesign/line/blog_post.html.twig'
13 changes: 13 additions & 0 deletions code_samples/search/faceted_search/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
App\Menu\Builder\CategoriesMenuBuilder:
public: true
tags:
- name: knp_menu.menu_builder
method: build
alias: app.menu.categories_facets

App\Menu\Builder\PublicationDateMenuBuilder:
public: true
tags:
- name: knp_menu.menu_builder
method: build
alias: app.menu.publication_date_facets
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);

namespace App\Controller;

use App\QueryType\BlogPostsQueryType;
use Ibexa\Contracts\Core\Repository\SearchService;
use Ibexa\Contracts\Taxonomy\Service\TaxonomyServiceInterface;
use Ibexa\Core\MVC\Symfony\View\ContentView;
use Ibexa\Core\Pagination\Pagerfanta\LocationSearchAdapter;
use Ibexa\Core\Pagination\Pagerfanta\Pagerfanta;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\AsController;

#[AsController]
final class BlogController extends AbstractController
{
public function __construct(
private readonly TaxonomyServiceInterface $taxonomyService,
private readonly SearchService $searchService,
private readonly BlogPostsQueryType $queryType
) {
}

public function __invoke(Request $request, ContentView $view): ContentView
{
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\LocationQuery $query */
$query = $this->queryType->getQuery($this->getQueryParams($request, $view));

$posts = new Pagerfanta(new LocationSearchAdapter($query, $this->searchService));
$posts->setCurrentPage($request->query->getInt('page'));
$posts->setMaxPerPage(10);

$view->addParameters([
'blog_posts' => $posts,
]);

return $view;
}

/**
* Creates query parameters from current request.
*/
private function getQueryParams(Request $request, ContentView $view): array
{
$parameters = [];

$content = $view->getContent();
if ($content->getContentType()->identifier === 'tag') {
$parameters['category'] = $this->taxonomyService->loadEntryByContentId($content->id);
}

return $parameters;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace App\Menu\Builder;

use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Base class for all facet based menus.
*
* @template TOptions array<string, mixed>
*/
abstract class AbstractFacetsMenuBuilder
{
public function __construct(
protected readonly FactoryInterface $itemFactory,
protected readonly RequestStack $requestStack
) {
}

/**
* @param array<string, mixed> $options
*/
final public function build(array $options): ItemInterface
{
$menu = $this->itemFactory->createItem('root');
$this->doBuild($menu, $this->resolveOptions($options));

return $menu;
}

/**
* @param TOptions $options
*/
abstract protected function doBuild(ItemInterface $menu, array $options): void;

protected function configureOptions(OptionsResolver $resolver): void
{
// No options
}

/**
* @param array<string, mixed> $options
*
* @return TOptions
*/
private function resolveOptions(array $options): array
{
$resolver = new OptionsResolver();
$this->configureOptions($resolver);

/** @var TOptions */
return $resolver->resolve($options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace App\Menu\Builder;

use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResult\TermAggregationResult;
use Ibexa\Contracts\Taxonomy\Service\TaxonomyServiceInterface;
use Ibexa\Contracts\Taxonomy\Value\TaxonomyEntry;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\OptionsResolver\OptionsResolver;

final class CategoriesMenuBuilder extends AbstractFacetsMenuBuilder
{
private readonly TaxonomyServiceInterface $taxonomyService;

public function __construct(
TaxonomyServiceInterface $taxonomyService,
RequestStack $requestStack,
FactoryInterface $itemFactory
) {
parent::__construct($itemFactory, $requestStack);

$this->taxonomyService = $taxonomyService;
}

protected function doBuild(ItemInterface $menu, array $options): void
{
$this->createTaxonomyEntryWithChildren(
$menu,
$this->taxonomyService->loadRootEntry('tags'),
$options,
);
}

protected function configureOptions(OptionsResolver $resolver): void
{
$resolver->define('aggregation_result')->required()->allowedTypes(TermAggregationResult::class);
}

private function createTaxonomyEntryWithChildren(
ItemInterface $menu,
TaxonomyEntry $parent,
array $options,
int $depth = 3,
): void {
$children = $this->taxonomyService->loadEntryChildren($parent);

foreach ($children as $child) {
$count = $this->getCount($options['aggregation_result'], $child);
if ($count > 1) {
$item = $this->createMenuItem($child, $count, $options);
if ($depth > 1) {
$this->createTaxonomyEntryWithChildren($item, $child, $options, $depth - 1);
}

$menu->addChild($item);
}
}
}

private function createMenuItem(TaxonomyEntry $entry, int $count): ItemInterface
{
return $this->itemFactory->createItem(
'taxonomy_entry_' . $entry->id,
[
'label' => sprintf('%s (%d)', $entry->getName(), $count),
'route' => 'ibexa.url.alias',
'routeParameters' => [
'contentId' => $entry->getContent()->id,
] + $this->requestStack->getCurrentRequest()->query->all(),
]
);
}

private function getCount(?TermAggregationResult $aggregations, TaxonomyEntry $needle): int
{
if ($aggregations !== null) {
foreach ($aggregations as $entry => $count) {
if ($entry->getIdentifier() === $needle->getIdentifier()) {
return $count;
}
}
}

return 0;
}
}
Loading