diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 7f8c7f4..24e38e5 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -11,11 +11,6 @@
-
-
-
-
-
diff --git a/src/Application/Cache/CacheInvalidator.php b/src/Application/Cache/CacheInvalidator.php
index 5528a98..601ff5b 100644
--- a/src/Application/Cache/CacheInvalidator.php
+++ b/src/Application/Cache/CacheInvalidator.php
@@ -11,6 +11,9 @@
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;
+/**
+ * @psalm-import-type Reason from CacheInvalidated
+ */
final class CacheInvalidator
{
public function __construct(
@@ -21,7 +24,10 @@ public function __construct(
) {
}
- public function invalidateFully(): bool
+ /**
+ * @psalm-param Reason $reason
+ */
+ public function invalidateFully(string $reason = CacheInvalidated::ENFORCED): bool
{
$result = $this->cache->clear();
if (!$result) {
@@ -30,16 +36,17 @@ public function invalidateFully(): bool
}
$this->logger->info('Successful degree program full cache invalidation.');
- $this->eventDispatcher->dispatch(CacheInvalidated::fully());
+ $this->eventDispatcher->dispatch(CacheInvalidated::fully($reason));
return true;
}
/**
* @psalm-param array $ids
+ * @psalm-param Reason $reason
*
* @throws InvalidArgumentException
*/
- public function invalidatePartially(array $ids): bool
+ public function invalidatePartially(array $ids, string $reason = CacheInvalidated::ENFORCED): bool
{
if (count($ids) === 0) {
$this->logger->debug(
@@ -81,7 +88,7 @@ public function invalidatePartially(array $ids): bool
implode(', ', $ids)
)
);
- $this->eventDispatcher->dispatch(CacheInvalidated::partially($ids));
+ $this->eventDispatcher->dispatch(CacheInvalidated::partially($ids, $reason));
return true;
}
diff --git a/src/Application/DegreeProgramViewTranslated.php b/src/Application/DegreeProgramViewTranslated.php
index e1d50cd..f119eb3 100644
--- a/src/Application/DegreeProgramViewTranslated.php
+++ b/src/Application/DegreeProgramViewTranslated.php
@@ -79,11 +79,15 @@
* }
* @psalm-type DegreeProgramViewTranslatedArrayType = DegreeProgramTranslation & array{
* id: int,
+ * date: string,
+ * modified: string,
* translations: array,
* }
*/
final class DegreeProgramViewTranslated implements JsonSerializable
{
+ public const DATE = 'date';
+ public const MODIFIED = 'modified';
public const LINK = 'link';
public const LANG = 'lang';
public const ADMISSION_REQUIREMENT_LINK = 'admission_requirement_link';
@@ -94,6 +98,8 @@ final class DegreeProgramViewTranslated implements JsonSerializable
public function __construct(
private DegreeProgramId $id,
+ private string $date,
+ private string $modified,
private string $link,
private string $slug,
/**
@@ -160,6 +166,8 @@ public static function empty(int $id, string $languageCode): self
{
return new self(
DegreeProgramId::fromInt($id),
+ date: '',
+ modified: '',
link: '',
slug: '',
lang: $languageCode,
@@ -220,6 +228,8 @@ public static function empty(int $id, string $languageCode): self
/**
* @psalm-param DegreeProgramTranslation & array{
* id: int | numeric-string,
+ * date: string,
+ * modified: string,
* translations?: array,
* } $data
*
@@ -229,6 +239,8 @@ public static function fromArray(array $data): self
{
$main = new self(
id: DegreeProgramId::fromInt((int) $data[DegreeProgram::ID]),
+ date: $data[self::DATE] ?? '',
+ modified: $data[self::MODIFIED] ?? '',
link: $data[self::LINK],
slug: $data[DegreeProgram::SLUG],
lang: $data[self::LANG],
@@ -285,12 +297,14 @@ public static function fromArray(array $data): self
campoKeys: CampoKeys::fromArray($data[DegreeProgram::CAMPO_KEYS] ?? []),
);
- if (empty($data[self::TRANSLATIONS])) {
+ if (!isset($data[self::TRANSLATIONS]) || count($data[self::TRANSLATIONS]) === 0) {
return $main;
}
foreach ($data[self::TRANSLATIONS] as $translationData) {
$translationData[DegreeProgram::ID] = $data[DegreeProgram::ID];
+ $translationData[self::DATE] = $data[self::DATE];
+ $translationData[self::MODIFIED] = $data[self::MODIFIED];
$main = $main->withTranslation(self::fromArray($translationData), $translationData[self::LANG]);
}
@@ -304,6 +318,8 @@ public function asArray(): array
{
return [
DegreeProgram::ID => $this->id->asInt(),
+ self::DATE => $this->date,
+ self::MODIFIED => $this->modified,
self::LINK => $this->link,
DegreeProgram::SLUG => $this->slug,
self::LANG => $this->lang,
@@ -408,7 +424,12 @@ private function translationsAsArray(): array
{
return array_map(static function (DegreeProgramViewTranslated $view): array {
$result = $view->asArray();
- unset($result[DegreeProgram::ID], $result[self::TRANSLATIONS]);
+ unset(
+ $result[DegreeProgram::ID],
+ $result[self::DATE],
+ $result[self::MODIFIED],
+ $result[self::TRANSLATIONS]
+ );
return $result;
}, $this->translations);
@@ -419,6 +440,16 @@ public function id(): int
return $this->id->asInt();
}
+ public function date(): string
+ {
+ return $this->date;
+ }
+
+ public function modified(): string
+ {
+ return $this->modified;
+ }
+
public function link(): string
{
return $this->link;
diff --git a/src/Application/Event/CacheInvalidated.php b/src/Application/Event/CacheInvalidated.php
index c94b7da..a333745 100644
--- a/src/Application/Event/CacheInvalidated.php
+++ b/src/Application/Event/CacheInvalidated.php
@@ -6,30 +6,41 @@
use Stringable;
+/**
+ * @psalm-type Reason = self::ENFORCED | self::DATA_CHANGED
+ */
final class CacheInvalidated implements Stringable
{
public const NAME = 'degree_program_cache_invalidated';
+ public const ENFORCED = 'enforced';
+ public const DATA_CHANGED = 'data_changed';
/**
* @param array $ids
+ * @param Reason $reason
*/
private function __construct(
private bool $isFully,
private array $ids,
+ private string $reason,
) {
}
- public static function fully(): self
+ /**
+ * @param Reason $reason
+ */
+ public static function fully(string $reason): self
{
- return new self(true, []);
+ return new self(true, [], $reason);
}
/**
* @param array $ids
+ * @param Reason $reason
*/
- public static function partially(array $ids): self
+ public static function partially(array $ids, string $reason): self
{
- return new self(false, $ids);
+ return new self(false, $ids, $reason);
}
public function isFully(): bool
@@ -45,6 +56,14 @@ public function ids(): array
return $this->ids;
}
+ /**
+ * @return Reason
+ */
+ public function reason(): string
+ {
+ return $this->reason;
+ }
+
public function __toString(): string
{
return self::NAME;
diff --git a/src/Infrastructure/Repository/TimestampRepository.php b/src/Infrastructure/Repository/TimestampRepository.php
new file mode 100644
index 0000000..7affc30
--- /dev/null
+++ b/src/Infrastructure/Repository/TimestampRepository.php
@@ -0,0 +1,49 @@
+asInt());
+
+ return $postDateTime instanceof DateTimeInterface ? $postDateTime : null;
+ }
+
+ /**
+ * The custom field is updated when the degree program or related settings or terms are updated.
+ * If the custom field does not exist,
+ * we fall back to the core WordPress "post modified" property.
+ */
+ public function modified(DegreeProgramId $id): ?DateTimeInterface
+ {
+ $timestamp = (int) get_post_meta($id->asInt(), self::MODIFIED_META_KEY, true);
+
+ if ($timestamp < 1) {
+ $timestamp = get_post_timestamp($id->asInt(), 'modified');
+ }
+
+ if (!is_int($timestamp)) {
+ return null;
+ }
+
+ $dateTime = new DateTimeImmutable();
+ $dateTime = $dateTime->setTimestamp($timestamp);
+
+ return $dateTime->setTimezone(wp_timezone());
+ }
+
+ public function updateModified(DegreeProgramId $id): void
+ {
+ update_post_meta($id->asInt(), self::MODIFIED_META_KEY, time());
+ }
+}
diff --git a/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php b/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php
index 7d32c54..efdb713 100644
--- a/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php
+++ b/src/Infrastructure/Repository/WordPressDatabaseDegreeProgramViewRepository.php
@@ -4,6 +4,7 @@
namespace Fau\DegreeProgram\Common\Infrastructure\Repository;
+use DateTimeInterface;
use Fau\DegreeProgram\Common\Application\AdmissionRequirementsTranslated;
use Fau\DegreeProgram\Common\Application\AdmissionRequirementTranslated;
use Fau\DegreeProgram\Common\Application\ConditionalFieldsFilter;
@@ -33,11 +34,14 @@
*/
final class WordPressDatabaseDegreeProgramViewRepository implements DegreeProgramViewRepository
{
+ private const DATE_TIME_FORMAT = DateTimeInterface::RFC3339;
+
public function __construct(
private DegreeProgramRepository $degreeProgramRepository,
private HtmlDegreeProgramSanitizer $htmlContentSanitizer,
private ConditionalFieldsFilter $conditionalFieldsFilter,
private FacultyRepository $facultyRepository,
+ private TimestampRepository $timestampRepository,
) {
}
@@ -100,6 +104,8 @@ private function translateDegreeProgram(
return new DegreeProgramViewTranslated(
id: $raw->id(),
+ date: (string) $this->timestampRepository->created($raw->id())?->format(self::DATE_TIME_FORMAT),
+ modified: (string) $this->timestampRepository->modified($raw->id())?->format(self::DATE_TIME_FORMAT),
link: $this->link(
$raw->id()->asInt(),
$raw->slug(),
diff --git a/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php b/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php
index 5248144..848a2a9 100644
--- a/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php
+++ b/src/Infrastructure/RestApi/TranslatedDegreeProgramController.php
@@ -315,6 +315,24 @@ public function get_item_schema(): array
),
'type' => 'integer',
],
+ DegreeProgramViewTranslated::DATE => [
+ 'description' => _x(
+ 'The date the degree program was created.',
+ 'rest_api: schema item description',
+ 'fau-degree-program-common'
+ ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ ],
+ DegreeProgramViewTranslated::MODIFIED => [
+ 'description' => _x(
+ 'The date the degree program was last modified.',
+ 'rest_api: schema item description',
+ 'fau-degree-program-common'
+ ),
+ 'type' => 'string',
+ 'format' => 'date-time',
+ ],
DegreeProgram::FEATURED_IMAGE => [
'description' => _x(
'Feature image.',