Skip to content

Commit

Permalink
handle unique arrays, make appending value arrays the default (#2)
Browse files Browse the repository at this point in the history
* handle unique arrays, make appending value arrays the default

* update readme with unique flag info

* use base interface for flags in helper class
  • Loading branch information
Harry Bragg authored Sep 12, 2018
1 parent b4a69d0 commit 029b268
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 86 deletions.
62 changes: 46 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![Quality Score](https://img.shields.io/scrutinizer/g/graze/array-merger.svg?style=flat-square)](https://scrutinizer-ci.com/g/graze/array-merger)
[![Total Downloads](https://img.shields.io/packagist/dt/graze/array-merger.svg?style=flat-square)](https://packagist.org/packages/graze/array-merger)

Array Merge allows you to recursively merge arrays and choose how the values should be merged.
Array Merger allows you to recursively merge arrays and choose how the values should be merged.

![Merge](https://media.giphy.com/media/cnEXDpXvkZ7lm/giphy.gif)

Expand All @@ -16,18 +16,27 @@ Array Merge allows you to recursively merge arrays and choose how the values sho
The php function: [`array_merge_recursive`](http://php.net/manual/en/function.array-merge-recursive.php)
does indeed merge arrays, but it converts values with duplicate keys to arrays rather than overwriting the value in
the first array with the duplicate value in the second array, as array_merge does. I.e., with array_merge_recursive,
this happens (documented behaviour):
this happens (documented behaviour).

```php
array_merge_recursive(['key' => 'org value'], ['key' => 'new value']);
// ['key' => ['org value', 'new value']];
array_merge_recursive(['key' => 'org value', 'key2' => 'first'], ['key' => 'new value', 'key2' => null]);
// ['key' => ['org value', 'new value'], 'key2' => ['first', null]];
```

This library allows you to get the value you actually want
There is also [`array_replace_recursive`](http://php.net/manual/en/function.array-replace-recursive.php)
which replaces values in the first with values in the second, but it handles value arrays differently and only supports
replacing with the last value.

```php
RecursiveArrayMerger::last(['key' => 'org value'], ['key' => 'new value']);
// ['key' => 'new value'];
array_replace_recursive(['key' => 'org value', 'key2' => 'first'], ['key' => 'new value', 'key2' => null]);
// ['key' => 'new value', 'key2' => null];
```

This library gives you the flexibility to ensure you get the values you actually want in the merge.

```php
RecursiveArrayMerger::lastNonNull(['key' => 'org value', 'key2' => 'first'], ['key' => 'new value', 'key2' => null]);
// ['key' => 'new value', 'key2' => 'first']);
```

## Install
Expand All @@ -40,14 +49,14 @@ composer require graze/array-merger

## Value Mergers

- **LastValue**: Takes the last value (default)
- **LastValue**: Takes the last value (default) _equivalent to `array_replace_recursive`_
- **LastNonNullValue**: Takes the last value, unless it is null then the first
- **FirstValue**: Takes the first value
- **FirstNonNullValue**: Takes the first value, unless it is null, then the second
- **RandomValue**: Takes a random value
- **SumValue**: If both values are numeric, will add them together
- **ProductValue**: If both values are numeric, will multiply them together
- **BothValues**: Will return both values in an array, (same as `array_merge_recursive`)
- **BothValues**: Will return both values in an array, _equivalent to `array_merge_recursive`_

## Usage

Expand Down Expand Up @@ -137,15 +146,36 @@ RecursiveArrayMerger::lastNonNull(

### Value Arrays

By default value arrays (arrays with no indexes supplied) will be treated as associated arrays and have their keys
merged. To append the second item instead, you can supply a flag:
By default value arrays (arrays with no indexes supplied) will be appended onto each other, if you want to treat them
as associative arrays, you can supply this flag: `RecursiveArrayMerger::FLAG_MERGE_VALUE_ARRAY`.

```php
$merger = new Graze\ArrayMerger\RecursiveArrayMerger(new LastValue(), RecursiveArrayMerger::FLAG_APPEND_VALUE_ARRAY);
$merger->merge(
['a' => 'first', 'b' => ['a','c','d']],
['a' => 'second', 'b' => ['e']]
);
$a = ['a' => 'first', 'b' => ['a','c','d']];
$b = ['a' => 'second', 'b' => ['e']];
$merger = new Graze\ArrayMerger\RecursiveArrayMerger(new LastValue());
$merger->merge($a,$b);
// ['a' => 'second', 'b' => ['a','c','d','e']]

$merger = new Graze\ArrayMerger\RecursiveArrayMerger(new LastValue(), RecursiveArrayMerger::FLAG_MERGE_VALUE_ARRAY);
$merger->merge($a,$b);
// ['a' => 'second', 'b' => ['e','c','d']]
```

#### Unique Values

By default, when we append value arrays it will keep duplicate values. If you want to remove the duplicate values you
can supply the flag: `RecursiveArrayMerger::FLAG_UNIQUE_VALUE_ARRAY`.
This flag will only be relevant if the `RecursiveArrayMerger::FLAG_MERGE_VALUE_ARRAY` flag is not supplied.

```php
$a = ['a' => 'first', 'b' => ['a','c','d']];
$b = ['a' => 'second', 'b' => ['d','e']];
$merger = new Graze\ArrayMerger\RecursiveArrayMerger(new LastValue());
$merger->merge($a,$b);
// ['a' => 'second', 'b' => ['a','c','d','d','e']]

$merger = new Graze\ArrayMerger\RecursiveArrayMerger(new LastValue(), RecursiveArrayMerger::FLAG_UNIQUE_VALUE_ARRAY);
$merger->merge($a,$b);
// ['a' => 'second', 'b' => ['a','c','d','e']]
```

Expand Down
25 changes: 3 additions & 22 deletions src/ArrayMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,10 @@
*/
class ArrayMerger implements ArrayMergerInterface
{
use StaticMethodsTrait;
use SequentialTrait;
use MergeHelpersTrait;

/** @var callable */
protected $valueMerger;
/** @var int */
private $flags;

/**
* @param callable $valueMerger
Expand Down Expand Up @@ -54,28 +51,12 @@ public static function mergeUsing(callable $valueMerger, array $array1, array $a
*/
public function merge(array $array1, array $arrays = null)
{
$arrays = array_slice(func_get_args(), 1);
if (count($arrays) === 0) {
return $array1;
}

// if all arrays are sequential and flag is set, append them all
if ($this->flags & static::FLAG_APPEND_VALUE_ARRAY == static::FLAG_APPEND_VALUE_ARRAY
&& $this->areSequential(array_merge([$array1], $arrays))) {
return call_user_func_array('array_merge', array_merge([$array1], $arrays));
}

$merged = $array1;
list($merged, $arrays) = $this->checkSimpleMerge($array1, array_slice(func_get_args(), 1));

foreach ($arrays as $toMerge) {
foreach ($toMerge as $key => &$value) {
if (array_key_exists($key, $merged)) {
if ($this->flags & static::FLAG_APPEND_VALUE_ARRAY == static::FLAG_APPEND_VALUE_ARRAY
&& $this->areSequential([$value, $merged[$key]])) {
$merged[$key] = array_merge($merged[$key], $value);
} else {
$merged[$key] = call_user_func($this->valueMerger, $merged[$key], $value);
}
$merged[$key] = call_user_func($this->valueMerger, $merged[$key], $value);
} else {
$merged[$key] = $value;
}
Expand Down
35 changes: 31 additions & 4 deletions src/ArrayMergerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,45 @@ interface ArrayMergerInterface
{
/**
* If 2 elements are both value arrays ['a','b','c'], etc.
* This will append the second array onto the first.
* This will treat the arrays as associative and replace the values using their indexes.
*
* Example:
* Flag off:
*
* ```php
* merge(['a' => ['a','b','c']],['b' => ['d','e','f']]);
* // ['a' => ['a','b','c','d','e','f']]
* ```
*
* If it is off, the first value will be replaced by the second
* Flag on:
*
* ```php
* merge(['a' => ['a','b','c']],['b' => ['d','e']]);
* // ['a' => ['d','e','c']]
* ```
*/
const FLAG_MERGE_VALUE_ARRAY = 1;

/**
* When appending value arrays (if FLAG_MERGE_VALUE_ARRAY is not set) it will include duplicate entries if both
* arrays have the same value.
*
* This flag will remove duplicate values from value arrays
*
* Flag off:
*
* ```php
* merge(['a' => ['a','b','c']],['b' => ['c','d','e']]);
* // ['a' => ['a', 'b', 'c', 'c', 'd', 'e']
* ```
*
* Flag on:
*
* ```php
* merge(['a' => ['a','b','c']],['b' => ['c','d','e']]);
* // ['a' => ['a', 'b', 'c', 'd', 'e']
* ```
*/
const FLAG_APPEND_VALUE_ARRAY = 1;
const FLAG_UNIQUE_VALUE_ARRAY = 2;

/**
* Merge the values from the subsequent set of arrays into the first array
Expand Down
21 changes: 21 additions & 0 deletions src/FlagTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Graze\ArrayMerger;

trait FlagTrait
{
/** @var int */
protected $flags;

/**
* Is the provided flag set?
*
* @param int $flag
*
* @return bool
*/
protected function isFlagSet($flag)
{
return ($this->flags & $flag) === $flag;
}
}
30 changes: 29 additions & 1 deletion src/StaticMethodsTrait.php → src/MergeHelpersTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,36 @@
use Graze\ArrayMerger\ValueMerger\RandomValue;
use Graze\ArrayMerger\ValueMerger\SumValue;

trait StaticMethodsTrait
trait MergeHelpersTrait
{
use SequentialTrait;
use FlagTrait;

/**
* @param array $array1
* @param array $arrays
*
* @return array
*/
protected function checkSimpleMerge(array $array1, array $arrays = [])
{
if (count($arrays) === 0) {
return [$array1, []];
}

// if all arrays are sequential and merge flag is not set, append them all
if (!$this->isFlagSet(ArrayMergerInterface::FLAG_MERGE_VALUE_ARRAY)
&& $this->areSequential(array_merge([$array1], $arrays))) {
$merged = call_user_func_array('array_merge', array_merge([$array1], $arrays));
if ($this->isFlagSet(ArrayMergerInterface::FLAG_UNIQUE_VALUE_ARRAY)) {
$merged = array_values(array_unique($merged));
}
return [$merged, []];
}

return [$array1, $arrays];
}

/**
* Merge using the FirstNonNull Value Merger
*
Expand Down
29 changes: 2 additions & 27 deletions src/RecursiveArrayMerger.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,12 @@

use Graze\ArrayMerger\ValueMerger\LastValue;

/**
* Class RecursiveArrayMerger
*
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
* keys to arrays rather than overwriting the value in the first array with the duplicate
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
* this happens (documented behavior):
*
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
* => array('key' => array('org value', 'new value'));
*/
class RecursiveArrayMerger implements ArrayMergerInterface
{
use StaticMethodsTrait;
use SequentialTrait;
use MergeHelpersTrait;

/** @var callable */
protected $valueMerger;
/** @var int */
private $flags;

/**
* @param callable $valueMerger
Expand Down Expand Up @@ -60,18 +46,7 @@ public static function mergeUsing(callable $valueMerger, array $array1, array $a
*/
public function merge(array $array1, array $arrays = null)
{
$arrays = array_slice(func_get_args(), 1);
if (count($arrays) === 0) {
return $array1;
}

// if all arrays are sequential and flag is set, append them all
if ($this->flags & static::FLAG_APPEND_VALUE_ARRAY == static::FLAG_APPEND_VALUE_ARRAY
&& $this->areSequential(array_merge([$array1], $arrays))) {
return call_user_func_array('array_merge', array_merge([$array1], $arrays));
}

$merged = $array1;
list($merged, $arrays) = $this->checkSimpleMerge($array1, array_slice(func_get_args(), 1));

foreach ($arrays as $toMerge) {
foreach ($toMerge as $key => &$value) {
Expand Down
47 changes: 38 additions & 9 deletions tests/unit/ArrayMergerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,32 +74,61 @@ public function testSequentialTopLevelArraysAreAppended()

$merger = new ArrayMerger(new FirstValue());

$this->assertEquals(
['first', 'second', 'third', 'fourth'],
$merger->merge($a, $b),
"Expected appended array"
);
$merger = new ArrayMerger(new FirstValue(), ArrayMerger::FLAG_MERGE_VALUE_ARRAY);

$this->assertEquals(
['first', 'second'],
$merger->merge($a, $b),
"Expected a non appended array when the flag is not set"
);
}

$merger = new ArrayMerger(new FirstValue(), ArrayMerger::FLAG_APPEND_VALUE_ARRAY);
public function testSequentialChildArraysAreNotAppendedBecauseThisIsNotRecursive()
{
$a = ['first' => ['a', 'c', 'd'], 'second' => 2];
$b = ['first' => ['b', 'e'], 'second' => null];

$merger = new ArrayMerger(new LastValue());

$this->assertEquals(
['first', 'second', 'third', 'fourth'],
['first' => ['b', 'e'], 'second' => null],
$merger->merge($a, $b),
"Expected appended array"
"Expected appended child array"
);

$merger = new ArrayMerger(new LastValue(), ArrayMerger::FLAG_MERGE_VALUE_ARRAY);

$this->assertEquals(
['first' => ['b', 'e'], 'second' => null],
$merger->merge($a, $b),
"Expected merged child array"
);
}

public function testSequentialChildArraysAreAppended()
public function testSequentialTopLevelArraysCanBeUnique()
{
$a = ['first' => ['a','c','d'], 'second' => 2];
$b = ['first' => ['b','e'], 'second' => null];
$a = ['first', 'second'];
$b = ['second', 'third'];

$merger = new ArrayMerger(new FirstValue());

$this->assertEquals(
['first', 'second', 'second', 'third'],
$merger->merge($a, $b),
"Expected duplicated second value"
);

$merger = new ArrayMerger(new FirstValue(), ArrayMerger::FLAG_APPEND_VALUE_ARRAY);
$merger = new ArrayMerger(new FirstValue(), ArrayMerger::FLAG_UNIQUE_VALUE_ARRAY);

$this->assertEquals(
['first' => ['a','c','d','b','e'], 'second' => 2],
['first', 'second', 'third'],
$merger->merge($a, $b),
"Expected appended child array"
"Expected unique output"
);
}

Expand Down
Loading

0 comments on commit 029b268

Please sign in to comment.