Skip to content

Commit 4e0f0b4

Browse files
committed
using SORT_REGULAR flag for optioned array_unique call, and adding nested array value merging, and fixing bug when types differ in recursive merge
1 parent 580cdf2 commit 4e0f0b4

File tree

4 files changed

+153
-23
lines changed

4 files changed

+153
-23
lines changed

Diff for: README.md

+75
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,79 @@ class stdClass#57 (1) {
132132
}
133133
}
134134
*/
135+
```
136+
137+
#### `OBJECT_MERGE_OPT_COMPARE_ARRAYS`
138+
139+
*NOTE*: This only has an effect during a *recursive* merge!
140+
141+
When this is provided, individual array offsets will have their values compared and merged, rather than merely appended
142+
together.
143+
144+
Example 1:
145+
```php
146+
$o1 = json_decode('{"arr":[{"key1":"value1"}]}');
147+
$o2 = json_decode('{"arr":[{"key2":"value2"}]}');
148+
$o3 = json_decode('{"arr":[{"key3":"value3"}]}');
149+
150+
$out = object_merge_recursive_opts(OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES, $o1, $o2, $o3);
151+
152+
var_dump($out);
153+
154+
/*
155+
class stdClass#120 (1) {
156+
public $arr =>
157+
array(1) {
158+
[0] =>
159+
class stdClass#116 (3) {
160+
public $key1 =>
161+
string(6) "value1"
162+
public $key2 =>
163+
string(6) "value2"
164+
public $key3 =>
165+
string(6) "value3"
166+
}
167+
}
168+
}
169+
*/
170+
```
171+
172+
Example 2:
173+
```php
174+
$o1 = json_decode('{"arr":[{"key1":"value1","arr":[{"key11":"value11"}]}]}');
175+
$o2 = json_decode('{"arr":[{"key2":"value2","arr":[{"key22":"value22"}]}]}');
176+
$o3 = json_decode('{"arr":[{"key3":"value3","arr":[{"key33":"value33"}]}]}');
177+
178+
$out = object_merge_recursive_opts(OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES, $o1, $o2, $o3);
179+
180+
var_dump($out);
181+
182+
/*
183+
class stdClass#56 (1) {
184+
public $arr =>
185+
array(1) {
186+
[0] =>
187+
class stdClass#107 (4) {
188+
public $key1 =>
189+
string(6) "value1"
190+
public $arr =>
191+
array(1) {
192+
[0] =>
193+
class stdClass#119 (3) {
194+
public $key11 =>
195+
string(7) "value11"
196+
public $key22 =>
197+
string(7) "value22"
198+
public $key33 =>
199+
string(7) "value33"
200+
}
201+
}
202+
public $key2 =>
203+
string(6) "value2"
204+
public $key3 =>
205+
string(6) "value3"
206+
}
207+
}
208+
}
209+
*/
135210
```

Diff for: files/constants.php

+1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
const OBJECT_MERGE_OPT_CONFLICT_OVERWRITE = 0x0;
2020
const OBJECT_MERGE_OPT_CONFLICT_EXCEPTION = 0x1;
2121
const OBJECT_MERGE_OPT_UNIQUE_ARRAYS = 0x2;
22+
const OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES = 0x4;
2223

2324
define('OBJECT_MERGE_UNDEFINED', uniqid('__OBJECT_MERGE_UNDEFINED_'));

Diff for: src/ObjectMerge.php

+46-22
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public static function mergeRecursiveOpts($opts, stdClass ...$objects)
106106
*/
107107
private static function optSet($opts, $opt)
108108
{
109-
return $opt === ($opts & (1 << ($opt - 1)));
109+
return 0 !== ($opts & $opt);
110110
}
111111

112112
/**
@@ -160,19 +160,39 @@ private static function compareTypes($left, $right)
160160
*/
161161
private static function mergeArrayValues($recurse, $opts, array $leftValue, array $rightValue)
162162
{
163-
$out = array_merge($leftValue, $rightValue);
164-
165-
foreach ($out as $i => &$v) {
166-
$vt = gettype($v);
167-
if (self::OBJECT_T === $vt) {
168-
$v = self::mergeObjectValues($recurse, $opts, new stdClass(), $v);
169-
} elseif (self::ARRAY_T === $vt) {
170-
$v = self::mergeArrayValues($recurse, $opts, [], $v);
163+
if (self::optSet($opts, OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES)) {
164+
$out = [];
165+
166+
$lc = count($leftValue);
167+
$rc = count($rightValue);
168+
$limit = $lc > $rc ? $lc : $rc;
169+
170+
for ($i = 0; $i < $limit; $i++) {
171+
$leftDefined = array_key_exists($i, $leftValue);
172+
$rightDefined = array_key_exists($i, $rightValue);
173+
$out[$i] = self::mergeValues(
174+
$recurse,
175+
$opts,
176+
$i,
177+
$leftDefined ? $leftValue[$i] : OBJECT_MERGE_UNDEFINED,
178+
$rightDefined ? $rightValue[$i] : OBJECT_MERGE_UNDEFINED
179+
);
180+
}
181+
} else {
182+
$out = array_merge($leftValue, $rightValue);
183+
184+
foreach ($out as $i => &$v) {
185+
$vt = gettype($v);
186+
if (self::OBJECT_T === $vt) {
187+
$v = self::mergeObjectValues($recurse, $opts, new stdClass(), $v);
188+
} elseif (self::ARRAY_T === $vt) {
189+
$v = self::mergeArrayValues($recurse, $opts, [], $v);
190+
}
171191
}
172192
}
173193

174194
if (self::optSet($opts, OBJECT_MERGE_OPT_UNIQUE_ARRAYS)) {
175-
return array_values(array_unique($out));
195+
return array_values(array_unique($out, SORT_REGULAR));
176196
}
177197

178198
return $out;
@@ -238,17 +258,21 @@ private static function mergeValues($recurse, $opts, $key, $leftValue, $rightVal
238258
$leftValue = self::newEmptyValue($rightValue);
239259
}
240260

241-
list($leftType, $rightType, $equal) = self::compareTypes($leftValue, $rightValue);
242-
243-
if (!$equal && self::optSet($opts, OBJECT_MERGE_OPT_CONFLICT_EXCEPTION)) {
244-
throw new UnexpectedValueException(
245-
sprintf(
246-
'Field "%s" has type "%s" on incoming object, but has type "%s" on the root object',
247-
$key,
248-
$rightType,
249-
$leftType
250-
)
251-
);
261+
list($leftType, $rightType, $equalTypes) = self::compareTypes($leftValue, $rightValue);
262+
263+
if (!$equalTypes) {
264+
if (self::optSet($opts, OBJECT_MERGE_OPT_CONFLICT_EXCEPTION)) {
265+
throw new UnexpectedValueException(
266+
sprintf(
267+
'Field "%s" has type "%s" on incoming object, but has type "%s" on the root object',
268+
$key,
269+
$rightType,
270+
$leftType
271+
)
272+
);
273+
}
274+
// todo: revisit this, inefficient.
275+
return self::mergeValues($recurse, $opts, $key, self::newEmptyValue($rightValue), $rightValue);
252276
}
253277

254278
if (!$recurse || in_array($leftType, self::$_SIMPLE_TYPES, true)) {
@@ -286,7 +310,7 @@ private static function doMerge($recurse, $opts, array $objects)
286310
continue;
287311
}
288312

289-
$root = self::mergeObjectValues($recurse, $opts, $root, $object);
313+
$root = self::mergeObjectValues($recurse, $opts, $root, !$recurse ? clone $object : $object);
290314
}
291315

292316
return $root;

Diff for: tests/ObjectMergeTest.php

+31-1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,36 @@ class ObjectMergeTest extends TestCase
123123
'expected' => '{"glossary":{"GlossDiv":{"GlossList":{"GlossEntry":{"Abbrev":"ISO 8879:1986","Acronym":"SGML","GlossDef":{"GlossSeeAlso":["GML","XML"],"leftOnly":["things"],"para":"A meta-markup language, used to create markup languages such as DocBook."},"GlossSee":"markup","GlossTerm":"Standard Generalized Markup Language","ID":"SGML","SortAs":"SGML","bothSides":{"leftKey":"leftValue","rightKey":"rightValue"},"leftOnly":{"leftKey":"leftValue"},"rightOnly":{"rightKey":"rightKey"}}},"leftOnly":"hello","title":"S"},"rightOnly":"hello","title":"example glossary"}}',
124124
'recurse' => true,
125125
'opts' => OBJECT_MERGE_OPT_UNIQUE_ARRAYS,
126+
],
127+
[
128+
'objects' => [
129+
'{"arr":[{"key1":"value1"}]}',
130+
'{"arr":[{"key2":"value2"}]}',
131+
'{"arr":[{"key3":"value3"}]}',
132+
],
133+
'expected' => '{"arr":[{"key1":"value1","key2":"value2","key3":"value3"}]}',
134+
'recurse' => true,
135+
'opts' => OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES | OBJECT_MERGE_OPT_UNIQUE_ARRAYS
136+
],
137+
[
138+
'objects' => [
139+
'{"arr":[{"key1":"value1","arr":[{"key11":"value11"}]}]}',
140+
'{"arr":[{"key2":"value2","arr":[{"key22":"value22"}]}]}',
141+
'{"arr":[{"key3":"value3","arr":[{"key33":"value33"}]}]}',
142+
],
143+
'expected' => '{"arr":[{"key1":"value1","key2":"value2","key3":"value3","arr":[{"key11":"value11","key22":"value22","key33":"value33"}]}]}',
144+
'recurse' => true,
145+
'opts' => OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES | OBJECT_MERGE_OPT_UNIQUE_ARRAYS
146+
],
147+
[
148+
'objects' => [
149+
'{"arr":[{"key1":"value1","arr":[{"key11":"value11"}]}]}',
150+
'{"arr":["not an array"]}',
151+
'{"arr":[7]}',
152+
],
153+
'expected' => '{"arr":[7]}',
154+
'recurse' => true,
155+
'opts' => OBJECT_MERGE_OPT_MERGE_ARRAY_VALUES | OBJECT_MERGE_OPT_UNIQUE_ARRAYS
126156
]
127157
);
128158

@@ -137,7 +167,7 @@ private function doDecode($test, $json)
137167
if (JSON_ERROR_NONE !== json_last_error()) {
138168
throw new RuntimeException(
139169
sprintf(
140-
'json_decode returned error while processing test "%s": %s; json =%s',
170+
'json_decode returned error while processing test "%s": %s; json=%s',
141171
$test,
142172
json_last_error_msg(),
143173
$json

0 commit comments

Comments
 (0)