sokil/php-mongo

PHP 对象文档映射器(ODM)用于 MongoDB

1.23.1 2019-07-08 19:52 UTC

README

SWUbanner

PHPMongo ODM

Total Downloads Build Status Coverage Status Scrutinizer Code Quality Code Climate Gitter

PHP 用于 MongoDB 的对象文档映射器。

⭐ 为什么使用这个ODM?你可以通过舒适的getter和setter轻松地处理文档数据,而不是数组,也不需要检查键是否存在于数组中。访问子文档使用点符号。你可以在保存文档之前验证传递给文档的数据。我们提供事件,你可以在文档生命周期的不同时刻处理这些事件。你可以创建关系,构建聚合,创建版本化文档,编写迁移并做更多使生活更轻松的事情。

🔢 在 MongoDB v.2.4.12, v.2.6.9, v.3.0.2, v.3.2.10, v.3.3.15, v.3.4.0 上进行了测试。详细信息请参阅 单元测试

ArmySOS - Help for Ukrainian Army

要求

  • PHP-Mongo v.1
  • PHP-Mongo v.2(目前处于开发中,无法在生产环境中使用)

目录



安装

常见安装

你可以通过 Composer 安装库

composer require sokil/php-mongo

下载最新版本:GitHub 最新源代码

与 PHP 7 的兼容性

PHPMongo当前基于旧的ext-mongo扩展。要使用此ODM与PHP 7配合,您需要添加alcaeus/mongo-php-adapter兼容层,该层在新的ext-mongodb扩展上实现了旧扩展的API。要开始使用PHPMongo与PHP7,请将alcaeus/mongo-php-adapter要求添加到composer中。有关使用兼容层的限制,您可以在原始适配器的已知问题中阅读。

需要适配旧ext-mongo API到新ext-mongodb

composer require alcaeus/mongo-php-adapter

Symfony 扩展包

如果您使用Symfony框架,可以使用Symfony MongoDB Bundle,它封装了这个库

composer require sokil/php-mongo-bundle

Laravel

如果您使用Laravel框架,请使用Laravel适配器

composer require phpmongokit/laravel-mongo-odm

Yii 组件

如果您使用Yii框架,可以使用Yii适配器,它封装了这个库

composer require sokil/php-mongo-yii

除了PHPMongo适配器之外,此包还包括MongoDb的数据提供者和日志路由器。

Yii2 组件

如果您使用Yii2框架,可以使用Yii2适配器,它封装了这个库

composer require phpmongokit/yii2-mongo-odm

迁移支持

如果您需要迁移,您可以将依赖添加到基于此库的Migrator

composer require sokil/php-mongo-migrator


连接

单连接

通过\Sokil\Mongo\Client类连接到MongoDB服务器

<?php
$client = new Client($dsn);

连接到服务器的DSN格式在PHP手册中描述。要连接到本地主机,使用以下DSN

mongodb://127.0.0.1

要连接到副本集,使用以下DSN

mongodb://server1.com,server2.com/?replicaSet=replicaSetName

连接池

如果您只有少量连接,您可能更喜欢连接池而不是管理不同的连接。使用\Sokil\Mongo\ClientPool实例初始化池对象

<?php

$pool = new ClientPool(array(
    'connect1' => array(
        'dsn' => 'mongodb://127.0.0.1',
        'defaultDatabase' => 'db2',
        'connectOptions' => array(
            'connectTimeoutMS' => 1000,
            'readPreference' => \MongoClient::RP_PRIMARY,
        ),
        'mapping' => array(
            'db1' => array(
                'col1' => '\Collection1',
                'col2' => '\Collection2',
            ),
            'db2' => array(
                'col1' => '\Collection3',
                'col2' => '\Collection4',
            )
        ),
    ),
    'connect2' => array(
        'dsn' => 'mongodb://127.0.0.1',
        'defaultDatabase' => 'db2',
        'mapping' => array(
            'db1' => array(
                'col1' => '\Collection5',
                'col2' => '\Collection6',
            ),
            'db2' => array(
                'col1' => '\Collection7',
                'col2' => '\Collection8',
            )
        ),
    ),
));

$connect1Client = $pool->get('connect1');
$connect2Client = $pool->get('connect2');


映射

选择数据库和集合

您可以通过其名称获取数据库和集合的实例。

获取数据库类\Sokil\Mongo\Database的实例

<?php
$database = $client->getDatabase('databaseName');
// or simply
$database = $client->databaseName;

获取集合类\Sokil\Mongo\Collection的实例

<?php
$collection = $database->getCollection('collectionName');
// or simply
$collection = $database->collectionName;

默认数据库可以指定,以便从\Sokil\Mongo\Client对象直接获取集合

<?php
$client->useDatabase('databaseName');
$collection = $client->getCollection('collectionName');

自定义集合

自定义集合用于在相关类中添加一些集合特定的功能。首先需要创建一个从\Sokil\Mongo\Collection扩展的类

<?php

// define class of collection
class CustomCollection extends \Sokil\Mongo\Collection
{

}

然后,将此类映射到集合名称,以便在请求集合时返回此类的对象。自定义集合按照标准方式引用

<?php
/**
 * @var \CustomCollection
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName');

集合定义

集合名称必须映射到集合类。如果您想向集合传递一些附加选项,您也可以在映射定义中配置它们

<?php
$client->map([
    'databaseName'  => [
        'collectionName' => [
            'class' => '\Some\Custom\Collection\Classname',
            'collectionOption1' => 'value1',
            'collectionOption2' => 'value2',
        ]
    ],
]);

所有选项都可以通过Collection::getOption()方法访问

<?php
// will return 'value1'
$client
    ->getDatabase('databaseName')
    ->getCollection('collectionName')
    ->getOption('collectionOption1');

预定义选项包括

如果省略了class,则使用标准\Sokil\Mongo\Collection类。

要覆盖默认文档类,请使用集合的documentClass选项

<?php
$client->map([
    'databaseName'  => [
        'collectionName' => [
            'documentClass' => '\Some\Document\Class',
        ]
    ],
]);

// is instance of \Some\Document\Class
$document = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName')
    ->createDocument();

集合名称到集合类的映射

如果仅定义了集合的类名,您可以在映射中简单地传递它。

<?php

// map class to collection name
$client->map([
    'databaseName'  => [
        'collectionName' => [
            'class' => \Acme\MyCollection',
        ],
    ],
]);

/**
 * @var \Acme\MyCollection
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName');

还有一种已弃用的方法可以指定集合的类名。请使用数组定义和选项class

<?php

// map class to collection name
$client->map([
    'databaseName'  => [
        'collectionName' => '\Acme\MyCollection'
    ],
]);

/**
 * @var \Acme\MyCollection
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName');

带类前缀的映射

未直接配置的集合可以自动通过在映射键中使用*进行映射。任何集合都可以映射到类,而无需枚举每个集合名称。

<?php
$client->map([
    'databaseName'  => [
        '*' => [
            'class' => '\Acme\Collection\Class\Prefix',
        ],
    ],
]);

/**
 * @var \Acme\Collection\Class\Prefix\CollectionName
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName');

/**
 * @var \Acme\Collection\Class\Prefix\CollectionName\SubName
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName.subName');

还可以使用已弃用的方法来指定类前缀。请使用*作为集合名称,并用带有class选项的数组定义。

<?php
$client->map([
    'databaseName'  => '\Acme\Collection\Class\Prefix',
]);

/**
 * @var \Acme\Collection\Class\Prefix\CollectionName
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName');

/**
 * @var \Acme\Collection\Class\Prefix\CollectionName\SubName
 */
$collection = $client
    ->getDatabase('databaseName')
    ->getCollection('collectionName.subName');

正则表达式映射

映射中的集合名称可以是正则表达式模式。模式必须以符号/开头

<?php
$database->map(array(
    '/someCollection(\d)/' => '\Some\Collection\Class',
));

任何名称与模式匹配的集合都将成为\Some\Collection\Class的实例

<?php
$col1 = $database->getCollection('someCollection1');
$col2 = $database->getCollection('someCollection2');
$col4 = $database->getCollection('someCollection4');

可以通过$collection->getOption('regex');获取存储的正则表达式值。

<?php
$database->map(array(
    '/someCollection(\d+)/' => '\Some\Collection\Class',
));
$col42 = $database->getCollection('someCollection42');
echo $col1->getOption('regexp')[0]; // someCollection42
echo $col1->getOption('regexp')[1]; // 42

文档模式和验证

自定义文档类

当需要在加载、获取或保存时对日期进行一些处理时,自定义文档类可能很有用。自定义文档类必须扩展\Sokil\Mongo\Document

<?php
class CustomDocument extends \Sokil\Mongo\Document
{

}

现在您必须通过覆盖方法Collection::getDocumentClassName()来配置其名称在集合的类中。

<?php
class CustomCollection extends \Sokil\Mongo\Collection
{
    public function getDocumentClassName(array $documentData = null) {
        return '\CustomDocument';
    }
}

单一集合继承

在单个集合中存储不同文档类的情况通常很有用。例如,您在商店中有产品SongVideoClip,它们继承自抽象的Product。它们有相同的字段,如作者或持续时间,但也可能有其他不同的字段和行为。这种情况在产品目录示例中描述。

您可以在\Sokil\Mongo\Collection::getDocumentClassName()中灵活地配置文档的类,相对于字段的实际值(这个字段称为判别器),或更复杂的逻辑

<?php
class CustomCollection extends \Sokil\Mongo\Collection
{
    public function getDocumentClassName(array $documentData = null) {
        return '\Custom' . ucfirst(strtolower($documentData['type'])) . 'Document';
    }
}

文档类也可以在集合映射中定义

<?php
$client->map([
    'databaseName'  => [
        'collectionName1' => [
            'documentClass' => '\CustomDocument',
        ],
        'collectionName2' => function(array $documentData = null) {
            return '\Custom' . ucfirst(strtolower($documentData['type'])) . 'Document';
        },
        'collectionName3' => [
            'documentClass' => function(array $documentData = null) {
                return '\Custom' . ucfirst(strtolower($documentData['type'])) . 'Document';
            },
        ],
    ],
]);

在上面的示例中,类\CustomVideoDocument{"_id": "45..", "type": "video"}相关联,而\CustomAudioDocument{"_id": "45..", "type": "audio"}相关联

文档模式

文档的方案是完全不必要的。如果字段是必需的并且有默认值,它可以在文档类的特殊属性Document::schema中定义

<?php
class CustomDocument extends \Sokil\Mongo\Document
{
    protected $schema = [
        'requiredField' => 'defaultValue',
        'someField'     => [
            'subDocumentField' => 'value',
        ],
    ];
}

还支持已弃用的格式Document::_data

<?php
class CustomDocument extends \Sokil\Mongo\Document
{
    protected $_data = [
        'requiredField' => 'defaultValue',
        'someField'     => [
            'subDocumentField' => 'value',
        ],
    ];
}


文档验证

在保存之前可以验证文档。要设置验证规则,您可以覆盖方法\Sokil\Mongo\Document::rules()并将验证规则传递到这里。支持的规则有

<?php
class CustomDocument extends \Sokil\Mongo\Document
{
    public function rules()
    {
        return array(
            array('email,password', 'required'),
            array('role', 'equals', 'to' => 'admin'),
            array('role', 'not_equals', 'to' => 'guest'),
            array('role', 'in', 'range' => array('admin', 'manager', 'user')),
            array('contract_number', 'numeric', 'message' => 'Custom error message, shown by getErrors() method'),
            array('contract_number' ,'null', 'on' => 'SCENARIO_WHERE_CONTRACT_MUST_BE_NULL'),
            array('code' ,'regexp', '#[A-Z]{2}[0-9]{10}#')
        );
    }
}

文档可以根据场景具有验证状态。场景可以通过方法Document::setScenario($scenario)指定。

<?php
$document->setScenario('register');

如果某些验证规则仅适用于某些场景,则必须在on键中传递这些场景,用逗号分隔。

<?php
public function rules()
    {
        return array(
            array('field' ,'null', 'on' => 'register,update'),
        );
    }

如果某些验证规则应用于除某些场景之外的所有场景,则必须在except键中传递这些场景,用逗号分隔。

<?php
public function rules()
    {
        return array(
            array('field' ,'null', 'except' => 'register,update'),
        );
    }

文档验证有两种相同的情况

try {
    $document->save();
} catch (\Sokil\Mongo\Document\InvalidDocumentException $e) {
    // get validation errors
    var_dump($document->getErrors());
    // get document instance from exception
    var_dump($e->getDocument()->getErrors());
}

if ($document->isValid())
    $document->save();
} else {
    var_dump($document->getErrors());
}

默认情况下,文档在保存之前进行验证,如果无效,则抛出\Sokil\Mongo\Document\InvalidDocumentException异常。在v.1.11.6之前,当文档无效时,会抛出异常\Sokil\Mongo\Document\Exception\Validate。从v.1.11.6开始,此异常已弃用。请使用\Sokil\Mongo\Document\InvalidDocumentException代替。

错误可以通过文档对象的Document::getErrors()方法访问。

还可以通过异常方法从异常中获取文档的实例

<?php
try {
    $document->save();
} catch(\Sokil\Mongo\Document\InvalidDocumentException $e) {
    $e->getDocument()->getErrors();
}

如果允许保存无效的文档,则在保存时禁用验证

$document->save(false);

可以通过调用方法triggerError($fieldName, $rule, $message)手动触发错误

<?php
$document->triggerError('someField', 'email', 'E-mail must be at domain example.com');

您可以通过向文档类添加方法并在方法名称中定义规则来添加您自己的验证规则

<?php
class CustomDocument extends \Sokil\Mongo\Document
{
    punlic function rules()
    {
        return array(
            array(
                'email',
                'uniqueFieldValidator',
                'message' => 'E-mail must be unique in collection'
            ),
        );
    }

    /**
     * Validator
     */
    public function uniqueFieldValidator($fieldName, $params)
    {
        // Some logic of checking unique mail.
        //
        // Before version 1.7 this method must return true if validator passes,
        // and false otherwise.
        //
        // Since version 1.7 this method return no values and must call
        // Document::addError() method to add error into stack.
    }
}

如果您想在几个类中使用验证器,则可以创建您自己的验证器类。只需从抽象验证器类\Sokil\Mongo\Validator扩展您的类,并注册您自己的验证器命名空间

<?php
namespace Vendor\Mongo\Validator;

/**
 * Validator class
 */
class MyOwnEqualsValidator extends \Sokil\Mongo\Validator
{
    public function validateField(\Sokil\Mongo\Document $document, $fieldName, array $params)
    {
        if (!$document->get($fieldName)) {
            return;
        }

        if ($document->get($fieldName) === $params['to']) {
            return;
        }

        if (!isset($params['message'])) {
            $params['message'] = 'Field "' . $fieldName . '" must be equals to "' . $params['to'] . '" in model ' . get_called_class();
        }

        $document->addError($fieldName, $this->getName(), $params['message']);
    }
}

/**
 * Registering validator in document
 */

class SomeDocument extends \Sokil\Mongo\Document
{
    public function beforeConstruct()
    {
        $this->addValidatorNamespace('Vendor\Mongo\Validator');
    }

    public function rules()
    {
        return array(
            // 'my_own_equals' converts to 'MyOwnEqualsValidator' class name
            array('field', 'my_own_equals', 'to' => 42, 'message' => 'Not equals'),
        );
    }
}


通过 id 获取文档

根据其id从集合中获取文档

<?php
$document = $collection->getDocument('5332d21b253fe54adf8a9327');

使用可调用对象添加额外的检查或查询修改器

<?php
$document = $collection->getDocument(
    '5332d21b253fe54adf8a9327',
    function(\Sokil\Mongo\Cursor $cursor) {
        // get document only if active
        $cursor->where('status', 'active');
        // slice embedded documents
        $cursor->slice('embdocs', 10, 30);
    }
);

请注意,如果指定了可调用对象,则文档始终直接加载,忽略文档池。

创建新文档

创建新的空文档对象

<?php
$document = $collection->createDocument();

或使用预定义的值

<?php
$document = $collection->createDocument([
    'param1' => 'value1',
    'param2' => 'value2'
]);


在文档中获取和设置数据

获取

要获取文档字段的值,可以使用以下方法之一

<?php
$document->requiredField; // defaultValue
$document->get('requiredField'); // defaultValue
$document->getRequiredField(); // defaultValue

$document->someField; // ['subDocumentField' => 'value']
$document->get('someField'); // ['subDocumentField' => 'value']
$document->getSomeField(); // ['subDocumentField' => 'value']
$document->get('someField.subDocumentField'); // 'value'

$document->get('some.unexisted.subDocumentField'); // null

如果字段不存在,则返回null值。

设置

要设置值,可以使用以下方法

<?php
$document->someField = 'someValue'; // {someField: 'someValue'}
$document->set('someField', 'someValue'); // {someField: 'someValue'}
$document->set('someField.sub.document.field', 'someValue'); // {someField: {sub: {document: {field: {'someValue'}}}}}
$document->setSomeField('someValue');  // {someField: 'someValue'}

推入

推入会将值添加到数组字段

<?php

$document->push('field', 1);
$document->push('field', 2);

$document->get('field'); // return [1, 2]

如果字段已存在且不是值列表,那么对于标量,标量将被转换为数组。如果值以子文档的形式推送到字段,则会触发 Sokil\Mongo\Document\InvalidOperationException



嵌套文档

获取嵌套文档

想象一下,你有一个文档,代表 User 模型

{
    "login": "beebee",
    "email": "beebee@gmail.com",
    "profile": {
        "birthday": "1984-08-11",
        "gender": "female",
        "country": "Ukraine",
        "city": "Kyiv"
    }
}

你可以将嵌入的 profile 文档定义为独立类

<?php

/**
 * Profile class
 */
class Profile extends \Sokil\Mongo\Structure
{
    public function getBirthday() { return $this->get('birthday'); }
    public function getGender() { return $this->get('gender'); }
    public function getCountry() { return $this->get('country'); }
    public function getCity() { return $this->get('city'); }
}

/**
 * User model
 */
class User extends \Sokil\Mongo\Document
{
    public function getProfile()
    {
        return $this->getObject('profile', '\Profile');
    }
}

现在你可以获取配置文件参数

<?php
$birthday = $user->getProfile()->getBirthday();

设置嵌套文档

你还可以设置嵌入的文档。如果嵌入的文档有验证规则,则它们将在嵌入文档之前进行检查

/**
 * Profile class
 */
class Profile extends \Sokil\Mongo\Structure
{
    public function getBirthday() { return $this->get('birthday'); }

    public function rules()
    {
        return array(
            array('birthday', 'required', 'message' => 'REQUIRED_FIELD_EMPTY_MESSAGE'),
        );
    }
}

/**
 * User model
 */
class User extends \Sokil\Mongo\Document
{
    public function setProfile(Profile $profile)
    {
        return $this->set('profile', $profile);
    }
}

如果嵌入的文档无效,它将抛出 Sokil\Mongo\Document\InvalidDocumentException。可以从异常对象获取嵌入的文档

try {
    $user->set('profile', $profile);
} catch (InvalidDocumentException $e) {
    $e->getDocument()->getErrors();
}

获取嵌套文档列表

想象一下,你在名为 'posts' 的集合中存储了帖子数据,帖子文档有嵌入的评论文档

{
    "title": "Storing embedded documents",
    "text": "MongoDb allows to atomically modify embedded documents",
    "comments": [
        {
            "author": "MadMike42",
            "text": "That is really cool",
            "date": ISODate("2015-01-06T06:49:41.622Z"
        },
        {
            "author": "beebee",
            "text": "Awesome!!!11!",
            "date": ISODate("2015-01-06T06:49:48.622Z"
        },
    ]
}

因此我们可以创建一个扩展 \Sokil\Mongo\StructureComment 模型

<?php
class Comment extends \Sokil\Mongo\Structure
{
    public function getAuthor() { return $this->get('author'); }
    public function getText() { return $this->get('text'); }
    public function getDate() { return $this->get('date')->sec; }
}

现在我们可以创建一个具有访问嵌入 Comment 模型的 Post 模型

<?php

class Post extends \Sokil\Mongo\Document
{
    public function getComments()
    {
        return $this->getObjectList('comments', '\Comment');
    }
}

Post::getComments() 方法允许你获取所有嵌入的文档。要分页嵌入的文档,可以使用 \Sokil\Mongo\Cursor::slice() 功能。

<?php
$collection->find()->slice('comments', $limit, $offset)->findAll();

如果你通过 Collection::getDocument() 获取 Document 实例,你可以定义用于加载它的附加表达式

<?php
$document = $collection->getDocument('54ab8585c90b73d6949d4159', function(Cursor $cursor) {
    $cursor->slice('comments', $limit, $offset);
});

设置嵌套文档列表

你可以将嵌入的文档存储到数组中,并在推送之前对其进行验证

<?php
$post->push('comments', new Comment(['author' => 'John Doe']));
$post->push('comments', new Comment(['author' => 'Joan Doe']));

嵌套文档验证

作为 Structure,嵌入的文档具有与 Document 相同的所有验证功能。目前,嵌入的文档只在将其设置为 Document 或手动设置之前进行验证。如果嵌入的文档无效,它将抛出 Sokil\Mongo\Document\InvalidDocumentException

class EmbeddedDocument extends Structure()
{
    public function rules() {}
}

$embeddedDocument = new EmbeddedDocument();
// auto validation
try {
    $document->set('some', embeddedDocument);
    $document->addToSet('some', embeddedDocument);
    $document->push('some', embeddedDocument);
} catch (InvalidDocumentException $e) {
    
}

// manual validation
if ($embeddedDocument->isValid()) {
    $document->set('some', embeddedDocument);
    $document->addToSet('some', embeddedDocument);
    $document->push('some', embeddedDocument);
}


DBRefs

在大多数情况下,你应该使用手动引用方法来连接两个或多个相关文档 - 包括一个文档的 _id 字段在另一个文档中。然后应用程序可以发出第二个查询以根据需要解析引用字段。有关在关系部分中支持手动引用的更多信息。

但是,如果你需要从多个集合引用文档,或者使用包含 DBRefs 的旧版数据库,请考虑使用 DBRefs。有关 DBRef 的更多信息,请参阅https://docs.mongodb.com/manual/reference/database-references/

如果你有 DBRef 数组,你可以获取文档实例

<?php
$collection->getDocumentByReference(array('$ref' => 'col', '$id' => '23ef12...ff452'));
$database->getDocumentByReference(array('$ref' => 'col', '$id' => '23ef12...ff452'));

在另一个文档中添加对单个文档的引用

<?php
$relatedDocument = $this->collection->createDocument(array('param' => 'value'))->save();
$document = $this->collection
    ->createDocument()
    ->setReference('related', $relatedDocument)
    ->save();

从引用字段获取文档

<?php

$relatedDocument = $document->getReferencedDocument('related');

将关系推送到关系列表中

<?php

$relatedDocument = $this->collection->createDocument(array('param' => 'value'))->save();
$document = $this->collection
    ->createDocument()
    ->pushReference('related', $relatedDocument)
    ->save();

获取相关文档列表

<?php

$foundRelatedDocumentList = $document->getReferencedDocumentList('related');

引用列表可能来自同一数据库的不同集合。不支持指定引用中的数据库。

存储文档

存储映射对象

如果你已经加载并修改了 \Sokil\Mongo\Document 的实例,只需保存它即可。如果文档已经存储,它将自动插入或更新。

<?php

// create new document and save
$document = $collection
    ->createDocument(['param' => 'value'])
    ->save();

// load existed document, modify and save
$document = $collection
    ->getDocument('23a4...')
    ->set('param', 'value')
    ->save();

不使用ODM插入和更新文档

如果要进行不带验证的快速文档插入,事件将仅将其作为数组插入

<?php
$collection->insert(['param' => 'value']);

要更新现有文档,请使用

<?php
$collection->update($expression, $data, $options);

表达式可以定义为数组、\Sokil\Mongo\Expressin 对象或可调用对象,该对象配置表达式。运算符可以定义为数组、\Sokil\Mongo\Operator 对象或可调用对象,该对象配置运算符。选项是允许的选项数组,如https://php.ac.cn/manual/ru/mongocollection.update.php 中所述。

例如

<?php
$collection->update(
    function(\Sokil\Mongo\Expression $expression) {
        $expression->where('status', 'active');
    },
    function(\Sokil\Mongo\Operator $operator) {
        $operator->increment('counter');
    },
    array(
        'multiple' => true,
    )
);

批量插入

一次性插入多个文档并进行插入文档的验证

<?php
$collection->batchInsert(array(
    array('i' => 1),
    array('i' => 2),
));

还支持通过接口使用 \MongoInsertBatch

<?php
$collection
    ->createBatchInsert()
    ->insert(array('i' => 1))
    ->insert(array('i' => 2))
    ->execute('majority');

批量更新

在几个文档上执行更改

<?php

$collection->batchUpdate(
    function(\Sokil\Mongo\Expression $expression) {
        return $expression->where('field', 'value');
    },
    ['field' => 'new value']
);

要更新所有文档,请使用

<?php

$collection->batchUpdate([], array('field' => 'new value'));

重命名文档的字段

<?php
$collection->batchUpdate(
    [],
    function(Operator $operator) {
        $operator->renameField('param', 'renamedParam');
    }
);

还支持通过接口使用 \MongoUpdateBatch

<?php
$collection
    ->createBatchUpdate()
    ->update(
        array('a' => 1),
        array('$set' => array('b' => 'updated1')),
        $multiple,
        $upsert
    )
    ->update(
        $collection->expression()->where('a', 2),
        $collection->operator()->set('b', 'updated2'),
        $multiple,
        $upsert
    )
    ->update(
        function(Expression $e) { $e->where('a', 3); },
        function(Operator $o) { $o->set('b', 'updated3'); },
        $multiple,
        $upsert
    )
    ->execute('majority');

在集合之间移动数据

根据表达式从一个集合复制文档到另一个集合

<?php
// to new collection of same database
$collection
    ->find()
    ->where('condition', 1)
    ->copyToCollection('newCollection');

// to new collection in new database
$collection
    ->find()
    ->where('condition', 1)
    ->copyToCollection('newCollection', 'newDatabase');

根据表达式将文档从一个集合移动到另一个集合

<?php
// to new collection of same database
$collection
    ->find()
    ->where('condition', 1)
    ->moveToCollection('newCollection');

// to new collection in new database
$collection
    ->find()
    ->where('condition', 1)
    ->moveToCollection('newCollection', 'newDatabase');

请注意,没有事务,如果在过程中发生错误,则不会回滚更改。

查询文档

查询构建器

要查询满足某些条件的文档,需要使用查询构建器

<?php
$cursor = $collection
    ->find()
    ->fields(['name', 'age'])
    ->where('name', 'Michael')
    ->whereGreater('age', 29)
    ->whereIn('interests', ['php', 'snowboard', 'traveling'])
    ->skip(20)
    ->limit(10)
    ->sort([
        'name'  => 1,
        'age'   => -1,
    ]);

所有 "where" 条件都以逻辑 AND 添加。要添加逻辑 OR 条件

<?php
$cursor = $collection
    ->find()
    ->whereOr(
        $collection->expression()->where('field1', 50),
        $collection->expression()->where('field2', 50),
    );

查询结果为迭代器 \Sokil\Mongo\Cursor,您可以对其进行迭代

<?php
foreach($cursor as $documentId => $document) {
    echo $document->get('name');
}

或者您可以获取结果数组

<?php
$result = $cursor->findAll();

获取单个结果

<?php
$document = $cursor->findOne();

获取单个随机结果

<?php
$document = $cursor->findRandom();

从文档结果集中获取单个字段的值

<?php
$columnValues = $cursor->pluck('some.field.name');

映射找到的文档

<?php
$result = $collection->find()->map(function(Document $document) {
    return $document->param;
});

过滤找到的文档

<?php
$result = $collection->find()->filter(function(Document $document) {
    return $document->param % 2;
});

要对结果应用函数链,请使用 ResultSet

<?php
$collection->find()
    ->getResultSet()
    ->filter(function($doc) { return $doc->param % 2 })
    ->filter(function($doc) { return $doc->param > 6 })
    ->map(function($item) {
        $item->param = 'update' . $item->param;
        return $item;
    });

当通过游标迭代时,客户端在一次往返中从服务器检索一些文档。为了定义这些文档的数量

<?php

$cursor->setBatchSize(20);

查询超时

客户端超时定义了停止等待响应,并在设定时间后抛出 \MongoCursorTimeoutException。超时可以在任何时间设置,并将影响游标上的后续查询,包括从数据库获取更多结果。

$collection->find()->where('name', 'Michael')->setClientTimeout(4200);

查询的服务器端超时指定了服务器允许处理游标上操作的总时间限制(毫秒)。

$collection->find()->where('name', 'Michael')->setServerTimeout(4200);

不同值

获取字段的不同值

<?php
// return all distinct values
$values = $collection->getDistinct('country');

值可以通过指定为数组、可调用对象或 Expression 对象的表达式进行筛选

<?php
// by array
$collection->getDistinct('country', array('age' => array('$gte' => 25)));
// by object
$collection->getDistinct('country', $collection->expression()->whereGreater('age', 25));
// by callable
$collection->getDistinct('country', function($expression) { return $expression->whereGreater('age', 25); });

扩展查询构建器

为了通过自定义条件方法扩展标准查询构建器类,您需要创建一个扩展 \Sokil\Mongo\Expression 的表达式类

<?php

// define expression
class UserExpression extends \Sokil\Mongo\Expression
{
    public function whereAgeGreaterThan($age)
    {
        $this->whereGreater('age', (int) $age);
    }
}

然后在集合映射中指定它

<?php

$client->map([
    'myDb' => [
        'user' => [
            'class' => '\UserCollection',
            'expressionClass' => '\UserExpression',
        ],
    ],
]);

还有一个已弃用的功能可以重写属性 Collection::$_queryExpressionClass

<?php

// define expression in collection
class UserCollection extends \Sokil\Mongo\Collection
{
    protected $_queryExpressionClass = 'UserExpression';
}

现在查询构建器中有了新的表达式方法

<?php
// use custom method for searching
$collection = $db->getCollection('user'); // instance of UserCollection
$queryBuilder = $collection->find(); // instance of UserExpression

// now methods available in query buider
$queryBuilder->whereAgeGreaterThan(18)->fetchRandom();

// since v.1.3.2 also supported query builder configuration through callable:
$collection
    ->find(function(UserExpression $e) {
        $e->whereAgeGreaterThan(18);
    })
    ->fetchRandom();

身份映射

假设您有两个不同的查询构建器,它们都返回相同的文档。标识符映射帮助我们从不同的查询中获得相同的对象实例,因此如果我们从第一个查询中修改了文档,这些更改将反映在第二个查询的文档中

<?php

$document1 = $collection->find()->whereGreater('age' > 18)->findOne();
$document2 = $collection->find()->where('gender', 'male')->findOne();

$document1->name = 'Mary';
echo $document2->name; // Mary

这两个文档引用了同一对象。默认情况下,集合将所有请求的文档存储在标识符映射中。如果我们直接通过 id 使用 Collection::getDocument() 获取文档,并且文档之前已加载到标识符映射中,它将直接从标识符映射中获取,而无需请求数据库。即使文档存在于标识符映射中,也可以使用具有与 Collection::getDocument() 相同语法的 Collection::getDocumentDirectly() 直接从数据库中获取。

如果序列请求获取相同的文档,则该文档不会替换在标识符映射中,但该文档的内容将更新。因此,不同的请求使用存储在标识符映射中的相同文档。

如果我们知道文档永远不会被重用,我们可以禁用将文档存储到标识符映射中

文档池可以在映射中启用或禁用。默认情况下已启用

<?php
$collection->map([
    'someDb' => [
        'someCollection', array(
            'documentPool' => false,
        ),
    ],
]);
<?php

$collection->disableDocumentPool();

启用标识符映射

<?php

$collection->enableDocumentPool();

检查是否启用了标识符映射

<?php

$collection->isDocumentPoolEnabled();

从先前存储的文档中清除池标识符映射

<?php

$collection->clearDocumentPool();

检查映射中是否已存在文档

<?php

$collection->isDocumentPoolEmpty();

如果文档已加载,但可能已从数据库中的另一个进程中更改,那么您的副本可能不是最新的。您可以手动刷新文档状态,将其与数据库同步

<?php

$document->refresh();

比较查询

如果您想缓存搜索结果或想比较两个查询,您需要一些标识符来唯一标识查询。您可以使用 Cursor::getHash() 来实现这一点。此哈希唯一标识查询参数,而不是文档结果集,因为它是从所有查询参数计算得出的

<?php

$queryBuilder = $this->collection
    ->find()
    ->field('_id')
    ->field('interests')
    ->sort(array(
        'age' => 1,
        'gender' => -1,
    ))
    ->limit(10, 20)
    ->whereAll('interests', ['php', 'snowboard']);

$hash = $queryBuilder->getHash(); // will return 508cc93b371c222c53ae90989d95caae

if($cache->has($hash)) {
    return $cache->get($hash);
}

$result = $queryBuilder->findAll();

$cache->set($hash, $result);


地理空间查询

在查询地理坐标之前,我们需要创建地理空间索引并添加一些数据。

2dsphere 索引自 MongoDB 版本 2.4 以来可用,可以通过几种方式创建

<?php
// creates index on location field
$collection->ensure2dSphereIndex('location');
// cerate compound index
$collection->ensureIndex(array(
    'location' => '2dsphere',
    'name'  => -1,
));

地理数据可以作为 GeoJson 格式的数组或使用 GeoJson 库的 GeoJson 对象添加

添加 GeoJson 对象作为数据

<?php

$document->setGeometry(
    'location',
    new \GeoJson\Geometry\Point(array(30.523400000000038, 50.4501))
);

$document->setGeometry(
    'location',
    new \GeoJson\Geometry\Polygon(array(
        array(24.012228, 49.831485), // Lviv
        array(36.230376, 49.993499), // Harkiv
        array(34.174927, 45.035993), // Simferopol
        array(24.012228, 49.831485), // Lviv
    ))
);

数据可以通过数组设置

<?php

// Point
$document->setPoint('location', 30.523400000000038, 50.4501);
// LineString
$document->setLineString('location', array(
    array(30.523400000000038, 50.4501),
    array(36.230376, 49.993499),
));
// Polygon
$document->setPolygon('location', array(
    array(
        array(24.012228, 49.831485), // Lviv
        array(36.230376, 49.993499), // Harkiv
        array(34.174927, 45.035993), // Simferopol
        array(24.012228, 49.831485), // Lviv
    ),
));
// MultiPoint
$document->setMultiPoint('location', array(
    array(24.012228, 49.831485), // Lviv
    array(36.230376, 49.993499), // Harkiv
    array(34.174927, 45.035993), // Simferopol
));
// MultiLineString
$document->setMultiLineString('location', array(
    // line string 1
    array(
        array(34.551416, 49.588264), // Poltava
        array(35.139561, 47.838796), // Zaporizhia
    ),
    // line string 2
    array(
        array(24.012228, 49.831485), // Lviv
        array(34.174927, 45.035993), // Simferopol
    )
));
// MultiPolygon
$document->setMultyPolygon('location', array(
    // polygon 1
    array(
        array(
            array(24.012228, 49.831485), // Lviv
            array(36.230376, 49.993499), // Harkiv
            array(34.174927, 45.035993), // Simferopol
            array(24.012228, 49.831485), // Lviv
        ),
    ),
    // polygon 2
    array(
        array(
            array(24.012228, 49.831485), // Lviv
            array(36.230376, 49.993499), // Harkiv
            array(34.174927, 45.035993), // Simferopol
            array(24.012228, 49.831485), // Lviv
        ),
    ),
));
// GeometryCollection
$document->setGeometryCollection('location', array(
    // point
    new \GeoJson\Geometry\Point(array(30.523400000000038, 50.4501)),
    // line string
    new \GeoJson\Geometry\LineString(array(
        array(30.523400000000038, 50.4501),
        array(24.012228, 49.831485),
        array(36.230376, 49.993499),
    )),
    // polygon
    new \GeoJson\Geometry\Polygon(array(
        // line ring 1
        array(
            array(24.012228, 49.831485), // Lviv
            array(36.230376, 49.993499), // Harkiv
            array(34.174927, 45.035993), // Simferopol
            array(24.012228, 49.831485), // Lviv
        ),
        // line ring 2
        array(
            array(34.551416, 49.588264), // Poltava
            array(32.049226, 49.431181), // Cherkasy
            array(35.139561, 47.838796), // Zaporizhia
            array(34.551416, 49.588264), // Poltava
        ),
    )),
));

查询位于水平面上、经纬度分别为49.588264和34.551416、距离该点1000米的附近的文档

<?php
$collection->find()->nearPoint('location', 34.551416, 49.588264, 1000);

此查询需要2dsphere2d索引。

距离可以指定为数组[minDistance, maxDistance]。此功能自MongoDB 2.6版本开始提供。如果某些值留空,则只应用存在的值。

<?php
// serch distance less 100 meters
$collection->find()->nearPoint('location', 34.551416, 49.588264, array(null, 1000));
// search distabce between 100 and 1000 meters
$collection->find()->nearPoint('location', 34.551416, 49.588264, array(100, 1000));
// search distabce greater than 1000 meters
$collection->find()->nearPoint('location', 34.551416, 49.588264, array(1000, null));

在球形表面上搜索

<?php
$collection->find()->nearPointSpherical('location', 34.551416, 49.588264, 1000);

找到与指定相交的几何形状

<?php
$this->collection
    ->find()
    ->intersects('link', new \GeoJson\Geometry\LineString(array(
        array(30.5326905, 50.4020355),
        array(34.1092134, 44.946798),
    )))
    ->findOne();

选择完全位于指定形状内的地理空间数据的文档

<?php
$point = $this->collection
    ->find()
    ->within('point', new \GeoJson\Geometry\Polygon(array(
        array(
            array(24.0122356, 49.8326891), // Lviv
            array(24.717129, 48.9117731), // Ivano-Frankivsk
            array(34.1092134, 44.946798), // Simferopol
            array(34.5572385, 49.6020445), // Poltava
            array(24.0122356, 49.8326891), // Lviv
        )
    )))
    ->findOne();

在水平圆内搜索文档

<?php
$this->collection
    ->find()
    ->withinCircle('point', 28.46963, 49.2347, 0.001)
    ->findOne();

在球形圆内搜索文档

<?php
$point = $this->collection
    ->find()
    ->withinCircleSpherical('point', 28.46963, 49.2347, 0.001)
    ->findOne();

在矩形内搜索存储为旧坐标的点文档

<?php
$point = $this->collection
    ->find()
    ->withinBox('point', array(0, 0), array(10, 10))
    ->findOne();

在多边形内搜索存储为旧坐标的点文档

<?php
$point = $this->collection
    ->find()
    ->withinPolygon(
        'point',
        array(
            array(0, 0),
            array(0, 10),
            array(10, 10),
            array(10, 0),
        )
    )
    ->findOne();


全文搜索

在搜索之前,必须将搜索字段先前索引为全文搜索字段

<?php

// one field
$collection->ensureFulltextIndex('somefield');

// couple of fields
$collection->ensureFulltextIndex(['somefield1', 'somefield2']);

在全文字段上搜索

<?php

$collection->find()->whereText('string searched in all fulltext fields')->findAll();


分页

查询构建器允许您创建分页。

<?php
$paginator = $collection->find()->where('field', 'value')->paginate(3, 20);
$totalDocumentNumber = $paginator->getTotalRowsCount();
$totalPageNumber = $paginator->getTotalPagesCount();

// iterate through documents
foreach($paginator as $document) {
    echo $document->getId();
}


持久性(工作单元)

现在我们可以将一些文档排队等待保存或删除,并一次性执行所有更改。这可以通过众所周知的单元工作模式来完成。如果安装的PHP驱动程序版本高于1.5.0且MongoDB版本高于,则持久性将使用MongoWriteBatch类,这些类可以一次性执行同一类型和同一集合中的所有操作。

让我们创建持久性管理器

<?php
$persistence = $client->createPersistence();

现在我们可以添加一些稍后要保存或删除的文档

<?php
$persistence->persist($document1);
$persistence->persist($document2);

$persistence->remove($document3);
$persistence->remove($document4);

如果稍后决定不保存或删除文档,我们可以将其从持久性管理器中分离出来

<?php
$persistence->detach($document1);
$persistence->detach($document3);

或者我们可以全部删除它们

<?php
$persistence->clear();

注意,在从持久性管理器中分离文档之后,其更改不会被删除,并且文档仍然可以直接保存或通过添加到持久性管理器来保存。

如果决定将更改存储到数据库中,我们可以刷新这些更改

<?php
$persistence->flush();

注意,在刷新后,持久化的文档不会被从持久性管理器中删除,但将被删除的文档将被删除。

删除集合和文档

删除集合

<?php
$collection->delete();

删除文档

<?php
$document->delete();

通过表达式删除少量文档

<?php

$collection->batchDelete($collection->expression()->where('param', 'value'));
// deprecated since 1.13
$collection->deleteDocuments($collection->expression()->where('param', 'value'));

还支持通过接口\MongoDeleteBatch

<?php
$batch = $collection->createBatchDelete();
$batch
    ->delete(array('a' => 2))
    ->delete($collection->expression()->where('a', 4))
    ->delete(function(Expression $e) { $e->where('a', 6); })
    ->execute();


聚合框架

创建聚合器

<?php
$aggregator = $collection->createAggregator();

然后您需要通过管道配置聚合器。

<?php
// through array
$aggregator->match(array(
    'field' => 'value'
));
// through callable
$aggregator->match(function($expression) {
    $expression->whereLess('date', new \MongoDate);
});

配置管道后,获取聚合结果

<?php
/**
 * @var array list of aggregation results
 */
$result = $aggregator->aggregate();
// or
$result = $collection->aggregate($aggregator);

您可以在没有先前创建的聚合器的情况下执行聚合

<?php
// by array
$collection->aggregate(array(
    array(
        '$match' => array(
            'field' => 'value',
        ),
    ),
));
// or callable
$collection->aggregate(function($aggregator) {
    $aggregator->match(function($expression) {
        $expression->whereLess('date', new \MongoDate);
    });
});

选项

可用的聚合选项可在https://docs.mongodb.org/manual/reference/command/aggregate/#dbcmd.aggregate找到。

选项可以作为aggregate方法的参数传递

<?php

// as argument of Pipeline::aggregate
$collection->createAggregator()->match()->group()->aggregate($options);

// as argument of Collection::aggregate
$collection->aggregate($pipelines, $options);

// as calling of Pipeline methods
$collection
    ->createAggregator()
    ->explain()
    ->allowDiskUse()
    ->setBatchSize(100);

调试

如果客户端处于调试模式并且已配置记录器,则将记录管道。有获取聚合解释的能力

<?php

// set explain option
$collection->aggregate($pipelines, ['explain' => true]);

// or configure pipeline
$collection->createAggregator()->match(...)->group(...)->explain()->aggregate();

聚合游标

Collection::aggregate返回数组作为结果,但也可以获取迭代器:更多信息请参阅https://php.ac.cn/manual/ru/mongocollection.aggregatecursor.php

<?php

// set as argument
$asCursor = true;
$collection->aggregate($pipelines, $options, $asCursor);

// or call method
$cursor = $collection->createAggregator()->match()->group()->aggregateCursor();


事件

基于Symfony的事件调度器组件的事件支持。您可以附加和触发任何您想要的事件,但有一些已经定义的事件

事件监听器是在事件触发时调用的函数

<?php
$listener = function(
    \Sokil\Mongo\Event $event, // instance of event
    string $eventName, // event name
    \Symfony\Component\EventDispatcher\EventDispatcher $eventDispatcher // instance of dispatcher
) {}

事件监听器可以通过Document::attachEvent()方法附加

<?php
$document->attachEvent('myOwnEvent', $listener, $priority);

它也可以通过辅助方法附加

<?php
$document->onMyOwnEvent($listener, $priority);
// which is equals to
$this->attachEvent('myOwnEvent', $listener, $priority);

事件可以在运行时或在Document类中通过重写Document::beforeConstruct()方法附加

<?php
class CustomDocument extends \Sokil\Mongo\Document
{
    public function beforeConstruct()
    {
        $this->onBeforeSave(function() {
            $this->set('date' => new \MongoDate);
        });
    }
}

可以通过触发事件来调用所有附加的事件监听器

<?php
$this->triggerEvent('myOwnEvent');

您可以通过创建自己的事件类,该类扩展自`\Sokil\Mongo\Event`并将其传递给监听器来创建自己的事件类。这允许您将一些数据传递给监听器

<?php
// create class
class OwnEvent extends \Sokil\Mongo\Event {
    public $status;
}

// define listener
$document->attachEvent('someEvent', function(\OwnEvent $event) {
    echo $event->status;
});

// configure event
$event = new \OwnEvent;
$event->status = 'ok';

// trigger event
$this->triggerEvent('someEvent', $event);

要取消在某个条件下执行的操作,请使用事件处理取消

<?php
$document->onBeforeSave(function(\Sokil\Mongo\Event $event) {
    if($this->get('field') === 42) {
        $event->cancel();
    }
})
->save();


行为

行为是扩展文档对象功能并在不同类的文档之间重用代码的可能性

行为是从\Sokil\Mongo\Behavior扩展的类。任何公共方法都可以通过附加行为的地方的文档访问。

<?php
class SomeBehavior extends \Sokil\Mongo\Behavior
{
    public function return42()
    {
        return 42;
    }
}

要获取附加了行为的对象实例,请调用Behavior::getOwner()方法

<?php
class SomeBehavior extends \Sokil\Mongo\Behavior
{
    public function getOwnerParam($selector)
    {
        return $this->getOwner()->get($selector);
    }
}

您可以在文档类中添加行为

<?php
class CustomDocument extends \Sokil\Mongo\Document
{
    public function behaviors()
    {
        return [
            '42behavior' => '\SomeBehavior',
        ];
    }
}

您也可以在运行时附加行为

<?php
// single behavior
$document->attachBehavior('42behavior', '\SomeBehavior');
// set of behaviors
$document->attachBehaviors([
    '42behavior' => '\SomeBehavior',
]);

行为可以定义为完全限定的类名、数组或Behavior实例

<?php
// class name
$document->attachBehavior('42behavior', '\SomeBehavior');

// array with parameters
$document->attachBehavior('42behavior', [
    'class'     => '\SomeBehavior',
    'param1'    => 1,
    'param2'    => 2,
]);

// Behavior instance
$document->attachBehavior('42behavior', new \SomeBehavior([
    'param1'    => 1,
    'param2'    => 2,
]);

然后您可以调用行为中的任何方法。这些方法按照附加行为的顺序进行搜索

<?php
echo $document->return42();


关系

您可以在不同的文档之间定义关系,这有助于您加载相关文档。

要定义与其他文档的关系,您需要重写Document::relations()方法,并返回格式为[relationName => [relationType, targetCollection, reference], ...]的关系数组。

您还可以在映射中定义关系

<?php

$collection->map([
    'someDb' => [
        'someCollection', array(
            'relations'     => array(
                'someRelation'   => array(self::RELATION_HAS_ONE, 'profile', 'user_id'),
            ),
        ),
    ],
]);

如果关系在映射和文档类中都指定了,则映射关系将合并到文档的关系中,因此映射关系具有更高的优先级。

一对一关系

我们有两个类User和Profile。User有一个Profile,Profile属于User。

<?php
class User extends \Sokil\Mongo\Document
{
    protected $schema = [
        'email'     => null,
        'password'  => null,
    ];

    public function relations()
    {
        return [
            'profileRelation' => [self::RELATION_HAS_ONE, 'profile', 'user_id'],
        ];
    }
}

class Profile extends \Sokil\Mongo\Document
{
    protected $schema = [
        'name' => [
            'last'  => null,
            'first' => null,
        ],
        'age'   => null,
    ];

    public function relations()
    {
        return [
            'userRelation' => [self::RELATION_BELONGS, 'user', 'user_id'],
        ];
    }
}

现在我们可以通过调用关系名称来延迟加载相关文档

<?php
$user = $userColletion->getDocument('234...');
echo $user->profileRelation->get('age');

$profile = $profileCollection->getDocument('234...');
echo $pfofile->userRelation->get('email');

一对多关系

一对一关系可以帮助您加载所有相关文档。User类有少量属于Post类的帖子

<?php
class User extends \Sokil\Mongo\Document
{
    protected $schema = [
        'email'     => null,
        'password'  => null,
    ];

    public function relations()
    {
        return [
            'postsRelation' => [self::RELATION_HAS_MANY, 'posts', 'user_id'],
        ];
    }
}

class Posts extends \Sokil\Mongo\Document
{
    protected $schema = [
        'user_id' => null,
        'message'   => null,
    ];

    public function relations()
    {
        return [
            'userRelation' => [self::RELATION_BELONGS, 'user', 'user_id'],
        ];
    }

    public function getMessage()
    {
        return $this->get('message');
    }
}

现在您可以加载文档的相关帖子

<?php
foreach($user->postsRelation as $post) {
    echo $post->getMessage();
}

多对多关系

关系数据库中的多对多关系使用一个中间表来存储两个表的相关行的ID。在Mongo中,这个表等价于两个相关文档中的一个嵌入。关系定义中的第3个位置必须设置为true。

<?php

// this document contains field 'driver_id' where array of ids stored
class CarDocument extends \Sokil\Mongo\Document
{
    protected $schema = [
        'brand' => null,
    ];

    public function relations()
    {
        return array(
            'drivers'   => array(self::RELATION_MANY_MANY, 'drivers', 'driver_id', true),
        );
    }
}

class DriverDocument extends \Sokil\Mongo\Document
{
    protected $schema = [
        'name' => null,
    ];

    public function relations()
    {
        return array(
            'cars'    => array(self::RELATION_MANY_MANY, 'cars', 'driver_id'),
        );
    }
}

现在您可以加载相关文档

<?php
foreach($car->drivers as $driver) {
    echo $driver->name;
}

添加关系

有一个辅助程序可以添加相关文档,如果您不想直接修改关系字段

<?php
$car->addRelation('drivers', $driver);

此辅助程序会自动解析存储关系数据的集合和字段。

移除关系

有一个辅助程序可以删除相关文档,如果您不想直接修改关系字段

<?php
$car->removeRelation('drivers', $driver);

此辅助程序会自动解析删除关系数据的集合和字段。如果关系类型是HAS_MANYBELONS_TO,则可以省略定义相关对象的第二个参数。

并发

乐观锁定

要启用乐观锁定,请在映射中指定锁模式

use Sokil\Mongo\Collection\Definition;
use Sokil\Mongo\Document\OptimisticLockFailureException;

$client->map([
    'db' => [
        'col' => [
            'lock' => Definition::LOCK_OPTIMISTIC,
        ],
    ]
]);

现在当某个进程尝试更新已更新的文档时,将抛出异常\Sokil\Mongo\Document\OptimisticLockFailureException

读取首选项

读取优先级描述了MongoDB客户端如何将读取操作路由到副本集的成员。您可以在任何级别配置读取优先级

<?php
// in constructor
$client = new Client($dsn, array(
    'readPreference' => 'nearest',
));

// by passing to \Sokil\Mongo\Client instance
$client->readNearest();

// by passing to database
$database = $client->getDatabase('databaseName')->readPrimaryOnly();

// by passing to collection
$collection = $database->getCollection('collectionName')->readSecondaryOnly();


写关注

写入关注描述了MongoDB在报告写入操作成功时提供的保证。您可以在任何级别配置写入关注

<?php

// by passing to \Sokil\Mongo\Client instance
$client->setMajorityWriteConcern(10000);

// by passing to database
$database = $client->getDatabase('databaseName')->setMajorityWriteConcern(10000);

// by passing to collection
$collection = $database->getCollection('collectionName')->setWriteConcern(4, 1000);


受限集合

要使用固定集合,您需要先创建它

<?php
$numOfElements = 10;
$sizeOfCollection = 10*1024;
$collection = $database->createCappedCollection('capped_col_name', $numOfElements, $sizeOfCollection);

现在您只能向集合中添加10个文档。所有旧的文档将被新元素重写。

执行命令

命令是执行MongoDB操作的一种通用方式。让我们获取集合的统计信息

<?php
$collection = $database->createCappedCollection('capped_col_name', $numOfElements, $sizeOfCollection);
$stats = $database->executeCommand(['collstat' => 'capped_col_name']);

结果在$stats中

array(13) {
  'ns' =>  string(29) "test.capped_col_name"
  'count' =>  int(0)
  'size' =>  int(0)
  'storageSize' =>  int(8192)
  'numExtents' =>  int(1)
  'nindexes' =>  int(1)
  'lastExtentSize' =>  int(8192)
  'paddingFactor' =>  double(1)
  'systemFlags' =>  int(1)
  'userFlags' =>  int(1)
  'totalIndexSize' =>  int(8176)
  'indexSizes' =>  array(1) {
    '_id_' =>    int(8176)
  }
  'ok' =>  double(1)
}


队列

队列提供了从一个进程发送消息到另一个进程并获取它们的功能。消息可以发送到不同的频道。

使用默认优先级向队列发送消息

<?php
$queue = $database->getQueue('channel_name');
$queue->enqueue('world');
$queue->enqueue(['param' => 'value']);

发送带优先级的消息

<?php
$queue->enqueue('hello', 10);

从频道读取消息

<?php
$queue = $database->getQueue('channel_name');
echo $queue->dequeue(); // hello
echo $queue->dequeue(); // world
echo $queue->dequeue()->get('param'); // value

队列中的消息数量

<?php
$queue = $database->getQueue('channel_name');
echo count($queue);


迁移

迁移允许您轻松地更改模式和数据版本。此功能在包https://github.com/sokil/php-mongo-migrator中实现,并通过composer安装

composer require sokil/php-mongo-migrator


GridFS

GridFS允许您将二进制数据存储在Mongo数据库中。详细信息请参阅http://docs.mongodb.org/manual/core/gridfs/

首先获取GridFS的实例。您可以为分区文件系统指定前缀

<?php
$imagesFS = $database->getGridFS('image');
$cssFS = $database->getGridFS('css');

现在您可以存储位于磁盘上的文件

<?php
$id = $imagesFS->storeFile('/home/sokil/images/flower.jpg');

您可以从二进制数据中存储文件

<?php
$id1 = $imagesFS->storeBytes('some text content');
$id2 = $imagesFS->storeBytes(file_get_contents('/home/sokil/images/flower.jpg'));

您可以与每个文件一起存储一些元数据

<?php
$id1 = $imagesFS->storeFile('/home/sokil/images/flower.jpg', [
    'category'  => 'flower',
    'tags'      => ['flower', 'static', 'page'],
]);

$id2 = $imagesFS->storeBytes('some text content', [
    'category' => 'books',
]);

通过ID获取文件

<?php
$imagesFS->getFileById('6b5a4f53...42ha54e');

通过元数据查找文件

<?php
foreach($imagesFS->find()->where('category', 'books') as $file) {
    echo $file->getFilename();
}

通过ID删除文件

<?php
$imagesFS->deleteFileById('6b5a4f53...42ha54e');
读取文件内容
<?php
// dump binary data to file
$file->dump($filename);

// get binary data
$file->getBytes();

// get resource
$file->getResource();

如果您想使用自己的GridFSFile类,则需要定义映射,就像处理集合一样。

<?php
// define mapping of prefix to GridFS class
$database->map([
    'GridFSPrefix' => '\GridFSClass',
]);

// define GridFSFile class
class GridFSClass extends \Sokil\Mongo\GridFS
{
    public function getFileClassName(\MongoGridFSFile $fileData = null)
    {
        return '\GridFSFileClass';
    }
}

// define file class
class GridFSFileClass extends \Sokil\Mongo\GridFSFile
{
    public function getMetaParam()
    {
        return $this->get('meta.param');
    }
}

// get file as instance of class \GridFSFileClass
$database->getGridFS('GridFSPrefix')->getFileById($id)->getMetaParam();


版本控制

版本控制允许您查看所有文档更改的历史记录。要在集合中启用文档的版本控制,可以将受保护属性Collection::$versioning设置为true,调用Collection::enableVersioning()方法或在映射中定义版本控制。

<?php
// througn protected property
class MyCollection extends \Sokil\Mongo\Collection
{
    protected $versioning = true;
}

// througn method
$collection = $database->getCollection('my');
$collection->enableVersioning();

// through mapping
$database->map('someCollectionName', [
    'versioning' => true,
]);

要检查集合中的文档是否已版本控制,请调用

<?php
if($collection->isVersioningEnabled()) {}

修订版是\Sokil\Mongo\Revision类的一个实例,并继承自\Sokil\Mongo\Document,因此可以将文档的任何方法应用于修订版。修订版可以访问

<?php
// get all revisions
$document->getRevisions();

// get slice of revisions
$limit = 10;
$offset = 15;
$document->getRevisions($limit, $offset);

要按id获取一个修订版,请使用

<?php
$revision = $document->getRevision($revisionKey);

要获取修订版的数量

<?php
$count = $document->getRevisionsCount();

要清除所有修订版

<?php
$document->clearRevisions();

修订版存储在名为{COLLECTION_NAME}.revisions的单独集合中。要从名为{COLLECTION_NAME}的集合的修订版中获取原始文档,该修订版是集合{COLLECTION_NAME}.revisions中的文档,请使用Revision::getDocument()方法。

<?php
$document->getRevision($revisionKey)->getDocument();

可以直接访问修订版中文档的属性

echo $document->property;
echo $document->getRevision($revisionKey)->property;

还可以从文档中获取修订版的创建日期

<?php
// return timestamp
echo $document->getRevision($revisionKey)->getDate();
// return formatted date string
echo $document->getRevision($revisionKey)->getDate('d.m.Y H:i:s');


索引

使用自定义选项创建索引(请参阅https://php.ac.cn/manual/en/mongocollection.ensureindex.php中的选项)

<?php
$collection->ensureIndex('field', [ 'unique' => true ]);

创建唯一索引

<?php
$collection->ensureUniqueIndex('field');

创建稀疏索引(有关稀疏索引的详细信息,请参阅http://docs.mongodb.org/manual/core/index-sparse/

<?php
$collection->ensureSparseIndex('field');

创建TTL索引(有关TTL索引的详细信息,请参阅http://docs.mongodb.org/manual/tutorial/expire-data/

<?php
$collection->ensureTTLIndex('field');

您可以将字段定义为数组,其中键是字段名,值是方向

<?php
$collection->ensureIndex(['field' => 1]);

您还可以定义复合索引

<?php
$collection->ensureIndex(['field1' => 1, 'field2' => -1]);

您可以将所有集合索引定义在属性Collection::$_index中,该属性是一个数组,其中每个项都是一个索引定义。每个索引定义必须包含键keys,其中包含字段列表和顺序,以及可选的选项,如https://php.ac.cn/manual/en/mongocollection.createindex.php中所述。

<?php
class MyCollection extends \Sokil\Mongo\Collection
{
    protected $_index = array(
        array(
            'keys' => array('field1' => 1, 'field2' => -1),
            'unique' => true
        ),
    );
}

然后您必须通过调用Collection::initIndexes()来创建这些索引

<?php
$collection = $database->getCollection('myCollection')->initIndexes();

您可以使用Mongo Migrator包来确保迁移脚本中的集合索引。

查询优化器会自动选择使用哪个索引,但您也可以手动定义它

<?php
$collection->find()->where('field', 1)->hind(array('field' => 1));


缓存和带TTL的文档

如果您想获取文档在指定时间后过期的集合,只需向该集合添加特殊索引即可。

<?php
$collection->ensureTTLIndex('createDate', 1000);

您也可以在迁移脚本中这样做,使用Mongo Migrator。有关详细信息,请参阅相关文档。

您还可以使用\Sokil\Mongo\Cache类,该类已实现此功能并与PSR-16接口兼容。

<?php

// Get cache instance
$cache = $database->getCache('some_namespace');

命名空间是在数据库中创建的集合的名称。

在使用缓存之前,必须通过调用方法Cache:init()来初始化缓存。

<?php
$cahce->init();

此操作在集合some_namespace中创建具有expireAfterSecond键的索引。

此操作可以在某些控制台命令或迁移脚本中执行,例如,通过使用迁移工具sokil/php-mongo-migrator,或者您可以在mongo控制台中手动创建

db.some_namespace.ensureIndex('e', {expireAfterSeconds: 0});

现在您可以使用以下方式存储新值

<?php
// this store value for 10 seconds
// expiration defined relatively to current time
$cache->set('key', 'value', 10);

您可以定义永不过期且必须手动删除的值

<?php
$cache->set('key', 'value', null);

您可以使用键定义一些标签

<?php
$cache->set('key', 'value', 10, ['php', 'c', 'java']);

要获取值

<?php
$value = $cache->get('key');

要按键删除缓存的值

<?php
$cache->delete('key');

按标签删除一些值

<?php
// delete all values with tag 'php'
$cache->deleteMatchingTag('php');
// delete all values without tag 'php'
$cache->deleteNotMatchingTag('php');
// delete all values with tags 'php' and 'java'
$cache->deleteMatchingAllTags(['php', 'java']);
// delete all values which don't have tags 'php' and 'java'
$cache->deleteMatchingNoneOfTags(['php', 'java']);
// Document deletes if it contains any of passed tags
$cache->deleteMatchingAnyTag(['php', 'elephant']);
// Document deletes if it contains any of passed tags
$cache->deleteNotMatchingAnyTag(['php', 'elephant']);


调试

在调试模式下,客户端可能将一些活动记录到预配置的日志记录器或显示扩展错误。

<?php

// start debugging
$client->debug();

// stop debugging
$client->debug(false);

// check debug state
$client->isDebugEnabled();

日志记录

库支持查询日志记录。要配置日志记录,需要将日志对象传递给实例,并启用客户端调试。由于PSR-3,日志必须实现

<?php
$client = new Client($dsn);
$client->setLogger($logger);

分析

有关性能分析更多的详细信息,请参阅分析数据库操作性能性能分析数据存储在system.profile集合中,您可以通过查询构建器进行查询

<?php

$qb = $database
    ->findProfilerRows()
    ->where('ns', 'social.users')
    ->where('op', 'update');

文章中描述的文档结构数据库性能分析输出

性能分析有三个级别,在文章性能分析命令中描述。可以通过调用方法在它们之间切换

<?php

// disable profiles
$database->disableProfiler();

// profile slow queries slower than 100 milliseconds
$database->profileSlowQueries(100);

// profile all queries
$database->profileAllQueries();

要获取当前的性能分析级别,请调用

<?php
$params = $database->getProfilerParams();
echo $params['was'];
echo $params['slowms'];

// or directly
$level = $database->getProfilerLevel();
$slowms = $database->getProfilerSlowMs();


单元测试

Build Status Coverage Status Scrutinizer Code Quality Code Climate SensioLabsInsight

本地PHPUnit测试

要启动单元测试,运行

./vendor/bin/phpunit -c tests/phpunit.xml tests

Docker PHPUnit测试

还有可用的Docker容器。它们默认启用xdebug,因此您可以登录到任何容器并在那里进行调试。

要启动所有支持的PHP和MongoDB版本的测试,运行

./run-docker-tests.sh

要运行特定平台的测试,请指定它们

./run-docker-tests.sh -p 56 -p 70 -m 30 -m 32

要运行具体的测试,请传递它

./run-docker-tests.sh -t DocumentTest.php

要运行具体的测试方法,请传递它

./run-docker-tests.sh -t DocumentTest.php -f ::testElemMatch

测试完成后,测试可以在./share/phpunit目录中找到。

贡献

欢迎提交拉取请求、错误报告和功能请求。请参阅CONTRIBUTING获取详细信息。

变更日志

请参阅CHANGELOG获取有关最近更改的更多信息。

许可证

MIT许可证(MIT)。请参阅许可证文件获取更多信息。