» Tailable cursors
are a special type of MongoDB cursor that allows the client to read some
results and then wait until more documents become available. These cursors
are primarily used with
» Capped Collections
and » Change Streams.
While normal cursors can be iterated once with foreach,
that approach will not work with tailable cursors. When
foreach is used with a tailable cursor, the loop will
stop upon reaching the end of the initial result set. Attempting to
continue iteration on the cursor with a second
foreach would throw an exception, since PHP attempts to
rewind the cursor. Similar to result objects in other database drivers,
cursors in MongoDB only support forward iteration, which means they cannot
be rewound.
In order to continuously read from a tailable cursor, the Cursor object
must be wrapped with an IteratorIterator. This
allows the application to directly control the cursor's iteration, avoid
inadvertently rewinding the cursor, and decide when to wait for new results
or stop iteration entirely.
In order to demonstrate a tailable cursor in action, two scripts will be
used: a "producer" and a "consumer". The producer script will create a new
capped collection using the
» create command
and proceed to insert a new document into that collection each second.
<?php
$manager = new MongoDB\Driver\Manager;
$manager->executeCommand('test', new MongoDB\Driver\Command([
'create' => 'asteroids',
'capped' => true,
'size' => 1048576,
]));
while (true) {
$bulkWrite = new MongoDB\Driver\BulkWrite;
$bulkWrite->insert(['createdAt' => new MongoDB\BSON\UTCDateTime]);
$manager->executeBulkWrite('test.asteroids', $bulkWrite);
sleep(1);
}
?>
With the producer script still running, a second consumer script may be
executed to read the inserted documents using a tailable cursor, indicated
by the tailable and awaitData options
to MongoDB\Driver\Query::__construct().
<?php
$manager = new MongoDB\Driver\Manager;
$query = new MongoDB\Driver\Query([], [
'tailable' => true,
'awaitData' => true,
]);
$cursor = $manager->executeQuery('test.asteroids', $query);
$iterator = new IteratorIterator($cursor);
$iterator->rewind();
while (true) {
if ($iterator->valid()) {
$document = $iterator->current();
printf("Consumed document created at: %s\n", $document->createdAt);
}
$iterator->next();
}
?>
The consumer script will start by quickly printing all available documents
in the capped collection (as if foreach had been used);
however, it will not terminate upon reaching the end of the initial result
set. Since the cursor is tailable, calling
IteratorIterator::next() will block and wait for
additional results. IteratorIterator::valid() is also
used to check if there is actually data available to read at each step.