Skip to content

Commit db87f76

Browse files
committed
Throw exceptions instead of trigger_error for stream open errors
1 parent adbd8f1 commit db87f76

File tree

6 files changed

+166
-83
lines changed

6 files changed

+166
-83
lines changed

psalm-baseline.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@
9292
</MixedArgument>
9393
</file>
9494
<file src="src/GridFS/StreamWrapper.php">
95+
<InvalidArgument>
96+
<code>$context</code>
97+
<code>$context</code>
98+
</InvalidArgument>
9599
<MixedAssignment>
96100
<code>$context</code>
97101
</MixedAssignment>

src/GridFS/Bucket.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use MongoDB\Exception\UnsupportedException;
3232
use MongoDB\GridFS\Exception\CorruptFileException;
3333
use MongoDB\GridFS\Exception\FileNotFoundException;
34+
use MongoDB\GridFS\Exception\LogicException;
3435
use MongoDB\GridFS\Exception\StreamException;
3536
use MongoDB\Model\BSONArray;
3637
use MongoDB\Model\BSONDocument;
@@ -786,19 +787,21 @@ private function registerStreamWrapper(): void
786787
* gridfs://database_name/collection_name.files/file_name
787788
* @param array{revision?: int, chunkSizeBytes?: int, disableMD5?: bool} $context The options provided to fopen()
788789
*
789-
* @return array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}|null
790+
* @return array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}
791+
*
792+
* @throws FileNotFoundException
793+
* @throws LogicException
790794
*/
791-
private function resolveStreamContext(string $path, string $mode, array $context): ?array
795+
private function resolveStreamContext(string $path, string $mode, array $context): array
792796
{
793-
$parts = explode('/', $path, 4);
794-
$filename = $parts[3] ?? '';
797+
// Fallback to an empty filename if the path does not contain one: "gridfs://alias"
798+
$filename = explode('/', $path, 4)[3] ?? '';
795799

796800
if ($mode === 'r' || $mode === 'rb') {
797801
$file = $this->collectionWrapper->findFileByFilenameAndRevision($filename, $context['revision'] ?? -1);
798802

799-
// File not found
800803
if (! is_object($file)) {
801-
return null;
804+
throw FileNotFoundException::byFilenameAndRevision($filename, $context['revision'] ?? -1, $path);
802805
}
803806

804807
return [
@@ -818,6 +821,6 @@ private function resolveStreamContext(string $path, string $mode, array $context
818821
];
819822
}
820823

821-
return null;
824+
throw LogicException::openModeNotSupported($mode);
822825
}
823826
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/*
3+
* Copyright 2016-present MongoDB, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
namespace MongoDB\GridFS\Exception;
19+
20+
use MongoDB\Exception\Exception;
21+
use MongoDB\GridFS\CollectionWrapper;
22+
23+
use function get_debug_type;
24+
use function sprintf;
25+
26+
class LogicException extends \LogicException implements Exception
27+
{
28+
/**
29+
* Thrown when using an unsupported stream mode with fopen('gridfs://...', $mode).
30+
*
31+
* @internal
32+
*/
33+
public static function openModeNotSupported(string $mode): self
34+
{
35+
return new self(sprintf('Mode "%s" is not supported by "gridfs://" files. Use one of "r", "rb", "w", or "wb".', $mode));
36+
}
37+
38+
/**
39+
* Throw when an invalid "gridfs" context option is provided.
40+
*
41+
* @param mixed $context
42+
* @internal
43+
*/
44+
public static function invalidContext($context): self
45+
{
46+
return new self(sprintf('The "gridfs" context option must be an "array" or "null". Got "%s".', get_debug_type($context)));
47+
}
48+
49+
/**
50+
* Thrown when a context is provided with an incorrect collection wrapper.
51+
*
52+
* @param mixed $object
53+
* @internal
54+
*/
55+
public static function invalidContextCollectionWrapper($object): self
56+
{
57+
return new self(sprintf('Context key "gridfs[collectionWrapper]" must be an instance of %s, %s given', CollectionWrapper::class, get_debug_type($object)));
58+
}
59+
60+
/**
61+
* Throw when a bucket alias is used with global gridfs stream wrapper, but the alias is not registered.
62+
*
63+
* @internal
64+
*/
65+
public static function bucketAliasNotRegistered(string $alias): self
66+
{
67+
return new self(sprintf('GridFS stream wrapper has no bucket alias: "%s"', $alias));
68+
}
69+
}

src/GridFS/StreamWrapper.php

Lines changed: 31 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,32 @@
1919

2020
use Closure;
2121
use MongoDB\BSON\UTCDateTime;
22+
use MongoDB\GridFS\Exception\FileNotFoundException;
23+
use MongoDB\GridFS\Exception\LogicException;
2224

2325
use function assert;
24-
use function count;
2526
use function explode;
2627
use function in_array;
2728
use function is_array;
2829
use function is_integer;
29-
use function is_object;
3030
use function is_resource;
31-
use function is_string;
32-
use function sprintf;
3331
use function stream_context_get_options;
3432
use function stream_get_wrappers;
3533
use function stream_wrapper_register;
3634
use function stream_wrapper_unregister;
37-
use function trigger_error;
3835

39-
use const E_USER_ERROR;
4036
use const SEEK_CUR;
4137
use const SEEK_END;
4238
use const SEEK_SET;
4339
use const STREAM_IS_URL;
44-
use const STREAM_REPORT_ERRORS;
4540

4641
/**
4742
* Stream wrapper for reading and writing a GridFS file.
4843
*
4944
* @internal
5045
* @see Bucket::openUploadStream()
5146
* @see Bucket::openDownloadStream()
52-
* @psalm-type ContextOptions = array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}|null
47+
* @psalm-type ContextOptions = array{collectionWrapper: CollectionWrapper, file: object}|array{collectionWrapper: CollectionWrapper, filename: string, options: array}
5348
*/
5449
class StreamWrapper
5550
{
@@ -81,6 +76,18 @@ public function getFile(): object
8176
return $this->stream->getFile();
8277
}
8378

79+
/** @return false|array */
80+
public function url_stat(string $path, int $flags)
81+
{
82+
try {
83+
$this->stream_open($path, 'r', 0, $openedPath);
84+
} catch (FileNotFoundException $e) {
85+
return false;
86+
}
87+
88+
return $this->stream_stat();
89+
}
90+
8491
/**
8592
* Register the GridFS stream wrapper.
8693
*
@@ -148,74 +155,44 @@ public function stream_eof(): bool
148155
*/
149156
public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
150157
{
151-
[$protocol] = explode('://', $path, 2);
152-
153158
$context = [];
159+
160+
/**
161+
* The Bucket methods { @see Bucket::openUploadStream() } and { @see Bucket::openDownloadStreamByFile() }
162+
* always set an internal context. But the context can also be set by the user.
163+
*/
154164
if (is_resource($this->context)) {
155-
$context = stream_context_get_options($this->context)[$protocol] ?? [];
165+
$context = stream_context_get_options($this->context)['gridfs'] ?? [];
156166

157167
if (! is_array($context)) {
158-
if ($options & STREAM_REPORT_ERRORS) {
159-
trigger_error(sprintf('Invalid context for "%s" protocol.', $protocol), E_USER_ERROR);
160-
}
161-
162-
return false;
168+
throw LogicException::invalidContext($context);
163169
}
164170
}
165171

166-
// Opening stream from gridfs
172+
// When the stream is opened using fopen(), the context is not required, it can contain only options.
167173
if (! isset($context['collectionWrapper'])) {
168-
$parts = explode('/', $path, 4);
169-
170-
if (count($parts) < 4) {
171-
if ($options & STREAM_REPORT_ERRORS) {
172-
trigger_error(sprintf('Invalid GridFS file name: "%s"', $path), E_USER_ERROR);
173-
}
174+
$bucketAlias = explode('/', $path, 4)[2] ?? '';
174175

175-
return false;
176+
if (! isset(self::$contextResolvers[$bucketAlias])) {
177+
throw LogicException::bucketAliasNotRegistered($bucketAlias);
176178
}
177179

178-
$resolver = self::$contextResolvers[$parts[2]] ?? null;
179-
if (null === $resolver) {
180-
if ($options & STREAM_REPORT_ERRORS) {
181-
trigger_error(sprintf('Unknown GridFS Bucket "%1$s". Call $bucket->asStreamWrap(\'%1$s\') on the Bucket you want to access.', $parts[2]), E_USER_ERROR);
182-
}
183-
184-
return false;
185-
}
186-
187-
$context = $resolver($path, $mode, $context);
188-
if ($context === null) {
189-
if ($options & STREAM_REPORT_ERRORS) {
190-
trigger_error(sprintf('File not found "%s".', $path), E_USER_ERROR);
191-
}
192-
193-
return false;
194-
}
180+
$context = self::$contextResolvers[$bucketAlias]($path, $mode, $context);
195181
}
196182

197183
if (! $context['collectionWrapper'] instanceof CollectionWrapper) {
198-
if ($options & STREAM_REPORT_ERRORS) {
199-
trigger_error('Invalid context: "gridfs[collectionWrapper]" must be a CollectionWrapper.', E_USER_ERROR);
200-
}
201-
202-
return false;
184+
throw LogicException::invalidContextCollectionWrapper($context['collectionWrapper']);
203185
}
204186

205187
if ($mode === 'r' || $mode === 'rb') {
206-
assert(isset($context['file']) && is_object($context['file']));
207-
208188
return $this->initReadableStream($context);
209189
}
210190

211191
if ($mode === 'w' || $mode === 'wb') {
212-
assert(isset($context['filename']) && is_string($context['filename']));
213-
assert(isset($context['options']) && is_array($context['options']));
214-
215192
return $this->initWritableStream($context);
216193
}
217194

218-
return false;
195+
throw LogicException::openModeNotSupported($mode);
219196
}
220197

221198
/**
@@ -332,18 +309,6 @@ public function stream_write(string $data): int
332309
return $this->stream->writeBytes($data);
333310
}
334311

335-
/** @return false|array */
336-
public function url_stat(string $path, int $flags)
337-
{
338-
$success = $this->stream_open($path, 'r', 0, $openedPath);
339-
340-
if (! $success) {
341-
return false;
342-
}
343-
344-
return $this->stream_stat();
345-
}
346-
347312
/**
348313
* Returns a stat template with default values.
349314
*/
@@ -371,8 +336,7 @@ private function getStatTemplate(): array
371336
/**
372337
* Initialize the internal stream for reading.
373338
*
374-
* @psalm-param array{collectionWrapper: CollectionWrapper, file: object, ...} $contextOptions
375-
* @see StreamWrapper::stream_open()
339+
* @param array{collectionWrapper: CollectionWrapper, file: object} $contextOptions
376340
*/
377341
private function initReadableStream(array $contextOptions): bool
378342
{
@@ -387,8 +351,7 @@ private function initReadableStream(array $contextOptions): bool
387351
/**
388352
* Initialize the internal stream for writing.
389353
*
390-
* @psalm-param array{collectionWrapper: CollectionWrapper, filename: string, options: array, ...} $contextOptions
391-
* @see StreamWrapper::stream_open()
354+
* @param array{collectionWrapper: CollectionWrapper, filename: string, options: array} $contextOptions
392355
*/
393356
private function initWritableStream(array $contextOptions): bool
394357
{

tests/GridFS/BucketFunctionalTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -905,8 +905,8 @@ public function testDanglingOpenWritableStreamWithGlobalStreamWrapperAlias(): vo
905905
require '%s';
906906
$client = MongoDB\Tests\FunctionalTestCase::createTestClient();
907907
$database = $client->selectDatabase(getenv('MONGODB_DATABASE') ?: 'phplib_test');
908-
$database->selectGridFSBucket()->registerGlobalStreamWrapperAlias('default');
909-
$stream = fopen('gridfs://default/hello.txt', 'w');
908+
$database->selectGridFSBucket()->registerGlobalStreamWrapperAlias('alias');
909+
$stream = fopen('gridfs://alias/hello.txt', 'w');
910910
fwrite($stream, 'Hello MongoDB!');
911911
PHP;
912912

0 commit comments

Comments
 (0)