meta | gitName | |||||
---|---|---|---|---|---|---|
|
laravel-mongo-auto-sync |
This package provides a better support for MongoDB relationships in Laravel Projects. At low level all CRUD operations has been handled by jenssegers/laravel-mongodb
composer require offlineagency/laravel-mongo-auto-sync
Make sure you have the MongoDB PHP driver installed. You can find installation instructions at http://php.net/manual/en/mongodb.installation.php
This package | Laravel | Laravel MongoDB |
---|---|---|
1.x | 5.8.x | 3.5.x |
1.x | 6.x | 3.6.x |
2.x | 5.8.x | 3.5.x |
2.x | 6.x | 3.6.x |
2.x | 7.x | 3.7.x |
2.x | 8.x | 3.8.x |
2.x | 9.x | 3.9.x |
- Version 1: PHP 7.1, 7.2, 7.3
- Version 2: PHP 7.4+
- Sync changes between collection with relationships after CRUD operations
- EmbedsOne & EmbedsMany
//create a new Article with title "Game of Thrones" with Category "TV Series"
//assign data to $article
$article->save();
/*
Article::class {
'title' => 'Game of Thrones',
'category' => Category::class {
'name' => 'TV Series'
}
}
*/
//Retrieve 'TV Series' category
$category = Category::where('name', 'TV Series')->first();
/*
Category::class {
'name' => 'Game of Thrones',
'articles' => null
}
*/
The sub document article has not been updated with the new article. So you will need some extra code to write in order to see the new article it in the category page. The number of sync depends on the number of the relationships and on the number of the entry in every single EmbedsMany relationships.
Total updates = ∑ (entry in all EmbedsMany relationships) + ∑ (EmbedsOne relationships)
As you can see the lines of extra code can rapidly increase, and you will write many redundant code.
//create a new Article with title "Game of Thrones" with Category "TV Series"
$article->storeWithSync($request);
/*
Article::class {
'title' => 'Game of Thrones',
'category' => Category::class {
'name' => 'TV Series'
}
}
*/
//Retrieve 'TV Series' category
$category = Category::where('name', 'TV Series')->first();
/*
Category::class {
'name' => 'Game of Thrones',
'articles' => Article::class {
'title' => 'Game of Thrones'
}
}
*/
The sub document article has been updated with the new article, with no need of extra code 🎉
You can see the new article on the category page because the package synchronizes the information for you by reading the Model Setup.
These example can be applied for all write operations on the database.
- Referenced sub documents
- Handle sub document as Model in order to exploit Laravel ORM support during write operation (without sync feature)
- Handle referenced sub document as Model in order to exploit Laravel ORM support during write operation (without sync feature)
- Advance cast field support
- Blog: see demo here
- Ecommerce
- API System for mobile application o for generated static site
- Any projects that require fast read operations and (slow) write operations that can be run on background
To understand how the package works we see an example based on the following Model:
and the following MongoDB relationships:
- Article EmbedsMany Category
- Category EmbedsMany Article
- Article EmbedsOne PrimaryCategory
- PrimaryCategory EmbedsMany Article
Your model has to extend our MDModel class in order to use our extended methods for CRUD operations.
class Article extends MDModel
{
//
}
Add $items
attribute on your model class and fill it with a key-value array.
The key indicates the name of the field and the value contain its configuration parameters.
Below is a list of all possible configurations:
Field Types
Editable
Default value
Below is a list of all possible field types:
Array
Boolean
Date
Default
Double
GeoJSON Objects
Int
Multi language
Slug
String
NB:
The key is between brackets and the value is in boolean format.
ex: for Array type the key is is-array
Validation or casting of an array field of any type.
Validation or casting in to a boolean value.
-
String (is-md)
Validation or casting a date in a string format and save it in UTC Mongo date time. -
Carbon (is-carbon-date)
Validation or casting a Carbon instance date field and save it in UTC Mongo date time.
This is the default value that is assigned if no field type is defined. No validation or casting will be applied with this field type.
Validation or casting in to a double value.
Validation or casting in to GeoJSON Objects.
Validation or casting in to an int value.
Save an array structure with where the key is the current language 1 and the value is the string passed.
/*
Article::class {
'title' => [
'en_EN' => 'Today news',
'es_ES' => 'Noticias de hoy',
'it_IT' => 'Notizie di oggi',
.
.
.
'zh_CN'=> '今天新闻'
}
}
*/
Validation or casting in to a slugified string value.
Validation or casting in to a string value.
This feature prevents unexpected field update.
Common use case: slug field of an article.
NB:
The key is between brackets, and the value is in the boolean format.
ex: for Editable the key is is-array
This feature allows the setting of a default value when null is passed.
class Article extends MDModel
{
{...}
protected $mongoRelation = [
'categories' => [
'type' => 'EmbedsMany',
'model' => 'App\Models\MiniCategory',
'modelTarget' => 'App\Models\Category',
'methodOnTarget' => 'articlelist',
'modelOnTarget' => 'App\Models\MiniArticle',
]
];
}
This is the possible configurations:
-
key: This is the relation name on the current collection
-
type: indicate the type of the relationship, and it can be EmbedsOne or EmbedsMany
-
mode: (optional) currently not used
-
model: is the MiniModel of current collection
-
modelTarget: is the Model of the related collection
-
methodOnTarget: is the relation name on the model of the related collection
-
modelOnTarget: is the MiniModel of sub document of the target collection
If you want to exploit all the benefits of Laravel ORM you have to define the relationships this way:
<?php
class Article extends MDModel
{
{...}
public function category()
{
return $this->embedsOne('App\Models\MiniCategory'];
}
}
So now when you dump $article->category
you will get an instance of MiniCategory
instead of an array.
For more information about this feature you can check here
I suggest you to generate the PHP doc that takes to do a check when you save or update an object. To do it use GenerateModelDocumentation command. Run in your terminal:
php artisan model-doc:generate {collection_name}
::: tip You can write the collection_name with capital letter or small letter :::
The generated doc will be like this:
/**
*
* Plain Fields
*
* @property string $id
* @property array $title
* @property string $slug
* @property array $content
* @property $planned_date
*
* Relationship
*
* @property MiniCategory
*
**/
The command checks if the model exist in your project and if it doesn’t, it will print an error message like this:
Error: <collection_name> Model not found
You can also change your model path in this file config\laravel-mongo-auto-sync.php
:
<?php
return [
'model_path' => app_path() . '/Models',
'model_namespace' => 'App\Models',
'other_models' => [
'user' => [
'model_path' => app_path(),
'model_namespace' => 'App'
]
]
];
It allows you to keep the current project structure.
If you need to drop a collection you can use DropCollection command. Run in your terminal:
drop:collection {collection_name}
::: tip You can write the collection_name with capital letter or small letter :::
The command checks if the model exist in your project and if it doesn’t it print an error message like this:
Error: collection_name Model not found
You can also change your model path in this file config\laravel-mongo-auto-sync.php
:
<?php
return [
'model_path' => app_path() . '/Models',
'model_namespace' => 'App\Models',
'other_models' => [
'user' => [
'model_path' => app_path(),
'model_namespace' => 'App'
]
]
];
It allows you to keep the current project structure.
This command, which will be added probably in the next release (see Roadmap section), allow you to check if the relations will be saved in the right way. It makes sure the sub document exist in the current collection and on the related collection.
First of all you have to create a function store()
where you receive a $request
in input.
Now you have to declare a new article instance.
<?php
namespace App\Controller;
use App\Http\Controllers\Controller;
use App\Models\Aticle;
class ArticleController extends Controller
{
public function store($request)
{
$article = new Article;
$additional_parameters = [
'slug' => Str::slug($request->input('title'))
];
$options = [];
$article->storeWithSync($request, $additional_parameters, $options);
}
}
You can pass to storeWithSync() two parameters:
- $request that is an instance of Request. If your request key is present on the $items array (see Model Setup section) the value will be stored to database with no extra code.
- $additional_parameters is an (optional) key-value array. You can pass here other fields, that can be stored to database.
- $options this is an (optional) key-value array. You can pass here advance options. This is the possibile values:
- 'partial-request' boolean
Now you have to add the relationships.
You need a json that contains:
- EmbedsOne: an array with an object that has all the fields of the MiniModel;
- EmbedsMany: an array with an object for each, in this case, category that contains all the fields of the MiniModel.
We choose Json format to ease integration with frontend.
For example, you can create new functions called getCategories
and getPrimaryCategory
as following:
<?php
namespace App\Controller;
use DateTime;
use App\Models\Article;
use App\Models\Category;
use Illuminate\Support\Str;
use MongoDB\BSON\UTCDateTime;
use App\Http\Controllers\Controller;
use stdClass;
class ArticleController extends Controller
{
public function store($request)
{
$article = new Article;
$arr = [
'slug' => Str::slug($request->input('title')),
'creation_date' => new UTCDateTime(new DateTime('now')),
'categories' => $this->getCategories($request->categories_id),
'primaryCategory' => $this->getPrimaryCategories($request->categories_id)
];
$article->storeWithSync($request, $arr);
}
public function getCategories($categories_id)
{
$arr = [];
$i = 0;
if ($categories_id != null) {
foreach ($categories_id as $category_id) {
$category = Category::find($category_id);
$newCategory = new stdClass();
$newCategory->ref_id = $category->id;
$newCategory->name = $category->name[cl()];
$newCategory->description = $category->description[cl()];
$arr[$i] = $newCategory;
$i++;
}
return json_encode($arr);
} else {
return null;
}
}
public function getPrimaryCategories($categories_id)
{
if ($categories_id != null && count($categories_id) > 0) {
$category = Category::find($categories_id[0]);
$newCategory = new stdClass();
$newCategory->ref_id = $category->id;
$newCategory->name = $category->name[cl()];
$newCategory->description = $category->description[cl()];
return json_encode($newCategory);
}else {
return null;
}
}
}
First of all you have to create an update()
function where you receive $request
and $id
in input.
After that you can search the article passing the $id
as parameter on find()
method.
If you want to edit an extra field you can create an array which contains this fields:
<?php
namespace App\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Article;
class ArticleController extends Controller
{
public function update($id, $request)
{
$article = Article::find($id);
$arr = [
'slug' => Str::slug($request->input('title'))
];
}
}
This is the supported relationships type:
- EmbedsOne: an array with an object that has all the fields of the MiniModel;
- EmbedsMany: an array with an object for each, in this case, category that contains all the fields of the MiniModel.
We choose Json format to ease integration with frontend.
For example, you can create new functions called getCategories
and getPrimaryCategory
as following:
<?php
namespace App\Controller;
use App\Models\Article;
use App\Models\Category;
use Illuminate\Support\Str;
use MongoDB\BSON\UTCDateTime;
use App\Http\Controllers\Controller;
use stdClass;
class ArticleController extends Controller
{
public function update($request)
{
$article = Article::find($id);
$arr = [
'slug' => Str::slug($request->input('title')),
'categories' => $this->getCategories($request->categories_id),
'primaryCategory' => $this->getPrimaryCategories($request->categories_id)
];
$article->updateWithSync($request, $arr);
}
public function getCategories($categories_id)
{
$arr = [];
$i = 0;
if ($categories_id != null) {
foreach ($categories_id as $category_id) {
$category = Category::find($category_id);
$newCategory = new stdClass();
$newCategory->ref_id = $category->id;
$newCategory->name = $category->name[cl()];
$newCategory->description = $category->description[cl()];
$arr[$i] = $newCategory;
$i++;
}
return json_encode($arr);
} else {
return null;
}
}
public function getPrimaryCategories($categories_id)
{
if ($categories_id != null && count($categories_id) > 0) {
$category = Category::find($categories_id[0]);
$newCategory = new stdClass();
$newCategory->ref_id = $category->id;
$newCategory->name = $category->name[cl()];
$newCategory->description = $category->description[cl()];
return json_encode($newCategory);
}else {
return null;
}
}
}
If you need to edit only a partition of items and relationships you can pass 'request_type' => 'partial'
on the $options
to the updateWithsSync()
method.
This configuration will disable all exceptions triggered by a missing field, and it will skip the field/relationships processing.
<?php
namespace App\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Article;
class ArticleController extends Controller
{
public function update($id, $request)
{
$article = Article::find($id);
$arr = [
'slug' => Str::slug($request->input('title')) . '-updated'
];
$options = [
'request_type' => 'partial'
];
}
}
Now you can save your changes:
<?php
namespace App\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Article;
class ArticleController extends Controller
{
public function update($id, $request)
{
$article = Article::find($id);
$arr = [
'slug' => Str::slug($request->input('title')) . '-updated'
];
$options = [
'request_type' => 'partial'
];
$article->updateWithSync($request, $arr, $options);
}
}
First of all you have to create an destroy()
function where you receive $id
in input.
After that you can search the article passing the $id
as parameter on find()
method.
With the destroyWithSync()
method you will remove the current instance from database and it will also search for any sub documents in other collection.
If you are familiar with Mysql you can find this feature similar to ON DELETE CASCADE.
<?php
namespace App\Controller;
use App\Http\Controllers\Controller;
use App\Models\Aticle;
class ArticleController extends Controller
{
public function destroy($id)
{
$article = new Article;
$article = $article->find($id);
$article->destroyWithSync();
}
}
- Refactor target synchronization to Observer pattern, so all this operation can be run on background using Laravel Queue System. This will also speed up all the operations in the collection that is primary involved in write operations.
- Command Analyse Database: This command will analyse the database in order to find some relationship error. Ex: An article with a category associated that is not present on the Category's sub document.
- Refactor save() method in order to handle CRUD operation on relationship also without sync.
- Support for referenced relationships.
- Better support for all field types.
- DestroyWithSync() without delete sub documents on other collections.
- Add more tests.
- Nested relationships.
- Benchmark MongoDB vs Mysql (write and read operation).
- Fix typo errors.
Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the package? Feel free to create an issue on GitHub, we’ll try to address it as soon as possible.
If you’ve found a bug regarding security please mail [email protected] instead of using the issue tracker.
[Offline Agency is an agency based in Padua, Italy.
Open source software is used in all projects we deliver. This is just a few of the free pieces of software we use every single day. For this, we are very grateful. When we feel we have solved a problem in a way that can help other developers, we release our code as open source software on GitHub.
This package was made by Giacomo Fabbian. There are many other contributors who devoted time and effort to make this package better.
Footnotes
-
See Laravel docs here to understand how localization works. ↩