diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 2c581fd1..c0c00271 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,31 +1,25 @@ name: Test -on: [push, pull_request] +on: + push: + branches: [ main ] + pull_request: + branches: '*' jobs: test: - name: PHP ${{ matrix.php-version }} (${{ matrix.experimental && 'experimental' || 'full support' }}) + name: PHP ${{ matrix.php-version }} - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest + timeout-minutes: 10 strategy: - fail-fast: false matrix: php-version: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - - 8.0 - experimental: [false] - include: - - php-version: 8.1 - experimental: true - - continue-on-error: ${{ matrix.experimental }} + - 8.4 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 @@ -34,11 +28,21 @@ jobs: coverage: pcov tools: composer:v2 - - name: Install Composer dependencies - uses: ramsey/composer-install@v1 + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 with: - composer-options: --prefer-dist - continue-on-error: ${{ matrix.experimental }} + path: vendor + key: ${{ runner.os }}-${{ matrix.php-versions }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.php-versions }}-composer + + - name: Install dependencies + if: steps.composer-cache.outputs.cache-hit != 'true' + run: composer install --prefer-dist --no-progress - name: Setup PCOV run: | @@ -47,9 +51,4 @@ jobs: continue-on-error: true - name: Run Tests - run: composer tests - continue-on-error: ${{ matrix.experimental }} - - - name: Check coding style - run: composer coding-style - continue-on-error: ${{ matrix.experimental }} + run: composer test diff --git a/.gitignore b/.gitignore index 09e8c9d5..44aebe75 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ phpcs.xml phpunit.xml .idea/ .phpunit.result.cache +.php-cs-fixer.cache diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 82% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 4238d7e8..edd33527 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,6 +1,6 @@ setRiskyAllowed(true) ->setRules([ 'native_function_invocation' => true, diff --git a/composer.json b/composer.json index ebfe9738..c65785d7 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "lstrojny/functional-php", + "name": "tithely/functional-php", "description": "Functional primitives for PHP", "keywords": [ "functional" @@ -17,12 +17,12 @@ } ], "require": { - "php": "^7.1|~8" + "php": "^8.4" }, "require-dev": { "squizlabs/php_codesniffer": "~3.0", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.5", - "friendsofphp/php-cs-fixer": "^2.17" + "phpunit/phpunit": "^9.6", + "friendsofphp/php-cs-fixer": "v3.84" }, "autoload": { "psr-4": { @@ -132,8 +132,16 @@ } }, "scripts": { - "tests": "vendor/bin/phpunit", - "coding-style": "vendor/bin/phpcs && vendor/bin/php-cs-fixer fix --dry-run --diff --config=.php_cs.dist", - "clear": "rm -rf vendor/" + "test": "vendor/bin/phpunit", + "test:84": "docker exec -w /app functional_php_84 ./vendor/bin/phpunit --colors=always --no-coverage", + "lint:scan": "vendor/bin/php-cs-fixer fix src --config=.php-cs-fixer.dist.php --dry-run", + "lint:fix": "vendor/bin/php-cs-fixer fix src --config=.php-cs-fixer.dist.php", + "clear": "rm -rf vendor/", + "clean-vendor": "rm -rf vendor && rm -rf composer.lock", + "docker:start": "docker compose -p functional_php -f docker/docker-compose.yml up -d", + "docker:stop": "docker compose -p functional_php -f docker/docker-compose.yml down", + "docker:rebuild": "docker compose -p functional_php -f docker/docker-compose.yml build --force-rm && docker-compose -p functional_php -f docker/docker-compose.yml up -d --remove-orphans", + "docker:rebuild:nocache": "docker compose -p functional_php -f docker/docker-compose.yml build --force-rm --no-cache && docker-compose -p functional_php -f docker/docker-compose.yml up -d --remove-orphans", + "docker:shell": "docker exec -w /app -it functional_php_84 /bin/bash" } } diff --git a/docker/custom.ini b/docker/custom.ini new file mode 100644 index 00000000..22411123 --- /dev/null +++ b/docker/custom.ini @@ -0,0 +1,2 @@ +post_max_size = 100M +max_execution_time = 300 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..c1b4a14e --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,17 @@ +services: + functional_php_84: + build: + dockerfile: docker/php/84.Dockerfile + context: ../ + container_name: functional_php_84 + volumes: + - ../:/app + networks: + - tithely_hub_code-network + environment: + - CONF_DISABLED=xdebug + - PCOV_ENABLED=1 + - PCOV_DIRECTORY=/app +networks: + tithely_hub_code-network: + external: true diff --git a/docker/php/84.Dockerfile b/docker/php/84.Dockerfile new file mode 100644 index 00000000..eb47b3c4 --- /dev/null +++ b/docker/php/84.Dockerfile @@ -0,0 +1,13 @@ +FROM tithely/php:8.4-apache-dev + +#------------------------------ +# Begin changes below this note +#------------------------------ + +# Install custom packages +RUN apt-get update -qq && \ + apt-get install -qq \ + vim + +# Copy custom php.ini settings +COPY docker/custom.ini /usr/local/etc/php/conf.d/custom_overrides.ini diff --git a/src/Functional/CompareObjectHashOn.php b/src/Functional/CompareObjectHashOn.php index c4194479..35e0ce04 100644 --- a/src/Functional/CompareObjectHashOn.php +++ b/src/Functional/CompareObjectHashOn.php @@ -14,11 +14,11 @@ * Returns a comparison function that can be used with e.g. `usort()` * * @param callable $comparison A function that compares the two values. Pick e.g. strcmp() or strnatcasecmp() - * @param callable $keyFunction A function that takes an argument and returns the value that should be compared + * @param callable|null $keyFunction A function that takes an argument and returns the value that should be compared * @return callable * @no-named-arguments */ -function compare_object_hash_on(callable $comparison, callable $keyFunction = null) +function compare_object_hash_on(callable $comparison, ?callable $keyFunction = null) { $keyFunction = $keyFunction ? compose($keyFunction, 'spl_object_hash') : 'spl_object_hash'; diff --git a/src/Functional/CompareOn.php b/src/Functional/CompareOn.php index b0e5cda0..2d7f2486 100644 --- a/src/Functional/CompareOn.php +++ b/src/Functional/CompareOn.php @@ -14,11 +14,11 @@ * Returns a comparison function that can be used with e.g. `usort()` * * @param callable $comparison A function that compares the two values. Pick e.g. strcmp() or strnatcasecmp() - * @param callable $reducer A function that takes an argument and returns the value that should be compared + * @param callable|null $reducer A function that takes an argument and returns the value that should be compared * @return callable * @no-named-arguments */ -function compare_on(callable $comparison, callable $reducer = null) +function compare_on(callable $comparison, ?callable $reducer = null) { if ($reducer === null) { return static function ($left, $right) use ($comparison) { diff --git a/src/Functional/Concat.php b/src/Functional/Concat.php index 09426c6e..c49dcb82 100644 --- a/src/Functional/Concat.php +++ b/src/Functional/Concat.php @@ -13,7 +13,7 @@ /** * Concatenates zero or more strings * - * @param string[] ...$strings + * @param string ...$strings * @return string * @no-named-arguments */ diff --git a/src/Functional/Curry.php b/src/Functional/Curry.php index 52099360..253b8d91 100644 --- a/src/Functional/Curry.php +++ b/src/Functional/Curry.php @@ -22,6 +22,7 @@ * @param bool $required curry optional parameters ? * @return callable a curryied version of the given function * @no-named-arguments + * @throws \ReflectionException */ function curry(callable $function, $required = true) { diff --git a/src/Functional/Entries.php b/src/Functional/Entries.php index 1fc22531..58b1482b 100644 --- a/src/Functional/Entries.php +++ b/src/Functional/Entries.php @@ -17,7 +17,7 @@ * Inspired by JavaScript’s `Object.entries`, and Python’s `enumerate`, * convert a key-value map into an array of key-value pairs * - * @see Functional\from_entries + * @see from_entries * @param Traversable|array $collection * @param int $start * @return array diff --git a/src/Functional/Every.php b/src/Functional/Every.php index e4d8ff62..667cd72b 100644 --- a/src/Functional/Every.php +++ b/src/Functional/Every.php @@ -22,7 +22,7 @@ * @return bool * @no-named-arguments */ -function every($collection, callable $callback = null) +function every($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/First.php b/src/Functional/First.php index 87db945e..f9e30afd 100644 --- a/src/Functional/First.php +++ b/src/Functional/First.php @@ -23,7 +23,7 @@ * @return mixed * @no-named-arguments */ -function first($collection, callable $callback = null) +function first($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/FromEntries.php b/src/Functional/FromEntries.php index 65dd5ffc..68bcac8c 100644 --- a/src/Functional/FromEntries.php +++ b/src/Functional/FromEntries.php @@ -17,7 +17,7 @@ * Inspired by JavaScript’s `Object.fromEntries`, * convert an array of key-value pairs into a key-value map * - * @see Functional\entries + * @see entries * @param Traversable|array $collection * @return array * @no-named-arguments diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index caaed895..06a9b1b3 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -12,7 +12,6 @@ final class Functional { - /** * @see \Function\ary */ @@ -254,58 +253,58 @@ final class Functional const less_than_or_equal = '\Functional\less_than_or_equal'; /** - * @see \Functional\lexicographic_compare + * @see lexicographic_compare */ const lexicographic_compare = '\Functional\lexicographic_compare'; /** - * @see \Functional\map + * @see map */ const map = '\Functional\map'; /** - * @see \Functional\matching + * @see matching * @deprecated */ const match = '\Functional\match'; /** - * @see \Functional\matching + * @see matching */ const matching = '\Functional\matching'; /** - * @see \Functional\maximum + * @see maximum */ const maximum = '\Functional\maximum'; /** - * @see \Functional\memoize + * @see memoize */ const memoize = '\Functional\memoize'; /** - * @see \Functional\minimum + * @see minimum */ const minimum = '\Functional\minimum'; /** - * @see \Functional\none + * @see none */ const none = '\Functional\none'; /** - * @see \Functional\noop + * @see noop */ const noop = '\Functional\noop'; /** - * @see \Functional\not + * @see not */ const not = '\Functional\not'; /** - * @see \Functional\omit_keys + * @see omit_keys */ const omit_keys = '\Functional\omit_keys'; diff --git a/src/Functional/Group.php b/src/Functional/Group.php index af2ed3a1..f3da56d5 100644 --- a/src/Functional/Group.php +++ b/src/Functional/Group.php @@ -32,6 +32,11 @@ function group($collection, callable $callback) InvalidArgumentException::assertValidArrayKey($groupKey, __FUNCTION__); + // Avoid implicit conversion, since float numbers cannot be used as array keys + if (\is_numeric($groupKey)) { + $groupKey = (int) $groupKey; + } + if (!isset($groups[$groupKey])) { $groups[$groupKey] = []; } diff --git a/src/Functional/Head.php b/src/Functional/Head.php index c6de5e86..3019ee86 100644 --- a/src/Functional/Head.php +++ b/src/Functional/Head.php @@ -17,11 +17,11 @@ * Alias for Functional\first * * @param Traversable|array $collection - * @param callable $callback + * @param callable|null $callback * @return mixed * @no-named-arguments */ -function head($collection, callable $callback = null) +function head($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Last.php b/src/Functional/Last.php index 8989f945..4ffa2e4e 100644 --- a/src/Functional/Last.php +++ b/src/Functional/Last.php @@ -22,7 +22,7 @@ * @return mixed * @no-named-arguments */ -function last($collection, callable $callback = null) +function last($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Memoize.php b/src/Functional/Memoize.php index 3813e3ce..aca716b5 100644 --- a/src/Functional/Memoize.php +++ b/src/Functional/Memoize.php @@ -21,7 +21,7 @@ * @return mixed * @no-named-arguments */ -function memoize(callable $callback = null, $arguments = [], $key = null) +function memoize(?callable $callback = null, $arguments = [], $key = null) { static $storage = []; if ($callback === null) { diff --git a/src/Functional/None.php b/src/Functional/None.php index edbd33a9..c90e4055 100644 --- a/src/Functional/None.php +++ b/src/Functional/None.php @@ -22,7 +22,7 @@ * @return bool * @no-named-arguments */ -function none($collection, callable $callback = null) +function none($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Pick.php b/src/Functional/Pick.php index b4be3c13..7d29f92c 100644 --- a/src/Functional/Pick.php +++ b/src/Functional/Pick.php @@ -24,7 +24,7 @@ * @return mixed * @no-named-arguments */ -function pick($collection, $index, $default = null, callable $callback = null) +function pick($collection, $index, $default = null, ?callable $callback = null) { InvalidArgumentException::assertArrayAccess($collection, __FUNCTION__, 1); diff --git a/src/Functional/Poll.php b/src/Functional/Poll.php index 7c632d77..8f920d24 100644 --- a/src/Functional/Poll.php +++ b/src/Functional/Poll.php @@ -26,7 +26,7 @@ * @return boolean * @no-named-arguments */ -function poll(callable $callback, $timeout, Traversable $delaySequence = null) +function poll(callable $callback, $timeout, ?Traversable $delaySequence = null) { InvalidArgumentException::assertIntegerGreaterThanOrEqual($timeout, 0, __FUNCTION__, 2); diff --git a/src/Functional/Reject.php b/src/Functional/Reject.php index 505acc91..0efddeee 100644 --- a/src/Functional/Reject.php +++ b/src/Functional/Reject.php @@ -22,7 +22,7 @@ * @return array * @no-named-arguments */ -function reject($collection, callable $callback = null) +function reject($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Retry.php b/src/Functional/Retry.php index 40ef0fa7..4466542a 100644 --- a/src/Functional/Retry.php +++ b/src/Functional/Retry.php @@ -29,7 +29,7 @@ * @return mixed Return value of the function * @no-named-arguments */ -function retry(callable $callback, $retries, Traversable $delaySequence = null) +function retry(callable $callback, $retries, ?Traversable $delaySequence = null) { InvalidArgumentException::assertIntegerGreaterThanOrEqual($retries, 1, __FUNCTION__, 2); diff --git a/src/Functional/Select.php b/src/Functional/Select.php index 93420357..eb0ab73d 100644 --- a/src/Functional/Select.php +++ b/src/Functional/Select.php @@ -22,7 +22,7 @@ * @return array * @no-named-arguments */ -function select($collection, callable $callback = null) +function select($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Sequences/ExponentialSequence.php b/src/Functional/Sequences/ExponentialSequence.php index 0a8b8d47..be414d9e 100644 --- a/src/Functional/Sequences/ExponentialSequence.php +++ b/src/Functional/Sequences/ExponentialSequence.php @@ -38,28 +38,28 @@ public function __construct($start, $percentage) $this->percentage = $percentage; } - public function current() + public function current(): mixed { return $this->value; } - public function next() + public function next(): void { $this->value = (int) \round(\pow($this->start * (1 + $this->percentage / 100), $this->times)); $this->times++; } - public function key() + public function key(): mixed { return null; } - public function valid() + public function valid(): bool { return true; } - public function rewind() + public function rewind(): void { $this->times = 1; $this->value = $this->start; diff --git a/src/Functional/Sequences/LinearSequence.php b/src/Functional/Sequences/LinearSequence.php index ad99dfbb..9947ff02 100644 --- a/src/Functional/Sequences/LinearSequence.php +++ b/src/Functional/Sequences/LinearSequence.php @@ -34,27 +34,27 @@ public function __construct($start, $amount) $this->amount = $amount; } - public function current() + public function current(): mixed { return $this->value; } - public function next() + public function next(): void { $this->value += $this->amount; } - public function key() + public function key(): mixed { return 0; } - public function valid() + public function valid(): bool { return true; } - public function rewind() + public function rewind(): void { $this->value = $this->start; } diff --git a/src/Functional/Some.php b/src/Functional/Some.php index df27e0a8..9ae81ade 100644 --- a/src/Functional/Some.php +++ b/src/Functional/Some.php @@ -22,7 +22,7 @@ * @return bool * @no-named-arguments */ -function some($collection, callable $callback = null) +function some($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Tail.php b/src/Functional/Tail.php index 495131d2..621b174d 100644 --- a/src/Functional/Tail.php +++ b/src/Functional/Tail.php @@ -22,7 +22,7 @@ * @return array * @no-named-arguments */ -function tail($collection, callable $callback = null) +function tail($collection, ?callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Unique.php b/src/Functional/Unique.php index 656ac2be..31b845b7 100644 --- a/src/Functional/Unique.php +++ b/src/Functional/Unique.php @@ -22,7 +22,7 @@ * @return array * @no-named-arguments */ -function unique($collection, callable $callback = null, $strict = true) +function unique($collection, ?callable $callback = null, $strict = true) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/tests/Functional/ErrorToExceptionTest.php b/tests/Functional/ErrorToExceptionTest.php index e68893e0..d059c50e 100644 --- a/tests/Functional/ErrorToExceptionTest.php +++ b/tests/Functional/ErrorToExceptionTest.php @@ -22,7 +22,7 @@ class ErrorToExceptionTest extends AbstractTestCase public function testErrorIsThrownAsException(): void { $origFn = function () { - \trigger_error('Some error', E_USER_ERROR); + \trigger_error('Some error', E_USER_DEPRECATED); }; $fn = error_to_exception($origFn); @@ -71,7 +71,7 @@ static function ($level, $message) use (&$errorMessage) { try { $fn(); self::fail('ErrorException expected'); - } catch (ErrorException $e) { + } catch (ErrorException) { self::assertNull($errorMessage); } diff --git a/tests/Functional/GroupTest.php b/tests/Functional/GroupTest.php index d8ea2f1c..8766ea02 100644 --- a/tests/Functional/GroupTest.php +++ b/tests/Functional/GroupTest.php @@ -38,11 +38,25 @@ public function test(): void self::assertSame(['' => ['k1' => 'val1', 'k3' => 'val3'], 'foo' => ['k2' => 'val2']], group($this->hashIterator, $fn)); } + public function testFloatKeys(): void + { + $hashWithFloat = [1 => 'val1', (int) 2.1 => 'val2', 3 => 'val3']; + $localHashIterator = new ArrayIterator($hashWithFloat); + + $fn = function ($v, $k, $collection) { + InvalidArgumentException::assertCollection($collection, __FUNCTION__, 3); + return (\is_int($k) ? ($k % 2 == 0) : ($v[3] % 2 == 0)) ? 'foo' : ''; + }; + + self::assertSame(['' => [1 => 'val1', 3 => 'val3'], 'foo' => [2 => 'val2']], group($hashWithFloat, $fn)); + self::assertSame(['' => [1 => 'val1', 3 => 'val3'], 'foo' => [2 => 'val2']], group($localHashIterator, $fn)); + } + public function testExceptionIsThrownWhenCallbacksReturnsInvalidKey(): void { $array = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6']; $keyMap = [true, 1, -1, 2.1, 'str', null]; - $fn = function ($v, $k, $collection) use (&$keyMap) { + $fn = function ($v, $k) use (&$keyMap) { return $keyMap[$k]; }; $result = [ @@ -118,4 +132,28 @@ public function testPassNonCallable(): void $this->expectCallableArgumentError('Functional\group', 2); group($this->list, 'undefinedFunction'); } + + public function testFloatGroupKeysAreBeingCastToInteger(): void + { + $values = [5, 10, 11, 15]; + $fn = function ($v) { + return $v / 5; + }; + + $actual = group($values, $fn); + $expected = [ + 1 => [ + 0 => 5 + ], + 2 => [ + 1 => 10, + 2 => 11 + ], + 3 => [ + 3 => 15 + ] + ]; + + self::assertEquals($expected, $actual); + } } diff --git a/tests/Functional/MatchingTest.php b/tests/Functional/MatchingTest.php index df7b16de..5d5c5bdd 100644 --- a/tests/Functional/MatchingTest.php +++ b/tests/Functional/MatchingTest.php @@ -16,8 +16,6 @@ use function Functional\equal; use function Functional\const_function; -use const PHP_VERSION_ID; - class MatchingTest extends AbstractTestCase { public function testMatching(): void @@ -94,18 +92,4 @@ public function testMatchingConditionCallables(): void ] ); } - - public function testDeprecatedAlias(): void - { - if (PHP_VERSION_ID >= 80000) { - self::markTestSkipped('Only works with PHP <8.0'); - } - - $this->expectDeprecation(); - $this->expectDeprecationMessage( - 'Functional\match() will be unavailable with PHP 8. Use Functional\matching() instead' - ); - - \call_user_func('Functional\match', []); - } } diff --git a/tests/Functional/PartialMethodTest.php b/tests/Functional/PartialMethodTest.php index 2417ed90..e52946b1 100644 --- a/tests/Functional/PartialMethodTest.php +++ b/tests/Functional/PartialMethodTest.php @@ -14,7 +14,6 @@ class PartialMethodTest extends AbstractPartialTestCase { - public function testWithNoArgs(): void { $method = partial_method('execute'); diff --git a/tests/Functional/RepeatTest.php b/tests/Functional/RepeatTest.php index 8f6ebab6..7fe2e5dc 100644 --- a/tests/Functional/RepeatTest.php +++ b/tests/Functional/RepeatTest.php @@ -46,7 +46,7 @@ public function testNegativeRepeatedTimes(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage( - 'Functional\{closure}() expects parameter 1 to be positive integer, negative integer given' + '{closure:Functional\repeat():26}() expects parameter 1 to be positive integer, negative integer given' ); repeat([$this->repeated, 'foo'])(-1);