Currently the cache is only invalidated when the datastore changes, while it should rely on selected period of time.
Fixes #1659
<?php
+declare(strict_types=1);
+
namespace Shaarli\Feed;
+use DatePeriod;
+
/**
* Simple cache system, mainly for the RSS/ATOM feeds
*/
class CachedPage
{
- // Directory containing page caches
- private $cacheDir;
+ /** Directory containing page caches */
+ protected $cacheDir;
+
+ /** Should this URL be cached (boolean)? */
+ protected $shouldBeCached;
- // Should this URL be cached (boolean)?
- private $shouldBeCached;
+ /** Name of the cache file for this URL */
+ protected $filename;
- // Name of the cache file for this URL
- private $filename;
+ /** @var DatePeriod|null Optionally specify a period of time for cache validity */
+ protected $validityPeriod;
/**
* Creates a new CachedPage
*
- * @param string $cacheDir page cache directory
- * @param string $url page URL
- * @param bool $shouldBeCached whether this page needs to be cached
+ * @param string $cacheDir page cache directory
+ * @param string $url page URL
+ * @param bool $shouldBeCached whether this page needs to be cached
+ * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
*/
- public function __construct($cacheDir, $url, $shouldBeCached)
+ public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
{
// TODO: check write access to the cache directory
$this->cacheDir = $cacheDir;
$this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
$this->shouldBeCached = $shouldBeCached;
+ $this->validityPeriod = $validityPeriod;
}
/**
if (!$this->shouldBeCached) {
return null;
}
- if (is_file($this->filename)) {
- return file_get_contents($this->filename);
+ if (!is_file($this->filename)) {
+ return null;
+ }
+ if ($this->validityPeriod !== null) {
+ $cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
+ if (
+ $cacheDate < $this->validityPeriod->getStartDate()
+ || $cacheDate > $this->validityPeriod->getEndDate()
+ ) {
+ return null;
+ }
}
- return null;
+
+ return file_get_contents($this->filename);
}
/**
public function rss(Request $request, Response $response): Response
{
$response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
+ $type = DailyPageHelper::extractRequestedType($request);
+ $cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
$pageUrl = page_url($this->container->environment);
- $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
+ $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
}
$days = [];
- $type = DailyPageHelper::extractRequestedType($request);
$format = DailyPageHelper::getFormatByType($type);
$length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) {
namespace Shaarli\Helper;
+use DatePeriod;
+use DateTimeImmutable;
+use Exception;
use Shaarli\Bookmark\Bookmark;
use Slim\Http\Request;
* @param string|null $requestedDate Input string extracted from the request
* @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
*
- * @return \DateTimeImmutable from input or latest bookmark.
+ * @return DateTimeImmutable from input or latest bookmark.
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
public static function extractRequestedDateTime(
string $type,
?string $requestedDate,
Bookmark $latestBookmark = null
- ): \DateTimeImmutable {
+ ): DateTimeImmutable {
$format = static::getFormatByType($type);
if (empty($requestedDate)) {
return $latestBookmark instanceof Bookmark
- ? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
- : new \DateTimeImmutable()
+ ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
+ : new DateTimeImmutable()
;
}
// W is not supported by createFromFormat...
if ($type === static::WEEK) {
- return (new \DateTimeImmutable())
+ return (new DateTimeImmutable())
->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
;
}
- return \DateTimeImmutable::createFromFormat($format, $requestedDate);
+ return DateTimeImmutable::createFromFormat($format, $requestedDate);
}
/**
*
* @see https://www.php.net/manual/en/datetime.format.php
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
public static function getFormatByType(string $type): string
{
case static::DAY:
return 'Ymd';
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
* and we don't want to alter original datetime.
*
* @param string $type month/week/day
- * @param \DateTimeImmutable $requested DateTime extracted from request input
+ * @param DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return \DateTimeInterface First DateTime of the time period
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
- public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
+ public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
{
switch ($type) {
case static::MONTH:
case static::DAY:
return $requested->modify('Today midnight');
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
* and we don't want to alter original datetime.
*
* @param string $type month/week/day
- * @param \DateTimeImmutable $requested DateTime extracted from request input
+ * @param DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return \DateTimeInterface Last DateTime of the time period
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
- public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
+ public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
{
switch ($type) {
case static::MONTH:
case static::DAY:
return $requested->modify('Today 23:59:59');
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
*
* @return string Localized time period description
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
public static function getDescriptionByType(
string $type,
}
return $out . format_date($requested, false);
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
*
* @return int number of elements
*
- * @throws \Exception Type not supported.
+ * @throws Exception Type not supported.
*/
public static function getRssLengthByType(string $type): int
{
case static::DAY:
return 30; // ~1 month
default:
- throw new \Exception('Unsupported daily format type');
+ throw new Exception('Unsupported daily format type');
}
}
+
+ /**
+ * Get the number of items to display in the RSS feed depending on the given type.
+ *
+ * @param string $type month/week/day
+ * @param ?DateTimeImmutable $requested Currently only used for UT
+ *
+ * @return DatePeriod number of elements
+ *
+ * @throws Exception Type not supported.
+ */
+ public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
+ {
+ $requested = $requested ?? new DateTimeImmutable();
+
+ return new DatePeriod(
+ static::getStartDateTimeByType($type, $requested),
+ new \DateInterval('P1D'),
+ static::getEndDateTimeByType($type, $requested)
+ );
+ }
}
namespace Shaarli\Render;
+use DatePeriod;
use Shaarli\Feed\CachedPage;
/**
$this->purgeCachedPages();
}
- public function getCachePage(string $pageUrl): CachedPage
+ /**
+ * Get CachedPage instance for provided URL.
+ *
+ * @param string $pageUrl
+ * @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
+ *
+ * @return CachedPage
+ */
+ public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
{
return new CachedPage(
$this->pageCacheDir,
$pageUrl,
- false === $this->isLoggedIn
+ false === $this->isLoggedIn,
+ $validityPeriod
);
}
}
*/
public function testConstruct()
{
- new CachedPage(self::$testCacheDir, '', true);
- new CachedPage(self::$testCacheDir, '', false);
- new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true);
- new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false);
+ new CachedPage(self::$testCacheDir, '', true, null);
+ new CachedPage(self::$testCacheDir, '', false, null);
+ new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null);
+ new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null);
$this->addToAssertionCount(1);
}
*/
public function testCache()
{
- $page = new CachedPage(self::$testCacheDir, self::$url, true);
+ $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
*/
public function testShouldNotCache()
{
- $page = new CachedPage(self::$testCacheDir, self::$url, false);
+ $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
*/
public function testCachedVersion()
{
- $page = new CachedPage(self::$testCacheDir, self::$url, true);
+ $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
*/
public function testCachedVersionNoFile()
{
- $page = new CachedPage(self::$testCacheDir, self::$url, true);
+ $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename);
$this->assertEquals(
*/
public function testNoCachedVersion()
{
- $page = new CachedPage(self::$testCacheDir, self::$url, false);
+ $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
$this->assertFileNotExists(self::$filename);
$this->assertEquals(
$page->cachedVersion()
);
}
+
+ /**
+ * Return a page's cached content within date period
+ */
+ public function testCachedVersionInDatePeriod()
+ {
+ $period = new \DatePeriod(
+ new \DateTime('yesterday'),
+ new \DateInterval('P1D'),
+ new \DateTime('tomorrow')
+ );
+ $page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
+
+ $this->assertFileNotExists(self::$filename);
+ $page->cache('<p>Some content</p>');
+ $this->assertFileExists(self::$filename);
+ $this->assertEquals(
+ '<p>Some content</p>',
+ $page->cachedVersion()
+ );
+ }
+
+ /**
+ * Return a page's cached content outside of date period
+ */
+ public function testCachedVersionNotInDatePeriod()
+ {
+ $period = new \DatePeriod(
+ new \DateTime('yesterday noon'),
+ new \DateInterval('P1D'),
+ new \DateTime('yesterday midnight')
+ );
+ $page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
+
+ $this->assertFileNotExists(self::$filename);
+ $page->cache('<p>Some content</p>');
+ $this->assertFileExists(self::$filename);
+ $this->assertNull($page->cachedVersion());
+ }
}
namespace Shaarli\Helper;
+use DateTimeImmutable;
+use DateTimeInterface;
use Shaarli\Bookmark\Bookmark;
use Shaarli\TestCase;
use Slim\Http\Request;
string $type,
string $input,
?Bookmark $bookmark,
- \DateTimeInterface $expectedDateTime,
+ DateTimeInterface $expectedDateTime,
string $compareFormat = 'Ymd'
): void {
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
*/
public function testGetStartDatesByType(
string $type,
- \DateTimeImmutable $dateTime,
- \DateTimeInterface $expectedDateTime
+ DateTimeImmutable $dateTime,
+ DateTimeInterface $expectedDateTime
): void {
$startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
- DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
+ DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable());
}
/**
*/
public function testGetEndDatesByType(
string $type,
- \DateTimeImmutable $dateTime,
- \DateTimeInterface $expectedDateTime
+ DateTimeImmutable $dateTime,
+ DateTimeInterface $expectedDateTime
): void {
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
- DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
+ DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable());
}
/**
*/
public function testGeDescriptionsByType(
string $type,
- \DateTimeImmutable $dateTime,
+ DateTimeImmutable $dateTime,
string $expectedDescription
): void {
$description = DailyPageHelper::getDescriptionByType($type, $dateTime);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
- DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
+ DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable());
}
/**
DailyPageHelper::getRssLengthByType('nope');
}
+ /**
+ * @dataProvider getCacheDatePeriodByType
+ */
+ public function testGetCacheDatePeriodByType(
+ string $type,
+ DateTimeImmutable $requested,
+ DateTimeInterface $start,
+ DateTimeInterface $end
+ ): void {
+ $period = DailyPageHelper::getCacheDatePeriodByType($type, $requested);
+
+ static::assertEquals($start, $period->getStartDate());
+ static::assertEquals($end, $period->getEndDate());
+ }
+
+ public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void
+ {
+ $this->expectException(\Exception::class);
+ $this->expectExceptionMessage('Unsupported daily format type');
+
+ DailyPageHelper::getCacheDatePeriodByType('nope');
+ }
+
/**
* Data provider for testExtractRequestedType() test method.
*/
public function getStartDatesByType(): array
{
return [
- [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
- [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
- [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
+ [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
+ [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
+ [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
];
}
public function getEndDatesByType(): array
{
return [
- [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
- [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
- [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
+ [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
+ [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
+ [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
];
}
public function getDescriptionsByType(): array
{
return [
- [DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
- [DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
- [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
- [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
- [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
+ [DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
+ [DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
+ [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
+ [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
+ [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
];
}
}
/**
- * Data provider for testGetDescriptionsByType() test method.
+ * Data provider for testGetRssLengthsByType() test method.
*/
public function getRssLengthsByType(): array
{
[DailyPageHelper::MONTH],
];
}
+
+ /**
+ * Data provider for testGetCacheDatePeriodByType() test method.
+ */
+ public function getCacheDatePeriodByType(): array
+ {
+ return [
+ [
+ DailyPageHelper::DAY,
+ new DateTimeImmutable('2020-10-09 04:05:06'),
+ new \DateTime('2020-10-09 00:00:00'),
+ new \DateTime('2020-10-09 23:59:59'),
+ ],
+ [
+ DailyPageHelper::WEEK,
+ new DateTimeImmutable('2020-10-09 04:05:06'),
+ new \DateTime('2020-10-05 00:00:00'),
+ new \DateTime('2020-10-11 23:59:59'),
+ ],
+ [
+ DailyPageHelper::MONTH,
+ new DateTimeImmutable('2020-10-09 04:05:06'),
+ new \DateTime('2020-10-01 00:00:00'),
+ new \DateTime('2020-10-31 23:59:59'),
+ ],
+ ];
+ }
}