chris-kruining/utilities

View on GitHub
src/Collections/Table.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace CPB\Utilities\Collections
{
    use Core\Utility\Exception\NotImplemented;
    use CPB\Utilities\Common\CollectionInterface;
    use CPB\Utilities\Common\Exceptions\NotFound;
    use CPB\Utilities\Common\Regex;
    use CPB\Utilities\Contracts\Queryable;
    use CPB\Utilities\Contracts\Resolvable;
    use CPB\Utilities\Enums\JoinStrategy;
    use CPB\Utilities\Enums\SortDirection;
    use CPB\Utilities\Parser\Expression;

    class Table extends Map implements Queryable
    {
        protected
            $type
        ;

        public function __construct(string $type = null)
        {
            $this->type = $type;
        }

        public static function from(iterable $items, string $type = null): CollectionInterface
        {
            $inst = parent::from($items);
            $inst->type = $type;

            return $inst;
        }

        public function has($key, string ...$keys): bool
        {
            \array_unshift($keys, $key);

            return $this->some(function($i, $row) use($keys){
                if($row instanceof Resolvable)
                {
                    return $row->has(...$keys);
                }
                elseif($row instanceof \Traversable)
                {
                    $exists = true;

                    foreach($keys as $key)
                    {
                        $exists &= isset($row[$key]);
                    }

                    return (bool)$exists;
                }
                elseif(\is_object($row))
                {
                    $exists = true;

                    foreach($keys as $key)
                    {
                        $exists &= \property_exists($row, $key);
                    }

                    return (bool)$exists;
                }
                elseif(\is_array($row))
                {
                    return count(array_diff($keys, array_keys($row))) === 0;
                }

                return false;
            });
        }

        public function get($key, string ...$keys): Resolvable
        {
            \array_unshift($keys, $key);

            if(!$this->has(...$keys))
            {
                throw new NotFound;
            }

            return $this->map(function($i, $row) use($keys){
                if($row instanceof Resolvable)
                {
                    return $row->get(...$keys);
                }
                elseif(\is_object($row))
                {
                    $res = [];

                    foreach($keys as $key)
                    {
                        $res[$key] = $row->$key;
                    }

                    return $res;
                }
                elseif(\is_array($row))
                {
                    $res = [];

                    foreach($keys as $key)
                    {
                        $res[$key] = $row[$key];
                    }

                    return $res;
                }
            });
        }

        /**
         * Queries over the items
         */
        public function select(string $query)
        {
            $result = Expression::init($query)($this);

            if(!$result instanceof CollectionInterface)
            {
                $result = static::from([ [ $query => $result] ]);
            }

            return $result;
        }

        /**
         * Inserts value into the Items based on provided query
         *
         * TODO(Chris Kruining)
         * Rethink implementation to
         * properly support overriding
         * existing keys...
         */
        public function insert(string $query, $value, &$newValue = null): Queryable
        {
            if($this->type !== null && (\is_object($value)
                ? !$value instanceof $this->type
                : \gettype($value) !== $this->type))
            {
                throw new \InvalidArgumentException(\sprintf(
                    'expected value of type %s, got %s',
                    $this->type,
                    \is_object($value)
                        ? \get_class($value)
                        : \gettype($value)
                ));
            }

            $this->items[] = $value;

            $newValue = $this->count() - 1;

            return $this;
        }

        public function delete(): Queryable
        {
            $this->items = [];

            return $this;
        }

        /**
         * Filters items
         *
         * @lazy-chainable true
         * @alias filter
         */
        public function where(string $query, iterable $variables = []): Queryable
        {
            $query = Expression::init(Regex::replace('/:([A-Za-z_][A-Za-z0-9_]*)/', $query, '{{$1}}'));

            return $this->filter(function($row) use($query, $variables){
                if($row instanceof Resolvable)
                {
                    return $query($row, $variables);
                }
                elseif(\is_iterable($row))
                {
                    return $query(Collection::from($row), $variables);
                }

                return false;
            })->values();
        }

        /**
         * Executes a mysql'esc JOIN on the Collection
         *
         * @lazy-chainable true
         */
        public function join(
            iterable $iterable,
            string $localKey,
            string $foreignKey,
            JoinStrategy $strategy = null
        ): Queryable
        {
            throw new NotImplemented;

            $iterable = static::from($iterable)
                ->map(fn($k, $v) => \array_combine(\array_map(fn($key) => 'right' . $key, \array_keys($v)), $v))
                ->toArray();
            $foreignKey = 'right' . $foreignKey;

            $leftIndex = array_map(fn($row) => $row[$localKey], $this->items);
            $rightIndex = array_map(fn($row) => $row[$foreignKey], $iterable);
            $matchedIndexes = array_map(fn($v) => array_search($v, $rightIndex), array_intersect($leftIndex, $rightIndex));

            switch($strategy ?? JoinStrategy::INNER)
            {
                // both collections need to have a matching value
                case Queryable::JOIN_INNER:
                    $result = array_map(
                        fn($k, $v) => \array_merge($this->items[$k], $iterable[$v]),
                        array_keys($matchedIndexes),
                        $matchedIndexes
                    );

                    break;
                // all rows from both collections and intersect matching rows
                case Queryable::JOIN_OUTER:
                    $result = [];
                    $usedIndexes = [];

                    foreach($this->items as $i => $row)
                    {
                        if(key_exists($i, $matchedIndexes))
                        {
                            $usedIndexes[] = $matchedIndexes[$i];

                            $right = $iterable[$matchedIndexes[$i]];
                        }

                        $result[] = array_merge(
                            $row,
                            $right ?? []
                        );
                    }

                    $result = array_merge(
                        $result,
                        array_filter($iterable, fn($i) => \in_array($i, $usedIndexes) === false, ARRAY_FILTER_USE_KEY)
                    );

                    break;
                // all rows from left collection and intersect matching rows
                case Queryable::JOIN_LEFT:
                    $result = [];

                    foreach($this->items as $i => $row)
                    {
                        $result[] = array_merge(
                            $row,
                            $iterable[$matchedIndexes[$i] ?? -1] ?? []
                        );
                    }

                    break;
                // all rows from right collection and intersect matching rows
                case Queryable::JOIN_RIGHT:
                    $result = [];

                    $matchedIndexes = array_flip($matchedIndexes);

                    foreach($iterable as $i => $row)
                    {
                        $result[] = array_merge(
                            $this->items[$matchedIndexes[$i] ?? -1] ?? [],
                            $row
                        );
                    }

                    break;
            }

            return static::from($result);
        }

        public function in(...$args)
        {
        }

        /**
         * Return sub-selection of items
         *
         * @alias slice
         */
        public function limit(int $length): Queryable
        {
            return $this->slice(0, $length);
        }

        /**
         * Return sub-selection of items
         *
         * @alias slice
         */
        public function offset(int $start): Queryable
        {
            return $this->slice($start, null);
        }

        /**
         * Executes a mysql'esc UNION on Collction
         *
         * @alias merge
         */
        public function union(iterable $iterable): Queryable
        {
            return static::from(array_merge($this->items, Collection::from($iterable)->toArray()));
        }

        /**
         * Fetches all unique values of provided key
         */
        public function distinct(string $key): Queryable
        {
            return static::from(array_unique(array_map(fn($v) => $v[$key], $this->items)));
        }

        /**
         * Sort items by provided key and direction
         */
        public function order(string $key, SortDirection $direction = null): Queryable
        {
            return $this->uASort(function($a, $b) use($key, $direction){
                if($direction ?? SortDirection::ASC === SortDirection::DESC)
                {
                    [$b, $a] = [$a, $b];
                }

                return $a[$key] ?? null <=> $b[$key] ?? null;
            });
        }

        /**
         * Prepares the groups of later queries
         *
         * This method sets the key by which the results
         * of: sum, average, max, min and clamp, will
         * group by
         */
        public function group(string $key): Queryable
        {
            $this->groupKey = $key;

            return $this;
        }

        public function count(string $key = null): int
        {
            if($key === null)
            {
                return parent::count();
            }

            // TODO(Chris Kruining)
            // Implement the usage
            // of the key argument.

            return count($this->items);
        }

        public function offsetGet($offset)
        {
            return \is_string($offset) && !\key_exists($offset, $this->items)
                ? $this->select($offset)
                : parent::offsetGet($offset);
        }
        public function offsetSet($offset, $value)
        {
            switch(\gettype($offset))
            {
                case 'string':
                case 'integer':
                    $this->insert($offset, $value);
                    break;

                case 'NULL':
                    $this->items[] = $value;
                    break;

                default:
                    throw new \InvalidArgumentException;
            }
        }

        public function resolve(string $key)
        {
            $result = $this->select($key);

            return count($result) === 1 && $result->toArray()[0] === [ $key => $key]
                ? new Collection
                : $result;
        }
    }
}