diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc3a0ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) sji + +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..491405b --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Typist + +Typist is a PHP library enforcing types of local variables. +It internally uses references to typed properties introduced in PHP 7.4. + +## Installation + +``` +composer require sj-i/typist +``` + +## Supported Versions + +- PHP 7.4 or later + +# Usage +## Basic Usage + +``` +use Typist\Typist; + +// type enforcements are valid during the lifetime of this `$_` +$_ = [ + Typist::int($typed_int, 1), + Typist::string($typed_string, 'str'), + Typist::bool($typed_bool, false), + Typist::float($typed_float, 0.1), + Typist::class(\DateTimeInterface::class, $typed_object, new \DateTime()), +]; + +assert($typed_int === 1); +assert($typed_string === 'str'); +assert($typed_bool === false); +assert($typed_float === 0.1); +assert($typed_object instanceof \DateTime); + +// modifications with valid types are OK +$typed_int = 2; +$typed_string = 'trs'; +$typed_bool = true; +$typed_float = -0.1; +$typed_object = new DateTimeImmutable(); + +// any statements below raises TypeError +$typed_int = 'a'; +$typed_string = 1; +$typed_bool = 'a'; +$typed_float = 'a'; +$typed_object = 'a'; +``` + +## Nullable Types + +``` +use Typist\Typist; + +$_ = [ + Typist::nullable()::int($typed_int1, 1), + Typist::nullable()::int($typed_int2, null), + Typist::nullable()::string($typed_string1, 'str'), + Typist::nullable()::string($typed_string2, null), + Typist::nullable()::bool($typed_bool1, false), + Typist::nullable()::bool($typed_bool2, null), + Typist::nullable()::float($typed_float1, 0.1), + Typist::nullable()::float($typed_float2, null), + Typist::nullable()::class(\DateTimeInterface::class, $typed_object1, new \DateTime()), + Typist::nullable()::class(\DateTimeInterface::class, $typed_object2, null), +]; +``` + +or if you use PHP8, `()` can be omitted. + +``` +use Typist\Typist; + +$_ = [ + Typist::nullable::int($typed_int2, null), + Typist::nullable::string($typed_string2, null), + Typist::nullable::bool($typed_bool2, null), + Typist::nullable::float($typed_float2, null), + Typist::nullable::class(\DateTimeInterface::class, $typed_object2, null), +]; +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..25fb520 --- /dev/null +++ b/composer.json @@ -0,0 +1,34 @@ +{ + "name": "sj-i/typist", + "description": "Enforcing types of local variables", + "type": "library", + "keywords": [ + "type-safety" + ], + "minimum-stability": "stable", + "license": "MIT", + "authors": [ + { + "name": "sji", + "homepage": "https://twitter.com/sji_ch" + } + ], + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0 || ^9.0", + "vimeo/psalm": "^3.12", + "squizlabs/php_codesniffer": "^3.5" + }, + "autoload": { + "psr-4": { + "Typist\\": "src/Typist" + } + }, + "autoload-dev": { + "psr-4": { + "Typist\\": "tests/Typist" + } + } +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..e247a9c --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..af1211c --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/src/Typist/BoolEnforcer.php b/src/Typist/BoolEnforcer.php new file mode 100644 index 0000000..3eda9f4 --- /dev/null +++ b/src/Typist/BoolEnforcer.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Typist; + +final class BoolEnforcer +{ + private bool $value; + + /** + * @internal + */ + public function __construct(bool &$value) + { + $this->value = &$value; + } +} diff --git a/src/Typist/ClassEnforcer.php b/src/Typist/ClassEnforcer.php new file mode 100644 index 0000000..82e8781 --- /dev/null +++ b/src/Typist/ClassEnforcer.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Typist; + +use ReflectionClass; + +/** + * @template T + */ +final class ClassEnforcer +{ + /** @var class-string[] */ + private static array $registered_classes = []; + + private GeneratedClassEnforcerInterface $enforcer; + + /** + * @internal + * @param class-string $passed_class_name + * @param T $value + * @throws \ReflectionException + */ + public function __construct(string $passed_class_name, &$value) + { + $class = new ReflectionClass($passed_class_name); + + $namespace = $class->getNamespaceName(); + $class_name = $class->getShortName(); + $fully_qualified_class_name = ($namespace === '' ? '\\' : $namespace) . $class_name; + + $enforcer_name = $class_name . 'Enforcer'; + $enforcer_namespace = rtrim(implode('\\', [__NAMESPACE__ , $namespace]), '\\'); + $enforcer_interface_name = GeneratedClassEnforcerInterface::class; + + if (!isset(self::$registered_classes[$fully_qualified_class_name])) { + $code = <<