Skip to content

Commit

Permalink
Add a file system cache class
Browse files Browse the repository at this point in the history
  • Loading branch information
Hectorhammett committed Aug 13, 2024
1 parent 1277062 commit 2e5872c
Show file tree
Hide file tree
Showing 3 changed files with 388 additions and 0 deletions.
206 changes: 206 additions & 0 deletions src/Cache/FileSystemCacheItemPool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Cache;

use ErrorException;
use Exception;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;

class FileSystemCacheItemPool implements CacheItemPoolInterface
{
private string $cachePath = 'cache/';
private array $buffer = [];

Check failure on line 28 in src/Cache/FileSystemCacheItemPool.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

Property Google\Auth\Cache\FileSystemCacheItemPool::$buffer type has no value type specified in iterable type array.

/**
* Creates a FileSystemCacheItemPool cache that stores values in local storage
*
* @var FileSystemCacheItemPoolOptions $options
*/
public function __construct(FileSystemCacheItemPoolOptions $options = new FileSystemCacheItemPoolOptions())

Check failure on line 35 in src/Cache/FileSystemCacheItemPool.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

PHPDoc tag @var above a method has no effect.
{
$this->cachePath = $options->cachePath;

if (is_dir($this->cachePath)) {
return;
}

if (!mkdir($this->cachePath)) {
throw new ErrorException("Cache folder couldn't be created.");
}
}

/**
* {@inheritdoc}
*/
public function getItem(string $key): CacheItemInterface
{
if (!$this->validKey($key)) {
throw new InvalidArgumentException("The key '$key' is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}

$itemPath = $this->cacheFilePath($key);

if (!file_exists($itemPath)) {
return new TypedItem($key);
}

$serializedItem = file_get_contents($itemPath);
return unserialize($serializedItem);

Check failure on line 64 in src/Cache/FileSystemCacheItemPool.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

Parameter #1 $data of function unserialize expects string, string|false given.
}

/**
* {@inheritdoc}
*/
public function getItems(array $keys = []): iterable

Check failure on line 70 in src/Cache/FileSystemCacheItemPool.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

Method Google\Auth\Cache\FileSystemCacheItemPool::getItems() return type has no value type specified in iterable type iterable.
{
$result = [];

foreach ($keys as $key) {
$result[$key] = $this->getItem($key);
}

return $result;
}

/**
* {@inheritdoc}
*/
public function save(CacheItemInterface $item): bool
{
if (!$this->validKey($item->getKey())) {
throw new InvalidArgumentException("The key " . $item->getKey() . " is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}

$itemPath = $this->cacheFilePath($item->getKey());
$serializedItem = serialize($item);

$result = file_put_contents($itemPath, $serializedItem);

if (!$result) {
return false;
}

return true;
}

/**
* {@inheritdoc}
*/
public function hasItem(string $key): bool
{
return $this->getItem($key)->isHit();
}

/**
* {@inheritdoc}
*/
public function clear(): bool
{
if (!is_dir($this->cachePath)) {
return false;
}

$files = scandir($this->cachePath);
if (!$files) {
return false;
}

foreach($files as $fileName) {
if ($fileName === '.' || $fileName === '..') {
continue;
}

if (!unlink($this->cachePath . "/" . $fileName)) {
return false;
}
}

return true;
}

/**
* {@inheritdoc}
*/
public function deleteItem(string $key): bool
{
if (!$this->validKey($key)) {
throw new Exception("The key '$key' is not valid. The key should follow the pattern |^[a-zA-Z0-9_\.! ]+$|");
}

$itemPath = $this->cacheFilePath($key);

if (!file_exists($itemPath)) {
return false;
}

return unlink($itemPath);
}

/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
$result = true;

foreach($keys as $key) {
if (!$this->deleteItem($key)) {
$result = false;
}
}

return $result;
}

/**
* {@inheritdoc}
*/
public function saveDeferred(CacheItemInterface $item): bool
{
array_push($this->buffer, $item);

return true;
}

/**
* {@inheritdoc}
*/
public function commit(): bool
{
$result = true;

foreach ($this->buffer as $item) {
if (!$this->save($item)) {
$result = false;
}
}

return $result;
}

private function cacheFilePath(string $key): string
{
return $this->cachePath . '/' . $key;
}

private function validKey(string $key): bool
{
return preg_match('|^[a-zA-Z0-9_\.! ]+$|', $key);

Check failure on line 204 in src/Cache/FileSystemCacheItemPool.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

Method Google\Auth\Cache\FileSystemCacheItemPool::validKey() should return bool but returns int|false.
}
}
23 changes: 23 additions & 0 deletions src/Cache/FileSystemCacheItemPoolOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/**
* Copyright 2024 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Cache;

class FileSystemCacheItemPoolOptions
{
public string $cachePath = 'cache/';
}
159 changes: 159 additions & 0 deletions tests/Cache/FileSystemCacheItemPoolTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php
/*
* Copyright 2024 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Google\Auth\Tests\Cache;

use Google\Auth\Cache\FileSystemCacheItemPool;
use Google\Auth\Cache\TypedItem;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;

class FileSystemCacheItemPoolTest extends TestCase
{
private string $defaultCacheDirectory = 'cache/';
private FileSystemCacheItemPool $pool;

public function setUp(): void
{
$this->pool = new FileSystemCacheItemPool();
}

public function tearDown(): void
{
$files = scandir($this->defaultCacheDirectory);

foreach($files as $fileName) {
if ($fileName === '.' || $fileName === '..') {
continue;
}

unlink($this->defaultCacheDirectory . "/" . $fileName);
}

rmdir($this->defaultCacheDirectory);
}

public function testInstanceCreatesCacheFolder()
{
$this->assertTrue(file_exists($this->defaultCacheDirectory));
$this->assertTrue(is_dir($this->defaultCacheDirectory));
}

public function testSaveAndGetItem()
{
$item = $this->getNewItem();
$this->pool->save($item);
$retrievedItem = $this->pool->getItem($item->getKey());

$this->assertTrue($retrievedItem->isHit());
$this->assertEquals($retrievedItem->get(), $item->get());
}

public function testHasItem()
{
$item = $this->getNewItem();
$this->assertFalse($this->pool->hasItem($item->getKey()));
$this->pool->save($item);
$this->assertTrue($this->pool->hasItem($item->getKey()));
}

public function testDeleteItem()
{
$item = $this->getNewItem();
$this->pool->save($item);

$this->assertTrue($this->pool->deleteItem($item->getKey()));
$this->assertFalse($this->pool->hasItem($item->getKey()));
}

public function testDeleteItems()
{
$items = [
$this->getNewItem(),
$this->getNewItem("NewItem2"),
$this->getNewItem("NewItem3")
];

foreach ($items as $item) {
$this->pool->save($item);
}

$result = $this->pool->deleteItems(array_map(function(CacheItemInterface $item) {
return $item->getKey();
}, $items));
$this->assertTrue($result);

$result = $this->pool->deleteItems(array_map(function(CacheItemInterface $item) {
return $item->getKey();
}, $items));
$this->assertFalse($result);
}

public function testGetItems()
{
$items = [
$this->getNewItem(),
$this->getNewItem("NewItem2"),
$this->getNewItem("NewItem3")
];

foreach ($items as $item) {
$this->pool->save($item);
}

$keys = array_map(function(CacheItemInterface $item) {
return $item->getKey();
}, $items);
array_push($keys, 'NonExistant');

$retrievedItems = $this->pool->getItems($keys);

foreach ($items as $item) {
$this->assertTrue($retrievedItems[$item->getKey()]->isHit());
}

$this->assertFalse($retrievedItems['NonExistant']->isHit());
}

public function testClear()
{
$item = $this->getNewItem();
$this->pool->save($item);
$this->assertLessThan(scandir($this->defaultCacheDirectory), 2);
$this->pool->clear();
// Clear removes all the files, but scandir returns `.` and `..` as files
$this->assertEquals(count(scandir($this->defaultCacheDirectory)), 2);
}

public function testSaveDeferredAndCommit()
{
$item = $this->getNewItem();
$this->pool->saveDeferred($item);
$this->assertFalse($this->pool->getItem($item->getKey())->isHit());

$this->pool->commit();
$this->assertTrue($this->pool->getItem($item->getKey())->isHit());
}

private function getNewItem(null|string $key = null): TypedItem
{
$item = new TypedItem($key ?? 'NewItem');
$item->set('NewValue');

return $item;
}
}

0 comments on commit 2e5872c

Please sign in to comment.