Skip to content

Commit e9a1a00

Browse files
committed
feat: CacheFilesystem
1 parent 613885d commit e9a1a00

File tree

2 files changed

+213
-0
lines changed

2 files changed

+213
-0
lines changed

src/Filesystem/CacheFilesystem.php

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the zenstruck/filesystem package.
5+
*
6+
* (c) Kevin Bond <kevinbond@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Zenstruck\Filesystem;
13+
14+
use Psr\Cache\CacheItemPoolInterface;
15+
use Symfony\Contracts\Cache\ItemInterface;
16+
use Symfony\Contracts\Cache\TagAwareCacheInterface;
17+
use Zenstruck\Filesystem;
18+
use Zenstruck\Filesystem\Node\Directory;
19+
use Zenstruck\Filesystem\Node\File;
20+
use Zenstruck\Filesystem\Node\File\Image;
21+
use Zenstruck\Filesystem\Node\File\Image\LazyImage;
22+
use Zenstruck\Filesystem\Node\File\LazyFile;
23+
use Zenstruck\Filesystem\Node\Mapping;
24+
25+
/**
26+
* @author Kevin Bond <kevinbond@gmail.com>
27+
*
28+
* @phpstan-import-type Format from Mapping
29+
* @phpstan-import-type Serialized from Mapping
30+
*/
31+
final class CacheFilesystem implements Filesystem
32+
{
33+
use DecoratedFilesystem;
34+
35+
private Mapping $mapping;
36+
37+
/**
38+
* @param Format $metadata
39+
*/
40+
public function __construct(
41+
private Filesystem $inner,
42+
private CacheItemPoolInterface $cache,
43+
array|string $metadata,
44+
private ?int $ttl = null,
45+
) {
46+
// ensure PATH is always included
47+
if (\is_string($metadata) && Mapping::PATH !== $metadata) {
48+
$metadata = \array_merge((array) $metadata, [Mapping::PATH]);
49+
}
50+
51+
if (\is_array($metadata) && !\in_array(Mapping::PATH, $metadata, true)) {
52+
$metadata[] = Mapping::PATH;
53+
}
54+
55+
$this->mapping = new Mapping($metadata, filesystem: '__none__'); // dummy filesystem as it's required if not using a dsn (todo: remove this requirement)
56+
}
57+
58+
public function node(string $path): File|Directory
59+
{
60+
$item = $this->cache->getItem($this->cacheKey($path));
61+
62+
if ($item->isHit() && $file = $this->unserialize($item->get())) {
63+
return $file;
64+
}
65+
66+
$node = $this->inner->node($path);
67+
68+
if ($node instanceof Directory) {
69+
// directories are not cacheable
70+
return $node;
71+
}
72+
73+
if ($this->ttl) {
74+
$item->expiresAfter($this->ttl);
75+
}
76+
77+
if ($this->cache instanceof TagAwareCacheInterface && $item instanceof ItemInterface) {
78+
$item->tag(['filesystem', "filesystem.{$this->name()}"]);
79+
}
80+
81+
$item->set($this->serialize($node));
82+
83+
return $node;
84+
}
85+
86+
public function file(string $path): File
87+
{
88+
return $this->node($path)->ensureFile();
89+
}
90+
91+
public function image(string $path): Image
92+
{
93+
return $this->node($path)->ensureImage();
94+
}
95+
96+
public function has(string $path): bool
97+
{
98+
if ($this->cache->hasItem($this->cacheKey($path))) {
99+
return true;
100+
}
101+
102+
return $this->inner->has($path);
103+
}
104+
105+
public function copy(string $source, string $destination, array $config = []): File
106+
{
107+
try {
108+
return $this->inner->copy($source, $destination, $config);
109+
} finally {
110+
$this->cache->deleteItem($this->cacheKey($destination));
111+
}
112+
}
113+
114+
public function move(string $source, string $destination, array $config = []): File
115+
{
116+
try {
117+
return $this->inner->move($source, $destination, $config);
118+
} finally {
119+
$this->cache->deleteItems([$this->cacheKey($source), $this->cacheKey($destination)]);
120+
}
121+
}
122+
123+
public function delete(string $path, array $config = []): self
124+
{
125+
$this->inner->delete($path, $config);
126+
$this->cache->deleteItem($this->cacheKey($path));
127+
128+
return $this;
129+
}
130+
131+
public function chmod(string $path, string $visibility): File|Directory
132+
{
133+
try {
134+
return $this->inner->chmod($path, $visibility);
135+
} finally {
136+
$this->cache->deleteItem($this->cacheKey($path));
137+
}
138+
}
139+
140+
public function write(string $path, mixed $value, array $config = []): File
141+
{
142+
try {
143+
return $this->inner->write($path, $value, $config);
144+
} finally {
145+
$this->cache->deleteItem($this->cacheKey($path));
146+
}
147+
}
148+
149+
protected function inner(): Filesystem
150+
{
151+
return $this->inner;
152+
}
153+
154+
/**
155+
* @return array{Serialized,bool}
156+
*/
157+
private function serialize(File $file): array
158+
{
159+
return [$this->mapping->serialize($file), $file instanceof Image];
160+
}
161+
162+
private function unserialize(mixed $value): ?File
163+
{
164+
if (!\is_array($value) || 2 !== \count($parts = $value)) {
165+
return null;
166+
}
167+
168+
[$data, $isImage] = $parts;
169+
170+
$file = $isImage ? new LazyImage($data) : new LazyFile($data);
171+
$file->setFilesystem($this->inner);
172+
173+
return $file;
174+
}
175+
176+
private function cacheKey(string $path): string
177+
{
178+
return \sprintf('filesystem.%s.%s', $this->name(), \str_replace('/', '.', $path));
179+
}
180+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the zenstruck/filesystem package.
5+
*
6+
* (c) Kevin Bond <kevinbond@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Zenstruck\Tests\Filesystem;
13+
14+
use Symfony\Component\Cache\Adapter\ArrayAdapter;
15+
use Zenstruck\Filesystem;
16+
use Zenstruck\Filesystem\CacheFilesystem;
17+
use Zenstruck\Filesystem\Node\Mapping;
18+
use Zenstruck\Tests\FilesystemTest;
19+
20+
/**
21+
* @author Kevin Bond <kevinbond@gmail.com>
22+
*/
23+
final class CacheFilesystemTest extends FilesystemTest
24+
{
25+
protected function createFilesystem(): Filesystem
26+
{
27+
return new CacheFilesystem(in_memory_filesystem(), new ArrayAdapter(), [
28+
Mapping::SIZE,
29+
Mapping::LAST_MODIFIED,
30+
Mapping::MIME_TYPE,
31+
]);
32+
}
33+
}

0 commit comments

Comments
 (0)