Commit ffe7c65b authored by Katherine Walker's avatar Katherine Walker

PHPLIB-190: Support object cloning for BSONArray and BSONDocument

parent 19a74922
<?php <?php
/* /*
* Copyright 2016-2017 MongoDB, Inc. * Copyright 2016-present MongoDB, Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -32,6 +32,16 @@ use JsonSerializable; ...@@ -32,6 +32,16 @@ use JsonSerializable;
*/ */
class BSONArray extends ArrayObject implements JsonSerializable, Serializable, Unserializable class BSONArray extends ArrayObject implements JsonSerializable, Serializable, Unserializable
{ {
/**
* Clone this BSONArray.
*/
public function __clone()
{
foreach ($this as $key => $value) {
$this[$key] = \MongoDB\recursive_copy($value);
}
}
/** /**
* Factory method for var_export(). * Factory method for var_export().
* *
......
<?php <?php
/* /*
* Copyright 2016-2017 MongoDB, Inc. * Copyright 2016-present MongoDB, Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -32,6 +32,16 @@ use JsonSerializable; ...@@ -32,6 +32,16 @@ use JsonSerializable;
*/ */
class BSONDocument extends ArrayObject implements JsonSerializable, Serializable, Unserializable class BSONDocument extends ArrayObject implements JsonSerializable, Serializable, Unserializable
{ {
/**
* Deep clone this BSONDocument.
*/
public function __clone()
{
foreach ($this as $key => $value) {
$this[$key] = \MongoDB\recursive_copy($value);
}
}
/** /**
* Constructor. * Constructor.
* *
......
...@@ -194,3 +194,27 @@ function is_string_array($input) { ...@@ -194,3 +194,27 @@ function is_string_array($input) {
return true; return true;
} }
/**
* Performs a deep copy of a value.
*
* This function will clone objects and recursively copy values within arrays.
*
* @internal
* @see https://bugs.php.net/bug.php?id=49664
* @param mixed $element Value to be copied
* @return mixed
*/
function recursive_copy($element) {
if (is_array($element)) {
foreach ($element as $key => $value) {
$element[$key] = recursive_copy($value);
}
return $element;
}
if ( ! is_object($element)) {
return $element;
}
return clone $element;
}
...@@ -6,11 +6,7 @@ use MongoDB\Driver\Command; ...@@ -6,11 +6,7 @@ use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor; use MongoDB\Driver\Cursor;
use MongoDB\Driver\Manager; use MongoDB\Driver\Manager;
use MongoDB\Driver\ReadPreference; use MongoDB\Driver\ReadPreference;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
use InvalidArgumentException;
use stdClass; use stdClass;
use Traversable;
use UnexpectedValueException; use UnexpectedValueException;
abstract class FunctionalTestCase extends TestCase abstract class FunctionalTestCase extends TestCase
...@@ -49,34 +45,6 @@ abstract class FunctionalTestCase extends TestCase ...@@ -49,34 +45,6 @@ abstract class FunctionalTestCase extends TestCase
$this->assertEquals((string) $expectedObjectId, (string) $actualObjectId); $this->assertEquals((string) $expectedObjectId, (string) $actualObjectId);
} }
protected function assertSameDocument($expectedDocument, $actualDocument)
{
$this->assertEquals(
\MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($expectedDocument))),
\MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($actualDocument)))
);
}
protected function assertSameDocuments(array $expectedDocuments, $actualDocuments)
{
if ($actualDocuments instanceof Traversable) {
$actualDocuments = iterator_to_array($actualDocuments);
}
if ( ! is_array($actualDocuments)) {
throw new InvalidArgumentException('$actualDocuments is not an array or Traversable');
}
$normalizeRootDocuments = function($document) {
return \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($document)));
};
$this->assertEquals(
array_map($normalizeRootDocuments, $expectedDocuments),
array_map($normalizeRootDocuments, $actualDocuments)
);
}
protected function getFeatureCompatibilityVersion(ReadPreference $readPreference = null) protected function getFeatureCompatibilityVersion(ReadPreference $readPreference = null)
{ {
if (version_compare($this->getServerVersion(), '3.4.0', '<')) { if (version_compare($this->getServerVersion(), '3.4.0', '<')) {
...@@ -127,48 +95,4 @@ abstract class FunctionalTestCase extends TestCase ...@@ -127,48 +95,4 @@ abstract class FunctionalTestCase extends TestCase
throw new UnexpectedValueException('Could not determine server version'); throw new UnexpectedValueException('Could not determine server version');
} }
/**
* Normalizes a BSON document or array for use with assertEquals().
*
* The argument will be converted to a BSONArray or BSONDocument based on
* its type and keys. Document fields will be sorted alphabetically. Each
* value within the array or document will then be normalized recursively.
*
* @param array|object $bson
* @return BSONDocument|BSONArray
* @throws InvalidArgumentException if $bson is not an array or object
*/
private function normalizeBSON($bson)
{
if ( ! is_array($bson) && ! is_object($bson)) {
throw new InvalidArgumentException('$bson is not an array or object');
}
if ($bson instanceof BSONArray || (is_array($bson) && $bson === array_values($bson))) {
if ( ! $bson instanceof BSONArray) {
$bson = new BSONArray($bson);
}
} else {
if ( ! $bson instanceof BSONDocument) {
$bson = new BSONDocument((array) $bson);
}
$bson->ksort();
}
foreach ($bson as $key => $value) {
if ($value instanceof BSONArray || (is_array($value) && $value === array_values($value))) {
$bson[$key] = $this->normalizeBSON($value);
continue;
}
if ($value instanceof stdClass || $value instanceof BSONDocument || is_array($value)) {
$bson[$key] = $this->normalizeBSON($value);
continue;
}
}
return $bson;
}
} }
...@@ -5,6 +5,7 @@ namespace MongoDB\Tests\Model; ...@@ -5,6 +5,7 @@ namespace MongoDB\Tests\Model;
use MongoDB\Model\BSONArray; use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument; use MongoDB\Model\BSONDocument;
use MongoDB\Tests\TestCase; use MongoDB\Tests\TestCase;
use stdClass;
class BSONArrayTest extends TestCase class BSONArrayTest extends TestCase
{ {
...@@ -17,6 +18,31 @@ class BSONArrayTest extends TestCase ...@@ -17,6 +18,31 @@ class BSONArrayTest extends TestCase
$this->assertSame(['foo', 'bar'], $array->bsonSerialize()); $this->assertSame(['foo', 'bar'], $array->bsonSerialize());
} }
public function testClone()
{
$array = new BSONArray([
[
'foo',
new stdClass,
['bar', new stdClass],
],
new BSONArray([
'foo',
new stdClass,
['bar', new stdClass],
]),
]);
$arrayClone = clone $array;
$this->assertSameDocument($array, $arrayClone);
$this->assertNotSame($array, $arrayClone);
$this->assertNotSame($array[0][1], $arrayClone[0][1]);
$this->assertNotSame($array[0][2][1], $arrayClone[0][2][1]);
$this->assertNotSame($array[1], $arrayClone[1]);
$this->assertNotSame($array[1][1], $arrayClone[1][1]);
$this->assertNotSame($array[1][2][1], $arrayClone[1][2][1]);
}
public function testJsonSerialize() public function testJsonSerialize()
{ {
$document = new BSONArray([ $document = new BSONArray([
......
...@@ -6,6 +6,7 @@ use MongoDB\Model\BSONArray; ...@@ -6,6 +6,7 @@ use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument; use MongoDB\Model\BSONDocument;
use MongoDB\Tests\TestCase; use MongoDB\Tests\TestCase;
use ArrayObject; use ArrayObject;
use stdClass;
class BSONDocumentTest extends TestCase class BSONDocumentTest extends TestCase
{ {
...@@ -25,6 +26,31 @@ class BSONDocumentTest extends TestCase ...@@ -25,6 +26,31 @@ class BSONDocumentTest extends TestCase
$this->assertEquals((object) [0 => 'foo', 2 => 'bar'], $document->bsonSerialize()); $this->assertEquals((object) [0 => 'foo', 2 => 'bar'], $document->bsonSerialize());
} }
public function testClone()
{
$document = new BSONDocument([
'a' => [
'a' => 'foo',
'b' => new stdClass,
'c' => ['bar', new stdClass],
],
'b' => new BSONDocument([
'a' => 'foo',
'b' => new stdClass,
'c' => ['bar', new stdClass],
]),
]);
$documentClone = clone $document;
$this->assertSameDocument($document, $documentClone);
$this->assertNotSame($document, $documentClone);
$this->assertNotSame($document['a']['b'], $documentClone['a']['b']);
$this->assertNotSame($document['a']['c'][1], $documentClone['a']['c'][1]);
$this->assertNotSame($document['b'], $documentClone['b']);
$this->assertNotSame($document['b']['b'], $documentClone['b']['b']);
$this->assertNotSame($document['b']['c'][1], $documentClone['b']['c'][1]);
}
public function testJsonSerialize() public function testJsonSerialize()
{ {
$document = new BSONDocument([ $document = new BSONDocument([
......
...@@ -5,9 +5,13 @@ namespace MongoDB\Tests; ...@@ -5,9 +5,13 @@ namespace MongoDB\Tests;
use MongoDB\Driver\ReadConcern; use MongoDB\Driver\ReadConcern;
use MongoDB\Driver\ReadPreference; use MongoDB\Driver\ReadPreference;
use MongoDB\Driver\WriteConcern; use MongoDB\Driver\WriteConcern;
use MongoDB\Model\BSONArray;
use MongoDB\Model\BSONDocument;
use PHPUnit\Framework\TestCase as BaseTestCase; use PHPUnit\Framework\TestCase as BaseTestCase;
use InvalidArgumentException;
use ReflectionClass; use ReflectionClass;
use stdClass; use stdClass;
use Traversable;
abstract class TestCase extends BaseTestCase abstract class TestCase extends BaseTestCase
{ {
...@@ -20,7 +24,7 @@ abstract class TestCase extends BaseTestCase ...@@ -20,7 +24,7 @@ abstract class TestCase extends BaseTestCase
parent::setExpectedException($exception); parent::setExpectedException($exception);
} }
public function expectExceptionMessage($exceptionMessage) public function expectExceptionMessage($exceptionMessage)
{ {
if (method_exists(BaseTestCase::class, 'expectExceptionMessage')) { if (method_exists(BaseTestCase::class, 'expectExceptionMessage')) {
parent::expectExceptionMessage($exceptionMessage); parent::expectExceptionMessage($exceptionMessage);
...@@ -48,6 +52,34 @@ abstract class TestCase extends BaseTestCase ...@@ -48,6 +52,34 @@ abstract class TestCase extends BaseTestCase
return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues()); return $this->wrapValuesForDataProvider($this->getInvalidDocumentValues());
} }
protected function assertSameDocument($expectedDocument, $actualDocument)
{
$this->assertEquals(
\MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($expectedDocument))),
\MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($actualDocument)))
);
}
protected function assertSameDocuments(array $expectedDocuments, $actualDocuments)
{
if ($actualDocuments instanceof Traversable) {
$actualDocuments = iterator_to_array($actualDocuments);
}
if ( ! is_array($actualDocuments)) {
throw new InvalidArgumentException('$actualDocuments is not an array or Traversable');
}
$normalizeRootDocuments = function($document) {
return \MongoDB\BSON\toJSON(\MongoDB\BSON\fromPHP($this->normalizeBSON($document)));
};
$this->assertEquals(
array_map($normalizeRootDocuments, $expectedDocuments),
array_map($normalizeRootDocuments, $actualDocuments)
);
}
/** /**
* Return the test collection name. * Return the test collection name.
* *
...@@ -190,4 +222,48 @@ abstract class TestCase extends BaseTestCase ...@@ -190,4 +222,48 @@ abstract class TestCase extends BaseTestCase
{ {
return array_map(function($value) { return [$value]; }, $values); return array_map(function($value) { return [$value]; }, $values);
} }
/**
* Normalizes a BSON document or array for use with assertEquals().
*
* The argument will be converted to a BSONArray or BSONDocument based on
* its type and keys. Document fields will be sorted alphabetically. Each
* value within the array or document will then be normalized recursively.
*
* @param array|object $bson
* @return BSONDocument|BSONArray
* @throws InvalidArgumentException if $bson is not an array or object
*/
private function normalizeBSON($bson)
{
if ( ! is_array($bson) && ! is_object($bson)) {
throw new InvalidArgumentException('$bson is not an array or object');
}
if ($bson instanceof BSONArray || (is_array($bson) && $bson === array_values($bson))) {
if ( ! $bson instanceof BSONArray) {
$bson = new BSONArray($bson);
}
} else {
if ( ! $bson instanceof BSONDocument) {
$bson = new BSONDocument((array) $bson);
}
$bson->ksort();
}
foreach ($bson as $key => $value) {
if ($value instanceof BSONArray || (is_array($value) && $value === array_values($value))) {
$bson[$key] = $this->normalizeBSON($value);
continue;
}
if ($value instanceof stdClass || $value instanceof BSONDocument || is_array($value)) {
$bson[$key] = $this->normalizeBSON($value);
continue;
}
}
return $bson;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment