Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.56% covered (success)
97.56%
40 / 41
90.91% covered (success)
90.91%
10 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
InMemoryFilesystemAdapter
97.56% covered (success)
97.56%
40 / 41
90.91% covered (success)
90.91%
10 / 11
21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 normalizePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 write
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 copy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createDirectory
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 deleteDirectory
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setVisibility
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 visibility
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 lastModified
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 createDirectories
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 directoryKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * An extension of League's in-memory adapter which:
4 *
5 * * back-fills `::directoryExists()` when `league/flysystem` v2 is installed
6 * * normalizes paths using an injectable {@see PathNormalizer}
7 * * creates parent directories when a file is written, so directory listings are complete
8 * * records the visibility of directories, which League's adapter does not track
9 *
10 * @package BrianHenryIE\FlysystemReadOnly
11 */
12
13namespace BrianHenryIE\FlysystemReadOnly;
14
15use League\Flysystem\Config;
16use League\Flysystem\FileAttributes;
17use League\Flysystem\InMemory\InMemoryFilesystemAdapter as LeagueInMemoryFilesystemAdapter;
18use League\Flysystem\PathNormalizer;
19use League\Flysystem\Visibility;
20use League\Flysystem\WhitespacePathNormalizer;
21use League\MimeTypeDetection\MimeTypeDetector;
22
23/**
24 * An in-memory adapter which records directory visibility and creates parent directories on write.
25 */
26class InMemoryFilesystemAdapter extends LeagueInMemoryFilesystemAdapter implements
27    FlysystemAdapterBackCompatTraitInterface
28{
29    use FlysystemAdapterBackCompatTrait;
30
31    protected PathNormalizer $pathNormalizer;
32
33    /**
34     * League's `$defaultVisibility` is private, so keep our own copy.
35     */
36    protected string $defaultFileVisibility;
37
38    /**
39     * League's adapter has no concept of a directory's visibility – directories only exist by
40     * implication – so record it here, keyed on the normalized path without a trailing slash.
41     *
42     * @var array<string, string>
43     */
44    protected array $directoryVisibility = [];
45
46    /**
47     * Constructor.
48     *
49     * @param string            $defaultVisibility Visibility applied to files and directories written without one.
50     * @param ?MimeTypeDetector $mimeTypeDetector  Detector used for `::mimeType()`; League's default when null.
51     * @param ?PathNormalizer   $pathNormalizer    Normalizer used for path comparisons; whitespace when null.
52     */
53    public function __construct(
54        string $defaultVisibility = Visibility::PUBLIC,
55        ?MimeTypeDetector $mimeTypeDetector = null,
56        ?PathNormalizer $pathNormalizer = null
57    ) {
58        parent::__construct($defaultVisibility, $mimeTypeDetector);
59
60        $this->defaultFileVisibility = $defaultVisibility;
61        $this->pathNormalizer        = $pathNormalizer ?? new WhitespacePathNormalizer();
62    }
63
64    /**
65     * Return the path in its canonical form, as this adapter stores it.
66     *
67     * @param string $path The path to normalize.
68     *
69     * @see FlysystemAdapterBackCompatTrait::directoryExistsImplementation()
70     */
71    public function normalizePath(string $path): string
72    {
73        return $this->pathNormalizer->normalizePath($path);
74    }
75
76    /**
77     * Write a {@see InMemoryFile}, first creating each of its ancestor directories.
78     *
79     * @param string $path     The path to write to.
80     * @param string $contents The file's contents.
81     * @param Config $config   Flysystem's per-operation config, e.g. visibility.
82     *
83     * @see \League\Flysystem\FilesystemAdapter::write()
84     */
85    public function write(string $path, string $contents, Config $config): void
86    {
87        /**
88         * `parent::createDirectory()` writes a dummy file to record the directory; without this guard
89         * `write()` → `createDirectories()` → `createDirectory()` → `write()` would recurse forever.
90         */
91        if (false === strpos($path, self::DUMMY_FILE_FOR_FORCED_LISTING_IN_FLYSYSTEM_TEST)) {
92            $this->createDirectories($path, $config);
93        }
94
95        parent::write($path, $contents, $config);
96    }
97
98    /**
99     * Copy a file, first creating each of the destination's ancestor directories.
100     *
101     * @param string $source      The path to copy from.
102     * @param string $destination The path to copy to.
103     * @param Config $config      Flysystem's per-operation config, e.g. visibility.
104     */
105    public function copy(string $source, string $destination, Config $config): void
106    {
107        $this->createDirectories($destination, $config);
108
109        parent::copy($source, $destination, $config);
110    }
111
112    /**
113     * Create a directory, recording its visibility, which League's adapter does not track.
114     *
115     * @param string $path   The directory to create.
116     * @param Config $config Flysystem's per-operation config, e.g. directory visibility.
117     */
118    public function createDirectory(string $path, Config $config): void
119    {
120        $visibility = $config->get(
121            Config::OPTION_DIRECTORY_VISIBILITY,
122            $config->get(Config::OPTION_VISIBILITY, $this->defaultFileVisibility)
123        );
124
125        // `Config::get()` returns `mixed`; anything that is not a visibility string is treated as absent.
126        $this->directoryVisibility[$this->directoryKey($path)] = is_string($visibility)
127            ? $visibility
128            : $this->defaultFileVisibility;
129
130        parent::createDirectory($path, $config);
131    }
132
133    /**
134     * Delete a directory, discarding the recorded visibility of it and of everything below it.
135     *
136     * @param string $path The directory to delete.
137     */
138    public function deleteDirectory(string $path): void
139    {
140        $prefix = $this->directoryKey($path);
141        foreach (array_keys($this->directoryVisibility) as $key) {
142            if ($key === $prefix || 0 === strpos($key, $prefix . '/')) {
143                unset($this->directoryVisibility[$key]);
144            }
145        }
146
147        parent::deleteDirectory($path);
148    }
149
150    /**
151     * Set the visibility of a file, or record it when the path is a directory.
152     *
153     * @param string $path       The path whose visibility to set.
154     * @param string $visibility One of {@see Visibility}'s constants.
155     */
156    public function setVisibility(string $path, string $visibility): void
157    {
158        if (!$this->fileExists($path) && $this->directoryExists($path)) {
159            $this->directoryVisibility[$this->directoryKey($path)] = $visibility;
160            return;
161        }
162
163        parent::setVisibility($path, $visibility);
164    }
165
166    /**
167     * Return the visibility of a file, or the recorded visibility when the path is a directory.
168     *
169     * @param string $path The path to read the visibility of.
170     */
171    public function visibility(string $path): FileAttributes
172    {
173        if (!$this->fileExists($path)) {
174            // Assume it is a directory.
175            return new FileAttributes(
176                $path,
177                null,
178                $this->directoryVisibility[$this->directoryKey($path)] ?? Visibility::PUBLIC
179            );
180        }
181
182        return parent::visibility($path);
183    }
184
185    /**
186     * Return the modified time of a file; directories, which have none, report `0`.
187     *
188     * @param string $path The path to read the modified time of.
189     */
190    public function lastModified(string $path): FileAttributes
191    {
192        if (!$this->fileExists($path)) {
193            // Assume it is a directory.
194            return new FileAttributes($path, null, null, 0);
195        }
196
197        return parent::lastModified($path);
198    }
199
200    /**
201     * Create each ancestor directory of the given file path.
202     *
203     * @param string $path   The file path whose ancestors to create.
204     * @param Config $config Flysystem's per-operation config, e.g. directory visibility.
205     */
206    protected function createDirectories(string $path, Config $config): void
207    {
208        $pathDirs = explode('/', dirname($path));
209        for ($level = 0; $level < count($pathDirs); $level++) {
210            $dir = implode('/', array_slice($pathDirs, 0, $level + 1));
211            $this->createDirectory($dir, $config);
212        }
213    }
214
215    /**
216     * Return the key a directory's visibility is recorded under: normalized, without a trailing slash.
217     *
218     * @param string $path The directory path to key.
219     */
220    protected function directoryKey(string $path): string
221    {
222        return rtrim($this->normalizePath($path), '/');
223    }
224}