district5/mondoc

Mondoc 是一个轻量级、易于使用的 MongoDB 模型和服务的抽象库

6.5.0 2024-09-04 06:53 UTC

README

CI

Composer...

使用 composer 将此库作为项目依赖项添加。

composer require district5/mondoc

用法...

设置连接...

MondoConnections 对象是一个单例。请在您的代码中的某个位置设置此对象以初始化连接,并在您的服务中定义 protected static function getConnectionId(): string 以返回相关模型的正确标识符。

<?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,
//     ]
// );

数据模型

<?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;
    }
}

此外,在模型中,您可以使用 $mondocFieldAliases 属性将字段从数据库映射到模型,以在存储中保持键短。这虽然是可选的,但在某些情况下可能很有用。以下是一个示例

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...
}
可选特性...
  • MondocVersionedModelTrait - 简单地对数据进行版本控制。
    • 您可以通过使用 \District5\Mondoc\Db\Model\Traits\MondocVersionedModelTrait 特性,轻松地对模型内的数据进行版本控制。此特性在模型中引入了一个 _v 变量,您可以选择在需要时递增。
      • 您可以通过在模型上调用 isVersionableModel() 来检测模型是否有版本。
  • MondocRevisionNumberTrait - 为模型添加一个 _rn 属性,用作修订号。此值在模型首次保存时自动设置为 1,每次更新模型时都会递增。您可以通过在继承模型上调用 setRevisionNumber 方法手动分配值到 _rn 属性来覆盖此行为。这与版本化模型特性不同,因为修订号在每次模型更新时都会递增,而不管所做的更改是什么。
    • 您可以通过在模型上调用 isRevisionNumberModel() 来检测模型是否有修订号。
  • MondocCreatedDateTrait - 为模型添加一个 cd 属性,用作创建日期。
    • 此值在模型首次保存时自动分配给当前的 UTC 日期,或者如果现有模型更新且 cd 属性尚未设置。您可以通过分配值到 cd 属性来覆盖此行为。
  • MondocModifiedDateTrait - 为模型添加一个 md 属性,用作更新日期。
    • 此值自动分配给当前的 UTC 日期,但在保存之前,您可以分配值到 md 属性来覆盖此行为。
  • MondocCloneableTrait - 为模型添加一个 clone 方法,该方法将返回一个具有与原始模型相同属性的新模型实例。当调用 ->clone 时,您可以传递一个布尔值来指示是否要将新模型持久化到数据库。可选的第二个参数是要克隆的对象或类。例如,您可以通过调用 $myModel->clone( < save:bool > , OtherModel::class) 来创建 MyModel 的一个克隆并将其转换为 OtherModel
  • MondocRetentionTrait - 将此特性添加到您的模型中,将公开 setMondocRetentionChangeMetasetMondocRetentionExpiry 方法,允许您设置模型的保留数据。这对于设置诸如保留期限和保留策略等事物很有用。

特性示例

<?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...
}

服务层

查询数据库的逻辑始终在服务层中执行。只有一个必需的方法,即 getCollectionName,它应返回数据库中集合的名称。

可选地,您可以为 getConnectionId 方法定义,以返回从 MondocConfig 连接管理器中使用的连接 ID。如果您正在使用多个连接,例如,用于身份验证的连接和用于主应用的连接,这非常有用。

Mondoc 原生查询在查询集合时自动将 DateTime 对象转换为 UTCDateTime 对象。

请注意:在 6.3.0 版本之前,PaginationTrait 需要您在每次方法调用中传递筛选器。现在不再需要,因为筛选器现在通过 MondocPaginationHelper 对象传递。

<?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.
    }
}

嵌套对象

您可以在彼此中嵌套对象。主要模型必须扩展 \District5\Mondoc\Db\Model\MondocAbstractModel,并且子模型必须在 $mondocNested 数组中定义。

子模型必须扩展 \District5\Mondoc\Db\Model\MondocAbstractSubModel

在实现 $mondocNested 时,您声明一个单个嵌套模型,或一个嵌套模型的数组。例如

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
];

请注意:在 5.1.0 版本之前,任何嵌套属性都必须在属性定义中包含 BSONDocumentBSONArray。现在不再需要,因为库将自动正确地膨胀类(或类)。

嵌套对象,无论深度如何,也可以利用 $mondocFieldAliases 属性将数据库字段映射到模型中。这可以在存储中保持键短,同时允许模型中使用更长的、更具描述性的键。对于上面的示例,您可以有如下所示

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();
    }
}

查找文档...

<?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']);

模型到数组...

您可以通过在模型上调用 asArray() 将模型导出为数组。这将返回模型的属性数组。

asArray() 方法返回的属性是已在模型上设置的属性和类型,这意味着它们可能无法直接编码为 JSON。为了解决这个问题,您可以在模型上调用 asJsonEncodableArray(),这将返回可以编码为 JSON 的数组。可选地,您还可以提供一个要从中省略字段的列表。

/* @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;

有用信息...

要使用预定义的 ObjectId 作为文档 _id,您可以调用模型上的 setPresetObjectId。这将强制模型吸收此 ObjectId,并在插入时不会生成新的 ObjectId。例如

<?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

此外,还有一个名为 assignDefaultVars 的方法,可以用于将默认值分配给模型属性。这对于设置属性默认值非常有用。在膨胀发生之后调用 assignDefaultVars,因此请注意属性可能已经分配了值。例如

<?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;
        }
    }
}

类型之间的转换

MongoDB 使用 BSON 类型进行数据。此库包含一个 MondocTypes 辅助工具,可以协助转换这些原生类型。

<?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'
]);

数据保留

当使用 MondocRetentionTrait 特性时,您可以通过调用 setMondocRetentionChangeMeta 来设置模型的保留数据。没有预设的保留数据,因此您必须自己设置。这有助于设置诸如发起更改的用户名称之类的信息,或者用于合规性和保留策略。此外,公开了 setMondocRetentionExpiry 方法,可以用于设置保留数据的过期日期。当保留期过期时,库不会自动删除数据,但您可以使用 MondocRetentionService 查询已过期的数据,使用 getPaginatorForExpiredRetentionForClassNamegetPaginatorForExpiredRetentionForObject,然后使用 getRetentionPage 方法。

MondocRetentionService 中公开的方法

  • create - 创建新的保留模型。当模型包含 MondocRetentionTrait 特性时,Mondoc 会自动调用此方法。
  • createStub - 创建新的保留模型,但不保存。这对于创建希望稍后保存的保留模型很有用。在插入多个模型时使用此方法。
  • getLatestRetentionModelForModel - 获取给定(之前已保存)模型的最新保留模型。
  • countRetentionModelsForClassName - 统计给定类名的保留模型数量。
  • countRetentionModelsForModel - 统计给定(之前已保存)模型的保留模型数量。
  • getRetentionHistoryPaginationHelperForClassName - 获取给定类名的保留历史记录分页助手。
  • getRetentionHistoryPaginationHelperForModel - 获取给定(之前已保存)模型的保留历史记录分页助手。
  • addIndexes - 向保留集合添加索引。Mondoc 不会自动调用此方法,您必须手动通过您的应用程序调用。
  • hasIndexes - 检查索引是否已添加到保留集合。这是一个辅助方法,允许您检查索引是否已添加;如果没有,您可以调用 addIndexes 来添加它们。
  • getPaginatorForExpiredRetentionForClassName - 获取给定类名的保留历史记录分页助手,其中保留已过期。
  • getPaginatorForExpiredRetentionForObject - 获取给定(之前已保存)模型的保留历史记录分页助手,其中保留已过期。
  • getRetentionPage - 从分页助手获取保留模型的一页。这是您在通过 getPaginatorForExpiredRetentionForClassNamegetPaginatorForExpiredRetentionForObject 获取分页助手之后使用的方法。

MondoRetentionModel 中,以下方法可用

  • toOriginalModel - 获取与保留数据关联的原始模型。这将返回在保留数据保存时保存的数据已膨胀的模型。
  • getSourceModelData - 获取在保留数据保存时保存的数据。这将返回以数组格式保存的保留数据保存时保存的数据。
  • getSourceObjectId - 获取与保留数据关联的原始模型的 ObjectId。
  • getSourceObjectIdString - 以字符串形式获取与保留数据关联的原始模型的 ObjectId。
  • getSourceClassName - 获取与保留数据关联的原始模型的类名。
  • getRetentionData - 获取在保留数据保存时保存的保留数据,如通过 setMondocRetentionChangeMeta 的原始调用在 MondocRetentionTrait 中设置的。
  • getRetentionExpiry - 获取在保存保留数据时保存的保留到期日期,该日期由原始调用 setMondocRetentionExpiry 设置,包含在 MondocRetentionTrait 中。
  • hasRetentionExpired - 检查此保留模型是否已过期。如果保留数据已过期,则返回 true,如果没有过期,则返回 false

以下是一个使用保留特征的示例

<?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

查询构建

查询构建由 MondocBuilder 库处理 https://github.com/district-5/php-mondoc-builder

查询构建器不考虑模型的属性,而是考虑数据库的属性。这意味着它不会监听或遵循模型上设置的任何字段映射。

例如,此映射要求查询构建器使用 n 键,而不是 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

测试

您可以通过运行 composer install 然后运行 phpunit 来运行 PHPUnit 对库的测试。在此操作之前,您需要将 MONGO_CONNECTION_STRING 环境变量分配给有效的 MongoDB 连接字符串。