Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d7fe9a4
Enhance ContractVisitor to respect @typephp-ignore tags and update te…
rcalicdan Aug 11, 2026
1ece4ec
Fix formatting and improve clarity in README.md
rcalicdan Aug 11, 2026
bb0172e
Enhance CommandRunner to handle unknown commands and validate PHP fil…
rcalicdan Aug 11, 2026
5be4d61
Reorganize test suites for typechecking
rcalicdan Aug 11, 2026
6ba90a4
Remove reorganize-tests.sh script for test directory restructuring
rcalicdan Aug 11, 2026
dfc2fa8
Add class constant key tests and fixtures for array shape validation
rcalicdan Aug 11, 2026
217b4cf
Add ClassStringFactoryContainer and corresponding tests for class-str…
rcalicdan Aug 11, 2026
437d369
Refactor SpecialTypeResolver to extract class and constant names for …
rcalicdan Aug 11, 2026
2a72504
Improve property hook checks and enhance node handling in PropertyHoo…
rcalicdan Aug 11, 2026
df9316b
Refactor CI workflow for PHP 8.2 and streamline test execution; add d…
rcalicdan Aug 11, 2026
3718f67
Refactor code style for consistency in SpecialTypeResolver; update ca…
rcalicdan Aug 11, 2026
2f15e6e
Fix typos and improve debug logging in SpecialTypeResolver; correct G…
rcalicdan Aug 11, 2026
941a889
Fix typos and improve type handling in SpecialTypeResolver; correct G…
rcalicdan Aug 11, 2026
777b2a6
Enhance debug logging in SpecialTypeResolver for PHP 8.2 compatibilit…
rcalicdan Aug 11, 2026
fa01170
Enhance debug logging in SpecialTypeResolver; add detailed output for…
rcalicdan Aug 11, 2026
c6c9626
Add support for wrapping class constant array shape keys in quotes fo…
rcalicdan Aug 11, 2026
44bb131
Refactor CI workflow to use matrix strategy for PHP versions and OS; …
rcalicdan Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
<a href="https://phpstan.org/"><img src="https://img.shields.io/badge/PHPStan-Level%20MAX-brightgreen.svg?style=flat" alt="PHPStan Level MAX"></a>
</p>

------
---

TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, setup complex build toolchains, or compile C-extensions and simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime.
TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, set up complex build toolchains, or compile C-extensions. Simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime.


**[Read the full TypePHP documentation »](https://typephp-php.github.io/typephp/)**
Expand All @@ -29,15 +29,17 @@ All the documentation lives on the [typephp-php.github.io/typephp website](https

* [Getting Started & Installation Guide](https://typephp-php.github.io/typephp/getting-started/installation)
* [Quick Start Guide](https://typephp-php.github.io/typephp/getting-started/quick-start)
* [Architecture: How It Works](https://typephp-php.github.io/typephp/architecture/how-it-works)
* [Core Concepts: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts)
* [Core Concepts: Generics & Bounds](https://typephp-php.github.io/typephp/core-concepts/generics-and-bounds)
* [Configuration Guide](https://typephp-php.github.io/typephp/getting-started/configuration)
* [CLI Commands Reference](https://typephp-php.github.io/typephp/getting-started/cli-commands)
* [Enforcement Boundaries: Function Contracts](https://typephp-php.github.io/typephp/core-concepts/function-contracts)
* [Runtime Generics & Bounds](https://typephp-php.github.io/typephp/generics/generics-and-bounds)
* [Supported Types: Arrays & Shapes](https://typephp-php.github.io/typephp/supported-types/arrays-and-shapes)
* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/advanced/troubleshooting)
* [Architecture: How It Works](https://typephp-php.github.io/typephp/advanced/how-it-works)
* [Troubleshooting & FAQ](https://typephp-php.github.io/typephp/troubleshooting)

## Inspiration

TypePHP is conceptually inspired by Python's [Beartype](https://github.com/beartype/beartype), but bringing transparent runtime type enforcement for type annotations to the PHP ecosystem without any decorators or attributes.
TypePHP is conceptually inspired by Python's [Beartype](https://github.com/beartype/beartype), bringing transparent runtime type enforcement for type annotations to the PHP ecosystem without any decorators or attributes.

## Sponsors

Expand Down
43 changes: 37 additions & 6 deletions src/Command/CommandRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

final class CommandRunner
{
private const KNOWN_COMMANDS = [
'config:init',
'cache:clear',
'cache:warm',
'cache:rebuild',
'help',
];

/**
* Parses CLI arguments and routes execution to the corresponding command class.
*
Expand All @@ -15,28 +23,51 @@ final class CommandRunner
*/
public static function run(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
{
$showHelp = \in_array('help', $args, true) || \in_array('typephp:help', $args, true) || \in_array('--help', $args, true) || \in_array('-h', $args, true);
$c = [CliFormatter::class, 'color'];

$showHelp = \in_array('help', $args, true)
|| \in_array('typephp:help', $args, true)
|| \in_array('--help', $args, true)
|| \in_array('-h', $args, true)
|| $args === [];

if ($showHelp || $args === []) {
if ($showHelp) {
return (new HelpCommand())->execute($args, $outputStream, $errorStream);
}

if (\in_array('config:init', $args, true) || \in_array('init', $args, true)) {
$firstArg = $args[0] ?? '';

if ($firstArg === 'config:init' || $firstArg === 'init') {
return (new ConfigInitCommand())->execute($args, $outputStream, $errorStream);
}

if (\in_array('cache:rebuild', $args, true)) {
if ($firstArg === 'cache:rebuild') {
return (new CacheRebuildCommand())->execute($args, $outputStream, $errorStream);
}

if (\in_array('cache:clear', $args, true)) {
if ($firstArg === 'cache:clear') {
return (new CacheClearCommand())->execute($args, $outputStream, $errorStream);
}

if (\in_array('cache:warm', $args, true)) {
if ($firstArg === 'cache:warm') {
return (new CacheWarmCommand())->execute($args, $outputStream, $errorStream);
}

$hasFileExtension = str_contains(basename($firstArg), '.');
$isFileTarget = file_exists($firstArg) || $hasFileExtension;

if (! $isFileTarget) {
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
fwrite($errorStream, ' ' . $c('✗', 'red') . ' Command ' . $c('"' . $firstArg . '"', 'bold') . " is not defined.\n\n");
fwrite($errorStream, ' ' . $c('Did you mean one of these?', 'yellow') . "\n");
foreach (self::KNOWN_COMMANDS as $cmd) {
fwrite($errorStream, ' ' . $c('•', 'cyan') . ' ' . $cmd . "\n");
}
fwrite($errorStream, "\n");

return 1;
}

return (new RunCommand())->execute($args, $outputStream, $errorStream);
}
}
13 changes: 12 additions & 1 deletion src/Command/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
*/
final class RunCommand implements CommandInterface
{
private const VALID_PHP_EXTENSIONS = ['php', 'phtml', 'php5', 'php7', 'php8', 'phps'];

public function execute(array $args, $outputStream = STDOUT, $errorStream = STDERR): int
{
$c = [CliFormatter::class, 'color'];
Expand All @@ -21,6 +23,15 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE
foreach ($args as $arg) {
if (! str_starts_with($arg, '--') && ! str_starts_with($arg, '-')) {
$givenTargetCandidate = $arg;
$ext = strtolower(pathinfo($arg, PATHINFO_EXTENSION));

if ($ext !== '' && ! \in_array($ext, self::VALID_PHP_EXTENSIONS, true)) {
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target file ' . $c('"' . $arg . '"', 'bold') . " is not a PHP script file. TypePHP can only execute PHP files.\n\n");

return 1;
}

if (file_exists($arg)) {
$target = $arg;
}
Expand All @@ -31,7 +42,7 @@ public function execute(array $args, $outputStream = STDOUT, $errorStream = STDE

if ($givenTargetCandidate !== null && $target === null) {
fwrite($errorStream, "\n " . $c(' TYPEPHP ', 'badge_red') . ' ' . $c('Error', 'bold') . "\n\n");
fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n");
fwrite($errorStream, ' ' . $c('✗', 'red') . ' Target script file ' . $c('"' . $givenTargetCandidate . '"', 'bold') . " does not exist or is not readable.\n\n");

return 1;
}
Expand Down
14 changes: 12 additions & 2 deletions src/Internal/ContractVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace TypePHP\Internal;

use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use TypePHP\Internal\Visitor\FunctionContractInjector;
use TypePHP\Internal\Visitor\NodeBuilder;
Expand All @@ -26,9 +27,9 @@ public function __construct()
/**
* Traverses and transforms AST nodes during entry.
*
* @return array<Node>|null
* @return array<Node>|int|null
*/
public function enterNode(Node $node): array|null
public function enterNode(Node $node): array|int|null
{
if ($node instanceof Node\Stmt\Function_
|| $node instanceof Node\Stmt\ClassMethod
Expand All @@ -47,6 +48,15 @@ public function enterNode(Node $node): array|null
}

if ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
$doc = $node->getDocComment();
if ($doc !== null) {
$docText = $doc->getText();
$shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true);
if ($shouldRespectIgnore && (str_contains($docText, '@typephp-ignore') || str_contains($docText, '@typephp-disable'))) {
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}
}

FunctionContractInjector::inject($node);

return null;
Expand Down
2 changes: 2 additions & 0 deletions src/Internal/DocblockNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ final class DocblockNormalizer
*/
public static function normalize(string $doc): string
{
$doc = preg_replace('/(\\\\?[a-zA-Z_\x80-\xff][\\\\a-zA-Z0-9_\x80-\xff]*::[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*(\??:)/', '"$1"$2', $doc) ?? $doc;

if (! str_contains($doc, '{')) {
return $doc;
}
Expand Down
12 changes: 6 additions & 6 deletions src/Internal/Visitor/PropertyHookInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final class PropertyHookInjector
{
public static function process(Node\Stmt\Property $node): void
{
if ($node->hooks === []) {
if (! isset($node->hooks) || ! \is_array($node->hooks) || $node->hooks === []) {
return;
}

Expand Down Expand Up @@ -76,15 +76,15 @@ public function __construct(private string $propertyName)
{
}

public function enterNode(Node $n): int|null
public function enterNode(Node $node): int|null
{
if ($n instanceof Node\Expr\Closure || $n instanceof Node\Expr\ArrowFunction || $n instanceof Node\Stmt\Function_ || $n instanceof Node\Stmt\ClassMethod) {
if ($node instanceof Node\Expr\Closure || $node instanceof Node\Expr\ArrowFunction || $node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassMethod) {
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}

if ($n instanceof Node\Stmt\Return_ && $n->expr !== null) {
$checkCall = NodeBuilder::createPropertyCheckCall($n->expr, new Node\Expr\Variable('this'), $this->propertyName);
$n->expr = NodeBuilder::createTernaryThrowExpr($checkCall);
if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) {
$checkCall = NodeBuilder::createPropertyCheckCall($node->expr, new Node\Expr\Variable('this'), $this->propertyName);
$node->expr = NodeBuilder::createTernaryThrowExpr($checkCall);
}

return null;
Expand Down
Loading
Loading