├── Diff.php ├── src ├── Patcher │ ├── PatcherException.php │ ├── Patcher.php │ ├── PreviewablePatcher.php │ ├── ListPatcher.php │ ├── ThrowingPatcher.php │ └── MapPatcher.php ├── Comparer │ ├── ValueComparer.php │ ├── StrictComparer.php │ ├── ComparableComparer.php │ └── CallbackComparer.php ├── ArrayComparer │ ├── NativeArrayComparer.php │ ├── ArrayComparer.php │ ├── StrictArrayComparer.php │ ├── StrategicArrayComparer.php │ └── OrderedArrayComparer.php ├── Differ │ ├── Differ.php │ ├── CallbackListDiffer.php │ ├── OrderedListDiffer.php │ ├── ListDiffer.php │ └── MapDiffer.php ├── DiffOp │ ├── AtomicDiffOp.php │ ├── DiffOp.php │ ├── DiffOpAdd.php │ ├── DiffOpRemove.php │ ├── DiffOpChange.php │ └── Diff │ │ └── Diff.php └── DiffOpFactory.php ├── COPYING ├── README.md └── RELEASE-NOTES.md /Diff.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class PatcherException extends RuntimeException { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/Comparer/ValueComparer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface ValueComparer { 16 | 17 | /** 18 | * @since 0.6 19 | * 20 | * @param mixed $firstValue 21 | * @param mixed $secondValue 22 | * 23 | * @return bool 24 | */ 25 | public function valuesAreEqual( $firstValue, $secondValue ): bool; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Comparer/StrictComparer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class StrictComparer implements ValueComparer { 16 | 17 | /** 18 | * @param mixed $firstValue 19 | * @param mixed $secondValue 20 | * 21 | * @return bool 22 | */ 23 | public function valuesAreEqual( $firstValue, $secondValue ): bool { 24 | return $firstValue === $secondValue; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Patcher/Patcher.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface Patcher { 18 | 19 | /** 20 | * Applies the applicable operations from the provided diff to 21 | * the provided base value. 22 | * 23 | * @since 0.4 24 | * 25 | * @param array $base 26 | * @param Diff $diffOps 27 | * 28 | * @return array 29 | */ 30 | public function patch( array $base, Diff $diffOps ): array; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/ArrayComparer/NativeArrayComparer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class NativeArrayComparer implements ArrayComparer { 16 | 17 | /** 18 | * @see ArrayComparer::diffArrays 19 | * 20 | * Uses @see array_diff. 21 | * 22 | * @since 0.8 23 | * 24 | * @param array $arrayOne 25 | * @param array $arrayTwo 26 | * 27 | * @return array 28 | */ 29 | public function diffArrays( array $arrayOne, array $arrayTwo ): array { 30 | return array_diff( $arrayOne, $arrayTwo ); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Comparer/ComparableComparer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ComparableComparer implements ValueComparer { 16 | 17 | /** 18 | * @param mixed $firstValue 19 | * @param mixed $secondValue 20 | * @return bool 21 | */ 22 | public function valuesAreEqual( $firstValue, $secondValue ): bool { 23 | if ( $firstValue && method_exists( $firstValue, 'equals' ) ) { 24 | return $firstValue->equals( $secondValue ); 25 | } 26 | 27 | return false; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Differ/Differ.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | interface Differ { 19 | 20 | /** 21 | * Takes two arrays, computes the diff, and returns this diff as an array of DiffOp. 22 | * 23 | * @since 0.4 24 | * 25 | * @param array $oldValues The first array 26 | * @param array $newValues The second array 27 | * 28 | * @throws Exception 29 | * @return DiffOp[] 30 | */ 31 | public function doDiff( array $oldValues, array $newValues ): array; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/ArrayComparer/ArrayComparer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | interface ArrayComparer { 17 | 18 | /** 19 | * Returns an array containing all the entries from arrayOne that are not present in arrayTwo. 20 | * 21 | * Implementations are allowed to hold quantity into account or to disregard it. 22 | * 23 | * @since 0.8 24 | * 25 | * @param array $firstArray 26 | * @param array $secondArray 27 | * 28 | * @return array 29 | */ 30 | public function diffArrays( array $firstArray, array $secondArray ): array; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Patcher/PreviewablePatcher.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | interface PreviewablePatcher extends Patcher { 21 | 22 | /** 23 | * Returns the operations that can be applied to the base. 24 | * The returned operations are thus the difference between 25 | * the result of @see patch and it's input base value. 26 | * 27 | * @since 0.4 28 | * 29 | * @param array $base 30 | * @param Diff $diffOps 31 | * 32 | * @return Diff 33 | */ 34 | public function getApplicableDiff( array $base, Diff $diffOps ); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Comparer/CallbackComparer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class CallbackComparer implements ValueComparer { 17 | 18 | /** @var callable */ 19 | private $callback; 20 | 21 | /** 22 | * @since 0.6 23 | * 24 | * @param callable $callback 25 | */ 26 | public function __construct( $callback ) { 27 | $this->callback = $callback; 28 | } 29 | 30 | /** 31 | * @param mixed $firstValue 32 | * @param mixed $secondValue 33 | * @return bool 34 | */ 35 | public function valuesAreEqual( $firstValue, $secondValue ): bool { 36 | $valuesAreEqual = call_user_func_array( $this->callback, [ $firstValue, $secondValue ] ); 37 | 38 | if ( !is_bool( $valuesAreEqual ) ) { 39 | throw new \RuntimeException( 'ValueComparer callback needs to return a boolean' ); 40 | } 41 | 42 | return $valuesAreEqual; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Differ/CallbackListDiffer.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class CallbackListDiffer implements Differ { 23 | 24 | /** 25 | * @var ListDiffer 26 | */ 27 | private $differ; 28 | 29 | /** 30 | * @since 0.5 31 | * 32 | * @param callable $comparisonCallback 33 | */ 34 | public function __construct( $comparisonCallback ) { 35 | $this->differ = new ListDiffer( 36 | new StrategicArrayComparer( new CallbackComparer( $comparisonCallback ) ) 37 | ); 38 | } 39 | 40 | /** 41 | * @see Differ::doDiff 42 | * 43 | * @since 0.5 44 | * 45 | * @param array $oldValues The first array 46 | * @param array $newValues The second array 47 | * 48 | * @return DiffOp[] 49 | */ 50 | public function doDiff( array $oldValues, array $newValues ): array { 51 | return $this->differ->doDiff( $oldValues, $newValues ); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Differ/OrderedListDiffer.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Tobias Gritschacher < tobias.gritschacher@wikimedia.de > 22 | */ 23 | class OrderedListDiffer implements Differ { 24 | 25 | /** 26 | * @var ListDiffer 27 | */ 28 | private $differ; 29 | 30 | /** 31 | * @since 0.9 32 | * 33 | * @param ValueComparer $comparer 34 | */ 35 | public function __construct( ValueComparer $comparer ) { 36 | $this->differ = new ListDiffer( new OrderedArrayComparer( $comparer ) ); 37 | } 38 | 39 | /** 40 | * @see Differ::doDiff 41 | * 42 | * @since 0.9 43 | * 44 | * @param array $oldValues The first array 45 | * @param array $newValues The second array 46 | * 47 | * @return DiffOp[] 48 | */ 49 | public function doDiff( array $oldValues, array $newValues ): array { 50 | return $this->differ->doDiff( $oldValues, $newValues ); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/ArrayComparer/StrictArrayComparer.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class StrictArrayComparer implements ArrayComparer { 24 | 25 | /** 26 | * @see ArrayComparer::diffArrays 27 | * 28 | * @since 0.8 29 | * 30 | * @param array $arrayOne 31 | * @param array $arrayTwo 32 | * 33 | * @return array 34 | */ 35 | public function diffArrays( array $arrayOne, array $arrayTwo ): array { 36 | $notInTwo = []; 37 | 38 | foreach ( $arrayOne as $element ) { 39 | $location = array_search( $element, $arrayTwo, !is_object( $element ) ); 40 | 41 | if ( $location === false ) { 42 | $notInTwo[] = $element; 43 | continue; 44 | } 45 | 46 | unset( $arrayTwo[$location] ); 47 | } 48 | 49 | return $notInTwo; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012-2018, Wikimedia Deutschland e. V. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/DiffOp/AtomicDiffOp.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Daniel Kinzler 16 | */ 17 | abstract class AtomicDiffOp implements DiffOp { 18 | 19 | /** 20 | * @see Countable::count 21 | * 22 | * @since 0.1 23 | * 24 | * @return int 25 | */ 26 | public function count(): int { 27 | return 1; 28 | } 29 | 30 | /** 31 | * @see DiffOp::isAtomic 32 | * 33 | * @since 0.1 34 | * 35 | * @return bool 36 | */ 37 | public function isAtomic(): bool { 38 | return true; 39 | } 40 | 41 | /** 42 | * Converts an object to an array using the given callback function. 43 | * If the convert callback is null or the value is not an object, the value is returned 44 | * unchanged. The Converter callback is intended for converting the value into an array, 45 | * but may also just leave the value unchanged if it cannot handle it. 46 | * 47 | * @since 0.5 48 | * 49 | * @param mixed $value The value to convert 50 | * @param callable|null $valueConverter The converter to use if $value is an object 51 | * 52 | * @return mixed The $value unchanged, or the return value of calling $valueConverter on $value. 53 | */ 54 | protected function objectToArray( $value, $valueConverter = null ) { 55 | if ( $valueConverter !== null && is_object( $value ) ) { 56 | $value = call_user_func( $valueConverter, $value ); 57 | } 58 | 59 | return $value; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/ArrayComparer/StrategicArrayComparer.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class StrategicArrayComparer implements ArrayComparer { 21 | 22 | private ValueComparer $valueComparer; 23 | 24 | public function __construct( ValueComparer $valueComparer ) { 25 | $this->valueComparer = $valueComparer; 26 | } 27 | 28 | /** 29 | * @see ArrayComparer::diffArrays 30 | * 31 | * @since 0.8 32 | * 33 | * @param array $arrayOne 34 | * @param array $arrayTwo 35 | * 36 | * @return array 37 | */ 38 | public function diffArrays( array $arrayOne, array $arrayTwo ): array { 39 | $notInTwo = []; 40 | 41 | foreach ( $arrayOne as $element ) { 42 | $valueOffset = $this->arraySearch( $element, $arrayTwo ); 43 | 44 | if ( $valueOffset === false ) { 45 | $notInTwo[] = $element; 46 | continue; 47 | } 48 | 49 | unset( $arrayTwo[$valueOffset] ); 50 | } 51 | 52 | return $notInTwo; 53 | } 54 | 55 | /** 56 | * @param string|int $needle 57 | * @param array $haystack 58 | * 59 | * @return bool|int|string 60 | */ 61 | private function arraySearch( $needle, array $haystack ) { 62 | foreach ( $haystack as $valueOffset => $thing ) { 63 | if ( $this->valueComparer->valuesAreEqual( $needle, $thing ) ) { 64 | return $valueOffset; 65 | } 66 | } 67 | 68 | return false; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Patcher/ListPatcher.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ListPatcher extends ThrowingPatcher { 18 | 19 | /** 20 | * @see Patcher::patch 21 | * 22 | * Applies the provided diff to the provided array and returns the result. 23 | * The provided diff needs to be non-associative. In other words, calling 24 | * isAssociative on it should return false. 25 | * 26 | * Note that remove operations can introduce gaps into the input array $base. 27 | * For instance, when the input is [ 0 => 'a', 1 => 'b', 2 => 'c' ], and there 28 | * is one remove operation for 'b', the result will be [ 0 => 'a', 2 => 'c' ]. 29 | * 30 | * @since 0.4 31 | * 32 | * @param array $base 33 | * @param Diff $diff 34 | * 35 | * @return array 36 | * @throws PatcherException 37 | */ 38 | public function patch( array $base, Diff $diff ): array { 39 | if ( $diff->looksAssociative() ) { 40 | $this->handleError( 'ListPatcher can only patch using non-associative diffs' ); 41 | } 42 | 43 | foreach ( $diff as $diffOp ) { 44 | if ( $diffOp instanceof DiffOpAdd ) { 45 | $base[] = $diffOp->getNewValue(); 46 | } elseif ( $diffOp instanceof DiffOpRemove ) { 47 | $needle = $diffOp->getOldValue(); 48 | $key = array_search( $needle, $base, !is_object( $needle ) ); 49 | 50 | if ( $key === false ) { 51 | $this->handleError( 'Cannot remove an element from a list if it is not present' ); 52 | continue; 53 | } 54 | 55 | unset( $base[$key] ); 56 | } 57 | } 58 | 59 | return $base; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/ArrayComparer/OrderedArrayComparer.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Tobias Gritschacher < tobias.gritschacher@wikimedia.de > 21 | */ 22 | class OrderedArrayComparer implements ArrayComparer { 23 | 24 | private ValueComparer $valueComparer; 25 | 26 | public function __construct( ValueComparer $valueComparer ) { 27 | $this->valueComparer = $valueComparer; 28 | } 29 | 30 | /** 31 | * @see ArrayComparer::diffArrays 32 | * 33 | * @since 0.9 34 | * 35 | * @param array $arrayOne 36 | * @param array $arrayTwo 37 | * 38 | * @return array 39 | */ 40 | public function diffArrays( array $arrayOne, array $arrayTwo ): array { 41 | $notInTwo = []; 42 | 43 | foreach ( $arrayOne as $valueOffset => $element ) { 44 | if ( !$this->arraySearch( $element, $arrayTwo, $valueOffset ) ) { 45 | $notInTwo[] = $element; 46 | continue; 47 | } 48 | 49 | unset( $arrayTwo[$valueOffset] ); 50 | } 51 | 52 | return $notInTwo; 53 | } 54 | 55 | /** 56 | * @param string|int $needle 57 | * @param array $haystack 58 | * @param int|string $valueOffset 59 | * 60 | * @return bool 61 | */ 62 | private function arraySearch( $needle, array $haystack, $valueOffset ): bool { 63 | if ( array_key_exists( $valueOffset, $haystack ) ) { 64 | return $this->valueComparer->valuesAreEqual( $needle, $haystack[$valueOffset] ); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/Differ/ListDiffer.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class ListDiffer implements Differ { 25 | 26 | /** 27 | * @var ArrayComparer 28 | */ 29 | private $arrayComparer; 30 | 31 | public function __construct( ?ArrayComparer $arrayComparer = null ) { 32 | $this->arrayComparer = $arrayComparer ?? new StrictArrayComparer(); 33 | } 34 | 35 | /** 36 | * @see Differ::doDiff 37 | * 38 | * @since 0.4 39 | * 40 | * @param array $oldValues The first array 41 | * @param array $newValues The second array 42 | * 43 | * @return DiffOp[] 44 | */ 45 | public function doDiff( array $oldValues, array $newValues ): array { 46 | $operations = []; 47 | 48 | foreach ( $this->diffArrays( $newValues, $oldValues ) as $addition ) { 49 | $operations[] = new DiffOpAdd( $addition ); 50 | } 51 | 52 | foreach ( $this->diffArrays( $oldValues, $newValues ) as $removal ) { 53 | $operations[] = new DiffOpRemove( $removal ); 54 | } 55 | 56 | return $operations; 57 | } 58 | 59 | /** 60 | * @param array $arrayOne 61 | * @param array $arrayTwo 62 | * 63 | * @return array 64 | */ 65 | private function diffArrays( array $arrayOne, array $arrayTwo ): array { 66 | return $this->arrayComparer->diffArrays( $arrayOne, $arrayTwo ); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/DiffOp/DiffOp.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | interface DiffOp extends Serializable, Countable { 22 | 23 | /** 24 | * Returns a string identifier for the operation type. 25 | * 26 | * @since 0.1 27 | * 28 | * @return string 29 | */ 30 | public function getType(): string; 31 | 32 | /** 33 | * Returns if the operation is atomic, opposing to it 34 | * being a composite that can contain one or more child elements. 35 | * 36 | * @since 0.1 37 | * 38 | * @return bool 39 | */ 40 | public function isAtomic(): bool; 41 | 42 | /** 43 | * Returns the DiffOp in array form. 44 | * 45 | * All element of the array with either be primitives or arrays, with the exception 46 | * of complex values. For instance an add operation containing an object will have this 47 | * object in the resulting array. 48 | * 49 | * This array form is particularly useful for serialization, as you can feed it 50 | * to serialization functions such as json_encode() or serialize(), keeping in mind 51 | * you might need extra handling for complex objects contained in the DiffOp. 52 | * 53 | * Roundtrips with DiffOpFactory::newFromArray. 54 | * 55 | * @since 0.5 56 | * 57 | * @param callable|null $valueConverter optional callback used to convert any 58 | * complex values to arrays. 59 | * 60 | * @return array 61 | */ 62 | public function toArray( ?callable $valueConverter = null ): array; 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/Patcher/ThrowingPatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | abstract class ThrowingPatcher implements PreviewablePatcher { 22 | 23 | /** 24 | * @var bool 25 | */ 26 | private $throwErrors; 27 | 28 | /** 29 | * @since 0.4 30 | * 31 | * @param bool $throwErrors 32 | */ 33 | public function __construct( bool $throwErrors = false ) { 34 | $this->throwErrors = $throwErrors; 35 | } 36 | 37 | /** 38 | * @since 0.4 39 | * 40 | * @param string $message 41 | * 42 | * @throws PatcherException 43 | */ 44 | protected function handleError( string $message ) { 45 | if ( $this->throwErrors ) { 46 | throw new PatcherException( $message ); 47 | } 48 | } 49 | 50 | /** 51 | * Set the patcher to ignore errors. 52 | * 53 | * @since 0.4 54 | */ 55 | public function ignoreErrors() { 56 | $this->throwErrors = false; 57 | } 58 | 59 | /** 60 | * Set the patcher to throw errors. 61 | * 62 | * @since 0.4 63 | */ 64 | public function throwErrors() { 65 | $this->throwErrors = true; 66 | } 67 | 68 | /** 69 | * @see PreviewablePatcher::getApplicableDiff 70 | * 71 | * @since 0.4 72 | * 73 | * @param array $base 74 | * @param Diff $diff 75 | * 76 | * @return Diff 77 | * @throws PatcherException 78 | */ 79 | public function getApplicableDiff( array $base, Diff $diff ): Diff { 80 | $throwErrors = $this->throwErrors; 81 | $this->throwErrors = false; 82 | 83 | $patched = $this->patch( $base, $diff ); 84 | 85 | $this->throwErrors = $throwErrors; 86 | 87 | $treatAsMap = $diff->looksAssociative(); 88 | 89 | $differ = $treatAsMap ? new MapDiffer( true ) : new ListDiffer(); 90 | 91 | $diffOps = $differ->doDiff( $base, $patched ); 92 | 93 | $diff = new Diff( $diffOps, $treatAsMap ); 94 | 95 | return $diff; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/DiffOp/DiffOpAdd.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DiffOpAdd extends AtomicDiffOp { 17 | 18 | /** @var mixed */ 19 | private $newValue; 20 | 21 | /** 22 | * @see DiffOp::getType 23 | * 24 | * @since 0.1 25 | * 26 | * @return string 27 | */ 28 | public function getType(): string { 29 | return 'add'; 30 | } 31 | 32 | /** 33 | * @since 0.1 34 | * 35 | * @param mixed $newValue 36 | */ 37 | public function __construct( $newValue ) { 38 | $this->newValue = $newValue; 39 | } 40 | 41 | /** 42 | * @since 0.1 43 | * 44 | * @return mixed 45 | */ 46 | public function getNewValue() { 47 | return $this->newValue; 48 | } 49 | 50 | /** 51 | * @see Serializable::serialize 52 | * 53 | * @since 0.1 54 | * 55 | * @return string|null 56 | */ 57 | #[\ReturnTypeWillChange] 58 | public function serialize() { 59 | return serialize( $this->newValue ); 60 | } 61 | 62 | /** 63 | * @since 3.3.0 64 | * 65 | * @return array 66 | */ 67 | public function __serialize(): array { 68 | return [ $this->newValue ]; 69 | } 70 | 71 | /** 72 | * @see Serializable::unserialize 73 | * 74 | * @since 0.1 75 | * 76 | * @param string $serialization 77 | */ 78 | #[\ReturnTypeWillChange] 79 | public function unserialize( $serialization ) { 80 | $this->newValue = unserialize( $serialization ); 81 | } 82 | 83 | /** 84 | * @since 3.3.0 85 | * 86 | * @param array $data 87 | */ 88 | public function __unserialize( $data ): void { 89 | [ $this->newValue ] = $data; 90 | } 91 | 92 | /** 93 | * @see DiffOp::toArray 94 | * 95 | * @since 0.5 96 | * 97 | * @param callable|null $valueConverter optional callback used to convert any 98 | * complex values to arrays. 99 | * 100 | * @return array 101 | */ 102 | public function toArray( ?callable $valueConverter = null ): array { 103 | return [ 104 | 'type' => $this->getType(), 105 | 'newvalue' => $this->objectToArray( $this->newValue, $valueConverter ), 106 | ]; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/DiffOp/DiffOpRemove.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DiffOpRemove extends AtomicDiffOp { 17 | 18 | /** @var mixed */ 19 | private $oldValue; 20 | 21 | /** 22 | * @see DiffOp::getType 23 | * 24 | * @since 0.1 25 | * 26 | * @return string 27 | */ 28 | public function getType(): string { 29 | return 'remove'; 30 | } 31 | 32 | /** 33 | * @since 0.1 34 | * 35 | * @param mixed $oldValue 36 | */ 37 | public function __construct( $oldValue ) { 38 | $this->oldValue = $oldValue; 39 | } 40 | 41 | /** 42 | * @since 0.1 43 | * 44 | * @return mixed 45 | */ 46 | public function getOldValue() { 47 | return $this->oldValue; 48 | } 49 | 50 | /** 51 | * @see Serializable::serialize 52 | * 53 | * @since 0.1 54 | * 55 | * @return string|null 56 | */ 57 | #[\ReturnTypeWillChange] 58 | public function serialize() { 59 | return serialize( $this->oldValue ); 60 | } 61 | 62 | /** 63 | * @since 3.3.0 64 | * 65 | * @return array 66 | */ 67 | public function __serialize(): array { 68 | return [ $this->oldValue ]; 69 | } 70 | 71 | /** 72 | * @see Serializable::unserialize 73 | * 74 | * @since 0.1 75 | * 76 | * @param string $serialization 77 | */ 78 | #[\ReturnTypeWillChange] 79 | public function unserialize( $serialization ) { 80 | $this->oldValue = unserialize( $serialization ); 81 | } 82 | 83 | /** 84 | * @since 3.3.0 85 | * 86 | * @param array $data 87 | */ 88 | public function __unserialize( $data ): void { 89 | [ $this->oldValue ] = $data; 90 | } 91 | 92 | /** 93 | * @see DiffOp::toArray 94 | * 95 | * @since 0.5 96 | * 97 | * @param callable|null $valueConverter optional callback used to convert any 98 | * complex values to arrays. 99 | * 100 | * @return array 101 | */ 102 | public function toArray( ?callable $valueConverter = null ): array { 103 | return [ 104 | 'type' => $this->getType(), 105 | 'oldvalue' => $this->objectToArray( $this->oldValue, $valueConverter ), 106 | ]; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/DiffOp/DiffOpChange.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DiffOpChange extends AtomicDiffOp { 17 | 18 | /** @var mixed */ 19 | private $newValue; 20 | /** @var mixed */ 21 | private $oldValue; 22 | 23 | /** 24 | * @see DiffOp::getType 25 | * 26 | * @since 0.1 27 | * 28 | * @return string 29 | */ 30 | public function getType(): string { 31 | return 'change'; 32 | } 33 | 34 | /** 35 | * @since 0.1 36 | * 37 | * @param mixed $oldValue 38 | * @param mixed $newValue 39 | */ 40 | public function __construct( $oldValue, $newValue ) { 41 | $this->oldValue = $oldValue; 42 | $this->newValue = $newValue; 43 | } 44 | 45 | /** 46 | * @since 0.1 47 | * 48 | * @return mixed 49 | */ 50 | public function getOldValue() { 51 | return $this->oldValue; 52 | } 53 | 54 | /** 55 | * @since 0.1 56 | * 57 | * @return mixed 58 | */ 59 | public function getNewValue() { 60 | return $this->newValue; 61 | } 62 | 63 | /** 64 | * @see Serializable::serialize 65 | * 66 | * @since 0.1 67 | * 68 | * @return string|null 69 | */ 70 | #[\ReturnTypeWillChange] 71 | public function serialize() { 72 | return serialize( $this->__serialize() ); 73 | } 74 | 75 | /** 76 | * @since 3.3.0 77 | * 78 | * @return array 79 | */ 80 | public function __serialize(): array { 81 | return [ $this->newValue, $this->oldValue ]; 82 | } 83 | 84 | /** 85 | * @see Serializable::unserialize 86 | * 87 | * @since 0.1 88 | * 89 | * @param string $serialization 90 | */ 91 | #[\ReturnTypeWillChange] 92 | public function unserialize( $serialization ) { 93 | $this->__unserialize( unserialize( $serialization ) ); 94 | } 95 | 96 | /** 97 | * @since 3.3.0 98 | * 99 | * @param array $data 100 | */ 101 | public function __unserialize( $data ): void { 102 | [ $this->newValue, $this->oldValue ] = $data; 103 | } 104 | 105 | /** 106 | * @see DiffOp::toArray 107 | * 108 | * @since 0.5 109 | * 110 | * @param callable|null $valueConverter optional callback used to convert any 111 | * complex values to arrays. 112 | * 113 | * @return array 114 | */ 115 | public function toArray( ?callable $valueConverter = null ): array { 116 | return [ 117 | 'type' => $this->getType(), 118 | 'newvalue' => $this->objectToArray( $this->newValue, $valueConverter ), 119 | 'oldvalue' => $this->objectToArray( $this->oldValue, $valueConverter ), 120 | ]; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/DiffOpFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Daniel Kinzler 21 | */ 22 | class DiffOpFactory { 23 | 24 | /** 25 | * @var callable|null 26 | */ 27 | private $valueConverter; 28 | 29 | /** 30 | * @since 0.5 31 | * 32 | * @param callable|null $valueConverter optional callback used to convert special 33 | * array structures into objects used as values in atomic diff ops. 34 | */ 35 | public function __construct( $valueConverter = null ) { 36 | $this->valueConverter = $valueConverter; 37 | } 38 | 39 | /** 40 | * Returns an instance of DiffOp constructed from the provided array. 41 | * 42 | * This roundtrips with @see DiffOp::toArray. 43 | * 44 | * @since 0.5 45 | * 46 | * @param array $diffOp 47 | * 48 | * @return DiffOp\DiffOp 49 | * @throws InvalidArgumentException 50 | */ 51 | public function newFromArray( array $diffOp ) { 52 | $this->assertHasKey( 'type', $diffOp ); 53 | 54 | if ( $diffOp['type'] === 'add' ) { 55 | $this->assertHasKey( 'newvalue', $diffOp ); 56 | return new DiffOpAdd( $this->arrayToObject( $diffOp['newvalue'] ) ); 57 | } 58 | 59 | if ( $diffOp['type'] === 'remove' ) { 60 | $this->assertHasKey( 'oldvalue', $diffOp ); 61 | return new DiffOpRemove( $this->arrayToObject( $diffOp['oldvalue'] ) ); 62 | } 63 | 64 | if ( $diffOp['type'] === 'change' ) { 65 | $this->assertHasKey( 'newvalue', $diffOp ); 66 | $this->assertHasKey( 'oldvalue', $diffOp ); 67 | return new DiffOpChange( 68 | $this->arrayToObject( $diffOp['oldvalue'] ), 69 | $this->arrayToObject( $diffOp['newvalue'] ) ); 70 | } 71 | 72 | if ( $diffOp['type'] === 'diff' ) { 73 | $this->assertHasKey( 'operations', $diffOp ); 74 | $this->assertHasKey( 'isassoc', $diffOp ); 75 | 76 | $operations = []; 77 | 78 | foreach ( $diffOp['operations'] as $key => $operation ) { 79 | $operations[$key] = $this->newFromArray( $operation ); 80 | } 81 | 82 | return new Diff( $operations, $diffOp['isassoc'] ); 83 | } 84 | 85 | throw new InvalidArgumentException( 'Invalid array provided. Unknown type' ); 86 | } 87 | 88 | /** 89 | * @since 0.5 90 | * 91 | * @param string $key 92 | * @param array $diffOp 93 | * 94 | * @throws InvalidArgumentException 95 | */ 96 | protected function assertHasKey( $key, array $diffOp ) { 97 | if ( !array_key_exists( $key, $diffOp ) ) { 98 | throw new InvalidArgumentException( 'Invalid array provided. Missing key "' . $key . '"' ); 99 | } 100 | } 101 | 102 | /** 103 | * Converts an array structure to an object using the value converter callback function 104 | * provided to the constructor, if any. 105 | * 106 | * If the convert callback is null or the value is not an array, the value is returned 107 | * unchanged. The Converter callback is intended for constructing an object from an array, 108 | * but may also just leave the value unchanged if it cannot handle it. 109 | * 110 | * @param mixed $value The value to convert 111 | * 112 | * @return mixed The $value unchanged, or the return value of calling the 113 | * value converter callback on $value. 114 | */ 115 | private function arrayToObject( $value ) { 116 | if ( $this->valueConverter !== null && is_array( $value ) ) { 117 | $value = call_user_func( $this->valueConverter, $value ); 118 | } 119 | 120 | return $value; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/Differ/MapDiffer.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class MapDiffer implements Differ { 27 | 28 | /** 29 | * @var bool 30 | */ 31 | private $recursively; 32 | 33 | /** 34 | * @var Differ 35 | */ 36 | private $listDiffer; 37 | 38 | /** 39 | * @var ValueComparer 40 | */ 41 | private $valueComparer; 42 | 43 | /** 44 | * The third argument ($comparer) was added in 3.0 45 | */ 46 | public function __construct( 47 | bool $recursively = false, 48 | ?Differ $listDiffer = null, 49 | ?ValueComparer $comparer = null 50 | ) { 51 | $this->recursively = $recursively; 52 | $this->listDiffer = $listDiffer ?? new ListDiffer(); 53 | $this->valueComparer = $comparer ?? new StrictComparer(); 54 | } 55 | 56 | /** 57 | * @see Differ::doDiff 58 | * 59 | * Computes the diff between two associate arrays. 60 | * 61 | * @since 0.4 62 | * 63 | * @param array $oldValues The first array 64 | * @param array $newValues The second array 65 | * 66 | * @throws Exception 67 | * @return DiffOp[] 68 | */ 69 | public function doDiff( array $oldValues, array $newValues ): array { 70 | $newSet = $this->arrayDiffAssoc( $newValues, $oldValues ); 71 | $oldSet = $this->arrayDiffAssoc( $oldValues, $newValues ); 72 | 73 | $diffSet = []; 74 | 75 | foreach ( $this->getAllKeys( $oldSet, $newSet ) as $key ) { 76 | $diffOp = $this->getDiffOpForElement( $key, $oldSet, $newSet ); 77 | 78 | if ( $diffOp !== null ) { 79 | $diffSet[$key] = $diffOp; 80 | } 81 | } 82 | 83 | return $diffSet; 84 | } 85 | 86 | private function getAllKeys( array $oldSet, array $newSet ): array { 87 | return array_unique( array_merge( 88 | array_keys( $oldSet ), 89 | array_keys( $newSet ) 90 | ) ); 91 | } 92 | 93 | /** 94 | * @param mixed $key 95 | * @param array $oldSet 96 | * @param mixed $newSet 97 | * @return DiffOp 98 | */ 99 | private function getDiffOpForElement( $key, array $oldSet, array $newSet ) { 100 | if ( $this->recursively ) { 101 | $diffOp = $this->getDiffOpForElementRecursively( $key, $oldSet, $newSet ); 102 | 103 | if ( $diffOp !== null ) { 104 | if ( $diffOp->isEmpty() ) { 105 | // there is no (relevant) difference 106 | return null; 107 | } else { 108 | return $diffOp; 109 | } 110 | } 111 | } 112 | 113 | $hasOld = array_key_exists( $key, $oldSet ); 114 | $hasNew = array_key_exists( $key, $newSet ); 115 | 116 | if ( $hasOld && $hasNew ) { 117 | return new DiffOpChange( $oldSet[$key], $newSet[$key] ); 118 | } elseif ( $hasOld ) { 119 | return new DiffOpRemove( $oldSet[$key] ); 120 | } elseif ( $hasNew ) { 121 | return new DiffOpAdd( $newSet[$key] ); 122 | } 123 | 124 | // @codeCoverageIgnoreStart 125 | throw new LogicException( 'The element needs to exist in either the old or new list to compare' ); 126 | // @codeCoverageIgnoreEnd 127 | } 128 | 129 | /** 130 | * @param mixed $key 131 | * @param array $oldSet 132 | * @param mixed $newSet 133 | * @return ?Diff 134 | */ 135 | private function getDiffOpForElementRecursively( $key, array $oldSet, array $newSet ) { 136 | $old = array_key_exists( $key, $oldSet ) ? $oldSet[$key] : []; 137 | $new = array_key_exists( $key, $newSet ) ? $newSet[$key] : []; 138 | 139 | if ( is_array( $old ) && is_array( $new ) ) { 140 | return $this->getDiffForArrays( $old, $new ); 141 | } 142 | 143 | return null; 144 | } 145 | 146 | private function getDiffForArrays( array $old, array $new ): Diff { 147 | if ( $this->isAssociative( $old ) || $this->isAssociative( $new ) ) { 148 | return new Diff( $this->doDiff( $old, $new ), true ); 149 | } 150 | 151 | return new Diff( $this->listDiffer->doDiff( $old, $new ), false ); 152 | } 153 | 154 | /** 155 | * Returns if an array is associative or not. 156 | * 157 | * @param array $array 158 | * 159 | * @return bool 160 | */ 161 | private function isAssociative( array $array ): bool { 162 | foreach ( $array as $key => $value ) { 163 | if ( is_string( $key ) ) { 164 | return true; 165 | } 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /** 172 | * Similar to the native array_diff_assoc function, except that it will 173 | * spot differences between array values. Very weird the native 174 | * function just ignores these... 175 | * 176 | * @see http://php.net/manual/en/function.array-diff-assoc.php 177 | * 178 | * @param array $from 179 | * @param array $to 180 | * 181 | * @return array 182 | */ 183 | private function arrayDiffAssoc( array $from, array $to ): array { 184 | $diff = []; 185 | 186 | foreach ( $from as $key => $value ) { 187 | if ( !array_key_exists( $key, $to ) || !$this->valueComparer->valuesAreEqual( $to[$key], $value ) ) { 188 | $diff[$key] = $value; 189 | } 190 | } 191 | 192 | return $diff; 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /src/Patcher/MapPatcher.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class MapPatcher extends ThrowingPatcher { 24 | 25 | /** 26 | * @var Patcher 27 | */ 28 | private $listPatcher; 29 | 30 | /** 31 | * @var ValueComparer|null 32 | */ 33 | private $comparer = null; 34 | 35 | /** 36 | * @since 0.4 37 | * 38 | * @param bool $throwErrors 39 | * @param Patcher|null $listPatcher The patcher that will be used for lists in the value 40 | */ 41 | public function __construct( bool $throwErrors = false, ?Patcher $listPatcher = null ) { 42 | parent::__construct( $throwErrors ); 43 | 44 | $this->listPatcher = $listPatcher ?: new ListPatcher( $throwErrors ); 45 | } 46 | 47 | /** 48 | * @see Patcher::patch 49 | * 50 | * Applies the provided diff to the provided array and returns the result. 51 | * The array is treated as a map, ie keys are held into account. 52 | * 53 | * It is possible to pass in non-associative diffs (those for which isAssociative) 54 | * returns false, however the likely intended behavior can be obtained via 55 | * a list patcher. 56 | * 57 | * @since 0.4 58 | * 59 | * @param array $base 60 | * @param Diff $diff 61 | * 62 | * @return array 63 | * @throws PatcherException 64 | */ 65 | public function patch( array $base, Diff $diff ): array { 66 | foreach ( $diff as $key => $diffOp ) { 67 | $this->applyOperation( $base, $key, $diffOp ); 68 | } 69 | 70 | return $base; 71 | } 72 | 73 | /** 74 | * @param array &$base 75 | * @param int|string $key 76 | * @param DiffOp $diffOp 77 | * 78 | * @throws PatcherException 79 | */ 80 | private function applyOperation( array &$base, $key, DiffOp $diffOp ) { 81 | if ( $diffOp instanceof DiffOpAdd ) { 82 | $this->applyDiffOpAdd( $base, $key, $diffOp ); 83 | } elseif ( $diffOp instanceof DiffOpChange ) { 84 | $this->applyDiffOpChange( $base, $key, $diffOp ); 85 | } elseif ( $diffOp instanceof DiffOpRemove ) { 86 | $this->applyDiffOpRemove( $base, $key, $diffOp ); 87 | } elseif ( $diffOp instanceof Diff ) { 88 | $this->applyDiff( $base, $key, $diffOp ); 89 | } else { 90 | $this->handleError( 'Unknown diff operation cannot be applied to map element' ); 91 | } 92 | } 93 | 94 | /** 95 | * @param array &$base 96 | * @param int|string $key 97 | * @param DiffOpAdd $diffOp 98 | * 99 | * @throws PatcherException 100 | */ 101 | private function applyDiffOpAdd( array &$base, $key, DiffOpAdd $diffOp ) { 102 | if ( array_key_exists( $key, $base ) ) { 103 | $this->handleError( 'Cannot add an element already present in a map' ); 104 | return; 105 | } 106 | 107 | $base[$key] = $diffOp->getNewValue(); 108 | } 109 | 110 | /** 111 | * @param array &$base 112 | * @param int|string $key 113 | * @param DiffOpRemove $diffOp 114 | * 115 | * @throws PatcherException 116 | */ 117 | private function applyDiffOpRemove( array &$base, $key, DiffOpRemove $diffOp ) { 118 | if ( !array_key_exists( $key, $base ) ) { 119 | $this->handleError( 'Cannot do a non-add operation with an element not present in a map' ); 120 | return; 121 | } 122 | 123 | if ( !$this->valuesAreEqual( $base[$key], $diffOp->getOldValue() ) ) { 124 | $this->handleError( 'Tried removing a map value that mismatches the current value' ); 125 | return; 126 | } 127 | 128 | unset( $base[$key] ); 129 | } 130 | 131 | /** 132 | * @param array &$base 133 | * @param int|string $key 134 | * @param DiffOpChange $diffOp 135 | * 136 | * @throws PatcherException 137 | */ 138 | private function applyDiffOpChange( array &$base, $key, DiffOpChange $diffOp ) { 139 | if ( !array_key_exists( $key, $base ) ) { 140 | $this->handleError( 'Cannot do a non-add operation with an element not present in a map' ); 141 | return; 142 | } 143 | 144 | if ( !$this->valuesAreEqual( $base[$key], $diffOp->getOldValue() ) ) { 145 | $this->handleError( 'Tried changing a map value from an invalid source value' ); 146 | return; 147 | } 148 | 149 | $base[$key] = $diffOp->getNewValue(); 150 | } 151 | 152 | /** 153 | * @param array &$base 154 | * @param int|string $key 155 | * @param Diff $diffOp 156 | * 157 | * @throws PatcherException 158 | */ 159 | private function applyDiff( &$base, $key, Diff $diffOp ) { 160 | if ( $this->isAttemptToModifyNotExistingElement( $base, $key, $diffOp ) ) { 161 | $this->handleError( 'Cannot apply a diff with non-add operations to an element not present in a map' ); 162 | return; 163 | } 164 | 165 | if ( !array_key_exists( $key, $base ) ) { 166 | $base[$key] = []; 167 | } 168 | 169 | $base[$key] = $this->patchMapOrList( $base[$key], $diffOp ); 170 | } 171 | 172 | /** 173 | * @param array $base 174 | * @param int|string $key 175 | * @param Diff $diffOp 176 | * 177 | * @return bool 178 | */ 179 | private function isAttemptToModifyNotExistingElement( $base, $key, Diff $diffOp ): bool { 180 | return !array_key_exists( $key, $base ) 181 | && ( $diffOp->getChanges() !== [] || $diffOp->getRemovals() !== [] ); 182 | } 183 | 184 | /** 185 | * @param array $base 186 | * @param Diff $diff 187 | * 188 | * @return array 189 | */ 190 | private function patchMapOrList( array $base, Diff $diff ): array { 191 | if ( $diff->looksAssociative() ) { 192 | return $this->patch( $base, $diff ); 193 | } 194 | 195 | return $this->listPatcher->patch( $base, $diff ); 196 | } 197 | 198 | /** 199 | * @param mixed $firstValue 200 | * @param mixed $secondValue 201 | * 202 | * @return bool 203 | */ 204 | private function valuesAreEqual( $firstValue, $secondValue ): bool { 205 | if ( $this->comparer === null ) { 206 | $this->comparer = new StrictComparer(); 207 | } 208 | 209 | return $this->comparer->valuesAreEqual( $firstValue, $secondValue ); 210 | } 211 | 212 | /** 213 | * Sets the value comparer that should be used to determine if values are equal. 214 | * 215 | * @since 0.6 216 | * 217 | * @param ValueComparer $comparer 218 | */ 219 | public function setValueComparer( ValueComparer $comparer ) { 220 | $this->comparer = $comparer; 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff 2 | 3 | [![Build Status](https://github.com/wmde/Diff/actions/workflows/push.yaml/badge.svg?branch=master)](https://github.com/wmde/Diff/actions/workflows/push.yaml) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/wmde/Diff/badges/coverage.png?s=6ef6a74a92b7efc6e26470bb209293125f70731e)](https://scrutinizer-ci.com/g/wmde/Diff/) 5 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/wmde/Diff/badges/quality-score.png?s=d75d876247594bb4088159574cedf7bd648b9db2)](https://scrutinizer-ci.com/g/wmde/Diff/) 6 | [![Packagist Version](https://img.shields.io/packagist/v/diff/diff?label=Version)](https://packagist.org/packages/diff/diff) 7 | [![Packagist Downloads](https://img.shields.io/packagist/dt/diff/diff?label=Downloads)](https://packagist.org/packages/diff/diff) 8 | 9 | **Diff** is a small standalone PHP library for representing differences between data 10 | structures, computing such differences, and applying them as patches. It is extremely 11 | well tested and allows users to define their own comparison strategies. 12 | 13 | Diff does not provide any support for computing or representing the differences 14 | between unstructured data, ie text. 15 | 16 | A full history of the different versions of Diff can be found in the [release notes](RELEASE-NOTES.md). 17 | 18 | ## Requirements 19 | 20 | **Diff 3.x:** 21 | 22 | * PHP 7.2 or later (tested with PHP 7.4 up to PHP 8.4) 23 | 24 | **Diff 2.x:** 25 | 26 | * PHP 5.3 or later (tested with PHP 5.3 up to PHP 7.1 and HHVM) 27 | 28 | ## Installation 29 | 30 | To add this package as a local, per-project dependency to your project, simply add a 31 | dependency on `diff/diff` to your project's [`composer.json`](https://getcomposer.org/) file. 32 | Here is a minimal example of a `composer.json` file that just defines a dependency on 33 | Diff 3.x: 34 | 35 | ```json 36 | { 37 | "require": { 38 | "diff/diff": "~3.0" 39 | } 40 | } 41 | ``` 42 | 43 | ## High level structure 44 | 45 | The Diff library can be subdivided into several components. The main components are: 46 | 47 | * **DiffOp** Value objects that represent add, change, remove and composite operations. 48 | * **Differ** Service objects to create a diff between two sets of data. 49 | * **Patcher** Service objects to apply a diff as patch to a set of data. 50 | 51 | There are two support components, which are nevertheless package public: 52 | 53 | * **Comparer** Service objects for determining if two values are equal. 54 | * **ArrayComparer** Service objects for computing the difference between to arrays. 55 | 56 | ## Usage 57 | 58 | ### Representing diffs 59 | 60 | A diff consists out of diff operations. These can be atomic operations such as add, 61 | change and remove. These can also be diffs themselves, when dealing with nested structures. 62 | Hence the [composite pattern](https://en.wikipedia.org/wiki/Composite_pattern) is used. 63 | 64 | Diff operations implement the **DiffOp** interface. 65 | 66 | The available operations are: 67 | 68 | * `DiffOpAdd` - addition of a value (newValue) 69 | * `DiffOpChange` - modification of a value (oldValue, newValue) 70 | * `DiffOpRemove` - removal of a value (oldValue) 71 | * `Diff` - a collection of diff operations 72 | 73 | These can all be found in [src/DiffOp](src/DiffOp). 74 | 75 | The `Diff` class can be set to be either associative or non-associative. In case of the later, only 76 | `DiffOpAdd` and `DiffOpRemove` are allowed in it. 77 | 78 | ### Diffing data 79 | 80 | To compute the difference between two data structures, an instance of **Differ** is used. 81 | The `Differ` interface has a single method. 82 | 83 | ```php 84 | /** 85 | * Takes two arrays, computes the diff, and returns this diff as an array of DiffOp. 86 | * 87 | * @param array $oldValues The first array 88 | * @param array $newValues The second array 89 | * 90 | * @throws Exception 91 | * @return DiffOp[] 92 | */ 93 | public function doDiff( array $oldValues, array $newValues ): array; 94 | ``` 95 | 96 | Implementations provided by Diff: 97 | 98 | * `ListDiffer`: Differ that only looks at the values of the arrays (and thus ignores key differences). 99 | * `MapDiffer`: Differ that does an associative diff between two arrays, with the option to do this recursively. 100 | * `CallbackListDiffer`: Differ that only looks at the values of the arrays and compares them with a callback. 101 | * `OrderedListDiffer`: Differ that looks at the order of the values and the values of the arrays. 102 | 103 | All differ functionality can be found in [src/Differ](src/Differ). 104 | 105 | ### Applying patches 106 | 107 | To apply a diff as a patch onto a data structure, an instance of **Patcher** is used. 108 | The `Patcher` interface has a single method. 109 | 110 | ```php 111 | /** 112 | * Applies the applicable operations from the provided diff to 113 | * the provided base value. 114 | * 115 | * @param array $base 116 | * @param Diff $diffOps 117 | * 118 | * @return array 119 | */ 120 | public function patch( array $base, Diff $diffOps ): array; 121 | ``` 122 | 123 | Implementations provided by Diff: 124 | 125 | * `ListPatcher`: Applies non-associative diffs to a base. With default options does the reverse of `ListDiffer` 126 | * `MapPatcher`: Applies diff to a base, recursively if needed. With default options does the reverse of `MapDiffer` 127 | 128 | All classes part of the patcher component can be found in [src/Patcher](src/Patcher) 129 | 130 | ### ValueComparer 131 | 132 | The `ValueComparer` interface contains one method: 133 | 134 | ```php 135 | /** 136 | * @param mixed $firstValue 137 | * @param mixed $secondValue 138 | * 139 | * @return bool 140 | */ 141 | public function valuesAreEqual( $firstValue, $secondValue ): bool; 142 | ``` 143 | 144 | Implementations provided by Diff: 145 | 146 | * `StrictComparer`: Value comparer that uses PHPs native strict equality check (ie ===). 147 | * `CallbackComparer`: Adapter around a comparison callback that implements the `ValueComparer` interface. 148 | * `ComparableComparer`: Value comparer for objects that provide an equals method taking a single argument. 149 | 150 | All classes part of the ValueComparer component can be found in [src/Comparer](src/Comparer) 151 | 152 | ### ArrayComparer 153 | 154 | The `ArrayComposer` interface contains one method: 155 | 156 | ```php 157 | /** 158 | * Returns an array containing all the entries from arrayOne that are not present 159 | * in arrayTwo. 160 | * 161 | * Implementations are allowed to hold quantity into account or to disregard it. 162 | * 163 | * @param array $firstArray 164 | * @param array $secondArray 165 | * 166 | * @return array 167 | */ 168 | public function diffArrays( array $firstArray, array $secondArray ): array; 169 | ``` 170 | 171 | Implementations provided by Diff: 172 | 173 | * `NativeArrayComparer`: Adapter for PHPs native array_diff method. 174 | * `StrategicArrayComparer`: Computes the difference between two arrays by comparing elements with a `ValueComparer`. 175 | * `StrictArrayComparer`: Does strict comparison of values and holds quantity into account. 176 | * `OrderedArrayComparer`: Computes the difference between two ordered arrays by comparing elements with a `ValueComparer`. 177 | 178 | All classes part of the ArrayComparer component can be found in [src/ArrayComparer](src/ArrayComparer) 179 | 180 | ## Examples 181 | 182 | ### Manually constructing a diff 183 | 184 | ```php 185 | $diff = new Diff( array( 186 | 'email' => new DiffOpAdd( 'nyan@c.at' ), 187 | 'awesome' => new DiffOpChange( 42, 9001 ), 188 | ) ); 189 | ``` 190 | 191 | ### Computing a diff 192 | 193 | ```php 194 | $oldVersion = array( 195 | 'awesome' => 42, 196 | ); 197 | 198 | $newVersion = array( 199 | 'email' => 'nyan@c.at', 200 | 'awesome' => 9001, 201 | ); 202 | 203 | $differ = new MapDiffer(); 204 | $diff = $differ->doDiff( $oldVersion, $newVersion ); 205 | ``` 206 | 207 | ### Applying a diff as patch 208 | 209 | ```php 210 | $oldVersion = array( 211 | /* ... */ 212 | ); 213 | 214 | $diff = new Diff( /* ... */ ); 215 | 216 | $patcher = new MapPatcher(); 217 | $newVersion = $patcher->patch( $oldVersion, $diff ); 218 | ``` 219 | 220 | ## Links 221 | 222 | * [Diff on Packagist](https://packagist.org/packages/diff/diff) 223 | * [Diff on OpenHub](https://www.openhub.net/p/phpdiff) 224 | * [Diff on ScrutinizerCI](https://scrutinizer-ci.com/g/wmde/Diff/) 225 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | These are the release notes for the [Diff library](README.md). 2 | 3 | Latest release: 4 | [![Latest Stable Version](https://poser.pugx.org/diff/diff/version.png)](https://packagist.org/packages/diff/diff) 5 | 6 | ## Version 3.4.0 (2024-12-12) 7 | 8 | * Drop support for PHP 7.2, 7.3 9 | * Upgrade codesniffer rules to current `mediawiki/mediawiki-codesniffer` version (45.0.0) 10 | * Make nullable type parameter declarations explicit for compatibility with PHP 8.4 11 | 12 | ## Version 3.3.1 (2022-10-06) 13 | 14 | * Made our __unserialize declarations match PHP 7's, to avoid PHP warnings 15 | 16 | ## Version 3.3 (2022-10-05) 17 | 18 | * Raised minimum PHP version from 7.0 to 7.2 19 | * Added testing with PHP 7.3, 7.4, 8.0 and 8.1 20 | 21 | ## Version 3.2 (2018-09-11) 22 | 23 | * Deprecated constant `Diff_VERSION` 24 | * Switched License from GPL-2.0-or-later to BSD-3-Clause 25 | 26 | ## Version 3.1 (2018-04-17) 27 | 28 | * Fixed bug in `ListPatcher` that caused it to compare objects by identity rather than by value 29 | * Add `.gitattributes` file to exclude not needed files from git exports 30 | * Removed MediaWiki extension registration 31 | 32 | ## Version 3.0 (2017-05-10) 33 | 34 | #### Improvements 35 | 36 | * Added return type hints where possible 37 | * Added scalar type hints where possible 38 | * Added strict_types declare statements to all files 39 | 40 | #### Breaking changes 41 | 42 | * Dropped support for PHP 5.x 43 | * Dropped class aliases deprecated since Diff 1.0 44 | * Removed `ListDiff` and `MapDiff`, deprecated since Diff 0.5 45 | * Removed `ListDiffer::MODE_NATIVE` and `ListDiffer::MODE_STRICT`, deprecated since Diff 0.8 46 | * Removed `MapDiffer::setComparisonCallback` in favour of a new constructor argument 47 | 48 | ## Version 2.3 (2018-04-11) 49 | 50 | * Fixed bug in `ListPatcher` that caused it to compare objects by identity rather than by value 51 | 52 | ## Version 2.2 (2017-08-09) 53 | 54 | * Removed MediaWiki extension registration 55 | * Add `.gitattributes` file to exclude not needed files from git exports 56 | 57 | ## Version 2.1 (2016-09-01) 58 | 59 | * Improved various PHPDocs 60 | 61 | ## Version 2.0 (2015-03-17) 62 | 63 | * Added `Diff::equals` 64 | * Removed unused `Diff\Appendable` interface 65 | * Removed `Diff.credits.php` 66 | * Changed visibility of most protected fields and methods to private 67 | 68 | #### Internal changes 69 | 70 | * `bootstrap.php` no longer runs `composer update` 71 | * Added PHPCS and PHPMD support and configuration (`phpcs.xml` and `phpmd.xml`) 72 | * Added `composer cs` command for running the code style checks 73 | * CI now runs `composer ci` (includes code style checks) instead of `phpunit` 74 | 75 | ## Version 1.0.1 (2014-05-07) 76 | 77 | * Removed not needed support for the MediaWiki i18n system 78 | * Updated the url in `Diff.credits.php` (used on Special:Version when included with MediaWiki) 79 | 80 | ## Version 1.0 (2014-04-10) 81 | 82 | #### Improvements 83 | 84 | * Diff is now PSR-4 compliant 85 | 86 | #### Breaking changes 87 | 88 | * Removed the `Diff\IDiff` interface (deprecated since 0.5) 89 | * Removed the `Diff\IDiffOp` interface (deprecated since 0.4) 90 | * Replaced custom autoloader with PSR-4 based loading via Composer 91 | 92 | #### Deprecations 93 | 94 | * The classes that got moved into other namespace now have their old names as deprecated aliases: 95 | * All Differ classes that resided directly in the Diff namespace are now in Diff\Differ. 96 | * All DiffOp classes that resided directly in the Diff namespace are now in Diff\DiffOp. 97 | * All Patcher classes that resided directly in the Diff namespace are now in Diff\Patcher. 98 | 99 | ## Version 0.9 (2013-10-04) 100 | 101 | #### Additions 102 | 103 | * Added `OrderedArrayComparer`, an `ArrayComparer` for ordered arrays 104 | * Added `OrderedListDiffer`, a Differ that acts as facade for a `ListDiffer` using an `OrderedArrayComparer` 105 | * Added `ComparableComparer`, a `ValueComparer` that makes use of a "equals" method of the objects it compares 106 | 107 | ## Version 0.8 (2013-08-26) 108 | 109 | #### Additions 110 | 111 | * Added Diff\ArrayComparer\ArrayComparer interface 112 | * Added NativeArrayComparer, ArrayComparer adapter for array_diff 113 | * Added StrictArrayComparer, containing the "strict mode" logic from ListDiffer 114 | * Added StrategicArrayComparer, implementation of ArrayComparer that takes a ValueComparer as strategy 115 | 116 | #### Improvements 117 | 118 | * MapPatcher will now report conflicts for remove operations that specify a value to be removed 119 | different from the value in the structure being patched. 120 | * ListDiffer now supports arbitrary array comparison behaviour by using an ArrayComparer strategy. 121 | * The installation and usage documentation can now be found in README.md. 122 | 123 | #### Removals 124 | 125 | * Removed obsolete tests/phpunit.php test runner 126 | * Removed obsolete INSTALL file. Installation instructions are now in README.md. 127 | 128 | #### Deprecations 129 | 130 | * The "comparison mode" flag in the ListDiffer constructor has been deprecated in favour of 131 | the ArrayComparer strategy it now has. 132 | 133 | ## Version 0.7 (2013-07-16) 134 | 135 | #### Improvements 136 | 137 | * Added extra tests for MapPatcher and ListPatcher 138 | * Added extra tests for Diff 139 | * Added extra tests for MapDiffer 140 | * Added @covers tags to the unit tests to improve coverage report accuracy 141 | 142 | #### Removals 143 | 144 | * Removed static methods from ListDiff and MapDiff (all deprecated since 0.4) 145 | * Removed DiffOpTestDummy 146 | 147 | #### Bug fixes 148 | 149 | * MapPatcher will now no longer stop patching after the first remove operation it encounters 150 | * MapPatcher now always treats its top level input diff as a map diff 151 | * Fixed several issues in ListPatcherTest 152 | 153 | ## Version 0.6 (2013-05-08) 154 | 155 | #### Compatibility changes 156 | 157 | * The tests can now run independently of MediaWiki 158 | * The tests now require PHPUnit 3.7 or later 159 | 160 | #### Additions 161 | 162 | * Added phpunit.php runner in the tests directory 163 | * Added Diff\Comparer\ValueComparer interface with CallbackComparer and StrictComparer implementations 164 | * Added MapPatcher::setValueComparer to facilitate patching maps containing objects 165 | * Added PHPUnit configuration file using the new tests/bootstrap.php 166 | 167 | #### Removals 168 | 169 | * GenericArrayObject has been removed from this package. 170 | Diff derives from ArrayObject rather than GenericArrayObject. 171 | Its interface has not changed expect for the points below. 172 | * The getObjectType method in Diff (previously defined in GenericArrayObject) 173 | is now private rather than public. 174 | * Adding a non-DiffOp element to a Diff will now result in an InvalidArgumentException 175 | rather than a MWException. 176 | * Removed Diff\Exception 177 | 178 | ## Version 0.5 (2013-02-26) 179 | 180 | #### Additions 181 | 182 | * Added DiffOpFactory 183 | * Added DiffOp::toArray 184 | * Added CallbackListDiffer 185 | * Added MapDiffer::setComparisonCallback 186 | 187 | #### Deprecations 188 | 189 | * Hard deprecated ListDiff, MapDiff and IDiff 190 | 191 | #### Removals 192 | 193 | * Removed Diff::getApplicableDiff 194 | 195 | ## Version 0.4 (2013-01-08) 196 | 197 | #### Additions 198 | 199 | * Split off diffing code from MapDiff and ListDiff to dedicated Differ classes 200 | * Added dedicated Patcher classes, which are used for the getApplicableDiff functionality 201 | 202 | #### Deprecations 203 | 204 | * Deprecated ListDiff:newFromArrays and MapDiff::newFromArrays 205 | * Deprecated ListDiff::newEmpty and MapDiff::newEmpty 206 | * Deprecated Diff::getApplicableDiff 207 | * Soft deprecated DiffOp interface in favour of DiffOp 208 | * Soft deprecated IDiff interface in favour of Diff 209 | * Soft deprecated MapDiff and ListDiff in favour of Diff 210 | 211 | #### Removals 212 | 213 | * Removed parentKey functionality from Diff 214 | * Removed constructor from Diff interface 215 | * Removed Diff::newEmpty 216 | 217 | ## Version 0.3 (2012-11-21) 218 | 219 | * Improved entry point and setup code. Diff.php is now the main entry point for both MW extension and standalone library 220 | * ListDiffs with only add operations can now be applied on top of bases that do not have their key 221 | * Added Diff::removeEmptyOperations 222 | * Improved type hinting 223 | * Improved test coverage 224 | * Added constructor tests for MapDiff and ListDiff 225 | * Added extra tests for Diff and MapDiff 226 | * Test coverage is now 100% 227 | * Removed static method from Diff interface 228 | 229 | ## Version 0.2 (2012-11-01) 230 | 231 | * Fixed tests to work with PHP 5.4 and above 232 | * Added translations 233 | * Added some profiling calls 234 | 235 | ## Version 0.1 (2012-9-25) 236 | 237 | Initial release with these features: 238 | 239 | * Classes to represent diffs or collections of diff operations: Diff, MapDiff, ListDiff 240 | * Classes to represent diff operations: Diff, MapDiff, ListDiff, DiffOpAdd, DiffOpChange, DiffOpRemove 241 | * Methods to compute list and maps diffs 242 | * Support for recursive diffs of arbitrary depth 243 | * Works as MediaWiki extension or as standalone library 244 | -------------------------------------------------------------------------------- /src/DiffOp/Diff/Diff.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Daniel Kinzler 23 | * @author Thiemo Kreuz 24 | */ 25 | class Diff extends ArrayObject implements DiffOp { 26 | 27 | /** 28 | * @var bool|null 29 | */ 30 | private $isAssociative = null; 31 | 32 | /** 33 | * Pointers to the operations of certain types for quick lookup. 34 | * 35 | * @var array[] 36 | */ 37 | private $typePointers = [ 38 | 'add' => [], 39 | 'remove' => [], 40 | 'change' => [], 41 | 'list' => [], 42 | 'map' => [], 43 | 'diff' => [], 44 | ]; 45 | 46 | /** 47 | * @var int 48 | */ 49 | private $indexOffset = 0; 50 | 51 | /** 52 | * @since 0.1 53 | * 54 | * @param DiffOp[] $operations 55 | * @param bool|null $isAssociative 56 | * 57 | * @throws InvalidArgumentException 58 | */ 59 | public function __construct( array $operations = [], $isAssociative = null ) { 60 | if ( $isAssociative !== null && !is_bool( $isAssociative ) ) { 61 | throw new InvalidArgumentException( '$isAssociative should be a boolean or null' ); 62 | } 63 | 64 | parent::__construct( [] ); 65 | 66 | foreach ( $operations as $offset => $operation ) { 67 | if ( !( $operation instanceof DiffOp ) ) { 68 | throw new InvalidArgumentException( 69 | 'All elements fed to the Diff constructor should be of type DiffOp' 70 | ); 71 | } 72 | 73 | $this->offsetSet( $offset, $operation ); 74 | } 75 | 76 | $this->isAssociative = $isAssociative; 77 | } 78 | 79 | /** 80 | * @since 0.1 81 | * 82 | * @return DiffOp[] 83 | */ 84 | public function getOperations(): array { 85 | return $this->getArrayCopy(); 86 | } 87 | 88 | /** 89 | * @since 0.1 90 | * 91 | * @param string $type 92 | * 93 | * @return DiffOp[] 94 | */ 95 | public function getTypeOperations( string $type ): array { 96 | return array_intersect_key( 97 | $this->getArrayCopy(), 98 | array_flip( $this->typePointers[$type] ) 99 | ); 100 | } 101 | 102 | /** 103 | * @since 0.1 104 | * 105 | * @param DiffOp[] $operations 106 | */ 107 | public function addOperations( array $operations ) { 108 | foreach ( $operations as $operation ) { 109 | $this->append( $operation ); 110 | } 111 | } 112 | 113 | /** 114 | * Gets called before a new element is added to the ArrayObject. 115 | * 116 | * At this point the index is always set (ie not null) and the 117 | * value is always of the type returned by @see getObjectType. 118 | * 119 | * Should return a boolean. When false is returned the element 120 | * does not get added to the ArrayObject. 121 | * 122 | * @param int|string $index 123 | * @param DiffOp $value 124 | * 125 | * @return bool 126 | * @throws InvalidArgumentException 127 | */ 128 | private function preSetElement( $index, DiffOp $value ): bool { 129 | if ( $this->isAssociative === false && ( $value->getType() !== 'add' && $value->getType() !== 'remove' ) ) { 130 | throw new InvalidArgumentException( 131 | 'Diff operation with invalid type "' . $value->getType() . '" provided.' 132 | ); 133 | } 134 | 135 | if ( array_key_exists( $value->getType(), $this->typePointers ) ) { 136 | $this->typePointers[$value->getType()][] = $index; 137 | } else { 138 | throw new InvalidArgumentException( 139 | 'Diff operation with invalid type "' . $value->getType() . '" provided.' 140 | ); 141 | } 142 | 143 | return true; 144 | } 145 | 146 | /** 147 | * @see Serializable::unserialize 148 | * 149 | * @since 0.1 150 | * 151 | * @param string $serialization 152 | */ 153 | #[\ReturnTypeWillChange] 154 | public function unserialize( $serialization ) { 155 | $this->__unserialize( unserialize( $serialization ) ); 156 | } 157 | 158 | /** 159 | * @since 3.3.0 160 | * 161 | * @param array $data 162 | */ 163 | public function __unserialize( $data ): void { 164 | foreach ( $data['data'] as $offset => $value ) { 165 | // Just set the element, bypassing checks and offset resolving, 166 | // as these elements have already gone through this. 167 | parent::offsetSet( $offset, $value ); 168 | } 169 | 170 | $this->indexOffset = $data['index']; 171 | 172 | $this->typePointers = $data['typePointers']; 173 | 174 | if ( array_key_exists( 'assoc', $data ) ) { 175 | $this->isAssociative = $data['assoc'] === 'n' ? null : $data['assoc'] === 't'; 176 | } 177 | } 178 | 179 | /** 180 | * @since 0.1 181 | * 182 | * @return DiffOpAdd[] 183 | */ 184 | public function getAdditions(): array { 185 | return $this->getTypeOperations( 'add' ); 186 | } 187 | 188 | /** 189 | * @since 0.1 190 | * 191 | * @return DiffOpRemove[] 192 | */ 193 | public function getRemovals(): array { 194 | return $this->getTypeOperations( 'remove' ); 195 | } 196 | 197 | /** 198 | * @since 0.1 199 | * 200 | * @return DiffOpChange[] 201 | */ 202 | public function getChanges(): array { 203 | return $this->getTypeOperations( 'change' ); 204 | } 205 | 206 | /** 207 | * @since 0.1 208 | * 209 | * @return array of mixed 210 | */ 211 | public function getAddedValues(): array { 212 | return array_map( 213 | static function ( DiffOpAdd $addition ) { 214 | return $addition->getNewValue(); 215 | }, 216 | $this->getTypeOperations( 'add' ) 217 | ); 218 | } 219 | 220 | /** 221 | * @since 0.1 222 | * 223 | * @return array of mixed 224 | */ 225 | public function getRemovedValues(): array { 226 | return array_map( 227 | static function ( DiffOpRemove $addition ) { 228 | return $addition->getOldValue(); 229 | }, 230 | $this->getTypeOperations( 'remove' ) 231 | ); 232 | } 233 | 234 | /** 235 | * @see DiffOp::isAtomic 236 | * 237 | * @since 0.1 238 | * 239 | * @return bool 240 | */ 241 | public function isAtomic(): bool { 242 | return false; 243 | } 244 | 245 | /** 246 | * @see DiffOp::getType 247 | * 248 | * @since 0.1 249 | * 250 | * @return string 251 | */ 252 | public function getType(): string { 253 | return 'diff'; 254 | } 255 | 256 | /** 257 | * Counts the number of atomic operations in the diff. 258 | * This means the size of a diff with as elements only empty diffs will be 0. 259 | * Or that the size of a diff with one atomic operation and one diff that itself 260 | * holds two atomic operations will be 3. 261 | * 262 | * @see Countable::count 263 | * 264 | * @since 0.1 265 | * 266 | * @return int 267 | */ 268 | public function count(): int { 269 | $count = 0; 270 | 271 | /** 272 | * @var DiffOp $diffOp 273 | */ 274 | foreach ( $this as $diffOp ) { 275 | $count += $diffOp->count(); 276 | } 277 | 278 | return $count; 279 | } 280 | 281 | /** 282 | * @since 0.3 283 | */ 284 | public function removeEmptyOperations() { 285 | foreach ( $this->getArrayCopy() as $key => $operation ) { 286 | if ( $operation instanceof self && $operation->isEmpty() ) { 287 | unset( $this[$key] ); 288 | } 289 | } 290 | } 291 | 292 | /** 293 | * Returns the value of the isAssociative flag. 294 | * 295 | * @since 0.4 296 | * 297 | * @return bool|null 298 | */ 299 | public function isAssociative() { 300 | return $this->isAssociative; 301 | } 302 | 303 | /** 304 | * Returns if the diff looks associative or not. 305 | * This first checks the isAssociative flag and in case its null checks 306 | * if there are any non-add-non-remove operations. 307 | * 308 | * @since 0.4 309 | * 310 | * @return bool 311 | */ 312 | public function looksAssociative(): bool { 313 | return $this->isAssociative === null ? $this->hasAssociativeOperations() : $this->isAssociative; 314 | } 315 | 316 | /** 317 | * Returns if the diff can be non-associative. 318 | * This means it does not contain any non-add-non-remove operations. 319 | * 320 | * @since 0.4 321 | * 322 | * @return bool 323 | */ 324 | public function hasAssociativeOperations(): bool { 325 | return !empty( $this->typePointers['change'] ) 326 | || !empty( $this->typePointers['diff'] ) 327 | || !empty( $this->typePointers['map'] ) 328 | || !empty( $this->typePointers['list'] ); 329 | } 330 | 331 | /** 332 | * Returns the Diff in array form where nested DiffOps are also turned into their array form. 333 | * 334 | * @see DiffOp::toArray 335 | * 336 | * @since 0.5 337 | * 338 | * @param callable|null $valueConverter optional callback used to convert any 339 | * complex values to arrays. 340 | * 341 | * @return array 342 | */ 343 | public function toArray( ?callable $valueConverter = null ): array { 344 | $operations = []; 345 | 346 | foreach ( $this->getOperations() as $key => $diffOp ) { 347 | $operations[$key] = $diffOp->toArray( $valueConverter ); 348 | } 349 | 350 | return [ 351 | 'type' => $this->getType(), 352 | 'isassoc' => $this->isAssociative, 353 | 'operations' => $operations 354 | ]; 355 | } 356 | 357 | /** 358 | * @since 2.0 359 | * 360 | * @param mixed $target 361 | * 362 | * @return bool 363 | */ 364 | public function equals( $target ) { 365 | if ( $target === $this ) { 366 | return true; 367 | } 368 | 369 | if ( !( $target instanceof self ) ) { 370 | return false; 371 | } 372 | 373 | return $this->isAssociative === $target->isAssociative 374 | && $this->getArrayCopy() == $target->getArrayCopy(); 375 | } 376 | 377 | /** 378 | * Finds a new offset for when appending an element. 379 | * The base class does this, so it would be better to integrate, 380 | * but there does not appear to be any way to do this... 381 | * 382 | * @return int 383 | */ 384 | private function getNewOffset(): int { 385 | while ( $this->offsetExists( $this->indexOffset ) ) { 386 | $this->indexOffset++; 387 | } 388 | 389 | return $this->indexOffset; 390 | } 391 | 392 | /** 393 | * @see ArrayObject::append 394 | * 395 | * @since 0.1 396 | * 397 | * @param mixed $value 398 | */ 399 | #[\ReturnTypeWillChange] 400 | public function append( $value ) { 401 | $this->setElement( null, $value ); 402 | } 403 | 404 | /** 405 | * @see ArrayObject::offsetSet() 406 | * 407 | * @since 0.1 408 | * 409 | * @param int|string $index 410 | * @param mixed $value 411 | */ 412 | #[\ReturnTypeWillChange] 413 | public function offsetSet( $index, $value ) { 414 | $this->setElement( $index, $value ); 415 | } 416 | 417 | /** 418 | * Method that actually sets the element and holds 419 | * all common code needed for set operations, including 420 | * type checking and offset resolving. 421 | * 422 | * If you want to do additional indexing or have code that 423 | * otherwise needs to be executed whenever an element is added, 424 | * you can overload @see preSetElement. 425 | * 426 | * @param int|string|null $index 427 | * @param mixed $value 428 | * 429 | * @throws InvalidArgumentException 430 | */ 431 | private function setElement( $index, $value ) { 432 | if ( !( $value instanceof DiffOp ) ) { 433 | throw new InvalidArgumentException( 434 | 'Can only add DiffOp implementing objects to ' . get_called_class() . '.' 435 | ); 436 | } 437 | 438 | if ( $index === null ) { 439 | $index = $this->getNewOffset(); 440 | } 441 | 442 | if ( $this->preSetElement( $index, $value ) ) { 443 | parent::offsetSet( $index, $value ); 444 | } 445 | } 446 | 447 | /** 448 | * @see Serializable::serialize 449 | * 450 | * @since 0.1 451 | * 452 | * @return string 453 | */ 454 | #[\ReturnTypeWillChange] 455 | public function serialize() { 456 | return serialize( $this->__serialize() ); 457 | } 458 | 459 | /** 460 | * @since 3.3.0 461 | * 462 | * @return array 463 | */ 464 | public function __serialize(): array { 465 | $assoc = $this->isAssociative === null ? 'n' : ( $this->isAssociative ? 't' : 'f' ); 466 | 467 | return [ 468 | 'data' => $this->getArrayCopy(), 469 | 'index' => $this->indexOffset, 470 | 'typePointers' => $this->typePointers, 471 | 'assoc' => $assoc 472 | ]; 473 | } 474 | 475 | /** 476 | * @since 0.1 477 | * 478 | * @return bool 479 | */ 480 | public function isEmpty(): bool { 481 | /** @var DiffOp $diffOp */ 482 | foreach ( $this as $diffOp ) { 483 | if ( $diffOp->count() > 0 ) { 484 | return false; 485 | } 486 | } 487 | 488 | return true; 489 | } 490 | 491 | } 492 | --------------------------------------------------------------------------------