diff --git a/composer.json b/composer.json index 8c17649ff..7b901972c 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "rector/rector": "^0.16.0", "squizlabs/php_codesniffer": "^3.7", "symfony/phpunit-bridge": "^5.2", - "vimeo/psalm": "^4.28" + "vimeo/psalm": "^5.13" }, "autoload": { "psr-4": { "MongoDB\\": "src/" }, diff --git a/docs/examples/csfle-explicit_encryption.php b/docs/examples/csfle-explicit_encryption.php index ce5ccf152..b089d72b2 100644 --- a/docs/examples/csfle-explicit_encryption.php +++ b/docs/examples/csfle-explicit_encryption.php @@ -41,6 +41,8 @@ /* Using the client configured without encryption, find the document and observe * that the field is not automatically decrypted. */ + +/** @var object{encryptedField: Binary} $document */ $document = $collection->findOne(); print_r($document); diff --git a/examples/changestream.php b/examples/changestream.php index 8c871f59c..4f75aa4cd 100644 --- a/examples/changestream.php +++ b/examples/changestream.php @@ -53,7 +53,7 @@ function toJSON(object $document): string $changeStream->next(); if (time() - $startTime > 3) { - printf("Aborting after 3 seconds...\n"); + echo "Aborting after 3 seconds...\n"; break; } } diff --git a/examples/command_logger.php b/examples/command_logger.php index 59669fd42..48fec754f 100644 --- a/examples/command_logger.php +++ b/examples/command_logger.php @@ -31,14 +31,14 @@ public function commandStarted(CommandStartedEvent $event): void printf("%s command started\n", $event->getCommandName()); printf("command: %s\n", toJson($event->getCommand())); - printf("\n"); + echo "\n"; } public function commandSucceeded(CommandSucceededEvent $event): void { printf("%s command succeeded\n", $event->getCommandName()); printf("reply: %s\n", toJson($event->getReply())); - printf("\n"); + echo "\n"; } public function commandFailed(CommandFailedEvent $event): void @@ -50,7 +50,7 @@ public function commandFailed(CommandFailedEvent $event): void printf("exception: %s\n", get_class($exception)); printf("exception.code: %d\n", $exception->getCode()); printf("exception.message: %s\n", $exception->getMessage()); - printf("\n"); + echo "\n"; } } diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 27dc4743b..e6b0a3436 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,381 +1,190 @@ - - - - new Binary(random_bytes(96)) - - - - - new Binary(random_bytes(96)) - - - - - new Binary(random_bytes(96)) - - + - - $clientEncryption->decrypt($document->encryptedField) - $document->encryptedField - - - $document->encryptedField - - - $document->encryptedField - - - $document->encryptedField - - - new Binary(random_bytes(96)) - - - - - new Binary(random_bytes(96)) - - - - - new Binary(random_bytes(96)) - - - - - new Binary(random_bytes(96)) - - - - - new Binary(random_bytes(96)) - - - - - $address - $emails - $id - $name - $type - + + decrypt($document->encryptedField)]]> + - + self::CURSOR_NOT_FOUND - - $driverOptions['driver'] ?? [] + + - - $mergedDriver['platform'] + + - + ($value is BSONType ? NativeType : $value) - + ($value is NativeType ? BSONType : $value) - + $cmd[$option] - $options['session'] + - + $cmd[$option] - $options['session'] - - - - - new static(sprintf('%s is immutable', $class)) - new static(sprintf('%s should not be called for an unacknowledged write result', $method)) - - - - - new static(sprintf('Expected %s to have type "%s" but found "%s"', $name, $expectedType, get_debug_type($value))) - - - - - new static('Resume token not found in change document') - new static(sprintf('Expected resume token to have type "array or object" but found "%s"', get_debug_type($value))) - - - - - new static('Array filters are not supported by the server executing this operation') - new static('Collations are not supported by the server executing this operation') - new static('Explain is not supported by the server executing this operation') - new static('Hint is not supported by the server executing this operation') - new static('Read concern is not supported by the server executing this command') - new static('The "allowDiskUse" option is not supported by the server executing this operation') - new static('The "commitQuorum" option is not supported by the server executing this operation') - new static('The "readConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.') - new static('The "writeConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.') - new static('Write concern is not supported by the server executing this command') - + + - - ! is_resource($destination) - ! is_resource($destination) - ! is_resource($source) - ! is_resource($stream) - - - delete - downloadToStream - downloadToStreamByName - drop - rename - - - $options['revision'] - $options['revision'] - - - - - new static(sprintf('Chunk not found for index "%d"', $expectedIndex)) - new static(sprintf('Expected chunk to have index "%d" but found "%d"', $expectedIndex, $index)) - new static(sprintf('Expected chunk to have size "%d" but found "%d"', $expectedSize, $size)) - new static(sprintf('Invalid data found for index "%d"', $chunkIndex)) - - - - - new static(sprintf('File "%s" not found in "%s"', $json, $namespace)) - new static(sprintf('File with name "%s" and revision "%d" not found in "%s"', $filename, $revision, $namespace)) - - - - - new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $filename)) - new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS identifier: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $idString)) - new static(sprintf('Uploading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationUri, $filename)) - + + + + - - $currentChunk->n - $this->file->length + + n]]> + file->length]]> - - $context[$this->protocol]['collectionWrapper'] - $context[$this->protocol]['collectionWrapper'] - $context[$this->protocol]['file'] - $context[$this->protocol]['filename'] - $context[$this->protocol]['options'] - - - $context[$this->protocol]['collectionWrapper'] - $context[$this->protocol]['collectionWrapper'] - $context[$this->protocol]['file'] - $context[$this->protocol]['filename'] - $context[$this->protocol]['options'] + + protocol]['collectionWrapper']]]> + protocol]['collectionWrapper']]]> + protocol]['file']]]> + protocol]['filename']]]> + protocol]['options']]]> + + + protocol]['collectionWrapper']]]> + protocol]['collectionWrapper']]]> + protocol]['file']]]> + protocol]['filename']]]> + protocol]['options']]]> - - stream_wrapper_unregister - - - - - new Binary($data) - - - hash_update - - - - - Traversable - - - call_user_func($this->getIterator) - - - $this[$key] - - - $key + $this[$key] $value - - new static() - - - $iteratorClass - - - $this[$key] - - - $key + + >|class-string>]]> + + $this[$key] $value - - new static() - - + $documentLength $documentLength - $this->options['typeMap'] + options['typeMap']]]> - - $this->position + + position]]> - + $documentLength - - - $currentItem[self::FIELD_KEY] - $currentItem[self::FIELD_VALUE] - - - $currentItem - $currentItem - - - - $reply->cursor->nextBatch - $this->current() + + cursor->nextBatch]]> - - $this->postBatchResumeToken + + postBatchResumeToken]]> - - $reply->cursor->nextBatch - $reply->cursor->postBatchResumeToken + + cursor->nextBatch]]> + cursor->postBatchResumeToken]]> - - addSubscriber - removeSubscriber - + + current()]]> + - + $key - - $this->info[$key] + + info[$key]]]> - - $info - - - $info['idIndex']['ns'] + + - - $info - - - $info['name'] + + - + $key - - $this->info[$key] + + info[$key]]]> - - current($this->databases) + + databases)]]> - + int - key($this->databases) + databases)]]> - + $key - - $this->info[$key] + + info[$key]]]> - - - $info - $info - - - $info['ns'] - - - $info - - - + $fieldName - $index['key'] + - + $fieldName $type - + $type - - $this->index['name'] + + index['name']]]> - - $this->options['typeMap'] - $this->options['typeMap'] + + options['typeMap']]]> - - $cmdOptions['maxAwaitTimeMS'] + + $cmd[$option] - $cmd['hint'] - $cmd['readConcern'] + + $options[$option] - - isDefault - isDefault + isInTransaction - - is_array($operation) - - + $args $args $args @@ -390,7 +199,7 @@ $args[1] $args[2] - + $args[0] $args[0] $args[0] @@ -417,26 +226,29 @@ $args[2] $args[2] $args[2] - $args[2]['upsert'] - $args[2]['upsert'] - $args[2]['upsert'] - $args[2]['upsert'] + + + + - + $args[1] $args[1] $args[1] + $args[2] $args[2] $args[2] $args[2] $args[2] $args[2] + + $operations[$i][$type][1] $operations[$i][$type][2] $operations[$i][$type][2] - + $args $args $args[2] @@ -446,398 +258,341 @@ $operations[$i][$type][2] $operations[$i][$type][2] $options[$option] - $options['session'] - $options['writeConcern'] + + - - isDefault + isInTransaction - + $args[2] $args[2] - + $cmd[$option] - $cmd['hint'] - $cmd['readConcern'] - $options['readConcern'] - $options['readPreference'] - $options['session'] + + + + + - + isInTransaction - - $options['pipeline'] - $this->options['typeMap'] + + options['typeMap']]]> - + $cmd[$option] - $options['session'] - $options['writeConcern'] + + - - $options['encryptedFields'] - $this->options['encryptedFields'] + + + options['encryptedFields']]]> - - is_array($index) - - + $cmd[$option] - $cmd['commitQuorum'] - $options['session'] - $options['writeConcern'] + + + - + isInTransaction - - $this->options['typeMap'] + + options['typeMap']]]> - - $options['readPreference'] - $options['session'] + + + - - $this->options['writeConcern'] + + options['writeConcern']]]> - - $cmd['comment'] - $deleteOptions['hint'] - $options['comment'] - $options['session'] - $options['writeConcern'] + + + + + + - + isInTransaction - - $this->options['typeMap'] + + options['typeMap']]]> - + $cmd[$option] - $cmd['readConcern'] - $options['readConcern'] - $options['readPreference'] - $options['session'] + + + + - + isInTransaction - - $this->options['typeMap'] + + options['typeMap']]]> - - $cmd['comment'] - $options['session'] - $options['writeConcern'] + + + + - + isInTransaction - - $this->options['typeMap'] + + options['typeMap']]]> - - $cmd['comment'] - $options['session'] - $options['writeConcern'] + + + + - - $options['encryptedFields'] + + - - $this->options['typeMap'] + + options['typeMap']]]> - + $cmd[$option] - $options['session'] - $options['writeConcern'] + + - + isInTransaction - - $this->options['typeMap'] + + options['typeMap']]]> - + $cmd[$option] - $options['readPreference'] - $options['session'] + + - - $this->options['typeMap'] + + options['typeMap']]]> - - $options['modifiers'][$modifier[1]] + + - + $options[$modifier[0]] $options[$option] - $options['readPreference'] - $options['session'] + + - + isInTransaction - - $this->options['typeMap'] - $this->options['writeConcern'] + + options['typeMap']]]> + options['writeConcern']]]> - + $cmd[$option] - $cmd['new'] - $cmd['upsert'] - $options['session'] - $options['writeConcern'] + + + + - + array|object|null - - isDefault + isInTransaction - - is_object($result) ? ($result->value ?? null) : null + + value ?? null) : null]]> + value ?? null) : null]]> - - $options['fields'] + + - - $options['fields'] + + - - $options['returnDocument'] - - - ! is_array($update) && ! is_object($update) - - - $options['fields'] - - - $options['returnDocument'] - + + + - + $insertedIds[$i] $options[$option] - $options['session'] - $options['writeConcern'] + + - - isDefault + isInTransaction - + $insertedId $options[$option] - $options['session'] - $options['writeConcern'] + + - - isDefault + isInTransaction - - - function (array $collectionInfo) { - - - + $cmd[$option] - $options['session'] + - - ! is_string($out) && ! is_array($out) && ! is_object($out) - - - $result->result->collection - $result->result->db - $this->options['typeMap'] - - + + result->collection]]> + result->db]]> + options['typeMap']]]> + + $cmd[$option] - $options['readConcern'] - $options['readPreference'] - $options['session'] - $options['writeConcern'] - - - getScope - isDefault - isDefault + + + + + + isInTransaction - - $this->options['typeMap'] + + options['typeMap']]]> - - $cmd['comment'] - $options['session'] - $options['writeConcern'] + + + + - - $this->options['typeMap'] + + options['typeMap']]]> - + $cmd[$option] - $options['session'] - $options['writeConcern'] + + - + isInTransaction - - ! is_array($update) && ! is_object($update) - - - $this->options['writeConcern'] - - - $cmd['bypassDocumentValidation'] + + options['writeConcern']]]> + + + $options[$option] - $options['session'] - $options['writeConcern'] + + $updateOptions[$option] - - isDefault + isInTransaction - - - ! is_array($update) && ! is_object($update) - - - - - ! is_array($update) && ! is_object($update) - - - - isset($resumeToken) && ! is_array($resumeToken) && ! is_object($resumeToken) - - - $reply->cursor->firstBatch + + cursor->firstBatch]]> - - $this->postBatchResumeToken + + postBatchResumeToken]]> - + array|object|null - - $reply->cursor->firstBatch - $reply->cursor->postBatchResumeToken + + cursor->firstBatch]]> + cursor->postBatchResumeToken]]> - - $this->changeStreamOptions['resumeAfter'] - $this->changeStreamOptions['startAfter'] + + changeStreamOptions['resumeAfter']]]> + changeStreamOptions['startAfter']]]> - - $firstBatchSize - $operationTime - - - $resumeToken === null && $this->operationTime !== null - $this->operationTime !== null - - - addSubscriber - removeSubscriber - - + $id - - is_array($document) - - + $stage $wireVersionForWriteStageOnSecondary - - $manager->getServers() + + getServers()]]> - - $collectionInfo['options']['encryptedFields'] + + - - $typeMap['fieldPaths'][$fieldPath] - $typeMap['fieldPaths'][substr($fieldPath, 0, -2)] + + + - + $element[$key] $stage $type - $typeMap['fieldPaths'][$fieldPath . '.' . $existingFieldPath] - $typeMap['fieldPaths'][$fieldPath] + + $value - + array|object|null array|object|null - - $collectionInfo['options']['encryptedFields'] ?? null - $encryptedFieldsMap[$databaseName . '.' . $collectionName] ?? null + + + + + diff --git a/psalm.xml.dist b/psalm.xml.dist index 7ca91ba36..27e104118 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -3,6 +3,8 @@ errorLevel="1" errorBaseline="psalm-baseline.xml" resolveFromConfigFile="true" + findUnusedBaselineEntry="true" + findUnusedCode="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" @@ -15,9 +17,22 @@ + + + + + + + + + + + + + diff --git a/src/ChangeStream.php b/src/ChangeStream.php index e94160874..6fc3bbaa1 100644 --- a/src/ChangeStream.php +++ b/src/ChangeStream.php @@ -37,6 +37,7 @@ * @see https://mongodb.com/docs/manual/reference/method/db.watch/#mongodb-method-db.watch * * @psalm-type ResumeCallable = callable(array|object|null, bool): ChangeStreamIterator + * @template-implements Iterator */ class ChangeStream implements Iterator { @@ -94,7 +95,7 @@ public function __construct(ChangeStreamIterator $iterator, callable $resumeCall /** * @see https://php.net/iterator.current - * @return mixed + * @return array|object|null */ #[ReturnTypeWillChange] public function current() @@ -124,7 +125,7 @@ public function getResumeToken() /** * @see https://php.net/iterator.key - * @return mixed + * @return int|null */ #[ReturnTypeWillChange] public function key() diff --git a/src/Codec/CodecLibrary.php b/src/Codec/CodecLibrary.php index d3cbf5461..92aa8a9dc 100644 --- a/src/Codec/CodecLibrary.php +++ b/src/Codec/CodecLibrary.php @@ -20,9 +20,12 @@ use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnsupportedValueException; +/** @template-implements Codec */ class CodecLibrary implements Codec { + /** @template-use DecodeIfSupported */ use DecodeIfSupported; + /** @template-use EncodeIfSupported */ use EncodeIfSupported; /** @var list */ diff --git a/src/Command/ListCollections.php b/src/Command/ListCollections.php index 66db017b8..857b35254 100644 --- a/src/Command/ListCollections.php +++ b/src/Command/ListCollections.php @@ -18,6 +18,7 @@ namespace MongoDB\Command; use MongoDB\Driver\Command; +use MongoDB\Driver\Cursor; use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException; use MongoDB\Driver\Server; use MongoDB\Driver\Session; @@ -99,11 +100,13 @@ public function __construct(string $databaseName, array $options = []) /** * Execute the operation. * + * @return CachingIterator * @see Executable::execute() * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ public function execute(Server $server): CachingIterator { + /** @var Cursor $cursor */ $cursor = $server->executeReadCommand($this->databaseName, $this->createCommand(), $this->createOptions()); $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php index b140ff3ae..e1070982a 100644 --- a/src/Exception/BadMethodCallException.php +++ b/src/Exception/BadMethodCallException.php @@ -31,7 +31,7 @@ class BadMethodCallException extends BaseBadMethodCallException implements Excep */ public static function classIsImmutable(string $class) { - return new static(sprintf('%s is immutable', $class)); + return new self(sprintf('%s is immutable', $class)); } /** @@ -42,6 +42,6 @@ public static function classIsImmutable(string $class) */ public static function unacknowledgedWriteResultAccess(string $method) { - return new static(sprintf('%s should not be called for an unacknowledged write result', $method)); + return new self(sprintf('%s should not be called for an unacknowledged write result', $method)); } } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 85ab352d7..fd043651c 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -54,7 +54,7 @@ public static function invalidType(string $name, $value, $expectedType) $expectedType = self::expectedTypesToString($expectedType); } - return new static(sprintf('Expected %s to have type "%s" but found "%s"', $name, $expectedType, get_debug_type($value))); + return new self(sprintf('Expected %s to have type "%s" but found "%s"', $name, $expectedType, get_debug_type($value))); } /** @param list $types */ diff --git a/src/Exception/ResumeTokenException.php b/src/Exception/ResumeTokenException.php index 4d3b23188..ce7a35c71 100644 --- a/src/Exception/ResumeTokenException.php +++ b/src/Exception/ResumeTokenException.php @@ -30,7 +30,7 @@ class ResumeTokenException extends RuntimeException */ public static function invalidType($value) { - return new static(sprintf('Expected resume token to have type "array or object" but found "%s"', get_debug_type($value))); + return new self(sprintf('Expected resume token to have type "array or object" but found "%s"', get_debug_type($value))); } /** @@ -40,6 +40,6 @@ public static function invalidType($value) */ public static function notFound() { - return new static('Resume token not found in change document'); + return new self('Resume token not found in change document'); } } diff --git a/src/Exception/UnsupportedException.php b/src/Exception/UnsupportedException.php index 51d3c84b8..2bf910a09 100644 --- a/src/Exception/UnsupportedException.php +++ b/src/Exception/UnsupportedException.php @@ -26,7 +26,7 @@ class UnsupportedException extends RuntimeException */ public static function allowDiskUseNotSupported() { - return new static('The "allowDiskUse" option is not supported by the server executing this operation'); + return new self('The "allowDiskUse" option is not supported by the server executing this operation'); } /** @@ -39,7 +39,7 @@ public static function allowDiskUseNotSupported() */ public static function arrayFiltersNotSupported() { - return new static('Array filters are not supported by the server executing this operation'); + return new self('Array filters are not supported by the server executing this operation'); } /** @@ -52,7 +52,7 @@ public static function arrayFiltersNotSupported() */ public static function collationNotSupported() { - return new static('Collations are not supported by the server executing this operation'); + return new self('Collations are not supported by the server executing this operation'); } /** @@ -63,7 +63,7 @@ public static function collationNotSupported() */ public static function commitQuorumNotSupported() { - return new static('The "commitQuorum" option is not supported by the server executing this operation'); + return new self('The "commitQuorum" option is not supported by the server executing this operation'); } /** @@ -73,7 +73,7 @@ public static function commitQuorumNotSupported() */ public static function explainNotSupported() { - return new static('Explain is not supported by the server executing this operation'); + return new self('Explain is not supported by the server executing this operation'); } /** @@ -83,7 +83,7 @@ public static function explainNotSupported() */ public static function hintNotSupported() { - return new static('Hint is not supported by the server executing this operation'); + return new self('Hint is not supported by the server executing this operation'); } /** @@ -93,7 +93,7 @@ public static function hintNotSupported() */ public static function readConcernNotSupported() { - return new static('Read concern is not supported by the server executing this command'); + return new self('Read concern is not supported by the server executing this command'); } /** @@ -103,7 +103,7 @@ public static function readConcernNotSupported() */ public static function readConcernNotSupportedInTransaction() { - return new static('The "readConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.'); + return new self('The "readConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.'); } /** @@ -113,7 +113,7 @@ public static function readConcernNotSupportedInTransaction() */ public static function writeConcernNotSupported() { - return new static('Write concern is not supported by the server executing this command'); + return new self('Write concern is not supported by the server executing this command'); } /** @@ -123,6 +123,6 @@ public static function writeConcernNotSupported() */ public static function writeConcernNotSupportedInTransaction() { - return new static('The "writeConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.'); + return new self('The "writeConcern" option cannot be specified within a transaction. Instead, specify it when starting the transaction.'); } } diff --git a/src/GridFS/Bucket.php b/src/GridFS/Bucket.php index 9b46d7b91..96c858535 100644 --- a/src/GridFS/Bucket.php +++ b/src/GridFS/Bucket.php @@ -205,6 +205,7 @@ public function __debugInfo() * attempt to delete orphaned chunks. * * @param mixed $id File ID + * @return void * @throws FileNotFoundException if no file could be selected * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ @@ -223,6 +224,7 @@ public function delete($id) * * @param mixed $id File ID * @param resource $destination Writable Stream + * @return void * @throws FileNotFoundException if no file could be selected * @throws InvalidArgumentException if $destination is not a stream * @throws StreamException if the file could not be uploaded @@ -262,6 +264,7 @@ public function downloadToStream($id, $destination) * @param string $filename Filename * @param resource $destination Writable Stream * @param array $options Download options + * @return void * @throws FileNotFoundException if no file could be selected * @throws InvalidArgumentException if $destination is not a stream * @throws StreamException if the file could not be uploaded @@ -283,6 +286,7 @@ public function downloadToStreamByName(string $filename, $destination, array $op * Drops the files and chunks collections associated with this GridFS * bucket. * + * @return void * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ public function drop() @@ -560,6 +564,7 @@ public function openUploadStream(string $filename, array $options = []) * * @param mixed $id File ID * @param string $newFilename New filename + * @return void * @throws FileNotFoundException if no file could be selected * @throws DriverRuntimeException for other driver errors (e.g. connection errors) */ diff --git a/src/GridFS/Exception/CorruptFileException.php b/src/GridFS/Exception/CorruptFileException.php index e31d7daeb..db1622908 100644 --- a/src/GridFS/Exception/CorruptFileException.php +++ b/src/GridFS/Exception/CorruptFileException.php @@ -28,7 +28,7 @@ class CorruptFileException extends RuntimeException */ public static function invalidChunkData(int $chunkIndex): self { - return new static(sprintf('Invalid data found for index "%d"', $chunkIndex)); + return new self(sprintf('Invalid data found for index "%d"', $chunkIndex)); } /** @@ -39,7 +39,7 @@ public static function invalidChunkData(int $chunkIndex): self */ public static function missingChunk(int $expectedIndex) { - return new static(sprintf('Chunk not found for index "%d"', $expectedIndex)); + return new self(sprintf('Chunk not found for index "%d"', $expectedIndex)); } /** @@ -51,7 +51,7 @@ public static function missingChunk(int $expectedIndex) */ public static function unexpectedIndex(int $index, int $expectedIndex) { - return new static(sprintf('Expected chunk to have index "%d" but found "%d"', $expectedIndex, $index)); + return new self(sprintf('Expected chunk to have index "%d" but found "%d"', $expectedIndex, $index)); } /** @@ -63,6 +63,6 @@ public static function unexpectedIndex(int $index, int $expectedIndex) */ public static function unexpectedSize(int $size, int $expectedSize) { - return new static(sprintf('Expected chunk to have size "%d" but found "%d"', $expectedSize, $size)); + return new self(sprintf('Expected chunk to have size "%d" but found "%d"', $expectedSize, $size)); } } diff --git a/src/GridFS/Exception/FileNotFoundException.php b/src/GridFS/Exception/FileNotFoundException.php index 5d0f5d5c5..14d4dceb7 100644 --- a/src/GridFS/Exception/FileNotFoundException.php +++ b/src/GridFS/Exception/FileNotFoundException.php @@ -35,7 +35,7 @@ class FileNotFoundException extends RuntimeException */ public static function byFilenameAndRevision(string $filename, int $revision, string $namespace) { - return new static(sprintf('File with name "%s" and revision "%d" not found in "%s"', $filename, $revision, $namespace)); + return new self(sprintf('File with name "%s" and revision "%d" not found in "%s"', $filename, $revision, $namespace)); } /** @@ -49,6 +49,6 @@ public static function byId($id, string $namespace) { $json = toJSON(fromPHP(['_id' => $id])); - return new static(sprintf('File "%s" not found in "%s"', $json, $namespace)); + return new self(sprintf('File "%s" not found in "%s"', $json, $namespace)); } } diff --git a/src/GridFS/Exception/StreamException.php b/src/GridFS/Exception/StreamException.php index 241f1b9a4..e60d8c23a 100644 --- a/src/GridFS/Exception/StreamException.php +++ b/src/GridFS/Exception/StreamException.php @@ -20,7 +20,7 @@ public static function downloadFromFilenameFailed(string $filename, $source, $de $sourceMetadata = stream_get_meta_data($source); $destinationMetadata = stream_get_meta_data($destination); - return new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $filename)); + return new self(sprintf('Downloading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $filename)); } /** @@ -34,7 +34,7 @@ public static function downloadFromIdFailed($id, $source, $destination): self $sourceMetadata = stream_get_meta_data($source); $destinationMetadata = stream_get_meta_data($destination); - return new static(sprintf('Downloading file from "%s" to "%s" failed. GridFS identifier: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $idString)); + return new self(sprintf('Downloading file from "%s" to "%s" failed. GridFS identifier: "%s"', $sourceMetadata['uri'], $destinationMetadata['uri'], $idString)); } /** @param resource $source */ @@ -42,6 +42,6 @@ public static function uploadFailed(string $filename, $source, string $destinati { $sourceMetadata = stream_get_meta_data($source); - return new static(sprintf('Uploading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationUri, $filename)); + return new self(sprintf('Uploading file from "%s" to "%s" failed. GridFS filename: "%s"', $sourceMetadata['uri'], $destinationUri, $filename)); } } diff --git a/src/MapReduceResult.php b/src/MapReduceResult.php index 61f5d2667..da2b69b3a 100644 --- a/src/MapReduceResult.php +++ b/src/MapReduceResult.php @@ -33,10 +33,15 @@ * * @see \MongoDB\Collection::mapReduce() * @see https://mongodb.com/docs/manual/reference/command/mapReduce/ + * @template-implements IteratorAggregate + * @psalm-type MapReduceCallable = callable(): Traversable */ class MapReduceResult implements IteratorAggregate { - /** @var callable */ + /** + * @var callable + * @psalm-var MapReduceCallable + */ private $getIterator; private int $executionTimeMS; @@ -49,6 +54,7 @@ class MapReduceResult implements IteratorAggregate * @internal * @param callable $getIterator Callback that returns a Traversable for mapReduce results * @param stdClass $result Result document from the mapReduce command + * @psalm-param MapReduceCallable $getIterator */ public function __construct(callable $getIterator, stdClass $result) { @@ -82,7 +88,7 @@ public function getExecutionTimeMS() * Return the mapReduce results as a Traversable. * * @see https://php.net/iteratoraggregate.getiterator - * @return Traversable + * @return Traversable */ #[ReturnTypeWillChange] public function getIterator() diff --git a/src/Model/BSONArray.php b/src/Model/BSONArray.php index c10d05faf..f055f4cb7 100644 --- a/src/Model/BSONArray.php +++ b/src/Model/BSONArray.php @@ -31,6 +31,8 @@ * * The internal data will be filtered through array_values() during BSON * serialization to ensure that it becomes a BSON array. + * + * @template-extends ArrayObject */ class BSONArray extends ArrayObject implements JsonSerializable, Serializable, Unserializable { @@ -53,7 +55,7 @@ public function __clone() */ public static function __set_state(array $properties) { - $array = new static(); + $array = new self(); $array->exchangeArray($properties); return $array; @@ -78,12 +80,12 @@ public function bsonSerialize() * Unserialize the document to BSON. * * @see https://php.net/mongodb-bson-unserializable.bsonunserialize - * @param array $data Array data + * @param array $data Array data */ #[ReturnTypeWillChange] public function bsonUnserialize(array $data) { - self::__construct($data); + parent::__construct($data); } /** diff --git a/src/Model/BSONDocument.php b/src/Model/BSONDocument.php index 56b7787bb..72d2f4bfb 100644 --- a/src/Model/BSONDocument.php +++ b/src/Model/BSONDocument.php @@ -17,6 +17,7 @@ namespace MongoDB\Model; +use ArrayIterator; use ArrayObject; use JsonSerializable; use MongoDB\BSON\Serializable; @@ -30,6 +31,8 @@ * * The internal data will be cast to an object during BSON serialization to * ensure that it becomes a BSON document. + * + * @template-extends ArrayObject */ class BSONDocument extends ArrayObject implements JsonSerializable, Serializable, Unserializable { @@ -48,8 +51,10 @@ public function __clone() * by default. * * @see https://php.net/arrayobject.construct + * @param array $input + * @psalm-param class-string>|class-string> $iteratorClass */ - public function __construct(array $input = [], int $flags = ArrayObject::ARRAY_AS_PROPS, string $iteratorClass = 'ArrayIterator') + public function __construct(array $input = [], int $flags = ArrayObject::ARRAY_AS_PROPS, string $iteratorClass = ArrayIterator::class) { parent::__construct($input, $flags, $iteratorClass); } @@ -63,7 +68,7 @@ public function __construct(array $input = [], int $flags = ArrayObject::ARRAY_A */ public static function __set_state(array $properties) { - $document = new static(); + $document = new self(); $document->exchangeArray($properties); return $document; @@ -85,7 +90,7 @@ public function bsonSerialize() * Unserialize the document to BSON. * * @see https://php.net/mongodb-bson-unserializable.bsonunserialize - * @param array $data Array data + * @param array $data Array data */ #[ReturnTypeWillChange] public function bsonUnserialize(array $data) diff --git a/src/Model/BSONIterator.php b/src/Model/BSONIterator.php index 1f630c669..9f6501ec8 100644 --- a/src/Model/BSONIterator.php +++ b/src/Model/BSONIterator.php @@ -31,6 +31,8 @@ /** * Iterator for BSON documents. + * + * @template-implements Iterator */ class BSONIterator implements Iterator { @@ -89,7 +91,7 @@ public function current() /** * @see https://php.net/iterator.key - * @return mixed + * @return int */ #[ReturnTypeWillChange] public function key() diff --git a/src/Model/CachingIterator.php b/src/Model/CachingIterator.php index 78c9a3626..15ba7ca1a 100644 --- a/src/Model/CachingIterator.php +++ b/src/Model/CachingIterator.php @@ -36,14 +36,19 @@ * those operations (e.g. MongoDB\Driver\Cursor). * * @internal + * @template TKey of array-key + * @template TValue + * @template-implements Iterator */ class CachingIterator implements Countable, Iterator { private const FIELD_KEY = 0; private const FIELD_VALUE = 1; + /** @var list */ private array $items = []; + /** @var Iterator */ private Iterator $iterator; private bool $iteratorAdvanced = false; @@ -56,7 +61,7 @@ class CachingIterator implements Countable, Iterator * Additionally, this mimics behavior of the SPL iterators and allows users * to omit an explicit call to rewind() before using the other methods. * - * @param Traversable $traversable + * @param Traversable $traversable */ public function __construct(Traversable $traversable) { @@ -83,12 +88,13 @@ public function current() { $currentItem = current($this->items); - return $currentItem !== false ? $currentItem[self::FIELD_VALUE] : false; + return $currentItem !== false ? $currentItem[self::FIELD_VALUE] : null; } /** * @see https://php.net/iterator.key * @return mixed + * @psalm-return TKey|null */ #[ReturnTypeWillChange] public function key() diff --git a/src/Model/ChangeStreamIterator.php b/src/Model/ChangeStreamIterator.php index b089b09f5..bf972a163 100644 --- a/src/Model/ChangeStreamIterator.php +++ b/src/Model/ChangeStreamIterator.php @@ -46,6 +46,8 @@ * rewind() do not execute getMore commands. * * @internal + * @template TValue of array|object + * @template-extends IteratorIterator> */ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber { @@ -67,6 +69,7 @@ class ChangeStreamIterator extends IteratorIterator implements CommandSubscriber /** * @internal * @param array|object|null $initialResumeToken + * @psalm-param Cursor $cursor */ public function __construct(Cursor $cursor, int $firstBatchSize, $initialResumeToken, ?object $postBatchResumeToken) { @@ -122,12 +125,13 @@ final public function commandSucceeded(CommandSucceededEvent $event): void /** * @see https://php.net/iteratoriterator.current - * @return mixed + * @return array|object|null + * @psalm-return TValue|null */ #[ReturnTypeWillChange] public function current() { - return $this->isValid ? parent::current() : null; + return $this->valid() ? parent::current() : null; } /** @@ -168,12 +172,12 @@ public function getServer(): Server /** * @see https://php.net/iteratoriterator.key - * @return mixed + * @return int|null */ #[ReturnTypeWillChange] public function key() { - return $this->isValid ? parent::key() : null; + return $this->valid() ? parent::key() : null; } /** @see https://php.net/iteratoriterator.rewind */ @@ -213,7 +217,10 @@ public function rewind(): void $this->onIteration(false); } - /** @see https://php.net/iteratoriterator.valid */ + /** + * @see https://php.net/iteratoriterator.valid + * @psalm-assert-if-true TValue $this->current() + */ public function valid(): bool { return $this->isValid; @@ -276,11 +283,11 @@ private function onIteration(bool $incrementBatchPosition): void /* Disable rewind()'s NOP behavior once we advance to a valid position. * This will allow the driver to throw a LogicException if rewind() is * called after the cursor has advanced past its first element. */ - if ($this->isRewindNop && $this->isValid) { + if ($this->isRewindNop && $this->valid()) { $this->isRewindNop = false; } - if ($incrementBatchPosition && $this->isValid) { + if ($incrementBatchPosition && $this->valid()) { $this->batchPosition++; } @@ -292,7 +299,7 @@ private function onIteration(bool $incrementBatchPosition): void * from the current document if possible. */ if ($this->isAtEndOfBatch() && $this->postBatchResumeToken !== null) { $this->resumeToken = $this->postBatchResumeToken; - } elseif ($this->isValid) { + } elseif ($this->valid()) { $this->resumeToken = $this->extractResumeToken($this->current()); } } diff --git a/src/Model/CollectionInfo.php b/src/Model/CollectionInfo.php index f6481a78c..ad5fdf8c7 100644 --- a/src/Model/CollectionInfo.php +++ b/src/Model/CollectionInfo.php @@ -32,6 +32,7 @@ * * @see \MongoDB\Database::listCollections() * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst + * @template-implements ArrayAccess */ class CollectionInfo implements ArrayAccess { diff --git a/src/Model/CollectionInfoCommandIterator.php b/src/Model/CollectionInfoCommandIterator.php index b0d4e0118..9a70b4c1d 100644 --- a/src/Model/CollectionInfoCommandIterator.php +++ b/src/Model/CollectionInfoCommandIterator.php @@ -30,11 +30,13 @@ * @see \MongoDB\Database::listCollections() * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst * @see https://mongodb.com/docs/manual/reference/command/listCollections/ + * @template-extends IteratorIterator> */ class CollectionInfoCommandIterator extends IteratorIterator implements CollectionInfoIterator { private ?string $databaseName = null; + /** @param Traversable $iterator */ public function __construct(Traversable $iterator, ?string $databaseName = null) { parent::__construct($iterator); diff --git a/src/Model/CollectionInfoIterator.php b/src/Model/CollectionInfoIterator.php index 87315d20a..1a68cf905 100644 --- a/src/Model/CollectionInfoIterator.php +++ b/src/Model/CollectionInfoIterator.php @@ -26,6 +26,7 @@ * This iterator is used for enumerating collections in a database. * * @see \MongoDB\Database::listCollections() + * @template-extends Iterator */ interface CollectionInfoIterator extends Iterator { diff --git a/src/Model/DatabaseInfo.php b/src/Model/DatabaseInfo.php index 4d4e57667..534481f6c 100644 --- a/src/Model/DatabaseInfo.php +++ b/src/Model/DatabaseInfo.php @@ -31,6 +31,7 @@ * * @see \MongoDB\Client::listDatabases() * @see https://mongodb.com/docs/manual/reference/command/listDatabases/ + * @template-implements ArrayAccess */ class DatabaseInfo implements ArrayAccess { diff --git a/src/Model/DatabaseInfoIterator.php b/src/Model/DatabaseInfoIterator.php index e135de368..b6052e8e5 100644 --- a/src/Model/DatabaseInfoIterator.php +++ b/src/Model/DatabaseInfoIterator.php @@ -26,6 +26,7 @@ * This iterator is used for enumerating databases on a server. * * @see \MongoDB\Client::listDatabases() + * @template-extends Iterator */ interface DatabaseInfoIterator extends Iterator { diff --git a/src/Model/IndexInfo.php b/src/Model/IndexInfo.php index 74f230254..0c36dd4d5 100644 --- a/src/Model/IndexInfo.php +++ b/src/Model/IndexInfo.php @@ -40,6 +40,7 @@ * @see \MongoDB\Collection::listIndexes() * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst * @see https://mongodb.com/docs/manual/reference/method/db.collection.createIndex/ + * @template-implements ArrayAccess */ class IndexInfo implements ArrayAccess { diff --git a/src/Model/IndexInfoIterator.php b/src/Model/IndexInfoIterator.php index 26e2d3d30..af6750e00 100644 --- a/src/Model/IndexInfoIterator.php +++ b/src/Model/IndexInfoIterator.php @@ -26,6 +26,7 @@ * This iterator is used for enumerating indexes in a collection. * * @see \MongoDB\Collection::listIndexes() + * @template-extends Iterator */ interface IndexInfoIterator extends Iterator { diff --git a/src/Model/IndexInfoIteratorIterator.php b/src/Model/IndexInfoIteratorIterator.php index 09537c046..3bbbcd53f 100644 --- a/src/Model/IndexInfoIteratorIterator.php +++ b/src/Model/IndexInfoIteratorIterator.php @@ -34,11 +34,13 @@ * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst * @see https://mongodb.com/docs/manual/reference/command/listIndexes/ * @see https://mongodb.com/docs/manual/reference/system-collections/ + * @template-extends IteratorIterator> */ class IndexInfoIteratorIterator extends IteratorIterator implements IndexInfoIterator { private ?string $ns = null; + /** @param Traversable $iterator */ public function __construct(Traversable $iterator, ?string $ns = null) { parent::__construct($iterator); diff --git a/src/Operation/ListIndexes.php b/src/Operation/ListIndexes.php index 041cc7c44..1a5723d70 100644 --- a/src/Operation/ListIndexes.php +++ b/src/Operation/ListIndexes.php @@ -144,6 +144,9 @@ private function executeCommand(Server $server): IndexInfoIteratorIterator $cursor->setTypeMap(['root' => 'array', 'document' => 'array']); - return new IndexInfoIteratorIterator(new CachingIterator($cursor), $this->databaseName . '.' . $this->collectionName); + /** @var CachingIterator $iterator */ + $iterator = new CachingIterator($cursor); + + return new IndexInfoIteratorIterator($iterator, $this->databaseName . '.' . $this->collectionName); } } diff --git a/src/Operation/MapReduce.php b/src/Operation/MapReduce.php index 364ea52e2..211b20176 100644 --- a/src/Operation/MapReduce.php +++ b/src/Operation/MapReduce.php @@ -52,6 +52,7 @@ * * @see \MongoDB\Collection::mapReduce() * @see https://mongodb.com/docs/manual/reference/command/mapReduce/ + * @psalm-import-type MapReduceCallable from MapReduceResult */ class MapReduce implements Executable { @@ -353,6 +354,7 @@ private function createCommand(): Command /** * Creates a callable for MapReduceResult::getIterator(). * + * @psalm-return MapReduceCallable * @throws UnexpectedValueException if the command response was malformed */ private function createGetIteratorCallable(stdClass $result, Server $server): callable diff --git a/src/Operation/Watch.php b/src/Operation/Watch.php index ff1052dfb..ee1fa0b2a 100644 --- a/src/Operation/Watch.php +++ b/src/Operation/Watch.php @@ -79,7 +79,7 @@ class Watch implements Executable, /* @internal */ CommandSubscriber private string $databaseName; - private int $firstBatchSize; + private int $firstBatchSize = 0; private bool $hasResumed = false; diff --git a/tests/Model/CachingIteratorFunctionalTest.php b/tests/Model/CachingIteratorFunctionalTest.php index 3d3ed002c..bb600013c 100644 --- a/tests/Model/CachingIteratorFunctionalTest.php +++ b/tests/Model/CachingIteratorFunctionalTest.php @@ -17,7 +17,7 @@ public function testEmptyCursor(): void $this->assertSame(0, $iterator->count()); $iterator->rewind(); $this->assertFalse($iterator->valid()); - $this->assertFalse($iterator->current()); + $this->assertNull($iterator->current()); $this->assertNull($iterator->key()); } @@ -43,7 +43,7 @@ public function testCursor(): void $iterator->next(); $this->assertFalse($iterator->valid()); - $this->assertFalse($iterator->current()); + $this->assertNull($iterator->current()); $this->assertNull($iterator->key()); } }