Understanding MongoDB's findOne() Behavior in PHP
Adam C. |

Working with MongoDB in PHP can be confusing, especially when it comes to the behavior of findOne() and how the results can be accessed. This blog post aims to clarify the nuances and help you avoid common pitfalls.

Photo by Mihai Surdu on Unsplash

The Basics: findOne() in PHP

The findOne() method retrieves a single document from a MongoDB collection. By default, it returns a MongoDB\Model\BSONDocument object. This object supports:

  1. Object-style access (->): Access document properties like $doc->key.
  2. Array-style access (['key']): Access properties like $doc['key'], because BSONDocument implements PHP's ArrayAccess interface.

Here's a simple example:

$doc = $collection->findOne([
    'slug' => 'john-doe',
]);

if ($doc) {
    // Object-style access
    echo "Slug (object-style): " . $doc->slug . "\n";

    // Array-style access
    echo "Slug (array-style): " . $doc['slug'] . "\n";
} else {
    echo "No document found with slug 'john-doe'.\n";
}

Both access styles work fine in this case. But what happens if you call jsonSerialize() or use the typeMap option?

Using jsonSerialize()

Calling jsonSerialize() on a BSONDocument converts it into a stdClass object, which only supports object-style access (->). Array-style access (['key']) will fail.

Here’s an example:

$serializedDoc = $doc->jsonSerialize();

// Object-style access works
echo "Slug (object-style): " . $serializedDoc->slug . "\n";

// Array-style access will fail
try {
    echo "Slug (array-style): " . $serializedDoc['slug'] . "\n";
} catch (Throwable $e) {
    echo "Error: " . $e->getMessage() . "\n";
}

Expected Output

If the document exists:

Slug (object-style): john-doe
Error: Cannot use object of type stdClass as array

This demonstrates that jsonSerialize() changes the access behavior.

Enforcing Return Types with typeMap

You can use the typeMap option to control how findOne() returns the document. For example:

  1. Return as an associative array:
$doc = $collection->findOne([
    'slug' => 'john-doe',
], [
    'typeMap' => ['root' => 'array'],
]);

if ($doc) {
    // Array-style access works
    echo "Slug (array-style): " . $doc['slug'] . "\n";

    // Object-style access will fail
    try {
        echo "Slug (object-style): " . $doc->slug . "\n";
    } catch (Throwable $e) {
        echo "Error: " . $e->getMessage() . "\n";
    }
}
  1. Return as a PHP object:
$doc = $collection->findOne([
    'slug' => 'john-doe',
], [
    'typeMap' => ['root' => 'object'],
]);

if ($doc) {
    // Object-style access works
    echo "Slug (object-style): " . $doc->slug . "\n";

    // Array-style access will fail
    try {
        echo "Slug (array-style): " . $doc['slug'] . "\n";
    } catch (Throwable $e) {
        echo "Error: " . $e->getMessage() . "\n";
    }
}

Key Takeaways

  • Default behavior: findOne() returns a BSONDocument, supporting both access styles.
  • With jsonSerialize(): Converts to stdClass, breaking array-style access.
  • With typeMap:
    • ['root' => 'array']: Returns an associative array, breaking object-style access.
    • ['root' => 'object']: Returns a PHP object, breaking array-style access.

Conclusion

MongoDB’s PHP driver offers flexibility in accessing documents, but it can be confusing due to the interplay of BSONDocument, jsonSerialize(), and typeMap. Here’s a quick summary:

Access TypeDefault (BSONDocument)After jsonSerialize()With typeMap => arrayWith typeMap => object
Object-style (->)WorksWorksFailsWorks
Array-style ([])WorksFailsWorksFails

When working with findOne(), choose the method that best suits your needs and stick to it for consistency. Let me know if this post helps clarify the confusion!