GoIntegro/hateoas

View on GitHub
JsonApi/Merge/Blender.php

Summary

Maintainability
B
4 hrs
Test Coverage
<?php
/**
 * @copyright 2014 Integ S.A.
 * @license http://opensource.org/licenses/MIT The MIT License (MIT)
 * @author Javier Lorenzana <javier.lorenzana@gointegro.com>
 */

namespace GoIntegro\Hateoas\JsonApi\Merge;

/**
 * Handles merging two JSON-API documents into one, if possible.
 *
 * Nothing gets overwritten; duplicate keys with different values result in
 * an exception thrown.
 * For non-native speakers, this is not called "merger" because that is
 * a well-known business term.
 * @see http://www.merriam-webster.com/dictionary/merger
 */
class Blender
{
    const ERROR_MISSING_TOP_LEVEL_LINKS = "Por algún motivo desconocido dos recursos del mismo tipo intentan registrar dos plantillas de URL distintas para la misma relación.";

    /**
     * @var array
     */
    private static $reservedTopLevel = ['links', 'linked', 'meta'];

    /**
     * @param array The original JSON-API document.
     * @param array The JSON-API document to merge with the original one.
     * @return array
     */
    public function merge(array $champion, array $challenger)
    {
        $this->mergeTopLevelLinks($champion, $challenger)
            ->mergePrimaryResources($champion, $challenger)
            ->mergeLinkedResources($champion, $challenger)
            ->mergeResourceMeta($champion, $challenger);

        return $champion;
    }

    /**
     * @param array &$champion
     * @param array $challenger
     * @return self
     */
    public function mergeTopLevelLinks(array &$champion, array $challenger)
    {
        static $key = 'links';

        if (isset($champion[$key]) || isset($challenger[$key])) {
            if (empty($champion[$key])) {
                $champion[$key] = $challenger[$key];
            } elseif (!empty($challenger[$key])) {
                $links
                    = array_merge($champion[$key], $challenger[$key]);
                $compare = function(array $linkA, array $linkB) {
                    $comparison = (integer) ($linkA !== $linkB);
                    return $comparison;
                };
                $diff = array_udiff_assoc(
                    $champion[$key], $links, $compare
                );

                if (!empty($diff)) {
                    throw new \LogicException(
                        self::ERROR_MISSING_TOP_LEVEL_LINKS
                    );
                } else {
                    $champion[$key] = $links;
                }
            }
        }

        return $this;
    }

    /**
     * @param array &$champion
     * @param array $challenger
     * @return self
     */
    public function mergePrimaryResources(array &$champion, array $challenger)
    {
        foreach ($challenger as $key => $value) {
            if (in_array($key, static::$reservedTopLevel)) continue;

            if (isset($champion[$key])) {
                $value = $this->resolveSameType($champion[$key], $value);
            }

            $champion[$key] = $value;
        }

        return $this;
    }

    /**
     * @param array &$champion
     * @param array $challenger
     * @return self
     */
    public function mergeLinkedResources(array &$champion, array $challenger)
    {
        static $key = 'linked';

        if (isset($champion[$key]) || isset($challenger[$key])) {
            if (empty($champion[$key])) {
                $champion[$key] = $challenger[$key];
            } elseif (!empty($challenger[$key])) {
                foreach ($challenger[$key] as $type => $collection) {
                    if (isset($champion[$key][$type])) {
                        $champion[$key][$type]
                            = $this->mergeResourceCollections(
                                $champion[$key][$type], $collection
                            );
                    } else {
                        $champion[$key][$type] = $collection;
                    }
                }
            }
        }

        return $this;
    }

    /**
     * @param array &$champion
     * @param array $challenger
     * @return self
     */
    public function mergeResourceMeta(array &$champion, array $challenger)
    {
        static $key = 'meta';

        if (isset($champion[$key]) || isset($challenger[$key])) {
            if (empty($champion[$key])) {
                $champion[$key] = $challenger[$key];
            } elseif (!empty($challenger[$key])) {
                foreach ($challenger[$key] as $type => $meta) {
                    if (empty($champion[$key][$type])) {
                        $champion[$key][$type] = $meta;
                    } else {
                        throw new UnmergeableResourcesException;
                    }
                }
            }
        }

        return $this;
    }

    /**
     * @param array $valueA
     * @param array $valueB
     * @return array
     */
    protected function resolveSameType(array $valueA, array $valueB)
    {
        if (
            static::isResourceObject($valueA)
            && static::isResourceObject($valueB)
        ) {
            return $this->resolveResourceObject($valueA, $valueB);
        } elseif (
            !static::isResourceObject($valueA)
            && !static::isResourceObject($valueB)
        ) {
            return $this->mergeResourceCollections($valueA, $valueB);
        } else {
            throw new UnmergeableResourcesException;
        }
    }

    /**
     * @param array $resourceA
     * @param array $resourceB
     * @return array
     */
    protected function resolveResourceObject(
        array $resourceA, array $resourceB
    )
    {
        if (self::resourcesAreSame($resourceA, $resourceB)) {
            return $this->mergeResourceObjects($resourceA, $resourceB);
        } else {
            throw new UnmergeableResourcesException;
        }
    }

    /**
     * @param array $resourceA
     * @param array $resourceB
     * @return array
     */
    protected function mergeResourceObjects(
        array $resourceA, array $resourceB
    )
    {
        return array_merge_recursive($resourceA, $resourceB);
    }

    /**
     * @param array $collectionA
     * @param array $collectionB
     * @return array
     */
    protected function mergeResourceCollections(
        array $collectionA, array $collectionB
    )
    {
        $byIds = [];

        $callback = function(array $resource) use (&$byIds) {
            $byIds[$resource['id']] = $resource;
        };

        array_walk($collectionA, $callback);
        array_walk($collectionB, $callback);

        return array_values($byIds);
    }

    /**
     * @param array $value
     * @return boolean
     */
    protected static function isResourceObject(array $value)
    {
        return isset($value['id']); // That simple.
    }

    /**
     * @param array $resourceA
     * @param array $resourceB
     * @return boolean
     */
    protected static function resourcesAreSame(
        array $resourceA, array $resourceB
    )
    {
        return $resourceA['id'] === $resourceB['id'];
    }
}