district5 / mondoc
Mondoc is a lightweight and easy to use model and service based abstraction library for MongoDB
Requires
- php: >=8.1.0
- ext-mongodb: *
- district5/date: *
- district5/mondoc-builder: *
- mongodb/mongodb: *
Requires (Dev)
- phpunit/phpunit: 9.*
This package is auto-updated.
Last update: 2024-12-16 09:15:42 UTC
README
Composer...
Use composer to add this library as a dependency onto your project.
composer require district5/mondoc
Usage...
Setting up connections...
The MondoConnections
object is a singleton. Set this up somewhere in your code to initialise the connection, and
within your services you can define protected static function getConnectionId(): string
to return the correct
identifier for the relevant model.
<?php use District5\Mondoc\MondocConfig; use MongoDB\Client; $connection = new Client('< mongo connection string >'); $database = $connection->selectDatabase('< database name >'); $config = MondocConfig::getInstance(); $config->addDatabase( $database, 'default' // a connection identifier ('default' is the default value). ); // Add another one for something else... // $config->addDatabase( // $database, // 'authentication' // ); $config->addServiceMapping( MyModel::class, // You can also just use a string like '\MyNamespace\Model\MyModel' MyService::class // You can also just use a string like '\MyNamespace\Service\MyService' ); // Or you can use... // $config->setServiceMap( // [ // MyModel::class => MyService::class, // Also replaceable by strings // AnotherModel::class => AnotherService::class, // ] // );
The data model
<?php namespace MyNs\Model; use District5\Mondoc\Db\Model\MondocAbstractModel; /** * Class MyModel * @package MyNs\Model */ class MyModel extends MondocAbstractModel { /** * @var string */ protected $name = null; // This is optional, but can be used to map fields from the database to the model to keep keys short in storage. protected array $mondocFieldAliases = [ 'n' => 'name', // the `name` value would be stored in the database as the key `n` ]; /** * @return string */ public function getName(): ?string { return $this->name; } /** * @param string $val * @return $this */ public function setName(string $val) { $this->name = trim($val); return $this; } }
Additionally, in the model you can leverage the $mondocFieldAliases
property to map fields from the database to the
model to keep keys short in storage. This is optional, but can be useful in some cases. An example of this is shown
below:
namespace MyNs\Model; use District5\Mondoc\Db\Model\MondocAbstractModel; class MyModel extends MondocAbstractModel { protected string $name = null; protected array $mondocFieldAliases = [ 'n' => 'name', ]; // Rest of your model code... }
Optional traits...
MondocVersionedModelTrait
- Version your data easily.- You can easily version data within a model by using the
\District5\Mondoc\Db\Model\Traits\MondocVersionedModelTrait
trait. This trait introduces a_v
variable in the model, which you can choose to increment when you choose.- You can detect if a model has a version by calling
isVersionableModel()
on the model.
- You can detect if a model has a version by calling
- You can easily version data within a model by using the
MondocRevisionNumberTrait
- Adds an_rn
property to a model to utilise as a revision number. This value is automatically set to1
upon initial save of a model, and is incremented each time the model is updated. You can override this behaviour by manually assigning a value to the_rn
property by callingsetRevisionNumber
method on your inheriting model. This is different from the versioned model trait, as the revision number is incremented each time the model is updated, regardless of the changes made.- You can detect if a model has a revision number by calling
isRevisionNumberModel()
on the model.
- You can detect if a model has a revision number by calling
MondocCreatedDateTrait
- Adds acd
property to a model to utilise as a created date.- This value is automatically assigned to the current UTC date upon initial save of a model,
or if an existing model is updated and the
cd
property has not been set. You can override this behaviour by assigning a value to thecd
property.
- This value is automatically assigned to the current UTC date upon initial save of a model,
or if an existing model is updated and the
MondocModifiedDateTrait
- Adds amd
property to a model to utilise as an updated date.- This value is automatically assigned the current UTC date, but you can override this
behaviour by assigning a value to the
md
property prior to saving.
- This value is automatically assigned the current UTC date, but you can override this
behaviour by assigning a value to the
MondocCloneableTrait
- Adds aclone
method to the model, which will return a new instance of the model with the same properties as the original. Optionally, when calling->clone
you can pass a boolean to indicate if you want to persist the new model to the database. The optional second parameter is the object or class to clone to. For example, you can make a clone ofMyModel
and convert it toOtherModel
by calling$myModel->clone( < save:bool > , OtherModel::class)
.MondocRetentionTrait
- Adding this to your model exposes thesetMondocRetentionChangeMeta
andsetMondocRetentionExpiry
methods, which allows you to set the retention data for the model. This is useful for setting things such as the retention period and the retention policy.
Traits examples
<?php class MyModel extends \District5\Mondoc\Db\Model\MondocAbstractModel { use \District5\Mondoc\Db\Model\Traits\MondocCreatedDateTrait; use \District5\Mondoc\Db\Model\Traits\MondocModifiedDateTrait; use \District5\Mondoc\Db\Model\Traits\MondocCloneableTrait; use \District5\Mondoc\Db\Model\Traits\MondocRevisionNumberTrait; use \District5\Mondoc\Db\Model\Traits\MondocVersionedModelTrait; use \District5\Mondoc\Db\Model\Traits\MondocRetentionTrait; // Rest of your model code... }
The service layer
The logic for querying the database is always performed in the service layer. There's only a single required method,
getCollectionName
, which should return the name of the collection in the database.
Optionally, you can define a getConnectionId
method to return the connection ID to use from the MondocConfig
connection manager. This is useful if you're using multiple connections, for example, a connection for authentication
and a connection for the main application.
All Mondoc native queries automatically convert DateTime
objects to UTCDateTime
objects when querying a collection.
Please note: in versions prior to 6.3.0 the
PaginationTrait
required you to pass the filter into each method call. This is no longer required, as the filter is carried through by theMondocPaginationHelper
object now.
<?php namespace Myns\Service; use MyNs\Model\MyModel; use District5\Mondoc\Db\Service\MondocAbstractService /** * Class MyService * @package MyNs\Service */ class MyService extends MondocAbstractService { /** * Get the collection name. * * @return string */ protected static function getCollectionName(): string { return 'users'; } /** * Get the connection ID to use from the MondocConfig manager. Defaults to 'default'. * This method isn't required if you're using the default connection, but if you're using * multiple connections, you can use this method to return the connection ID. * * @return string */ protected static function getConnectionId() : string { return 'default'; // this is the default connection. } }
Nesting objects
You can nest objects in each other. The main model must extend \District5\Mondoc\Db\Model\MondocAbstractModel
and have
the sub models defined in the $mondocNested
array.
Sub models must extend \District5\Mondoc\Db\Model\MondocAbstractSubModel
.
When implementing $mondocNested
, you declare a single nested model, or an array of nested models. For example:
protected Food $favouriteFood; protected array $allFoods; // Array of 'Food' objects protected array $mondocNested = [ 'favouriteFood' => Food::class, // Single nested model 'allFoods' => Food::class . '[]' // Array of nested models ];
Please note: in versions prior to 5.1.0 any nested property was required to have
BSONDocument
orBSONArray
as part of the property definition. This is no longer required as the library will automatically inflate the class(es) correctly
Nested objects, regardless of depths, can also take advantage of the $mondocFieldAliases
property to map fields from the
database to the model. This keeps the keys short in storage, while allowing for longer, more descriptive keys in the
model. For the above example, you could have the following:
protected Food $favouriteFood; protected array $allFoods; // Array of 'Food' objects protected array $mondocFieldAliases = [ 'ff' => 'favouriteFood', 'af' => 'allFoods' ];
use District5\Mondoc\Db\Model\MondocAbstractModel; use District5\Mondoc\Db\Model\MondocAbstractSubModel; class FavouriteFood extends MondocAbstractSubModel { protected string $foodName; protected string $foodDescription; // This is optional, but can be used to map fields from the database to the model to keep keys short in storage protected array $mondocFieldAliases = [ 'fd' => 'foodDescription', ]; public function getFoodName() { return $this->foodName; } public function getFoodDescription() { return $this->foodDescription; } } class Car extends MondocAbstractSubModel { protected string|null $brand = null; protected string|null $colour = null; public function getBrand(): ?string { return $this->brand; } public function getColour(): ?string { return $this->colour; } } class Person extends MondocAbstractModel { /** * @var string|null */ protected string|null $name = null; /** * @var FavouriteFood */ protected FavouriteFood|null $favouriteFood = null; // Having BSONDocument here is important as inflation will use the property /** * @var FavouriteFood[] */ protected array $allFoods = []; // Having BSONArray here is important as inflation will use the property /** * @var Car */ protected Car|null $car = null; // Having BSONDocument here is important as inflation will use the property /** * @var string[] */ protected array $mondocNested = [ 'allFoods' => FavouriteFood::class . '[]', // Indicates an array of FavouriteFood objects 'favouriteFood' => FavouriteFood::class, 'car' => Car::class ]; public function getAllFoods(): array { return $this->allFoods; } public function getFavouriteFoodName(): ?string { return $this->favouriteFood->getFoodName(); } public function getCarBrand(): ?string { return $this->car->getBrand(); } public function getCarColour(): ?string { return $this->car->getColour(); } }
Finding documents..
<?php // count documents matching a filter \District5Tests\MondocTests\TestObjects\MyService::countAll([], []); // count documents using a query builder $builder = \District5Tests\MondocTests\TestObjects\MyService::getQueryBuilder(); \District5Tests\MondocTests\TestObjects\MyService::countAllByQueryBuilder($builder); // get single model by id, accepts a string or ObjectId \District5Tests\MondocTests\TestObjects\MyService::getById('the-mongo-id'); // get multiple models by ids. accepts string or ObjectIds \District5Tests\MondocTests\TestObjects\MyService::getByIds(['an-id', 'another-id']); // get single model with options \District5Tests\MondocTests\TestObjects\MyService::getOneByCriteria(['foo' => 'bar'], ['sort' => ['foo' => -1]]); // get multiple models with options \District5Tests\MondocTests\TestObjects\MyService::getMultiByCriteria(['foo' => 'bar'], ['sort' => ['foo' => -1]]); // working with dates, both of these queries are the same $phpDate = new \DateTime(); \District5Tests\MondocTests\TestObjects\MyService::getMultiByCriteria(['dateField' => ['$lte' => $phpDate]]); $mongoDate = \District5\Mondoc\Helper\MondocTypes::phpDateToMongoDateTime($phpDate); \District5Tests\MondocTests\TestObjects\MyService::getMultiByCriteria(['dateField' => ['$lte' => $mongoDate]]); // paginating results by page number $currentPage = 1; $perPage = 10; $sortByField = 'foo'; $sortDirection = -1; $pagination = \District5Tests\MondocTests\TestObjects\MyService::getPaginationHelper($currentPage, $perPage, ['foo' => 'bar']) $results = \District5Tests\MondocTests\TestObjects\MyService::getPage($pagination, $sortByField, $sortDirection); // Since 6.3.0 the filter is carried through by the pagination helper // paginating results by ID number descending (first page) $currentId = null; $perPage = 10; $sortDirection = -1; $pagination = \District5Tests\MondocTests\TestObjects\MyService::getPaginationHelperForObjectIdPagination($perPage, ['foo' => 'bar']) $results = \District5Tests\MondocTests\TestObjects\MyService::getPageByByObjectIdPagination($pagination, $currentId, $perPage, $sortDirection); // Since 6.3.0 the filter is carried through by the pagination helper // paginating results by ID number descending $currentId = '5f7deca120c41f29827c0c60'; // or new ObjectId('5f7deca120c41f29827c0c60'); $perPage = 10; $sortDirection = -1; $pagination = \District5Tests\MondocTests\TestObjects\MyService::getPaginationHelperForObjectIdPagination($perPage, ['foo' => 'bar']) $results = \District5Tests\MondocTests\TestObjects\MyService::getPageByByObjectIdPagination($pagination, $currentId, $perPage, $sortDirection); // Since 6.3.0 the filter is carried through by the pagination helper // paginating results by ID number ascending $currentId = '5f7deca120c41f29827c0c60'; // or new ObjectId('5f7deca120c41f29827c0c60'); $perPage = 10; $sortDirection = 1; $pagination = \District5Tests\MondocTests\TestObjects\MyService::getPaginationHelperForObjectIdPagination($perPage, ['foo' => 'bar']) $results = \District5Tests\MondocTests\TestObjects\MyService::getPageByByObjectIdPagination($pagination, $currentId, $perPage, $sortDirection); // Since 6.3.0 the filter is carried through by the pagination helper // get the distinct values for 'age' with a filter and options \District5Tests\MondocTests\TestObjects\MyService::getDistinctValuesForKey('age', ['foo' => 'bar'], ['sort' => ['age' => 1]]); // average age with filter \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getAverage('age', ['foo' => 'bar']); // 10% percentile, sorted asc with filter \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getPercentile('age', 0.1, 1, ['foo' => 'bar']); // get sum of a field with a given filter \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getSum('age', ['foo' => 'bar']); // get the min value of a field with a given filter \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getMin('age', ['foo' => 'bar']); // ...or with a string... // \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getMin('name', ['foo' => 'bar']); // get the max value of a field with a given filter \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getMax('age', ['foo' => 'bar']); // ...or with a string... // \District5Tests\MondocTests\TestObjects\MyService::aggregate()->getMax('name', ['foo' => 'bar']);
Model to array...
You can export a model to an array by calling asArray()
on the model.
This will return an array of the model's properties.
The properties returned by the asArray()
method are the properties and
types that have been set on the model, which means they're likely not able to be directly encoded to JSON. To get around
this, you can call asJsonEncodableArray()
on the model, which will return an array that can be encoded to JSON.
Optionally, you can provide a list of fields to omit from the returned array.
/* @var $model \District5\Mondoc\Db\Model\MondocAbstractModel */ $mongoInsertionDocument = $model->asArray(); // Not encodable to JSON $jsonEncodable = $model->asJsonEncodableArray(); // Encodable to JSON $jsonEncodable = $model->asJsonEncodableArray(['password', 'secret']); // Encodable to JSON omitting the 'password' and 'secret' properties $encodedJson = json_encode($jsonEncodable, JSON_THROW_ON_ERROR); echo $encodedJson;
Useful information...
To use a pre-determined ObjectId as the document _id
, you can call setPresetObjectId
against the model. This will
force the model to absorb this ObjectId and not generate a new one upon insertion. For example:
<?php /** @noinspection SpellCheckingInspection */ $theId = new \MongoDB\BSON\ObjectId('61dfee5591efcf44e023d692'); $person = new Person(); $person->setPresetObjectId(new ObjectId()); $insertOrUpdateOptions = []; $person->save($insertOrUpdateOptions); // optional echo $person->getObjectIdString(); // 61dfee5591efcf44e023d692
Additionally, there's a method called assignDefaultVars
which can be used to assign default values to the model's
properties. This is useful for setting default values for properties. The call to assignDefaultVars
occurs AFTER
inflation has occurred, so it's important to note that the properties may already have values assigned. For example:
<?php use District5\Mondoc\Db\Model\MondocAbstractModel; class MyModel extends MondocAbstractModel { protected string $name = null; protected int $version = 0; protected function assignDefaultVars() { if ($this->version < 1) { $this->version = 1; } } }
Converting between types
MongoDB uses BSON types for data. This library holds a MondocTypes
helper, which can assist in the conversion of these
native types.
<?php use \District5\Mondoc\Helper\MondocTypes; // Dates $mongoDateTime = MondocTypes::phpDateToMongoDateTime(new \DateTime()); $phpDateTime = MondocTypes::dateToPHPDateTime($mongoDateTime); // BSON documents $bsonDocument = new \MongoDB\Model\BSONDocument(['foo' => 'bar']); $phpArrayFromDoc = MondocTypes::arrayToPhp($bsonDocument); // BSON arrays $bsonArray = new \MongoDB\Model\BSONArray(['foo', 'bar']); $phpArrayFromArray = MondocTypes::arrayToPhp($bsonArray); // ObjectIds /** @noinspection SpellCheckingInspection */ $anId = '61dfee5591efcf44e023d692'; $objectId = MondocTypes::toObjectId($anId); // You can also pass existing ObjectId's into the conversion and nothing happens. // MondocTypes::toObjectId(new \MongoDB\BSON\ObjectId()); // MondocTypes::toObjectId($objectId); $string = MondocTypes::objectIdToString($objectId); // less used, but still handled... $objectId = MondocTypes::toObjectId([ '$oid' => '61dfee5591efcf44e023d692' ]); $objectId = MondocTypes::toObjectId([ 'oid' => '61dfee5591efcf44e023d692' ]);
Retention of data
When using the MondocRetentionTrait
trait, you can set the retention data for a model by calling
setMondocRetentionChangeMeta
. There is no preset retention data, so you must set this yourself. This is useful for
setting things such as the user's name who initiated the change, or similar for compliance and the retention policy.
Additionally, the method setMondocRetentionExpiry
method is exposed, and can be used to set the expiry date for the
retention data. The library does not automatically delete the data when the retention period has expired, but you can
use the MondocRetentionService
to query for data that has expired, using either
getPaginatorForExpiredRetentionForClassName
or getPaginatorForExpiredRetentionForObject
, and then subsequently the
getRetentionPage
method.
Exposed methods in the MondocRetentionService
:
create
- Create a new retention model. This is called automatically by Mondoc when a model contains theMondocRetentionTrait
trait.createStub
- Create a new retention model, but don't save it. This is useful for creating a retention model that you want to save at a later date. This is used when inserting multiple models.getLatestRetentionModelForModel
- Get the latest retention model for a given (previously saved) model.countRetentionModelsForClassName
- Count the number of retention models for a given class name.countRetentionModelsForModel
- Count the number of retention models for a given (previously saved) model.getRetentionHistoryPaginationHelperForClassName
- Get a pagination helper for the retention history for a given class name.getRetentionHistoryPaginationHelperForModel
- Get a pagination helper for the retention history for a given (previously saved) model.addIndexes
- Add the indexes to the retention collection. This is NOT called automatically by Mondoc, and must be called manually by your application.hasIndexes
- Check if the indexes have been added to the retention collection. This is a helper method to allow you to check if the indexes have been added, and if not, you can calladdIndexes
to add them.getPaginatorForExpiredRetentionForClassName
- Get a pagination helper for the retention history for a given class name, where the retention has expired.getPaginatorForExpiredRetentionForObject
- Get a pagination helper for the retention history for a given (previously saved) model, where the retention has expired.getRetentionPage
- Get a page of retention models from the pagination helper. This is the method you use after retrieving a pagination helper viagetPaginatorForExpiredRetentionForClassName
orgetPaginatorForExpiredRetentionForObject
.
Within a MondoRetentionModel
, the following methods are available:
toOriginalModel
- Get the original model that the retention data is associated with. This will return the model inflated with the data that was saved at the time of the retention data being saved.getSourceModelData
- Get the data that was saved at the time of the retention data being saved. This will return the data as it was saved at the time of the retention data being saved, in array format.getSourceObjectId
- Get the ObjectId of the original model that the retention data is associated with.getSourceObjectIdString
- Get the ObjectId of the original model that the retention data is associated with, as a string.getSourceClassName
- Get the class name of the original model that the retention data is associated with.getRetentionData
- Get the retention data that was saved at the time of the retention data being saved, as set by the original call tosetMondocRetentionChangeMeta
, contained in theMondocRetentionTrait
.getRetentionExpiry
- Get the retention expiry date that was saved at the time of the retention data being saved, as set by the original call tosetMondocRetentionExpiry
, contained in theMondocRetentionTrait
.hasRetentionExpired
- Check if this retention model has expired. This will returntrue
if the retention data has expired, andfalse
if it has not.
A working example of using the retention trait is shown below:
<?php use District5\Date\Date; use District5\Mondoc\Db\Model\MondocAbstractModel; use District5\Mondoc\Db\Model\Traits\MondocRetentionTrait; use District5\Mondoc\Helper\MondocTypes; use District5\Mondoc\Extensions\Retention\MondocRetentionService; class MyService extends MondocAbstractService { protected static function getCollectionName(): string { return 'data'; } } class MyModel extends MondocAbstractModel { use MondocRetentionTrait; protected string $name = null; public function setName(string $name): self { $this->name = $name; return $this; } } $model = new MyModel(); $model->setName('John Doe'); $model->setMondocRetentionData([ 'user' => 'joe.bloggs', ]); $model->setMondocRetentionExpiry( Date::modify( Date::nowUtc() )->plus()->days(30) ); $model->save(); // There is now both a `MyModel` saved, and a `MondocRetentionModel` saved with the retention data. $retrieved = MyService::getById($model->getObjectIdString()); $retrieved->setMondocRetentionData([ 'user' => 'jane.bloggs', ]); $retrieved->setMondocRetentionExpiry( null // this data will never expire ); $retrieved->save(); // A new `MondocRetentionModel` is saved with the updated retention data. There are now two `MondocRetentionModel`'s $paginator = MondocRetentionService::getRetentionHistoryPaginationHelperForClassName( MyModel::class, 1, 10, ['user' => 'joe.bloggs'] ); $results = MondocRetentionService::getPage( $paginator // The filter is carried through by the pagination helper ); // This will return the `MongoRetentionModel` for the `MyModel` with the user `joe.bloggs` $firstResultInflated = $results[0]->toOriginalModel(); echo $firstResultInflated->getName(); // John Doe
Query building
Query building is handled by the MondocBuilder
library https://github.com/district-5/php-mondoc-builder.
The query builder does not take into account for the model's properties, but rather the database's properties. This means that it will not listen or adhere to any field mappings that have been set on the model.
For example, this map would require the query builder to use the n
key, not name
:
<?php use District5\Mondoc\Db\Model\MondocAbstractSubModel; class MyModel extends MondocAbstractSubModel { protected array $mondocFieldAliases = [ 'n' => 'name', ]; // Rest of your model code... } $wontWork = new \District5\Mondoc\Db\Builder\MondocBuilder\MondocBuilder(); $wontWork->addFilter('name', 'John'); // This WILL NOT WORK with the field mapping $willWork = new \District5\Mondoc\Db\Builder\MondocBuilder\MondocBuilder(); $willWork->addFilter('n', 'John'); // This will work with the field mapping
Testing
You can run PHPUnit against the library by running composer install
and then running phpunit
. Before doing so,
you'll need to assign the MONGO_CONNECTION_STRING
environment variable to a valid MongoDB connection string.