Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4015887
Add PHP 8.x support: enums, typed constants, trait constants, interse…
Copilot Apr 7, 2026
2dcf493
Add comprehensive tests for all PHP 8.x features
Copilot Apr 7, 2026
4ec532d
Address code review feedback: add type hint and use backslash-prefixe…
Copilot Apr 7, 2026
53fd46f
Phase 1: Modernize infrastructure - PHP >=8.1, CI 8.3/8.4, parser fac…
Copilot Apr 7, 2026
9151385
Phase 2: Add PHP 8.0 attributes support to all model classes with #[O…
Copilot Apr 7, 2026
91941db
Phase 3: Add tests for attributes, #[Override], and modern PHP syntax
Copilot Apr 7, 2026
21749ea
Phase 4: Fix README - correct links, add feature support table, remov…
Copilot Apr 7, 2026
2736156
Fix PHP 8.4 deprecation in Dummy3.php and correct pre-existing test e…
Copilot Apr 7, 2026
36d4690
Fix ParseError escaping narrow Exception catches when autoloading PHP…
Copilot Apr 7, 2026
f13eb62
Fix PHP 8.1 uncatchable E_COMPILE_ERROR when autoloading traits with …
Copilot Apr 7, 2026
c40ef74
Fix PHP 8.1 uncatchable E_COMPILE_ERROR from autoloading classes with…
Copilot Apr 7, 2026
6edff4c
Fix FQN resolution for type hints by splitting AST traversal into two…
Copilot Apr 7, 2026
f303d03
Clarify comment on standalone null type detection (PHP 8.2+)
Copilot Apr 7, 2026
9bbb352
Apply PR review feedback: PHP 8.3 guard, pin CI actions, phpstan in r…
Copilot Apr 8, 2026
1b22dd3
Fix critical php-parser v4 compat, typeNodeToString backslash, and re…
Copilot Apr 8, 2026
ea59fa3
Fix testIntersectionTypes CI failure: version-aware assertions for PH…
Copilot Apr 8, 2026
edef06f
Add Scrutinizer coverage upload step to GitHub Actions CI
Copilot Apr 9, 2026
edbe712
Remove Scrutinizer integration (ocular incompatible with symfony/cons…
Copilot Apr 9, 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
24 changes: 10 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ jobs:
fail-fast: false
matrix:
php: [
7.4,
8.0,
8.1,
8.2
8.2,
8.3,
8.4
]
composer: [basic]
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Setup PHP
uses: shivammathur/setup-php@2.25.5
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
with:
Comment on lines 28 to 33
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow switched from pinning actions/checkout/setup-php to a specific commit SHA to using floating tags (@v4, @v2). This weakens supply-chain security because the referenced code can change without review. Prefer pinning third-party actions to an immutable commit SHA (and optionally keeping a comment with the corresponding version).

Copilot uses AI. Check for mistakes.
php-version: ${{ matrix.php }}
coverage: xdebug
Expand All @@ -38,21 +38,17 @@ jobs:

- name: Determine composer cache directory
id: composer-cache
run: echo "::set-output name=directory::$(composer config cache-dir)"
run: echo "directory=$(composer config cache-dir)" >> "$GITHUB_OUTPUT"

- name: Cache composer dependencies
uses: actions/cache@v3.3.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ steps.composer-cache.outputs.directory }}
key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ matrix.php }}-composer-

- name: Install dependencies
run: |
if [[ "${{ matrix.php }}" == "8.0" ]]; then
composer require phpstan/phpstan --no-update
fi;

if [[ "${{ matrix.composer }}" == "lowest" ]]; then
composer update --prefer-dist --no-interaction --prefer-lowest --prefer-stable
fi;
Expand All @@ -70,7 +66,7 @@ jobs:

- name: Run phpstan
continue-on-error: true
if: ${{ matrix.php == '7.4' }}
if: ${{ matrix.php == '8.3' }}
run: |
php vendor/bin/phpstan analyse
Comment on lines 67 to 71
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phpstan step installs dependencies via composer require during CI, which mutates composer.json/composer.lock and makes the run non-reproducible (and can interact poorly with caching). It’s more deterministic to declare phpstan in require-dev (or install it with composer require --no-update plus a composer update that respects the lock), then run vendor/bin/phpstan.

Copilot uses AI. Check for mistakes.

Expand All @@ -82,13 +78,13 @@ jobs:
php-coveralls --coverage_clover=build/logs/clover.xml -v

- name: Upload coverage results to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
files: build/logs/clover.xml

- name: Archive logs artifacts
if: ${{ failure() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: logs_composer-${{ matrix.composer }}_php-${{ matrix.php }}
path: |
Expand Down
3 changes: 0 additions & 3 deletions .scrutinizer.yml

This file was deleted.

62 changes: 56 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@

You can simply scan a string, a file or a full directory and you can see a simple data structure from your php code.
- Classes (**PHPClass**)
- Properties (**PHPProperties**)
- Properties (**PHPProperty**)
- Constants (**PHPConst**)
- Methods (**PHPMethod**)
- Interfaces (**PHPInterface**)
- Traits (**PHPTrait**)
- Enums (**PHPEnum**)
- Functions (**PHPFunction**)
- Parameter (**PHPParameter**)
- Attributes (**PHPAttribute**)

This code is forked from [JetBrains/phpstorm-stubs](https://github.com/JetBrains/phpstorm-stubs/tree/master/tests) but you can't use the classes from "phpstorm-stubs" directly,
because they are in a test namespace and the autoloader is "autoload-dev", so here is a extended version.
Expand All @@ -27,6 +29,31 @@ We will use:
- [phpDocumentor](https://github.com/phpDocumentor/)
- [PHPStan/phpdoc-parser](https://github.com/phpstan/phpdoc-parser)

### Requirements

- PHP >= 8.1

### Supported PHP Features

| Feature | PHP Version | Supported |
|---|---|---|
| Attributes (class, method, property, parameter, constant) | 8.0+ | ✅ |
| Constructor property promotion | 8.0+ | ✅ |
| Union types | 8.0+ | ✅ |
| Named arguments | 8.0+ | ✅ |
| Match expressions | 8.0+ | ✅ |
| Nullsafe operator | 8.0+ | ✅ |
| Enums (unit, string-backed, int-backed) | 8.1+ | ✅ |
| Readonly properties | 8.1+ | ✅ |
| Intersection types | 8.1+ | ✅ |
| `never` return type | 8.1+ | ✅ |
| First-class callable syntax | 8.1+ | ✅ |
| Readonly classes | 8.2+ | ✅ |
| DNF types | 8.2+ | ✅ |
| Standalone `true`, `false`, `null` types | 8.2+ | ✅ |
| Trait constants | 8.2+ | ✅ |
| Typed class constants | 8.3+ | ✅ |
| `#[\Override]` attribute detection | 8.3+ | ✅ |

### Install via "composer require"

Expand Down Expand Up @@ -96,19 +123,42 @@ $phpClasses = $phpCode->getClasses();
var_dump($phpClasses[Dummy::class]); // "PHPClass"-object
````

Access enums:
```php
$phpCode = \voku\SimplePhpParser\Parsers\PhpCodeParser::getPhpFiles(__DIR__ . '/src');
$phpEnums = $phpCode->getEnums();
// PHPEnum objects with scalarType, cases, methods, constants, attributes
````

Access attributes:
```php
$phpCode = \voku\SimplePhpParser\Parsers\PhpCodeParser::getPhpFiles(__DIR__ . '/src');
$phpClasses = $phpCode->getClasses();
$class = $phpClasses['MyClass'];

// Class-level attributes
foreach ($class->attributes as $attr) {
echo $attr->name; // e.g. "MyAttribute"
print_r($attr->arguments); // constructor arguments (array)
}

// Method/property/parameter attributes work the same way
foreach ($class->methods['myMethod']->attributes as $attr) { ... }
foreach ($class->properties['myProp']->attributes as $attr) { ... }
foreach ($class->methods['myMethod']->parameters['param']->attributes as $attr) { ... }
````


### Support

For support and donations please visit [Github](https://github.com/voku/simple_html_dom/) | [Issues](https://github.com/voku/simple_html_dom/issues) | [PayPal](https://paypal.me/moelleken) | [Patreon](https://www.patreon.com/voku).
For support and donations please visit [GitHub](https://github.com/voku/Simple-PHP-Code-Parser/) | [Issues](https://github.com/voku/Simple-PHP-Code-Parser/issues) | [PayPal](https://paypal.me/moelleken) | [Patreon](https://www.patreon.com/voku).

For status updates and release announcements please visit [Releases](https://github.com/voku/simple_html_dom/releases) | [Twitter](https://twitter.com/suckup_de) | [Patreon](https://www.patreon.com/voku/posts).
For status updates and release announcements please visit [Releases](https://github.com/voku/Simple-PHP-Code-Parser/releases) | [Patreon](https://www.patreon.com/voku/posts).

For professional support please contact [me](https://about.me/voku).

### Thanks

- Thanks to [GitHub](https://github.com) (Microsoft) for hosting the code and a good infrastructure including Issues-Managment, etc.
- Thanks to [GitHub](https://github.com) (Microsoft) for hosting the code and a good infrastructure including Issues-Management, etc.
- Thanks to [IntelliJ](https://www.jetbrains.com) as they make the best IDEs for PHP and they gave me an open source license for PhpStorm!
- Thanks to [Travis CI](https://travis-ci.com/) for being the most awesome, easiest continous integration tool out there!
- Thanks to [StyleCI](https://styleci.io/) for the simple but powerfull code style check.
- Thanks to [PHPStan](https://github.com/phpstan/phpstan) && [Psalm](https://github.com/vimeo/psalm) for really great Static analysis tools and for discover bugs in the code!
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@
}
],
"require": {
"php": ">=7.4",
"php": ">=8.1",
"react/async": "~3.0.0 || ~4.1.0",
"react/filesystem": "^0.2@dev",
"phpdocumentor/type-resolver": "~1.7.2",
"phpdocumentor/reflection-docblock": "~5.3",
"phpdocumentor/reflection-common": "~2.2",
"phpstan/phpdoc-parser": "~1.23",
"voku/simple-cache": "~4.1",
"nikic/php-parser": "~4.16"
"nikic/php-parser": "~4.18"
},
"require-dev": {
"phpunit/phpunit": "~6.0 || ~7.0 || ~9.0"
"phpunit/phpunit": "~9.6",
"phpstan/phpstan": "^1.10"
},
"autoload": {
"psr-4": {
Expand Down
7 changes: 7 additions & 0 deletions src/voku/SimplePhpParser/Model/BasePHPClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ abstract class BasePHPClass extends BasePHPElement
*/
public array $constants = [];

/**
* PHP 8.0+ attributes on this class/interface/trait/enum.
*
* @var PHPAttribute[]
*/
public array $attributes = [];

public ?bool $is_final = null;

public ?bool $is_abstract = null;
Expand Down
29 changes: 29 additions & 0 deletions src/voku/SimplePhpParser/Model/PHPAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace voku\SimplePhpParser\Model;

/**
* Represents a single PHP 8.0+ attribute instance.
*/
class PHPAttribute
{
/**
* Fully qualified attribute class name.
*/
public string $name;

/**
* Attribute constructor arguments.
*
* @var array<int|string, mixed>
*/
public array $arguments = [];

public function __construct(string $name, array $arguments = [])
{
$this->name = $name;
$this->arguments = $arguments;
}
}
126 changes: 120 additions & 6 deletions src/voku/SimplePhpParser/Model/PHPClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,26 @@

$this->is_anonymous = $node->isAnonymous();

// Extract PHP 8.0+ attributes
if (!empty($node->attrGroups)) {
$this->attributes = Utils::extractAttributesFromAstNode($node->attrGroups);
}

// PHP < 8.2 raises an uncatchable E_COMPILE_ERROR for certain PHP 8.2+ syntax
// (standalone true/false/null types, DNF types, readonly class). Similarly,
// PHP < 8.3 raises an error for PHP 8.3+ syntax (typed class constants).
// Skip autoloading in those cases; AST data is still read from the node below.
$canAutoload = (\PHP_VERSION_ID >= 80200 || !self::nodeUsesPHP82PlusSyntax($node))
&& (\PHP_VERSION_ID >= 80300 || !self::nodeUsesPHP83PlusSyntax($node));
Comment on lines +62 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic to skip autoloading for PHP 8.2+ or 8.3+ syntax on older PHP versions is correct to avoid uncatchable E_COMPILE_ERROR. However, if the class is already loaded in memory (e.g., via a manual require or a different autoloader), we should still attempt to use reflection. Consider checking class_exists($this->name, false) first.

        $classExists = \class_exists($this->name, false);
        if (!$classExists && $canAutoload) {
            try {
                if (\class_exists($this->name, true)) {
                    $classExists = true;
                }
            } catch (\Throwable $e) {
                // nothing
            }
        }

$classExists = false;
try {
if (\class_exists($this->name, true)) {
$classExists = true;
if ($canAutoload) {
try {
if (\class_exists($this->name, true)) {
$classExists = true;
}
} catch (\Throwable $e) {
// nothing
}
} catch (\Exception $e) {
// nothing
}
if ($classExists) {
$reflectionClass = Utils::createClassReflectionInstance($this->name);
Expand Down Expand Up @@ -156,6 +169,9 @@

$this->is_iterable = $clazz->isIterable();

// Extract PHP 8.0+ attributes
$this->attributes = Utils::extractAttributesFromReflection($clazz);

$parent = $clazz->getParentClass();
if ($parent) {
$this->parentClass = $parent->getName();
Expand All @@ -169,7 +185,7 @@
) {
$classExists = true;
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
// nothing
}
if ($classExists) {
Expand Down Expand Up @@ -442,4 +458,102 @@
$this->parseError[\md5($tmpErrorMessage)] = $tmpErrorMessage;
}
}

/**
* Returns true if the class node uses syntax that requires PHP 8.2+ and would
* cause an uncatchable E_COMPILE_ERROR when autoloaded on PHP < 8.2.
*
* @param Class_ $node
*
* @return bool
*/
private static function nodeUsesPHP82PlusSyntax(Class_ $node): bool

Check warning on line 470 in src/voku/SimplePhpParser/Model/PHPClass.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 5 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=voku_Simple-PHP-Code-Parser&issues=AZ1p4-B2HrVw0w8dl3Zk&open=AZ1p4-B2HrVw0w8dl3Zk&pullRequest=69

Check failure on line 470 in src/voku/SimplePhpParser/Model/PHPClass.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=voku_Simple-PHP-Code-Parser&issues=AZ1p4-B2HrVw0w8dl3Zm&open=AZ1p4-B2HrVw0w8dl3Zm&pullRequest=69
{
// readonly class is PHP 8.2+
if (\method_exists($node, 'isReadOnly') && $node->isReadOnly()) {
return true;
}

foreach ($node->stmts as $stmt) {
if ($stmt instanceof \PhpParser\Node\Stmt\ClassMethod) {
if (self::containsPHP82PlusType($stmt->returnType)) {
return true;
}
foreach ($stmt->params as $param) {
if (self::containsPHP82PlusType($param->type)) {
return true;
}
}
} elseif ($stmt instanceof \PhpParser\Node\Stmt\Property) {
if (self::containsPHP82PlusType($stmt->type)) {
return true;
}
}
}

return false;
}

/**
* Returns true if the class node uses syntax that requires PHP 8.3+ and would
* cause an uncatchable E_COMPILE_ERROR when autoloaded on PHP < 8.3.
*
* Covers: typed class constants (Stmt\ClassConst with a non-null type).
*
* @param Class_ $node
*
* @return bool
*/
private static function nodeUsesPHP83PlusSyntax(Class_ $node): bool
{
foreach ($node->stmts as $stmt) {
// Typed class constants are PHP 8.3+
if ($stmt instanceof \PhpParser\Node\Stmt\ClassConst && $stmt->type !== null) {
return true;
}
}

return false;
}

/**
* Returns true if the given type node is a PHP 8.2+ type that causes an
* uncatchable E_COMPILE_ERROR when loaded on PHP < 8.2.
*
* Covers: standalone true/false/null types and DNF types (union of intersections).
*
* @param \PhpParser\Node|null $typeNode
*
* @return bool
*/
private static function containsPHP82PlusType($typeNode): bool

Check warning on line 529 in src/voku/SimplePhpParser/Model/PHPClass.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This method has 5 returns, which is more than the 3 allowed.

See more on https://sonarcloud.io/project/issues?id=voku_Simple-PHP-Code-Parser&issues=AZ1p4-B2HrVw0w8dl3Zl&open=AZ1p4-B2HrVw0w8dl3Zl&pullRequest=69
{
if ($typeNode === null) {
return false;
}

// Standalone true, false, null as the *sole* type (not in a nullable like ?string)
// are PHP 8.2+ only. PHP-Parser represents these as Identifier nodes (not Name).
// Nullable null (?null) is syntactically invalid; NullableType wraps the inner type.
if ($typeNode instanceof \PhpParser\Node\Identifier) {
$name = \strtolower($typeNode->name);
return $name === 'true' || $name === 'false' || $name === 'null';
}

// DNF types: union type containing an intersection type (PHP 8.2+)
if ($typeNode instanceof \PhpParser\Node\UnionType) {
foreach ($typeNode->types as $t) {
if ($t instanceof \PhpParser\Node\IntersectionType || self::containsPHP82PlusType($t)) {
return true;
}
}
}

// Recurse into nullable type
if ($typeNode instanceof \PhpParser\Node\NullableType) {
return self::containsPHP82PlusType($typeNode->type);
}

return false;
}
}
Loading
Loading