├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── merge-dependabot-upgrades.yml ├── .gitignore ├── .laminas-ci.json ├── .php-cs-fixer.dist.php ├── CNAME ├── CONTRIBUTE.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── phpunit.xml ├── psalm.xml ├── src ├── TypedInput.php └── Value.php └── tests ├── TypedInputTest.php └── ValueTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: "Continuous Integration" 3 | 4 | on: 5 | pull_request: 6 | push: 7 | 8 | jobs: 9 | matrix: 10 | name: Generate job matrix 11 | runs-on: ubuntu-latest 12 | outputs: 13 | matrix: ${{ steps.matrix.outputs.matrix }} 14 | steps: 15 | - name: Gather CI configuration 16 | id: matrix 17 | uses: laminas/laminas-ci-matrix-action@v1 18 | 19 | qa: 20 | name: QA Checks 21 | needs: [matrix] 22 | runs-on: ${{ matrix.operatingSystem }} 23 | strategy: 24 | fail-fast: false 25 | matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }} 26 | steps: 27 | - name: ${{ matrix.name }} 28 | uses: laminas/laminas-continuous-integration-action@v1 29 | with: 30 | job: ${{ matrix.job }} 31 | -------------------------------------------------------------------------------- /.github/workflows/merge-dependabot-upgrades.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/ridedott/merge-me-action/ 2 | # This workflow automates merges from patches sent by Dependabot, and 3 | # only by dependabot, once the other CI workflows pass 4 | name: Auto-merge Dependabot PRs 5 | 6 | on: 7 | workflow_run: 8 | types: 9 | - completed 10 | workflows: 11 | - "Continuous Integration" 12 | 13 | jobs: 14 | merge-me: 15 | name: Auto-merge Dependabot PRs 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Auto-Merge 19 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 20 | uses: ridedott/merge-me-action@v2.9.54 21 | with: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | MERGE_METHOD: MERGE 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .phpunit.result.cache 3 | .php-cs-fixer.cache 4 | .idea -------------------------------------------------------------------------------- /.laminas-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_php_platform_requirements": { 3 | "8.4": true 4 | }, 5 | "backwardCompatibilityCheck": true 6 | } 7 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([__DIR__ . '/src', __DIR__ . '/tests']); 5 | 6 | return (new PhpCsFixer\Config()) 7 | ->setRules([ 8 | 'declare_strict_types' => true, 9 | 'strict_param' => true, 10 | 'array_syntax' => ['syntax' => 'short'], 11 | 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 12 | 'phpdoc_to_comment' => false, 13 | ]) 14 | ->setFinder($finder) 15 | ->setRiskyAllowed(true); 16 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | typed-input.michael-petri.com -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome. I accept merge requests on [GitHub][]. 4 | This project follows semantic versioning and the [semantic branching model][]. 5 | 6 | ## Communication Channels 7 | 8 | You can find help and discussion in the following places: 9 | 10 | - [GitHub Issues][issues] 11 | 12 | ## Reporting Bugs 13 | 14 | Bugs get tracked in the project's [issue tracker][issues]. 15 | 16 | When submitting a bug report, please include enough information to reproduce the 17 | bug. A good bug report includes the following sections: 18 | 19 | - Given input 20 | - Expected output 21 | - Actual output 22 | - Steps to reproduce, including sample code 23 | - Any other information that will help debug and reproduce the issue, including 24 | stack traces, system/environment information, screenshots or at best a 25 | merge request with a test scenario which proofs the error. 26 | 27 | ## Fixing Bugs 28 | 29 | I welcome merge requests to fix bugs! 30 | 31 | If you see a bug report that you'd like to fix, please feel free to do so. 32 | See the [bug fixes][] section of the [semantic branching model][] documentation. 33 | 34 | ## Adding New Features 35 | 36 | If you have an idea for a new feature, it's a good idea to check out the 37 | [issues][] or active [merge requests][] first to see if the feature has already 38 | requested and being worked on. If not, feel free to submit an issue first, asking 39 | whether the feature is beneficial to the project. This will save you from doing a 40 | lot of development work only to have your feature rejected. I don't enjoy rejecting 41 | your hard work, but some features just don't fit with the goals of the project. 42 | 43 | When you do begin working on your feature, here are some guidelines to consider: 44 | 45 | - Check the [branch semantics][] section of the [semantic branching model][] documentation. 46 | - Your merge request description should clearly detail the changes you have made. 47 | I will use this description to update the CHANGELOG. If there is no 48 | description, or it does not adequately describe your feature, I will ask you 49 | to update the description. 50 | - This package follows the **[PSR-2 coding standard][psr-2]**. Please 51 | ensure your code does, too. 52 | - Please **write tests** for any new features you add. 53 | - Please **ensure that tests pass** before submitting your merge request. 54 | This package has automatically running tests for merge requests. 55 | However, running the tests locally will help save time. 56 | - Use **feature/{issue-id}.** branches. Please do not ask to merge from your master 57 | branch. 58 | - **Submit one feature per merge request.** If you have multiple features you 59 | wish to submit, please break them up into separate merge requests. 60 | - **Write good commit messages.** Make sure each individual commit in your merge 61 | request is meaningful. If you had to make multiple intermediate commits while 62 | developing, please squash them before submitting. 63 | 64 | 65 | ## Running Tests and Linters 66 | 67 | The following must pass before I will accept a merge request. If this does not 68 | pass, it will result in a complete build failure. Before you can run this, be 69 | sure to `composer install`. 70 | 71 | To run all the tests and coding standards checks, execute the following from the 72 | command line, while in the project root directory: 73 | 74 | ``` 75 | php vendor/bin/php-cs-fixer fix src --dry-run 76 | php vendor/bin/psalm 77 | php vendor/bin/phpunit 78 | ``` 79 | 80 | [GitHub]: https://github.com/michaelpetri/typed-input 81 | [issues]: https://github.com/michaelpetri/typed-input/issues 82 | [bug fixes]: https://dev-cafe.github.io/branching-model#bugfixes 83 | [branch semantics]: https://dev-cafe.github.io/branching-model/#branch-semantics 84 | [merge reqeusts]: https://github.com/michaelpetri/typed-input/compare 85 | [semantic branching model]: https://dev-cafe.github.io/branching-model -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Petri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typed Input 2 | 3 | [![Type Coverage](https://shepherd.dev/github/michaelpetri/typed-input/coverage.svg)](https://shepherd.dev/github/michaelpetri/typed-input) 4 | [![Latest Stable Version](https://poser.pugx.org/michaelpetri/typed-input/v)](//packagist.org/packages/michaelpetri/typed-input) 5 | [![License](https://poser.pugx.org/michaelpetri/typed-input/license)](//packagist.org/packages/michaelpetri/typed-input) 6 | 7 | ## Installation 8 | 9 | ```shell 10 | composer require michaelpetri/typed-input 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```php 16 | $typedInput = TypedInput::fromInput($input); 17 | 18 | echo $typedInput->getOption('my-option')->asNonEmptyString(); 19 | echo $typedInput->getArgument('my-argument')->asInteger(); 20 | ``` 21 | 22 | ## Available methods 23 | 24 | * `asBoolean` 25 | * `asBooleanOrNull` 26 | * `asInteger` 27 | * `asIntegerOrNull` 28 | * `asPositiveInteger` 29 | * `asPositiveIntegerOrNull` 30 | * `asNaturalInteger` 31 | * `asNaturalIntegerOrNull` 32 | * `asString` 33 | * `asStringOrNull` 34 | * `asNonEmptyString` 35 | * `asNonEmptyStringOrNull` 36 | * `asNonEmptyStrings` 37 | * `asDateTimeImmutable` 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "michaelpetri/typed-input", 3 | "description": "Type safe input wrapper for symfony input", 4 | "authors": [ 5 | { 6 | "name": "Michael Petri", 7 | "email": "mpetri@lyska.io" 8 | } 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0", 13 | "symfony/console": "^5.2 || ^6.0 || ^7.0", 14 | "webmozart/assert": "^1.11" 15 | }, 16 | "require-dev": { 17 | "friendsofphp/php-cs-fixer": "^3.0", 18 | "phpunit/phpunit": "^10.1.1", 19 | "roave/security-advisories": "dev-latest", 20 | "vimeo/psalm": "^5.1" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "MichaelPetri\\TypedInput\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "MichaelPetri\\TypedInput\\Tests\\": "tests/" 30 | } 31 | }, 32 | "conflict": { 33 | "stevebauman/unfinalize": "*" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests/ 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/TypedInput.php: -------------------------------------------------------------------------------- 1 | input = $input; 16 | } 17 | 18 | public static function fromInput(InputInterface $input): self 19 | { 20 | return new self($input); 21 | } 22 | 23 | public function getOption(string $name): Value 24 | { 25 | return new Value($this->input->getOption($name)); 26 | } 27 | 28 | public function getArgument(string $name): Value 29 | { 30 | return new Value($this->input->getArgument($name)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Value.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | } 23 | 24 | public function asBoolean(): bool 25 | { 26 | $value = $this->asBooleanOrNull(); 27 | Assert::notNull($value); 28 | 29 | return $value; 30 | } 31 | 32 | public function asBooleanOrNull(): ?bool 33 | { 34 | Assert::nullOrBoolean($this->value); 35 | 36 | return $this->value; 37 | } 38 | 39 | public function asInteger(): int 40 | { 41 | $value = $this->asIntegerOrNull(); 42 | Assert::notNull($value); 43 | 44 | return $value; 45 | } 46 | 47 | public function asIntegerOrNull(): ?int 48 | { 49 | Assert::nullOrIntegerish($this->value); 50 | 51 | return null !== $this->value 52 | ? (int) $this->value 53 | : null; 54 | } 55 | 56 | /** @psalm-return positive-int */ 57 | public function asPositiveInteger(): int 58 | { 59 | $value = $this->asPositiveIntegerOrNull(); 60 | Assert::notNull($value); 61 | 62 | return $value; 63 | } 64 | 65 | /** 66 | * @psalm-return positive-int|null 67 | * @psalm-suppress MoreSpecificReturnType 68 | */ 69 | public function asPositiveIntegerOrNull(): ?int 70 | { 71 | $value = $this->asIntegerOrNull(); 72 | Assert::nullOrGreaterThan($value, 0); 73 | 74 | /** @psalm-suppress LessSpecificReturnStatement */ 75 | return $value; 76 | } 77 | 78 | /** 79 | * @psalm-return positive-int|0 80 | * @psalm-suppress MoreSpecificReturnType 81 | */ 82 | public function asNaturalInteger(): int { 83 | $value = $this->asNaturalIntegerOrNull(); 84 | Assert::notNull($value); 85 | 86 | return $value; 87 | } 88 | 89 | /** 90 | * @psalm-return positive-int|0|null 91 | * @psalm-suppress LessSpecificReturnStatement 92 | * @psalm-suppress MoreSpecificReturnType 93 | */ 94 | public function asNaturalIntegerOrNull(): ?int { 95 | $value = $this->asIntegerOrNull(); 96 | Assert::nullOrGreaterThanEq($value, 0); 97 | 98 | return $value; 99 | } 100 | 101 | public function asString(): string 102 | { 103 | $value = $this->asStringOrNull(); 104 | Assert::string($value); 105 | 106 | return $value; 107 | } 108 | 109 | public function asStringOrNull(): ?string 110 | { 111 | Assert::nullOrString($this->value); 112 | 113 | return $this->value; 114 | } 115 | 116 | /** @psalm-return non-empty-string */ 117 | public function asNonEmptyString(): string 118 | { 119 | $value = $this->asNonEmptyStringOrNull(); 120 | Assert::notNull($value); 121 | 122 | return $value; 123 | } 124 | 125 | /** @psalm-return non-empty-string|null */ 126 | public function asNonEmptyStringOrNull(): ?string 127 | { 128 | Assert::nullOrStringNotEmpty($this->value); 129 | 130 | return $this->value; 131 | } 132 | 133 | /** 134 | * @psalm-return list 135 | * @return string[] 136 | */ 137 | public function asNonEmptyStrings(): array 138 | { 139 | Assert::isArray($this->value); 140 | Assert::allStringNotEmpty($this->value); 141 | 142 | return array_values($this->value); 143 | } 144 | 145 | public function asDateTimeImmutable(?string $format = null): DateTimeImmutable 146 | { 147 | $value = $this->asNonEmptyString(); 148 | 149 | try { 150 | $date = $format !== null 151 | ? DateTimeImmutable::createFromFormat($format, $value) 152 | : new DateTimeImmutable($value); 153 | } catch (Exception $e) { 154 | throw new TypeError(sprintf('"%s" is not a valid date string.', $value), 412, $e); 155 | } 156 | 157 | if(!$date instanceof DateTimeImmutable) { 158 | if($format !== null) { 159 | throw new TypeError(sprintf('Invalid date "%s" with format "%s"', $value, $format), 412); 160 | } 161 | 162 | throw new TypeError(sprintf('"%s" is not a valid date string.', $value), 412); 163 | } 164 | 165 | return $date; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/TypedInputTest.php: -------------------------------------------------------------------------------- 1 | addOption(new InputOption( 20 | 'my-option', 21 | null, 22 | InputOption::VALUE_REQUIRED, 23 | 'My option which is an non-empty-string', 24 | 'my-default-value' 25 | )); 26 | 27 | $input = new ArrayInput(['--my-option' => 'my-value'], $definition); 28 | 29 | $typedInput = TypedInput::fromInput($input); 30 | self::assertSame('my-value', $typedInput->getOption('my-option')->asNonEmptyString()); 31 | } 32 | 33 | public function testGetArgument(): void 34 | { 35 | $definition = new InputDefinition(); 36 | $definition->addArgument(new InputArgument( 37 | 'my-argument', 38 | InputArgument::OPTIONAL, 39 | 'My option which is an non-empty-string', 40 | 'my-default-value' 41 | )); 42 | 43 | $input = new ArrayInput(['my-argument' => 'my-value'], $definition); 44 | 45 | $typedInput = TypedInput::fromInput($input); 46 | self::assertSame('my-value', $typedInput->getArgument('my-argument')->asNonEmptyString()); 47 | } 48 | } -------------------------------------------------------------------------------- /tests/ValueTest.php: -------------------------------------------------------------------------------- 1 | asBoolean()); 23 | } 24 | 25 | /** 26 | * @psalm-param string|string[]|bool|null $raw 27 | * @dataProvider booleanOrNullProvider 28 | */ 29 | public function testAsBooleanOrNull($raw, ?bool $expected): void 30 | { 31 | $value = new Value($raw); 32 | self::assertSame($expected, $value->asBooleanOrNull()); 33 | } 34 | 35 | /** 36 | * @psalm-param string|string[]|bool|null $raw 37 | * @dataProvider integerProvider 38 | */ 39 | public function testAsInteger($raw, int $expected): void 40 | { 41 | $value = new Value($raw); 42 | self::assertSame($expected, $value->asInteger()); 43 | } 44 | 45 | /** 46 | * @psalm-param string|string[]|bool|null $raw 47 | * @dataProvider integerProvider 48 | */ 49 | public function testAsIntegerOrNull($raw, ?int $expected): void 50 | { 51 | $value = new Value($raw); 52 | self::assertSame($expected, $value->asIntegerOrNull()); 53 | } 54 | 55 | /** 56 | * @psalm-param string|string[]|bool|null $raw 57 | * @psalm-param positive-int $expected 58 | * @dataProvider positiveIntegerProvider 59 | */ 60 | public function testAsPositiveInteger($raw, int $expected): void 61 | { 62 | $value = new Value($raw); 63 | self::assertSame($expected, $value->asPositiveInteger()); 64 | } 65 | 66 | /** 67 | * @psalm-param string|string[]|bool|null $raw 68 | * @dataProvider nonPositiveIntegerOrNullProvider 69 | */ 70 | public function testFailAsPositiveInteger($raw): void 71 | { 72 | $value = new Value($raw); 73 | $this->expectException(InvalidArgumentException::class); 74 | $value->asPositiveInteger(); 75 | } 76 | 77 | /** 78 | * @psalm-param string|string[]|bool|null $raw 79 | * @psalm-param positive-int|null $expected 80 | * @dataProvider positiveIntegerOrNullProvider 81 | */ 82 | public function testAsPositiveIntegerOrNull($raw, ?int $expected): void 83 | { 84 | $value = new Value($raw); 85 | self::assertSame($expected, $value->asPositiveIntegerOrNull()); 86 | } 87 | 88 | /** 89 | * @psalm-param string|string[]|bool|null $raw 90 | * @dataProvider nonPositiveIntegerProvider 91 | */ 92 | public function testFailAsPositiveIntegerOrNull($raw): void 93 | { 94 | $value = new Value($raw); 95 | $this->expectException(InvalidArgumentException::class); 96 | $value->asPositiveIntegerOrNull(); 97 | } 98 | 99 | /** 100 | * @psalm-param string|string[]|bool|null $raw 101 | * @psalm-param positive-int|0 $expected 102 | * @dataProvider naturalIntegerProvider 103 | */ 104 | public function testAsNaturalInteger($raw, int $expected): void 105 | { 106 | $value = new Value($raw); 107 | self::assertSame($expected, $value->asNaturalInteger()); 108 | } 109 | 110 | /** 111 | * @psalm-param string|string[]|bool|null $raw 112 | * @dataProvider negativeIntegerOrNullProvider 113 | */ 114 | public function testFailAsNaturalInteger($raw): void 115 | { 116 | $value = new Value($raw); 117 | $this->expectException(InvalidArgumentException::class); 118 | $value->asNaturalInteger(); 119 | } 120 | 121 | /** 122 | * @psalm-param string|string[]|bool|null $raw 123 | * @psalm-param positive-int|0|null $expected 124 | * @dataProvider naturalIntegerOrNullProvider 125 | */ 126 | public function testAsNaturalIntegerOrNull($raw, ?int $expected): void 127 | { 128 | $value = new Value($raw); 129 | self::assertSame($expected, $value->asNaturalIntegerOrNull()); 130 | } 131 | 132 | /** 133 | * @psalm-param string|string[]|bool|null $raw 134 | * @dataProvider negativeIntegerProvider 135 | */ 136 | public function testFailAsNaturalIntegerOrNull($raw): void 137 | { 138 | $value = new Value($raw); 139 | $this->expectException(InvalidArgumentException::class); 140 | $value->asNaturalIntegerOrNull(); 141 | } 142 | 143 | /** 144 | * @psalm-param string|string[]|bool|null $raw 145 | * @dataProvider stringProvider 146 | */ 147 | public function testAsString($raw, string $expected): void 148 | { 149 | $value = new Value($raw); 150 | self::assertSame($expected, $value->asString()); 151 | } 152 | 153 | /** 154 | * @psalm-param string|string[]|bool|null $raw 155 | * @dataProvider stringOrNullProvider 156 | */ 157 | public function testAsStringOrNull($raw, ?string $expected): void 158 | { 159 | $value = new Value($raw); 160 | self::assertSame($expected, $value->asStringOrNull()); 161 | } 162 | 163 | /** 164 | * @psalm-param string|string[]|bool|null $raw 165 | * @psalm-param non-empty-string $expected 166 | * @dataProvider nonEmptyStringProvider 167 | */ 168 | public function testAsNonEmptyString($raw, string $expected): void 169 | { 170 | $value = new Value($raw); 171 | self::assertSame($expected, $value->asStringOrNull()); 172 | } 173 | 174 | /** 175 | * @psalm-param string|string[]|bool|null $raw 176 | * @dataProvider emptyStringOrNullProvider 177 | */ 178 | public function testFailAsNonEmptyString($raw): void 179 | { 180 | $value = new Value($raw); 181 | $this->expectException(InvalidArgumentException::class); 182 | $value->asNonEmptyString(); 183 | } 184 | 185 | /** 186 | * @psalm-param string|string[]|bool|null $raw 187 | * @psalm-param non-empty-string|null $expected 188 | * @dataProvider nonEmptyStringOrNullProvider 189 | */ 190 | public function testAsNonEmptyStringOrNull($raw, ?string $expected): void 191 | { 192 | $value = new Value($raw); 193 | self::assertSame($expected, $value->asNonEmptyStringOrNull()); 194 | } 195 | 196 | /** 197 | * @psalm-param string|string[]|bool|null $raw 198 | * @dataProvider emptyStringProvider 199 | */ 200 | public function testFailAsNonEmptyStringOrNull($raw): void 201 | { 202 | $value = new Value($raw); 203 | $this->expectException(InvalidArgumentException::class); 204 | $value->asNonEmptyStringOrNull(); 205 | } 206 | 207 | /** 208 | * @psalm-param string|string[]|bool|null $raw 209 | * @psalm-param non-empty-string $expected 210 | * @dataProvider nonEmptyStringProvider 211 | */ 212 | public function testAsNonEmptyStrings($raw, string $expected): void 213 | { 214 | $value = new Value([$raw, $raw]); 215 | self::assertSame([$expected, $expected], $value->asNonEmptyStrings()); 216 | } 217 | 218 | /** 219 | * @psalm-param string|string[]|bool|null $raw 220 | * @dataProvider emptyStringOrNullProvider 221 | */ 222 | public function testFailAsNonEmptyStrings($raw): void 223 | { 224 | $value = new Value([$raw, $raw]); 225 | $this->expectException(InvalidArgumentException::class); 226 | $value->asNonEmptyStrings(); 227 | } 228 | 229 | /** 230 | * @dataProvider dateTimeProvider 231 | * 232 | * @psalm-param mixed $raw 233 | * @psalm-param class-string|null $expectedException 234 | */ 235 | public function testAsDateTimeImmutable($raw, ?string $expectedException, $expected, ?string $format): void 236 | { 237 | $value = new Value($raw); 238 | 239 | if ($expectedException !== null) { 240 | $this->expectException($expectedException); 241 | } 242 | 243 | $date = $value->asDateTimeImmutable($format); 244 | 245 | self::assertSame($expected, $date->format(DateTimeInterface::RSS)); 246 | } 247 | 248 | public static function dateTimeProvider(): iterable 249 | { 250 | yield [null, InvalidArgumentException::class, null, null]; 251 | yield [1234, InvalidArgumentException::class, null, null]; 252 | yield ['blub', TypeError::class, null, null]; 253 | yield ['20221111', null, 'Fri, 11 Nov 2022 00:00:00 +0000', null]; 254 | yield ['2022-11-11', null, 'Fri, 11 Nov 2022 00:00:00 +0000', null]; 255 | yield ['2022-11-11T05:06:07+01:00', null, 'Fri, 11 Nov 2022 05:06:07 +0100', null]; 256 | yield ['2022-11-11', TypeError::class, null, DateTimeInterface::ATOM]; 257 | yield ['2022-11-11T03:04:02+00:00', null, 'Fri, 11 Nov 2022 03:04:02 +0000', DateTimeInterface::ATOM]; 258 | } 259 | 260 | /** 261 | * @psalm-return iterable 265 | */ 266 | public static function booleanProvider(): iterable 267 | { 268 | yield [true, true]; 269 | yield [false, false]; 270 | } 271 | 272 | /** 273 | * @psalm-return iterable 277 | */ 278 | public static function booleanOrNullProvider(): iterable 279 | { 280 | yield from self::booleanProvider(); 281 | yield from self::nullProvider(); 282 | } 283 | 284 | /** 285 | * @psalm-return iterable 289 | */ 290 | public static function integerProvider(): iterable 291 | { 292 | yield from self::naturalIntegerProvider(); 293 | yield from self::negativeIntegerProvider(); 294 | } 295 | 296 | /** 297 | * @psalm-return iterable 301 | */ 302 | public static function integerOrNullProvider(): iterable 303 | { 304 | yield from self::integerProvider(); 305 | yield from self::nullProvider(); 306 | } 307 | 308 | /** 309 | * @psalm-return iterable 313 | */ 314 | private static function zeroIntegerProvider(): iterable 315 | { 316 | yield ['0', 0]; 317 | } 318 | 319 | /** 320 | * @psalm-return iterable 324 | */ 325 | public static function positiveIntegerProvider(): iterable 326 | { 327 | yield ['1', 1]; 328 | } 329 | 330 | /** 331 | * @psalm-return iterable 335 | */ 336 | public static function positiveIntegerOrNullProvider(): iterable 337 | { 338 | yield from self::positiveIntegerProvider(); 339 | yield from self::nullProvider(); 340 | } 341 | 342 | /** 343 | * @psalm-return iterable 347 | */ 348 | public static function naturalIntegerProvider(): iterable 349 | { 350 | yield from self::zeroIntegerProvider(); 351 | yield from self::positiveIntegerProvider(); 352 | } 353 | 354 | public static function naturalIntegerOrNullProvider(): iterable 355 | { 356 | yield from self::naturalIntegerProvider(); 357 | yield from self::nullProvider(); 358 | } 359 | 360 | /** 361 | * @psalm-return iterable 365 | */ 366 | public static function negativeIntegerProvider(): iterable 367 | { 368 | yield ['-1', -1]; 369 | } 370 | 371 | /** 372 | * @psalm-return iterable 376 | */ 377 | public static function negativeIntegerOrNullProvider(): iterable 378 | { 379 | yield from self::negativeIntegerProvider(); 380 | yield [null, null]; 381 | } 382 | 383 | /** 384 | * @psalm-return iterable 388 | */ 389 | public static function nonPositiveIntegerProvider(): iterable 390 | { 391 | yield from self::negativeIntegerProvider(); 392 | yield from self::zeroIntegerProvider(); 393 | } 394 | 395 | /** 396 | * @psalm-return iterable 400 | */ 401 | public static function nonPositiveIntegerOrNullProvider(): iterable 402 | { 403 | yield from self::negativeIntegerOrNullProvider(); 404 | yield from self::zeroIntegerProvider(); 405 | } 406 | 407 | /** 408 | * @psalm-return iterable 412 | */ 413 | public static function stringProvider(): iterable 414 | { 415 | yield from self::nonEmptyStringProvider(); 416 | yield from self::emptyStringProvider(); 417 | } 418 | 419 | /** 420 | * @psalm-return iterable 424 | */ 425 | public static function stringOrNullProvider(): iterable 426 | { 427 | yield from self::stringProvider(); 428 | yield from self::nullProvider(); 429 | } 430 | 431 | /** 432 | * @psalm-return iterable 436 | */ 437 | public static function emptyStringProvider(): iterable 438 | { 439 | yield ['', '']; 440 | } 441 | 442 | /** 443 | * @psalm-return iterable 447 | */ 448 | public static function emptyStringOrNullProvider(): iterable 449 | { 450 | yield from self::emptyStringProvider(); 451 | yield from self::nullProvider(); 452 | } 453 | 454 | /** 455 | * @psalm-return iterable 459 | */ 460 | public static function nonEmptyStringProvider(): iterable 461 | { 462 | yield [' ', ' ']; 463 | yield ['non-empty-string', 'non-empty-string']; 464 | yield ['true', 'true']; 465 | yield ['false', 'false']; 466 | yield ['1', '1']; 467 | yield ['0', '0']; 468 | yield ['-1', '-1']; 469 | } 470 | 471 | /** 472 | * @psalm-return iterable 476 | */ 477 | public static function nonEmptyStringOrNullProvider(): iterable 478 | { 479 | yield from self::nonEmptyStringProvider(); 480 | yield from self::nullProvider(); 481 | } 482 | 483 | /** 484 | * @psalm-return iterable 488 | */ 489 | private static function nullProvider(): iterable 490 | { 491 | yield [null, null]; 492 | } 493 | } 494 | --------------------------------------------------------------------------------