diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..7b4b342
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,28 @@
+{
+ "name": "syntatis/php-utils",
+ "image": "ghcr.io/syntatis/php:7.4-fpm",
+ "customizations": {
+ "vscode": {
+ "settings": {
+ "php.validate.executablePath": "/usr/local/bin/php",
+ "terminal.integrated.profiles.linux": {
+ "bash": {
+ "path": "/bin/bash",
+ "args": [
+ "-l"
+ ]
+ }
+ },
+ "terminal.integrated.defaultProfile.linux": "bash"
+ },
+ "extensions": [
+ "bmewburn.vscode-intelephense-client",
+ "christian-kohler.path-intellisense",
+ "editorConfig.editorConfig",
+ "neilbrayfield.php-docblocker",
+ "SanderRonde.phpstan-vscode",
+ "wongjn.php-sniffer"
+ ]
+ }
+ }
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..03c9d82
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,17 @@
+# editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = tab
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.{json,neon,neon.dist,yml,md}]
+indent_size = 2
+
+[*.{yml,md,neon,neon.dist}]
+indent_style = space
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..b5bfc95
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+/.devcontainer export-ignore
+/.editorconfig export-ignore
+/.gitattributes export-ignore
+/.github export-ignore
+/.gitignore export-ignore
+/.vscode export-ignore
+/docs export-ignore
+/phpstan.neon.dist export-ignore
+/phpcs.xml.dist export-ignore
+/phpunit.xml.dist export-ignore
+/tests export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..c24ad21
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "monthly"
+
+ - package-ecosystem: "composer"
+ versioning-strategy: increase
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "sunday"
+ groups:
+ composer-require:
+ dependency-type: "production"
+ update-types:
+ - "minor"
+ - "patch"
+ composer-require-dev:
+ dependency-type: "development"
+ update-types:
+ - "minor"
+ - "patch"
diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
new file mode 100644
index 0000000..385cb35
--- /dev/null
+++ b/.github/workflows/php.yml
@@ -0,0 +1,84 @@
+name: php
+
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - main
+ paths:
+ - ".github/workflows/php.yml"
+ - "**.php"
+ - "composer.json"
+ - "composer.lock"
+ - "phpcs.xml.dist"
+ - "phpstan.neon.dist"
+ - "phpunit.xml.dist"
+ push:
+ branches:
+ - main
+ paths:
+ - ".github/workflows/php.yml"
+ - "**.php"
+ - "composer.json"
+ - "composer.lock"
+ - "phpcs.xml.dist"
+ - "phpstan.neon.dist"
+ - "phpunit.xml.dist"
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 7.4
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-ansi --no-interaction --no-progress
+
+ - name: Run linter
+ run: composer phpcs
+
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ version: [7.4, 8.0, 8.1, 8.2]
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Setup Composer cache
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: composer-${{ matrix.version }}-${{ hashFiles('**/composer.json') }}
+ restore-keys: php-${{ matrix.version }}-composer-
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.version }}
+ tools: composer:v2
+
+ - name: Install dependencies
+ run: composer update --prefer-dist --no-ansi --no-interaction --no-progress
+
+ - name: Run test
+ run: |
+ composer phpstan
+ vendor/bin/phpunit --testdox --coverage-clover coverage.xml
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v3
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4df777d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+#OS
+.DS_*
+
+# Composer
+/vendor
+composer.lock
+auth.json
+
+# Log, cache, and tmp files
+*.cache
+*.log
+*.meta
+tmp/
+
+# Dotenv
+.env.*
+!.env
+
+# Tests #
+#############
+artifacts/
+phpunit.xml
+phpstan.neon
+phpcs.xml
+
+# Docker
+docker-composer.override.yml
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..887c373
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+ "recommendations": [
+ "bmewburn.vscode-intelephense-client",
+ "christian-kohler.path-intellisense",
+ "editorconfig.editorconfig",
+ "kasik96.latte",
+ "mikestead.dotenv",
+ "ms-vscode-remote.remote-containers",
+ "neilbrayfield.php-docblocker",
+ "sanderronde.phpstan-vscode",
+ "wmaurer.change-case",
+ "dotjoshjohnson.xml"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..f4731d4
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,103 @@
+{
+ "intelephense.stubs": [
+ "apache",
+ "bcmath",
+ "bz2",
+ "calendar",
+ "com_dotnet",
+ "Core",
+ "ctype",
+ "curl",
+ "date",
+ "dba",
+ "dom",
+ "enchant",
+ "exif",
+ "FFI",
+ "fileinfo",
+ "filter",
+ "fpm",
+ "ftp",
+ "gd",
+ "gettext",
+ "gmp",
+ "hash",
+ "iconv",
+ "imap",
+ "intl",
+ "json",
+ "ldap",
+ "libxml",
+ "mbstring",
+ "meta",
+ "mysqli",
+ "oci8",
+ "odbc",
+ "openssl",
+ "pcntl",
+ "pcre",
+ "pdo_ibm",
+ "pdo_mysql",
+ "pdo_pgsql",
+ "pdo_sqlite",
+ "PDO",
+ "pgsql",
+ "Phar",
+ "posix",
+ "pspell",
+ "readline",
+ "redis",
+ "Reflection",
+ "session",
+ "shmop",
+ "SimpleXML",
+ "snmp",
+ "soap",
+ "sockets",
+ "sodium",
+ "SPL",
+ "sqlite3",
+ "standard",
+ "superglobals",
+ "sysvmsg",
+ "sysvsem",
+ "sysvshm",
+ "tidy",
+ "tokenizer",
+ "wordpress",
+ "xml",
+ "xmlreader",
+ "xmlrpc",
+ "xmlwriter",
+ "xsl",
+ "Zend OPcache",
+ "zip",
+ "zlib"
+ ],
+ "[json]": {
+ "editor.defaultFormatter": "vscode.json-language-features"
+ },
+ "[php]": {
+ "editor.defaultFormatter": "wongjn.php-sniffer"
+ },
+ "files.exclude": {
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true
+ },
+ "search.exclude": {
+ "**/dist": true
+ },
+ "intelephense.files.exclude": [
+ "**/.git/**",
+ "**/.svn/**",
+ "**/.hg/**",
+ "**/.DS_Store/**",
+ "**/.history/**",
+ "**/CVS/**"
+ ],
+ "phpSniffer.autoDetect": true,
+ "phpSniffer.run": "onSave"
+}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..ddb2790
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,132 @@
+# Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+ community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+ any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+ without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official email address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..2a8e318
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Syntatis
+
+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/README.md b/README.md
new file mode 100644
index 0000000..f8a3e49
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+
+
📦 utils-php
+
Handy functions for PHP
+
+ [![php](https://github.com/syntatis/utils-php/actions/workflows/php.yml/badge.svg)](https://github.com/syntatis/utils-php/actions/workflows/php.yml) [![codecov](https://codecov.io/gh/syntatis/utils-php/graph/badge.svg?token=1DJG88CULL)](https://codecov.io/gh/syntatis/utils-php) ![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/syntatis/utils/php)
+
+
+---
+
+## Installation
+
+```bash
+composer require syntatis/utils
+```
+
+## Usage
+
+This PHP package includes a number of functions to perform common value validation, such as if a value is an email, URL, or not being blank.
+
+| Function | Description |
+| --- | --- |
+| `is_blank` | Validates whether a value is blank or empty. |
+| `is_email` | Validates whether a value is a valid email address. |
+| `is_url` | Validates whether a value is a valid URL. |
+| `is_uuid` | Validates whether a value is a valid UUID. |
+| `is_semver` | Validates whether a value is a valid SemVer format. |
+| `is_ip_address` | Validates whether a value is a valid IPv4 or IPv6 address. |
+| `is_unique` | Validates that all elements in the provided collection are unique. |
+
+For examples:
+
+```php
+use function Syntatis\Utils\is_blank;
+use function Syntatis\Utils\is_email;
+
+// Whether a value is blank or empty.
+is_blank(''); // `true`.
+is_blank(' '); // `true`.
+is_blank('foo '); // `false`.
+```
+
+For other functions and examples, please refer to the [Wiki](https://github.com/syntatis/utils-php/wiki).
diff --git a/app/CaseConverter/CaseConverter.php b/app/CaseConverter/CaseConverter.php
new file mode 100644
index 0000000..374f93b
--- /dev/null
+++ b/app/CaseConverter/CaseConverter.php
@@ -0,0 +1,23 @@
+convert($value)->toCamel();
+}
+
+/**
+ * Convert a string to "kebab-case" format.
+ */
+function kebabcased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toKebab();
+}
+
+/**
+ * Convert a string to "snake_case" format.
+ */
+function snakecased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toSnake();
+}
+
+/**
+ * Convert a string to "PascalCase" format.
+ */
+function pascalcased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toPascal();
+}
+
+/**
+ * Convert a string to "Title case" format.
+ */
+function titlecased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toTitle();
+}
+
+/**
+ * Convert a string to "lowercased" format.
+ */
+function lowercased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toLower();
+}
+
+/**
+ * Convert a string to "UPPERCASE" format.
+ */
+function uppercased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toUpper();
+}
+
+/**
+ * Convert a string to "MACRO_CASED" format.
+ */
+function macrocased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toMacro();
+}
+
+/**
+ * Convert a string to "COBOL-CASED" format.
+ */
+function cobolcased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toCobol();
+}
+
+/**
+ * Convert a string to "Sentence case" format.
+ */
+function sentencecased(string $value): string
+{
+ return CaseConverter::instance()->convert($value)->toSentence();
+}
diff --git a/app/Validator/Validator.php b/app/Validator/Validator.php
new file mode 100644
index 0000000..9fe34fa
--- /dev/null
+++ b/app/Validator/Validator.php
@@ -0,0 +1,24 @@
+validate(
+ $value,
+ new Sequentially(
+ [
+ new NotBlank(null, null, null, 'trim'),
+ new Email(null, null, Email::VALIDATION_MODE_STRICT),
+ ],
+ ),
+ )->count() <= 0;
+}
+
+/**
+ * Validates that a value is a valid Universally unique identifier (UUID) per RFC 4122
+ * {@link https://www.rfc-editor.org/rfc/rfc4122}
+ *
+ * @param mixed $value
+ * @param array|null $versions
+ *
+ * @phpstan-param array>|null $versions
+ */
+function is_uuid($value, ?array $versions = null): bool
+{
+ return Validator::instance()
+ ->validate(
+ $value,
+ new Uuid(
+ null,
+ null,
+ $versions, // Versions.
+ true, // Stricts.
+ ),
+ )->count() <= 0;
+}
+
+/**
+ * @param mixed ...$value
+ *
+ * @phpstan-assert-if-true ''|array{}|false|null $value
+ */
+function is_blank(...$value): bool
+{
+ foreach ($value as $k => $v) {
+ if (Validator::instance()->validate($v, new NotBlank(null, null, null, 'trim'))->count() >= 1) {
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @param mixed $value
+ * @param array $protocols
+ *
+ * @phpstan-assert-if-true non-empty-string $value
+ */
+function is_url($value, array $protocols = ['http', 'https']): bool
+{
+ return Validator::instance()->validate(
+ $value,
+ [
+ new Sequentially(
+ [
+ new NotBlank(null, null, null, 'trim'),
+ new Url(null, null, $protocols, null, 'trim'),
+ ],
+ ),
+ ],
+ )->count() <= 0;
+}
+
+/**
+ * @param mixed $value
+ *
+ * @phpstan-assert-if-true non-empty-string $value
+ */
+function is_semver($value): bool
+{
+ return Validator::instance()->validate(
+ $value,
+ new Regex(
+ /** @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string */
+ '/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/',
+ ),
+ )->count() <= 0;
+}
+
+/**
+ * Validate if the value is a valid IPv4 or IPv6 address.
+ *
+ * @param mixed $value
+ *
+ * @phpstan-assert-if-true non-empty-string $value
+ */
+function is_ip_address($value): bool
+{
+ return Validator::instance()->validate(
+ $value,
+ new Sequentially(
+ [
+ new NotBlank(null, null, null, 'trim'),
+ new Ip([
+ 'normalizer' => 'trim',
+ 'version' => Ip::ALL,
+ ]),
+ ],
+ ),
+ )->count() <= 0;
+}
+
+/**
+ * Validates that all elements in the provided collection are unique, employing
+ * strict comparison by default (treating '7' and 7 as distinct elements).
+ *
+ * @param mixed $value
+ * @param string|array $fields Specifies the key or keys in a collection
+ * to be examined for uniqueness.
+ * Required PHP 8.1 or higher.
+ *
+ * @phpstan-assert-if-true non-empty-array $value
+ */
+function is_unique($value, $fields = []): bool
+{
+ $notBlank = new NotBlank(null, null, null);
+
+ if (PHP_VERSION_ID < 80100) {
+ return Validator::instance()->validate(
+ $value,
+ new Sequentially([
+ $notBlank,
+ new Unique(),
+ ]),
+ )->count() <= 0;
+ }
+
+ return Validator::instance()->validate(
+ $value,
+ new Sequentially([
+ $notBlank,
+ new Unique(['fields' => $fields]),
+ ]),
+ )->count() <= 0;
+}
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..3f356b0
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,3 @@
+comment:
+ layout: "condensed_header, condensed_files, condensed_footer"
+ hide_project_coverage: true
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..005ef56
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,84 @@
+{
+ "name": "syntatis/utils",
+ "license": "MIT",
+ "description": "Handy functions for PHP",
+ "keywords": [
+ "utilities",
+ "validator",
+ "case-converter"
+ ],
+ "homepage": "https://github.com/syntatis/utils-php#README",
+ "authors": [
+ {
+ "name": "Thoriq Firdaus",
+ "homepage": "https://github.com/tfirdaus",
+ "role": "Developer"
+ }
+ ],
+ "config": {
+ "allow-plugins": {
+ "composer/installers": true,
+ "dealerdirect/phpcodesniffer-composer-installer": true,
+ "phpstan/extension-installer": true
+ },
+ "cache-dir": "tmp/composer/cache",
+ "preferred-install": "dist",
+ "process-timeout": 0,
+ "sort-packages": true
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "autoload": {
+ "files": [
+ "app/CaseConverter/functions.php",
+ "app/Validator/functions.php"
+ ],
+ "psr-4": {
+ "Syntatis\\Utils\\": "app"
+ }
+ },
+ "autoload-dev": {
+ "files": [
+ "app/CaseConverter/functions.php",
+ "app/Validator/functions.php"
+ ],
+ "psr-4": {
+ "Syntatis\\Utils\\Tests\\": "tests"
+ }
+ },
+ "scripts": {
+ "phpcs:fix": "vendor/bin/phpcbf",
+ "phpcs": "vendor/bin/phpcs",
+ "phpstan": "vendor/bin/phpstan analyse --xdebug",
+ "phpunit:coverage": [
+ "@putenv XDEBUG_MODE=coverage",
+ "vendor/bin/phpunit"
+ ],
+ "phpunit": "vendor/bin/phpunit --no-coverage",
+ "test": [
+ "@phpcs",
+ "@phpstan",
+ "@phpunit"
+ ]
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "egulias/email-validator": "^3.2 || ^4.0",
+ "jawira/case-converter": "^3.5",
+ "symfony/validator": "^5.4 || ^6.1 || ^7.0"
+ },
+ "require-dev": {
+ "composer/installers": "^2.2",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/extension-installer": "^1.3",
+ "phpstan/phpstan": "^1.10",
+ "phpstan/phpstan-deprecation-rules": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpstan/phpstan-strict-rules": "^1.5",
+ "phpunit/phpunit": "^9.6",
+ "squizlabs/php_codesniffer": "^3.8",
+ "symfony/var-dumper": "^5.4 || ^6.1 || ^7.0",
+ "syntatis/coding-standard": "^1.0"
+ }
+}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
new file mode 100644
index 0000000..24f1567
--- /dev/null
+++ b/phpcs.xml.dist
@@ -0,0 +1,39 @@
+
+
+ PHP Coding Standard
+
+
+
+
+
+
+
+
+ ./app/
+ ./tests/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ /cache/
+
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 0000000..3ad371d
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,4 @@
+parameters:
+ level: 9
+ paths:
+ - app
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..61e22f3
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,22 @@
+
+
+
+
+ ./tests/
+
+
+
+
+ ./app/
+
+
+
+
+
+
diff --git a/tests/CaseConverterTest.php b/tests/CaseConverterTest.php
new file mode 100644
index 0000000..83f28e9
--- /dev/null
+++ b/tests/CaseConverterTest.php
@@ -0,0 +1,163 @@
+assertEquals($expect, camelcased($value));
+ }
+
+ /**
+ * @dataProvider dataKebabCased
+ * @testdox it can convert string to camelcase
+ */
+ public function testKebabCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, kebabcased($value));
+ }
+
+ /**
+ * @dataProvider dataSnakeCased
+ * @testdox it can convert string to snakecase
+ */
+ public function testSnakeCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, snakecased($value));
+ }
+
+ /**
+ * @dataProvider dataPascalCased
+ * @testdox it can convert string to pascalcase
+ */
+ public function testPascalCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, pascalcased($value));
+ }
+
+ /**
+ * @dataProvider dataTitleCased
+ * @testdox it can convert string to titlecase
+ */
+ public function testTitleCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, titlecased($value));
+ }
+
+ /**
+ * @dataProvider dataLowerCased
+ * @testdox it can convert string to lowercase
+ */
+ public function testLowerCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, lowercased($value));
+ }
+
+ /**
+ * @dataProvider dataUpperCased
+ * @testdox it can convert string to uppercase
+ */
+ public function testUpperCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, uppercased($value));
+ }
+
+ /**
+ * @dataProvider dataMacroCased
+ * @testdox it can convert string to macrocase
+ */
+ public function testMacroCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, macrocased($value));
+ }
+
+ /**
+ * @dataProvider dataCobolCased
+ * @testdox it can convert string to cobolcase
+ */
+ public function testCobolCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, cobolcased($value));
+ }
+
+ /**
+ * @dataProvider dataSentenceCased
+ * @testdox it can convert string to sentencecase
+ */
+ public function testSentenceCased(string $value, string $expect): void
+ {
+ $this->assertEquals($expect, sentencecased($value));
+ }
+
+ public function dataCamelCased(): iterable
+ {
+ return [['foo_bar', 'fooBar'], ['foo-bar', 'fooBar'], ['foo bar', 'fooBar'], ['fooBar', 'fooBar'], ['FooBar', 'fooBar']];
+ }
+
+ public function dataKebabCased(): iterable
+ {
+ return [['foo_bar', 'foo-bar'], ['foo-bar', 'foo-bar'], ['foo bar', 'foo-bar'], ['fooBar', 'foo-bar'], ['FooBar', 'foo-bar']];
+ }
+
+ public function dataSnakeCased(): iterable
+ {
+ return [['foo_bar', 'foo_bar'], ['foo-bar', 'foo_bar'], ['foo bar', 'foo_bar'], ['fooBar', 'foo_bar'], ['FooBar', 'foo_bar']];
+ }
+
+ public function dataPascalCased(): iterable
+ {
+ return [['foo_bar', 'FooBar'], ['foo-bar', 'FooBar'], ['foo bar', 'FooBar'], ['fooBar', 'FooBar'], ['FooBar', 'FooBar']];
+ }
+
+ public function dataTitleCased(): iterable
+ {
+ return [['foo_bar', 'Foo Bar'], ['foo-bar', 'Foo Bar'], ['foo bar', 'Foo Bar'], ['fooBar', 'Foo Bar'], ['FooBar', 'Foo Bar']];
+ }
+
+ public function dataLowerCased(): iterable
+ {
+ return [['foo_bar', 'foo bar'], ['foo-bar', 'foo bar'], ['foo bar', 'foo bar'], ['fooBar', 'foo bar'], ['FooBar', 'foo bar']];
+ }
+
+ public function dataUpperCased(): iterable
+ {
+ return [['foo_bar', 'FOO BAR'], ['foo-bar', 'FOO BAR'], ['foo bar', 'FOO BAR'], ['fooBar', 'FOO BAR'], ['FooBar', 'FOO BAR']];
+ }
+
+ public function dataMacroCased(): iterable
+ {
+ return [['foo_bar', 'FOO_BAR'], ['foo-bar', 'FOO_BAR'], ['foo bar', 'FOO_BAR'], ['fooBar', 'FOO_BAR'], ['FooBar', 'FOO_BAR']];
+ }
+
+ public function dataCobolCased(): iterable
+ {
+ return [['foo_bar', 'FOO-BAR'], ['foo-bar', 'FOO-BAR'], ['foo bar', 'FOO-BAR'], ['fooBar', 'FOO-BAR'], ['FooBar', 'FOO-BAR']];
+ }
+
+ public function dataSentenceCased(): iterable
+ {
+ return [['foo_bar', 'Foo bar'], ['foo-bar', 'Foo bar'], ['foo bar', 'Foo bar'], ['fooBar', 'Foo bar'], ['FooBar', 'Foo bar']];
+ }
+}
diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php
new file mode 100644
index 0000000..71a3f8d
--- /dev/null
+++ b/tests/ValidatorTest.php
@@ -0,0 +1,698 @@
+assertTrue(is_email($value));
+ }
+
+ /**
+ * @dataProvider dataIsEmailInvalid
+ * @testdox it can validate a invalid email
+ *
+ * @param mixed $value
+ */
+ public function testIsEmailInvalid($value): void
+ {
+ $this->assertFalse(is_email($value));
+ }
+
+ /**
+ * @dataProvider dataIsURLValid
+ * @testdox it can validate a valid url
+ *
+ * @param mixed $value
+ */
+ public function testIsURLValid($value): void
+ {
+ $this->assertTrue(is_url($value));
+ }
+
+ /**
+ * @dataProvider dataIsURLInvalid
+ * @testdox it can validate an invalid url
+ *
+ * @param mixed $value
+ */
+ public function testIsURLInvalid($value): void
+ {
+ $this->assertFalse(is_url($value));
+ }
+
+ /**
+ * @dataProvider dataIsBlank
+ * @testdox it can validate a blank value
+ *
+ * @param mixed $value
+ */
+ public function testIsBlank($value): void
+ {
+ $this->assertTrue(is_blank($value));
+ }
+
+ /** @testdox it can validate a multiple blank value */
+ public function testIsBlankMultiple(): void
+ {
+ $this->assertTrue(is_blank([], '', ' ', false, null));
+ $this->assertFalse(is_blank([], 'this is not blank', ' ', false, null));
+ }
+
+ /**
+ * @dataProvider dataIsNotBlank
+ * @testdox it can validate that is a not blank value
+ *
+ * @param mixed $value
+ */
+ public function testIsNotBlank($value): void
+ {
+ $this->assertFalse(is_blank($value));
+ }
+
+ /**
+ * @dataProvider dataIsUUID
+ * @testdox it can validate a uuid value
+ *
+ * @param mixed $value
+ */
+ public function testIsUUID($value): void
+ {
+ $this->assertTrue(is_uuid($value));
+ }
+
+ /**
+ * @dataProvider dataIsSemver
+ * @testdox it can validate a semver value
+ *
+ * @param mixed $value
+ */
+ public function testIsSemver($value): void
+ {
+ $this->assertTrue(is_semver($value));
+ }
+
+ /**
+ * @dataProvider dataIsNotSemver
+ * @testdox it can validate an invalid semver value
+ *
+ * @param mixed $value
+ */
+ public function testIsNotSemver($value): void
+ {
+ $this->assertFalse(is_semver($value));
+ }
+
+ /**
+ * @dataProvider dataIsIpAddress
+ * @testdox it can validate a valid ip address
+ *
+ * @param mixed $value
+ */
+ public function testIsIpAddress($value): void
+ {
+ $this->assertTrue(is_ip_address($value));
+ }
+
+ /**
+ * @dataProvider dataIsNotIpAddress
+ * @testdox it can validate a invalid ip address
+ *
+ * @param mixed $value
+ */
+ public function testIsNotIpAddress($value): void
+ {
+ $this->assertFalse(is_ip_address($value));
+ }
+
+ /**
+ * @dataProvider dataIsUnique
+ * @testdox it can validate unique collection
+ *
+ * @param mixed $value
+ */
+ public function testIsUnique($value): void
+ {
+ $this->assertTrue(is_unique($value));
+ }
+
+ /**
+ * @dataProvider dataIsUniqueOptionFields
+ * @requires PHP 8.1
+ * @testdox it can validate unique collection
+ *
+ * @param mixed $value
+ * @param string|array $fields The list of fields to check uniqueness.
+ */
+ public function testIsUniqueOptionFields($value, $fields = []): void
+ {
+ $this->assertTrue(is_unique($value, $fields));
+ }
+
+ /**
+ * @dataProvider dataIsNotUnique
+ * @testdox it can validate non-unique collection
+ *
+ * @param mixed $value
+ */
+ public function testIsNotUnique($value): void
+ {
+ $this->assertFalse(is_unique($value));
+ }
+
+ public static function dataIsEmailValid(): array
+ {
+ return [
+ ['â@iana.org'],
+ ['fabien@symfony.com'],
+ ['example@example.co.uk'],
+ ['fabien_potencier@example.fr'],
+ ['fab\'ien@symfony.com'],
+ ['fab\ ien@symfony.com'],
+ ['fabien+a@symfony.com'],
+ ['exampl=e@example.com'],
+ ['инфо@письмо.рф'],
+ ['müller@möller.de'],
+ ['1500111@профи-инвест.рф'],
+ [sprintf('example@%s.com', str_repeat('ъ', 40))],
+ ];
+ }
+
+ /** @see https://github.com/symfony/validator/blob/46c4193ec1eefdd7f9568261c685450d6f3b32c9/Tests/Constraints/EmailValidatorTest.php#L342 */
+ public static function dataIsEmailInvalid(): array
+ {
+ return [
+ [' '],
+ [''],
+ [null],
+ ['"""@iana.org'],
+ ['""@iana.org'],
+ ['"\""@iana.org'],
+ ['"\"@iana.org'],
+ ['"\a"@iana.org'],
+ ['"test""test"@iana.org'],
+ ['"test"' . chr(0) . '@iana.org'],
+ ['"test"."test"@iana.org'],
+ ['"test".test@iana.org'],
+ ['"test"test@iana.org'],
+ ['"test\ test"@iana.org'],
+ ['"test\"@iana.org'],
+ ['"user name"@example.com'],
+ ['"user,name"@example.com'],
+ ['"user@name"@example.com'],
+ ['"user\"name"@example.com'],
+ ['"username"@example.com'],
+ ['(test_exampel@example.fr'],
+ ['.example@localhost'],
+ ['0'],
+ ['\r\n \r\n test@iana.org'],
+ ['\r\n \r\ntest@iana.org'],
+ ['\r\n test@iana.org'],
+ ['\r\ntest@iana.org'],
+ ['ex\ample@localhost'],
+ ['example((example))@fakedfake.co.uk'],
+ ['example(example]example@example.co.uk'],
+ ['example.@example.co.uk'],
+ ['example@example@example.co.uk'],
+ ['examp║le@symfony.com'],
+ ['test;123@foobar.com'],
+ ['test@example.com test'],
+ ['user name@example.com'],
+ ['user name@example.com'],
+ ['user name@example.com'],
+ ['user[na]me@example.com'],
+ ['usern,ame@example.com'],
+ [chr(226) . '@iana.org'],
+ ['"""@iana.org'],
+ ['"\"@iana.org'],
+ ['"test""test"@iana.org'],
+ ['"test"' . chr(0) . '@iana.org'],
+ ['"test"."test"@iana.org'],
+ ['"test".test@iana.org'],
+ ['"test"test@iana.org'],
+ ['"test\"@iana.org'],
+ ['(test_exampel@example.fr)'],
+ ['.example@localhost'],
+ ['\r\n \r\n test@iana.org'],
+ ['\r\n \r\ntest@iana.org'],
+ ['\r\n test@iana.org'],
+ ['\r\ntest@iana.org'],
+ ['email.email@email."'],
+ ['ex\ample@localhost'],
+ ['example(example)example@example.co.uk'],
+ ['example.@example.co.uk'],
+ ['example@(fake).com'],
+ ['example@(fake.com'],
+ ['example@example@example.co.uk'],
+ ['example@local\host'],
+ ['example@localhost.'],
+ ['test;123@foobar.com'],
+ ['test@' . chr(226) . '.org'],
+ ['test@email<'],
+ ['test@email>'],
+ ['test@email{'],
+ ['test@example..com'],
+ ['test@foo;bar.com'],
+ ['test@iana.org \r\n\r\n '],
+ ['test@iana.org \r\n '],
+ ['test@iana.org \r\n \r\n'],
+ ['test@iana.org \r\n'],
+ ['test@iana.org \r\n\r\n'],
+ ['test@iana/icann.org'],
+ ['user name@example.com'],
+ ['user name@example.com'],
+ ['user name@example.com'],
+ ['user[na]me@example.com'],
+ ['usern,ame@example.com'],
+ ['username@ example . com'],
+ ['username@example,com'],
+ [chr(226) . '@iana.org'],
+ [str_repeat('x', 254) . '@example.com'], // email with warnings
+ ];
+ }
+
+ /** @see https://github.com/symfony/validator/blob/46c4193ec1eefdd7f9568261c685450d6f3b32c9/Tests/Constraints/UrlValidatorTest.php#L98 */
+ public static function dataIsURLValid(): array
+ {
+ return [
+ ['http://a.pl'],
+ ['http://www.example.com'],
+ ['http://tt.example.com'],
+ ['http://m.example.com'],
+ ['http://m.m.m.example.com'],
+ ['http://example.m.example.com'],
+ ['https://long-string_with+symbols.m.example.com'],
+ ['http://www.example.com.'],
+ ['http://www.example.museum'],
+ ['https://example.com/'],
+ ['https://example.com:80/'],
+ ['http://examp_le.com'],
+ ['http://www.sub_domain.examp_le.com'],
+ ['http://www.example.coop/'],
+ ['http://www.test-example.com/'],
+ ['http://www.symfony.com/'],
+ ['http://symfony.fake/blog/'],
+ ['http://symfony.com/?'],
+ ['http://symfony.com/search?type=&q=url+validator'],
+ ['http://symfony.com/#'],
+ ['http://symfony.com/#?'],
+ ['http://www.symfony.com/doc/current/book/validation.html#supported-constraints'],
+ ['http://very.long.domain.name.com/'],
+ ['http://localhost/'],
+ ['http://myhost123/'],
+ ['http://internal-api'],
+ ['http://internal-api.'],
+ ['http://internal-api/'],
+ ['http://internal-api/path'],
+ ['http://127.0.0.1/'],
+ ['http://127.0.0.1:80/'],
+ ['http://[::1]/'],
+ ['http://[::1]:80/'],
+ ['http://[1:2:3::4:5:6:7]/'],
+ ['http://sãopaulo.com/'],
+ ['http://xn--sopaulo-xwa.com/'],
+ ['http://sãopaulo.com.br/'],
+ ['http://xn--sopaulo-xwa.com.br/'],
+ ['http://пример.испытание/'],
+ ['http://xn--e1afmkfd.xn--80akhbyknj4f/'],
+ ['http://مثال.إختبار/'],
+ ['http://xn--mgbh0fb.xn--kgbechtv/'],
+ ['http://例子.测试/'],
+ ['http://xn--fsqu00a.xn--0zwm56d/'],
+ ['http://例子.測試/'],
+ ['http://xn--fsqu00a.xn--g6w251d/'],
+ ['http://例え.テスト/'],
+ ['http://xn--r8jz45g.xn--zckzah/'],
+ ['http://مثال.آزمایشی/'],
+ ['http://xn--mgbh0fb.xn--hgbk6aj7f53bba/'],
+ ['http://실례.테스트/'],
+ ['http://xn--9n2bp8q.xn--9t4b11yi5a/'],
+ ['http://العربية.idn.icann.org/'],
+ ['http://xn--ogb.idn.icann.org/'],
+ ['http://xn--e1afmkfd.xn--80akhbyknj4f.xn--e1afmkfd/'],
+ ['http://xn--espaa-rta.xn--ca-ol-fsay5a/'],
+ ['http://xn--d1abbgf6aiiy.xn--p1ai/'],
+ ['http://☎.com/'],
+ ['http://username:password@symfony.com'],
+ ['http://user.name:password@symfony.com'],
+ ['http://user_name:pass_word@symfony.com'],
+ ['http://username:pass.word@symfony.com'],
+ ['http://user.name:pass.word@symfony.com'],
+ ['http://user-name@symfony.com'],
+ ['http://user_name@symfony.com'],
+ ['http://u%24er:password@symfony.com'],
+ ['http://user:pa%24%24word@symfony.com'],
+ ['http://symfony.com?'],
+ ['http://symfony.com?query=1'],
+ ['http://symfony.com/?query=1'],
+ ['http://symfony.com#'],
+ ['http://symfony.com#fragment'],
+ ['http://symfony.com/#fragment'],
+ ['http://symfony.com/#one_more%20test'],
+ ['http://example.com/exploit.html?hello[0]=test'],
+ ['http://বিডিআইএ.বাংলা'],
+
+ // Valid URL with whitespace.
+ '\x20http://www.example.com' => ["\x20http://www.example.com"],
+ '\x09\x09http://www.example.com.' => ["\x09\x09http://www.example.com."],
+ 'http://symfony.fake/blog/\x0A' => ["http://symfony.fake/blog/\x0A"],
+ 'http://symfony.com/search?type=&q=url+validator\x0D\x0D' => ["http://symfony.com/search?type=&q=url+validator\x0D\x0D"],
+ '\x00https://example.com:80\x00' => ["\x00https://example.com:80\x00"],
+ '\x0B\x0Bhttp://username:password@symfony.com\x0B\x0B' => ["\x0B\x0Bhttp://username:password@symfony.com\x0B\x0B"],
+ ];
+ }
+
+ public static function dataIsURLInvalid(): array
+ {
+ return [
+ "' '" => [' '],
+ "''" => [''],
+ 'null' => [null],
+ 'example.com' => ['example.com'],
+ '://example.com' => ['://example.com'],
+ 'http ://example.com' => ['http ://example.com'],
+ 'http:/example.com' => ['http:/example.com'],
+ 'http://example.com::aa' => ['http://example.com::aa'],
+ 'http://example.com:aa' => ['http://example.com:aa'],
+ 'ftp://example.fr' => ['ftp://example.fr'],
+ 'faked://example.fr' => ['faked://example.fr'],
+ 'http://127.0.0.1:aa/' => ['http://127.0.0.1:aa/'],
+ 'ftp://[::1]/' => ['ftp://[::1]/'],
+ 'http://[::1' => ['http://[::1'],
+ 'http://☎' => ['http://☎'],
+ 'http://☎.' => ['http://☎.'],
+ 'http://☎/' => ['http://☎/'],
+ 'http://☎/path' => ['http://☎/path'],
+ 'http://hello.☎' => ['http://hello.☎'],
+ 'http://hello.☎.' => ['http://hello.☎.'],
+ 'http://hello.☎/' => ['http://hello.☎/'],
+ 'http://hello.☎/path' => ['http://hello.☎/path'],
+ 'http://:password@symfony.com' => ['http://:password@symfony.com'],
+ 'http://:password@@symfony.com' => ['http://:password@@symfony.com'],
+ 'http://username:passwordsymfony.com' => ['http://username:passwordsymfony.com'],
+ 'http://usern@me:password@symfony.com' => ['http://usern@me:password@symfony.com'],
+ 'http://nota%hex:password@symfony.com' => ['http://nota%hex:password@symfony.com'],
+ 'http://username:nota%hex@symfony.com' => ['http://username:nota%hex@symfony.com'],
+ 'http://example.com/exploit.html?' => ['http://example.com/exploit.html?'],
+ 'http://example.com/exploit.html?hel lo' => ['http://example.com/exploit.html?hel lo'],
+ 'http://example.com/exploit.html?not_a%hex' => ['http://example.com/exploit.html?not_a%hex'],
+ 'http://' => ['http://'],
+ 'http://www..com' => ['http://www..com'],
+ 'http://www..example.com' => ['http://www..example.com'],
+ 'http://www..m.example.com' => ['http://www..m.example.com'],
+ 'http://.m.example.com' => ['http://.m.example.com'],
+ 'http://wwww.example..com' => ['http://wwww.example..com'],
+ 'http://.www.example.com' => ['http://.www.example.com'],
+ 'http://example.co-' => ['http://example.co-'],
+ 'http://example.co-/path' => ['http://example.co-/path'],
+ 'http:///path' => ['http:///path'],
+
+ // Relative URLs.
+ '/example.com' => ['/example.com'],
+ '//example.com::aa' => ['//example.com::aa'],
+ '//example.com:aa' => ['//example.com:aa'],
+ '//127.0.0.1:aa/' => ['//127.0.0.1:aa/'],
+ '//[::1' => ['//[::1'],
+ '//hello.☎/' => ['//hello.☎/'],
+ '//:password@symfony.com' => ['//:password@symfony.com'],
+ '//:password@@symfony.com' => ['//:password@@symfony.com'],
+ '//username:passwordsymfony.com' => ['//username:passwordsymfony.com'],
+ '//usern@me:password@symfony.com' => ['//usern@me:password@symfony.com'],
+ '//example.com/exploit.html?' => ['//example.com/exploit.html?'],
+ '//example.com/exploit.html?hel lo' => ['//example.com/exploit.html?hel lo'],
+ '//example.com/exploit.html?not_a%hex' => ['//example.com/exploit.html?not_a%hex'],
+ '//' => ['//'],
+ ];
+ }
+
+ public static function dataIsBlank(): array
+ {
+ return [
+ "''" => [''],
+ "' '" => [' '],
+ '[]' => [[]],
+ 'null' => [null],
+ 'false' => [false],
+
+ // Whitespace characters.
+ '\x20' => ["\x20"],
+ '\x09\x09' => ["\x09\x09"],
+ '\x0A' => ["\x0A"],
+ '\x0D\x0D' => ["\x0D\x0D"],
+ '\x00' => ["\x00"],
+ '\x0B\x0B' => ["\x0B\x0B"],
+ ];
+ }
+
+ public static function dataIsNotBlank(): array
+ {
+ return [
+ 'foobar' => ['foobar'],
+ '0' => [0],
+ '0.0' => [0.0],
+ "'0'" => ['0'],
+ '1234' => [1234],
+ '[1234]' => [[1234]],
+ 'true' => [true],
+ ];
+ }
+
+ public static function dataIsUUID(): array
+ {
+ return [
+ '216fff40-98d9-11e3-a5e2-0800200c9a66' => ['216fff40-98d9-11e3-a5e2-0800200c9a66'],
+ 'e22a9860-8e9f-11ed-95f6-5d3ec56dc459' => ['e22a9860-8e9f-11ed-95f6-5d3ec56dc459'],
+ ];
+ }
+
+ public static function dataIsNotUUID(): array
+ {
+ return [
+ "' '" => [' '],
+ "''" => [''],
+ 'null' => [null],
+ '{216fff40-98d9-11e3-a5e2-0800200c9a66}' => ['{216fff40-98d9-11e3-a5e2-0800200c9a66}'],
+ '216fff4098d911e3a5e20800200c9a66' => ['216fff4098d911e3a5e20800200c9a66'],
+ '216f-ff40-98d9-11e3-a5e2-0800-200c-9a66' => ['216f-ff40-98d9-11e3-a5e2-0800-200c-9a66'],
+ ];
+ }
+
+ public static function dataIsSemver(): array
+ {
+ return [
+ '0.0.4' => ['0.0.4'],
+ '1.2.3' => ['1.2.3'],
+ '10.20.30' => ['10.20.30'],
+ '1.1.2-prerelease+meta' => ['1.1.2-prerelease+meta'],
+ '1.1.2+meta' => ['1.1.2+meta'],
+ '1.1.2+meta-valid' => ['1.1.2+meta-valid'],
+ '1.0.0-alpha' => ['1.0.0-alpha'],
+ '1.0.0-beta' => ['1.0.0-beta'],
+ '1.0.0-alpha.beta' => ['1.0.0-alpha.beta'],
+ '1.0.0-alpha.beta.1' => ['1.0.0-alpha.beta.1'],
+ '1.0.0-alpha.1' => ['1.0.0-alpha.1'],
+ '1.0.0-alpha0.valid' => ['1.0.0-alpha0.valid'],
+ '1.0.0-alpha.0valid' => ['1.0.0-alpha.0valid'],
+ '1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' => ['1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay'],
+ '1.0.0-rc.1+build.1' => ['1.0.0-rc.1+build.1'],
+ '2.0.0-rc.1+build.123' => ['2.0.0-rc.1+build.123'],
+ '1.2.3-beta' => ['1.2.3-beta'],
+ '10.2.3-DEV-SNAPSHOT' => ['10.2.3-DEV-SNAPSHOT'],
+ '1.2.3-SNAPSHOT-123' => ['1.2.3-SNAPSHOT-123'],
+ '1.0.0' => ['1.0.0'],
+ '2.0.0' => ['2.0.0'],
+ '1.1.7' => ['1.1.7'],
+ '2.0.0+build.1848' => ['2.0.0+build.1848'],
+ '2.0.1-alpha.1227' => ['2.0.1-alpha.1227'],
+ '1.0.0-alpha+beta' => ['1.0.0-alpha+beta'],
+ '1.2.3----RC-SNAPSHOT.12.9.1--.12+788' => ['1.2.3----RC-SNAPSHOT.12.9.1--.12+788'],
+ '1.2.3----R-S.12.9.1--.12+meta' => ['1.2.3----R-S.12.9.1--.12+meta'],
+ '1.2.3----RC-SNAPSHOT.12.9.1--.12' => ['1.2.3----RC-SNAPSHOT.12.9.1--.12'],
+ '1.0.0+0.build.1-rc.10000aaa-kk-0.1' => ['1.0.0+0.build.1-rc.10000aaa-kk-0.1'],
+ '99999999999999999999999.999999999999999999.99999999999999999' => ['99999999999999999999999.999999999999999999.99999999999999999'],
+ '1.0.0-0A.is.legal' => ['1.0.0-0A.is.legal'],
+
+ // With v* prefix
+ 'v0.0.4' => ['v0.0.4'],
+ 'v1.2.3' => ['v1.2.3'],
+ 'v10.20.30' => ['v10.20.30'],
+ 'v1.1.2-prerelease+meta' => ['v1.1.2-prerelease+meta'],
+ 'v1.1.2+meta' => ['v1.1.2+meta'],
+ 'v1.1.2+meta-valid' => ['v1.1.2+meta-valid'],
+ 'v1.0.0-alpha' => ['v1.0.0-alpha'],
+ 'v1.0.0-beta' => ['v1.0.0-beta'],
+ 'v1.0.0-alpha.beta' => ['v1.0.0-alpha.beta'],
+ 'v1.0.0-alpha.beta.1' => ['v1.0.0-alpha.beta.1'],
+ 'v1.0.0-alpha.1' => ['v1.0.0-alpha.1'],
+ 'v1.0.0-alpha0.valid' => ['v1.0.0-alpha0.valid'],
+ 'v1.0.0-alpha.0valid' => ['v1.0.0-alpha.0valid'],
+ 'v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' => ['v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay'],
+ 'v1.0.0-rc.1+build.1' => ['v1.0.0-rc.1+build.1'],
+ 'v2.0.0-rc.1+build.123' => ['v2.0.0-rc.1+build.123'],
+ 'v1.2.3-beta' => ['v1.2.3-beta'],
+ 'v10.2.3-DEV-SNAPSHOT' => ['v10.2.3-DEV-SNAPSHOT'],
+ 'v1.2.3-SNAPSHOT-123' => ['v1.2.3-SNAPSHOT-123'],
+ 'v1.0.0' => ['v1.0.0'],
+ 'v2.0.0' => ['v2.0.0'],
+ 'v1.1.7' => ['v1.1.7'],
+ 'v2.0.0+build.1848' => ['v2.0.0+build.1848'],
+ 'v2.0.1-alpha.1227' => ['v2.0.1-alpha.1227'],
+ 'v1.0.0-alpha+beta' => ['v1.0.0-alpha+beta'],
+ 'v1.2.3----RC-SNAPSHOT.12.9.1--.12+788' => ['v1.2.3----RC-SNAPSHOT.12.9.1--.12+788'],
+ 'v1.2.3----R-S.12.9.1--.12+meta' => ['v1.2.3----R-S.12.9.1--.12+meta'],
+ 'v1.2.3----RC-SNAPSHOT.12.9.1--.12' => ['v1.2.3----RC-SNAPSHOT.12.9.1--.12'],
+ 'v1.0.0+0.build.1-rc.10000aaa-kk-0.1' => ['v1.0.0+0.build.1-rc.10000aaa-kk-0.1'],
+ 'v99999999999999999999999.999999999999999999.99999999999999999' => ['v99999999999999999999999.999999999999999999.99999999999999999'],
+ 'v1.0.0-0A.is.legal' => ['v1.0.0-0A.is.legal'],
+ ];
+ }
+
+ public static function dataIsNotSemver(): array
+ {
+ return [
+ '1' => ['1'],
+ '1.2' => ['1.2'],
+ '1.2.3-0123' => ['1.2.3-0123'],
+ '1.2.3-0123.0123' => ['1.2.3-0123.0123'],
+ '1.1.2+.123' => ['1.1.2+.123'],
+ '+invalid' => ['+invalid'],
+ '-invalid' => ['-invalid'],
+ '-invalid+invalid' => ['-invalid+invalid'],
+ '-invalid.01' => ['-invalid.01'],
+ 'alpha' => ['alpha'],
+ 'alpha.beta' => ['alpha.beta'],
+ 'alpha.beta.1' => ['alpha.beta.1'],
+ 'alpha.1' => ['alpha.1'],
+ 'alpha+beta' => ['alpha+beta'],
+ 'alpha_beta' => ['alpha_beta'],
+ 'alpha.' => ['alpha.'],
+ 'alpha..' => ['alpha..'],
+ 'beta' => ['beta'],
+ '1.0.0-alpha_beta' => ['1.0.0-alpha_beta'],
+ '-alpha.' => ['-alpha.'],
+ '1.0.0-alpha..' => ['1.0.0-alpha..'],
+ '1.0.0-alpha..1' => ['1.0.0-alpha..1'],
+ '1.0.0-alpha...1' => ['1.0.0-alpha...1'],
+ '1.0.0-alpha....1' => ['1.0.0-alpha....1'],
+ '1.0.0-alpha.....1' => ['1.0.0-alpha.....1'],
+ '1.0.0-alpha......1' => ['1.0.0-alpha......1'],
+ '1.0.0-alpha.......1' => ['1.0.0-alpha.......1'],
+ '01.1.1' => ['01.1.1'],
+ '1.01.1' => ['1.01.1'],
+ '1.1.01' => ['1.1.01'],
+ '1.2' => ['1.2'],
+ '1.2.3.DEV' => ['1.2.3.DEV'],
+ '1.2-SNAPSHOT' => ['1.2-SNAPSHOT'],
+ '1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788' => ['1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788'],
+ '1.2-RC-SNAPSHOT' => ['1.2-RC-SNAPSHOT'],
+ '-1.0.3-gamma+b7718' => ['-1.0.3-gamma+b7718'],
+ '+justmeta' => ['+justmeta'],
+ '9.8.7+meta+meta' => ['9.8.7+meta+meta'],
+ '9.8.7-whatever+meta+meta' => ['9.8.7-whatever+meta+meta'],
+ '99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12' => ['99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12'],
+ ];
+ }
+
+ public static function dataIsIpAddress(): array
+ {
+ return [
+ '127.0.0.1' => ['127.0.0.1'],
+ '1.2.3.4' => ['1.2.3.4'],
+ '2001:db8:3333:4444:5555:6666:7777:8888' => ['2001:db8:3333:4444:5555:6666:7777:8888'],
+ '2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF' => ['2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'],
+ '::' => ['::'],
+ '2001:db8::' => ['2001:db8::'],
+ '::1234:5678' => ['::1234:5678'],
+ '2001:db8::1234:5678' => ['2001:db8::1234:5678'],
+ '2001:0db8:0001:0000:0000:0ab9:C0A8:0102' => ['2001:0db8:0001:0000:0000:0ab9:C0A8:0102'],
+ '2001:db8:3333:4444:5555:6666:1.2.3.4' => ['2001:db8:3333:4444:5555:6666:1.2.3.4'],
+ '::11.22.33.44' => ['::11.22.33.44'],
+ '2001:db8::123.123.123.123' => ['2001:db8::123.123.123.123'],
+ '::1234:5678:91.123.4.56' => ['::1234:5678:91.123.4.56'],
+ '::1234:5678:1.2.3.4' => ['::1234:5678:1.2.3.4'],
+ '2001:db8::1234:5678:5.6.7.8' => ['2001:db8::1234:5678:5.6.7.8'],
+ ];
+ }
+
+ public static function dataIsNotIpAddress(): array
+ {
+ return [
+ "''" => [''],
+ "' '" => [' '],
+ 'null' => [null],
+ 'false' => [false],
+ 'zero' => [0],
+ 'float' => [0.1],
+ 'array' => [[]],
+ 'object' => [new stdClass()],
+ '99999999999999999999999' => ['99999999999999999999999'],
+ '1.2.3.4.5' => ['1.2.3.4.5'],
+ '1,2,3,4' => ['1,2,3,4'],
+ '270.0.0.1' => ['270.0.0.1'],
+ ];
+ }
+
+ public static function dataIsUnique(): array
+ {
+ return [
+ 'sequential' => [[1, 2, 3]],
+ 'associative' => [['a' => 1, 'b' => 2, 'c' => 3]],
+ 'multidimensional' => [['a' => 1, 'b' => [1, 2], 'c' => ['a' => 1, 'b' => 2]]],
+ ];
+ }
+
+ public static function dataIsUniqueOptionFields(): array
+ {
+ return [
+ [
+ [
+ [
+ 'label' => 'foo',
+ 'latitude' => '1',
+ 'longitude' => '2',
+ ],
+ [
+ 'label' => 'bar',
+ 'latitude' => '3',
+ 'longitude' => '4',
+ ],
+ ],
+ ['latitude', 'longitude'],
+ ],
+ ];
+ }
+
+ public static function dataIsNotUnique(): array
+ {
+ return [
+ 'sequential' => [[1, 2, 2]], // index 1 and 2 are not unique.
+ 'associative' => [['a' => 1, 'b' => 2, 'c' => 2]], // b and c are not unique.
+ 'multidimensional' => [['a' => 1, 'b' => [1, 2], 'c' => [1, 2]]], // b and c are not unique.
+ ];
+ }
+}