From c237d06cc721a9da2e63caec8b4c684791c6e2a9 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Mon, 4 Aug 2014 15:22:17 -0700 Subject: [PATCH] Initial release. --- CONTRIBUTING.md | 34 + LICENSE | 17 + README.md | 172 ++ composer.json | 29 + src/Parse/Internal/AddOperation.php | 108 ++ src/Parse/Internal/AddUniqueOperation.php | 136 ++ src/Parse/Internal/DeleteOperation.php | 50 + src/Parse/Internal/Encodable.php | 22 + src/Parse/Internal/FieldOperation.php | 35 + src/Parse/Internal/IncrementOperation.php | 99 ++ src/Parse/Internal/ParseRelationOperation.php | 280 +++ src/Parse/Internal/RemoveOperation.php | 127 ++ src/Parse/Internal/SetOperation.php | 92 + src/Parse/ParseACL.php | 554 ++++++ src/Parse/ParseAggregateException.php | 39 + src/Parse/ParseAnalytics.php | 76 + src/Parse/ParseBytes.php | 75 + src/Parse/ParseClient.php | 360 ++++ src/Parse/ParseCloud.php | 35 + src/Parse/ParseException.php | 27 + src/Parse/ParseFile.php | 415 +++++ src/Parse/ParseGeoPoint.php | 101 ++ src/Parse/ParseInstallation.php | 18 + src/Parse/ParseMemoryStorage.php | 59 + src/Parse/ParseObject.php | 1189 +++++++++++++ src/Parse/ParsePush.php | 68 + src/Parse/ParseQuery.php | 775 +++++++++ src/Parse/ParseRelation.php | 138 ++ src/Parse/ParseRole.php | 107 ++ src/Parse/ParseSessionStorage.php | 70 + src/Parse/ParseStorageInterface.php | 72 + src/Parse/ParseUser.php | 309 ++++ tests/IncrementTest.php | 235 +++ tests/ParseACLTest.php | 391 +++++ tests/ParseAnalyticsTest.php | 79 + tests/ParseBytesTest.php | 51 + tests/ParseCloudTest.php | 93 + tests/ParseFileTest.php | 126 ++ tests/ParseGeoBoxTest.php | 156 ++ tests/ParseGeoPointTest.php | 207 +++ tests/ParseMemoryStorageTest.php | 71 + tests/ParseObjectTest.php | 929 ++++++++++ tests/ParsePushTest.php | 49 + tests/ParseQueryTest.php | 1520 +++++++++++++++++ tests/ParseRelationTest.php | 135 ++ tests/ParseRoleTest.php | 217 +++ tests/ParseSessionStorageTest.php | 76 + tests/ParseSubclassTest.php | 37 + tests/ParseTestHelper.php | 35 + tests/ParseUserTest.php | 430 +++++ tests/bootstrap.php | 16 + tests/cloudcode/cloud/main.js | 43 + tests/phpunit.xml | 8 + 53 files changed, 10592 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 composer.json create mode 100755 src/Parse/Internal/AddOperation.php create mode 100755 src/Parse/Internal/AddUniqueOperation.php create mode 100755 src/Parse/Internal/DeleteOperation.php create mode 100644 src/Parse/Internal/Encodable.php create mode 100755 src/Parse/Internal/FieldOperation.php create mode 100755 src/Parse/Internal/IncrementOperation.php create mode 100644 src/Parse/Internal/ParseRelationOperation.php create mode 100644 src/Parse/Internal/RemoveOperation.php create mode 100755 src/Parse/Internal/SetOperation.php create mode 100644 src/Parse/ParseACL.php create mode 100644 src/Parse/ParseAggregateException.php create mode 100644 src/Parse/ParseAnalytics.php create mode 100644 src/Parse/ParseBytes.php create mode 100755 src/Parse/ParseClient.php create mode 100644 src/Parse/ParseCloud.php create mode 100644 src/Parse/ParseException.php create mode 100755 src/Parse/ParseFile.php create mode 100755 src/Parse/ParseGeoPoint.php create mode 100644 src/Parse/ParseInstallation.php create mode 100644 src/Parse/ParseMemoryStorage.php create mode 100755 src/Parse/ParseObject.php create mode 100644 src/Parse/ParsePush.php create mode 100755 src/Parse/ParseQuery.php create mode 100644 src/Parse/ParseRelation.php create mode 100644 src/Parse/ParseRole.php create mode 100644 src/Parse/ParseSessionStorage.php create mode 100644 src/Parse/ParseStorageInterface.php create mode 100644 src/Parse/ParseUser.php create mode 100644 tests/IncrementTest.php create mode 100644 tests/ParseACLTest.php create mode 100644 tests/ParseAnalyticsTest.php create mode 100644 tests/ParseBytesTest.php create mode 100644 tests/ParseCloudTest.php create mode 100644 tests/ParseFileTest.php create mode 100644 tests/ParseGeoBoxTest.php create mode 100644 tests/ParseGeoPointTest.php create mode 100644 tests/ParseMemoryStorageTest.php create mode 100644 tests/ParseObjectTest.php create mode 100644 tests/ParsePushTest.php create mode 100644 tests/ParseQueryTest.php create mode 100644 tests/ParseRelationTest.php create mode 100644 tests/ParseRoleTest.php create mode 100644 tests/ParseSessionStorageTest.php create mode 100644 tests/ParseSubclassTest.php create mode 100644 tests/ParseTestHelper.php create mode 100644 tests/ParseUserTest.php create mode 100644 tests/bootstrap.php create mode 100644 tests/cloudcode/cloud/main.js create mode 100644 tests/phpunit.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fe9db8f4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +Contributing +------------ + +For us to accept contributions you will have to first have signed the +[Contributor License Agreement]. + +When committing, keep all lines to less than 80 characters, and try to +follow the existing style. Before creating a pull request, squash your commits +into a single commit. Please provide ample explanation in the commit message. + +Installation +------------ + +Testing the Parse PHP SDK involves some set-up. You'll need to create a Parse +App just for testing, and deploy some cloud code to it. + +* [Get Composer], the PHP package manager. +* Run "composer install" to download dependencies. +* Create a new app here: [Create Parse App] +* Use the Parse CLI to create a Cloud Code folder for the new app. +* Copy tests/cloudcode/cloud/main.js into the newly created cloud/ folder. +* Run "parse deploy" in your cloud folder. +* Paste your App ID, REST API Key, and Master Key in tests/ParseTestHelper.php + +You should now be able to execute, from the tests/ folder: + + ../vendor/bin/phpunit --stderr . + +At present the full suite of tests takes around 20 minutes. + + +[Get Composer]: https://getcomposer.org/download/ +[Contributor License Agreement]: https://developers.facebook.com/opensource/cla +[Create Parse App]: https://parse.com/apps/new \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1d04eb88 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Copyright (c) 2014, Parse, LLC. All rights reserved. + +You are hereby granted a non-exclusive, worldwide, royalty-free license to use, +copy, modify, and distribute this software in source code or binary form for use +in connection with the web services and APIs provided by Parse. + +As with any software that integrates with the Parse platform, your use of +this software is subject to the Parse Terms of Service at: https://www.parse.com/about/terms +This copyright notice shall be included in all copies or substantial portions of the +software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index e69de29b..8326612c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,172 @@ +Parse PHP SDK +------------- + +The Parse PHP SDK gives you access to the powerful Parse cloud platform +from your PHP app or script. + +Installation +------------ + +[Get Composer], the PHP package manager. Then create a composer.json file in + your projects root folder, containing: + + { + "require": { + "parse/php-sdk" : "1.0.*" + } + } + +Run "composer install" to download the SDK and set up the autoloader, +and then require it from your PHP script: + + require 'vendor/autoload.php'; + +Usage +----- + +Check out the [Parse PHP Guide] for the full documentation. + +Add the "use" declarations where you'll be using the classes. For all of the +sample code in this file: + + use Parse\ParseObject; + use Parse\ParseQuery; + use Parse\ParseACL; + use Parse\ParsePush; + use Parse\ParseUser; + use Parse\ParseInstallation; + use Parse\ParseException; + use Parse\ParseAnalytics; + use Parse\ParseFile; + use Parse\ParseCloud; + + +Objects: + + $object = ParseObject::create("TestObject"); + $objectId = $object->getObjectId(); + $php = $object->get("elephant"); + + // Set values: + $object->set("elephant", "php"); + $object->set("today", new DateTime()); + $object->setArray("mylist", [1, 2, 3]); + $object->setAssociativeArray( + "languageTypes", array("php" => "awesome", "ruby" => "wtf") + ); + + // Save: + $object->save(); + +Users: + + // Signup + $user = new ParseUser(); + $user->setUsername("foo"); + $user->setPassword("Q2w#4!o)df"); + try { + $user->signUp(); + } catch (ParseException $ex) { + // error in $ex->getMessage(); + } + + // Login + try { + $user = ParseUser::logIn("foo", "Q2w#4!o)df"); + } catch(ParseException $ex) { + // error in $ex->getMessage(); + } + + // Current user + $user = ParseUser::getCurrentUser(); + +Security: + + // Access only by the ParseUser in $user + $userACL = ParseACL::createACLWithUser($user); + + // Access only by master key + $restrictedACL = new ParseACL(); + + // Set individual access rights + $acl = new ParseACL(); + $acl->setPublicReadAccess(true); + $acl->setPublicWriteAccess(false); + $acl->setUserWriteAccess($user, true); + $acl->setRoleWriteAccessWithName("PHPFans", true); + +Queries: + + $query = new ParseQuery("TestObject"); + + // Get a specific object: + $object = $query->get("anObjectId"); + + $query->limit(10); // default 100, max 1000 + + // All results: + $results = $query->find(); + + // Just the first result: + $first = $query->first(); + + // Process ALL (without limit) results with "each". + // Will throw if sort, skip, or limit is used. + $query->each(function($obj) { + echo $obj->getObjectId(); + }); + +Cloud Functions: + + $results = ParseCloud::run("aCloudFunction", array("from" => "php")); + +Analytics: + + PFAnalytics::trackEvent("logoReaction", array( + "saw" => "elephant", + "said" => "cute" + )); + +Files: + + // Get from a Parse Object: + $file = $aParseObject->get("aFileColumn"); + $name = $file->getName(); + $url = $file->getURL(); + // Download the contents: + $contents = $file->getData(); + + // Upload from a local file: + $file = ParseFile::createFromFile( + "/tmp/foo.bar", "Parse.txt", "text/plain" + ); + + // Upload from variable contents (string, binary) + $file = ParseFile::createFromData($contents, "Parse.txt", "text/plain"); + +Push: + + $data = array("alert" => "Hi!"); + + // Push to Channels + ParsePush::send(array( + "channels" => ["PHPFans"], + "data" => $data + )); + + // Push to Query + $query = ParseInstallation::query(); + $query->equalTo("design", "rad"); + ParsePush::send(array( + "where" => $query, + "data" => $data + )); + +Contributing / Testing +---------------------- + +See the CONTRIBUTORS.md file for information on testing and contributing to +the Parse PHP SDK. We welcome fixes and enhancements. + +[Get Composer]: https://getcomposer.org/download/ +[Parse PHP Guide]: https://www.parse.com/docs/php_guide \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..f99372f0 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "parse/php-sdk", + "description": "Parse PHP SDK", + "keywords": ["parse", "sdk"], + "type": "library", + "homepage": "https://github.com/parseplatform/parse-php-sdk", + "license": "Parse Platform License", + "authors": [ + { + "name": "Parse", + "homepage": "https://github.com/parseplatform/parse-php-sdk/contributors" + } + ], + "require": { + "php": ">=5.4.0", + "ext-curl": "*", + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*", + "squizlabs/php_codesniffer": "1.*", + "phpdocumentor/phpdocumentor": "*" + }, + "autoload": { + "psr-4": { + "Parse\\": "src/Parse/" + } + } +} \ No newline at end of file diff --git a/src/Parse/Internal/AddOperation.php b/src/Parse/Internal/AddOperation.php new file mode 100755 index 00000000..9584f500 --- /dev/null +++ b/src/Parse/Internal/AddOperation.php @@ -0,0 +1,108 @@ + + */ +class AddOperation implements FieldOperation +{ + + /** + * @var - Array with objects to add. + */ + private $objects; + + /** + * Creates an AddOperation with the provided objects. + * + * @param array $objects Objects to add. + * + * @throws ParseException + */ + public function __construct($objects) + { + if (!is_array($objects)) { + throw new ParseException("AddOperation requires an array."); + } + $this->objects = $objects; + } + + /** + * Gets the objects for this operation. + * + * @return mixed + */ + public function getValue() + { + return $this->objects; + } + + /** + * Returns associative array representing encoded operation. + * + * @return array + */ + public function _encode() + { + return array('__op' => 'Add', + 'objects' => ParseClient::_encode($this->objects, true)); + } + + /** + * Takes a previous operation and returns a merged operation to replace it. + * + * @param FieldOperation $previous Previous operation. + * + * @return FieldOperation Merged operation. + * @throws ParseException + */ + public function _mergeWithPrevious($previous) + { + if (!$previous) { + return $this; + } + if ($previous instanceof DeleteOperation) { + return new SetOperation($this->objects); + } + if ($previous instanceof SetOperation) { + $oldList = $previous->getValue(); + return new SetOperation( + array_merge((array)$oldList, (array)$this->objects) + ); + } + if ($previous instanceof AddOperation) { + $oldList = $previous->getValue(); + return new SetOperation( + array_merge((array)$oldList, (array)$this->objects) + ); + } + throw new ParseException( + 'Operation is invalid after previous operation.' + ); + } + + /** + * Applies current operation, returns resulting value. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $obj Value being applied. + * @param string $key Key this operation affects. + * + * @return array + */ + public function _apply($oldValue, $obj, $key) + { + if (!$oldValue) { + return $this->objects; + } + return array_merge((array)$oldValue, (array)$this->objects); + } + +} \ No newline at end of file diff --git a/src/Parse/Internal/AddUniqueOperation.php b/src/Parse/Internal/AddUniqueOperation.php new file mode 100755 index 00000000..c4666f31 --- /dev/null +++ b/src/Parse/Internal/AddUniqueOperation.php @@ -0,0 +1,136 @@ + + */ +class AddUniqueOperation implements FieldOperation +{ + + /** + * @var - Array containing objects to add. + */ + private $objects; + + /** + * Creates an operation for adding unique values to an array key. + * + * @param array $objects Objects to add. + * + * @throws ParseException + */ + public function __construct($objects) + { + if (!is_array($objects)) { + throw new ParseException("AddUniqueOperation requires an array."); + } + $this->objects = $objects; + } + + /** + * Returns the values for this operation. + * + * @return mixed + */ + public function getValue() + { + return $this->objects; + } + + /** + * Returns an associative array encoding of this operation. + * + * @return array + */ + public function _encode() + { + return array('__op' => 'AddUnique', + 'objects' => ParseClient::_encode($this->objects, true)); + } + + /** + * Merge this operation with the previous operation and return the result. + * + * @param FieldOperation $previous Previous Operation. + * + * @return FieldOperation Merged Operation. + * @throws ParseException + */ + public function _mergeWithPrevious($previous) + { + if (!$previous) { + return $this; + } + if ($previous instanceof DeleteOperation) { + return new SetOperation($this->objects); + } + if ($previous instanceof SetOperation) { + $oldValue = $previous->getValue(); + $result = $this->_apply($oldValue, null, null); + return new SetOperation($result); + } + if ($previous instanceof AddUniqueOperation) { + $oldList = $previous->getValue(); + $result = $this->_apply($oldList, null, null); + return new AddUniqueOperation($result); + } + throw new ParseException( + 'Operation is invalid after previous operation.' + ); + } + + /** + * Apply the current operation and return the result. + * + * @param mixed $oldValue Value prior to this operation. + * @param array $obj Value being applied. + * @param string $key Key this operation affects. + * + * @return array + */ + public function _apply($oldValue, $obj, $key) + { + if (!$oldValue) { + return $this->objects; + } + if (!is_array($oldValue)) { + $oldValue = (array)$oldValue; + } + foreach ($this->objects as $object) { + if ($object instanceof ParseObject && $object->getObjectId()) { + if (!$this->isParseObjectInArray($object, $oldValue)) { + $oldValue[] = $object; + } + } else if (is_object($object)) { + if (!in_array($object, $oldValue, true)) { + $oldValue[] = $object; + } + } else { + if (!in_array($object, $oldValue, true)) { + $oldValue[] = $object; + } + } + } + return $oldValue; + } + + private function isParseObjectInArray($parseObject, $oldValue) + { + foreach ($oldValue as $object) { + if ($object instanceof ParseObject && $object->getObjectId() != null) { + if ($object->getObjectId() == $parseObject->getObjectId()) { + return true; + } + } + } + return false; + } + +} \ No newline at end of file diff --git a/src/Parse/Internal/DeleteOperation.php b/src/Parse/Internal/DeleteOperation.php new file mode 100755 index 00000000..c55e0544 --- /dev/null +++ b/src/Parse/Internal/DeleteOperation.php @@ -0,0 +1,50 @@ + + */ +class DeleteOperation implements FieldOperation +{ + + /** + * Returns an associative array encoding of the current operation. + * + * @return array Associative array encoding the operation. + */ + public function _encode() + { + return array('__op' => 'Delete'); + } + + /** + * Applies the current operation and returns the result. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $object Unused for this operation type. + * @param string $key Key to remove from the target object. + * + * @return null + */ + public function _apply($oldValue, $object, $key) + { + return null; + } + + /** + * Merge this operation with a previous operation and return the result. + * + * @param FieldOperation $previous Previous operation. + * + * @return FieldOperation Always returns the current operation. + */ + public function _mergeWithPrevious($previous) + { + return $this; + } + +} \ No newline at end of file diff --git a/src/Parse/Internal/Encodable.php b/src/Parse/Internal/Encodable.php new file mode 100644 index 00000000..0236e8e3 --- /dev/null +++ b/src/Parse/Internal/Encodable.php @@ -0,0 +1,22 @@ + + */ +interface Encodable +{ + + /** + * Returns an associate array encoding of the implementing class. + * + * @return mixed + */ + public function _encode(); + +} \ No newline at end of file diff --git a/src/Parse/Internal/FieldOperation.php b/src/Parse/Internal/FieldOperation.php new file mode 100755 index 00000000..5c3b8c94 --- /dev/null +++ b/src/Parse/Internal/FieldOperation.php @@ -0,0 +1,35 @@ + + */ +interface FieldOperation extends Encodable +{ + + /** + * Applies the current operation and returns the result. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $object Value for this operation. + * @param string $key Key to perform this operation on. + * + * @return mixed Result of the operation. + */ + public function _apply($oldValue, $object, $key); + + /** + * Merge this operation with a previous operation and return the new + * operation. + * + * @param FieldOperation $previous Previous operation. + * + * @return FieldOperation Merged operation result. + */ + public function _mergeWithPrevious($previous); + +} \ No newline at end of file diff --git a/src/Parse/Internal/IncrementOperation.php b/src/Parse/Internal/IncrementOperation.php new file mode 100755 index 00000000..d6fe3822 --- /dev/null +++ b/src/Parse/Internal/IncrementOperation.php @@ -0,0 +1,99 @@ + + */ +class IncrementOperation implements FieldOperation +{ + + /** + * @var int - Amount to increment by. + */ + private $value; + + /** + * Creates an IncrementOperation object. + * + * @param int $value Amount to increment by. + */ + public function __construct($value = 1) + { + $this->value = $value; + } + + /** + * Get the value for this operation. + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * Get an associative array encoding for this operation. + * + * @return array + */ + public function _encode() + { + return array('__op' => 'Increment', 'amount' => $this->value); + } + + /** + * Apply the current operation and return the result. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $object Value for this operation. + * @param string $key Key to set Value on. + * + * @return int New value after application. + * @throws ParseException + */ + public function _apply($oldValue, $object, $key) + { + if ($oldValue && !is_numeric($oldValue)) { + throw new ParseException('Cannot increment a non-number type.'); + } + return $oldValue + $this->value; + } + + /** + * Merge this operation with a previous operation and return the + * resulting operation. + * + * @param FieldOperation $previous Previous Operation. + * + * @return FieldOperation + * @throws ParseException + */ + public function _mergeWithPrevious($previous) + { + if (!$previous) { + return $this; + } + if ($previous instanceof DeleteOperation) { + return new SetOperation($this->value); + } + if ($previous instanceof SetOperation) { + return new SetOperation($previous->getValue() + $this->value); + } + if ($previous instanceof IncrementOperation) { + return new IncrementOperation( + $previous->getValue() + $this->value + ); + } + throw new ParseException( + 'Operation is invalid after previous operation.' + ); + } + +} diff --git a/src/Parse/Internal/ParseRelationOperation.php b/src/Parse/Internal/ParseRelationOperation.php new file mode 100644 index 00000000..7f92a157 --- /dev/null +++ b/src/Parse/Internal/ParseRelationOperation.php @@ -0,0 +1,280 @@ + + */ + +class ParseRelationOperation implements FieldOperation{ + + /** + * @var string - The className of the target objects. + */ + private $targetClassName; + /** + * @var array - Array of objects to add to this relation. + */ + private $relationsToAdd = array(); + /** + * @var array - Array of objects to remove from this relation. + */ + private $relationsToRemove = array(); + + public function __construct($objectsToAdd, $objectsToRemove) + { + $this->targetClassName = null; + $this->relationsToAdd['null'] = array(); + $this->relationsToRemove['null'] = array(); + if ( $objectsToAdd !== null) { + $this->checkAndAssignClassName($objectsToAdd); + $this->addObjects($objectsToAdd, $this->relationsToAdd); + } + if ( $objectsToRemove !== null) { + $this->checkAndAssignClassName($objectsToRemove); + $this->addObjects($objectsToRemove, $this->relationsToRemove); + } + if ($this->targetClassName === null) { + throw new \Exception('Cannot create a ParseRelationOperation with no objects.'); + } + } + + /** + * Helper function to check that all passed ParseObjects have same class name + * and assign targetClassName variable. + * + * @param array $objects ParseObject array. + * + * @throws \Exception + */ + private function checkAndAssignClassName($objects) + { + foreach ($objects as $object) { + if ($this->targetClassName === null) { + $this->targetClassName = $object->getClassName(); + } + if ($this->targetClassName != $object->getClassName()) { + throw new \Exception('All objects in a relation must be of the same class.'); + } + } + } + + /** + * Adds an object or array of objects to the array, replacing any + * existing instance of the same object. + * + * @param array $objects Array of ParseObjects to add. + * @param array $container Array to contain new ParseObjects. + */ + private function addObjects($objects, &$container) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + foreach ($objects as $object) { + if ($object->getObjectId() == null) { + $container['null'][] = $object; + } else { + $container[$object->getObjectID()] = $object; + } + } + } + + /** + * Removes an object (and any duplicate instances of that object) from the array. + * + * @param array $objects Array of ParseObjects to remove. + * @param array $container Array to remove from it ParseObjects. + */ + private function removeObjects($objects, &$container) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + $nullObjects = array(); + foreach ($objects as $object) { + if ($object->getObjectId() == null) { + $nullObjects[] = $object; + } else { + unset($container[$object->getObjectID()]); + } + } + if (!empty($nullObjects)) { + self::removeElementsFromArray($nullObjects, $container['null']); + } + } + + + /** + * Applies the current operation and returns the result. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $object Value for this operation. + * @param string $key Key to perform this operation on. + * + * @return mixed Result of the operation. + * + * @throws \Exception + */ + public function _apply($oldValue, $object, $key) + { + if ($oldValue == null) { + return new ParseRelation($object, $key, $this->targetClassName); + } else if ($oldValue instanceof ParseRelation) { + if ($this->targetClassName != null + && $oldValue->getTargetClass() !== $this->targetClassName) { + throw new \Exception('Related object object must be of class ' + . $this->targetClassName . ', but ' . $oldValue->getTargetClass() + . ' was passed in.'); + } + return $oldValue; + } else { + throw new \Exception("Operation is invalid after previous operation."); + } + } + + /** + * Merge this operation with a previous operation and return the new + * operation. + * + * @param FieldOperation $previous Previous operation. + * + * @return FieldOperation Merged operation result. + * + * @throws \Exception + */ + public function _mergeWithPrevious($previous) + { + if ($previous == null) { + return $this; + } + if ($previous instanceof ParseRelationOperation) { + if ($previous->targetClassName != null + && $previous->targetClassName != $this->targetClassName + ) { + throw new \Exception('Related object object must be of class ' + . $this->targetClassName . ', but ' . $previous->targetClassName + . ' was passed in.'); + } + $newRelationToAdd = self::convertToOneDimensionalArray( + $this->relationsToAdd); + $newRelationToRemove = self::convertToOneDimensionalArray( + $this->relationsToRemove); + + $previous->addObjects($newRelationToAdd, + $previous->relationsToAdd); + $previous->removeObjects($newRelationToAdd, + $previous->relationsToRemove); + + $previous->removeObjects($newRelationToRemove, + $previous->relationsToAdd); + $previous->addObjects($newRelationToRemove, + $previous->relationsToRemove); + + $newRelationToAdd = self::convertToOneDimensionalArray( + $previous->relationsToAdd); + $newRelationToRemove = self::convertToOneDimensionalArray( + $previous->relationsToRemove); + + return new ParseRelationOperation($newRelationToAdd, + $newRelationToRemove); + } + throw new \Exception('Operation is invalid after previous operation.'); + } + + /** + * Returns an associative array encoding of the current operation. + * + * @return mixed + * + * @throws \Exception + */ + public function _encode() + { + $addRelation = array(); + $removeRelation = array(); + if (!empty($this->relationsToAdd)) { + $addRelation = array( + '__op' => 'AddRelation', + 'objects' => ParseClient::_encode( + self::convertToOneDimensionalArray($this->relationsToAdd), + true + ) + ); + } + if (!empty($this->relationsToRemove)) { + $removeRelation = array( + '__op' => 'RemoveRelation', + 'objects' => ParseClient::_encode( + self::convertToOneDimensionalArray($this->relationsToRemove), + true + ) + ); + } + if (!empty($addRelation) && !empty($removeRelation)) { + return array( + '__op' => 'Batch', + 'ops' => [$addRelation, $removeRelation] + ); + } + return empty($addRelation) ? $removeRelation : $addRelation; + } + + public function _getTargetClass() + { + return $this->targetClassName; + } + + /** + * Remove element or array of elements from one dimensional array. + * + * @param mixed $elements + * @param array $array + */ + public static function removeElementsFromArray($elements, &$array) + { + if (!is_array($elements)) { + $elements = [$elements]; + } + $length = count($array); + for ($i = 0; $i < $length; $i++) { + $exist = false; + foreach ($elements as $element) { + if ($array[$i] == $element) { + $exist = true; + break; + } + } + if ($exist) { + unset($array[$i]); + } + } + $array = array_values($array); + } + + /** + * Convert any array to one dimensional array. + * + * @param array $array + * + * @return array + */ + public static function convertToOneDimensionalArray($array) + { + $newArray = array(); + if (is_array($array)) { + foreach ($array as $value) { + $newArray = array_merge($newArray, self::convertToOneDimensionalArray($value)); + } + } else { + $newArray[] = $array; + } + return $newArray; + } +} diff --git a/src/Parse/Internal/RemoveOperation.php b/src/Parse/Internal/RemoveOperation.php new file mode 100644 index 00000000..0e44f11b --- /dev/null +++ b/src/Parse/Internal/RemoveOperation.php @@ -0,0 +1,127 @@ + + */ +class RemoveOperation implements FieldOperation +{ + + /** + * @var - Array with objects to remove. + */ + private $objects; + + /** + * Creates an RemoveOperation with the provided objects. + * + * @param array $objects Objects to remove. + * + * @throws ParseException + */ + public function __construct($objects) + { + if (!is_array($objects)) { + throw new ParseException("RemoveOperation requires an array."); + } + $this->objects = $objects; + } + + /** + * Gets the objects for this operation. + * + * @return mixed + */ + public function getValue() + { + return $this->objects; + } + + /** + * Returns associative array representing encoded operation. + * + * @return array + */ + public function _encode() + { + return array('__op' => 'Remove', + 'objects' => ParseClient::_encode($this->objects, true)); + } + + /** + * Takes a previous operation and returns a merged operation to replace it. + * + * @param FieldOperation $previous Previous operation. + * + * @return FieldOperation Merged operation. + * @throws ParseException + */ + public function _mergeWithPrevious($previous) + { + if (!$previous) { + return $this; + } + if ($previous instanceof DeleteOperation) { + return $previous; + } + if ($previous instanceof SetOperation) { + return new SetOperation( + $this->_apply($previous->getValue(), $this->objects, null) + ); + } + if ($previous instanceof RemoveOperation) { + $oldList = $previous->getValue(); + return new RemoveOperation( + array_merge((array)$oldList, (array)$this->objects) + ); + } + throw new ParseException( + 'Operation is invalid after previous operation.' + ); + } + + /** + * Applies current operation, returns resulting value. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $obj Value being applied. + * @param string $key Key this operation affects. + * + * @return array + */ + public function _apply($oldValue, $obj, $key) + { + if (empty($oldValue)) { + return array(); + } + $newValue = array(); + foreach ($oldValue as $oldObject) { + foreach ($this->objects as $newObject) { + if ($oldObject instanceof ParseObject) { + if ($newObject instanceof ParseObject + && !$oldObject->isDirty() + && $oldObject->getObjectId() == $newObject->getObjectId()) { + // found the object, won't add it. + } else { + $newValue[] = $oldObject; + } + } else { + if ($oldObject !== $newObject) { + $newValue[] = $oldObject; + } + } + } + } + return $newValue; + } + +} \ No newline at end of file diff --git a/src/Parse/Internal/SetOperation.php b/src/Parse/Internal/SetOperation.php new file mode 100755 index 00000000..b4da5107 --- /dev/null +++ b/src/Parse/Internal/SetOperation.php @@ -0,0 +1,92 @@ + + */ +class SetOperation implements FieldOperation +{ + + /** + * @var - Value to set for this operation. + */ + private $value; + + /** + * @var - If the value should be forced as object. + */ + private $isAssociativeArray; + + /** + * Create a SetOperation with a value. + * + * @param mixed $value Value to set for this operation. + * @param bool $isAssociativeArray If the value should be forced as object. + */ + public function __construct($value, $isAssociativeArray = false) + { + $this->value = $value; + $this->isAssociativeArray = $isAssociativeArray; + } + + /** + * Get the value for this operation. + * + * @return mixed Value. + */ + public function getValue() + { + return $this->value; + } + + /** + * Returns an associative array encoding of the current operation. + * + * @return mixed + */ + public function _encode() + { + if ($this->isAssociativeArray) { + $object = new \stdClass(); + foreach ($this->value as $key => $value) { + $object->$key = $value; + } + return ParseClient::_encode($object, true); + } + return ParseClient::_encode($this->value, true); + } + + /** + * Apply the current operation and return the result. + * + * @param mixed $oldValue Value prior to this operation. + * @param mixed $object Value for this operation. + * @param string $key Key to set this value on. + * + * @return mixed + */ + public function _apply($oldValue, $object, $key) + { + return $this->value; + } + + /** + * Merge this operation with a previous operation and return the + * resulting operation. + * + * @param FieldOperation $previous Previous operation. + * + * @return FieldOperation + */ + public function _mergeWithPrevious($previous) + { + return $this; + } + +} \ No newline at end of file diff --git a/src/Parse/ParseACL.php b/src/Parse/ParseACL.php new file mode 100644 index 00000000..880da868 --- /dev/null +++ b/src/Parse/ParseACL.php @@ -0,0 +1,554 @@ + + */ +class ParseACL implements Encodable{ + + /* + * @ignore + */ + const PUBLIC_KEY = '*'; + /** + * @var array - + */ + private $permissionsById = array(); + /** + * @var bool - + */ + private $shared = false; + /** + * @var ParseUser - + */ + private static $lastCurrentUser = null; + /** + * @var ParseACL - + */ + private static $defaultACLWithCurrentUser = null; + /** + * @var ParseACL - + */ + private static $defaultACL = null; + /** + * @var bool - + */ + private static $defaultACLUsesCurrentUser = false; + + /** + * Create new ParseACL with read and write access for the given user. + * + * @param ParseUser $user + * + * @return ParseACL + */ + public static function createACLWithUser($user) + { + $acl = new ParseACL(); + $acl->setUserReadAccess($user, true); + $acl->setUserWriteAccess($user, true); + return $acl; + } + + /** + * Create new ParseACL from existing permissions. + * + * @param array $data represents permissions. + * + * @return ParseACL + * @throws \Exception + * @ignore + */ + public static function _createACLFromJSON($data) + { + $acl = new ParseACL(); + foreach ($data as $id => $permissions) { + if (!is_string($id)) { + throw new \Exception('Tried to create an ACL with an invalid userId.'); + } + foreach ($permissions as $accessType => $value) { + if ($accessType !== 'read' && $accessType !== 'write') { + throw new \Exception( + 'Tried to create an ACL with an invalid permission type.'); + } + if (!is_bool($value)) { + throw new \Exception( + 'Tried to create an ACL with an invalid permission value.'); + } + $acl->setAccess($accessType, $id, $value); + } + } + return $acl; + } + + /** + * Return if ParseACL shared or not. + * + * @return bool + * @ignore + */ + public function _isShared() + { + return $this->shared; + } + + /** + * Set shared for ParseACL + * + * @param bool $shared + * @ignore + */ + public function _setShared($shared) + { + $this->shared = $shared; + } + + /** + * @ignore + */ + public function _encode() + { + if (empty($this->permissionsById)) { + return new \stdClass(); + } + return $this->permissionsById; + } + + /** + * Set access permission with access name, user id and if + * the user has permission for accessing or not. + * + * @param string $accessType Access name. + * @param string $userId User id. + * @param bool $allowed If user allowed to access or not. + * + * @throws ParseException + */ + private function setAccess($accessType, $userId, $allowed) + { + if ($userId instanceof ParseUser) { + $userId = $userId->getObjectId(); + } + if ($userId instanceof ParseRole) { + $userId = "role:" . $userId->getName(); + } + if (!is_string($userId)) { + throw new ParseException( + "Invalid target for access control." + ); + } + if (!isset($this->permissionsById[$userId])) { + if (!$allowed) { + return; + } + $this->permissionsById[$userId] = array(); + } + if ($allowed) { + $this->permissionsById[$userId][$accessType] = true; + } else { + unset($this->permissionsById[$userId][$accessType]); + if (empty($this->permissionsById[$userId])) { + unset($this->permissionsById[$userId]); + } + } + } + + /** + * Get if the given userId has a permission for the given access type or not. + * + * @param string $accessType Access name. + * @param string $userId User id. + * + * @return bool + */ + private function getAccess($accessType, $userId) + { + if (!isset($this->permissionsById[$userId])) { + return false; + } + if (!isset($this->permissionsById[$userId][$accessType])) { + return false; + } + return $this->permissionsById[$userId][$accessType]; + } + + /** + * Set whether the given user id is allowed to read this object. + * + * @param string $userId User id. + * @param bool $allowed If user allowed to read or not. + * + * @throws \Exception + */ + public function setReadAccess($userId, $allowed) + { + if (!$userId) { + throw new \Exception("cannot setReadAccess for null userId"); + } + $this->setAccess('read', $userId, $allowed); + } + + /** + * Get whether the given user id is *explicitly* allowed to read this + * object. Even if this returns false, the user may still be able to + * access it if getPublicReadAccess returns true or a role that the + * user belongs to has read access. + * + * @param string $userId User id. + * + * @return bool + * + * @throws \Exception + */ + public function getReadAccess($userId) + { + if (!$userId) { + throw new \Exception("cannot getReadAccess for null userId"); + } + return $this->getAccess('read', $userId); + } + + /** + * Set whether the given user id is allowed to write this object. + * + * @param string $userId User id. + * @param bool $allowed If user allowed to write or not. + * + * @throws \Exception + */ + public function setWriteAccess($userId, $allowed) + { + if (!$userId) { + throw new \Exception("cannot setWriteAccess for null userId"); + } + $this->setAccess('write', $userId, $allowed); + } + + /** + * Get whether the given user id is *explicitly* allowed to write this + * object. Even if this returns false, the user may still be able to + * access it if getPublicWriteAccess returns true or a role that the + * user belongs to has write access. + * + * @param string $userId User id. + * + * @return bool + * + * @throws \Exception + */ + public function getWriteAccess($userId) + { + if (!$userId) { + throw new \Exception("cannot getWriteAccess for null userId"); + } + return $this->getAccess('write', $userId); + } + + + /** + * Set whether the public is allowed to read this object. + * + * @param bool $allowed + */ + public function setPublicReadAccess($allowed) + { + $this->setReadAccess(self::PUBLIC_KEY, $allowed); + } + + /** + * Get whether the public is allowed to read this object. + * + * @return bool + */ + public function getPublicReadAccess() + { + return $this->getReadAccess(self::PUBLIC_KEY); + } + + /** + * Set whether the public is allowed to write this object. + * + * @param bool $allowed + */ + public function setPublicWriteAccess($allowed) + { + $this->setWriteAccess(self::PUBLIC_KEY, $allowed); + } + + /** + * Get whether the public is allowed to write this object. + * + * @return bool + */ + public function getPublicWriteAccess() + { + return $this->getWriteAccess(self::PUBLIC_KEY); + } + + /** + * Set whether the given user is allowed to read this object. + * + * @param ParseUser $user + * @param bool $allowed + * + * @throws \Exception + */ + public function setUserReadAccess($user, $allowed) + { + if (!$user->getObjectId()) { + throw new \Exception("cannot setReadAccess for a user with null id"); + } + $this->setReadAccess($user->getObjectId(), $allowed); + } + + /** + * Get whether the given user is *explicitly* allowed to read this object. + * Even if this returns false, the user may still be able to access it if + * getPublicReadAccess returns true or a role that the user belongs to has + * read access. + * + * @param ParseUser $user + * + * @return bool + * + * @throws \Exception + */ + public function getUserReadAccess($user) + { + if (!$user->getObjectId()) { + throw new \Exception("cannot getReadAccess for a user with null id"); + } + return $this->getReadAccess($user->getObjectId()); + } + + /** + * Set whether the given user is allowed to write this object. + * + * @param ParseUser $user + * @param bool $allowed + * + * @throws \Exception + */ + public function setUserWriteAccess($user, $allowed) + { + if (!$user->getObjectId()) { + throw new \Exception("cannot setWriteAccess for a user with null id"); + } + $this->setWriteAccess($user->getObjectId(), $allowed); + } + + /** + * Get whether the given user is *explicitly* allowed to write this object. + * Even if this returns false, the user may still be able to access it if + * getPublicWriteAccess returns true or a role that the user belongs to has + * write access. + * + * @param ParseUser $user + * + * @return bool + * + * @throws \Exception + */ + public function getUserWriteAccess($user) + { + if (!$user->getObjectId()) { + throw new \Exception("cannot getWriteAccess for a user with null id"); + } + return $this->getWriteAccess($user->getObjectId()); + } + + /** + * Get whether users belonging to the role with the given roleName are + * allowed to read this object. Even if this returns false, the role may + * still be able to read it if a parent role has read access. + * + * @param string $roleName The name of the role. + * + * @return bool + */ + public function getRoleReadAccessWithName($roleName) + { + return $this->getReadAccess('role:' . $roleName); + } + + /** + * Set whether users belonging to the role with the given roleName + * are allowed to read this object. + * + * @param string $roleName The name of the role. + * + * @param bool $allowed Whether the given role can read this object. + */ + public function setRoleReadAccessWithName($roleName, $allowed) + { + $this->setReadAccess('role:' . $roleName, $allowed); + } + + /** + * Get whether users belonging to the role with the given roleName are + * allowed to write this object. Even if this returns false, the role may + * still be able to write it if a parent role has write access. + * + * @param string $roleName The name of the role. + * + * @return bool + */ + public function getRoleWriteAccessWithName($roleName) + { + return $this->getWriteAccess('role:' . $roleName); + } + + /** + * Set whether users belonging to the role with the given roleName + * are allowed to write this object. + * + * @param string $roleName The name of the role. + * @param bool $allowed Whether the given role can write this object. + */ + public function setRoleWriteAccessWithName($roleName, $allowed) + { + $this->setWriteAccess('role:' . $roleName, $allowed); + } + + /** + * Check whether the role is valid or not. + * + * @param ParseRole $role + * + * @throws \Exception + */ + private static function validateRoleState($role) + { + if (!$role->getObjectId()) { + throw new \Exception( + "Roles must be saved to the server before they can be used in an ACL."); + } + } + + /** + * Get whether users belonging to the given role are allowed to read this + * object. Even if this returns false, the role may still be able to read + * it if a parent role has read access. The role must already be saved on + * the server and its data must have been fetched in order to use this method. + * + * @param ParseRole $role The role to check for access. + * + * @return bool + */ + public function getRoleReadAccess($role) + { + $this->validateRoleState($role); + return $this->getRoleReadAccessWithName($role->getName()); + } + + /** + * Set whether users belonging to the given role are allowed to read this + * object. The role must already be saved on the server and its data must + * have been fetched in order to use this method. + * + * @param ParseRole $role The role to assign access. + * @param bool $allowed Whether the given role can read this object. + */ + public function setRoleReadAccess($role, $allowed) + { + $this->validateRoleState($role); + $this->setRoleReadAccessWithName($role->getName(), $allowed); + } + + /** + * Get whether users belonging to the given role are allowed to write this + * object. Even if this returns false, the role may still be able to write + * it if a parent role has write access. The role must already be saved on + * the server and its data must have been fetched in order to use this method. + * + * @param ParseRole $role The role to check for access. + * + * @return bool + */ + public function getRoleWriteAccess($role) + { + $this->validateRoleState($role); + return $this->getRoleWriteAccessWithName($role->getName()); + } + + /** + * Set whether users belonging to the given role are allowed to write this + * object. The role must already be saved on the server and its data must + * have been fetched in order to use this method. + * + * @param ParseRole $role The role to assign access. + * @param bool $allowed Whether the given role can read this object. + */ + public function setRoleWriteAccess($role, $allowed) + { + $this->validateRoleState($role); + $this->setWriteAccessWithName($role->getName(), $allowed); + } + + /** + * Sets a default ACL that will be applied to all ParseObjects when they + * are created. + * + * @param ParseACL $acl The ACL to use as a template for all ParseObjects + * created after setDefaultACL has been called. This + * value will be copied and used as a template for the + * creation of new ACLs, so changes to the instance + * after setDefaultACL() has been called will not be + * reflected in new ParseObjects. + * @param bool $withAccessForCurrentUser If true, the ParseACL that is applied to + * newly-created ParseObjects will provide read + * and write access to the ParseUser#getCurrentUser() + * at the time of creation. If false, the provided + * ACL will be used without modification. If acl is + * null, this value is ignored. + */ + public static function setDefaultACL($acl, $withAccessForCurrentUser) + { + self::$defaultACLWithCurrentUser = null; + self::$lastCurrentUser = null; + if ($acl) { + self::$defaultACL = clone $acl; + self::$defaultACL->_setShared(true); + self::$defaultACLUsesCurrentUser = $withAccessForCurrentUser; + } else { + self::$defaultACL = null; + } + } + + /** + * Get the defaultACL. + * + * @return ParseACL + * @ignore + */ + public static function _getDefaultACL() + { + if (self::$defaultACLUsesCurrentUser && self::$defaultACL) { + $last = self::$lastCurrentUser ? clone self::$lastCurrentUser : null; + if (!ParseUser::getCurrentUser()) { + return self::$defaultACL; + } + if ($last != ParseUser::getCurrentUser()) { + self::$defaultACLWithCurrentUser = clone self::$defaultAC; + self::$defaultACLWithCurrentUser->_setShared(true); + self::$defaultACLWithCurrentUser->setUserReadAccess(ParseUser::getCurrentUser(), true); + self::$defaultACLWithCurrentUser->setUserWriteAccess(ParseUser::getCurrentUser(), true); + self::$lastCurrentUser = clone ParseUser::getCurrentUser(); + } + return self::$defaultACLWithCurrentUser; + } + return self::$defaultACL; + } + +} diff --git a/src/Parse/ParseAggregateException.php b/src/Parse/ParseAggregateException.php new file mode 100644 index 00000000..58410118 --- /dev/null +++ b/src/Parse/ParseAggregateException.php @@ -0,0 +1,39 @@ + + */ +class ParseAggregateException extends ParseException +{ + + private $errors; + + /** + * Constructs a Parse\ParseAggregateException + * + * @param string $message Message for the Exception. + * @param array $errors Collection of error values. + * @param \Exception $previous Previous exception. + */ + public function __construct($message, $errors = array(), $previous = null) + { + parent::__construct($message, 600, $previous); + $this->errors = $errors; + } + + /** + * Return the aggregated errors that were thrown. + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + +} \ No newline at end of file diff --git a/src/Parse/ParseAnalytics.php b/src/Parse/ParseAnalytics.php new file mode 100644 index 00000000..8f40b22a --- /dev/null +++ b/src/Parse/ParseAnalytics.php @@ -0,0 +1,76 @@ + + */ +class ParseAnalytics +{ + + /** + * Tracks the occurrence of a custom event with additional dimensions. + * Parse will store a data point at the time of invocation with the given + * event name. + * + * Dimensions will allow segmentation of the occurrences of this custom + * event. Keys and values should be strings, and will throw + * otherwise. + * + * To track a user signup along with additional metadata, consider the + * following: + *
+   * $dimensions = array(
+   *  'gender' => 'm',
+   *  'source' => 'web',
+   *  'dayType' => 'weekend'
+   * );
+   * ParseAnalytics::track('signup', $dimensions);
+   * 
+ * + * There is a default limit of 4 dimensions per event tracked. + * + * @param string $name The name of the custom event + * @param array $dimensions The dictionary of segment information + * + * @throws \Exception + * @return mixed + */ + public static function track($name, $dimensions = array()) + { + $name = trim($name); + if (strlen($name) === 0) { + throw new Exception('A name for the custom event must be provided.'); + } + foreach ($dimensions as $key => $value) { + if (!is_string($key) || !is_string($value)) { + throw new Exception('Dimensions expected string keys and values.'); + } + } + return ParseClient::_request( + 'POST', + '/1/events/' . $name, + null, + static::_toSaveJSON($dimensions) + ); + } + + /** + * @ignore + */ + public static function _toSaveJSON($data) + { + return json_encode( + array( + 'dimensions' => $data + ), + JSON_FORCE_OBJECT + ); + } + +} \ No newline at end of file diff --git a/src/Parse/ParseBytes.php b/src/Parse/ParseBytes.php new file mode 100644 index 00000000..506f72a4 --- /dev/null +++ b/src/Parse/ParseBytes.php @@ -0,0 +1,75 @@ + + */ +class ParseBytes implements Internal\Encodable +{ + + /** + * @var - byte array + */ + private $byteArray; + + /** + * Create a ParseBytes object with a given byte array. + * + * @param array $byteArray + * + * @return ParseBytes + */ + public static function createFromByteArray(array $byteArray) + { + $bytes = new ParseBytes(); + $bytes->setByteArray($byteArray); + return $bytes; + } + + /** + * Create a ParseBytes object with a given base 64 encoded data string + * + * @param string $base64Data + * + * @return ParseBytes + */ + public static function createFromBase64Data($base64Data) + { + $bytes = new ParseBytes(); + $bytes->setBase64Data($base64Data); + return $bytes; + } + + private function setBase64Data($base64Data) + { + $byteArray = unpack('C*', base64_decode($base64Data)); + $this->setByteArray($byteArray); + } + + private function setByteArray(array $byteArray) + { + $this->byteArray = $byteArray; + } + + /** + * Encode to associative array representation + * + * @return array + * @ignore + */ + public function _encode() + { + $data = ""; + foreach ($this->byteArray as $byte) { + $data .= chr($byte); + } + return array( + '__type' => 'Bytes', + 'base64' => base64_encode($data) + ); + } +} diff --git a/src/Parse/ParseClient.php b/src/Parse/ParseClient.php new file mode 100755 index 00000000..a405bbad --- /dev/null +++ b/src/Parse/ParseClient.php @@ -0,0 +1,360 @@ + + */ +final class ParseClient +{ + + /** + * Constant for the API Server Host Address. + * @ignore + */ + const HOST_NAME = 'https://api.parse.com'; + + /** + * @var - String for applicationId. + */ + private static $applicationId; + + /** + * @var - String for REST API Key. + */ + private static $restKey; + + /** + * @var - String for Master Key. + */ + private static $masterKey; + + /** + * @var ParseStorageInterface Object for managing persistence + */ + private static $storage; + + /** + * Constant for version string to include with requests. + * @ignore + */ + const VERSION_STRING = 'php1.0.0'; + + /** + * Parse\Client::initialize, must be called before using Parse features. + * + * @param string $app_id Parse Application ID + * @param string $rest_key Parse REST API Key + * @param string $master_key Parse Master Key + * + * @return null + */ + public static function initialize($app_id, $rest_key, $master_key) + { + ParseUser::registerSubclass(); + ParseRole::registerSubclass(); + ParseInstallation::registerSubclass(); + self::$applicationId = $app_id; + self::$restKey = $rest_key; + self::$masterKey = $master_key; + if (!static::$storage) { + if (session_status() === PHP_SESSION_ACTIVE) { + self::setStorage(new ParseSessionStorage()); + } else { + self::setStorage(new ParseMemoryStorage()); + } + } + } + + /** + * ParseClient::_encode, internal method for encoding object values. + * + * @param mixed $value Value to encode + * @param bool $allowParseObjects Allow nested objects + * + * @return mixed Encoded results. + * + * @throws \Exception + * @ignore + */ + public static function _encode($value, $allowParseObjects) + { + if ($value instanceof \DateTime) { + return array( + '__type' => 'Date', 'iso' => self::getProperDateFormat($value) + ); + } + + if ($value instanceof \stdClass) { + return $value; + } + + if ($value instanceof ParseObject) { + if (!$allowParseObjects) { + throw new \Exception('ParseObjects not allowed here.'); + } + return $value->_toPointer(); + } + + if ($value instanceof Encodable) { + return $value->_encode(); + } + + if (is_array($value)) { + return self::_encodeArray($value, $allowParseObjects); + } + + if (!is_scalar($value) && $value !== null) { + throw new \Exception('Invalid type encountered.'); + } + return $value; + } + + /** + * ParseClient::_decode, internal method for decoding server responses. + * + * @param mixed $data The value to decode + * + * @return mixed + * @ignore + */ + public static function _decode($data) + { + // The json decoded response from Parse will make JSONObjects into stdClass + // objects. We'll change it to an associative array here. + if ($data instanceof \stdClass) { + $tmp = (array)$data; + if (!empty($tmp)) { + return self::_decode(get_object_vars($data)); + } + } + + if (!$data && !is_array($data)) { + return null; + } + + if (is_array($data)) { + $typeString = (isset($data['__type']) ? $data['__type'] : null); + + if ($typeString === 'Date') { + return new \DateTime($data['iso']); + } + + if ($typeString === 'Bytes') { + return base64_decode($data['base64']); + } + + if ($typeString === 'Pointer') { + return ParseObject::create($data['className'], $data['objectId']); + } + + if ($typeString === 'File') { + return ParseFile::_createFromServer($data['name'], $data['url']); + } + + if ($typeString === 'GeoPoint') { + return new ParseGeoPoint($data['latitude'], $data['longitude']); + } + + if ($typeString === 'Object') { + $output = ParseObject::create($data['className']); + $output->_mergeAfterFetch($data); + return $output; + } + + if ($typeString === 'Relation') { + return $data; + } + + $newDict = array(); + foreach ($data as $key => $value) { + $newDict[$key] = static::_decode($value); + } + return $newDict; + + } + + return $data; + } + + /** + * ParseClient::_encodeArray, internal method for encoding arrays. + * + * @param array $value Array to encode. + * @param bool $allowParseObjects Allow nested objects. + * + * @return array Encoded results. + * @ignore + */ + public static function _encodeArray($value, $allowParseObjects) + { + $output = array(); + foreach ($value as $key => $item) { + $output[$key] = self::_encode($item, $allowParseObjects); + } + return $output; + } + + /** + * Parse\Client::_request, internal method for communicating with Parse. + * + * @param string $method HTTP Method for this request. + * @param string $relativeUrl REST API Path. + * @param null $sessionToken Session Token. + * @param null $data Data to provide with the request. + * @param bool $useMasterKey Whether to use the Master Key. + * + * @return mixed Result from Parse API Call. + * @throws \Exception + * @ignore + */ + public static function _request($method, $relativeUrl, $sessionToken = null, + $data = null, $useMasterKey = false) + { + if ($data === '[]') { + $data = '{}'; + } + self::assertParseInitialized(); + $headers = self::_getRequestHeaders($sessionToken, $useMasterKey); + + $url = self::HOST_NAME . $relativeUrl; + if ($method === 'GET' && !empty($data)) { + $url .= '?' . http_build_query($data); + } + $rest = curl_init(); + curl_setopt($rest, CURLOPT_URL, $url); + curl_setopt($rest, CURLOPT_RETURNTRANSFER, 1); + if ($method === 'POST') { + $headers[] = 'Content-Type: application/json'; + curl_setopt($rest, CURLOPT_POST, 1); + curl_setopt($rest, CURLOPT_POSTFIELDS, $data); + } + if ($method === 'PUT') { + $headers[] = 'Content-Type: application/json'; + curl_setopt($rest, CURLOPT_CUSTOMREQUEST, $method); + curl_setopt($rest, CURLOPT_POSTFIELDS, $data); + } + if ($method === 'DELETE') { + curl_setopt($rest, CURLOPT_CUSTOMREQUEST, $method); + } + curl_setopt($rest, CURLOPT_HTTPHEADER, $headers); + $response = curl_exec($rest); + $status = curl_getinfo($rest, CURLINFO_HTTP_CODE); + $contentType = curl_getinfo($rest, CURLINFO_CONTENT_TYPE); + if (curl_errno($rest)) { + throw new ParseException(curl_error($rest), curl_errno($rest)); + } + curl_close($rest); + if (strpos($contentType, 'text/html') !== false) { + throw new ParseException('Bad Request', -1); + } + + $decoded = json_decode($response, true); + if (isset($decoded['error'])) { + throw new ParseException($decoded['error'], + isset($decoded['code']) ? $decoded['code'] : 0 + ); + } + return $decoded; + + } + + /** + * ParseClient::setStorage, will update the storage object used for + * persistence. + * + * @param ParseStorageInterface $storageObject + * + * @return null + */ + public static function setStorage(ParseStorageInterface $storageObject) + { + self::$storage = $storageObject; + } + + /** + * ParseClient::getStorage, will return the storage object used for + * persistence. + + * @return ParseStorageInterface + */ + public static function getStorage() + { + return self::$storage; + } + + /** + * ParseClient::_unsetStorage, will null the storage object. + * + * Without some ability to clear the storage objects, all test cases would + * use the first assigned storage object. + * + * @return null + * @ignore + */ + public static function _unsetStorage() + { + self::$storage = null; + } + + private static function assertParseInitialized() + { + if (self::$applicationId === null) { + throw new \Exception( + 'You must call Parse::initialize() before making any requests.' + ); + } + } + + /** + * @param $sessionToken + * @param $useMasterKey + * + * @return array + * @ignore + */ + public static function _getRequestHeaders($sessionToken, $useMasterKey) + { + $headers = array('X-Parse-Application-Id: ' . self::$applicationId, + 'X-Parse-Client-Version: ' . self::VERSION_STRING); + if ($sessionToken) { + $headers[] = 'X-Parse-Session-Token: ' . $sessionToken; + } + if ($useMasterKey) { + $headers[] = 'X-Parse-Master-Key: ' . self::$masterKey; + } else { + $headers[] = 'X-Parse-REST-API-Key: ' . self::$restKey; + } + /** + * Set an empty Expect header to stop the 100-continue behavior for post + * data greater than 1024 bytes. + * http://pilif.github.io/2007/02/the-return-of-except-100-continue/ + */ + $headers[] = 'Expect: '; + return $headers; + } + + /** + * Get a date value in the format stored on Parse. + * + * All the SDKs do some slightly different date handling. + * PHP provides 6 digits for the microseconds (u) so we have to chop 3 off. + * + * @param \DateTime $value DateTime value to format. + * + * @return string + */ + public static function getProperDateFormat($value) + { + $dateFormatString = 'Y-m-d\TH:i:s.u'; + $date = date_format($value, $dateFormatString); + $date = substr($date, 0, -3) . 'Z'; + return $date; + } + +} \ No newline at end of file diff --git a/src/Parse/ParseCloud.php b/src/Parse/ParseCloud.php new file mode 100644 index 00000000..dec5a426 --- /dev/null +++ b/src/Parse/ParseCloud.php @@ -0,0 +1,35 @@ + + */ +class ParseCloud +{ + + /** + * Makes a call to a Cloud function + * + * @param string $name Cloud function name + * @param array $data Parameters to pass + * @param boolean $useMasterKey Whether to use the Master Key + * + * @return mixed + */ + public static function run($name, $data = array(), $useMasterKey = false) + { + $response = ParseClient::_request( + 'POST', + '/1/functions/' . $name, + null, + json_encode(ParseClient::_encode($data, null, false)), + $useMasterKey + ); + return ParseClient::_decode($response['result']); + } + +} \ No newline at end of file diff --git a/src/Parse/ParseException.php b/src/Parse/ParseException.php new file mode 100644 index 00000000..b10499a6 --- /dev/null +++ b/src/Parse/ParseException.php @@ -0,0 +1,27 @@ + + */ +class ParseException extends \Exception +{ + + /** + * Constructs a Parse\Exception + * + * @param string $message Message for the Exception. + * @param int $code Error code. + * @param \Exception $previous Previous Exception. + */ + public function __construct($message, $code = 0, + \Exception $previous = null) + { + parent::__construct($message, $code, $previous); + } + +} \ No newline at end of file diff --git a/src/Parse/ParseFile.php b/src/Parse/ParseFile.php new file mode 100755 index 00000000..a9313188 --- /dev/null +++ b/src/Parse/ParseFile.php @@ -0,0 +1,415 @@ + + */ +class ParseFile implements \Parse\Internal\Encodable +{ + + /** + * @var - Filename + */ + private $name; + /** + * @var - URL of File data stored on Parse. + */ + private $url; + /** + * @var - Data + */ + private $data; + /** + * @var - Mime type + */ + private $mimeType; + + /** + * Return the data for the file, downloading it if not already present. + * + * @returns mixed + * + * @throws ParseException + */ + public function getData() + { + if ($this->data) { + return $this->data; + } + if (!$this->url) { + throw new ParseException("Cannot retrieve data for unsaved ParseFile."); + } + $this->data = $this->download(); + return $this->data; + } + + /** + * Return the URL for the file, if saved. + * + * @returns string|null + */ + public function getURL() + { + return $this->url; + } + + /** + * Return the name for the file + * Upon saving to Parse, the name will change to a unique identifier. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Return the mimeType for the file, if set. + * + * @return string|null + */ + public function getMimeType() + { + return $this->mimeType; + } + + /** + * Create a Parse File from data + * i.e. $file = ParseFile::createFromData("hello world!", "hi.txt"); + * + * @param mixed $contents The file contents + * @param string $name The file name on Parse, can be used to detect mimeType + * @param string $mimeType Optional, The mime-type to use when saving the file + * + * @returns ParseFile + */ + public static function createFromData($contents, $name, $mimeType = null) + { + $file = new ParseFile(); + $file->name = $name; + $file->mimeType = $mimeType; + $file->data = $contents; + return $file; + } + + /** + * Create a Parse File from the contents of a local file + * i.e. $file = ParseFile::createFromFile("/tmp/foo.bar", + * "foo.bar"); + * + * @param string $path Path to local file + * @param string $name Filename to use on Parse, can be used to detect mimeType + * @param string $mimeType Optional, The mime-type to use when saving the file + * + * @returns ParseFile + */ + public static function createFromFile($path, $name, $mimeType = null) + { + $contents = file_get_contents($path, "rb"); + return static::createFromData($contents, $name, $mimeType); + } + + /** + * Internal method used when constructing a Parse File from Parse. + * + * @param $name + * @param $url + * + * @return ParseFile + * @ignore + */ + public static function _createFromServer($name, $url) + { + $file = new ParseFile(); + $file->name = $name; + $file->url = $url; + return $file; + } + + /** + * Encode to associative array representation. + * + * @return string + * @ignore + */ + public function _encode() + { + return array( + '__type' => 'File', + 'url' => $this->url, + 'name' => $this->name + ); + } + + /** + * Uploads the file contents to Parse, if not saved. + * + * @return bool + */ + public function save() + { + if (!$this->url) { + $response = $this->upload(); + $this->url = $response['url']; + $this->name = $response['name']; + } + return true; + } + + private function upload() + { + $fileParts = explode('.', $this->getName()); + $extension = array_pop($fileParts); + $mimeType = $this->mimeType ?: $this->getMimeTypeForExtension($extension); + + $headers = ParseClient::_getRequestHeaders(null, false); + $url = ParseClient::HOST_NAME . '/1/files/' . $this->getName(); + $rest = curl_init(); + curl_setopt($rest, CURLOPT_URL, $url); + curl_setopt($rest, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($rest, CURLOPT_BINARYTRANSFER, 1); + $headers[] = 'Content-Type: ' . $mimeType; + curl_setopt($rest, CURLOPT_POST, 1); + curl_setopt($rest, CURLOPT_POSTFIELDS, $this->getData()); + curl_setopt($rest, CURLOPT_HTTPHEADER, $headers); + $response = curl_exec($rest); + $contentType = curl_getinfo($rest, CURLINFO_CONTENT_TYPE); + if (curl_errno($rest)) { + throw new ParseException(curl_error($rest), curl_errno($rest)); + } + curl_close($rest); + if (strpos($contentType, 'text/html') !== false) { + throw new ParseException('Bad Request', -1); + } + + $decoded = json_decode($response, true); + if (isset($decoded['error'])) { + throw new ParseException($decoded['error'], + isset($decoded['code']) ? $decoded['code'] : 0 + ); + } + return $decoded; + + } + + private function download() + { + $rest = curl_init(); + curl_setopt($rest, CURLOPT_URL, $this->url); + curl_setopt($rest, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($rest, CURLOPT_BINARYTRANSFER, 1); + $response = curl_exec($rest); + if (curl_errno($rest)) { + throw new ParseException(curl_error($rest), curl_errno($rest)); + } + $this->mimeType = curl_getinfo($rest, CURLINFO_CONTENT_TYPE); + $this->data = $response; + curl_close($rest); + return $response; + } + + private function getMimeTypeForExtension($extension) + { + $knownTypes = array( + "ai" => "application/postscript", + "aif" => "audio/x-aiff", + "aifc" => "audio/x-aiff", + "aiff" => "audio/x-aiff", + "asc" => "text/plain", + "atom" => "application/atom+xml", + "au" => "audio/basic", + "avi" => "video/x-msvideo", + "bcpio" => "application/x-bcpio", + "bin" => "application/octet-stream", + "bmp" => "image/bmp", + "cdf" => "application/x-netcdf", + "cgm" => "image/cgm", + "class" => "application/octet-stream", + "cpio" => "application/x-cpio", + "cpt" => "application/mac-compactpro", + "csh" => "application/x-csh", + "css" => "text/css", + "dcr" => "application/x-director", + "dif" => "video/x-dv", + "dir" => "application/x-director", + "djv" => "image/vnd.djvu", + "djvu" => "image/vnd.djvu", + "dll" => "application/octet-stream", + "dmg" => "application/octet-stream", + "dms" => "application/octet-stream", + "doc" => "application/msword", + "docx" =>"application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "docm" =>"application/vnd.ms-word.document.macroEnabled.12", + "dotm" =>"application/vnd.ms-word.template.macroEnabled.12", + "dtd" => "application/xml-dtd", + "dv" => "video/x-dv", + "dvi" => "application/x-dvi", + "dxr" => "application/x-director", + "eps" => "application/postscript", + "etx" => "text/x-setext", + "exe" => "application/octet-stream", + "ez" => "application/andrew-inset", + "gif" => "image/gif", + "gram" => "application/srgs", + "grxml" => "application/srgs+xml", + "gtar" => "application/x-gtar", + "hdf" => "application/x-hdf", + "hqx" => "application/mac-binhex40", + "htm" => "text/html", + "html" => "text/html", + "ice" => "x-conference/x-cooltalk", + "ico" => "image/x-icon", + "ics" => "text/calendar", + "ief" => "image/ief", + "ifb" => "text/calendar", + "iges" => "model/iges", + "igs" => "model/iges", + "jnlp" => "application/x-java-jnlp-file", + "jp2" => "image/jp2", + "jpe" => "image/jpeg", + "jpeg" => "image/jpeg", + "jpg" => "image/jpeg", + "js" => "application/x-javascript", + "kar" => "audio/midi", + "latex" => "application/x-latex", + "lha" => "application/octet-stream", + "lzh" => "application/octet-stream", + "m3u" => "audio/x-mpegurl", + "m4a" => "audio/mp4a-latm", + "m4b" => "audio/mp4a-latm", + "m4p" => "audio/mp4a-latm", + "m4u" => "video/vnd.mpegurl", + "m4v" => "video/x-m4v", + "mac" => "image/x-macpaint", + "man" => "application/x-troff-man", + "mathml" => "application/mathml+xml", + "me" => "application/x-troff-me", + "mesh" => "model/mesh", + "mid" => "audio/midi", + "midi" => "audio/midi", + "mif" => "application/vnd.mif", + "mov" => "video/quicktime", + "movie" => "video/x-sgi-movie", + "mp2" => "audio/mpeg", + "mp3" => "audio/mpeg", + "mp4" => "video/mp4", + "mpe" => "video/mpeg", + "mpeg" => "video/mpeg", + "mpg" => "video/mpeg", + "mpga" => "audio/mpeg", + "ms" => "application/x-troff-ms", + "msh" => "model/mesh", + "mxu" => "video/vnd.mpegurl", + "nc" => "application/x-netcdf", + "oda" => "application/oda", + "ogg" => "application/ogg", + "pbm" => "image/x-portable-bitmap", + "pct" => "image/pict", + "pdb" => "chemical/x-pdb", + "pdf" => "application/pdf", + "pgm" => "image/x-portable-graymap", + "pgn" => "application/x-chess-pgn", + "pic" => "image/pict", + "pict" => "image/pict", + "png" => "image/png", + "pnm" => "image/x-portable-anymap", + "pnt" => "image/x-macpaint", + "pntg" => "image/x-macpaint", + "ppm" => "image/x-portable-pixmap", + "ppt" => "application/vnd.ms-powerpoint", + "pptx" =>"application/vnd.openxmlformats-officedocument.presentationml.presentation", + "potx" =>"application/vnd.openxmlformats-officedocument.presentationml.template", + "ppsx" =>"application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "ppam" =>"application/vnd.ms-powerpoint.addin.macroEnabled.12", + "pptm" =>"application/vnd.ms-powerpoint.presentation.macroEnabled.12", + "potm" =>"application/vnd.ms-powerpoint.template.macroEnabled.12", + "ppsm" =>"application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + "ps" => "application/postscript", + "qt" => "video/quicktime", + "qti" => "image/x-quicktime", + "qtif" => "image/x-quicktime", + "ra" => "audio/x-pn-realaudio", + "ram" => "audio/x-pn-realaudio", + "ras" => "image/x-cmu-raster", + "rdf" => "application/rdf+xml", + "rgb" => "image/x-rgb", + "rm" => "application/vnd.rn-realmedia", + "roff" => "application/x-troff", + "rtf" => "text/rtf", + "rtx" => "text/richtext", + "sgm" => "text/sgml", + "sgml" => "text/sgml", + "sh" => "application/x-sh", + "shar" => "application/x-shar", + "silo" => "model/mesh", + "sit" => "application/x-stuffit", + "skd" => "application/x-koan", + "skm" => "application/x-koan", + "skp" => "application/x-koan", + "skt" => "application/x-koan", + "smi" => "application/smil", + "smil" => "application/smil", + "snd" => "audio/basic", + "so" => "application/octet-stream", + "spl" => "application/x-futuresplash", + "src" => "application/x-wais-source", + "sv4cpio" => "application/x-sv4cpio", + "sv4crc" => "application/x-sv4crc", + "svg" => "image/svg+xml", + "swf" => "application/x-shockwave-flash", + "t" => "application/x-troff", + "tar" => "application/x-tar", + "tcl" => "application/x-tcl", + "tex" => "application/x-tex", + "texi" => "application/x-texinfo", + "texinfo" => "application/x-texinfo", + "tif" => "image/tiff", + "tiff" => "image/tiff", + "tr" => "application/x-troff", + "tsv" => "text/tab-separated-values", + "txt" => "text/plain", + "ustar" => "application/x-ustar", + "vcd" => "application/x-cdlink", + "vrml" => "model/vrml", + "vxml" => "application/voicexml+xml", + "wav" => "audio/x-wav", + "wbmp" => "image/vnd.wap.wbmp", + "wbmxl" => "application/vnd.wap.wbxml", + "wml" => "text/vnd.wap.wml", + "wmlc" => "application/vnd.wap.wmlc", + "wmls" => "text/vnd.wap.wmlscript", + "wmlsc" => "application/vnd.wap.wmlscriptc", + "wrl" => "model/vrml", + "xbm" => "image/x-xbitmap", + "xht" => "application/xhtml+xml", + "xhtml" => "application/xhtml+xml", + "xls" => "application/vnd.ms-excel", + "xml" => "application/xml", + "xpm" => "image/x-xpixmap", + "xsl" => "application/xml", + "xlsx" =>"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xltx" =>"application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "xlsm" =>"application/vnd.ms-excel.sheet.macroEnabled.12", + "xltm" =>"application/vnd.ms-excel.template.macroEnabled.12", + "xlam" =>"application/vnd.ms-excel.addin.macroEnabled.12", + "xlsb" =>"application/vnd.ms-excel.sheet.binary.macroEnabled.12", + "xslt" => "application/xslt+xml", + "xul" => "application/vnd.mozilla.xul+xml", + "xwd" => "image/x-xwindowdump", + "xyz" => "chemical/x-xyz", + "zip" => "application/zip" + ); + + if (isset($knownTypes[$extension])) { + return $knownTypes[$extension]; + } + return 'unknown/unknown'; + } + +} \ No newline at end of file diff --git a/src/Parse/ParseGeoPoint.php b/src/Parse/ParseGeoPoint.php new file mode 100755 index 00000000..11bef9f8 --- /dev/null +++ b/src/Parse/ParseGeoPoint.php @@ -0,0 +1,101 @@ + + */ +class ParseGeoPoint implements \Parse\Internal\Encodable +{ + + /** + * @var - Float value for latitude. + */ + private $latitude; + /** + * @var - Float value for longitude. + */ + private $longitude; + + /** + * Create a Parse GeoPoint object. + * + * @param float $lat Latitude. + * @param float $lon Longitude. + */ + public function __construct($lat, $lon) + { + $this->setLatitude($lat); + $this->setLongitude($lon); + } + + /** + * Returns the Latitude value for this GeoPoint. + * + * @return float + */ + public function getLatitude() + { + return $this->latitude; + } + + /** + * Set the Latitude value for this GeoPoint. + * + * @param $lat + * + * @throws ParseException + */ + public function setLatitude($lat) + { + if ($lat > 90.0 || $lat < -90.0) { + throw new ParseException("Latitude must be within range [-90.0, 90.0]"); + } + $this->latitude = $lat; + } + + /** + * Returns the Longitude value for this GeoPoint. + * + * @return float + */ + public function getLongitude() + { + return $this->longitude; + } + + /** + * Set the Longitude value for this GeoPoint. + * + * @param $lon + * + * @throws ParseException + */ + public function setLongitude($lon) + { + if ($lon > 180.0 || $lon < -180.0) { + throw new ParseException( + "Longitude must be within range [-180.0, 180.0]" + ); + } + $this->longitude = $lon; + } + + /** + * Encode to associative array representation + * + * @return array + * @ignore + */ + public function _encode() + { + return array( + '__type' => 'GeoPoint', + 'latitude' => $this->latitude, + 'longitude' => $this->longitude + ); + } +} diff --git a/src/Parse/ParseInstallation.php b/src/Parse/ParseInstallation.php new file mode 100644 index 00000000..10c61cd0 --- /dev/null +++ b/src/Parse/ParseInstallation.php @@ -0,0 +1,18 @@ + + */ +class ParseInstallation extends ParseObject +{ + + public static $parseClassName = "_Installation"; + +} \ No newline at end of file diff --git a/src/Parse/ParseMemoryStorage.php b/src/Parse/ParseMemoryStorage.php new file mode 100644 index 00000000..57b449fe --- /dev/null +++ b/src/Parse/ParseMemoryStorage.php @@ -0,0 +1,59 @@ + + */ +class ParseMemoryStorage implements ParseStorageInterface +{ + + /** + * @var array + */ + private $storage = array(); + + public function set($key, $value) + { + $this->storage[$key] = $value; + } + + public function remove($key) + { + unset($this->storage[$key]); + } + + public function get($key) + { + if (isset($this->storage[$key])) { + return $this->storage[$key]; + } + return null; + } + + public function clear() + { + $this->storage = array(); + } + + public function save() + { + // No action required. + return; + } + + public function getKeys() + { + return array_keys($this->storage); + } + + public function getAll() + { + return $this->storage; + } + +} \ No newline at end of file diff --git a/src/Parse/ParseObject.php b/src/Parse/ParseObject.php new file mode 100755 index 00000000..3dd2920b --- /dev/null +++ b/src/Parse/ParseObject.php @@ -0,0 +1,1189 @@ + + */ +class ParseObject implements Encodable +{ + + /** + * @var array - Data as it exists on the server. + */ + protected $serverData; + /** + * @var array - Set of unsaved operations. + */ + protected $operationSet; + /** + * @var array - Estimated value of applying operationSet to serverData. + */ + private $estimatedData; + /** + * @var array - Determine if data available for a given key or not. + */ + private $dataAvailability; + /** + * @var - Class Name for data on Parse. + */ + private $className; + /** + * @var string - Unique identifier on Parse. + */ + private $objectId; + /** + * @var \DateTime - Timestamp when object was created. + */ + private $createdAt; + /** + * @var \DateTime - Timestamp when object was last updated. + */ + private $updatedAt; + /** + * @var bool - Whether the object has been fully fetched from Parse. + */ + private $hasBeenFetched; + + /** + * @var array - Holds the registered subclasses and Parse class names. + */ + private static $registeredSubclasses = array(); + + /** + * Create a Parse Object + * + * Creates a pointer object if an objectId is provided, + * otherwise creates a new object. + * + * @param string $className Class Name for data on Parse. + * @param mixed $objectId Object Id for Existing object. + * @param bool $isPointer + * + * @throws Exception + */ + public function __construct($className = null, $objectId = null, + $isPointer = false) + { + $subclass = static::getSubclass(); + $class = get_called_class(); + if (!$className && $subclass !== false) { + $className = $subclass; + } + if ($class !== __CLASS__ && $className !== $subclass) { + throw new Exception( + 'You must specify a Parse class name or register the appropriate ' . + 'subclass when creating a new Object. Use ParseObject::create to ' . + 'create a subclass object.' + ); + } + + $this->className = $className; + $this->serverData = array(); + $this->operationSet = array(); + $this->estimatedData = array(); + $this->dataAvailability = array(); + if ($objectId || $isPointer) { + $this->objectId = $objectId; + $this->hasBeenFetched = false; + } else { + $this->hasBeenFetched = true; + } + } + + /** + * Gets the Subclass className if exists, otherwise false. + */ + private static function getSubclass() + { + return array_search(get_called_class(), self::$registeredSubclasses); + } + + /** + * Setter to catch property calls and protect certain fields. + * + * @param string $key Key to set a value on. + * @param mixed $value Value to assign. + * + * @return null + * @throws Exception + * @ignore + */ + public function __set($key, $value) + { + if ($key != 'objectId' + && $key != 'createdAt' + && $key != 'updatedAt' + && $key != 'className' + ) { + $this->set($key, $value); + } else { + throw new Exception('Protected field could not be set.'); + } + } + + /** + * Getter to catch direct property calls and pass them to the get function. + * + * @param string $key Key to retrieve from the Object. + * + * @return mixed + * @ignore + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Get current value for an object property. + * + * @param string $key Key to retrieve from the estimatedData array. + * + * @return mixed + * + * @throws \Exception + */ + public function get($key) + { + if (!$this->_isDataAvailable($key)) { + throw new \Exception( + 'ParseObject has no data for this key. Call fetch() to get the data.'); + } + if (isset($this->estimatedData[$key])) { + return $this->estimatedData[$key]; + } + return null; + } + + /** + * Check if the object has a given key + * + * @param string $key Key to check + * + * @return boolean + */ + public function has($key) + { + return isset($this->estimatedData[$key]); + } + + /** + * Check if the a value associated with a key has been + * added/updated/removed and not saved yet. + * + * @param string $key + * @return bool + */ + public function isKeyDirty($key) + { + return isset($this->operationSet[$key]); + } + + /** + * Check if the object or any of its child objects have unsaved operations. + * + * @return bool + */ + public function isDirty() + { + return $this->_isDirty(true); + } + + /** + * Detects if the object (and optionally the child objects) has unsaved + * changes. + * + * @param $considerChildren + * + * @return bool + * @ignore + */ + private function _isDirty($considerChildren) + { + return + (count($this->operationSet) || $this->objectId === null) || + ($considerChildren && $this->hasDirtyChildren()); + } + + private function hasDirtyChildren() + { + $result = false; + self::traverse(true, $this->estimatedData, function ($object) use (&$result) { + if ($object instanceof ParseObject) { + if ($object->isDirty()) { + $result = true; + } + } + }); + return $result; + } + + /** + * Validate and set a value for an object key. + * + * @param string $key Key to set a value for on the object. + * @param mixed $value Value to set on the key. + * + * @return null + * @throws Exception + */ + public function set($key, $value) + { + if (!$key) { + throw new Exception('key may not be null.'); + } + if (is_array($value)) { + throw new Exception( + 'Must use setArray() or setAssociativeArray() for this value.' + ); + } + $this->_performOperation($key, new SetOperation($value)); + } + + /** + * Set an array value for an object key. + * + * @param string $key Key to set the value for on the object. + * @param array $value Value to set on the key. + * + * @return null + * @throws Exception + */ + public function setArray($key, $value) + { + if (!$key) { + throw new Exception('key may not be null.'); + } + if (!is_array($value)) { + throw new Exception( + 'Must use set() for non-array values.' + ); + } + $this->_performOperation($key, new SetOperation($value)); + } + + /** + * Set an associative array value for an object key. + * + * @param string $key Key to set the value for on the object. + * @param array $value Value to set on the key. + * + * @return null + * @throws Exception + */ + public function setAssociativeArray($key, $value) + { + if (!$key) { + throw new Exception('key may not be null.'); + } + if (!is_array($value)) { + throw new Exception( + 'Must use set() for non-array values.' + ); + } + $this->_performOperation($key, new SetOperation($value, true)); + } + + /** + * Remove a value from an array for an object key. + * + * @param string $key Key to remove the value from on the object. + * @param mixed $value Value to remove from the array. + * + * @return null + * @throws Exception + */ + public function remove($key, $value) + { + if (!$key) { + throw new Exception('key may not be null.'); + } + if (!is_array($value)) { + $value = [$value]; + } + $this->_performOperation($key, new RemoveOperation($value)); + } + + /** + * Revert all unsaved operations. + * + * @return null + */ + public function revert() + { + $this->operationSet = array(); + $this->rebuildEstimatedData(); + } + + /** + * Clear all keys on this object by creating delete operations + * for each key. + * + * @return null + */ + public function clear() + { + foreach ($this->estimatedData as $key => $value) { + $this->delete($key); + } + } + + /** + * Perform an operation on an object property. + * + * @param string $key Key to perform an operation upon. + * @param FieldOperation $operation Operation to perform. + * + * @return null + * @ignore + */ + public function _performOperation($key, FieldOperation $operation) + { + $oldValue = null; + if (isset($this->estimatedData[$key])) { + $oldValue = $this->estimatedData[$key]; + } + $newValue = $operation->_apply($oldValue, $this, $key); + if ($newValue !== null) { + $this->estimatedData[$key] = $newValue; + } else if (isset($this->estimatedData[$key])) { + unset($this->estimatedData[$key]); + } + + if (isset($this->operationSet[$key])) { + $oldOperations = $this->operationSet[$key]; + $newOperations = $operation->_mergeWithPrevious($oldOperations); + $this->operationSet[$key] = $newOperations; + } else { + $this->operationSet[$key] = $operation; + } + $this->dataAvailability[$key] = true; + } + + /** + * Get the Parse Class Name for the object. + * + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * Get the objectId for the object, or null if unsaved. + * + * @return string|null + */ + public function getObjectId() + { + return $this->objectId; + } + + /** + * Get the createdAt for the object, or null if unsaved. + * + * @return \DateTime|null + */ + public function getCreatedAt() + { + return $this->createdAt; + } + + /** + * Returns true if the object has been fetched. + * + * @return bool + */ + public function isDataAvailable() + { + return $this->hasBeenFetched; + } + + private function _isDataAvailable($key) + { + return $this->isDataAvailable() || isset($this->dataAvailability[$key]); + + } + + /** + * Get the updatedAt for the object, or null if unsaved. + * + * @return \DateTime|null + */ + public function getUpdatedAt() + { + return $this->updatedAt; + } + + /** + * Static method which returns a new Parse Object for a given class + * Optionally creates a pointer object if the objectId is provided. + * + * @param string $className Class Name for data on Parse. + * @param string $objectId Unique identifier for existing object. + * @param bool $isPointer If the object is a pointer. + * + * @return Object + */ + public static function create($className, $objectId = null, + $isPointer = false) + { + if (isset(self::$registeredSubclasses[$className])) { + return new self::$registeredSubclasses[$className]( + $className, $objectId, $isPointer + ); + } else { + return new ParseObject($className, $objectId, $isPointer); + } + } + + /** + * Fetch the whole object from the server and update the local object. + * + * @return null + */ + public function fetch() + { + $sessionToken = null; + if (ParseUser::getCurrentUser()) { + $sessionToken = ParseUser::getCurrentUser()->getSessionToken(); + } + $response = ParseClient::_request( + 'GET', + '/1/classes/' . $this->className . '/' . $this->objectId, + $sessionToken + ); + $this->_mergeAfterFetch($response); + } + + /** + * Merges data received from the server. + * + * @param array $result Data retrieved from the server. + * @param bool $completeData Fetch all data or not. + * + * @return null + * @ignore + */ + public function _mergeAfterFetch($result, $completeData = true) + { + // This loop will clear operations for keys provided by the server + // It will not clear operations for new keys the server doesn't have. + foreach ($result as $key => $value) { + if (isset($this->operationSet[$key])) { + unset($this->operationSet[$key]); + } + } + $this->serverData = array(); + $this->dataAvailability = array(); + $this->mergeFromServer($result, $completeData); + $this->rebuildEstimatedData(); + } + + /** + * Merges data received from the server with a given selected keys. + * + * @param array $result Data retrieved from the server. + * @param array $selectedKeys Keys to be fetched. Null or empty means all + * data will be fetched. + * @return null + * @ignore + */ + public function _mergeAfterFetchWithSelectedKeys($result, $selectedKeys) + { + $this->_mergeAfterFetch($result, $selectedKeys ? empty($selectedKeys) : true); + foreach ($selectedKeys as $key) { + $this->dataAvailability[$key] = true; + } + } + + /** + * Merges data received from the server. + * + * @param array $data Data retrieved from server. + * @param bool $completeData Fetch all data or not. + * + * @return null + */ + private function mergeFromServer($data, $completeData = true) + { + $this->hasBeenFetched = ($this->hasBeenFetched || $completeData) ? true : false; + $this->mergeMagicFields($data); + foreach ($data as $key => $value) { + if ($key === '__type' && $value === 'className') { + continue; + } + + $decodedValue = ParseClient::_decode($value); + + if (is_array($decodedValue)) { + if (isset($decodedValue['__type'])) { + if ($decodedValue['__type'] === 'Relation') { + $className = $decodedValue['className']; + $decodedValue = new ParseRelation($this, $key, $className); + } + } + if ($key == 'ACL') { + $decodedValue = ParseACL::_createACLFromJSON($decodedValue); + } + } + $this->serverData[$key] = $decodedValue; + $this->dataAvailability[$key] = true; + + } + if (!$this->updatedAt && $this->createdAt) { + $this->updatedAt = $this->createdAt; + } + } + + /** + * Handle merging of special fields for the object. + * + * @param array &$data Data received from server. + * + * @return null + */ + private function mergeMagicFields(&$data) + { + if (isset($data['objectId'])) { + $this->objectId = $data['objectId']; + unset($data['objectId']); + } + if (isset($data['createdAt'])) { + $this->createdAt = new \DateTime($data['createdAt']); + unset($data['createdAt']); + } + if (isset($data['updatedAt'])) { + $this->updatedAt = new \DateTime($data['updatedAt']); + unset($data['updatedAt']); + } + if (isset($data['ACL'])) { + $acl = ParseACL::_createACLFromJSON($data['ACL']); + $this->serverData['ACL'] = $acl; + unset($data['ACL']); + } + + } + + /** + * Start from serverData and process operations to generate the current + * value set for an object. + * + * @return null + */ + protected function rebuildEstimatedData() + { + $this->estimatedData = array(); + foreach ($this->serverData as $key => $value) { + $this->estimatedData[$key] = $value; + } + $this->applyOperations($this->operationSet, $this->estimatedData); + } + + /** + * Apply operations to a target object + * + * @param array $operations Operations set to apply. + * @param array &$target Target data to affect. + * + * @return null + */ + private function applyOperations($operations, &$target) + { + foreach ($operations as $key => $operation) { + $oldValue = (isset($target[$key]) ? $target[$key] : null); + $newValue = $operation->_apply($oldValue, $this, $key); + if (empty($newValue) && !is_array($newValue) + && $newValue !== null && !is_scalar($newValue) + ) { + unset($target[$key]); + unset($this->dataAvailability[$key]); + } else { + $target[$key] = $newValue; + $this->dataAvailability[$key] = true; + } + } + } + + /** + * Delete the object from Parse. + * + * @param bool $useMasterKey Whether to use the master key. + * + * @return null + */ + public function destroy($useMasterKey = false) + { + if (!$this->objectId) { + return; + } + $sessionToken = null; + if (ParseUser::getCurrentUser()) { + $sessionToken = ParseUser::getCurrentUser()->getSessionToken(); + } + ParseClient::_request( + 'DELETE', '/1/classes/' . $this->className . + '/' . $this->objectId, $sessionToken, null, $useMasterKey + ); + } + + /** + * Delete an array of objects. + * + * @param array $objects Objects to destroy. + * @param boolean $useMasterKey Whether to use the master key or not. + * + * @throws ParseAggregateException + * @return null + */ + public static function destroyAll(array $objects, $useMasterKey = false) + { + $errors = []; + $count = count($objects); + if ($count) { + $batchSize = 40; + $processed = 0; + $currentBatch = []; + $currentcount = 0; + while ($processed < $count) { + $currentcount++; + $currentBatch[] = $objects[$processed++]; + if ($currentcount == $batchSize || $processed == $count) { + $results = static::destroyBatch($currentBatch); + $errors = array_merge($errors, $results); + $currentBatch = []; + $currentcount = 0; + } + } + if (count($errors)) { + throw new ParseAggregateException( + "Errors during batch destroy.", $errors + ); + } + } + return null; + } + + private static function destroyBatch(array $objects, $useMasterKey = false) + { + $data = []; + $errors = []; + foreach ($objects as $object) { + $data[] = array( + "method" => "DELETE", + "path" => "/1/classes/" . $object->getClassName() . + "/" . $object->getObjectId() + ); + } + $sessionToken = null; + if (ParseUser::getCurrentUser()) { + $sessionToken = ParseUser::getCurrentUser()->getSessionToken(); + } + $result = ParseClient::_request( + "POST", "/1/batch", $sessionToken, + json_encode(array("requests" => $data)), + $useMasterKey + ); + foreach ($objects as $key => $object) { + if (isset($result[$key]['error'])) { + $error = $result[$key]['error']['error']; + $code = isset($result[$key]['error']['code']) ? + $result[$key]['error']['code'] : -1; + $errors[] = array( + 'error' => $error, + 'code' => $code + ); + } + } + return $errors; + } + + /** + * Increment a numeric key by a certain value. + * + * @param string $key Key for numeric value on object to increment. + * @param int $value Value to increment by. + * + * @return null + */ + public function increment($key, $value = 1) + { + $this->_performOperation($key, new IncrementOperation($value)); + } + + /** + * Add a value to an array property. + * + * @param string $key Key for array value on object to add a value to. + * @param mixed $value Value to add. + * + * @return null + */ + public function add($key, $value) + { + $this->_performOperation($key, new AddOperation($value)); + } + + /** + * Add unique values to an array property. + * + * @param string $key Key for array value on object. + * @param mixed $value Value list to add uniquely. + * + * @return null + */ + public function addUnique($key, $value) + { + $this->_performOperation($key, new AddUniqueOperation($value)); + } + + /** + * Delete a key from an object. + * + * @param string $key Key to remove from object. + * + * @return null + */ + public function delete($key) + { + $this->_performOperation($key, new DeleteOperation()); + } + + /** + * Return a JSON encoded value of the object. + * + * @return string + * @ignore + */ + public function _encode() + { + $out = array(); + if ($this->objectId) { + $out['objectId'] = $this->objectId; + } + if ($this->createdAt) { + $out['createdAt'] = $this->createdAt; + } + if ($this->updatedAt) { + $out['updatedAt'] = $this->updatedAt; + } + foreach ($this->serverData as $key => $value) { + $out[$key] = $value; + } + foreach ($this->estimatedData as $key => $value) { + if (is_object($value) && $value instanceof ParseObject) { + $out[$key] = $value->_encode(); + } else if (is_array($value)) { + $out[$key] = array(); + foreach ($value as $item) { + if (is_object($item) && $item instanceof ParseObject) { + $out[$key][] = $item->_encode(); + } else { + $out[$key][] = $item; + } + } + } else { + $out[$key] = $value; + } + } + return json_encode($out); + } + + /** + * Returns JSON object of the unsaved operations. + * + * @return array + */ + private function getSaveJSON() + { + return ParseClient::_encode($this->operationSet, true); + } + + /** + * Save Object to Parse + * + * @return null + */ + public function save() + { + if (!$this->isDirty()) { + return; + } + static::deepSave($this); + } + + /** + * Save all the objects in the provided array + * + * @param array $list + * + * @return null + */ + public static function saveAll($list) + { + static::deepSave($list); + } + + /** + * Save Object and unsaved children within. + * + * @param $target + * + * @return null + * + * @throws ParseException + */ + private static function deepSave($target) + { + $unsavedChildren = array(); + $unsavedFiles = array(); + static::findUnsavedChildren($target, $unsavedChildren, $unsavedFiles); + $sessionToken = null; + if (ParseUser::getCurrentUser()) { + $sessionToken = ParseUser::getCurrentUser()->getSessionToken(); + } + + foreach ($unsavedFiles as &$file) { + $file->save(); + } + + $objects = array(); + // Get the set of unique objects among the children. + foreach ($unsavedChildren as &$obj) { + if (!in_array($obj, $objects, true)) { + $objects[] = $obj; + } + } + $remaining = $objects; + + while (count($remaining) > 0) { + + $batch = array(); + $newRemaining = array(); + + foreach ($remaining as $key => &$object) { + if (count($batch) > 40) { + $newRemaining[] = $object; + continue; + } + if ($object->canBeSerialized()) { + $batch[] = $object; + } else { + $newRemaining[] = $object; + } + } + $remaining = $newRemaining; + + if (count($batch) === 0) { + throw new Exception("Tried to save a batch with a cycle."); + } + + $requests = array(); + foreach ($batch as $obj) { + $json = $obj->getSaveJSON(); + $method = 'POST'; + $path = '/1/classes/' . $obj->getClassName(); + if ($obj->getObjectId()) { + $path .= '/' . $obj->getObjectId(); + $method = 'PUT'; + } + $requests[] = array('method' => $method, + 'path' => $path, + 'body' => $json + ); + } + + if (count($requests) === 1) { + $req = $requests[0]; + $result = ParseClient::_request($req['method'], + $req['path'], $sessionToken, json_encode($req['body'])); + $batch[0]->mergeAfterSave($result); + } else { + $result = ParseClient::_request('POST', '/1/batch', $sessionToken, + json_encode(array("requests" => $requests))); + + $errorCollection = array(); + + foreach ($batch as $key => &$obj) { + if (isset($result[$key]['success'])) { + $obj->mergeAfterSave($result[$key]['success']); + } else if (isset($result[$key]['error'])) { + $response = $result[$key]; + $error = $response['error']['error']; + $code = isset($response['error']['code']) ? + $response['error']['code'] : -1; + $errorCollection[] = array( + 'error' => $error, + 'code' => $code, + 'object' => $obj + ); + } else { + $errorCollection[] = array( + 'error' => 'Unknown error in batch save.', + 'code' => -1, + 'object' => $obj + ); + } + } + if (count($errorCollection)) { + throw new ParseAggregateException( + "Errors during batch save.", $errorCollection + ); + } + } + } + } + + /** + * Find unsaved children inside an object. + * + * @param ParseObject $object Object to search. + * @param array &$unsavedChildren Array to populate with children. + * @param array &$unsavedFiles Array to populate with files. + */ + private static function findUnsavedChildren($object, + &$unsavedChildren, &$unsavedFiles) + { + static::traverse(true, $object, function ($obj) use ( + &$unsavedChildren, + &$unsavedFiles + ) { + if ($obj instanceof ParseObject) { + if ($obj->_isDirty(false)) { + $unsavedChildren[] = $obj; + } + } else if ($obj instanceof ParseFile) { + if (!$obj->getURL()) { + $unsavedFiles[] = $obj; + } + } + + }); + } + + /** + * Traverse object to find children. + * + * @param boolean $deep Should this call traverse deeply + * @param ParseObject|array &$object Object to traverse. + * @param callable $mapFunction Function to call for every item. + * @param array $seen Objects already seen. + * + * @return mixed The result of calling mapFunction on the root object. + */ + private static function traverse($deep, &$object, $mapFunction, + $seen = array()) + { + if ($object instanceof ParseObject) { + if (in_array($object, $seen, true)) { + return null; + } + $seen[] = $object; + if ($deep) { + self::traverse( + $deep, $object->estimatedData, $mapFunction, $seen + ); + } + return $mapFunction($object); + } + if ($object instanceof ParseRelation || $object instanceof ParseFile) { + return $mapFunction($object); + } + if (is_array($object)) { + foreach ($object as $key => $value) { + self::traverse($deep, $value, $mapFunction, $seen); + } + return $mapFunction($object); + } + return $mapFunction($object); + } + + /** + * Determine if the current object can be serialized for saving. + * + * @return bool + */ + private function canBeSerialized() + { + return self::canBeSerializedAsValue($this->estimatedData); + } + + /** + * Checks the given object and any children to see if the whole object + * can be serialized for saving. + * + * @param mixed $object The value to check. + * + * @return bool + */ + private static function canBeSerializedAsValue($object) + { + $result = true; + self::traverse(false, $object, function ($obj) use (&$result) { + // short circuit as soon as possible. + if ($result === false) { + return; + } + // cannot make a pointer to an unsaved object. + if ($obj instanceof ParseObject) { + if (!$obj->getObjectId()) { + $result = false; + return; + } + } + }); + return $result; + } + + /** + * Merge server data after a save completes. + * + * @param array $result Data retrieved from server. + * + * @return null + */ + private function mergeAfterSave($result) + { + $this->applyOperations($this->operationSet, $this->serverData); + $this->mergeFromServer($result); + $this->operationSet = array(); + $this->rebuildEstimatedData(); + } + + /** + * Access or create a Relation value for a key. + * + * @param string $key The key to access the relation for. + * @return ParseRelation The ParseRelation object if the relation already + * exists for the key or can be created for this key. + */ + public function getRelation($key) + { + $relation = new ParseRelation($this, $key); + if (isset($this->estimatedData[$key])) { + $object = $this->estimatedData[$key]; + if ($object instanceof ParseRelation) { + $relation->setTargetClass($object->getTargetClass()); + } + } + return $relation; + } + + /** + * Gets a Pointer referencing this Object. + * + * @return array + * + * @throws \Exception + * @ignore + */ + public function _toPointer() + { + if (!$this->objectId) { + throw new \Exception("Can't serialize an unsaved Parse.Object"); + } + return array( + '__type' => "Pointer", + 'className' => $this->className, + 'objectId' => $this->objectId); + } + + /** + * Set ACL for this object. + * + * @param ParseACL $acl + */ + public function setACL($acl) + { + $this->_performOperation('ACL', new SetOperation($acl)); + } + + /** + * Get ACL assigned to the object. + * + * @return ParseACL + */ + public function getACL() + { + return $this->getACLWithCopy(true); + } + + private function getACLWithCopy($mayCopy) + { + if (!isset($this->estimatedData['ACL'])) { + return null; + } + $acl = $this->estimatedData['ACL']; + if ($mayCopy && $acl->_isShared()) { + return clone $acl; + } + return $acl; + } + + /** + * Register a subclass. Should be called before any other Parse functions. + * Cannot be called on the base class ParseObject. + * @throws \Exception + */ + public static function registerSubclass() + { + if (isset(static::$parseClassName)) { + if (!in_array(static::$parseClassName, self::$registeredSubclasses)) { + self::$registeredSubclasses[static::$parseClassName] = + get_called_class(); + } + } else { + throw new \Exception( + "Cannot register a subclass that does not have a parseClassName" + ); + } + } + + /** + * Un-register a subclass. + * Cannot be called on the base class ParseObject. + * @ignore + */ + public static function _unregisterSubclass() + { + $subclass = static::getSubclass(); + unset(self::$registeredSubclasses[$subclass]); + } + + /** + * Creates a ParseQuery for the subclass of ParseObject. + * Cannot be called on the base class ParseObject. + * + * @return ParseQuery + * + * @throws \Exception + */ + public static function query() + { + $subclass = static::getSubclass(); + if ($subclass === false) { + throw new Exception( + 'Cannot create a query for an unregistered subclass.' + ); + } else { + return new ParseQuery($subclass); + } + } + +} diff --git a/src/Parse/ParsePush.php b/src/Parse/ParsePush.php new file mode 100644 index 00000000..c8cdc5ce --- /dev/null +++ b/src/Parse/ParsePush.php @@ -0,0 +1,68 @@ + + */ +class ParsePush +{ + + /** + * Sends a push notification. + * + * @param array $data The data of the push notification. Valid fields + * are: + * channels - An Array of channels to push to. + * push_time - A Date object for when to send the push. + * expiration_time - A Date object for when to expire + * the push. + * expiration_interval - The seconds from now to expire the push. + * where - A ParseQuery over ParseInstallation that is used to match + * a set of installations to push to. + * data - The data to send as part of the push + * @param boolean $useMasterKey Whether to use the Master Key for the request + * + * @throws \Exception, ParseException + * @return mixed + */ + public static function send($data, $useMasterKey = false) + { + if (isset($data['expiration_time']) + && isset($data['expiration_interval'])) { + throw new \Exception( + 'Both expiration_time and expiration_interval can\'t be set.' + ); + } + if (isset($data['where'])) { + if ($data['where'] instanceof ParseQuery) { + $data['where'] = $data['where']->_getOptions(); + } else { + throw new \Exception( + 'Where parameter for Parse Push must be of type ParseQuery' + ); + } + } + if (isset($data['push_time'])) { + $data['push_time'] = ParseClient::_encode( + $data['push_time'], false + )['iso']; + } + if (isset($data['expiration_time'])) { + $data['expiration_time'] = ParseClient::_encode( + $data['expiration_time'], false + )['iso']; + } + return ParseClient::_request( + 'POST', + '/1/push', + null, + json_encode($data), + $useMasterKey + ); + } + +} \ No newline at end of file diff --git a/src/Parse/ParseQuery.php b/src/Parse/ParseQuery.php new file mode 100755 index 00000000..cf47d445 --- /dev/null +++ b/src/Parse/ParseQuery.php @@ -0,0 +1,775 @@ + + */ +class ParseQuery +{ + + /** + * @var - Class Name for data stored on Parse. + */ + private $className; + /** + * @var array - Where constraints. + */ + private $where = array(); + /** + * @var array - Order By keys. + */ + private $orderBy = array(); + /** + * @var array - Include nested objects. + */ + private $includes = array(); + /** + * @var array - Include certain keys only. + */ + private $selectedKeys = array(); + /** + * @var int - Skip from the beginning of the search results. + */ + private $skip = 0; + /** + * @var - Determines if the query is a count query or a results query. + */ + private $count; + /** + * @var int - Limit of results, defaults to 100 when not explicitly set. + */ + private $limit = -1; + + /** + * Create a Parse Query for a given Parse Class. + * + * @param mixed $className Class Name of data on Parse. + */ + public function __construct($className) + { + $this->className = $className; + } + + /** + * Execute a query to retrieve a specific object + * + * @param string $objectId Unique object id to retrieve. + * @param bool $useMasterKey If the query should use the master key + * + * @return array + * + * @throws ParseException + */ + public function get($objectId, $useMasterKey = false) + { + $this->equalTo('objectId', $objectId); + $result = $this->first($useMasterKey); + if (empty($result)) { + throw new ParseException("Object not found.", 101); + } + return $result; + } + + /** + * Set a constraint for a field matching a given value. + * + * @param string $key Key to set up an equals constraint. + * @param mixed $value Value the key must equal. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function equalTo($key, $value) + { + if ($value === null) { + $this->doesNotExist($key); + } else { + $this->where[$key] = $value; + } + return $this; + } + + /** + * Helper for condition queries. + */ + private function addCondition($key, $condition, $value) + { + if (!isset($this->where[$key])) { + $this->where[$key] = array(); + } + $this->where[$key][$condition] = ParseClient::_encode($value, true); + } + + /** + * Add a constraint to the query that requires a particular key's value to + * be not equal to the provided value. + * + * @param string $key The key to check. + * @param mixed $value The value that must not be equalled. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function notEqualTo($key, $value) + { + $this->addCondition($key, '$ne', $value); + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * be less than the provided value. + * + * @param string $key The key to check. + * @param mixed $value The value that provides an Upper bound. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function lessThan($key, $value) + { + $this->addCondition($key, '$lt', $value); + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * be greater than the provided value. + * + * @param string $key The key to check. + * @param mixed $value The value that provides an Lower bound. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function greaterThan($key, $value) + { + $this->addCondition($key, '$gt', $value); + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * be greater than or equal to the provided value. + * + * @param string $key The key to check. + * @param mixed $value The value that provides an Lower bound. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function greaterThanOrEqualTo($key, $value) + { + $this->addCondition($key, '$gte', $value); + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * be less than or equal to the provided value. + * + * @param string $key The key to check. + * @param mixed $value The value that provides an Upper bound. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function lessThanOrEqualTo($key, $value) + { + $this->addCondition($key, '$lte', $value); + return $this; + } + + /** + * Converts a string into a regex that matches it. + * Surrounding with \Q .. \E does this, we just need to escape \E's in + * the text separately. + */ + private function quote($s) { + return "\\Q" . str_replace("\\E", "\\E\\\\E\\Q", $s) . "\\E"; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * start with the provided value. + * + * @param string $key The key to check. + * @param mixed $value The substring that the value must start with. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function startsWith($key, $value) + { + $this->addCondition($key, '$regex', "^".$this->quote($value)); + return $this; + } + + /** + * Returns an associative array of the query constraints. + * + * @return array + * @ignore + */ + public function _getOptions() + { + $opts = array(); + if (!empty($this->where)) { + $opts['where'] = $this->where; + } + if (count($this->includes)) { + $opts['include'] = join(',', $this->includes); + } + if (count($this->selectedKeys)) { + $opts['keys'] = join(',', $this->selectedKeys); + } + if ($this->limit >= 0) { + $opts['limit'] = $this->limit; + } + if ($this->skip > 0) { + $opts['skip'] = $this->skip; + } + if ($this->orderBy) { + $opts['order'] = join(',', $this->orderBy); + } + if ($this->count) { + $opts['count'] = $this->count; + } + return $opts; + } + + /** + * Execute a query to get only the first result. + * + * @param bool $useMasterKey If the query should use the master key + * + * @return array + */ + public function first($useMasterKey = false) + { + $this->limit = 1; + $result = $this->find($useMasterKey); + if (count($result)) { + return $result[0]; + } else { + return array(); + } + } + + /** + * Build query string from query constraints. + * @param array $queryOptions Associative array of the query constraints. + * + * @return string Query string. + */ + private function buildQueryString($queryOptions) + { + if (isset($queryOptions["where"])) { + $queryOptions["where"] = ParseClient::_encode($queryOptions["where"], true); + $queryOptions["where"] = json_encode($queryOptions["where"]); + } + return http_build_query($queryOptions); + } + + /** + * Execute a count query and return the count. + * + * @param bool $useMasterKey If the query should use the master key + * + * @return int + */ + public function count($useMasterKey = false) + { + $this->limit = 0; + $this->count = 1; + $queryString = $this->buildQueryString($this->_getOptions()); + $result = ParseClient::_request('GET', + '/1/classes/' . $this->className . + '?' . $queryString, null, null, $useMasterKey); + return $result['count']; + } + + /** + * Execute a find query and return the results. + * + * @param boolean $useMasterKey + * + * @return array + */ + public function find($useMasterKey = false) + { + $sessionToken = null; + if (ParseUser::getCurrentUser()) { + $sessionToken = ParseUser::getCurrentUser()->getSessionToken(); + } + $queryString = $this->buildQueryString($this->_getOptions()); + $result = ParseClient::_request('GET', + '/1/classes/' . $this->className . + '?' . $queryString, $sessionToken, null, $useMasterKey); + $output = array(); + foreach ($result['results'] as $row) { + $obj = ParseObject::create($this->className, $row['objectId']); + $obj->_mergeAfterFetchWithSelectedKeys($row, $this->selectedKeys); + $output[] = $obj; + } + return $output; + } + + /** + * Set the skip parameter as a query constraint. + * + * @param int $n Number of objects to skip from start of results. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function skip($n) + { + $this->skip = $n; + return $this; + } + + /** + * Set the limit parameter as a query constraint + * + * @param int $n Number of objects to return from the query. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function limit($n) + { + $this->limit = $n; + return $this; + } + + /** + * Set the query orderBy to ascending for the given key(s). It overwrites the + * existing order criteria. + * @param mixed $key Key(s) to sort by, which is a string or an array of strings. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function ascending($key) + { + $this->orderBy = array(); + return $this->addAscending($key); + } + + /** + * Set the query orderBy to ascending for the given key(s). It can also add + * secondary sort descriptors without overwriting the existing order. + * + * @param mixed $key Key(s) to sort by, which is a string or an array of strings. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function addAscending($key) + { + if (is_array($key)) { + $this->orderBy = array_merge($this->orderBy, $key); + } else { + $this->orderBy[] = $key; + } + return $this; + } + + /** + * Set the query orderBy to descending for a given key(s). It overwrites the + * existing order criteria. + * @param mixed $key Key(s) to sort by, which is a string or an array of strings. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function descending($key) + { + $this->orderBy = array(); + return $this->addDescending($key); + } + + /** + * Set the query orderBy to descending for a given key(s). It can also add + * secondary sort descriptors without overwriting the existing order. + * + * @param mixed $key Key(s) to sort by, which is a string or an array of strings. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function addDescending($key) + { + if (is_array($key)) { + $key = array_map(function ($element) { + return '-' . $element; + }, $key); + $this->orderBy = array_merge($this->orderBy, $key); + } else { + $this->orderBy[] = "-" . $key; + } + return $this; + } + + /** + * Add a proximity based constraint for finding objects with key point + * values near the point given. + * + * @param string $key The key that the ParseGeoPoint is stored in. + * @param ParseGeoPoint $point The reference ParseGeoPoint that is used. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function near($key, $point) + { + $this->addCondition($key, '$nearSphere', $point); + return $this; + } + + /** + * Add a proximity based constraint for finding objects with key point + * values near the point given and within the maximum distance given. + * + * @param string $key The key of the ParseGeoPoint + * @param ParseGeoPoint $point The ParseGeoPoint that is used. + * @param int $maxDistance Maximum distance (in radians) + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function withinRadians($key, $point, $maxDistance) + { + $this->near($key, $point); + $this->addCondition($key, '$maxDistance', $maxDistance); + return $this; + } + + /** + * Add a proximity based constraint for finding objects with key point + * values near the point given and within the maximum distance given. + * Radius of earth used is 3958.5 miles. + * + * @param string $key The key of the ParseGeoPoint + * @param ParseGeoPoint $point The ParseGeoPoint that is used. + * @param int $maxDistance Maximum distance (in miles) + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function withinMiles($key, $point, $maxDistance) + { + $this->near($key, $point); + $this->addCondition($key, '$maxDistance', $maxDistance / 3958.8); + return $this; + } + + /** + * Add a proximity based constraint for finding objects with key point + * values near the point given and within the maximum distance given. + * Radius of earth used is 6371.0 kilometers. + * + * @param string $key The key of the ParseGeoPoint + * @param ParseGeoPoint $point The ParseGeoPoint that is used. + * @param int $maxDistance Maximum distance (in kilometers) + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function withinKilometers($key, $point, $maxDistance) + { + $this->near($key, $point); + $this->addCondition($key, '$maxDistance', $maxDistance / 6371.0); + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's + * coordinates be contained within a given rectangular geographic bounding + * box. + * + * @param string $key The key of the ParseGeoPoint + * @param ParseGeoPoint $southwest The lower-left corner of the box. + * @param ParseGeoPoint $northeast The upper-right corner of the box. + * + * @return ParseQuery Returns this query, so you can chain this call. + */ + public function withinGeoBox($key, $southwest, $northeast) + { + $this->addCondition($key, '$within', + ['$box' => [$southwest, $northeast]]); + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * be contained in the provided list of values. + * + * @param string $key The key to check. + * @param array $values The values that will match. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function containedIn($key, $values) + { + $this->addCondition($key, '$in', $values); + return $this; + } + + /** + * Iterates over each result of a query, calling a callback for each one. The + * items are processed in an unspecified order. The query may not have any + * sort order, and may not use limit or skip. + * + * @param callable $callback Callback that will be called with each result + * of the query. + * @param boolean $useMasterKey + * @param int $batchSize + * + * @throws \Exception If query has sort, skip, or limit. + */ + public function each($callback, $useMasterKey = false, $batchSize = 100) + { + if ($this->orderBy || $this->skip || ($this->limit >= 0)) { + throw new \Exception( + "Cannot iterate on a query with sort, skip, or limit."); + } + $query = new ParseQuery($this->className); + $query->where = $this->where; + $query->includes = $this->includes; + $query->limit = $batchSize; + $query->ascending("objectId"); + + $finished = false; + while (!$finished) { + $results = $query->find($useMasterKey); + $length = count($results); + for ($i = 0; $i < $length; $i++) { + $callback($results[$i]); + } + if ($length == $query->limit) { + $query->greaterThan("objectId", $results[$length - 1]->getObjectId()); + } else { + $finished = true; + } + } + } + + /** + * Add a constraint to the query that requires a particular key's value to + * not be contained in the provided list of values. + * + * @param string $key The key to check. + * @param array $values The values that will not match. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function notContainedIn($key, $values) + { + $this->addCondition($key, '$nin', $values); + return $this; + } + + /** + * Add a constraint that requires that a key's value matches a ParseQuery + * constraint. + * + * @param string $key The key that the contains the object to match + * the query. + * @param ParseQuery $query The query that should match. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function matchesQuery($key, $query) + { + $queryParam = $query->_getOptions(); + $queryParam["className"] = $query->className; + $this->addCondition($key, '$inQuery', $queryParam); + return $this; + } + + /** + * Add a constraint that requires that a key's value not matches a ParseQuery + * constraint. + * + * @param string $key The key that the contains the object not to + * match the query. + * @param ParseQuery $query The query that should not match. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function doesNotMatchQuery($key, $query) + { + $queryParam = $query->_getOptions(); + $queryParam["className"] = $query->className; + $this->addCondition($key, '$notInQuery', $queryParam); + return $this; + } + + /** + * Add a constraint that requires that a key's value matches a value in an + * object returned by the given query. + * + * @param string $key The key that contains teh value that is being + * matched. + * @param string $queryKey The key in objects returned by the query to + * match against. + * @param ParseQuery $query The query to run. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function matchesKeyInQuery($key, $queryKey, $query) + { + $queryParam = $query->_getOptions(); + $queryParam["className"] = $query->className; + $this->addCondition($key, '$select', + ['key' => $queryKey, 'query' => $queryParam]); + return $this; + } + + /** + * Add a constraint that requires that a key's value not match a value in an + * object returned by the given query. + * + * @param string $key The key that contains teh value that is being + * excluded. + * @param string $queryKey The key in objects returned by the query to + * match against. + * @param ParseQuery $query The query to run. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function doesNotMatchKeyInQuery($key, $queryKey, $query) + { + $queryParam = $query->_getOptions(); + $queryParam["className"] = $query->className; + $this->addCondition($key, '$dontSelect', + ['key' => $queryKey, 'query' => $queryParam]); + return $this; + } + + /** + * Constructs a ParseQuery object that is the OR of the passed in queries objects. + * All queries must have same class name. + * + * @param array $queryObjects Array of ParseQuery objects to OR. + * + * @return ParseQuery The query that is the OR of the passed in queries. + * + * @throws \Exception If all queries don't have same class. + */ + public static function orQueries($queryObjects) + { + $className = null; + $length = count($queryObjects); + for ($i = 0; $i < $length; $i++) { + if (is_null($className)) { + $className = $queryObjects[$i]->className; + } + if ($className != $queryObjects[$i]->className) { + throw new \Exception("All queries must be for the same class"); + } + } + $query = new ParseQuery($className); + $query->_or($queryObjects); + return $query; + } + + /** + * Add constraint that at least one of the passed in queries matches. + * + * @param array $queries The list of queries to OR. + * + * @return ParseQuery Returns the query, so you can chain this call. + * @ignore + */ + private function _or($queries) + { + $this->where['$or'] = array(); + $length = count($queries); + for ($i = 0; $i < $length; $i++) { + $this->where['$or'][] = $queries[$i]->where; + } + return $this; + } + + /** + * Add a constraint to the query that requires a particular key's value to + * contain each one of the provided list of values. + * + * @param string $key The key to check. This key's value must be an array. + * @param array $values The values that will match. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function containsAll($key, $values) + { + $this->addCondition($key, '$all', $values); + return $this; + } + + /** + * Add a constraint for finding objects that contain the given key. + * + * @param string $key The key that should exist. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function exists($key) + { + $this->addCondition($key, '$exists', true); + return $this; + } + + /** + * Add a constraint for finding objects that not contain the given key. + * + * @param string $key The key that should not exist. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function doesNotExist($key) + { + $this->addCondition($key, '$exists', false); + return $this; + } + + /** + * Restrict the fields of the returned Parse Objects to include only the + * provided keys. If this is called multiple times, then all of the keys + * specified in each of the calls will be included. + * + * @param mixed $key The name(s) of the key(s) to include. It could be + * string, or an Array of string. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function select($key) + { + if (is_array($key)) { + $this->selectedKeys = array_merge($this->selectedKeys, $key); + } else { + $this->selectedKeys[] = $key; + } + return $this; + } + + /** + * Include nested Parse Objects for the provided key. You can use dot + * notation to specify which fields in the included object are also fetch. + * + * @param mixed $key The name(s) of the key(s) to include. It could be + * string, or an Array of string. + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function includeKey($key) + { + if (is_array($key)) { + $this->includes = array_merge($this->includes, $key); + } else { + $this->includes[] = $key; + } + } + + /** + * Add constraint for parse relation. + * @param string $key + * @param mixed $value + * + * @return ParseQuery Returns the query, so you can chain this call. + */ + public function relatedTo($key, $value) + { + $this->addCondition('$relatedTo', $key, $value); + return $this; + } +} diff --git a/src/Parse/ParseRelation.php b/src/Parse/ParseRelation.php new file mode 100644 index 00000000..e3a00210 --- /dev/null +++ b/src/Parse/ParseRelation.php @@ -0,0 +1,138 @@ + + */ + +class ParseRelation { + + /** + * @var ParseObject - The parent of this relation. + */ + private $parent; + /** + * @var string - The key of the relation in the parent object. + */ + private $key; + /** + * @var string - The className of the target objects. + */ + private $targetClassName; + + /** + * Creates a new Relation for the given parent object, key and class name of target objects. + * + * @param ParseObject $parent The parent of this relation. + * @param string $key The key of the relation in the parent object. + * @param string $targetClassName The className of the target objects. + */ + public function __construct($parent, $key, $targetClassName = null) + { + $this->parent = $parent; + $this->key = $key; + $this->targetClassName = $targetClassName; + } + + /** + * Makes sure that this relation has the right parent and key. + * + * @param $parent + * @param $key + * + * @throws \Exception + */ + private function ensureParentAndKey($parent, $key) + { + if (!$this->parent) { + $this->parent = $parent; + } + if (!$this->key) { + $this->key = $key; + } + if ($this->parent != $parent) { + throw new \Exception('Internal Error. Relation retrieved from two different Objects.'); + } + if ($this->key != $key) { + throw new \Exception('Internal Error. Relation retrieved from two different keys.'); + } + } + + /** + * Adds a ParseObject or an array of ParseObjects to the relation. + * + * @param mixed $objects The item or items to add. + */ + public function add($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + $operation = new ParseRelationOperation($objects, null); + $this->targetClassName = $operation->_getTargetClass(); + $this->parent->_performOperation($this->key, $operation); + } + + /** + * Removes a ParseObject or an array of ParseObjects from this relation. + * + * @param mixed $objects The item or items to remove. + */ + public function remove($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + $operation = new ParseRelationOperation(null, $objects); + $this->targetClassName = $operation->_getTargetClass(); + $this->parent->_performOperation($this->key, $operation); + } + + /** + * Returns the target classname for the relation. + * + * @return string + */ + public function getTargetClass() + { + return $this->targetClassName; + } + + /** + * Set the target classname for the relation. + * + * @param $className + */ + public function setTargetClass($className) + { + $this->targetClassName = $className; + } + + /** + * Set the parent object for the relation. + * + * @param $parent + */ + public function setParent($parent) { + $this->parent = $parent; + } + + /** + * Gets a query that can be used to query the objects in this relation. + * + * @return ParseQuery That restricts the results to objects in this relations. + */ + public function getQuery() + { + $query = new ParseQuery($this->targetClassName); + $query->relatedTo('object', $this->parent->_toPointer()); + $query->relatedTo('key', $this->key); + return $query; + } +} diff --git a/src/Parse/ParseRole.php b/src/Parse/ParseRole.php new file mode 100644 index 00000000..f2065dc6 --- /dev/null +++ b/src/Parse/ParseRole.php @@ -0,0 +1,107 @@ + + */ +class ParseRole extends ParseObject +{ + + public static $parseClassName = "_Role"; + + /** + * Create a ParseRole object with a given name and ACL. + * + * @param string $name + * @param ParseACL $acl + * + * @return ParseRole + */ + public static function createRole($name, ParseACL $acl) + { + $role = ParseObject::create(static::$parseClassName); + $role->setName($name); + $role->setACL($acl); + return $role; + } + + /** + * Returns the role name. + * + * @return string + */ + public function getName() + { + return $this->get("name"); + } + + /** + * Sets the role name. + * + * @param string $name The role name + * + * @return null + */ + public function setName($name) + { + if ($this->getObjectId()) { + throw new ParseException( + "A role's name can only be set before it has been saved." + ); + } + if (!is_string($name)) { + throw new ParseException( + "A role's name must be a string." + ); + } + return $this->set("name", $name); + } + + /** + * Gets the ParseRelation for the ParseUsers which are direct children of + * this role. These users are granted any privileges that this role + * has been granted. + * + * @return ParseRelation + */ + public function getUsers() + { + return $this->getRelation("users"); + } + + /** + * Gets the ParseRelation for the ParseRoles which are direct children of + * this role. These roles' users are granted any privileges that this role + * has been granted. + * + * @return ParseRelation + */ + public function getRoles() + { + return $this->getRelation("roles"); + } + + public function save() + { + if (!$this->getACL()) { + throw new ParseException( + "Roles must have an ACL." + ); + } + if (!$this->getName() || !is_string($this->getName())) { + throw new ParseException( + "Roles must have a name." + ); + } + return parent::save(); + } + + + +} \ No newline at end of file diff --git a/src/Parse/ParseSessionStorage.php b/src/Parse/ParseSessionStorage.php new file mode 100644 index 00000000..2d38ee11 --- /dev/null +++ b/src/Parse/ParseSessionStorage.php @@ -0,0 +1,70 @@ + + */ +class ParseSessionStorage implements ParseStorageInterface +{ + + /** + * @var string Parse will store its values in a specific key. + */ + private $storageKey = 'parseData'; + + public function __construct() + { + if (session_status() !== PHP_SESSION_ACTIVE) { + throw new ParseException( + 'PHP session_start() must be called first.' + ); + } + if (!isset($_SESSION[$this->storageKey])) { + $_SESSION[$this->storageKey] = array(); + } + } + + public function set($key, $value) + { + $_SESSION[$this->storageKey][$key] = $value; + } + + public function remove($key) + { + unset($_SESSION[$this->storageKey][$key]); + } + + public function get($key) + { + if (isset($_SESSION[$this->storageKey][$key])) { + return $_SESSION[$this->storageKey][$key]; + } + return null; + } + + public function clear() + { + $_SESSION[$this->storageKey] = array(); + } + + public function save() + { + // No action required. PHP handles persistence for $_SESSION. + return; + } + + public function getKeys() + { + return array_keys($_SESSION[$this->storageKey]); + } + + public function getAll() + { + return $_SESSION[$this->storageKey]; + } + +} \ No newline at end of file diff --git a/src/Parse/ParseStorageInterface.php b/src/Parse/ParseStorageInterface.php new file mode 100644 index 00000000..c3700048 --- /dev/null +++ b/src/Parse/ParseStorageInterface.php @@ -0,0 +1,72 @@ + + */ +interface ParseStorageInterface +{ + + /** + * Sets a key-value pair in storage. + * + * @param string $key The key to set + * @param mixed $value The value to set + * + * @return null + */ + public function set($key, $value); + + /** + * Remove a key from storage. + * + * @param string $key The key to remove. + * + * @return null + */ + public function remove($key); + + /** + * Gets the value for a key from storage. + * + * @param string $key The key to get the value for + * + * @return mixed + */ + public function get($key); + + /** + * Clear all the values in storage. + * + * @return null + */ + public function clear(); + + /** + * Save the data, if necessary. This would be a no-op when using the + * $_SESSION implementation, but could be used for saving to file or + * database as an action instead of on every set. + * + * @return null + */ + public function save(); + + /** + * Get all keys in storage. + * + * @return array + */ + public function getKeys(); + + /** + * Get all key-value pairs from storage. + * + * @return array + */ + public function getAll(); + +} \ No newline at end of file diff --git a/src/Parse/ParseUser.php b/src/Parse/ParseUser.php new file mode 100644 index 00000000..37db05a8 --- /dev/null +++ b/src/Parse/ParseUser.php @@ -0,0 +1,309 @@ + + */ +class ParseUser extends ParseObject +{ + + public static $parseClassName = "_User"; + + /** + * @var ParseUser The currently logged-in user. + */ + private static $currentUser = null; + + /** + * @var string The sessionToken for an authenticated user. + */ + protected $_sessionToken = null; + + /** + * Returns the username. + * + * @return string|null + */ + public function getUsername() + { + return $this->get("username"); + } + + /** + * Sets the username for the ParseUser. + * + * @param string $username The username + * + * @return null + */ + public function setUsername($username) + { + return $this->set("username", $username); + } + + /** + * Sets the password for the ParseUser. + * + * @param string $password The password + * + * @return null + */ + public function setPassword($password) + { + return $this->set("password", $password); + } + + /** + * Returns the email address, if set, for the ParseUser. + * + * @return string|null + */ + public function getEmail() + { + return $this->get("email"); + } + + /** + * Sets the email address for the ParseUser. + * + * @param string $email The email address + * + * @return null + */ + public function setEmail($email) + { + return $this->set("email", $email); + } + + /** + * Checks whether this user has been authenticated. + * + * @return boolean + */ + public function isAuthenticated() + { + return $this->_sessionToken !== null; + } + + /** + * Signs up the current user, or throw if invalid. + * This will create a new ParseUser on the server, and also persist the + * session so that you can access the user using ParseUser::getCurrentUser(); + */ + public function signUp() + { + if (!$this->get('username')) { + throw new ParseException("Cannot sign up user with an empty name"); + } + if (!$this->get('password')) { + throw new ParseException( + "Cannot sign up user with an empty password." + ); + } + if ($this->getObjectId()) { + throw new ParseException( + "Cannot sign up an already existing user." + ); + } + parent::save(); + $this->handleSaveResult(true); + } + + /** + * Logs in a and returns a valid ParseUser, or throws if invalid. + * + * @param string $username + * @param string $password + * + * @return ParseUser + * + * @throws ParseException + */ + public static function logIn($username, $password) + { + if (!$username) { + throw new ParseException("Cannot log in user with an empty name"); + } + if (!$password) { + throw new ParseException( + "Cannot log in user with an empty password." + ); + } + $data = array("username" => $username, "password" => $password); + $result = ParseClient::_request("GET", "/1/login", "", $data); + $user = new ParseUser(); + $user->_mergeAfterFetch($result); + $user->handleSaveResult(true); + ParseClient::getStorage()->set("user", $user); + return $user; + } + + /** + * Logs in a user with a session token. Calls the /users/me route and if + * valid, creates and returns the current user. + * + * @param string $sessionToken + * + * @return ParseUser + */ + public static function become($sessionToken) + { + $result = ParseClient::_request('GET', '/1/users/me', $sessionToken); + $user = new ParseUser(); + $user->_mergeAfterFetch($result); + $user->handleSaveResult(true); + ParseClient::getStorage()->set("user", $user); + return $user; + } + + /** + * Log out the current user. This will clear the storage and future calls + * to current will return null + * + * @return null + */ + public static function logOut() + { + if (ParseUser::getCurrentUser()) { + static::$currentUser = null; + } + ParseClient::getStorage()->remove('user'); + } + + /** + * After a save, perform User object specific logic. + * + * @param boolean $makeCurrent Whether to set the current user. + * + * @return null + */ + private function handleSaveResult($makeCurrent = false) + { + if (isset($this->serverData['password'])) { + unset($this->serverData['password']); + } + if (isset($this->serverData['sessionToken'])) { + $this->_sessionToken = $this->serverData['sessionToken']; + unset($this->serverData['sessionToken']); + } + if ($makeCurrent) { + static::$currentUser = $this; + static::saveCurrentUser(); + } + $this->rebuildEstimatedData(); + } + + /** + * Retrieves the currently logged in ParseUser with a valid session, + * either from memory or the storage provider, if necessary. + * + * @return ParseUser|null + */ + public static function getCurrentUser() + { + if (static::$currentUser instanceof ParseUser) { + return static::$currentUser; + } + $storage = ParseClient::getStorage(); + $userData = $storage->get("user"); + if ($userData instanceof ParseUser) { + static::$currentUser = $userData; + return $userData; + } + if (isset($userData["id"]) && isset($userData["_sessionToken"])) { + $user = ParseUser::create("_User", $userData["id"]); + unset($userData["id"]); + $user->_sessionToken = $userData["_sessionToken"]; + unset($userData["_sessionToken"]); + foreach ($userData as $key => $value) { + $user->set($key, $value); + } + $user->_opSetQueue = array(); + static::$currentUser = $user; + return $user; + } + return null; + } + + /** + * Persists the current user to the storage provider. + * + * @return null + */ + protected static function saveCurrentUser() + { + $storage = ParseClient::getStorage(); + $storage->set('user', ParseUser::getCurrentUser()); + } + + /** + * Returns the session token, if available + * + * @return string|null + */ + public function getSessionToken() + { + return $this->_sessionToken; + } + + /** + * Returns true if this user is the current user. + * + * @return boolean + */ + public function isCurrent() + { + if (ParseUser::getCurrentUser() && $this->getObjectId()) { + if ($this->getObjectId() == ParseUser::getCurrentUser()->getObjectId()) { + return true; + } + } + return false; + } + + /** + * Save the current user object, unless it is not signed up. + * + * @return null + * + * @throws ParseException + */ + public function save() + { + if ($this->getObjectId()) { + parent::save(); + } else { + throw new ParseException( + "You must call signUp to create a new User." + ); + } + } + + /** + * Requests a password reset email to be sent to the specified email + * address associated with the user account. This email allows the user + * to securely reset their password on the Parse site. + * + * @param string $email + * + * @return null + */ + public static function requestPasswordReset($email) + { + $json = json_encode(array('email' => $email)); + ParseClient::_request('POST', '/1/requestPasswordReset', null, $json); + } + + /** + * @ignore + */ + public static function _clearCurrentUserVariable() + { + static::$currentUser = null; + } + +} \ No newline at end of file diff --git a/tests/IncrementTest.php b/tests/IncrementTest.php new file mode 100644 index 00000000..2e35f8db --- /dev/null +++ b/tests/IncrementTest.php @@ -0,0 +1,235 @@ +set('yo', 1); + $obj->increment('yo'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals($result->get('yo'), 2, 'Increment did not work'); + } + + public function testIncrement() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $obj->increment('yo', 1); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals($result->get('yo'), 2, 'Increment did not work'); + } + + public function testIncrementByValue() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $obj->increment('yo', 5); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals($result->get('yo'), 6, 'Increment did not work'); + } + + public function testIncrementNegative() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $obj->increment('yo', -1); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals($result->get('yo'), 0, 'Increment did not work'); + } + + public function testIncrementFloat() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $obj->increment('yo', 1.5); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals($result->get('yo'), 2.5, 'Increment did not work'); + } + + public function testIncrementAtomic() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $objAgainOne = $query->first(); + $queryAgain = new ParseQuery('TestObject'); + $queryAgain->equalTo('objectId', $objAgainOne->getObjectId()); + $objAgainTwo = $queryAgain->first(); + $objAgainOne->increment('yo'); + $objAgainTwo->increment('yo'); + $objAgainOne->save(); + $objAgainOne->increment('yo'); + $objAgainOne->save(); + $objAgainTwo->save(); + $queryAgainTwo = new ParseQuery('TestObject'); + $queryAgainTwo->equalTo('objectId', $objAgainTwo->getObjectId()); + $objAgainThree = $query->first(); + $this->assertEquals($objAgainThree->get('yo'), 4); + } + + public function testIncrementGetsValueBack() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $objAgainOne = $query->first(); + $obj->increment('yo'); + $obj->save(); + $objAgainOne->increment('yo'); + $objAgainOne->save(); + $this->assertEquals($objAgainOne->get('yo'), 3); + } + + public function testIncrementWithOtherUpdates() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->set('foo', 'bar'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $objAgainOne = $query->first(); + $objAgainOne->increment('yo'); + $objAgainOne->set('foo', 'parse'); + $objAgainOne->save(); + $queryAgain = new ParseQuery('TestObject'); + $queryAgain->equalTo('objectId', $objAgainOne->getObjectId()); + $objAgainTwo = $queryAgain->first(); + $this->assertEquals($objAgainOne->get('foo'), 'parse'); + $this->assertEquals($objAgainOne->get('yo'), 2); + } + + public function testIncrementNonNumber() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $this->setExpectedException( + 'Parse\ParseException', 'Cannot increment a non-number type' + ); + $obj->increment('foo'); + $obj->save(); + } + + public function testIncrementOnDeletedField() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yo', 1); + $obj->save(); + $obj->delete('yo'); + $obj->increment('yo'); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals( + $result->get('yo'), 1, 'Error in increment on deleted field' + ); + } + + public function testIncrementEmptyFieldOnFreshObject() + { + $obj = ParseObject::create('TestObject'); + $obj->increment('yo'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $result = $query->first(); + $this->assertEquals($result->get('yo'), 1, + 'Error in increment on empty field of fresh object' + ); + } + + public function testIncrementEmptyField() + { + $obj = ParseObject::create('TestObject'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $objAgain = $query->first(); + $obj->increment('yo'); + $objAgain->increment('yo'); + $obj->save(); + $objAgain->save(); + $queryAgain = new ParseQuery('TestObject'); + $queryAgain->equalTo('objectId', $objAgain->getObjectId()); + $objectAgainTwo = $queryAgain->first(); + $this->assertEquals($objectAgainTwo->get('yo'), 2, + 'Error in increment on empty field' + ); + } + + public function testIncrementEmptyFieldAndTypeConflict() + { + $obj = ParseObject::create('TestObject'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $objAgain = $query->first(); + $obj->set('randomkey', 'bar'); + $obj->save(); + $objAgain->increment('randomkey'); + $this->setExpectedException('Parse\ParseException', + "can't increment a field that isn't a number" + ); + $objAgain->save(); + } + + public function testIncrementEmptyFieldSolidifiesType() + { + $obj = ParseObject::create('TestObject'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $objAgain = $query->first(); + $objAgain->set('randomkeyagain', 'bar'); + $obj->increment('randomkeyagain'); + $obj->save(); + $this->setExpectedException('Parse\ParseException', + 'invalid type for key randomkeyagain, ' . + 'expected number, but got string' + ); + $objAgain->save(); + } +} \ No newline at end of file diff --git a/tests/ParseACLTest.php b/tests/ParseACLTest.php new file mode 100644 index 00000000..683e77f3 --- /dev/null +++ b/tests/ParseACLTest.php @@ -0,0 +1,391 @@ +setUsername('alice'); + $user->setPassword('wonderland'); + $user->signUp(); + $object = ParseObject::create('Object'); + $acl = ParseACL::createACLWithUser($user); + $object->setACL($acl); + $object->save(); + $this->assertTrue($object->getACL()->getUserReadAccess($user)); + $this->assertTrue($object->getACL()->getUserWriteAccess($user)); + $this->assertFalse($object->getACL()->getPublicReadAccess()); + $this->assertFalse($object->getACL()->getPublicWriteAccess()); + + $user->logOut(); + $query = new ParseQuery('Object'); + try { + $query->get($object->getObjectId()); + $this->fail('public should be unable to get'); + } catch (\Parse\ParseException $e) { + } + + $this->assertEquals(0, count($query->find())); + $object->set('foo', 'bar'); + try { + $object->save(); + $this->fail('update should fail with object not found'); + } catch (\Parse\ParseException $e) { + } + + try { + $object->destroy(); + $this->fail('delete should fail with object not found'); + } catch (\Parse\ParseException $e) { + } + + ParseUser::logIn('alice', 'wonderland'); + + $result = $query->get($object->getObjectId()); + $this->assertNotNull($result); + $this->assertTrue($result->getACL()->getUserReadAccess($user)); + $this->assertTrue($result->getACL()->getUserWriteAccess($user)); + $this->assertFalse($result->getACL()->getPublicReadAccess()); + $this->assertFalse($result->getACL()->getPublicWriteAccess()); + + $this->assertEquals(1, count($query->find())); + $object->save(); + $object->destroy(); + + } + + public function testACLMakingAnObjectPubliclyReadable() + { + $user = new ParseUser(); + $user->setUsername('alice'); + $user->setPassword('wonderland'); + $user->signUp(); + $object = ParseObject::create('Object'); + $acl = ParseACL::createACLWithUser($user); + $object->setACL($acl); + $object->save(); + $this->assertTrue($object->getACL()->getUserReadAccess($user)); + $this->assertTrue($object->getACL()->getUserWriteAccess($user)); + $this->assertFalse($object->getACL()->getPublicReadAccess()); + $this->assertFalse($object->getACL()->getPublicWriteAccess()); + + $acl->setPublicReadAccess(true); + $object->setACL($acl); + $object->save(); + + $this->assertTrue($object->getACL()->getUserReadAccess($user)); + $this->assertTrue($object->getACL()->getUserWriteAccess($user)); + $this->assertTrue($object->getACL()->getPublicReadAccess()); + $this->assertFalse($object->getACL()->getPublicWriteAccess()); + + $user->logOut(); + $query = new ParseQuery('Object'); + $result = $query->get($object->getObjectId()); + $this->assertNotNull($result); + + $this->assertTrue($result->getACL()->getUserReadAccess($user)); + $this->assertTrue($result->getACL()->getUserWriteAccess($user)); + $this->assertTrue($result->getACL()->getPublicReadAccess()); + $this->assertFalse($result->getACL()->getPublicWriteAccess()); + $this->assertEquals(1, count($query->find())); + $object->set('foo', 'bar'); + try { + $object->save(); + $this->fail('update should fail with object not found'); + } catch (\Parse\ParseException $e) { + } + + try { + $object->destroy(); + $this->fail('delete should fail with object not found'); + } catch (\Parse\ParseException $e) { + } + } + + public function testACLMakingAnObjectPubliclyWritable() + { + $user = new ParseUser(); + $user->setUsername('alice'); + $user->setPassword('wonderland'); + $user->signUp(); + $object = ParseObject::create('Object'); + $acl = ParseACL::createACLWithUser($user); + $object->setACL($acl); + $object->save(); + $this->assertTrue($object->getACL()->getUserReadAccess($user)); + $this->assertTrue($object->getACL()->getUserWriteAccess($user)); + $this->assertFalse($object->getACL()->getPublicReadAccess()); + $this->assertFalse($object->getACL()->getPublicWriteAccess()); + + $acl->setPublicWriteAccess(true); + $object->setACL($acl); + $object->save(); + + $this->assertTrue($object->getACL()->getUserReadAccess($user)); + $this->assertTrue($object->getACL()->getUserWriteAccess($user)); + $this->assertFalse($object->getACL()->getPublicReadAccess()); + $this->assertTrue($object->getACL()->getPublicWriteAccess()); + + $user->logOut(); + + $query = new ParseQuery('Object'); + try { + $query->get($object->getObjectId()); + $this->fail('public should be unable to get'); + } catch (\Parse\ParseException $e) { + } + + $this->assertEquals(0, count($query->find())); + $object->set('foo', 'bar'); + + $object->save(); + $object->destroy(); + } + + public function testACLSharingWithAnotherUser() + { + $bob = new ParseUser(); + $bob->setUsername('bob'); + $bob->setPassword('pass'); + $bob->signUp(); + $bob->logOut(); + + $alice = new ParseUser(); + $alice->setUsername('alice'); + $alice->setPassword('wonderland'); + $alice->signUp(); + $object = ParseObject::create('Object'); + $acl = ParseACL::createACLWithUser($alice); + $acl->setUserReadAccess($bob, true); + $acl->setUserWriteAccess($bob, true); + $object->setACL($acl); + $object->save(); + $this->assertTrue($object->getACL()->getUserReadAccess($alice)); + $this->assertTrue($object->getACL()->getUserWriteAccess($alice)); + $this->assertTrue($object->getACL()->getUserReadAccess($bob)); + $this->assertTrue($object->getACL()->getUserWriteAccess($bob)); + $this->assertFalse($object->getACL()->getPublicReadAccess()); + $this->assertFalse($object->getACL()->getPublicWriteAccess()); + + ParseUser::logOut(); + + $query = new ParseQuery('Object'); + try { + $query->get($object->getObjectId()); + $this->fail('public should be unable to get'); + } catch (\Parse\ParseException $e) { + } + + $this->assertEquals(0, count($query->find())); + $object->set('foo', 'bar'); + try { + $object->save(); + $this->fail('update should fail with object not found'); + } catch (\Parse\ParseException $e) { + } + + try { + $object->destroy(); + $this->fail('delete should fail with object not found'); + } catch (\Parse\ParseException $e) { + } + + ParseUser::logIn('bob', 'pass'); + + $query = new ParseQuery('Object'); + $result = $query->get($object->getObjectId()); + $this->assertNotNull($result); + $this->assertTrue($result->getACL()->getUserReadAccess($alice)); + $this->assertTrue($result->getACL()->getUserWriteAccess($alice)); + $this->assertTrue($result->getACL()->getUserReadAccess($bob)); + $this->assertTrue($result->getACL()->getUserWriteAccess($bob)); + $this->assertFalse($result->getACL()->getPublicReadAccess()); + $this->assertFalse($result->getACL()->getPublicWriteAccess()); + $this->assertEquals(1, count($query->find())); + $object->set('foo', 'bar'); + $object->save(); + $object->destroy(); + + } + + public function testACLSaveAllWithPermissions() + { + $alice = new ParseUser(); + $alice->setUsername('alice'); + $alice->setPassword('wonderland'); + $alice->signUp(); + $acl = ParseACL::createACLWithUser($alice); + $object1 = ParseObject::create('Object'); + $object1->setACL($acl); + $object1->save(); + $object2 = ParseObject::create('Object'); + $object2->setACL($acl); + $object2->save(); + + $this->assertTrue($object1->getACL()->getUserReadAccess($alice)); + $this->assertTrue($object1->getACL()->getUserWriteAccess($alice)); + $this->assertFalse($object1->getACL()->getPublicReadAccess()); + $this->assertFalse($object1->getACL()->getPublicWriteAccess()); + $this->assertTrue($object2->getACL()->getUserReadAccess($alice)); + $this->assertTrue($object2->getACL()->getUserWriteAccess($alice)); + $this->assertFalse($object2->getACL()->getPublicReadAccess()); + $this->assertFalse($object2->getACL()->getPublicWriteAccess()); + + $object1->set('foo', 'bar'); + $object2->set('foo', 'bar'); + ParseObject::saveAll([$object1, $object2]); + + $query = new ParseQuery('Object'); + $query->equalTo('foo', 'bar'); + $this->assertEquals(2, count($query->find())); + + } + + public function testACLModifyingAfterLoad() + { + $user = new ParseUser(); + $user->setUsername('alice'); + $user->setPassword('wonderland'); + $user->signUp(); + $object = ParseObject::create('Object'); + $acl = ParseACL::createACLWithUser($user); + $object->setACL($acl); + $object->save(); + $this->assertTrue($object->getACL()->getUserReadAccess($user)); + $this->assertTrue($object->getACL()->getUserWriteAccess($user)); + $this->assertFalse($object->getACL()->getPublicReadAccess()); + $this->assertFalse($object->getACL()->getPublicWriteAccess()); + $query = new ParseQuery('Object'); + $objectAgain = $query->get($object->getObjectId()); + $objectAgain->getACL()->setPublicReadAccess(true); + + $this->assertTrue($objectAgain->getACL()->getUserReadAccess($user)); + $this->assertTrue($objectAgain->getACL()->getUserWriteAccess($user)); + $this->assertTrue($objectAgain->getACL()->getPublicReadAccess()); + $this->assertFalse($objectAgain->getACL()->getPublicWriteAccess()); + + + } + + public function testACLRequiresObjectId() + { + $acl = new ParseACL(); + try { + $acl->setReadAccess(null, true); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + try { + $acl->getReadAccess(null); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + try { + $acl->setWriteAccess(null, true); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + try { + $acl->getWriteAccess(null); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + + $user = new ParseUser(); + try { + $acl->setReadAccess($user, true); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + try { + $acl->getReadAccess($user); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + try { + $acl->setWriteAccess($user, true); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + try { + $acl->getWriteAccess($user); + $this->fail('Exception should have thrown'); + } catch (Exception $e) { + } + + } + + public function testIncludedObjectsGetACLs() + { + ParseTestHelper::clearClass("Test"); + ParseTestHelper::clearClass("Related"); + $object = ParseObject::create('Test'); + $acl = new ParseACL(); + $acl->setPublicReadAccess(true); + $object->setACL($acl); + $object->save(); + $this->assertTrue($object->getACL()->getPublicReadAccess()); + + $related = ParseObject::create('Related'); + $related->set('test', $object); + $related->save(); + + $query = new ParseQuery('Related'); + $query->includeKey('test'); + $objectAgain = $query->first()->get('test'); + + $this->assertTrue($objectAgain->getACL()->getPublicReadAccess()); + $this->assertFalse($objectAgain->getACL()->getPublicWriteAccess()); + } + + public function testIncludedObjectsGetACLWithDefaultACL() + { + ParseTestHelper::clearClass("Test"); + ParseTestHelper::clearClass("Related"); + $defaultACL = new ParseACL(); + $defaultACL->setPublicReadAccess(true); + $defaultACL->setPublicWriteAccess(true); + ParseACL::setDefaultACL($defaultACL, true); + + $object = ParseObject::create('Test'); + $acl = new ParseACL(); + $acl->setPublicReadAccess(true); + $object->setACL($acl); + $object->save(); + + $this->assertTrue($object->getACL()->getPublicReadAccess()); + $related = ParseObject::create('Related'); + $related->set('test', $object); + $related->save(); + + $query = new ParseQuery('Related'); + $query->includeKey('test'); + $objectAgain = $query->first()->get('test'); + $this->assertTrue($objectAgain->getACL()->getPublicReadAccess()); + $this->assertFalse($objectAgain->getACL()->getPublicWriteAccess()); + + } + +} diff --git a/tests/ParseAnalyticsTest.php b/tests/ParseAnalyticsTest.php new file mode 100644 index 00000000..877cca3c --- /dev/null +++ b/tests/ParseAnalyticsTest.php @@ -0,0 +1,79 @@ +assertEquals($expectedJSON, $json); + ParseAnalytics::track($event, $params ?: array()); + } + + public function testTrackEvent() + { + $expected = '{"dimensions":{}}'; + $this->assertAnalyticsValidation('testTrackEvent', null, $expected); + } + + public function testFailsOnEventName1() + { + $this->setExpectedException( + 'Exception', 'A name for the custom event must be provided.' + ); + ParseAnalytics::track(''); + } + + public function testFailsOnEventName2() + { + $this->setExpectedException( + 'Exception', 'A name for the custom event must be provided.' + ); + ParseAnalytics::track(' '); + } + + public function testFailsOnEventName3() + { + $this->setExpectedException( + 'Exception', 'A name for the custom event must be provided.' + ); + ParseAnalytics::track(" \n"); + } + + public function testTrackEventDimensions() + { + $expected = '{"dimensions":{"foo":"bar","bar":"baz"}}'; + $params = array( + 'foo' => 'bar', + 'bar' => 'baz' + ); + $this->assertAnalyticsValidation('testDimensions', $params, $expected); + + $date = date(DATE_RFC3339); + $expected = '{"dimensions":{"foo":"bar","bar":"baz","someDate":"' . + $date . '"}}'; + $params = array( + 'foo' => 'bar', + 'bar' => 'baz', + 'someDate' => $date + ); + $this->assertAnalyticsValidation('testDate', $params, $expected); + } + +} \ No newline at end of file diff --git a/tests/ParseBytesTest.php b/tests/ParseBytesTest.php new file mode 100644 index 00000000..00975754 --- /dev/null +++ b/tests/ParseBytesTest.php @@ -0,0 +1,51 @@ +set("byteColumn", $bytes); + $obj->save(); + + $query = new ParseQuery("BytesObject"); + $objAgain = $query->get($obj->getObjectId()); + $this->assertEquals("Fosco", $objAgain->get("byteColumn")); + } + + public function testParseBytesFromBase64Data() + { + $obj = ParseObject::create("BytesObject"); + $bytes = ParseBytes::createFromBase64Data("R3JhbnRsYW5k"); + $obj->set("byteColumn", $bytes); + $obj->save(); + + $query = new ParseQuery("BytesObject"); + $objAgain = $query->get($obj->getObjectId()); + $this->assertEquals("Grantland", $objAgain->get("byteColumn")); + } + +} \ No newline at end of file diff --git a/tests/ParseCloudTest.php b/tests/ParseCloudTest.php new file mode 100644 index 00000000..c8749acc --- /dev/null +++ b/tests/ParseCloudTest.php @@ -0,0 +1,93 @@ +set('name', 'Zanzibar'); + $obj->save(); + $params = array('key1' => $obj); + $this->setExpectedException('\Exception', 'ParseObjects not allowed'); + ParseCloud::run('foo', $params); + } + + public function testFunctionsWithGeoPointParamsDoNotThrow() + { + $params = array('key1' => new ParseGeoPoint(50, 50)); + $this->setExpectedException('Parse\ParseException', 'function not found'); + ParseCloud::run('unknown_function', $params); + } + + public function testExplicitFunctionFailure() + { + $params = array('key1' => 'value1'); + $this->setExpectedException('Parse\ParseException','bad stuff happened'); + ParseCloud::run('bar', $params); + } + + public function testUnknownFunctionFailure() + { + $params = array('key1' => 'value1'); + $this->setExpectedException('Parse\ParseException','function not found'); + ParseCloud::run('unknown_function', $params); + } + + public function testFunctions() + { + $params = array( + 'key1' => 'value1', + 'key2' => array(1,2,3) + ); + $response = ParseCloud::run('foo', $params); + $obj = $response['object']; + $this->assertTrue($obj instanceof ParseObject); + $this->assertEquals('Foo', $obj->className); + $this->assertEquals(2, $obj->get('x')); + $relation = $obj->get('relation'); + $this->assertTrue($relation instanceof ParseObject); + $this->assertEquals('Bar', $relation->className); + $this->assertEquals(3, $relation->get('x')); + $obj = $response['array'][0]; + $this->assertTrue($obj instanceof ParseObject); + $this->assertEquals('Bar', $obj->className); + $this->assertEquals(2, $obj->get('x')); + + $response = ParseCloud::run('foo', array('key1' => 'value1')); + $this->assertEquals(2, $response['a']); + + try { + $response = ParseCloud::run('bar', array('key1' => 'value1')); + $this->fail('Should have thrown an exception.'); + } catch(Parse\ParseException $ex) { + // A parse exception should occur. + } + + $response = ParseCloud::run('bar', array('key2' => 'value1')); + $this->assertEquals('Foo', $response); + + $obj = ParseObject::create('SomeClass'); + $obj->set('name', 'Zanzibar'); + $obj->save(); + + $params = array('key2' => 'value1', 'key1' => $obj); + try { + $response = ParseCloud::run('foo', $params); + $this->fail('Should have thrown an exception.'); + } catch (\Exception $ex) { + // An exception should occur. + } + } +} \ No newline at end of file diff --git a/tests/ParseFileTest.php b/tests/ParseFileTest.php new file mode 100644 index 00000000..53720dba --- /dev/null +++ b/tests/ParseFileTest.php @@ -0,0 +1,126 @@ +assertEquals("http://", $file->getURL()); + $this->assertEquals("hi.txt", $file->getName()); + $this->assertEquals("hello", $file2->getData()); + $this->assertEquals("hi.txt", $file2->getName()); + $this->assertTrue( + strpos( + $file3->getData(), 'i am looking for myself' + ) !== false + ); + } + + public function testParseFileUpload() + { + $file = ParseFile::createFromData("Fosco", "test.txt"); + $file->save(); + $this->assertTrue( + strpos($file->getURL(), 'http') !== false + ); + $this->assertNotEquals("test.txt", $file->getName()); + } + + public function testParseFileDownload() + { + $file = ParseFile::_createFromServer("index.html", "http://example.com"); + $data = $file->getData(); + $this->assertTrue( + strpos($data, 'Example Domain') !== false + ); + } + + public function testParseFileRoundTrip() + { + $contents = "What would Bryan do?"; + $file = ParseFile::createFromData($contents, "test.txt"); + $this->assertEquals($contents, $file->getData()); + $file->save(); + + $fileAgain = ParseFile::_createFromServer($file->getName(), $file->getURL()); + $this->assertEquals($contents, $fileAgain->getData()); + $fileAgain->save(); + $this->assertEquals($file->getURL(), $fileAgain->getURL()); + } + + public function testParseFileTypes() + { + $contents = "a fractal of rad design"; + $file = ParseFile::createFromData($contents, "noextension"); + $file2 = ParseFile::createFromData($contents, "photo.png", "text/plain"); + $file3 = ParseFile::createFromData($contents, "photo.png"); + $file->save(); + $file2->save(); + $file3->save(); + + $fileAgain = ParseFile::_createFromServer($file->getName(), $file->getURL()); + $file2Again = ParseFile::_createFromServer($file2->getName(), $file2->getURL()); + $file3Again = ParseFile::_createFromServer($file3->getName(), $file3->getURL()); + + $this->assertEquals($contents, $fileAgain->getData()); + $this->assertEquals($contents, $file2Again->getData()); + $this->assertEquals($contents, $file3Again->getData()); + + $this->assertEquals("unknown/unknown", $fileAgain->getMimeType()); + $this->assertEquals("text/plain", $file2Again->getMimeType()); + $this->assertEquals("image/png", $file3Again->getMimeType()); + } + + public function testFileOnObject() + { + $contents = "irrelephant"; + $file = ParseFile::createFromData($contents, "php.txt"); + $file->save(); + + $obj = ParseObject::create("TestFileObject"); + $obj->set("file", $file); + $obj->save(); + + $query = new ParseQuery("TestFileObject"); + $objAgain = $query->get($obj->getObjectId()); + $fileAgain = $objAgain->get("file"); + $contentsAgain = $fileAgain->getData(); + $this->assertEquals($contents, $contentsAgain); + } + + public function testUnsavedFileOnObjectSave() + { + $contents = "remember"; + $file = ParseFile::createFromData($contents, "bones.txt"); + $obj = ParseObject::create("TestFileObject"); + $obj->set("file", $file); + $obj->save(); + + $query = new ParseQuery("TestFileObject"); + $objAgain = $query->get($obj->getObjectId()); + $fileAgain = $objAgain->get("file"); + $contentsAgain = $fileAgain->getData(); + $this->assertEquals($contents, $contentsAgain); + } + +} diff --git a/tests/ParseGeoBoxTest.php b/tests/ParseGeoBoxTest.php new file mode 100644 index 00000000..cc79d1e9 --- /dev/null +++ b/tests/ParseGeoBoxTest.php @@ -0,0 +1,156 @@ +set('location', $caltrainStationLocation); + $caltrainStation->set('name', 'caltrain'); + $caltrainStation->save(); + + $santaClaraLocation = new ParseGeoPoint(37.325635, -121.945753); + $santaClara = new ParseObject('TestObject'); + + $santaClara->set('location', $santaClaraLocation); + $santaClara->set('name', 'santa clara'); + $santaClara->save(); + + $southwestOfSF = new ParseGeoPoint(37.708813, -122.526398); + $northeastOfSF = new ParseGeoPoint(37.822802, -122.373962); + + // Try a correct query + $query = new ParseQuery('TestObject'); + $query->withinGeoBox('location', $southwestOfSF, $northeastOfSF); + $objectsInSF = $query->find(); + $this->assertEquals(1, count($objectsInSF)); + $this->assertEquals('caltrain', $objectsInSF[0]->get('name')); + + // Switch order of args, should fail because it crosses the dateline + $query = new ParseQuery('TestObject'); + $query->withinGeoBox('location', $northeastOfSF, $southwestOfSF); + try { + $results = $query->find(); + $this->assertTrue(FALSE, 'Query should fail because it crosses dateline'); + } catch (ParseException $e) { + } + + $northwestOfSF = new ParseGeoPoint(37.822802, -122.526398); + $southeastOfSF = new ParseGeoPoint(37.708813, -122.373962); + + // Switch just longitude, should fail because it crosses the dateline + $query = new ParseQuery('TestObject'); + $query->withinGeoBox('location', $southeastOfSF, $northwestOfSF); + try { + $query->find(); + $this->assertTrue(FALSE, 'Query should fail because it crosses dateline'); + } catch (ParseException $e) { + } + + // Switch just the latitude, should fail because it doesnt make sense + $query = new ParseQuery('TestObject'); + $query->withinGeoBox('location', $northwestOfSF, $southeastOfSF); + try { + $query->find(); + $this->assertTrue(FALSE, 'Query should fail because it makes no sense'); + } catch (ParseException $e) { + } + } + + public function testGeoBoxSmallNearDateLine() + { + $nearWestOfDateLine = new ParseGeoPoint(0, 175); + $nearWestObject = ParseObject::create('TestObject'); + + $nearWestObject->set('location', $nearWestOfDateLine); + $nearWestObject->set('name', 'near west'); + $nearWestObject->set('order', 1); + $nearWestObject->save(); + + $nearEastOfDateLine = new ParseGeoPoint(0, -175); + $nearEastObject = ParseObject::create('TestObject'); + + $nearEastObject->set('location', $nearEastOfDateLine); + $nearEastObject->set('name', 'near east'); + $nearEastObject->set('order', 2); + $nearEastObject->save(); + + $farWestOfDateLine = new ParseGeoPoint(0, 165); + $farWestObject = ParseObject::create('TestObject'); + + $farWestObject->set('location', $farWestOfDateLine); + $farWestObject->set('name', 'far west'); + $farWestObject->set('order', 3); + $farWestObject->save(); + + $farEastOfDateLine = new ParseGeoPoint(0, -165); + $farEastObject = ParseObject::create('TestObject'); + + $farEastObject->set('location', $farEastOfDateLine); + $farEastObject->set('name', 'far east'); + $farEastObject->set('order', 4); + $farEastObject->save(); + + $southwestOfDateLine = new ParseGeoPoint(-10, 170); + $northeastOfDateLine = new ParseGeoPoint(10, -170); + + $query = new ParseQuery('TestObject'); + $query->withinGeoBox('location', $southwestOfDateLine, $northeastOfDateLine); + $query->ascending('order'); + try { + $query->find(); + $this->assertTrue(FALSE, 'Query should fail for crossing the date line.'); + } catch (ParseException $e) { + } + } + + public function testGeoBoxTooLarge() + { + $centerPoint = new ParseGeoPoint(0, 0); + $center = ParseObject::create('TestObject'); + + $center->set('location', $centerPoint); + $center->save(); + + $southwest = new ParseGeoPoint(-89, -179); + $northeast = new ParseGeoPoint(89, 179); + + // This is an interesting test case because mongo can actually handle this + // kind of query, but + // if one actually happens, it's probably that the developer switches the + // two points. + $query = new ParseQuery('TestObject'); + $query->withinGeoBox('location', $southwest, $northeast); + try { + $query->find(); + $this->assertTrue(FALSE, 'Query should fail for being too large.'); + } catch (ParseException $e) { + } + } +} + diff --git a/tests/ParseGeoPointTest.php b/tests/ParseGeoPointTest.php new file mode 100644 index 00000000..e68f488b --- /dev/null +++ b/tests/ParseGeoPointTest.php @@ -0,0 +1,207 @@ +set('location', $point); + + $obj->set('name', 'Ferndale'); + $obj->save(); + + // Non geo query + $query = new ParseQuery('TestObject'); + $query->equalTo('name', 'Ferndale'); + $results = $query->find(); + $this->assertEquals(1, count($results)); + + // Round trip encoding + $actualPoint = $results[0]->get('location'); + $this->assertEquals(44.0, $actualPoint->getLatitude(), '', 0.0001); + $this->assertEquals(-11.0, $actualPoint->getLongitude(), '', 0.0001); + + // nearsphere + $point->setLatitude(66.0); + $query = new ParseQuery('TestObject'); + $query->near('location', $point); + $results = $query->find(); + $this->assertEquals(1, count($results)); + } + + public function testGeoLine() + { + for ($i = 0; $i < 10; ++$i) { + $obj = ParseObject::create('TestObject'); + $point = new ParseGeoPoint($i * 4.0 - 12.0, $i * 3.2 - 11.0); + $obj->set('location', $point); + $obj->set('construct', 'line'); + $obj->set('seq', $i); + $obj->save(); + } + + $query = new ParseQuery('TestObject'); + $point = new ParseGeoPoint(24.0, 19.0); + $query->equalTo('construct', 'line'); + $query->withinMiles('location', $point, 10000); + $results = $query->find(); + $this->assertEquals(10, count($results)); + $this->assertEquals(9, $results[0]->get('seq')); + $this->assertEquals(6, $results[3]->get('seq')); + } + + public function testGeoMaxDistance() + { + for ($i = 0; $i < 3; ++$i) { + $obj = ParseObject::create('TestObject'); + $point = new ParseGeoPoint(0.0, $i * 45.0); + $obj->set('location', $point); + $obj->set('id', $i); + $obj->save(); + } + + // baseline all + $query = new ParseQuery('TestObject'); + $point = new ParseGeoPoint(1.0, -1.0); + $query->near('location', $point); + $results = $query->find(); + $this->assertEquals(3, count($results)); + + // all + $query = new ParseQuery('TestObject'); + $query->withinRadians('location', $point, 3.14 * 2); + $results = $query->find(); + $this->assertEquals(3, count($results)); + + // all + $query = new ParseQuery('TestObject'); + $query->withinRadians('location', $point, 3.14); + $results = $query->find(); + $this->assertEquals(3, count($results)); + + // 2 + $query = new ParseQuery('TestObject'); + $query->withinRadians('location', $point, 3.14 * 0.5); + $results = $query->find(); + $this->assertEquals(2, count($results)); + $this->assertEquals(1, $results[1]->get('id')); + + // 1 + $query = new ParseQuery('TestObject'); + $query->withinRadians('location', $point, 3.14 * 0.25); + $results = $query->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals(0, $results[0]->get('id')); + + } + + public function testGeoMaxDistanceWithUnits() + { + ParseTestHelper::clearClass("PlaceObject"); + // [SAC] 38.52 -121.50 Sacramento,CA + $sacramento = new ParseGeoPoint(38.52, -121.50); + $obj = ParseObject::create('PlaceObject'); + $obj->set('location', $sacramento); + $obj->set('name', 'Sacramento'); + $obj->save(); + + // [HNL] 21.35 -157.93 Honolulu Int,HI + $honolulu = new ParseGeoPoint(21.35, -157.93); + $obj = ParseObject::create('PlaceObject'); + $obj->set('location', $honolulu); + $obj->set('name', 'Honolulu'); + $obj->save(); + + // [51Q] 37.75 -122.68 San Francisco,CA + $sanfran = new ParseGeoPoint(37.75, -122.68); + $obj = ParseObject::create('PlaceObject'); + $obj->set('location', $sanfran); + $obj->set('name', 'San Francisco'); + $obj->save(); + + // test point SFO + $point = new ParseGeoPoint(37.6189722, -122.3748889); + + // Kilometers + // baseline all + $query = new ParseQuery('PlaceObject'); + $query->near('location', $point); + $results = $query->find(); + $this->assertEquals(3, count($results)); + + // max with all + $query = new ParseQuery('PlaceObject'); + $query->withinKilometers('location', $point, 4000.0); + $results = $query->find(); + $this->assertEquals(3, count($results)); + + // drop hawaii + $query = new ParseQuery('PlaceObject'); + $query->withinKilometers('location', $point, 3700.0); + $results = $query->find(); + $this->assertEquals(2, count($results)); + + // drop sacramento + $query = new ParseQuery('PlaceObject'); + $query->withinKilometers('location', $point, 100.0); + $results = $query->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals('San Francisco', $results[0]->get('name')); + + // drop SF + $query = new ParseQuery('PlaceObject'); + $query->withinKilometers('location', $point, 10.0); + $results = $query->find(); + $this->assertEquals(0, count($results)); + + // Miles + // max with all + $query = new ParseQuery('PlaceObject'); + $query->withinMiles('location', $point, 2500.0); + $results = $query->find(); + $this->assertEquals(3, count($results)); + + // drop hawaii + $query = new ParseQuery('PlaceObject'); + $query->withinMiles('location', $point, 2200.0); + $results = $query->find(); + $this->assertEquals(2, count($results)); + + // drop sacramento + $query = new ParseQuery('PlaceObject'); + $query->withinMiles('location', $point, 75.0); + $results = $query->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals('San Francisco', $results[0]->get('name')); + + // drop SF + $query = new ParseQuery('PlaceObject'); + $query->withinMiles('location', $point, 10.0); + $results = $query->find(); + $this->assertEquals(0, count($results)); + } +} diff --git a/tests/ParseMemoryStorageTest.php b/tests/ParseMemoryStorageTest.php new file mode 100644 index 00000000..113ba3ac --- /dev/null +++ b/tests/ParseMemoryStorageTest.php @@ -0,0 +1,71 @@ +clear(); + } + + public function testIsUsingDefaultStorage() + { + $this->assertTrue( + self::$parseStorage instanceof Parse\ParseMemoryStorage + ); + } + + public function testSetAndGet() + { + self::$parseStorage->set('foo', 'bar'); + $this->assertEquals('bar', self::$parseStorage->get('foo')); + } + + public function testRemove() + { + self::$parseStorage->set('foo', 'bar'); + self::$parseStorage->remove('foo'); + $this->assertNull(self::$parseStorage->get('foo')); + } + + public function testClear() + { + self::$parseStorage->set('foo', 'bar'); + self::$parseStorage->set('foo2', 'bar'); + self::$parseStorage->set('foo3', 'bar'); + self::$parseStorage->clear(); + $this->assertEmpty(self::$parseStorage->getKeys()); + } + + public function testGetAll() + { + self::$parseStorage->set('foo', 'bar'); + self::$parseStorage->set('foo2', 'bar'); + self::$parseStorage->set('foo3', 'bar'); + $result = self::$parseStorage->getAll(); + $this->assertEquals('bar', $result['foo']); + $this->assertEquals('bar', $result['foo2']); + $this->assertEquals('bar', $result['foo3']); + $this->assertEquals(3, count($result)); + } + +} \ No newline at end of file diff --git a/tests/ParseObjectTest.php b/tests/ParseObjectTest.php new file mode 100644 index 00000000..89921ad6 --- /dev/null +++ b/tests/ParseObjectTest.php @@ -0,0 +1,929 @@ +set('test', 'test'); + $obj->save(); + } + + public function testUpdate() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $obj->set('foo', 'changed'); + $obj->save(); + $this->assertEquals($obj->foo, 'changed', + 'Update should have succeeded'); + } + + public function testSaveCycle() + { + $a = ParseObject::create('TestObject'); + $b = ParseObject::create('TestObject'); + $a->set('b', $b); + $a->save(); + $this->assertFalse($a->isDirty()); + $this->assertNotNull($a->getObjectId()); + $this->assertNotNull($b->getObjectId()); + $b->set('a', $a); + $b->save(); + $this->assertEquals($b, $a->get('b')); + $this->assertEquals($a, $b->get('a')); + } + + public function testReturnedObjectIsAParseObject() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + + $query = new ParseQuery('TestObject'); + $returnedObject = $query->get($obj->getObjectId()); + $this->assertTrue($returnedObject instanceOf ParseObject, + 'Returned object was not a ParseObject'); + $this->assertEquals('bar', $returnedObject->foo, + 'Value of foo was not saved.'); + } + + public function testFetch() + { + $obj = ParseObject::create('TestObject'); + $obj->set('test', 'test'); + $obj->save(); + $t2 = ParseObject::create('TestObject', $obj->getObjectId()); + $t2->fetch(); + $this->assertEquals('test', $t2->get('test'), 'Fetch failed.'); + } + + public function testDelete() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $obj->destroy(); + $query = new ParseQuery('TestObject'); + $this->setExpectedException('Parse\ParseException', 'Object not found'); + $out = $query->get($obj->getObjectId()); + } + + public function testFind() + { + ParseTestHelper::clearClass('TestObject'); + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('foo', 'bar'); + $response = $query->count(); + $this->assertTrue($response == 1); + } + + public function testRelationalFields() + { + ParseTestHelper::clearClass("Item"); + ParseTestHelper::clearClass("Container"); + $item = ParseObject::create("Item"); + $item->set("property", "x"); + $item->save(); + + $container = ParseObject::create("Container"); + $container->set("item", $item); + $container->save(); + + $query = new ParseQuery("Container"); + $query->includeKey("item"); + $containerAgain = $query->get($container->getObjectId()); + $itemAgain = $containerAgain->get("item"); + $this->assertEquals("x", $itemAgain->get("property")); + + $query->equalTo("item", $item); + $results = $query->find(); + $this->assertEquals(1, count($results)); + } + + public function testRelationDeletion() + { + ParseTestHelper::clearClass("SimpleObject"); + ParseTestHelper::clearClass("Child"); + $simple = ParseObject::create("SimpleObject"); + $child = ParseObject::create("Child"); + $simple->set('child', $child); + $simple->save(); + $this->assertNotNull($simple->get('child')); + $simple->delete('child'); + $this->assertNull($simple->get('child')); + $this->assertTrue($simple->isDirty()); + $this->assertTrue($simple->isKeyDirty('child')); + $simple->save(); + $this->assertNull($simple->get('child')); + $this->assertFalse($simple->isDirty()); + $this->assertFalse($simple->isKeyDirty('child')); + + $query = new ParseQuery("SimpleObject"); + $simpleAgain = $query->get($simple->getObjectId()); + $this->assertNull($simpleAgain->get('child')); + } + + public function testSaveAddsNoDataKeys() + { + $obj = ParseObject::create('TestObject'); + $obj->save(); + $json = $obj->_encode(); + $data = get_object_vars(json_decode($json)); + unset($data['objectId']); + unset($data['createdAt']); + unset($data['updatedAt']); + $this->assertEquals(0, count($data)); + } + + public function testRecursiveSave() + { + ParseTestHelper::clearClass('Container'); + ParseTestHelper::clearClass('Item'); + $a = ParseObject::create('Container'); + $b = ParseObject::create('Item'); + $b->set('foo', 'bar'); + $a->set('item', $b); + $a->save(); + $query = new ParseQuery('Container'); + $result = $query->find(); + $this->assertEquals(1, count($result)); + $containerAgain = $result[0]; + $itemAgain = $containerAgain->get('item'); + $itemAgain->fetch(); + $this->assertEquals('bar', $itemAgain->get('foo')); + } + + public function testFetchRemovesOldFields() + { + $obj = ParseObject::create('SimpleObject'); + $obj->set('foo', 'bar'); + $obj->set('test', 'foo'); + $obj->save(); + + $query = new ParseQuery('SimpleObject'); + $object1 = $query->get($obj->getObjectId()); + $object2 = $query->get($obj->getObjectId()); + $this->assertEquals('foo', $object1->get('test')); + $this->assertEquals('foo', $object2->get('test')); + $object2->delete('test'); + $this->assertEquals('foo', $object1->get('test')); + $object2->save(); + $object1->fetch(); + $this->assertEquals(null, $object1->get('test')); + $this->assertEquals(null, $object2->get('test')); + $this->assertEquals('bar', $object1->get('foo')); + $this->assertEquals('bar', $object2->get('foo')); + } + + public function testCreatedAtAndUpdatedAtExposed() + { + $obj = ParseObject::create('TestObject'); + $obj->save(); + $this->assertNotNull($obj->getObjectId()); + $this->assertNotNull($obj->getCreatedAt()); + $this->assertNotNull($obj->getUpdatedAt()); + } + + public function testCreatedAtDoesNotChange() + { + $obj = ParseObject::create('TestObject'); + $obj->save(); + $this->assertNotNull($obj->getObjectId()); + $objAgain = ParseObject::create('TestObject', $obj->getObjectId()); + $objAgain->fetch(); + $this->assertEquals( + $obj->getCreatedAt(), $objAgain->getCreatedAt() + ); + } + + public function testUpdatedAtGetsUpdated() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $this->assertNotNull($obj->getUpdatedAt()); + $firstUpdate = $obj->getUpdatedAt(); + // Parse is so fast, this test was flaky as the \DateTimes were equal. + sleep(1); + $obj->set('foo', 'baz'); + $obj->save(); + $this->assertNotEquals($obj->getUpdatedAt(), $firstUpdate); + } + + public function testCreatedAtIsReasonable() + { + $startTime = new \DateTime(); + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $endTime = new \DateTime(); + $startDiff = abs( + $startTime->getTimestamp() - $obj->getCreatedAt()->getTimestamp() + ); + $endDiff = abs( + $endTime->getTimestamp() - $obj->getCreatedAt()->getTimestamp() + ); + $this->assertLessThan(5000, $startDiff); + $this->assertLessThan(5000, $endDiff); + } + + public function testCanSetNull() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', null); + $obj->save(); + $this->assertEquals(null, $obj->get('foo')); + } + + public function testCanSetBoolean() + { + $obj = ParseObject::create('TestObject'); + $obj->set('yes', true); + $obj->set('no', false); + $obj->save(); + $this->assertTrue($obj->get('yes')); + $this->assertFalse($obj->get('no')); + } + + public function testInvalidClassName() + { + $obj = ParseObject::create('Foo^bar'); + $this->setExpectedException('Parse\ParseException', 'Bad Request'); + $obj->save(); + } + + public function testInvalidKeyName() + { + $obj = ParseObject::create("TestItem"); + $obj->set('foo^bar', 'baz'); + $this->setExpectedException('Parse\ParseException', + 'invalid field name'); + $obj->save(); + } + + public function testSimpleFieldDeletion() + { + $obj = ParseObject::create("TestObject"); + $obj->set('foo', 'bar'); + $obj->save(); + $obj->delete('foo'); + $this->assertFalse($obj->has('foo'), 'foo should have been unset.'); + $this->assertTrue($obj->isKeyDirty('foo'), 'foo should be dirty.'); + $this->assertTrue($obj->isDirty(), 'the whole object should be dirty.'); + $obj->save(); + $this->assertFalse($obj->has('foo'), 'foo should have been unset.'); + $this->assertFalse($obj->isKeyDirty('foo'), 'object was just saved.'); + $this->assertFalse($obj->isDirty(), 'object was just saved.'); + + $query = new ParseQuery("TestObject"); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('foo'), 'foo was not removed.'); + } + + public function testFieldDeletionBeforeFirstSave() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->delete('foo'); + $this->assertFalse($obj->has('foo'), 'foo should have been unset.'); + $this->assertTrue($obj->isKeyDirty('foo'), 'foo should be dirty.'); + $this->assertTrue($obj->isDirty(), 'the whole object should be dirty.'); + $obj->save(); + $this->assertFalse($obj->has('foo'), 'foo should have been unset.'); + $this->assertFalse($obj->isKeyDirty('foo'), 'object was just saved.'); + $this->assertFalse($obj->isDirty(), 'object was just saved.'); + } + + public function testDeletedKeysGetCleared() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->delete('foo'); + $obj->save(); + $obj->set('foo', 'baz'); + $obj->save(); + + $query = new ParseQuery("TestObject"); + $result = $query->get($obj->getObjectId()); + $this->assertEquals('baz', $result->get('foo')); + } + + public function testSettingAfterDeleting() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $obj->delete('foo'); + $obj->set('foo', 'baz'); + $obj->save(); + + $query = new ParseQuery("TestObject"); + $result = $query->get($obj->getObjectId()); + $this->assertEquals('baz', $result->get('foo')); + } + + public function testDirtyKeys() + { + $obj = ParseObject::create('TestObject'); + $obj->set('cat', 'good'); + $obj->set('dog', 'bad'); + $obj->save(); + $this->assertFalse($obj->isDirty()); + $this->assertFalse($obj->isKeyDirty('cat')); + $this->assertFalse($obj->isKeyDirty('dog')); + $obj->set('dog', 'okay'); + $this->assertTrue($obj->isKeyDirty('dog')); + $this->assertTrue($obj->isDirty()); + } + + public function testOldAttributeUnsetThenUnset() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 3); + $obj->save(); + $obj->delete('x'); + $obj->delete('x'); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testNewAttributeUnsetThenUnset() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 5); + $obj->delete('x'); + $obj->delete('x'); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testUnknownAttributeUnsetThenUnset() + { + $obj = ParseObject::create('TestObject'); + $obj->delete('x'); + $obj->delete('x'); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function oldAttributeUnsetThenClear() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 3); + $obj->save(); + $obj->delete('x'); + $obj->clear(); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testNewAttributeUnsetThenClear() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 5); + $obj->delete('x'); + $obj->clear(); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testUnknownAttributeUnsetThenClear() + { + $obj = ParseObject::create('TestObject'); + $obj->delete('x'); + $obj->clear(); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function oldAttributeClearThenUnset() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 3); + $obj->save(); + $obj->clear(); + $obj->delete('x'); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testNewAttributeClearThenUnset() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 5); + $obj->clear(); + $obj->delete('x'); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testUnknownAttributeClearThenUnset() + { + $obj = ParseObject::create('TestObject'); + $obj->clear(); + $obj->delete('x'); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function oldAttributeClearThenClear() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 3); + $obj->save(); + $obj->clear(); + $obj->clear(); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testNewAttributeClearThenClear() + { + $obj = ParseObject::create('TestObject'); + $obj->set('x', 5); + $obj->clear(); + $obj->clear(); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testUnknownAttributeClearThenClear() + { + $obj = ParseObject::create('TestObject'); + $obj->clear(); + $obj->clear(); + $obj->save(); + $this->assertFalse($obj->has('x')); + $this->assertNull($obj->get('x')); + + $query = new ParseQuery('TestObject'); + $result = $query->get($obj->getObjectId()); + $this->assertFalse($result->has('x')); + $this->assertNull($result->get('x')); + } + + public function testSavingChildrenInArray() + { + ParseTestHelper::clearClass("Parent"); + ParseTestHelper::clearClass("Child"); + $parent = ParseObject::create("Parent"); + $child1 = ParseObject::create("Child"); + $child2 = ParseObject::create("Child"); + $child1->set('name', 'tyrian'); + $child2->set('name', 'cersei'); + $parent->setArray('children', array($child1, $child2)); + $parent->save(); + + $query = new ParseQuery("Child"); + $query->ascending('name'); + $results = $query->find(); + $this->assertEquals(2, count($results)); + $this->assertEquals('cersei', $results[0]->get('name')); + $this->assertEquals('tyrian', $results[1]->get('name')); + } + + public function testManySaveAfterAFailure() + { + $obj = ParseObject::create("TestObject"); + $obj->set("number", 1); + $obj->save(); + $obj2 = ParseObject::create("TestObject"); + $obj2->set("number", "two"); + $exceptions = 0; + try { + $obj2->save(); + } catch (\Parse\ParseException $pe) { + $exceptions++; + } + $obj2->set('foo', 'bar'); + try { + $obj2->save(); + } catch (\Parse\ParseException $pe) { + $exceptions++; + } + $obj2->set('foo', 'baz'); + try { + $obj2->save(); + } catch (\Parse\ParseException $pe) { + $exceptions++; + } + $obj2->set('number', 3); + $obj2->save(); + if ($exceptions != 3) { + $this->fail("Did not cause expected # of exceptions."); + } + } + + public function testNewKeyIsDirtyAfterSave() + { + $obj = ParseObject::create("TestObject"); + $obj->save(); + $obj->set('content', 'x'); + $obj->fetch(); + $this->assertTrue($obj->isKeyDirty('content')); + } + + public function testAddWithAnObject() + { + $parent = ParseObject::create("Person"); + $child = ParseObject::create("Person"); + $child->save(); + $parent->add("children", array($child)); + $parent->save(); + + $query = new ParseQuery("Person"); + $parentAgain = $query->get($parent->getObjectId()); + $children = $parentAgain->get("children"); + $this->assertEquals( + $child->getObjectId(), $children[0]->getObjectId() + ); + } + + public function testAddUnique() + { + $obj = ParseObject::create("TestObject"); + $obj->setArray('arr', [1, 2, 3]); + $obj->addUnique('arr', [1]); + $this->assertEquals(3, count($obj->get('arr'))); + $obj->addUnique('arr', [4]); + $this->assertEquals(4, count($obj->get('arr'))); + + $obj->save(); + $obj2 = ParseObject::create("TestObject"); + $obj3 = ParseObject::create("TestObject"); + $obj2->save(); + $obj3->save(); + + $obj4 = ParseObject::create("TestObject"); + $obj4->setArray('parseObjects', [$obj, $obj2]); + $obj4->save(); + $obj4->addUnique('parseObjects', [$obj3]); + $this->assertEquals(3, count($obj4->get('parseObjects'))); + $obj4->addUnique('parseObjects', [$obj2]); + $this->assertEquals(3, count($obj4->get('parseObjects'))); + } + + public function testToJSONSavedObject() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $json = $obj->_encode(); + $decoded = json_decode($json); + $this->assertTrue(isset($decoded->objectId)); + $this->assertTrue(isset($decoded->createdAt)); + $this->assertTrue(isset($decoded->updatedAt)); + $this->assertTrue(isset($decoded->foo)); + } + + public function testToJSONUnsavedObject() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $json = $obj->_encode(); + $decoded = json_decode($json); + $this->assertFalse(isset($decoded->objectId)); + $this->assertFalse(isset($decoded->createdAt)); + $this->assertFalse(isset($decoded->updatedAt)); + $this->assertTrue(isset($decoded->foo)); + } + + public function testRemoveOperation() + { + $obj = ParseObject::create('TestObject'); + $obj->setArray('arr', [1, 2, 3]); + $obj->save(); + $this->assertEquals(3, count($obj->get('arr'))); + $obj->remove('arr', 1); + $this->assertEquals(2, count($obj->get('arr'))); + $obj->remove('arr', 1); + $obj->save(); + $query = new ParseQuery("TestObject"); + $objAgain = $query->get($obj->getObjectId()); + $this->assertEquals(2, count($objAgain->get('arr'))); + $objAgain->remove('arr', 2); + $this->assertEquals(1, count($objAgain->get('arr'))); + } + + public function testRemoveOperationWithParseObjects() + { + $o1 = ParseObject::create('TestObject'); + $o2 = ParseObject::create('TestObject'); + $o3 = ParseObject::create('TestObject'); + ParseObject::saveAll([$o1, $o2, $o3]); + $obj = ParseObject::create('TestObject'); + $obj->setArray('objs', [$o1, $o2, $o3]); + $obj->save(); + $this->assertEquals(3, count($obj->get('objs'))); + $obj->remove('objs', $o3); + $this->assertEquals(2, count($obj->get('objs'))); + $obj->remove('objs', $o3); + $obj->save(); + $query = new ParseQuery("TestObject"); + $objAgain = $query->get($obj->getObjectId()); + $this->assertEquals(2, count($objAgain->get('objs'))); + $objAgain->remove('objs', $o2); + $this->assertEquals(1, count($objAgain->get('objs'))); + } + + public function testDestroyAll() + { + ParseTestHelper::clearClass("TestObject"); + $o1 = ParseObject::create('TestObject'); + $o2 = ParseObject::create('TestObject'); + $o3 = ParseObject::create('TestObject'); + ParseObject::saveAll([$o1, $o2, $o3]); + ParseObject::destroyAll([$o1, $o2, $o3]); + $query = new ParseQuery("TestObject"); + $results = $query->find(); + $this->assertEquals(0, count($results)); + } + + public function testEmptyArray() + { + $obj = ParseObject::create('TestObject'); + $obj->setArray('baz', array()); + $obj->save(); + $query = new ParseQuery('TestObject'); + $returnedObject = $query->get($obj->getObjectId()); + $this->assertTrue(is_array($returnedObject->get('baz')), + 'Value was not stored as an array.'); + $this->assertEquals(0, count($returnedObject->get('baz'))); + } + + public function testArraySetAndAdd() + { + $obj = ParseObject::create('TestObject'); + $obj->setArray('arrayfield', array('a', 'b')); + $obj->save(); + $obj->add('arrayfield', array('c', 'd', 'e')); + $obj->save(); + } + + public function testObjectIsDirty() + { + $obj = ParseObject::create('Gogo'); + $key1 = 'awesome'; + $key2 = 'great'; + $key3 = 'arrayKey'; + $value1 = 'very true'; + $value2 = true; + + $obj->set($key1, $value1); + $this->assertTrue($obj->isKeyDirty($key1)); + $this->assertFalse($obj->isKeyDirty($key2)); + $this->assertTrue($obj->isDirty()); + + $obj->save(); + $this->assertFalse($obj->isKeyDirty($key1)); + $this->assertFalse($obj->isKeyDirty($key2)); + $this->assertFalse($obj->isDirty()); + + $obj->set($key2, $value2); + $this->assertTrue($obj->isKeyDirty($key2)); + $this->assertFalse($obj->isKeyDirty($key1)); + $this->assertTrue($obj->isDirty()); + + $query = new ParseQuery('Gogo'); + $queriedObj = $query->get($obj->getObjectId()); + $this->assertEquals($value1, $queriedObj->get($key1)); + $this->assertFalse($queriedObj->get($key2) === $value2); + + // check dirtiness of queried item + $this->assertFalse($queriedObj->isKeyDirty($key1)); + $this->assertFalse($queriedObj->isKeyDirty($key2)); + $this->assertFalse($queriedObj->isDirty()); + + $obj->save(); + $queriedObj = $query->get($obj->getObjectId()); + $this->assertEquals($value1, $queriedObj->get($key1)); + $this->assertEquals($value2, $queriedObj->get($key2)); + $this->assertFalse($queriedObj->isKeyDirty($key1)); + $this->assertFalse($queriedObj->isKeyDirty($key2)); + $this->assertFalse($queriedObj->isDirty()); + + // check array + $obj->add($key3, array($value1, $value2, $value1)); + $this->assertTrue($obj->isDirty()); + + $obj->save(); + $this->assertFalse($obj->isDirty()); + } + + public function testObjectIsDirtyWithChildren() + { + $obj = ParseObject::create('Sito'); + $key = 'testKey'; + $childKey = 'testChildKey'; + $childSimultaneousKey = 'testChildKeySimultaneous'; + $value = 'someRandomValue'; + $child = ParseObject::create('Sito'); + $childSimultaneous = ParseObject::create('Sito'); + $childArray1 = ParseObject::create('Sito'); + $childArray2 = ParseObject::create('Sito'); + + $child->set('randomKey', 'randomValue'); + $this->assertTrue($child->isDirty()); + + $obj->set($key, $value); + $this->assertTrue($obj->isDirty()); + + $obj->save(); + $this->assertFalse($obj->isDirty()); + + $obj->set($childKey, $child); + $this->assertTrue($obj->isKeyDirty($childKey)); + $this->assertTrue($obj->isDirty()); + + // check when child is saved, parent should still be dirty + $child->save(); + $this->assertFalse($child->isDirty()); + $this->assertTrue($obj->isDirty()); + + $obj->save(); + $this->assertFalse($child->isDirty()); + $this->assertFalse($obj->isDirty()); + + $childSimultaneous->set('randomKey', 'randomValue'); + $obj->set($childSimultaneousKey, $childSimultaneous); + $this->assertTrue($obj->isDirty()); + + // check case with array + $childArray1->set('random', 'random2'); + $obj->add('arrayKey', array($childArray1, $childArray2)); + $this->assertTrue($obj->isDirty()); + $childArray1->save(); + $childArray2->save(); + $this->assertFalse($childArray1->getObjectId() === null); + $this->assertFalse($childArray2->getObjectId() === null); + $this->assertFalse($obj->getObjectId() === null); + $this->assertTrue($obj->isDirty()); + $obj->save(); + $this->assertFalse($obj->isDirty()); + + // check simultaneous save + $obj->save(); + $this->assertFalse($obj->isDirty()); + $this->assertFalse($childSimultaneous->isDirty()); + } + + public function testSaveAll() + { + ParseTestHelper::clearClass("TestObject"); + $objs = array(); + for ($i = 1; $i <= 90; $i++) { + $obj = ParseObject::create('TestObject'); + $obj->set('test', 'test'); + $objs[] = $obj; + } + ParseObject::saveAll($objs); + $query = new ParseQuery('TestObject'); + $result = $query->find(); + $this->assertEquals(90, count($result)); + } + + public function testEmptyObjectsAndArrays() + { + $obj = ParseObject::create('TestObject'); + $obj->setArray('arr', array()); + $obj->setAssociativeArray('obj', array()); + $saveOpArray = new SetOperation(array()); + $saveOpAssoc = new SetOperation(array(), true); + $this->assertTrue( + is_array($saveOpArray->_encode()), "Value should be array." + ); + $this->assertTrue( + is_object($saveOpAssoc->_encode()), "Value should be object." + ); + $obj->save(); + $obj->setAssociativeArray('obj', array( + 'foo' => 'bar', + 'baz' => 'yay' + )); + $obj->save(); + $query = new ParseQuery('TestObject'); + $objAgain = $query->get($obj->getObjectId()); + $this->assertTrue(is_array($objAgain->get('arr'))); + $this->assertTrue(is_array($objAgain->get('obj'))); + $this->assertEquals('bar', $objAgain->get('obj')['foo']); + $this->assertEquals('yay', $objAgain->get('obj')['baz']); + } + + public function testDatetimeHandling() + { + $date = new DateTime('2014-04-30T12:34:56.789Z'); + $obj = ParseObject::create('TestObject'); + $obj->set('f8', $date); + $obj->save(); + $query = new ParseQuery('TestObject'); + $objAgain = $query->get($obj->getObjectId()); + $dateAgain = $objAgain->get('f8'); + $this->assertTrue($date->getTimestamp() == $dateAgain->getTimestamp()); + } + + public function testBatchSaveExceptions() + { + $obj1 = ParseObject::create("TestObject"); + $obj2 = ParseObject::create("TestObject"); + $obj1->set("fos^^co", "hi"); + $obj2->set("fo^^mo", "hi"); + try { + ParseObject::saveAll([$obj1, $obj2]); + $this->fail("Save should have failed."); + } catch (\Parse\ParseAggregateException $ex) { + $errors = $ex->getErrors(); + $this->assertContains("invalid field name", $errors[0]['error']); + $this->assertContains("invalid field name", $errors[1]['error']); + } + } + +} diff --git a/tests/ParsePushTest.php b/tests/ParsePushTest.php new file mode 100644 index 00000000..d58066e0 --- /dev/null +++ b/tests/ParsePushTest.php @@ -0,0 +1,49 @@ + array(''), + 'data' => array('alert' => 'sample message') + )); + } + + public function testPushToQuery() + { + $query = ParseInstallation::query(); + $query->equalTo('key', 'value'); + ParsePush::send(array( + 'data' => array('alert' => 'iPhone 5 is out!'), + 'where' => $query + )); + } + + public function testPushDates() + { + ParsePush::send(array( + 'data' => array('alert' => 'iPhone 5 is out!'), + 'push_time' => new DateTime(), + 'expiration_time' => new DateTime(), + 'channels' => array() + )); + } +} \ No newline at end of file diff --git a/tests/ParseQueryTest.php b/tests/ParseQueryTest.php new file mode 100644 index 00000000..71a23e73 --- /dev/null +++ b/tests/ParseQueryTest.php @@ -0,0 +1,1520 @@ +saveObjects($numberOfObjects, function ($i) { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar' . $i); + return $obj; + }); + } + + public function testBasicQuery() + { + $baz = new ParseObject("TestObject"); + $baz->set("foo", "baz"); + $qux = new ParseObject("TestObject"); + $qux->set("foo", "qux"); + $baz->save(); + $qux->save(); + + $query = new ParseQuery("TestObject"); + $query->equalTo("foo", "baz"); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not find object.'); + $this->assertEquals("baz", $results[0]->get("foo"), + 'Did not return the correct object.'); + } + + public function testQueryWithLimit() + { + $baz = new ParseObject("TestObject"); + $baz->set("foo", "baz"); + $qux = new ParseObject("TestObject"); + $qux->set("foo", "qux"); + $baz->save(); + $qux->save(); + + $query = new ParseQuery("TestObject"); + $query->limit(1); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + } + + public function testEqualTo() + { + $obj = ParseObject::create('TestObject'); + $obj->set('foo', 'bar'); + $obj->save(); + $query = new ParseQuery('TestObject'); + $query->equalTo('objectId', $obj->getObjectId()); + $results = $query->find(); + $this->assertTrue(count($results) == 1, 'Did not find object.'); + } + + public function testNotEqualTo() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->notEqualTo('foo', 'bar9'); + $results = $query->find(); + $this->assertEquals(count($results), 9, + 'Did not find 9 objects, found ' . count($results)); + } + + public function testLessThan() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->lessThan('foo', 'bar1'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'LessThan function did not return correct number of objects.'); + $this->assertEquals($results[0]->get('foo'), 'bar0', + 'LessThan function did not return the correct object'); + } + + public function testLessThanOrEqualTo() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->lessThanOrEqualTo('foo', 'bar0'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'LessThanOrEqualTo function did not return correct number of objects.'); + $this->assertEquals($results[0]->get('foo'), 'bar0', + 'LessThanOrEqualTo function did not return the correct object.'); + } + + public function testStartsWithSingle() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'bar0'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'StartsWith function did not return correct number of objects.'); + $this->assertEquals($results[0]->get('foo'), 'bar0', + 'StartsWith function did not return the correct object.'); + } + + public function testStartsWithMultiple() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'bar'); + $results = $query->find(); + $this->assertEquals(count($results), 10, + 'StartsWith function did not return correct number of objects.'); + } + + public function testStartsWithMiddle() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'ar'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + } + + public function testStartsWithRegexDelimiters() + { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "foob\E"); + $testObject->save(); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'foob\E'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'StartsWith function did not return correct number of objects.'); + $query->startsWith('foo', 'foobE'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + } + + public function testStartsWithRegexDot() + { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "foobarfoo"); + $testObject->save(); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'foo(.)*'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + $query->startsWith('foo', 'foo.*'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + $query->startsWith('foo', 'foo'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'StartsWith function did not return correct number of objects.'); + } + + public function testStartsWithRegexSlash() + { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "foobarfoo"); + $testObject->save(); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'foo/bar'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + $query->startsWith('foo', 'foobar'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'StartsWith function did not return correct number of objects.'); + } + + public function testStartsWithRegexQuestionmark() + { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "foobarfoo"); + $testObject->save(); + $query = new ParseQuery('TestObject'); + $query->startsWith('foo', 'foox?bar'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + $query->startsWith('foo', 'foo(x)?bar'); + $results = $query->find(); + $this->assertEquals(count($results), 0, + 'StartsWith function did not return correct number of objects.'); + $query->startsWith('foo', 'foobar'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'StartsWith function did not return correct number of objects.'); + } + + public function testGreaterThan() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->greaterThan('foo', 'bar8'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'GreaterThan function did not return correct number of objects.'); + $this->assertEquals($results[0]->get('foo'), 'bar9', + 'GreaterThan function did not return the correct object.'); + } + + public function testGreaterThanOrEqualTo() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->greaterThanOrEqualTo('foo', 'bar9'); + $results = $query->find(); + $this->assertEquals(count($results), 1, + 'GreaterThanOrEqualTo function did not return correct number of objects.'); + $this->assertEquals($results[0]->get('foo'), 'bar9', + 'GreaterThanOrEqualTo function did not return the correct object.'); + } + + public function testLessThanOrEqualGreaterThanOrEqual() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->lessThanOrEqualTo('foo', 'bar4'); + $query->greaterThanOrEqualTo('foo', 'bar2'); + $results = $query->find(); + $this->assertEquals(3, count($results), + 'LessThanGreaterThan did not return correct number of objects.'); + } + + public function testLessThanGreaterThan() + { + $this->provideTestObjects(10); + $query = new ParseQuery('TestObject'); + $query->lessThan('foo', 'bar5'); + $query->greaterThan('foo', 'bar3'); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'LessThanGreaterThan did not return correct number of objects.'); + $this->assertEquals('bar4', $results[0]->get('foo'), + 'LessThanGreaterThan did not return the correct object.'); + } + + public function testObjectIdEqualTo() + { + ParseTestHelper::clearClass("BoxedNumber"); + $boxedNumberArray = array(); + $this->saveObjects(5, function ($i) use (&$boxedNumberArray) { + $boxedNumber = new ParseObject("BoxedNumber"); + $boxedNumber->set("number", $i); + $boxedNumberArray[] = $boxedNumber; + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->equalTo("objectId", $boxedNumberArray[4]->getObjectId()); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not find object.'); + $this->assertEquals(4, $results[0]->get("number"), + 'Did not return the correct object.'); + } + + public function testFindNoElements() + { + ParseTestHelper::clearClass("BoxedNumber"); + $this->saveObjects(5, function ($i) { + $boxedNumber = new ParseObject("BoxedNumber"); + $boxedNumber->set("number", $i); + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->equalTo("number", 17); + $results = $query->find(); + $this->assertEquals(0, count($results), + 'Did not return correct number of objects.'); + } + + public function testFindWithError() + { + $query = new ParseQuery("TestObject"); + $this->setExpectedException('Parse\ParseException', 'Invalid key', 102); + $query->equalTo('$foo', 'bar'); + $query->find(); + } + + public function testGet() + { + $testObj = ParseObject::create("TestObject"); + $testObj->set("foo", "bar"); + $testObj->save(); + $query = new ParseQuery("TestObject"); + $result = $query->get($testObj->getObjectId()); + $this->assertEquals($testObj->getObjectId(), $result->getObjectId(), + 'Did not return the correct object.'); + $this->assertEquals("bar", $result->get("foo"), + 'Did not return the correct object.'); + } + + public function testGetError() + { + $obj = ParseObject::create("TestObject"); + $obj->set('foo', 'bar'); + $obj->save(); + $query = new ParseQuery("TestObject"); + $this->setExpectedException('Parse\ParseException', 'Object not found', 101); + $query->get("InvalidObjectID"); + } + + public function testGetNull() + { + $obj = ParseObject::create("TestObject"); + $obj->set('foo', 'bar'); + $obj->save(); + $query = new ParseQuery("TestObject"); + $this->setExpectedException('Parse\ParseException', 'Object not found', 101); + $query->get(null); + } + + public function testFirst() + { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "bar"); + $testObject->save(); + $query = new ParseQuery("TestObject"); + $query->equalTo("foo", "bar"); + $result = $query->first(); + $this->assertEquals("bar", $result->get("foo"), + 'Did not return the correct object.'); + } + + public function testFirstWithError() + { + $query = new ParseQuery("TestObject"); + $query->equalTo('$foo', 'bar'); + $this->setExpectedException('Parse\ParseException', 'Invalid key', 102); + $query->first(); + } + + public function testFirstNoResult() + { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "bar"); + $testObject->save(); + $query = new ParseQuery("TestObject"); + $query->equalTo("foo", "baz"); + $result = $query->first(); + $this->assertTrue(empty($result), + 'Did not return correct number of objects.'); + } + + public function testFirstWithTwoResults() + { + $this->saveObjects(2, function ($i) { + $testObject = ParseObject::create("TestObject"); + $testObject->set("foo", "bar"); + return $testObject; + }); + $query = new ParseQuery("TestObject"); + $query->equalTo("foo", "bar"); + $result = $query->first(); + $this->assertEquals("bar", $result->get("foo"), + 'Did not return the correct object.'); + } + + public function testNotEqualToObject() + { + ParseTestHelper::clearClass("Container"); + ParseTestHelper::clearClass("Item"); + $items = array(); + $this->saveObjects(2, function ($i) use (&$items) { + $items[] = ParseObject::create("Item"); + return $items[$i]; + }); + $this->saveObjects(2, function ($i) use ($items) { + $container = ParseObject::create("Container"); + $container->set("item", $items[$i]); + return $container; + }); + $query = new ParseQuery("Container"); + $query->notEqualTo("item", $items[0]); + $result = $query->find(); + $this->assertEquals(1, count($result), + 'Did not return the correct object.'); + } + + public function testSkip() + { + $this->saveObjects(2, function ($i) { + return ParseObject::create("TestObject"); + }); + $query = new ParseQuery("TestObject"); + $query->skip(1); + $result = $query->find(); + $this->assertEquals(1, count($result), + 'Did not return the correct object.'); + $query->skip(3); + $result = $query->find(); + $this->assertEquals(0, count($result), + 'Did not return the correct object.'); + } + + public function testSkipDoesNotAffectCount() + { + $this->saveObjects(2, function ($i) { + return ParseObject::create("TestObject"); + }); + $query = new ParseQuery("TestObject"); + $count = $query->count(); + $this->assertEquals(2, $count, + 'Did not return correct number of objects.'); + $query->skip(1); + $this->assertEquals(2, $count, + 'Did not return correct number of objects.'); + $query->skip(3); + $this->assertEquals(2, $count, + 'Did not return correct number of objects.'); + } + + public function testCount() + { + ParseTestHelper::clearClass("BoxedNumber"); + $this->saveObjects(3, function ($i) { + $boxedNumber = ParseObject::create("BoxedNumber"); + $boxedNumber->set("x", $i + 1); + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->greaterThan("x", 1); + $count = $query->count(); + $this->assertEquals(2, $count, + 'Did not return correct number of objects.'); + } + + public function testCountError() + { + $query = new ParseQuery("Test"); + $query->equalTo('$foo', "bar"); + $this->setExpectedException('Parse\ParseException', 'Invalid key', 102); + $query->count(); + } + + public function testOrderByAscendingNumber() + { + ParseTestHelper::clearClass("BoxedNumber"); + $numbers = [3, 1, 2]; + $this->saveObjects(3, function ($i) use ($numbers) { + $boxedNumber = ParseObject::create("BoxedNumber"); + $boxedNumber->set("number", $numbers[$i]); + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->ascending("number"); + $results = $query->find(); + $this->assertEquals(3, count($results), + 'Did not return correct number of objects.'); + for ($i = 0; $i < 3; $i++) { + $this->assertEquals($i + 1, $results[$i]->get("number"), + 'Did not return the correct object.'); + } + } + + public function testOrderByDescendingNumber() + { + ParseTestHelper::clearClass("BoxedNumber"); + $numbers = [3, 1, 2]; + $this->saveObjects(3, function ($i) use ($numbers) { + $boxedNumber = ParseObject::create("BoxedNumber"); + $boxedNumber->set("number", $numbers[$i]); + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->descending("number"); + $results = $query->find(); + $this->assertEquals(3, count($results), + 'Did not return correct number of objects.'); + for ($i = 0; $i < 3; $i++) { + $this->assertEquals(3 - $i, $results[$i]->get("number"), + 'Did not return the correct object.'); + } + } + + public function provideTestObjectsForQuery($numberOfObjects) + { + $this->saveObjects($numberOfObjects, function ($i) { + $parent = ParseObject::create("ParentObject"); + $child = ParseObject::create("ChildObject"); + $child->set("x", $i); + $parent->set("x", 10 + $i); + $parent->set("child", $child); + return $parent; + }); + } + + public function testMatchesQuery() + { + ParseTestHelper::clearClass("ChildObject"); + ParseTestHelper::clearClass("ParentObject"); + $this->provideTestObjectsForQuery(10); + $subQuery = new ParseQuery("ChildObject"); + $subQuery->greaterThan("x", 5); + $query = new ParseQuery("ParentObject"); + $query->matchesQuery("child", $subQuery); + $results = $query->find(); + + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + foreach ($results as $parentObj) { + $this->assertGreaterThan(15, $parentObj->get("x"), + 'Did not return the correct object.'); + } + } + + public function testDoesNotMatchQuery() + { + ParseTestHelper::clearClass("ChildObject"); + ParseTestHelper::clearClass("ParentObject"); + $this->provideTestObjectsForQuery(10); + $subQuery = new ParseQuery("ChildObject"); + $subQuery->greaterThan("x", 5); + $query = new ParseQuery("ParentObject"); + $query->doesNotMatchQuery("child", $subQuery); + $results = $query->find(); + + $this->assertEquals(6, count($results), + 'Did not return the correct object.'); + foreach ($results as $parentObj) { + $this->assertLessThanOrEqual(15, $parentObj->get("x"), + 'Did not return the correct object.'); + $this->assertGreaterThanOrEqual(10, $parentObj->get("x"), + 'Did not return the correct object.'); + } + } + + public function provideTestObjectsForKeyInQuery() + { + ParseTestHelper::clearClass("Restaurant"); + ParseTestHelper::clearClass("Person"); + $restaurantLocations = ["Djibouti", "Ouagadougou"]; + $restaurantRatings = [5, 3]; + $numberOFRestaurantObjects = count($restaurantLocations); + + $personHomeTown = ["Djibouti", "Ouagadougou", "Detroit"]; + $personName = ["Bob", "Tom", "Billy"]; + $numberOfPersonObjects = count($personHomeTown); + + $this->saveObjects($numberOFRestaurantObjects, function ($i) use ($restaurantRatings, $restaurantLocations) { + $restaurant = ParseObject::create("Restaurant"); + $restaurant->set("ratings", $restaurantRatings[$i]); + $restaurant->set("location", $restaurantLocations[$i]); + return $restaurant; + }); + + $this->saveObjects($numberOfPersonObjects, function ($i) use ($personHomeTown, $personName) { + $person = ParseObject::create("Person"); + $person->set("hometown", $personHomeTown[$i]); + $person->set("name", $personName[$i]); + return $person; + }); + } + + public function testMatchesKeyInQuery() + { + $this->provideTestObjectsForKeyInQuery(); + $subQuery = new ParseQuery("Restaurant"); + $subQuery->greaterThan("ratings", 4); + + $query = new ParseQuery("Person"); + $query->matchesKeyInQuery("hometown", "location", $subQuery); + $results = $query->find(); + + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + $this->assertEquals("Bob", $results[0]->get("name"), + 'Did not return the correct object.'); + } + + public function testDoesNotMatchKeyInQuery() + { + $this->provideTestObjectsForKeyInQuery(); + $subQuery = new ParseQuery("Restaurant"); + $subQuery->greaterThanOrEqualTo("ratings", 3); + + $query = new ParseQuery("Person"); + $query->doesNotmatchKeyInQuery("hometown", "location", $subQuery); + $results = $query->find(); + + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + $this->assertEquals("Billy", $results[0]->get("name"), + 'Did not return the correct object.'); + } + + public function testOrQueries() + { + $this->provideTestObjects(10); + $subQuery1 = new ParseQuery("TestObject"); + $subQuery1->lessThan("foo", "bar2"); + $subQuery2 = new ParseQuery("TestObject"); + $subQuery2->greaterThan("foo", "bar5"); + + $mainQuery = ParseQuery::orQueries([$subQuery1, $subQuery2]); + $results = $mainQuery->find(); + $length = count($results); + $this->assertEquals(6, $length, + 'Did not return correct number of objects.'); + for ($i = 0; $i < $length; $i++) { + $this->assertTrue($results[$i]->get("foo") < "bar2" || + $results[$i]->get("foo") > "bar5", + 'Did not return the correct object.'); + } + } + + public function testComplexQueries() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $this->saveObjects(10, function ($i) { + $child = new ParseObject("Child"); + $child->set("x", $i); + $parent = new ParseObject("Parent"); + $parent->set("y", $i); + $parent->set("child", $child); + return $parent; + }); + $subQuery = new ParseQuery("Child"); + $subQuery->equalTo("x", 4); + $query1 = new ParseQuery("Parent"); + $query1->matchesQuery("child", $subQuery); + $query2 = new ParseQuery("Parent"); + $query2->lessThan("y", 2); + + $orQuery = ParseQuery::orQueries([$query1, $query2]); + $results = $orQuery->find(); + $this->assertEquals(3, count($results), + 'Did not return correct number of objects.'); + } + + public function testEach() + { + ParseTestHelper::clearClass("Object"); + $total = 50; + $count = 25; + $this->saveObjects($total, function ($i) { + $obj = new ParseObject("Object"); + $obj->set("x", $i + 1); + return $obj; + }); + $query = new ParseQuery("Object"); + $query->lessThanOrEqualTo("x", $count); + + $values = array(); + $query->each(function ($obj) use (&$values) { + $values[] = $obj->get("x"); + }, 10); + + $valuesLength = count($values); + $this->assertEquals($count, $valuesLength, + 'Did not return correct number of objects.'); + sort($values); + for ($i = 0; $i < $valuesLength; $i++) { + $this->assertEquals($i + 1, $values[$i], + 'Did not return the correct object.'); + } + } + + public function testEachFailsWithOrder() + { + ParseTestHelper::clearClass("Object"); + $total = 50; + $count = 25; + $this->saveObjects($total, function ($i) { + $obj = new ParseObject("Object"); + $obj->set("x", $i + 1); + return $obj; + }); + $query = new ParseQuery("Object"); + $query->lessThanOrEqualTo("x", $count); + $query->ascending("x"); + $this->setExpectedException('\Exception', 'sort'); + $query->each(function ($obj) { + }); + } + + public function testEachFailsWithSkip() + { + $total = 50; + $count = 25; + $this->saveObjects($total, function ($i) { + $obj = new ParseObject("Object"); + $obj->set("x", $i + 1); + return $obj; + }); + $query = new ParseQuery("Object"); + $query->lessThanOrEqualTo("x", $count); + $query->skip(5); + $this->setExpectedException('\Exception', 'skip'); + $query->each(function ($obj) { + }); + } + + public function testEachFailsWithLimit() + { + $total = 50; + $count = 25; + $this->saveObjects($total, function ($i) { + $obj = new ParseObject("Object"); + $obj->set("x", $i + 1); + return $obj; + }); + $query = new ParseQuery("Object"); + $query->lessThanOrEqualTo("x", $count); + $query->limit(5); + $this->setExpectedException('\Exception', 'limit'); + $query->each(function ($obj) { + }); + } + + public function testContainsAllNumberArrayQueries() + { + ParseTestHelper::clearClass("NumberSet"); + $numberSet1 = new ParseObject("NumberSet"); + $numberSet1->setArray("numbers", [1, 2, 3, 4, 5]); + $numberSet2 = new ParseObject("NumberSet"); + $numberSet2->setArray("numbers", [1, 3, 4, 5]); + $numberSet1->save(); + $numberSet2->save(); + + $query = new ParseQuery("NumberSet"); + $query->containsAll("numbers", [1, 2, 3]); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + } + + public function testContainsAllStringArrayQueries() + { + ParseTestHelper::clearClass("StringSet"); + $stringSet1 = new ParseObject("StringSet"); + $stringSet1->setArray("strings", ["a", "b", "c", "d", "e"]); + $stringSet1->save(); + $stringSet2 = new ParseObject("StringSet"); + $stringSet2->setArray("strings", ["a", "c", "d", "e"]); + $stringSet2->save(); + + $query = new ParseQuery("StringSet"); + $query->containsAll("strings", ["a", "b", "c"]); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + } + + public function testContainsAllDateArrayQueries() + { + ParseTestHelper::clearClass("DateSet"); + $dates1 = array( + new DateTime("2013-02-01T00:00:00Z"), + new DateTime("2013-02-02T00:00:00Z"), + new DateTime("2013-02-03T00:00:00Z"), + new DateTime("2013-02-04T00:00:00Z") + ); + $dates2 = array( + new DateTime("2013-02-01T00:00:00Z"), + new DateTime("2013-02-03T00:00:00Z"), + new DateTime("2013-02-04T00:00:00Z") + ); + + $obj1 = ParseObject::create("DateSet"); + $obj1->setArray("dates", $dates1); + $obj1->save(); + $obj2 = ParseObject::create("DateSet"); + $obj2->setArray("dates", $dates2); + $obj2->save(); + + $query = new ParseQuery("DateSet"); + $query->containsAll("dates", array( + new DateTime("2013-02-01T00:00:00Z"), + new DateTime("2013-02-02T00:00:00Z"), + new DateTime("2013-02-03T00:00:00Z") + )); + $result = $query->find(); + $this->assertEquals(1, count($result), + 'Did not return correct number of objects.'); + } + + public function testContainsAllObjectArrayQueries() + { + ParseTestHelper::clearClass("MessageSet"); + $messageList = array(); + $this->saveObjects(4, function ($i) use (&$messageList) { + $messageList[] = ParseObject::create("TestObject"); + $messageList[$i]->set("i", $i); + return $messageList[$i]; + }); + $messageSet1 = ParseObject::create("MessageSet"); + $messageSet1->setArray("messages", $messageList); + $messageSet1->save(); + $messageSet2 = ParseObject::create("MessageSet"); + $messageSet2->setArray("message", + array($messageList[0], $messageList[1], $messageList[3]) + ); + $messageSet2->save(); + + $query = new ParseQuery("MessageSet"); + $query->containsAll("messages", array($messageList[0], $messageList[2])); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + } + + public function testContainedInObjectArrayQueries() + { + $messageList = array(); + $this->saveObjects(4, function ($i) use (&$messageList) { + $message = ParseObject::create("TestObject"); + if ($i > 0) { + $message->set("prior", $messageList[$i - 1]); + } + $messageList[] = $message; + return $message; + }); + $query = new ParseQuery("TestObject"); + $query->containedIn("prior", array($messageList[0], $messageList[2])); + $results = $query->find(); + $this->assertEquals(2, count($results), + 'Did not return correct number of objects.'); + } + + public function testContainedInQueries() + { + ParseTestHelper::clearClass("BoxedNumber"); + $this->saveObjects(10, function ($i) { + $boxedNumber = ParseObject::create("BoxedNumber"); + $boxedNumber->set("number", $i); + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->containedIn("number", [3, 5, 7, 9, 11]); + $results = $query->find(); + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + } + + public function testNotContainedInQueries() + { + ParseTestHelper::clearClass("BoxedNumber"); + $this->saveObjects(10, function ($i) { + $boxedNumber = ParseObject::create("BoxedNumber"); + $boxedNumber->set("number", $i); + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->notContainedIn("number", [3, 5, 7, 9, 11]); + $results = $query->find(); + $this->assertEquals(6, count($results), + 'Did not return correct number of objects.'); + } + + public function testObjectIdContainedInQueries() + { + ParseTestHelper::clearClass("BoxedNumber"); + $objects = array(); + $this->saveObjects(5, function ($i) use (&$objects) { + $boxedNumber = ParseObject::create("BoxedNumber"); + $boxedNumber->set("number", $i); + $objects[] = $boxedNumber; + return $boxedNumber; + }); + $query = new ParseQuery("BoxedNumber"); + $query->containedIn("objectId", [$objects[2]->getObjectId(), + $objects[3]->getObjectId(), + $objects[0]->getObjectId(), + "NONSENSE"] + ); + $query->ascending("number"); + $results = $query->find(); + $this->assertEquals(3, count($results), + 'Did not return correct number of objects.'); + $this->assertEquals(0, $results[0]->get("number"), + 'Did not return the correct object.'); + $this->assertEquals(2, $results[1]->get("number"), + 'Did not return the correct object.'); + $this->assertEquals(3, $results[2]->get("number"), + 'Did not return the correct object.'); + } + + public function testStartsWith() + { + $someAscii = "\\E' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU" . + "VWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'"; + $prefixes = ['zax', 'start', '', '']; + $suffixes = ['qub', '', 'end', '']; + $this->saveObjects(4, function ($i) use ($prefixes, $suffixes, $someAscii) { + $obj = ParseObject::create("TestObject"); + $obj->set("myString", $prefixes[$i] . $someAscii . $suffixes[$i]); + return $obj; + }); + $query = new ParseQuery("TestObject"); + $query->startsWith("myString", $someAscii); + $results = $query->find(); + $this->assertEquals(2, count($results), + 'Did not return correct number of objects.'); + } + + public function provideTestObjectsForOrderBy() + { + ParseTestHelper::clearClass("BoxedNumber"); + $strings = ['a', 'b', 'c', 'd']; + $numbers = [3, 1, 3, 2]; + for ($i = 0; $i < 4; $i++) { + $obj = ParseObject::create("BoxedNumber"); + $obj->set('string', $strings[$i]); + $obj->set('number', $numbers[$i]); + $obj->save(); + } + } + + public function testOrderByAscNumberThenDescString() + { + $this->provideTestObjectsForOrderBy(); + $query = new ParseQuery("BoxedNumber"); + $query->ascending('number')->addDescending('string'); + $results = $query->find(); + $expected = [[1, 'b'], [2, 'd'], [3, 'c'], [3, 'a']]; + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + for ($i = 0; $i < 4; $i++) { + $this->assertEquals($expected[$i][0], $results[$i]->get('number'), + 'Did not return the correct object.'); + $this->assertEquals($expected[$i][1], $results[$i]->get('string'), + 'Did not return the correct object.'); + } + } + + public function testOrderByDescNumberThenAscString() + { + $this->provideTestObjectsForOrderBy(); + $query = new ParseQuery("BoxedNumber"); + $query->descending('number')->addAscending('string'); + $results = $query->find(); + $expected = [[3, 'a'], [3, 'c'], [2, 'd'], [1, 'b']]; + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + for ($i = 0; $i < 4; $i++) { + $this->assertEquals($expected[$i][0], $results[$i]->get('number'), + 'Did not return the correct object.'); + $this->assertEquals($expected[$i][1], $results[$i]->get('string'), + 'Did not return the correct object.'); + } + } + + public function testOrderByDescNumberAndString() + { + $this->provideTestObjectsForOrderBy(); + $query = new ParseQuery("BoxedNumber"); + $query->descending(['number', 'string']); + $results = $query->find(); + $expected = [[3, 'c'], [3, 'a'], [2, 'd'], [1, 'b']]; + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + for ($i = 0; $i < 4; $i++) { + $this->assertEquals($expected[$i][0], $results[$i]->get('number'), + 'Did not return the correct object.'); + $this->assertEquals($expected[$i][1], $results[$i]->get('string'), + 'Did not return the correct object.'); + } + } + + public function testCannotOrderByPassword() + { + $this->provideTestObjectsForOrderBy(); + $query = new ParseQuery("BoxedNumber"); + $query->ascending('_password'); + $this->setExpectedException('Parse\ParseException', "", 105); + $query->find(); + } + + public function testOrderByCreatedAtAsc() + { + $this->provideTestObjectsForOrderBy(); + $query = new ParseQuery("BoxedNumber"); + $query->ascending('createdAt'); + $query->find(); + $results = $query->find(); + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + $expected = [3, 1, 3, 2]; + for ($i = 0; $i < 4; $i++) { + $this->assertEquals($expected[$i], $results[$i]->get('number'), + 'Did not return the correct object.'); + } + } + + public function testOrderByCreatedAtDesc() + { + $this->provideTestObjectsForOrderBy(); + $query = new ParseQuery("BoxedNumber"); + $query->descending('createdAt'); + $query->find(); + $results = $query->find(); + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + $expected = [2, 3, 1, 3]; + for ($i = 0; $i < 4; $i++) { + $this->assertEquals($expected[$i], $results[$i]->get('number'), + 'Did not return the correct object.'); + } + } + + public function testOrderByUpdatedAtAsc() + { + $numbers = [3, 1, 2]; + $objects = array(); + $this->saveObjects(3, function ($i) use ($numbers, &$objects) { + $obj = ParseObject::create("TestObject"); + $obj->set('number', $numbers[$i]); + $objects[] = $obj; + return $obj; + }); + $objects[1]->set('number', 4); + $objects[1]->save(); + $query = new ParseQuery("TestObject"); + $query->ascending('updatedAt'); + $results = $query->find(); + $this->assertEquals(3, count($results), + 'Did not return correct number of objects.'); + $expected = [3, 2, 4]; + for ($i = 0; $i < 3; $i++) { + $this->assertEquals($expected[$i], $results[$i]->get('number'), + 'Did not return the correct object.'); + } + } + + public function testOrderByUpdatedAtDesc() + { + $numbers = [3, 1, 2]; + $objects = array(); + $this->saveObjects(3, function ($i) use ($numbers, &$objects) { + $obj = ParseObject::create("TestObject"); + $obj->set('number', $numbers[$i]); + $objects[] = $obj; + return $obj; + }); + $objects[1]->set('number', 4); + $objects[1]->save(); + $query = new ParseQuery("TestObject"); + $query->descending('updatedAt'); + $results = $query->find(); + $this->assertEquals(3, count($results), + 'Did not return correct number of objects.'); + $expected = [4, 2, 3]; + for ($i = 0; $i < 3; $i++) { + $this->assertEquals($expected[$i], $results[$i]->get('number'), + 'Did not return the correct object.'); + } + } + + public function testSelectKeysQuery() + { + $obj = ParseObject::create("TestObject"); + $obj->set('foo', 'baz'); + $obj->set('bar', 1); + $obj->save(); + $query = new ParseQuery("TestObject"); + $query->select('foo'); + $result = $query->first(); + $this->assertEquals('baz', $result->get('foo'), + 'Did not return the correct object.'); + $this->setExpectedException('\Exception', 'Call fetch()'); + $result->get('bar'); + + } + + public function testGetWithoutError() + { + $obj = ParseObject::create("TestObject"); + $obj->set('foo', 'baz'); + $obj->set('bar', 1); + $this->assertEquals('baz', $obj->get('foo'), + 'Did not return the correct object.'); + $this->assertEquals(1, $obj->get('bar'), + 'Did not return the correct object.'); + $obj->save(); + } + public function testSelectKeysQueryArrayArg() + { + $obj = ParseObject::create("TestObject"); + $obj->set('foo', 'baz'); + $obj->set('bar', 1); + $obj->save(); + $query = new ParseQuery("TestObject"); + $query->select(['foo', 'bar']); + $result = $query->first(); + $this->assertEquals('baz', $result->get('foo'), + 'Did not return the correct object.'); + $this->assertEquals(1, $result->get('bar'), + 'Did not return the correct object.'); + + } + + public function testExists() + { + $this->saveObjects(9, function ($i) { + $obj = ParseObject::create("TestObject"); + if ($i & 1) { + $obj->set('y', $i); + } else { + $obj->set('x', $i); + } + return $obj; + }); + $query = new ParseQuery("TestObject"); + $query->exists('x'); + $results = $query->find(); + $this->assertEquals(5, count($results), + 'Did not return correct number of objects.'); + } + + public function testDoesNotExist() + { + $this->saveObjects(9, function ($i) { + $obj = ParseObject::create("TestObject"); + if ($i & 1) { + $obj->set('y', $i); + } else { + $obj->set('x', $i); + } + return $obj; + }); + $query = new ParseQuery("TestObject"); + $query->doesNotExist('x'); + $results = $query->find(); + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + } + + public function testExistsRelation() + { + ParseTestHelper::clearClass("Item"); + $this->saveObjects(9, function ($i) { + $obj = ParseObject::create("TestObject"); + if ($i & 1) { + $obj->set('y', $i); + } else { + $item = ParseObject::create("Item"); + $item->set('e', $i); + $obj->set('e', $item); + } + return $obj; + }); + $query = new ParseQuery("TestObject"); + $query->exists('e'); + $results = $query->find(); + $this->assertEquals(5, count($results), + 'Did not return correct number of objects.'); + } + + public function testDoesNotExistRelation() + { + ParseTestHelper::clearClass("Item"); + $this->saveObjects(9, function ($i) { + $obj = ParseObject::create("TestObject"); + if ($i & 1) { + $obj->set('y', $i); + } else { + $item = ParseObject::create("Item"); + $item->set('x', $i); + $obj->set('x', $i); + } + return $obj; + }); + $query = new ParseQuery("TestObject"); + $query->doesNotExist('x'); + $results = $query->find(); + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + } + + public function testDoNotIncludeRelation() + { + $child = ParseObject::create("Child"); + $child->set('x', 1); + $child->save(); + $parent = ParseObject::create("Parent"); + $parent->set('child', $child); + $parent->set('y', 1); + $parent->save(); + $query = new ParseQuery('Parent'); + $result = $query->first(); + $this->setExpectedException('\Exception', 'Call fetch()'); + $result->get('child')->get('x'); + } + + public function testIncludeRelation() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $child = ParseObject::create("Child"); + $child->set('x', 1); + $child->save(); + $parent = ParseObject::create("Parent"); + $parent->set('child', $child); + $parent->set('y', 1); + $parent->save(); + $query = new ParseQuery('Parent'); + $query->includeKey('child'); + $result = $query->first(); + $this->assertEquals($result->get('y'), $result->get('child')->get('x'), + 'Object should be fetched.'); + $this->assertEquals(1, $result->get('child')->get('x'), + 'Object should be fetched.'); + } + + public function testNestedIncludeRelation() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + ParseTestHelper::clearClass("GrandParent"); + $child = ParseObject::create("Child"); + $child->set('x', 1); + $child->save(); + $parent = ParseObject::create("Parent"); + $parent->set('child', $child); + $parent->set('y', 1); + $parent->save(); + $grandParent = ParseObject::create("GrandParent"); + $grandParent->set('parent', $parent); + $grandParent->set('z', 1); + $grandParent->save(); + + $query = new ParseQuery('GrandParent'); + $query->includeKey('parent.child'); + $result = $query->first(); + $this->assertEquals($result->get('z'), $result->get('parent')->get('y'), + 'Object should be fetched.'); + $this->assertEquals($result->get('z'), + $result->get('parent')->get('child')->get('x'), + 'Object should be fetched.'); + } + + public function testIncludeArrayRelation() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $children = array(); + $this->saveObjects(5, function ($i) use (&$children) { + $child = ParseObject::create("Child"); + $child->set('x', $i); + $children[] = $child; + return $child; + }); + $parent = ParseObject::create("Parent"); + $parent->setArray('children', $children); + $parent->save(); + + $query = new ParseQuery("Parent"); + $query->includeKey('children'); + $result = $query->find(); + $this->assertEquals(1, count($result), + 'Did not return correct number of objects.'); + $children = $result[0]->get('children'); + $length = count($children); + for ($i = 0; $i < $length; $i++) { + $this->assertEquals($i, $children[$i]->get('x'), + 'Object should be fetched.'); + } + } + + public function testIncludeWithNoResults() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $query = new ParseQuery("Parent"); + $query->includeKey('children'); + $result = $query->find(); + $this->assertEquals(0, count($result), + 'Did not return correct number of objects.'); + } + + public function testIncludeWithNonExistentKey() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $parent = ParseObject::create("Parent"); + $parent->set('foo', 'bar'); + $parent->save(); + + $query = new ParseQuery("Parent"); + $query->includeKey('child'); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + } + + public function testIncludeOnTheWrongKeyType() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $parent = ParseObject::create("Parent"); + $parent->set('foo', 'bar'); + $parent->save(); + + $query = new ParseQuery("Parent"); + $query->includeKey('foo'); + $this->setExpectedException('Parse\ParseException', '', 102); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + } + + public function testIncludeWhenOnlySomeObjectsHaveChildren() + { + ParseTestHelper::clearClass("Child"); + ParseTestHelper::clearClass("Parent"); + $child = ParseObject::create('Child'); + $child->set('foo', 'bar'); + $child->save(); + $this->saveObjects(4, function ($i) use ($child) { + $parent = ParseObject::create('Parent'); + $parent->set('num', $i); + if ($i & 1) { + $parent->set('child', $child); + } + return $parent; + }); + + $query = new ParseQuery('Parent'); + $query->includeKey(['child']); + $query->ascending('num'); + $results = $query->find(); + $this->assertEquals(4, count($results), + 'Did not return correct number of objects.'); + $length = count($results); + for ($i = 0; $i < $length; $i++) { + if ($i & 1) { + $this->assertEquals('bar', $results[$i]->get('child')->get('foo'), + 'Object should be fetched'); + } else { + $this->assertEquals(null, $results[$i]->get('child'), + 'Should not have child'); + } + } + } + + public function testIncludeMultipleKeys() + { + ParseTestHelper::clearClass("Foo"); + ParseTestHelper::clearClass("Bar"); + ParseTestHelper::clearClass("Parent"); + $foo = ParseObject::create('Foo'); + $foo->set('rev', 'oof'); + $foo->save(); + $bar = ParseObject::create('Bar'); + $bar->set('rev', 'rab'); + $bar->save(); + + $parent = ParseObject::create('Parent'); + $parent->set('foofoo', $foo); + $parent->set('barbar', $bar); + $parent->save(); + + $query = new ParseQuery('Parent'); + $query->includeKey(['foofoo', 'barbar']); + $result = $query->first(); + $this->assertEquals('oof', $result->get('foofoo')->get('rev'), + 'Object should be fetched'); + $this->assertEquals('rab', $result->get('barbar')->get('rev'), + 'Object should be fetched'); + } + + public function testEqualToObject() + { + ParseTestHelper::clearClass("Item"); + ParseTestHelper::clearClass("Container"); + $items = array(); + $this->saveObjects(2, function ($i) use (&$items) { + $items[] = ParseObject::create("Item"); + $items[$i]->set('x', $i); + return $items[$i]; + }); + $this->saveObjects(2, function ($i) use ($items) { + $container = ParseObject::create("Container"); + $container->set('item', $items[$i]); + return $container; + }); + $query = new ParseQuery("Container"); + $query->equalTo('item', $items[0]); + $result = $query->find(); + $this->assertEquals(1, count($result), + 'Did not return the correct object.'); + } + + public function testEqualToNull() + { + $this->saveObjects(10, function ($i) { + $obj = ParseObject::create('TestObject'); + $obj->set('num', $i); + return $obj; + }); + $query = new ParseQuery('TestObject'); + $query->equalTo('num', null); + $results = $query->find(); + $this->assertEquals(0, count($results), + 'Did not return correct number of objects.'); + } + + public function provideTimeTestObjects() + { + ParseTestHelper::clearClass("TimeObject"); + $items = array(); + $this->saveObjects(3, function ($i) use (&$items) { + $timeObject = ParseObject::create('TimeObject'); + $timeObject->set('name', 'item' . $i); + $timeObject->set('time', new DateTime()); + sleep(1); + $items[] = $timeObject; + return $timeObject; + }); + return $items; + } + + public function testTimeEquality() + { + $items = $this->provideTimeTestObjects(); + $query = new ParseQuery('TimeObject'); + $query->equalTo('time', $items[1]->get('time')); + $results = $query->find(); + $this->assertEquals(1, count($results), + 'Did not return correct number of objects.'); + $this->assertEquals('item1', $results[0]->get('name')); + } + + public function testTimeLessThan() + { + $items = $this->provideTimeTestObjects(); + $query = new ParseQuery('TimeObject'); + $query->lessThan('time', $items[2]->get('time')); + $results = $query->find(); + $this->assertEquals(2, count($results), + 'Did not return correct number of objects.'); + } + + public function testRestrictedGetFailsWithoutMasterKey() + { + $obj = ParseObject::create("TestObject"); + $restrictedACL = new ParseACL(); + $obj->setACL($restrictedACL); + $obj->save(); + $query = new ParseQuery("TestObject"); + $this->setExpectedException('Parse\ParseException', 'not found'); + $objAgain = $query->get($obj->getObjectId()); + } + + public function testRestrictedGetWithMasterKey() + { + $obj = ParseObject::create("TestObject"); + $restrictedACL = new ParseACL(); + $obj->setACL($restrictedACL); + $obj->save(); + + $query = new ParseQuery("TestObject"); + $objAgain = $query->get($obj->getObjectId(), true); + $this->assertEquals($obj->getObjectId(), $objAgain->getObjectId()); + } + + public function testRestrictedCount() + { + $obj = ParseObject::create("TestObject"); + $restrictedACL = new ParseACL(); + $obj->setACL($restrictedACL); + $obj->save(); + + $query = new ParseQuery("TestObject"); + $count = $query->count(); + $this->assertEquals(0, $count); + $count = $query->count(true); + $this->assertEquals(1, $count); + } + +} diff --git a/tests/ParseRelationTest.php b/tests/ParseRelationTest.php new file mode 100644 index 00000000..c19a1b17 --- /dev/null +++ b/tests/ParseRelationTest.php @@ -0,0 +1,135 @@ +saveObjects(10, function ($i) use (&$children) { + $child = ParseObject::create('ChildObject'); + $child->set('x', $i); + $children[] = $child; + return $child; + }); + $parent = ParseObject::create('ParentObject'); + $relation = $parent->getRelation('children'); + $relation->add($children[0]); + $parent->set('foo', 1); + $parent->save(); + + $results = $relation->getQuery()->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals($children[0]->getObjectId(), $results[0]->getObjectId()); + $this->assertFalse($parent->isDirty()); + + $parentAgain = (new ParseQuery('ParentObject'))->get($parent->getObjectId()); + $relationAgain = $parentAgain->get('children'); + $this->assertNotNull($relationAgain, 'Error'); + + $results = $relation->getQuery()->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals($children[0]->getObjectId(), $results[0]->getObjectId()); + + $relation->remove($children[0]); + $relation->add([$children[4], $children[5]]); + $parent->set('bar', 3); + $parent->save(); + + + $results = $relation->getQuery()->find(); + $this->assertEquals(2, count($results)); + $this->assertFalse($parent->isDirty()); + + $relation->remove($children[5]); + $relation->add([ + $children[5], + $children[6], + $children[7], + $children[8] + ]); + $parent->save(); + + $results = $relation->getQuery()->find(); + $this->assertEquals(5, count($results)); + $this->assertFalse($parent->isDirty()); + + $relation->remove($children[8]); + $parent->save(); + $results = $relation->getQuery()->find(); + $this->assertEquals(4, count($results)); + $this->assertFalse($parent->isDirty()); + + $query = $relation->getQuery(); + $query->lessThan('x', 5); + $results = $query->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals($children[4]->getObjectId(), $results[0]->getObjectId()); + + } + + public function testQueriesOnRelationFields() + { + $children = array(); + $this->saveObjects(10, function ($i) use (&$children) { + $child = ParseObject::create('ChildObject'); + $child->set('x', $i); + $children[] = $child; + return $child; + }); + + $parent = ParseObject::create('ParentObject'); + $parent->set('x', 4); + $relation = $parent->getRelation('children'); + $relation->add([ + $children[0], + $children[1], + $children[2] + ]); + $parent->save(); + $parent2 = ParseObject::create('ParentObject'); + $parent2->set('x', 3); + $relation2 = $parent2->getRelation('children'); + $relation2->add([ + $children[4], + $children[5], + $children[6] + ]); + $parent2->save(); + $query = new ParseQuery('ParentObject'); + $query->containedIn('children', [$children[4], $children[9]]); + $results = $query->find(); + $this->assertEquals(1, count($results)); + $this->assertEquals($results[0]->getObjectId(), $parent2->getObjectId()); + + } + +} diff --git a/tests/ParseRoleTest.php b/tests/ParseRoleTest.php new file mode 100644 index 00000000..26326343 --- /dev/null +++ b/tests/ParseRoleTest.php @@ -0,0 +1,217 @@ +aclPublic()); + $role->save(); + $this->assertNotNull($role->getObjectId(), "Role should have objectId."); + } + + public function testRoleWithoutACLFails() + { + $role = new ParseRole(); + $role->setName("Admin"); + $this->setExpectedException('Parse\ParseException', 'ACL'); + $role->save(); + } + + public function testNameValidation() + { + $role = ParseRole::createRole("Admin", $this->aclPublic()); + $this->assertEquals("Admin", $role->getName()); + $role->setName("Superuser"); + $this->assertEquals("Superuser", $role->getName()); + $role->setName("Super-Users"); + $this->assertEquals("Super-Users", $role->getName()); + $role->setName("A1234"); + $this->assertEquals("A1234", $role->getName()); + $role->save(); + $this->setExpectedException('Parse\ParseException', 'has been saved'); + $role->setName("Moderators"); + } + + public function testGetCreatedRole() + { + $role = ParseRole::createRole("Admin", $this->aclPublic()); + $role->save(); + $query = ParseRole::query(); + $obj = $query->get($role->getObjectId()); + $this->assertTrue($obj instanceof ParseRole); + $this->assertEquals($role->getObjectId(), $obj->getObjectId()); + } + + public function testFindRolesByName() + { + $admin = ParseRole::createRole("Admin", $this->aclPublic()); + $mod = ParseRole::createRole("Moderator", $this->aclPublic()); + ParseObject::saveAll([$admin, $mod]); + $query1 = ParseRole::query(); + $query1->equalTo("name", "Admin"); + $this->assertEquals(1, $query1->count(), "Count should be 1."); + $query2 = ParseRole::query(); + $query2->equalTo("name", "Moderator"); + $this->assertEquals(1, $query2->count(), "Count should be 1."); + $query3 = ParseRole::query(); + $this->assertEquals(2, $query3->count()); + } + + public function testRoleNameUnique() + { + $role = ParseRole::createRole("Admin", $this->aclPublic()); + $role->save(); + $role2 = ParseRole::createRole("Admin", $this->aclPublic()); + $this->setExpectedException('Parse\ParseException', 'duplicate'); + $role2->save(); + } + + public function testExplicitRoleACL() + { + $eden = $this->createEden(); + ParseUser::logIn("adam", "adam"); + $query = new ParseQuery("Things"); + $apple = $query->get($eden['apple']->getObjectId()); + ParseUser::logIn("eve", "eve"); + $apple = $query->get($eden['apple']->getObjectId()); + ParseUser::logIn("snake", "snake"); + $this->setExpectedException('Parse\ParseException', 'not found'); + $apple = $query->get($eden['apple']->getObjectId()); + } + + public function testRoleHierarchyAndPropagation() + { + $eden = $this->createEden(); + ParseUser::logIn("adam", "adam"); + $query = new ParseQuery("Things"); + $garden = $query->get($eden['garden']->getObjectId()); + ParseUser::logIn("eve", "eve"); + $garden = $query->get($eden['garden']->getObjectId()); + ParseUser::logIn("snake", "snake"); + $garden = $query->get($eden['garden']->getObjectId()); + + $eden['edenkin']->getRoles()->remove($eden['humans']); + $eden['edenkin']->save(); + ParseUser::logIn("adam", "adam"); + try { + $query->get($eden['garden']->getObjectId()); + $this->fail("Get should have failed."); + } catch (\Parse\ParseException $ex) { + if ($ex->getMessage() != "Object not found.") throw $ex; + } + ParseUser::logIn("eve", "eve"); + try { + $query->get($eden['garden']->getObjectId()); + $this->fail("Get should have failed."); + } catch (\Parse\ParseException $ex) { + if ($ex->getMessage() != "Object not found.") throw $ex; + } + ParseUser::logIn("snake", "snake"); + $query->get($eden['garden']->getObjectId()); + } + + public function testAddUserAfterFetch() + { + $user = new ParseUser(); + $user->setUsername("bob"); + $user->setPassword("barker"); + $user->signUp(); + $role = ParseRole::createRole("MyRole", ParseACL::createACLWithUser($user)); + $role->save(); + $query = ParseRole::query(); + $roleAgain = $query->get($role->getObjectId()); + $roleAgain->getUsers()->add($user); + $roleAgain->save(); + } + + + /** + * Utilities + */ + + public function aclPrivateTo($someone) + { + $acl = new ParseACL(); + $acl->setReadAccess($someone, true); + $acl->setWriteAccess($someone, true); + return $acl; + } + + public function aclPublic() + { + $acl = new ParseACL(); + $acl->setPublicReadAccess(true); + $acl->setPublicWriteAccess(true); + return $acl; + } + + public function createUser($username) + { + $user = new ParseUser(); + $user->setUsername($username); + $user->setPassword($username); + return $user; + } + + public function createEden() + { + $eden = array(); + $eden['adam'] = $this->createUser('adam'); + $eden['eve'] = $this->createUser('eve'); + $eden['snake'] = $this->createUser('snake'); + $eden['adam']->signUp(); + $eden['eve']->signUp(); + $eden['snake']->signUp(); + $eden['humans'] = ParseRole::createRole("humans", $this->aclPublic()); + $eden['humans']->getUsers()->add($eden['adam']); + $eden['humans']->getUsers()->add($eden['eve']); + $eden['creatures'] = ParseRole::createRole( + "creatures", $this->aclPublic() + ); + $eden['creatures']->getUsers()->add($eden['snake']); + ParseObject::saveAll([$eden['humans'], $eden['creatures']]); + $eden['edenkin'] = ParseRole::createRole("edenkin", $this->aclPublic()); + $eden['edenkin']->getRoles()->add($eden['humans']); + $eden['edenkin']->getRoles()->add($eden['creatures']); + $eden['edenkin']->save(); + + $eden['apple'] = ParseObject::create("Things"); + $eden['apple']->set("name", "apple"); + $eden['apple']->set("ACL", $this->aclPrivateTo($eden['humans'])); + + $eden['garden'] = ParseObject::create("Things"); + $eden['garden']->set("name", "garden"); + $eden['garden']->set("ACL", $this->aclPrivateTo($eden['edenkin'])); + + ParseObject::saveAll([$eden['apple'], $eden['garden']]); + + return $eden; + + } + +} diff --git a/tests/ParseSessionStorageTest.php b/tests/ParseSessionStorageTest.php new file mode 100644 index 00000000..a7cdbfbd --- /dev/null +++ b/tests/ParseSessionStorageTest.php @@ -0,0 +1,76 @@ +clear(); + } + + public static function tearDownAfterClass() + { + session_destroy(); + } + + public function testIsUsingParseSession() + { + $this->assertTrue(self::$parseStorage instanceof Parse\ParseSessionStorage); + } + + public function testSetAndGet() + { + self::$parseStorage->set('foo', 'bar'); + $this->assertEquals('bar', self::$parseStorage->get('foo')); + } + + public function testRemove() + { + self::$parseStorage->set('foo', 'bar'); + self::$parseStorage->remove('foo'); + $this->assertNull(self::$parseStorage->get('foo')); + } + + public function testClear() + { + self::$parseStorage->set('foo', 'bar'); + self::$parseStorage->set('foo2', 'bar'); + self::$parseStorage->set('foo3', 'bar'); + self::$parseStorage->clear(); + $this->assertEmpty(self::$parseStorage->getKeys()); + } + + public function testGetAll() + { + self::$parseStorage->set('foo', 'bar'); + self::$parseStorage->set('foo2', 'bar'); + self::$parseStorage->set('foo3', 'bar'); + $result = self::$parseStorage->getAll(); + $this->assertEquals('bar', $result['foo']); + $this->assertEquals('bar', $result['foo2']); + $this->assertEquals('bar', $result['foo3']); + $this->assertEquals(3, count($result)); + } + +} diff --git a/tests/ParseSubclassTest.php b/tests/ParseSubclassTest.php new file mode 100644 index 00000000..cf75b199 --- /dev/null +++ b/tests/ParseSubclassTest.php @@ -0,0 +1,37 @@ +assertTrue($install instanceof ParseInstallation); + $this->assertTrue(is_subclass_of($install, 'Parse\ParseObject')); + } + + public function testCreateFromParseObject() + { + $install = ParseObject::create("_Installation"); + $this->assertTrue($install instanceof ParseInstallation); + $this->assertTrue(is_subclass_of($install, 'Parse\ParseObject')); + } + +} \ No newline at end of file diff --git a/tests/ParseTestHelper.php b/tests/ParseTestHelper.php new file mode 100644 index 00000000..fcba5823 --- /dev/null +++ b/tests/ParseTestHelper.php @@ -0,0 +1,35 @@ +each(function(ParseObject $obj) { + $obj->destroy(true); + }, true); + } + +} \ No newline at end of file diff --git a/tests/ParseUserTest.php b/tests/ParseUserTest.php new file mode 100644 index 00000000..aefad0b8 --- /dev/null +++ b/tests/ParseUserTest.php @@ -0,0 +1,430 @@ +setUsername("asdf"); + $user->setPassword("zxcv"); + $user->signUp(); + $this->assertTrue($user->isAuthenticated()); + } + + public function testLoginSuccess() + { + $this->testUserSignUp(); + $user = ParseUser::logIn("asdf", "zxcv"); + $this->assertTrue($user->isAuthenticated()); + $this->assertEquals("asdf", $user->get('username')); + } + + public function testLoginWrongUsername() + { + $this->setExpectedException('Parse\ParseException', 'invalid login'); + $user = ParseUser::logIn("non_existent_user", "bogus"); + } + + public function testLoginWrongPassword() + { + $this->testUserSignUp(); + $this->setExpectedException('Parse\ParseException', 'invalid login'); + $user = ParseUser::logIn("asdf", "bogus"); + } + + public function testBecome() + { + $user = new ParseUser(); + $user->setUsername("asdf"); + $user->setPassword("zxcv"); + $user->signUp(); + $this->assertEquals(ParseUser::getCurrentUser(), $user); + + $sessionToken = $user->getSessionToken(); + + $newUser = ParseUser::become($sessionToken); + $this->assertEquals(ParseUser::getCurrentUser(), $newUser); + $this->assertEquals("asdf", $newUser->get('username')); + + $this->setExpectedException('Parse\ParseException', 'invalid session'); + $failUser = ParseUser::become('garbage_token'); + } + + public function testCannotAlterOtherUser() + { + $user = new ParseUser(); + $user->setUsername("asdf"); + $user->setPassword("zxcv"); + $user->signUp(); + + $otherUser = new ParseUser(); + $otherUser->setUsername("hacker"); + $otherUser->setPassword("password"); + $otherUser->signUp(); + + $this->assertEquals(ParseUser::getCurrentUser(), $otherUser); + + $this->setExpectedException( + 'Parse\ParseException', 'UserCannotBeAlteredWithoutSession' + ); + $user->setUsername('changed'); + $user->save(); + } + + public function testCannotDeleteOtherUser() + { + $user = new ParseUser(); + $user->setUsername("asdf"); + $user->setPassword("zxcv"); + $user->signUp(); + + $otherUser = new ParseUser(); + $otherUser->setUsername("hacker"); + $otherUser->setPassword("password"); + $otherUser->signUp(); + + $this->assertEquals(ParseUser::getCurrentUser(), $otherUser); + + $this->setExpectedException( + 'Parse\ParseException', 'UserCannotBeAlteredWithoutSession' + ); + $user->destroy(); + } + + public function testCannotSaveAllWithOtherUser() + { + $user = new ParseUser(); + $user->setUsername("asdf"); + $user->setPassword("zxcv"); + $user->signUp(); + + $otherUser = new ParseUser(); + $otherUser->setUsername("hacker"); + $otherUser->setPassword("password"); + $otherUser->signUp(); + + $this->assertEquals(ParseUser::getCurrentUser(), $otherUser); + + $obj = ParseObject::create("TestObject"); + $obj->set('user', $otherUser); + $obj->save(); + + $item1 = ParseObject::create("TestObject"); + $item1->set('num', 0); + $item1->save(); + + $item1->set('num', 1); + $item2 = ParseObject::create("TestObject"); + $item2->set('num', 2); + $user->setUsername('changed'); + $this->setExpectedException( + 'Parse\ParseAggregateException', 'Errors during batch save.' + ); + ParseObject::saveAll(array($item1, $item2, $user)); + } + + public function testCurrentUser() + { + $user = new ParseUser(); + $user->setUsername("asdf"); + $user->setPassword("zxcv"); + $user->signUp(); + + $current = ParseUser::getCurrentUser(); + $this->assertEquals($current->getObjectId(), $user->getObjectId()); + $this->assertNotNull($user->getSessionToken()); + + $currentAgain = ParseUser::getCurrentUser(); + $this->assertEquals($current, $currentAgain); + + ParseUser::logOut(); + $this->assertNull(ParseUser::getCurrentUser()); + } + + public function testIsCurrent() + { + $user1 = new ParseUser(); + $user2 = new ParseUser(); + $user3 = new ParseUser(); + + $user1->setUsername('a'); + $user2->setUsername('b'); + $user3->setUsername('c'); + + $user1->setPassword('password'); + $user2->setPassword('password'); + $user3->setPassword('password'); + + $user1->signUp(); + $this->assertTrue($user1->isCurrent()); + $this->assertFalse($user2->isCurrent()); + $this->assertFalse($user3->isCurrent()); + + $user2->signUp(); + $this->assertTrue($user2->isCurrent()); + $this->assertFalse($user1->isCurrent()); + $this->assertFalse($user3->isCurrent()); + + $user3->signUp(); + $this->assertTrue($user3->isCurrent()); + $this->assertFalse($user1->isCurrent()); + $this->assertFalse($user2->isCurrent()); + + $user = ParseUser::logIn('a', 'password'); + $this->assertTrue($user1->isCurrent()); + $this->assertFalse($user2->isCurrent()); + $this->assertFalse($user3->isCurrent()); + + $user = ParseUser::logIn('b', 'password'); + $this->assertTrue($user2->isCurrent()); + $this->assertFalse($user1->isCurrent()); + $this->assertFalse($user3->isCurrent()); + + $user = ParseUser::logIn('c', 'password'); + $this->assertTrue($user3->isCurrent()); + $this->assertFalse($user1->isCurrent()); + $this->assertFalse($user2->isCurrent()); + + ParseUser::logOut(); + $this->assertFalse($user1->isCurrent()); + $this->assertFalse($user2->isCurrent()); + $this->assertFalse($user3->isCurrent()); + } + + public function testPasswordReset() + { + $user = new ParseUser(); + $user->setUsername('asdf'); + $user->setPassword('zxcv'); + $user->set('email', 'asdf@example.com'); + $user->signUp(); + + ParseUser::requestPasswordReset('asdf@example.com'); + } + + public function testPasswordResetFails() + { + $this->setExpectedException( + 'Parse\ParseException', 'no user found with email' + ); + ParseUser::requestPasswordReset('non_existent@example.com'); + } + + public function testUserAssociations() + { + $child = ParseObject::create("TestObject"); + $child->save(); + + $user = new ParseUser(); + $user->setUsername('asdf'); + $user->setPassword('zxcv'); + $user->set('child', $child); + $user->signUp(); + + $object = ParseObject::create("TestObject"); + $object->set('user', $user); + $object->save(); + + $query = new ParseQuery("TestObject"); + $objectAgain = $query->get($object->getObjectId()); + $userAgain = $objectAgain->get('user'); + $userAgain->fetch(); + + $this->assertEquals($userAgain->getObjectId(), $user->getObjectId()); + $this->assertEquals( + $userAgain->get('child')->getObjectId(), $child->getObjectId() + ); + } + + public function testUserQueries() + { + ParseTestHelper::clearClass(ParseUser::$parseClassName); + $user = new ParseUser(); + $user->setUsername('asdf'); + $user->setPassword('zxcv'); + $user->set('email', 'asdf@example.com'); + $user->signUp(); + + $query = ParseUser::query(); + $users = $query->find(); + + $this->assertEquals(1, count($users)); + $this->assertEquals($user->getObjectId(), $users[0]->getObjectId()); + $this->assertEquals('asdf@example.com', $users[0]->get('email')); + } + + public function testContainedInUserArrayQueries() + { + ParseTestHelper::clearClass(ParseUser::$parseClassName); + ParseTestHelper::clearClass("TestObject"); + $userList = array(); + for ($i = 0; $i < 4; $i++) { + $user = new ParseUser(); + $user->setUsername('user_num_' . $i); + $user->setPassword('password'); + $user->set('email', 'asdf_' . $i . '@example.com'); + $user->signUp(); + $userList[] = $user; + } + $messageList = array(); + for ($i = 0; $i < 5; $i++) { + $message = ParseObject::create('TestObject'); + $toUser = ($i + 1) % 4; + $fromUser = $i % 4; + $message->set('to', $userList[$toUser]); + $message->set('from', $userList[$fromUser]); + $message->save(); + $messageList[] = $message; + } + + $inList = array($userList[0], $userList[3], $userList[3]); + $query = new ParseQuery("TestObject"); + $query->containedIn('from', $inList); + $results = $query->find(); + + $this->assertEquals(3, count($results)); + } + + public function testSavingUserThrows() + { + $user = new ParseUser(); + $user->setUsername('asdf'); + $user->setPassword('zxcv'); + $this->setExpectedException('Parse\ParseException', 'You must call signUp'); + $user->save(); + } + + public function testUserUpdates() + { + $user = new ParseUser(); + $user->setUsername('asdf'); + $user->setPassword('zxcv'); + $user->set('email', 'asdf@example.com'); + $user->signUp(); + $this->assertNotNull(ParseUser::getCurrentUser()); + $user->setUsername('test'); + $user->save(); + $this->assertNotNull($user->get('username')); + $this->assertNotNull($user->get('email')); + $user->destroy(); + + $query = ParseUser::query(); + $this->setExpectedException('Parse\ParseException', 'Object not found'); + $fail = $query->get($user->getObjectId()); + } + + public function testCountUsers() + { + ParseTestHelper::clearClass(ParseUser::$parseClassName); + $ilya = new ParseUser(); + $ilya->setUsername('ilya'); + $ilya->setPassword('password'); + $ilya->signUp(); + + $kevin = new ParseUser(); + $kevin->setUsername('kevin'); + $kevin->setPassword('password'); + $kevin->signUp(); + + $james = new ParseUser(); + $james->setUsername('james'); + $james->setPassword('password'); + $james->signUp(); + + $query = ParseUser::query(); + $result = $query->count(); + $this->assertEquals(3, $result); + } + + public function testUserLoadedFromStorageFromSignUp() + { + ParseTestHelper::clearClass(ParseUser::$parseClassName); + $fosco = new ParseUser(); + $fosco->setUsername('fosco'); + $fosco->setPassword('password'); + $fosco->signUp(); + $id = $fosco->getObjectId(); + $this->assertNotNull($id); + $current = ParseUser::getCurrentUser(); + $this->assertEquals($id, $current->getObjectId()); + ParseUser::_clearCurrentUserVariable(); + $current = ParseUser::getCurrentUser(); + $this->assertEquals($id, $current->getObjectId()); + } + + public function testUserLoadedFromStorageFromLogIn() + { + ParseTestHelper::clearClass(ParseUser::$parseClassName); + $fosco = new ParseUser(); + $fosco->setUsername('fosco'); + $fosco->setPassword('password'); + $fosco->signUp(); + $id = $fosco->getObjectId(); + $this->assertNotNull($id); + ParseUser::logOut(); + ParseUser::_clearCurrentUserVariable(); + $current = ParseUser::getCurrentUser(); + $this->assertNull($current); + ParseUser::logIn("fosco", "password"); + $current = ParseUser::getCurrentUser(); + $this->assertEquals($id, $current->getObjectId()); + ParseUser::_clearCurrentUserVariable(); + $current = ParseUser::getCurrentUser(); + $this->assertEquals($id, $current->getObjectId()); + } + + public function testUserWithMissingUsername() + { + $user = new ParseUser(); + $user->setPassword('test'); + $this->setExpectedException('Parse\ParseException', 'empty name'); + $user->signUp(); + } + + public function testUserWithMissingPassword() + { + $user = new ParseUser(); + $user->setUsername('test'); + $this->setExpectedException('Parse\ParseException', 'empty password'); + $user->signUp(); + } + + public function testCurrentUserIsNotDirty() + { + $user = new ParseUser(); + $user->setUsername('asdf'); + $user->setPassword('zxcv'); + $user->set('bleep', 'bloop'); + $user->signUp(); + $this->assertFalse($user->isKeyDirty('bleep')); + $userAgain = ParseUser::getCurrentUser(); + $this->assertFalse($userAgain->isKeyDirty('bleep')); + } + +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..b2634320 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/tests/cloudcode/cloud/main.js b/tests/cloudcode/cloud/main.js new file mode 100644 index 00000000..d0cddd46 --- /dev/null +++ b/tests/cloudcode/cloud/main.js @@ -0,0 +1,43 @@ +Parse.Cloud.define("bar", function(request, response) { + if (request.params.key2 === "value1") { + response.success('Foo'); + } else { + response.error("bad stuff happened"); + } +}); + +Parse.Cloud.define("foo", function(request, response) { + var key1 = request.params.key1; + var key2 = request.params.key2; + if (key1 === "value1" && key2 + && key2.length === 3 && key2[0] === 1 + && key2[1] === 2 && key2[2] === 3) { + result = { + object: { + __type: 'Object', + className: 'Foo', + objectId: '1', + x: 2, + relation: { + __type: 'Object', + className: 'Bar', + objectId: '2', + x: 3 + } + }, + array:[ + { + __type: 'Object', + className: 'Bar', + objectId: '10', + x: 2 + } + ] + }; + response.success(result); + } else if (key1 === "value1") { + response.success({a: 2}); + } else { + response.error('invalid!'); + } +}); diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 00000000..c6d42c16 --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,8 @@ + + + + tests + + + \ No newline at end of file