Learn Continuously

As one of the best developers, you know you have to stay on top of all the new technologies. You know that you have to have a trusted training partner you can turn to when you need to learn something new.

We are your trusted training partner. We announce new training events on our mailing list often. Join us and come learn with us.

Our Students

Your needs are unique. What you need to learn depends largely on your career path and current level. We teach students at each level differently.

Senior Devs

Require a deep knowledge with very little hand-holding.

Mid-Level Devs

Deep learning with the ability to ask questions to fully understand.

New Devs

Need instructors who know how to lead them to understanding.

Our Instructors

The best instructors to teach developers are professional developers. We are professional developers.

Andrew Caya

Andrew Caya

Senior Consultant and Trainer

Andrew Caya is a Zend Certified PHP Engineer and a Zend Certified Architect. He is also the creator of Linux for PHP and LightMVC, the lead developer of a popular Joomla extension and a contributor to many open source projects.
Cal Evans

Cal Evans

Senior Consultant and Trainer

For the past 15 years Cal has worked with PHP and MySQL on Linux, OSX, and Windows. He has built a variety of projects ranging in size from simple web pages to multi-million dollar web applications.
Doug Bierer

Doug Bierer

Senior Consultant and Trainer

Doug is certified in PHP 5.1, 5.3, 5.5 and 7.1, Zend Framework 1 and 2. He has authored a bunch of books and videos for O'Reilly / Packt Publishing on PHP, Security and MongoDB.

Recent Blog Posts

ZF to Laminas Migration

Two months ago, after making changes to one of my websites, I encountered this message after running composer update:

No developer likes nasty surprises … but in this case it was not entirely unexpected.  The announcement to move Zend Framework (ZF) to Laminas had been made many moons ago.  So … being faced with the new reality, I decided to bite the bullet and do the migration.  I won’t bore you with too many details, but it’s important to set the proper context in order to understand why you want to migrate your ZF apps to Laminas, so let’s climb into the Wayback Machine.  Mr. Peabody, if you will …

A Little Bit of History

Zend Framework 1 was introduced in March of 2006.  It had a successful run, and is still used to this day.  However, the increasing lengthy class names, and the problem of global namespace pollution, among other issues, led to a radical restructuring, resulting in Zend Framework 2 being announced in September of 2012.  After version 2.4, a shift occurred whereby components were split out into their own repositories, and became separate Composer packages, a change that allowed for faster and parallel development, not possible in the monolithic structure imposed by ZF 2.4 and earlier.  Eventually, through a gradual evolutionary process, Zend Framework 3 was announced in June of 2016.

In the meantime … in October 2015, Rogue Wave Software acquired Zend. And then, in January of 2019, Perforce announced its acquisition of Rogue Wave!  Matching dates, you can see that ZF went from version 2 to 3 while Zend was part of Rogue Wave Software.  Understand that Rogue Wave is a software company, so it made perfect sense for them to host Zend Framework.  However, the main focus of Perforce is version control, application lifecycle management, AGILE support and statistical code analysis (all taken from their homepage).  Where does Zend Framework fit into this picture?  Short answer: it doesn’t.  So, bearing in mind that Perforce does in fact support the framework, it doesn’t fit into their long term plans, so the framework needed to find a new home, so to speak.

After much behind-the-scenes negotiation, to which us common folk are not privy, a deal was struck with the Linux Foundation, an excellent choice all told.  There was a major show-stopper in the transition, however: the name Zend is a corporate brand, meaning for legal reasons a new name had to be chosen: Laminas.  That being the case, it made sense to also convert all of the PHP namespace references from Zend to Laminas, and thus the need for a migration tool.  It should also be noted, for the record, that Zend Expressive components are now in the Mezzio namespace, and Apigility components moved from zfcampus to Laminas\ApiTools.

OK … now that we’ve dealt with the elephant in the room, the next reasonable question is why migrate?

Why Migrate At All?

First and foremost, please bear in mind that even if you choose not to migrate for some insane reason, your code will still work.  Just because ZF has transitioned to Laminas doesn’t mean the original framework and all of its components mystically stop working. So, assuming that is the case, why bother to migrate at all?  The answer is seen in the screen shot above: all of the Zend Framework components, even though they still exist as-is in their respective repositories, experience no new development of any type.  As Composer clearly tells us: the code in the legacy Zend Framework repositories is abandoned, and that means no new features, no security patches, no bug fixes … no nothing.

So, at least for now, your Zend Framework applications are “safe” … however, even as we speak, evil hackers are busy discovering vulnerabilities and figuring out ways to exploit them.  If you fail to migrate now my crystal ball foresees the eventual downfall of your Zend Framework-based websites.  To quote directly from a blog posted by Matthew Weir O’Phinney, the project lead for Zend Framework, now Laminas:

“To receive security updates, bug fixes, and new features for your Zend Framework code, you need to migrate it to the Laminas-branded packages.”

If this doesn’t light your fire, consider this: if you migrate now, your code will work as-is, unchanged.  The reason is because right now (i.e. April 2020), the source code in the Zend namespace is identical to the code in the Laminas namespace.  But if you wait too long … the Laminas source code will start to grow and evolve as new features are added, enhancements made, bugs fixed, and security patches applied.

Migration steps are well documented on the new Laminas website.  The purpose of this article is to take you through an actual migration, and to highlight potential gotchas.  OK … seatbelts fastened?  Tray tables in the upright position? (Sigh … really miss those days!) Let’s start by looking at what all is involved in preparing for a ZF to Laminas migration.

Prepare to Migrate

Before proceeding with the migration, you need to perform a minimal amount of migration preparation.  Here is a high-level summary of the steps to take:

  • Update Composer and third party software
  • Prepare version control
  • Make sure your application works OK
  • Install the laminas-migration tool

Let’s start at the top and deal with Composer.


The first thing you need to do is to update Composer itself.  This step is critical as, later in the process, you could very well see an error such as this:

Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 73 installs, 0 updates, 0 removals
  - Installing laminas/laminas-dependency-plugin (1.0.3): Loading from cache
PHP Fatal error:  Uncaught Error: Undefined class constant 'PRE_COMMAND_RUN' 
    in vendor/laminas/laminas-dependency-plugin/src/DependencyRewriterPlugin.php:63

To be on the safe side I would recommend the Composer version should be 1.9 or above.  To update Composer, if you have it installed as a phar file, use this syntax:

$ php composer.phar self-update

Otherwise, if you have it installed “globally” as a command, use this command:

$ composer self-update

Please note that if you have Composer installed globally, it’s probably in a restricted directory, so when you update it, be sure you are logged in as a user with sufficient rights.  If issuing this command on a Linux server, if you have the ability, you might also be able to prepend either of the above commands with sudo to temporarily acquire root privileges.  This would also be a great time to update all the third party software in your application’s vendor folder.  A simple composer update will do the trick.

Next we look at what needs to be done with regards to your version control software.

Preparing Version Control

Before starting the actual migration, it’s not a bad idea to backup your application software.  If you are using version control (you are using version control, aren’t you?), commit and push any changes still lingering around.  You should then create and checkout a new branch.  Stay on this branch while performing the actual update.  If something goes horribly wrong for some reason, you can always wipe out the branch and start all over again.

Here is a screenshot of how this might appear:

Next step: make sure your application works as-is.

Pre-Migration Application Test

Before migrating … before making any major changes for that matter, it’s a good idea to test your application to make sure that it works OK.  This might be a good time to put all those unit tests you’ve spent hours developing to work.  (Uh … you are writing unit tests for your applications, correct??) Also, if you are set up in this manner, you could also run tests using Selenium, or Apache JMeter as the tools are available.  Here is an example of a pre-migration unit test result:

Next: install the migration tool.

Install the Migration Tool

The final step in migration preparation is to install the Laminas Migration Tool.  The tool itself is in the form of a Composer package laminas/laminas-migration, so naturally you could use Composer to install the tool.  Alternatively, you could clone its repository, located at https://github.com/laminas/laminas-migration.  Now … here’s the trick: do not install the tool inside the directory structure of your project!  Theoretically this is possible, but personally I would not recommend this.  The best approach is to install the migration tool in a separate directory, well away from your application source code.

Once you have installed the tool using whatever means you prefer, you will see that the actual migration tool itself resides in the laminas-migration/bin folder. There is an executable shell script in that folder named laminas-migration that invokes PHP code.  Add laminas-migration/bin/laminas-migration to the operating system path on your server.  Alternatively, you could create a symlink from this file to a directory already in your path.  Here is an example of how that might appear:

jed@jed:~/Repos/unlikelysource.com$ echo $PATH
jed@jed:~/Repos/unlikelysource.com$ ls -l /usr/local/bin/laminas-migration 
lrwxrwxrwx 1 root root 55 Jan 27 13:19 /usr/local/bin/laminas-migration -> /home/jed/Repos/laminas-migration/bin/laminas-migration

Now we’re ready to proceed with the migration itself.

Performing the Migration

Once you complete the preparation outline above, the actual migration process looks something like this:

  • Perform the migration using the migration tool
  • Confirm differences in the code base
  • Use Composer to install the new Laminas components
  • Address any configuration provider injection issues
  • Test and debug

Let’s start with the main show, actually performing the migration.

Using the Migration Tool

The migration tool is extremely simple to use.  There are two main options migrate and nested-deps, as seen here:

The migrate option is designed for Zend Framework, Apigility or Zend Expressive project migration.  The nested-deps option is designed for projects that use Zend Framework components, but are not strictly Zend Framework projects themselves.  An example of the latter would be the LightMVC framework.  In either case, all references to the Zend namespace are replaced with Laminas.  Zend Expressive specific components end up as Mezzio, and any Apigility code formerly residing in the zf-campus namespace (simply ZF) ends up in Laminas\ApiTools.

Now … before you get all fired-up and rush off to run the migration tool, please don’t forget to change to the application project directory!  You need to be at the same directory level where your Zend Framework vendor, config, public, etc. folders are housed.

Here is an example using the migrate option:

The tool operates quite rapidly.  In my experience, even migrating a Zend Expressive based application with over 100,000 lines of code, the actual migration only took seconds.

Another thing the tool does is to install and inject a reference to the laminas-zendframework-bridge component.  A reference to this is added to the traditional config/modules.config.php file, or, in the case of Zend Expressive, at the end of config/config.php.  The purpose of this component is to provide a fallback autoloader that maps legacy Zend Framework classes to their Laminas equivalent.

Next up: confirming code differences.

Confirming Code Differences

Humans being are naturally curious (er … in most cases anyhow!): we want to know what changed post-migration.  Here’s where version control comes back to save the day.  In most version control systems, there is a way to view differences between what has been committed and what has been changed.  In the screenshot below you can see a simple git diff command that shows the old code in red, and the changes in green:

Next: install the new components.

Install Laminas Components

Another thing you might (or might not!) have noticed is that the migration tool deletes the vendor folder.  Oops!  Did I forget to mention that?  Sorry!  Anyhow, all silliness aside, when you step back to consider, this makes absolute sense.  The migration tool rewrites the composer.json file, replacing references from zendframework/* packages to laminas/* or mezzio/* packages in their place.  Everything else in the composer.json file is left alone.  Accordingly, there’s no need to retain the contents of the original vendor folder.

With this in mind, the next step is to install the new Laminas infrastructure by running the composer install command.  Here is an example of how that might appear:

As with a legacy Zend Framework application, Laminas includes certain components that need to be added to the appropriate configuration file.  Select the correct option, and away you go.  In most cases the installation should complete successfully.  In the case of Zend Expressive applications, you might see this error appear:

If you see something like this, just re-run the composer install command, select Do not inject, and manually perform ZendFrameworkBridge injections as needed afterwards.  In the case shown above, the reason for the error was due to the fact that the migrate process had already injected a reference to Laminas\ZendFrameworkBridge\ConfigPostProcessor in config/config.php as seen here:

use Laminas\ConfigAggregator\ArrayProvider;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ConfigAggregator\PhpFileProvider;
$aggregator = new ConfigAggregator([
    // not all config shown ...
    new PhpFileProvider(realpath(__DIR__) . '/development.config.php'),
], $cacheConfig['config_cache_path'], [\Laminas\ZendFrameworkBridge\ConfigPostProcessor::class]);
return $aggregator->getMergedConfig();

There is an excellent FAQ that includes a discussion of ConfigPostProcessor in the Laminas Migration documentation.  Next up: post migration testing.

Post Migration: Test and Merge Branches

Now that the hard work is pretty much over, you should return to the original tests you performed prior to migration.  If the tests do not return the same results, you need to go back to the top, and carefully review all steps to make sure you got things right.  Also, if worse comes to worse, you can always pull the plug: destroy the branch and start over again.

If all tests complete successfully, you can consider merging the new branch back down into your master branch.  You might consider doing this by way of a pull request just to make the process formal and documented.  Here are the commands to merge the branch created above:

git checkout master
git pull origin master
git merge update_laminas
git push origin master

And, as the saying goes, that’s all she wrote.


There a number of key takeaways from this article.  First and foremost: planning and preparation are vital to the success of your migration.  As you noticed, proper use of version control software can help you not only identify changes that took place, but also serves as a safety valve in case of a problem.  Another key point it to update everything managed by Composer, as well as Composer itself before you start.

The actual process of migration is quite trivial, as you learned.  What is more important is that you come up with a solid set of tests, preferably automated, that can be run both before and after the migration.  Ideally the test results should be identical.  If not, review your migration checklist and possibly start over again.

Now for the mandatory sales pitch!  At PHP-CL we offer a Mini-Course on ZF to Laminas migration.  We go over pretty much the same material, but with a live instructor.  Having read this article, you now have the opportunity to prepare a list of questions to ask the instructor as you attend.  Thanks for reading and stay safe everyone!

Grabbing a Filtered Directory Tree Using PHP Iteration

Iterate or not to Iterate … that is the question!  Excuses for misquoting Shakespeare dear readers, but I had to grab your attention away from your current attention-grabbing-addiction somehow!  So, now that I’ve got your attention (presumably not having lost it by now!), I would like to discuss the pressing topic of grabbing entire directory trees with a single command.  Granted, this would normally be a rather mundane task, so to add a further twist, I want to exclude certain directories at the same time.


In the SPL (Standard PHP Library) there lives an incredibly useful iterator known as RecursiveDirectoryIterator.  As an example, we parse the directory structure of a typical Laminas project:

├── composer.json
├── config
│   ├── application.config.php
│   ├── autoload
│   │   ├── global.php
│   │   └── laminas-developer-tools.local-development.php
│   ├── development.config.php.dist
│   └── modules.config.php
├── data
│   └── cache
├── docker-compose.yml
├── Dockerfile
├── LICENSE.md
├── module
│   ├── Application
│   │   ├── config
│   │   │   └── module.config.php
│   │   ├── src
│   │   │   ├── Controller
│   │   │   │   ├── IndexControllerFactory.php
│   │   │   │   └── IndexController.php
│   │   │   ├── Module.php
│   │   │   └── Service
│   │   │       └── Calendar.php
│   │   ├── test
│   │   │   └── Controller
│   │   │       └── IndexControllerTest.php
│   │   └── view
│   │       ├── application
│   │       │   └── index
│   │       │       ├── calendar.phtml
│   │       │       └── index.phtml
│   │       ├── error
│   │       │   ├── 404.phtml
│   │       │   └── index.phtml
│   │       └── layout
│   │           └── layout.phtml
├── public
│   ├── css
│   ├── img
│   ├── index.php
│   ├── js
│   └── web.config
├── README.md
├── Vagrantfile
└── vendor
    ├── autoload.php
    ├── bin
    ├── composer
    │   ├── autoload_classmap.php
    │   └── LICENSE
    ├── laminas
    │   ├── laminas-component-installer
    │   ├── laminas-config

As you can imagine, the vendor directory has a ton of open-source software installed via Composer.  Further, the public directory includes lots of stylesheets and JavaScript.  So the task at hand is to iterate through the directory structure, excluding these two directories.  Your first thought might be to define a path and create a RecursiveDirectoryIterator and be done with it.  Throw in a simple foreach() loop and we’re good to go, right?  (Don’t answer!  Rhetorical question.) Before we dive into the code, please be aware that by default RecursiveDirectoryIterator returns an iteration with the full filename (including path) as a key, and an SplFileInfo object as the value.

So, let’s get down to producing some code to achieve the desired results.  A good place to start might be to define a function (or class method) that determines the acceptance criteria.  In this case we want to be able to exclude one or more directory paths from the final output.  Thus we define a simple function accept() that returns FALSE if the given path includes any of the directory paths in the $excludes array:

function accept(string $name, array $excludes = [])
    $result = TRUE;
    if ($excludes) {
        foreach ($excludes as $item) {
            if (strpos($name, $item) !== FALSE) {
                $result = FALSE;
    return $result;

Next we define a function show() that performs the actual iteration, using accept() to include or exclude iteration entries.

function show(string $path, Iterator $iteration, array $excludes)
    $output = '';
    foreach ($iteration as $key => $value)
        if (accept($key, $excludes))
            $output .= str_replace($path, '', $key) . "\n";
    return $output;

Finally, we create the RecursiveDirectoryIterator instance, giving it the initial path, and a flag to eliminate the “dot” directories (e.g. “.” and “..”).

$path = '/path/to/laminas_project';
$excludes = ['/vendor','/public'];
$iteration = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
echo show($path, $iteration, $excludes);

And here is the resulting output:


Wait, you might cry out (well, maybe not, but for the sake of argument, imagine an outraged developer screaming insults at the PHP engine :-), what happened to all the subdirectories and associated files?  Good question!  Oddly, the RecursiveDirectoryIterator was doing its job!  It returns the first entry in the path specified, and the recursively continues to provide subsequent entries in the path specified.  So, in the case of the RecursiveDirectoryIterator, its recursion isn’t that it goes “deep”, but rather that it goes through the entire directory path specified before it stops.  To up its game so to speak, we need to call upon the mighty RecursiveIteratorIterator class.


The relationship between any given iterator and RecursiveIteratorIterator is like that of a bodybuilder to steroids.  This class causes the associated iterator to continue to iterate until all child nodes have been explored.  When associated with RecursiveDirectoryIterator, it is perfect for parsing entire directory sub-trees.  One word of caution, however, is that if you point it to the wrong path, especially paths with thousands of files and hundreds of subdirectories … you can quickly enter PHP Fatal Error territory. That consideration aside, RecursiveIteratorIterator is a really cool classname, isn’t it?  It gives one a warm fuzzy Department-Of-Redundancy-Department kind of feeling doesn’t it?  (Monty Python Fans take note!)

All joking aside, let’s have a look at its application to the code described above.  Really, the only thing that needs to be done is to wrap the RecursiveDirectoryIterator instance into a RecursiveIteratorIterator instance, and we’re good to go.  The modified code might appear as follows:

$iteration = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$recurse = new RecursiveIteratorIterator($iteration);
echo show($path, $recurse, $excludes);

And here are the results we expected from the start:


But wait … there’s more!  Let me pose you a question: wouldn’t it be nice to do all this with a single iterator?  Hah!  That got your attention, didn’t it?  So, without further ado, let’s have a look at the last iterator class to be discussed in this article: FilterIterator.


As with RecursiveIteratorIterator, the FilterIterator class cannot stand alone: it provides a wrapper for an existing iterator.  But there’s a bigger problem: this class is marked abstract which means you cannot use it directly!  This makes perfect sense when you understand that the abstract method accept() (sound familiar?) simply cannot be defined by the PHP core development team.  They have no idea what needs to be filtered.  Accordingly its definition is left to the developer.  This still doesn’t stop it from being super-annoying, however!  Why do I need to develop an entirely new class which extends FilterIterator just because it’s abstract?  Arghhhh!!!  Hang on folks … there is another way!

Many of us tend to forget one of the most discussed new feature of PHP 7.0: the anonymous class.  This feature was discussed endlessly, and the subject of many an article or blog post.  Eventually it was forgotten and faded into obscurity.  But, it just so happens that an anonymous class might be just the ticket in the situation we are discussing in this article.

Imagine the following:

  • We create an iterator in the form of an anonymous class that extends FilterIterator
  • In the anonymous class we define a static property to contain an array of directory paths to exclude
  • We move the logic from the accept() function described above into a class method
  • VOILA: we’re done!

The only real change that needs to be made in accept() is to not accept any arguments, and substitute parent::current() in place of $name.  If $excludes becomes a public static property, it can be assigned from the calling program. Here is how the alternative code solution might appear:

$iteration = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$recurse = new RecursiveIteratorIterator($iteration);
$filter = new class($recurse) extends FilterIterator {
    public static $excludes = [];
    public function accept() {
        if (!self::$excludes) return TRUE;
        $actual = 0;
        foreach (self::$excludes as $item)
            if (strpos(parent::current(), $item) !== FALSE) $actual++;
        return !((bool) $actual);
$filter::$excludes = $excludes;
foreach ($filter as $key => $value)
    echo str_replace($path, '', $key) . "\n";

An added benefit is that we no longer need the show() function.  In this example the iteration itself already includes filtering, so all we need to do is to iterate through the pre-filtered result.  The resulting output is not shown here as it’s identical to the output from the previous code example.  So, to summarize, a single iterator, FilterIterator, allows us to produce a single iteration that doesn’t need any additional logic.

That’s about all for today dear readers.  Happy coding!

MongoDB BulkWrite: the Highly Misunderstood Method

A Story of Pathos, Drama and Neglect

If a story of neglect, pretty much lacking drama, but loaded with pathos could be written about an OOP method, the main character in the book would be the MongoDB collection level bulkWrite() method.  This highly misunderstood method, often lightly skipped over by most DevOps (myself included!), is often misconstrued for a glorified version of the collection level insertMany() method.  When perusing the documentation for the command, one quickly sees that the syntax, at least at first glance, appears convoluted, and not worth the bother.  However, upon closer examination, come to find out, this potent little method actually packs quite a punch, allowing you to perform not only bulk inserts, but updates and deletes, all in a single command.

Now that I’ve (hopefully) gotten your attention, before diving into the nitty-gritty, the question of why bother needs to be addressed.

Why Bother With a Poor Cousin?

Given that bulkWrite() allows you to perform multiple inserts, deletes and updates in a single command, the next logical question that might come to mind is: why bother?  You could simply issue a series of insert, delete and update commands and be done with it.  Here is an example which adds four documents to the test.users collection, updates Betty to “active” status, and deletes Barney:

include __DIR__ . '/../vendor/autoload.php';
use MongoDB\Client;
$data = [
    ['key' => 'FRF','first' => 'Fred',  'last' => 'Flintstone','active' => 1],
    ['key' => 'WIF','first' => 'Wilma', 'last' => 'Flintstone','active' => 1],
    ['key' => 'BAR','first' => 'Barney','last' => 'Rubble',    'active' => 0],
    ['key' => 'BER','first' => 'Betty', 'last' => 'Rubble',    'active' => 0],
$client = new Client('mongodb://localhost:27017');
// drop users collection from test database
$client->test->users->updateOne(['key'=>'BER'],['$set' => ['active' => 1]]);
$query = $client->test->users->find();
foreach ($query as $document)var_dump($document);

Absolutely nothing wrong with this block of code … except that the insertMany(), updateOne() and deleteOne() calls each cost a round trip to and from the database.  If this example were to be converted into the same operation but using bulkWrite() instead, only one round trip would be required: much more efficient!  The same argument can also apply to your decision on whether or use bulkWrite() or deleteMany() or updateMany().

At this point, at least if you’ve gotten this far, you might be sold on the concept, but want to know more.  Let’s now look at bulkWrite() operations.

What Are BulkWrite Operations?

bulkWrite() operations are a set of pre-defined keys you need to add to the bulk write document that dictate which method is to be called next.  Here is a table that summarizes the operations, and associated options:

Operation Options Arguments
insertOne document <insert doc>
updateOne filter <query doc>
update <update doc>
updateMany filter <query doc>
update <update doc>
replaceOne filter <query doc>
replacement <replacement doc>
deleteOne filter <query doc>

Where the argument mentions “doc”, when running a bulkWrite() operation using the mongo shell, this would be a JSON document.  On the other hand, when running the same operation using the PHP MongoDB library, “doc” would take the form of a PHP associative array.  The operation and options would be  array keys.  The arguments would be a sub-array, itself consisting of key/value pairs.

OK, yes, I hear you: get to the good stuff will you?  Show me how to do it!

Using BulkWrite in the Mongo Shell

The beauty of using the PHP MongoDB Library, which leverages the MongoDB extension, is that you can first model commands using the mongo shell, and then later map the same command almost directly into your PHP app.  First step is to define the bulk write document, including operations and options.

bulkDoc = [
  {"insertOne" : { "document" : {"key" : "FRF", "first" : "Fred","last" : "Flintstone","active" : 1}}},
  {"insertOne" : { "document" : {"key" : "WIF", "first" : "Wilma","last" : "Flintstone","active" : 1}}},
  {"insertOne" : { "document" : {"key" : "BAR", "first" : "Barney","last" : "Rubble","active" : 0}}},
  {"insertOne" : { "document" : {"key" : "BER", "first" : "Betty","last" : "Rubble","active" : 0}}},
  {"updateOne" : { "filter" : {"key" : "BER"}, "update" : {"$set" : {"active" : 1}}}},
  {"deleteOne" : { "filter" : {"key" : "BAR"}}}

After that, it’s just a matter of running bulkWrite(), in this example on the test.users collection.  Note that the users collection is first dropped so that we get consistent test results.  We also throw in a find() to view results:

use test;

Here is how the output might appear:

So far, so good, eh?   Now to translate the query using the PHP MongoDB library.

Happily Bulk Writing in PHP

The first step is to translate the query from a JSON document into a PHP array for consumption by the MongoDB\Collection::bulkWrite() method.

$bulkDoc = [
    ['insertOne' => [['key' => 'FRF', 'first' => 'Fred', 'last' => 'Flintstone', 'active' => 1]]], 
    ['insertOne' => [['key' => 'WIF', 'first' => 'Wilma', 'last' => 'Flintstone', 'active' => 1]]], 
    ['insertOne' => [['key' => 'BAR', 'first' => 'Barney','last' => 'Rubble', 'active' => 0]]], 
    ['insertOne' => [['key' => 'BER', 'first' => 'Betty', 'last' => 'Rubble', 'active' => 0]]], 
    ['updateOne' => [['key' => 'BER'], ['$set' => ['active' => 1]]]], 
    ['deleteOne' => [['key' => 'BAR']]]

Right away (er … if you’re still awake at this point!), you might notice the double square brackets after each operation key.  The reason for this is because the MongoDB PHP library is not written to accept the keys document, filter and update needed by the equivalent shell method.  However, when the command is transmitted to MongoDB, the array-within-array structure needs to be maintained, thus the redundant square brackets.

Next, as with the shell example, we drop the users collection, run bulkWrite(), and run find() to view results:

$client = new Client('mongodb://localhost:27017');
$query = $client->test->users->find([],['projection' => ['_id' => 0]]);
    foreach ($query as $document)
        vprintf("%4s : %12s : %12s : %2d\n", $document->getArrayCopy());

The result is pretty much the same as above, taking into account the minor formatting difference:

And that about wraps things up.  To summarize: bulkWrite() is an efficient way to perform bulk operations involving a combination of insert, update and/or delete.  Use this command if you need to perform mass operations involving more than just insert, update or delete as it saves round trips between the application and database.