├── docs
├── Gemfile
├── index.md
└── _config.yml
├── .github
├── benchmarks.png
├── logo-black.png
├── logo-white.png
└── workflows
│ ├── linting.yml
│ └── ci.yml
├── .gitignore
├── tests
├── bootstrap.php
├── Helpers
│ ├── MockIteratorAggregate.php
│ ├── Generators.php
│ └── ArrayAccessible.php
└── __
│ ├── Arrays
│ ├── PrependTest.php
│ ├── RepeatTest.php
│ ├── AppendTest.php
│ ├── RangeTest.php
│ ├── PatchTest.php
│ ├── RandomizeTest.php
│ ├── CompactTest.php
│ ├── DropTest.php
│ ├── DropRightTest.php
│ ├── DropRightWhileTest.php
│ └── DropWhileTest.php
│ ├── Utilities
│ ├── NowTest.php
│ └── IdentityTest.php
│ ├── Objects
│ ├── IsNullTest.php
│ ├── IsArrayTest.php
│ ├── IsNumberTest.php
│ ├── IsStringTest.php
│ ├── IsEmailTest.php
│ ├── IsObjectTest.php
│ ├── IsFunctionTest.php
│ └── IsCollectionTest.php
│ ├── Strings
│ ├── CapitalizeTest.php
│ ├── LowerFirstTest.php
│ ├── UpperFirstTest.php
│ ├── SplitTest.php
│ ├── KebabCaseTest.php
│ ├── SnakeCaseTest.php
│ ├── ToLowerTest.php
│ ├── ToUpperTest.php
│ ├── CamelCaseTest.php
│ ├── LowerCaseTest.php
│ ├── StartCaseTest.php
│ ├── UpperCaseTest.php
│ └── WordsTest.php
│ ├── Functions
│ ├── TruncateTest.php
│ ├── SlugTest.php
│ └── UrlifyTest.php
│ ├── Collections
│ ├── MaxTest.php
│ ├── MinTest.php
│ ├── UneaseTest.php
│ ├── FirstTest.php
│ ├── ReduceRightTest.php
│ ├── DoForEachRightTest.php
│ ├── HasTest.php
│ ├── AssignTest.php
│ ├── ReverseIterableTest.php
│ ├── HasKeysTest.php
│ ├── FindTest.php
│ ├── FindLastTest.php
│ ├── WhereTest.php
│ ├── SomeTest.php
│ ├── FindLastIndexTest.php
│ ├── LastTest.php
│ ├── EveryTest.php
│ ├── DoForEachTest.php
│ ├── EaseTest.php
│ ├── PickTest.php
│ ├── PluckTest.php
│ ├── SizeTest.php
│ ├── IsEmptyTest.php
│ ├── MergeTest.php
│ ├── MapTest.php
│ ├── SetTest.php
│ ├── GetTest.php
│ ├── FindIndexTest.php
│ └── ConcatDeepTest.php
│ └── Sequences
│ └── ChainTest.php
├── src
└── __
│ ├── utilities
│ ├── now.php
│ └── identity.php
│ ├── objects
│ ├── isNull.php
│ ├── isArray.php
│ ├── isObject.php
│ ├── isString.php
│ ├── isFunction.php
│ ├── isNumber.php
│ ├── isEmail.php
│ ├── isCollection.php
│ ├── isIterable.php
│ └── isEqual.php
│ ├── strings
│ ├── toUpper.php
│ ├── lowerFirst.php
│ ├── toLower.php
│ ├── upperFirst.php
│ ├── capitalize.php
│ ├── lowerCase.php
│ ├── upperCase.php
│ ├── snakeCase.php
│ ├── kebabCase.php
│ ├── camelCase.php
│ ├── startCase.php
│ └── split.php
│ ├── arrays
│ ├── prepend.php
│ ├── append.php
│ ├── randomize.php
│ ├── repeat.php
│ ├── range.php
│ ├── compact.php
│ ├── patch.php
│ ├── flatten.php
│ ├── drop.php
│ ├── dropRight.php
│ ├── chunk.php
│ ├── dropWhile.php
│ └── dropRightWhile.php
│ ├── sequences
│ └── chain.php
│ ├── collections
│ ├── min.php
│ ├── max.php
│ ├── getIterator.php
│ ├── isEmpty.php
│ ├── first.php
│ ├── doForEachRight.php
│ ├── reverseIterable.php
│ ├── doForEach.php
│ ├── last.php
│ ├── unease.php
│ ├── hasKeys.php
│ ├── size.php
│ ├── where.php
│ ├── map.php
│ ├── reduceRight.php
│ ├── pick.php
│ ├── mapValues.php
│ ├── merge.php
│ ├── every.php
│ ├── assign.php
│ ├── ease.php
│ ├── some.php
│ ├── has.php
│ ├── pluck.php
│ ├── mapKeys.php
│ ├── filter.php
│ ├── findLastEntry.php
│ ├── find.php
│ ├── concat.php
│ ├── get.php
│ ├── concatDeep.php
│ ├── findLast.php
│ ├── findEntry.php
│ ├── findIndex.php
│ ├── findLastIndex.php
│ ├── reduce.php
│ └── set.php
│ └── functions
│ ├── truncate.php
│ └── urlify.php
├── vendor-bin
└── php-cs-fixer
│ └── composer.json
├── bottomline.php
├── .editorconfig
├── docgen
├── Parsers.php
└── ArgumentDocumentation.php
├── phpunit.xml
├── LICENSE
├── .php-cs-fixer.dist.php
├── README.md
├── composer.json
└── CONTRIBUTING.md
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'github-pages', group: :jekyll_plugins
4 |
--------------------------------------------------------------------------------
/.github/benchmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejczyzewski/bottomline/HEAD/.github/benchmarks.png
--------------------------------------------------------------------------------
/.github/logo-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejczyzewski/bottomline/HEAD/.github/logo-black.png
--------------------------------------------------------------------------------
/.github/logo-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maciejczyzewski/bottomline/HEAD/.github/logo-white.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .php_cs.cache
3 | .php-cs-fixer.cache
4 | .phpunit.result.cache
5 | _site/
6 | composer.phar
7 | Gemfile.lock
8 | vendor/
9 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | data = $data;
12 | }
13 |
14 | #[\ReturnTypeWillChange]
15 | public function getIterator()
16 | {
17 | return new \ArrayIterator($this->data);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/__/Arrays/PrependTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([4, 1, 2, 3], $x);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/__/Utilities/NowTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, is_numeric($x));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/__/Arrays/RepeatTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([$string, $string, $string], $x);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/__/strings/toUpper.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/__/utilities/identity.php:
--------------------------------------------------------------------------------
1 | $value) {
17 | yield $key => $value;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsArrayTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsNumberTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsStringTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsEmailTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsObjectTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(false, $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/__/Strings/CapitalizeTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('Fred', $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/__/arrays/prepend.php:
--------------------------------------------------------------------------------
1 | assertEquals([1, 2, 3, 4], $x);
19 | $this->assertEquals([1, 2, 3, [4, 5]], $x2);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/__/arrays/append.php:
--------------------------------------------------------------------------------
1 | assertEquals([1, 2, 3, 4, 5], $x);
19 | $this->assertEquals([-2, -1, 0, 1, 2], $y);
20 | $this->assertEquals([1, 3, 5, 7, 9], $z);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsFunctionTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/__/Utilities/IdentityTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('arg 1', $x);
19 |
20 | // Act
21 | $x = __::identity();
22 |
23 | // Assert
24 | $this->assertEquals(null, $x);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/__/Functions/TruncateTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('Lorem ipsum dolor sit amet, ...', $x);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/__/Strings/LowerFirstTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('fred', $x);
24 | $this->assertEquals('fRED', $y);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/__/Strings/UpperFirstTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('Fred', $x);
24 | $this->assertEquals('FRED', $y);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/__/Arrays/PatchTest.php:
--------------------------------------------------------------------------------
1 | ['country' => 'US', 'tel' => [123]], 99];
14 | $p = ['/0' => 2, '/1' => 3, '/contacts/country' => 'CA', '/contacts/tel/0' => 3456];
15 |
16 | // Act
17 | $x = __::patch($a, $p);
18 |
19 | // Assert
20 | $this->assertEquals([2, 3, 1, 'contacts' => ['country' => 'CA', 'tel' => [3456]], 99], $x);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/__/arrays/randomize.php:
--------------------------------------------------------------------------------
1 | compact()
16 | * ->prepend(4)
17 | * ->value()
18 | * ;
19 | * ```
20 | *
21 | * **Result**
22 | *
23 | * ```
24 | * [4, 1, 2, 3]
25 | * ```
26 | *
27 | * @param mixed $initialValue
28 | *
29 | * @return \BottomlineWrapper
30 | */
31 | function chain($initialValue)
32 | {
33 | return new \BottomlineWrapper($initialValue);
34 | }
35 |
--------------------------------------------------------------------------------
/src/__/arrays/range.php:
--------------------------------------------------------------------------------
1 | assertEquals(['github', 'com'], $x);
25 | $this->assertEquals(['a', 'b', 'c'], $y);
26 | $this->assertEquals(['a', 'b-c'], $z);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/__/Functions/SlugTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('jakies-zdanie-z-duza-iloscia-obcych-znakow', $actual);
18 | }
19 |
20 | public function testSlugWithAscii()
21 | {
22 | $input = 'Hello World!';
23 | $actual = __::slug($input);
24 |
25 | $this->assertEquals('hello-world', $actual);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/__/Strings/KebabCaseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('foo-bar', $x);
26 | $this->assertEquals('foo-bar', $y);
27 | $this->assertEquals('foo-bar', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/SnakeCaseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('foo_bar', $x);
26 | $this->assertEquals('foo_bar', $y);
27 | $this->assertEquals('foo_bar', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/ToLowerTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('--foo-bar--', $x);
26 | $this->assertEquals('foobar', $y);
27 | $this->assertEquals('__foo_bar__', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/ToUpperTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('--FOO-BAR--', $x);
26 | $this->assertEquals('FOOBAR', $y);
27 | $this->assertEquals('__FOO_BAR__', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/CamelCaseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('fooBar', $x);
26 | $this->assertEquals('fooBar', $y);
27 | $this->assertEquals('fooBar', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/LowerCaseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('foo bar', $x);
26 | $this->assertEquals('foo bar', $y);
27 | $this->assertEquals('foo bar', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/StartCaseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('Foo Bar', $x);
26 | $this->assertEquals('Foo Bar', $y);
27 | $this->assertEquals('FOO BAR', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Strings/UpperCaseTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('FOO BAR', $x);
26 | $this->assertEquals('FOO BAR', $y);
27 | $this->assertEquals('FOO BAR', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Collections/MaxTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(3, $x);
22 | }
23 |
24 | public function testMaxIterable()
25 | {
26 | // Arrange
27 | $a = new ArrayIterator([1, 2, 3]);
28 |
29 | // Act
30 | $x = __::max($a);
31 |
32 | // Assert
33 | $this->assertEquals(3, $x);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/__/Collections/MinTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(1, $x);
22 | }
23 |
24 | public function testMinIterable()
25 | {
26 | // Arrange
27 | $a = new ArrayIterator([1, 2, 3]);
28 |
29 | // Act
30 | $x = __::min($a);
31 |
32 | // Assert
33 | $this->assertEquals(1, $x);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/__/Objects/IsCollectionTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(true, $x);
26 | $this->assertEquals(true, $y);
27 | $this->assertEquals(false, $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/__/Arrays/RandomizeTest.php:
--------------------------------------------------------------------------------
1 | assertNotEquals([1, 2, 3, 4], $x);
26 | $this->assertEquals([1], $y);
27 | $this->assertEquals([2, 1], $z);
28 | $this->assertEquals([], $f);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/__/collections/min.php:
--------------------------------------------------------------------------------
1 | $maxValue) {
32 | $maxValue = $value;
33 | }
34 | }
35 | return $maxValue;
36 | }
37 |
--------------------------------------------------------------------------------
/src/__/strings/lowerCase.php:
--------------------------------------------------------------------------------
1 | content);
13 | }
14 |
15 | #[\ReturnTypeWillChange]
16 | public function offsetGet($offset)
17 | {
18 | return $this->content[$offset];
19 | }
20 |
21 | #[\ReturnTypeWillChange]
22 | public function offsetSet($offset, $value)
23 | {
24 | $this->content[$offset] = $value;
25 | }
26 |
27 | #[\ReturnTypeWillChange]
28 | public function offsetUnset($offset)
29 | {
30 | unset($this->content[$offset]);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/__/strings/snakeCase.php:
--------------------------------------------------------------------------------
1 | create(ParserFactory::PREFER_PHP5);
25 | }
26 | if (!isset(self::$markdown)) {
27 | self::$markdown = new Parsedown();
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/__/strings/camelCase.php:
--------------------------------------------------------------------------------
1 | $limit) {
30 | $words = str_word_count($text, 2);
31 | $pos = array_keys($words);
32 | $text = mb_substr($text, 0, $pos[$limit], 'UTF-8') . '...';
33 | }
34 |
35 | return $text;
36 | }
37 |
--------------------------------------------------------------------------------
/src/__/strings/startCase.php:
--------------------------------------------------------------------------------
1 | assertEquals('I love google.com', $x);
26 | $this->assertEquals('I love google.com', $y);
27 | $this->assertEquals('I love google.com !', $z);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/__/collections/getIterator.php:
--------------------------------------------------------------------------------
1 | getIterator();
25 | }
26 |
27 | throw new \InvalidArgumentException('$input should implement the Iterator or IteratorAggregate interface');
28 | }
29 |
--------------------------------------------------------------------------------
/src/__/collections/isEmpty.php:
--------------------------------------------------------------------------------
1 | assertEquals(['fred', 'barney', 'pebbles'], $x);
29 | $this->assertEquals(['fred', 'barney', '&', 'pebbles'], $y);
30 | $this->assertEquals([], $z);
31 | $this->assertEquals(['foo', 'Bar'], $u);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/__/strings/split.php:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | ## Installation
13 |
14 | Install bottomline in your project via [Composer](https://getcomposer.org/).
15 |
16 | ```bash
17 | composer require maciejczyzewski/bottomline
18 | ```
19 |
20 | ### Requirements
21 |
22 | - PHP 5.5+
23 |
24 |
25 |
26 |
27 | ## Documentation
28 |
29 | This library organizes functions into several namespaces based on their own functionality. These are the available namespaces:
30 |
31 | {% assign namespaces = site.data.fxn_registry.methods | map: "namespace" | uniq %}
32 | {% for method in namespaces -%}
33 | - [{{ method | capitalize }}](documentation/#{{ method }})
34 | {% endfor %}
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/__/collections/first.php:
--------------------------------------------------------------------------------
1 | 'ter', 'baz.0' => 'b', 'baz.1' => 'z'];
15 |
16 | // Act
17 | $x = __::unease($a);
18 |
19 | // Assert
20 | $this->assertCount(2, $x);
21 | $this->assertEquals(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']], $x);
22 | }
23 |
24 | public function testUneaseIterable()
25 | {
26 | // Arrange
27 | $a = new ArrayIterator(['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z']);
28 |
29 | // Act
30 | $x = __::unease($a);
31 |
32 | // Assert
33 | $this->assertCount(2, $x);
34 | $this->assertEquals(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']], $x);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/__/Collections/FirstTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(1, $x);
22 | $this->assertEquals([1, 2], $y);
23 | }
24 |
25 | public function testFirstIterable()
26 | {
27 | if (version_compare(PHP_VERSION, '7.1', '<')) {
28 | return;
29 | }
30 | // Arrange
31 | $a = new ArrayIterator([1, 2, 3, 4, 5]);
32 |
33 | // Act
34 | $x = __::first($a);
35 | $y = __::first($a, 2);
36 |
37 | // Assert
38 | $this->assertEquals(1, $x);
39 | $this->assertEquals([1, 2], $y);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/__/collections/doForEachRight.php:
--------------------------------------------------------------------------------
1 | current($iterable_values);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/__/collections/doForEach.php:
--------------------------------------------------------------------------------
1 | $value) {
33 | if ($iteratee($value, $key, $collection) === false) {
34 | break;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/__/collections/last.php:
--------------------------------------------------------------------------------
1 | = $take) {
40 | return false;
41 | }
42 |
43 | $result = \__::prepend($result, $value);
44 | });
45 |
46 | return $result;
47 | }
48 |
--------------------------------------------------------------------------------
/src/__/collections/unease.php:
--------------------------------------------------------------------------------
1 | 'ter', 'baz.0' => 'b', , 'baz.1' => 'z']);
13 | * ```
14 | *
15 | * **Result**
16 | *
17 | * ```
18 | * ['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]
19 | * ```
20 | *
21 | * @since 0.2.0 added support for iterables
22 | *
23 | * @param iterable|\stdClass $collection Hash map of values
24 | * @param string $separator The glue used in the keys
25 | *
26 | * @return array
27 | */
28 | function unease($collection, $separator = '.')
29 | {
30 | $nonDefaultSeparator = $separator !== '.';
31 | $map = [];
32 | foreach ($collection as $key => $value) {
33 | $map = \__::set(
34 | $map,
35 | $nonDefaultSeparator ? str_replace($separator, '.', $key) : $key,
36 | $value
37 | );
38 | }
39 |
40 | return $map;
41 | }
42 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | tests
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | tests
27 | vendor
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/__/collections/hasKeys.php:
--------------------------------------------------------------------------------
1 | 'bar', 'foz' => 'baz'], ['foo', 'foz']);
13 | * ```
14 | *
15 | * **Result**
16 | *
17 | * ```
18 | * true
19 | * ```
20 | *
21 | * @param array|\stdClass $collection of key values pairs
22 | * @param array $keys collection of keys to look for
23 | * @param bool $strict to exclusively check
24 | *
25 | * @return bool
26 | */
27 | function hasKeys($collection = [], array $keys = [], $strict = false)
28 | {
29 | $keyCount = count($keys);
30 | if ($strict && count($collection) !== $keyCount) {
31 | return false;
32 | }
33 | return \__::every(
34 | \__::map($keys, function ($key) use ($collection) {
35 | return \__::has($collection, $key);
36 | }),
37 | function ($v) {
38 | return $v === true;
39 | }
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/__/collections/size.php:
--------------------------------------------------------------------------------
1 | assertEquals('cba', $x);
25 | }
26 |
27 | public function testReduceRightIterable()
28 | {
29 | if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
30 | // Arrange
31 | $a = new ArrayIterator(['a', 'b', 'c']);
32 | $aReducer = function ($word, $char) {
33 | return $word . $char;
34 | };
35 |
36 | // Act
37 | $x = __::reduceRight($a, $aReducer, '');
38 |
39 | // Assert
40 | $this->assertEquals('cba', $x);
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/__/collections/where.php:
--------------------------------------------------------------------------------
1 | 'fred', 'age' => 32],
13 | * ['name' => 'maciej', 'age' => 16]
14 | * ];
15 | *
16 | * __::where($a, ['age' => 16]);
17 | * ```
18 | *
19 | * **Result**
20 | *
21 | * ```
22 | * [['name' => 'maciej', 'age' => 16]]
23 | * ```
24 | *
25 | * @todo: implement compatibility with more than 2 dimensional arrays:
26 | *
27 | * @param array|iterable $array array of values
28 | * @param array $cond condition in format of ['KEY'=>'VALUE']
29 | *
30 | * @see find
31 | * @see findIndex
32 | * @see findLast
33 | * @see findLastIndex
34 | * @see where
35 | *
36 | * @return array
37 | */
38 | function where($array = [], array $cond = [])
39 | {
40 | $result = [];
41 | foreach ($array as $arrItem) {
42 | foreach ($cond as $condK => $condV) {
43 | if (!isset($arrItem[$condK]) || $arrItem[$condK] !== $condV) {
44 | continue 2;
45 | }
46 | }
47 | $result[] = $arrItem;
48 | }
49 |
50 | return $result;
51 | }
52 |
--------------------------------------------------------------------------------
/tests/__/Collections/DoForEachRightTest.php:
--------------------------------------------------------------------------------
1 | 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'];
25 |
26 | // Act.
27 | $aAppend = [];
28 | $aMapped = [];
29 | $bMapped = [];
30 | __::doForEachRight($a, $makeAppend($aAppend));
31 | __::doForEachRight($a, $makeMapper($aMapped));
32 | __::doForEachRight($b, $makeMapper($bMapped));
33 |
34 | // Assert
35 | $this->assertEquals(array_reverse($a), $aAppend);
36 | $this->assertEquals($a, $aMapped);
37 | $this->assertEquals($b, $bMapped);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/__/collections/map.php:
--------------------------------------------------------------------------------
1 | $item) {
16 | yield $iteratee($item, $key, $collection);
17 | }
18 | }
19 |
20 | /**
21 | * Returns an array of values by mapping each in collection through the iteratee.
22 | *
23 | * **Usage**
24 | *
25 | * ```php
26 | * __::map([1, 2, 3], function($value, $key, $collection) {
27 | * return $value * 3;
28 | * });
29 | * ```
30 | *
31 | * **Result**
32 | *
33 | * ```
34 | * [3, 6, 9]
35 | * ```
36 | *
37 | * @since 0.2.0 added support for iterables
38 | *
39 | * @param iterable|\stdClass $collection The collection of values to map over.
40 | * @param \Closure $iteratee The function to apply on each value.
41 | *
42 | * @return array|\Generator
43 | */
44 | function map($collection, \Closure $iteratee)
45 | {
46 | if (is_array($collection) || $collection instanceof \stdClass) {
47 | return iterator_to_array(mapIterable($collection, $iteratee));
48 | }
49 |
50 | return mapIterable($collection, $iteratee);
51 | }
52 |
--------------------------------------------------------------------------------
/src/__/collections/reduceRight.php:
--------------------------------------------------------------------------------
1 | 1,
16 | * 'b' => ['c' => 3, 'd' => 4]
17 | * ],
18 | * ['a', 'b.d']
19 | * );
20 | * ```
21 | *
22 | * **Result**
23 | *
24 | * ```
25 | * [
26 | * 'a' => 1,
27 | * 'b' => ['d' => 4]
28 | * ]
29 | * ```
30 | *
31 | * @since 0.2.0 added support for iterables
32 | *
33 | * @param iterable|\stdClass $collection The collection to iterate over.
34 | * @param array $paths Array paths to pick
35 | * @param mixed $default The default value that will be used if the specified path does not exist.
36 | *
37 | * @return array
38 | */
39 | function pick($collection = [], array $paths = [], $default = null)
40 | {
41 | return \__::reduce($paths, function ($results, $path) use ($collection, $default) {
42 | return \__::set($results, $path, \__::get($collection, $path, $default));
43 | }, \__::isObject($collection) && !($collection instanceof \ArrayAccess) ? new \stdClass() : []);
44 | }
45 |
--------------------------------------------------------------------------------
/src/__/arrays/patch.php:
--------------------------------------------------------------------------------
1 | [
14 | * 'country' => 'US',
15 | * 'zip' => 12345
16 | * ]
17 | * ],
18 | * [
19 | * '/addr/country' => 'CA',
20 | * '/addr/zip' => 54321
21 | * ]
22 | * );
23 | * ```
24 | *
25 | * **Result**
26 | *
27 | * ```
28 | * ['addr' => ['country' => 'CA', 'zip' => 54321]]
29 | * ```
30 | *
31 | * @param array $array The array to patch
32 | * @param array $patches List of new xpath-value pairs
33 | * @param string $parent
34 | *
35 | * @return array Returns patched array
36 | */
37 | function patch($array, $patches, $parent = '')
38 | {
39 | $parent .= '/';
40 |
41 | foreach ($array as $key => &$value) {
42 | $z = $parent . $key;
43 |
44 | if (isset($patches[$z])) {
45 | $value = $patches[$z];
46 | unset($patches[$z]);
47 |
48 | if (!count($patches)) {
49 | break;
50 | }
51 | }
52 |
53 | if (\is_array($value)) {
54 | $value = patch($array[$key], $patches, $z);
55 | }
56 | }
57 |
58 | return $array;
59 | }
60 |
--------------------------------------------------------------------------------
/src/__/collections/mapValues.php:
--------------------------------------------------------------------------------
1 | $value) {
16 | yield $key => call_user_func_array($closure, [$value, $key, $iterable]);
17 | }
18 | }
19 |
20 | /**
21 | * Transforms the values in a collection by running each value through the iterator.
22 | *
23 | * **Usage**
24 | *
25 | * ```php
26 | * __::mapValues(['x' => 1], function($value, $key, $collection) {
27 | * return "{$key}_{$value}";
28 | * });
29 | * ```
30 | *
31 | * **Result**
32 | *
33 | * ```
34 | * ['x' => 'x_1']
35 | * ```
36 | *
37 | * @since 0.2.0 added support for iterables
38 | *
39 | * @param iterable $iterable Array of values
40 | * @param \Closure|null $closure Closure to map the values
41 | *
42 | * @return array|\Generator
43 | */
44 | function mapValues($iterable, \Closure $closure = null)
45 | {
46 | if (is_null($closure)) {
47 | $closure = '__::identity';
48 | }
49 |
50 | if (is_array($iterable)) {
51 | return iterator_to_array(mapValuesIterable($iterable, $closure));
52 | }
53 |
54 | return mapValuesIterable($iterable, $closure);
55 | }
56 |
--------------------------------------------------------------------------------
/src/__/collections/merge.php:
--------------------------------------------------------------------------------
1 | ['favorite' => 'red', 'model' => 3, 5], 3],
18 | * [10, 'color' => ['favorite' => 'green', 'blue']]
19 | * );
20 | * ```
21 | *
22 | * **Result**
23 | *
24 | * ```
25 | * ['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10]
26 | * ```
27 | *
28 | * @param iterable|\stdClass ...$_ Collections to merge.
29 | *
30 | * @return array|object Concatenated collection.
31 | */
32 | function merge()
33 | {
34 | return \__::reduceRight(func_get_args(), function ($source, $result) {
35 | \__::doForEach($source, function ($sourceValue, $key) use (&$result) {
36 | $value = $sourceValue;
37 | if (\__::isCollection($value)) {
38 | $value = merge(\__::get($result, $key), $sourceValue);
39 | }
40 | $result = \__::set($result, $key, $value);
41 | });
42 | return $result;
43 | }, []);
44 | }
45 |
--------------------------------------------------------------------------------
/src/__/collections/every.php:
--------------------------------------------------------------------------------
1 | [
20 | * 'favorite' => 'red',
21 | * 5
22 | * ],
23 | * 3
24 | * ],
25 | * [
26 | * 10,
27 | * 'color' => [
28 | * 'favorite' => 'green',
29 | * 'blue'
30 | * ]
31 | * ]
32 | * );
33 | * ```
34 | *
35 | * **Result**
36 | *
37 | * ```
38 | * [
39 | * 'color' => ['favorite' => 'green', 'blue'],
40 | * 10
41 | * ]
42 | * ```
43 | *
44 | * @param array|object ...$_ Collections to assign.
45 | *
46 | * @return array|object Assigned collection.
47 | */
48 | function assign($_)
49 | {
50 | return \__::reduceRight(func_get_args(), function ($source, $result) {
51 | \__::doForEach($source, function ($sourceValue, $key) use (&$result) {
52 | $result = \__::set($result, $key, $sourceValue);
53 | });
54 | return $result;
55 | }, []);
56 | }
57 |
--------------------------------------------------------------------------------
/tests/__/Collections/HasTest.php:
--------------------------------------------------------------------------------
1 | 'bar'];
15 | $b = (object)['foo' => 'bar'];
16 | $c = ['foo' => ['bar' => 'foie']];
17 | $d = [5];
18 | $e = (object)[5];
19 |
20 | // Act.
21 | $x = __::has($a, 'foo');
22 | $y = __::has($a, 'foz');
23 | $z = __::has($b, 'foo');
24 | $xa = __::has($b, 'foz');
25 | $xb = __::has($c, 'foo.bar');
26 | $xc = __::has($d, 0);
27 | $xd = __::has($e, 0);
28 |
29 | // Assert.
30 | $this->assertTrue($x);
31 | $this->assertFalse($y);
32 | $this->assertTrue($z);
33 | $this->assertFalse($xa);
34 | $this->assertTrue($xb);
35 | $this->assertTrue($xc);
36 | $this->assertTrue($xd);
37 | }
38 |
39 | public function testHasArrayAccess()
40 | {
41 | $aa = new ArrayAccessible();
42 | $aa['qux'] = true;
43 | $aa['field'] = null;
44 |
45 | $this->assertTrue(__::has($aa, 'qux'));
46 | $this->assertTrue(__::has($aa, 'field'));
47 | $this->assertFalse(__::has($aa, 'non-existent'));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/__/collections/ease.php:
--------------------------------------------------------------------------------
1 | $value) {
18 | if (is_array($value)) {
19 | _ease($map, $value, $glue, $prefix . $index . $glue);
20 | } else {
21 | $map[$prefix . $index] = $value;
22 | }
23 | }
24 | }
25 |
26 | /**
27 | * Flattens a complex collection by mapping each ending leafs value to a key
28 | * consisting of all previous indexes.
29 | *
30 | * **Usage**
31 | *
32 | * ```php
33 | * __::ease([
34 | * 'foo' => ['bar' => 'ter'],
35 | * 'baz' => ['b', 'z']
36 | * ]);
37 | * ```
38 | *
39 | * **Result**
40 | *
41 | * ```
42 | * [
43 | * 'foo.bar' => 'ter',
44 | * 'baz.0' => 'b',
45 | * 'baz.1' => 'z'
46 | * ]
47 | * ```
48 | *
49 | * @since 0.2.0 added support for iterables
50 | *
51 | * @param iterable $collection array of values
52 | * @param string $glue glue between key path
53 | *
54 | * @return array flatten collection
55 | */
56 | function ease($collection, $glue = '.')
57 | {
58 | $map = [];
59 | _ease($map, $collection, $glue);
60 |
61 | return $map;
62 | }
63 |
--------------------------------------------------------------------------------
/src/__/collections/some.php:
--------------------------------------------------------------------------------
1 | 1, 'rungis' => [2, 3]], ['honfleur' => 1, 'rungis' => [1, 2]]);
17 | * ```
18 | *
19 | * **Result**
20 | *
21 | * ```
22 | * false
23 | * ```
24 | *
25 | * @param mixed $object1
26 | * @param mixed $object2
27 | *
28 | * @return bool
29 | */
30 | function isEqual($object1, $object2)
31 | {
32 | if (\__::isCollection($object1)) {
33 | // Is not equal if number of keys differ.
34 | $object1Keys = \__::isObject($object1) ? array_keys(get_object_vars($object1)) : array_keys($object1);
35 | $object2Keys = \__::isObject($object2) ? array_keys(get_object_vars($object2)) : array_keys($object2);
36 | if (count($object1Keys) !== count($object2Keys)) {
37 | return false;
38 | }
39 | foreach ($object1 as $key1 => $value1) {
40 | if (!\__::has($object2, $key1) || !\__::isEqual($value1, \__::get($object2, $key1))) {
41 | return false;
42 | }
43 | }
44 | return true;
45 | }
46 | return $object1 === $object2;
47 | }
48 |
--------------------------------------------------------------------------------
/src/__/functions/urlify.php:
--------------------------------------------------------------------------------
1 | google.com'
18 | * ```
19 | *
20 | * @param string $string
21 | *
22 | * @return string
23 | */
24 | function urlify($string)
25 | {
26 | /*
27 | * Proposed by:
28 | * Søren Løvborg
29 | * http://stackoverflow.com/users/136796/soren-lovborg
30 | * http://stackoverflow.com/questions/17900004/turn-plain-text-urls-into-active-links-using-php/17900021#17900021
31 | */
32 |
33 | $rexProtocol = '(https?://)?';
34 | $rexDomain = '((?:[-a-zA-Z0-9]{1,63}\.)+[-a-zA-Z0-9]{2,63}|(?:[0-9]{1,3}\.){3}[0-9]{1,3})';
35 | $rexPort = '(:[0-9]{1,5})?';
36 | $rexPath = '(/[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]*?)?';
37 | $rexQuery = '(\?[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?';
38 | $rexFragment = '(#[!$-/0-9:;=@_\':;!a-zA-Z\x7f-\xff]+?)?';
39 |
40 | return preg_replace_callback(
41 | "&\\b$rexProtocol$rexDomain$rexPort$rexPath$rexQuery$rexFragment(?=[?.!,;:\"]?(\s|$))&",
42 | function ($match) {
43 | $completeUrl = $match[1] ? $match[0] : "http://{$match[0]}";
44 |
45 | return '' . $match[2] . $match[3] . $match[4] . '';
46 | },
47 | htmlspecialchars($string)
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/tests/__/Collections/AssignTest.php:
--------------------------------------------------------------------------------
1 | ['favorite' => 'red', 'model' => 3, 5], 3];
14 | $a2 = [10, 'color' => ['favorite' => 'green', 'blue']];
15 | $b1 = ['a' => 0];
16 | $b2 = ['a' => 1, 'b' => 2];
17 | $b3 = ['c' => 3, 'd' => 4];
18 |
19 | // Act
20 | $x = __::assign($a1, $a2);
21 | $y = __::assign($b1, $b2, $b3);
22 |
23 | // Assert
24 | $this->assertEquals(['color' => ['favorite' => 'green', 'blue'], 10], $x);
25 | $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $y);
26 | }
27 |
28 | public function testAssignObject()
29 | {
30 | // Arrange
31 | $a1 = (object)['color' => (object)['favorite' => 'red', 5]];
32 | $a2 = (object)[10, 'color' => (object)['favorite' => 'green', 'blue']];
33 | $b1 = (object)['a' => 0];
34 | $b2 = (object)['a' => 1, 'b' => 2, 5];
35 | $b3 = (object)['c' => 3, 'd' => 4, 6];
36 |
37 | // Act
38 | $x = __::assign($a1, $a2);
39 | $y = __::assign($b1, $b2, $b3);
40 |
41 | // Assert
42 | $this->assertEquals((object)['color' => (object)['favorite' => 'green', 'blue'], 10], $x);
43 | $this->assertEquals((object)['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 6], $y);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/__/Collections/ReverseIterableTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($x instanceof \Generator);
22 | $xValues = [];
23 | foreach ($x as $value) {
24 | $xValues[] = $value;
25 | }
26 | $this->assertEquals([3, 2, 1], $xValues);
27 |
28 | // Note how the keys have been preserved and inverted.
29 | // TODO Add an option preserve_keys as array_reverse or iterator_to_array?
30 | $this->assertEquals([2 => 3, 1 => 2, 0 => 1], iterator_to_array(__::reverseIterable($a)));
31 | $this->assertEquals([3, 2, 1], iterator_to_array(__::reverseIterable($a), false));
32 | }
33 |
34 | public function testReverseIterableArrayIterable()
35 | {
36 | // Arrange
37 | $a = new ArrayIterator([1, 2, 3]);
38 |
39 | // Act
40 | $x = __::reverseIterable($a);
41 |
42 | // Assert
43 | // Check we got back a Generator.
44 | $this->assertTrue($x instanceof \Generator);
45 | $xValues = [];
46 | foreach ($x as $value) {
47 | $xValues[] = $value;
48 | }
49 | $this->assertEquals([3, 2, 1], $xValues);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/tests/__/Arrays/CompactTest.php:
--------------------------------------------------------------------------------
1 | [0, 1, false, 2, '', 3],
17 | 'expected' => [1, 2, 3],
18 | ],
19 | [
20 | 'sourceArray' => new ArrayIterator([0, 1, false, 2, '', 3]),
21 | 'expected' => [1, 2, 3],
22 | ],
23 | [
24 | 'sourceArray' => new MockIteratorAggregate([0, 1, false, 2, '', 3]),
25 | 'expected' => [1, 2, 3],
26 | ],
27 | [
28 | 'sourceArray' => call_user_func(function () {
29 | yield 0;
30 | yield 1;
31 | yield false;
32 | yield 2;
33 | yield '';
34 | yield 3;
35 | }),
36 | 'expected' => [1, 2, 3],
37 | ],
38 | ];
39 | }
40 |
41 | /**
42 | * @dataProvider provideCompactCases
43 | *
44 | * @param array|iterable $sourceArray
45 | * @param array $expected
46 | */
47 | public function testCompact($sourceArray, $expected)
48 | {
49 | $actual = __::compact($sourceArray);
50 |
51 | foreach ($actual as $i => $item) {
52 | $this->assertEquals($expected[$i], $item);
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/tests/__/Sequences/ChainTest.php:
--------------------------------------------------------------------------------
1 | compact()
20 | ->prepend(4)
21 | ->value();
22 |
23 | // Assert
24 | $this->assertEquals([4, 1, 2, 3], $result);
25 | }
26 |
27 | public function testChainReturnsNumber()
28 | {
29 | // Arrange
30 | $collection = [0, 1, 2, 3, null];
31 |
32 | // Act
33 | $result = __::chain($collection)
34 | ->compact()
35 | ->prepend(4)
36 | ->reduce(function ($sum, $number) {
37 | return $sum + $number;
38 | }, 0)
39 | ->value();
40 |
41 | // Assert
42 | $this->assertEquals(10, $result);
43 | }
44 |
45 | public function testChainWithError()
46 | {
47 | if (method_exists($this, 'expectException')) {
48 | // new phpunit
49 | $this->expectException('\Exception');
50 | } else {
51 | // old phpunit
52 | $this->setExpectedException('\Exception');
53 | }
54 |
55 | // Arrange
56 | $collection = [0, 1, 2, 3, null];
57 |
58 | // Act
59 | __::chain($collection)
60 | ->randomFunc()
61 | ->value();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/__/Collections/HasKeysTest.php:
--------------------------------------------------------------------------------
1 | 'bar'];
14 | $b = ['foo' => ['bar' => 'foie'], 'estomac' => true];
15 |
16 | // Act
17 | $x = __::hasKeys($a, ['foo', 'foz'], false);
18 | $y = __::hasKeys($a, ['foo', 'foz'], true);
19 | $z = __::hasKeys($b, ['foo.bar', 'estomac']);
20 |
21 | // Assert
22 | $this->assertFalse($x);
23 | $this->assertFalse($y);
24 | $this->assertTrue($z);
25 |
26 | // Rearrange
27 | $a['foz'] = 'baz';
28 |
29 | // React
30 | $x = __::hasKeys($a, ['foo', 'foz'], false);
31 | $y = __::hasKeys($a, ['foo', 'foz'], true);
32 |
33 | // Assert
34 | $this->assertTrue($x);
35 | $this->assertTrue($y);
36 |
37 | // Rearrange
38 | $a['xxx'] = 'bay';
39 |
40 | // React
41 | $x = __::hasKeys($a, ['foo', 'foz'], false);
42 | $y = __::hasKeys($a, ['foo', 'foz'], true);
43 |
44 | // Assert
45 | $this->assertTrue($x);
46 | $this->assertFalse($y);
47 | }
48 |
49 | public function testHasKeysObject()
50 | {
51 | // Arrange.
52 | $a = (object)['foo' => 'bar'];
53 |
54 | // Act
55 | $x = __::hasKeys($a, ['foo']);
56 | $y = __::hasKeys($a, ['foo', 'foz']);
57 |
58 | // Assert
59 | $this->assertTrue($x);
60 | $this->assertFalse($y);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/__/collections/has.php:
--------------------------------------------------------------------------------
1 | ['bar' => 'num'], 'foz' => 'baz'], 'foo.bar');
14 | * ```
15 | *
16 | * **Result**
17 | *
18 | * ```
19 | * true
20 | * ```
21 | *
22 | * **Object Usage**
23 | *
24 | * ```php
25 | * __::hasKeys((object) ['foo' => 'bar', 'foz' => 'baz'], 'bar');
26 | * ```
27 | *
28 | * **Result**
29 | *
30 | * ```
31 | * false
32 | * ```
33 | *
34 | * @param array|object $collection Array or object to search a key for
35 | * @param string|int $path Path to look for. Supports dot notation for traversing multiple levels.
36 | *
37 | * @return bool
38 | */
39 | function has($collection, $path)
40 | {
41 | $portions = \__::split($path, \__::DOT_NOTATION_DELIMITER, 2);
42 | $key = $portions[0];
43 |
44 | if (\count($portions) === 1) {
45 | // Calling array_key_exists on an ArrayAccess object will not call `offsetExists()`
46 | // See: http://php.net/manual/en/class.arrayaccess.php#104061
47 | if ($collection instanceof \ArrayAccess) {
48 | return $collection->offsetExists($key);
49 | }
50 |
51 | // We use a cast to array to handle the numeric keys for objects (workaround).
52 | // See: https://wiki.php.net/rfc/convert_numeric_keys_in_object_array_casts
53 | return array_key_exists($key, (array)$collection);
54 | }
55 |
56 | return has(\__::get($collection, $key), $portions[1]);
57 | }
58 |
--------------------------------------------------------------------------------
/.php-cs-fixer.dist.php:
--------------------------------------------------------------------------------
1 | exclude('./vendor/')
5 | ->in(__DIR__)
6 | ;
7 |
8 | $config = new PhpCsFixer\Config();
9 | $config
10 | ->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers())
11 | ->setRiskyAllowed(true)
12 | ->setRules([
13 | '@PSR1' => true,
14 | '@PSR2' => true,
15 | PhpCsFixerCustomFixers\Fixer\CommentSurroundedBySpacesFixer::name() => true,
16 | PhpCsFixerCustomFixers\Fixer\DataProviderNameFixer::name() => true,
17 | PhpCsFixerCustomFixers\Fixer\DataProviderStaticFixer::name() => true,
18 | PhpCsFixerCustomFixers\Fixer\NoDuplicatedImportsFixer::name() => true,
19 | PhpCsFixerCustomFixers\Fixer\NoPhpStormGeneratedCommentFixer::name() => true,
20 | PhpCsFixerCustomFixers\Fixer\NoUselessParenthesisFixer::name() => true,
21 | PhpCsFixerCustomFixers\Fixer\PhpUnitAssertArgumentsOrderFixer::name() => true,
22 | PhpCsFixerCustomFixers\Fixer\PhpUnitDedicatedAssertFixer::name() => true,
23 | PhpCsFixerCustomFixers\Fixer\PhpUnitNoUselessReturnFixer::name() => true,
24 | PhpCsFixerCustomFixers\Fixer\PhpdocNoIncorrectVarAnnotationFixer::name() => true,
25 | PhpCsFixerCustomFixers\Fixer\PhpdocParamTypeFixer::name() => true,
26 | PhpCsFixerCustomFixers\Fixer\PhpdocSelfAccessorFixer::name() => true,
27 | PhpCsFixerCustomFixers\Fixer\PhpdocSingleLineVarFixer::name() => true,
28 | PhpCsFixerCustomFixers\Fixer\PhpdocTypesTrimFixer::name() => true,
29 | PhpCsFixerCustomFixers\Fixer\StringableInterfaceFixer::name() => true,
30 | ])
31 | ->setFinder($finder)
32 | ;
33 |
34 | return $config;
35 |
--------------------------------------------------------------------------------
/tests/__/Collections/FindTest.php:
--------------------------------------------------------------------------------
1 | assertEquals("explain", __::find($data, "explain"));
14 | $this->assertNull(__::find($data, "nonexistent"));
15 | }
16 |
17 | public function testWithAssociativeArray()
18 | {
19 | $data = [
20 | "table" => "trick",
21 | "pen" => "defend",
22 | "motherly" => "wide",
23 | "may" => "needle",
24 | "sweat" => "cake",
25 | "sword" => "defend",
26 | ];
27 |
28 | $this->assertEquals("defend", __::find($data, "defend"));
29 | $this->assertNull(__::find($data, "nonexistent"));
30 | }
31 |
32 | public function testWithCallback()
33 | {
34 | $data = [
35 | "table" => (object)["name" => "trick"],
36 | "pen" => (object)["name" => "defend"],
37 | "motherly" => (object)["name" => "wide"],
38 | "may" => (object)["name" => "needle"],
39 | "sweat" => (object)["name" => "cake"],
40 | "sword" => (object)["name" => "defend"],
41 | ];
42 |
43 | $this->assertEquals($data["pen"], __::find($data, static function ($object) {
44 | return $object->name === "defend";
45 | }));
46 | $this->assertNull(__::find($data, static function ($value) {
47 | return $value === "potato";
48 | }));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/__/Collections/FindLastTest.php:
--------------------------------------------------------------------------------
1 | assertEquals("explain", __::findLast($data, "explain"));
14 | $this->assertNull(__::findLast($data, "nonexistent"));
15 | }
16 |
17 | public function testWithAssociativeArray()
18 | {
19 | $data = [
20 | "table" => "trick",
21 | "pen" => "defend",
22 | "motherly" => "wide",
23 | "may" => "needle",
24 | "sweat" => "cake",
25 | "sword" => "defend",
26 | ];
27 |
28 | $this->assertEquals("defend", __::findLast($data, "defend"));
29 | $this->assertNull(__::findLast($data, "nonexistent"));
30 | }
31 |
32 | public function testWithCallback()
33 | {
34 | $data = [
35 | "table" => (object)["name" => "trick"],
36 | "pen" => (object)["name" => "defend"],
37 | "motherly" => (object)["name" => "wide"],
38 | "may" => (object)["name" => "needle"],
39 | "sweat" => (object)["name" => "cake"],
40 | "sword" => (object)["name" => "defend"],
41 | ];
42 |
43 | $this->assertEquals($data["sword"], __::findLast($data, static function ($object) {
44 | return $object->name === "defend";
45 | }));
46 | $this->assertNull(__::findLast($data, static function ($value) {
47 | return $value === "potato";
48 | }));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/__/Collections/WhereTest.php:
--------------------------------------------------------------------------------
1 | 'fred', 'age' => 32],
17 | ['name' => 'maciej', 'age' => 16],
18 | ['a' => 'b', 'c' => 'd']
19 | ];
20 |
21 | // Act
22 | $x = __::where($a, ['age' => 16]);
23 | $x2 = __::where($a, ['age' => 16, 'name' => 'fred']);
24 | $x3 = __::where($a, ['name' => 'maciej', 'age' => 16]);
25 | $x4 = __::where($a, ['name' => 'unknown']);
26 |
27 | // Assert
28 | $this->assertEquals([$a[1]], $x);
29 | $this->assertEquals([], $x2);
30 | $this->assertEquals([$a[1]], $x3);
31 | $this->assertEquals([], $x4);
32 | }
33 |
34 | public function testWhereIterable()
35 | {
36 | // Arrange
37 | $a = new ArrayIterator([
38 | ['name' => 'fred', 'age' => 32],
39 | ['name' => 'maciej', 'age' => 16],
40 | ['a' => 'b', 'c' => 'd']
41 | ]);
42 |
43 | // Act
44 | $x = __::where($a, ['age' => 16]);
45 | $x2 = __::where($a, ['age' => 16, 'name' => 'fred']);
46 | $x3 = __::where($a, ['name' => 'maciej', 'age' => 16]);
47 | $x4 = __::where($a, ['name' => 'unknown']);
48 |
49 | // Assert
50 | $this->assertEquals([$a[1]], $x);
51 | $this->assertEquals([], $x2);
52 | $this->assertEquals([$a[1]], $x3);
53 | $this->assertEquals([], $x4);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/__/collections/pluck.php:
--------------------------------------------------------------------------------
1 | 'bar', 'bis' => 'ter' ],
13 | * ['foo' => 'bar2', 'bis' => 'ter2'],
14 | * ];
15 | *
16 | * __::pluck($a, 'foo');
17 | * ```
18 | *
19 | * **Result**
20 | *
21 | * ```
22 | * ['bar', 'bar2']
23 | * ```
24 | *
25 | * @since 0.2.0 added support for iterables
26 | *
27 | * @param iterable|\stdClass $collection Array or object that can be converted to array
28 | * @param string $property property name
29 | *
30 | * @return array
31 | */
32 | function pluck($collection, $property)
33 | {
34 | $result = array_map(function ($value) use ($property) {
35 | if (is_array($value) && isset($value[$property])) {
36 | return $value[$property];
37 | } elseif (is_object($value) && isset($value->{$property})) {
38 | return $value->{$property};
39 | }
40 |
41 | foreach (\__::split($property, \__::DOT_NOTATION_DELIMITER) as $segment) {
42 | if (is_object($value)) {
43 | if (isset($value->{$segment})) {
44 | $value = $value->{$segment};
45 | } else {
46 | return null;
47 | }
48 | } else {
49 | if (isset($value[$segment])) {
50 | $value = $value[$segment];
51 | } else {
52 | return null;
53 | }
54 | }
55 | }
56 |
57 | return $value;
58 | }, (array)$collection);
59 |
60 | return array_values($result);
61 | }
62 |
--------------------------------------------------------------------------------
/src/__/arrays/drop.php:
--------------------------------------------------------------------------------
1 | "Having at least one even number should return true",
15 | 'actual' => [1, 3, 5, 10, 7, 9],
16 | 'callback' => static function ($item) {
17 | return $item % 2 === 0;
18 | },
19 | 'expected' => true
20 | ],
21 | [
22 | 'description' => "Having no even numbers should return false",
23 | 'actual' => [1, 3, 5, 7, 9],
24 | 'callback' => static function ($item) {
25 | return $item % 2 === 0;
26 | },
27 | 'expected' => false
28 | ],
29 | [
30 | 'description' => "Having at least one truthy value should return true",
31 | 'actual' => [false, [], 1],
32 | 'callback' => null,
33 | 'expected' => true
34 | ],
35 | [
36 | 'description' => "Having all falsey values should return false",
37 | 'actual' => [false, [], 0],
38 | 'callback' => null,
39 | 'expected' => false
40 | ],
41 | ];
42 | }
43 |
44 | /**
45 | * @dataProvider provideSameCases
46 | */
47 | public function testSame($message, $actual, $callback, $expected)
48 | {
49 | $actual = __::some($actual, $callback);
50 |
51 | $this->assertSame($expected, $actual, $message);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |

4 |
5 |
6 | # bottomline
7 |
8 | [](https://github.com/maciejczyzewski/bottomline/actions/workflows/ci.yml)
9 | 
10 | [](https://packagist.org/packages/maciejczyzewski/bottomline)
11 | [](https://packagist.org/packages/maciejczyzewski/bottomline)
12 |
13 | bottomline is a PHP utility library, similar to Underscore/Lodash, that utilizes namespaces and dynamic autoloading to improve performance.
14 |
15 | **NOTE:** bottomline is not currently in feature parity with Underscore/Lodash. Review the [contributing](./CONTRIBUTING.md) section for more information.
16 |
17 | [View the documentation »](https://maciejczyzewski.github.io/bottomline/)
18 |
19 | ## Requirements
20 |
21 | - PHP 5.5+
22 | - mbstring
23 |
24 | ## Benchmarks
25 |
26 |
27 |

28 |
29 |
30 | ## Thanks
31 |
32 | * Yoan Tournade ([@MonsieurV](https://github.com/MonsieurV))
33 | * Vladimir Jimenez ([@allejo](https://github.com/allejo))
34 | * Brandtley McMinn ([@bmcminn](https://github.com/bmcminn))
35 | * Ivan Ternovtsiy ([@diaborn19](https://github.com/diaborn19))
36 | * Tobias Seipke ([@nullpunkt](https://github.com/nullpunkt))
37 |
38 | ## License
39 |
40 | [MIT](./LICENSE)
41 |
--------------------------------------------------------------------------------
/tests/__/Collections/FindLastIndexTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(5, __::findLastIndex($data, "explain"));
14 | $this->assertEquals(-1, __::findLastIndex($data, "nonexistent"));
15 | }
16 |
17 | public function testWithAssociativeArray()
18 | {
19 | $data = [
20 | "table" => "trick",
21 | "pen" => "defend",
22 | "motherly" => "wide",
23 | "may" => "needle",
24 | "sweat" => "cake",
25 | "sword" => "defend",
26 | ];
27 |
28 | $this->assertEquals("sword", __::findLastIndex($data, "defend"));
29 | $this->assertEquals(-1, __::findLastIndex($data, "nonexistent"));
30 | }
31 |
32 | public function testWithCallback()
33 | {
34 | $data = [
35 | "table" => (object)["name" => "trick"],
36 | "pen" => (object)["name" => "defend"],
37 | "motherly" => (object)["name" => "wide"],
38 | "may" => (object)["name" => "needle"],
39 | "sweat" => (object)["name" => "cake"],
40 | "sword" => (object)["name" => "defend"],
41 | ];
42 |
43 | $this->assertEquals("sword", __::findLastIndex($data, static function ($object) {
44 | return $object->name === "defend";
45 | }));
46 | $this->assertEquals(-1, __::findLastIndex($data, static function ($value) {
47 | return $value === "potato";
48 | }));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/__/Collections/LastTest.php:
--------------------------------------------------------------------------------
1 | [1, 2, 3, 4, 5],
17 | 'take' => 2,
18 | 'expected' => [4, 5],
19 | ],
20 | [
21 | 'source' => [1, 2, 3, 4, 5],
22 | 'take' => null,
23 | 'expected' => 5,
24 | ],
25 | [
26 | 'source' => new MockIteratorAggregate([1, 2, 3, 4, 5]),
27 | 'take' => 3,
28 | 'expected' => [3, 4, 5],
29 | ],
30 | [
31 | 'source' => new ArrayIterator([1, 2, 3, 4, 5]),
32 | 'take' => 4,
33 | 'expected' => [2, 3, 4, 5],
34 | ],
35 | [
36 | 'source' => call_user_func(function () {
37 | yield 1;
38 | yield 2;
39 | yield 3;
40 | yield 4;
41 | yield 5;
42 | }),
43 | 'take' => 2,
44 | 'expected' => [4, 5],
45 | ],
46 | ];
47 | }
48 |
49 | /**
50 | * @dataProvider provideLastCases
51 | *
52 | * @param iterable $source
53 | * @param int|null $take
54 | * @param array $expected
55 | */
56 | public function testLast($source, $take, $expected)
57 | {
58 | $actual = __::last($source, $take);
59 |
60 | $this->assertEquals($expected, $actual);
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/__/collections/mapKeys.php:
--------------------------------------------------------------------------------
1 | $value) {
18 | // For PHP 5.5 support, this can't be replaced with `$closure($key, ...)`
19 | $newKey = call_user_func_array($closure, array($key, $value, $iterable));
20 |
21 | // key must be a number or string
22 | if (!is_numeric($newKey) && !is_string($newKey)) {
23 | throw new \InvalidArgumentException('closure must returns a number or string');
24 | }
25 |
26 | yield $newKey => $value;
27 | }
28 | }
29 |
30 | /**
31 | * Transforms the keys in a collection by running each key through the iterator.
32 | *
33 | * This function throws an `\Exception` when the closure doesn't return a valid
34 | * key that can be used in a PHP array.
35 | *
36 | * **Usage**
37 | *
38 | * ```php
39 | * __::mapKeys(['x' => 1], function($key, $value, $collection) {
40 | * return "{$key}_{$value}";
41 | * });
42 | * ```
43 | *
44 | * **Result**
45 | *
46 | * ```
47 | * ['x_1' => 1]
48 | * ```
49 | *
50 | * @since 0.2.0 added support for iterables
51 | *
52 | * @param iterable $iterable Array/iterable of values
53 | * @param \Closure|null $closure Closure to map the keys
54 | *
55 | * @throws \InvalidArgumentException when closure doesn't return a valid key that can be used in PHP array
56 | *
57 | * @return array|\Generator
58 | */
59 | function mapKeys($iterable, \Closure $closure = null)
60 | {
61 | if (is_null($closure)) {
62 | $closure = '__::identity';
63 | }
64 |
65 | if (is_array($iterable)) {
66 | return iterator_to_array(mapKeysIterable($iterable, $closure));
67 | }
68 |
69 | return mapKeysIterable($iterable, $closure);
70 | }
71 |
--------------------------------------------------------------------------------
/tests/__/Collections/EveryTest.php:
--------------------------------------------------------------------------------
1 | "Passing an array with one or more non-bool values should return false when given a callback to check for booleans",
15 | 'actual' => [true, 1, null, 'yes'],
16 | 'callback' => static function ($v) {
17 | return is_bool($v);
18 | },
19 | 'expected' => false,
20 | ],
21 | [
22 | 'description' => "Passing an array with only booleans should return true when given a callback to check for booleans",
23 | 'actual' => [true, false],
24 | 'callback' => static function ($v) {
25 | return is_bool($v);
26 | },
27 | 'expected' => true,
28 | ],
29 | [
30 | 'description' => "Passing an array with only integers should return true when given a callback to check for integers",
31 | 'actual' => [1, 3, 4],
32 | 'callback' => static function ($v) {
33 | return is_int($v);
34 | },
35 | 'expected' => true,
36 | ],
37 | [
38 | 'description' => "Passing an array with truthy values should return true when given null as the callback",
39 | 'actual' => [1, "hello", true],
40 | 'callback' => null,
41 | 'expected' => true,
42 | ],
43 | ];
44 | }
45 |
46 | /**
47 | * @dataProvider provideEveryCases
48 | */
49 | public function testEvery($message, $actual, $callback, $expected)
50 | {
51 | $actual = __::every($actual, $callback);
52 |
53 | $this->assertSame($expected, $actual, $message);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/__/collections/filter.php:
--------------------------------------------------------------------------------
1 | $value) {
18 | if ($closure) {
19 | if ($closure($value)) {
20 | yield $value;
21 | }
22 | } elseif ($value) {
23 | yield $value;
24 | }
25 | }
26 | }
27 |
28 | /**
29 | * Returns the values in the collection that pass the truth test.
30 | *
31 | * When `$closure` is set to null, this function will automatically remove falsey
32 | * values. When `$closure` is given, then values where the closure returns false
33 | * will be removed.
34 | *
35 | * **Usage**
36 | *
37 | * ```php
38 | * $a = [
39 | * ['name' => 'fred', 'age' => 32],
40 | * ['name' => 'maciej', 'age' => 16]
41 | * ];
42 | *
43 | * __::filter($a, function($n) {
44 | * return $n['age'] > 24;
45 | * });
46 | * ```
47 | *
48 | * **Result**
49 | *
50 | * ```
51 | * [['name' => 'fred', 'age' => 32]]
52 | * ```
53 | *
54 | * @since 0.2.0 iterable objects are now supported
55 | *
56 | * @param iterable $iterable Array to filter
57 | * @param \Closure|null $closure Closure to filter the array
58 | *
59 | * @throws \InvalidArgumentException when an non-array or non-traversable object is given for $iterable.
60 | *
61 | * @return array|\Generator When given a `\Traversable` object for `$iterable`, a generator will be returned.
62 | * Otherwise, an array will be returned.
63 | */
64 | function filter($iterable, \Closure $closure = null)
65 | {
66 | if (is_array($iterable)) {
67 | return iterator_to_array(filterIterable($iterable, $closure));
68 | }
69 |
70 | return filterIterable($iterable, $closure);
71 | }
72 |
--------------------------------------------------------------------------------
/.github/workflows/linting.yml:
--------------------------------------------------------------------------------
1 | name: Linting
2 |
3 | on:
4 | push: ~
5 | pull_request: ~
6 |
7 | jobs:
8 | docs:
9 | name: Documentation Kept Updated
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v3
14 |
15 | - name: Set up PHP 8.2
16 | uses: shivammathur/setup-php@v2
17 | with:
18 | php-version: '8.2'
19 | extensions: mbstring
20 |
21 | - name: Get Composer Cache Directory
22 | id: composer-cache
23 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
24 |
25 | - name: Cache dependencies
26 | uses: actions/cache@v3
27 | with:
28 | path: ${{ steps.composer-cache.outputs.dir }}
29 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
30 | restore-keys: ${{ runner.os }}-composer-
31 |
32 | - name: Install dependencies
33 | run: composer install
34 |
35 | - name: Generate the documentation
36 | run: composer doc
37 |
38 | - name: Check Generated Docs
39 | run: git diff --exit-code
40 |
41 | formatting:
42 | name: Code Formatting
43 | runs-on: ubuntu-latest
44 | steps:
45 | - name: Checkout
46 | uses: actions/checkout@v3
47 |
48 | - name: Set up PHP 8.2
49 | uses: shivammathur/setup-php@v2
50 | with:
51 | php-version: '8.2'
52 | extensions: mbstring
53 |
54 | - name: Get Composer Cache Directory
55 | id: composer-cache
56 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
57 |
58 | - name: Cache dependencies
59 | uses: actions/cache@v3
60 | with:
61 | path: ${{ steps.composer-cache.outputs.dir }}
62 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
63 | restore-keys: ${{ runner.os }}-composer-
64 |
65 | - name: Install dependencies
66 | run: composer install
67 |
68 | - name: Check formatting
69 | run: composer cs-check
70 |
--------------------------------------------------------------------------------
/src/__/collections/findLastEntry.php:
--------------------------------------------------------------------------------
1 | "trick",
13 | * "pen" => "defend",
14 | * "motherly" => "wide",
15 | * "may" => "needle",
16 | * "sweat" => "cake",
17 | * "sword" => "defend",
18 | * ];
19 | *
20 | * __::findLastEntry($data, "defend");
21 | * ```
22 | *
23 | * **Result**
24 | *
25 | * ```
26 | * ["sword", "defend"]
27 | * ```
28 | *
29 | * **Find by Callback in an Array**
30 | *
31 | * ```php
32 | * $data = [
33 | * "table" => (object)["name" => "trick"],
34 | * "pen" => (object)["name" => "defend"],
35 | * "motherly" => (object)["name" => "wide"],
36 | * "may" => (object)["name" => "needle"],
37 | * "sweat" => (object)["name" => "cake"],
38 | * "sword" => (object)["name" => "defend"],
39 | * ];
40 | *
41 | * __::findLastEntry($data, static function ($object, $key, $collection) {
42 | * return $object->name === "defend";
43 | * });
44 | * ```
45 | *
46 | * **Result**
47 | *
48 | * ```
49 | * ["sword", $data["sword"]]
50 | * ```
51 | *
52 | * @since 0.2.1 added to bottomline
53 | *
54 | * @param iterable $collection an array or iterable of values to look through
55 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback
56 | *
57 | * @see find
58 | * @see findEntry
59 | * @see findLastIndex
60 | * @see findIndex
61 | * @see findLast
62 | * @see where
63 | *
64 | * @return array|null An array with two values, the 0th index is the key and the 1st index is the value. Null is
65 | * returned if no entries can be found in the given collection.
66 | */
67 | function findLastEntry($collection, $condition)
68 | {
69 | return \__::findEntry(\__::reverseIterable($collection), $condition);
70 | }
71 |
--------------------------------------------------------------------------------
/tests/__/Collections/DoForEachTest.php:
--------------------------------------------------------------------------------
1 | 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'];
20 | $c = (object)['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'];
21 |
22 | // Act.
23 | $aMapped = [];
24 | $bMapped = [];
25 | $cMapped = [];
26 | __::doForEach($a, $makeMapper($aMapped));
27 | __::doForEach($b, $makeMapper($bMapped));
28 | __::doForEach($c, $makeMapper($cMapped));
29 |
30 | // Assert
31 | $this->assertEquals($a, $aMapped);
32 | $this->assertEquals($b, $bMapped);
33 | $this->assertEquals($c, (object)$cMapped);
34 | $this->assertEquals((array)$c, $cMapped);
35 | }
36 |
37 | public function testDoForEachPrematureReturn()
38 | {
39 | // Arrange
40 | $makeMapper = function (&$array, $returnAtKey) {
41 | return function ($value, $key) use (&$array, $returnAtKey) {
42 | $array[$key] = $value;
43 | if ($returnAtKey === $key) {
44 | return false;
45 | }
46 | };
47 | };
48 | $a = [1, 2, 3, 4];
49 | $b = ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'];
50 |
51 | // Act.
52 | $aMapped = [];
53 | $bMapped = [];
54 | __::doForEach($a, $makeMapper($aMapped, 1));
55 | __::doForEach($b, $makeMapper($bMapped, 'city'));
56 |
57 | // Assert
58 | $this->assertEquals([1, 2], $aMapped);
59 | $this->assertEquals(['state' => 'IN', 'city' => 'Indianapolis'], $bMapped);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/__/Collections/EaseTest.php:
--------------------------------------------------------------------------------
1 | ['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']],
18 | 'glue' => '.',
19 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'],
20 | ],
21 | [
22 | 'source' => ['foo' => ['bar' => $object], 'baz' => ['b', 'z']],
23 | 'glue' => '_',
24 | 'expected' => ['foo_bar' => $object, 'baz_0' => 'b', 'baz_1' => 'z'],
25 | ],
26 | [
27 | 'source' => new ArrayIterator(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]),
28 | 'glue' => '.',
29 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'],
30 | ],
31 | [
32 | 'source' => new MockIteratorAggregate(['foo' => ['bar' => 'ter'], 'baz' => ['b', 'z']]),
33 | 'glue' => '.',
34 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'],
35 | ],
36 | [
37 | 'source' => call_user_func(function () {
38 | yield 'foo' => ['bar' => 'ter'];
39 | yield 'baz' => ['b', 'z'];
40 | }),
41 | 'glue' => '.',
42 | 'expected' => ['foo.bar' => 'ter', 'baz.0' => 'b', 'baz.1' => 'z'],
43 | ],
44 | ];
45 | }
46 |
47 | /**
48 | * @dataProvider provideEaseCases
49 | *
50 | * @param mixed $source
51 | * @param string $glue
52 | * @param array $expected
53 | */
54 | public function testEase($source, $glue, $expected)
55 | {
56 | $this->assertEquals($expected, __::ease($source, $glue));
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/__/arrays/dropRight.php:
--------------------------------------------------------------------------------
1 | "trick",
13 | * "pen" => "defend",
14 | * "motherly" => "wide",
15 | * "may" => "needle",
16 | * "sweat" => "cake",
17 | * "sword" => "defend",
18 | * ];
19 | *
20 | * __::find($data, "defend");
21 | * ```
22 | *
23 | * **Result**
24 | *
25 | * ```
26 | * "defend"
27 | * ```
28 | *
29 | * **Find by Callback in an Array**
30 | *
31 | * ```php
32 | * $data = [
33 | * "table" => (object)["name" => "trick"],
34 | * "pen" => (object)["name" => "defend"],
35 | * "motherly" => (object)["name" => "wide"],
36 | * "may" => (object)["name" => "needle"],
37 | * "sweat" => (object)["name" => "cake"],
38 | * "sword" => (object)["name" => "defend"],
39 | * ];
40 | *
41 | * __::find($data, static function ($object, $key, $collection) {
42 | * return $object->name === "defend";
43 | * });
44 | * ```
45 | *
46 | * **Result**
47 | *
48 | * ```
49 | * $data["pen"]
50 | * ```
51 | *
52 | * @since 0.2.1 added to bottomline
53 | *
54 | * @param array|iterable $collection an array or iterable of values to look through
55 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback
56 | * @param mixed|null $returnValue the value to return if nothing matches
57 | *
58 | * @see findEntry
59 | * @see findLastEntry
60 | * @see findIndex
61 | * @see findLast
62 | * @see findLastIndex
63 | * @see where
64 | *
65 | * @return mixed|null The entity from the iterable. If no value is found, `$returnValue` is returned, which defaults to
66 | * `null`.
67 | */
68 | function find($collection, $condition, $returnValue = null)
69 | {
70 | $entry = \__::findEntry($collection, $condition);
71 |
72 | if ($entry === null) {
73 | return $returnValue;
74 | }
75 |
76 | return $entry[1];
77 | }
78 |
--------------------------------------------------------------------------------
/src/__/collections/concat.php:
--------------------------------------------------------------------------------
1 | ['favorite' => 'red', 5], 3],
18 | * [10, 'color' => ['favorite' => 'green', 'blue']]
19 | * );
20 | * ```
21 | *
22 | * **Result**
23 | *
24 | * ```
25 | * [
26 | * 'color' => ['favorite' => ['green'], 5, 'blue'],
27 | * 3,
28 | * 10
29 | * ]
30 | * ```
31 | *
32 | * @since 0.2.0 added support for iterables
33 | *
34 | * @param iterable|\stdClass $collection Collection to assign to.
35 | * @param iterable|\stdClass ...$_ N other collections to assign.
36 | *
37 | * @return array|\stdClass If the first argument given to this function is an
38 | * `\stdClass`, an `\stdClass` will be returned. Otherwise, an array will be
39 | * returned.
40 | */
41 | function concat($collection, $_)
42 | {
43 | $args = func_get_args();
44 | $areArrayish = \__::every($args, function ($arg) {
45 | return \__::isArray($arg) || $arg instanceof \stdClass;
46 | });
47 |
48 | if ($areArrayish) {
49 | $argsAsArrays = \__::map($args, function ($arg) {
50 | return (array)$arg;
51 | });
52 | $merged = call_user_func_array('array_merge', $argsAsArrays);
53 |
54 | return ($collection instanceof \stdClass) ? (object)$merged : $merged;
55 | }
56 |
57 | if ($collection instanceof \Iterator || $collection instanceof \IteratorAggregate) {
58 | $result = iterator_to_array(\__::getIterator($collection));
59 | } else {
60 | $result = (array)$collection;
61 | }
62 |
63 | foreach (\__::drop($args, 1) as $iterable) {
64 | foreach ($iterable as $key => $item) {
65 | if (\__::isNumber($key)) {
66 | $result[] = $item;
67 | } else {
68 | $result[$key] = $item;
69 | }
70 | }
71 | }
72 |
73 | return $result;
74 | }
75 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push: ~
5 | pull_request: ~
6 |
7 | jobs:
8 | tests:
9 | name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }}
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | php-versions:
15 | - '5.5'
16 | - '5.6'
17 | - '7.0'
18 | - '7.1'
19 | - '7.2'
20 | - '7.3'
21 | - '7.4'
22 | - '8.0'
23 | - '8.1'
24 | os: [ubuntu-latest]
25 |
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v3
29 |
30 | - name: Set up PHP ${{ matrix.php-versions }}
31 | uses: shivammathur/setup-php@v2
32 | with:
33 | php-version: ${{ matrix.php-versions }}
34 | extensions: mbstring
35 | coverage: pcov
36 |
37 | - name: Setup Problem Matchers for PHP
38 | run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
39 |
40 | - name: Setup Problem Matchers for PHPUnit
41 | run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
42 |
43 | - name: Validate composer.json and composer.lock
44 | run: composer validate
45 |
46 | - name: Get Composer Cache Directory
47 | id: composer-cache
48 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
49 |
50 | - name: Cache dependencies
51 | uses: actions/cache@v3
52 | with:
53 | path: ${{ steps.composer-cache.outputs.dir }}
54 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
55 | restore-keys: ${{ runner.os }}-composer-
56 |
57 | - name: Install dependencies
58 | run: |
59 | # Don't use our lock file for unit tests
60 | rm composer.lock
61 |
62 | # Remove PHP 8.1 requirement
63 | sed -i '/"php": ">=8\.1",/d' composer.json
64 |
65 | # Remove dependencies we don't need for unit tests that break platform requirements
66 | composer remove --dev \
67 | bamarni/composer-bin-plugin
68 |
69 | composer install
70 |
71 | - name: Run test suite
72 | run: |
73 | composer test
74 | composer bench
75 |
--------------------------------------------------------------------------------
/src/__/arrays/chunk.php:
--------------------------------------------------------------------------------
1 | $item) {
22 | if ($counter === $size) {
23 | yield $workspace;
24 |
25 | // Reset our workspace after we've yielded our last chunk
26 | $workspace = [];
27 | $counter = 0;
28 | }
29 |
30 | $key = $preserveKeys ? $key : $counter;
31 | $workspace[$key] = $item;
32 | ++$counter;
33 | }
34 |
35 | yield $workspace;
36 | }
37 |
38 | /**
39 | * Creates an array of elements split into groups the length of `$size`.
40 | *
41 | * If array can't be split evenly, the final chunk will be the remaining
42 | * elements. When `$preserveKeys` is set to TRUE, keys will be preserved.
43 | * Default is FALSE, which will reindex the chunk numerically.
44 | *
45 | * **Usage**
46 | *
47 | * ```php
48 | * __::chunk([1, 2, 3, 4, 5], 3);
49 | * ```
50 | *
51 | * **Result**
52 | *
53 | * ```
54 | * [[1, 2, 3], [4, 5]]
55 | * ```
56 | *
57 | * @since 0.2.0 iterable objects are now supported
58 | *
59 | * @param iterable $iterable The original array
60 | * @param int $size The chunk size
61 | * @param bool $preserveKeys Whether or not to preserve index keys
62 | *
63 | * @throws \InvalidArgumentException when an non-array or non-traversable object is given for $iterable.
64 | * @throws \Exception when an `\IteratorAggregate` is given and `getIterator()` throws an exception.
65 | *
66 | * @return array|\Generator When given a `\Traversable` object for `$iterable`, a generator will be returned.
67 | * Otherwise, an array will be returned.
68 | */
69 | function chunk($iterable, $size = 1, $preserveKeys = false)
70 | {
71 | if (is_array($iterable)) {
72 | return array_chunk($iterable, $size, $preserveKeys);
73 | }
74 |
75 | return chunkIterable($iterable, $size, $preserveKeys);
76 | }
77 |
--------------------------------------------------------------------------------
/src/__/collections/get.php:
--------------------------------------------------------------------------------
1 | ['bar' => 'ter']], 'foo.bar');
17 | * ```
18 | *
19 | * **Result**
20 | *
21 | * ```
22 | * 'ter'
23 | * ```
24 | *
25 | * @param array|object $collection Array of values or object
26 | * @param string $path Array key or object attribute. Use a period
27 | * for depicting a new level in a multidimensional
28 | * array
29 | * @param mixed $default Default value to return if index not exist
30 | *
31 | * @return array|mixed|null
32 | */
33 | function get($collection, $path, $default = null)
34 | {
35 | // TODO Make the algorithm recursive.
36 | // TODO Factorize between object and array access (use a $getter function,
37 | // as the $setter in __::set()).
38 | if (is_array($collection) && isset($collection[$path])) {
39 | return $collection[$path];
40 | }
41 |
42 | foreach (\__::split($path, \__::DOT_NOTATION_DELIMITER) as $segment) {
43 | if (\is_object($collection) && !($collection instanceof \ArrayAccess)) {
44 | if (isset($collection->{$segment})) {
45 | $collection = $collection->{$segment};
46 | } else {
47 | // TODO Remove Closure option: what is the point if it has no parameter:
48 | // it will always yield the same value? KISS.
49 | return $default && $default instanceof \Closure ? $default() : $default;
50 | }
51 | } else {
52 | if (isset($collection[$segment])) {
53 | $collection = $collection[$segment];
54 | } else {
55 | // TODO Same as above on Closure.
56 | return $default && $default instanceof \Closure ? $default() : $default;
57 | }
58 | }
59 | }
60 |
61 | return $collection;
62 | }
63 |
--------------------------------------------------------------------------------
/src/__/collections/concatDeep.php:
--------------------------------------------------------------------------------
1 | ['favorite' => 'red', 5], 3],
18 | * [10, 'color' => ['favorite' => 'green', 'blue']]
19 | * );
20 | * ```
21 | *
22 | * **Result**
23 | *
24 | * ```
25 | * [
26 | * 'color' => [
27 | * 'favorite' => ['red', 'green'],
28 | * 5,
29 | * 'blue'
30 | * ],
31 | * 3,
32 | * 10
33 | * ]
34 | * ```
35 | *
36 | * @since 0.2.0 iterable support was added
37 | *
38 | * @param iterable|\stdClass $collection First collection to concatDeep.
39 | * @param iterable|\stdClass ...$_ N other collections to concatDeep.
40 | *
41 | * @return array|\stdClass A concatenated collection. When the first argument given
42 | * is an `\stdClass`, then resulting value will be an `\stdClass`. Otherwise,
43 | * an array will always be returned.
44 | */
45 | function concatDeep($collection, $_)
46 | {
47 | return \__::reduceRight(func_get_args(), function ($source, $result) {
48 | if ($result instanceof \Iterator || $result instanceof \IteratorAggregate) {
49 | $result = iterator_to_array(\__::getIterator($result));
50 | }
51 |
52 | \__::doForEach($source, function ($sourceValue, $key) use (&$result) {
53 | if (!\__::has($result, $key)) {
54 | $result = \__::set($result, $key, $sourceValue);
55 | } elseif (is_numeric($key)) {
56 | $result = \__::concat($result, [$sourceValue]);
57 | } else {
58 | $resultValue = \__::get($result, $key);
59 | $result = \__::set($result, $key, concatDeep(
60 | \__::isCollection($resultValue) ? $resultValue : (array)$resultValue,
61 | \__::isCollection($sourceValue) ? $sourceValue : (array)$sourceValue
62 | ));
63 | }
64 | });
65 | return $result;
66 | }, []);
67 | }
68 |
--------------------------------------------------------------------------------
/src/__/collections/findLast.php:
--------------------------------------------------------------------------------
1 | "trick",
16 | * "pen" => "defend",
17 | * "motherly" => "wide",
18 | * "may" => "needle",
19 | * "sweat" => "cake",
20 | * "sword" => "defend",
21 | * ];
22 | *
23 | * __::findLast($data, "defend");
24 | * ```
25 | *
26 | * **Result**
27 | *
28 | * ```
29 | * "defend"
30 | * ```
31 | *
32 | * **Find by Callback in an Array**
33 | *
34 | * ```php
35 | * $data = [
36 | * "table" => (object)["name" => "trick"],
37 | * "pen" => (object)["name" => "defend"],
38 | * "motherly" => (object)["name" => "wide"],
39 | * "may" => (object)["name" => "needle"],
40 | * "sweat" => (object)["name" => "cake"],
41 | * "sword" => (object)["name" => "defend"],
42 | * ];
43 | *
44 | * __::findLast($data, static function ($object, $key, $collection) {
45 | * return $object->name === "defend";
46 | * });
47 | * ```
48 | *
49 | * **Result**
50 | *
51 | * ```
52 | * $data["sword"]
53 | * ```
54 | *
55 | * @since 0.2.1 added to bottomline
56 | *
57 | * @param iterable $collection an array or iterable of values to look through
58 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback
59 | * @param mixed|null $returnValue the value to return if nothing matches
60 | *
61 | * @see find
62 | * @see findEntry
63 | * @see findLastEntry
64 | * @see findIndex
65 | * @see findLastIndex
66 | * @see where
67 | *
68 | * @return mixed|null The entity from the iterable. If no value is found, `$returnValue` is returned, which defaults to
69 | * `null`.
70 | */
71 | function findLast($collection, $condition, $returnValue = null)
72 | {
73 | return \__::find(\__::reverseIterable($collection), $condition, $returnValue);
74 | }
75 |
--------------------------------------------------------------------------------
/tests/__/Collections/PickTest.php:
--------------------------------------------------------------------------------
1 | 1, 'b' => ['c' => 3, 'd' => 4], 'h' => 5];
16 |
17 | // Act
18 | $x = __::pick($a, ['a', 'b.d', 'e', 'f.g']);
19 |
20 | // Assert
21 | $this->assertEquals([
22 | 'a' => 1,
23 | 'b' => ['d' => 4],
24 | 'e' => null,
25 | 'f' => ['g' => null]
26 | ], $x);
27 | }
28 |
29 | public function testPickDefaults()
30 | {
31 | // Arrange.
32 | $a = ['nasa' => 1, 'cnsa' => 42];
33 | $b = ['a' => 1, 'b' => ['c' => 3, 'd' => 4]];
34 |
35 | // Act.
36 | $x = __::pick($a, ['cnsa', 'esa', 'jaxa'], 26);
37 | $y = __::pick($b, ['a', 'b.d', 'e', 'f.g'], 'default');
38 |
39 | // Assert.
40 | $this->assertEquals([
41 | 'cnsa' => 42,
42 | 'esa' => 26,
43 | 'jaxa' => 26,
44 | ], $x);
45 | $this->assertEquals([
46 | 'a' => 1,
47 | 'b' => ['d' => 4],
48 | 'e' => 'default',
49 | 'f' => ['g' => 'default']
50 | ], $y);
51 | }
52 |
53 | public function testPickObject()
54 | {
55 | // Arrange.
56 | $a = new \stdClass();
57 | $a->paris = 10659489;
58 | $a->marseille = 1578484;
59 | $a->lyon = 1620331;
60 |
61 | // Act.
62 | $x = __::pick($a, ['marseille', 'london']);
63 |
64 | // Assert.
65 | $this->assertEquals((object)[
66 | 'marseille' => 1578484,
67 | 'london' => null
68 | ], $x);
69 | }
70 |
71 | public function testPickIterable()
72 | {
73 | // Arrange
74 | $a = new ArrayIterator(['a' => 1, 'b' => ['c' => 3, 'd' => 4], 'h' => 5]);
75 |
76 | // Act
77 | $x = __::pick($a, ['a', 'b.d', 'e', 'f.g']);
78 |
79 | // Assert
80 | $this->assertEquals([
81 | 'a' => 1,
82 | 'b' => ['d' => 4],
83 | 'e' => null,
84 | 'f' => ['g' => null]
85 | ], $x);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/__/Collections/PluckTest.php:
--------------------------------------------------------------------------------
1 | 'bar', 'bis' => 'ter', '' => 0],
17 | ['foo' => 'bar2', 'bis' => 'ter2', '' => 1],
18 | ];
19 |
20 | $b = new \stdClass();
21 | $b->one = new \stdClass();
22 | $b->one->foo = 'bar';
23 | $b->two = new \stdClass();
24 | $b->two->foo = 'bar2';
25 | $b->three = new \stdClass();
26 | $c = [$b->one, $b->two];
27 |
28 | $d = [
29 | ['foo' => ['bar' => ['baz' => 1]]],
30 | ['foo' => ['bar' => ['baz' => 2]]]
31 | ];
32 | $e = new \stdClass();
33 | $e->one = new \stdClass();
34 | $e->one->foo = new \stdClass();
35 | $e->one->foo->bar = ['baz' => 1];
36 | $e->two = new \stdClass();
37 | $e->two->foo = new \stdClass();
38 | $e->two->foo->bar = new \stdClass();
39 | $e->two->foo->bar->baz = 2;
40 |
41 | // Act
42 | $x = __::pluck($a, 'foo');
43 | $x2 = __::pluck($a, '');
44 |
45 | $y = __::pluck($b, 'foo');
46 | $y2 = __::pluck($c, 'foo');
47 |
48 | $z = __::pluck($d, 'foo.bar.baz');
49 | $z2 = __::pluck($e, 'foo.bar.baz');
50 |
51 | // Assert
52 | $this->assertEquals(['bar', 'bar2'], $x);
53 | $this->assertEquals([0, 1], $x2);
54 |
55 | $this->assertEquals(['bar', 'bar2', null], $y);
56 | $this->assertEquals(['bar', 'bar2'], $y2);
57 |
58 | $this->assertEquals([1, 2], $z);
59 | $this->assertEquals([1, 2], $z2);
60 | }
61 |
62 | public function testPluckIterable()
63 | {
64 | // Arrange
65 | $a = new ArrayIterator([
66 | ['foo' => 'bar', 'bis' => 'ter', '' => 0],
67 | ['foo' => 'bar2', 'bis' => 'ter2', '' => 1],
68 | ]);
69 |
70 | // Act
71 | $x = __::pluck($a, 'foo');
72 | $x2 = __::pluck($a, '');
73 |
74 | // Assert
75 | $this->assertEquals(['bar', 'bar2'], $x);
76 | $this->assertEquals([0, 1], $x2);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/__/collections/findEntry.php:
--------------------------------------------------------------------------------
1 | "trick",
13 | * "pen" => "defend",
14 | * "motherly" => "wide",
15 | * "may" => "needle",
16 | * "sweat" => "cake",
17 | * "sword" => "defend",
18 | * ];
19 | *
20 | * __::findEntry($data, "defend");
21 | * ```
22 | *
23 | * **Result**
24 | *
25 | * ```
26 | * ["pen", "defend"]
27 | * ```
28 | *
29 | * **Find by Callback in an Array**
30 | *
31 | * ```php
32 | * $data = [
33 | * "table" => (object)["name" => "trick"],
34 | * "pen" => (object)["name" => "defend"],
35 | * "motherly" => (object)["name" => "wide"],
36 | * "may" => (object)["name" => "needle"],
37 | * "sweat" => (object)["name" => "cake"],
38 | * "sword" => (object)["name" => "defend"],
39 | * ];
40 | *
41 | * __::findEntry($data, static function ($object, $key, $collection) {
42 | * return $object->name === "defend";
43 | * });
44 | * ```
45 | *
46 | * **Result**
47 | *
48 | * ```
49 | * ["pen", $data["pen"]]
50 | * ```
51 | *
52 | * @since 0.2.1 added to bottomline
53 | *
54 | * @param iterable $collection an array or iterable of values to look through
55 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback
56 | *
57 | * @see find
58 | * @see findLastEntry
59 | * @see findLastIndex
60 | * @see findIndex
61 | * @see findLast
62 | * @see where
63 | *
64 | * @return array|null An array with two values, the 0th index is the key and the 1st index is the value. Null is
65 | * returned if no entries can be found in the given collection.
66 | */
67 | function findEntry($collection, $condition)
68 | {
69 | $comparison = is_callable($condition) ? $condition : static function ($value, $_key, $_arr) use ($condition) {
70 | return $value === $condition;
71 | };
72 |
73 | foreach ($collection as $key => $arrItem) {
74 | if ($comparison($arrItem, $key, $collection)) {
75 | return [$key, $arrItem];
76 | }
77 | }
78 |
79 | return null;
80 | }
81 |
--------------------------------------------------------------------------------
/tests/__/Collections/SizeTest.php:
--------------------------------------------------------------------------------
1 | [],
17 | 'expected' => 0,
18 | ],
19 | [
20 | 'source' => [1, 2, 3],
21 | 'expected' => 3,
22 | ],
23 | [
24 | 'source' => new \stdClass(),
25 | 'expected' => 0,
26 | ],
27 | [
28 | 'source' => (object)['hello' => 'world'],
29 | 'expected' => 1,
30 | ],
31 | [
32 | 'source' => false,
33 | 'expected' => false,
34 | ],
35 | [
36 | 'source' => null,
37 | 'expected' => false,
38 | ],
39 | [
40 | 'source' => 3,
41 | 'expected' => false,
42 | ],
43 | [
44 | 'source' => new ArrayIterator([]),
45 | 'expected' => 0,
46 | ],
47 | [
48 | 'source' => new ArrayIterator([1, 2, 3]),
49 | 'expected' => 3,
50 | ],
51 | [
52 | 'source' => new MockIteratorAggregate([]),
53 | 'expected' => 0,
54 | ],
55 | [
56 | 'source' => new MockIteratorAggregate([1, 2, 3]),
57 | 'expected' => 3,
58 | ],
59 | [
60 | 'source' => call_user_func(function () {
61 | yield 1;
62 | }),
63 | 'expected' => 1,
64 | ],
65 | [
66 | 'source' => call_user_func(function () {
67 | }),
68 | 'expected' => 0,
69 | ],
70 | ];
71 | }
72 |
73 | /**
74 | * @dataProvider provideSizeCases
75 | *
76 | * @param mixed $source
77 | * @param int $expected
78 | */
79 | public function testSize($source, $expected)
80 | {
81 | $this->assertEquals($expected, __::size($source));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/__/Arrays/DropTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([2, 3], $x);
25 | $this->assertEquals([3], $y);
26 | $this->assertEquals([], $z);
27 | $this->assertEquals([1, 2, 3], $xa);
28 | }
29 |
30 | public function testDropWithIterator()
31 | {
32 | $a = [1, 2, 3, 4, 5];
33 | $aItr = new ArrayIterator($a);
34 |
35 | $expected = __::drop($a, 3);
36 | $actual = __::drop($aItr, 3);
37 | $itrSize = 0;
38 |
39 | foreach ($actual as $i => $item) {
40 | ++$itrSize;
41 | $this->assertEquals($item, $expected[$i]);
42 | }
43 |
44 | $this->assertEquals(count($expected), $itrSize);
45 | }
46 |
47 | public function testDropWithIteratorAggregate()
48 | {
49 | $a = [1, 2, 3, 4, 5];
50 | $aItrAgg = new MockIteratorAggregate($a);
51 |
52 | $expected = __::drop($a, 3);
53 | $actual = __::drop($aItrAgg, 3);
54 | $itrSize = 0;
55 |
56 | foreach ($actual as $i => $item) {
57 | ++$itrSize;
58 | $this->assertEquals($item, $expected[$i]);
59 | }
60 |
61 | $this->assertEquals(count($expected), $itrSize);
62 | }
63 |
64 | public function testDropWithGenerator()
65 | {
66 | $a = [1, 2, 3, 4, 5];
67 | $generator = call_user_func(function () use ($a) {
68 | foreach ($a as $item) {
69 | yield $item;
70 | }
71 | });
72 |
73 | $this->assertInstanceOf(\Generator::class, $generator);
74 |
75 | $expected = __::drop($a, 3);
76 | $actual = __::drop($generator, 3);
77 | $itrSize = 0;
78 |
79 | foreach ($actual as $i => $item) {
80 | ++$itrSize;
81 | $this->assertEquals($item, $expected[$i]);
82 | }
83 |
84 | $this->assertEquals(count($expected), $itrSize);
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/tests/__/Collections/IsEmptyTest.php:
--------------------------------------------------------------------------------
1 | [],
18 | 'expected' => true,
19 | ],
20 | [
21 | 'source' => ['Falcon', 'Heavy'],
22 | 'expected' => false,
23 | ],
24 | [
25 | 'source' => new \stdClass(),
26 | 'expected' => true,
27 | ],
28 | [
29 | 'source' => (object)['Baie' => 'Goji'],
30 | 'expected' => false,
31 | ],
32 |
33 | // Assert non-collections
34 | [
35 | 'source' => null,
36 | 'expected' => true,
37 | ],
38 | [
39 | 'source' => 3,
40 | 'expected' => true,
41 | ],
42 | [
43 | 'source' => true,
44 | 'expected' => true,
45 | ],
46 |
47 |
48 | // Assert iterators
49 | [
50 | 'source' => new ArrayIterator([]),
51 | 'expected' => true,
52 | ],
53 | [
54 | 'source' => new MockIteratorAggregate([]),
55 | 'expected' => true,
56 | ],
57 | [
58 | 'source' => new ArrayIterator([1, 2]),
59 | 'expected' => false,
60 | ],
61 | [
62 | 'source' => new MockIteratorAggregate([1, 2]),
63 | 'expected' => false,
64 | ],
65 | [
66 | 'source' => call_user_func(function () {
67 | yield 1;
68 | yield 5;
69 | }),
70 | 'expected' => false,
71 | ],
72 | ];
73 | }
74 |
75 | /**
76 | * @dataProvider provideIsEmptyCases
77 | *
78 | * @param mixed $source
79 | * @param bool $expected
80 | */
81 | public function testIsEmpty($source, $expected)
82 | {
83 | $this->assertEquals($expected, __::isEmpty($source));
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/__/Arrays/DropRightTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([1, 2], $out1);
22 | $this->assertEquals([1], $out2);
23 | $this->assertEquals([], $out3);
24 | $this->assertEquals([1, 2, 3], $out4);
25 | }
26 |
27 | public function testDropRightWithIterator()
28 | {
29 | $a = [1, 2, 3, 4, 5];
30 | $aItr = new ArrayIterator($a);
31 |
32 | $expected = __::dropRight($a, 3);
33 | $actual = __::dropRight($aItr, 3);
34 | $itrSize = 0;
35 |
36 | foreach ($actual as $i => $item) {
37 | ++$itrSize;
38 | $this->assertEquals($item, $expected[$i]);
39 | }
40 |
41 | $this->assertEquals(count($expected), $itrSize);
42 | }
43 |
44 | public function testDropRightWithIteratorAggregate()
45 | {
46 | $a = [1, 2, 3, 4, 5];
47 | $aItrAgg = new MockIteratorAggregate($a);
48 |
49 | $expected = __::dropRight($a, 3);
50 | $actual = __::dropRight($aItrAgg, 3);
51 | $itrSize = 0;
52 |
53 | foreach ($actual as $i => $item) {
54 | ++$itrSize;
55 | $this->assertEquals($item, $expected[$i]);
56 | }
57 |
58 | $this->assertEquals(count($expected), $itrSize);
59 | }
60 |
61 | public function testDropRightWithGenerator()
62 | {
63 | $a = [1, 2, 3, 4, 5];
64 | $generator = call_user_func(function () use ($a) {
65 | foreach ($a as $item) {
66 | yield $item;
67 | }
68 | });
69 |
70 | $this->assertInstanceOf(\Generator::class, $generator);
71 |
72 | $expected = __::dropRight($a, 3);
73 | $actual = __::dropRight($generator, 3);
74 | $itrSize = 0;
75 |
76 | foreach ($actual as $i => $item) {
77 | ++$itrSize;
78 | $this->assertEquals($item, $expected[$i]);
79 | }
80 |
81 | $this->assertEquals(count($expected), $itrSize);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/tests/__/Collections/MergeTest.php:
--------------------------------------------------------------------------------
1 | ['favorite' => 'red', 'model' => 3, 5], 3];
16 | $a2 = [10, 'color' => ['favorite' => 'green', 'blue']];
17 | $b1 = ['a' => 0];
18 | $b2 = ['a' => 1, 'b' => 2];
19 | $b3 = ['c' => 3, 'd' => 4];
20 |
21 | // Act
22 | $x = __::merge($a1, $a2);
23 | $y = __::merge($b1, $b2, $b3);
24 |
25 | // Assert
26 | $this->assertEquals(['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10], $x);
27 | $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $y);
28 | }
29 |
30 | public function testMergeObject()
31 | {
32 | // Arrange
33 | $a1 = (object)['color' => (object)['favorite' => 'red', 'model' => 3, 5]];
34 | $a2 = (object)[10, 'color' => (object)['favorite' => 'green', 'blue']];
35 | $b1 = (object)['a' => 0];
36 | $b2 = (object)['a' => 1, 'b' => 2, 5];
37 | $b3 = (object)['c' => 3, 'd' => 4, 6];
38 |
39 | // Act
40 | $x = __::merge($a1, $a2);
41 | $y = __::merge($b1, $b2, $b3);
42 |
43 | // Assert
44 | $this->assertEquals((object)['color' => (object)['favorite' => 'green', 'model' => 3, 'blue'], 10], $x);
45 | $this->assertEquals((object)['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 6], $y);
46 | }
47 |
48 | public function testMergeIterable()
49 | {
50 | // Arrange
51 | $a1 = new ArrayIterator(['color' => ['favorite' => 'red', 'model' => 3, 5], 3]);
52 | $a2 = new ArrayIterator([10, 'color' => ['favorite' => 'green', 'blue']]);
53 |
54 | // Act
55 | $x = __::merge($a1, $a2);
56 |
57 | // Assert
58 | // Check we got back an array.
59 | $this->assertTrue(is_array($x));
60 | $xValues = [];
61 | foreach ($x as $key => $value) {
62 | $xValues[$key] = $value;
63 | }
64 | $this->assertEquals(new ArrayIterator(['color' => ['favorite' => 'red', 'model' => 3, 5], 3]), $a1);
65 | $this->assertEquals(new ArrayIterator([10, 'color' => ['favorite' => 'green', 'blue']]), $a2);
66 | $this->assertEquals(['color' => ['favorite' => 'green', 'model' => 3, 'blue'], 10], $xValues);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "maciejczyzewski/bottomline",
3 | "description": "A full-on PHP manipulation utility belt that provides support for working with arrays, objects, and iterables; a lodash or underscore equivalent for PHP.",
4 | "keywords": ["__", "bottomline", "library", "utility", "functions", "underscore", "lodash"],
5 | "license": "MIT",
6 | "type": "library",
7 | "authors": [
8 | {
9 | "name": "Maciej A. Czyzewski",
10 | "email": "maciejanthonyczyzewski@gmail.com"
11 | }
12 | ],
13 | "require": {
14 | "php": ">=5.5.0",
15 | "ext-mbstring": "*"
16 | },
17 | "require-dev": {
18 | "php": ">=8.1",
19 | "ext-json": "*",
20 | "bamarni/composer-bin-plugin": "^1.8",
21 | "doctrine/instantiator": "^1.0||^1.1||^1.4",
22 | "erusev/parsedown": "^1.7",
23 | "nikic/php-parser": "^3.1||^4.13",
24 | "phpdocumentor/reflection-docblock": "^3.0||^4.3||^5.2",
25 | "phpunit/phpunit": "^4.8||^6.5||^9.5"
26 | },
27 | "autoload": {
28 | "classmap": ["bottomline.php"],
29 | "psr-4": {"__\\": "src/__/"}
30 | },
31 | "autoload-dev": {
32 | "psr-4": {
33 | "__\\DocGen\\": "docgen/",
34 | "__\\Test\\": "tests/__/",
35 | "__\\TestHelpers\\": "tests/Helpers/"
36 | }
37 | },
38 | "scripts": {
39 | "post-install-cmd": [
40 | "@composer bin php-cs-fixer install --ansi"
41 | ],
42 | "bench": "php tools/bench.php",
43 | "bin": "echo 'bin not installed'",
44 | "cs-fix": "php-cs-fixer fix -v",
45 | "cs-check": "php-cs-fixer fix --dry-run -v --diff",
46 | "doc": [
47 | "php tools/phpDocGen.php",
48 | "@cs-fix"
49 | ],
50 | "site": [
51 | "cd docs; bundle info github-pages > /dev/null 2>&1 || bundle install",
52 | "cd docs; bundle exec jekyll serve"
53 | ],
54 | "test": [
55 | "# Silence PHP deprecation warnings before PHPUnit is run",
56 | "php -d error_reporting=24575 ./vendor/bin/phpunit"
57 | ]
58 | },
59 | "config": {
60 | "sort-packages": true,
61 | "allow-plugins": {
62 | "bamarni/composer-bin-plugin": true
63 | }
64 | },
65 | "extra": {
66 | "branch-alias": {
67 | "dev-master": "0.2-dev"
68 | },
69 | "bamarni-bin": {
70 | "bin-links": true,
71 | "forward-command": true
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/__/Collections/MapTest.php:
--------------------------------------------------------------------------------
1 | [1, 2, 3],
17 | 'closure' => function ($n) {
18 | return $n * 3;
19 | },
20 | 'isGenerator' => false,
21 | 'expected' => [3, 6, 9],
22 | ],
23 | [
24 | 'source' => (object)['a' => 1, 'b' => 2, 'c' => 3],
25 | 'closure' => function ($n, $key) {
26 | return $key === 'c' ? $n : $n * 3;
27 | },
28 | 'isGenerator' => false,
29 | 'expected' => [3, 6, 3],
30 | ],
31 | [
32 | 'source' => new ArrayIterator([1, 2, 3]),
33 | 'closure' => function ($n) {
34 | return $n * 3;
35 | },
36 | 'isGenerator' => true,
37 | 'expected' => [3, 6, 9],
38 | ],
39 | [
40 | 'source' => new MockIteratorAggregate([1, 2, 3]),
41 | 'closure' => function ($n) {
42 | return $n * 6;
43 | },
44 | 'isGenerator' => true,
45 | 'expected' => [6, 12, 18],
46 | ],
47 | [
48 | 'source' => call_user_func(function () {
49 | yield 1;
50 | yield 2;
51 | yield 3;
52 | }),
53 | 'closure' => function ($n) {
54 | return $n * 3;
55 | },
56 | 'isGenerator' => true,
57 | 'expected' => [3, 6, 9],
58 | ],
59 | ];
60 | }
61 |
62 | /**
63 | * @dataProvider provideMapCases
64 | *
65 | * @param iterable $source
66 | * @param \Closure $closure
67 | * @param bool $isGenerator
68 | * @param array $expected
69 | */
70 | public function testMap($source, $closure, $isGenerator, $expected)
71 | {
72 | $actual = __::map($source, $closure);
73 |
74 | if ($isGenerator) {
75 | $this->assertInstanceOf(\Generator::class, $actual);
76 | } else {
77 | $this->assertTrue(is_array($actual));
78 | }
79 |
80 | foreach ($actual as $key => $value) {
81 | $this->assertEquals($expected[$key], $value);
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/tests/__/Collections/SetTest.php:
--------------------------------------------------------------------------------
1 | ['bar' => 'ter']];
17 |
18 | // Act
19 | $x = __::set($a, 'foo.baz.ber', 'fer');
20 | $y = __::set($a, 'foo.bar', 'fer2');
21 |
22 | // Assert
23 | $this->assertEquals(['foo' => ['bar' => 'ter']], $a);
24 | $this->assertEquals(['ber' => 'fer'], $x['foo']['baz']);
25 | $this->assertEquals(['foo' => ['bar' => 'fer2']], $y);
26 | }
27 |
28 | public function testSetArrayAccess()
29 | {
30 | $aa = new ArrayAccessible();
31 |
32 | __::set($aa, 'foo.ubi', [
33 | 'bar' => 'qaz',
34 | ]);
35 | __::set($aa, 'faa.raot.uft', 100);
36 |
37 | $this->assertTrue(is_array(__::get($aa, 'foo')));
38 | $this->assertTrue(is_array(__::get($aa, 'faa')));
39 | $this->assertTrue(is_array(__::get($aa, 'faa.raot')));
40 |
41 | $this->assertEquals('qaz', __::get($aa, 'foo.ubi.bar'));
42 | $this->assertEquals(42, __::get($aa, 'foo.nonexistent', 42));
43 | }
44 |
45 | public function testSetObject()
46 | {
47 | // Arrange.
48 | $a = (object)['foo' => (object)['bar' => 'ter']];
49 |
50 | // Act.
51 | $x = __::set($a, 'foo.baz.ber', 'fer');
52 | $y = __::set($a, 'foo.bar', 'fer2');
53 |
54 | // Assert.
55 | $this->assertEquals((object )['foo' => (object)['bar' => 'ter']], $a);
56 | $this->assertEquals((object)['ber' => 'fer'], $x->foo->baz);
57 | $this->assertEquals((object)['foo' => (object)['bar' => 'fer2']], $y);
58 | }
59 |
60 | public function testSetOverride()
61 | {
62 | // Arrange
63 | $a = ['foo' => ['bar' => 'ter']];
64 |
65 | // Act
66 | $x = __::set($a, 'foo.bar.not_exist', 'baz');
67 |
68 | // Assert.
69 | $this->assertEquals(['foo' => ['bar' => 'ter']], $a);
70 | $this->assertEquals(['foo' => ['bar' => ['not_exist' => 'baz']]], $x);
71 | }
72 |
73 | public function testSetIterable()
74 | {
75 | // Arrange
76 | $a = new ArrayIterator(['foo' => ['bar' => 'ter']]);
77 |
78 | // Act
79 | $x = __::set($a, 'foo.baz.ber', 'fer');
80 |
81 | // Assert
82 | $this->assertEquals(new ArrayIterator(['foo' => ['bar' => 'ter']]), $a);
83 | $this->assertEquals(['ber' => 'fer'], $x['foo']['baz']);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/__/Collections/GetTest.php:
--------------------------------------------------------------------------------
1 | ['bar' => 'ter'],
17 | 'baz' => ['foo' => ['obj' => $o]]
18 | ];
19 |
20 | // Act
21 | $x = __::get($a, 'foo.bar');
22 | $x2 = __::get($a, 'foo.bar', 'default');
23 | $y = __::get($a, 'foo.baz');
24 | $y2 = __::get($a, 'foo.baz', 'default');
25 | $y3 = __::get($a, 'foo.baz', function () {
26 | return 'default_from_callback';
27 | });
28 | $z = __::get($a, 'baz.foo.obj');
29 |
30 | // Assert
31 | $this->assertEquals('ter', $x);
32 | $this->assertEquals('ter', $x2);
33 | $this->assertNull($y);
34 | $this->assertEquals('default', $y2);
35 | $this->assertEquals('default_from_callback', $y3);
36 | $this->assertEquals($o, $z);
37 | }
38 |
39 | public function testGetArrayAccess()
40 | {
41 | $aa = new ArrayAccessible();
42 | $aa['foo'] = [
43 | 'bar' => 'quim',
44 | ];
45 | $aa['bar'] = 5;
46 | $aa['caz'] = new \stdClass();
47 | $aa['caz']->daer = 'heft';
48 |
49 | $this->assertEquals('quim', __::get($aa, 'foo.bar'));
50 | $this->assertEquals(5, __::get($aa, 'bar'));
51 | $this->assertEquals('heft', __::get($aa, 'caz.daer'));
52 |
53 | $this->assertNull(__::get($aa, 'foo.cat'));
54 | }
55 |
56 | public function testGetObjects()
57 | {
58 | // Arrange
59 | $o = new \stdClass();
60 | $a = new \stdClass();
61 | $a->foo = new \stdClass();
62 | $a->foo->bar = 'ter';
63 | $a->baz = new \stdClass();
64 | $a->baz->foo = new \stdClass();
65 | $a->baz->foo->obj = $o;
66 |
67 | // Act
68 | $x = __::get($a, 'foo.bar');
69 | $x2 = __::get($a, 'foo.bar', 'default');
70 | $y = __::get($a, 'foo.baz');
71 | $y2 = __::get($a, 'foo.baz', 'default');
72 | $y3 = __::get($a, 'foo.baz', function () {
73 | return 'default_from_callback';
74 | });
75 | $z = __::get($a, 'baz.foo.obj');
76 |
77 | // Assert
78 | $this->assertEquals('ter', $x);
79 | $this->assertEquals('ter', $x2);
80 | $this->assertNull($y);
81 | $this->assertEquals('default', $y2);
82 | $this->assertEquals('default_from_callback', $y3);
83 | $this->assertEquals($o, $z);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/tests/__/Arrays/DropRightWhileTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([1, 2], $out1);
19 | $this->assertEquals([1, 2], $out2);
20 | $this->assertEquals([], $out3);
21 | }
22 |
23 | public function testDropRightWhileWithCallback()
24 | {
25 | $f = static function ($item) {
26 | return $item >= 3;
27 | };
28 |
29 | $out1 = __::dropRightWhile([1, 2, 3, 4], $f);
30 | $out2 = __::dropRightWhile([1, 2], $f);
31 | $out3 = __::dropRightWhile([], $f);
32 |
33 | $this->assertEquals([1, 2], $out1);
34 | $this->assertEquals([1, 2], $out2);
35 | $this->assertEquals([], $out3);
36 | }
37 |
38 | public function testDropRightWhileWithIterator()
39 | {
40 | $a = [1, 2, 3, 4, 5];
41 | $aItr = new ArrayIterator($a);
42 |
43 | $expected = __::dropRightWhile($a, 3);
44 | $actual = __::dropRightWhile($aItr, 3);
45 | $itrSize = 0;
46 |
47 | foreach ($actual as $i => $item) {
48 | ++$itrSize;
49 | $this->assertEquals($item, $expected[$i]);
50 | }
51 |
52 | $this->assertEquals(count($expected), $itrSize);
53 | }
54 |
55 | public function testDropRightWhileWithIteratorAggregate()
56 | {
57 | $a = [1, 2, 3, 4, 5];
58 | $aItrAgg = new MockIteratorAggregate($a);
59 |
60 | $expected = __::dropRightWhile($a, 3);
61 | $actual = __::dropRightWhile($aItrAgg, 3);
62 | $itrSize = 0;
63 |
64 | foreach ($actual as $i => $item) {
65 | ++$itrSize;
66 | $this->assertEquals($item, $expected[$i]);
67 | }
68 |
69 | $this->assertEquals(count($expected), $itrSize);
70 | }
71 |
72 | public function testDropRightWhileWithGenerator()
73 | {
74 | $a = [1, 2, 3, 4, 5];
75 | $generator = call_user_func(function () use ($a) {
76 | foreach ($a as $item) {
77 | yield $item;
78 | }
79 | });
80 |
81 | $this->assertInstanceOf(\Generator::class, $generator);
82 |
83 | $expected = __::dropRightWhile($a, 3);
84 | $actual = __::dropRightWhile($generator, 3);
85 | $itrSize = 0;
86 |
87 | foreach ($actual as $i => $item) {
88 | ++$itrSize;
89 | $this->assertEquals($item, $expected[$i]);
90 | }
91 |
92 | $this->assertEquals(count($expected), $itrSize);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/__/collections/findIndex.php:
--------------------------------------------------------------------------------
1 | "trick",
35 | * "pen" => "defend",
36 | * "motherly" => "wide",
37 | * "may" => "needle",
38 | * "sweat" => "cake",
39 | * "sword" => "defend",
40 | * ];
41 | *
42 | * __::findIndex($data, "defend")
43 | * ```
44 | *
45 | * **Result**
46 | *
47 | * ```
48 | * "pen"
49 | * ```
50 | *
51 | * **Find by Callback Usage**
52 | *
53 | * ```php
54 | * $data = [
55 | * "table" => (object)["name" => "trick"],
56 | * "pen" => (object)["name" => "defend"],
57 | * "motherly" => (object)["name" => "wide"],
58 | * "may" => (object)["name" => "needle"],
59 | * "sweat" => (object)["name" => "cake"],
60 | * "sword" => (object)["name" => "defend"],
61 | * ];
62 | *
63 | * __::findIndex($data, static function ($object, $key, $collection) {
64 | * return $object->name === "defend";
65 | * })
66 | * ```
67 | *
68 | * **Result**
69 | *
70 | * ```
71 | * "pen"
72 | * ```
73 | *
74 | * @since 0.2.1 added to bottomline
75 | *
76 | * @param iterable $collection an array or iterable of values to look through
77 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback
78 | * @param int|string $returnValue the value to return if nothing matches
79 | *
80 | * @see find
81 | * @see findEntry
82 | * @see findLastEntry
83 | * @see findLast
84 | * @see findLastIndex
85 | * @see where
86 | *
87 | * @return int|string The index where the respective value is found. When given a numerically
88 | * indexed array, an int will be returned but when an associative array is given, a string will
89 | * be returned.
90 | * If no value is found, `$returnValue` is returned, which defaults to `-1`.
91 | */
92 | function findIndex($collection, $condition, $returnValue = -1)
93 | {
94 | $entry = \__::findEntry($collection, $condition);
95 |
96 | if ($entry === null) {
97 | return $returnValue;
98 | }
99 |
100 | return $entry[0];
101 | }
102 |
--------------------------------------------------------------------------------
/src/__/collections/findLastIndex.php:
--------------------------------------------------------------------------------
1 | "trick",
38 | * "pen" => "defend",
39 | * "motherly" => "wide",
40 | * "may" => "needle",
41 | * "sweat" => "cake",
42 | * "sword" => "defend",
43 | * ];
44 | *
45 | * __::findLastIndex($data, "defend")
46 | * ```
47 | *
48 | * **Result**
49 | *
50 | * ```
51 | * "sword"
52 | * ```
53 | *
54 | * **Find by Callback Usage**
55 | *
56 | * ```php
57 | * $data = [
58 | * "table" => (object)["name" => "trick"],
59 | * "pen" => (object)["name" => "defend"],
60 | * "motherly" => (object)["name" => "wide"],
61 | * "may" => (object)["name" => "needle"],
62 | * "sweat" => (object)["name" => "cake"],
63 | * "sword" => (object)["name" => "defend"],
64 | * ];
65 | *
66 | * __::findLastIndex($data, static function ($object, $key, $collection) {
67 | * return $object->name === "defend";
68 | * })
69 | * ```
70 | *
71 | * **Result**
72 | *
73 | * ```
74 | * "sword"
75 | * ```
76 | *
77 | * @since 0.2.1 added to bottomline
78 | *
79 | * @param iterable $collection an array or iterable of values to look through
80 | * @param bool|\Closure|double|int|string $condition condition to match using either a primitive value or a callback
81 | * @param int|string $returnValue the value to return if nothing matches
82 | *
83 | * @see find
84 | * @see findEntry
85 | * @see findLastEntry
86 | * @see findIndex
87 | * @see findLast
88 | * @see where
89 | *
90 | * @return int|string The index where the respective value is found. When given a numerically
91 | * indexed array, an int will be returned but when an associative array is given, a string will
92 | * be returned.
93 | * If no value is found, `$returnValue` is returned, which defaults to `-1`.
94 | */
95 | function findLastIndex($collection, $condition, $returnValue = -1)
96 | {
97 | return \__::findIndex(\__::reverseIterable($collection), $condition, $returnValue);
98 | }
99 |
--------------------------------------------------------------------------------
/src/__/arrays/dropWhile.php:
--------------------------------------------------------------------------------
1 | name = $documentedParam->getVariableName();
28 | $this->description = $documentedParam->getDescription()->render();
29 | $this->isVariadic = $documentedParam->isVariadic();
30 | $this->type = $documentedParam->getType();
31 | $this->defaultValue = null;
32 | $this->defaultValueAsString = null;
33 |
34 | if ($reflectedParam !== null && $reflectedParam->isOptional()) {
35 | try {
36 | $defaultValue = $reflectedParam->getDefaultValue();
37 | $this->defaultValue = $defaultValue;
38 | if ($defaultValue === null) {
39 | $this->defaultValueAsString = 'null';
40 | } elseif (is_bool($defaultValue)) {
41 | $this->defaultValueAsString = $defaultValue ? 'true' : 'false';
42 | } elseif (is_string($defaultValue)) {
43 | $this->defaultValueAsString = sprintf("'%s'", $defaultValue);
44 | } elseif (is_array($defaultValue)) {
45 | $this->defaultValueAsString = '[]';
46 | } else {
47 | $this->defaultValueAsString = $defaultValue;
48 | }
49 | } catch (Exception $e) {
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * @see Method
56 | */
57 | public function getMethodArgumentDefinition(): array
58 | {
59 | return [
60 | 'name' => $this->getSignature(),
61 | 'type' => $this->type,
62 | ];
63 | }
64 |
65 | public function getSignature(): string
66 | {
67 | if ($this->defaultValueAsString) {
68 | return "{$this->name} = {$this->defaultValueAsString}";
69 | }
70 | if ($this->isVariadic) {
71 | return "{$this->name},...";
72 | }
73 |
74 | return $this->name;
75 | }
76 |
77 | public function jsonSerialize(): mixed
78 | {
79 | return [
80 | 'name' => $this->name,
81 | 'isVariadic' => $this->isVariadic,
82 | 'description' => $this->description,
83 | 'defaultValue' => $this->defaultValue,
84 | 'defaultValueAsString' => $this->defaultValueAsString,
85 | 'type' => (string)$this->type,
86 | ];
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/__/arrays/dropRightWhile.php:
--------------------------------------------------------------------------------
1 | = 0; $i--) {
38 | if ($comparison($input[$i])) {
39 | continue;
40 | }
41 |
42 | return array_slice($input, 0, $i + 1);
43 | }
44 |
45 | return [];
46 | }
47 |
48 | /**
49 | * Creates a slice of the provided array with all elements matching the condition
50 | * removed from the end.
51 | *
52 | * If the provided iterator is an array, then an array will be returned.
53 | * Otherwise, a Generator will be returned. In this latter case, the entire
54 | * iterable will be buffered in memory. If the iterable contains many
55 | * elements, then this could cause memory issues.
56 | *
57 | * **Drop by Primitive Condition**
58 | *
59 | * ```php
60 | * __::dropRightWhile([1, 2, 3, 3], 3);
61 | * ```
62 | *
63 | * **Result**
64 | *
65 | * ```php
66 | * [1, 2]
67 | * ```
68 | *
69 | * **Drop by Callback**
70 | *
71 | * ```php
72 | * __::dropRightWhile([1, 2, 3, 4, 5], static function ($item) {
73 | * return $item > 3;
74 | * });
75 | * ```
76 | *
77 | * **Result**
78 | *
79 | * ```php
80 | * [1, 2, 3]
81 | * ```
82 | *
83 | * @param iterable $input An array of values.
84 | * @param \Closure|double|int|string|bool $condition Condition to drop by using either a primitive value or a callback
85 | *
86 | * @see drop
87 | * @see dropRight
88 | * @see dropRightWhile
89 | *
90 | * @since 0.2.3 added to Bottomline
91 | *
92 | * @return array|\Generator An array containing a subset of the input array with front items matching the condition
93 | * removed. If the provided iterable is not an array, then a Generator will be returned.
94 | */
95 | function dropRightWhile($input, $condition)
96 | {
97 | $comparison = is_callable($condition)
98 | ? $condition
99 | : static function ($item) use ($condition) {
100 | return $item === $condition;
101 | };
102 |
103 | if (is_array($input)) {
104 | return dropArrayRightWhile($input, $comparison);
105 | }
106 |
107 | return dropIteratorRightWhile($input, $comparison);
108 | }
109 |
--------------------------------------------------------------------------------
/src/__/collections/reduce.php:
--------------------------------------------------------------------------------
1 | 'IN', 'city' => 'Indianapolis', 'object' => 'School bus'],
32 | * ['state' => 'IN', 'city' => 'Indianapolis', 'object' => 'Manhole'],
33 | * ['state' => 'IN', 'city' => 'Plainfield', 'object' => 'Basketball'],
34 | * ['state' => 'CA', 'city' => 'San Diego', 'object' => 'Light bulb'],
35 | * ['state' => 'CA', 'city' => 'Mountain View', 'object' => 'Space pen'],
36 | * ];
37 | *
38 | * $iteratee = function ($accumulator, $value) {
39 | * if (isset($accumulator[$value['city']]))
40 | * $accumulator[$value['city']]++;
41 | * else
42 | * $accumulator[$value['city']] = 1;
43 | * return $accumulator;
44 | * };
45 | *
46 | * __::reduce($c, $iteratee, []);
47 | * ```
48 | *
49 | * **Result**
50 | *
51 | * ```
52 | * [
53 | * 'Indianapolis' => 2,
54 | * 'Plainfield' => 1,
55 | * 'San Diego' => 1,
56 | * 'Mountain View' => 1,
57 | * ]
58 | * ```
59 | *
60 | * **Usage: Objects**
61 | *
62 | * ```php
63 | * $object = new \stdClass();
64 | * $object->a = 1;
65 | * $object->b = 2;
66 | * $object->c = 1;
67 | *
68 | * __::reduce($object, function ($result, $value, $key) {
69 | * if (!isset($result[$value]))
70 | * $result[$value] = [];
71 | *
72 | * $result[$value][] = $key;
73 | *
74 | * return $result;
75 | * }, [])
76 | * ```
77 | *
78 | * **Result**
79 | *
80 | * ```
81 | * [
82 | * '1' => ['a', 'c'],
83 | * '2' => ['b']
84 | * ]
85 | * ```
86 | *
87 | * @since 0.2.0 added support for iterables
88 | *
89 | * @param iterable|\stdClass $collection The collection to iterate over.
90 | * @param \Closure $iteratee The function invoked per iteration.
91 | * @param array|\stdClass|mixed $accumulator The initial value.
92 | *
93 | * @return array|\stdClass|mixed Returns the accumulated value.
94 | */
95 | function reduce($collection, \Closure $iteratee, $accumulator = null)
96 | {
97 | if ($accumulator === null) {
98 | $accumulator = \__::first($collection);
99 | }
100 | \__::doForEach(
101 | $collection,
102 | function ($value, $key, $collection) use (&$accumulator, $iteratee) {
103 | $accumulator = $iteratee($accumulator, $value, $key, $collection);
104 | }
105 | );
106 | return $accumulator;
107 | }
108 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Guidelines
4 |
5 | :octocat: It is recommended to have an issue open for any work you take on and intend to submit as a pull request - it helps core members review your concept and direction early and is a good way to discuss what you're planning to do. If there is no issue tied to the Pull Request, please include a detailed description in the PR.
6 |
7 | :100: We are aiming for 100% code coverage. Please keep this in mind when you're submitting new code.
8 |
9 | :ok_hand: We use [PSR standard](http://www.php-fig.org/psr/) to keep our code sanitized and easy to follow.
10 |
11 | ## Development Environment
12 |
13 | The library itself requires PHP >= 5.5 but for development purposes, we require PHP >= 8.1. The library itself only needs `ext-mbstring` to handle special characters but for development, we also need `ext-json` available. Use [Composer v2](https://getcomposer.org/download/) for handling dependencies.
14 |
15 | For generating the documentation website, you'll also need Ruby with [`bundle`](https://bundler.io/).
16 |
17 | ## Development setup
18 |
19 | 1. Fork our repository
20 | 2. Clone your forked repository `git clone git@github.com:/bottomline.git`
21 | 3. Install development dependencies `composer install`
22 | 4. Run tests `composer run test`
23 |
24 | Also useful:
25 |
26 | * Run benchmarks: `composer run bench`
27 | * Format the code: `composer run cs-fix`
28 | * Generate the doc: `composer run doc`
29 | * Generate the doc website: `composer run site`
30 |
31 | ## Development checklist
32 |
33 | - Add or update phpDocs for the new function with a **Usage** and **Result** section ;
34 | - Always add tests for the code that you write, including edge cases ;
35 | - Place the new functions where they belong (collections, arrays, utilities, etc.) ;
36 | - Add `README.md` doc entry for the functions ;
37 | - Execute `php composer.phar run doc` script to automatically build an updated `load.php` ;
38 | - Update the benchmark `bench.php`, this helps us to validate the performance of the library ;
39 | - Update `CHANGELOG.md` with your changes.
40 |
41 | ## How to open a PR
42 |
43 | - Create a branch in your forked repository and push your code into it ;
44 | - Create a PR in [bottomline](https://github.com/maciejczyzewski/bottomline) that points to your forked branch ;
45 | - Add description of the PR (issue links, etc.).
46 |
47 | ### PR Title Recommendations
48 |
49 | - Squash commits into 1 commit before the PR is merged into master, this help reduces git tree and makes it easier to revert to a certain state. (can be multiple commits, but they have to be meaningful commits) ;
50 | - Open PR title starting with one of the following words:
51 | + `Fix` - for a bug fix ;
52 | + `New` - implemented a new feature ;
53 | + `Update` - for a backwards-compatible enhancement ;
54 | + `Breaking` - for a backwards-incompatible enhancement or feature ;
55 | + `Docs` - changes to documentation only ;
56 | - Use `LF` line endings.
57 |
58 | ## Release Checklist
59 |
60 | - [ ] Update `CHANGELOG.md` with the new version and date of release
61 | - [ ] Update the `bottomline v0.x.x` comment located in `src/__/__.php`
62 | - [ ] Ensure that any new functions added during this release have an `@since` tag in their phpDocs
63 |
--------------------------------------------------------------------------------
/tests/__/Collections/FindIndexTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(2, __::findIndex($data, "explain"));
14 | $this->assertEquals(-1, __::findIndex($data, "nonexistent"));
15 | }
16 |
17 | public function testWithAssociativeArray()
18 | {
19 | $data = [
20 | "table" => "trick",
21 | "pen" => "defend",
22 | "motherly" => "wide",
23 | "may" => "needle",
24 | "sweat" => "cake",
25 | "sword" => "defend",
26 | ];
27 |
28 | $this->assertEquals("pen", __::findIndex($data, "defend"));
29 | $this->assertEquals(-1, __::findIndex($data, "nonexistent"));
30 | }
31 |
32 | public function testWithAssociativeArrayMinusOne()
33 | {
34 | $data = [
35 | -1 => "minusOne",
36 | "-1" => "minusOneStr",
37 | "table" => "trick",
38 | "pen" => "defend",
39 | "motherly" => "wide",
40 | "may" => "needle",
41 | "sweat" => "cake",
42 | "sword" => "defend",
43 | ];
44 |
45 | $this->assertEquals("pen", __::findIndex($data, "defend"));
46 | $this->assertEquals(-1, __::findIndex($data, "nonexistent"));
47 | $this->assertEquals(-1, __::findIndex($data, "minusOne"));
48 | $this->assertEquals("-1", __::findIndex($data, "minusOneStr"));
49 | $this->assertEquals(null, __::findIndex($data, "minusOne", null));
50 | }
51 |
52 | public function testWithAssociativeArrayNull()
53 | {
54 | $data = [
55 | null => "nullValue",
56 | "table" => "trick",
57 | "pen" => "defend",
58 | "motherly" => "wide",
59 | "may" => "needle",
60 | "sweat" => "cake",
61 | "sword" => "defend",
62 | ];
63 |
64 | $this->assertEquals("pen", __::findIndex($data, "defend"));
65 | $this->assertEquals(-1, __::findIndex($data, "nonexistent"));
66 | $this->assertEquals(null, __::findIndex($data, "nullValue"));
67 | $this->assertEquals(null, __::findIndex($data, "nullValue", null));
68 | $this->assertEquals(null, __::findIndex($data, "nullValueNop", null));
69 | }
70 |
71 | public function testWithCallback()
72 | {
73 | $data = [
74 | "table" => (object)["name" => "trick"],
75 | "pen" => (object)["name" => "defend"],
76 | "motherly" => (object)["name" => "wide"],
77 | "may" => (object)["name" => "needle"],
78 | "sweat" => (object)["name" => "cake"],
79 | "sword" => (object)["name" => "defend"],
80 | ];
81 |
82 | $this->assertEquals("pen", __::findIndex($data, static function ($object) {
83 | return $object->name === "defend";
84 | }));
85 | $this->assertEquals(-1, __::findIndex($data, static function ($value) {
86 | return $value === "potato";
87 | }));
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/tests/__/Collections/ConcatDeepTest.php:
--------------------------------------------------------------------------------
1 | [
17 | ['color' => ['favorite' => 'red', 5], 3],
18 | [10, 'color' => ['favorite' => 'green', 'blue']],
19 | ],
20 | 'expected' => ['color' => ['favorite' => ['red', 'green'], 5, 'blue'], 3, 10],
21 | ],
22 | [
23 | 'sources' => [
24 | ['a' => 0],
25 | ['a' => 1, 'b' => 2],
26 | ['c' => 3, 'd' => 4],
27 | ],
28 | 'expected' => ['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4],
29 | ],
30 | [
31 | 'sources' => [
32 | (object)['color' => (object)['favorite' => 'red', 5]],
33 | (object)[10, 'color' => (object)['favorite' => 'green', 'blue']],
34 | ],
35 | 'expected' => (object)['color' => (object)['favorite' => ['red', 'green'], 5, 'blue'], 10],
36 | ],
37 | [
38 | 'sources' => [
39 | (object)['a' => 0],
40 | (object)['a' => 1, 'b' => 2],
41 | (object)['c' => 3, 'd' => 4],
42 | ],
43 | 'expected' => (object)['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4],
44 | ],
45 | [
46 | 'sources' => [
47 | new ArrayIterator(['a' => 0]),
48 | new ArrayIterator(['a' => 1, 'b' => 2]),
49 | new ArrayIterator(['c' => 3, 'd' => 4]),
50 | ],
51 | 'expected' => ['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4],
52 | ],
53 | [
54 | 'sources' => [
55 | new MockIteratorAggregate(['color' => (object)['favorite' => 'red', 5]]),
56 | (object)[10, 'color' => (object)['favorite' => 'green', 'blue']],
57 | ],
58 | 'expected' => ['color' => (object)['favorite' => ['red', 'green'], 5, 'blue'], 10],
59 | ],
60 | [
61 | 'sources' => [
62 | (object)[],
63 | call_user_func(function () {
64 | yield 'a' => 0;
65 | }),
66 | call_user_func(function () {
67 | yield 'a' => 1;
68 | yield 'b' => 2;
69 | }),
70 | new ArrayIterator(['c' => 3, 'd' => 4]),
71 | ],
72 | 'expected' => (object)['a' => [0, 1], 'b' => 2, 'c' => 3, 'd' => 4],
73 | ],
74 | ];
75 | }
76 |
77 | /**
78 | * @dataProvider provideConcatDeepCases
79 | *
80 | * @param array $sources
81 | * @param array $expected
82 | */
83 | public function testConcatDeep($sources, $expected)
84 | {
85 | $actual = call_user_func_array('__::concatDeep', $sources);
86 |
87 | $this->assertEquals($expected, $actual);
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/__/collections/set.php:
--------------------------------------------------------------------------------
1 | $key = $value;
12 | return $newObject;
13 | };
14 | $set_array = function ($array, $key, $value) {
15 | $array[$key] = $value;
16 | return $array;
17 | };
18 | $set_iterator = function ($array, $key, $value) {
19 | // We ensure we do not modify an iterator original values,
20 | // by making a copy to an array.
21 | $array = iterator_to_array($array);
22 | $array[$key] = $value;
23 | return $array;
24 | };
25 | $setter = $set_array;
26 | if (\__::isObject($collection) && !($collection instanceof \ArrayAccess)) {
27 | $setter = $set_object;
28 | }
29 | if ($collection instanceof \Iterator) {
30 | $setter = $set_iterator;
31 | }
32 | return call_user_func_array($setter, [$collection, $key, $value]);
33 | }
34 |
35 | /**
36 | * Return a new collection with the item set at index to given value. Index can
37 | * be a path of nested indexes.
38 | *
39 | * - If `$collection` is an object that implements the ArrayAccess interface,
40 | * this function will treat it as an array.
41 | * - If a portion of path doesn't exist, it's created. Arrays are created for
42 | * missing index in an array; objects are created for missing property in an
43 | * object.
44 | *
45 | * This function throws an `\Exception` if the path consists of a non-collection.
46 | *
47 | * **Usage**
48 | *
49 | * ```php
50 | * __::set(['foo' => ['bar' => 'ter']], 'foo.baz.ber', 'fer');
51 | * ```
52 | *
53 | * **Result**
54 | *
55 | * ```
56 | * ['foo' => ['bar' => 'ter', 'baz' => ['ber' => 'fer']]]
57 | * ```
58 | *
59 | * @param array|iterable|object $collection Collection of values
60 | * @param string $path Key or index. Supports dot notation
61 | * @param mixed $value The value to set at position $key
62 | *
63 | * @throws \Exception if the path consists of a non collection
64 | *
65 | * @return array|object the new collection with the item set
66 | */
67 | function set($collection, $path, $value = null)
68 | {
69 | if ($path === null) {
70 | return $collection;
71 | }
72 |
73 | $portions = \__::split($path, \__::DOT_NOTATION_DELIMITER, 2);
74 | $key = $portions[0];
75 |
76 | if (count($portions) === 1) {
77 | return _universal_set($collection, $key, $value);
78 | }
79 | // Here we manage the case where the portion of the path points to nothing,
80 | // or to a value that does not match the type of the source collection
81 | // (e.g. the path portion 'foo.bar' points to an integer value, while we
82 | // want to set a string at 'foo.bar.fun'. We first set an object or array
83 | // - following the current collection type - to 'for.bar' before setting
84 | // 'foo.bar.fun' to the specified value).
85 | if (
86 | !\__::has($collection, $key)
87 | || (\__::isObject($collection) && !\__::isObject(\__::get($collection, $key)))
88 | || (\__::isArray($collection) && !\__::isArray(\__::get($collection, $key)))
89 | ) {
90 | $collection = _universal_set($collection, $key, (\__::isObject($collection) && !($collection instanceof \ArrayAccess)) ? new \stdClass : []);
91 | }
92 | return _universal_set($collection, $key, set(\__::get($collection, $key), $portions[1], $value));
93 | }
94 |
--------------------------------------------------------------------------------
/tests/__/Arrays/DropWhileTest.php:
--------------------------------------------------------------------------------
1 | assertEquals([3, 4, 5], $out1);
27 | $this->assertEquals([1, 2, 3, 4, 5], $out2);
28 | $this->assertEquals([], $out3);
29 | }
30 |
31 | public function testDropWhileWithPrimitive()
32 | {
33 | $arr = [1, 1, 2, 3, 4, 5];
34 |
35 | $actual = __::dropWhile($arr, 1);
36 |
37 | $this->assertEquals([2, 3, 4, 5], $actual);
38 | }
39 |
40 | public function testDropWhileWithIterator()
41 | {
42 | $arr = [1, 2, 3, 4, 5];
43 | $arrItr = new ArrayIterator($arr);
44 |
45 | $f = static function ($item) {
46 | return $item < 3;
47 | };
48 | $expected = __::dropWhile($arr, $f);
49 | $actual = __::dropWhile($arrItr, $f);
50 | $itrSize = 0;
51 |
52 | foreach ($actual as $i => $item) {
53 | ++$itrSize;
54 | $this->assertEquals($item, $expected[$i]);
55 | }
56 |
57 | $this->assertEquals(count($expected), $itrSize);
58 | }
59 |
60 | public function testDropWhileWithIteratorAggregate()
61 | {
62 | $arr = [1, 2, 3, 4, 5];
63 | $arrItr = new MockIteratorAggregate($arr);
64 |
65 | $f = static function ($item) {
66 | return $item < 3;
67 | };
68 | $expected = __::dropWhile($arr, $f);
69 | $actual = __::dropWhile($arrItr, $f);
70 | $itrSize = 0;
71 |
72 | foreach ($actual as $i => $item) {
73 | ++$itrSize;
74 | $this->assertEquals($item, $expected[$i]);
75 | }
76 |
77 | $this->assertEquals(count($expected), $itrSize);
78 | }
79 |
80 | public function testDropWhileWithGenerator()
81 | {
82 | $arr = [1, 2, 3, 4, 5];
83 | $generator = call_user_func(function () use ($arr) {
84 | foreach ($arr as $item) {
85 | yield $item;
86 | }
87 | });
88 |
89 | $this->assertInstanceOf(\Generator::class, $generator);
90 |
91 | $f = static function ($item) {
92 | return $item < 3;
93 | };
94 | $expected = __::dropWhile($arr, $f);
95 | $actual = __::dropWhile($generator, $f);
96 | $itrSize = 0;
97 |
98 | foreach ($actual as $i => $item) {
99 | ++$itrSize;
100 | $this->assertEquals($item, $expected[$i]);
101 | }
102 |
103 | $this->assertEquals(count($expected), $itrSize);
104 | }
105 |
106 | public function testDropWhileWithMatchAfterDroppingEnds()
107 | {
108 | $arr = [1, 1, 2, 1, 1];
109 | $arrItr = new ArrayIterator($arr);
110 |
111 | $expected = __::dropWhile($arr, 1);
112 | $actual = __::dropWhile($arrItr, 1);
113 | $itrSize = 0;
114 |
115 | foreach ($actual as $i => $item) {
116 | ++$itrSize;
117 | $this->assertEquals($item, $expected[$i]);
118 | }
119 |
120 | $this->assertEquals(count($expected), $itrSize);
121 | }
122 | }
123 |
--------------------------------------------------------------------------------