miquido/data-structure

View on GitHub
src/Map/Map.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Miquido\DataStructure\Map;

use Miquido\DataStructure\ArrayConverter;
use Miquido\DataStructure\ArrayConvertibleInterface;
use Miquido\DataStructure\TypedCollection\StringCollection;
use Miquido\DataStructure\TypedCollection\StringCollectionInterface;
use Miquido\DataStructure\Value\Value;
use Miquido\DataStructure\Value\ValueInterface;
use Webmozart\Assert\Assert;

final class Map implements MapInterface
{
    /**
     * @var array
     */
    private $data;

    public static function create($values = null): MapInterface
    {
        return new Map($values);
    }

    /**
     * @param array|ArrayConvertibleInterface|MapInterface|null $values
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($values = null)
    {
        if (null === $values) {
            $values = [];
        }

        if ($values instanceof ArrayConvertibleInterface) {
            $values = $values->toArray();
        }

        Assert::isArray($values, \sprintf('Invalid input type "%s" (allowed: array, %s)', \gettype($values), ArrayConvertibleInterface::class));

        $this->data = $values;
    }

    public function set(string $key, $value): MapInterface
    {
        return new Map(\array_merge($this->data, [$key => $value]));
    }

    public function get(string $key, $default = null, bool $nullDefault = false)
    {
        if (\array_key_exists($key, $this->data)) {
            return $this->data[$key];
        }

        if (null !== $default || (null === $default && $nullDefault)) {
            return $default;
        }

        throw new \OutOfBoundsException(\sprintf('Key "%s" does not exist', $key));
    }

    public function getValue(string $key, $default = null, bool $nullDefault = false): ValueInterface
    {
        return new Value($this->get($key, $default, $nullDefault));
    }

    public function has(string $key): bool
    {
        return \array_key_exists($key, $this->data);
    }

    public function hasOneOf(string ...$keys): bool
    {
        foreach ($keys as $name) {
            if ($this->has($name)) {
                return true;
            }
        }

        return false;
    }

    public function hasAll(string ...$keys): bool
    {
        Assert::minCount($keys, 1, 'Please provide at least one key');
        foreach ($keys as $name) {
            if (!$this->has($name)) {
                return false;
            }
        }

        return true;
    }

    public function remove(string ...$keysToRemove): MapInterface
    {
        return new Map(\array_filter($this->data, function (/* @noinspection PhpUnusedParameterInspection */$value, $key) use ($keysToRemove): bool {
            return !\in_array($key, $keysToRemove, true);
        }, \ARRAY_FILTER_USE_BOTH));
    }

    public function pick(string ...$keysToPick): MapInterface
    {
        return $this->filterByKeys(function (string $key) use ($keysToPick): bool {
            return \in_array($key, $keysToPick, true);
        });
    }

    public function rename(string $key, string $newName): MapInterface
    {
        if (!$this->has($key)) {
            throw new \InvalidArgumentException(\sprintf('Key "%s" does not exist', $key));
        }
        if ($this->has($newName)) {
            throw new \InvalidArgumentException(\sprintf('Key "%s" already exists', $newName));
        }

        return $this->remove($key)->set($newName, $this->get($key));
    }

    public function filter(callable $callback): MapInterface
    {
        return new Map(\array_filter($this->data, function ($value, string $key) use ($callback): bool {
            $result = $callback($value, $key);
            Assert::boolean($result, \sprintf('Callback should return boolean, got %s', \gettype($result)));

            return $result;
        }, \ARRAY_FILTER_USE_BOTH));
    }

    public function filterByValues(callable $callback): MapInterface
    {
        return new Map(\array_filter($this->data, function ($value) use ($callback): bool {
            $result = $callback($value);
            Assert::boolean($result, \sprintf('Callback should return boolean, got %s', \gettype($result)));

            return $result;
        }));
    }

    public function filterByKeys(callable $callback): MapInterface
    {
        return new Map(\array_filter($this->data, function (string $key) use ($callback): bool {
            $result = $callback($key);
            Assert::boolean($result, \sprintf('Callback should return boolean, got %s', \gettype($result)));

            return $result;
        }, \ARRAY_FILTER_USE_KEY));
    }

    public function merge(MapInterface $map): MapInterface
    {
        return new Map(\array_merge($this->toArray(), $map->toArray()));
    }

    public function equals(MapInterface $map): bool
    {
        if ($this->count() !== $map->count()) {
            return false;
        }

        foreach ($this as $key => $value) {
            if (!$map->has($key) || $map->get($key) !== $value) {
                return false;
            }
        }

        return true;
    }

    public function keys(): StringCollectionInterface
    {
        return new StringCollection(...\array_map(function ($key): string {
            return (string) $key;
        }, \array_keys($this->data)));
    }

    public function values(): array
    {
        return \array_values($this->data);
    }

    public function mapKeys(callable $callback): MapInterface
    {
        $transformed = new Map([]);
        foreach ($this->data as $key => $value) {
            $mappedKey = $callback($key);
            Assert::string($mappedKey, 'Callback should return a string');

            if ($transformed->has($mappedKey)) {
                throw new \LogicException(\sprintf('Duplicated key "%s"', $mappedKey));
            }

            $transformed = $transformed->set($mappedKey, $value);
        }

        return $transformed;
    }

    public function toArray(): array
    {
        return ArrayConverter::toArray($this->data);
    }

    /**
     * @param mixed $offset
     *
     * @return bool
     */
    public function offsetExists($offset): bool
    {
        return $this->has($offset);
    }

    /**
     * @param mixed $offset
     *
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * @param mixed $offset
     * @param mixed $value
     */
    public function offsetSet($offset, $value): void
    {
        throw new \LogicException(\sprintf('%s - method not allowed. Please use "set" method.', __METHOD__));
    }

    /**
     * @param mixed $offset
     */
    public function offsetUnset($offset): void
    {
        throw new \LogicException(\sprintf('%s - method not allowed. Please use "delete" method.', __METHOD__));
    }

    /**
     * @return \Traversable
     */
    public function getIterator(): \Traversable
    {
        return new \ArrayIterator($this->data);
    }

    public function count(): int
    {
        return \count($this->data);
    }
}