diff --git a/README.md b/README.md index 919f5c5b..5e986c44 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ from your PHP app or script. Designed to work with the self-hosted Parse Server - [Users](#users) - [ACLs/Security](#acls) - [Queries](#queries) + - [Aggregate](#aggregate) + - [Distinct](#distinct) - [Relative Time](#relative-time) - [Cloud Functions](#cloud-functions) - [Cloud Jobs](#cloud-jobs) @@ -42,7 +44,7 @@ from your PHP app or script. Designed to work with the self-hosted Parse Server - [Contributing / Testing](#contributing--testing) ## Installation -There are various ways to install and use this sdk. We'll elaborate on a couple here. +There are various ways to install and use this sdk. We'll elaborate on a couple here. Note that the Parse PHP SDK requires PHP 5.4 or newer. ### Install with Composer @@ -224,7 +226,7 @@ use Parse\ParseAudience; ### Parse Objects Parse Objects hold your data, can be saved, queried for, serialized and more! -Objects are at the core of this sdk, they allow you to persist your data from php without having to worry about any databasing code. +Objects are at the core of this sdk, they allow you to persist your data from php without having to worry about any databasing code. ```php $object = ParseObject::create("TestObject"); @@ -254,7 +256,7 @@ $decodedObject = ParseObject::decode($encoded); ### Users -Users are a special kind of object. +Users are a special kind of object. This class allows individuals to access your applications with their unique information and allows you to identify them distinctly. Users may also be linked with 3rd party accounts such as facebook, twitter, etc. @@ -302,7 +304,7 @@ $acl->setRoleWriteAccessWithName("PHPFans", true); ### Queries -Queries allow you to recall objects that you've saved to parse-server. +Queries allow you to recall objects that you've saved to parse-server. Query methods and parameters allow allow a varying degree of querying for objects, from all objects of a class to objects created within a particular date range and more. ```php @@ -327,6 +329,59 @@ $first = $query->first(); $query->each(function($obj) { echo $obj->getObjectId(); }); + +``` +#### Aggregate + +Queries can be made using aggregates, allowing you to retrieve objects over a set of input values. +Keep in mind that `_id` does not exist in parse-server. Please replace with `objectId`. MasterKey is Required + +For a list of available operators please refer to Mongo Aggregate Documentation. + + Mongo 3.2 Aggregate Operators + +```php +// group pipeline is similar to distinct, can apply $sum, $avg, $max, $min +// accumulate sum and store in total field +$pipeline = [ + 'group' => [ + 'objectId' => null, + 'total' => [ '$sum' => '$score'] + ] +]; +$results = $query->aggregate($pipeline); + +// project pipeline is similar to keys, add or remove existing fields +// includes name key +$pipeline = [ + 'project' => [ + 'name' => 1 + ] +]; +$results = $query->aggregate($pipeline); + +// match pipeline is similar to equalTo +// filter out objects with score greater than 15 + $pipeline = [ + 'match' => [ + 'score' => [ '$gt' => 15 ] + ] +]; +$results = $query->aggregate($pipeline); +``` + +#### Distinct + +Queries can be made using distinct, allowing you find unique values for a specified field. +Keep in mind that MasterKey is required. +```php +// finds score that are unique +$results = $query->distinct('score'); + +// can be used with equalTo +$query = new ParseQuery('TestObject'); +$query->equalTo('name', 'foo'); +$results = $query->distinct('score'); ``` #### Relative Time @@ -557,7 +612,7 @@ $globalConfigFeatures = ParseServerInfo::getGlobalConfigFeatures(); * "delete" : true * } */ - + // you can always get all feature data $data = ParseServerInfo::getFeatures(); ``` diff --git a/src/Parse/ParseQuery.php b/src/Parse/ParseQuery.php index 3d5a7d8e..20c23d3d 100755 --- a/src/Parse/ParseQuery.php +++ b/src/Parse/ParseQuery.php @@ -519,6 +519,65 @@ public function count($useMasterKey = false) return $result['count']; } + /** + * Execute a distinct query and return unique values. + * + * @param string $key field to find distinct values + * + * @return array + */ + public function distinct($key) + { + $sessionToken = null; + if ($user = ParseUser::getCurrentUser()) { + $sessionToken = $user->getSessionToken(); + } + $opts = []; + if (!empty($this->where)) { + $opts['where'] = $this->where; + } + $opts['distinct'] = $key; + $queryString = $this->buildQueryString($opts); + $result = ParseClient::_request( + 'GET', + 'aggregate/'.$this->className.'?'.$queryString, + $sessionToken, + null, + true + ); + + return $result['results']; + } + + /** + * Execute an aggregate query and returns aggregate results. + * + * @param array $pipeline stages to process query + * + * @return array + */ + public function aggregate($pipeline) + { + $sessionToken = null; + if ($user = ParseUser::getCurrentUser()) { + $sessionToken = $user->getSessionToken(); + } + $stages = []; + foreach ($pipeline as $stage => $value) { + $stages[$stage] = json_encode($value); + } + $queryString = $this->buildQueryString($stages); + $result = ParseClient::_request( + 'GET', + 'aggregate/'.$this->className.'?'.$queryString, + $sessionToken, + null, + true + ); + + return $result['results']; + } + /** * Execute a find query and return the results. * diff --git a/tests/Parse/ParseQueryAggregateTest.php b/tests/Parse/ParseQueryAggregateTest.php new file mode 100644 index 00000000..f78bda7c --- /dev/null +++ b/tests/Parse/ParseQueryAggregateTest.php @@ -0,0 +1,276 @@ +set('score', 10); + $obj2->set('score', 10); + $obj3->set('score', 10); + $obj4->set('score', 20); + + $obj1->set('name', 'foo'); + $obj2->set('name', 'foo'); + $obj3->set('name', 'bar'); + $obj4->set('name', 'dpl'); + + $objects = [$obj1, $obj2, $obj3, $obj4]; + ParseObject::saveAll($objects); + } + + public function testDistinctQuery() + { + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $results = $query->distinct('score'); + + $this->assertEquals(2, count($results)); + $this->assertEquals(in_array(10, $results), true); + $this->assertEquals(in_array(20, $results), true); + } + + public function testDistinctWhereQuery() + { + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $query->equalTo('name', 'foo'); + $results = $query->distinct('score'); + + $this->assertEquals(1, count($results)); + $this->assertEquals($results[0], 10); + } + + public function testDistinctClassNotExistQuery() + { + $this->loadObjects(); + $query = new ParseQuery('UnknownClass'); + $results = $query->distinct('score'); + + $this->assertEquals(0, count($results)); + } + + public function testDistinctFieldNotExistQuery() + { + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $results = $query->distinct('unknown'); + + $this->assertEquals(0, count($results)); + } + + public function testDistinctOnUsers() + { + Helper::clearClass(ParseUser::$parseClassName); + $user1 = new ParseUser(); + $user1->setUsername('foo'); + $user1->setPassword('password'); + $user1->set('score', 10); + $user1->signUp(); + + $user2 = new ParseUser(); + $user2->setUsername('bar'); + $user2->setPassword('password'); + $user2->set('score', 10); + $user2->signUp(); + + $user3 = new ParseUser(); + $user3->setUsername('hello'); + $user3->setPassword('password'); + $user3->set('score', 20); + $user3->signUp(); + + $query = ParseUser::query(); + $results = $query->distinct('score'); + + $this->assertEquals(2, count($results)); + $this->assertEquals($results[0], 10); + $this->assertEquals($results[1], 20); + } + + public function testAggregateGroupQuery() + { + $pipeline = [ + 'group' => [ + 'objectId' => '$name' + ] + ]; + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $results = $query->aggregate($pipeline); + + $this->assertEquals(3, count($results)); + } + + public function testAggregateGroupClassNotExistQuery() + { + $pipeline = [ + 'group' => [ + 'objectId' => '$name' + ] + ]; + $this->loadObjects(); + $query = new ParseQuery('UnknownClass'); + $results = $query->aggregate($pipeline); + + $this->assertEquals(0, count($results)); + } + + public function testAggregateGroupFieldNotExistQuery() + { + $pipeline = [ + 'group' => [ + 'objectId' => '$unknown' + ] + ]; + $this->loadObjects(); + $query = new ParseQuery('UnknownClass'); + $results = $query->aggregate($pipeline); + + $this->assertEquals(0, count($results)); + } + + public function testAggregateMatchQuery() + { + $pipeline = [ + 'match' => [ + 'score' => [ '$gt' => 15 ] + ] + ]; + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $results = $query->aggregate($pipeline); + + $this->assertEquals(1, count($results)); + $this->assertEquals(20, $results[0]['score']); + } + + public function testAggregateProjectQuery() + { + $pipeline = [ + 'project' => [ + 'name' => 1 + ] + ]; + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $results = $query->aggregate($pipeline); + + foreach ($results as $result) { + $this->assertEquals(array_key_exists('name', $result), true); + $this->assertEquals(array_key_exists('objectId', $result), true); + $this->assertEquals(array_key_exists('score', $result), false); + } + } + + public function testAggregatePipelineInvalid() + { + $pipeline = [ + 'unknown' => [] + ]; + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $this->setExpectedException( + 'Parse\ParseException', + 'Invalid parameter for query: unknown', + 102 + ); + $results = $query->aggregate($pipeline); + } + + public function testAggregateGroupInvalid() + { + $pipeline = [ + 'group' => [ + '_id' => '$name' + ] + ]; + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $this->setExpectedException( + 'Parse\ParseException', + 'Invalid parameter for query: group. Please use objectId instead of _id', + 102 + ); + $results = $query->aggregate($pipeline); + } + + public function testAggregateGroupObjectIdRequired() + { + $pipeline = [ + 'group' => [] + ]; + $this->loadObjects(); + $query = new ParseQuery('TestObject'); + $this->setExpectedException( + 'Parse\ParseException', + 'Invalid parameter for query: group. objectId is required', + 102 + ); + $results = $query->aggregate($pipeline); + } + + public function testAggregateOnUsers() + { + Helper::clearClass(ParseUser::$parseClassName); + $user1 = new ParseUser(); + $user1->setUsername('foo'); + $user1->setPassword('password'); + $user1->set('score', 10); + $user1->signUp(); + + $user2 = new ParseUser(); + $user2->setUsername('bar'); + $user2->setPassword('password'); + $user2->set('score', 10); + $user2->signUp(); + + $user3 = new ParseUser(); + $user3->setUsername('hello'); + $user3->setPassword('password'); + $user3->set('score', 20); + $user3->signUp(); + + $pipeline = [ + 'match' => [ + 'score' => [ '$gt' => 15 ] + ] + ]; + + $query = ParseUser::query(); + $results = $query->aggregate($pipeline); + + $this->assertEquals(1, count($results)); + $this->assertEquals($results[0]['score'], 20); + } +}