How To Build a Laravel + MongoDB Back End Service
Rate this tutorial
Laravel is a leading PHP framework that vastly increases the productivity of PHP developers worldwide. I come from a WordPress background, but when asked to build a web service for a front end app, Laravel and MongoDB come to mind, especially when combined with the MongoDB Atlas developer data platform.
This Laravel MongoDB tutorial addresses prospective and existing Laravel developers considering using MongoDB as a database.
Let's create a simple REST back end for a front-end app and go over aspects of MongoDB that might be new. Using MongoDB doesn't affect the web front-end aspect of Laravel, so we'll use Laravel's built-in API routing in this article.
MongoDB support in Laravel is provided by the official mongodb/laravel-mongodb package, which extends Eloquent, Laravel's built-in ORM.
First, let's establish a baseline by creating a default Laravel app. We'll mirror some of the instructions provided on our MongoDB Laravel Integration page, which is the primary entry point for all things Laravel at MongoDB. Any Laravel environment should work, but we'll be using some Linux commands under Ubuntu in this article.
- A MongoDB Atlas cluster
- A code editor
Note: We'll go over creating the Laravel project with Composer but the article's code repository is available.
The "Setting Up and Configuring Your Laravel Project" instructions in the MongoDB and Laravel Integration show how to configure a Laravel-MongoDB development environment. We'll cover the Laravel application creation and the MongoDB configuration below.
Here are handy links, just in case:
- Official Laravel installation instructions (10.23.0 here)
- Official PHP installation instructions (PHP 8.1.6+ here)
- Install Composer (Composer 2.3.5 here)
- The MongoDB PHP extension (1.13.0 here)
With our development environment working, let's create a Laravel project by creating a Laravel project directory. From inside that new directory, create a new Laravel project called
laraproject
by running the command, which specifies using Laravel:composer create-project laravel/laravel laraproject
After that, your directory structure should look like this:
1 └── ./laraproject 2 ├── ./app 3 ├── ./artisan 4 ├── ./bootstrap 5 ├── ...
Once our development environment is properly configured, we can browse to the Laravel site (likely 'localhost', for most people) and view the homepage:
Check if the MongoPHP driver is installed and running
To check the MongoDB driver is up and running in our web server, we can add a webpage to our Laravel website. in the code project, open
/routes/web.php
and add a route as follows:1 Route::get('/info', function () { 2 phpinfo(); 3 });
Subsequently visit the web page at localhost/info/ and we should see the PHPinfo page. Searching for the MongoDB section in the page, we should see something like the below. It means the MongoDB PHP driver is loaded and ready. If there are experience errors, our MongoDB PHP error handling goes over typical issues.
We can use Composer to add the Laravel MongoDB package to the application. In the command prompt, go to the project's directory and run the command below to add the package to the
<project>/vendor/
directory.composer require mongodb/laravel-mongodb:4.0.0
Next, update the database configuration to add a MongoDB connection string and credentials. Open the
/config/database.php
file and update the 'connection' array as follows:1 'connections' => [ 2 'mongodb' => [ 3 'driver' => 'mongodb', 4 'dsn' => env('MONGODB_URI'), 5 'database' => 'YOUR_DATABASE_NAME', 6 ],
env('MONGODB_URI')
refers to the content of the default .env
file of the project. Make sure this file does not end up in the source control. Open the <project>/.env
file and add the DB_URI environment variable with the connection string and credentials in the form:MONGODB_URI=mongodb+srv://USERNAME:PASSWORD@clustername.subdomain.mongodb.net/?retryWrites=true&w=majority
Your connection string may look a bit different. Learn how to get the connection string in Atlas. Remember to allow the web server's IP address to access the MongoDB cluster. Most developers will add their current IP address to the cluster.
In
/config/database.php
, we can optionally set the default database connection. At the top of the file, change 'default' to this: 'default' => env('DB_CONNECTION', 'mongodb'),
Our Laravel application can connect to our MongoDB database. Let's create an API endpoint that pings it. In
/routes/api.php
, add the route below, save, and visit localhost/api/ping/
. The API should return the object {"msg": "MongoDB is accessible!"}. If there's an error message, it's probably a configuration issue. Here are some general PHP MongoDB error handling tips.1 Route::get('/ping', function (Request $request) { 2 $connection = DB::connection('mongodb'); 3 $msg = 'MongoDB is accessible!'; 4 try { 5 $connection->command(['ping' => 1]); 6 } catch (\Exception $e) { 7 $msg = 'MongoDB is not accessible. Error: ' . $e->getMessage(); 8 } 9 return ['msg' => $msg]; 10 });
Laravel comes with Eloquent, an ORM that abstracts the database back end so users can use different databases utilizing a common interface. Thanks to the Laravel MongoDB package, developers can opt for a MongoDB database to benefit from a flexible schema, excellent performance, and scalability.
Eloquent has a "Model" class, the interface between our code and a specific database table (or "collection," in MongoDB terminology). Instances of the Model classes represent rows of tables in relational databases.
In MongoDB, they are documents in the collection. In relational databases, we can set values only for columns defined in the database, but MongoDB allows any field to be set.
The models can define fillable fields if we want to enforce a document schema in our application and prevent errors like name typos. This is not required if we want full flexibility of being schemaless to be faster.
For new Laravel developers, there are many Eloquent features and philosophies. The official Eloquent documentation is the best place to learn more about that. For now, we will highlight the most important aspects of using MongoDB with Eloquent. We can use both MongoDB and an SQL database in the same Laravel application. Each model is associated with one connection or the other.
First, we create a classic model with its associated migration code by running the command:
php artisan make:model CustomerSQL --migration
After execution, the command created two files,
<project>/app/Models/CustomerSQL.php
and <project>/database/migrations/YY_MM_DD_xxxxxx_create_customer_s_q_l_s_table.php
. The migration code is meant to be executed once in the prompt to initialize the table and schema. In the extended Migration class, check the code in the up()
function.We'll edit the migration's
up()
function to build a simple customer schema like this:1 public function up() 2 { 3 Schema::connection('mysql')->create('customer_sql', function (Blueprint $table) { 4 $table->id(); 5 $table->uuid('guid')->unique(); 6 $table->string('first_name'); 7 $table->string('family_name'); 8 $table->string('email'); 9 $table->text('address'); 10 $table->timestamps(); 11 }); 12 }
Our migration code is ready, so let's execute it to build the table and index associated with our Eloquent model.
php artisan migrate --path=/database/migrations/YY_MM_DD_xxxxxx_create_customer_s_q_l_s_table.php
In the MySQL database, the migration created a 'customer_sql' table with the required schema, along with the necessary indexes. Laravel keeps track of which migrations have been executed in the 'migrations' table.
Next, we can modify the model code in
<project>/app/Models/CustomerSQL.php
to match our schema.1 // This is the standard Eloquent Model 2 use Illuminate\Database\Eloquent\Model; 3 class CustomerSQL extends Model 4 { 5 use HasFactory; 6 // the selected database as defined in /config/database.php 7 protected $connection = 'mysql'; 8 // the table as defined in the migration 9 protected $table= 'customer_sql'; 10 // our selected primary key for this model 11 protected $primaryKey = 'guid'; 12 //the attributes' names that match the migration's schema 13 protected $fillable = ['guid', 'first_name', 'family_name', 'email', 'address']; 14 }
Let's create an Eloquent model for our MongoDB database named "CustomerMongoDB" by running this Laravel prompt command from the project's directory"
php artisan make:model CustomerMongoDB
Laravel creates a
CustomerMongoDB
class in the file \models\CustomerMongoDB.php
shown in the code block below. By default, models use the 'default' database connection, but we can specify which one to use by adding the $connection
member to the class. Likewise, it is possible to specify the collection name via a $collection
member.Note how the base model class is replaced in the 'use' statement. This is necessary to set "_id" as the primary key and profit from MongoDB's advanced features like array push/pull.
1 //use Illuminate\Database\Eloquent\Model; 2 use MongoDB\Laravel\Eloquent\Model; 3 4 class CustomerMongoDB extends Model 5 { 6 use HasFactory; 7 8 // the selected database as defined in /config/database.php 9 protected $connection = 'mongodb'; 10 11 // equivalent to $table for MySQL 12 protected $collection = 'laracoll'; 13 14 // defines the schema for top-level properties (optional). 15 protected $fillable = ['guid', 'first_name', 'family_name', 'email', 'address']; 16 }
The extended class definition is nearly identical to the default Laravel one. Note that
$table
is replaced by $collection
to use MongoDB's naming. That's it.We can still use Eloquent Migrations with MongoDB (more on that below), but defining the schema and creating a collection with a Laravel-MongoDB Migration is optional because of MongoDB's flexible schema. At a high level, each document in a MongoDB collection can have a different schema.
If we want to enforce a schema, we can! MongoDB has a great schema validation mechanism that works by providing a validation document when manually creating the collection using db.createcollection(). We'll cover this in an upcoming article.
With the models ready, creating data for a MongoDB back end isn't different, and that's what we expect from an ORM.\
Below, we can compare the
/api/create_eloquent_mongo/
and /api/create_eloquent_sql/
API endpoints. The code is identical, except for the different CustomerMongoDB
and CustomerSQL
model names.1 Route::get('/create_eloquent_sql/', function (Request $request) { 2 $success = CustomerSQL::create([ 3 'guid'=> 'cust_0000', 4 'first_name'=> 'John', 5 'family_name' => 'Doe', 6 'email' => 'j.doe@gmail.com', 7 'address' => '123 my street, my city, zip, state, country' 8 ]); 9 10 ... 11 }); 12 13 Route::get('/create_eloquent_mongo/', function (Request $request) { 14 $success = CustomerMongoDB::create([ 15 'guid'=> 'cust_1111', 16 'first_name'=> 'John', 17 'family_name' => 'Doe', 18 'email' => 'j.doe@gmail.com', 19 'address' => '123 my street, my city, zip, state, country' 20 ]); 21 22 ... 23 });
After adding the document, we can retrieve it using Eloquent's "where" function as follows:
1 Route::get('/find_eloquent/', function (Request $request) { 2 $customer = CustomerMongoDB::where('guid', 'cust_1111')->get(); 3 ... 4 });
Eloquent allows developers to find data using complex queries with multiple matching conditions, and there's more to learn by studying both Eloquent and the MongoDB Laravel extension. The Laravel MongoDB query tests are an excellent place to look for additional syntax examples and will be kept up-to-date.
Of course, we can also Update and Delete records using Eloquent as shown in the code below:
1 Route::get('/update_eloquent/', function (Request $request) { 2 $result = CustomerMongoDB::where('guid', 'cust_1111')->update( ['first_name' => 'Jimmy'] ); 3 ... 4 }); 5 6 Route::get('/delete_eloquent/', function (Request $request) { 7 $result = CustomerMongoDB::where('guid', 'cust_1111')->delete(); 8 ... 9 });
At this point, our MongoDB-connected back-end service is up and running, and this could be the end of a typical "CRUD" article. However, MongoDB is capable of much more, so keep reading.
To extract the full power of MongoDB, it's best to fully utilize its document model and native Query API.
The document model is conceptually like a JSON object, but it is based on BSON (a binary representation with more fine-grained typing) and backed by a high-performance storage engine. Document supports complex BSON types, including object, arrays, and regular expressions. Its native Query API can efficiently access and process such data.
Let's discuss a few benefits of the document model.
Embedded documents and arrays paired with data modeling allow developers to avoid expensive database "join" operations, especially on the most critical workloads, queries, and huge collections. If needed, MongoDB does support join-like operations with the $lookup operator, but the document model lets developers keep such operations to a minimum or get rid of them entirely. Reducing joins also makes it easier to shard collections across multiple servers to increase capacity.
This NoSQL strategy is critical to increasing database workload efficiency, to reduce billing. That's why Amazon eliminated most of its internal relational database workloads years ago. Learn more by watching Rick Houlihan, who led this effort at Amazon, tell that story on YouTube, or read about it on our blog. He is now MongoDB's Field CTO for Strategic Accounts.
MongoDB documents are contained within "collections" (tables, in SQL parlance). The big difference between SQL and MongoDB is that each document in a collection can have a different schema. We could store completely different schemas in the same collection. This enables strategies like schema versioning to avoid downtime during schema updates and more!
Data modeling goes beyond the scope of this article, but it is worth spending 15 minutes watching the Principles of Data Modeling for MongoDB video featuring Daniel Coupal, the author of MongoDB Data Modeling and Schema Design, a book that many of us at MongoDB have on our desks. At the very least, read this short 6 Rules of Thumb for MongoDB Schema article.
The Laravel MongoDB Eloquent extension does offer MongoDB-specific operations for nested data. However, adding nested data is also very intuitive without using the embedsMany() and embedsOne() methods provided by the extension.
As shown earlier, it is easy to define the top-level schema attributes with Eloquent. However, it is more tricky to do so when using arrays and embedded documents.
Fortunately, we can intuitively create the Model's data structures in PHP. In the example below, the 'address' field has gone from a string to an object type. The 'email' field went from a string to an array [of strings] type. Arrays and objects are not supported types in MySQL.
1 Route::get('/create_nested/', function (Request $request) { 2 $message = "executed"; 3 $success = null; 4 5 $address = new stdClass; 6 $address->street = '123 my street name'; 7 $address->city = 'my city'; 8 $address->zip= '12345'; 9 $emails = ['j.doe@gmail.com', 'j.doe@work.com']; 10 11 try { 12 $customer = new CustomerMongoDB(); 13 $customer->guid = 'cust_2222'; 14 $customer->first_name = 'John'; 15 $customer->family_name= 'Doe'; 16 $customer->email= $emails; 17 $customer->address= $address; 18 $success = $customer->save(); // save() returns 1 or 0 19 } 20 catch (\Exception $e) { 21 $message = $e->getMessage(); 22 } 23 return ['msg' => $message, 'data' => $success]; 24 });
If we run the
localhost/api/create_nested/
API endpoint, it will create a document as the JSON representation below shows. The updated_at
and created_at
datetime fields are automatically added by Eloquent, and it is possible to disable this Eloquent feature (check the Timestamps in the official Laravel documentation).MongoDB has a native query API optimized to manipulate and transform complex data. There's also a powerful aggregation framework with which we can pipe data from one stage to another, making it intuitive for developers to create very complex aggregations. The native query is accessible via the MongoDB "collection" object.
Eloquent has an intelligent way of exposing the full capabilities of the underlying database by using "raw queries," which are sent "as is" to the database without any processing from the Eloquent Query Builder, thus exposing the native query API. Read about raw expressions in the official Laravel documentation.
We can perform a raw native MongoDB query from the model as follows, and the model will return an Eloquent collection
1 $mongodbquery = ['guid' => 'cust_1111']; 2 3 // returns a "Illuminate\Database\Eloquent\Collection" Object 4 $results = CustomerMongoDB::whereRaw( $mongodbquery )->get();
It's also possible to obtain the native MongoDB collection object and perform a query that will return objects such as native MongoDB documents or cursors:
1 $mongodbquery = ['guid' => 'cust_1111', ]; 2 3 $mongodb_native_collection = DB::connection('mongodb')->getCollection('laracoll'); 4 5 $document = $mongodb_native_collection->findOne( $mongodbquery ); 6 $cursor = $mongodb_native_collection->find( $mongodbquery );
Using the MongoDB collection directly is the sure way to access all the MongoDB features. Typically, people start using the native collection.insert(), collection.find(), and collection.update() first.
Common MongoDB Query API functions work using a similar logic and require matching conditions to identify documents for selection or deletion. An optional projection defines which fields we want in the results.
1 /* 2 Find records using a native MongoDB Query 3 1 - with Model->whereRaw() 4 2 - with native Collection->findOne() 5 3 - with native Collection->find() 6 */ 7 8 Route::get('/find_native/', function (Request $request) { 9 // a simple MongoDB query that looks for a customer based on the guid 10 $mongodbquery = ['guid' => 'cust_2222']; 11 12 // Option #1 13 //========== 14 // use Eloquent's whereRaw() function. This is the easiest way to stay close to the Laravel paradigm 15 // returns a "Illuminate\Database\Eloquent\Collection" Object 16 17 $results = CustomerMongoDB::whereRaw( $mongodbquery )->get(); 18 19 // Option #2 & #3 20 //=============== 21 // use the native MongoDB driver Collection object. with it, you can use the native MongoDB Query API 22 $mdb_collection = DB::connection('mongodb')->getCollection('laracoll'); 23 24 // find the first document that matches the query 25 $mdb_bsondoc= $mdb_collection->findOne( $mongodbquery ); // returns a "MongoDB\Model\BSONDocument" Object 26 27 // if we want to convert the MongoDB Document to a Laravel Model, use the Model's newFromBuilder() method 28 $cust= new CustomerMongoDB(); 29 $one_doc = $cust->newFromBuilder((array) $mdb_bsondoc); 30 31 // find all documents that matches the query 32 // Note: we're using find without any arguments, so ALL documents will be returned 33 34 $mdb_cursor = $mdb_collection->find( ); // returns a "MongoDB\Driver\Cursor" object 35 $cust_array = array(); 36 foreach ($mdb_cursor->toArray() as $bson) { 37 $cust_array[] = $cust->newFromBuilder( $bson ); 38 } 39 40 return ['msg' => 'executed', 'whereraw' => $results, 'document' => $one_doc, 'cursor_array' => $cust_array]; 41 });
Updating documents is done by providing a list of updates in addition to the matching criteria. Here's an example using updateOne(), but updateMany() works similarly. updateOne() returns a document that contains information about how many documents were matched and how many were actually modified.
1 /* 2 Update a record using a native MongoDB Query 3 */ 4 Route::get('/update_native/', function (Request $request) { 5 $mdb_collection = DB::connection('mongodb')->getCollection('laracoll'); 6 $match = ['guid' => 'cust_2222']; 7 $update = ['$set' => ['first_name' => 'Henry', 'address.street' => '777 new street name'] ]; 8 $result = $mdb_collection->updateOne($match, $update ); 9 return ['msg' => 'executed', 'matched_docs' => $result->getMatchedCount(), 'modified_docs' => $result->getModifiedCount()]; 10 });
Deleting documents is as easy as finding them. Again, there's a matching criterion, and the API returns a document indicating the number of deleted documents.
1 Route::get('/delete_native/', function (Request $request) { 2 $mdb_collection = DB::connection('mongodb')->getCollection('laracoll'); 3 $match = ['guid' => 'cust_2222']; 4 $result = $mdb_collection->deleteOne($match ); 5 return ['msg' => 'executed', 'deleted_docs' => $result->getDeletedCount() ]; 6 });
Since we now have access to the MongoDB native API, let's introduce the aggregation pipeline. An aggregation pipeline is a task in MongoDB's aggregation framework. Developers use the aggregation framework to perform various tasks, from real-time dashboards to "big data" analysis.
We will likely use it to query, filter, and sort data at first. The aggregations introduction of the free online book Practical MongoDB Aggregations by Paul Done gives a good overview of what can be done with it.
An aggregation pipeline consists of multiple stages where the output of each stage is the input of the next, like piping in Unix.
We will use the "sample_mflix" sample database that should have been loaded when creating our Atlas cluster. Laravel lets us access multiple MongoDB databases in the same app, so let's add the sample_mflix database (to
database.php
):1 'mongodb_mflix' => [ 2 'driver' => 'mongodb', 3 'dsn' => env('DB_URI'), 4 'database' => 'sample_mflix', 5 ],
Next, we can build an /aggregate/ API endpoint and define a three-stage aggregation pipeline to fetch data from the "movies" collection, compute the average movie rating per genre, and return a list. More details about this movie ratings aggregation.
1 Route::get('/aggregate/', function (Request $request) { 2 $mdb_collection = DB::connection('mongodb_mflix')->getCollection('movies'); 3 4 $stage0 = ['$unwind' => ['path' => '$genres']]; 5 $stage1 = ['$group' => ['_id' => '$genres', 'averageGenreRating' => ['$avg' => '$imdb.rating']]]; 6 $stage2 = ['$sort' => ['averageGenreRating' => -1]]; 7 8 $aggregation = [$stage0, $stage1, $stage2]; 9 $mdb_cursor = $mdb_collection->aggregate( $aggregation ); 10 return ['msg' => 'executed', 'data' => $mdb_cursor->toArray() ]; 11 });
This shows how easy it is to compose several stages to group, compute, transform, and sort data. This is the preferred method to perform aggregation operations, and it's even possible to output a document, which is subsequently used by the updateOne() method. There's a whole aggregation course.
We now know how to perform CRUD operations, native queries, and aggregations. However, don't forget about indexing to increase performance. MongoDB indexing strategies and best practices are beyond the scope of this article, but let's look at how we can create indexes.
First, we can use Eloquent's Migrations. Even though we could do without Migrations because we have a flexible schema, they could be a vessel to store how indexes are defined and created.
Since we have not used the --migration option when creating the model, we can always create the migration later. In this case, we can run this command:
Since we have not used the --migration option when creating the model, we can always create the migration later. In this case, we can run this command:
php artisan make:migration create_customer_mongo_db_table
It will create a Migration located at
<project>/database/migrations/YYYY_MM_DD_xxxxxx_create_customer_mongo_db_table.php
.We can update the code of our up() function to create an index for our collection. For example, we'll create an index for our 'guid' field, and make it a unique constraint. By default, MongoDB always has an _id primary key field initialized with an ObjectId by default. We can provide our own unique identifier in place of MongoDB's default ObjectId.
1 public function up() { 2 Schema::connection('mongodb')->create('laracoll', function ($collection) { 3 $collection->unique('guid'); // Ensure the guid is unique since it will be used as a primary key. 4 }); 5 }
As previously, this migration
up()
function can be executed using the command:php artisan migrate --path=/database/migrations/2023_08_09_051124_create_customer_mongo_db_table.php
If the 'laracoll' collection does not exist, it is created and an index is created for the 'guid' field. In the Atlas GUI, it looks like this:
The second option is to use the native MongoDB createIndex() function which might have new options not yet covered by the Laravel MongoDB package. Here's a simple example that creates an index with the 'guid' field as the unique constraint.
1 Route::get('/create_index/', function (Request $request) { 2 3 $indexKeys = ["guid" => 1]; 4 $indexOptions = ["unique" => true]; 5 $result = DB::connection('mongodb')->getCollection('laracoll')->createIndex($indexKeys, $indexOptions); 6 7 return ['msg' => 'executed', 'data' => $result ]; 8 });
Finally, we can also create an Index in the web Atlas GUI interface, using a visual builder or from JSON. The GUI interface is handy for experimenting. The same is true inside MongoDB Compass, our MongoDB GUI application.
This article covered creating a back-end service with PHP/Laravel, powered by MongoDB, for a front-end web application. We've seen how easy it is for Laravel developers to leverage their existing skills with a MongoDB back end.
It also showed why the document model, associated with good data modeling, leads to higher database efficiency and scalability. We can fully use it with the native MongoDB Query API to unlock the full power of MongoDB to create better apps with less downtime.
Learn more about the Laravel MongoDB extension syntax by looking at the official documentation and repo's example tests on GitHub. For plain PHP MongoDB examples, look at the example tests of our PHP Library.
Consider taking the MongoDB Data Modeling Path at MongoDB University or the overall PHP/MongoDB course, although it's not specific to Laravel.
We will build more PHP/Laravel content, so subscribe to our various channels, including YouTube and LinkedIn. Finally, join our official community forums! There's a PHP tag where fellow developers and MongoDB engineers discuss all things data and PHP.