• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

68publishers / file-storage / 14999924922

13 May 2025 02:57PM UTC coverage: 92.308% (-0.2%) from 92.542%
14999924922

Pull #7

github

web-flow
Merge a8dc99e08 into d186f6ffe
Pull Request #7: PSR-7 stream support

34 of 39 new or added lines in 3 files covered. (87.18%)

13 existing lines in 1 file now uncovered.

648 of 702 relevant lines covered (92.31%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.42
/src/Resource/ResourceFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace SixtyEightPublishers\FileStorage\Resource;
6

7
use finfo;
8
use League\Flysystem\FilesystemException as LeagueFilesystemException;
9
use League\Flysystem\FilesystemReader;
10
use League\Flysystem\UnableToRetrieveMetadata;
11
use Psr\Http\Message\StreamInterface;
12
use RuntimeException;
13
use SixtyEightPublishers\FileStorage\Exception\FileNotFoundException;
14
use SixtyEightPublishers\FileStorage\Exception\FilesystemException;
15
use SixtyEightPublishers\FileStorage\PathInfoInterface;
16
use function array_key_exists;
17
use function array_shift;
18
use function error_clear_last;
19
use function error_get_last;
20
use function explode;
21
use function file_exists;
22
use function filter_var;
23
use function fopen;
24
use function fread;
25
use function fstat;
26
use function ftell;
27
use function fwrite;
28
use function get_debug_type;
29
use function is_file;
30
use function is_resource;
31
use function rewind;
32
use function sprintf;
33
use function str_starts_with;
34
use function stream_context_create;
35
use function stream_get_meta_data;
36
use function strlen;
37
use function strtolower;
38
use function trim;
39
use const FILEINFO_MIME_TYPE;
40

41
final class ResourceFactory implements ResourceFactoryInterface
42
{
43
    public function __construct(
1✔
44
        private readonly FilesystemReader $filesystemReader,
45
    ) {}
1✔
46

47
    /**
48
     * @throws FileNotFoundException
49
     * @throws LeagueFilesystemException
50
     * @throws FilesystemException
51
     */
52
    public function createResource(PathInfoInterface $pathInfo): ResourceInterface
1✔
53
    {
54
        $path = $pathInfo->getPath();
1✔
55

56
        if (false === $this->filesystemReader->fileExists($path)) {
1✔
57
            throw new FileNotFoundException($path);
1✔
58
        }
59

60
        try {
61
            $source = $this->filesystemReader->readStream($path);
1✔
62
        } catch (LeagueFilesystemException $e) {
1✔
63
            throw new FilesystemException(
1✔
64
                message: sprintf(
1✔
65
                    'Can not read stream from file "%s".',
1✔
66
                    $path,
1✔
67
                ),
68
                previous: $e,
69
            );
70
        }
71

72
        return new StreamResource(
1✔
73
            pathInfo: $pathInfo,
1✔
74
            source: $source,
75
            mimeType: function () use ($path): ?string {
1✔
76
                try {
77
                    return $this->filesystemReader->mimeType($path);
1✔
78
                } catch (LeagueFilesystemException|UnableToRetrieveMetadata $e) {
79
                    return null;
80
                }
81
            },
1✔
82
            filesize: function () use ($path): ?int {
1✔
83
                try {
84
                    return $this->filesystemReader->fileSize($path);
1✔
85
                } catch (LeagueFilesystemException|UnableToRetrieveMetadata $e) {
86
                    return null;
87
                }
88
            },
1✔
89
        );
90
    }
91

92
    public function createResourceFromFile(PathInfoInterface $pathInfo, string $filename): ResourceInterface
1✔
93
    {
94
        return match (true) {
95
            (bool) filter_var($filename, FILTER_VALIDATE_URL) => $this->getResourceFromUrl(
1✔
96
                pathInfo: $pathInfo,
×
97
                url: $filename,
98
            ),
99
            file_exists($filename) && is_file($filename) => $this->getResourceFromLocalFile(
1✔
100
                pathInfo: $pathInfo,
1✔
101
                filename: $filename,
102
            ),
103
            default => throw new FileNotFoundException($filename),
1✔
104
        };
105
    }
106

107
    public function createResourceFromPsrStream(PathInfoInterface $pathInfo, StreamInterface $stream): ResourceInterface
1✔
108
    {
109
        $size = $stream->getSize();
1✔
110
        $streamCopy = clone $stream;
1✔
111
        $source = $streamCopy->detach();
1✔
112

113
        # For non resource based streams:
114
        if (!is_resource($source)) {
1✔
115
            $source = @fopen('php://temp', 'rb+');
1✔
116

117
            if (false === $source) {
1✔
NEW
118
                throw new FilesystemException(
×
NEW
119
                    message: sprintf(
×
NEW
120
                        'Unable to create temporary stream for PSR-7 stream of type %s.',
×
NEW
121
                        get_debug_type($stream),
×
122
                    ),
123
                );
124
            }
125

126
            try {
127
                $stream->rewind();
1✔
128
            } catch (RuntimeException $e) {
1✔
129
                # ignore
130
            }
131

132
            # clone data to the resource
133
            while (!$stream->eof()) {
1✔
134
                $chunk = $stream->read(8192);
1✔
135
                if ('' === $chunk) {
1✔
NEW
136
                    break;
×
137
                }
138

139
                fwrite($source, $chunk);
1✔
140
            }
141
            rewind($source);
1✔
142
        }
143

144
        return new StreamResource(
1✔
145
            pathInfo: $pathInfo,
1✔
146
            source: $source,
147
            mimeType: function (StreamResource $resource): ?string {
1✔
148
                $source = $resource->getSource();
1✔
149

150
                if (ftell($source) !== 0 && stream_get_meta_data($source)['seekable']) {
1✔
151
                    rewind($source);
1✔
152
                }
153

154
                $finfo = new finfo(FILEINFO_MIME_TYPE);
1✔
155
                $mimeType = null;
1✔
156
                $chunk = fread($source, 1024) ?: '';
1✔
157

158
                rewind($source);
1✔
159

160
                if ('' !== $chunk) {
1✔
161
                    $mimeType = $finfo->buffer($chunk) ?: null;
1✔
162
                }
163

164
                if (null === $mimeType) {
1✔
165
                    $mimeType = @mime_content_type($resource->getSource()) ?: null;
166
                }
167

168
                return $mimeType;
1✔
169
            },
1✔
170
            filesize: $size ?? function (StreamResource $resource): ?int {
1✔
171
                $stat = fstat($resource->getSource());
172

173
                return false !== $stat && array_key_exists('size', $stat) ? (int) $stat['size'] : null;
174
            },
1✔
175
        );
176
    }
177

178
    /**
179
     * @throws FilesystemException
180
     */
181
    private function getResourceFromUrl(PathInfoInterface $pathInfo, string $url): ResourceInterface
182
    {
183
        error_clear_last();
×
184

185
        $context = stream_context_create(
×
186
            options: [
187
                'http' => [
188
                    'method' => 'GET',
×
189
                    'protocol_version' => 1.1,
190
                    'header' => "Accept-language: en\r\n" . "User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36\r\n",
191
                ],
192
            ],
193
        );
194

UNCOV
195
        $source = @fopen(
×
196
            filename: $url,
×
197
            mode: 'rb',
×
198
            context: $context,
×
199
        );
200

UNCOV
201
        if (false === $source) {
×
202
            throw new FilesystemException(
×
203
                message: sprintf(
×
204
                    'Can not read stream from url "%s". %s',
×
205
                    $url,
×
206
                    error_get_last()['message'] ?? '',
×
207
                ),
208
            );
209
        }
210

UNCOV
211
        $headers = stream_get_meta_data($source)['wrapper_data'] ?? [];
×
212

UNCOV
213
        return new StreamResource(
×
214
            pathInfo: $pathInfo,
×
215
            source: $source,
UNCOV
216
            mimeType: function () use ($headers): ?string {
×
217
                $contentTypeHeader = $this->getHeaderValue(
218
                    headers: $headers,
219
                    name: 'Content-Type',
220
                );
221

222
                if (null === $contentTypeHeader) {
223
                    return null;
224
                }
225

226
                $parts = explode(
227
                    separator: ';',
228
                    string: $contentTypeHeader,
229
                );
230

231
                return array_shift($parts);
UNCOV
232
            },
×
233
            filesize: function () use ($headers): ?int {
×
234
                $filesize = $this->getHeaderValue(
235
                    headers: $headers,
236
                    name: 'Content-Length',
237
                );
238

239
                return null !== $filesize ? (int) $filesize : null;
UNCOV
240
            },
×
241
        );
242
    }
243

244
    /**
245
     * @throws FilesystemException
246
     */
247
    private function getResourceFromLocalFile(PathInfoInterface $pathInfo, string $filename): ResourceInterface
1✔
248
    {
249
        error_clear_last();
1✔
250

251
        $source = @fopen(
1✔
252
            filename: $filename,
1✔
253
            mode: 'rb',
1✔
254
        );
255

256
        if (false === $source) {
1✔
257
            throw new FilesystemException(
1✔
258
                message: sprintf(
1✔
259
                    'Can not read stream from file "%s". %s',
1✔
260
                    $filename,
1✔
261
                    error_get_last()['message'] ?? '',
1✔
262
                ),
263
            );
264
        }
265

266
        return new StreamResource(
1✔
267
            pathInfo: $pathInfo,
1✔
268
            source: $source,
269
            mimeType: function (StreamResource $resource): ?string {
1✔
270
                $mimeType = @mime_content_type($resource->getSource());
1✔
271

272
                return false === $mimeType ? null : $mimeType;
1✔
273
            },
1✔
274
            filesize: function () use ($filename): ?int {
1✔
275
                $filesize = @filesize(
1✔
276
                    filename: $filename,
1✔
277
                );
278

279
                return false === $filesize ? null : $filesize;
1✔
280
            },
1✔
281
        );
282
    }
283

284
    /**
285
     * @param array<int, string> $headers
286
     */
287
    private function getHeaderValue(array $headers, string $name): ?string
288
    {
UNCOV
289
        $name = strtolower($name);
×
290

UNCOV
291
        foreach ($headers as $header) {
×
292
            $header = trim(strtolower($header));
×
293

UNCOV
294
            if (!str_starts_with($header, $name . ':')) {
×
295
                continue;
×
296
            }
297

UNCOV
298
            $value = substr(
×
299
                string: $header,
×
300
                offset: strlen($name) + 1,
×
301
            );
302

UNCOV
303
            return trim($value);
×
304
        }
305

UNCOV
306
        return null;
×
307
    }
308
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc