-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4f8fa46
Showing
16 changed files
with
2,740 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
* text eol=lf | ||
|
||
/tests/ export-ignore | ||
/.gitattributes export-ignore | ||
/.gitignore export-ignore | ||
/phpcs.xml.dist export-ignore | ||
/phpunit.xml.dist export-ignore | ||
/psalm.xml export-ignore | ||
/README.md export-ignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
name: Quality Assurance | ||
|
||
on: | ||
push: | ||
paths: | ||
- '**workflows/quality-assurance.yml' | ||
- '**.php' | ||
- '**phpcs.xml.dist' | ||
- '**phpunit.xml.dist' | ||
- '**psalm.xml' | ||
pull_request: | ||
paths: | ||
- '**workflows/quality-assurance.yml' | ||
- '**.php' | ||
- '**phpcs.xml.dist' | ||
- '**phpunit.xml.dist' | ||
- '**psalm.xml' | ||
workflow_dispatch: | ||
inputs: | ||
jobs: | ||
required: true | ||
type: choice | ||
default: 'Run all' | ||
description: 'Choose jobs to run' | ||
options: | ||
- 'Run all' | ||
- 'Run PHPCS only' | ||
- 'Run Psalm only' | ||
- 'Run lint only' | ||
- 'Run static analysis' | ||
- 'Run unit tests only' | ||
- 'Run mutation tests only' | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
lint: | ||
if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run lint only') || (github.event.inputs.jobs == 'Run static analysis')) }} | ||
uses: inpsyde/reusable-workflows/.github/workflows/lint-php.yml@main | ||
strategy: | ||
matrix: | ||
php: [ '8.1', '8.2', '8.3' ] | ||
with: | ||
PHP_VERSION: ${{ matrix.php }} | ||
LINT_ARGS: '-e php --colors --show-deprecated ./src' | ||
|
||
coding-standards-analysis: | ||
if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run PHPCS only') || (github.event.inputs.jobs == 'Run static analysis')) }} | ||
uses: inpsyde/reusable-workflows/.github/workflows/coding-standards-php.yml@main | ||
|
||
static-code-analysis: | ||
if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run Psalm only') || (github.event.inputs.jobs == 'Run static analysis')) }} | ||
uses: inpsyde/reusable-workflows/.github/workflows/static-analysis-php.yml@main | ||
strategy: | ||
matrix: | ||
php: [ '8.1', '8.2', '8.3' ] | ||
with: | ||
PHP_VERSION: ${{ matrix.php }} | ||
PSALM_ARGS: --output-format=github --no-suggestions --no-cache --no-diff --find-unused-psalm-suppress | ||
|
||
unit-tests: | ||
if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run unit tests only')) }} | ||
uses: inpsyde/reusable-workflows/.github/workflows/tests-unit-php.yml@main | ||
strategy: | ||
matrix: | ||
php: [ '8.1', '8.2', '8.3' ] | ||
with: | ||
PHP_VERSION: ${{ matrix.php }} | ||
PHPUNIT_ARGS: '--no-coverage' | ||
|
||
mutation-tests: | ||
if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run mutation tests only')) }} | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: shivammathur/setup-php@v2 | ||
with: | ||
php-version: '8.3' | ||
tools: infection, phpunit:10 | ||
- run: infection --min-covered-msi=95 --threads=max |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
composer.phar | ||
composer.lock | ||
/vendor/ | ||
|
||
/.phpunit.result.cache | ||
/phpcs.xml | ||
/phpunit.xml | ||
/infection.json5 | ||
|
||
*.log | ||
.env | ||
.DS_Store | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# Type Checker | ||
|
||
|
||
|
||
## What is this | ||
|
||
You can think of it as a way to build an [`is_a()`](https://www.php.net/manual/it/function.is-a.php) function on steroids. | ||
|
||
`is_a()` only works for classes, interfaces and enums. It does not work for scalars (`string`, `int`, etc., including unary types like `true` and `false`) and it does not work for virtual types (`iterable`, `callable`). | ||
|
||
Moreover, it does not work for complex types, such as unions, intersections, and DNF. | ||
|
||
If you ever wanted to do something like: | ||
|
||
```php | ||
is_a('iterable|MyThing|(Iterator&Countable)|null', $thing); | ||
``` | ||
|
||
Then this package is for you. But there's more. | ||
|
||
`is_a()` accepts a type string, but sometimes one wants to check against a [`ReflectionType`](https://www.php.net/manual/it/class.reflectiontype.php), or even a [`ReflectionClass`](https://www.php.net/manual/it/class.reflectionclass.php) and this package can do those things as well. | ||
|
||
An example **with a string**: | ||
|
||
```php | ||
use Toobo\TypeChecker\Type; | ||
|
||
Type::byString('iterable|MyThing|(Iterator&Countable)|null')->satisfiedBy($thing); | ||
``` | ||
|
||
with a **`ReflectionType`**: | ||
|
||
```php | ||
use Toobo\TypeChecker\Type; | ||
|
||
function test(iterable|MyThing|(Iterator&Countable)|null $param) {} | ||
$refType = (new ReflectionFunction('test'))->getParameters()[0]->getType(); | ||
|
||
Type::byReflectionType($refType)->satisfiedBy($thing); | ||
``` | ||
|
||
and with a **`ReflectionClass`**: | ||
|
||
```php | ||
use Toobo\TypeChecker\Type; | ||
|
||
Type::byReflectionType(new ReflectionClass($this))->satisfiedBy($this); | ||
``` | ||
|
||
|
||
|
||
## A deeper look | ||
|
||
|
||
|
||
### Named constructors | ||
|
||
Besides by strings and by reflection, the `Type` class can also be instantiated using named constructors such as `Type::string()`, `Type::resource()`, or `Type::mixed()`, but also `Type::iterable()`, `Type::callable()` or even `Type::null()`, `Type::true()`, and `Type::false()`. | ||
|
||
For completeness' sake, it is also possible to create instances for types that will never match any value, like `Type::void()` or `Type::never()`. | ||
|
||
|
||
|
||
### Matching types | ||
|
||
The `Type` class aims at representing the entire PHP type system. And it is possible to compare one instance with another: | ||
|
||
```php | ||
assert(Type::byString('IteratorAggregate&Countable')->matchedBy('ArrayObject')); | ||
assert(Type::byString('IteratorAggregate&Countable')->matchedBy(Type::byString('ArrayObject'))); | ||
|
||
assert(Type::byString('ArrayObject')->isA('IteratorAggregate&Countable')); | ||
assert(Type::byString('ArrayObject')->isA(Type::byString('IteratorAggregate&Countable'))); | ||
``` | ||
|
||
`Type::matchedBy()` and `Type::isA()` both accept a string or another type instance and check type compliance. They are the inverse of each other. | ||
|
||
`Type::matchedBy()` behavior can be described as: _if a function's argument type is represented by the type calling the method, would it be satisfied by a value whose type is represented by the type passed as argument_? | ||
|
||
`Type::isA()` behavior can be described as: _if a function's argument type is represented by the type passed as argument, would it be satisfied by a value whose type is represented by the instance calling the method_? | ||
|
||
|
||
|
||
### Type information | ||
|
||
The `Type` class has several methods to get information about the PHP type it represents. | ||
|
||
- `isStandalone()` | ||
- `isUnion()` | ||
- `isIntersection()` | ||
- `isDnf()` | ||
|
||
can tell what kind of composite type is, or if not composed at all. | ||
|
||
There's also a `isNullable()` method. | ||
|
||
|
||
|
||
### Type position utils | ||
|
||
PHP allows type declarations in three places: | ||
|
||
- Function arguments types | ||
- Function return | ||
- Properties declaration | ||
|
||
And a slightly different set of types is supported in the three positions. | ||
|
||
For example, `void` and `never` can only be used as return types, and `callable` can not be used as property type. | ||
|
||
The `Type` class has three methods: `Type::isPropertySafe()`, `Type::isArgumentSafe()`, and `Type::isReturnSafe()` which can be used to determine if the instance represents a type that can be used in the three positions. | ||
|
||
|
||
|
||
## Comparison with other libraries | ||
|
||
There are other libraries out there that deals with "objects representing types". | ||
|
||
- [PHPDoc Parser for PHPStan](https://github.com/phpstan/phpdoc-parser) is an amazing type parser from doc bloc. While it is similar to this package about creating "type objects" from strings, | ||
the PHPStan package more powerful supporting [much more than the PHP native type system](https://phpstan.org/writing-php-code/phpdoc-types). | ||
However, it does not support the possibility to check if a value belongs to that type, which is this package's main reason to exist. | ||
|
||
- [PHP Documentor's TypeResolver](https://github.com/phpDocumentor/TypeResolver) is based on the PHPStan's package mentioned above. And the same differences highlighted above apply. | ||
|
||
- [Symfony type-info component](https://github.com/symfony/type-info). The "new guy" on the scene, still experimental. It is also based on the PHPStan library for string parsing, but similarly | ||
to this library also deals with reflection types. At the moment of writing, the possibility to check a value satisfy a type is only limited to native "standalone" types, which includes virtual types such as `iterable` and `callable`, but does not include composed types. | ||
|
||
In general, this package dependency-free, simpler in only targeting PHP-supported type rather than "advanced" types that are used in documentation and static analysis. This limited scope allows for much simpler code in a single class. Moreover, this library focuses on _checking_ type of values more than "parsing" or "extracting" types from strings, like other libraries. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"name": "toobo/type-checker", | ||
"minimum-stability": "dev", | ||
"prefer-stable": true, | ||
"require": { | ||
"php": ">=8.1 < 8.4" | ||
}, | ||
"require-dev": { | ||
"phpunit/phpunit": "^10.5.18", | ||
"inpsyde/php-coding-standards": "^2", | ||
"vimeo/psalm": "^5.23.1" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Toobo\\TypeChecker\\": "src/" | ||
} | ||
}, | ||
"autoload-dev": { | ||
"psr-4": { | ||
"Toobo\\TypeChecker\\Tests\\": [ | ||
"tests/src/", | ||
"tests/cases/" | ||
] | ||
} | ||
}, | ||
"config": { | ||
"optimize-autoloader": true, | ||
"allow-plugins": { | ||
"composer/*": true, | ||
"dealerdirect/phpcodesniffer-composer-installer": true | ||
} | ||
}, | ||
"scripts": { | ||
"cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", | ||
"psalm": "@php ./vendor/vimeo/psalm/psalm --no-suggestions --report-show-info=false --find-unused-psalm-suppress --no-diff --no-cache --no-file-cache --output-format=compact", | ||
"tests": "@php ./vendor/phpunit/phpunit/phpunit --no-coverage", | ||
"qa": [ | ||
"@cs", | ||
"@psalm", | ||
"@tests" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<ruleset> | ||
<file>./src/</file> | ||
<file>./tests/</file> | ||
|
||
<arg value="sp"/> | ||
<config name="testVersion" value="8.1-"/> | ||
|
||
<rule ref="Inpsyde"> | ||
<exclude name="WordPress.WP"/> | ||
<exclude name="WordPress.Security.EscapeOutput"/> | ||
<exclude name="WordPress.PHP.DevelopmentFunctions"/> | ||
<exclude name="WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting"/> | ||
<exclude name="Inpsyde.CodeQuality.DisableMagicSerialize"/> | ||
</rule> | ||
|
||
<rule ref="Inpsyde.CodeQuality.Psr4"> | ||
<properties> | ||
<property | ||
name="psr4" | ||
type="array" | ||
value=" | ||
Toobo\TypeChecker=>src, | ||
Toobo\TypeChecker\Tests=>tests/src|tests/cases | ||
" | ||
/> | ||
</properties> | ||
</rule> | ||
</ruleset> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<phpunit | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" | ||
bootstrap="tests/bootstrap.php"> | ||
|
||
<testsuites> | ||
<testsuite name="unit"> | ||
<directory>tests/cases</directory> | ||
</testsuite> | ||
</testsuites> | ||
|
||
<source> | ||
<include> | ||
<directory suffix=".php">src</directory> | ||
</include> | ||
</source> | ||
</phpunit> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?xml version="1.0"?> | ||
<psalm | ||
allowNamedArgumentCalls="false" | ||
errorLevel="1" | ||
findUnusedBaselineEntry="true" | ||
findUnusedCode="false" | ||
hideExternalErrors="true" | ||
strictBinaryOperands="true" | ||
useDocblockPropertyTypes="true" | ||
usePhpDocMethodsWithoutMagicCall="true" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xmlns="https://getpsalm.org/schema/config" | ||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" | ||
> | ||
<projectFiles> | ||
<directory name="src"/> | ||
<ignoreFiles> | ||
<directory name="vendor"/> | ||
</ignoreFiles> | ||
</projectFiles> | ||
|
||
<issueHandlers> | ||
<InvalidDocblock errorLevel="suppress"/> | ||
</issueHandlers> | ||
</psalm> |
Oops, something went wrong.