3
\$\begingroup\$

I've been working on a class(2 actually) that wraps around the array data structure of PHP. It doesn't bring anything new to the table, it simple makes the function somewhat more abstracted with the possibility of method chaining.

It's made mostly for fun and practice purposes, I don't expect(nor encourage) anybody to use this code, there are other libraries out there that do what I intended, only better and more fluent.


Long story short, as it stands, the following classes are present:

Arr - a wrapper for some default interfaces that PHP provides.

ArrayGroup - a class that extends Arr, provides additional functionality and wraps some commonly used array_* functions. Some functions are missing because I didn't get a chance to write them. Just wanna know if "so far so good", so to speak.

In the order written above, here they are:


<?php

namespace PackageName\Components\Arr;

/**
 * Class Arr
 *
 * @package PackageName\Components\Arr
 */
class Arr implements \ArrayAccess, \Countable, \Iterator
{
    /**
     * @var
     */
    protected $arr;

    /**
     * Arr constructor.
     *
     * @param array $array
     * @throws \Exception
     */
    public function __construct(array $array)
    {
        if(empty($array))
        {
            throw new \Exception('Parameter for construct cannot be empty');
        }

        $this->arr = $array;
    }

    /**
     * Returns a count of the elements in the array
     *
     * @return int
     */
    public function count()
    {
        return count($this->arr);
    }

    /**
     * Checks if the current array is empty
     *
     * @return bool
     */
    public function isEmpty()
    {
        return empty($this->arr);
    }

    /**
     * Uses dot notation to determine if an offset exists
     *
     * @param mixed $offset
     *
     * @return bool
     */
    public function offsetExists($offset)
    {
        if(empty($offset))
        {
            throw new \InvalidArgumentException('Offset is invalid');
        }

        // Remove any prepended or trailing dots
        $offset = trim($offset, '.');

        $keys = explode('.', $offset);

        $array = $this->arr;

        foreach($keys as $key)
        {
            if(isset($array[ $key ]))
            {
                $array = $array[ $key ];
            }
            else
            {
                return false;
            }
        }

        return true;
    }

    /**
     * Uses dot notation to return an offset given that it exists
     *
     * @param mixed $offset
     *
     * @return mixed
     */
    public function offsetGet($offset)
    {
        if($this->isEmpty())
        {
            throw new \InvalidArgumentException('Cannot get offset on a empty array');
        }

        if(empty($offset))
        {
            throw new \InvalidArgumentException('Offset is invalid');
        }

        $array = $this->arr;

        // Remove any prepended or trailing dots
        $offset = trim($offset, '.');

        $keys = explode('.', $offset);

        foreach($keys as $key)
        {
            if(isset($array[ $key ]))
            {
                $array = $array[ $key ];
            }
        }

        return $array;
    }

    /**
     * Uses dot notation to set a new element in the array
     * If the array key is already taken it will throw
     *
     * @param mixed $offset
     * @param mixed $value
     *
     * @return $this
     * @throws \Exception
     */
    public function offsetSet($offset, $value)
    {
        if($this->offsetExists($offset))
        {
            throw new \InvalidArgumentException(sprintf('Unable to set key "%s", a key with that name already exists.', $offset));
        }

        if(empty($offset))
        {
            if($this->isEmpty())
            {
                $offset = 0;
            }
            else
            {
                $offset = max(array_keys($this->arr)) + 1;
            }
        }

        // Remove any prepended or trailing dots
        $offset = trim($offset, '.');

        $array = &$this->arr;

        $keys = explode('.', $offset);

        while(count($keys) > 0)
        {
            if(count($keys) === 1)
            {
                if( ! is_array($array))
                {
                    throw new \OutOfBoundsException(sprintf('Unable to set value. %s is not an array.', gettype($offset)));
                }

                $array[ array_shift($keys) ] = $value;
            }
            else
            {
                $key = array_shift($keys);

                if(is_array($array[ $key ]))
                {
                    $array = &$array[ $key ];
                }
                else
                {
                    $array[ $key ] = [];

                    $array = &$array[ $key ];
                }
            }
        }

        return $this;
    }

    /**
     * Uses dot notation to unset an array offset
     *
     * @param mixed $offset
     *
     * @return $this
     * @throws \Exception
     */
    public function offsetUnset($offset)
    {
        if( ! $this->offsetExists($offset))
        {
            throw new \OutOfBoundsException('Offset does not exist');
        }

        if(empty($offset))
        {
            throw new \InvalidArgumentException('Offset is invalid');
        }

        $array = &$this->arr;

        // Remove any prepended or trailing dots
        $offset = trim($offset, '.');

        $keys = explode('.', $offset);

        while(count($keys) > 0)
        {
            if(count($keys) === 1)
            {
                if(is_array($array))
                {
                    unset($array[ array_shift($keys) ]);
                }

                if( ! is_array($array))
                {
                    throw new \OutOfBoundsException(sprintf('Unable to unset value. %s is not an array.', gettype($offset)));
                }
            }
            else
            {
                $key = array_shift($keys);

                if( ! isset($array[ $key ]))
                {
                    $array[ $key ] = [];
                }

                $array = &$array[ $key ];
            }
        }

        return $this;
    }

    /**
     * Returns the current element in the array at
     * which the internal pointer is pointing
     *
     * @return mixed
     * @throws \Exception
     */
    public function current()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to current element of an empty array');
        }

        return current($this->arr);
    }

    /**
     * Sets the internal pointer to the next element of the
     * array and returns the newly pointed at element
     *
     * @return mixed
     */
    public function next()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to get next element in an empty array');
        }

        return next($this->arr);
    }

    /**
     * Returns the current key at which the internal
     * pointer is pointing at. Returns null in case
     * there are no more elements in the array
     *
     * @return mixed
     */
    public function key()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to get key in an empty array');
        }

        return key($this->arr);
    }

    /**
     * Checks if the current element is a valid
     *
     * @return boolean
     */
    public function valid()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to validate an empty array');
        }

        return false !== current($this->arr);
    }

    /**
     * Rewind the array to the first element
     *
     * @return $this
     */
    public function rewind()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to reset an empty array');
        }

        reset($this->arr);

        return $this;
    }
}

<?php

namespace PackageName\Components\Arr;

use PackageName\Components\Contracts\Jsonable;

/**
 * Class ArrayGroup
 *
 * @package PackageName\Components\Arr
 */
class ArrayGroup extends Arr implements Jsonable, \JsonSerializable
{
    /**
     * Counts recursively the elements in the array
     *
     * @return int
     */
    public function countRecursive()
    {
        return count($this->arr, COUNT_RECURSIVE);
    }

    /**
     * Uses dot notation
     *
     * Alias for offsetUnset
     *
     * @param null $value
     *
     * @throws \Exception
     * @return $this
     */
    public function forget($value)
    {
        return $this->offsetUnset($value);
    }

    /**
     * Uses dot notation
     *
     * Alias for offsetExists
     *
     * @param $value
     *
     * @return bool|void
     */
    public function has($value)
    {
        return $this->offsetExists($value);
    }

    /**
     * Uses dot notation
     *
     * Alias for offsetGet
     *
     * @param $value
     *
     * @return bool|void
     */
    public function get($value)
    {
        return $this->offsetGet($value);
    }

    /**
     * Uses dot notation
     *
     * Alias for offsetSet
     *
     * @param $key
     * @param $value
     *
     * @return $this
     */
    public function set($key, $value)
    {
        return $this->offsetSet($key, $value);
    }

    /**
     * Alias for avg()
     *
     * @return float
     */
    public function average()
    {
        return $this->avg();
    }

    /**
     * Compute the average sum off the values in the array
     *
     * Strings, booleans, etc will be treated as int
     *
     * @return float
     * @throws \Exception
     */
    public function avg()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot average an empty array');
        }

        return (array_sum($this->arr) / $this->count());
    }

    /**
     * Filters an array based on a closure passed as the argument
     *
     * @param \Closure $callback
     *
     * @return $this
     * @throws \Exception
     */
    public function filter(\Closure $callback)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot filter an empty array');
        }

        return new $this(array_filter($this->arr, $callback, ARRAY_FILTER_USE_BOTH));
    }

    /**
     * Combines two arrays
     *
     * The keys for $this->arr are used as keys for the new array
     * The values from $array are used as values for the new array
     *
     * Returns a new instance of this class with $this->arr as
     * the result of the combining
     *
     * @param array $array
     *
     * @return $this
     */
    public function combine(array $array)
    {
        if($this->isEmpty() || empty($array))
        {
            throw new \OutOfBoundsException('Cannot combine an empty array');
        }

        return new $this(array_combine($this->keys(), array_values($array)));
    }

    /**
     * @param \Closure $callback
     *
     * @throws \OutOfBoundsException
     * @return $this
     */
    public function map(\Closure $callback)
    {
        return new $this(
            array_combine($this->keys(),
                array_map(
                    $callback,
                    $this->arr,
                    $this->keys())
            )
        );
    }

    /**
     * Returns the keys of the array
     *
     * @return array|null
     * @throws \Exception
     */
    public function keys()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get keys of an empty array');
        }

        return array_keys($this->arr);
    }

    /**
     * Excludes some values from the array and returns a new instance of $this
     *
     * @param $values
     *
     * @return $this
     */
    public function except($values)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get values of an empty array');
        }

        if(is_array($values))
        {
            if($this->countRecursive() !== $this->count())
            {
                throw new \OutOfBoundsException('Cannot accept a multidimensional array');
            }

            foreach($values as $value)
            {
                $this->forget($value);
            }

            return $this;
        }

        $this->forget($values);

        return $this;
    }

    /**
     * Flips the array and returns a new instance of this class
     *
     * @return $this
     */
    public function flip()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot flip an empty array');
        }

        if($this->countRecursive() !== $this->count())
        {
            throw new \OutOfBoundsException('The current array is not suitable for flipping. It has keys that are not integers or strings.');
        }

        return new $this(array_flip($this->arr));
    }

    /**
     * Returns the values of the array
     *
     * @return array
     * @throws \Exception
     */
    public function values()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get values of an empty array');
        }

        return array_values($this->arr);
    }

    /**
     * Alias for current
     *
     * @return mixed
     */
    public function value()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get value of an empty array');
        }

        return $this->current();
    }

    /**
     * Returns all the values in the array
     *
     * @return array
     */
    public function all()
    {
        // no need to throw an exception here
        // the array may be legitimately empty
        return $this->arr;
    }

    /**
     * Splits the array into chunks and returns a new instance of
     * $this with $this->arr as the result of the chunking
     *
     * @param $chunk
     *
     * @return $this
     * @throws \Exception
     */
    public function chunk($chunk)
    {
        if((int)$chunk !== $chunk)
        {
            throw new \InvalidArgumentException(sprintf('Argument must be of type int, %s given.', gettype($chunk)));
        }

        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot chunk an empty array');
        }

        return new $this(array_chunk($this->arr, $chunk, true));
    }

    /**
     * Returns the first element in the array
     *
     * @return null
     * @throws \Exception
     */
    public function first()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get first element of an empty array');
        }

        return array_values($this->arr)[0];
    }

    /**
     * Return the last element in the array
     *
     * @return array|null
     * @throws \Exception
     */
    public function last()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get last element of an empty array');
        }

        return (array_slice($this->arr, -1));
    }

    /**
     * Returns the smallest value of the array
     *
     * @return mixed|null
     * @throws \Exception
     */
    public function minValue()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get minValue of an empty array');
        }

        return min(array_values($this->arr));
    }

    /**
     * Returns the largest value of the array
     *
     * @throws \Exception
     * @return mixed|null
     */
    public function maxValue()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get maxValue of an empty array');
        }

        return max(array_values($this->arr));
    }

    /**
     * Returns the smallest key of the array
     *
     * @throws \Exception
     * @return mixed|null
     */
    public function minKey()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get minKey of an empty array');
        }

        return min(array_keys($this->arr));
    }

    /**
     * Returns the largest key of the array
     *
     * @return mixed|null
     */
    public function maxKey()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get minKey of an empty array');
        }

        return max(array_keys($this->arr));
    }

    /**
     * Returns the sum of the array
     *
     * @return null|number
     */
    public function sum()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot compute sum of an empty array');
        }

        return array_sum($this->arr);
    }

    /**
     * Returns the values in a key
     *
     * @param      $column
     * @param null $indexKey
     *
     * @return array
     */
    public function column($column, $indexKey = null)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get column of an empty array');
        }

        return array_column($this->arr, $column, $indexKey);
    }

    /**
     * Walks the array and implodes all values with a given glue
     *
     * @param $glue
     *
     * @return string
     */
    public function implodeRecursive($glue)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot implode an empty array');
        }

        $result = '';

        array_walk_recursive($this->arr, function ($value, $key) use (&$result, $glue)
        {
            $result .= $value . $glue;
        });

        return rtrim($result, $glue);
    }

    /**
     * Implodes the value of the array with a given glue
     *
     * @param $glue
     *
     * @return string
     */
    public function implode($glue)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot implode an empty array');
        }

        return implode($glue, $this->arr);
    }

    /**
     * Removes any values in $this->arr that are not present in the $array argument
     *
     * @param array $array
     *
     * @return mixed
     */
    public function intersect(array $array)
    {
        return new $this(array_intersect($this->arr, $array));
    }

    /**
     * Flattens an array
     *
     * @return array
     */
    public function flatten()
    {
        $result = [];

        array_walk_recursive($this->arr, function ($key, $value) use (&$result)
        {
            $result[] = $value;
        });

        return $result;
    }

    /**
     * Get every nth element in the array
     *
     * @param $nth
     *
     * @return array
     */
    public function nth($nth)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get nth element of an empty array');
        }

        if((int)$nth !== $nth || (int)$nth == 0 || $nth < 0)
        {
            throw new \OutOfBoundsException(sprintf('Function argument for "%s" must be of type int, not equal to zero and positive'), __METHOD__);
        }

        $result = [];

        $i = 0;

        foreach($this->arr as $value)
        {
            if($i++ % (int)$nth == 0)
            {
                $result[] = $value;
            }
        }

        return $result;
    }

    /**
     * Returns a random element in the array
     *
     * @return mixed
     */
    public function random()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot get random element of an empty array');
        }

        return array_rand($this->arr);
    }

    /**
     * Returns an new ArrayIterator instance
     *
     * @return \ArrayIterator
     */
    public function arrayIterator()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot iterate over an empty array');
        }

        return new \ArrayIterator($this->arr);
    }

    /**
     * Pop the element off the end of array
     *
     * @return mixed the last value of array
     */
    public function pop()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to pop elements in a empty array');
        }

        array_pop($this->arr);

        return $this;
    }

    /**
     * Shift an element off the beginning of array
     *
     * @return mixed the shifted value
     */
    public function shift()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to shift elements in a empty array');
        }

        array_shift($this->arr);
        return $this;
    }

    /**
     * Prepend one or more elements to the beginning of an array
     *
     * @param $value
     *
     * @return int
     */
    public function unshift($value)
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Unable to unshift elements in a empty array');
        }

        array_unshift($this->arr, $value);

        return $this;
    }

    /**
     * Push one or more elements onto the end of array
     *
     * @param $value
     *
     * @return int the new number of elements in the array.
     */
    public function push($value)
    {
        array_push($this->arr, $value);

        return $this;
    }

    /**
     * Json encode the current array
     *
     * @return string
     *
     * @throw \Exception
     */
    public function toJson()
    {
        if($this->isEmpty())
        {
            throw new \OutOfBoundsException('Cannot jsonify an empty array');
        }

        return json_encode($this->arr, JSON_UNESCAPED_UNICODE);
    }

    /**
     * Serialize a json string and returns the result
     *
     * @return string
     */
    public function jsonSerialize()
    {
        return serialize($this->toJson());
    }
}

<?php

namespace PackageName\Components\Contracts;

/**
 * Interface Jsonable
 *
 * @package PackageName\Components\Arr\Contracts
 */
interface Jsonable
{
    /**
     * Implement whenever something should return a json
     *
     * @return mixed
     */
    public function toJson();
}

Example:

require 'vendor/autoload.php';
echo '<pre>';

$array = [
    1, 'snake_case', 3, 'camelCase' => [
        'isCamelCase' => 'aValue'
    ], 4,
];

$arrayGroup = new \PackageName\Components\Arr\ArrayGroup($array);

$result = $arrayGroup
    ->forget('camelCase.isCamelCase') // Uses "dot" notation
    ->filter(function ($value, $key)
    {
        return is_int($value) ? true : false;
    })
    ->map(function ($value, $key)
    {
        return $value * 2;
    })
    ->all();

print_r($result);

Array
(
    [0] => 2
    [2] => 6
    [4] => 8
)

Explanation:

->forget() will unset a selected key

->filter() is a simple wrapper with annonymous functions for array_filter

->map() is another wrapper for array_map

->all() simply returns the array


print_r($arrayGroup->chunk(2)->all());

Array
(
    [0] => Array
        (
            [0] => 1
            [1] => snake_case
        )

    [1] => Array
        (
            [2] => 3
            [camelCase] => Array
                (
                )

        )

    [2] => Array
        (
            [3] => 4
        )

)

I could go on an and on, but I assume you get the idea.

\$\endgroup\$
  • \$\begingroup\$ As we all want to make our code more efficient or improve it in one way or another, try to write a title that summarizes what your code does, not what you want to get out of a review. Please see How to get the best value out of Code Review - Asking Questions for guidance on writing good question titles. \$\endgroup\$ – Mast Oct 8 '16 at 18:56
  • \$\begingroup\$ Well, that's the best I could come up with on such short notice. \$\endgroup\$ – user2994883 Oct 8 '16 at 18:59
  • \$\begingroup\$ The new one is much better. It's not that difficult, but it has a major positive effect on the visibility of your question. You do want your question to be read, otherwise there won't be any answers posted :-) \$\endgroup\$ – Mast Oct 8 '16 at 19:03
  • \$\begingroup\$ As far as I can tell PHP doesn't get much love here anyway. I'm mostly a lurker(well, obviously, my reputation is close to none). So I doubt people are gonna line up to answer, or rather review this question. But, hey, fair point, can't argue with that. \$\endgroup\$ – user2994883 Oct 8 '16 at 19:05
1
\$\begingroup\$

I would immediately question the value of writing any code that, as you said "doesn't bring anything new to the table". You are adding complexity and obfuscation around fundamental language behavior that most PHP developers are intimately familiar with. When I create something as fundamental as an array, I just want it to work the way I expect it to, without any intervening logic.

Outside of that, I find your code generally very well written compared to most examples on here, with appropriate data validation around your public methods, great documentation/comments, appropriate level of functionality in your methods, etc.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.