Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gmazzap committed May 5, 2024
0 parents commit 4f8fa46
Show file tree
Hide file tree
Showing 16 changed files with 2,740 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitattributes
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
82 changes: 82 additions & 0 deletions .github/workflows/quality-assurance.yml
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
13 changes: 13 additions & 0 deletions .gitignore
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/
128 changes: 128 additions & 0 deletions README.md
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.
43 changes: 43 additions & 0 deletions composer.json
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"
]
}
}
29 changes: 29 additions & 0 deletions phpcs.xml.dist
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>
18 changes: 18 additions & 0 deletions phpunit.xml.dist
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>
25 changes: 25 additions & 0 deletions psalm.xml
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>
Loading

0 comments on commit 4f8fa46

Please sign in to comment.