laudis-technologies/neo4j-php-client

View on GitHub
src/Types/Map.php

Summary

Maintainability
D
1 day
Test Coverage
<?php

declare(strict_types=1);

/*
 * This file is part of the Neo4j PHP Client and Driver package.
 *
 * (c) Nagels <https://nagels.tech>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Laudis\Neo4j\Types;

use function array_key_exists;
use function array_key_last;

use ArrayIterator;

use function count;
use function func_num_args;

use Generator;

use function is_array;
use function is_callable;
use function is_iterable;

use Laudis\Neo4j\Databags\Pair;
use Laudis\Neo4j\Exception\RuntimeTypeException;
use Laudis\Neo4j\TypeCaster;
use OutOfBoundsException;

use function sprintf;

use stdClass;

/**
 * An immutable ordered map of items.
 *
 * @template TValue
 *
 * @extends AbstractCypherSequence<TValue, string>
 */
class Map extends AbstractCypherSequence
{
    /**
     * @param iterable<mixed, TValue>|callable():Generator<mixed, TValue> $iterable
     *
     * @psalm-mutation-free
     */
    public function __construct($iterable = [])
    {
        if (is_array($iterable)) {
            $i = 0;
            foreach ($iterable as $key => $value) {
                if (!$this->isStringable($key)) {
                    $key = (string) $i;
                }
                /** @var string $key */
                $this->keyCache[] = $key;
                /** @var TValue $value */
                $this->cache[$key] = $value;
                ++$i;
            }
            /** @var ArrayIterator<string, TValue> */
            $it = new ArrayIterator([]);
            $this->generator = $it;
            $this->generatorPosition = count($this->keyCache);
        } else {
            $this->generator = function () use ($iterable): Generator {
                $i = 0;
                /** @var Generator<mixed, TValue> $it */
                $it = is_callable($iterable) ? $iterable() : $iterable;
                /** @var mixed $key */
                foreach ($it as $key => $value) {
                    if ($this->isStringable($key)) {
                        yield (string) $key => $value;
                    } else {
                        yield (string) $i => $value;
                    }
                    ++$i;
                }
            };
        }
    }

    /**
     * @template Value
     *
     * @param callable():(\Generator<mixed, Value>) $operation
     *
     * @return static<Value>
     *
     * @psalm-mutation-free
     */
    protected function withOperation($operation): Map
    {
        /** @psalm-suppress UnsafeInstantiation */
        return new static($operation);
    }

    /**
     * Returns the first pair in the map.
     *
     * @return Pair<string, TValue>
     */
    public function first(): Pair
    {
        foreach ($this as $key => $value) {
            return new Pair($key, $value);
        }
        throw new OutOfBoundsException('Cannot grab first element of an empty map');
    }

    /**
     * Returns the last pair in the map.
     *
     * @return Pair<string, TValue>
     */
    public function last(): Pair
    {
        $array = $this->toArray();
        if (count($array) === 0) {
            throw new OutOfBoundsException('Cannot grab last element of an empty map');
        }

        $key = array_key_last($array);

        return new Pair($key, $array[$key]);
    }

    /**
     * Returns the pair at the nth position of the map.
     *
     * @return Pair<string, TValue>
     */
    public function skip(int $position): Pair
    {
        $i = 0;
        foreach ($this as $key => $value) {
            if ($i === $position) {
                return new Pair($key, $value);
            }
            ++$i;
        }

        throw new OutOfBoundsException(sprintf('Cannot skip to a pair at position: %s', $position));
    }

    /**
     * Returns the keys in the map in order.
     *
     * @return ArrayList<string>
     *
     * @psalm-suppress UnusedForeachValue
     */
    public function keys(): ArrayList
    {
        return ArrayList::fromIterable((function () {
            foreach ($this as $key => $value) {
                yield $key;
            }
        })());
    }

    /**
     * Returns the pairs in the map in order.
     *
     * @return ArrayList<Pair<string, TValue>>
     */
    public function pairs(): ArrayList
    {
        return ArrayList::fromIterable((function () {
            foreach ($this as $key => $value) {
                yield new Pair($key, $value);
            }
        })());
    }

    /**
     * Create a new map sorted by keys. Natural ordering will be used if no comparator is provided.
     *
     * @param (callable(string, string):int)|null $comparator
     *
     * @return static<TValue>
     */
    public function ksorted(callable $comparator = null): Map
    {
        return $this->withOperation(function () use ($comparator) {
            $pairs = $this->pairs()->sorted(static function (Pair $x, Pair $y) use ($comparator) {
                if ($comparator !== null) {
                    return $comparator($x->getKey(), $y->getKey());
                }

                return $x->getKey() <=> $y->getKey();
            });

            foreach ($pairs as $pair) {
                yield $pair->getKey() => $pair->getValue();
            }
        });
    }

    /**
     * Returns the values in the map in order.
     *
     * @return ArrayList<TValue>
     */
    public function values(): ArrayList
    {
        return ArrayList::fromIterable((function () {
            yield from $this;
        })());
    }

    /**
     * Creates a new map using exclusive or on the keys.
     *
     * @param iterable<array-key, TValue> $map
     *
     * @return static<TValue>
     */
    public function xor(iterable $map): Map
    {
        return $this->withOperation(function () use ($map) {
            $map = Map::fromIterable($map);
            foreach ($this as $key => $value) {
                if (!$map->hasKey($key)) {
                    yield $key => $value;
                }
            }

            foreach ($map as $key => $value) {
                if (!$this->hasKey($key)) {
                    yield $key => $value;
                }
            }
        });
    }

    /**
     * @template NewValue
     *
     * @param iterable<mixed, NewValue> $values
     *
     * @psalm-suppress LessSpecificImplementedReturnType
     *
     * @return self<TValue|NewValue>
     *
     * @psalm-mutation-free
     */
    public function merge(iterable $values): Map
    {
        return $this->withOperation(function () use ($values) {
            $tbr = $this->toArray();
            $values = Map::fromIterable($values);

            foreach ($values as $key => $value) {
                $tbr[$key] = $value;
            }

            yield from $tbr;
        });
    }

    /**
     * Creates a union of this and the provided map. The items in the original map take precedence.
     *
     * @param iterable<mixed, TValue> $map
     *
     * @return static<TValue>
     */
    public function union(iterable $map): Map
    {
        return $this->withOperation(function () use ($map) {
            $map = Map::fromIterable($map)->toArray();
            $x = $this->toArray();

            yield from $x;

            foreach ($map as $key => $value) {
                if (!array_key_exists($key, $x)) {
                    yield $key => $value;
                }
            }
        });
    }

    /**
     * Creates a new map from the existing one filtering the values based on the keys that don't exist in the provided map.
     *
     * @param iterable<array-key, TValue> $map
     *
     * @return static<TValue>
     */
    public function intersect(iterable $map): Map
    {
        return $this->withOperation(function () use ($map) {
            $map = Map::fromIterable($map)->toArray();
            foreach ($this as $key => $value) {
                if (array_key_exists($key, $map)) {
                    yield $key => $value;
                }
            }
        });
    }

    /**
     * Creates a new map from the existing one filtering the values based on the keys that also exist in the provided map.
     *
     * @param iterable<array-key, TValue> $map
     *
     * @return static<TValue>
     */
    public function diff(iterable $map): Map
    {
        return $this->withOperation(function () use ($map) {
            $map = Map::fromIterable($map)->toArray();
            foreach ($this as $key => $value) {
                if (!array_key_exists($key, $map)) {
                    yield $key => $value;
                }
            }
        });
    }

    /**
     * Gets the value with the provided key. If a default value is provided, it will return the default instead of throwing an error when the key does not exist.
     *
     * @template TDefault
     *
     * @param TDefault $default
     *
     * @throws OutOfBoundsException
     *
     * @return (func_num_args() is 1 ? TValue : TValue|TDefault)
     */
    public function get(string $key, $default = null)
    {
        if (!$this->offsetExists($key)) {
            if (func_num_args() === 1) {
                throw new OutOfBoundsException(sprintf('Cannot get item in sequence with key: %s', $key));
            }

            return $default;
        }

        return $this->offsetGet($key);
    }

    public function jsonSerialize(): mixed
    {
        if ($this->isEmpty()) {
            return new stdClass();
        }

        return parent::jsonSerialize();
    }

    public function getAsString(string $key, mixed $default = null): string
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }
        $tbr = TypeCaster::toString($value);
        if ($tbr === null) {
            throw new RuntimeTypeException($value, 'string');
        }

        return $tbr;
    }

    public function getAsInt(string $key, mixed $default = null): int
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }
        $tbr = TypeCaster::toInt($value);
        if ($tbr === null) {
            throw new RuntimeTypeException($value, 'int');
        }

        return $tbr;
    }

    public function getAsFloat(string $key, mixed $default = null): float
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }
        $tbr = TypeCaster::toFloat($value);
        if ($tbr === null) {
            throw new RuntimeTypeException($value, 'float');
        }

        return $tbr;
    }

    public function getAsBool(string $key, mixed $default = null): bool
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }
        $tbr = TypeCaster::toBool($value);
        if ($tbr === null) {
            throw new RuntimeTypeException($value, 'bool');
        }

        return $tbr;
    }

    /**
     * @return null
     */
    public function getAsNull(string $key, mixed $default = null)
    {
        if (func_num_args() === 1) {
            /** @psalm-suppress UnusedMethodCall */
            $this->get($key);
        }

        return TypeCaster::toNull();
    }

    /**
     * @template U
     *
     * @param class-string<U> $class
     *
     * @return U
     */
    public function getAsObject(string $key, string $class, mixed $default = null): object
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }
        $tbr = TypeCaster::toClass($value, $class);
        if ($tbr === null) {
            throw new RuntimeTypeException($value, $class);
        }

        return $tbr;
    }

    /**
     * @return Map<mixed>
     */
    public function getAsMap(string $key, mixed $default = null): Map
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }

        if (!is_iterable($value)) {
            throw new RuntimeTypeException($value, self::class);
        }

        return new Map($value);
    }

    /**
     * @return ArrayList<mixed>
     */
    public function getAsArrayList(string $key, mixed $default = null): ArrayList
    {
        if (func_num_args() === 1) {
            $value = $this->get($key);
        } else {
            /** @var mixed */
            $value = $this->get($key, $default);
        }
        if (!is_iterable($value)) {
            throw new RuntimeTypeException($value, ArrayList::class);
        }

        return new ArrayList($value);
    }

    /**
     * @template Value
     *
     * @param iterable<Value> $iterable
     *
     * @return Map<Value>
     */
    public static function fromIterable(iterable $iterable): Map
    {
        return new self($iterable);
    }
}