Skip to content

Commit

Permalink
#12 add list object for sorting, filtering and pagination
Browse files Browse the repository at this point in the history
- remove unused category grouping from result-set
- add caching incl. CacheWarmer
  • Loading branch information
davidhoelzel committed Mar 20, 2024
1 parent f3c1607 commit 4958eb8
Show file tree
Hide file tree
Showing 14 changed files with 453 additions and 148 deletions.
5 changes: 5 additions & 0 deletions config/documentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Qossmic\TwigDocBundle\Cache\ComponentsWarmer;
use Qossmic\TwigDocBundle\Component\ComponentItemFactory;
use Qossmic\TwigDocBundle\Controller\TwigDocController;
use Qossmic\TwigDocBundle\Service\CategoryService;
Expand Down Expand Up @@ -36,5 +37,9 @@
->autowire()
->tag('twig.extension')
->alias(TwigDocExtension::class, 'twig_doc.twig.extension')

->set('twig_doc.cache_warmer', ComponentsWarmer::class)
->arg('$container', service('service_container'))
->tag('kernel.cache_warmer')
;
};
30 changes: 30 additions & 0 deletions src/Cache/ComponentsWarmer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Qossmic\TwigDocBundle\Cache;

use Psr\Container\ContainerInterface;
use Qossmic\TwigDocBundle\Service\ComponentService;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

class ComponentsWarmer implements CacheWarmerInterface
{
public function __construct(private readonly ContainerInterface $container)
{
}

public function isOptional(): bool
{
return true;
}

public function warmUp(string $cacheDir, ?string $buildDir = null): array
{
$componentService ??= $this->container->get('twig_doc.service.component');

if ($componentService instanceof ComponentService) {
$componentService->getComponents();
}

return [];
}
}
7 changes: 6 additions & 1 deletion src/Component/ComponentCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/**
* @codeCoverageIgnore
*/
class ComponentCategory
class ComponentCategory implements \Stringable
{
public const DEFAULT_CATEGORY = 'Components';

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

return $this;
}

public function __toString(): string
{
return $this->name;
}
}
104 changes: 104 additions & 0 deletions src/Component/ComponentItemList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Qossmic\TwigDocBundle\Component;

/**
* @method ComponentItem[] getArrayCopy()
*/
class ComponentItemList extends \ArrayObject
{
public const SORT_ASC = 'asc';
public const SORT_DESC = 'desc';

private array $sortableFields = [
'name',
'category',
'title',
];

/**
* @param ComponentItem[] $items
*/
public function __construct(array $items)
{
parent::__construct($items);
}

/**
* @return ComponentItem[]
*/
public function paginate(int $start = 0, int $limit = 15): array
{
return \array_slice($this->getArrayCopy(), $start, $limit);
}

public function sort(string $field, string $direction = self::SORT_ASC): void
{
if (!\in_array($field, $this->sortableFields)) {
throw new \InvalidArgumentException(sprintf('field "%s" is not sortable', $field));
}

$method = sprintf('get%s', ucfirst($field));

$this->uasort(function (ComponentItem $item, ComponentItem $item2) use ($method, $direction) {
if ($direction === self::SORT_DESC) {
return \call_user_func([$item2, $method]) <=> \call_user_func([$item, $method]);
}

return \call_user_func([$item, $method]) <=> \call_user_func([$item2, $method]);
});
}

public function filter(string $query, ?string $type): self
{
$components = [];
switch ($type) {
case 'category':
$components = array_filter(
$this->getArrayCopy(),
function (ComponentItem $item) use ($query) {
$category = $item->getCategory()->getName();
$parent = $item->getCategory()->getParent();
while ($parent !== null) {
$category = $parent->getName();
$parent = $parent->getParent();
}

return strtolower($category) === strtolower($query);
}
);

break;
case 'sub_category':
$components = array_filter(
$this->getArrayCopy(),
fn (ComponentItem $item) => $item->getCategory()->getParent() !== null
&& strtolower($item->getCategory()->getName()) === strtolower($query)
);

break;
case 'tags':
$tags = array_map('trim', explode(',', strtolower($query)));
$components = array_filter($this->getArrayCopy(), function (ComponentItem $item) use ($tags) {
return array_intersect($tags, array_map('strtolower', $item->getTags())) !== [];
});

break;
case 'name':
$components = array_filter(
$this->getArrayCopy(),
fn (ComponentItem $item) => str_contains(strtolower($item->getName()), strtolower($query))
);

break;
default:
foreach (['category', 'sub_category', 'tags', 'name'] as $type) {
$components = array_merge($components, (array) $this->filter($query, $type));
}

break;
}

return new self(array_unique($components, \SORT_REGULAR));
}
}
2 changes: 1 addition & 1 deletion src/Controller/TwigDocController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function __construct(

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

if ($filterQuery = $request->query->get('filterQuery')) {
$filterType = $request->query->get('filterType');
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/TwigDocExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function load(array $configs, ContainerBuilder $container): void

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

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

Expand Down
146 changes: 53 additions & 93 deletions src/Service/ComponentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,135 +4,95 @@

namespace Qossmic\TwigDocBundle\Service;

use Psr\Cache\InvalidArgumentException;
use Qossmic\TwigDocBundle\Component\ComponentInvalid;
use Qossmic\TwigDocBundle\Component\ComponentItem;
use Qossmic\TwigDocBundle\Component\ComponentItemFactory;
use Qossmic\TwigDocBundle\Component\ComponentItemList;
use Qossmic\TwigDocBundle\Exception\InvalidComponentConfigurationException;
use Symfony\Contracts\Cache\CacheInterface;

class ComponentService
{
/**
* @var ComponentItem[]
*/
private array $components = [];

/**
* @var array<string, array<int, ComponentItem>>
*/
private array $categories = [];

/**
* @var ComponentInvalid[]
*/
private array $invalidComponents = [];

public function __construct(
private readonly ComponentItemFactory $itemFactory,
private readonly array $componentsConfig,
private readonly CacheInterface $cache,
private readonly int $configReadTime = 0
) {
$this->parse();
}

/**
* @return ComponentItem[]
* @return ComponentItemList<ComponentItem>
*/
public function getComponentsByCategory(string $category): array
public function getComponentsByCategory(string $category): ComponentItemList
{
return $this->categories[$category] ?? [];
return $this->filter($category, 'category');
}

/**
* @return array<string, array<int, ComponentItem>>
* @throws InvalidArgumentException
*/
public function getCategories(): array
public function getComponents(): ComponentItemList
{
return $this->categories;
}

private function parse(): void
{
$components = $categories = $invalidComponents = [];

foreach ($this->componentsConfig as $componentData) {
try {
$item = $this->itemFactory->create($componentData);
} catch (InvalidComponentConfigurationException $e) {
$item = new ComponentInvalid($e->getViolationList(), $componentData);
$invalidComponents[] = $item;
continue;
}
$components[] = $item;
$categories[$item->getMainCategory()->getName()][] = $item;
}

$this->components = $components;
$this->categories = $categories;
$this->invalidComponents = $invalidComponents;
}

public function filter(string $filterQuery, string $filterType): array
{
$components = array_unique($this->filterComponents($filterQuery, $filterType), \SORT_REGULAR);

$result = [];

foreach ($components as $component) {
$result[$component->getMainCategory()->getName()][] = $component;
}
return new ComponentItemList(
$this->cache->get('twig_doc.parsed.components'.$this->configReadTime, function () {
$components = [];
foreach ($this->componentsConfig as $componentData) {
try {
$components[] = $this->itemFactory->create($componentData);
} catch (InvalidComponentConfigurationException) {
continue;
}
}

return $result;
return $components;
})
);
}

private function filterComponents(string $filterQuery, string $filterType): array
public function filter(string $filterQuery, string $filterType): ComponentItemList
{
$components = [];
switch ($filterType) {
case 'category':
$components = array_filter($this->categories, fn (string $category) => strtolower($category) === strtolower($filterQuery), \ARRAY_FILTER_USE_KEY);

return $components[array_key_first($components)] ?? [];
case 'sub_category':
$components = array_filter(
$this->components,
fn (ComponentItem $item) => $item->getCategory()->getParent() !== null
&& strtolower($item->getCategory()->getName()) === strtolower($filterQuery)
);

break;
case 'tags':
$tags = array_map('trim', explode(',', strtolower($filterQuery)));
$components = array_filter($this->components, function (ComponentItem $item) use ($tags) {
return array_intersect($tags, array_map('strtolower', $item->getTags())) !== [];
});

break;
case 'name':
$components = array_filter(
$this->components,
fn (ComponentItem $item) => str_contains(strtolower($item->getName()), strtolower($filterQuery)));

break;
default:
foreach (['category', 'sub_category', 'tags', 'name'] as $type) {
$components = array_merge($components, $this->filterComponents($filterQuery, $type));
}

break;
}
$hash = sprintf('twig_doc_bundle.search.%s.%s', md5($filterQuery.$filterType), $this->configReadTime);

return $components;
return $this->cache->get($hash, function () use ($filterQuery, $filterType) {
return $this->getComponents()->filter($filterQuery, $filterType);
});
}

/**
* @return ComponentInvalid[]
*
* @throws InvalidArgumentException
*/
public function getInvalidComponents(): array
{
return $this->invalidComponents;
return $this->cache->get('twig_doc_bundle.invalid_components'.$this->configReadTime, function () {
$invalid = array_filter($this->componentsConfig, function ($cmpData) {
foreach ($this->getComponents()->getArrayCopy() as $cmp) {
if ($cmp->getName() === $cmpData['name'] ?? null) {
return false;
}
}

return true;
});
$invalidComponents = [];

foreach ($invalid as $cmpData) {
try {
$this->itemFactory->create($cmpData);
} catch (InvalidComponentConfigurationException $e) {
$invalidComponents[] = new ComponentInvalid($e->getViolationList(), $cmpData);
}
}

return $invalidComponents;
});
}

public function getComponent(string $name): ?ComponentItem
{
return array_values(array_filter($this->components, fn (ComponentItem $c) => $c->getName() === $name))[0] ?? null;
return array_values(array_filter((array) $this->getComponents(), fn (ComponentItem $c) => $c->getName() === $name))[0] ?? null;
}
}
3 changes: 2 additions & 1 deletion src/Twig/TwigDocExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Qossmic\TwigDocBundle\Component\ComponentCategory;
use Qossmic\TwigDocBundle\Component\ComponentInvalid;
use Qossmic\TwigDocBundle\Component\ComponentItem;
use Qossmic\TwigDocBundle\Component\ComponentItemList;
use Qossmic\TwigDocBundle\Service\CategoryService;
use Qossmic\TwigDocBundle\Service\ComponentService;
use Symfony\UX\TwigComponent\ComponentRendererInterface;
Expand Down Expand Up @@ -37,7 +38,7 @@ public function getFunctions(): array
];
}

public function filterComponents(string $filterQuery, ?string $type = null): array
public function filterComponents(string $filterQuery, ?string $type = null): ComponentItemList
{
return $this->componentService->filter($filterQuery, $type);
}
Expand Down
Loading

0 comments on commit 4958eb8

Please sign in to comment.