diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 85aa328..f2ce236 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -116,6 +116,9 @@ class DefaultController extends AbstractController } } + $category['title'] = $category['title'] ?? ''; + $category['summary'] = $category['summary'] ?? ''; + return $this->render('pages/category.html.twig', [ 'list' => $list, 'category' => $category diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index c9e35c8..6772e9e 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -3,6 +3,7 @@ namespace App\Service; use App\Entity\Article; +use App\Entity\Event as PublicationEventEntity; use App\Enum\KindsEnum; use App\Factory\ArticleFactory; use Doctrine\ORM\EntityManagerInterface; @@ -815,11 +816,15 @@ class NostrClient } } - public function getMagazineIndex( $npub, $dTag) + /** + * Latest kind 30040 index for this author and #d tag, as {@see PublicationEventEntity} + * so callers can use {@see PublicationEventEntity::getTags()} (relay payloads are otherwise stdClass). + */ + public function getMagazineIndex(mixed $npub, mixed $dTag): ?PublicationEventEntity { $request = $this->createNostrRequest( kinds: [KindsEnum::PUBLICATION_INDEX], - filters: ['authors' => [$npub], 'tag' => ['#d', [$dTag]]], + filters: ['authors' => [(string) $npub], 'tag' => ['#d', [(string) $dTag]]], ); $response = $request->send(); $this->logger->info('Getting magazine index', ['npub' => $npub, 'dTag' => $dTag, 'response' => $response]); @@ -831,8 +836,56 @@ class NostrClient $this->logger->warning('No magazine index found', ['npub' => $npub, 'dTag' => $dTag]); return null; } - // Sort by date and return the most recent - usort($events, fn($a, $b) => $b->created_at <=> $a->created_at); - return $events[0]; + usort($events, static function ($a, $b): int { + return self::magazineEventCreatedAt($b) <=> self::magazineEventCreatedAt($a); + }); + + return self::magazineEventToPublicationEntity($events[0]); + } + + private static function magazineEventCreatedAt(mixed $event): int + { + if ($event instanceof PublicationEventEntity) { + return $event->getCreatedAt(); + } + if (\is_object($event) && isset($event->created_at)) { + return (int) $event->created_at; + } + + return 0; + } + + /** + * Normalize relay / library event objects to the app's Event entity (not persisted). + */ + private static function magazineEventToPublicationEntity(mixed $raw): ?PublicationEventEntity + { + if ($raw instanceof PublicationEventEntity) { + return $raw; + } + if (!\is_object($raw)) { + return null; + } + + try { + /** @var array $data */ + $data = json_decode(json_encode($raw, \JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR); + } catch (\JsonException) { + return null; + } + if (!\is_array($data)) { + return null; + } + $entity = new PublicationEventEntity(); + $entity->setId((string) ($data['id'] ?? '')); + $entity->setKind((int) ($data['kind'] ?? 0)); + $entity->setPubkey((string) ($data['pubkey'] ?? '')); + $entity->setContent((string) ($data['content'] ?? '')); + $entity->setCreatedAt((int) ($data['created_at'] ?? 0)); + $tags = $data['tags'] ?? []; + $entity->setTags(\is_array($tags) ? $tags : []); + $entity->setSig((string) ($data['sig'] ?? '')); + + return $entity; } } diff --git a/src/Twig/Components/Molecules/CategoryLink.php b/src/Twig/Components/Molecules/CategoryLink.php index d3dc120..9a8f7fe 100644 --- a/src/Twig/Components/Molecules/CategoryLink.php +++ b/src/Twig/Components/Molecules/CategoryLink.php @@ -8,8 +8,9 @@ use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] final class CategoryLink { - public string $title; - public string $slug; + public string $title = ''; + + public string $slug = ''; public function __construct(private CacheInterface $cache) { @@ -17,22 +18,28 @@ final class CategoryLink public function mount($category): void { - $parts = explode(':', $category[1]); - $this->slug = $parts[2]; + $coord = $category[1] ?? ''; + $parts = explode(':', (string) $coord, 3); + $this->slug = $parts[2] ?? ''; + $this->title = $this->slug !== '' ? $this->slug : 'Category'; + try { - $cat = $this->cache->get('magazine-' . $parts[2], function (){ - throw new \Exception('Not found'); + $cat = $this->cache->get('magazine-' . $this->slug, function () { + throw new \RuntimeException('Not found'); }); - $tags = $cat->getTags(); + $tags = method_exists($cat, 'getTags') ? $cat->getTags() : []; - $title = array_filter($tags, function($tag) { - return ($tag[0] === 'title'); + $titleTags = array_filter($tags, static function ($tag): bool { + return isset($tag[0]) && $tag[0] === 'title' && isset($tag[1]); }); - $this->title = $title[array_key_first($title)][1]; - } catch (\Exception $e) { - // Handle cache miss + $first = array_key_first($titleTags); + if ($first !== null) { + $this->title = (string) $titleTags[$first][1]; + } + } catch (\Throwable) { + // Cache miss or unreadable index: keep slug-based fallback title } } } diff --git a/src/Twig/Components/Organisms/FeaturedList.php b/src/Twig/Components/Organisms/FeaturedList.php index 8969c0a..6d6def6 100644 --- a/src/Twig/Components/Organisms/FeaturedList.php +++ b/src/Twig/Components/Organisms/FeaturedList.php @@ -3,78 +3,105 @@ namespace App\Twig\Components\Organisms; use App\Repository\ArticleRepository; +use App\Service\NostrClient; use Psr\Cache\InvalidArgumentException; -use swentel\nostr\Event\Event; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent] final class FeaturedList { - public string $category; - public string $title; + public string $category = ''; + + public string $title = ''; + public array $list = []; public function __construct( private readonly CacheInterface $cache, - private readonly ArticleRepository $articleRepository) - { + private readonly ArticleRepository $articleRepository, + private readonly NostrClient $nostrClient, + private readonly ParameterBagInterface $params, + ) { } /** * @throws InvalidArgumentException - * @throws \Exception */ public function mount($category): void { - $parts = explode(':', $category[1]); - /** @var Event $catIndex */ - $catIndex = $this->cache->get('magazine-' . $parts[2], function (){ - throw new \Exception('Not found'); - }); + $this->list = []; + $this->title = ''; + + $coord = $category[1] ?? ''; + $this->category = (string) $coord; + $parts = explode(':', $this->category, 3); + if (\count($parts) < 3) { + return; + } + + $slug = $parts[2]; + $npub = (string) $this->params->get('npub'); + + try { + $catIndex = $this->cache->get('magazine-' . $slug, function (ItemInterface $item) use ($npub, $slug) { + $item->expiresAfter(300); + $mag = $this->nostrClient->getMagazineIndex($npub, $slug); + if ($mag === null) { + throw new \RuntimeException('Category index not found for '.$slug); + } + + return $mag; + }); + } catch (\Throwable) { + return; + } $slugs = []; foreach ($catIndex->getTags() as $tag) { - if ($tag[0] === 'title') { - $this->title = $tag[1]; + if (($tag[0] ?? null) === 'title' && isset($tag[1])) { + $this->title = (string) $tag[1]; } - if ($tag[0] === 'a') { - $parts = explode(':', $tag[1], 3); - $slugs[] = end($parts); - if (count($slugs) >= 5) { - break; // Limit to 5 items + if (($tag[0] ?? null) === 'a' && isset($tag[1])) { + $segs = explode(':', (string) $tag[1], 3); + $slugs[] = end($segs); + if (\count($slugs) >= 5) { + break; } } } - // Use database query instead of Elasticsearch - if (!empty($slugs)) { - $articles = $this->articleRepository->findBySlugsCriteria($slugs); - - // Create a map of slug => item to get the latest version of each - $slugMap = []; - foreach ($articles as $article) { - $slug = $article->getSlug(); - if ($slug !== '') { - if (!isset($slugMap[$slug])) { - $slugMap[$slug] = $article; - } elseif ($article->getCreatedAt() > $slugMap[$slug]->getCreatedAt()) { - $slugMap[$slug] = $article; - } - } - } + if ($this->title === '') { + $this->title = $slug; + } + + if ($slugs === []) { + return; + } - // Build ordered list based on original slugs order - $orderedList = []; - foreach ($slugs as $slug) { - if (isset($slugMap[$slug])) { - $orderedList[] = $slugMap[$slug]; + $articles = $this->articleRepository->findBySlugsCriteria($slugs); + + $slugMap = []; + foreach ($articles as $article) { + $articleSlug = $article->getSlug(); + if ($articleSlug !== '') { + if (!isset($slugMap[$articleSlug])) { + $slugMap[$articleSlug] = $article; + } elseif ($article->getCreatedAt() > $slugMap[$articleSlug]->getCreatedAt()) { + $slugMap[$articleSlug] = $article; } } + } - $this->list = array_slice($orderedList, 0, 4); - } else { - $this->list = []; + $orderedList = []; + foreach ($slugs as $articleSlug) { + if (isset($slugMap[$articleSlug])) { + $orderedList[] = $slugMap[$articleSlug]; + } } + + $this->list = array_slice($orderedList, 0, 4); } } diff --git a/templates/pages/category.html.twig b/templates/pages/category.html.twig index 91814c1..60fcb30 100644 --- a/templates/pages/category.html.twig +++ b/templates/pages/category.html.twig @@ -1,10 +1,10 @@ {% extends 'base.html.twig' %} {% block ogtags %} - + - + {% endblock %}