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

#12 add list object for sorting, filtering and pagination #15

Merged
merged 3 commits into from
Mar 21, 2024
Merged
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
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
Loading