mongodb / mongodb/mongo-php-driver
foreach over ObjectId and Garbage Collection
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 923
- Forks
- 218
- Avg merge
- 15h 15m
- Merged PRs (30d)
- 32
Description
Bug Report
foreach over ObjectId changes behavior after circular reference collector.
Before GC foreach over ObjectId executes one loop iteration with $key="oid".
After GC foreach over ObjectId executes zero loop iterations.
Environment
PHP inside Docker on Windows 10.
Docker Image: php:8.2.27-apache
PHP: 8.2.27
MongoDB PHP Driver: 1.20.1
MongoDB PHP Library: 1.20.0
MongoDB: 8.0.0
mongodb/laravel-mongodb: 4.8.1
MongoDB: I run DB in separate Docker container locally and use in PHP only HOST and PORT of DB.
www-data@5c2174446320:~/html$ php -i | grep -E 'mongodb|libmongoc|libbson'
Cannot load Xdebug - it was already loaded
/usr/local/etc/php/conf.d/mongodb.ini,
mongodb
libbson bundled version => 1.28.1
libmongoc bundled version => 1.28.1
libmongoc SSL => enabled
libmongoc SSL library => OpenSSL
libmongoc crypto => enabled
libmongoc crypto library => libcrypto
libmongoc crypto system profile => disabled
libmongoc SASL => enabled
libmongoc SRV => enabled
libmongoc compression => enabled
libmongoc compression snappy => enabled
libmongoc compression zlib => enabled
libmongoc compression zstd => enabled
libmongocrypt bundled version => 1.11.0
libmongocrypt crypto => enabled
libmongocrypt crypto library => libcrypto
mongodb.debug => no value => no value
Test Script
I've used mongodb/laravel-mongodb to reproduce the bug but there is not so much Laravel specific and main issue in \MongoDB\BSON\ObjectId.
ObjectIdTestModel.php
<?php
namespace App\Models;
class ObjectIdTestModel extends \MongoDB\Laravel\Eloquent\Model
{
}
Test.php
\dump();
// Uncommenting following line fixes the test
//gc_disable();
$objectId1 = new ObjectId();
$objectId2 = new ObjectId();
// Here we see public property "oid"
echo 'objectId1: ';
xdebug_debug_zval('objectId1');
echo 'objectId2: ';
xdebug_debug_zval('objectId2');
// Check possible assumption about iterating
$is_traversable = ($objectId1 instanceof \Traversable);
echo 'is_traversable: ' . var_export($is_traversable, true) . "\n";
// Check there are no properties for iterating in foreach hence there is not property "oid"
$reflect = new \ReflectionClass($objectId1);
$properties = $reflect->getProperties();
echo 'properties: ' . print_r($properties, true);
// Allocate a lot of objects
for ($i = 0; $i < 200; $i++) {
$model = new ObjectIdTestModel();
$model->save();
}
$a = ObjectIdTestModel::all();
// First foreach over \MongoDB\BSON\ObjectId
$number1 = 0;
foreach ($objectId1 as $key => $value) {
$number1 += 1;
}
// Allocate a lot of objects again
$a = ObjectIdTestModel::all();
// Second foreach over \MongoDB\BSON\ObjectId
$number2 = 0;
foreach ($objectId2 as $key => $value) {
$number2 += 1;
}
echo 'objectId1: ';
xdebug_debug_zval('objectId1');
echo 'objectId2: ';
xdebug_debug_zval('objectId2');
$this->assertEquals(1, $number1, 'number1');
$this->assertEquals(1, $number2, 'number2');
Test output:
objectId1: objectId1: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f004' }
objectId2: objectId2: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f005' }
is_traversable: false
properties: Array
(
)
objectId1: objectId1: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f004' }
objectId2: objectId2: (refcount=1, is_ref=0)=class MongoDB\BSON\ObjectId { public $oid = (refcount=1, is_ref=0)='679a97b2d1cc47db6503f005' }
...
number2
Failed asserting that 0 matches expected 1.
If the bug is not repoduced you can increase number of created objects in line for ($i = 0; $i < 200; $i++) {
Expected and Actual Behavior
Expected
$number1 === 1
$number2 === 1
Circular reference collector do not change script behavior.
Actual
$number1 === 1
$number2 === 0
Calling gc_disable() "fixes" the bug.
Preffered solution
Prohibit to iterate over \MongoDB\BSON\ObjectId especially by hidden public property "oid".
I have iterated it by mistake and prefer empty loop over object without public properties instead of strange behavior.
Debug Log
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the behavior with the Test.php script and the MongoDB\BSON\ObjectId entry point, comparing foreach results before and after garbage collection. Review the ObjectId implementation and its interaction with PHP's circular reference collector; done means iteration behavior is consistent and matches the expected assertions without relying on gc_disable().
Written by the indexing model from the issue text.
Assessment
- Tech stack
- mongodb, php
- Domain
- backend, database
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100