Skip to content

Commit 70b6bb3

Browse files
authored
add list object for sorting, filtering and pagination
1 parent f3c1607 commit 70b6bb3

16 files changed

Lines changed: 524 additions & 148 deletions

config/documentation.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
66

7+
use Qossmic\TwigDocBundle\Cache\ComponentsWarmer;
78
use Qossmic\TwigDocBundle\Component\ComponentItemFactory;
89
use Qossmic\TwigDocBundle\Controller\TwigDocController;
910
use Qossmic\TwigDocBundle\Service\CategoryService;
@@ -36,5 +37,9 @@
3637
->autowire()
3738
->tag('twig.extension')
3839
->alias(TwigDocExtension::class, 'twig_doc.twig.extension')
40+
41+
->set('twig_doc.cache_warmer', ComponentsWarmer::class)
42+
->arg('$container', service('service_container'))
43+
->tag('kernel.cache_warmer')
3944
;
4045
};

src/Cache/ComponentsWarmer.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace Qossmic\TwigDocBundle\Cache;
4+
5+
use Psr\Container\ContainerInterface;
6+
use Qossmic\TwigDocBundle\Service\ComponentService;
7+
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
8+
9+
class ComponentsWarmer implements CacheWarmerInterface
10+
{
11+
public function __construct(private readonly ContainerInterface $container)
12+
{
13+
}
14+
15+
public function isOptional(): bool
16+
{
17+
return true;
18+
}
19+
20+
public function warmUp(string $cacheDir, ?string $buildDir = null): array
21+
{
22+
$componentService ??= $this->container->get('twig_doc.service.component');
23+
24+
if ($componentService instanceof ComponentService) {
25+
$componentService->getComponents();
26+
}
27+
28+
return [];
29+
}
30+
}

src/Component/ComponentCategory.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* @codeCoverageIgnore
1111
*/
12-
class ComponentCategory
12+
class ComponentCategory implements \Stringable
1313
{
1414
public const DEFAULT_CATEGORY = 'Components';
1515

@@ -41,4 +41,9 @@ public function setName(string $name): self
4141

4242
return $this;
4343
}
44+
45+
public function __toString(): string
46+
{
47+
return $this->name;
48+
}
4449
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?php
2+
3+
namespace Qossmic\TwigDocBundle\Component;
4+
5+
/**
6+
* @method ComponentItem[] getArrayCopy()
7+
*/
8+
class ComponentItemList extends \ArrayObject
9+
{
10+
public const SORT_ASC = 'asc';
11+
public const SORT_DESC = 'desc';
12+
13+
private array $sortableFields = [
14+
'name',
15+
'category',
16+
'title',
17+
];
18+
19+
/**
20+
* @param ComponentItem[] $items
21+
*/
22+
public function __construct(array $items)
23+
{
24+
parent::__construct($items);
25+
}
26+
27+
/**
28+
* @return ComponentItem[]
29+
*/
30+
public function paginate(int $start = 0, int $limit = 15): array
31+
{
32+
return \array_slice($this->getArrayCopy(), $start, $limit);
33+
}
34+
35+
public function sort(string $field, string $direction = self::SORT_ASC): void
36+
{
37+
if (!\in_array($field, $this->sortableFields)) {
38+
throw new \InvalidArgumentException(sprintf('field "%s" is not sortable', $field));
39+
}
40+
41+
$method = sprintf('get%s', ucfirst($field));
42+
43+
$this->uasort(function (ComponentItem $item, ComponentItem $item2) use ($method, $direction) {
44+
if ($direction === self::SORT_DESC) {
45+
return \call_user_func([$item2, $method]) <=> \call_user_func([$item, $method]);
46+
}
47+
48+
return \call_user_func([$item, $method]) <=> \call_user_func([$item2, $method]);
49+
});
50+
}
51+
52+
public function filter(string $query, ?string $type): self
53+
{
54+
$components = [];
55+
switch ($type) {
56+
case 'category':
57+
$components = array_filter(
58+
$this->getArrayCopy(),
59+
function (ComponentItem $item) use ($query) {
60+
$category = $item->getCategory()->getName();
61+
$parent = $item->getCategory()->getParent();
62+
while ($parent !== null) {
63+
$category = $parent->getName();
64+
$parent = $parent->getParent();
65+
}
66+
67+
return strtolower($category) === strtolower($query);
68+
}
69+
);
70+
71+
break;
72+
case 'sub_category':
73+
$components = array_filter(
74+
$this->getArrayCopy(),
75+
fn (ComponentItem $item) => $item->getCategory()->getParent() !== null
76+
&& strtolower($item->getCategory()->getName()) === strtolower($query)
77+
);
78+
79+
break;
80+
case 'tags':
81+
$tags = array_map('trim', explode(',', strtolower($query)));
82+
$components = array_filter($this->getArrayCopy(), function (ComponentItem $item) use ($tags) {
83+
return array_intersect($tags, array_map('strtolower', $item->getTags())) !== [];
84+
});
85+
86+
break;
87+
case 'name':
88+
$components = array_filter(
89+
$this->getArrayCopy(),
90+
fn (ComponentItem $item) => str_contains(strtolower($item->getName()), strtolower($query))
91+
);
92+
93+
break;
94+
default:
95+
foreach (['category', 'sub_category', 'tags', 'name'] as $type) {
96+
$components = array_merge($components, (array) $this->filter($query, $type));
97+
}
98+
99+
break;
100+
}
101+
102+
return new self(array_unique($components, \SORT_REGULAR));
103+
}
104+
}

src/Controller/TwigDocController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct(
2222

2323
public function index(Request $request): Response
2424
{
25-
$components = $this->componentService->getCategories();
25+
$components = $this->componentService->getComponents();
2626

2727
if ($filterQuery = $request->query->get('filterQuery')) {
2828
$filterType = $request->query->get('filterType');

src/DependencyInjection/TwigDocExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public function load(array $configs, ContainerBuilder $container): void
2222

2323
$definition = $container->getDefinition('twig_doc.service.component');
2424
$definition->setArgument('$componentsConfig', $config['components']);
25+
$definition->setArgument('$configReadTime', time());
2526

2627
$categories = array_merge([['name' => ComponentCategory::DEFAULT_CATEGORY]], $config['categories']);
2728

src/Service/ComponentService.php

Lines changed: 53 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,135 +4,95 @@
44

55
namespace Qossmic\TwigDocBundle\Service;
66

7+
use Psr\Cache\InvalidArgumentException;
78
use Qossmic\TwigDocBundle\Component\ComponentInvalid;
89
use Qossmic\TwigDocBundle\Component\ComponentItem;
910
use Qossmic\TwigDocBundle\Component\ComponentItemFactory;
11+
use Qossmic\TwigDocBundle\Component\ComponentItemList;
1012
use Qossmic\TwigDocBundle\Exception\InvalidComponentConfigurationException;
13+
use Symfony\Contracts\Cache\CacheInterface;
1114

1215
class ComponentService
1316
{
14-
/**
15-
* @var ComponentItem[]
16-
*/
17-
private array $components = [];
18-
19-
/**
20-
* @var array<string, array<int, ComponentItem>>
21-
*/
22-
private array $categories = [];
23-
24-
/**
25-
* @var ComponentInvalid[]
26-
*/
27-
private array $invalidComponents = [];
28-
2917
public function __construct(
3018
private readonly ComponentItemFactory $itemFactory,
3119
private readonly array $componentsConfig,
20+
private readonly CacheInterface $cache,
21+
private readonly int $configReadTime = 0
3222
) {
33-
$this->parse();
3423
}
3524

3625
/**
37-
* @return ComponentItem[]
26+
* @return ComponentItemList<ComponentItem>
3827
*/
39-
public function getComponentsByCategory(string $category): array
28+
public function getComponentsByCategory(string $category): ComponentItemList
4029
{
41-
return $this->categories[$category] ?? [];
30+
return $this->filter($category, 'category');
4231
}
4332

4433
/**
45-
* @return array<string, array<int, ComponentItem>>
34+
* @throws InvalidArgumentException
4635
*/
47-
public function getCategories(): array
36+
public function getComponents(): ComponentItemList
4837
{
49-
return $this->categories;
50-
}
51-
52-
private function parse(): void
53-
{
54-
$components = $categories = $invalidComponents = [];
55-
56-
foreach ($this->componentsConfig as $componentData) {
57-
try {
58-
$item = $this->itemFactory->create($componentData);
59-
} catch (InvalidComponentConfigurationException $e) {
60-
$item = new ComponentInvalid($e->getViolationList(), $componentData);
61-
$invalidComponents[] = $item;
62-
continue;
63-
}
64-
$components[] = $item;
65-
$categories[$item->getMainCategory()->getName()][] = $item;
66-
}
67-
68-
$this->components = $components;
69-
$this->categories = $categories;
70-
$this->invalidComponents = $invalidComponents;
71-
}
72-
73-
public function filter(string $filterQuery, string $filterType): array
74-
{
75-
$components = array_unique($this->filterComponents($filterQuery, $filterType), \SORT_REGULAR);
76-
77-
$result = [];
78-
79-
foreach ($components as $component) {
80-
$result[$component->getMainCategory()->getName()][] = $component;
81-
}
38+
return new ComponentItemList(
39+
$this->cache->get('twig_doc.parsed.components'.$this->configReadTime, function () {
40+
$components = [];
41+
foreach ($this->componentsConfig as $componentData) {
42+
try {
43+
$components[] = $this->itemFactory->create($componentData);
44+
} catch (InvalidComponentConfigurationException) {
45+
continue;
46+
}
47+
}
8248

83-
return $result;
49+
return $components;
50+
})
51+
);
8452
}
8553

86-
private function filterComponents(string $filterQuery, string $filterType): array
54+
public function filter(string $filterQuery, string $filterType): ComponentItemList
8755
{
88-
$components = [];
89-
switch ($filterType) {
90-
case 'category':
91-
$components = array_filter($this->categories, fn (string $category) => strtolower($category) === strtolower($filterQuery), \ARRAY_FILTER_USE_KEY);
92-
93-
return $components[array_key_first($components)] ?? [];
94-
case 'sub_category':
95-
$components = array_filter(
96-
$this->components,
97-
fn (ComponentItem $item) => $item->getCategory()->getParent() !== null
98-
&& strtolower($item->getCategory()->getName()) === strtolower($filterQuery)
99-
);
100-
101-
break;
102-
case 'tags':
103-
$tags = array_map('trim', explode(',', strtolower($filterQuery)));
104-
$components = array_filter($this->components, function (ComponentItem $item) use ($tags) {
105-
return array_intersect($tags, array_map('strtolower', $item->getTags())) !== [];
106-
});
107-
108-
break;
109-
case 'name':
110-
$components = array_filter(
111-
$this->components,
112-
fn (ComponentItem $item) => str_contains(strtolower($item->getName()), strtolower($filterQuery)));
113-
114-
break;
115-
default:
116-
foreach (['category', 'sub_category', 'tags', 'name'] as $type) {
117-
$components = array_merge($components, $this->filterComponents($filterQuery, $type));
118-
}
119-
120-
break;
121-
}
56+
$hash = sprintf('twig_doc_bundle.search.%s.%s', md5($filterQuery.$filterType), $this->configReadTime);
12257

123-
return $components;
58+
return $this->cache->get($hash, function () use ($filterQuery, $filterType) {
59+
return $this->getComponents()->filter($filterQuery, $filterType);
60+
});
12461
}
12562

12663
/**
12764
* @return ComponentInvalid[]
65+
*
66+
* @throws InvalidArgumentException
12867
*/
12968
public function getInvalidComponents(): array
13069
{
131-
return $this->invalidComponents;
70+
return $this->cache->get('twig_doc_bundle.invalid_components'.$this->configReadTime, function () {
71+
$invalid = array_filter($this->componentsConfig, function ($cmpData) {
72+
foreach ($this->getComponents()->getArrayCopy() as $cmp) {
73+
if ($cmp->getName() === $cmpData['name'] ?? null) {
74+
return false;
75+
}
76+
}
77+
78+
return true;
79+
});
80+
$invalidComponents = [];
81+
82+
foreach ($invalid as $cmpData) {
83+
try {
84+
$this->itemFactory->create($cmpData);
85+
} catch (InvalidComponentConfigurationException $e) {
86+
$invalidComponents[] = new ComponentInvalid($e->getViolationList(), $cmpData);
87+
}
88+
}
89+
90+
return $invalidComponents;
91+
});
13292
}
13393

13494
public function getComponent(string $name): ?ComponentItem
13595
{
136-
return array_values(array_filter($this->components, fn (ComponentItem $c) => $c->getName() === $name))[0] ?? null;
96+
return array_values(array_filter((array) $this->getComponents(), fn (ComponentItem $c) => $c->getName() === $name))[0] ?? null;
13797
}
13898
}

src/Twig/TwigDocExtension.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Qossmic\TwigDocBundle\Component\ComponentCategory;
88
use Qossmic\TwigDocBundle\Component\ComponentInvalid;
99
use Qossmic\TwigDocBundle\Component\ComponentItem;
10+
use Qossmic\TwigDocBundle\Component\ComponentItemList;
1011
use Qossmic\TwigDocBundle\Service\CategoryService;
1112
use Qossmic\TwigDocBundle\Service\ComponentService;
1213
use Symfony\UX\TwigComponent\ComponentRendererInterface;
@@ -37,7 +38,7 @@ public function getFunctions(): array
3738
];
3839
}
3940

40-
public function filterComponents(string $filterQuery, ?string $type = null): array
41+
public function filterComponents(string $filterQuery, ?string $type = null): ComponentItemList
4142
{
4243
return $this->componentService->filter($filterQuery, $type);
4344
}

0 commit comments

Comments
 (0)