├── .gitignore ├── tests ├── bootstrap.php ├── Fixture │ ├── Collection │ │ └── TestCollection.php │ └── Mutator │ │ └── ChefMutator.php ├── Dummy.php ├── RegistryTest.php ├── CollectionCopyTest.php ├── CollectionTest.php ├── FunctionsTest.php └── UnderscoreTest.php ├── src ├── Accessor.php ├── Initializer.php ├── Mutator │ ├── CloneMutator.php │ ├── CompactMutator.php │ ├── KeysMutator.php │ ├── ValuesMutator.php │ ├── ThruMutator.php │ ├── LastMutator.php │ ├── HeadMutator.php │ ├── TailMutator.php │ ├── InitialMutator.php │ ├── UniqMutator.php │ ├── FilterMutator.php │ ├── MergeMutator.php │ ├── FlattenMutator.php │ ├── InvokeMutator.php │ ├── RejectMutator.php │ ├── MapMutator.php │ ├── IntersectionMutator.php │ ├── GroupByMutator.php │ ├── TapMutator.php │ ├── WithoutMutator.php │ ├── DefaultsMutator.php │ ├── ExtendMutator.php │ ├── ShuffleMutator.php │ ├── ZipMutator.php │ ├── PluckMutator.php │ ├── WhereMutator.php │ ├── DifferenceMutator.php │ └── SortByMutator.php ├── Accessor │ ├── ValueAccessor.php │ ├── CollectionAccessor.php │ ├── ToArrayAccessor.php │ ├── MaxAccessor.php │ ├── MinAccessor.php │ ├── HasAccessor.php │ ├── SizeAccessor.php │ ├── IndexOfAccessor.php │ ├── LastIndexOfAccessor.php │ ├── ContainsAccessor.php │ ├── ReduceAccessor.php │ ├── AllAccessor.php │ ├── ReduceRightAccessor.php │ ├── FindAccessor.php │ └── AnyAccessor.php ├── Mutator.php ├── Initializer │ ├── FromInitializer.php │ ├── TimesInitializer.php │ └── RangeInitializer.php ├── Collection.php ├── Registry.php ├── Underscore.php └── Functions.php ├── .travis.yml ├── .scrutinizer.yml ├── composer.json ├── LICENSE ├── phpunit.xml.dist └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | composer.lock 4 | /tests/coverage/ 5 | /build/ 6 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | value(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Accessor/CollectionAccessor.php: -------------------------------------------------------------------------------- 1 | toArray(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - 7.0 8 | - hhvm 9 | 10 | matrix: 11 | fast_finish: true 12 | 13 | install: 14 | - composer install --prefer-source 15 | 16 | after_script: 17 | - bash -c '[[ -f "build/clover.xml" ]] && wget https://scrutinizer-ci.com/ocular.phar' 18 | - bash -c '[[ -f "build/clover.xml" ]] && php ocular.phar code-coverage:upload --format=php-clover build/clover.xml' 19 | 20 | sudo: false 21 | -------------------------------------------------------------------------------- /src/Accessor/MaxAccessor.php: -------------------------------------------------------------------------------- 1 | toArray()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Accessor/MinAccessor.php: -------------------------------------------------------------------------------- 1 | toArray()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Accessor/HasAccessor.php: -------------------------------------------------------------------------------- 1 | offsetExists($key); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mutator.php: -------------------------------------------------------------------------------- 1 | exchangeArray($values); 19 | 20 | return $collection; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Accessor/SizeAccessor.php: -------------------------------------------------------------------------------- 1 | count(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Accessor/IndexOfAccessor.php: -------------------------------------------------------------------------------- 1 | toArray(); 19 | return array_search($value, $values, true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mutator/CompactMutator.php: -------------------------------------------------------------------------------- 1 | toArray()); 19 | 20 | return $this->copyCollectionWith($collection, $values); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mutator/ValuesMutator.php: -------------------------------------------------------------------------------- 1 | toArray()); 19 | 20 | return $this->copyCollectionWith($collection, $values); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Accessor/LastIndexOfAccessor.php: -------------------------------------------------------------------------------- 1 | toArray(), true); 19 | return array_search($value, $values, true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mutator/ThruMutator.php: -------------------------------------------------------------------------------- 1 | toArray()); 20 | 21 | return $this->copyCollectionWith($collection, $values); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mutator/LastMutator.php: -------------------------------------------------------------------------------- 1 | toArray(), -$count); 22 | 23 | return $this->copyCollectionWith($collection, $values); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mutator/HeadMutator.php: -------------------------------------------------------------------------------- 1 | toArray(), 0, $count); 22 | 23 | return $this->copyCollectionWith($collection, $values); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mutator/TailMutator.php: -------------------------------------------------------------------------------- 1 | toArray(), $count); 22 | 23 | return $this->copyCollectionWith($collection, $values); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - php 3 | 4 | tools: 5 | external_code_coverage: 6 | timeout: 600 7 | 8 | php_code_sniffer: true 9 | 10 | php_cpd: true 11 | 12 | php_cs_fixer: true 13 | 14 | php_mess_detector: true 15 | 16 | php_pdepend: true 17 | 18 | php_loc: true 19 | 20 | php_analyzer: 21 | config: 22 | doc_comment_fixes: 23 | enabled: true 24 | 25 | sensiolabs_security_checker: true 26 | 27 | changetracking: 28 | bug_patterns: ["\bfix(?:es|ed)?\b"] 29 | feature_patterns: ["\badd(?:s|ed)?\b", "\bimplement(?:s|ed)?\b"] 30 | -------------------------------------------------------------------------------- /src/Mutator/InitialMutator.php: -------------------------------------------------------------------------------- 1 | toArray(), 0, -$count); 22 | 23 | return $this->copyCollectionWith($collection, $values); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mutator/UniqMutator.php: -------------------------------------------------------------------------------- 1 | $v) { 20 | if (!in_array($v, $seen, true)) { 21 | $seen[$k] = $v; 22 | } 23 | } 24 | 25 | return $this->copyCollectionWith($collection, $seen); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mutator/FilterMutator.php: -------------------------------------------------------------------------------- 1 | copyCollectionWith($collection, array_filter( 20 | $collection->toArray(), 21 | $iterator 22 | )); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mutator/MergeMutator.php: -------------------------------------------------------------------------------- 1 | $value) { 22 | $collection[$key] = $value; 23 | } 24 | 25 | return $collection; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Initializer/FromInitializer.php: -------------------------------------------------------------------------------- 1 | copyCollectionWith($collection, $newCollection); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mutator/InvokeMutator.php: -------------------------------------------------------------------------------- 1 | $v) { 23 | call_user_func($iterator, $v, $k, $collection); 24 | } 25 | 26 | return $collection; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Mutator/RejectMutator.php: -------------------------------------------------------------------------------- 1 | $v) { 24 | $collection[$k] = call_user_func($iterator, $v, $k, $collection); 25 | } 26 | 27 | return $collection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mutator/IntersectionMutator.php: -------------------------------------------------------------------------------- 1 | exchangeArray(array_intersect( 26 | $collection->getArrayCopy(), 27 | $values 28 | )); 29 | 30 | return $collection; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "im0rtality/underscore", 3 | "description": "Functional programming library for PHP", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Laurynas Veržukauskas", 8 | "homepage": "https://github.com/im0rtality" 9 | }, 10 | { 11 | "name": "Woody Gilk", 12 | "email": "woody.gilk@gmail.com" 13 | } 14 | ], 15 | "autoload": { 16 | "psr-4": { 17 | "Underscore\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Underscore\\Test\\": "tests/" 23 | } 24 | }, 25 | "require": { 26 | "php": ">=5.4" 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "~4.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mutator/GroupByMutator.php: -------------------------------------------------------------------------------- 1 | copyCollectionWith($collection, $newCollection); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Accessor/ReduceAccessor.php: -------------------------------------------------------------------------------- 1 | toArray()); 26 | 27 | return $collection; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Accessor/AllAccessor.php: -------------------------------------------------------------------------------- 1 | getIteratorReversed() as $value) { 25 | $initial = call_user_func($iterator, $initial, $value); 26 | } 27 | 28 | return $initial; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Accessor/FindAccessor.php: -------------------------------------------------------------------------------- 1 | $v) { 25 | if (call_user_func($iterator, $v, $k, $collection)) { 26 | $found = true; 27 | break; 28 | } 29 | } 30 | 31 | return $found; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Initializer/TimesInitializer.php: -------------------------------------------------------------------------------- 1 | $value) { 29 | if (!isset($collection[$key])) { 30 | $collection[$key] = $value; 31 | } 32 | } 33 | } 34 | 35 | return $collection; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mutator/ExtendMutator.php: -------------------------------------------------------------------------------- 1 | $value) { 30 | $collection[$key] = $value; 31 | } 32 | } 33 | 34 | return $collection; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Mutator/ShuffleMutator.php: -------------------------------------------------------------------------------- 1 | shuffleAssoc($collection->toArray()); 19 | 20 | return $this->copyCollectionWith($collection, $values); 21 | } 22 | 23 | /** 24 | * Shuffle an array while preserving keys. 25 | * 26 | * @param array $values 27 | * 28 | * @return array 29 | */ 30 | private function shuffleAssoc(array $values) 31 | { 32 | $keys = array_keys($values); 33 | shuffle($keys); 34 | 35 | $output = []; 36 | foreach ($keys as $key) { 37 | $output[$key] = $values[$key]; 38 | } 39 | 40 | return $output; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Mutator/ZipMutator.php: -------------------------------------------------------------------------------- 1 | toArray(); 21 | $keys = call_user_func(new ValuesMutator, new Collection($keys))->toArray(); 22 | 23 | if (count($values) !== count($keys)) { 24 | throw new \LogicException('Keys and values count must match'); 25 | } 26 | 27 | $newCollection = []; 28 | 29 | foreach ($values as $index => $value) { 30 | $newCollection[$keys[$index]] = $value; 31 | } 32 | 33 | return $this->copyCollectionWith($collection, $newCollection); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | getIteratorClass(); 25 | $collection = array_reverse($this->getArrayCopy()); 26 | 27 | return new $iterator($collection); 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function value() 34 | { 35 | return $this->toArray(); 36 | } 37 | 38 | /** 39 | * @return mixed[] 40 | */ 41 | public function toArray() 42 | { 43 | return $this->getArrayCopy(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Mutator/PluckMutator.php: -------------------------------------------------------------------------------- 1 | {$key}) ? $value->{$key} : null; 27 | } 28 | } else { 29 | return isset($value[$key]) ? $value[$key] : null; 30 | } 31 | }; 32 | 33 | $map = new MapMutator(); 34 | 35 | return $map($collection, $iterator); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mutator/WhereMutator.php: -------------------------------------------------------------------------------- 1 | $value) { 26 | if (empty($item[$key]) 27 | || ($strict && $item[$key] !== $value) 28 | || (!$strict && $item[$key] != $value) 29 | ) { 30 | return false; 31 | } 32 | } 33 | 34 | return true; 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Laurynas Veržukauskas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./src 21 | 22 | ./tests/Underscore/Test/Fixture 23 | 24 | 25 | 26 | 27 | 28 | 29 | ./tests/ 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Mutator/DifferenceMutator.php: -------------------------------------------------------------------------------- 1 | getArrayCopy(); 26 | 27 | if ($this->isAssoc($current)) { 28 | $values = array_diff_assoc($current, $values); 29 | } else { 30 | $values = array_diff($current, $values); 31 | } 32 | 33 | $collection->exchangeArray($values); 34 | 35 | return $collection; 36 | } 37 | 38 | /** 39 | * Check if an array is associative. 40 | * 41 | * @param array $values 42 | * 43 | * @return boolean 44 | */ 45 | private function isAssoc(array $values) 46 | { 47 | $keys = array_keys($values); 48 | return $keys !== array_keys($keys); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Mutator/SortByMutator.php: -------------------------------------------------------------------------------- 1 | value(); 33 | 34 | $mapFunc = function ($key) use ($val) { 35 | return $val[$key]; 36 | }; 37 | 38 | $collection = call_user_func(new KeysMutator(), $collection); 39 | $collection = call_user_func(new ThruMutator(), $collection, $sortFunc); 40 | $collection = call_user_func(new MapMutator(), $collection, $mapFunc); 41 | $collection = call_user_func(new FlattenMutator(), $collection); 42 | 43 | return $collection; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/RegistryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf('Underscore\Accessor\ValueAccessor', $registry->instance('value')); 14 | $this->assertInstanceOf('Underscore\Initializer\FromInitializer', $registry->instance('from')); 15 | $this->assertInstanceOf('Underscore\Mutator\LastMutator', $registry->instance('last')); 16 | } 17 | 18 | public function testConstructorAlias() 19 | { 20 | $registry = new Registry([ 21 | 'last' => 'Underscore\Test\Fixture\Mutator\ChefMutator', 22 | ]); 23 | 24 | $this->assertInstanceOf('Underscore\Test\Fixture\Mutator\ChefMutator', $registry->instance('last')); 25 | } 26 | 27 | public function testAliasSpec() 28 | { 29 | $registry = new Registry(); 30 | 31 | $this->assertInstanceOf('Underscore\Mutator\LastMutator', $registry->instance('last')); 32 | 33 | $registry->alias('last', 'Underscore\Test\Fixture\Mutator\ChefMutator'); 34 | 35 | $this->assertInstanceOf('Underscore\Test\Fixture\Mutator\ChefMutator', $registry->instance('last')); 36 | } 37 | 38 | public function testAliasConcrete() 39 | { 40 | $registry = new Registry(); 41 | 42 | $accessor = function ($collection) { 43 | }; 44 | 45 | $registry->alias('fin', $accessor); 46 | 47 | $this->assertSame($accessor, $registry->instance('fin')); 48 | } 49 | 50 | /** 51 | * @expectedException BadMethodCallException 52 | */ 53 | public function testMissingInstance() 54 | { 55 | $registry = new Registry(); 56 | 57 | $registry->instance('invalid'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Underscore 2 | 3 | [![Build Status](https://travis-ci.org/Im0rtality/Underscore.svg?branch=master)](https://travis-ci.org/Im0rtality/Underscore) 4 | [![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/Im0rtality/Underscore/badges/quality-score.png?s=a360715f49ea4bba225b7991146981ca80b61337)](https://scrutinizer-ci.com/g/Im0rtality/Underscore/) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/Im0rtality/Underscore/badges/coverage.png?s=e9c5669f300b4d8cb96d0fa8645119ab546fbd62)](https://scrutinizer-ci.com/g/Im0rtality/Underscore/) 6 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d7ef44af-707d-4638-b91e-8fd043dc99e0/mini.png)](https://insight.sensiolabs.com/projects/d7ef44af-707d-4638-b91e-8fd043dc99e0) 7 | 8 | Functional programming library for PHP 9 | 10 | # Code example 11 | 12 | Library aims to be as easy to use as possible. Here is example of doing some not-so-meaningful operations to show off: 13 | 14 | ```php 15 | Underscore::from([1,2,3,4,5]) 16 | // convert array format 17 | ->map(function($num) { return ['number' => $num];}) 18 | // filter out odd elements 19 | ->filter(function($item) { return ($item['number'] % 2) == 0;}) 20 | // vardump elements 21 | ->invoke(function($item) { var_dump($item);}) 22 | // changed my mind, I only want numbers 23 | ->pick('number') 24 | // add numbers to 1000 25 | ->reduce(function($sum, $num) { $sum += $num; return $sum; }, 1000) 26 | // take result 27 | ->value(); 28 | // 1006 29 | ``` 30 | 31 | # Motivation 32 | 33 | Originaly I needed functional programming magic for other project, so had to pick one lib or write my own. 34 | 35 | There is several PHP ports of UnderscoreJS, however none of those fit my requirements (nice code, easy to write, standardized): 36 | - [brianhaveri/Underscore.php](https://github.com/brianhaveri/Underscore.php) - not maintained, messy code 37 | - [Anahkiasen/underscore-php](https://github.com/Anahkiasen/underscore-php) - Laravel4 package => incompatible with PSR-2 38 | 39 | # Installation 40 | 41 | Via composer: 42 | 43 | $ composer require im0rtality/underscore:* 44 | 45 | Composer docs recommend to use specific version. You can look them up in [Releases](https://github.com/Im0rtality/Underscore/releases). 46 | 47 | # Documentation 48 | 49 | See [wiki](https://github.com/Im0rtality/Underscore/wiki/Intro) 50 | 51 | # Tests 52 | 53 | Tests generate coverage reports in clover.xml format 54 | 55 | $ vendor/bin/phpunit 56 | 57 | # License 58 | 59 | MIT License: You can do whatever you want as long as you include the original copyright. 60 | -------------------------------------------------------------------------------- /tests/CollectionCopyTest.php: -------------------------------------------------------------------------------- 1 | assertNotSame($collection, $modified); 79 | 80 | // But should be the same type 81 | $this->assertInstanceOf(get_class($collection), $modified); 82 | } 83 | 84 | public function testWrapAndUnwrap() 85 | { 86 | $collection = new TestCollection([ 87 | 'a', 'b', 'c' 88 | ]); 89 | 90 | // Wrap and unwrap the collection 91 | $unwrapped = Underscore::from($collection)->collection(); 92 | 93 | // The collection should a clone 94 | $this->assertNotSame($collection, $unwrapped); 95 | 96 | // But should be the same type 97 | $this->assertInstanceOf(get_class($collection), $unwrapped); 98 | 99 | // And have the same values 100 | $this->assertSame($collection->toArray(), $unwrapped->toArray()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Registry.php: -------------------------------------------------------------------------------- 1 | alias('keys', 'Acme\KeysMutator'); 13 | * 14 | * New methods can be added in the same way: 15 | * 16 | * $registry->alias('fancy', 'Acme\FancyInitializer'); 17 | * 18 | * Every alias is a singleton and will only be created once! If your callable 19 | * requires construction, alias a fully constructed object instead: 20 | * 21 | * $registry->alias('fancy', new FancyInitializer($parameters)); 22 | * 23 | * NOTE: Mutators, accessors, and initializers all use the same registery and 24 | * differ only in how they are used internally by Underscore. 25 | */ 26 | class Registry 27 | { 28 | /** 29 | * @var array Classes aliased by name of method. 30 | */ 31 | private $aliases = []; 32 | 33 | /** 34 | * @var array Callable instances aliased by name of method. 35 | */ 36 | private $instances = []; 37 | 38 | /** 39 | * @param array $aliases 40 | */ 41 | public function __construct(array $aliases = []) 42 | { 43 | $this->aliases = $aliases; 44 | } 45 | 46 | /** 47 | * Get the callable for an Underscore method alias. 48 | * 49 | * @param string $name 50 | * @return callable 51 | */ 52 | public function instance($name) 53 | { 54 | if (empty($this->instances[$name])) { 55 | if (empty($this->aliases[$name])) { 56 | $this->aliasDefault($name); 57 | } 58 | $this->instances[$name] = new $this->aliases[$name]; 59 | } 60 | 61 | return $this->instances[$name]; 62 | } 63 | 64 | /** 65 | * Alias method name to a callable. 66 | * 67 | * @param string $name 68 | * @param callable $spec 69 | * @return void 70 | */ 71 | public function alias($name, $spec) 72 | { 73 | if (is_callable($spec)) { 74 | $this->instances[$name] = $spec; 75 | } else { 76 | $this->aliases[$name] = $spec; 77 | 78 | // Ensure that the new alias will be used when called. 79 | unset($this->instances[$name]); 80 | } 81 | } 82 | 83 | /** 84 | * Define a default mutator, accessor, or initializer for a method name. 85 | * 86 | * @throws \BadMethodCallException If no default can be located. 87 | * @param string $name 88 | * @return void 89 | */ 90 | private function aliasDefault($name) 91 | { 92 | $spec = sprintf('\Underscore\Mutator\%sMutator', ucfirst($name)); 93 | 94 | if (!class_exists($spec)) { 95 | $spec = sprintf('\Underscore\Accessor\%sAccessor', ucfirst($name)); 96 | } 97 | 98 | if (!class_exists($spec)) { 99 | $spec = sprintf('\Underscore\Initializer\%sInitializer', ucfirst($name)); 100 | } 101 | 102 | if (!class_exists($spec)) { 103 | throw new \BadMethodCallException(sprintf('Unknown method Underscore->%s()', $name)); 104 | } 105 | 106 | $this->aliases[$name] = $spec; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/CollectionTest.php: -------------------------------------------------------------------------------- 1 | assertSame(2, $collection->count()); 34 | // ArrayAccess test 35 | 36 | // isset 37 | $this->assertTrue(isset($collection['name'])); 38 | $this->assertFalse(isset($collection['precious'])); 39 | $this->assertFalse(isset($collection['foo'])); 40 | // get 41 | $this->assertSame('dummy', $collection['name']); 42 | $this->assertSame(42, $collection['answer']); 43 | // set 44 | $collection['foo'] = 'bar'; 45 | $this->assertTrue(isset($collection['foo'])); 46 | $this->assertSame('bar', $collection['foo']); 47 | $this->assertSame(3, $collection->count()); 48 | // unset 49 | unset($collection['foo']); 50 | $this->assertFalse(isset($collection['foo'])); 51 | $this->assertSame(2, $collection->count()); 52 | 53 | // Iterator test 54 | $this->assertInstanceOf('\Traversable', $collection->getIterator()); 55 | 56 | $out = []; 57 | foreach ($collection as $key => $value) { 58 | $out[$key] = $value; 59 | } 60 | $this->assertSame(['name' => 'dummy', 'answer' => 42], $out); 61 | 62 | // Reversed iterator test 63 | $this->assertInstanceOf('\Traversable', $collection->getIteratorReversed()); 64 | 65 | $out = []; 66 | foreach ($collection->getIteratorReversed() as $key => $value) { 67 | $out[$key] = $value; 68 | } 69 | $this->assertSame(['answer' => 42, 'name' => 'dummy'], $out); 70 | 71 | // toArray & value 72 | $this->assertSame(['name' => 'dummy', 'answer' => 42], $collection->toArray()); 73 | $this->assertSame($collection->toArray(), $collection->value()); 74 | 75 | $collection['foo'] = 'baz'; 76 | // here we have one extra key-value pair 77 | $this->assertSame(['name' => 'dummy', 'answer' => 42, 'foo' => 'baz'], $collection->toArray()); 78 | 79 | // collection wrapping 80 | $wrapped = new Collection($collection); 81 | $this->assertSame($collection->toArray(), $wrapped->toArray()); 82 | } 83 | 84 | /** 85 | * @dataProvider getTestCollectionData 86 | */ 87 | public function testSetValueOnNullKey($item) 88 | { 89 | $collection = new Collection($item); 90 | /** @var $collection Collection */ 91 | 92 | $this->assertEquals(2, $collection->count()); 93 | 94 | $collection[] = 'foo'; 95 | $this->assertEquals(3, $collection->count()); 96 | 97 | // test finding next free numeric key 98 | $collection[] = 'foo'; 99 | $this->assertEquals(4, $collection->count()); 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function getTestCloneData() 106 | { 107 | $json = '{"foo":"bar", "baz":"qux"}'; 108 | $out = []; 109 | // case #0 110 | $out[] = [ 111 | json_decode($json, true), 112 | ]; 113 | 114 | return $out; 115 | } 116 | 117 | /** 118 | * @dataProvider getTestCloneData 119 | */ 120 | public function testClone($clonee) 121 | { 122 | $result = new Collection($clonee); 123 | 124 | $clone = clone $result; 125 | 126 | $value = $clone->value(); 127 | 128 | $this->assertEquals($clonee, $value); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/FunctionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, $counter); 26 | } 27 | 28 | public function testNop() 29 | { 30 | $nop = Functions::nop(); 31 | 32 | $ref = new \stdClass(); 33 | 34 | $this->assertSame($ref, $nop($ref)); 35 | } 36 | 37 | public function testOnce() 38 | { 39 | $counter = 0; 40 | 41 | $initialize = Functions::once(function () use (&$counter) { 42 | return ++$counter; 43 | }); 44 | 45 | $this->assertEquals(0, $counter); 46 | 47 | $initialize(); // $counter = 1 48 | 49 | $this->assertEquals(1, $initialize()); 50 | 51 | $another = Functions::once(function () use (&$counter) { 52 | return ++$counter; 53 | }); 54 | 55 | $another(); // $counter = 2 56 | 57 | $this->assertEquals(2, $another()); 58 | } 59 | 60 | public function testPartial() 61 | { 62 | $const = Functions::p(); 63 | $this->assertSame('placeholder for partial', $const()); 64 | 65 | $subtract = function ($a, $b) { 66 | return $b - $a; 67 | }; 68 | 69 | $sub5 = Functions::partial($subtract, 5); 70 | 71 | $this->assertEquals(15, $sub5(20)); 72 | 73 | $subFrom20 = Functions::partial($subtract, Functions::p(), 20); 74 | 75 | $this->assertEquals(15, $subFrom20(5)); 76 | } 77 | 78 | public function testCompose() 79 | { 80 | $greet = function ($name) { 81 | return "hi: $name"; 82 | }; 83 | 84 | $exclaim = function ($statement) { 85 | return strtoupper($statement) . '!'; 86 | }; 87 | 88 | $welcome = Functions::compose($greet, $exclaim); 89 | 90 | $this->assertEquals('hi: MOE!', $welcome('moe')); 91 | } 92 | 93 | public function testWrap() 94 | { 95 | $hello = function ($name) { 96 | return "hello: $name"; 97 | }; 98 | 99 | $moe = Functions::wrap($hello, function ($func) { 100 | return 'before, ' . $func('moe') . ', after'; 101 | }); 102 | 103 | $this->assertEquals('before, hello: moe, after', $moe()); 104 | 105 | $anon = Functions::wrap($hello, function ($func, $name) { 106 | return 'before, ' . $func($name) . ', after'; 107 | }); 108 | 109 | $this->assertEquals('before, hello: sue, after', $anon('sue')); 110 | } 111 | 112 | public function testThrottle() 113 | { 114 | $count = 0; 115 | 116 | $ping = Functions::throttle(function () use (&$count) { 117 | return ++$count; 118 | }, 100); 119 | 120 | // Creating the throttle calls it once inititally 121 | $this->assertEquals(1, $count); 122 | 123 | $ping(); 124 | $ping(); 125 | $ping(); 126 | 127 | // Should still not be called 128 | $this->assertEquals(1, $count); 129 | 130 | usleep(100 * 1000); // convert to ms 131 | 132 | $ping(); 133 | 134 | $this->assertEquals(2, $count); 135 | 136 | $ping = Functions::throttle(function () use (&$count) { 137 | return ++$count; 138 | }, 100, ['leading' => false]); 139 | 140 | // Disabled initial call 141 | $this->assertEquals(2, $count); 142 | 143 | usleep(100 * 1000); // convert to ms 144 | 145 | $ping(); 146 | 147 | $this->assertEquals(3, $count); 148 | } 149 | 150 | public function testAfter() 151 | { 152 | $count = 0; 153 | 154 | $finished = Functions::after(3, function () use (&$count) { 155 | return ++$count; 156 | }); 157 | 158 | $finished(); 159 | $finished(); 160 | 161 | $this->assertEquals(0, $count); 162 | 163 | $finished(); // $count = 1 164 | $finished(); // $count = 2 165 | 166 | $this->assertEquals(2, $count); 167 | } 168 | 169 | public function testBefore() 170 | { 171 | $salary = 0; 172 | 173 | $askForRaise = function () use (&$salary) { 174 | return ++$salary; 175 | }; 176 | 177 | $monthlyMeeting = Functions::before(3, $askForRaise); 178 | 179 | $monthlyMeeting(); 180 | $monthlyMeeting(); // $salary = 2 181 | $monthlyMeeting(); // memoized 182 | 183 | $this->assertEquals(2, $salary); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Underscore.php: -------------------------------------------------------------------------------- 1 | $function) { 105 | static::getRegistry()->alias($name, $function); 106 | } 107 | } 108 | 109 | /** 110 | * @param Collection $wrapped 111 | */ 112 | public function __construct(Collection $wrapped = null) 113 | { 114 | $this->wrapped = $wrapped; 115 | } 116 | 117 | /** 118 | * @param string $method 119 | * @param array $args 120 | * @throws \BadMethodCallException 121 | * @return $this 122 | */ 123 | public function __call($method, $args) 124 | { 125 | return $this->executePayload(static::getRegistry()->instance($method), $args); 126 | } 127 | 128 | /** 129 | * @param string $method 130 | * @param array $args 131 | * @return $this 132 | */ 133 | public static function __callStatic($method, $args) 134 | { 135 | return call_user_func_array(static::getRegistry()->instance($method), $args); 136 | } 137 | 138 | /** 139 | * Payload is either Mutator or Accessor. Both are supposed to be callable. 140 | * 141 | * @param callable $payload 142 | * @param array $args 143 | * @return $this|mixed 144 | */ 145 | protected function executePayload($payload, $args) 146 | { 147 | array_unshift($args, $this->wrapped); 148 | 149 | if ($payload instanceof Mutator) { 150 | /** @var $payload callable */ 151 | $this->wrapped = call_user_func_array($payload, $args); 152 | 153 | return $this; 154 | } else { 155 | return call_user_func_array($payload, $args); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Functions.php: -------------------------------------------------------------------------------- 1 | false`. 176 | * 177 | * NOTE: Does not support the `trailing` option because there is no timeout 178 | * functionality in PHP (without using threads). 179 | * 180 | * NOTE: No arguments are passed to the function on the leading edge call! 181 | * 182 | * @param callable $function 183 | * @param integer $wait 184 | * @param array $options 185 | * @return \Closure 186 | */ 187 | public static function throttle($function, $wait, array $options = []) 188 | { 189 | $options += [ 190 | 'leading' => true, 191 | ]; // @codeCoverageIgnore 192 | 193 | $previous = 0; 194 | 195 | $callback = function () use ($function, $wait, &$previous) { 196 | $now = floor(microtime(true) * 1000); // convert float to integer 197 | 198 | if (($wait - ($now - $previous)) <= 0) { 199 | $args = func_get_args(); 200 | call_user_func_array($function, $args); 201 | $previous = $now; 202 | } 203 | }; 204 | 205 | if ($options['leading'] !== false) { 206 | $callback(); 207 | } 208 | 209 | return $callback; 210 | } 211 | 212 | /** 213 | * Creates a version of the function that will only be run after first being called count times. 214 | * 215 | * Useful for grouping asynchronous responses, where you want to be sure that 216 | * all the async calls have finished, before proceeding. 217 | * 218 | * @param integer $count 219 | * @param callable $function 220 | * @return \Closure 221 | */ 222 | public static function after($count, $function) 223 | { 224 | return function () use ($function, &$count) { 225 | if (--$count < 1) { 226 | $args = func_get_args(); 227 | 228 | return call_user_func_array($function, $args); 229 | } 230 | }; 231 | } 232 | 233 | /** 234 | * Creates a version of the function that can be called no more than count times. 235 | * 236 | * The result of the last function call is memoized and returned when count has been reached. 237 | * 238 | * @param integer $count 239 | * @param callable $function 240 | * @return \Closure 241 | */ 242 | public static function before($count, $function) 243 | { 244 | $memo = null; 245 | 246 | return function () use ($function, &$count, &$memo) { 247 | if (--$count > 0) { 248 | $args = func_get_args(); 249 | $memo = call_user_func_array($function, $args); 250 | } 251 | 252 | return $memo; 253 | }; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /tests/UnderscoreTest.php: -------------------------------------------------------------------------------- 1 | name = 'dummy'; 14 | $dummy->foo = 'bar'; 15 | $dummy->baz = 'qux'; 16 | 17 | return $dummy; 18 | } 19 | 20 | /** 21 | * @return object 22 | */ 23 | protected function getDummy2() 24 | { 25 | $dummy = $this->getDummy(); 26 | $dummy->false = false; 27 | $dummy->null = null; 28 | $dummy->zero = 0; 29 | 30 | return $dummy; 31 | } 32 | 33 | protected function getDummy3() 34 | { 35 | $dummy = [ 36 | 'Angela' => [ 37 | 'position' => 'dean', 38 | 'sex' => 'female', 39 | ], 40 | 'Bob' => [ 41 | 'position' => 'janitor', 42 | 'sex' => 'male', 43 | ], 44 | 'Mark' => [ 45 | 'position' => 'teacher', 46 | 'sex' => 'male', 47 | 'tenured' => true, 48 | ], 49 | 'Wendy' => [ 50 | 'position' => 'teacher', 51 | 'sex' => 'female', 52 | 'tenured' => 1, 53 | ], 54 | ]; 55 | 56 | return $dummy; 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function getTestRangeData() 63 | { 64 | $out = []; 65 | // case #0 66 | $out[] = [0, 4, 1, [0, 1, 2, 3]]; 67 | // case #1 68 | $out[] = [1, 5, 1, [1, 2, 3, 4]]; 69 | // case #2 70 | $out[] = [0, 20, 5, [0, 5, 10, 15]]; 71 | // case #3 72 | $out[] = [0, 0, 1, []]; 73 | // case #4 74 | $out[] = [1, 2, 0, [], '\LogicException']; 75 | 76 | return $out; 77 | } 78 | 79 | public function testRegistry() 80 | { 81 | // Get current registry 82 | $registry = Underscore::getRegistry(); 83 | 84 | $this->assertInstanceOf('Underscore\Registry', $registry); 85 | 86 | $mocked = $this->getMockBuilder('Underscore\Registry')->getMock(); 87 | 88 | Underscore::setRegistry($mocked); 89 | 90 | $this->assertSame($mocked, Underscore::getRegistry()); 91 | 92 | // Restore original registry 93 | Underscore::setRegistry($registry); 94 | } 95 | 96 | /** 97 | * @dataProvider getTestRangeData 98 | */ 99 | public function testRange($start, $stop, $step, $expected, $exception = null) 100 | { 101 | $exception && $this->setExpectedException($exception); 102 | 103 | $this->assertEquals( 104 | $expected, 105 | Underscore::range($start, $stop, $step)->toArray() 106 | ); 107 | } 108 | 109 | public function testTimes() 110 | { 111 | $fib = function ($n) { 112 | // http://stackoverflow.com/a/27190248/49146 113 | return (int) round(pow((sqrt(5) + 1) / 2, $n) / sqrt(5)); 114 | }; 115 | 116 | $expected = [0, 1, 1, 2, 3, 5, 8, 13]; 117 | 118 | $this->assertSame( 119 | $expected, 120 | Underscore::times(8, $fib)->toArray() 121 | ); 122 | } 123 | 124 | public function testValue() 125 | { 126 | $value = Underscore::from($this->getDummy())->value(); 127 | $this->assertEquals((array)$this->getDummy(), $value); 128 | } 129 | 130 | public function testInvoke() 131 | { 132 | $buffer = ''; 133 | Underscore::from($this->getDummy()) 134 | ->invoke( 135 | function ($value, $key) use (&$buffer) { 136 | $buffer .= sprintf('%s:%s|', $key, $value); 137 | } 138 | ); 139 | $this->assertSame('name:dummy|foo:bar|baz:qux|', $buffer); 140 | } 141 | 142 | public function testMap() 143 | { 144 | $value = Underscore::from($this->getDummy()) 145 | ->map( 146 | function ($value, $key) use (&$buffer) { 147 | return sprintf('%s:%s', $key, $value); 148 | } 149 | )->toArray(); 150 | 151 | $this->assertSame( 152 | ['name' => 'name:dummy', 'foo' => 'foo:bar', 'baz' => 'baz:qux'], 153 | $value 154 | ); 155 | } 156 | 157 | public function testReduce() 158 | { 159 | $value = Underscore::from($this->getDummy()) 160 | ->reduce( 161 | function ($accu, $value) { 162 | $accu .= $value . ' '; 163 | 164 | return $accu; 165 | }, 166 | '' 167 | ); 168 | 169 | $this->assertSame('dummy bar qux ', $value); 170 | } 171 | 172 | public function testReduceRight() 173 | { 174 | $value = Underscore::from($this->getDummy()) 175 | ->reduceRight( 176 | function ($accumulator, $value) { 177 | $accumulator .= $value . ' '; 178 | 179 | return $accumulator; 180 | }, 181 | '' 182 | ); 183 | 184 | $this->assertSame('qux bar dummy ', $value); 185 | } 186 | 187 | public function testIndexOf() 188 | { 189 | $value = Underscore::from($this->getDummy()) 190 | ->indexOf('dummy'); 191 | 192 | $this->assertSame('name', $value); 193 | 194 | $value = Underscore::from($this->getDummy2()) 195 | ->indexOf(false); 196 | 197 | $this->assertSame('false', $value); 198 | } 199 | 200 | public function testLastIndexOf() 201 | { 202 | $value = Underscore::from([1, 2, 3]) 203 | ->lastIndexOf(2); 204 | 205 | $this->assertSame(1, $value); 206 | 207 | $value = Underscore::from([1, 2, 3, 4, 2]) 208 | ->lastIndexOf(2); 209 | 210 | $this->assertSame(4, $value); 211 | 212 | $object = new \stdClass; 213 | $value = Underscore::from([clone $object, clone $object, $object]) 214 | ->lastIndexOf($object); 215 | 216 | $this->assertSame(2, $value); 217 | } 218 | 219 | public function testMin() 220 | { 221 | $value = Underscore::from([1, 5, 1, 100, 8, 3]) 222 | ->min(); 223 | 224 | $this->assertSame(1, $value); 225 | } 226 | 227 | public function testMax() 228 | { 229 | $value = Underscore::from([1, 5, 1, 100, 8, 3]) 230 | ->max(); 231 | 232 | $this->assertSame(100, $value); 233 | } 234 | 235 | public function testpluck() 236 | { 237 | $value = Underscore::from([$this->getDummy(), $this->getDummy(), $this->getDummy()]) 238 | ->pluck('foo') 239 | ->toArray(); 240 | 241 | $this->assertSame(['bar', 'bar', 'bar'], $value); 242 | 243 | $value = Underscore::from($this->getDummy3()) 244 | ->pluck('position') 245 | ->toArray(); 246 | 247 | $this->assertSame([ 248 | 'Angela' => 'dean', 249 | 'Bob' => 'janitor', 250 | 'Mark' => 'teacher', 251 | 'Wendy' => 'teacher', 252 | ], $value); 253 | } 254 | 255 | public function testpluckGetter() 256 | { 257 | $value = Underscore::from([new Dummy()]) 258 | ->pluck('getFoo') 259 | ->toArray(); 260 | 261 | $this->assertSame(['foo'], $value); 262 | } 263 | 264 | public function testContains() 265 | { 266 | $this->assertTrue(Underscore::from($this->getDummy())->contains('bar')); 267 | $this->assertFalse(Underscore::from($this->getDummy())->contains('baz')); 268 | } 269 | 270 | public function testFind() 271 | { 272 | $iterator = function ($needle) { 273 | return function ($value) use ($needle) { 274 | return $value === $needle; 275 | }; 276 | }; 277 | $this->assertTrue(Underscore::from($this->getDummy())->find($iterator('bar'))); 278 | $this->assertFalse(Underscore::from($this->getDummy())->find($iterator('foo'))); 279 | } 280 | 281 | public function testFilter() 282 | { 283 | $value = Underscore::from($this->getDummy()) 284 | ->filter( 285 | function ($value) { 286 | return 3 < strlen($value); 287 | } 288 | ) 289 | ->toArray(); 290 | 291 | $this->assertSame(['name' => 'dummy'], $value); 292 | } 293 | 294 | public function testReject() 295 | { 296 | $value = Underscore::from($this->getDummy()) 297 | ->reject( 298 | function ($value) { 299 | return 3 < strlen($value); 300 | } 301 | ) 302 | ->toArray(); 303 | 304 | $this->assertSame(['foo' => 'bar', 'baz' => 'qux'], $value); 305 | } 306 | 307 | public function testAny() 308 | { 309 | $value = Underscore::from($this->getDummy()) 310 | ->any( 311 | function ($value) { 312 | return 3 < strlen($value); 313 | } 314 | ); 315 | 316 | $this->assertSame(true, $value); 317 | 318 | $value = Underscore::from($this->getDummy()) 319 | ->any( 320 | function ($value) { 321 | return strlen($value) < 2; 322 | } 323 | ); 324 | 325 | $this->assertSame(false, $value); 326 | } 327 | 328 | public function testAll() 329 | { 330 | $value = Underscore::from($this->getDummy()) 331 | ->all( 332 | function ($value) { 333 | return 3 <= strlen($value); 334 | } 335 | ); 336 | 337 | $this->assertSame(true, $value); 338 | 339 | $value = Underscore::from($this->getDummy()) 340 | ->all( 341 | function ($value) { 342 | return 3 < strlen($value); 343 | } 344 | ); 345 | 346 | $this->assertSame(false, $value); 347 | } 348 | 349 | public function testSize() 350 | { 351 | $value = Underscore::from($this->getDummy()) 352 | ->size(); 353 | 354 | $this->assertSame(3, $value); 355 | } 356 | 357 | public function testHead() 358 | { 359 | $value = Underscore::from($this->getDummy()) 360 | ->head(2) 361 | ->value(); 362 | 363 | $this->assertSame(['name' => 'dummy', 'foo' => 'bar'], $value); 364 | } 365 | 366 | public function testTail() 367 | { 368 | $value = Underscore::from($this->getDummy()) 369 | ->tail(1) 370 | ->value(); 371 | 372 | $this->assertSame(['foo' => 'bar', 'baz' => 'qux'], $value); 373 | } 374 | 375 | public function testInitial() 376 | { 377 | $value = Underscore::from($this->getDummy()) 378 | ->initial(2) 379 | ->value(); 380 | 381 | $this->assertSame(['name' => 'dummy'], $value); 382 | } 383 | 384 | public function testLast() 385 | { 386 | $value = Underscore::from($this->getDummy()) 387 | ->last(2) 388 | ->value(); 389 | 390 | $this->assertSame(['foo' => 'bar', 'baz' => 'qux'], $value); 391 | } 392 | 393 | public function testCompact() 394 | { 395 | $value = Underscore::from($this->getDummy2()) 396 | ->compact() 397 | ->toArray(); 398 | 399 | $this->assertSame(['name' => 'dummy', 'foo' => 'bar', 'baz' => 'qux'], $value); 400 | } 401 | 402 | public function testWithout() 403 | { 404 | $value = Underscore::from($this->getDummy()) 405 | ->without(['dummy']) 406 | ->toArray(); 407 | 408 | $this->assertSame(['foo' => 'bar', 'baz' => 'qux'], $value); 409 | } 410 | 411 | public function testMerge() 412 | { 413 | $value = Underscore::from($this->getDummy()) 414 | ->merge(new Collection($this->getDummy2())) 415 | ->toArray(); 416 | 417 | $this->assertSame( 418 | [ 419 | 'name' => 'dummy', 420 | 'foo' => 'bar', 421 | 'baz' => 'qux', 422 | 'false' => false, 423 | 'null' => null, 424 | 'zero' => 0, 425 | ], 426 | $value 427 | ); 428 | } 429 | 430 | public function testDifference() 431 | { 432 | $one = ['yellow', 'pink', 'red', 'white', 'blue']; 433 | $two = ['red', 'white', 'blue']; 434 | 435 | $value = Underscore::from($one) 436 | ->difference($two) 437 | ->toArray(); 438 | 439 | $this->assertSame(array_values($value), [ 440 | 'yellow', 441 | 'pink', 442 | ]); 443 | 444 | $one = [ 445 | 'name' => 'John', 446 | 'food' => 'bacon', 447 | 'sport' => 'tennis', 448 | 'color' => 'red', 449 | ]; 450 | 451 | $two = [ 452 | 'name' => 'John', 453 | 'color' => 'gray', 454 | 'food' => 'tofu', 455 | 'sport' => 'tennis', 456 | ]; 457 | 458 | $value = Underscore::from($one) 459 | ->difference($two) 460 | ->toArray(); 461 | 462 | // In this scenario, preserving keys is important. 463 | $this->assertSame($value, [ 464 | 'food' => 'bacon', 465 | 'color' => 'red', 466 | ]); 467 | 468 | // No difference, with a Traversable 469 | $values = new \ArrayObject([ 470 | 'foo' => 'bar', 471 | 'fizz' => 'bizz', 472 | ]); 473 | 474 | $value = Underscore::from($values) 475 | ->difference($values) 476 | ->toArray(); 477 | 478 | $this->assertEmpty($value); 479 | } 480 | 481 | public function testIntersection() 482 | { 483 | $one = ['yellow', 'pink', 'white']; 484 | $two = ['red', 'white', 'blue']; 485 | 486 | $value = Underscore::from($one) 487 | ->intersection($two) 488 | ->toArray(); 489 | 490 | // Intersection will preserve keys, but we do not need to verify that. 491 | $this->assertSame(array_values($value), [ 492 | 'white', 493 | ]); 494 | 495 | // Run the same test, but with an array instead of a collection. 496 | $value = Underscore::from($one) 497 | ->intersection(new Collection($two)) 498 | ->toArray(); 499 | 500 | $this->assertSame(array_values($value), [ 501 | 'white', 502 | ]); 503 | 504 | $one = [ 505 | 'name' => 'John', 506 | 'food' => 'bacon', 507 | 'sport' => 'tennis', 508 | 'color' => 'red', 509 | ]; 510 | 511 | $two = [ 512 | 'name' => 'John', 513 | 'color' => 'gray', 514 | 'food' => 'tofu', 515 | 'sport' => 'tennis', 516 | ]; 517 | 518 | $value = Underscore::from($one) 519 | ->intersection($two) 520 | ->toArray(); 521 | 522 | // In this scenario, preserving keys is important. 523 | $this->assertSame($value, [ 524 | 'name' => 'John', 525 | 'sport' => 'tennis', 526 | ]); 527 | } 528 | 529 | public function testValues() 530 | { 531 | $value = Underscore::from($this->getDummy()) 532 | ->values() 533 | ->toArray(); 534 | 535 | $this->assertSame( 536 | [ 537 | 'dummy', 538 | 'bar', 539 | 'qux', 540 | ], 541 | $value 542 | ); 543 | } 544 | 545 | public function testKeys() 546 | { 547 | $value = Underscore::from($this->getDummy()) 548 | ->keys() 549 | ->toArray(); 550 | 551 | $this->assertSame( 552 | [ 553 | 'name', 554 | 'foo', 555 | 'baz', 556 | ], 557 | $value 558 | ); 559 | } 560 | 561 | public function testHas() 562 | { 563 | $collection = Underscore::from($this->getDummy()); 564 | 565 | $this->assertTrue($collection->has('name')); 566 | $this->assertTrue($collection->has('foo')); 567 | $this->assertTrue($collection->has('baz')); 568 | 569 | $this->assertFalse($collection->has('nope')); 570 | $this->assertFalse($collection->has('missing')); 571 | } 572 | 573 | public function testClone() 574 | { 575 | $original = $this->getDummy(); 576 | $cloned = Underscore::from($original) 577 | ->clone() 578 | ->without(['dummy']) 579 | ->value(); 580 | 581 | $this->assertNotEquals( 582 | Underscore::from($original)->value(), 583 | $cloned 584 | ); 585 | } 586 | 587 | public function testZip() 588 | { 589 | $value = Underscore::from($this->getDummy()) 590 | ->zip(['a', 1, '42']) 591 | ->toArray(); 592 | 593 | $this->assertSame( 594 | [ 595 | 'a' => 'dummy', 596 | 1 => 'bar', 597 | '42' => 'qux', 598 | ], 599 | $value 600 | ); 601 | 602 | $this->setExpectedException('\LogicException'); 603 | Underscore::from($this->getDummy()) 604 | ->zip(['a']); 605 | } 606 | 607 | public function testGroupBy() 608 | { 609 | $value = Underscore::from($this->getDummy()) 610 | ->groupBy('strlen') 611 | ->toArray(); 612 | 613 | $this->assertSame( 614 | [ 615 | 5 => ['dummy'], 616 | 3 => ['bar', 'qux'], 617 | ], 618 | $value 619 | ); 620 | } 621 | 622 | public function testSortBy() 623 | { 624 | $value = Underscore::from($this->getDummy()) 625 | ->sortBy('strlen') 626 | ->toArray(); 627 | 628 | $this->assertSame( 629 | [ 630 | 'bar', 631 | 'qux', 632 | 'dummy', 633 | ], 634 | $value 635 | ); 636 | } 637 | 638 | /** 639 | * @return array 640 | */ 641 | public function getTestFlattenData() 642 | { 643 | $out = []; 644 | // case #0 645 | $out[] = [ 646 | [1, 2, [3, 4]], 647 | [1, 2, 3, 4], 648 | ]; 649 | 650 | return $out; 651 | } 652 | 653 | /** 654 | * @dataProvider getTestFlattenData 655 | */ 656 | public function testFlatten($input, $expected) 657 | { 658 | $value = Underscore::from($input) 659 | ->flatten() 660 | ->toArray(); 661 | 662 | $this->assertSame($expected, $value); 663 | } 664 | 665 | public function testTap() 666 | { 667 | $dummy = $this->getDummy(); 668 | 669 | $mock = $this->getMock('stdClass', ['test']); 670 | $mock->expects($this->once())->method('test')->with((array)$dummy); 671 | 672 | Underscore::from($dummy)->tap([$mock, 'test']); 673 | } 674 | 675 | public function testThru() 676 | { 677 | $dummy = $this->getDummy(); 678 | 679 | $mock = $this->getMock('stdClass', ['test']); 680 | $mock->expects($this->once())->method('test')->with((array)$dummy)->willReturn([123]); 681 | 682 | $this->assertEquals([123], Underscore::from($dummy)->thru([$mock, 'test'])->value()); 683 | } 684 | 685 | /** 686 | * @expectedException \BadMethodCallException 687 | */ 688 | public function testCallUnknownMethod() 689 | { 690 | Underscore::from([])->foobar(); 691 | } 692 | 693 | /** 694 | * @return array 695 | */ 696 | public function getTestUniqData() 697 | { 698 | $out = []; 699 | // case #0 700 | $out[] = [ 701 | [1, 2, 3, 4, 4, 3], 702 | [1, 2, 3, 4], 703 | ]; 704 | // case #1 705 | $obj1 = new \StdClass; 706 | $obj2 = new \StdClass; 707 | $obj3 = $obj1; 708 | $out[] = [ 709 | [$obj1, $obj1, $obj2, $obj3], 710 | [$obj1, $obj2], 711 | ]; 712 | // case #2 713 | $out[] = [ 714 | [true, false, 1, 0, 0.0, 0.00001], 715 | [true, false, 1, 0, 0.0, 0.00001], 716 | ]; 717 | 718 | return $out; 719 | } 720 | 721 | /** 722 | * @dataProvider getTestUniqData 723 | */ 724 | public function testUniq($input, $expected) 725 | { 726 | $value = Underscore::from($input) 727 | ->uniq() 728 | ->toArray(); 729 | 730 | $this->assertEquals(array_values($expected), array_values($value)); 731 | } 732 | 733 | public function testExtend() 734 | { 735 | $collection = Underscore::from($this->getDummy()) 736 | ->extend([ 737 | 'name' => 'extended', 738 | ]); 739 | 740 | $this->assertSame([ 741 | 'name' => 'extended', 742 | 'foo' => 'bar', 743 | 'baz' => 'qux', 744 | ], $collection->toArray()); 745 | 746 | $collection = $collection->extend((object)[ 747 | 'name' => 'obj', 748 | ], (object)[ 749 | 'name' => 'test', 750 | 'add' => 'multi' 751 | ]); 752 | 753 | $this->assertSame([ 754 | 'name' => 'test', 755 | 'foo' => 'bar', 756 | 'baz' => 'qux', 757 | 'add' => 'multi' 758 | ], $collection->toArray()); 759 | } 760 | 761 | public function testDefaults() 762 | { 763 | $collection = Underscore::from($this->getDummy()) 764 | ->defaults([ 765 | 'name' => 'extended', 766 | ]); 767 | 768 | $this->assertSame([ 769 | 'name' => 'dummy', 770 | 'foo' => 'bar', 771 | 'baz' => 'qux', 772 | ], $collection->toArray()); 773 | 774 | $collection = $collection->defaults((object)[ 775 | 'bar' => 'gold', 776 | ], (object)[ 777 | 'bar' => 'silver', 778 | 'color' => 'blue' 779 | ]); 780 | 781 | $this->assertSame([ 782 | 'name' => 'dummy', 783 | 'foo' => 'bar', 784 | 'baz' => 'qux', 785 | 'bar' => 'gold', 786 | 'color' => 'blue' 787 | ], $collection->toArray()); 788 | } 789 | 790 | public function testWhere() 791 | { 792 | $found = Underscore::from($this->getDummy3()) 793 | ->where([ 794 | 'sex' => 'female', 795 | ]) 796 | ->keys() 797 | ->toArray(); 798 | 799 | $this->assertSame(['Angela', 'Wendy'], $found); 800 | 801 | $found = Underscore::from($this->getDummy3()) 802 | ->where([ 803 | 'position' => 'teacher', 804 | ]) 805 | ->keys() 806 | ->toArray(); 807 | 808 | $this->assertSame(['Mark', 'Wendy'], $found); 809 | 810 | $found = Underscore::from($this->getDummy3()) 811 | ->where([ 812 | 'position' => 'teacher', 813 | 'tenured' => true, 814 | ]) 815 | ->keys() 816 | ->toArray(); 817 | 818 | $this->assertSame(['Mark'], $found); 819 | 820 | $found = Underscore::from($this->getDummy3()) 821 | ->where([ 822 | 'position' => 'teacher', 823 | 'tenured' => true, 824 | ], $strict = false) 825 | ->keys() 826 | ->toArray(); 827 | 828 | $this->assertSame(['Mark', 'Wendy'], $found); 829 | 830 | $found = Underscore::from($this->getDummy3()) 831 | ->where([ 832 | 'sex' => 'female', 833 | 'position' => 'teacher', 834 | ]) 835 | ->keys() 836 | ->toArray(); 837 | 838 | $this->assertSame(['Wendy'], $found); 839 | 840 | $found = Underscore::from($this->getDummy3()) 841 | ->where([ 842 | 'sex' => 'male', 843 | 'position' => 'dean', 844 | ]) 845 | ->keys() 846 | ->toArray(); 847 | 848 | $this->assertSame([], $found); 849 | } 850 | 851 | public function testMixin() 852 | { 853 | Underscore::mixin([ 854 | 'falsey' => function ($collection) { 855 | $output = clone $collection; 856 | 857 | foreach ($collection as $k => $v) { 858 | if (!empty($v)) { 859 | unset($output[$k]); 860 | } 861 | } 862 | 863 | return $output; 864 | } 865 | ]); 866 | 867 | $value = Underscore::from($this->getDummy2()) 868 | ->falsey() 869 | ->toArray(); 870 | 871 | $this->assertSame($value, [ 872 | 'false' => false, 873 | 'null' => null, 874 | 'zero' => 0, 875 | ]); 876 | $value = Underscore::from($this->getDummy()) 877 | ->falsey() 878 | ->toArray(); 879 | 880 | $this->assertSame($value, []); 881 | } 882 | 883 | public function testShuffle() 884 | { 885 | $original = $this->getDummy3(); 886 | $values = Underscore::from($original) 887 | ->shuffle() 888 | ->toArray(); 889 | 890 | // Not necessarily the same, but contains the same values. 891 | // We cannot check the sorting of the keys because it is entirely 892 | // possible that keys were not randomized at all! 893 | $this->assertEquals($original, $values); 894 | } 895 | } 896 | --------------------------------------------------------------------------------