Skip to content

thinkbox/php-mongo

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PHPMongo

Build Status Latest Stable Version Coverage Status

Object Document Mapper for MongoDB.

Why to use this library? You can easily work with document data through comfortable getters and setters instead of array and don't check if key exist in array. Access to subdocument use dot-syntax. You can validate data passed to document before save. We give you events, which you can handle in different moments of document's life, and more things which make you life easier.

Requirements

  • PHP 5.3 or above
  • PHP MongoDB Extension 0.9 or above (Some features require >= 1.5)
  • Symfony Event Dispatcher
  • PSR-3 logger interface

Table of contents

Installation

You can install library through Composer:

{
    "require": {
        "sokil/php-mongo": "dev-master"
    }
}

If you use Symfony framework, you can use Symfony MongoDB Bundle which wraps this library

{
    "require": {
        "sokil/php-mongo-bundle": "dev-master"
    }
}

If you use Yii Framework, you can use Yii Adapter which wraps this library

{
    "require": {
        "sokil/php-mongo-yii": "dev-master"
    }
}

If you require migrations, you can add dependency to "sokil/php-mongo-migrator", based on this library:

{
    "require": {
        "sokil/php-mongo-migrator": "dev-master"
    }
}

Connecting

Single connection

Connecting to MongoDB server made through \Sokil\Mongo\Client class:

$client = new Client($dsn);

Format of DSN used to connect to server described in PHP manual. To connect to localhost use next DSN:

mongodb://127.0.0.1

To connect to replica set use next DSN:

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

Pool of connections

If you have few connections you may prefer connection pool instead of managing different connections. Use \Sokil\Mongo\ClientPool instance to initialize pool object:

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

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

Selecting database and collection

To get instance of database class \Sokil\Mongo\Database:

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

To get instance of collection class \Sokil\Mongo\Collection:

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

Default database may be specified to get collection directly from $client object:

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

If you need to use your own collection classes, you must create class extended from \Sokil\Mongo\Collection and map it to collection class:

class CustomCollection extends \Sokil\Mongo\Collection
{

}

$client->map([
    'databaseName'  => [
        'collectionName' => '\CustomCollection'
    ],
]);

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

Mapping may be specified through class prefix.

$client->map([
    'databaseName'  => '\Class\Prefix',
]);

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

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

Document schema

Document object is instance of class \Sokil\Mongo\Document. If you want to use your own class, you must configure its name in collection's class:

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

class CustomDocument extends \Sokil\Mongo\Document
{

}

You may flexibly configure document's class in \Sokil\Mongo\Collection::getDocumentClassName() relatively to concrete document's data:

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

In example above class \CustomVideoDocument related to {"_id": "45..", "type": "video"}, and \CustomAudioDocument to {"_id": "45..", type: "audio"}

Document's scheme is completely not required. If field is required and has default value, it can be defined in special property of document class:

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

Getting documents by id

To get document from collection by its id:

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

Create new document

Create new empty document object:

$document = $collection->createDocument();

Or with pre-defined values:

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

Get and set data in document

To get value of document's field you may use one of following ways:

$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

If field not exists, null value returned.

To set value you may use following ways:

$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'}

Storing document

To store document in database just save it.

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

$document = $collection->getDocument('23a4...')->set('param', 'value')->save();

Querying documents

To query documents, which satisfy some conditions you need to use query builder:

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

All "where" conditions added with logical AND. To add condition with logical OR:

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

Result of the query is iterator \Sokil\Mongo\QueryBuilder, which you can then iterate:

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

Or you can get result array:

$result = $cursor->findAll();

To get only one result:

$document = $cursor->findOne();

To get only one random result:

$document = $cursor->findRandom();

Pagination

Query builder allows you to create pagination.

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

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

Update few documents

Making changes in few documents:

$expression = $collection
    ->expression()
    ->where('field', 'value');
    
$collection
    ->multiUpdate($expression, array('field' => 'new value'));

Document validation

Document can be validated before save. To set validation rules method \Sokil\Mongo\Document::roles() must be override with validation rules. Supported rules are:

class CustomDocument except \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 can have validation state, based on scenario. Scenarion can be specified by method Document::setScenario($scenario).

$document->setScenario('register');

If some validation rule applied only for some scenarios, this scenarios must be passed on 'on' key, separated by comma.

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

If some validation rule applied to all except some scenarios, this scenarios must be passed on 'except' key, separated by comma.

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

If document invalid, \Sokil\Mongo\Document\Exception\Validate will trigger and errors may be accessed through Document::getErrors() method of document object. This document may be get from exception method:

try {

} catch(\Sokil\Mongo\Document\Exception\Validate $e) {
    $e->getDocument()->getErrors();
}

Error may be triggered manually by calling method triggerError($fieldName, $rule, $message)

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

You may add you custom validation rule just adding method to document class and defining method name as rule:

class CustomDocument extends \Sokil\Mongo\Document
{
    punlic function rules() 
    {
        return array(
            array('email', 'uniqueFieldValidator', 'message' => 'E-mail must be unique in collection'),
        );
    }
    
    /**
     * @return bool if true, validator passes, if false - failed
     */
    public function uniqueFieldValidator($fieldName, $params)
    {
        // some logic of checking unique mail. Return true if validator passes, and false otherwise
    }
}

Deleting collections and documents

Deleting of collection:

$collection->delete();

Deleting of document:

$document = $collection->getDocument($documentId);
$collection->deleteDocument($document);
// or simply
$document->delete();

Deleting of few documents:

$collection->deleteDocuments($collection->expression()->where('param', 'value'));

Aggregation framework

To do aggregation you need first to create pipelines object:

$pipeline = $collection->createPipeline();

To get results of aggregation after configuring pipelines:

/**
 * @var array list of aggregation results
 */
$result = $pipeline->aggregate();

Match pipeline:

$pipeline-> match([
    'date' => [
        '$lt' => new \MongoDate,
    ]
]);

Events

Event support based on Symfony's Event Dispatcher component. Events can be attached in class while initialusing object or any time to the object. To attach events in Document class you need to override Document::beforeConstruct() method:

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

Or you can attach event handler to document object:

$document->onBeforeSave(function() {
    $this->set('date' => new \MongoDate);
});

Behaviors

Behavior is a posibility to extend functionality of document object and reuse code among documents of different class. Behavior is a class extended from \Sokil\Mongo\Behavior:

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

To get instance of object, to which behavior is attached, call Behavior::getOwner() method:

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

You can add behavior in document class:

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

You can attach behavior in runtime too:

$document->attachBehavior(new \SomeBehavior);

Then you can call any methods of behaviors. This methods searches in order of atraching behaviors:

echo $document->return42();

Relations

You can define relations between different documents, which helps you to load related doluments. Library supports relations one-to-one and one-to-many

To define relation to other document you need to override Document::relations() method and returl array of relations in format [relationName => [relationType, targetCollection, reference], ...]

One-to-one relation

We have to classes User and Profile. User has one profile, and profile belongs to User.

class User extends \Sokil\Mongo\Document
{
    protected $_data = [
        'email'     => null,
        'password'  => null,
    ];
    
    public function relations()
    {
        return [
            'profileRelation' => [self::RELATION_HAS_ONE, 'profile', 'user_id'],
        ];
    }
}

class Profile extends \Sokil\Mongo\Document
{
    protected $_data = [
        'name' => [
            'last'  => null,
            'first' => null,
        ],
        'age'   => null,
    ];
    
    public function relations()
    {
        return [
            'userRelation' => [self::RELATION_BELONGS, 'user', 'user_id'],
        ];
    }
}

Now we can lazy load related documnts just calling relation name:

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

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

One-to-many relation

One-to-many relation helps you to load all related documents. Class User has few posts of class Post:

class User extends \Sokil\Mongo\Document
{
    protected $_data = [
        'email'     => null,
        'password'  => null,
    ];
    
    public function relations()
    {
        return [
            'postsRelation' => [self::RELATION_HAS_MANY, 'posts', 'user_id'],
        ];
    }
}

class Posts extends \Sokil\Mongo\Document
{
    protected $_data = [
        'user_id' => null,
        'message'   => null,
    ];
    
    public function relations()
    {
        return [
            'userRelation' => [self::RELATION_BELONGS, 'user', 'user_id'],
        ];
    }
    
    public function getMessage()
    {
        return $this->get('message');
    }
}

Not you can load related posts of document:

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

Read preferences

Read preference describes how MongoDB clients route read operations to members of a replica set. You can configure read preferences at any level:

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

Write concern

Write concern describes the guarantee that MongoDB provides when reporting on the success of a write operation. You can configure write concern at any level:

// 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);

Debugging

Library suports logging of queries. To configure logging, you need to pass logger object to instance of \Sokil\Mongo\Client. Logger must implement \Psr\Log\LoggerInterface due to PSR-3:

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

Capped collections

To use capped collection you need previously to create it:

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

Now you can add only 10 documents to collection. All old documents will ve rewritted ny new elements.

Executing commands

Command is universal way to do anything with mongo. Let's get stats of collection:

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

Result in $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)
}

Queue

Queue gives functionality to send messages from one process and get them in another process. Messages can be send to different channels.

Sending message to queue with default priority:

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

Send message with priority

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

Reading messages from channel:

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

Number of messages in queue

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

Migrations

Migrations allows you easily change schema and data versions. This functionality implemented in packet https://github.com/sokil/php-mongo-migrator and can be installed through composer:

{
    "require": {
        "sokil/php-mongo-migrator": "dev-master"
    }
}

GridFS

GridFS allows you to store binary data in mongo database. Details at http://docs.mongodb.org/manual/core/gridfs/.

First get instance of GridFS. You can specify prefix for partitioning filesystem:

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

Now you can store file, located on disk:

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

You can store file from binary data:

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

You are able to store some metadata with every file:

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

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

Get file by id:

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

Find file by metadata:

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

Deleting files by id:

$imagesFS->deleteFileById('6b5a4f53...42ha54e');

If you want to use your own GridFSFile classes, you need to define mapping, as it does with collections:

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


Pull requests, bug reports and feature requests is welcome.

About

MongoDB ODM for PHP

Resources

License

Stars

Watchers

Forks

Packages

No packages published