[PHP-WEBMASTER] [web-php] preview: Update NewsHandler with docblock type, reusable filtering. Add reliable fallback for 'updated' => 'published'.

Author: Mark Randall
Committer: Derick Rethans (derickr)
Date: 2026-06-24T09:48:49+01:00

Commit: Update NewsHandler with docblock type, reusable filtering. Add reliab… · php/web-php@1c98aa6 · GitHub
Raw diff: https://github.com/php/web-php/commit/1c98aa654b831d543178d64f3041d9e623b82975.diff

Update NewsHandler with docblock type, reusable filtering. Add reliable fallback for 'updated' => 'published'.

Changed paths:
  M include/layout.inc
  M phpstan-baseline.neon
  M public/index.php
  M src/News/NewsHandler.php

Diff:

diff --git a/include/layout.inc b/include/layout.inc
index f065310020..857128d64c 100644
--- a/include/layout.inc
+++ b/include/layout.inc
@@ -542,7 +542,7 @@ function get_news_changes()
         return false;
     }

- $date = date_create($lastNews["updated"]);
+ $date = date_create($lastNews["updated"] ?? $lastNews["published"]);
     if (isset($_COOKIE["LAST_NEWS"]) && $_COOKIE["LAST_NEWS"] >= $date->getTimestamp()) {
         return false;
     }
@@ -557,6 +557,8 @@ function get_news_changes()

     $date->modify("+1 week");
     if ($date->getTimestamp() > $_SERVER["REQUEST_TIME"]) {
+ assert(isset($lastNews["link"][0]["href"]));
+
         $link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '/', $lastNews["link"][0]["href"]);
         $title = $lastNews["title"];
         return "<a href='{$link}'>{$title}</a>";
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index eba441413e..ed6401d99c 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -2532,24 +2532,6 @@ parameters:
       count: 1
       path: src/News/Entry.php

- -
- message: '#^Call to function is_array\(\) with null will always evaluate to false\.$#'
- identifier: function.impossibleType
- count: 1
- path: src/News/NewsHandler.php
-
- -
- message: '#^Method phpweb\\News\\NewsHandler\:\:getLastestNews\(\) return type has no value type specified in iterable type array\.$#'
- identifier: missingType.iterableValue
- count: 1
- path: src/News/NewsHandler.php
-
- -
- message: '#^Method phpweb\\News\\NewsHandler\:\:getPregeneratedNews\(\) return type has no value type specified in iterable type array\.$#'
- identifier: missingType.iterableValue
- count: 1
- path: src/News/NewsHandler.php
-
     -
       message: '#^Method phpweb\\Themes\\FeatureComparison\:\:__construct\(\) has parameter \$links with no value type specified in iterable type array\.$#'
       identifier: missingType.iterableValue
diff --git a/public/index.php b/public/index.php
index 6758a83781..1694e3a246 100644
--- a/public/index.php
+++ b/public/index.php
@@ -61,7 +61,7 @@
foreach ((new NewsHandler())->getFrontPageNews() as $entry) {
     $link = preg_replace('~^(http://php.net/|https://www.php.net/)~', '', $entry["id"]);
     $id = parse_url($entry["id"], PHP_URL_FRAGMENT);
- $date = date_create($entry['updated']);
+ $date = date_create($entry['updated'] ?? $entry['published']);
     $date_human = date_format($date, 'd M Y');
     $date_w3c = date_format($date, DATE_W3C);
     $content .= <<<NEWSENTRY
diff --git a/src/News/NewsHandler.php b/src/News/NewsHandler.php
index a7e97f766e..c22c7e3e2f 100644
--- a/src/News/NewsHandler.php
+++ b/src/News/NewsHandler.php
@@ -5,15 +5,42 @@
namespace phpweb\News;

use DateTimeImmutable;
-
use function array_filter;
use function array_values;
+use function count;
use function is_array;

+/**
+ * @phpstan-type NewsEntryStruct array{
+ * title: string,
+ * id: string,
+ * published: string,
+ * updated?: string,
+ * link?: list<array{
+ * href: string,
+ * rel: string,
+ * type?: string,
+ * }>,
+ * category: list<array{
+ * term: string,
+ * label: string,
+ * }>,
+ * newsImage?: array{
+ * link: string,
+ * content: string,
+ * },
+ * content: string,
+ * intro?: string,
+ * finalTeaserDate?: string,
+ * }
+ */
final class NewsHandler
{
     private const MAX_FRONT_PAGE_NEWS = 25;

+ /**
+ * @return NewsEntryStruct|null
+ */
     public function getLastestNews(): array|null
     {
         $news = $this->getPregeneratedNews();
@@ -24,45 +51,48 @@ public function getLastestNews(): array|null
         return $news[0];
     }

- /** @return list<array> */
- public function getFrontPageNews(): array
+ /**
+ * @param list<string> $tags
+ * @return list<NewsEntryStruct>
+ */
+ public function getTaggedEntries(array $tags, ?int $limit = null): array
     {
- $frontPage = [];
+ $entries = [];
         foreach ($this->getPregeneratedNews() as $entry) {
- foreach ($entry['category'] as $category) {
- if ($category['term'] !== 'frontpage') {
- continue;
- }
-
- $frontPage[] = $entry;
- if (count($frontPage) >= self::MAX_FRONT_PAGE_NEWS) {
- break 2;
- }
+ if (!self::isTagged($entry, $tags)) {
+ continue;
+ }
+
+ $entries[] = $entry;
+ if ($limit !== null && count($entries) >= $limit) {
+ break;
             }
         }

- return $frontPage;
+ return $entries;
     }

- /** @return list<array> */
- public function getConferences(): array
+ /**
+ * Looks up generated news with frontpage tags.
+ *
+ * @return list<NewsEntryStruct>
+ */
+ public function getFrontPageNews(int $limit = self::MAX_FRONT_PAGE_NEWS): array
     {
- $conferences = [];
- foreach ($this->getPregeneratedNews() as $entry) {
- foreach ($entry['category'] as $category) {
- if ($category['term'] !== 'cfp' && $category['term'] !== 'conferences') {
- continue;
- }
-
- $conferences[] = $entry;
- break;
- }
- }
+ return $this->getTaggedEntries(['frontpage'], $limit);
+ }

- return $conferences;
+ /**
+ * @return list<NewsEntryStruct>
+ */
+ public function getConferences(?int $limit = null): array
+ {
+ return $this->getTaggedEntries(['cfp', 'conferences'], $limit);
     }

- /** @return list<array> */
+ /**
+ * @return list<NewsEntryStruct>
+ */
     public function getNewsByYear(int $year): array
     {
         return array_values(array_filter(
@@ -71,11 +101,32 @@ public function getNewsByYear(int $year): array
         ));
     }

+ /**
+ * @return list<NewsEntryStruct>
+ */
     public function getPregeneratedNews(): array
     {
         $NEWS_ENTRIES = null;
         include __DIR__ . '/../../include/pregen-news.inc';

+ /** @phpstan-ignore-next-line - pregen-news sets global variable */
         return is_array($NEWS_ENTRIES) ? $NEWS_ENTRIES : [];
     }
+
+ /**
+ * @param NewsEntryStruct $data
+ * @param list<string>|string $tags
+ */
+ public static function isTagged(array $data, array|string $tags): bool
+ {
+ $tags = is_array($tags) ? $tags : [$tags];
+
+ foreach ($data['category'] as $category) {
+ if (in_array($category['term'], $tags, true)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}