diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 4cb7f52..4104904 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -1,48 +1,49 @@ name: PHP Package -on: [push] +on: [ push ] jobs: - phpcs: - name: PHPCS + # phpcs: + # name: PHPCS + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v2 + # - name: PHPCS check + # uses: chekalsky/phpcs-action@v1 + # with: + # enable_warnings: true + + lint-changelog: + name: Lint changelog file runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: PHPCS check - uses: chekalsky/phpcs-action@v1 + - name: Check out code + uses: actions/checkout@v4 + - name: Lint changelog file + uses: avto-dev/markdown-lint@v1 with: - enable_warnings: true - -# lint-changelog: -# name: Lint changelog file -# runs-on: ubuntu-latest -# steps: -# - name: Check out code -# uses: actions/checkout@v2 -# - name: Lint changelog file -# uses: avto-dev/markdown-lint@v1 -# with: -# rules: './.github/workflows/lint/rules/changelog.js' -# config: '/lint/config/changelog.yml' -# args: './CHANGELOG.md' + rules: './.github/workflows/lint/rules/changelog.js' + config: '/lint/config/changelog.yml' + args: './CHANGELOG.md' testing: name: Test on PHP ${{ matrix.php }} with ${{ matrix.setup }} dependencies runs-on: ubuntu-latest timeout-minutes: 10 + needs: [ lint-changelog ] strategy: fail-fast: false matrix: - setup: ['basic', 'lowest', 'stable'] - php: ['7.4','8.0'] + setup: [ 'basic', 'lowest', 'stable' ] + php: [ '8.4' ] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v1 # Action page: + uses: shivammathur/setup-php@v2 # Action page: with: php-version: ${{ matrix.php }} extensions: mbstring @@ -51,10 +52,10 @@ jobs: - name: Get Composer Cache Directory # Docs: id: composer-cache run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} @@ -86,4 +87,4 @@ jobs: # Docs: https://getcomposer.org/doc/articles/scripts.md - name: Run test suite - run: composer test + run: XDEBUG_MODE=coverage composer test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3c2b9f0..14a52cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,14 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@master + uses: actions/checkout@v4 - name: Create Release - id: create_release - uses: actions/create-release@v1 + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + name: Release ${{ github.ref }} draft: false prerelease: false diff --git a/.gitignore b/.gitignore index 1fb8642..13299c0 100755 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ clover.xml summary.log per-mutator.md infection.log + +/.phpunit.cache diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 16fe7f7..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,53 +0,0 @@ -cache: - key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG" - paths: - - vendor/ - -.job-template: &job-template - stage: test - before_script: - - pecl install xdebug - - docker-php-ext-enable xdebug - # Install composer dependencies - - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - - php composer-setup.php - - php -r "unlink('composer-setup.php');" - - php composer.phar install - # Install Xdebug - coverage: '/^\s*Lines:\s*\d+.\d+\%/' - only: - - master - script: - - ./vendor/bin/phpunit --coverage-text --colors=never - - ./vendor/bin/phpstan analyze --ansi --level=max ./src - artifacts: - paths: - - tests/_output/coverage - expire_in: 1 month - -# We test PHP7.1 -test:PHP-7.1: - <<: *job-template - image: php:7.1 - only: - - php7 - except: - - master - -# We test PHP7.2 -test:PHP-7.2: - <<: *job-template - image: php:7.2 - only: - - php7 - except: - - master - -# We test PHP7.3 -test:PHP-7.3: - <<: *job-template - image: php:7.3 - only: - - php7 - except: - - master diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9eea3d4..0000000 --- a/.travis.yml +++ /dev/null @@ -1,51 +0,0 @@ -language: php - -cache: - directories: - - $HOME/.composer/cache - -env: - global: - - setup=basic - - coverage=false - -before_install: - - composer install - - composer global require hirak/prestissimo --update-no-dev - - if [[ $coverage = 'false' ]]; then phpenv config-rm xdebug.ini || true; fi - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - - chmod +x ./cc-test-reporter - - ./cc-test-reporter before-build - -install: - - if [[ $setup = 'basic' ]]; then travis_retry composer update --prefer-dist --no-interaction --no-suggest; fi - - if [[ $setup = 'stable' ]]; then travis_retry composer update --prefer-dist --no-interaction --no-suggest --prefer-stable; fi - - if [[ $setup = 'lowest' ]]; then travis_retry composer update --prefer-dist --no-interaction --no-suggest --prefer-lowest; fi - - composer dump-autoload -o - -script: - - if [[ $coverage = 'true' ]]; then composer test-cover; else composer test; fi - -after_script: - - if [[ $coverage = 'true' ]]; then ./cc-test-reporter after-build --coverage-input-type clover --exit-code $TRAVIS_TEST_RESULT - -after_success: - - if [[ $coverage = 'true' ]]; then bash <(curl -s https://codecov.io/bash); fi - -matrix: - include: - # - php: 7.4 - # env: setup=lowest - - php: 7.4 - env: setup=lowest - - - php: 7.4 - env: setup=stable - - - php: 8.0 - env: setup=stable - - - php: nightly - - allow_failures: - - php: nightly diff --git a/CHANGELOG.md b/CHANGELOG.md index 501cf82..9ec0ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,218 +4,224 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog][keepachangelog] and this project adheres to [Semantic Versioning][semver]. -## v3.1.1 +## v5.1.0 -### Changed - -- Remove default option `JSON_THROW_ON_ERROR` from `JSON::encode` & `JSON::decode` +### Added -## v3.1.0 +- Add `HashCollection` Collection. -### Changed +## v5.0.0 -- Changed behavior on `JSON::encode` & `JSON::decode`: add default option `JSON_THROW_ON_ERROR`. Remove - custom `JsonException` +### Added -## v3.0.0 +- Add `PHP 8.4` support +- Add `UseStorage` trait. +- Add `UseConfigurabeStorage` trait. -### Added +### Removed -- Add support PHP: minimal >= 7.4 and maximum <=8.0 +- Remove Trait `ArrayStorage`. Use `UseStorage` instead +- Remove Trait `ArrayStorageConfigurableTrait`. Use `UseConfigurabeStorage` instead -## v2.14.0 +## v4.28.0 ### Added -- Add new global function `class_basename`. Get the class "basename" of the given object / class. -- Add new global function `trait_uses_recursive`. Returns all traits used by a trait and its traits. -- Add new global function `class_uses_recursive`. Returns all traits used by a class, its parent classes and trait of - their traits. -- Add trait `TraitBooter`. Helps to boot trait's static `boot-function`. -- Add trait `TraitInitializer`. Helps to init trait's `initialize-function`. +- Add global functions: `attributeToGetterMethod`, `attributeToSetterMethod`, `findGetterMethod`, `public_property_exists`, `get_property_value` -## v2.13.0 +## v4.27.0 ### Added -- Add new global function `instance` +- Add func for Enum's traits: `WithEnhances::toKeyValueArray`, `WithEnhances::toValueKeyArray` +- Add `Arr::map` -## v2.12.0 +## v4.26.0 ### Added -- Add new trait `ReadOnlyProperties` +- Add support `PHP 8.3` -## v2.11.6 +## v4.25.0 ### Added -- Add into trait `Thrower` new function `throwIf` +- `Arr::random` - Get one or a specified number of random values from an array +- `ArrayCollection::random` - Get one or a specified number of items randomly from the collection +- `ArrayCollection::clone` - Clone elements and returns Collection +- `ArrayCollection::groupBy` - Group an associative array by a field or using a callback -## v2.11.3 - -### Added +### Changed -- Add into `Str` helper function for define Regular Expression `isRegExp` +- `ArrayCollection::map` - works with keys now +- `ArrayCollection::createFrom` - receives Collections -## v2.11.2 +## v4.24.0 ### Added -- Add into `Arr` helper function for fill a keyed array by values from another array: `fillKeysByValues` +- Add an argument `separator` to methods `Arr::set`, `Arr::get`, `Arr::has` -## v2.11.1 +## v4.23.0 ### Added -- Add into `Arr` helper function for finding duplicates: `duplicates` +- Add methods `trimPrefix`, `trimSuffix` into `Str` -## v2.11.0 +## v4.22.0 ### Added -- Add trait `Whener` -- Add trait `Thrower` +- Add method `mapInto` into `ArrayCollection` -## v2.10.0 +## v4.21.0 ### Added -- Add global function: `when` +- Add method `whereInstanceOf` into `ArrayCollection` -## v2.9.0 +## v4.20.0 ### Added -- Add global function: `isTrue` +- Add method `slugifyWithFormat` into `Str` -## v2.8.0 +## v4.19.0 ### Added -- Add Helper for base64: `B64` +- Add traits for + Enums: [WithEnhances.php](./src/Enums/WithEnhances.php), [WithEnhancesForStrings](./src/Enums/WithEnhancesForStrings.php) + with following methods: + - `casesToString`- Returns string of Enum's names or values + - `casesToEscapeString`- Returns string of Enum's escaped names or values + - `values`- Returns list of Enum's values + - `names` - Returns list of Enum's names + - `hasValue` - Check if the Enum has provided Value + - `hasName` - Check if the Enum has provided Name -## v2.7.0 +## v4.18.0 ### Added -- Add types `Point` and `GeoPoint` +- Add function `Collection::reject` -## v2.6.3 +## v4.17.1 -### Added +### Changed -- Helper `Bit`: function `decBinPad`. Convert decimal to binary string with left pad zero-filling +- `Collection::filter(Closure $func = null)` - The argument `$func` may be `null` -## v2.6.0 +## v4.17.0 ### Added -- Helper `Bit`: contains operations with bits and bit-masks +- Add support `PHP 8.2` -## v2.5.0 +### Removed -### Changed - -- Logic has been changed for the trait `ConfigurableTrait::configurable`: - at first, on applying props, checking a magic method and then a property +- Remove support `PHP 8.0` -## v2.4.2 +## v4.16.0 -### Changed +### Added -- Fix the trait `ConfigurableTrait::configurable` +- Add global method `dataGet` +- Add helper method `Arr::collapse` +- Add helper method `Arr::prepend` +- Add Structures: `ArrayCollection` and its interfaces -## v2.4.0 +## v4.15.0 ### Added -- Add global function `classNamespace` +- Add global method `mapValue` Returns an array containing the results of applying func to the items of the $collection +- Add global method `eachValue` Apply a $fn to all the items of the $collection -## v2.3.0 +## v4.14.0 ### Added -- Move Helper::Arr::ToPostgresArray from candidate to basic functionality +- Add method `Number::isInteger` Allows you to determine whether the $value is an integer or not -## v2.2.5 +## v4.13.0 ### Added -- Add functionality to trait `ArrayStorage`: now it implements `Arrayable` +- Add support `PHP 8.1` -## v2.2.4 +## v4.9.0 ### Added -- Add functionality to trait `ArrayStorage`: now it implements `ArrayAccess` +- Add method `Str::truncate`: truncate a string to a specified length without cutting a word off +- Add method `Str::slugify`: generate a string safe for use in URLs from any given string +- Add method `Str::seemsUTF8`: checks to see if a string is utf8 encoded +- Add method `Str::removeAccents`: converts all accent characters to ASCII characters +- Add method `URLify::downcode`: transliterates characters to their ASCII equivalents -## v2.2.3 +## v4.8.0 ### Added -- Add trait `ArrayStorageConfigurableTrait` +- Add methods `toPostgresPoint`, `fromPostgresPoint` to `Arr` helper -## v2.2.2 +## v4.7.0 ### Added -- Fix CI +- Add exception `MissingMethodException` +- Add global function `remoteStaticCallOrTrow` -## v2.2.0 +## v4.6.0 ### Added -- Add function `has` to `Array` Helper -- Add function `set` to `Array` Helper -- Add function `remove` to `Array` Helper +- Add class `ConditionalHandler` -## v2.1.1 +## v4.5.0 ### Added -- Add trait Maker +- Add trait `HasPrePostActions` + +## v4.4.2 -## v2.1.0 +### Changed + +- Add param `removeNull` to method: `Metable::setMetaAttribute` + +## v4.4.0 ### Added -- Add trait Metable +- Add global function: `does_trait_use` -## v2.0.5 +## v4.3.1 ### Added -- Add new String helpers: - + `replaceByTemplate` Replace templates into string -- Add new Array helpers: - + `replaceByTemplate` Replace templates into array +- Add global function: `remoteCall` +- Add global function: `remoteStaticCall` -## v2.0.2 +## v4.2.0 ### Added -- Add new String helpers: - + `replaceStrTo` Replace substr by start and finish indents +- Add method to trait `Metable`: `setMetaAttribute` -## v2.0.1 +## v4.1.0 ### Added -- Add `CHANGELOG.md` -- Add new Array helpers: - + `get` Get an item from an array using "dot" notation -- Add new String helpers: - + `toSnake` Converts a string to `snake_case` - + `toScreamingSnake` Converts a string to `SCREAMING_SNAKE_CASE` - + `toKebab` Converts a string to `kebab-case` - + `toCamel` Converts a string to `CamelCase` - + `toLowerCamel` Converts a string to `lowerCamelCase` - + `removeMultiSpace` Converts all multi-spaced characters to once +- Add new Helper Class: `Number` +- Add method, working with integers: `Number::safeInt` -## v2.0.0 +## v4.0.0 + +### Changed -### Reformat +- The package has PHP's minimal version is 8.0 now [keepachangelog]:https://keepachangelog.com/en/1.0.0/ diff --git a/composer.json b/composer.json index 77033a6..68eb080 100755 --- a/composer.json +++ b/composer.json @@ -1,10 +1,11 @@ { "name": "efureev/support", - "description": "PHP Support Package", + "description": "PHP Support Package is a collection of useful functions and snippets", "license": "MIT", "type": "library", "keywords": [ "php", + "utility", "support", "helpers" ], @@ -15,44 +16,48 @@ } ], "require": { - "php": "^7.4|^8.0", - "ext-json": "*", + "php": "^8.4", "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "^9.2", - "phpstan/phpstan": "~0.12.69", - "squizlabs/php_codesniffer": "^3.5" + "ergebnis/composer-normalize": "^2.45", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5", + "squizlabs/php_codesniffer": "^3.11", + "symfony/var-dumper": "^7.2" }, "autoload": { - "files": [ - "src/Global/base.php" - ], "psr-4": { "Php\\Support\\": "src/" - } + }, + "files": [ + "src/Global/base.php" + ] }, "autoload-dev": { "psr-4": { "Php\\Support\\Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + } + }, "scripts": { - "phpcs": "@php ./vendor/bin/phpcs", "cs-fix": "@php ./vendor/bin/phpcbf", - "phpunit": "@php ./vendor/bin/phpunit --no-coverage --testdox --colors=always", - "phpunit-test": "@php ./vendor/bin/phpunit --no-coverage --testdox --colors=always", "infection": "@php ./vendor/bin/infection --coverage=./storage/coverage --threads=4", + "phpcs": "@php ./vendor/bin/phpcs", + "phpstan": "@php ./vendor/bin/phpstan analyze -c phpstan.neon --no-progress --ansi", + "phpunit": "@php ./vendor/bin/phpunit --no-coverage --testdox --colors=always", "phpunit-cover": "@php ./vendor/bin/phpunit --coverage-text", - "phpstan": "@php ./vendor/bin/phpstan analyze -c ./phpstan.neon.dist --no-progress --ansi", + "phpunit-test": "@php ./vendor/bin/phpunit --no-coverage --testdox --colors=always", "test": [ "@phpstan", - "@phpcs", "@phpunit" ], "test-cover": [ "@phpstan", - "@phpcs", "@phpunit-cover" ] } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..803d483 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +parameters: + level: '2' + paths: + - src +# - tests +# ignoreErrors: +# - +# identifier: trait.unused + excludePaths: + - 'src/Types/Point.php' + - 'src/Types/GeoPoint.php' + - 'src/Structures/Collections/ArrayCollection.php' diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 02e585c..0000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,9 +0,0 @@ -parameters: - level: 'max' - paths: - - src - checkMissingIterableValueType: false - checkGenericClassInNonGenericObjectType: false - excludes_analyse: - - 'src/Types/Point.php' - - 'src/Types/GeoPoint.php' diff --git a/phpunit.xml b/phpunit.xml index 1071b9a..6b0174f 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,30 +1,27 @@ - - - - ./tests - - - - - ./src - - - - - - - - - + + + + + + + + + + + + ./tests + + + + + + + + ./src + + diff --git a/readme.md b/readme.md index ca04ec8..3700315 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,6 @@ # PHP Support -![](https://img.shields.io/badge/php->=7.2-blue.svg) + +![](https://img.shields.io/badge/php-8.1|8.2-blue.svg) ![PHP Package](https://github.com/efureev/php-support/workflows/PHP%20Package/badge.svg?branch=master) [![Build Status](https://travis-ci.org/efureev/php-support.svg?branch=master)](https://travis-ci.org/efureev/php-support) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/a53fb85fd1ab46169758e10dd2d818cb)](https://app.codacy.com/app/efureev/php-support?utm_source=github.com&utm_medium=referral&utm_content=efureev/php-support&utm_campaign=Badge_Grade_Settings) @@ -11,12 +12,26 @@ ## Install +For php >= 8.4 + +```bash +composer require efureev/support "^5.1" +``` + +For php >= 8.1 (8.1, 8.2, 8.3) + +```bash +composer require efureev/support "^4.19" +``` + For php >= 7.4 and <=8.0 + ```bash composer require efureev/support "^3.0" ``` For php >= 7.2 && <=7.4 + ```bash composer require efureev/support "^2.0" ``` @@ -24,94 +39,132 @@ composer require efureev/support "^2.0" ## Content - Helpers - + Array - - accessible - - dataToArray - - exists - - fromPostgresArray - - get - - has - - merge - - remove - - removeByValue - - set - - toArray - - toIndexedArray - - toPostgresArray - - replaceByTemplate - + String - - removeMultiSpace - - replaceByTemplate - - replaceStrTo - - toCamel - - toDelimited - - toKebab - - toLowerCamel - - toScreamingDelimited - - toScreamingSnake - - toSnake - + Json - - decode - - encode - - htmlEncode - + Bit - - addFlag - - checkFlag - - decBinPad - - exist - - grant - - removeFlag - + B64 - - decode - - decodeSafe - - encode - - encodeSafe + + Array + - collapse (^4.16.0) + - prepend (^4.16.0) + - accessible + - dataToArray + - exists + - fromPostgresArray + - fromPostgresPoint (^4.8.0) + - get + - has + - merge + - random (^4.25.0) + - remove + - removeByValue + - replaceByTemplate + - set + - toArray + - toIndexedArray + - toPostgresArray + - toPostgresPoint (^4.8.0) + + String + - removeAccents (^4.9.0) + - removeMultiSpace + - replaceByTemplate + - replaceStrTo + - seemsUTF8 (^4.9.0) + - slugify (^4.9.0) + - toCamel + - toDelimited + - toKebab + - toLowerCamel + - toScreamingDelimited + - toScreamingSnake + - toSnake + - truncate (^4.9.0) + + Json + - decode + - encode + - htmlEncode + + Bit + - addFlag + - checkFlag + - decBinPad + - exist + - grant + - removeFlag + + B64 + - decode + - decodeSafe + - encode + - encodeSafe + + Number + - isInteger (^4.14.0) + - safeInt (^4.1.0) + - Global functions - + classNamespace - + class_basename - + class_uses_recursive - + instance - + isTrue - + trait_uses_recursive - + value - + when + + classNamespace + + class_basename + + class_uses_recursive + + dataGet (^4.16.0) + + does_trait_use (^4.4.0) + + eachValue (^4.15.0) + + instance + + isTrue + + mapValue (^4.15.0) + + remoteCall (^4.3.1) + + remoteStaticCall (^4.3.1) + + remoteStaticCallOrTrow (^4.7.0) + + trait_uses_recursive + + value + + when + +- Enums (^4.19.0) + - casesToEscapeString + - casesToString + - hasName + - hasValue + - names + - values + - Exceptions - + ConfigException - + Exception - + InvalidArgumentException - + InvalidCallException - + InvalidConfigException - + InvalidParamException - + InvalidValueException - + JsonException - + MethodNotAllowedException - + MissingClassException - + MissingConfigException - + MissingPropertyException - + NotSupportedException - + UnknownMethodException - + UnknownPropertyException + + ConfigException + + Exception + + InvalidArgumentException + + InvalidCallException + + InvalidConfigException + + InvalidParamException + + InvalidValueException + + JsonException + + MethodNotAllowedException + + MissingClassException + + MissingConfigException + + MissingPropertyException + + MissingMethodException (^4.7.0) + + NotSupportedException + + UnknownMethodException + + UnknownPropertyException + - Interfaces - + Arrayable - + Command - + Jsonable - + Prototype + + Arrayable + + Command + + Jsonable + + Prototype + +- Structures + - Collections (^4.16.0) + - ArrayCollection + - HashCollection (^5.1.0) + - Traits - + ArrayStorage - + ArrayStorageConfigurableTrait - + ConfigurableTrait - + ConsolePrint - + Maker - + Metable - + ReadOnlyProperties - + Singleton - + Thrower - + TraitBooter - + TraitInitializer - + Whener + + UseStorage + + UseConfigurableStorage + + ConfigurableTrait + + ConsolePrint + + Maker + + Metable + + ReadOnlyProperties + + Singleton + + Thrower + + TraitBooter + + TraitInitializer + + Whener + - Types - + GeoPoint - + Point + + GeoPoint + + Point ## Test diff --git a/src/ConditionalHandler.php b/src/ConditionalHandler.php new file mode 100644 index 0000000..534ca41 --- /dev/null +++ b/src/ConditionalHandler.php @@ -0,0 +1,97 @@ + MorphMany::make( + * self::translate('Notifications'), + * 'notifications', + * NotificationResource::class + * ) + * ) + * ->handleIf(static function (Request $request) { + * $request->user()->id === Auth()->id() + * }); + * + * // call + * $field($request); + * // or + * $field->resolve($request); + */ +final class ConditionalHandler +{ + /** + * @var array + */ + private array $params = []; + + /** + * @param Closure(mixed ...): mixed $handler + * @param bool|(Closure(mixed ...): bool) $condition + */ + public function __construct(private Closure $handler, private bool|Closure $condition = true) + { + } + + /** + * @param (Closure(mixed ...): bool)|bool $fn + * @return ConditionalHandler + */ + public function handleIf(Closure|bool $fn): self + { + $this->condition = $fn; + + return $this; + } + + private function resolveCondition(): bool + { + if ($this->condition instanceof Closure) { + return ($this->condition)(...$this->params); + } + + return $this->condition; + } + + /** + * @param mixed ...$params + */ + public function resolve(mixed ...$params): mixed + { + $this->params = $params; + + if (!$this->resolveCondition()) { + return null; + } + + return ($this->handler)(...$this->params); + } + + /** + * @param mixed ...$params + */ + public function __invoke(mixed ...$params): mixed + { + return $this->resolve(...$params); + } + + /** + * @param Closure(mixed ...): mixed $fn + * @param bool|(Closure(mixed ...): bool) $condition + * @return ConditionalHandler + */ + public static function make(Closure $fn, bool|Closure $condition = true): self + { + return new self($fn, $condition); + } +} diff --git a/src/Enums/WithEnhances.php b/src/Enums/WithEnhances.php new file mode 100644 index 0000000..e4b50c6 --- /dev/null +++ b/src/Enums/WithEnhances.php @@ -0,0 +1,67 @@ + $enumItem->value, self::cases()); + } + + /** + * @return string[] + */ + public static function names(): array + { + return array_map(static fn(self $enumItem) => $enumItem->name, self::cases()); + } + + public static function hasName(string $value): bool + { + return in_array($value, static::names(), true); + } + + /** + * @return array + */ + public static function toKeyValueArray(): array + { + $list = []; + foreach (self::cases() as $case) { + $list[$case->name] = $case->value; + } + + return $list; + } + + /** + * @return array + */ + public static function toValueKeyArray(): array + { + $list = []; + foreach (self::cases() as $case) { + $list[$case->value] = $case->name; + } + + return $list; + } +} diff --git a/src/Enums/WithEnhancesForStrings.php b/src/Enums/WithEnhancesForStrings.php new file mode 100644 index 0000000..a506b3d --- /dev/null +++ b/src/Enums/WithEnhancesForStrings.php @@ -0,0 +1,35 @@ + "{$enumItem->value}"; + } + + return self::casesToStringBase($decorator, $delimiter); + } + + public static function casesToEscapeString(string $delimiter = ', '): string + { + return static::casesToString($delimiter, static fn(self $enumItem) => "'$enumItem->value'"); + } + + public static function hasValue(string $value): bool + { + return in_array($value, static::values(), true); + } +} diff --git a/src/ErrorCollection.php b/src/ErrorCollection.php index 6fee9e0..42e7f90 100644 --- a/src/ErrorCollection.php +++ b/src/ErrorCollection.php @@ -6,6 +6,4 @@ class ErrorCollection { - - } diff --git a/src/Exceptions/ConfigException.php b/src/Exceptions/ConfigException.php index 6f54460..e7932d2 100644 --- a/src/Exceptions/ConfigException.php +++ b/src/Exceptions/ConfigException.php @@ -6,34 +6,15 @@ /** * Class ConfigException - * - * @package Php\Support\Exceptions */ class ConfigException extends Exception { /** - * @var mixed - */ - protected $config; - - /** - * ConfigException constructor. - * - * @param mixed|null $config * @param string $message + * @param array $config */ - public function __construct($message = 'Config Exception', $config = null) + public function __construct(string $message = 'Config Exception', protected(set) array $config = []) { parent::__construct($message); - - $this->config = $config; - } - - /** - * @return array|null - */ - public function getConfig(): ?array - { - return $this->config; } } diff --git a/src/Exceptions/Exception.php b/src/Exceptions/Exception.php index e9da400..d387860 100644 --- a/src/Exceptions/Exception.php +++ b/src/Exceptions/Exception.php @@ -10,8 +10,6 @@ /** * Class Exception - * - * @package Php\Support\Exceptions */ class Exception extends \Exception { @@ -21,11 +19,11 @@ class Exception extends \Exception /** * Exception constructor. * - * @param null|string $message + * @param ?string $message * @param int $code - * @param Throwable|null $previous + * @param ?Throwable $previous */ - public function __construct(?string $message = null, $code = 0, Throwable $previous = null) + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { parent::__construct($message ?? $this->getName(), $code, $previous); } diff --git a/src/Exceptions/InvalidArgumentException.php b/src/Exceptions/InvalidArgumentException.php index a51e1e1..8493632 100644 --- a/src/Exceptions/InvalidArgumentException.php +++ b/src/Exceptions/InvalidArgumentException.php @@ -8,20 +8,10 @@ /** * Class InvalidArgumentException - * - * @package Php\Support\Exceptions */ class InvalidArgumentException extends \InvalidArgumentException { - - /** - * Exception constructor. - * - * @param null|string $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, $code = 0, Throwable $previous = null) + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { parent::__construct($message ?? $this->getName(), $code, $previous); } diff --git a/src/Exceptions/InvalidCallException.php b/src/Exceptions/InvalidCallException.php index 507d846..ef9c186 100644 --- a/src/Exceptions/InvalidCallException.php +++ b/src/Exceptions/InvalidCallException.php @@ -8,13 +8,9 @@ /** * Class InvalidCallException - * @package Php\Support\Exceptions */ class InvalidCallException extends BadMethodCallException { - /** - * @return string - */ public function getName(): string { return 'Invalid Call'; diff --git a/src/Exceptions/InvalidConfigException.php b/src/Exceptions/InvalidConfigException.php index c954ea2..63070d9 100644 --- a/src/Exceptions/InvalidConfigException.php +++ b/src/Exceptions/InvalidConfigException.php @@ -6,18 +6,10 @@ /** * Class InvalidConfigException - * - * @package Php\Support\Exceptions */ class InvalidConfigException extends ConfigException { - /** - * InvalidConfigException constructor. - * - * @param mixed|null $config - * @param string $message - */ - public function __construct($config = null, $message = 'Invalid Configuration') + public function __construct(array $config = [], string $message = 'Invalid Configuration') { parent::__construct($message, $config); } diff --git a/src/Exceptions/InvalidParamException.php b/src/Exceptions/InvalidParamException.php index 8c2faec..2f9989a 100644 --- a/src/Exceptions/InvalidParamException.php +++ b/src/Exceptions/InvalidParamException.php @@ -6,41 +6,17 @@ use LogicException; -/** - * Class InvalidParamException - * - * @package Php\Support\Exceptions - */ class InvalidParamException extends LogicException { - /** @var string|null */ - protected $param; - - /** - * InvalidParamException constructor. - * - * @param null|string $param - * @param null|string $message - */ - public function __construct(?string $message = null, ?string $param = null) + public function __construct(?string $message = null, public private(set) readonly ?string $name = null) { - $this->param = $param; - parent::__construct($message ?? sprintf('Invalid Parameter' . ($this->param ? ': %s' : ''), $this->param)); + parent::__construct( + $message ?? sprintf('Invalid Parameter' . ($this->name ? ': %s' : ''), $this->name) + ); } - /** - * @return string - */ public function getName(): string { return 'Invalid Parameter'; } - - /** - * @return null|string - */ - public function getParam(): ?string - { - return $this->param; - } } diff --git a/src/Exceptions/InvalidValueException.php b/src/Exceptions/InvalidValueException.php index c9af7f6..9a9c270 100644 --- a/src/Exceptions/InvalidValueException.php +++ b/src/Exceptions/InvalidValueException.php @@ -8,13 +8,9 @@ /** * Class InvalidValueException - * @package Php\Support\Exceptions */ class InvalidValueException extends UnexpectedValueException { - /** - * @return string - */ public function getName(): string { return 'Invalid Return Value'; diff --git a/src/Exceptions/MethodNotAllowedException.php b/src/Exceptions/MethodNotAllowedException.php index 73271b3..e44f789 100644 --- a/src/Exceptions/MethodNotAllowedException.php +++ b/src/Exceptions/MethodNotAllowedException.php @@ -6,23 +6,13 @@ /** * Class MethodNotAllowedException - * - * @package Php\Support\Exceptions */ class MethodNotAllowedException extends Exception { - /** @var string */ - protected $reason; - - /** - * MethodNotAllowedException constructor. - * - * @param string $reason - * @param string $message - */ - public function __construct(string $reason, $message = 'Method Not Allowed') + public function __construct(protected string $reason, string $message = 'Method Not Allowed') { $this->reason = $reason; - parent::__construct($message); + + parent::__construct($message ? "$message: $reason" : $reason); } } diff --git a/src/Exceptions/MissingClassException.php b/src/Exceptions/MissingClassException.php index 9429db9..c57c58a 100644 --- a/src/Exceptions/MissingClassException.php +++ b/src/Exceptions/MissingClassException.php @@ -6,20 +6,11 @@ /** * Class MissingClassException - * - * @package Php\Support\Exceptions */ class MissingClassException extends Exception { - /** - * MissingClassException constructor. - * - * @param string|null $className - * @param string $message - */ - public function __construct($className = null, $message = 'Missing Class') + public function __construct(string $class, string $message = 'Missing Class') { - $message .= $className ? (': ' . $className) : ''; - parent::__construct($message); + parent::__construct($message . ": $class"); } } diff --git a/src/Exceptions/MissingConfigException.php b/src/Exceptions/MissingConfigException.php index cf570ec..1fe1dc9 100644 --- a/src/Exceptions/MissingConfigException.php +++ b/src/Exceptions/MissingConfigException.php @@ -11,20 +11,8 @@ */ class MissingConfigException extends ConfigException { - /** @var string|null */ - protected $needKey; - - /** - * MissingConfigException constructor. - * - * @param mixed $config - * @param string $needKey - * @param string $message - */ - public function __construct($config = null, $needKey = null, $message = 'Missing Config') + public function __construct(array $config = [], protected ?string $needKey = null, string $message = 'Missing Config') { - $this->needKey = $needKey; - parent::__construct($message, $config); } } diff --git a/src/Exceptions/MissingMethodException.php b/src/Exceptions/MissingMethodException.php new file mode 100644 index 0000000..ba57a16 --- /dev/null +++ b/src/Exceptions/MissingMethodException.php @@ -0,0 +1,32 @@ +method; + } + }, + ?string $message = null + ) { + parent::__construct($message ?? ($this->getName() . ": $this->method")); + } + + /** + * @return string + */ + public function getName(): string + { + return 'Missing method'; + } +} diff --git a/src/Exceptions/MissingPropertyException.php b/src/Exceptions/MissingPropertyException.php index b96085a..8ebb21b 100644 --- a/src/Exceptions/MissingPropertyException.php +++ b/src/Exceptions/MissingPropertyException.php @@ -6,28 +6,12 @@ /** * Class MissingPropertyException - * - * @package Php\Support\Exceptions */ class MissingPropertyException extends ConfigException { - /** @var string|null */ - protected $property; - - /** - * MissingPropertyException constructor. - * - * @param null|string $message - * @param null|string $property - * @param mixed $config - */ - public function __construct(?string $message = null, ?string $property = null, $config = null) + public function __construct(protected(set) ?string $property, ?string $message = null, array $config = []) { - $this->property = $property; - parent::__construct( - $message ?? ($this->getName() . ($this->property ? ': "' . $this->property . '"' : '')), - $config - ); + parent::__construct($message ?? ($this->getName() . ": $this->property"), $config); } /** @@ -37,12 +21,4 @@ public function getName(): string { return 'Missing property'; } - - /** - * @return null|string - */ - public function getProperty(): ?string - { - return $this->property; - } } diff --git a/src/Exceptions/NotSupportedException.php b/src/Exceptions/NotSupportedException.php index 4a9b9ef..0fc4c26 100644 --- a/src/Exceptions/NotSupportedException.php +++ b/src/Exceptions/NotSupportedException.php @@ -6,20 +6,13 @@ /** * Class NotSupportedException - * - * @package Php\Support\Exceptions */ class NotSupportedException extends Exception { - /** - * MissingClassException constructor. - * - * @param string|null $className - * @param string $message - */ - public function __construct($className = null, $message = 'Not Supported') + public function __construct(?string $className = null, string $message = 'Not Supported') { - $message .= $className ? (': ' . $className) : ''; + $message .= $className ? ": $className" : ''; + parent::__construct($message); } } diff --git a/src/Exceptions/UnknownMethodException.php b/src/Exceptions/UnknownMethodException.php index 11d6885..8eb84e9 100644 --- a/src/Exceptions/UnknownMethodException.php +++ b/src/Exceptions/UnknownMethodException.php @@ -8,24 +8,13 @@ /** * Class UnknownMethodException - * - * @package Php\Support\Exceptions */ class UnknownMethodException extends BadMethodCallException { - /** @var string|null */ - protected $method; - - /** - * UnknownMethodException constructor. - * - * @param null|string $method - * @param null|string $message - */ - public function __construct(?string $message = null, ?string $method = null) + public function __construct(protected(set) string $method, ?string $message = null) { $this->method = $method; - parent::__construct($message ?? ($this->getName() . ($this->method ? ': "' . $this->method . '"' : ''))); + parent::__construct($message ?? ($this->getName() . ": $this->method")); } /** @@ -35,12 +24,4 @@ public function getName(): string { return 'Unknown method'; } - - /** - * @return null|string - */ - public function getMethod(): ?string - { - return $this->method; - } } diff --git a/src/Exceptions/UnknownPropertyException.php b/src/Exceptions/UnknownPropertyException.php index 8f81bb2..9a882ba 100644 --- a/src/Exceptions/UnknownPropertyException.php +++ b/src/Exceptions/UnknownPropertyException.php @@ -6,39 +6,16 @@ /** * Class UnknownPropertyException - * - * @package Php\Support\Exceptions */ class UnknownPropertyException extends Exception { - /** @var string|null */ - protected $property; - - /** - * UnknownPropertyException constructor. - * - * @param null|string $property - * @param null|string $message - */ - public function __construct(?string $message = null, ?string $property = null) + public function __construct(protected(set) string $property, ?string $message = null) { - $this->property = $property; - parent::__construct($message ?? ($this->getName() . ($this->property ? ': "' . $this->property . '"' : ''))); + parent::__construct($message ?? ($this->getName() . ": $this->property")); } - /** - * @return string - */ public function getName(): string { return 'Unknown property'; } - - /** - * @return null|string - */ - public function getProperty(): ?string - { - return $this->property; - } } diff --git a/src/Global/base.php b/src/Global/base.php index 4d99581..f7ebf42 100644 --- a/src/Global/base.php +++ b/src/Global/base.php @@ -1,32 +1,118 @@ $segment) { + unset($key[$i]); + + if ($segment === null) { + return $target; + } + + if ($segment === '*') { + if ($target instanceof ReadableCollection) { + $target = $target->all(); + } elseif (!is_iterable($target)) { + return value($default); + } + + $result = []; + + foreach ($target as $item) { + $result[] = dataGet($item, $key); + } + + return in_array('*', $key) ? Arr::collapse($result) : $result; + } + + if (Arr::accessible($target) && Arr::exists($target, $segment)) { + $target = $target[$segment]; + } elseif (is_object($target) && isset($target->{$segment})) { + $target = $target->{$segment}; + } else { + return value($default); + } + } + + return $target; + } +} + + +if (!function_exists('mapValue')) { + /** + * @template TKey of array-key + * @template TValue + * @param callable $fn + * @param iterable $collection + * @param mixed ...$args + * @return array + */ + function mapValue(callable $fn, iterable $collection, mixed ...$args): array + { + $result = []; + + foreach ($collection as $key => $value) { + $result[$key] = $fn($value, $key, ...$args); + } + + return $result; + } +} + +if (!function_exists('eachValue')) { + /** + * @template TKey of array-key + * @template TValue + * @param callable $fn + * @param iterable $collection + * @param mixed ...$args + * @return void + */ + function eachValue(callable $fn, iterable $collection, mixed ...$args): void + { + foreach ($collection as $key => $value) { + $fn($value, $key, ...$args); + } + } +} + +if (!function_exists('when')) { + /** + * Returns a value when a condition is truthy. + */ + function when(mixed $condition, mixed $value, mixed $default = null): mixed { if ($result = value($condition)) { return $value instanceof Closure ? $value($result) : $value; @@ -37,12 +123,7 @@ function when($condition, $value, $default = null) } if (!function_exists('classNamespace')) { - /** - * @param object|string $class - * - * @return string - */ - function classNamespace($class): string + function classNamespace(object|string $class): string { if (is_object($class)) { $class = get_class($class); @@ -55,13 +136,8 @@ function classNamespace($class): string if (!function_exists('isTrue')) { /** * Returns bool value of a value - * - * @param mixed $val - * @param bool $return_null - * - * @return bool|null */ - function isTrue($val, bool $return_null = false): ?bool + function isTrue(mixed $val, bool $return_null = false): ?bool { if ($val === null && $return_null) { return null; @@ -76,12 +152,13 @@ function isTrue($val, bool $return_null = false): ?bool if (!function_exists('instance')) { /** - * @param string|object $instance + * @phpstan-param T|class-string|null $instance * @param mixed ...$params + * @phpstan-return T|null * - * @return object|null + * @template T as object */ - function instance($instance, ...$params) + function instance(string|object|null $instance, mixed ...$params): ?object { if (is_object($instance)) { return $instance; @@ -98,12 +175,8 @@ function instance($instance, ...$params) if (!function_exists('class_basename')) { /** * Get the class "basename" of the given object / class. - * - * @param string|object $class - * - * @return string */ - function class_basename($class) + function class_basename(string|object $class): string { $class = is_object($class) ? get_class($class) : $class; @@ -115,9 +188,7 @@ function class_basename($class) /** * Returns all traits used by a trait and its traits. * - * @param string $trait - * - * @return array + * @return string[] */ function trait_uses_recursive(string $trait): array { @@ -125,7 +196,7 @@ function trait_uses_recursive(string $trait): array return []; } - foreach ((array)$traits as $trt) { + foreach ($traits as $trt) { $traits += trait_uses_recursive($trt); } @@ -133,18 +204,23 @@ function trait_uses_recursive(string $trait): array } } +if (!function_exists('does_trait_use')) { + function does_trait_use(string $class, string $trait): bool + { + return isset(trait_uses_recursive($class)[$trait]); + } +} + if (!function_exists('class_uses_recursive')) { /** * Returns all traits used by a class, its parent classes and trait of their traits. * - * @param object|string $class - * - * @return array + * @return string[] */ - function class_uses_recursive($class): array + function class_uses_recursive(object|string $class): array { if (is_object($class)) { - $class = get_class($class); + $class = $class::class; } $results = []; @@ -156,3 +232,135 @@ function class_uses_recursive($class): array return array_unique($results); } } + + +if (!function_exists('remoteStaticCall')) { + /** + * Returns result of an object's method if it exists in the object. + */ + function remoteStaticCall(object|string|null $class, string $method, mixed ...$params): mixed + { + if (!$class) { + return null; + } + + if ((is_object($class) || class_exists($class)) && method_exists($class, $method)) { + return $class::$method(...$params); + } + + return null; + } +} + +if (!function_exists('remoteStaticCall')) { + /** + * Returns result of an object's method if it exists in the object or trow exception. + */ + function remoteStaticCallOrTrow(object|string|null $class, string $method, mixed ...$params): mixed + { + if (!$class) { + throw new RuntimeException('Target Class is absent'); + } + + if ((is_object($class) || class_exists($class)) && method_exists($class, $method)) { + return $class::$method(...$params); + } + + $strClass = is_object($class) ? $class::class : $class; + throw new \Php\Support\Exceptions\MissingMethodException("$strClass::$method"); + } +} + +if (!function_exists('remoteCall')) { + /** + * Returns result of an object's method if it exists in the object. + */ + function remoteCall(?object $class, string $method, mixed ...$params): mixed + { + if (!$class) { + return null; + } + if (method_exists($class, $method)) { + return $class->$method(...$params); + } + + return null; + } +} + +if (!function_exists('attributeToGetterMethod')) { + /** + * Returns getter-method's name or null by an attribute + */ + function attributeToGetterMethod(string $attribute): string + { + return 'get' . ucfirst($attribute); + } +} + +if (!function_exists('attributeToSetterMethod')) { + /** + * Returns getter-method's name or null by an attribute + */ + function attributeToSetterMethod(string $attribute): string + { + return 'set' . ucfirst($attribute); + } +} + +if (!function_exists('findGetterMethod')) { + /** + * Returns getter-method's name or null by an attribute + */ + function findGetterMethod(object $instance, string $attribute): ?string + { + if (method_exists($instance, $method = attributeToGetterMethod($attribute))) { + return $method; + } + + return null; + } +} + +if (!function_exists('findSetterMethodByProp')) { + /** + * Returns getter-method's name or null by an attribute + */ + function findSetterMethodByProp(object $instance, string $attribute): ?string + { + if (method_exists($instance, $method = attributeToSetterMethod($attribute))) { + return $method; + } + + return null; + } +} + +if (!function_exists('public_property_exists')) { + /** + * Returns existing public method (name) or null if missing + */ + function public_property_exists(object $instance, string $attribute): ?string + { + $property = Str::toLowerCamel($attribute); + $vars = get_object_vars($instance); + + return array_key_exists($property, $vars) ? $property : null; + } +} + + +if (!function_exists('getPropertyValue')) { + /** + * Returns a value from public property or null + */ + function getPropertyValue(object $instance, string $attribute): mixed + { + $property = public_property_exists($instance, $attribute); + if ($property) { + return $instance->$property; + } + + return null; + } +} diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index 1d9e004..0624083 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -4,29 +4,73 @@ namespace Php\Support\Helpers; +use Closure; use ArrayAccess; +use ArrayObject; use JsonSerializable; use Php\Support\Interfaces\Arrayable; use Php\Support\Interfaces\Jsonable; +use Php\Support\Structures\Collections\ReadableCollection; use Traversable; +use function array_diff_assoc; +use function array_merge; +use function array_rand; +use function array_search; +use function array_shift; +use function array_unique; +use function array_unshift; +use function array_values; +use function explode; +use function func_num_args; +use function is_array; +use function is_int; +use function is_numeric; +use function is_object; +use function iterator_to_array; +use function mb_substr; +use function str_contains; +use function str_replace; + /** - * Class Arr - * - * @package Php\Support\Helpers + * @template TKey of array-key + * @template T */ class Arr { + /** + * Collapse an array of arrays into a single array. + * + * @param iterable $array + * @return array + */ + public static function collapse(iterable $array): array + { + $results = []; + + foreach ($array as $values) { + if ($values instanceof ReadableCollection) { + $values = $values->all(); + } elseif (!is_array($values)) { + continue; + } + + $results[] = $values; + } + + return array_merge([], ...$results); + } + /** * Remove one element from array by value * - * @param array $array + * @param array $array * @param mixed $val If $val is a string, the comparison is done in a case-sensitive manner. * @param bool $reindex * - * @return string|int|null Index of removed element or null if don't exist + * @return string|int|null Index of removed element or null if it don't exist */ - public static function removeByValue(array &$array, $val, $reindex = false) + public static function removeByValue(array &$array, mixed $val, bool $reindex = false): string|int|null { if (($key = array_search($val, $array, false)) !== false) { unset($array[$key]); @@ -42,33 +86,31 @@ public static function removeByValue(array &$array, $val, $reindex = false) * * @param mixed $items * - * @return array + * @return T[]|array */ - public static function toArray($items): array + public static function toArray(mixed $items): array { if (is_array($items)) { - $res = $items; - } else { - if ($items instanceof Arrayable) { - $res = $items->toArray(); - } else { - if ($items instanceof Jsonable) { - $res = Json::decode($items->toJson()); - } else { - if ($items instanceof JsonSerializable) { - $res = $items->jsonSerialize(); - } else { - if ($items instanceof Traversable) { - $res = iterator_to_array($items); - } else { - $res = (array)$items; - } - } - } - } + return $items; } - return $res; + if ($items instanceof Arrayable) { + return $items->toArray(); + } + + if ($items instanceof Traversable) { + return iterator_to_array($items); + } + if ($items instanceof Jsonable) { + $res = Json::decode($items->toJson()); + return is_array($res) ? $res : []; + } + + if ($items instanceof JsonSerializable) { + return (array)$items->jsonSerialize(); + } + + return (array)$items; } /** @@ -78,7 +120,7 @@ public static function toArray($items): array * * @return array|mixed|null */ - public static function dataToArray($items) + public static function dataToArray(mixed $items): mixed { if (is_object($items)) { if ($items instanceof JsonSerializable) { @@ -118,14 +160,14 @@ public static function dataToArray($items) } /** - * @param array $res array to be merged to - * @param array $b array to be merged from. You can specify additional + * @param array $res array to be merged to + * @param array $b array to be merged from. You can specify additional * arrays via third argument, fourth argument etc. * @param bool $replaceArray Replace or Add values into Array, if key existed. * - * @return array the merged array (the original arrays are not changed.) + * @return array the merged array (the original arrays are not changed.) */ - public static function merge($res, $b, $replaceArray = true): array + public static function merge(array $res, array $b, bool $replaceArray = true): array { foreach ($b as $key => $val) { if (is_int($key)) { @@ -149,7 +191,7 @@ public static function merge($res, $b, $replaceArray = true): array /** * Changes PHP array to default Postgres array format * - * @param array $array + * @param array $array * * @return string */ @@ -162,12 +204,30 @@ public static function toPostgresArray(array $array): string return str_replace(['[', ']', '"'], ['{', '}', ''], $json); } + /** + * @param int[] $array + * @return ?string + */ + public static function toPostgresPoint(array $array): ?string + { + if (count($array) !== 2) { + return null; + } + + [ + $x, + $y, + ] = $array; + + return '(' . $x . ',' . $y . ')'; + } + /** * Remove named keys from arrays * - * @param array $array + * @param array $array * - * @return array + * @return array */ public static function toIndexedArray(array $array): array { @@ -186,13 +246,25 @@ public static function toIndexedArray(array $array): array * * @param string|null $s * @param int $start - * @param null $end + * @param ?int $end + * @param array{string,string} $braces * - * @return array + * @return float[] */ - public static function fromPostgresArray(?string $s, int $start = 0, &$end = null): array - { - if (empty($s) || $s[0] !== '{') { + public static function fromPostgresArrayWithBraces( + ?string $s, + int $start = 0, + ?int &$end = null, + array $braces = [ + '{', + '}', + ] + ): array { + [ + $braceOpen, + $braceClose, + ] = $braces; + if (empty($s) || $s[0] !== $braceOpen) { return []; } @@ -204,14 +276,14 @@ public static function fromPostgresArray(?string $s, int $start = 0, &$end = nul for ($i = $start + 1; $i < $len; $i++) { $ch = $s[$i]; - if (!$string && $ch === '}') { + if (!$string && $ch === $braceClose) { if ($v !== '' || !empty($return)) { $return[] = $v; } $end = $i; break; } else { - if (!$string && $ch === '{') { + if (!$string && $ch === $braceOpen) { $v = self::fromPostgresArray($s, (int)$i, $i); } else { if (!$string && $ch === ',') { @@ -250,16 +322,53 @@ public static function fromPostgresArray(?string $s, int $start = 0, &$end = nul return $return; } + /** + * @param string|null $s + * @param int $start + * @param ?int $end + * + * @return float[] + */ + public static function fromPostgresArray(?string $s, int $start = 0, ?int &$end = null): array + { + return static::fromPostgresArrayWithBraces($s, $start, $end, ['{', '}']); + } + + /** + * @param ?string $value + * + * @return ?array{float,float} + */ + public static function fromPostgresPoint(?string $value): ?array + { + if (empty($value)) { + return null; + } + + $string = mb_substr($value, 1, -1); + if (empty($string)) { + return null; + } + + [ + $x, + $y, + ] = explode(',', $string); + return [ + (float)$x, + (float)$y, + ]; + } + /** * Get an item from an array using "dot" notation. * * @param mixed $array - * @param null|string $key + * @param string|int|null $key * @param mixed $default - * - * @return mixed + * @param non-empty-string $separator */ - public static function get($array, ?string $key, $default = null) + public static function get(mixed $array, string|int|null $key, mixed $default = null, string $separator = '.'): mixed { if (!static::accessible($array)) { return value($default); @@ -273,11 +382,11 @@ public static function get($array, ?string $key, $default = null) return $array[$key]; } - if (strpos($key, '.') === false) { + if (is_int($key) || !str_contains($key, $separator)) { return $array[$key] ?? value($default); } - foreach (explode('.', $key) as $segment) { + foreach (explode($separator, $key) as $segment) { if (static::accessible($array) && static::exists($array, $segment)) { $array = $array[$segment]; } else { @@ -295,7 +404,7 @@ public static function get($array, ?string $key, $default = null) * * @return bool */ - public static function accessible($value): bool + public static function accessible(mixed $value): bool { return is_array($value) || $value instanceof ArrayAccess; } @@ -303,12 +412,12 @@ public static function accessible($value): bool /** * Determine if the given key exists in the provided array. * - * @param ArrayAccess|array $array + * @param ArrayAccess|array $array * @param string|int $key * * @return bool */ - public static function exists($array, $key): bool + public static function exists(ArrayAccess|array $array, string|int $key): bool { if ($array instanceof ArrayAccess) { return $array->offsetExists($key); @@ -320,12 +429,12 @@ public static function exists($array, $key): bool /** * Check if an item or items exist in an array using "dot" notation. * - * @param ArrayAccess|array $array - * @param string|array $keys - * + * @param ArrayAccess|array $array + * @param string|string[] $keys + * @param non-empty-string $separator * @return bool */ - public static function has($array, $keys): bool + public static function has(ArrayAccess|array $array, string|array $keys, string $separator = '.'): bool { $keys = (array)$keys; @@ -340,7 +449,7 @@ public static function has($array, $keys): bool continue; } - foreach (explode('.', $key) as $segment) { + foreach (explode($separator, $key) as $segment) { if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { $subKeyArray = $subKeyArray[$segment]; } else { @@ -357,19 +466,20 @@ public static function has($array, $keys): bool * * If no key is given to the method, the entire array will be replaced. * - * @param ?array $array + * @param array|ArrayObject|array $array + * @param-out array|ArrayObject|array $array * @param string $key * @param mixed $value - * - * @return array|null + * @param non-empty-string $separator + * @return T[]|array|ArrayObject */ - public static function set(&$array, string $key, $value): ?array - { - if ($array === null) { - return $array; - } - - $keys = explode('.', $key); + public static function set( + array|ArrayObject &$array, + string $key, + mixed $value, + string $separator = '.' + ): array|ArrayObject { + $keys = explode($separator, $key); while (count($keys) > 1) { $key = array_shift($keys); @@ -389,12 +499,12 @@ public static function set(&$array, string $key, $value): ?array /** * Remove one or many array items from a given array using "dot" notation. * - * @param array $array - * @param array|string $keys + * @param array|ArrayObject $array + * @param string[]|string $keys * * @return void */ - public static function remove(&$array, $keys): void + public static function remove(array|ArrayObject &$array, array|string $keys): void { $original = &$array; $keys = (array)$keys; @@ -435,36 +545,32 @@ public static function remove(&$array, $keys): void * Key = search value * Value = replace value * - * @param array $array - * @param array $replace + * @param array $array + * @param array $replace * - * @return array + * @return array */ public static function replaceByTemplate(array $array, array $replace): array { - $res = []; - foreach ($array as $key => $item) { - $res[$key] = static::itemReplaceByTemplate($item, $replace); - } - return $res; + return array_map(static fn($item) => self::itemReplaceByTemplate($item, $replace), $array); } /** * Replace templates into item * * @param mixed $item - * @param array $replace + * @param array $replace * - * @return array|mixed + * @return string|string[]|mixed */ - private static function itemReplaceByTemplate($item, array $replace) + private static function itemReplaceByTemplate(mixed $item, array $replace): mixed { if (is_array($item)) { - $item = self::replaceByTemplate($item, $replace); - } else { - if (is_string($item)) { - $item = Str::replaceByTemplate($item, $replace); - } + return self::replaceByTemplate($item, $replace); + } + + if (is_string($item)) { + return Str::replaceByTemplate($item, $replace); } return $item; @@ -473,9 +579,9 @@ private static function itemReplaceByTemplate($item, array $replace) /** * Find duplicates into an array * - * @param array $array + * @param array $array * - * @return array + * @return array */ public static function duplicates(array $array): array { @@ -485,10 +591,10 @@ public static function duplicates(array $array): array /** * Fill a keyed array by values from another array * - * @param array $keys - * @param array $values + * @param array $keys + * @param array $values * - * @return array + * @return array */ public static function fillKeysByValues(array $keys, array $values): array { @@ -500,4 +606,85 @@ public static function fillKeysByValues(array $keys, array $values): array return $result; } + + /** + * Push an item onto the beginning of an array. + * + * @param array $array + * @param mixed $value + * @param string|int|null $key + * @return array + */ + public static function prepend(array $array, mixed $value, string|int|null $key = null): array + { + if (func_num_args() === 2) { + array_unshift($array, $value); + } else { + $array = [$key => $value] + $array; + } + + return $array; + } + + /** + * Get one or a specified number of random values from an array. + * + * @param array $array + * @param int|null $number + * @param bool $preserveKeys + * @return T[]|array + * + * @throws \InvalidArgumentException + */ + public static function random(array $array, ?int $number = null, bool $preserveKeys = false): mixed + { + $requested = $number ?? 1; + + $count = count($array); + + if ($requested > $count) { + throw new \InvalidArgumentException( + "You requested {$requested} items, but there are only {$count} items available." + ); + } + + if ($number === null) { + return $array[array_rand($array)]; + } + + if ($number === 0) { + return []; + } + + $keys = array_rand($array, $number); + + $results = []; + + if ($preserveKeys) { + foreach ((array)$keys as $key) { + $results[$key] = $array[$key]; + } + } else { + foreach ((array)$keys as $key) { + $results[] = $array[$key]; + } + } + + return $results; + } + + /** + * @template U + * @param array $elements + * @param Closure $func + * @phpstan-param Closure(array): U[] $func + * @return array + */ + public static function map(array $elements, Closure $func): array + { + $keys = array_keys($elements); + $map = array_map($func, $elements, $keys); + + return array_combine($keys, $map); + } } diff --git a/src/Helpers/B64.php b/src/Helpers/B64.php index bff24ce..4d0d2b0 100644 --- a/src/Helpers/B64.php +++ b/src/Helpers/B64.php @@ -16,14 +16,14 @@ class B64 * * @var string */ - private const LAST_THREE_STANDARD = '+/='; + private const string LAST_THREE_STANDARD = '+/='; /** * The last three characters from the alphabet of the URL-safe implementation * * @var string */ - private const LAST_THREE_URL_SAFE = '-_~'; + private const string LAST_THREE_URL_SAFE = '-_~'; /** * Encodes the supplied data to Base64 diff --git a/src/Helpers/Bit.php b/src/Helpers/Bit.php index 7e7fe4b..aac8780 100644 --- a/src/Helpers/Bit.php +++ b/src/Helpers/Bit.php @@ -15,27 +15,27 @@ class Bit { /** - * Remove bit from $value + * Remove a bit from $value * * @param int|string $value * @param int $bit * * @return int */ - public static function removeFlag($value, int $bit): int + public static function removeFlag(int|string $value, int $bit): int { return static::toInt($value) & ~$bit; } /** - * Set bit to $value + * Set a bit to $value * * @param int|string $value * @param int $bit * * @return int */ - public static function addFlag($value, int $bit): int + public static function addFlag(int|string $value, int $bit): int { return static::toInt($value) | $bit; } @@ -45,13 +45,13 @@ public static function addFlag($value, int $bit): int * * @return int */ - protected static function toInt($value): int + protected static function toInt(int|string $value): int { if (is_string($value)) { return (int)bindec($value); } - return (int)$value; + return $value; } /** @@ -62,7 +62,7 @@ protected static function toInt($value): int * * @return bool */ - public static function checkFlag($value, int $bit): bool + public static function checkFlag(int|string $value, int $bit): bool { return (static::toInt($value) & $bit) > 0; } @@ -71,7 +71,7 @@ public static function checkFlag($value, int $bit): bool /** * Check a bit is existing in flag`s list * - * @param array $list + * @param int[] $list * @param int $bit * * @return bool @@ -84,13 +84,13 @@ public static function exist(array $list, int $bit): bool /** * Return value of sum of all bits in list * - * @param array $list + * @param int[] $list * * @return int */ public static function grant(array $list): int { - return array_reduce($list, fn($prev, $next) => $prev | $next, 0); + return array_reduce($list, fn(int $prev, int $next) => $prev | $next, 0); } /** diff --git a/src/Helpers/Json.php b/src/Helpers/Json.php index 545ec1d..5b1457e 100644 --- a/src/Helpers/Json.php +++ b/src/Helpers/Json.php @@ -7,10 +7,6 @@ use function json_decode; use function json_encode; -/** - * Class Json - * @package Php\Support\Helpers - */ class Json { /** @@ -41,15 +37,16 @@ public static function htmlEncode($value): ?string * @param int $options the encoding options. For more details please refer to * . Default is * `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. - * @param int $depth + * @param int<1, max> $depth * * @return string|null */ - public static function encode($value, $options = 320, int $depth = 512): ?string + public static function encode($value, int $options = 320, int $depth = 512): ?string { $value = Arr::dataToArray($value); $json = json_encode($value, $options, $depth); + return $json ?: null; } @@ -59,16 +56,22 @@ public static function encode($value, $options = 320, int $depth = 512): ?string * @param null|string $json the JSON string to be decoded * @param bool $asArray whether to return objects in terms of associative arrays. * @param int $options - * @param int $depth + * @param int<1, max> $depth * * @return mixed|null */ - public static function decode(?string $json, $asArray = true, int $options = 0, int $depth = 512) + public static function decode(?string $json, bool $asArray = true, int $options = 0, int $depth = 512) { if ($json === null || $json === '') { return null; } + // @see https://www.php.net/manual/en/json.constants.php#constant.json-invalid-utf8-ignore + $validateOpts = Bit::checkFlag($options, JSON_INVALID_UTF8_IGNORE) ? JSON_INVALID_UTF8_IGNORE : 0; + if (!json_validate($json, $depth, $validateOpts)) { + return null; + } + return json_decode($json, $asArray, $depth, $options); } } diff --git a/src/Helpers/Number.php b/src/Helpers/Number.php new file mode 100644 index 0000000..ad0e2f2 --- /dev/null +++ b/src/Helpers/Number.php @@ -0,0 +1,33 @@ += 9007199254740991 || $value <= -9007199254740991)) { + return (string)$value; + } + + return is_numeric($value) ? (int)$value : (string)$value; + } + + public static function isInteger(mixed $value): bool + { + return is_int($value) || (string)$value === (string)(int)($value); + } +} diff --git a/src/Helpers/Str.php b/src/Helpers/Str.php index 8027707..743b4f8 100644 --- a/src/Helpers/Str.php +++ b/src/Helpers/Str.php @@ -11,32 +11,17 @@ use function preg_replace; use function trim; -/** - * Class Str - * - * @package Php\Support\Helpers - */ class Str { /** * The cache of delimited converted-cased words. * - * @var array + * @var array */ protected static array $delimitedCache = []; - /** - * The cache of camel-cased words. - * - * @var array - */ - protected static array $camelCache = []; /** * Converts a string to snake_case - * - * @param string $str - * - * @return string */ public static function toSnake(string $str): string { @@ -45,11 +30,6 @@ public static function toSnake(string $str): string /** * Converts a string to delimited.snake.case (in this case `del = '.'`) - * - * @param string $str - * @param string $delimiter - * - * @return string */ public static function toDelimited(string $str, string $delimiter): string { @@ -59,12 +39,6 @@ public static function toDelimited(string $str, string $delimiter): string /** * Converts a string to SCREAMING.DELIMITED.SNAKE.CASE (in this case `del = '.'; screaming = true`) or * delimited.snake.case (in this case `del = '.'; screaming = false`) - * - * @param string $str - * @param string $delimiter - * @param bool $screaming - * - * @return string */ public static function toScreamingDelimited(string $str, string $delimiter, bool $screaming): string { @@ -150,11 +124,6 @@ public static function removeMultiSpace(string $str): string return is_string($res) ? $res : $str; } - /** - * @param string $str - * - * @return string - */ private static function addWordBoundariesToNumbers(string $str): string { $res = preg_replace('/([a-zA-Z])(\d+)([a-zA-Z]?)/u', '$1 $2 $3', $str); @@ -163,10 +132,6 @@ private static function addWordBoundariesToNumbers(string $str): string /** * Converts a string to SCREAMING_SNAKE_CASE - * - * @param string $str - * - * @return string */ public static function toScreamingSnake(string $str): string { @@ -175,10 +140,6 @@ public static function toScreamingSnake(string $str): string /** * Converts a string to kebab-case - * - * @param string $str - * - * @return string */ public static function toKebab(string $str): string { @@ -187,10 +148,6 @@ public static function toKebab(string $str): string /** * Converts a string to CamelCase - * - * @param string $str - * - * @return string */ public static function toCamel(string $str): string { @@ -199,11 +156,6 @@ public static function toCamel(string $str): string /** * Converts a string to CamelCase - * - * @param string $str - * @param bool $initCase - * - * @return string */ public static function toCamelInitCase(string $str, bool $initCase): string { @@ -257,10 +209,6 @@ public static function toCamelInitCase(string $str, bool $initCase): string /** * Converts a string to lowerCamelCase - * - * @param string $str - * - * @return string */ public static function toLowerCamel(string $str): string { @@ -273,13 +221,6 @@ public static function toLowerCamel(string $str): string /** * Replace substr by start and finish indents - * - * @param string $str - * @param int $from_start - * @param int $from_end - * @param string $toStr - * - * @return string */ public static function replaceStrTo(string $str, int $from_start, int $from_end, string $toStr = '*'): string { @@ -305,22 +246,132 @@ public static function replaceStrTo(string $str, int $from_start, int $from_end, * Value = replace value * * @param string $str - * @param array $replace + * @param array $replace * - * @return mixed + * @return string|string[] */ - public static function replaceByTemplate(string $str, array $replace) + public static function replaceByTemplate(string $str, array $replace): array|string { return str_replace(array_keys($replace), array_values($replace), $str); } + public static function isRegExp(string $regex): bool + { + return !empty($regex) && @preg_match($regex, '') !== false; + } + + /** + * Truncate a string to a specified length without cutting a word off + */ + public static function truncate(string $str, int $length, string $append = '...'): string + { + $ret = mb_substr($str, 0, $length); + $last_space = mb_strrpos($ret, ' '); + + if ($last_space !== false && $str !== $ret) { + $ret = mb_substr($ret, 0, $last_space); + } + + if ($ret !== $str) { + $ret .= $append; + } + + return $ret; + } + + /** - * @param string $regex + * Generate a string safe for use in URLs from any given string. + * + * @param string $str + * @param string $separator + * @param bool $firstLetterOnly + * + * @return string + */ + public static function slugify(string $str, string $separator = '-', bool $firstLetterOnly = false): string + { + return self::slugifyWithFormat($str, $separator, '([^a-z\d]+)', $firstLetterOnly); + } + + public static function slugifyWithFormat( + string $str, + string $separator = '-', + string $format = '([^a-z\d]+)', + bool $firstLetterOnly = false + ): string { + $slug = preg_replace("/$format/", $separator, mb_strtolower(self::removeAccents($str))); + if (empty($slug)) { + return ''; + } + + if ($firstLetterOnly) { + $digits = [ + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + ]; + + if (is_numeric(mb_substr($slug, 0, 1))) { + $slug = $digits[mb_substr($slug, 0, 1)] . mb_substr($slug, 1); + } + } + + return $slug; + } + + + /** + * Checks to see if a string is utf8 encoded. + * + * NOTE: This function checks for 5-Byte sequences, UTF8 + * has Bytes Sequences with a maximum length of 4. + * + * Written by Tony Ferrara + * + * @param string $string The string to be checked * * @return bool */ - public static function isRegExp(string $regex): bool + public static function seemsUTF8(string $string): bool + { + return URLify::seemsUTF8($string); + } + + /** + * Converts all accent characters to ASCII characters. + */ + public static function removeAccents(string $str, string $language = ''): string + { + if (!preg_match('/[\x80-\xff]/', $str)) { + return $str; + } + + return URLify::downcode($str, $language); + } + + public static function trimPrefix(string $str, string $prefix): string + { + if (str_starts_with($str, $prefix)) { + return mb_substr($str, mb_strlen($prefix)); + } + + return $str; + } + + public static function trimSuffix(string $str, string $suffix): string { - return empty($regex) ? false : @preg_match($regex, '') !== false; + if (str_ends_with($str, $suffix)) { + return mb_substr($str, 0, mb_strlen($str) - mb_strlen($suffix)); + } + + return $str; } } diff --git a/src/Helpers/URLify.php b/src/Helpers/URLify.php new file mode 100644 index 0000000..4c390bf --- /dev/null +++ b/src/Helpers/URLify.php @@ -0,0 +1,794 @@ + + */ + private static array $map = []; + + + /** + * The character list as a string. + * + * @see https://github.com/jbroadway/urlify/blob/master/URLify.php + */ + private static string $chars = ''; + + /** + * The character list as a regular expression. + * + * @see https://github.com/jbroadway/urlify/blob/master/URLify.php + */ + private static string $regex = ''; + + /** + * Map of special non-ASCII characters and suitable ASCII replacement + * characters. + * + * Part of the URLify.php Project + * + * @see https://github.com/jbroadway/urlify/blob/master/URLify.php + * + * @var array> + */ + public static array $maps = [ + 'de' => [/* German */ + 'Ä' => 'Ae', + 'Ö' => 'Oe', + 'Ü' => 'Ue', + 'ä' => 'ae', + 'ö' => 'oe', + 'ü' => 'ue', + 'ß' => 'ss', + 'ẞ' => 'SS', + ], + 'latin' => [ + 'À' => 'A', + 'Á' => 'A', + 'Â' => 'A', + 'Ã' => 'A', + 'Ä' => 'A', + 'Å' => 'A', + 'Ă' => 'A', + 'Æ' => 'AE', + 'Ç' => + 'C', + 'È' => 'E', + 'É' => 'E', + 'Ê' => 'E', + 'Ë' => 'E', + 'Ì' => 'I', + 'Í' => 'I', + 'Î' => 'I', + 'Ï' => 'I', + 'Ð' => 'D', + 'Ñ' => 'N', + 'Ò' => 'O', + 'Ó' => 'O', + 'Ô' => 'O', + 'Õ' => 'O', + 'Ö' => + 'O', + 'Ő' => 'O', + 'Ø' => 'O', + 'Ș' => 'S', + 'Ț' => 'T', + 'Ù' => 'U', + 'Ú' => 'U', + 'Û' => 'U', + 'Ü' => 'U', + 'Ű' => 'U', + 'Ý' => 'Y', + 'Þ' => 'TH', + 'ß' => 'ss', + 'à' => 'a', + 'á' => 'a', + 'â' => 'a', + 'ã' => 'a', + 'ä' => + 'a', + 'å' => 'a', + 'ă' => 'a', + 'æ' => 'ae', + 'ç' => 'c', + 'è' => 'e', + 'é' => 'e', + 'ê' => 'e', + 'ë' => 'e', + 'ì' => 'i', + 'í' => 'i', + 'î' => 'i', + 'ï' => 'i', + 'ð' => 'd', + 'ñ' => 'n', + 'ò' => 'o', + 'ó' => + 'o', + 'ô' => 'o', + 'õ' => 'o', + 'ö' => 'o', + 'ő' => 'o', + 'ø' => 'o', + 'ș' => 's', + 'ț' => 't', + 'ù' => 'u', + 'ú' => 'u', + 'û' => 'u', + 'ü' => 'u', + 'ű' => 'u', + 'ý' => 'y', + 'þ' => 'th', + 'ÿ' => 'y', + ], + 'latin_symbols' => [ + '©' => '(c)', + '®' => '(r)', + ], + 'el' => [/* Greek */ + 'α' => 'a', + 'β' => 'b', + 'γ' => 'g', + 'δ' => 'd', + 'ε' => 'e', + 'ζ' => 'z', + 'η' => 'h', + 'θ' => '8', + 'ι' => 'i', + 'κ' => 'k', + 'λ' => 'l', + 'μ' => 'm', + 'ν' => 'n', + 'ξ' => '3', + 'ο' => 'o', + 'π' => 'p', + 'ρ' => 'r', + 'σ' => 's', + 'τ' => 't', + 'υ' => 'y', + 'φ' => 'f', + 'χ' => 'x', + 'ψ' => 'ps', + 'ω' => 'w', + 'ά' => 'a', + 'έ' => 'e', + 'ί' => 'i', + 'ό' => 'o', + 'ύ' => 'y', + 'ή' => 'h', + 'ώ' => 'w', + 'ς' => 's', + 'ϊ' => 'i', + 'ΰ' => 'y', + 'ϋ' => 'y', + 'ΐ' => 'i', + 'Α' => 'A', + 'Β' => 'B', + 'Γ' => 'G', + 'Δ' => 'D', + 'Ε' => 'E', + 'Ζ' => 'Z', + 'Η' => 'H', + 'Θ' => '8', + 'Ι' => 'I', + 'Κ' => 'K', + 'Λ' => 'L', + 'Μ' => 'M', + 'Ν' => 'N', + 'Ξ' => '3', + 'Ο' => 'O', + 'Π' => 'P', + 'Ρ' => 'R', + 'Σ' => 'S', + 'Τ' => 'T', + 'Υ' => 'Y', + 'Φ' => 'F', + 'Χ' => 'X', + 'Ψ' => 'PS', + 'Ω' => 'W', + 'Ά' => 'A', + 'Έ' => 'E', + 'Ί' => 'I', + 'Ό' => 'O', + 'Ύ' => 'Y', + 'Ή' => 'H', + 'Ώ' => 'W', + 'Ϊ' => 'I', + 'Ϋ' => 'Y', + ], + 'tr' => [/* Turkish */ + 'ş' => 's', + 'Ş' => 'S', + 'ı' => 'i', + 'İ' => 'I', + 'ç' => 'c', + 'Ç' => 'C', + 'ü' => 'u', + 'Ü' => 'U', + 'ö' => 'o', + 'Ö' => 'O', + 'ğ' => 'g', + 'Ğ' => 'G', + ], + 'ru' => [/* Russian */ + 'а' => 'a', + 'б' => 'b', + 'в' => 'v', + 'г' => 'g', + 'д' => 'd', + 'е' => 'e', + 'ё' => 'yo', + 'ж' => 'zh', + 'з' => 'z', + 'и' => 'i', + 'й' => 'j', + 'к' => 'k', + 'л' => 'l', + 'м' => 'm', + 'н' => 'n', + 'о' => 'o', + 'п' => 'p', + 'р' => 'r', + 'с' => 's', + 'т' => 't', + 'у' => 'u', + 'ф' => 'f', + 'х' => 'h', + 'ц' => 'c', + 'ч' => 'ch', + 'ш' => 'sh', + 'щ' => 'sh', + 'ъ' => '', + 'ы' => 'y', + 'ь' => '', + 'э' => 'e', + 'ю' => 'yu', + 'я' => 'ya', + 'А' => 'A', + 'Б' => 'B', + 'В' => 'V', + 'Г' => 'G', + 'Д' => 'D', + 'Е' => 'E', + 'Ё' => 'Yo', + 'Ж' => 'Zh', + 'З' => 'Z', + 'И' => 'I', + 'Й' => 'J', + 'К' => 'K', + 'Л' => 'L', + 'М' => 'M', + 'Н' => 'N', + 'О' => 'O', + 'П' => 'P', + 'Р' => 'R', + 'С' => 'S', + 'Т' => 'T', + 'У' => 'U', + 'Ф' => 'F', + 'Х' => 'H', + 'Ц' => 'C', + 'Ч' => 'Ch', + 'Ш' => 'Sh', + 'Щ' => 'Sh', + 'Ъ' => '', + 'Ы' => 'Y', + 'Ь' => '', + 'Э' => 'E', + 'Ю' => 'Yu', + 'Я' => 'Ya', + '№' => '', + ], + 'uk' => [/* Ukrainian */ + 'Є' => 'Ye', + 'І' => 'I', + 'Ї' => 'Yi', + 'Ґ' => 'G', + 'є' => 'ye', + 'і' => 'i', + 'ї' => 'yi', + 'ґ' => 'g', + ], + 'cs' => [/* Czech */ + 'č' => 'c', + 'ď' => 'd', + 'ě' => 'e', + 'ň' => 'n', + 'ř' => 'r', + 'š' => 's', + 'ť' => 't', + 'ů' => 'u', + 'ž' => 'z', + 'Č' => 'C', + 'Ď' => 'D', + 'Ě' => 'E', + 'Ň' => 'N', + 'Ř' => 'R', + 'Š' => 'S', + 'Ť' => 'T', + 'Ů' => 'U', + 'Ž' => 'Z', + ], + 'pl' => [/* Polish */ + 'ą' => 'a', + 'ć' => 'c', + 'ę' => 'e', + 'ł' => 'l', + 'ń' => 'n', + 'ó' => 'o', + 'ś' => 's', + 'ź' => 'z', + 'ż' => 'z', + 'Ą' => 'A', + 'Ć' => 'C', + 'Ę' => 'e', + 'Ł' => 'L', + 'Ń' => 'N', + 'Ó' => 'O', + 'Ś' => 'S', + 'Ź' => 'Z', + 'Ż' => 'Z', + ], + 'ro' => [/* Romanian */ + 'ă' => 'a', + 'â' => 'a', + 'î' => 'i', + 'ș' => 's', + 'ț' => 't', + 'Ţ' => 'T', + 'ţ' => 't', + ], + 'lv' => [/* Latvian */ + 'ā' => 'a', + 'č' => 'c', + 'ē' => 'e', + 'ģ' => 'g', + 'ī' => 'i', + 'ķ' => 'k', + 'ļ' => 'l', + 'ņ' => 'n', + 'š' => 's', + 'ū' => 'u', + 'ž' => 'z', + 'Ā' => 'A', + 'Č' => 'C', + 'Ē' => 'E', + 'Ģ' => 'G', + 'Ī' => 'i', + 'Ķ' => 'k', + 'Ļ' => 'L', + 'Ņ' => 'N', + 'Š' => 'S', + 'Ū' => 'u', + 'Ž' => 'Z', + ], + 'lt' => [/* Lithuanian */ + 'ą' => 'a', + 'č' => 'c', + 'ę' => 'e', + 'ė' => 'e', + 'į' => 'i', + 'š' => 's', + 'ų' => 'u', + 'ū' => 'u', + 'ž' => 'z', + 'Ą' => 'A', + 'Č' => 'C', + 'Ę' => 'E', + 'Ė' => 'E', + 'Į' => 'I', + 'Š' => 'S', + 'Ų' => 'U', + 'Ū' => 'U', + 'Ž' => 'Z', + ], + 'vn' => [/* Vietnamese */ + 'Á' => 'A', + 'À' => 'A', + 'Ả' => 'A', + 'Ã' => 'A', + 'Ạ' => 'A', + 'Ă' => 'A', + 'Ắ' => 'A', + 'Ằ' => 'A', + 'Ẳ' => 'A', + 'Ẵ' => 'A', + 'Ặ' => 'A', + 'Â' => 'A', + 'Ấ' => 'A', + 'Ầ' => 'A', + 'Ẩ' => 'A', + 'Ẫ' => 'A', + 'Ậ' => 'A', + 'á' => 'a', + 'à' => 'a', + 'ả' => 'a', + 'ã' => 'a', + 'ạ' => 'a', + 'ă' => 'a', + 'ắ' => 'a', + 'ằ' => 'a', + 'ẳ' => 'a', + 'ẵ' => 'a', + 'ặ' => 'a', + 'â' => 'a', + 'ấ' => 'a', + 'ầ' => 'a', + 'ẩ' => 'a', + 'ẫ' => 'a', + 'ậ' => 'a', + 'É' => 'E', + 'È' => 'E', + 'Ẻ' => 'E', + 'Ẽ' => 'E', + 'Ẹ' => 'E', + 'Ê' => 'E', + 'Ế' => 'E', + 'Ề' => 'E', + 'Ể' => 'E', + 'Ễ' => 'E', + 'Ệ' => 'E', + 'é' => 'e', + 'è' => 'e', + 'ẻ' => 'e', + 'ẽ' => 'e', + 'ẹ' => 'e', + 'ê' => 'e', + 'ế' => 'e', + 'ề' => 'e', + 'ể' => 'e', + 'ễ' => 'e', + 'ệ' => 'e', + 'Í' => 'I', + 'Ì' => 'I', + 'Ỉ' => 'I', + 'Ĩ' => 'I', + 'Ị' => 'I', + 'í' => 'i', + 'ì' => 'i', + 'ỉ' => 'i', + 'ĩ' => 'i', + 'ị' => 'i', + 'Ó' => 'O', + 'Ò' => 'O', + 'Ỏ' => 'O', + 'Õ' => 'O', + 'Ọ' => 'O', + 'Ô' => 'O', + 'Ố' => 'O', + 'Ồ' => 'O', + 'Ổ' => 'O', + 'Ỗ' => 'O', + 'Ộ' => 'O', + 'Ơ' => 'O', + 'Ớ' => 'O', + 'Ờ' => 'O', + 'Ở' => 'O', + 'Ỡ' => 'O', + 'Ợ' => 'O', + 'ó' => 'o', + 'ò' => 'o', + 'ỏ' => 'o', + 'õ' => 'o', + 'ọ' => 'o', + 'ô' => 'o', + 'ố' => 'o', + 'ồ' => 'o', + 'ổ' => 'o', + 'ỗ' => 'o', + 'ộ' => 'o', + 'ơ' => 'o', + 'ớ' => 'o', + 'ờ' => 'o', + 'ở' => 'o', + 'ỡ' => 'o', + 'ợ' => 'o', + 'Ú' => 'U', + 'Ù' => 'U', + 'Ủ' => 'U', + 'Ũ' => 'U', + 'Ụ' => 'U', + 'Ư' => 'U', + 'Ứ' => 'U', + 'Ừ' => 'U', + 'Ử' => 'U', + 'Ữ' => 'U', + 'Ự' => 'U', + 'ú' => 'u', + 'ù' => 'u', + 'ủ' => 'u', + 'ũ' => 'u', + 'ụ' => 'u', + 'ư' => 'u', + 'ứ' => 'u', + 'ừ' => 'u', + 'ử' => 'u', + 'ữ' => 'u', + 'ự' => 'u', + 'Ý' => 'Y', + 'Ỳ' => 'Y', + 'Ỷ' => 'Y', + 'Ỹ' => 'Y', + 'Ỵ' => 'Y', + 'ý' => 'y', + 'ỳ' => 'y', + 'ỷ' => 'y', + 'ỹ' => 'y', + 'ỵ' => 'y', + 'Đ' => 'D', + 'đ' => 'd', + ], + 'ar' => [/* Arabic */ + 'أ' => 'a', + 'ب' => 'b', + 'ت' => 't', + 'ث' => 'th', + 'ج' => 'g', + 'ح' => 'h', + 'خ' => 'kh', + 'د' => 'd', + 'ذ' => 'th', + 'ر' => 'r', + 'ز' => 'z', + 'س' => 's', + 'ش' => 'sh', + 'ص' => 's', + 'ض' => 'd', + 'ط' => 't', + 'ظ' => 'th', + 'ع' => 'aa', + 'غ' => 'gh', + 'ف' => 'f', + 'ق' => 'k', + 'ك' => 'k', + 'ل' => 'l', + 'م' => 'm', + 'ن' => 'n', + 'ه' => 'h', + 'و' => 'o', + 'ي' => 'y', + ], + 'sr' => [/* Serbian */ + 'ђ' => 'dj', + 'ј' => 'j', + 'љ' => 'lj', + 'њ' => 'nj', + 'ћ' => 'c', + 'џ' => 'dz', + 'đ' => 'dj', + 'Ђ' => 'Dj', + 'Ј' => 'j', + 'Љ' => 'Lj', + 'Њ' => 'Nj', + 'Ћ' => 'C', + 'Џ' => 'Dz', + 'Đ' => 'Dj', + ], + 'az' => [/* Azerbaijani */ + 'ç' => 'c', + 'ə' => 'e', + 'ğ' => 'g', + 'ı' => 'i', + 'ö' => 'o', + 'ş' => 's', + 'ü' => 'u', + 'Ç' => 'C', + 'Ə' => 'E', + 'Ğ' => 'G', + 'İ' => 'I', + 'Ö' => 'O', + 'Ş' => 'S', + 'Ü' => 'U', + ], + 'fi' => [/* Finnish */ + 'ä' => 'a', + 'ö' => 'o', + ], + 'fa' => [ /* Farsi */ + 'آ' => 'aa', + 'ا' => 'a', + 'ب' => 'b', + 'پ' => 'p', + 'ت' => 't', + 'ث' => 'th', + 'ج' => 'j', + 'چ' => 'ch', + 'ح' => 'h', + 'خ' => 'kh', + 'د' => 'd', + 'ذ' => 'z', + 'ر' => 'r', + 'ز' => 'z', + 'ژ' => 'zh', + 'س' => 's', + 'ش' => 'sh', + 'ص' => 's', + 'ض' => 'z', + 'ط' => 't', + 'ظ' => 'th', + 'ع' => 'aa', + 'غ' => 'gh', + 'ف' => 'f', + 'ق' => 'gh', + 'ك' => 'k', + 'گ' => 'g', + 'ل' => 'l', + 'م' => 'm', + 'ن' => 'n', + 'ه' => 'h', + 'و' => 'o', + 'ي' => 'y', + 'ی' => 'y', + 'ِ' => 'e', + 'ُ' => 'o', + 'َ' => 'a', + ], + ]; + + + /** + * Transliterates characters to their ASCII equivalents. + * + * Part of the URLify.php Project + * + * @see https://github.com/jbroadway/urlify/blob/master/URLify.php + * + * @param string $text Text that might have not-ASCII characters + * @param string $language Specifies a priority for a specific language. + * + * @return string Filtered string with replaced "nice" characters + */ + public static function downcode(string $text, string $language = ''): string + { + self::initLanguageMap($language); + + if (self::seemsUTF8($text)) { + if (preg_match_all(self::$regex, $text, $matches)) { + for ($i = 0, $iMax = count($matches[0]); $i < $iMax; $i++) { + $char = $matches[0][$i]; + if (isset(self::$map[$char])) { + $text = str_replace($char, self::$map[$char], $text); + } + } + } + } else { + // Not a UTF-8 string so we assume its ISO-8859-1 + $search = "\x80\x83\x8a\x8e\x9a\x9e\x9f\xa2\xa5\xb5\xc0\xc1\xc2\xc3\xc4\xc5\xc7\xc8\xc9\xca\xcb\xcc\xcd"; + $search .= "\xce\xcf\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xe0\xe1\xe2\xe3\xe4\xe5\xe7\xe8\xe9"; + $search .= "\xea\xeb\xec\xed\xee\xef\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xff"; + $text = strtr($text, $search, 'EfSZszYcYuAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy'); + + // These latin characters should be represented by two characters so + // we can't use strtr + $complexSearch = [ + "\x8c", + "\x9c", + "\xc6", + "\xd0", + "\xde", + "\xdf", + "\xe6", + "\xf0", + "\xfe", + ]; + $complexReplace = [ + 'OE', + 'oe', + 'AE', + 'DH', + 'TH', + 'ss', + 'ae', + 'dh', + 'th', + ]; + $text = str_replace($complexSearch, $complexReplace, $text); + } + + return $text; + } + + + /** + * Checks to see if a string is utf8 encoded. + * + * NOTE: This function checks for 5-Byte sequences, UTF8 + * has Bytes Sequences with a maximum length of 4. + * + * Written by Tony Ferrara + * + * @param string $string The string to be checked + * + * @return bool + */ + public static function seemsUTF8(string $string): bool + { + if (function_exists('mb_check_encoding')) { + // If mb-string is available, this is significantly faster than using PHP regexps. + return mb_check_encoding($string, 'UTF-8'); + } + + // @codeCoverageIgnoreStart + return self::seemsUTF8Regex($string); + // @codeCoverageIgnoreEnd + } + + /** + * A non-Mb-string UTF-8 checker. + * + * @param string $string + * + * @return bool + */ + protected static function seemsUTF8Regex(string $string): bool + { + // Obtained from http://stackoverflow.com/a/11709412/430062 with permission. + $regex = '/( + [\xC0-\xC1] # Invalid UTF-8 Bytes + | [\xF5-\xFF] # Invalid UTF-8 Bytes + | \xE0[\x80-\x9F] # Overlong encoding of prior code point + | \xF0[\x80-\x8F] # Overlong encoding of prior code point + | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start + | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start + | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start + | (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle + | (? + * + * @see https://github.com/jbroadway/urlify/blob/master/URLify.php + */ + private static function initLanguageMap(string $language = ''): void + { + if (count(self::$map) > 0 && (($language === '') || ($language === self::$language))) { + return; + } + + // Is a specific map associated with $language? + if (isset(self::$maps[$language]) && is_array(self::$maps[$language])) { + // Move this map to end. This means it will have priority over others + $map = self::$maps[$language]; + unset(self::$maps[$language]); + self::$maps[$language] = $map; + } + + // Reset static vars + self::$language = $language; + self::$map = []; + self::$chars = ''; + + foreach (self::$maps as $map) { + foreach ($map as $orig => $conv) { + self::$map[$orig] = $conv; + self::$chars .= $orig; + } + } + + self::$regex = '/[' . self::$chars . ']/u'; + } +} diff --git a/src/Interfaces/Arrayable.php b/src/Interfaces/Arrayable.php index 331814c..d36184d 100644 --- a/src/Interfaces/Arrayable.php +++ b/src/Interfaces/Arrayable.php @@ -5,14 +5,13 @@ namespace Php\Support\Interfaces; /** - * Interface Arrayable - * - * @package Php\Support\Interfaces + * @template TKey of array-key + * @template TValue */ interface Arrayable { /** - * @return array + * @return array */ public function toArray(): array; } diff --git a/src/Storage.php b/src/Storage.php new file mode 100644 index 0000000..c976842 --- /dev/null +++ b/src/Storage.php @@ -0,0 +1,110 @@ + + * @mixin ArrayAccess + */ +class Storage implements ArrayAccess, Countable, JsonSerializable +{ + /** @var array */ + public private(set) array $data = []; + + /** + * @param array $init + */ + public function __construct(array $init = []) + { + $this->data = $init; + } + + public function set(string $key, mixed $value): void + { + Arr::set($this->data, $key, $value); + } + + public function remove(string $key): void + { + Arr::remove($this->data, $key); + } + + public function get(string $key, mixed $default = null): mixed + { + return Arr::get($this->data, $key, $default); + } + + public function exist(string $key): bool + { + return Arr::has($this->data, $key); + } + + public function __isset(string $name): bool + { + return $this->exist($name); + } + + public function __get(string $name): mixed + { + return $this->get($name); + } + + public function __set(string $name, mixed $value): void + { + $this->set($name, $value); + } + + public function __unset(string $name): void + { + $this->remove($name); + } + + public function offsetExists(mixed $offset): bool + { + return $this->exist($offset); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->set($offset, $value); + } + + public function offsetUnset(mixed $offset): void + { + $this->remove($offset); + } + + public function count(): int + { + return count($this->data); + } + + public function __toString(): string + { + return (string)Json::encode($this->data); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return $this->data; + } +} diff --git a/src/Structures/Collections/ArrayCollection.php b/src/Structures/Collections/ArrayCollection.php new file mode 100644 index 0000000..ed6e8de --- /dev/null +++ b/src/Structures/Collections/ArrayCollection.php @@ -0,0 +1,868 @@ + + * + * @psalm-consistent-constructor + */ +class ArrayCollection implements Collection, Stringable +{ + /** + * @var array + * @psalm-var array + */ + protected array $elements = []; + + /** + * @param array|Collection $elements + * @psalm-param array $elements + */ + public function __construct(array|Collection $elements = []) + { + if ($elements instanceof Collection) { + $this->elements = $elements->all(); + } else { + $this->elements = $elements; + } + } + + /** + * {@inheritDoc} + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * {@inheritDoc} + */ + public function all(): array + { + return $this->elements; + } + + /** + * {@inheritDoc} + * + * @return Traversable + * @psalm-return Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->elements); + } + + /** + * @param TKey $offset + * @return bool + */ + public function offsetExists(mixed $offset): bool + { + return $this->containsKey($offset); + } + + /** + * @param TKey $offset + * @return T|null + */ + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + /** + * @param int|string|null $offset + * @param T $value + * @psalm-param TKey|null $offset + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (!isset($offset)) { + $this->add($value); + + return; + } + + $this->set($offset, $value); + } + + /** + * @param TKey $offset + * @return void + */ + public function offsetUnset(mixed $offset): void + { + $this->remove($offset); + } + + /** + * {@inheritDoc} + * + * @return int<0, max> + */ + public function count(): int + { + return count($this->elements); + } + + /** + * {@inheritDoc} + */ + public function containsKey(int|string $key): bool + { + return isset($this->elements[$key]) || array_key_exists($key, $this->elements); + } + + /** + * {@inheritDoc} + */ + public function get(int|string $key): mixed + { + return $this->elements[$key] ?? null; + } + + + /** + * {@inheritDoc} + */ + public function set(int|string $key, mixed $value): void + { + $this->elements[$key] = $value; + } + + /** + * {@inheritDoc} + * + * @psalm-suppress InvalidPropertyAssignmentValue + */ + public function add(mixed $element): bool + { + $this->elements[] = $element; + + return true; + } + + /** + * {@inheritDoc} + */ + public function remove(int|string $key): mixed + { + if (!isset($this->elements[$key]) && !array_key_exists($key, $this->elements)) { + return null; + } + + $removed = $this->elements[$key]; + unset($this->elements[$key]); + + return $removed; + } + + + /** + * {@inheritDoc} + */ + public function isEmpty(): bool + { + return empty($this->elements); + } + + /** + * {@inheritDoc} + */ + public function getKeys(): array + { + return array_keys($this->elements); + } + + /** + * {@inheritDoc} + */ + public function getValues(): array + { + return array_values($this->elements); + } + + + /** + * {@inheritDoc} + * + * @template TMaybeContained + */ + public function contains(mixed $element): bool + { + return in_array($element, $this->elements, true); + } + + /** + * {@inheritDoc} + * + * @psalm-param Closure(T):U $func + * + * @return static + * @psalm-return static + * + * @psalm-template U + */ + public function map(Closure $func): static + { + $keys = array_keys($this->elements); + $map = array_map($func, $this->elements, $keys); + + return $this->createFrom(array_combine($keys, $map)); + } + + /** + * Map the values into a new class. + * + * @param string $class + * @param mixed ...$params + * @return static + */ + public function mapInto(string $class, mixed ...$params): static + { + return $this->map(static fn($value) => new $class($value, ...$params)); + } + + /** + * {@inheritDoc} + * + * @psalm-param null|Closure(T,TKey):U $func + * + * @return static + * @psalm-return static + * + * @psalm-template U + */ + public function mapByKey(string $keyName, ?string $valueName = null, ?Closure $func = null): static + { + $result = []; + foreach ($this->elements as $ind => $element) { + if ($valueName === null) { + $value = $element; + } else { + $value = $func ? $func($element, $ind) : $this->getProperty($element, $valueName); + } + $result[$this->getProperty($element, $keyName)] = $value; + } + return $this->createFrom($result); + } + + private function getProperty(mixed $target, string|int $keyName, bool $throwOnMiss = true): mixed + { + return match (true) { + is_array($target) || $target instanceof \ArrayAccess + => $throwOnMiss ? $target[$keyName] : ($target[$keyName] ?? null), + is_object($target) + => $throwOnMiss ? $target->$keyName : (property_exists($target, $keyName) ? $target->$keyName : null), + }; + } + + /** + * {@inheritDoc} + * + * @return static + * @psalm-return static + */ + public function filter(Closure $func = null): static + { + return $this->createFrom(array_filter($this->elements, $func, ARRAY_FILTER_USE_BOTH)); + } + + public function whereInstanceOf(string|array $type): static + { + return $this->filter( + static function ($value) use ($type) { + foreach ((array)$type as $classType) { + if ($value instanceof $classType) { + return true; + } + } + + return false; + } + ); + } + + /** + * {@inheritDoc} + * + * @return static + * @psalm-return static + */ + public function reject(Closure $callback): static + { + return $this->filter(static fn($value, $key) => !$callback($value, $key)); + } + + /** + * {@inheritDoc} + */ + public function each(callable $func): static + { + foreach ($this as $key => $item) { + if ($func($item, $key) === false) { + break; + } + } + + return $this; + } + + /** + * {@inheritDoc} + */ + public function transform(Closure $func): static + { + $this->elements = array_map($func, $this->elements); + + return $this; + } + + /** + * {@inheritDoc} + */ + public function merge(iterable $items): static + { + return $this->createFrom(array_merge($this->elements, Arr::toArray($items))); + } + + /** + * Creates a new instance from the specified elements. + * + * This method is provided for derived classes to specify how a new + * instance should be created when constructor semantics have changed. + * + * @param array|Collection $elements Elements. + * @psalm-param array|Collection $elements + * + * @return static + * @psalm-return static + * + * @psalm-template K of array-key + * @psalm-template V + */ + protected function createFrom(array|Collection $elements): static + { + return new static($elements); + } + + /** + * {@inheritDoc} + */ + public function clear(): void + { + $this->elements = []; + } + + /** + * {@inheritDoc} + */ + public function removeElement(mixed $element): bool + { + $key = array_search($element, $this->elements, true); + + if ($key === false) { + return false; + } + + unset($this->elements[$key]); + + return true; + } + + /** + * {@inheritDoc} + */ + public function first(): mixed + { + return reset($this->elements); + } + + /** + * {@inheritDoc} + */ + public function last(): mixed + { + return end($this->elements); + } + + /** + * {@inheritDoc} + */ + public function key(): int|string|null + { + return key($this->elements); + } + + /** + * {@inheritDoc} + */ + public function current(): mixed + { + return current($this->elements); + } + + /** + * {@inheritDoc} + */ + public function next(): mixed + { + return next($this->elements); + } + + /** + * {@inheritDoc} + */ + public function slice(int $offset, ?int $length = null): array + { + return array_slice($this->elements, $offset, $length, true); + } + + /** + * {@inheritDoc} + */ + public function exists(Closure $func): bool + { + foreach ($this->elements as $key => $element) { + if ($func($key, $element)) { + return true; + } + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function partition(Closure $func): array + { + $matches = $noMatches = []; + + foreach ($this->elements as $key => $element) { + if ($func($key, $element)) { + $matches[$key] = $element; + } else { + $noMatches[$key] = $element; + } + } + + return [ + $this->createFrom($matches), + $this->createFrom($noMatches), + ]; + } + + /** + * {@inheritDoc} + */ + public function testForAll(Closure $func): bool + { + foreach ($this->elements as $key => $element) { + if (!$func($key, $element)) { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + * + * @psalm-param TMaybeContained $element + * + * @return string|int|false + * @psalm-return (TMaybeContained is T ? TKey|false : false) + * + * @template TMaybeContained + */ + public function indexOf(mixed $element): string|int|bool + { + return array_search($element, $this->elements, true); + } + + /** + * {@inheritDoc} + */ + public function findFirst(Closure $func): mixed + { + foreach ($this->elements as $key => $element) { + if ($func($key, $element)) { + return $element; + } + } + + return null; + } + + /** + * {@inheritDoc} + */ + public function reduce(Closure $func, mixed $initial = null): mixed + { + return array_reduce($this->elements, $func, $initial); + } + + /** + * Collapse the collection of items into a single array. + * + * + * @return static + * @psalm-return static + */ + public function collapse(): static + { + return $this->createFrom(Arr::collapse($this->elements)); + } + + /** + * Push an element onto the beginning of the collection. + * + * @param T $value + * @param TKey $key + * @return static + */ + public function prepend(mixed $value, $key = null): static + { + $this->elements = Arr::prepend($this->elements, ...func_get_args()); + + return $this; + } + + + /** + * Push one or more elements onto the end of the collection. + * + * @param T ...$values + * @return static + */ + public function push(...$values): static + { + foreach ($values as $value) { + $this->elements[] = $value; + } + + return $this; + } + + /** + * Reverse elements order. + * + * @return static + */ + public function reverse(): static + { + return $this->createFrom(array_reverse($this->elements, true)); + } + + /** + * Chunk the collection into chunks of the given size. + * + * @param int $size + * + * @return static + */ + public function chunk(int $size): static + { + if ($size <= 0) { + return $this->createFrom([]); + } + + $chunks = []; + + foreach (array_chunk($this->elements, $size, true) as $chunk) { + $chunks[] = $this->createFrom($chunk); + } + + return $this->createFrom($chunks); + } + + public function clone(): static + { + return $this->createFrom($this); + } + + /** + * Push all the given items onto the collection. + * + * @param iterable $source + */ + public function concat(iterable $source): static + { + $result = $this->clone(); + + foreach ($source as $item) { + $result->push($item); + } + + return $result; + } + + /** + * Sort through each item with a callback. + * + * @param (callable(T, T): int)|null|int $func + * + * @return static + */ + public function sort(callable|int|null $func = null): static + { + $items = $this->elements; + + $func && is_callable($func) + ? uasort($items, $func) + : asort($items, $func ?? SORT_REGULAR); + + return $this->createFrom($items); + } + + + /** + * Sort items in descending order. + * + * @param int $options + * @return static + */ + public function sortDesc(int $options = SORT_REGULAR): static + { + $items = $this->elements; + + arsort($items, $options); + + return $this->createFrom($items); + } + + /** + * Sort the collection using the given callback. + * + * @param array|(callable(T, TKey): mixed)|string $callback + * @param int $options + * @param bool $descending + * @return static + */ + public function sortBy(array|string|callable $callback, int $options = SORT_REGULAR, bool $descending = false) + { + if (is_array($callback) && !is_callable($callback)) { + return $this->sortByMany($callback); + } + + $results = []; + + if (is_callable($callback)) { + // First we will loop through the items and get the comparator from a callback + // function which we were given. Then, we will sort the returned values and + // grab all the corresponding values for the sorted keys from this array. + foreach ($this->elements as $key => $value) { + $results[$key] = $callback($value, $key); + } + } + $descending ? arsort($results, $options) + : asort($results, $options); + + // Once we have sorted all of the keys in the array, we will loop through them + // and grab the corresponding model so we can set the underlying items list + // to the sorted version. Then we'll just return the collection instance. + foreach (array_keys($results) as $key) { + $results[$key] = $this->elements[$key]; + } + + return $this->createFrom($results); + } + + /** + * Sort the collection using multiple comparisons. + * + * @param array $comparisons + * @return static + */ + protected function sortByMany(array $comparisons = []): static + { + $items = $this->elements; + + uasort( + $items, + function ($a, $b) use ($comparisons) { + foreach ($comparisons as $comparison) { + $comparison = (array)$comparison; + + $prop = $comparison[0]; + + $ascending = Arr::get($comparison, 1, true) === true || + Arr::get($comparison, 1, true) === 'asc'; + + if (!is_string($prop) && is_callable($prop)) { + $result = $prop($a, $b); + } else { + $values = [ + dataGet($a, $prop), + dataGet($b, $prop), + ]; + + if (!$ascending) { + $values = array_reverse($values); + } + + $result = $values[0] <=> $values[1]; + } + + if ($result === 0) { + continue; + } + + return $result; + } + } + ); + + return $this->createFrom($items); + } + + /** + * Get one or a specified number of items randomly from the collection. + * + * @param (callable(self): int)|int|null $number + * @param bool $preserveKeys + * + * @return static|T + * + * @throws \InvalidArgumentException + */ + public function random(callable|int|null $number = null, bool $preserveKeys = false): mixed + { + if ($number === null) { + return Arr::random($this->elements); + } + + if (is_callable($number)) { + return new static(Arr::random($this->elements, $number($this), $preserveKeys)); + } + + return new static(Arr::random($this->elements, $number, $preserveKeys)); + } + + /** + * Sort the collection keys. + * + * @param int $options + * @param bool $descending + * @return static + */ + public function sortKeys(int $options = SORT_REGULAR, bool $descending = false): static + { + $items = $this->elements; + + $descending ? krsort($items, $options) : ksort($items, $options); + + return $this->createFrom($items); + } + + protected function useAsCallable(mixed $value): bool + { + return !is_string($value) && is_callable($value); + } + + protected function valueRetriever(mixed $value): callable + { + if ($this->useAsCallable($value)) { + return $value; + } + + return fn($item) => Arr::get($item, $value); + } + + /** + * Group an associative array by a field or using a callback. + * + * @param (callable(T, TKey): array-key)|string[]|string $groupBy + * @param bool $preserveKeys + * @psalm-return static> + * @return static> + */ + public function groupBy(callable|array|string $groupBy, bool $preserveKeys = false): static + { + if (is_array($groupBy) && !$this->useAsCallable($groupBy)) { + $nextGroups = $groupBy; + + $groupBy = array_shift($nextGroups); + } + + $groupBy = $this->valueRetriever($groupBy); + + $results = []; + + foreach ($this->elements as $key => $value) { + $groupKeys = $groupBy($value, $key); + + if (!is_array($groupKeys)) { + $groupKeys = [$groupKeys]; + } + + foreach ($groupKeys as $groupKey) { + $groupKey = match (true) { + is_bool($groupKey) => (int)$groupKey, + $groupKey instanceof \BackedEnum => $groupKey->value, + $groupKey instanceof \Stringable => (string)$groupKey, + default => $groupKey, + }; + + if (!array_key_exists($groupKey, $results)) { + $results[$groupKey] = $this->createFrom([]); + } + + $results[$groupKey]->offsetSet($preserveKeys ? $key : null, $value); + } + } + + $result = $this->createFrom($results); + + if (!empty($nextGroups)) { + return $result->map(fn(Collection $item) => $item->groupBy($nextGroups, $preserveKeys)); + } + + return $result; + } + + public function __toString(): string + { + return self::class . '@' . spl_object_hash($this); + } +} diff --git a/src/Structures/Collections/Collection.php b/src/Structures/Collections/Collection.php new file mode 100644 index 0000000..842f98b --- /dev/null +++ b/src/Structures/Collections/Collection.php @@ -0,0 +1,107 @@ +ordered map that can also be used + * like a list. + * + * A Collection has an internal iterator just like a PHP array. In addition, + * a Collection can be iterated with external iterators, which is preferable. + * To use an external iterator simply use the foreach language construct to + * iterate over the collection (which calls {@link getIterator()} internally) or + * explicitly retrieve an iterator though {@link getIterator()} which can then be + * used to iterate over the collection. + * You can not rely on the internal iterator of the collection being at a certain + * position unless you explicitly positioned it before. Prefer iteration with + * external iterators. + * + * @author Doctrine + * + * @phpstan-template TKey of array-key + * @phpstan-template TValue + * @template-extends ReadableCollection + * @template-extends ArrayAccess + */ +interface Collection extends ReadableCollection, ArrayAccess +{ + /** + * Adds an element at the end of the collection. + * + * @param mixed $element The element to add. + * @phpstan-param TValue $element + */ + public function add(mixed $element): bool; + + /** + * Clears the collection, removing all elements. + */ + public function clear(): void; + + /** + * Removes the element at the specified index from the collection. + * + * @param string|int $key The key/index of the element to remove. + * @phpstan-param TKey $key + * + * @return mixed The removed element or NULL, if the collection did not contain the element. + * @phpstan-return ?TValue + */ + public function remove(string|int $key): mixed; + + /** + * Removes the specified element from the collection, if it is found. + * + * @param mixed $element The element to remove. + * @phpstan-param TValue $element + * + * @return bool TRUE if this collection contained the specified element, FALSE otherwise. + */ + public function removeElement(mixed $element): bool; + + /** + * Sets an element in the collection at the specified key/index. + * + * @param string|int $key The key/index of the element to set. + * @param mixed $value The element to set. + * @phpstan-param TKey $key + * @phpstan-param TValue $value + */ + public function set(string|int $key, mixed $value): void; + + /** + * Push all the given items onto the collection. + * + * @param iterable $source + */ + public function concat(iterable $source): static; + + public function clone(): static; + + /** + * Get one or a specified number of items randomly from the collection. + * + * @param (callable(self): int)|int|null $number + * + * @return static|TValue + * + * @throws \InvalidArgumentException + */ + public function random(callable|int|null $number = null, bool $preserveKeys = false): mixed; + + /** + * Group an associative array by a field or using a callback. + * + * @param (callable(TValue, TKey): array-key)|string[]|string $groupBy + * @param bool $preserveKeys + * @phpstan-param (callable(TValue, TKey): array-key)|string[]|string $groupBy + */ + public function groupBy(callable|array|string $groupBy, bool $preserveKeys = false): static; +} diff --git a/src/Structures/Collections/HashCollection.php b/src/Structures/Collections/HashCollection.php new file mode 100644 index 0000000..ce735ce --- /dev/null +++ b/src/Structures/Collections/HashCollection.php @@ -0,0 +1,198 @@ + $elements + */ + public function __construct(protected array $elements = []) + { + } + + /** + * Gets a native PHP array of the elements. + * + * @return array + */ + public function all(): array + { + return $this->elements; + } + + /** + * Checks whether the collection contains an element with the specified key/index. + * + * @param string $key The key/index to check for. + * + * @return bool TRUE if the collection contains an element with the specified key/index, + * FALSE otherwise. + */ + public function hasKey(string $key): bool + { + return isset($this->elements[$key]) || array_key_exists($key, $this->elements); + } + + /** + * Gets the element at the specified key/index. + * + * @param string $key The key/index of the element to retrieve. + * + * @return T|null + */ + public function get(string $key): mixed + { + return $this->elements[$key] ?? null; + } + + + /** + * Sets an element in the collection at the specified key/index. + * + * @param string $key The key/index of the element to set. + * @param T $value The element to set. + */ + public function set(string $key, mixed $value): void + { + $this->elements[$key] = $value; + } + + + /** + * Adds an element at the end of the collection. + * + * @param T $element The element to add. + */ + public function add(object $element): bool + { + $this->elements[$element::class] = $element; + + return true; + } + + /** + * Removes the element at the specified index from the collection. + * + * @param string $key The key/index of the element to remove. + * + * @return T|null The removed element or NULL, if the collection did not contain the element. + */ + public function remove(string $key): mixed + { + if (!isset($this->elements[$key]) && !array_key_exists($key, $this->elements)) { + return null; + } + + $removed = $this->elements[$key]; + unset($this->elements[$key]); + + return $removed; + } + + /** + * @return int<0, max> + */ + public function count(): int + { + return count($this->elements); + } + + /** + * @param string $offset + */ + public function offsetExists(mixed $offset): bool + { + return $this->hasKey($offset); + } + + /** + * @param string $offset + * + * @return T|null + */ + public function offsetGet(mixed $offset): mixed + { + return $this->get($offset); + } + + /** + * @param string|null $offset + * @param T $value + */ + public function offsetSet(mixed $offset, mixed $value): void + { + if (!isset($offset)) { + $this->add($value); + + return; + } + + $this->set($offset, $value); + } + + /** + * @param string $offset + */ + public function offsetUnset(mixed $offset): void + { + $this->remove($offset); + } + + /** + * Checks whether the collection is empty (contains no elements). + */ + public function isEmpty(): bool + { + return empty($this->elements); + } + + /** + * Checks whether an element is contained in the collection. + * This is an O(n) operation, where n is the size of the collection. + * + * @param TMaybeContained $element The element to search for. + * + * @return bool TRUE if the collection contains the element, FALSE otherwise. + * @phpstan-return (TMaybeContained is T ? bool : false) + * + * @template TMaybeContained + */ + public function contains(mixed $element): bool + { + return in_array($element, $this->elements, true); + } + + /** + * Clears the collection, removing all elements. + */ + public function clear(): void + { + $this->elements = []; + } + + /** + * Returns the first element of this collection that satisfies the predicate $func. + * + * @param Closure(string, T):bool $func The predicate. + * + * @return null|T The first element respecting the predicate, null if no element respects the predicate. + */ + public function find(Closure $func): mixed + { + return array_find($this->elements, fn($element, $key) => $func($key, $element)); + } +} diff --git a/src/Structures/Collections/ReadableCollection.php b/src/Structures/Collections/ReadableCollection.php new file mode 100644 index 0000000..6d45bdb --- /dev/null +++ b/src/Structures/Collections/ReadableCollection.php @@ -0,0 +1,297 @@ + + */ +interface ReadableCollection extends Countable, IteratorAggregate +{ + /** + * Checks whether an element is contained in the collection. + * This is an O(n) operation, where n is the size of the collection. + * + * @param mixed $element The element to search for. + * @phpstan-param TMaybeContained $element + * + * @return bool TRUE if the collection contains the element, FALSE otherwise. + * @phpstan-return (TMaybeContained is TValue ? bool : false) + * + * @template TMaybeContained + */ + public function contains(mixed $element): bool; + + /** + * Checks whether the collection is empty (contains no elements). + * + * @return bool TRUE if the collection is empty, FALSE otherwise. + */ + public function isEmpty(): bool; + + /** + * Checks whether the collection contains an element with the specified key/index. + * + * @param string|int $key The key/index to check for. + * @phpstan-param TKey $key + * + * @return bool TRUE if the collection contains an element with the specified key/index, + * FALSE otherwise. + */ + public function containsKey(string|int $key): bool; + + /** + * Gets the element at the specified key/index. + * + * @param string|int $key The key/index of the element to retrieve. + * @phpstan-param TKey $key + * + * @return mixed + * @phpstan-return ?TValue + */ + public function get(string|int $key): mixed; + + /** + * Gets all keys/indices of the collection. + * + * @return int[]|string[] The keys/indices of the collection, in the order of the corresponding + * elements in the collection. + * @phpstan-return TKey[] + */ + public function getKeys(): array; + + /** + * Gets all values of the collection. + * + * @return array The values of all elements in the collection, in the + * order they appear in the collection. + * @phpstan-return TValue[] + */ + public function getValues(): array; + + /** + * Gets a native PHP array representation of the collection. + * + * @return array + * @phpstan-return array + */ + public function toArray(): array; + + /** + * Gets a native PHP array of the elements. + * + * @return array + * @phpstan-return array + */ + public function all(): array; + + /** + * Sets the internal iterator to the first element in the collection and returns this element. + * + * @return mixed + * @phpstan-return TValue|false + */ + public function first(): mixed; + + /** + * Sets the internal iterator to the last element in the collection and returns this element. + * + * @return mixed + * @phpstan-return TValue|false + */ + public function last(): mixed; + + /** + * Gets the key/index of the element at the current iterator position. + * + * @return int|string|null + * @phpstan-return ?TKey + */ + public function key(): int|string|null; + + /** + * Gets the element of the collection at the current iterator position. + * + * @phpstan-return TValue|false + */ + public function current(): mixed; + + /** + * Moves the internal iterator position to the next element and returns this element. + * + * @phpstan-return TValue|false + */ + public function next(): mixed; + + /** + * Extracts a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @param int $offset The offset to start from. + * @param int|null $length The maximum number of elements to return, or null for no limit. + * + * @return array + * @phpstan-return array + */ + public function slice(int $offset, ?int $length = null): array; + + /** + * Tests for the existence of an element that satisfies the given predicate. + * + * @param Closure $func The predicate. + * @phpstan-param Closure(TKey, TValue):bool $func + * + * @return bool TRUE if the predicate is TRUE for at least one element, FALSE otherwise. + */ + public function exists(Closure $func): bool; + + /** + * Returns all the elements of this collection that satisfy the predicate $func. + * The order of the elements is preserved. + * + * @param ?Closure $func The predicate used for filtering. + * @phpstan-param null|Closure(TValue, TKey):bool $func + * + * @return ReadableCollection A collection with the results of the filter operation. + * @phpstan-return ReadableCollection + */ + public function filter(?Closure $func = null): ReadableCollection; + + /** + * Create a collection of all elements that do not pass a given truth test. + * + * @param Closure $callback The predicate used for filtering. + * @phpstan-param Closure(TValue, TKey):bool $callback + * + * @return ReadableCollection A collection with the results of the filter operation. + * @phpstan-return ReadableCollection + */ + public function reject(Closure $callback): ReadableCollection; + + /** + * Applies the given function to each element in the collection and returns + * a new collection with the elements returned by the function. + * + * @phpstan-param Closure(TValue):U $func + * + * @return ReadableCollection + * @phpstan-return ReadableCollection + * + * @phpstan-template U + */ + public function map(Closure $func): ReadableCollection; + + /** + * Returns a new collection with Key = $keyName and the elements returned by the function if it exists. + * + * @param string $keyName + * @param ?string $valueName + * @phpstan-param null|Closure(TValue,TKey):U $func + * + * @return ReadableCollection + * @phpstan-return ReadableCollection + * + * @phpstan-template U + */ + public function mapByKey(string $keyName, ?string $valueName = null, ?Closure $func = null): ReadableCollection; + + /** + * Partitions this collection in two collections according to a predicate. + * Keys are preserved in the resulting collections. + * + * @param Closure $func The predicate on which to partition. + * @phpstan-param Closure(TKey, TValue):bool $func + * + * @return ReadableCollection[] An array with two elements. The first element contains the collection + * of elements where the predicate returned TRUE, the second element + * contains the collection of elements where the predicate returned FALSE. + * @phpstan-return array{0: ReadableCollection, 1: ReadableCollection} + */ + public function partition(Closure $func): array; + + /** + * Tests whether the given predicate $func holds for all elements of this collection. + * + * @param Closure $func The predicate. + * @phpstan-param Closure(TKey, TValue):bool $func + * + * @return bool TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function testForAll(Closure $func): bool; + + /** + * Applies the given function to each element of the Collection. Returns the same Collection. + * + * @param callable $func The predicate. + * @phpstan-param callable(TKey, TValue):bool $func + */ + public function each(callable $func): static; + + /** + * Transform each item in the collection using a callback. + * + * @param Closure $func The predicate. + * @phpstan-param Closure(TKey, TValue):void $func + */ + public function transform(Closure $func): static; + + + /** + * Merge the collection with the given items. + * + * @param iterable $items + */ + public function merge(iterable $items): static; + + /** + * Gets the index/key of a given element. The comparison of two elements is strict, + * that means not only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element The element to search for. + * @phpstan-param TMaybeContained $element + * + * @return int|string|bool The key/index of the element or FALSE if the element was not found. + * @phpstan-return (TMaybeContained is TValue ? TKey|false : false) + * + * @template TMaybeContained + */ + public function indexOf(mixed $element): string|int|bool; + + /** + * Returns the first element of this collection that satisfies the predicate $func. + * + * @param Closure $func The predicate. + * @phpstan-param Closure(TKey, TValue):bool $func + * + * @return mixed The first element respecting the predicate, + * null if no element respects the predicate. + * @phpstan-return ?TValue + */ + public function findFirst(Closure $func): mixed; + + /** + * Applies iteratively the given function to each element in the collection, + * to reduce the collection to a single value. + * + * @phpstan-param Closure(TReturn|TInitial|null, TValue):(TInitial|TReturn) $func + * @phpstan-param TInitial|null $initial + * + * @return mixed + * @phpstan-return TReturn|TInitial|null + * + * @phpstan-template TReturn + * @phpstan-template TInitial + */ + public function reduce(Closure $func, mixed $initial = null): mixed; +} diff --git a/src/Testing/AdditionalAssertionsTrait.php b/src/Testing/AdditionalAssertionsTrait.php new file mode 100644 index 0000000..916deb7 --- /dev/null +++ b/src/Testing/AdditionalAssertionsTrait.php @@ -0,0 +1,85 @@ + $class] as $class_iterate) { + $results[] = $trait_uses_recursive($class_iterate); + } + return array_values(array_merge(...$results)); + }; + + $uses = $class_uses_recursive($class); + $uses = array_flip($uses); + + foreach ((array)$expected_traits as $k => $trait_class) { + static::assertArrayHasKey( + $trait_class, + $uses, + $message === '' + ? 'Class does not uses passed traits' + : $message + ); + } + } +} diff --git a/src/Testing/TestingHelper.php b/src/Testing/TestingHelper.php new file mode 100755 index 0000000..eb94311 --- /dev/null +++ b/src/Testing/TestingHelper.php @@ -0,0 +1,50 @@ +setAccessible(true); + return $methodReflex->invoke($class, ...$params); + } + + /** + * Get a instance property (public/private/protected) value. + * + * @param object|string $object + * @param string $propertyName + * + * @return mixed + * @throws \ReflectionException + * + */ + protected static function getProperty(object|string $object, string $propertyName): mixed + { + $reflection = new ReflectionClass($object); + + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + + return $property->getValue($object); + } +} diff --git a/src/Traits/ArrayStorage.php b/src/Traits/ArrayStorage.php deleted file mode 100644 index 8906137..0000000 --- a/src/Traits/ArrayStorage.php +++ /dev/null @@ -1,209 +0,0 @@ -get($name); - } - - /** - * @param string $name - * @param mixed $value - */ - public function __set(string $name, $value): void - { - $this->set($name, $value); - } - - /** - * @param string $name - * - * @return mixed|null - */ - public function get(string $name) - { - if ($this->propertyExists($name)) { - return $this->$name; - } - - if (Arr::has($this->data, $name)) { - return Arr::get($this->data, $name); - } - - if ($this->showErrorOnGetIfNull) { - $trace = debug_backtrace(); - trigger_error( - "Undefined property in __get(): $name in file {$trace[0]['file']} in line {$trace[0]['line']}", - E_USER_NOTICE - ); - } - - return null; - } - - /** - * @param string $name - * - * @return bool - */ - protected function propertyExists(string $name): bool - { - return $name !== 'data' && property_exists($this, $name); - } - - /** - * @param string $name - * @param $value - */ - public function set(string $name, $value): void - { - if ($this->propertyExists($name)) { - $this->$name = $value; - return; - } - - Arr::set($this->data, $name, $value); - } - - /** - * @param string $name - * - * @return bool - */ - public function __isset(string $name): bool - { - return $this->propertyExists($name) || Arr::has($this->data, $name); - } - - /** - * @param string $name - */ - public function __unset(string $name) - { - if ($this->propertyExists($name)) { - $this->$name = null; - return; - } - - Arr::remove($this->data, $name); - } - - /** - * Determine if an item exists at an offset. - * - * @param string $key - * - * @return bool - */ - public function offsetExists($key): bool - { - return $this->valueExists($key); - } - - /** - * @param string $name - * - * @return bool - */ - public function valueExists(string $name): bool - { - return $this->propertyExists($name) || Arr::has($this->data, $name); - } - - /** - * Get an item at a given offset. - * - * @param mixed $key - * - * @return mixed - */ - public function offsetGet($key) - { - return $this->get($key); - } - - /** - * Set the item at a given offset. - * - * @param mixed $key - * @param mixed $value - * - * @return void - */ - public function offsetSet($key, $value): void - { - $this->set($key, $value); - } - - /** - * Unset the item at a given offset. - * - * @param string $key - * - * @return void - */ - public function offsetUnset($key): void - { - unset($this->$key); - } - - /** - * @return int - */ - public function count(): int - { - return count($this->getData()); - } - - /** - * @return array - */ - public function getData(): array - { - return $this->data; - } - - /** - * @return string - * @throws JsonException - */ - public function __toString(): string - { - return (string)Json::encode($this->toArray()); - } - - /** - * @return array - */ - public function toArray(): array - { - return $this->getData(); - } -} diff --git a/src/Traits/ArrayStorageConfigurableTrait.php b/src/Traits/ArrayStorageConfigurableTrait.php deleted file mode 100644 index eb7bf81..0000000 --- a/src/Traits/ArrayStorageConfigurableTrait.php +++ /dev/null @@ -1,31 +0,0 @@ -set($key, $value); - - return true; - } -} diff --git a/src/Traits/ConfigurableTrait.php b/src/Traits/ConfigurableTrait.php index d6fffa3..8036537 100644 --- a/src/Traits/ConfigurableTrait.php +++ b/src/Traits/ConfigurableTrait.php @@ -6,77 +6,48 @@ use Php\Support\Exceptions\InvalidParamException; /** - * Trait ConfigurableTrait - * @package Php\Support\Traits + * @template TKey of array-key + * @template TValue + * @implements ArrayAccess + * @mixin ArrayAccess */ trait ConfigurableTrait { - /** - * @param array|ArrayAccess $attributes - * @param bool $exceptOnMiss - * - * @return self - */ - public function configurable($attributes, ?bool $exceptOnMiss = true) + public function configurable(array|ArrayAccess $attributes, bool $throwOnMissingProp = true): static { foreach ($attributes as $key => $value) { - if (!$this->applyValue($key, $value) && $exceptOnMiss) { - throw new InvalidParamException("Property $key is absent at class: " . get_class($this)); + if (!$this->applyValue($key, $value) && $throwOnMissingProp) { + throw new InvalidParamException("Property $key is absent at class: " . $this::class); } } return $this; } - /** - * @param string $key - * @param $value - * - * @return bool - */ - protected function applyValue(string $key, $value): bool + protected function applyValue(string $key, mixed $value): bool { - if (!$res = $this->callMethod($key, $value)) { - $res = $this->setPropConfigurable($key, $value); - } - return $res; + return $this->callSetterProp($key, $value) || $this->setPropValue($key, $value); } - /** - * @param string $key - * @param $value - * - * @return bool - */ - protected function setPropConfigurable(string $key, $value): bool + protected function setPropValue(string $key, mixed $value): bool { if ($this->propertyExists($key)) { $this->{$key} = $value; + return true; } return false; } - /** - * @param string $key - * - * @return bool - */ - protected function propertyExists(string $key): bool + protected function propertyExists(string $name): bool { - return property_exists($this, $key); + return property_exists($this, $name); } - /** - * @param string $key - * @param $value - * - * @return bool - */ - protected function callMethod(string $key, $value): bool + protected function callSetterProp(string $key, mixed $value): bool { - if (method_exists($this, $method = 'set' . ucfirst($key))) { + if ($method = findSetterMethodByProp($this, $key)) { $this->$method($value); return true; diff --git a/src/Traits/ConsolePrint.php b/src/Traits/ConsolePrint.php index 92b858c..97c3be4 100644 --- a/src/Traits/ConsolePrint.php +++ b/src/Traits/ConsolePrint.php @@ -10,21 +10,15 @@ */ trait ConsolePrint { - /** - * @param mixed $msg - * @param bool $newLine - */ - public function print($msg, bool $newLine = true): void + public function print(mixed $msg, bool $newLine = true): void { fwrite(STDOUT, print_r($msg, true) . ($newLine ? PHP_EOL : '')); } /** - * @param mixed $msg - * @param bool $newLine * @codeCoverageIgnore */ - public function printError($msg, bool $newLine = true): void + public function printError(mixed $msg, bool $newLine = true): void { fwrite(STDERR, print_r($msg, true) . ($newLine ? PHP_EOL : '')); } diff --git a/src/Traits/HasPrePostActions.php b/src/Traits/HasPrePostActions.php new file mode 100644 index 0000000..6241b7b --- /dev/null +++ b/src/Traits/HasPrePostActions.php @@ -0,0 +1,37 @@ +> */ + protected array $executeCallbacks = []; + + public function addCallbackAction(string $key, callable $action): self + { + $this->executeCallbacks[$key][] = $action; + + return $this; + } + + public function getCallbackActions(string $key = null): array + { + return $key ? + $this->executeCallbacks[$key] ?? [] + : $this->executeCallbacks; + } + + protected function runActions(string $actionGroup, ...$arguments): bool + { + foreach ($this->getCallbackActions($actionGroup) as $action) { + $res = $action(...$arguments); + if ($res === false) { + return false; + } + } + + return true; + } +} diff --git a/src/Traits/Maker.php b/src/Traits/Maker.php index 0901408..b0d0504 100755 --- a/src/Traits/Maker.php +++ b/src/Traits/Maker.php @@ -6,7 +6,6 @@ /** * Trait Maker - * @package Php\Support\Traits */ trait Maker { diff --git a/src/Traits/Metable.php b/src/Traits/Metable.php index 1f5fd06..ea170e4 100755 --- a/src/Traits/Metable.php +++ b/src/Traits/Metable.php @@ -7,47 +7,47 @@ use Php\Support\Helpers\Arr; /** - * Trait Metable - * @package Php\Support\Traits + * @template TValue */ trait Metable { /** - * The meta data for the element. + * The metadata for the element. * - * @var array + * @var array */ protected array $meta = []; /** * Get additional meta information to merge with the element payload. * - * @return array + * @return array */ public function meta(): array { return $this->meta; } - /** - * @param string $key - * @param mixed $default - * - * @return mixed - */ - public function metaAttribute(string $key, $default = null) + public function metaAttribute(string $key, mixed $default = null): mixed { return Arr::get($this->meta, $key, $default); } + public function setMetaAttribute(string $key, mixed $value, bool $removeNull = false): static + { + if ($value !== null || !$removeNull) { + Arr::set($this->meta, $key, $value); + } + + return $this; + } + /** * Set additional meta information for the element. * - * @param array $meta - * - * @return $this + * @param array $meta */ - public function withMeta(array $meta): self + public function withMeta(array $meta): static { $this->meta = Arr::merge($this->meta, $meta); diff --git a/src/Traits/ReadOnlyProperties.php b/src/Traits/ReadOnlyProperties.php index e183c5d..d5ee2db 100755 --- a/src/Traits/ReadOnlyProperties.php +++ b/src/Traits/ReadOnlyProperties.php @@ -8,7 +8,7 @@ trait ReadOnlyProperties { - public function __get($key) + public function __get(string $key): mixed { if (property_exists($this, $key)) { return $this->$key; diff --git a/src/Traits/Singleton.php b/src/Traits/Singleton.php index 9318e19..23dedc8 100644 --- a/src/Traits/Singleton.php +++ b/src/Traits/Singleton.php @@ -8,13 +8,11 @@ /** * Trait Singleton - * - * @package Php\Support\Traits */ trait Singleton { /** - * @var array + * @var array */ protected static array $instances = []; @@ -25,9 +23,6 @@ protected function __construct() { } - /** - * @return Singleton - */ public static function getInstance(): self { $cls = static::class; diff --git a/src/Traits/Thrower.php b/src/Traits/Thrower.php index a8c2119..daa7cba 100755 --- a/src/Traits/Thrower.php +++ b/src/Traits/Thrower.php @@ -6,30 +6,26 @@ /** * Trait Thrower - * @package Php\Support\Traits */ trait Thrower { - /** - * Throw Exception - * - * @param mixed ...$arguments - */ - public static function throw(...$arguments): void + public static function throw(mixed ...$arguments): void { // @phpstan-ignore-next-line throw new static(...$arguments); } - - /** - * @param mixed $value - * @param mixed ...$arguments - */ - public static function throwIf($value, ...$arguments): void + public static function throwIf(mixed $value, mixed ...$arguments): void { if ($value) { static::throw(...$arguments); } } + + public static function throwIfReturn(mixed $value, mixed ...$arguments): bool + { + static::throwIf($value, ...$arguments); + + return true; + } } diff --git a/src/Traits/TraitBooter.php b/src/Traits/TraitBooter.php index 7abaaa4..a218e84 100755 --- a/src/Traits/TraitBooter.php +++ b/src/Traits/TraitBooter.php @@ -6,21 +6,20 @@ /** * Trait TraitBooter - * @package Php\Support\Traits */ trait TraitBooter { /** * The array of booted classes. * - * @var array + * @var class-string[] */ protected static array $booted = []; /** * The array of trait initializers that will be called on each new instance. * - * @return array + * @return class-string[] */ protected static function bootTraits(): array { @@ -54,8 +53,6 @@ protected function bootIfNotBooted(): void /** * Perform any actions required before the instance boots. - * - * @return void */ protected static function booting(): void { @@ -64,8 +61,6 @@ protected static function booting(): void /** * Bootstrap the instance and its traits. - * - * @return void */ protected static function boot(): void { @@ -74,8 +69,6 @@ protected static function boot(): void /** * Perform any actions required after the instance boots. - * - * @return void */ protected static function booted(): void { @@ -84,8 +77,6 @@ protected static function booted(): void /** * Clear the list of booted models so they will be re-booted. - * - * @return void */ public static function clearBooted(): void { diff --git a/src/Traits/TraitInitializer.php b/src/Traits/TraitInitializer.php index 80867e6..424f6e9 100755 --- a/src/Traits/TraitInitializer.php +++ b/src/Traits/TraitInitializer.php @@ -6,7 +6,6 @@ /** * Trait TraitInitializer - * @package Php\Support\Traits */ trait TraitInitializer { @@ -18,7 +17,7 @@ trait TraitInitializer /** * The array of trait initializers that will be called on each new instance. * - * @var array + * @var array */ protected static array $traitInitializers = []; diff --git a/src/Traits/UseConfigurableStorage.php b/src/Traits/UseConfigurableStorage.php new file mode 100644 index 0000000..5a96fe0 --- /dev/null +++ b/src/Traits/UseConfigurableStorage.php @@ -0,0 +1,28 @@ + + * @mixin ArrayAccess + */ +trait UseConfigurableStorage +{ + use UseStorage; + use ConfigurableTrait { + UseStorage::propertyExists insteadof ConfigurableTrait; + } + + protected function configureProps(string $key, mixed $value): bool + { + $this->set($key, $value); + + return true; + } +} diff --git a/src/Traits/UseErrorsBox.php b/src/Traits/UseErrorsBox.php index 3293e97..0790373 100644 --- a/src/Traits/UseErrorsBox.php +++ b/src/Traits/UseErrorsBox.php @@ -5,16 +5,13 @@ namespace Php\Support\Traits; /** - * Trait UseErrorsBox - * @package Php\Support\Traits - * * Use errors into your class */ trait UseErrorsBox { private array $errors = []; - public function setError($message): self + public function setError(string|\Throwable $message): static { if ($message instanceof \Exception) { $message = $message->getMessage(); @@ -35,7 +32,7 @@ public function errors(): array return $this->errors; } - public function clearErrors(): self + public function clearErrors(): static { $this->errors = []; diff --git a/src/Traits/UseSetter.php b/src/Traits/UseSetter.php new file mode 100755 index 0000000..09812be --- /dev/null +++ b/src/Traits/UseSetter.php @@ -0,0 +1,25 @@ +$method($value); + + return true; + } + + if (property_exists($this, $key)) { + return $this->$key; + } + + throw new MissingPropertyException(null, $key); + } +} diff --git a/src/Traits/UseStorage.php b/src/Traits/UseStorage.php new file mode 100644 index 0000000..9f727bd --- /dev/null +++ b/src/Traits/UseStorage.php @@ -0,0 +1,96 @@ + + * @mixin ArrayAccess + */ +trait UseStorage +{ + private Storage $storage; + + protected function propertyExists(string $name): bool + { + return $name !== 'storage' && property_exists($this, $name); + } + + + public function set(string $name, mixed $value): void + { + if ($this->propertyExists($name)) { + $this->$name = $value; + return; + } + + $this->storage->set($name, $value); + } + + public function get(string $name, mixed $default = null): mixed + { + if ($this->propertyExists($name)) { + return $this->$name; + } + + return $this->storage->get($name, $default); + } + + public function __get(string $name): mixed + { + return $this->get($name); + } + + public function __set(string $name, mixed $value): void + { + $this->set($name, $value); + } + + public function __isset(string $name): bool + { + return $this->propertyExists($name) || $this->storage->exist($name); + } + + public function __unset(string $name): void + { + if ($this->propertyExists($name)) { + $this->$name = null; + return; + } + + $this->storage->remove($name); + } + + public function offsetExists(mixed $key): bool + { + return $this->propExists($key); + } + + public function propExists(string $name): bool + { + return $this->propertyExists($name) || $this->storage->exist($name); + } + + public function offsetGet(mixed $key): mixed + { + return $this->get($key); + } + + public function offsetSet(mixed $key, mixed $value): void + { + $this->set($key, $value); + } + + public function offsetUnset(mixed $key): void + { + unset($this->$key); + } +} diff --git a/src/Traits/Whener.php b/src/Traits/Whener.php index 3f23611..8f47809 100755 --- a/src/Traits/Whener.php +++ b/src/Traits/Whener.php @@ -10,14 +10,7 @@ */ trait Whener { - /** - * @param mixed $value - * @param callable $callback - * @param null|callable $default - * - * @return $this - */ - public function when($value, callable $callback, ?callable $default = null): self + public function when(mixed $value, callable $callback, ?callable $default = null): mixed { if ($value) { return $callback($this, $value) ?: $this; diff --git a/src/Types/GeoPoint.php b/src/Types/GeoPoint.php index 4cadd4e..b8b3924 100644 --- a/src/Types/GeoPoint.php +++ b/src/Types/GeoPoint.php @@ -17,7 +17,6 @@ */ class GeoPoint extends Point { - /** * @param int $options * diff --git a/src/Types/Point.php b/src/Types/Point.php index e7e6621..31b89c3 100644 --- a/src/Types/Point.php +++ b/src/Types/Point.php @@ -5,6 +5,7 @@ namespace Php\Support\Types; use Php\Support\Exceptions\InvalidParamException; +use Php\Support\Helpers\Arr; use Php\Support\Helpers\Json; use Php\Support\Interfaces\Arrayable; use Php\Support\Interfaces\Jsonable; @@ -16,34 +17,10 @@ */ class Point implements Jsonable, Arrayable { - public float $x = 0; - - public float $y = 0; - - /** - * Point constructor. - * - * @param float $x - * @param float $y - */ - public function __construct(float $x, float $y) + public function __construct(public float $x = 0, public float $y = 0) { - $this->x = $x; - $this->y = $y; } - - /** - * @return string - */ - /*public function __toString() - { - return $this->toDB(); - }*/ - - /** - * @return array - */ public function toArray(): array { return [ @@ -71,7 +48,6 @@ public static function fromArray(array $array): ?self * @param int $options * * @return string|null - * @throws \Php\Support\Exceptions\JsonException */ public function toJson($options = 320): ?string { @@ -88,7 +64,6 @@ public function toJson($options = 320): ?string * @param string|null $string * * @return Jsonable|null - * @throws \Php\Support\Exceptions\JsonException */ public static function fromJson(?string $string): ?Jsonable { @@ -114,17 +89,15 @@ public function toPgDB(): string */ public function castFromDatabase(?string $value): ?self { - if (empty($value)) { - return null; - } - - $string = mb_substr($value, 1, -1); - - if (empty($string)) { + if (!$result = Arr::fromPostgresPoint($value)) { return null; } - return new static(...explode(',', $string)); + [ + $x, + $y, + ] = $result; + return new static((float)$x, (float)$y); } diff --git a/tests/Enums/WithEnhancesForStringsTest.php b/tests/Enums/WithEnhancesForStringsTest.php new file mode 100644 index 0000000..3d3c5df --- /dev/null +++ b/tests/Enums/WithEnhancesForStringsTest.php @@ -0,0 +1,68 @@ +assertInstanceOf(InvalidParamException::class, $e); $this->assertSame('Invalid Parameter', $e->getMessage()); $this->assertSame('Invalid Parameter', $e->getName()); - $this->assertNull($e->getParam()); + $this->assertNull($e->name); } try { throw new InvalidParamException('Invalid Param', 'prop'); } catch (InvalidParamException $e) { $this->assertInstanceOf(InvalidParamException::class, $e); - $this->assertSame('prop', $e->getParam()); + $this->assertSame('prop', $e->name); $this->assertSame('Invalid Param', $e->getMessage()); $this->assertSame('Invalid Parameter', $e->getName()); } @@ -45,7 +44,7 @@ public function testThrow() throw new InvalidParamException(null, 'prop'); } catch (InvalidParamException $e) { $this->assertInstanceOf(InvalidParamException::class, $e); - $this->assertSame('prop', $e->getParam()); + $this->assertSame('prop', $e->name); $this->assertSame('Invalid Parameter', $e->getName()); $this->assertSame('Invalid Parameter: prop', $e->getMessage()); } diff --git a/tests/exceptions/MissingClassTest.php b/tests/Exceptions/MissingClassTest.php similarity index 62% rename from tests/exceptions/MissingClassTest.php rename to tests/Exceptions/MissingClassTest.php index 68417f0..eb64497 100644 --- a/tests/exceptions/MissingClassTest.php +++ b/tests/Exceptions/MissingClassTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Exceptions; use Php\Support\Exceptions\MissingClassException; use PHPUnit\Framework\TestCase; @@ -15,13 +15,6 @@ final class MissingClassTest extends TestCase { public function testThrow() { - try { - throw new MissingClassException(); - } catch (Throwable $e) { - $this->assertInstanceOf(MissingClassException::class, $e); - $this->assertSame('Missing Class', $e->getMessage()); - } - try { throw new MissingClassException(MissingClassException::class); } catch (Throwable $e) { @@ -29,12 +22,6 @@ public function testThrow() $this->assertSame('Missing Class: ' . MissingClassException::class, $e->getMessage()); } - try { - throw new MissingClassException(null, 'Test Message'); - } catch (Throwable $e) { - $this->assertInstanceOf(MissingClassException::class, $e); - $this->assertSame('Test Message', $e->getMessage()); - } try { throw new MissingClassException(MissingClassException::class, 'Test Message'); } catch (Throwable $e) { diff --git a/tests/Exceptions/MissingPropertyTest.php b/tests/Exceptions/MissingPropertyTest.php new file mode 100644 index 0000000..91317ef --- /dev/null +++ b/tests/Exceptions/MissingPropertyTest.php @@ -0,0 +1,26 @@ +assertInstanceOf(MissingPropertyException::class, $e); + $this->assertSame('Missing property: test', $e->getMessage()); + $this->assertSame('test', $e->property); + } + } +} diff --git a/tests/exceptions/NotSupportedTest.php b/tests/Exceptions/NotSupportedTest.php similarity index 97% rename from tests/exceptions/NotSupportedTest.php rename to tests/Exceptions/NotSupportedTest.php index 7c20507..bfe1b5d 100644 --- a/tests/exceptions/NotSupportedTest.php +++ b/tests/Exceptions/NotSupportedTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Exceptions; use Php\Support\Exceptions\NotSupportedException; use PHPUnit\Framework\TestCase; diff --git a/tests/Exceptions/UnknownMethodTest.php b/tests/Exceptions/UnknownMethodTest.php new file mode 100644 index 0000000..b556bd4 --- /dev/null +++ b/tests/Exceptions/UnknownMethodTest.php @@ -0,0 +1,25 @@ +assertInstanceOf(UnknownMethodException::class, $e); + $this->assertSame('Unknown method: method', $e->getMessage()); + } + } +} diff --git a/tests/Exceptions/UnknownPropertyTest.php b/tests/Exceptions/UnknownPropertyTest.php new file mode 100644 index 0000000..40d4a01 --- /dev/null +++ b/tests/Exceptions/UnknownPropertyTest.php @@ -0,0 +1,26 @@ +assertInstanceOf(UnknownPropertyException::class, $e); + $this->assertSame('Unknown property: test', $e->getMessage()); + $this->assertSame('test', $e->property); + } + } +} diff --git a/tests/Global/BaseTest.php b/tests/Global/BaseTest.php index 9a6b6c8..4168df1 100644 --- a/tests/Global/BaseTest.php +++ b/tests/Global/BaseTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Global; use Php\Support\Types\Point; use PHPUnit\Framework\TestCase; @@ -18,30 +18,30 @@ final class BaseTest extends TestCase private static function values(): array { return [ - 'key' => 'value 2', - 'int1' => 2, - 'int2' => -12, - 'array' => [ + 'key' => 'value 2', + 'int1' => 2, + 'int2' => -12, + 'array' => [ 1, 2, 3, 4, 5, ], - 'string' => 'string value', - 'null' => null, - 'false' => false, - 'true' => true, - 'float' => 12.31, - 'empty' => '', + 'string' => 'string value', + 'null' => null, + 'false' => false, + 'true' => true, + 'float' => 12.31, + 'empty' => '', 'emptyArray' => [], - 'cls' => new class { - function __invoke() + 'cls' => new class { + public function __invoke() { return 'cls.test'; } }, - 'fn' => static function () { + 'fn' => static function () { return 'fn.test'; }, ]; @@ -64,45 +64,196 @@ public function testIsTrue(): void { foreach ( [ - ['val' => new \stdClass, 'res' => true, 'resNull' => true,], - ['val' => [1, 2], 'res' => true, 'resNull' => true], - ['val' => [1], 'res' => true, 'resNull' => true], - ['val' => [0], 'res' => true, 'resNull' => true], - ['val' => 1, 'res' => true, 'resNull' => true], - ['val' => 42, 'res' => true, 'resNull' => true], - ['val' => -42, 'res' => true, 'resNull' => true], - ['val' => 'true', 'res' => true, 'resNull' => true], - ['val' => '1', 'res' => true, 'resNull' => true], - ['val' => 'on', 'res' => true, 'resNull' => true], - ['val' => 'On', 'res' => true, 'resNull' => true], - ['val' => 'ON', 'res' => true, 'resNull' => true], - ['val' => 'yes', 'res' => true, 'resNull' => true], - ['val' => 'YES', 'res' => true, 'resNull' => true], - ['val' => 'TRUE', 'res' => true, 'resNull' => true], - - - ['val' => 'off', 'res' => false, 'resNull' => false], - ['val' => 'Off', 'res' => false, 'resNull' => false], - ['val' => 'OFF', 'res' => false, 'resNull' => false], - ['val' => 'no', 'res' => false, 'resNull' => false], - ['val' => 'ja', 'res' => false, 'resNull' => false], - ['val' => 'nein', 'res' => false, 'resNull' => false], - ['val' => 'нет', 'res' => false, 'resNull' => false], - ['val' => 'да', 'res' => false, 'resNull' => false], - ['val' => null, 'res' => false, 'resNull' => null], - ['val' => 0, 'res' => false, 'resNull' => false], - ['val' => 'false', 'res' => false, 'resNull' => false], - ['val' => 'FALSE', 'res' => false, 'resNull' => false], - ['val' => 'string', 'res' => false, 'resNull' => false], - ['val' => 'bool', 'res' => false, 'resNull' => false], - ['val' => '0.0', 'res' => false, 'resNull' => false], - ['val' => '4.2', 'res' => false, 'resNull' => false], - ['val' => '0', 'res' => false, 'resNull' => false], - ['val' => '', 'res' => false, 'resNull' => false], - ['val' => '[]', 'res' => false, 'resNull' => false], - ['val' => '{}', 'res' => false, 'resNull' => false], - ['val' => 'false', 'res' => false, 'resNull' => false], - ['val' => 'bar', 'res' => false, 'resNull' => false], + [ + 'val' => new \stdClass(), + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => [ + 1, + 2, + ], + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => [1], + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => [0], + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 1, + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 42, + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => -42, + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'true', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => '1', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'on', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'On', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'ON', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'yes', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'YES', + 'res' => true, + 'resNull' => true, + ], + [ + 'val' => 'TRUE', + 'res' => true, + 'resNull' => true, + ], + + + [ + 'val' => 'off', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'Off', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'OFF', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'no', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'ja', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'nein', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'нет', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'да', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => null, + 'res' => false, + 'resNull' => null, + ], + [ + 'val' => 0, + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'false', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'FALSE', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'string', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'bool', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => '0.0', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => '4.2', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => '0', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => '', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => '[]', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => '{}', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'false', + 'res' => false, + 'resNull' => false, + ], + [ + 'val' => 'bar', + 'res' => false, + 'resNull' => false, + ], ] as $data ) { @@ -132,12 +283,6 @@ public function testInstance(): void foreach ( [ - 1, - 0, - -1, - 12.21, - true, - false, null, '1', 'true', @@ -157,10 +302,10 @@ public function testTraitUsesRecursive(): void static::assertEquals( [ - \Php\Support\Traits\Singleton::class => \Php\Support\Traits\Singleton::class, - \Php\Support\Traits\ArrayStorageConfigurableTrait::class => \Php\Support\Traits\ArrayStorageConfigurableTrait::class, - \Php\Support\Traits\ArrayStorage::class => \Php\Support\Traits\ArrayStorage::class, - \Php\Support\Traits\ConfigurableTrait::class => \Php\Support\Traits\ConfigurableTrait::class, + \Php\Support\Traits\Singleton::class => \Php\Support\Traits\Singleton::class, + \Php\Support\Traits\UseConfigurableStorage::class => \Php\Support\Traits\UseConfigurableStorage::class, + \Php\Support\Traits\UseStorage::class => \Php\Support\Traits\UseStorage::class, + \Php\Support\Traits\ConfigurableTrait::class => \Php\Support\Traits\ConfigurableTrait::class, ], $traits ); @@ -172,11 +317,11 @@ public function testClassUsesRecursive(): void static::assertEquals( [ - \Php\Support\Traits\Singleton::class => \Php\Support\Traits\Singleton::class, - \Php\Support\Traits\ArrayStorageConfigurableTrait::class => \Php\Support\Traits\ArrayStorageConfigurableTrait::class, - \Php\Support\Traits\ArrayStorage::class => \Php\Support\Traits\ArrayStorage::class, - \Php\Support\Traits\ConfigurableTrait::class => \Php\Support\Traits\ConfigurableTrait::class, - \Php\Support\Traits\Maker::class => \Php\Support\Traits\Maker::class, + \Php\Support\Traits\Singleton::class => \Php\Support\Traits\Singleton::class, + \Php\Support\Traits\UseConfigurableStorage::class => \Php\Support\Traits\UseConfigurableStorage::class, + \Php\Support\Traits\UseStorage::class => \Php\Support\Traits\UseStorage::class, + \Php\Support\Traits\ConfigurableTrait::class => \Php\Support\Traits\ConfigurableTrait::class, + \Php\Support\Traits\Maker::class => \Php\Support\Traits\Maker::class, ], $traits ); @@ -189,17 +334,15 @@ public function testClassBasename(): void $name = class_basename(new \stdClass()); static::assertEquals('stdClass', $name); } - } class TraitUsesRecursiveClass { + use \Php\Support\Traits\Singleton; + use \Php\Support\Traits\UseConfigurableStorage; protected $username; - - use \Php\Support\Traits\Singleton; - use \Php\Support\Traits\ArrayStorageConfigurableTrait; } class RecursiveClass extends TraitUsesRecursiveClass diff --git a/tests/Global/EachValueTest.php b/tests/Global/EachValueTest.php new file mode 100644 index 0000000..a9f6276 --- /dev/null +++ b/tests/Global/EachValueTest.php @@ -0,0 +1,24 @@ + mb_strtoupper($value); + $result = mapValue($fnColl, ['test', 'app']); + $expect = [ + 'TEST', + 'APP', + ]; + self::assertEquals($expect, $result); + } + + #[Test] + public function mapValueWithParams(): void + { + $fnColl = static fn(string $value, $key, string $prefix, string $suffix) => + $prefix . mb_strtoupper($value) . $suffix; + $result = mapValue($fnColl, ['test', 'app'], '- ', '.'); + $expect = [ + '- TEST.', + '- APP.', + ]; + self::assertEquals($expect, $result); + } +} diff --git a/tests/helpers/ArrTest.php b/tests/Helpers/ArrTest.php similarity index 78% rename from tests/helpers/ArrTest.php rename to tests/Helpers/ArrTest.php index 38b31a8..0ec1602 100644 --- a/tests/helpers/ArrTest.php +++ b/tests/Helpers/ArrTest.php @@ -2,13 +2,16 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Helpers; +use ArrayObject; use Php\Support\Helpers\Arr; use Php\Support\Helpers\Json; use Php\Support\Interfaces\Jsonable; +use Php\Support\Structures\Collections\ArrayCollection; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; -use ArrayObject; /** * Class ArrTest @@ -105,7 +108,7 @@ public function testMerge(): void } - public function providerDataToArray(): array + public static function providerDataToArray(): array { $arrayableClass = new class () implements \Php\Support\Interfaces\Arrayable { private $data = [ @@ -119,7 +122,7 @@ public function toArray(): array return $this->data; } }; - $jsonableClass = new class () implements \Php\Support\Interfaces\Jsonable { + $jsonableClass = new class () implements \Php\Support\Interfaces\Jsonable { private $data = [ '32', 12, @@ -143,7 +146,8 @@ public function toJson($options = 320): ?string 1, 2, 3, - ], [ + ], + [ 1, 2, 3, @@ -159,24 +163,24 @@ public function toJson($options = 320): ?string ], [ [ - 'test' => 1, - 0 => 14, + 'test' => 1, + 0 => 14, 'nested' => [ - 'cl' => $arrayableClass, + 'cl' => $arrayableClass, 'cl2' => $arrayableClass, - '1' => [ + '1' => [ 1, 2, $jsonableClass, ], ], - 'csl' => $arrayableClass, + 'csl' => $arrayableClass, ], [ - 'test' => 1, - 0 => 14, + 'test' => 1, + 0 => 14, 'nested' => [ - 'cl' => [ + 'cl' => [ '1', 2, 'test', @@ -186,7 +190,7 @@ public function toJson($options = 320): ?string 2, 'test', ], - '1' => [ + '1' => [ 1, 2, [ @@ -196,7 +200,7 @@ public function toJson($options = 320): ?string ], ], ], - 'csl' => [ + 'csl' => [ '1', 2, 'test', @@ -219,23 +223,26 @@ public function toJson($options = 320): ?string 'test', ], ], - [new class () implements \JsonSerializable { - private $data = [ + [ + new class () implements \JsonSerializable { + private $data = + [ + + '132', + 12, + 'test', + ]; + public function jsonSerialize(): mixed + { + return $this->data; + } + }, + [ '132', 12, 'test', - ]; - - public function jsonSerialize() - { - return $this->data; - } - }, [ - '132', - 12, - 'test', - ], + ], ], [ new ArrayObject([12, 'test 1']), @@ -247,14 +254,7 @@ public function jsonSerialize() ]; } - /** - * @dataProvider providerDataToArray - * - * @param mixed $items - * @param $exp - * - * @throws \Php\Support\Exceptions\JsonException - */ + #[DataProvider('providerDataToArray')] public function testDataToArray($items, $exp): void { $result = Arr::dataToArray($items); @@ -262,10 +262,7 @@ public function testDataToArray($items, $exp): void static::assertEquals($exp, $result); } - /** - * @return array - */ - public function providerToArray(): array + public static function providerToArray(): array { $arrayableClass = new class () implements \Php\Support\Interfaces\Arrayable { private $data = [ @@ -279,7 +276,7 @@ public function toArray(): array return $this->data; } }; - $jsonableClass = new class () implements \Php\Support\Interfaces\Jsonable { + $jsonableClass = new class () implements \Php\Support\Interfaces\Jsonable { private $data = [ '32', 12, @@ -303,7 +300,8 @@ public function toJson($options = 320): ?string 1, 2, 3, - ], [ + ], + [ 1, 2, 3, @@ -341,23 +339,26 @@ public function toJson($options = 320): ?string 'test', ], ], - [new class () implements \JsonSerializable { - private $data = [ + [ + new class () implements \JsonSerializable { + private $data = + [ + + '132', + 12, + 'test', + ]; + public function jsonSerialize(): mixed + { + return $this->data; + } + }, + [ '132', 12, 'test', - ]; - - public function jsonSerialize() - { - return $this->data; - } - }, [ - '132', - 12, - 'test', - ], + ], ], [ new ArrayObject([12, 'test 1']), @@ -369,14 +370,7 @@ public function jsonSerialize() ]; } - /** - * @dataProvider providerToArray - * - * @param $items - * @param $exp - * - * @throws \Php\Support\Exceptions\JsonException - */ + #[DataProvider('providerToArray')] public function testToArray($items, $exp): void { $result = Arr::toArray($items); @@ -417,8 +411,8 @@ public function testExists(): void { $array = [ 'key1' => 'val1', - 2 => 'val2', - 0 => 'val0', + 2 => 'val2', + 0 => 'val0', 'test' => 'test', ]; @@ -429,7 +423,6 @@ public function testExists(): void static::assertFalse(Arr::exists($array, 'test.key1')); static::assertFalse(Arr::exists($array, 'te')); static::assertFalse(Arr::exists($array, '')); - static::assertFalse(Arr::exists($array, null)); static::assertFalse(Arr::exists($array, 1)); @@ -449,9 +442,9 @@ public function testExists(): void public function testToIndexedArray(): void { $array = [ - 'key1' => 'val1', - 'test' => 'test', - 'nested' => [ + 'key1' => 'val1', + 'test' => 'test', + 'nested' => [ 'n1' => 'test1', 'n2' => 'test2', ], @@ -495,19 +488,15 @@ public function testToIndexedArray(): void static::assertEquals($expected, $res); } - - /** - * @throws \Php\Support\Exceptions\JsonException - */ public function testToPostgresArray(): void { static::assertEquals( '{val1,test,null,,null}', Arr::ToPostgresArray( [ - 'key1' => 'val1', - 'test' => 'test', - 'nested' => null, + 'key1' => 'val1', + 'test' => 'test', + 'nested' => null, 'indexed1' => '', 'indexed2' => null, ] @@ -550,43 +539,74 @@ public function testToPostgresArray(): void static::assertEquals('{}', Arr::ToPostgresArray([])); } + public function testToPostgresPoint(): void + { + static::assertEquals('(123,0.332)', Arr::ToPostgresPoint([123.00, 0.332])); + static::assertEquals('(123.012,10.332)', Arr::ToPostgresPoint([123.012, 10.332])); + + static::assertNull(Arr::ToPostgresPoint([])); + static::assertNull(Arr::ToPostgresPoint([1])); + static::assertNull(Arr::ToPostgresPoint([1, 2, 3])); + } + public function testFromPostgresArray(): void { static::assertEquals(['val1', 'test', 'null', '', 'null'], Arr::fromPostgresArray('{val1,test,null,,null}')); - static::assertEquals(['val1', '1', '', '3', 'null', '', 'null'], Arr::fromPostgresArray('{val1,1,,3,null,,null}')); + static::assertEquals( + [ + 'val1', + '1', + '', + '3', + 'null', + '', + 'null', + ], + Arr::fromPostgresArray('{val1,1,,3,null,,null}') + ); static::assertEquals([], Arr::fromPostgresArray('{}')); } - /** - * @return array - */ - public function providerRemoveByValue(): array + public function testFromPostgresPoint(): void + { + static::assertEquals([32.323, 2342342.0], Arr::fromPostgresPoint('(32.323,2342342)')); + static::assertEquals([12.3223, 0.3223], Arr::fromPostgresPoint('(12.3223,0.3223)')); + static::assertNull(Arr::fromPostgresPoint('()')); + static::assertNull(Arr::fromPostgresPoint(null)); + static::assertNull(Arr::fromPostgresPoint('')); + } + + public static function providerRemoveByValue(): array { return [ [ [ 0 => 1, 2 => 3, - ], 1, + ], + 1, [ 1, 2, 3, - ], 2, + ], + 2, ], [ [ 1 => 'val 21', 2 => 'vat', 3 => 'test', - ], 0, + ], + 0, [ 'val 2', 'val 21', 'vat', 'test', - ], 'val 2', + ], + 'val 2', ], [ [ @@ -594,27 +614,31 @@ public function providerRemoveByValue(): array 2 => 'val 21', 3 => 'vat', 4 => 'test', - ], 0, + ], + 0, [ 'val 2', 'val 2', 'val 21', 'vat', 'test', - ], 'val 2', + ], + 'val 2', ], [ [ 'val 2', 'val 21', 'vat', - ], 3, + ], + 3, [ 'val 2', 'val 21', 'vat', null, - ], null, + ], + null, ], [ [ @@ -622,24 +646,28 @@ public function providerRemoveByValue(): array 'val 21', 'vat', null, - ], null, + ], + null, [ 'val 2', 'val 21', 'vat', null, - ], 1, + ], + 1, ], [ [ 'key 1' => 'val 1', 'key 2' => 'val 2', - ], 'key 4', + ], + 'key 4', [ 'key 1' => 'val 1', 'key 2' => 'val 2', 'key 4' => 'val 4', - ], 'val 4', + ], + 'val 4', ], [ ['a'], @@ -692,14 +720,7 @@ public function providerRemoveByValue(): array ]; } - /** - * @dataProvider providerRemoveByValue - * - * @param $expArray - * @param $expIdx - * @param $array - * @param $val - */ + #[DataProvider('providerRemoveByValue')] public function testRemoveByValue($expArray, $expIdx, $array, $val): void { $idx = Arr::removeByValue($array, $val); @@ -707,35 +728,36 @@ public function testRemoveByValue($expArray, $expIdx, $array, $val): void static::assertEquals($expIdx, $idx); } - /** - * @return array - */ - public function providerRemoveByValueAndReindex(): array + public static function providerRemoveByValueAndReindex(): array { return [ [ [ 1, 3, - ], 1, + ], + 1, [ 1, 2, 3, - ], 2, + ], + 2, ], [ [ 'val 21', 'vat', 'test', - ], 0, + ], + 0, [ 'val 2', 'val 21', 'vat', 'test', - ], 'val 2', + ], + 'val 2', ], [ [ @@ -743,27 +765,31 @@ public function providerRemoveByValueAndReindex(): array 'val 21', 'vat', 'test', - ], 0, + ], + 0, [ 'val 2', 'val 2', 'val 21', 'vat', 'test', - ], 'val 2', + ], + 'val 2', ], [ [ 'val 2', 'val 21', 'vat', - ], 3, + ], + 3, [ 'val 2', 'val 21', 'vat', null, - ], null, + ], + null, ], [ [ @@ -771,24 +797,28 @@ public function providerRemoveByValueAndReindex(): array 'val 21', 'vat', null, - ], null, + ], + null, [ 'val 2', 'val 21', 'vat', null, - ], 1, + ], + 1, ], [ [ 'val 1', 'val 2', - ], 'key 4', + ], + 'key 4', [ 'key 1' => 'val 1', 'key 2' => 'val 2', 'key 4' => 'val 4', - ], 'val 4', + ], + 'val 4', ], [ @@ -842,14 +872,7 @@ public function providerRemoveByValueAndReindex(): array ]; } - /** - * @dataProvider providerRemoveByValueAndReindex - * - * @param $expArray - * @param $expIdx - * @param $array - * @param $val - */ + #[DataProvider('providerRemoveByValueAndReindex')] public function testRemoveByValueAndReindex($expArray, $expIdx, $array, $val): void { $idx = Arr::removeByValue($array, $val, true); @@ -861,15 +884,16 @@ public function testRemoveByValueAndReindex($expArray, $expIdx, $array, $val): v /** * @return array */ - public function providerGet(): array + public static function providerGet(): array { $array = [ - 'key' => [ + 'key' => [ 'sub1' => 'val1', 'sub2' => [ 'val2', 'val3', - ], 'sub4' => ['sub4sub' => 'val3'], + ], + 'sub4' => ['sub4sub' => 'val3'], ], 'key2' => 2, 'key4' => 1, @@ -895,7 +919,8 @@ public function providerGet(): array [ 'val2', 'val3', - ], $array, + ], + $array, 'key.sub2', ], [ @@ -949,13 +974,7 @@ public function providerGet(): array ]; } - /** - * @dataProvider providerGet - * - * @param $expVal - * @param $array - * @param $key - */ + #[DataProvider('providerGet')] public function testGet($expVal, $array, $key): void { $val = Arr::get($array, $key); @@ -967,18 +986,16 @@ public function testGet($expVal, $array, $key): void static::assertEquals($expVal ?? 'test', $val); } - /** - * @return array - */ - public function providerHas(): array + public static function providerHas(): array { $array = [ - 'key' => [ + 'key' => [ 'sub1' => 'val1', 'sub2' => [ 'val2', 'val3', - ], 'sub4' => ['sub4sub' => 'val3'], + ], + 'sub4' => ['sub4sub' => 'val3'], ], 'key2' => 2, 'key4' => 1, @@ -1015,32 +1032,6 @@ public function providerHas(): array $array, 'key.sub12', ], - [ - false, - $array, - null, - ], - [ - false, - null, - null, - ], - [ - false, - null, - [], - ], - [ - false, - null, - 0, - ], - [ - false, - '', - 0, - ], - [ true, $array, @@ -1054,22 +1045,13 @@ public function providerHas(): array ]; } - /** - * @dataProvider providerHas - * - * @param $expVal - * @param $array - * @param $key - */ + #[DataProvider('providerHas')] public function testHas($expVal, $array, $key): void { static::assertEquals($expVal, Arr::has($array, $key)); } - /** - * @return array - */ - public function providerSet(): array + public static function providerSet(): array { $array = []; @@ -1093,7 +1075,8 @@ public function providerSet(): array [ 'val2', 'val3', - ], $array, + ], + $array, 'key.sub2', ], [ @@ -1116,23 +1099,10 @@ public function providerSet(): array $array, 'key3', ], - [ - null, - null, - '2', - ], - ]; } - /** - * @dataProvider providerSet - * - * @param $expVal - * @param $array - * @param $key - * @param $val - */ + #[DataProvider('providerSet')] public function testSet($expVal, $array, $key): void { Arr::set($array, $key, $expVal); @@ -1142,25 +1112,29 @@ public function testSet($expVal, $array, $key): void public function testSet2(): void { - $array = null; - static::assertNull(Arr::set($array, '', 1)); - $array = []; static::assertEquals(['' => 1], Arr::set($array, '', 1)); } - /** - * @return array - */ - public function providerRemove(): array + public function testSetWithDivider(): void + { + $array = ['key' => ['sub2' => 1]]; + $expVal = 121; + Arr::set($array, 'key/sub3/sub4sub', $expVal, '/'); + + static::assertEquals(['key' => ['sub2' => 1, 'sub3' => ['sub4sub' => 121]]], $array); + } + + public static function providerRemove(): array { $array = [ - 'key' => [ + 'key' => [ 'sub1' => 'val1', 'sub2' => [ 'val2', 'val3', - ], 'sub4' => ['sub4sub' => 'val3'], + ], + 'sub4' => ['sub4sub' => 'val3'], ], 'key2' => 2, 'key4' => 1, @@ -1202,13 +1176,7 @@ public function providerRemove(): array ]; } - - /** - * @dataProvider providerRemove - * - * @param $array - * @param $key - */ + #[DataProvider('providerRemove')] public function testRemove($array, $key): void { Arr::remove($array, $key); @@ -1222,15 +1190,12 @@ public function testRemove2(): void 'key2' => 2, 'key4' => 1, ]; - Arr::remove($array, null); + Arr::remove($array, []); static::assertEquals($array, Arr::get($array, null)); } - /** - * @return array - */ - public function dataReplaceByTemplate(): array + public static function dataReplaceByTemplate(): array { return [ [ @@ -1240,73 +1205,79 @@ public function dataReplaceByTemplate(): array ], [ [ - 'key' => '{{%KEY%}}', + 'key' => '{{%KEY%}}', 'token' => '{{%TOKEN%}}', - ], [ - '{{%KEY%}}' => 'vKey', + ], + [ + '{{%KEY%}}' => 'vKey', '{{%TOKEN%}}' => 'vToken', - ], [ - 'key' => 'vKey', + ], + [ + 'key' => 'vKey', 'token' => 'vToken', ], ], [ [ - 'key' => '{{%KEY%}}', + 'key' => '{{%KEY%}}', 'token' => '{{%TOKEN%}}', - ], ['{{%KEY%}}' => 'vKey'], + ], + ['{{%KEY%}}' => 'vKey'], [ - 'key' => 'vKey', + 'key' => 'vKey', 'token' => '{{%TOKEN%}}', ], ], [ [ - 'key' => '{{%KEY%}}', + 'key' => '{{%KEY%}}', 'token' => '{{%TOKEN%}}', - ], ['{{%KEY%}}' => ''], + ], + ['{{%KEY%}}' => ''], [ - 'key' => '', + 'key' => '', 'token' => '{{%TOKEN%}}', ], ], [ [ - 'key' => '{{%KEY%}}', + 'key' => '{{%KEY%}}', 'token' => '{{%TOKEN%}}', - ], ['{{%KEY%}}' => null], + ], + ['{{%KEY%}}' => null], [ - 'key' => '', + 'key' => '', 'token' => '{{%TOKEN%}}', ], ], [ [ 'step1' => [ - 'key' => '{{%KEY%}}', + 'key' => '{{%KEY%}}', 'token' => '{{%TOKEN%}}', ], 'step2' => [ 'subStep2' => [ 'token' => '{{%TOKEN%}}', - 'key' => '{{%KEY%}}', + 'key' => '{{%KEY%}}', ], ], 'step3' => ['val' => '{{%VALUE%}}'], - ], [ - '{{%KEY%}}' => 'vKey', + ], + [ + '{{%KEY%}}' => 'vKey', '{{%TOKEN%}}' => 'vToken', '{{%VALUE%}}' => 12, ], [ 'step1' => [ - 'key' => 'vKey', + 'key' => 'vKey', 'token' => 'vToken', ], 'step2' => [ 'subStep2' => [ 'token' => 'vToken', - 'key' => 'vKey', + 'key' => 'vKey', ], ], 'step3' => ['val' => '12'], @@ -1315,9 +1286,10 @@ public function dataReplaceByTemplate(): array [ ['sdasdas'], [ - '{{%KEY%}}' => 'key', + '{{%KEY%}}' => 'key', '{{%TOKEN%}}' => 'token', - ], ['sdasdas'], + ], + ['sdasdas'], ], [ ['sdaas'], @@ -1327,21 +1299,33 @@ public function dataReplaceByTemplate(): array ]; } - /** - * @dataProvider dataReplaceByTemplate - * - * @param $array - * @param $replace - * @param $exp - */ + #[DataProvider('dataReplaceByTemplate')] public function testReplaceByTemplate(array $array, array $replace, array $exp): void { $res = Arr::replaceByTemplate($array, $replace); -// var_dump($res); -// var_dump($exp); -// static::assertEquals($exp, $res); + // var_dump($res); + // var_dump($exp); + // static::assertEquals($exp, $res); static::assertJsonStringEqualsJsonString(\json_encode($exp), \json_encode($res)); -// static::assertEquals($exp, $res); + // static::assertEquals($exp, $res); + } + + #[Test] + public function collapse(): void + { + $list = [new ArrayCollection([1, 2, 3]), 4, 5, 6, [7, 8, 9]]; + + self::assertEquals([1, 2, 3, 7, 8, 9], Arr::collapse($list)); + } + + #[Test] + public function prepend(): void + { + $list = [1, 2, 3]; + self::assertEquals([5, 1, 2, 3], Arr::prepend($list, 5)); + + $list = ['One' => 1, 'Two' => 2]; + self::assertEquals(['Five' => 5, 'One' => 1, 'Two' => 2], Arr::prepend($list, 5, 'Five')); } } diff --git a/tests/helpers/B64Test.php b/tests/Helpers/B64Test.php similarity index 90% rename from tests/helpers/B64Test.php rename to tests/Helpers/B64Test.php index 21ac32d..7b0f341 100644 --- a/tests/helpers/B64Test.php +++ b/tests/Helpers/B64Test.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Helpers; use Php\Support\Helpers\B64; use PHPUnit\Framework\TestCase; @@ -24,8 +24,9 @@ final class B64Test extends TestCase '12Кириллик' => 'MTLQmtC40YDQuNC70LvQuNC6', "12Кир\nиллик\nen" => 'MTLQmtC40YAK0LjQu9C70LjQugplbg==', "12Кир\tиллик\ten" => 'MTLQmtC40YAJ0LjQu9C70LjQugllbg==', - "'πάντα χωρεῖ καὶ οὐδὲν μένει …'" => 'J8+AzqzOvc+EzrEgz4fPic+BzrXhv5YgzrrOseG9tiDOv+G9kM604b2yzr0gzrzOrc69zrXOuSDigKYn', - '🤪 🤪 😈' => '8J+kqiDwn6SqIPCfmIg=', + "'πάντα χωρεῖ καὶ οὐδὲν μένει …'" => + 'J8+AzqzOvc+EzrEgz4fPic+BzrXhv5YgzrrOseG9tiDOv+G9kM604b2yzr0gzrzOrc69zrXOuSDigKYn', + '🤪 🤪 😈' => '8J+kqiDwn6SqIPCfmIg=', ]; private static $emptyList = [ diff --git a/tests/helpers/BitTest.php b/tests/Helpers/BitTest.php similarity index 99% rename from tests/helpers/BitTest.php rename to tests/Helpers/BitTest.php index fd5fad7..32151a7 100644 --- a/tests/helpers/BitTest.php +++ b/tests/Helpers/BitTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Helpers; use Php\Support\Helpers\Bit; use PHPUnit\Framework\TestCase; diff --git a/tests/Helpers/HasReflection.php b/tests/Helpers/HasReflection.php new file mode 100644 index 0000000..3d686a4 --- /dev/null +++ b/tests/Helpers/HasReflection.php @@ -0,0 +1,19 @@ +getMethod($name); + $method->setAccessible(true); + return $method; + } +} diff --git a/tests/helpers/JsonTest.php b/tests/Helpers/JsonTest.php similarity index 81% rename from tests/helpers/JsonTest.php rename to tests/Helpers/JsonTest.php index 76a4456..3a239bf 100644 --- a/tests/helpers/JsonTest.php +++ b/tests/Helpers/JsonTest.php @@ -2,21 +2,20 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Helpers; use JsonException; use Php\Support\Helpers\Json; use PHPUnit\Framework\TestCase; use Throwable; - /** * Class JsonTest */ final class JsonTest extends TestCase { /** - * @throws ReflectionException + * @throws \ReflectionException */ public function testEncode(): void { @@ -97,11 +96,6 @@ public function testHtmlEncode(): void // JsonSerializable $data = new JsonModel(); self::assertSame('{"json":"serializable"}', Json::htmlEncode($data)); - - // $postsStack = new \SplStack(); - // $postsStack->push(new Post(915, 'record1')); - // $postsStack->push(new Post(456, 'record2')); - // self::assertSame('{"1":{"id":456,"title":"record2"},"0":{"id":915,"title":"record1"}}', Json::encode($postsStack)); } /** @@ -115,6 +109,7 @@ public function testDecode(): void // basic data decoding $json = '"1"'; self::assertSame('1', Json::decode($json)); + self::assertSame('1', Json::decode($json, true, JSON_INVALID_UTF8_IGNORE)); // array decoding $json = '{"a":1,"b":2}'; self::assertSame(['a' => 1, 'b' => 2], Json::decode($json)); @@ -124,16 +119,16 @@ public function testDecode(): void self::assertEquals([], Json::decode("[]", true, JSON_THROW_ON_ERROR, 2)); // exception $json = '{"a":1,"b":2'; - $this->expectException(JsonException::class); - Json::decode($json, true, JSON_THROW_ON_ERROR); +// $this->expectException(JsonException::class); + self::assertNull(Json::decode($json, true, JSON_THROW_ON_ERROR)); } /** */ public function testDecodeInvalidParamException(): void { - $this->expectException(JsonException::class); - $this->expectExceptionMessage('Syntax error'); +// $this->expectException(JsonException::class); +// $this->expectExceptionMessage('Syntax error'); $res = Json::decode('sa', true, JSON_THROW_ON_ERROR); self::assertNull($res); @@ -152,28 +147,9 @@ public function testDecodeInvalidParamException2(): void */ public function testHandleJsonError(): void { - try { - $json = "{'a': '1'}"; - Json::decode($json, true, JSON_THROW_ON_ERROR); - } catch (Throwable $exception) { - self::assertInstanceOf(\JsonException::class, $exception); - self::assertSame('Syntax error', $exception->getMessage()); - } - - try { - $fp = fopen('php://stdin', 'rb'); - $data = ['a' => $fp]; - Json::encode($data); - fclose($fp); - } catch (Throwable $exception) { - self::assertInstanceOf(\JsonException::class, $exception); - if (PHP_VERSION_ID >= 50500) { - self::assertSame( - 'Type is not supported', - $exception->getMessage() - ); - } - } + $json = "{'a': '1'}"; + static::assertNull(Json::decode($json, )); + static::assertNull(Json::decode($json, true, JSON_THROW_ON_ERROR)); } @@ -228,7 +204,7 @@ public function jsonSerialize() }*/ /** - * @dataProvider providerToArray + * dataProvider providerToArray * * @param $items * @param $exp @@ -253,7 +229,7 @@ class JsonModel implements \JsonSerializable /** @var array */ public $data = ['json' => 'serializable']; - public function jsonSerialize() + public function jsonSerialize(): mixed { return $this->data; } diff --git a/tests/Helpers/NumberTest.php b/tests/Helpers/NumberTest.php new file mode 100644 index 0000000..3f57381 --- /dev/null +++ b/tests/Helpers/NumberTest.php @@ -0,0 +1,187 @@ + 'key', + '{{%KEY%}}' => 'key', '{{%TOKEN%}}' => 'token', ], '"key-token" - test', @@ -619,7 +591,7 @@ public function dataReplaceByTemplate(): array [ 'sdasdas', [ - '{{%KEY%}}' => 'key', + '{{%KEY%}}' => 'key', '{{%TOKEN%}}' => 'token', ], 'sdasdas', @@ -632,42 +604,170 @@ public function dataReplaceByTemplate(): array ]; } - /** - * @dataProvider dataReplaceByTemplate - * - * @param string $str - * @param array $replaced - * @param string $exp - */ + + #[DataProvider('dataReplaceByTemplate')] public function testReplaceByTemplate(string $str, array $replaced, string $exp): void { $result = Str::replaceByTemplate($str, $replaced); static::assertEquals($exp, $result); } - public function dataRegExps(): array + public static function dataRegExps(): array { return [ - ['/^(\d+)$/', true], - ['/([A-Z])\w+/', true], - ['/\{(?[\w]+?)(:(?[\\\$^()+\w]+?))?}/', true], - - ['^(\d+)$', false], - ['\d+)$', false], - ['', false], - ['test', false], - ['/\{(?[\w]+?)(:(?[\\\$^()+\w]+?)?}/', false], + [ + '/^(\d+)$/', + true, + ], + [ + '/([A-Z])\w+/', + true, + ], + [ + '/\{(?[\w]+?)(:(?[\\\$^()+\w]+?))?}/', + true, + ], + + [ + '^(\d+)$', + false, + ], + [ + '\d+)$', + false, + ], + [ + '', + false, + ], + [ + 'test', + false, + ], + [ + '/\{(?[\w]+?)(:(?[\\\$^()+\w]+?)?}/', + false, + ], ]; } - /** - * @dataProvider dataRegExps - * - * @param string $regexp - * @param bool $result - */ + #[DataProvider('dataRegExps')] public function testIsRegExp(string $regexp, bool $result): void { - static::assertEquals($result, Str::isRegExp($regexp)); + self::assertEquals($result, Str::isRegExp($regexp)); + } + + + #[Test] + public function truncate(): void + { + self::assertEquals( + 'The quick brown fox...', + Str::truncate('The quick brown fox jumps over the lazy dog', 24) + ); + self::assertEquals( + 'The quick brown fox>', + Str::truncate('The quick brown fox jumps over the lazy dog', 24, '>') + ); + self::assertEquals( + 'The quick brown fox jumps over the lazy dog', + Str::truncate('The quick brown fox jumps over the lazy dog', 55) + ); + self::assertEquals('Th...', Str::truncate('The quick brown fox jumps over the lazy dog', 2)); + self::assertEquals('The...', Str::truncate('The quick brown fox jumps over the lazy dog', 3)); + self::assertEquals('The...', Str::truncate('The quick brown fox jumps over the lazy dog', 7)); + } + + #[Test] + public function seemsUTF8(): void + { + // Test a valid UTF-8 sequence: "ÜTF-8 Fµñ". + $validUTF8 = "\xC3\x9CTF-8 F\xC2\xB5\xC3\xB1"; + self::assertTrue(Str::seemsUTF8($validUTF8)); + + self::assertTrue( + Str::seemsUTF8("\xEF\xBF\xBD this has \xEF\xBF\xBD\xEF\xBF\xBD some invalid UTF-8 \xEF\xBF\xBD") + ); + + // Test invalid UTF-8 sequences + $invalidUTF8 = "\xc3 this has \xe6\x9d some invalid UTF-8 \xe6"; + self::assertFalse(Str::seemsUTF8($invalidUTF8)); + + // And test some plain ASCII + self::assertTrue(Str::seemsUTF8('The quick brown fox jumps over the lazy dog')); + + // Test an invalid non-UTF-8 string. + if (function_exists('mb_convert_encoding')) { + mb_internal_encoding('UTF-8'); + // Converts the 'ç' UTF-8 character to UCS-2LE + $utf8Char = pack('n', 50087); + $ucsChar = mb_convert_encoding($utf8Char, 'UCS-2LE', 'UTF-8'); + + self::assertEquals( + $utf8Char, + 'ç', + 'This PHP system\'s internal character set is not properly set as UTF-8.' + ); + self::assertEquals($utf8Char, pack('n', 50087), 'Something is wrong with your ICU unicode library.'); + + // Test for not UTF-8. + self::assertFalse(Str::seemsUTF8($ucsChar)); + + // Test the worker method. + $method = self::setMethodAccessible(URLify::class, 'seemsUTF8Regex'); + self::assertFalse( + $method->invoke(null, $invalidUTF8), + self::class . '::seemsUTF8Regex did not properly detect invalid UTF-8.' + ); + self::assertTrue( + $method->invoke(null, $validUTF8), + self::class . '::seemsUTF8Regex did not properly detect valid UTF-8.' + ); + } + } + + #[Test] + public function slugify(): void + { + $this->assertEquals('a-simple-title', Str::slugify('A simple title')); + $this->assertEquals('this-post-it-has-a-dash', Str::slugify('This post -- it has a dash')); + $this->assertEquals('123-1251251', Str::slugify('123----1251251')); + $this->assertEquals('one23-1251251', Str::slugify('123----1251251', '-', true)); + + $this->assertEquals('a-simple-title', Str::slugify('A simple title', '-')); + $this->assertEquals('this-post-it-has-a-dash', Str::slugify('This post -- it has a dash', '-')); + $this->assertEquals('123-1251251', Str::slugify('123----1251251', '-')); + $this->assertEquals('one23-1251251', Str::slugify('123----1251251', '-', true)); + + $this->assertEquals('a_simple_title', Str::slugify('A simple title', '_')); + $this->assertEquals('this_post_it_has_a_dash', Str::slugify('This post -- it has a dash', '_')); + $this->assertEquals('123_1251251', Str::slugify('123----1251251', '_')); + $this->assertEquals('one23_1251251', Str::slugify('123----1251251', '_', true)); + + // Blank separator test + $this->assertEquals('asimpletitle', Str::slugify('A simple title', '')); + $this->assertEquals('thispostithasadash', Str::slugify('This post -- it has a dash', '')); + $this->assertEquals('1231251251', Str::slugify('123----1251251', '')); + $this->assertEquals('one231251251', Str::slugify('123----1251251', '', true)); + } + + #[Test] + public function trimPrefix(): void + { + $this->assertEquals('title', Str::trimPrefix('a-simple:title', 'a-simple:')); + $this->assertEquals('a-simple:title', Str::trimPrefix('a-simple:title', '')); + $this->assertEquals('a-simple:title', Str::trimPrefix('a-simple:title', 'asdas')); + $this->assertEquals('a-simple:title', Str::trimPrefix('a-simple:title', 'a-sdas')); + $this->assertEquals('', Str::trimPrefix('', 'a-simple:')); + } + + #[Test] + public function trimSuffix(): void + { + $this->assertEquals('a-simple:', Str::trimSuffix('a-simple:title', 'title')); + $this->assertEquals('a-simple:title', Str::trimSuffix('a-simple:title', '')); + $this->assertEquals('a-simple:title', Str::trimSuffix('a-simple:title', 'asdas')); + $this->assertEquals('a-simple:title', Str::trimSuffix('a-simple:title', 'a-sdas')); + $this->assertEquals('', Str::trimSuffix('', 'a-simple:')); } } diff --git a/tests/StorageTest.php b/tests/StorageTest.php new file mode 100644 index 0000000..739d78a --- /dev/null +++ b/tests/StorageTest.php @@ -0,0 +1,150 @@ +jsonSerialize()); + } + + #[Test] + public function setSimpleProp(): void + { + $storage = new Storage(); + $storage->test = 'name'; + + self::assertEquals(['test' => 'name'], $storage->jsonSerialize()); + } + + #[Test] + public function getSimpleProp(): void + { + $storage = new Storage(); + $storage->test = 'name'; + + self::assertEquals('name', $storage->test); + } + + #[Test] + public function setPathProp(): void + { + $storage = new Storage(); + $storage->{'first.second'} = 'name'; + + self::assertEquals(['first' => ['second' => 'name']], $storage->jsonSerialize()); + } + + #[Test] + public function getPathProp(): void + { + $storage = new Storage(); + $storage->{'first.second'} = 'name'; + $storage->{'first.second2'} = 'test'; + + self::assertEquals(['second' => 'name', 'second2' => 'test'], $storage->{'first'}); + self::assertEquals('name', $storage->{'first.second'}); + self::assertEquals('test', $storage->{'first.second2'}); + } + + #[Test] + public function setFn(): void + { + $storage = new Storage(); + $storage->set('first.second', 'name'); + + self::assertEquals(['first' => ['second' => 'name']], $storage->jsonSerialize()); + } + + #[Test] + public function getFn(): void + { + $storage = new Storage(); + $storage->set('first.second', 'name'); + $storage->set('first.second2', 1); + + self::assertEquals(['second' => 'name', 'second2' => 1], $storage->get('first')); + self::assertEquals('name', $storage->get('first.second')); + self::assertEquals(1, $storage->get('first.second2')); + } + + #[Test] + public function remove(): void + { + $storage = new Storage(); + $storage->set('first.second', 'name'); + $storage->set('first.second2', 1); + + $storage->remove('first'); + self::assertEquals([], $storage->jsonSerialize()); + + $storage->set('first.second', 'name'); + $storage->set('first.second2', 1); + + $storage->remove('first.second'); + + self::assertEquals(['second2' => 1], $storage->get('first')); + self::assertEquals(1, $storage->get('first.second2')); + self::assertNull($storage->get('first.second')); + } + + #[Test] + public function unsetFn(): void + { + $storage = new Storage(); + $storage->set('first.second', 'name'); + $storage->set('first.second2', 1); + + unset($storage->{'first.second'}); + + self::assertEquals(['second2' => 1], $storage->get('first')); + self::assertEquals(1, $storage->get('first.second2')); + self::assertNull($storage->get('first.second')); + } + + #[Test] + public function exist(): void + { + $storage = new Storage(); + self::assertFalse($storage->exist('first')); + $storage->set('first', 1); + + self::assertTrue($storage->exist('first')); + } + + #[Test] + public function issetFn(): void + { + $storage = new Storage(); + self::assertFalse(isset($storage->first)); + $storage->set('first', 1); + + self::assertTrue(isset($storage->first)); + } + + #[Test] + public function offsets(): void + { + $storage = new Storage(); + self::assertFalse(isset($storage['first'])); + $storage['first'] = 1; + + self::assertTrue(isset($storage['first'])); + self::assertEquals(1, $storage['first']); + + unset($storage['first']); + self::assertNull($storage['first']); + self::assertEquals([], $storage->jsonSerialize()); + + } +} \ No newline at end of file diff --git a/tests/traits/ConfigurableTest.php b/tests/Traits/ConfigurableTest.php similarity index 96% rename from tests/traits/ConfigurableTest.php rename to tests/Traits/ConfigurableTest.php index 4592308..0da9aad 100644 --- a/tests/traits/ConfigurableTest.php +++ b/tests/Traits/ConfigurableTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Traits; use Php\Support\Exceptions\InvalidParamException; use PHPUnit\Framework\TestCase; @@ -53,7 +53,7 @@ public function testConfigurableThrow(): void $cls->configurable(['prop' => 'success', 'test' => 'fake']); } catch (\Throwable $exception) { $this->assertInstanceOf(InvalidParamException::class, $exception); - $this->assertNull($exception->getParam()); + $this->assertNull($exception->name); } } diff --git a/tests/traits/ConsolePrintTest.php b/tests/Traits/ConsolePrintTest.php similarity index 97% rename from tests/traits/ConsolePrintTest.php rename to tests/Traits/ConsolePrintTest.php index 86e766b..3d8300a 100644 --- a/tests/traits/ConsolePrintTest.php +++ b/tests/Traits/ConsolePrintTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests\traits; +namespace Php\Support\Tests\Traits; use Php\Support\Traits\ConsolePrint; use PHPUnit\Framework\TestCase; diff --git a/tests/traits/InterceptFilter.php b/tests/Traits/InterceptFilter.php similarity index 94% rename from tests/traits/InterceptFilter.php rename to tests/Traits/InterceptFilter.php index a8231ec..e8cd607 100644 --- a/tests/traits/InterceptFilter.php +++ b/tests/Traits/InterceptFilter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests\traits; +namespace Php\Support\Tests\Traits; /** * Class InterceptFilter diff --git a/tests/traits/MakerTest.php b/tests/Traits/MakerTest.php similarity index 96% rename from tests/traits/MakerTest.php rename to tests/Traits/MakerTest.php index 50ac6ef..b1e96ca 100644 --- a/tests/traits/MakerTest.php +++ b/tests/Traits/MakerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Traits; use PHPUnit\Framework\TestCase; diff --git a/tests/traits/MetableTest.php b/tests/Traits/MetableTest.php similarity index 59% rename from tests/traits/MetableTest.php rename to tests/Traits/MetableTest.php index 9cdbffc..3ecaccf 100644 --- a/tests/traits/MetableTest.php +++ b/tests/Traits/MetableTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Traits; use PHPUnit\Framework\TestCase; @@ -80,6 +80,43 @@ public function testRecursive(): void ] ); } + + public function testSetMetaAttribute(): void + { + $instance = new MetableClassTest(); + + $instance->setMetaAttribute('test', 123); + static::assertEquals($instance->metaAttribute('test'), 123); + + $instance->setMetaAttribute('params.id', 1); + $instance->setMetaAttribute('params.isBool', true); + $instance->setMetaAttribute('params.string', 'test'); + static::assertEquals(1, $instance->metaAttribute('params.id')); + static::assertEquals(true, $instance->metaAttribute('params.isBool')); + static::assertEquals('test', $instance->metaAttribute('params.string')); + + static::assertEquals( + [ + 'id' => 1, + 'isBool' => true, + 'string' => 'test', + ], + $instance->metaAttribute('params') + ); + + static::assertEquals( + [ + 'test' => 123, + 'params' => + [ + 'id' => 1, + 'isBool' => true, + 'string' => 'test', + ], + ], + $instance->meta() + ); + } } /** diff --git a/tests/traits/SingletonTest.php b/tests/Traits/SingletonTest.php similarity index 98% rename from tests/traits/SingletonTest.php rename to tests/Traits/SingletonTest.php index bb841e6..322f471 100644 --- a/tests/traits/SingletonTest.php +++ b/tests/Traits/SingletonTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Traits; use Php\Support\Exceptions\Exception; use PHPUnit\Framework\TestCase; diff --git a/tests/traits/TraitBooterTest.php b/tests/Traits/TraitBooterTest.php similarity index 53% rename from tests/traits/TraitBooterTest.php rename to tests/Traits/TraitBooterTest.php index 073a586..c56ade5 100644 --- a/tests/traits/TraitBooterTest.php +++ b/tests/Traits/TraitBooterTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Support\Tests; +namespace Php\Support\Tests\Traits; use Php\Support\Traits\TraitBooter; use Php\Support\Traits\TraitInitializer; @@ -12,43 +12,35 @@ final class TraitBooterTest extends TestCase { public function testBootTrait(): void { - self::assertEquals('class', BootClass::$type); - $class = new BootClass(); + $class = new class { + use TraitBooter; + use BootTrait; + + public static $type = 'class'; + + public function __construct() + { + $this->bootIfNotBooted(); + // $this->initializeTraits(); + } + }; self::assertEquals('trait', $class::$type); } public function testInitTrait(): void { - $class = new InitClass(); - self::assertEquals('load initialize from InitTrait', $class->title); - } -} - -class BootClass -{ - use TraitBooter; - use BootTrait; - - public static $type = 'class'; + $class = new class { + use TraitInitializer; + use InitTrait; - public function __construct() - { - $this->bootIfNotBooted(); - // $this->initializeTraits(); + public $title = ''; - } -} - -class InitClass -{ - use TraitInitializer; - use InitTrait; - - public $title = ''; - - public function __construct() - { - $this->bootIfNotBooted(); + public function __construct() + { + $this->bootIfNotBooted(); + } + }; + self::assertEquals('load initialize from InitTrait', $class->title); } } diff --git a/tests/Traits/TraitWhenerTest.php b/tests/Traits/TraitWhenerTest.php new file mode 100644 index 0000000..98d67fa --- /dev/null +++ b/tests/Traits/TraitWhenerTest.php @@ -0,0 +1,22 @@ +when(true, fn()=>1)); + } +} \ No newline at end of file diff --git a/tests/exceptions/MissingPropertyTest.php b/tests/exceptions/MissingPropertyTest.php deleted file mode 100644 index bd886b6..0000000 --- a/tests/exceptions/MissingPropertyTest.php +++ /dev/null @@ -1,53 +0,0 @@ -assertInstanceOf(MissingPropertyException::class, $e); - $this->assertSame('Invalid Arg', $e->getMessage()); - } - - try { - throw new MissingPropertyException(null, 'test'); - } catch (Throwable $e) { - $this->assertInstanceOf(MissingPropertyException::class, $e); - $this->assertSame('Missing property', $e->getName()); - $this->assertSame('Missing property: "test"', $e->getMessage()); - $this->assertSame('test', $e->getProperty()); - } - - try { - throw new MissingPropertyException(); - } catch (Throwable $e) { - $this->assertInstanceOf(MissingPropertyException::class, $e); - $this->assertSame('Missing property', $e->getName()); - $this->assertSame('Missing property', $e->getMessage()); - } - - - try { - throw new MissingPropertyException(null, 'test', ['key' => 'val']); - } catch (Throwable $e) { - $this->assertInstanceOf(MissingPropertyException::class, $e); - $this->assertSame('Missing property', $e->getName()); - $this->assertSame('Missing property: "test"', $e->getMessage()); - $this->assertSame('test', $e->getProperty()); - $this->assertSame(['key' => 'val'], $e->getConfig()); - } - } -} diff --git a/tests/exceptions/UnknownMethodTest.php b/tests/exceptions/UnknownMethodTest.php deleted file mode 100644 index ac0f89a..0000000 --- a/tests/exceptions/UnknownMethodTest.php +++ /dev/null @@ -1,42 +0,0 @@ -assertInstanceOf(UnknownMethodException::class, $e); - $this->assertSame('Invalid Arg', $e->getMessage()); - } - - try { - throw new UnknownMethodException(null, 'test'); - } catch (Throwable $e) { - $this->assertInstanceOf(UnknownMethodException::class, $e); - $this->assertSame('Unknown method', $e->getName()); - $this->assertSame('Unknown method: "test"', $e->getMessage()); - $this->assertSame('test', $e->getMethod()); - } - - try { - throw new UnknownMethodException(); - } catch (Throwable $e) { - $this->assertInstanceOf(UnknownMethodException::class, $e); - $this->assertSame('Unknown method', $e->getName()); - $this->assertSame('Unknown method', $e->getMessage()); - } - } -} diff --git a/tests/exceptions/UnknownPropertyTest.php b/tests/exceptions/UnknownPropertyTest.php deleted file mode 100644 index 74e23ca..0000000 --- a/tests/exceptions/UnknownPropertyTest.php +++ /dev/null @@ -1,42 +0,0 @@ -assertInstanceOf(UnknownPropertyException::class, $e); - $this->assertSame('Invalid Arg', $e->getMessage()); - } - - try { - throw new UnknownPropertyException(null, 'test'); - } catch (Throwable $e) { - $this->assertInstanceOf(UnknownPropertyException::class, $e); - $this->assertSame('Unknown property', $e->getName()); - $this->assertSame('Unknown property: "test"', $e->getMessage()); - $this->assertSame('test', $e->getProperty()); - } - - try { - throw new UnknownPropertyException(); - } catch (Throwable $e) { - $this->assertInstanceOf(UnknownPropertyException::class, $e); - $this->assertSame('Unknown property', $e->getName()); - $this->assertSame('Unknown property', $e->getMessage()); - } - } -} diff --git a/tests/traits/ArrayStorageConfigurableTest.php b/tests/traits/ArrayStorageConfigurableTest.php deleted file mode 100644 index 04bd88a..0000000 --- a/tests/traits/ArrayStorageConfigurableTest.php +++ /dev/null @@ -1,58 +0,0 @@ - 'Damn', - 'id' => 'test', - 'k' => 1, - 's.k.d' => 2, - 'remote' => true, - ] - ); - - static::assertEquals('Damn', $config->name); - static::assertEquals('test', $config->id); - static::assertEquals(true, $config->remote); - - static::assertEquals(1, $config->get('k')); - static::assertEquals(2, $config->get('s.k.d')); - static::assertIsArray($config->getData()); - static::assertNotEmpty($config->getData()); - - static::assertTrue(property_exists($config, 'name')); - // $this->assertTrue(property_exists($config, 'id')); - static::assertEquals('test', $config->get('id')); - static::assertTrue(isset($config->id)); - static::assertTrue(isset($config->k)); - } -} - - -/** - * Class ArrayStorageConfigurableClassTest - */ -class ArrayStorageConfigurableClassTest -{ - use \Php\Support\Traits\ArrayStorageConfigurableTrait; - use \Php\Support\Traits\Maker; - - protected $name; - - public function __construct(array $data = []) - { - $this->configurable($data); - } -} diff --git a/tests/traits/ArrayStorageTest.php b/tests/traits/ArrayStorageTest.php deleted file mode 100644 index 5b39cac..0000000 --- a/tests/traits/ArrayStorageTest.php +++ /dev/null @@ -1,207 +0,0 @@ -name = 'test'; - static::assertEquals('test', $config->name); - - $config->test = 'test in test'; - static::assertEquals('test in test', $config->test); - - $config->data = 1; - static::assertEquals(1, $config->data); - - $config->null = null; - static::assertNull($config->null); - } - - public function testDeepData(): void - { - $config = new ArrayStorageClassTest(); - $key = 'test.sub.key'; - - $config->$key = 1; - - static::assertEquals(1, $config->$key); - - $config->{'upper.sad.as'} = 'test'; - - static::assertEquals('test', $config->{'upper.sad.as'}); - } - - public function testGetData(): void - { - $config = new ArrayStorageClassTest(); - - $config->{'test.sub.key'} = 1; - $config->{'test.sub.val'} = 'value'; - - $config->{'test.next'} = 'next value'; - - $expected = [ - 'test' => [ - 'sub' => [ - 'key' => 1, - 'val' => 'value', - ], - 'next' => 'next value', - ], - ]; - - static::assertEquals($expected['test'], $config->test); - static::assertEquals($expected, $config->getData()); - } - - public function testUnset(): void - { - $config = new ArrayStorageClassTest(); - - $config->name = 'name'; - unset($config->name); - static::assertTrue(isset($config->name)); - static::assertNull($config->name); - - $key = 'test.sub.key'; - $config->$key = 1; - - static::assertEquals(1, $config->$key); - - unset($config->$key); - - static::assertFalse(isset($config->$key)); - - $this->expectNotice(); - static::assertNull($config->$key); - - unset($config->{'tst.sdf'}); - - static::assertFalse(isset($config->{'tst.sdf'})); - - $this->expectNotice(); - static::assertNull($config->$key); - } - - public function testAbsent(): void - { - $config = new ArrayStorageClassTest(); - - $this->expectNotice(); - static::assertNull($config->test); - } - - public function testIsset(): void - { - $config = new ArrayStorageClassTest(); - - static::assertFalse(isset($config->test)); - - $config->test2 = 'test2'; - static::assertTrue(isset($config->test2)); - - $config->null = null; - static::assertTrue(isset($config->null)); - - $config->name = 'name'; - static::assertTrue(isset($config->name)); - - static::assertFalse(isset($config->{'nullable.one.1'})); - } - - public function testValueExist(): void - { - $config = new ArrayStorageClassTest(); - - $config->test = 'test2'; - - static::assertTrue($config->valueExists('name')); - static::assertTrue($config->valueExists('test')); - static::assertFalse($config->valueExists('data')); - static::assertFalse($config->valueExists('test2')); - unset($config->test); - - static::assertFalse($config->valueExists('test')); - } - - - public function testOffsetExists(): void - { - $config = new ArrayStorageClassTest(); - $config->test2 = 'test2'; - - static::assertTrue($config->offsetExists('test2')); - static::assertTrue(isset($config['test2'])); - - $config->null = null; - static::assertTrue($config->offsetExists('null')); - static::assertFalse($config->offsetExists('null2')); - } - - public function testOffsetGet(): void - { - $config = new ArrayStorageClassTest(); - $config->test2 = 'test2'; - - static::assertEquals('test2', $config->offsetGet('test2')); - static::assertEquals('test2', $config['test2']); - - $config->null = null; - static::assertNull($config['null']); - - $this->expectNotice(); - static::assertNull($config['null2']); - } - - public function testOffsetSet(): void - { - $config = new ArrayStorageClassTest(); - $config['test2'] = 'val2'; - - static::assertEquals('val2', $config->test2); - static::assertEquals('val2', $config->offsetGet('test2')); - static::assertEquals('val2', $config['test2']); - - $config['null'] = null; - static::assertNull($config['null']); - - $this->expectNotice(); - static::assertNull($config['null2']); - } - - public function testOffsetUnset(): void - { - $config = new ArrayStorageClassTest(); - $config['test2'] = 'val2'; - - static::assertEquals('val2', $config->test2); - static::assertEquals('val2', $config->offsetGet('test2')); - static::assertEquals('val2', $config['test2']); - - unset($config['test2']); - - $this->expectNotice(); - static::assertNull($config['test2']); - } -} - -/** - * Class ArrayStorageClassTest - */ -class ArrayStorageClassTest implements \ArrayAccess -{ - use \Php\Support\Traits\ArrayStorage; - - protected $name; -}