diff --git a/.github/workflows/lint/rules/changelog.js b/.github/workflows/lint/rules/changelog.js new file mode 100644 index 0000000..d15c2c9 --- /dev/null +++ b/.github/workflows/lint/rules/changelog.js @@ -0,0 +1,72 @@ +"use strict"; + +module.exports = [{ + names: ["CHANGELOG-RULE-001"], + description: "Version header format", + tags: ["headings", "headers", "changelog"], + function: (params, onError) => { + params.tokens.filter(function filterToken(token) { + return token.type === "heading_open"; + }).forEach(function forToken(token) { + if (token.tag === "h2") { + if (/^## [vV]?[\[]?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+|)[\]]?$/m.test(token.line)) { + return; + } + + if (/^## [vV]?[\[]?\d+\.\d+\.\d+(-[0-9A-Za-z-.]+|)[\]]? - 20[12][0-9]-[01][0-9]-[0-3][0-9]$/m.test(token.line)) { + return; + } + + if (/^## [\[]?unreleased[\]]?$/mi.test(token.line)) { + return; + } + + return onError({ + lineNumber: token.lineNumber, + detail: "Allowed formats:\n-'[vX.X.X(-pre.release)]'\n-'vX.X.X(-pre.release)'\n-'vX.X.X(-pre.release) - YYYY-MM-DD'\n-'UNRELEASED'\n-'[unreleased]'", + context: token.line + }); + } + }); + } +}, { + names: ["CHANGELOG-RULE-002"], + description: "Type of changes format", + tags: ["headings", "headers", "changelog"], + function: (params, onError) => { + params.tokens.filter(function filterToken(token) { + return token.type === "heading_open"; + }).forEach(function forToken(token) { + if (token.tag === "h3") { + if (/^### (Added|Changed|Deprecated|Removed|Fixed|Security)$/m.test(token.line)) { + return; + } + + return onError({ + lineNumber: token.lineNumber, + detail: "Allowed types is: Added, Changed, Deprecated, Removed, Fixed or Security", + context: token.line + }); + } + }); + } +}, { + names: ["CHANGELOG-RULE-003"], + description: "The list items must be without punctuation marks at the end", + tags: ["lists", "changelog"], + function: (params, onError) => { + params.tokens.filter(function filterToken(token) { + return token.type === "list_item_open"; + }).forEach(function forToken(token) { + if (token.tag === "li") { + if (/[;,\.]$/m.test(token.line)) { + return onError({ + lineNumber: token.lineNumber, + detail: "'.', ';' or ',' at the end of list entry", + context: token.line + }); + } + } + }); + } +}]; diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..4104904 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,90 @@ +name: PHP Package + +on: [ push ] + +jobs: + # 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: + - name: Check out code + uses: actions/checkout@v4 + - 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' + + 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: [ '8.4' ] + + steps: + - uses: actions/checkout@v4 + + - name: Use PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 # Action page: + with: + php-version: ${{ matrix.php }} + extensions: mbstring + coverage: xdebug + + - name: Get Composer Cache Directory # Docs: + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - name: Validate composer.json + run: composer validate + + - name: Install [LOWEST] Composer dependencies + if: matrix.setup == 'lowest' + run: composer update --prefer-dist --no-interaction --no-suggest --prefer-lowest + + - name: Install [BASIC] Composer dependencies + if: matrix.setup == 'basic' + run: composer update --prefer-dist --no-interaction --no-suggest + + - name: Install [STABLE] Composer dependencies + if: matrix.setup == 'stable' + run: composer update --prefer-dist --no-interaction --no-suggest --prefer-stable + + - name: Composer DUMP + run: composer dump-autoload -o + + - name: Show most important packages' versions + run: composer info | grep -e phpunit/phpunit + + # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" + # Docs: https://getcomposer.org/doc/articles/scripts.md + + - name: Run test suite + run: XDEBUG_MODE=coverage composer test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..14a52cd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: 'Create Release' + +on: + push: + tags: + - 'v[1-9].[0-9]+.0' + +jobs: + build: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Create Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: Release ${{ github.ref }} + draft: false + prerelease: false diff --git a/.gitignore b/.gitignore index 04369d6..13299c0 100755 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,12 @@ composer.lock /.idea /storage /vendor +clover.xml + +/.git-hooks + +summary.log +per-mutator.md +infection.log + +/.phpunit.cache diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index e7a7fc7..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,63 +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 PHP5.6 -test:PHP-5.6: - <<: *job-template - image: php:5.6 - -# We test PHP7.0 -test:PHP-7.0: - <<: *job-template - image: php:7.0 - -# 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/.phpcs.xml b/.phpcs.xml new file mode 100644 index 0000000..861f09e --- /dev/null +++ b/.phpcs.xml @@ -0,0 +1,117 @@ + + + The PSR2 coding standard. + + src/ + + vendor + resources + database + coverage + node_modules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + error + + + + + error + + + + + tests/bootstrap.php + + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7603bc5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: php -php: -- '7.1' -- '7.2' -- '7.3' - -before_install: -- 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 - -before_script: -- composer install - -script: -- ./vendor/bin/phpunit --coverage-text --coverage-clover=clover.xml - -after_script: -- ./cc-test-reporter after-build --coverage-input-type clover --exit-code $TRAVIS_TEST_RESULT - -after_success: -- bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9ec0ccb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,228 @@ +# Changelog + +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]. + +## v5.1.0 + +### Added + +- Add `HashCollection` Collection. + +## v5.0.0 + +### Added + +- Add `PHP 8.4` support +- Add `UseStorage` trait. +- Add `UseConfigurabeStorage` trait. + +### Removed + +- Remove Trait `ArrayStorage`. Use `UseStorage` instead +- Remove Trait `ArrayStorageConfigurableTrait`. Use `UseConfigurabeStorage` instead + +## v4.28.0 + +### Added + +- Add global functions: `attributeToGetterMethod`, `attributeToSetterMethod`, `findGetterMethod`, `public_property_exists`, `get_property_value` + +## v4.27.0 + +### Added + +- Add func for Enum's traits: `WithEnhances::toKeyValueArray`, `WithEnhances::toValueKeyArray` +- Add `Arr::map` + +## v4.26.0 + +### Added + +- Add support `PHP 8.3` + +## v4.25.0 + +### Added + +- `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 + +### Changed + +- `ArrayCollection::map` - works with keys now +- `ArrayCollection::createFrom` - receives Collections + +## v4.24.0 + +### Added + +- Add an argument `separator` to methods `Arr::set`, `Arr::get`, `Arr::has` + +## v4.23.0 + +### Added + +- Add methods `trimPrefix`, `trimSuffix` into `Str` + +## v4.22.0 + +### Added + +- Add method `mapInto` into `ArrayCollection` + +## v4.21.0 + +### Added + +- Add method `whereInstanceOf` into `ArrayCollection` + +## v4.20.0 + +### Added + +- Add method `slugifyWithFormat` into `Str` + +## v4.19.0 + +### Added + +- 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 + +## v4.18.0 + +### Added + +- Add function `Collection::reject` + +## v4.17.1 + +### Changed + +- `Collection::filter(Closure $func = null)` - The argument `$func` may be `null` + +## v4.17.0 + +### Added + +- Add support `PHP 8.2` + +### Removed + +- Remove support `PHP 8.0` + +## v4.16.0 + +### Added + +- Add global method `dataGet` +- Add helper method `Arr::collapse` +- Add helper method `Arr::prepend` +- Add Structures: `ArrayCollection` and its interfaces + +## v4.15.0 + +### Added + +- 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 + +## v4.14.0 + +### Added + +- Add method `Number::isInteger` Allows you to determine whether the $value is an integer or not + +## v4.13.0 + +### Added + +- Add support `PHP 8.1` + +## v4.9.0 + +### Added + +- 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 + +## v4.8.0 + +### Added + +- Add methods `toPostgresPoint`, `fromPostgresPoint` to `Arr` helper + +## v4.7.0 + +### Added + +- Add exception `MissingMethodException` +- Add global function `remoteStaticCallOrTrow` + +## v4.6.0 + +### Added + +- Add class `ConditionalHandler` + +## v4.5.0 + +### Added + +- Add trait `HasPrePostActions` + +## v4.4.2 + +### Changed + +- Add param `removeNull` to method: `Metable::setMetaAttribute` + +## v4.4.0 + +### Added + +- Add global function: `does_trait_use` + +## v4.3.1 + +### Added + +- Add global function: `remoteCall` +- Add global function: `remoteStaticCall` + +## v4.2.0 + +### Added + +- Add method to trait `Metable`: `setMetaAttribute` + +## v4.1.0 + +### Added + +- Add new Helper Class: `Number` +- Add method, working with integers: `Number::safeInt` + +## v4.0.0 + +### Changed + +- The package has PHP's minimal version is 8.0 now + +[keepachangelog]:https://keepachangelog.com/en/1.0.0/ + +[semver]:https://semver.org/spec/v2.0.0.html diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..acdb057 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 feugene + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Traits/HasMutator.php b/candidates/Traits/HasMutator.php similarity index 80% rename from src/Traits/HasMutator.php rename to candidates/Traits/HasMutator.php index 61d8995..0ac105b 100644 --- a/src/Traits/HasMutator.php +++ b/candidates/Traits/HasMutator.php @@ -1,8 +1,8 @@ =7.2", - "ext-json": "*" + "php": "^8.4", + "ext-mbstring": "*" }, "require-dev": { - "phpunit/phpunit": "~8.0", - "phpstan/phpstan": "~0.11", - "avto-dev/php-cs-fixer": "1.0.*" + "ergebnis/composer-normalize": "^2.45", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5", + "squizlabs/php_codesniffer": "^3.11", + "symfony/var-dumper": "^7.2" }, "autoload": { + "psr-4": { + "Php\\Support\\": "src/" + }, "files": [ - "src/global.php" - ], + "src/Global/base.php" + ] + }, + "autoload-dev": { "psr-4": { - "Php\\Support\\": "src/", "Php\\Support\\Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + } + }, "scripts": { - "test": "@php ./vendor/bin/phpunit --no-coverage --testdox", - "test-cover": "@php ./vendor/bin/phpunit --coverage-text --testdox", - "phpstan": "@php ./vendor/bin/phpstan analyze --ansi --level=max ./src" + "cs-fix": "@php ./vendor/bin/phpcbf", + "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", + "phpunit-test": "@php ./vendor/bin/phpunit --no-coverage --testdox --colors=always", + "test": [ + "@phpstan", + "@phpunit" + ], + "test-cover": [ + "@phpstan", + "@phpunit-cover" + ] } } diff --git a/infection.json b/infection.json new file mode 100644 index 0000000..cc86909 --- /dev/null +++ b/infection.json @@ -0,0 +1,28 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "timeout": 10, + "logs": { + "text": "infection.log", + "summary": "summary.log", + "perMutator": "per-mutator.md", + "badge": { + "branch": "master" + } + }, + "tmpDir": "/tmp", + "mutators": { + "@default": true, + "@function_signature": false, + "TrueValue": { + "ignore": [ + "NameSpace\\*\\Class::method" + ] + } + }, + "testFramework": "phpunit", + "testFrameworkOptions": "-vvv" +} 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/phpunit.xml b/phpunit.xml index f857f75..6b0174f 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,26 +1,27 @@ - - - - ./tests - - - - - ./src - - - - - - - + + + + + + + + + + + + ./tests + + + + + + + + ./src + + diff --git a/readme.md b/readme.md index a60f44a..3700315 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,174 @@ # PHP Support -![](https://img.shields.io/badge/php->=7.1-blue.svg) -[![Latest Stable Version](https://poser.pugx.org/efureev/support/v/stable?format=flat)](https://packagist.org/packages/efureev/support) +![](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) +[![Latest Stable Version](https://poser.pugx.org/efureev/support/v/stable?format=flat)](https://packagist.org/packages/efureev/support) +[![Total Downloads](https://poser.pugx.org/efureev/support/downloads)](https://packagist.org/packages/efureev/support) [![Maintainability](https://api.codeclimate.com/v1/badges/a7cf8708bf58fa7e5096/maintainability)](https://codeclimate.com/github/efureev/php-support/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/a7cf8708bf58fa7e5096/test_coverage)](https://codeclimate.com/github/efureev/php-support/test_coverage) -[![codecov](https://codecov.io/gh/efureev/php-support/branch/master/graph/badge.svg)](https://codecov.io/gh/efureev/php-support) +[![codecov](https://codecov.io/gh/efureev/php-support/branch/v2/graph/badge.svg)](https://codecov.io/gh/efureev/php-support/tree/v2) + +## 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" +``` + +## Content + +- Helpers + + 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 + + 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 + + MissingMethodException (^4.7.0) + + NotSupportedException + + UnknownMethodException + + UnknownPropertyException + +- Interfaces + + Arrayable + + Command + + Jsonable + + Prototype + +- Structures + - Collections (^4.16.0) + - ArrayCollection + - HashCollection (^5.1.0) + +- Traits + + UseStorage + + UseConfigurableStorage + + ConfigurableTrait + + ConsolePrint + + Maker + + Metable + + ReadOnlyProperties + + Singleton + + Thrower + + TraitBooter + + TraitInitializer + + Whener + +- Types + + GeoPoint + + Point ## Test + ```bash composer test composer test-cover # with coverage -``` \ No newline at end of file +``` diff --git a/src/Components/BaseObject.php b/src/Components/BaseObject.php deleted file mode 100644 index a5a9ca1..0000000 --- a/src/Components/BaseObject.php +++ /dev/null @@ -1,86 +0,0 @@ -canGetProperty($name, $checkVars) || $this->canSetProperty($name, false); - } - - - /** - * @param string $name - * @param bool $checkVars - * - * @return bool - */ - public function canGetProperty(string $name, bool $checkVars = true): bool - { - return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name); - } - - /** - * @param string $name - * @param bool $checkVars - * - * @return bool - */ - public function canSetProperty(string $name, bool $checkVars = true): bool - { - return method_exists($this, 'set' . $name) || $checkVars && property_exists($this, $name); - } - - /** - * @param string $name - * - * @return bool - */ - public function hasMethod(string $name): bool - { - return method_exists($this, $name); - } -} diff --git a/src/Components/BaseParams.php b/src/Components/BaseParams.php deleted file mode 100644 index c26502e..0000000 --- a/src/Components/BaseParams.php +++ /dev/null @@ -1,273 +0,0 @@ -init(); - $this->fromArray($array ?? []); - } - - /** - * Initialization component - */ - public function init() - { - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return array_map(function ($value) { - if ($value instanceof \JsonSerializable) { - return $value->jsonSerialize(); - } elseif ($value instanceof Jsonable) { - return Json::decode($value->toJson(), true); - } elseif ($value instanceof Arrayable) { - return $value->toArray(); - } - - return $value; - }, $this->_items); - } - - /** - * Get items as JSON - * - * @param int $options - * - * @return string|null - */ - public function toJson($options = 320): ?string - { - return Json::encode($this->jsonSerialize(), $options); - } - - /** - * @param string $string - * - * @return \Php\Support\Interfaces\Jsonable|\Php\Support\Components\Params - */ - public static function fromJson(string $string): ?Jsonable - { - return new static(Json::decode($string)); - } - - /** - * Convert items to its string representation. - * - * @return string - */ - public function __toString() - { - return $this->toJson() ?? '{}'; - } - - - /** - * @param array $keys - * - * @return array - */ - public function toArray(array $keys = []): array - { - if (!$keys) { - return Json::dataToArray($this->_items); - } - - $result = []; - - foreach ($keys as $key) { - if (isset($this->_items[ $key ])) { - $result[ $key ] = $this->_items[ $key ]; - } - } - - return Json::dataToArray($result); - } - - /** - * @param array $array - * - * @return $this - */ - public function fromArray(array $array) - { - $this->_items = $array; - - return $this; - } - - /** - * Clear elements - * - * @return $this - */ - public function clear() - { - $this->_items = []; - - return $this; - } - - /** - * @return int - */ - public function count(): int - { - return count($this->_items); - } - - /** - * Checks if the given key or index exists in the array - * - * @param mixed $offset - * - * @return bool - */ - public function offsetExists($offset): bool - { - return array_key_exists($offset, $this->_items); - } - - /** - * Offset to retrieve - * - * @param mixed $offset - * - * @return mixed - */ - public function offsetGet($offset) - { - return $this->_items[ $offset ]; - } - - /** - * Offset to set - * - * @param mixed $offset The offset to assign the value to. - * @param mixed $value The value to set. - */ - public function offsetSet($offset, $value) - { - if (is_null($offset)) { - $this->_items[] = $value; - } else { - $this->_items[ $offset ] = $value; - } - } - - /** - * Offset to unset - * - * @param mixed $offset The offset to unset. - */ - public function offsetUnset($offset) - { - unset($this->_items[ $offset ]); - } - - /** - * @return \ArrayIterator|\Traversable - */ - public function getIterator() - { - return new \ArrayIterator($this->_items); - } - - /** - * @param string $name - * - * @return mixed - * @throws \Php\Support\Exceptions\UnknownPropertyException - */ - public function __get(string $name) - { - if ($this->offsetExists($name)) { - return $this->offsetGet($name); - } - - return self::__getParent($name); - } - - /** - * @param string $name - * - * @return bool - */ - public function __isset(string $name) - { - if ($res = $this->offsetExists($name)) { - return true; - } - - $getter = static::getter($name); - if (method_exists($this, $getter)) { - return $this->$getter() !== null; - } - - return false; - } - - /** - * @param string $name - * @param mixed $value - */ - public function __set(string $name, $value) - { - $setter = 'set' . ucfirst($name); - if (method_exists($this, $setter)) { - $this->$setter($value); - } - - if ($this->offsetExists($name)) { - $this->offsetSet($name, $value); - } - } - - /** - * @param string $name - */ - public function __unset(string $name) - { - if ($this->offsetExists($name)) { - $this->offsetUnset($name); - - return; - } - - $setter = 'set' . ucfirst($name); - if (method_exists($this, $setter)) { - $this->$setter(null); - } - } - -} diff --git a/src/Components/Params.php b/src/Components/Params.php deleted file mode 100644 index 3bc0ede..0000000 --- a/src/Components/Params.php +++ /dev/null @@ -1,91 +0,0 @@ -fromArray(Json::decode($string)); - } - - - /** - * Alias of a offsetGet - * - * @param mixed $key - * - * @return mixed - */ - public function get($key) - { - return $this->offsetGet($key); - } - - - /** - * Alias of a offsetSet - * - * @param mixed $key - * @param mixed $value - */ - public function set($key, $value) - { - $this->offsetSet($key, $value); - } - - /** - * @var string|null - */ - protected $uniqueKey; - - /** - * Set unique key name for elements collection - * - * @param string $key - * - * @return $this - */ - public function setUniqueKeyName(string $key) - { - $this->uniqueKey = $key; - - return $this; - } - - /** - * Add new element with key. If key is absent - create dynamic key: by unique index (uniqueKey) or by hash of group - * fields (dynamicHashKeys) - * - * @param mixed $value - * @param null|string $key - * - * @return null|string - */ - public function add($value, ?string $key = null) - { - if (!$key) { - $key = $this->uniqueKey ? $value[ $this->uniqueKey ] ?? null : $this->dynamicHash($value); - } - - $this->offsetSet($key, $value); - - if (!$key) { - $key = count($this->_items) - 1; - } - - return $key; - } - -} \ No newline at end of file diff --git a/src/Components/ParamsJson.php b/src/Components/ParamsJson.php deleted file mode 100644 index e5d02ba..0000000 --- a/src/Components/ParamsJson.php +++ /dev/null @@ -1,105 +0,0 @@ -normalizeElements(); - } - - /** - * @param array $array - * - * @return \Php\Support\Components\ParamsJson - */ - public function fromArray(array $array) - { - $this->_itemsRaw = $array; - parent::fromArray($array); - - return $this->setElementsType($this->_type); - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->_type && $this->_items - ? [ - '_type' => $this->_type, - '_data' => parent::jsonSerialize() - ] - : parent::jsonSerialize(); - } - - /** - * @return $this - */ - public function normalizeElements() - { - if ($this->_type) { - $this->_items = Arr::applyCls($this->_itemsRaw, $this->_type); - } - - return $this; - } - - - /** - * Set class|type of elements - * - * @param string|null $type - * - * @return $this - */ - public function setElementsType(?string $type) - { - $this->_type = $type; - - return $this->normalizeElements(); - } - - /** - * @return null|string - */ - public function getElementsType(): ?string - { - return $this->_type; - } - - /** - * @param string $string - * - * @return \Php\Support\Interfaces\Jsonable|\Php\Support\Components\ParamsJson - */ - public static function fromJson(string $string): ?Jsonable - { - $array = Json::decode($string); - - $instance = new static(); - - if (isset($array['_type'])) { - $instance - ->fromArray($array['_data'] ?? []) - ->setElementsType($array['_type']); - } else { - $instance->fromArray($array ?? []); - } - - return $instance; - } -} 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/Entities/JwtToken.php b/src/Entities/JwtToken.php deleted file mode 100644 index d33f565..0000000 --- a/src/Entities/JwtToken.php +++ /dev/null @@ -1,209 +0,0 @@ - 'none'], - array $claims = [], - ?string $signature = null, - array $payload = ['', ''] - ) { - $this->headers = $headers; - $this->claims = $claims; - $this->signature = $signature; - $this->payload = $payload; - } - - /** - * Returns the token headers - * - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } - - /** - * Returns if the header is configured - * - * @param string $name - * - * @return boolean - */ - public function hasHeader(string $name): bool - { - return array_key_exists($name, $this->headers); - } - - /** - * Returns the value of a token header - * - * @param string $name - * @param mixed $default - * - * @return mixed - * @throws \OutOfBoundsException - */ - public function getHeader(string $name, $default = null) - { - if ($this->hasHeader($name)) { - return $this->getHeaderValue($name); - } - - if ($default === null) { - throw new \OutOfBoundsException('Requested header is not configured'); - } - - return $default; - } - - /** - * Returns the value stored in header - * - * @param string $name - * - * @return string - */ - private function getHeaderValue(string $name): string - { - return $this->headers[ $name ]; - } - - /** - * Returns the token claim set - * - * @return array - */ - public function getClaims(): array - { - return $this->claims; - } - - /** - * Returns if the claim is configured - * - * @param string $name - * - * @return boolean - */ - public function hasClaim(string $name): bool - { - return array_key_exists($name, $this->claims); - } - - /** - * Returns the value of a token claim - * - * @param string $name - * @param mixed $default - * - * @return mixed - * @throws \OutOfBoundsException - */ - public function getClaim(string $name, $default = null) - { - if ($this->hasClaim($name)) { - return $this->claims[ $name ]; - } - - if ($default === null) { - throw new \OutOfBoundsException('Requested claim is not configured'); - } - - return $default; - } - - /** - * Determine if the token is expired. - * - * @param \DateTimeInterface|null $now - * - * @return bool - * @throws \Exception - */ - public function isExpired(\DateTimeInterface $now = null) - { - $exp = $this->getClaim('exp', false); - - if ($exp === false) { - return false; - } - - $now = $now ?: new \DateTime(); - - $expiresAt = new \DateTime(); - $expiresAt->setTimestamp($exp); - - return $now > $expiresAt; - } - - /** - * Returns the token payload - * - * @return string - */ - public function getPayload(): string - { - return $this->payload[0] . '.' . $this->payload[1]; - } - - /** - * Returns an encoded representation of the token - * - * @return string - */ - public function __toString() - { - $data = implode('.', $this->payload); - - if ($this->signature === null) { - $data .= '.'; - } - - return $data; - } -} 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 new file mode 100644 index 0000000..42e7f90 --- /dev/null +++ b/src/ErrorCollection.php @@ -0,0 +1,9 @@ + $config */ - protected $config; - - /** - * ConfigException constructor. - * - * @param mixed|null $config - * @param string $message - */ - 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 e829310..d387860 100644 --- a/src/Exceptions/Exception.php +++ b/src/Exceptions/Exception.php @@ -1,27 +1,38 @@ getName(), $code, $previous); } + + /** + * @return string + */ + public function getName(): string + { + return 'Exception'; + } } diff --git a/src/Exceptions/InvalidArgumentException.php b/src/Exceptions/InvalidArgumentException.php index 57f64ff..8493632 100644 --- a/src/Exceptions/InvalidArgumentException.php +++ b/src/Exceptions/InvalidArgumentException.php @@ -1,32 +1,26 @@ getName(), $code, $previous); } /** - * Exception constructor. - * - * @param null|string $message - * @param int $code - * @param \Throwable|null $previous + * @return string */ - public function __construct(?string $message = null, $code = 0, \Throwable $previous = null) + public function getName(): string { - parent::__construct($message ?? $this->getName(), $code, $previous); + return 'Invalid Argument'; } } diff --git a/src/Exceptions/InvalidCallException.php b/src/Exceptions/InvalidCallException.php index 3f8bbb4..ef9c186 100644 --- a/src/Exceptions/InvalidCallException.php +++ b/src/Exceptions/InvalidCallException.php @@ -1,10 +1,17 @@ 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 null|string - */ - public function getParam(): ?string + public function getName(): string { - return $this->param; + return 'Invalid Parameter'; } } diff --git a/src/Exceptions/InvalidValueException.php b/src/Exceptions/InvalidValueException.php index d834e29..9a9c270 100644 --- a/src/Exceptions/InvalidValueException.php +++ b/src/Exceptions/InvalidValueException.php @@ -1,10 +1,17 @@ reason = $reason; + + parent::__construct($message ? "$message: $reason" : $reason); } } diff --git a/src/Exceptions/MissingClassException.php b/src/Exceptions/MissingClassException.php index cc13adc..c57c58a 100644 --- a/src/Exceptions/MissingClassException.php +++ b/src/Exceptions/MissingClassException.php @@ -1,23 +1,16 @@ 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 2dce73f..8ebb21b 100644 --- a/src/Exceptions/MissingPropertyException.php +++ b/src/Exceptions/MissingPropertyException.php @@ -1,43 +1,24 @@ property = $property; - parent::__construct($message ?? ($this->getName() . ($this->property ? ': "' . $this->property . '"' : '')), $config); + parent::__construct($message ?? ($this->getName() . ": $this->property"), $config); } /** * @return string */ - public function getName() + 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 10d2a2c..0fc4c26 100644 --- a/src/Exceptions/NotSupportedException.php +++ b/src/Exceptions/NotSupportedException.php @@ -1,23 +1,18 @@ method = $method; - parent::__construct($message ?? ($this->getName() . ($this->method ? ': "' . $this->method . '"' : ''))); + parent::__construct($message ?? ($this->getName() . ": $this->method")); } /** * @return string */ - public function getName() + 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 9492c48..9a882ba 100644 --- a/src/Exceptions/UnknownPropertyException.php +++ b/src/Exceptions/UnknownPropertyException.php @@ -1,42 +1,21 @@ property = $property; - parent::__construct($message ?? ($this->getName() . ($this->property ? ': "' . $this->property . '"' : ''))); + parent::__construct($message ?? ($this->getName() . ": $this->property")); } - /** - * @return string - */ - public function getName() + 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 new file mode 100644 index 0000000..f7ebf42 --- /dev/null +++ b/src/Global/base.php @@ -0,0 +1,366 @@ + $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; + } + + return value($default); + } +} + +if (!function_exists('classNamespace')) { + function classNamespace(object|string $class): string + { + if (is_object($class)) { + $class = get_class($class); + } + + return implode('\\', array_slice(explode("\\", $class), 0, -1)); + } +} + +if (!function_exists('isTrue')) { + /** + * Returns bool value of a value + */ + function isTrue(mixed $val, bool $return_null = false): ?bool + { + if ($val === null && $return_null) { + return null; + } + + $boolVal = (is_string($val) + ? filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) + : (bool)$val); + return ($boolVal === null && !$return_null ? false : $boolVal); + } +} + +if (!function_exists('instance')) { + /** + * @phpstan-param T|class-string|null $instance + * @param mixed ...$params + * @phpstan-return T|null + * + * @template T as object + */ + function instance(string|object|null $instance, mixed ...$params): ?object + { + if (is_object($instance)) { + return $instance; + } + + if (is_string($instance) && class_exists($instance)) { + return new $instance(...$params); + } + + return null; + } +} + +if (!function_exists('class_basename')) { + /** + * Get the class "basename" of the given object / class. + */ + function class_basename(string|object $class): string + { + $class = is_object($class) ? get_class($class) : $class; + + return basename(str_replace('\\', '/', $class)); + } +} + +if (!function_exists('trait_uses_recursive')) { + /** + * Returns all traits used by a trait and its traits. + * + * @return string[] + */ + function trait_uses_recursive(string $trait): array + { + if (!$traits = class_uses($trait)) { + return []; + } + + foreach ($traits as $trt) { + $traits += trait_uses_recursive($trt); + } + + return $traits; + } +} + +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. + * + * @return string[] + */ + function class_uses_recursive(object|string $class): array + { + if (is_object($class)) { + $class = $class::class; + } + + $results = []; + + foreach (array_reverse((array)class_parents($class)) + [$class => $class] as $cls) { + $results += trait_uses_recursive((string)$cls); + } + + 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 cb3a532..0624083 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -1,148 +1,374 @@ $array + * @return array */ - public static function arrayReplaceByTemplate(array &$array, array $replace) + public static function collapse(iterable $array): array { - foreach ($array as &$item) { - static::itemReplaceByTemplate($item, $replace); + $results = []; + + foreach ($array as $values) { + if ($values instanceof ReadableCollection) { + $values = $values->all(); + } elseif (!is_array($values)) { + continue; + } + + $results[] = $values; } + + return array_merge([], ...$results); } /** - * Replace templates into item + * Remove one element from array by value * - * @param mixed $item - * @param array $replace + * @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 it don't exist */ - private static function itemReplaceByTemplate(&$item, array $replace) + public static function removeByValue(array &$array, mixed $val, bool $reindex = false): string|int|null { - if (is_array($item)) { - self::arrayReplaceByTemplate($item, $replace); - } else if (is_string($item)) { - $item = Str::stringReplaceByTemplate($item, $replace); + if (($key = array_search($val, $array, false)) !== false) { + unset($array[$key]); } + if ($reindex) { + $array = array_values($array); + } + return $key ?: null; } /** - * Remove elements from array by value + * Simple variable to array * - * @param array $array - * @param mixed $val + * @param mixed $items + * + * @return T[]|array */ - public static function removeByValue(array &$array, $val) + public static function toArray(mixed $items): array { - if (($key = array_search($val, $array)) !== false) { - unset($array[$key]); + if (is_array($items)) { + return $items; + } + + 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; } /** + * Nested variable data to array + * * @param mixed $items * - * @return array + * @return array|mixed|null */ - public static function toArray($items): array + public static function dataToArray(mixed $items): mixed { - if (is_array($items)) { - $res = $items; - } elseif ($items instanceof Arrayable) { - $res = $items->toArray(); - } elseif ($items instanceof Jsonable) { - $res = Json::decode($items->toJson()); - } elseif ($items instanceof \JsonSerializable) { - $res = $items->jsonSerialize(); - } elseif ($items instanceof \Traversable) { - $res = iterator_to_array($items); - } else { - $res = (array)$items; + if (is_object($items)) { + if ($items instanceof JsonSerializable) { + return static::dataToArray($items->jsonSerialize()); + } + + if ($items instanceof Jsonable) { + return Json::decode($items->toJson()); + } + + if ($items instanceof Arrayable) { + $items = $items->toArray(); + } elseif ($items instanceof Traversable) { + $items = iterator_to_array($items); + } else { + $result = []; + if (is_iterable($items)) { + foreach ($items as $name => $value) { + $result[$name] = $value; + } + } + $items = $result; + } + } + + if (!is_array($items)) { + return $items; + } + + foreach ($items as $key => &$value) { + if (is_array($value) || is_object($value)) { + $value = static::dataToArray($value); + } + } + + return $items; + } + + /** + * @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.) + */ + public static function merge(array $res, array $b, bool $replaceArray = true): array + { + foreach ($b as $key => $val) { + if (is_int($key)) { + if (isset($res[$key])) { + $res[] = $val; + } else { + $res[$key] = $val; + } + } else { + if (is_array($val) && isset($res[$key]) && is_array($res[$key])) { + $res[$key] = ($replaceArray ? $val : self::merge($res[$key], $val, $replaceArray)); + } else { + $res[$key] = $val; + } + } } return $res; } /** - * Apply class or type to every element into collection + * Changes PHP array to default Postgres array format * - * @param array $array - * @param string $cls - * @param \Closure|null $fn + * @param array $array * - * @return array + * @return string */ - public static function applyCls(array $array, string $cls, \Closure $fn = null) + public static function toPostgresArray(array $array): string { - $fn = self::getNoopClosureForApplyCls($fn); - - return array_map(function ($element) use ($cls, $fn) { - switch ($cls) { - case 'array': - $result = (array)$element; - break; - case 'string': - $result = (string)$element; - break; - case 'integer': - $result = (int)$element; - break; - - default: - $result = $fn($cls, $element); - } + if (!$json = Json::encode(self::toIndexedArray($array), JSON_UNESCAPED_UNICODE)) { + return '{}'; + } + + return str_replace(['[', ']', '"'], ['{', '}', ''], $json); + } + + /** + * @param int[] $array + * @return ?string + */ + public static function toPostgresPoint(array $array): ?string + { + if (count($array) !== 2) { + return null; + } - return $result; + [ + $x, + $y, + ] = $array; - }, $array); + return '(' . $x . ',' . $y . ')'; } /** - * @param \Closure|null $fn + * Remove named keys from arrays * - * @return \Closure + * @param array $array + * + * @return array */ - public static function getNoopClosureForApplyCls(\Closure $fn = null) + public static function toIndexedArray(array $array): array { - if ($fn === null) { - $fn = function ($cls, $data) { - if (class_exists($cls)) { - return new $cls($data); + $array = array_values($array); + foreach ($array as &$value) { + if (is_array($value)) { + $value = static::toIndexedArray($value); + } + } + + return $array; + } + + /** + * Load from PG array to PHP array + * + * @param string|null $s + * @param int $start + * @param ?int $end + * @param array{string,string} $braces + * + * @return float[] + */ + 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 []; + } + + $return = []; + $string = false; + $quote = ''; + $len = strlen($s); + $v = ''; + + for ($i = $start + 1; $i < $len; $i++) { + $ch = $s[$i]; + if (!$string && $ch === $braceClose) { + if ($v !== '' || !empty($return)) { + $return[] = $v; + } + $end = $i; + break; + } else { + if (!$string && $ch === $braceOpen) { + $v = self::fromPostgresArray($s, (int)$i, $i); + } else { + if (!$string && $ch === ',') { + $return[] = $v; + $v = ''; + } else { + if (!$string && ($ch === '"' || $ch === "'")) { + $string = true; + $quote = $ch; + } else { + if ($string && $ch === $quote) { + if ($s[$i - 1] === "\\") { + $v = substr($v, 0, -1) . $ch; + } else { + $string = false; + } + } else { + $v .= $ch; + } + } + } } + } + } - return null; - }; + foreach ($return as &$r) { + if (is_numeric($r)) { + if (ctype_digit((string)$r)) { + $r = (int)$r; + } else { + $r = (float)$r; + } + } } - return $fn; + return $return; } /** - * Get an item from an array using "dot" notation. + * @param string|null $s + * @param int $start + * @param ?int $end * - * @param \ArrayAccess|array $array - * @param null|string $key - * @param mixed $default + * @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. * - * @return mixed + * @param mixed $array + * @param string|int|null $key + * @param mixed $default + * @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); @@ -156,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 { @@ -171,17 +397,29 @@ public static function get($array, ?string $key, $default = null) return $array; } + /** + * Determine whether the given value is array accessible. + * + * @param mixed $value + * + * @return bool + */ + public static function accessible(mixed $value): bool + { + return is_array($value) || $value instanceof ArrayAccess; + } + /** * Determine if the given key exists in the provided array. * - * @param \ArrayAccess|array $array - * @param string|int $key + * @param ArrayAccess|array $array + * @param string|int $key * * @return bool */ - public static function exists($array, $key) + public static function exists(ArrayAccess|array $array, string|int $key): bool { - if ($array instanceof \ArrayAccess) { + if ($array instanceof ArrayAccess) { return $array->offsetExists($key); } @@ -189,138 +427,264 @@ public static function exists($array, $key) } /** - * Determine whether the given value is array accessible. - * - * @param mixed $value + * Check if an item or items exist in an array using "dot" notation. * + * @param ArrayAccess|array $array + * @param string|string[] $keys + * @param non-empty-string $separator * @return bool */ - public static function accessible($value) + public static function has(ArrayAccess|array $array, string|array $keys, string $separator = '.'): bool { - return is_array($value) || $value instanceof \ArrayAccess; + $keys = (array)$keys; + + if (!$array || $keys === []) { + return false; + } + + foreach ($keys as $key) { + $subKeyArray = $array; + + if (static::exists($array, $key)) { + continue; + } + + foreach (explode($separator, $key) as $segment) { + if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { + $subKeyArray = $subKeyArray[$segment]; + } else { + return false; + } + } + } + + return true; } /** - * @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. + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * @param array|ArrayObject|array $array + * @param-out array|ArrayObject|array $array + * @param string $key + * @param mixed $value + * @param non-empty-string $separator + * @return T[]|array|ArrayObject + */ + 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); + + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = []; + } + + $array = &$array[$key]; + } + + $array[array_shift($keys)] = $value; + + return $array; + } + + /** + * Remove one or many array items from a given array using "dot" notation. * - * @return array the merged array (the original arrays are not changed.) + * @param array|ArrayObject $array + * @param string[]|string $keys + * + * @return void */ - public static function merge($res, $b, $replaceArray = true) + public static function remove(array|ArrayObject &$array, array|string $keys): void { - foreach ($b as $key => $val) { - if (is_int($key)) { - if (isset($res[$key])) { - $res[] = $val; + $original = &$array; + $keys = (array)$keys; + + if (count($keys) === 0) { + return; + } + + foreach ($keys as $key) { + // if the exact key exists in the top-level, remove it + if (static::exists($array, $key)) { + unset($array[$key]); + + continue; + } + + $parts = explode('.', $key); + + // clean up before each pass + $array = &$original; + + while (count($parts) > 1) { + $part = array_shift($parts); + + if (isset($array[$part]) && is_array($array[$part])) { + $array = &$array[$part]; } else { - $res[$key] = $val; + continue 2; } - } elseif (is_array($val) && isset($res[$key]) && is_array($res[$key])) { - $res[$key] = ($replaceArray ? $val : self::merge($res[$key], $val, $replaceArray)); - } else { - $res[$key] = $val; } + + unset($array[array_shift($parts)]); } + } - return $res; + /** + * Replace templates into array + * Key = search value + * Value = replace value + * + * @param array $array + * @param array $replace + * + * @return array + */ + public static function replaceByTemplate(array $array, array $replace): array + { + return array_map(static fn($item) => self::itemReplaceByTemplate($item, $replace), $array); } /** - * Changes PHP array to default Postgres array format + * Replace templates into item * - * @param array $array + * @param mixed $item + * @param array $replace * - * @return string + * @return string|string[]|mixed */ - public static function toPostgresArray(array $array): string + private static function itemReplaceByTemplate(mixed $item, array $replace): mixed { - $json = \json_encode(self::toIndexedArray($array), JSON_UNESCAPED_UNICODE); + if (is_array($item)) { + return self::replaceByTemplate($item, $replace); + } - return str_replace(['[', ']', '"'], ['{', '}', ''], $json); + if (is_string($item)) { + return Str::replaceByTemplate($item, $replace); + } + + return $item; } /** - * @param string|null $s - * @param int $start - * @param null $end + * Find duplicates into an array + * + * @param array $array * - * @return array + * @return array */ - public static function fromPostgresArray(?string $s, $start = 0, &$end = null): array + public static function duplicates(array $array): array { - if (empty($s) || $s[0] !== '{') { - return []; - } + return array_unique(array_diff_assoc($array, array_unique($array))); + } - $return = []; - $string = false; - $quote = ''; - $len = strlen($s); - $v = ''; + /** + * Fill a keyed array by values from another array + * + * @param array $keys + * @param array $values + * + * @return array + */ + public static function fillKeysByValues(array $keys, array $values): array + { + $result = []; - for ($i = $start + 1; $i < $len; $i++) { - $ch = $s[$i]; - if (!$string && $ch === '}') { - if ($v !== '' || !empty($return)) { - $return[] = $v; - } - $end = $i; - break; - } else { - if (!$string && $ch === '{') { - $v = self::fromPostgresArray($s, $i, $i); - } else - if (!$string && $ch === ',') { - $return[] = $v; - $v = ''; - } else - if (!$string && ($ch === '"' || $ch === "'")) { - $string = true; - $quote = $ch; - } else - if ($string && $ch === $quote) { - if ($s[$i - 1] === "\\") { - $v = substr($v, 0, -1) . $ch; - } else { - $string = false; - } - } else { - $v .= $ch; - } - } + foreach ($keys as $key => $keyName) { + $result[$keyName] = $values[$key] ?? null; } - foreach ($return as &$r) { - if (is_numeric($r)) { - if (ctype_digit($r)) { - $r = (int)$r; - } else { - $r = (float)$r; - } - } + 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 $return; + return $array; } /** - * Remove named keys from arrays + * Get one or a specified number of random values from an array. * - * @param array $array + * @param array $array + * @param int|null $number + * @param bool $preserveKeys + * @return T[]|array * - * @return array + * @throws \InvalidArgumentException */ - public static function toIndexedArray(array $array): array + public static function random(array $array, ?int $number = null, bool $preserveKeys = false): mixed { - $array = array_values($array); - foreach ($array as &$value) { - if (is_array($value)) { - $value = static::toIndexedArray($value); + $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 $array; + 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 new file mode 100644 index 0000000..4d0d2b0 --- /dev/null +++ b/src/Helpers/B64.php @@ -0,0 +1,88 @@ + 0; + } + + + /** + * Check a bit is existing in flag`s list + * + * @param int[] $list + * @param int $bit + * + * @return bool + */ + public static function exist(array $list, int $bit): bool + { + return self::checkFlag(self::grant($list), $bit); + } + + /** + * Return value of sum of all bits in list + * + * @param int[] $list + * + * @return int + */ + public static function grant(array $list): int + { + return array_reduce($list, fn(int $prev, int $next) => $prev | $next, 0); + } + + /** + * Convert decimal to binary string with left pad zero-filling + * + * @param int $bit + * @param int $length + * + * @return string + */ + public static function decBinPad(int $bit, int $length): string + { + return sprintf("%0{$length}d", decbin($bit)); + } +} diff --git a/src/Helpers/Json.php b/src/Helpers/Json.php index 783780e..5b1457e 100644 --- a/src/Helpers/Json.php +++ b/src/Helpers/Json.php @@ -1,28 +1,31 @@ 'The maximum stack depth has been exceeded.', - 'JSON_ERROR_STATE_MISMATCH' => 'Invalid or malformed JSON.', - 'JSON_ERROR_CTRL_CHAR' => 'Control character error, possibly incorrectly encoded.', - 'JSON_ERROR_SYNTAX' => 'Syntax error.', - 'JSON_ERROR_UTF8' => 'Malformed UTF-8 characters, possibly incorrectly encoded.', // PHP 5.3.3 - 'JSON_ERROR_RECURSION' => 'One or more recursive references in the value to be encoded.', // PHP 5.5.0 - 'JSON_ERROR_INF_OR_NAN' => 'One or more NAN or INF values in the value to be encoded', // PHP 5.5.0 - 'JSON_ERROR_UNSUPPORTED_TYPE' => 'A value of a type that cannot be encoded was given', // PHP 5.5.0 - ]; - + public static function htmlEncode($value): ?string + { + return static::encode( + $value, + JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS + ); + } /** * Encodes the given value into a JSON string. @@ -34,37 +37,17 @@ class Json * @param int $options the encoding options. For more details please refer to * . Default is * `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`. + * @param int<1, max> $depth * - * @return null|string the encoding result. - * @throws InvalidArgumentException if there is any encoding error. + * @return string|null */ - public static function encode($value, $options = 320): ?string + public static function encode($value, int $options = 320, int $depth = 512): ?string { - $value = static::dataToArray($value); - set_error_handler(function () { - static::handleJsonError(JSON_ERROR_SYNTAX); - }, E_WARNING); - $json = \json_encode($value, $options); - restore_error_handler(); - static::handleJsonError(json_last_error()); + $value = Arr::dataToArray($value); - return $json ?: null; - } + $json = json_encode($value, $options, $depth); - /** - * Encodes the given value into a JSON string HTML-escaping entities so it is safe to be embedded in HTML code. - * The method enhances `json_encode()` by supporting JavaScript expressions. - * Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification. - * You must ensure strings passed to this method have proper encoding before passing them. - * - * @param mixed $value the data to be encoded - * - * @return string|null the encoding result - * @throws InvalidArgumentException if there is any encoding error - */ - public static function htmlEncode($value): ?string - { - return static::encode($value, JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS); + return $json ?: null; } /** @@ -72,78 +55,23 @@ public static function htmlEncode($value): ?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<1, max> $depth * - * @return mixed the PHP data - * @throws InvalidArgumentException if there is any decoding error + * @return mixed|null */ - public static function decode(?string $json, $asArray = true) + public static function decode(?string $json, bool $asArray = true, int $options = 0, int $depth = 512) { - if (is_null($json) || $json === '') { + if ($json === null || $json === '') { return null; } - $decode = \json_decode($json, $asArray); - static::handleJsonError(json_last_error()); - - return $decode; - } - - /** - * Handles [[encode()]] and [[decode()]] errors by throwing exceptions with the respective error message. - * - * @param int $lastError error code from [json_last_error()](http://php.net/manual/en/function.json-last-error.php). - * - * @throws InvalidArgumentException if there is any encoding/decoding error. - */ - protected static function handleJsonError($lastError) - { - if ($lastError === JSON_ERROR_NONE) { - return; - } - $availableErrors = []; - foreach (static::$jsonErrorMessages as $const => $message) { - if (defined($const)) { - $availableErrors[constant($const)] = $message; - } - } - if (isset($availableErrors[$lastError])) { - throw new InvalidArgumentException($availableErrors[$lastError], $lastError); - } - throw new InvalidArgumentException('Unknown JSON encoding/decoding error.'); - } - /** - * Pre-processes the data before sending it to `json_encode()`. - * - * @param mixed $data the data to be processed - * - * @return mixed the processed data - */ - public static function dataToArray($data) - { - if (is_object($data)) { - if ($data instanceof \JsonSerializable) { - return static::dataToArray($data->jsonSerialize()); - } elseif ($data instanceof Arrayable) { - $data = $data->toArray(); - } else { - $result = []; - if (is_iterable($data)) { - foreach ($data as $name => $value) { - $result[$name] = $value; - } - } - $data = $result; - } - } - - if (is_array($data)) { - foreach ($data as $key => $value) { - if (is_array($value) || is_object($value)) { - $data[$key] = static::dataToArray($value); - } - } + // @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 $data; + return json_decode($json, $asArray, $depth, $options); } } diff --git a/src/Helpers/JwtParser.php b/src/Helpers/JwtParser.php deleted file mode 100644 index 478f056..0000000 --- a/src/Helpers/JwtParser.php +++ /dev/null @@ -1,163 +0,0 @@ -token = $token; - $this->decodeFn = $decodeFn ?? \Closure::fromCallable([$this, 'base64UrlDecode']); - } - - /** - * @param string $data - * - * @return bool|string - */ - public static function base64UrlDecode(string $data) - { - if ($remainder = strlen($data) % 4) { - $data .= str_repeat('=', 4 - $remainder); - } - - return base64_decode(strtr($data, '-_', '+/')); - } - - /** - * @param string $json - * - * @return mixed - */ - private static function toArray(string $json) - { - return Json::decode($json); - } - - /** - * @param string $token - * - * @return \Php\Support\Entities\JwtToken - */ - public static function parseToken(string $token) - { - return (new self($token))->parse(); - } - - /** - * Parse data token - * - * @return \Php\Support\Entities\JwtToken - */ - private function parse() - { - $data = $this->splitJwt(); - - $header = $this->parseHeader($data[0]); - $claims = $this->parseClaims($data[1]); - $signature = $this->parseSignature($header, $data[2]); - - foreach ($claims as $name => $value) { - if (isset($header[ $name ])) { - $header[ $name ] = $value; - } - } - - if ($signature === null) { - unset($data[2]); - } - - return new JwtToken($header, $claims, $signature, $data); - } - - /** - * @return array - */ - private function splitJwt() - { - $data = explode('.', $this->token); - - if (count($data) !== 3) { - throw new InvalidArgumentException('The JWT string must have two dots'); - } - - return $data; - } - - /** - * @param string $data - * - * @return string - */ - private function decodeFn(string $data): string - { - $fn = $this->decodeFn; - - return $fn($data); - } - - /** - * @param string $data - * - * @return array - */ - private function parseHeader(string $data) - { - $header = static::toArray($this->decodeFn($data)); - - if (isset($header['enc'])) { - throw new InvalidArgumentException('Encryption is not supported yet'); - } - - return $header; - } - - /** - * Parses the claim set from a string - * - * @param string $data - * - * @return array - */ - private function parseClaims(string $data) - { - return static::toArray($this->decodeFn($data)); - } - - /** - * Returns the signature from given data - * - * @param array $header - * @param string $data - * - * @return string|null - */ - private function parseSignature(array $header, string $data): ?string - { - if ($data === '' || !isset($header['alg']) || $header['alg'] === 'none') { - return null; - } - - return $this->decodeFn($data); - } -} 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 8ef4987..743b4f8 100644 --- a/src/Helpers/Str.php +++ b/src/Helpers/Str.php @@ -1,86 +1,377 @@ + */ + protected static array $delimitedCache = []; + + /** + * Converts a string to snake_case + */ + public static function toSnake(string $str): string + { + return self::toDelimited($str, '_'); + } + + /** + * Converts a string to delimited.snake.case (in this case `del = '.'`) + */ + public static function toDelimited(string $str, string $delimiter): string + { + return self::toScreamingDelimited($str, $delimiter, false); + } + + /** + * Converts a string to SCREAMING.DELIMITED.SNAKE.CASE (in this case `del = '.'; screaming = true`) or + * delimited.snake.case (in this case `del = '.'; screaming = false`) + */ + public static function toScreamingDelimited(string $str, string $delimiter, bool $screaming): string + { + $str = self::removeMultiSpace($str); + $str = self::addWordBoundariesToNumbers($str); + $str = trim($str); + + if (isset(static::$delimitedCache[$str][$delimiter][$screaming])) { + return static::$delimitedCache[$str][$delimiter][$screaming]; + } + + $res = ''; + $len = mb_strlen($str, 'UTF-8'); + + $get_letter = static function (int $idx, string $s) { + return mb_substr($s, $idx, 1, 'UTF-8'); + }; + + for ($i = 0; $i < $len; $i++) { + // treat acronyms as words, eg for JSONData -> JSON is a whole word + $next_case_is_changed = false; + + $letter = $get_letter($i, $str); + + if ($i + 1 < $len) { + $next_letter = $get_letter($i + 1, $str); + if ( + ( + $letter >= 'A' + && $letter <= 'Z' + && $next_letter >= 'a' + && $next_letter <= 'z' + ) + || ( + $letter >= 'a' + && $letter <= 'z' + && $next_letter >= 'A' + && $next_letter <= 'Z' + ) + ) { + $next_case_is_changed = true; + } + } + + if ($i > 0 && ($get_letter(mb_strlen($res, 'UTF-8') - 1, $res) !== $delimiter) && $next_case_is_changed) { + // add underscore if next letter case type is changed + if ($letter >= 'A' && $letter <= 'Z') { + $res .= $delimiter . $letter; + } else { + if ($letter >= 'a' && $letter <= 'z') { + $res .= $letter . $delimiter; + } + } + } else { + if ($letter === ' ' || $letter === '_' || $letter === '-') { + // replace spaces/underscores with delimiters + $res .= $delimiter; + } else { + $res .= $letter; + } + } + } + + if ($screaming) { + $res = mb_strtoupper($res, 'UTF-8'); + } else { + $res = mb_strtolower($res, 'UTF-8'); + } + + return static::$delimitedCache[$str][$delimiter][$screaming] = $res; + } + + /** + * Remove all multi-spaced characters + * + * @param string $str + * + * @return string + */ + public static function removeMultiSpace(string $str): string + { + $res = preg_replace('/\s+/', ' ', $str); + return is_string($res) ? $res : $str; + } + + private static function addWordBoundariesToNumbers(string $str): string + { + $res = preg_replace('/([a-zA-Z])(\d+)([a-zA-Z]?)/u', '$1 $2 $3', $str); + return is_string($res) ? $res : $str; + } + + /** + * Converts a string to SCREAMING_SNAKE_CASE + */ + public static function toScreamingSnake(string $str): string + { + return self::toScreamingDelimited($str, '_', true); + } + + /** + * Converts a string to kebab-case + */ + public static function toKebab(string $str): string + { + return self::toDelimited($str, '-'); + } + + /** + * Converts a string to CamelCase + */ + public static function toCamel(string $str): string + { + return self::toCamelInitCase($str, true); + } + + /** + * Converts a string to CamelCase + */ + public static function toCamelInitCase(string $str, bool $initCase): string + { + $str = self::removeMultiSpace($str); + $str = self::addWordBoundariesToNumbers($str); + $str = trim($str); + + if (isset(static::$delimitedCache[$str][$initCase])) { + return static::$delimitedCache[$str][$initCase]; + } + + + $len = mb_strlen($str, 'UTF-8'); + + $get_letter = static function (int $idx, string $s) { + return mb_substr($s, $idx, 1, 'UTF-8'); + }; + + $res = ''; + + $cap_next = $initCase; + + for ($i = 0; $i < $len; $i++) { + $letter = $get_letter($i, $str); + + if ($letter >= 'A' && $letter <= 'Z') { + $res .= $letter; + } + + if ($letter >= '0' && $letter <= '9') { + $res .= $letter; + } + + if ($letter >= 'a' && $letter <= 'z') { + if ($cap_next) { + $res .= mb_strtoupper($letter); + } else { + $res .= $letter; + } + } + + if ($letter === '_' || $letter === ' ' || $letter === '-') { + $cap_next = true; + } else { + $cap_next = false; + } + } + + return static::$delimitedCache[$str][$initCase] = $res; + } + + /** + * Converts a string to lowerCamelCase + */ + public static function toLowerCamel(string $str): string + { + if ($str === '') { + return $str; + } + + return self::toCamelInitCase(lcfirst($str), false); + } + + /** + * Replace substr by start and finish indents + */ + public static function replaceStrTo(string $str, int $from_start, int $from_end, string $toStr = '*'): string + { + $from_start = $from_start < 0 ? 0 : $from_start; + $from_end = $from_end < 0 ? 0 : $from_end; + $len = mb_strlen($str); + + if ($from_start + $from_end >= $len) { + return $str; + } + + $start_str = mb_substr($str, 0, $from_start); + $end_str = $from_end ? mb_substr($str, -$from_end) : ''; + + $replace_str = str_repeat($toStr, mb_strlen(mb_substr($str, $from_start, $len - $from_end - $from_start))); + + return $start_str . $replace_str . $end_str; + } + /** * Replace templates into string * Key = search value * Value = replace value * * @param string $str - * @param array $replace + * @param array $replace * - * @return mixed + * @return string|string[] */ - public static function stringReplaceByTemplate(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; + } + /** - * The cache of studly-cased words. - * - * @var array + * Truncate a string to a specified length without cutting a word off */ - protected static $studlyCache = []; + 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; + } + /** - * Convert a value to studly caps case. + * Generate a string safe for use in URLs from any given string. * - * @param string $value + * @param string $str + * @param string $separator + * @param bool $firstLetterOnly * * @return string */ - public static function studly($value): string + public static function slugify(string $str, string $separator = '-', bool $firstLetterOnly = false): string { - $key = $value; + return self::slugifyWithFormat($str, $separator, '([^a-z\d]+)', $firstLetterOnly); + } - if (isset(static::$studlyCache[$key])) { - return static::$studlyCache[$key]; + 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 ''; } - $value = ucwords(str_replace(['-', '_'], ' ', $value)); + 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 static::$studlyCache[$key] = str_replace(' ', '', $value); + return $slug; } + /** - * The cache of snake-cased words. + * 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 * - * @var array + * @return bool */ - protected static $snakeCache = []; + public static function seemsUTF8(string $string): bool + { + return URLify::seemsUTF8($string); + } /** - * Convert a string to snake case. - * - * @param string $value - * @param string $delimiter - * - * @return string + * Converts all accent characters to ASCII characters. */ - public static function snake($value, $delimiter = '_'): string + public static function removeAccents(string $str, string $language = ''): string { - $key = $value; + if (!preg_match('/[\x80-\xff]/', $str)) { + return $str; + } + + return URLify::downcode($str, $language); + } - if (isset(static::$snakeCache[$key][$delimiter])) { - return static::$snakeCache[$key][$delimiter]; + public static function trimPrefix(string $str, string $prefix): string + { + if (str_starts_with($str, $prefix)) { + return mb_substr($str, mb_strlen($prefix)); } - if (!ctype_lower($value)) { - $value = \preg_replace('/\s+/u', '', ucwords($value)); + return $str; + } - $value = \preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, (string)$value); - $value = \mb_strtolower((string)$value, 'UTF-8'); + public static function trimSuffix(string $str, string $suffix): string + { + if (str_ends_with($str, $suffix)) { + return mb_substr($str, 0, mb_strlen($str) - mb_strlen($suffix)); } - return static::$snakeCache[$key][$delimiter] = $value; + 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 6146e36..d36184d 100644 --- a/src/Interfaces/Arrayable.php +++ b/src/Interfaces/Arrayable.php @@ -1,18 +1,17 @@ */ - public function toArray(array $fields = []); + public function toArray(): array; } diff --git a/src/Interfaces/BaseStorage.php b/src/Interfaces/BaseStorage.php deleted file mode 100644 index 62394db..0000000 --- a/src/Interfaces/BaseStorage.php +++ /dev/null @@ -1,28 +0,0 @@ - + * @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/ConfigurableTrait.php b/src/Traits/ConfigurableTrait.php index aea32c8..8036537 100644 --- a/src/Traits/ConfigurableTrait.php +++ b/src/Traits/ConfigurableTrait.php @@ -2,37 +2,36 @@ namespace Php\Support\Traits; +use ArrayAccess; use Php\Support\Exceptions\InvalidParamException; +/** + * @template TKey of array-key + * @template TValue + * @implements ArrayAccess + * @mixin ArrayAccess + */ trait ConfigurableTrait { - - /** - * @param array $attributes - * @param bool $exceptOnMiss - * - * @return $this - */ - public function configurable(array $attributes, ?bool $exceptOnMiss = true) + public function configurable(array|ArrayAccess $attributes, bool $throwOnMissingProp = true): static { foreach ($attributes as $key => $value) { - if (!$this->setProp($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 - */ - private function setProp(string $key, $value) + protected function applyValue(string $key, mixed $value): bool + { + return $this->callSetterProp($key, $value) || $this->setPropValue($key, $value); + } + + protected function setPropValue(string $key, mixed $value): bool { - if (property_exists($this, $key)) { + if ($this->propertyExists($key)) { $this->{$key} = $value; return true; @@ -40,4 +39,20 @@ private function setProp(string $key, $value) return false; } -} \ No newline at end of file + + protected function propertyExists(string $name): bool + { + return property_exists($this, $name); + } + + protected function callSetterProp(string $key, mixed $value): bool + { + if ($method = findSetterMethodByProp($this, $key)) { + $this->$method($value); + + return true; + } + + return false; + } +} diff --git a/src/Traits/ConsolePrint.php b/src/Traits/ConsolePrint.php index e842a97..97c3be4 100644 --- a/src/Traits/ConsolePrint.php +++ b/src/Traits/ConsolePrint.php @@ -1,29 +1,25 @@ dynamicHashKeys = $keys; - - return $this; - } - - /** - * Create Dynamic hash from $value on keys from dynamicHashKeys - * - * @param mixed $value - * @param string $delimiter - * - * @return null|string - */ - public function dynamicHash($value, string $delimiter = '|'): ?string - { - if ($this->dynamicHashKeys) { - $values = static::buildValuesPack($this->dynamicHashKeys, $value); - - if ($values) { - return static::hash(implode($delimiter, $values)); - } - } - - return null; - } - - /** - * @param array $keys - * @param mixed $value - * - * @return array - */ - protected static function buildValuesPack(array $keys, $value): array - { - return array_filter(array_map(function ($key) use ($value) { - return $value[ $key ] ?? null; - }, $keys)); - } - - /** - * @param string $value - * - * @return string - */ - protected static function hash(string $value): string - { - return md5($value); - } -} \ No newline at end of file diff --git a/src/Traits/Getter.php b/src/Traits/Getter.php deleted file mode 100644 index efbd413..0000000 --- a/src/Traits/Getter.php +++ /dev/null @@ -1,58 +0,0 @@ -$getter(); - } elseif (method_exists($this, 'set' . ucfirst($name))) { - throw new InvalidCallException('Getting write-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Getting unknown property: ' . get_class($this) . '::' . $name, $name); - } - } - - /** - * @param string $name - * - * @return string - */ - public static function getter(string $name) - { - return 'get' . ucfirst($name); - } - - /** - * @param string $name - * - * @return bool - */ - public function __isset(string $name) - { - $getter = static::getter($name); - if (method_exists($this, $getter)) { - return $this->$getter() !== null; - } else { - return false; - } - } -} \ No newline at end of file diff --git a/src/Traits/HasAttributes.php b/src/Traits/HasAttributes.php deleted file mode 100644 index b4a93a9..0000000 --- a/src/Traits/HasAttributes.php +++ /dev/null @@ -1,414 +0,0 @@ -attributes; - } - - /** - * @param string $key - * - * @return bool - */ - public function hasAttribute(string $key): bool - { - return array_key_exists($key, $this->attributes); - } - - /** - * Get an attribute from the model. - * - * @param string $key - * - * @return mixed - */ - public function getAttribute($key) - { - if ($this->hasAttribute($key)) { - return $this->getAttributeValue($key); - } - - return null; - } - - /** - * @param string $key - * - * @return mixed - * @throws \Php\Support\Exceptions\UnknownPropertyException - */ - public function __get(string $key) - { - if ($this->hasAttribute($key)) { - return $this->getAttributeValue($key); - } - - return static::__getParent($key); - } - - /** - * @param string $key - * - * @return bool - */ - public function __isset(string $key) - { - if ($this->hasAttribute($key)) { - return isset($this->attributes[ $key ]); - } - - return static::__issetParent($key); - } - - /** - * @param string $key - */ - public function __unset(string $key) - { - if ($this->hasAttribute($key)) { - unset($this->attributes[ $key ]); - } else { - static::__unsetParent($key); - } - } - - /** - * Get a plain attribute (not a relationship). - * - * @param string $key - * - * @return mixed - */ - public function getAttributeValue($key) - { - $value = $this->getAttributeFromArray($key); - - if ($this->hasGetMutator($key)) { - return $this->mutateAttribute($key, $value); - } - - return $value; - } - - /** - * Get an attribute from the $attributes array. - * - * @param string $key - * - * @return mixed - */ - protected function getAttributeFromArray($key) - { - if (isset($this->attributes[ $key ])) { - return $this->attributes[ $key ]; - } - - return null; - } - - - /** - * Set a given attribute on the model. - * - * @param string $key - * @param mixed $value - * - * @return mixed - */ - public function setAttribute(string $key, $value) - { - if ($this->hasSetMutator($key)) { - return $this->setMutatedAttributeValue($key, $value); - } - - $this->attributes[ $key ] = $value; - - return $this; - } - - /** - * Alias of setAttribute - * - * @param string $key - * @param mixed $value - * - * @return $this - */ - public function addAttribute(string $key, $value) - { - $this->setAttribute($key, $value); - - return $this; - } - - /** - * @param string $key - * @param mixed $value - * - * @throws \Php\Support\Exceptions\UnknownPropertyException - */ - public function __set(string $key, $value) - { - if ($this->hasAttribute($key)) { - $this->setAttribute($key, $value); - } else { - static::__setParent($key, $value); - } - } - - /** - * Set the array of model attributes. No checking is done. - * - * @param array $attributes - * @param bool $sync - * - * @return $this - */ - public function setRawAttributes(array $attributes, $sync = false) - { - $this->attributes = $attributes; - - if ($sync) { - $this->syncOriginal(); - } - - return $this; - } - - /** - * Get the model's original attribute values. - * - * @param string|null $key - * @param mixed $default - * - * @return mixed|array - */ - public function getOriginal($key = null, $default = null) - { - return Arr::get($this->original, $key, $default); - } - - /** - * Get a subset of the model's attributes. - * - * @param array|mixed $attributes - * - * @return array - */ - public function only($attributes) - { - $results = []; - - foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) { - $results[ $attribute ] = $this->getAttribute($attribute); - } - - return $results; - } - - /** - * Sync the original attributes with the current. - * - * @return $this - */ - public function syncOriginal() - { - $this->original = $this->attributes; - - return $this; - } - - /** - * Sync a single original attribute with its current value. - * - * @param string $attribute - * - * @return $this - */ - public function syncOriginalAttribute($attribute) - { - $this->original[ $attribute ] = $this->attributes[ $attribute ]; - - return $this; - } - - /** - * Sync the changed attributes. - * - * @return $this - */ - public function syncChanges() - { - $this->changes = $this->getDirty(); - - return $this; - } - - /** - * Determine if the model or given attribute(s) have been modified. - * - * @param array|string|null $attributes - * - * @return bool - */ - public function isDirty($attributes = null) - { - return $this->hasChanges( - $this->getDirty(), is_array($attributes) ? $attributes : func_get_args() - ); - } - - /** - * Determine if the model or given attribute(s) have remained the same. - * - * @param array|string|null $attributes - * - * @return bool - */ - public function isClean($attributes = null) - { - return !$this->isDirty(...func_get_args()); - } - - /** - * Determine if the model or given attribute(s) have been modified. - * - * @param array|string|null $attributes - * - * @return bool - */ - public function wasChanged($attributes = null) - { - return $this->hasChanges( - $this->getChanges(), is_array($attributes) ? $attributes : func_get_args() - ); - } - - /** - * Determine if the given attributes were changed. - * - * @param array $changes - * @param array|string|null $attributes - * - * @return bool - */ - protected function hasChanges($changes, $attributes = null) - { - if (empty($attributes)) { - return count($changes) > 0; - } - - foreach ((array)$attributes as $attribute) { - if (array_key_exists($attribute, $changes)) { - return true; - } - } - - return false; - } - - /** - * Get the attributes that have been changed since last sync. - * - * @return array - */ - public function getDirty() - { - $dirty = []; - - foreach ($this->getAttributes() as $key => $value) { - if (!$this->originalIsEquivalent($key, $value)) { - $dirty[ $key ] = $value; - } - } - - return $dirty; - } - - /** - * Get the attributes that were changed. - * - * @return array - */ - public function getChanges() - { - return $this->changes; - } - - /** - * Determine if the new and old values for a given key are equivalent. - * - * @param string $key - * @param mixed $current - * - * @return bool - */ - protected function originalIsEquivalent($key, $current) - { - if (!array_key_exists($key, $this->original)) { - return false; - } - - $original = $this->getOriginal($key); - - if ($current === $original) { - return true; - } elseif (is_null($current)) { - return false; - } - - return is_numeric($current) && is_numeric($original) - && strcmp((string)$current, (string)$original) === 0; - } - - -} 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 new file mode 100755 index 0000000..b0d0504 --- /dev/null +++ b/src/Traits/Maker.php @@ -0,0 +1,24 @@ + + */ + protected array $meta = []; + + /** + * Get additional meta information to merge with the element payload. + * + * @return array + */ + public function meta(): array + { + return $this->meta; + } + + 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 + */ + public function withMeta(array $meta): static + { + $this->meta = Arr::merge($this->meta, $meta); + + return $this; + } +} diff --git a/src/Traits/ReadOnlyProperties.php b/src/Traits/ReadOnlyProperties.php new file mode 100755 index 0000000..d5ee2db --- /dev/null +++ b/src/Traits/ReadOnlyProperties.php @@ -0,0 +1,19 @@ +$key; + } + + throw new MissingPropertyException(null, $key); + } +} diff --git a/src/Traits/Setter.php b/src/Traits/Setter.php deleted file mode 100644 index 364ace4..0000000 --- a/src/Traits/Setter.php +++ /dev/null @@ -1,58 +0,0 @@ -$setter($value); - } elseif (method_exists($this, 'get' . ucfirst($name))) { - throw new InvalidCallException('Setting read-only property: ' . get_class($this) . '::' . $name); - } else { - throw new UnknownPropertyException('Setting unknown property: ' . get_class($this) . '::' . $name); - } - } - - /** - * @param string $name - * - * @return string - */ - public static function setter(string $name) - { - return 'set' . ucfirst($name); - } - - /** - * @param string $name - */ - public function __unset(string $name) - { - $setter = static::setter($name); - - if (method_exists($this, $setter)) { - $this->$setter(null); - } elseif (method_exists($this, 'get' . ucfirst($name))) { - throw new InvalidCallException('Unsetting read-only property: ' . get_class($this) . '::' . $name); - } - } -} \ No newline at end of file diff --git a/src/Traits/Singleton.php b/src/Traits/Singleton.php new file mode 100644 index 0000000..23dedc8 --- /dev/null +++ b/src/Traits/Singleton.php @@ -0,0 +1,52 @@ + + */ + protected static array $instances = []; + + /** + * prevent the creation of an object through the new operator + */ + protected function __construct() + { + } + + public static function getInstance(): self + { + $cls = static::class; + if (!isset(static::$instances[$cls])) { + static::$instances[$cls] = new static(); + } + + return static::$instances[$cls]; + } + + /** + * Singletons should not be recoverable from strings + * + * @throws Exception + */ + public function __wakeup() + { + throw new Exception('Cannot unserialize a singleton.'); + } + + /** + * Singletons should not be cloned + */ + protected function __clone() + { + } +} diff --git a/src/Traits/Thrower.php b/src/Traits/Thrower.php new file mode 100755 index 0000000..daa7cba --- /dev/null +++ b/src/Traits/Thrower.php @@ -0,0 +1,31 @@ + + */ + protected static array $traitInitializers = []; + + protected static function bootTraits(): array + { + $class = static::class; + + static::$traitInitializers[$class] = []; + + $traits = static::parentBootTraits(); + + + foreach ($traits as $trait) { + if (method_exists($class, $method = 'initialize' . class_basename($trait))) { + static::$traitInitializers[$class][] = $method; + + static::$traitInitializers[$class] = \array_unique( + static::$traitInitializers[$class] + ); + } + } + + return $traits; + } + + /** + * Initialize any initializable traits on the model. + * + * @return void + */ + protected function initializeTraits(): void + { + foreach (static::$traitInitializers[static::class] as $method) { + $this->{$method}(); + } + } + + protected function bootIfNotBooted(): void + { + $this->parentBootIfNotBooted(); + + $this->initializeTraits(); + } +} 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 new file mode 100644 index 0000000..0790373 --- /dev/null +++ b/src/Traits/UseErrorsBox.php @@ -0,0 +1,41 @@ +getMessage(); + } + + $this->errors[] = (string)$message; + + return $this; + } + + public function hasErrors(): bool + { + return count($this->errors) > 0; + } + + public function errors(): array + { + return $this->errors; + } + + public function clearErrors(): static + { + $this->errors = []; + + return $this; + } +} 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 new file mode 100755 index 0000000..8f47809 --- /dev/null +++ b/src/Traits/Whener.php @@ -0,0 +1,25 @@ + longitude + * @learn: y => latitude + */ +class GeoPoint extends Point +{ + /** + * @param int $options + * + * @return string + * @throws \Php\Support\Exceptions\JsonException + */ + public function toJson($options = 320): string + { + return Json::encode( + [ + 'longitude' => $this->x, + 'latitude' => $this->y, + ], + $options + ); + } + + /** + * @param string|null $string + * + * @return Jsonable|null + * @throws \Php\Support\Exceptions\JsonException + */ + public static function fromJson(?string $string): ?Jsonable + { + if (!$array = Json::decode($string)) { + return null; + } + + return new static($array['longitude'], $array['latitude']); + } +} diff --git a/src/Types/Point.php b/src/Types/Point.php index f4499b7..31b89c3 100644 --- a/src/Types/Point.php +++ b/src/Types/Point.php @@ -1,8 +1,11 @@ longitude = (float)$long; - $this->latitude = (float)$lat; } - /** - * @return string - */ - public function __toString() - { - return $this->toDB(); - } - - /** - * @param array $fields - * - * @return array - */ - public function toArray(array $fields = []) + public function toArray(): array { return [ - $this->longitude, - $this->latitude, + $this->x, + $this->y, ]; } @@ -62,17 +34,13 @@ public function toArray(array $fields = []) * * @return Point|null */ - public static function fromArray(array $array) + public static function fromArray(array $array): ?self { if (count($array) !== 2) { - throw new InvalidParamException('Должно быть два параметра'); - } - - if (empty($array[0]) || empty($array[1])) { - return null; + throw new InvalidParamException('Array must contains 2 elements: [ x, y ]'); } - return new static($array[0], $array[1]); + return new static(...$array); } @@ -81,65 +49,55 @@ public static function fromArray(array $array) * * @return string|null */ - public function toJson($options = 320) + public function toJson($options = 320): ?string { - return Json::encode([ - 'longitude' => $this->longitude, - 'latitude' => $this->latitude, - ], $options); + return Json::encode( + [ + 'x' => $this->x, + 'y' => $this->y, + ], + $options + ); } /** - * @param string $string + * @param string|null $string * - * @return \Php\Support\Interfaces\Jsonable + * @return Jsonable|null */ - public static function fromJson(string $string): ?Jsonable + public static function fromJson(?string $string): ?Jsonable { if (!$array = Json::decode($string)) { return null; } - return new static($array['longitude'], $array['latitude']); - } - - /** - * @param string $string - * - * @return \Php\Support\Interfaces\Jsonable|Point|null - */ - public static function fromDB(string $string): ?Jsonable - { - if (empty($string)) { - return null; - } - - $string = mb_substr($string, 1, -1); - - if (empty($string)) { - return null; - } - - list($long, $lat) = explode(',', $string); - - return new static($long, $lat); + return new static($array['x'], $array['y']); } /** * @return string */ - public function toDB(): string + public function toPgDB(): string { - return '(' . $this->longitude . ',' . $this->latitude . ')'; + return '(' . $this->x . ',' . $this->y . ')'; } - /** - * @return bool + * @param string|null $value + * + * @return $this|null */ - public function isEmpty() + public function castFromDatabase(?string $value): ?self { - return !$this->longitude || !$this->latitude; + if (!$result = Arr::fromPostgresPoint($value)) { + return null; + } + + [ + $x, + $y, + ] = $result; + return new static((float)$x, (float)$y); } @@ -153,25 +111,6 @@ public function isEmpty() */ public static function calcDistance(Point $point1, Point $point2): float { - return sqrt(pow($point1->latitude - $point2->latitude, 2) + pow($point1->longitude - $point2->longitude, 2)); - } - - /** - * Возвращает первую точку из набора, которая строго ближе заданной дистанции - * - * @param array $points - * @param float $distance - * - * @return Point|null - */ - public function getNearPoint(array $points, float $distance): ?Point - { - foreach ($points as $point) { - if (static::calcDistance($this, $point) < $distance) { - return $point; - } - } - - return null; + return sqrt((($point1->x - $point2->x) ** 2) + (($point1->y - $point2->y) ** 2)); } } diff --git a/src/global.php b/src/global.php deleted file mode 100644 index 71dc36d..0000000 --- a/src/global.php +++ /dev/null @@ -1,21 +0,0 @@ -assertInstanceOf(BaseObject::class, new BaseObject()); - } - - public function testCanBeEqualClassName(): void - { - $this->assertEquals(BaseObject::className(), BaseObject::class); - $this->assertEquals(BaseObject::shortClassName(), 'BaseObject'); - } - - public function testMagicMethods(): void - { - $cls = new class() extends BaseObject - { - public $prop; - - public function getKeyName() - { - return $this->prop; - } - - public function setKeyName($val) - { - $this->prop = $val; - } - }; - - - $this->assertNull($cls->prop); - $cls->keyName = 'val 1'; - - $this->assertEquals('val 1', $cls->prop); - $this->assertEquals('val 1', $cls->getKeyName()); - $this->assertEquals('val 1', $cls->keyName); - $this->assertNotEquals('val 2', $cls->keyName); - - unset($cls->keyName); - $this->assertNull($cls->prop); - } - -} 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 @@ +expectException(Exception::class); + $this->expectExceptionMessage('Invalid Arg'); + + throw new Exception('Invalid Arg'); + } + + public function testThrow2() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Exception'); + + throw new Exception(); + } +} diff --git a/tests/exceptions/InvalidArgumentTest.php b/tests/Exceptions/InvalidArgumentTest.php similarity index 81% rename from tests/exceptions/InvalidArgumentTest.php rename to tests/Exceptions/InvalidArgumentTest.php index 45b15be..3f20d2d 100644 --- a/tests/exceptions/InvalidArgumentTest.php +++ b/tests/Exceptions/InvalidArgumentTest.php @@ -1,23 +1,30 @@ assertInstanceOf(InvalidArgumentException::class, $e); $this->assertSame('Invalid Arg', $e->getMessage()); } try { throw new InvalidArgumentException(); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->assertInstanceOf(InvalidArgumentException::class, $e); $this->assertSame('Invalid Argument', $e->getName()); $this->assertSame('Invalid Argument', $e->getMessage()); diff --git a/tests/exceptions/InvalidParamTest.php b/tests/Exceptions/InvalidParamTest.php similarity index 62% rename from tests/exceptions/InvalidParamTest.php rename to tests/Exceptions/InvalidParamTest.php index 7e7f839..0d89862 100644 --- a/tests/exceptions/InvalidParamTest.php +++ b/tests/Exceptions/InvalidParamTest.php @@ -1,42 +1,51 @@ assertInstanceOf(InvalidParamException::class, $e); $this->assertSame('Invalid Param', $e->getMessage()); + $this->assertSame('Invalid Parameter', $e->getName()); } try { throw new InvalidParamException(); - } catch (\Throwable $e) { + } catch (InvalidParamException $e) { $this->assertInstanceOf(InvalidParamException::class, $e); $this->assertSame('Invalid Parameter', $e->getMessage()); - $this->assertNull($e->getParam()); + $this->assertSame('Invalid Parameter', $e->getName()); + $this->assertNull($e->name); } try { throw new InvalidParamException('Invalid Param', 'prop'); - } catch (\Throwable $e) { + } 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()); } try { throw new InvalidParamException(null, 'prop'); - } catch (\Throwable $e) { + } 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 58% rename from tests/exceptions/MissingClassTest.php rename to tests/Exceptions/MissingClassTest.php index 886bfa8..eb64497 100644 --- a/tests/exceptions/MissingClassTest.php +++ b/tests/Exceptions/MissingClassTest.php @@ -1,8 +1,12 @@ assertInstanceOf(MissingClassException::class, $e); - $this->assertSame('Missing Class', $e->getMessage()); - } - try { throw new MissingClassException(MissingClassException::class); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->assertInstanceOf(MissingClassException::class, $e); $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) { + } catch (Throwable $e) { $this->assertInstanceOf(MissingClassException::class, $e); $this->assertSame('Test Message: ' . MissingClassException::class, $e->getMessage()); } 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 86% rename from tests/exceptions/NotSupportedTest.php rename to tests/Exceptions/NotSupportedTest.php index 980ae7f..bfe1b5d 100644 --- a/tests/exceptions/NotSupportedTest.php +++ b/tests/Exceptions/NotSupportedTest.php @@ -1,8 +1,12 @@ assertInstanceOf(NotSupportedException::class, $e); $this->assertSame('Not Supported', $e->getMessage()); } try { throw new NotSupportedException(NotSupportedException::class); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->assertInstanceOf(NotSupportedException::class, $e); $this->assertSame('Not Supported: ' . NotSupportedException::class, $e->getMessage()); } try { throw new NotSupportedException(null, 'Test Message'); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->assertInstanceOf(NotSupportedException::class, $e); $this->assertSame('Test Message', $e->getMessage()); } try { throw new NotSupportedException(NotSupportedException::class, 'Test Message'); - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->assertInstanceOf(NotSupportedException::class, $e); $this->assertSame('Test Message: ' . NotSupportedException::class, $e->getMessage()); } 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 new file mode 100644 index 0000000..4168df1 --- /dev/null +++ b/tests/Global/BaseTest.php @@ -0,0 +1,351 @@ + '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' => '', + 'emptyArray' => [], + 'cls' => new class { + public function __invoke() + { + return 'cls.test'; + } + }, + 'fn' => static function () { + return 'fn.test'; + }, + ]; + } + + public function testValue(): void + { + foreach (static::values() as $key => $val) { + $result = value($val); + + if (is_callable($val)) { + $this->assertEquals("$key.test", $result); + } else { + $this->assertEquals($val, $result); + } + } + } + + 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, + ], + + ] as $data + ) { + // $this->assertEquals(isTrue($data['val']), $data['res']); + $this->assertEquals(isTrue($data['val'], true), $data['resNull']); + } + } + + + public function testInstance(): void + { + $cls = instance(\stdClass::class); + static::assertEquals(\stdClass::class, \get_class($cls)); + static::assertTrue(is_object($cls)); + + $cls2 = instance($cls); + static::assertEquals(\get_class($cls), \get_class($cls2)); + static::assertEquals($cls, $cls2); + static::assertTrue(is_object($cls2)); + + /** @var Point $point */ + $point = instance(Point::class, 1, 10); + static::assertEquals(Point::class, \get_class($point)); + static::assertTrue(is_object($point)); + static::assertEquals(1, $point->x); + static::assertEquals(10, $point->y); + + foreach ( + [ + null, + '1', + 'true', + 'false', + 'null', + '0', + ] as $val + ) { + static::assertNull(instance($val)); + } + } + + + public function testTraitUsesRecursive(): void + { + $traits = trait_uses_recursive(TraitUsesRecursiveClass::class); + + static::assertEquals( + [ + \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 + ); + } + + public function testClassUsesRecursive(): void + { + $traits = class_uses_recursive(RecursiveClass::class); + + static::assertEquals( + [ + \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 + ); + } + + public function testClassBasename(): void + { + $name = class_basename(RecursiveClass::class); + static::assertEquals('RecursiveClass', $name); + $name = class_basename(new \stdClass()); + static::assertEquals('stdClass', $name); + } +} + + +class TraitUsesRecursiveClass +{ + use \Php\Support\Traits\Singleton; + use \Php\Support\Traits\UseConfigurableStorage; + + protected $username; +} + +class RecursiveClass extends TraitUsesRecursiveClass +{ + use \Php\Support\Traits\Maker; +} 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 new file mode 100644 index 0000000..0ec1602 --- /dev/null +++ b/tests/Helpers/ArrTest.php @@ -0,0 +1,1331 @@ + 'value11', + 'key12' => [ + 'value121', + 'value122', + ], + 'key13' => [ + 'key11' => 'value', + 'key12' => [ + 'value121', + 'value122', + ], + 'key13' => 'value13', + ], + + ]; + + $array2 = [ + + 'key11' => 'replace_value11', + 'key12' => [ + 'replace_value121', + 'replace_value122', + ], + 'key13' => [ + 'key11' => 'replace_value', + 'key12' => [ + 'replace_value121', + 'replace_value122', + ], + 'key13' => 'replace_value13', + ], + ]; + + $except_replace = [ + 'key11' => 'replace_value11', + 'key12' => [ + 'replace_value121', + 'replace_value122', + ], + 'key13' => [ + 'key11' => 'replace_value', + 'key12' => [ + 'replace_value121', + 'replace_value122', + ], + 'key13' => 'replace_value13', + ], + ]; + + $except_add = [ + 'key11' => 'replace_value11', + 'key12' => [ + 'value121', + 'value122', + 'replace_value121', + 'replace_value122', + ], + 'key13' => [ + 'key11' => 'replace_value', + 'key12' => [ + 'value121', + 'value122', + 'replace_value121', + 'replace_value122', + ], + 'key13' => 'replace_value13', + ], + ]; + + $result = Arr::merge($array1, $array2); + + static::assertTrue( + empty(array_diff_key($except_replace, $result)) && empty(array_diff_key($result, $except_replace)) + ); + $this->assertEquals($except_replace, $result); + + + $result_add = Arr::merge($array1, $array2, false); + + static::assertTrue( + empty(array_diff_key($except_add, $result_add)) && empty(array_diff_key($result_add, $except_add)) + ); + $this->assertEquals($except_add, $result_add); + } + + + public static function providerDataToArray(): array + { + $arrayableClass = new class () implements \Php\Support\Interfaces\Arrayable { + private $data = [ + '1', + 2, + 'test', + ]; + + public function toArray(): array + { + return $this->data; + } + }; + $jsonableClass = new class () implements \Php\Support\Interfaces\Jsonable { + private $data = [ + '32', + 12, + 'test', + ]; + + public static function fromJson(string $json): ?Jsonable + { + return new self(); + } + + public function toJson($options = 320): ?string + { + return Json::encode($this->data, $options); + } + }; + + return [ + [ + [ + 1, + 2, + 3, + ], + [ + 1, + 2, + 3, + ], + ], + [ + [], + [], + ], + [ + [null], + [null], + ], + [ + [ + 'test' => 1, + 0 => 14, + 'nested' => [ + 'cl' => $arrayableClass, + 'cl2' => $arrayableClass, + '1' => [ + 1, + 2, + $jsonableClass, + ], + ], + 'csl' => $arrayableClass, + ], + [ + 'test' => 1, + 0 => 14, + 'nested' => [ + 'cl' => [ + '1', + 2, + 'test', + ], + 'cl2' => [ + '1', + 2, + 'test', + ], + '1' => [ + 1, + 2, + [ + '32', + 12, + 'test', + ], + ], + ], + 'csl' => [ + '1', + 2, + 'test', + ], + ], + ], + [ + $arrayableClass, + [ + '1', + 2, + 'test', + ], + ], + [ + $jsonableClass, + [ + '32', + 12, + 'test', + ], + ], + [ + new class () implements \JsonSerializable { + private $data = + [ + + '132', + 12, + 'test', + ]; + + public function jsonSerialize(): mixed + { + return $this->data; + } + }, + [ + '132', + 12, + 'test', + ], + ], + [ + new ArrayObject([12, 'test 1']), + [ + 12, + 'test 1', + ], + ], + ]; + } + + #[DataProvider('providerDataToArray')] + public function testDataToArray($items, $exp): void + { + $result = Arr::dataToArray($items); + + static::assertEquals($exp, $result); + } + + public static function providerToArray(): array + { + $arrayableClass = new class () implements \Php\Support\Interfaces\Arrayable { + private $data = [ + '1', + 2, + 'test', + ]; + + public function toArray(): array + { + return $this->data; + } + }; + $jsonableClass = new class () implements \Php\Support\Interfaces\Jsonable { + private $data = [ + '32', + 12, + 'test', + ]; + + public static function fromJson(string $json): ?Jsonable + { + return new self(); + } + + public function toJson($options = 320): ?string + { + return Json::encode($this->data, $options); + } + }; + + return [ + [ + [ + 1, + 2, + 3, + ], + [ + 1, + 2, + 3, + ], + ], + [ + [], + [], + ], + [ + [null], + [null], + ], + [ + 1, + [1], + ], + [ + 'test', + ['test'], + ], + [ + $arrayableClass, + [ + '1', + 2, + 'test', + ], + ], + [ + $jsonableClass, + [ + '32', + 12, + 'test', + ], + ], + [ + new class () implements \JsonSerializable { + private $data = + [ + + '132', + 12, + 'test', + ]; + + public function jsonSerialize(): mixed + { + return $this->data; + } + }, + [ + '132', + 12, + 'test', + ], + ], + [ + new ArrayObject([12, 'test 1']), + [ + 12, + 'test 1', + ], + ], + ]; + } + + #[DataProvider('providerToArray')] + public function testToArray($items, $exp): void + { + $result = Arr::toArray($items); + + static::assertEquals($exp, $result); + } + + + public function testAccessible(): void + { + static::assertTrue(Arr::accessible([])); + static::assertTrue(Arr::accessible([1, 2])); + static::assertTrue(Arr::accessible([1, []])); + static::assertTrue(Arr::accessible(new ArrayObject([12, 'test 1']))); + + static::assertFalse(Arr::accessible(1)); + static::assertFalse(Arr::accessible('test')); + static::assertFalse(Arr::accessible('0')); + static::assertFalse(Arr::accessible(null)); + static::assertFalse(Arr::accessible(12.3)); + static::assertFalse( + Arr::accessible( + function () { + } + ) + ); + static::assertFalse( + Arr::accessible( + new class () { + } + ) + ); + static::assertFalse(Arr::accessible(new \stdClass())); + } + + + public function testExists(): void + { + $array = [ + 'key1' => 'val1', + 2 => 'val2', + 0 => 'val0', + 'test' => 'test', + ]; + + static::assertTrue(Arr::exists($array, 'key1')); + static::assertTrue(Arr::exists($array, 2)); + static::assertTrue(Arr::exists($array, 0)); + static::assertTrue(Arr::exists($array, 'test')); + static::assertFalse(Arr::exists($array, 'test.key1')); + static::assertFalse(Arr::exists($array, 'te')); + static::assertFalse(Arr::exists($array, '')); + static::assertFalse(Arr::exists($array, 1)); + + + $array = new ArrayObject($array); + + static::assertTrue(Arr::exists($array, 'key1')); + static::assertTrue(Arr::exists($array, 2)); + static::assertTrue(Arr::exists($array, 0)); + static::assertTrue(Arr::exists($array, 'test')); + static::assertFalse(Arr::exists($array, 'test.key1')); + static::assertFalse(Arr::exists($array, 'te')); + static::assertFalse(Arr::exists($array, '')); + static::assertFalse(Arr::exists($array, 1)); + } + + + public function testToIndexedArray(): void + { + $array = [ + 'key1' => 'val1', + 'test' => 'test', + 'nested' => [ + 'n1' => 'test1', + 'n2' => 'test2', + ], + 'indexed1' => [ + 1, + 2, + 3, + 4, + ], + 'indexed2' => [ + 'val1', + 'val2', + 'val3', + 'val4', + ], + ]; + + $res = Arr::toIndexedArray($array); + + $expected = [ + 'val1', + 'test', + [ + 'test1', + 'test2', + ], + [ + 1, + 2, + 3, + 4, + ], + [ + 'val1', + 'val2', + 'val3', + 'val4', + ], + ]; + + static::assertEquals($expected, $res); + } + + public function testToPostgresArray(): void + { + static::assertEquals( + '{val1,test,null,,null}', + Arr::ToPostgresArray( + [ + 'key1' => 'val1', + 'test' => 'test', + 'nested' => null, + 'indexed1' => '', + 'indexed2' => null, + ] + ) + ); + static::assertEquals( + '{val1,test,indexed2}', + Arr::ToPostgresArray( + [ + 'val1', + 'test', + 'indexed2', + ] + ) + ); + static::assertEquals( + '{1,12,32323}', + Arr::ToPostgresArray( + [ + 1, + 12, + 32323, + ] + ) + ); + + static::assertEquals( + '{1,null,0,,null}', + Arr::ToPostgresArray( + [ + 1, + null, + 0, + '', + null, + ] + ) + ); + + 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([], Arr::fromPostgresArray('{}')); + } + + 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, + 2, + 3, + ], + 2, + ], + [ + [ + 1 => 'val 21', + 2 => 'vat', + 3 => 'test', + ], + 0, + [ + 'val 2', + 'val 21', + 'vat', + 'test', + ], + 'val 2', + ], + [ + [ + 1 => 'val 2', + 2 => 'val 21', + 3 => 'vat', + 4 => 'test', + ], + 0, + [ + 'val 2', + 'val 2', + 'val 21', + 'vat', + 'test', + ], + 'val 2', + ], + [ + [ + 'val 2', + 'val 21', + 'vat', + ], + 3, + [ + 'val 2', + 'val 21', + 'vat', + null, + ], + null, + ], + [ + [ + 'val 2', + 'val 21', + 'vat', + null, + ], + null, + [ + 'val 2', + 'val 21', + 'vat', + null, + ], + 1, + ], + [ + [ + 'key 1' => 'val 1', + 'key 2' => 'val 2', + ], + 'key 4', + [ + 'key 1' => 'val 1', + 'key 2' => 'val 2', + 'key 4' => 'val 4', + ], + 'val 4', + ], + [ + ['a'], + null, + ['a'], + 'c', + ], + [ + ['a'], + null, + ['a'], + null, + ], + [ + ['a'], + null, + ['a'], + '', + ], + [ + ['a'], + null, + ['a'], + '1', + ], + [ + ['a'], + null, + ['a'], + 1, + ], + [ + [2], + null, + [2], + 1, + ], + [ + [], + null, + [], + 'c', + ], + [ + [], + null, + [], + null, + ], + ]; + } + + #[DataProvider('providerRemoveByValue')] + public function testRemoveByValue($expArray, $expIdx, $array, $val): void + { + $idx = Arr::removeByValue($array, $val); + static::assertEquals($expArray, $array); + static::assertEquals($expIdx, $idx); + } + + public static function providerRemoveByValueAndReindex(): array + { + return [ + [ + [ + 1, + 3, + ], + 1, + [ + 1, + 2, + 3, + ], + 2, + ], + [ + [ + 'val 21', + 'vat', + 'test', + ], + 0, + [ + 'val 2', + 'val 21', + 'vat', + 'test', + ], + 'val 2', + ], + [ + [ + 'val 2', + 'val 21', + 'vat', + 'test', + ], + 0, + [ + 'val 2', + 'val 2', + 'val 21', + 'vat', + 'test', + ], + 'val 2', + ], + [ + [ + 'val 2', + 'val 21', + 'vat', + ], + 3, + [ + 'val 2', + 'val 21', + 'vat', + null, + ], + null, + ], + [ + [ + 'val 2', + 'val 21', + 'vat', + null, + ], + null, + [ + 'val 2', + 'val 21', + 'vat', + null, + ], + 1, + ], + [ + [ + 'val 1', + 'val 2', + ], + 'key 4', + [ + 'key 1' => 'val 1', + 'key 2' => 'val 2', + 'key 4' => 'val 4', + ], + 'val 4', + ], + + [ + ['a'], + null, + ['a'], + 'c', + ], + [ + ['a'], + null, + ['a'], + null, + ], + [ + ['a'], + null, + ['a'], + '', + ], + [ + ['a'], + null, + ['a'], + '1', + ], + [ + ['a'], + null, + ['a'], + 1, + ], + [ + [2], + null, + [2], + 1, + ], + [ + [], + null, + [], + 'c', + ], + [ + [], + null, + [], + null, + ], + ]; + } + + #[DataProvider('providerRemoveByValueAndReindex')] + public function testRemoveByValueAndReindex($expArray, $expIdx, $array, $val): void + { + $idx = Arr::removeByValue($array, $val, true); + + static::assertEquals($expArray, $array); + static::assertEquals($expIdx, $idx); + } + + /** + * @return array + */ + public static function providerGet(): array + { + $array = [ + 'key' => [ + 'sub1' => 'val1', + 'sub2' => [ + 'val2', + 'val3', + ], + 'sub4' => ['sub4sub' => 'val3'], + ], + 'key2' => 2, + 'key4' => 1, + ]; + + return [ + [ + 2, + $array, + 'key2', + ], + [ + 1, + $array, + 'key4', + ], + [ + 'val1', + $array, + 'key.sub1', + ], + [ + [ + 'val2', + 'val3', + ], + $array, + 'key.sub2', + ], + [ + 'val3', + $array, + 'key.sub4.sub4sub', + ], + [ + null, + $array, + 'key.sub3.sub4sub', + ], + [ + 'val3', + new ArrayObject($array), + 'key.sub4.sub4sub', + ], + + [ + $array, + $array, + null, + ], + + [ + null, + $array, + 'key3', + ], + + [ + null, + 1, + '1', + ], + [ + null, + 1, + '2', + ], + [ + null, + null, + '2', + ], + [ + null, + null, + null, + ], + ]; + } + + #[DataProvider('providerGet')] + public function testGet($expVal, $array, $key): void + { + $val = Arr::get($array, $key); + + static::assertEquals($expVal, $val); + + $val = Arr::get($array, $key, 'test'); + + static::assertEquals($expVal ?? 'test', $val); + } + + public static function providerHas(): array + { + $array = [ + 'key' => [ + 'sub1' => 'val1', + 'sub2' => [ + 'val2', + 'val3', + ], + 'sub4' => ['sub4sub' => 'val3'], + ], + 'key2' => 2, + 'key4' => 1, + ]; + + return [ + [ + true, + $array, + 'key2', + ], + [ + true, + $array, + 'key4', + ], + [ + false, + $array, + 'key3', + ], + [ + true, + $array, + 'key.sub1', + ], + [ + true, + $array, + 'key.sub2', + ], + [ + false, + $array, + 'key.sub12', + ], + [ + true, + $array, + 'key.sub4.sub4sub', + ], + [ + false, + $array, + 'key.sub3.sub4sub', + ], + ]; + } + + #[DataProvider('providerHas')] + public function testHas($expVal, $array, $key): void + { + static::assertEquals($expVal, Arr::has($array, $key)); + } + + public static function providerSet(): array + { + $array = []; + + return [ + [ + 2, + $array, + 'key2', + ], + [ + 1, + $array, + 'key4', + ], + [ + 'val1', + $array, + 'key.sub1', + ], + [ + [ + 'val2', + 'val3', + ], + $array, + 'key.sub2', + ], + [ + 'val3', + $array, + 'key.sub4.sub4sub', + ], + [ + null, + $array, + 'key.sub3.sub4sub', + ], + [ + 'val3', + new ArrayObject($array), + 'key.sub4.sub4sub', + ], + [ + null, + $array, + 'key3', + ], + ]; + } + + #[DataProvider('providerSet')] + public function testSet($expVal, $array, $key): void + { + Arr::set($array, $key, $expVal); + + static::assertEquals($expVal, Arr::get($array, $key)); + } + + public function testSet2(): void + { + $array = []; + static::assertEquals(['' => 1], Arr::set($array, '', 1)); + } + + 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' => [ + 'sub1' => 'val1', + 'sub2' => [ + 'val2', + 'val3', + ], + 'sub4' => ['sub4sub' => 'val3'], + ], + 'key2' => 2, + 'key4' => 1, + ]; + + return [ + [ + $array, + 'key2', + ], + [ + $array, + 'key4', + ], + [ + $array, + 'key.sub1', + ], + [ + $array, + 'key.sub2', + ], + [ + $array, + 'test', + ], + [ + $array, + 'key.sub4.sub4sub', + ], + [ + $array, + 'key.sub3.sub4sub', + ], + [ + new ArrayObject($array), + 'key.sub4.sub4sub', + ], + ]; + } + + #[DataProvider('providerRemove')] + public function testRemove($array, $key): void + { + Arr::remove($array, $key); + + static::assertNull(Arr::get($array, $key)); + } + + public function testRemove2(): void + { + $array = [ + 'key2' => 2, + 'key4' => 1, + ]; + Arr::remove($array, []); + + static::assertEquals($array, Arr::get($array, null)); + } + + public static function dataReplaceByTemplate(): array + { + return [ + [ + ['text {{%TOKEN%}} value'], + ['{{%TOKEN%}}' => 'token'], + ['text token value'], + ], + [ + [ + 'key' => '{{%KEY%}}', + 'token' => '{{%TOKEN%}}', + ], + [ + '{{%KEY%}}' => 'vKey', + '{{%TOKEN%}}' => 'vToken', + ], + [ + 'key' => 'vKey', + 'token' => 'vToken', + ], + ], + [ + [ + 'key' => '{{%KEY%}}', + 'token' => '{{%TOKEN%}}', + ], + ['{{%KEY%}}' => 'vKey'], + [ + 'key' => 'vKey', + 'token' => '{{%TOKEN%}}', + ], + ], + [ + [ + 'key' => '{{%KEY%}}', + 'token' => '{{%TOKEN%}}', + ], + ['{{%KEY%}}' => ''], + [ + 'key' => '', + 'token' => '{{%TOKEN%}}', + ], + ], + [ + [ + 'key' => '{{%KEY%}}', + 'token' => '{{%TOKEN%}}', + ], + ['{{%KEY%}}' => null], + [ + 'key' => '', + 'token' => '{{%TOKEN%}}', + ], + ], + [ + [ + 'step1' => [ + 'key' => '{{%KEY%}}', + 'token' => '{{%TOKEN%}}', + ], + 'step2' => [ + 'subStep2' => [ + 'token' => '{{%TOKEN%}}', + 'key' => '{{%KEY%}}', + ], + ], + 'step3' => ['val' => '{{%VALUE%}}'], + ], + [ + '{{%KEY%}}' => 'vKey', + '{{%TOKEN%}}' => 'vToken', + '{{%VALUE%}}' => 12, + ], + [ + 'step1' => [ + 'key' => 'vKey', + 'token' => 'vToken', + ], + 'step2' => [ + 'subStep2' => [ + 'token' => 'vToken', + 'key' => 'vKey', + ], + ], + 'step3' => ['val' => '12'], + ], + ], + [ + ['sdasdas'], + [ + '{{%KEY%}}' => 'key', + '{{%TOKEN%}}' => 'token', + ], + ['sdasdas'], + ], + [ + ['sdaas'], + [], + ['sdaas'], + ], + ]; + } + + #[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); + static::assertJsonStringEqualsJsonString(\json_encode($exp), \json_encode($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 new file mode 100644 index 0000000..7b0f341 --- /dev/null +++ b/tests/Helpers/B64Test.php @@ -0,0 +1,67 @@ + 'dXJs', + '1' => 'MQ==', + 'null' => 'bnVsbA==', + '0' => 'MA==', + 'https://github.com/' => 'aHR0cHM6Ly9naXRodWIuY29tLw==', + 'http://xn--e1ajeds9e.xn--p1ai/' => 'aHR0cDovL9C60YDQtdC80LvRjC7RgNGE', + 'https://github.com/?+%' => 'aHR0cHM6Ly9naXRodWIuY29tLz8rJQ==', + '(https|http)?_-' => 'KGh0dHBzfGh0dHApP18t', + '12Кириллик' => 'MTLQmtC40YDQuNC70LvQuNC6', + "12Кир\nиллик\nen" => 'MTLQmtC40YAK0LjQu9C70LjQugplbg==', + "12Кир\tиллик\ten" => 'MTLQmtC40YAJ0LjQu9C70LjQugllbg==', + "'πάντα χωρεῖ καὶ οὐδὲν μένει …'" => + 'J8+AzqzOvc+EzrEgz4fPic+BzrXhv5YgzrrOseG9tiDOv+G9kM604b2yzr0gzrzOrc69zrXOuSDigKYn', + '🤪 🤪 😈' => '8J+kqiDwn6SqIPCfmIg=', + ]; + + private static $emptyList = [ + '' => '', + null => '', + ]; + + public function testEncode(): void + { + foreach (self::$list as $str => $decodeStr) { + $h = B64::encode((string)$str); + static::assertEquals($decodeStr, $h); + } + + foreach (self::$emptyList as $str => $decodeStr) { + $h = B64::encode((string)$str); + static::assertEquals($decodeStr, $h); + } + } + + public function testDecode(): void + { + foreach (array_flip(self::$list) as $str => $decodeStr) { + $h = B64::decode($str); + static::assertEquals((string)$decodeStr, $h); + } + } + + public function testFull(): void + { + for ($i = 0; $i < 1000; $i++) { + $data = \openssl_random_pseudo_bytes(\mt_rand(12, 24)); + + static::assertEquals($data, B64::decode(B64::encode($data))); + static::assertEquals($data, B64::decodeSafe(B64::encodeSafe($data))); + } + } +} diff --git a/tests/Helpers/BitTest.php b/tests/Helpers/BitTest.php new file mode 100644 index 0000000..32151a7 --- /dev/null +++ b/tests/Helpers/BitTest.php @@ -0,0 +1,137 @@ + 0); + static::assertTrue(Bit::checkFlag($val, self::LOGIN)); + + $val = Bit::addFlag(Bit::decBinPad($val, count(self::permissions())), self::READ); + static::assertEquals(3, $val); + static::assertTrue(($val & self::READ) > 0); + static::assertTrue(Bit::checkFlag($val, self::READ)); + + static::assertSame(($val & self::CREATE), 0); + static::assertSame(($val & self::UPDATE), 0); + static::assertSame(($val & self::DELETE), 0); + static::assertFalse(Bit::checkFlag($val, self::CREATE)); + static::assertFalse(Bit::checkFlag($val, self::UPDATE)); + static::assertFalse(Bit::checkFlag($val, self::DELETE)); + } + + public function testRemoveFlag(): void + { + $val = Bit::removeFlag(0, self::CREATE); + static::assertEquals(0, $val); + + $val = Bit::addFlag(Bit::decBinPad($val, count(self::permissions())), self::LOGIN); + static::assertTrue(Bit::checkFlag($val, self::LOGIN)); + + $val1 = Bit::removeFlag($val, self::CREATE); + static::assertEquals($val1, $val); + + $val = Bit::removeFlag($val1, self::LOGIN); + static::assertEquals(0, $val); + + $val = Bit::addFlag( + $val, + self::READ | self::READ | self::DELETE + ); + + static::assertTrue(Bit::checkFlag($val, self::READ)); + static::assertTrue(Bit::checkFlag($val, self::DELETE)); + static::assertEquals(bindec(b'10010'), $val); + + $val = Bit::removeFlag($val, self::READ | self::DELETE); + static::assertEquals(0, $val); + foreach (self::permissions() as $permission) { + static::assertFalse(Bit::checkFlag($val, $permission)); + } + + $val = Bit::addFlag( + $val, + self::READ | self::CREATE | self::DELETE + ); + + $val = Bit::removeFlag($val, self::READ); + static::assertEquals(bindec(b'10100'), $val); + } + + + public function testExistFlag(): void + { + foreach (self::permissions() as $permission) { + static::assertTrue(Bit::exist(self::permissions(), $permission)); + } + + for ($i = 5; $i <= 20; $i++) { + static::assertFalse(Bit::exist(self::permissions(), 1 << $i)); + } + } + + public function testDecBinPad(): void + { + $perms = [ + self::LOGIN => '00001', + self::READ => '00010', + self::CREATE => '00100', + self::UPDATE => '01000', + self::DELETE => '10000', + ]; + + $padLength = count($perms); + + foreach ($perms as $flag => $expected) { + static::assertEquals($expected, Bit::decBinPad($flag, $padLength)); + } + } +} 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 new file mode 100644 index 0000000..3a239bf --- /dev/null +++ b/tests/Helpers/JsonTest.php @@ -0,0 +1,258 @@ +getMockBuilder(\Php\Support\Interfaces\Arrayable::class)->getMock(); + $data_arrayable->method('toArray')->willReturn([]); + + $actual = Json::encode($data_arrayable); + self::assertSame('[]', $actual); + // basic data encoding + $data = '1'; + self::assertSame('"1"', Json::encode($data)); + // simple array encoding + $data = [ + 1, + 2, + ]; + self::assertSame('[1,2]', Json::encode($data)); + $data = [ + 'a' => 1, + 'b' => 2, + ]; + self::assertSame('{"a":1,"b":2}', Json::encode($data)); + // simple object encoding + $data = new \stdClass(); + $data->a = 1; + $data->b = 2; + self::assertSame('[]', Json::encode($data)); + // empty data encoding + $data = []; + self::assertSame('[]', Json::encode($data)); + $data = new \stdClass(); + self::assertSame('[]', Json::encode($data)); + + $data = (object)null; + self::assertSame('[]', Json::encode($data)); + // JsonSerializable + $data = new JsonModel(); + self::assertSame('{"json":"serializable"}', Json::encode($data)); + + $data = new JsonModel(); + $data->data = []; + self::assertSame('[]', Json::encode($data)); + $data = new JsonModel(); + $data->data = (object)null; + self::assertSame('[]', Json::encode($data)); + } + + /** + */ + public function testHtmlEncode(): void + { + // HTML escaped chars + $data = '&<>"\'/'; + self::assertSame('"\u0026\u003C\u003E\u0022\u0027\/"', Json::htmlEncode($data)); + // basic data encoding + $data = '1'; + self::assertSame('"1"', Json::htmlEncode($data)); + // simple array encoding + $data = [ + 1, + 2, + ]; + self::assertSame('[1,2]', Json::htmlEncode($data)); + $data = [ + 'a' => 1, + 'b' => 2, + ]; + self::assertSame('{"a":1,"b":2}', Json::htmlEncode($data)); + // simple object encoding + $data = new \stdClass(); + $data->a = 1; + $data->b = 2; + self::assertSame('[]', Json::htmlEncode($data)); + + $data = (object)null; + self::assertSame('[]', Json::htmlEncode($data)); + // JsonSerializable + $data = new JsonModel(); + self::assertSame('{"json":"serializable"}', Json::htmlEncode($data)); + } + + /** + */ + public function testDecode(): void + { + // empty value + $json = ''; + $actual = Json::decode($json); + self::assertNull($actual); + // 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)); + + self::assertEquals([], Json::decode('{}', true, JSON_THROW_ON_ERROR, 2)); + self::assertEquals([], Json::decode("{}", true, JSON_THROW_ON_ERROR, 2)); + self::assertEquals([], Json::decode("[]", true, JSON_THROW_ON_ERROR, 2)); + // exception + $json = '{"a":1,"b":2'; +// $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'); + + $res = Json::decode('sa', true, JSON_THROW_ON_ERROR); + self::assertNull($res); + } + + public function testDecodeInvalidParamException2(): void + { + $res = Json::decode('sa'); + self::assertNull($res); + self::assertEquals(JSON_ERROR_SYNTAX, json_last_error()); + } + + + /** + * + */ + public function testHandleJsonError(): void + { + $json = "{'a': '1'}"; + static::assertNull(Json::decode($json, )); + static::assertNull(Json::decode($json, true, JSON_THROW_ON_ERROR)); + } + + + /** + * @return array + */ + /*public function providerToArray(): array + { + return [ + [[1, 2, 3], [1, 2, 3]], + [[], []], + [[null], [null]], + [1, [1]], + ['test', ['test']], + [new class () implements \Php\Support\Interfaces\Arrayable + { + private $data = ['1', 2, 'test']; + + public function toArray(): array + { + return $this->data; + } + + }, ['1', 2, 'test']], + [new class () implements \Php\Support\Interfaces\Jsonable + { + private $data = ['32', 12, 'test']; + + public static function fromJson(string $json): ?Jsonable + { + return new self; + } + + public function toJson($options = 320): ?string + { + return Json::encode($this->data, $options); + } + + }, ['32', 12, 'test']], + [new class () implements \JsonSerializable + { + private $data = ['132', 12, 'test']; + + public function jsonSerialize() + { + return $this->data; + } + + }, ['132', 12, 'test']], + [new ArrayObject([12, 'test 1']), [12, 'test 1']], + ]; + }*/ + + /** + * dataProvider providerToArray + * + * @param $items + * @param $exp + * + * @throws JsonException + */ + /*public function testDataToArray($items, $exp): void + { + $result = Arr::toArray($items); + + static::assertTrue( + empty(array_diff_key($exp, $result)) && empty(array_diff_key($result, $exp)) + ); + }*/ +} + +/** + * Class JsonModel + */ +class JsonModel implements \JsonSerializable +{ + /** @var array */ + public $data = ['json' => 'serializable']; + + public function jsonSerialize(): mixed + { + return $this->data; + } + + /** + * @return array + */ + public function rules(): array + { + return [ + [ + 'name', + 'required', + ], + [ + 'name', + 'string', + 'max' => 100, + ], + ]; + } + + public function init(): void + { + } +} 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 @@ + 'token'], + 'text token value', + ], + [ + '"{{%KEY%}}-{{%TOKEN%}}" - test', + [ + '{{%KEY%}}' => 'key', + '{{%TOKEN%}}' => 'token', + ], + '"key-token" - test', + ], + [ + 'sdasdas', + [ + '{{%KEY%}}' => 'key', + '{{%TOKEN%}}' => 'token', + ], + 'sdasdas', + ], + [ + 'sdaas', + [], + 'sdaas', + ], + ]; + } + + + #[DataProvider('dataReplaceByTemplate')] + public function testReplaceByTemplate(string $str, array $replaced, string $exp): void + { + $result = Str::replaceByTemplate($str, $replaced); + static::assertEquals($exp, $result); + } + + 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, + ], + ]; + } + + #[DataProvider('dataRegExps')] + public function testIsRegExp(string $regexp, bool $result): void + { + 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/Interfaces/ArrayableTest.php b/tests/Interfaces/ArrayableTest.php new file mode 100644 index 0000000..60699e7 --- /dev/null +++ b/tests/Interfaces/ArrayableTest.php @@ -0,0 +1,32 @@ +getMockBuilder(\Php\Support\Interfaces\Arrayable::class)->getMock(); + + $data_arrayable + ->method('toArray') + ->willReturn(['key' => 'value']); + + $this->assertIsArray($data_arrayable->toArray()); + $this->assertEquals(['key' => 'value'], $data_arrayable->toArray()); + + + $actual = json_encode($data_arrayable->toArray()); + $this->assertSame('{"key":"value"}', $actual); + } +} diff --git a/tests/Interfaces/JsonableTest.php b/tests/Interfaces/JsonableTest.php new file mode 100644 index 0000000..08bfe65 --- /dev/null +++ b/tests/Interfaces/JsonableTest.php @@ -0,0 +1,37 @@ +getMockBuilder(\Php\Support\Interfaces\Jsonable::class)->getMock(); + + $data_jsonable + ->method('toJson') + ->willReturn('{"key":"value"}'); + + $this->assertIsString($data_jsonable->toJson()); + $this->assertEquals(json_encode(['key' => 'value']), $data_jsonable->toJson()); + + $null_jsonable = $this->getMockBuilder(\Php\Support\Interfaces\Jsonable::class)->getMock(); + + $null_jsonable + ->method('toJson') + ->willReturn(null); + + $this->assertNull($null_jsonable->toJson()); + $this->assertEquals(null, $null_jsonable->toJson()); + } +} diff --git a/tests/ParamsJsonTest.php b/tests/ParamsJsonTest.php deleted file mode 100644 index 8d463d7..0000000 --- a/tests/ParamsJsonTest.php +++ /dev/null @@ -1,147 +0,0 @@ - 1, - "type" => "PHONE", - "notification" => "false", - "contact" => "8-4912-25-97-22" - ]; - - protected static $contact_2 = [ - "type" => "email", - "notification" => "true", - "contact" => "mail@yahoo.com" - ]; - - protected static $resultOneJson = '{"id":1,"type":"PHONE","notification":"false","contact":"8-4912-25-97-22"}'; - protected static $resultJson = '{"_type":"Contact","_data":[{"id":1,"type":"PHONE","notification":"false","contact":"8-4912-25-97-22"},{"type":"email","notification":"true","contact":"mail@yahoo.com"}]}'; - - - private static function valuesCollection() - { - return new Contacts([static::$contact_1, static::$contact_2]); - } - - private static function values() - { - return new Contact(static::$contact_1); - } - - public function testCanBeOneInstance(): void - { - $contact = self::values(); - - $this->assertInstanceOf(Contact::class, $contact); - } - - public function testCanBeEmpty(): void - { - $contacts = new Contacts(); - - $this->assertInstanceOf(Contacts::class, $contacts); - $this->assertCount(0, $contacts); - - $this->assertEquals('[]', (string)$contacts); - $this->assertEquals('[]', $contacts->toJson()); - } - - public function testOneInstanceCanBeJson(): void - { - $contact = self::values(); - - $this->assertEquals(self::$resultOneJson, $contact->toJson()); - $this->assertEquals("PHONE", $contact->type); - $this->assertJson($contact->toJson()); - } - - public function testOneInstanceCanBeLoadFrom(): void - { - $contact = Contact::fromJson(self::$resultOneJson); - $this->assertInstanceOf(Contact::class, Contact::fromJson(self::$resultOneJson)); - - $this->assertNull($contact->getElementsType()); - $this->assertEquals(self::$resultOneJson, $contact->toJson()); - - $contact = new Contact; - $contact->setElementsType('string')->fromJsonString(self::$resultOneJson); - - foreach ($contact->toArray() as $field => $element) { - $this->assertIsString($element); - } - - } - - public function testOneInstanceCanBeChangeType(): void - { - $contact = Contact::fromJson(self::$resultOneJson); - $this->assertInstanceOf(Contact::class, Contact::fromJson(self::$resultOneJson)); - - $this->assertIsInt($contact->id); - - $contact->setElementsType('string'); - foreach ($contact->toArray() as $field => $element) { - $this->assertIsString($element); - } - - $contact->setElementsType('array'); - foreach ($contact->toArray() as $field => $element) { - $this->assertIsArray($element); - } - } - - public function testCollectionCanBeInstances(): void - { - $contacts = self::valuesCollection(); - - $this->assertInstanceOf(Contacts::class, $contacts); - - /** - * @var int $key - * @var Contact $contact - */ - foreach ($contacts as $key => $contact) { - $this->assertInstanceOf($contacts->getElementsType(), $contact); - } - } - - public function testCollectionCanBeJsonString(): void - { - $contacts = self::valuesCollection(); - - $this->assertEquals(self::$resultJson, $contacts->toJson()); - } - - public function testCollectionCanBeLoadFromJson(): void - { - $contacts = Contacts::fromJson(self::$resultJson); - - $this->assertEquals(self::$resultJson, $contacts->toJson()); - - /** - * @var int $key - * @var Contact $contact - */ - foreach ($contacts as $key => $contact) { - $this->assertInstanceOf($contacts->getElementsType(), $contact); - - $this->assertArrayHasKey('type', $contact->toArray()); - $this->assertJson($contact->toJson()); - } - - } -} - -class Contact extends ParamsJson -{ -} - -class Contacts extends ParamsJson -{ - protected $_type = Contact::class; -} diff --git a/tests/ParamsTest.php b/tests/ParamsTest.php deleted file mode 100644 index 36b2e1a..0000000 --- a/tests/ParamsTest.php +++ /dev/null @@ -1,301 +0,0 @@ - 'value', - 'int1' => 2, - 'int2' => -12, - 'array' => [1, 2, 3, 4, 5], - 'string' => 'string value', - 'null' => null, - 'false' => false, - 'true' => true, - 'float' => 12.31, - 'empty' => '', - 'emptyArray' => [], - 'cls' => new stdClass(), - ]; - } - - public function testCanBeInstanceEmpty(): void - { - $params = new Params(); - $this->assertInstanceOf(Params::class, $params); - } - - public function testCanBeInstanceFillDifferentValues(): void - { - $params = new Params(static::values()); - $this->assertInstanceOf( - Params::class, - $params - ); - } - - public function testCanBeGetByKey(): void - { - $params = new Params(static::values()); - $keys = ['int2', 'false', 'string', 'emptyArray']; - $array = $params->toArray($keys); - - $this->assertCount(count($keys), $array); - $this->assertEquals($params->offsetGet('int2'), $array['int2']); - $this->assertEquals($params->string, $array['string']); - $this->assertEquals($params->emptyArray, $array['emptyArray']); - } - - public function testCanBeCollect(): void - { - $phone1 = [ - "id" => 1, - "type" => "PHONE", - "notification" => "false", - "phone" => "8-4912-25-97-22" - ]; - $phone2 = [ - "id" => 2, - "type" => "FAX", - "notification" => "false", - "phone" => "8-4912-25-97-22" - ]; - - $phones = new Params([$phone1, $phone2]); - - $this->assertCount(2, $phones); - $this->assertEquals($phones->toArray(), [$phone1, $phone2]); - - $json = '[{"id":1,"type":"PHONE","notification":"false","phone":"8-4912-25-97-22"},{"id":2,"type":"FAX","notification":"false","phone":"8-4912-25-97-22"}]'; - $this->assertEquals($phones->toJson(), $json); - - $params = Params::fromJson($json); - - $this->assertEquals($params->toJson(), $json); - $this->assertEquals($params, $phones); - } - - public function testCanBeJson(): void - { - $phone1 = new Params([ - "id" => 1, - "type" => "PHONE", - "notification" => "false", - "phone" => "8-4912-25-97-22" - ]); - - $phones = new Params([$phone1]); - - $json = '[{"id":1,"type":"PHONE","notification":"false","phone":"8-4912-25-97-22"}]'; - $this->assertEquals($phones->toJson(), $json); - - } - - public function testFromJson(): void - { - $phone1 = new Phone([ - "id" => 1, - "type" => "PHONE", - "notification" => "false", - "phone" => "8-4912-25-97-22" - ]); - - $phones = (new Phones)->fromArray([$phone1]); - - $json = '[{"id":1,"type":"PHONE","notification":"false","phone":"8-4912-25-97-22"}]'; - $this->assertEquals($phones->toJson(), $json); - - $newPhones = Phones::fromJson($json); - - $this->assertInstanceOf(Phones::class, $newPhones); - - /** - * @var int $key - * @var Phone $element - */ - foreach ($newPhones as $key => $element) { - $this->assertIsArray($element); - - $this->assertEquals($phones[$key]['id'], $element['id']); - } - } - - - public function testInstanceCanAddObjectElement(): void - { - $phones = new Phones; - - $this->assertEquals(0, $phones->add(new Phone(['id' => 1]))); - $this->assertEquals(1, $phones->add(new Phone(['id' => 2]))); - $this->assertCount(2, $phones); - - $phone1 = $phones->get(0); - $this->assertInstanceOf(Phone::class, $phone1); - - $phone2 = $phones->get(1); - $this->assertInstanceOf(Phone::class, $phone2); - - } - - public function testInstanceCanAddObjectElementByIndex(): void - { - $phones = new Phones; - $key1 = 'phone 1'; - $key2 = 'phone 2'; - - $this->assertEquals($key1, $phones->add(new Phone(['id' => 1]), $key1)); - $this->assertEquals($key2, $phones->add(new Phone(['id' => 2]), $key2)); - $this->assertCount(2, $phones); - - $phone1 = $phones->get($key1); - $this->assertInstanceOf(Phone::class, $phone1); - - $phone2 = $phones->get($key2); - $this->assertInstanceOf(Phone::class, $phone2); - - } - - public function testInstanceCanAddObjectElementByPrimaryKey(): void - { - $phones = (new Phones)->setUniqueKeyName('id'); - $hash1 = $phones->add(new Phone(['id' => 1])); - $hash2 = $phones->add(new Phone(['id' => 2])); - - $this->assertEquals(1, $hash1); - $this->assertEquals(2, $hash2); - - $this->assertCount(2, $phones); - - $phone1 = $phones->get($hash1); - $this->assertInstanceOf(Phone::class, $phone1); - $this->assertEquals(1, $phone1->id); - - $phone2 = $phones->get($hash2); - $this->assertInstanceOf(Phone::class, $phone2); - $this->assertEquals(2, $phone2->id); - $this->assertEquals('{"1":{"id":1},"2":{"id":2}}', $phones->toJson()); - } - - public function testInstanceCanAddObjectElementByDynamicHash(): void - { - $phones = (new Phones)->setDynamicHashKeys(['type', 'val']); - $hash1 = $phones->add(new Phone(['type' => 'phone', 'val' => '8-4912-25-97-21'])); - $hash2 = $phones->add(new Phone(['type' => 'fax', 'val' => '8-4912-25-97-22'])); - - $this->assertIsString($hash1); - $this->assertIsString($hash2); - $this->assertNotNull($hash1); - $this->assertNotNull($hash2); - - $this->assertCount(2, $phones); - - $phone1 = $phones->get($hash1); - $this->assertInstanceOf(Phone::class, $phone1); - - $phone2 = $phones->get($hash2); - $this->assertInstanceOf(Phone::class, $phone2); - $this->assertEquals('8-4912-25-97-21', $phone1->val); - $this->assertEquals('8-4912-25-97-22', $phone2->val); - $this->assertEquals('fax', $phone2->type); - - } - - public function testInstanceCanAddSimpleElements(): void - { - $phones = new Phones; - $hash1 = $phones->add('phone 1'); - $hash2 = $phones->add('phone 2'); - $hash3 = $phones->add(3); - - $this->assertIsInt($hash1); - $this->assertIsInt($hash2); - $this->assertIsInt($hash3); - $this->assertNotNull($hash1); - $this->assertNotNull($hash2); - $this->assertNotNull($hash3); - - $this->assertCount(3, $phones); - - $phone1 = $phones->get($hash1); - $this->assertIsString($phone1); - $phone2 = $phones->get($hash2); - $this->assertIsString($phone2); - $phone3 = $phones->get($hash3); - $this->assertIsInt($phone3); - - $this->assertEquals('phone 2', $phone2); - $this->assertEquals(3, $phone3); - - } - - public function testInstanceCanAddSimpleElementsDynamicKeys(): void - { - $phones = (new Phones)->setDynamicHashKeys(['type', 'val']); - $hash1 = $phones->add('phone 1'); - $hash2 = $phones->add('phone 2'); - $hash3 = $phones->add(3); - - $this->assertIsInt($hash1); - $this->assertIsInt($hash2); - $this->assertIsInt($hash3); - $this->assertNotNull($hash1); - $this->assertNotNull($hash2); - $this->assertNotNull($hash3); - - $this->assertCount(3, $phones); - - $phone1 = $phones->get($hash1); - $this->assertIsString($phone1); - $phone2 = $phones->get($hash2); - $this->assertIsString($phone2); - $phone3 = $phones->get($hash3); - $this->assertIsInt($phone3); - - $this->assertEquals('phone 2', $phone2); - $this->assertEquals(3, $phone3); - - } - - public function testInstanceCanAddSimpleElementsByUniqueKeys(): void - { - $phones = (new Phones)->setUniqueKeyName('id'); - $hash1 = $phones->add('phone 1'); - $hash2 = $phones->add('phone 2'); - $hash3 = $phones->add(3); - - $this->assertIsInt($hash1); - $this->assertIsInt($hash2); - $this->assertIsInt($hash3); - $this->assertNotNull($hash1); - $this->assertNotNull($hash2); - $this->assertNotNull($hash3); - - $this->assertCount(3, $phones); - - $phone1 = $phones->get($hash1); - $this->assertIsString('string', $phone1); - $phone2 = $phones->get($hash2); - $this->assertIsString($phone2); - $phone3 = $phones->get($hash3); - $this->assertIsInt($phone3); - - $this->assertEquals('phone 2', $phone2); - $this->assertEquals(3, $phone3); - - } - -} - -class Phone extends Params -{ -} - -class Phones extends Params -{ - -} 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 new file mode 100644 index 0000000..0da9aad --- /dev/null +++ b/tests/Traits/ConfigurableTest.php @@ -0,0 +1,90 @@ +_fn = $val; + } + + public function setProp2($val) + { + $this->prop2 = $val * 10; + } + }; + + $this->assertNull($cls->prop); + $cls->configurable(['prop' => 'success', 'test' => 'fake', 'fn' => 123, 'prop2' => 10], false); + + $this->assertEquals('success', $cls->prop); + $this->assertEquals(123, $cls->_fn); + $this->assertEquals(100, $cls->prop2); + $this->assertFalse(property_exists($cls, 'test')); + } + + public function testConfigurableThrow(): void + { + $cls = new class () { + use \Php\Support\Traits\ConfigurableTrait; + + public $prop; + }; + + try { + $cls->configurable(['prop' => 'success', 'test' => 'fake']); + } catch (\Throwable $exception) { + $this->assertInstanceOf(InvalidParamException::class, $exception); + $this->assertNull($exception->name); + } + } + + public function testConfigurable2(): void + { + $cls = new ConfigurableTraitTestClass(); + + $this->assertNull($cls->prop); + $cls->configurable(['prop' => 'success', 'test' => 'fake', 'fn' => 123], false); + + $this->assertEquals('success', $cls->prop); + $this->assertEquals(123, $cls->_fn); + $this->assertFalse(property_exists($cls, 'test')); + } +} + +class ConfigurableTraitTestClass +{ + use \Php\Support\Traits\ConfigurableTrait; + + public $prop; + public $_fn; + + + public function propertyExist(string $key): bool + { + return true; + } + + public function setFn($val) + { + $this->_fn = $val; + } +} diff --git a/tests/Traits/ConsolePrintTest.php b/tests/Traits/ConsolePrintTest.php new file mode 100644 index 0000000..3d8300a --- /dev/null +++ b/tests/Traits/ConsolePrintTest.php @@ -0,0 +1,62 @@ +cls()->print($str1); + $this->assertEquals($str1 . PHP_EOL, InterceptFilter::$cache); + + $this->cls()->print($str1, false); + $this->assertEquals($str1, InterceptFilter::$cache); + + $array = [ + 'key' => 'value', + 'int' => 323, + 'float' => 3.12, + ]; + + $this->cls()->print($array, false); + $this->assertEquals(print_r($array, true), InterceptFilter::$cache); + + $this->cls()->print($array); + $this->assertEquals(print_r($array, true) . PHP_EOL, InterceptFilter::$cache); + } + + /* + public function testErrOut(): void + { + stream_filter_register('intercept', InterceptFilter::class); + + stream_filter_append(STDERR, 'intercept'); + + $str1 = 'Error message'; + $this->cls()->printError($str1); + $this->assertEquals($str1 . PHP_EOL, InterceptFilter::$cache); + + $this->cls()->printError($str1, false); + $this->assertEquals($str1, InterceptFilter::$cache); + }*/ + + private function cls() + { + return new class () { + use ConsolePrint; + }; + } +} diff --git a/tests/Traits/InterceptFilter.php b/tests/Traits/InterceptFilter.php new file mode 100644 index 0000000..e8cd607 --- /dev/null +++ b/tests/Traits/InterceptFilter.php @@ -0,0 +1,33 @@ +data; + + $consumed += $bucket->datalen; + stream_bucket_append($out, $bucket); + } + + return PSFS_PASS_ON; + } +} diff --git a/tests/Traits/MakerTest.php b/tests/Traits/MakerTest.php new file mode 100644 index 0000000..b1e96ca --- /dev/null +++ b/tests/Traits/MakerTest.php @@ -0,0 +1,58 @@ +public); + + $instance = MakerArgClassTest::make(23); + static::assertInstanceOf(MakerArgClassTest::class, $instance); + static::assertEquals(23, $instance->public); + } +} + +/** + * Class MakerClassTest + */ +class MakerClassTest +{ + use \Php\Support\Traits\Maker; +} + +/** + * Class MakerArgClassTest + */ +class MakerArgClassTest +{ + use \Php\Support\Traits\Maker; + + public $public; + + /** + * MakerArgClassTest constructor. + * + * @param int $a + */ + public function __construct($a = null) + { + $this->public = $a; + } +} diff --git a/tests/Traits/MetableTest.php b/tests/Traits/MetableTest.php new file mode 100644 index 0000000..3ecaccf --- /dev/null +++ b/tests/Traits/MetableTest.php @@ -0,0 +1,128 @@ + 'val', + '2key' => 'val2', + ]; + $instance->withMeta($meta); + + static::assertEquals($instance->meta(), $meta); + + $meta = [ + 'key' => 'val2', + 'array' => null, + 'key333' => '3', + ]; + + $instance->withMeta($meta); + + + static::assertEquals( + $instance->meta(), + [ + 'key' => 'val2', + 'array' => null, + '2key' => 'val2', + 'key333' => '3', + ] + ); + } + + public function testRecursive(): void + { + $instance = new MetableClassTest(); + + $meta = [ + 'key' => 'val', + 'array' => [ + 'sk' => 1, + 'sk2' => 2, + ], + '2key' => 'val2', + ]; + + $instance->withMeta($meta); + + static::assertEquals($instance->meta(), $meta); + + $meta = [ + 'key' => 'val2', + 'array' => null, + 'array_2' => ['sk' => 1], + 'key333' => '3', + ]; + + $instance->withMeta($meta); + + static::assertEquals( + $instance->meta(), + [ + 'key' => 'val2', + 'array' => null, + '2key' => 'val2', + 'key333' => '3', + 'array_2' => ['sk' => 1], + ] + ); + } + + 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() + ); + } +} + +/** + * Class MetableClassTest + */ +class MetableClassTest +{ + use \Php\Support\Traits\Metable; +} diff --git a/tests/Traits/SingletonTest.php b/tests/Traits/SingletonTest.php new file mode 100644 index 0000000..322f471 --- /dev/null +++ b/tests/Traits/SingletonTest.php @@ -0,0 +1,87 @@ +expectException(\Error::class); + $parent = new SingletonParentClassTest(); + } + + public function testPreventClone(): void + { + $this->expectException(\Error::class); + $instance = SingletonParentClassTest::getInstance(); + $instance2 = clone $instance; + } + + public function testPreventWakeup(): void + { + $this->expectException(Exception::class); + $instance = SingletonChildClassTest::getInstance(); + + $str = serialize($instance); + + unserialize($str); + } +} + +/** + * Class SingletonParentClassTest + */ +class SingletonParentClassTest +{ + protected $username; + + use \Php\Support\Traits\Singleton; +} + + +/** + * Class SingletonChildClassTest + */ +final class SingletonChildClassTest extends SingletonParentClassTest +{ + private $password; + + public function __sleep() + { + return [ + 'username', + 'password', + ]; + } +} diff --git a/tests/Traits/TraitBooterTest.php b/tests/Traits/TraitBooterTest.php new file mode 100644 index 0000000..c56ade5 --- /dev/null +++ b/tests/Traits/TraitBooterTest.php @@ -0,0 +1,61 @@ +bootIfNotBooted(); + // $this->initializeTraits(); + } + }; + self::assertEquals('trait', $class::$type); + } + + public function testInitTrait(): void + { + $class = new class { + use TraitInitializer; + use InitTrait; + + public $title = ''; + + public function __construct() + { + $this->bootIfNotBooted(); + } + }; + self::assertEquals('load initialize from InitTrait', $class->title); + } +} + +trait BootTrait +{ + public static function bootBootTrait() + { + static::$type = 'trait'; + } +} + +trait InitTrait +{ + public function initializeInitTrait() + { + $this->title = 'load initialize from InitTrait'; + } +} 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 10eeb3d..0000000 --- a/tests/exceptions/MissingPropertyTest.php +++ /dev/null @@ -1,49 +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 0a1aac8..0000000 --- a/tests/exceptions/UnknownMethodTest.php +++ /dev/null @@ -1,35 +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 34188b1..0000000 --- a/tests/exceptions/UnknownPropertyTest.php +++ /dev/null @@ -1,35 +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/helpers/ArrTest.php b/tests/helpers/ArrTest.php deleted file mode 100644 index 1136655..0000000 --- a/tests/helpers/ArrTest.php +++ /dev/null @@ -1,123 +0,0 @@ - 'value11', 'key12' => 'value12', 'key13' => 'value13'], - ['key21' => 'value21', 'key22' => 'value22', 'key23' => 'value23'], - ]; - - $result = Arr::applyCls($array, $type); - - /** - * @var int $key - * @var \Php\Support\Components\Params $element - */ - foreach ($result as $key => $element) { - $this->assertInstanceOf($type, $element); - - $this->assertEquals($array[ $key ], $element->toArray()); - } - - $result = Arr::applyCls($array, $type, function ($cls, $data) { - return (new $cls)->fromArray($data); - }); - - /** - * @var int $key - * @var \Php\Support\Components\Params $element - */ - foreach ($result as $key => $element) { - $this->assertInstanceOf($type, $element); - - $this->assertEquals($array[ $key ], $element->toArray()); - } - - } - - - public function testMerge() - { - $array1 = [ - - 'key11' => 'value11', - 'key12' => [ - 'value121', - 'value122' - ], - 'key13' => [ - 'key11' => 'value', - 'key12' => [ - 'value121', 'value122' - ], - 'key13' => 'value13' - ], - - ]; - - $array2 = [ - - 'key11' => 'replace_value11', - 'key12' => [ - 'replace_value121', - 'replace_value122' - ], - 'key13' => [ - 'key11' => 'replace_value', - 'key12' => [ - 'replace_value121', 'replace_value122' - ], - 'key13' => 'replace_value13' - ], - ]; - - $exceptReplace = [ - 'key11' => 'replace_value11', - 'key12' => [ - 'replace_value121', - 'replace_value122' - ], - 'key13' => [ - 'key11' => 'replace_value', - 'key12' => [ - 'replace_value121', 'replace_value122' - ], - 'key13' => 'replace_value13' - ], - ]; - - $exceptAdd = [ - 'key11' => 'replace_value11', - 'key12' => [ - 'value121', - 'value122', - 'replace_value121', - 'replace_value122' - ], - 'key13' => [ - 'key11' => 'replace_value', - 'key12' => [ - 'value121', 'value122', 'replace_value121', 'replace_value122' - ], - 'key13' => 'replace_value13' - ], - ]; - - $result = Arr::merge($array1, $array2); - - $this->assertArraySubset($exceptReplace, $result); - $this->assertEquals($exceptReplace, $result); - - $result_add = Arr::merge($array1, $array2, false); - $this->assertArraySubset($exceptAdd, $result_add); - $this->assertEquals($exceptAdd, $result_add); - } -} diff --git a/tests/helpers/JsonTest.php b/tests/helpers/JsonTest.php deleted file mode 100644 index 42a77fe..0000000 --- a/tests/helpers/JsonTest.php +++ /dev/null @@ -1,159 +0,0 @@ -getMockBuilder('Php\\Support\\Interfaces\\Arrayable')->getMock(); - $dataArrayable->method('toArray')->willReturn([]); - - $actual = Json::encode($dataArrayable); - $this->assertSame('[]', $actual); - // basic data encoding - $data = '1'; - $this->assertSame('"1"', Json::encode($data)); - // simple array encoding - $data = [1, 2]; - $this->assertSame('[1,2]', Json::encode($data)); - $data = ['a' => 1, 'b' => 2]; - $this->assertSame('{"a":1,"b":2}', Json::encode($data)); - // simple object encoding - $data = new \stdClass(); - $data->a = 1; - $data->b = 2; - $this->assertSame('[]', Json::encode($data)); - // empty data encoding - $data = []; - $this->assertSame('[]', Json::encode($data)); - $data = new \stdClass(); - $this->assertSame('[]', Json::encode($data)); - - $data = (object)null; - $this->assertSame('[]', Json::encode($data)); - // JsonSerializable - $data = new JsonModel(); - $this->assertSame('{"json":"serializable"}', Json::encode($data)); - - $data = new JsonModel(); - $data->data = []; - $this->assertSame('[]', Json::encode($data)); - $data = new JsonModel(); - $data->data = (object)null; - $this->assertSame('[]', Json::encode($data)); - } - - public function testHtmlEncode() - { - // HTML escaped chars - $data = '&<>"\'/'; - $this->assertSame('"\u0026\u003C\u003E\u0022\u0027\/"', Json::htmlEncode($data)); - // basic data encoding - $data = '1'; - $this->assertSame('"1"', Json::htmlEncode($data)); - // simple array encoding - $data = [1, 2]; - $this->assertSame('[1,2]', Json::htmlEncode($data)); - $data = ['a' => 1, 'b' => 2]; - $this->assertSame('{"a":1,"b":2}', Json::htmlEncode($data)); - // simple object encoding - $data = new \stdClass(); - $data->a = 1; - $data->b = 2; - $this->assertSame('[]', Json::htmlEncode($data)); - - $data = (object)null; - $this->assertSame('[]', Json::htmlEncode($data)); - // JsonSerializable - $data = new JsonModel(); - $this->assertSame('{"json":"serializable"}', Json::htmlEncode($data)); - -// $postsStack = new \SplStack(); -// $postsStack->push(new Post(915, 'record1')); -// $postsStack->push(new Post(456, 'record2')); -// $this->assertSame('{"1":{"id":456,"title":"record2"},"0":{"id":915,"title":"record1"}}', Json::encode($postsStack)); - } - - public function testDecode() - { - // empty value - $json = ''; - $actual = Json::decode($json); - $this->assertNull($actual); - // basic data decoding - $json = '"1"'; - $this->assertSame('1', Json::decode($json)); - // array decoding - $json = '{"a":1,"b":2}'; - $this->assertSame(['a' => 1, 'b' => 2], Json::decode($json)); - // exception - $json = '{"a":1,"b":2'; - $this->expectException('Php\Support\Exceptions\InvalidArgumentException'); - Json::decode($json); - } - - /** - * @expectedException \Php\Support\Exceptions\InvalidArgumentException - * @expectedExceptionMessage Syntax error. - */ - public function testDecodeInvalidParamException() - { - Json::decode('sa'); - } - - public function testHandleJsonError() - { - // Basic syntax error - try { - $json = "{'a': '1'}"; - Json::decode($json); - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\InvalidArgumentException::class, $e); - $this->assertSame(Json::$jsonErrorMessages['JSON_ERROR_SYNTAX'], $e->getMessage()); - } - // Unsupported type since PHP 5.5 - try { - $fp = fopen('php://stdin', 'r'); - $data = ['a' => $fp]; - Json::encode($data); - fclose($fp); - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\InvalidArgumentException::class, $e); - if (PHP_VERSION_ID >= 50500) { - $this->assertSame(Json::$jsonErrorMessages['JSON_ERROR_UNSUPPORTED_TYPE'], $e->getMessage()); - } else { - $this->assertSame(Json::$jsonErrorMessages['JSON_ERROR_SYNTAX'], $e->getMessage()); - } - } - } - - -} - -class JsonModel implements \JsonSerializable -{ - public $data = ['json' => 'serializable']; - - public function jsonSerialize() - { - return $this->data; - } - - public function rules() - { - return [ - ['name', 'required'], - ['name', 'string', 'max' => 100] - ]; - } - - public function init() - { - - } -} diff --git a/tests/helpers/JwtParserTest.php b/tests/helpers/JwtParserTest.php deleted file mode 100644 index 4261153..0000000 --- a/tests/helpers/JwtParserTest.php +++ /dev/null @@ -1,54 +0,0 @@ -assertInstanceOf(\Php\Support\Entities\JwtToken::class, $token); - - $this->assertEquals('HS256', $token->getHeader('alg')); - $this->assertEquals(2, $token->getClaim('sub')); - $this->assertEquals('IVANOV', $token->getClaim('username')); - $this->assertEquals('Иванов Иван Иванович', $token->getClaim('name')); - } - - public function testParseNullToken() - { - $this->expectException(\Php\Support\Exceptions\InvalidArgumentException::class); - $this->expectExceptionMessage('The JWT string must have two dots'); - JwtParser::parseToken(''); - } - - public function testParseFailClaimToken() - { - $this->expectException(\Php\Support\Exceptions\InvalidArgumentException::class); - $this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded'); - JwtParser::parseToken('eyJraWQiOiJvY3QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyIaXNzIjoia4xLyIsIm5pY2tuYW1lIjoiSVZBTk9WIiwiZXhwIM4NDU0MDcsImp0aSI6ImQyNzRiNTljLWIzODYtNDQ2MS1hZGM1LWRkNjI2Mzk5ZjBhYiIsInBob30L3QsNGH0LXQvdCwIiwiZmFtaWx5X25hbWUiOiLQmNCy0LDQvdC-0LIiLCJwb3NpdGlvbl9pZCI6MCwidXNlcm5hbWUiOiJJVkFOT1YifQ.GFnYCTAznSuRmoI'); - } - - public function testParseFailHeaderToken() - { - $this->expectException(\Php\Support\Exceptions\InvalidArgumentException::class); - $this->expectExceptionMessage('Malformed UTF-8 characters, possibly incorrectly encoded'); - JwtParser::parseToken('raWQiOiJviJI32323UzI1N.eyJzdWIiOiIyIaXNzIjoia4xLyIsIm5pY2tuYW1lIjoiSVZBTk9WIiwiZXhwIM4NDU0MDcsImp0aSI6ImQyNzRiNTljLWIzODYtNDQ2MS1hZGM1LWRkNjI2Mzk5ZjBhYiIsInBob30L3QsNGH0LXQvdCwIiwiZmFtaWx5X25hbWUiOiLQmNCy0LDQvdC-0LIiLCJwb3NpdGlvbl9pZCI6MCwidXNlcm5hbWUiOiJJVkFOT1YifQ.GFnYCTAznSuRmoI'); - } - -} diff --git a/tests/interfaces/ArrayableTest.php b/tests/interfaces/ArrayableTest.php deleted file mode 100644 index 54915bc..0000000 --- a/tests/interfaces/ArrayableTest.php +++ /dev/null @@ -1,25 +0,0 @@ -getMockBuilder(\Php\Support\Interfaces\Arrayable::class)->getMock(); - $dataArrayable->method('toArray')->willReturn(['key' => 'value']); - - $actual = \Php\Support\Helpers\Json::encode($dataArrayable); - $this->assertSame('{"key":"value"}', $actual); - - $param = new \Php\Support\Components\BaseParams; - $param->fromArray($dataArrayable->toArray()); - - $this->assertSame('{"key":"value"}', $param->toJson()); - - } - - -} diff --git a/tests/traits/ConfigurableTest.php b/tests/traits/ConfigurableTest.php deleted file mode 100644 index a75a11a..0000000 --- a/tests/traits/ConfigurableTest.php +++ /dev/null @@ -1,32 +0,0 @@ -assertNull($cls->prop); - $cls->configurable(['prop' => 'success', 'test' => 'fake'], false); - - $this->assertEquals('success', $cls->prop); - $this->assertFalse(property_exists($cls, 'test')); - - try { - $cls->configurable(['prop' => 'success', 'test' => 'fake']); - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\InvalidParamException::class, $e); - $this->assertNull($e->getParam()); - } - - } - -} diff --git a/tests/traits/ConsolePrintTest.php b/tests/traits/ConsolePrintTest.php deleted file mode 100644 index cddf326..0000000 --- a/tests/traits/ConsolePrintTest.php +++ /dev/null @@ -1,77 +0,0 @@ -cls()->print($str1); - $this->assertEquals($str1 . PHP_EOL, Intercept::$cache); - - $this->cls()->print($str1, false); - $this->assertEquals($str1, Intercept::$cache); - - $array = [ - 'key' => 'value', - 'int' => 323, - 'float' => 3.12, - ]; - - $this->cls()->print($array, false); - $this->assertEquals(print_r($array, true), Intercept::$cache); - - $this->cls()->print($array); - $this->assertEquals(print_r($array, true) . PHP_EOL, Intercept::$cache); - } - - public function testErrOut(): void - { - stream_filter_register("intercept", "Intercept"); - - stream_filter_append(STDERR, "intercept"); - - $str1 = 'Error message'; - $this->cls()->print($str1); - $this->assertEquals($str1 . PHP_EOL, Intercept::$cache); - - $this->cls()->print($str1, false); - $this->assertEquals($str1, Intercept::$cache); - } - - private function cls() - { - return new class() - { - use \Php\Support\Traits\ConsolePrint; - }; - } -} - -/** - * Class Intercept - */ -class Intercept extends \php_user_filter -{ - public static $cache = ''; - - public function filter($in, $out, &$consumed, $closing) - { - while ($bucket = stream_bucket_make_writeable($in)) { - self::$cache = $bucket->data; - $consumed += $bucket->datalen; - stream_bucket_append($out, $bucket); - } - - return PSFS_PASS_ON; - } -} \ No newline at end of file diff --git a/tests/traits/GetterTest.php b/tests/traits/GetterTest.php deleted file mode 100644 index 3df267a..0000000 --- a/tests/traits/GetterTest.php +++ /dev/null @@ -1,47 +0,0 @@ -prop; - } - - public function setWriteOnly($val) - { - $this->prop = $val; - } - }; - - $this->assertEquals('secret', $cls->keyName); - $this->assertTrue(isset($cls->keyName)); - $this->assertFalse(isset($cls->val)); - $this->assertEquals('getJackDaniels', $cls::getter('jackDaniels')); - - try { - $this->assertEquals('success', $cls->writeOnly); - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\InvalidCallException::class, $e); - } - - try { - $val = $cls->valName; - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\UnknownPropertyException::class, $e); - } - - - } - -} diff --git a/tests/traits/SetterTest.php b/tests/traits/SetterTest.php deleted file mode 100644 index 3be45c3..0000000 --- a/tests/traits/SetterTest.php +++ /dev/null @@ -1,78 +0,0 @@ -prop; - } - - public function setWriteOnly($val) - { - $this->prop = $val; - } - }; - } - - public function testInstance(): void - { - $cls = $this->cls(); - - $this->assertEquals('secret', $cls->getKeyName()); - $cls->writeOnly = 'test'; - $this->assertEquals('test', $cls->getKeyName()); - - $this->assertEquals('setJackDaniels', $cls::setter('jackDaniels')); - } - - - public function testInvalidCallException(): void - { - $cls = $this->cls(); - - try { - $cls->keyName = 'test'; - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\InvalidCallException::class, $e); - } - } - - public function testUnknownPropertyException(): void - { - $cls = $this->cls(); - - try { - $cls->valName = 'test'; - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\UnknownPropertyException::class, $e); - } - } - - public function testUnset(): void - { - $cls = $this->cls(); - - unset($cls->writeOnly); - - try { - unset($cls->keyName); - } catch (\Throwable $e) { - $this->assertInstanceOf(Php\Support\Exceptions\InvalidCallException::class, $e); - } - - - } - -} diff --git a/tests/types/PointTest.php b/tests/types/PointTest.php deleted file mode 100644 index f041292..0000000 --- a/tests/types/PointTest.php +++ /dev/null @@ -1,52 +0,0 @@ -assertInstanceOf(Point::class, new Point(1, 2)); - - $listInvalid = [ - ['', 12], - ['', ''], - ['2', ''], - ['s', 'sts'], - ]; - foreach ($listInvalid as $item) { - $p = new Point($item[0], $item[1]); - $this->assertInstanceOf(Point::class, $p); - $this->assertTrue($p->isEmpty()); - } - } - - public function testGetAndSetFromDB(): void - { - $dbStrings = [ - '(54.94114600000000,63.57531800000000)', - '(54.941146,63.575318)', - // '(54.941,63.575)', - ]; - - foreach ($dbStrings as $dbString) { - $point = Point::fromDB($dbString); - $this->assertInstanceOf(Point::class, $point); - - $this->assertEquals($point->longitude, 54.94114600000000); - $this->assertEquals($point->latitude, 63.57531800000000); - - $this->assertIsFloat($point->latitude); - $this->assertIsFloat($point->longitude); - -// $this->assertEquals($dbString, $point->toDB()); - } - - } - -}