Skip to content

[Bug]: Class template name read as a class when the property's declaring class is outside include #104

Closed

Description

@fdaviddpt

Summary

When a property is typed with a class-level template (@var TEntity) and the class that declares it is not in include, an assignment made from an included file is checked against a class literally named TEntity:

Property App\\Fields\\TextInputField::$entity must be of type TEntity, App\\Entities\\Project given

TEntity is an unbound @template (no of), so any value should pass. With the declaring class included, the same assignment passes.

Reproduction (5 files, multi-file so no playground link: the bug needs a split include)

typephp.php

<?php

return ['include' => ['src/Commands/**'], 'array_validation' => 'full'];

src/Fields/FieldBase.php

<?php

declare(strict_types=1);

namespace App\\Fields;

/**
 * @template TEntity
 */
abstract class FieldBase
{
    /**
     * @var TEntity
     */
    public $entity;

    /**
     * @var int<0, max>
     */
    public int $count = 0;
}

src/Fields/InputField.php

<?php

declare(strict_types=1);

namespace App\\Fields;

/**
 * @template TEntity
 * @extends FieldBase<TEntity>
 */
abstract class InputField extends FieldBase
{
}

src/Fields/TextInputField.php

<?php

declare(strict_types=1);

namespace App\\Fields;

class TextInputField extends InputField
{
}

src/Entities/Project.php

<?php

declare(strict_types=1);

namespace App\\Entities;

final class Project
{
}

src/Commands/FieldBuilder.php (the only included file)

<?php

declare(strict_types=1);

namespace App\\Commands;

use App\\Fields\\InputField;

final class FieldBuilder
{
    public function build(string $className, object $entity): InputField
    {
        $field = new $className();
        $field->entity = $entity;
        return $field;
    }

    public function probe(InputField $field): void
    {
        $field->count = -1;
    }
}

run.php

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use App\\Commands\\FieldBuilder;
use App\\Entities\\Project;
use App\\Fields\\TextInputField;

$builder = new FieldBuilder();
try {
    $builder->probe(new TextInputField());
    echo "probe: NOT caught\\n";
} catch (\\Throwable $e) {
    echo "probe: CAUGHT ", $e->getMessage(), "\\n";
}
try {
    $builder->build(TextInputField::class, new Project());
    echo "entity: assigned OK\\n";
} catch (\\Throwable $e) {
    echo "entity: ", $e->getMessage(), "\\n";
}

composer.json: PSR-4 App\\\\ => src/, require typephp/typephp.

Actual

probe: CAUGHT Property App\\Fields\\TextInputField::$count must be >= 0, -1 given
entity: Property App\\Fields\\TextInputField::$entity must be of type TEntity, App\\Entities\\Project given

Expected

probe: CAUGHT Property App\\Fields\\TextInputField::$count must be >= 0, -1 given
entity: assigned OK

Controls

  • include => ['src/**'] (declaring class included too): entity: assigned OK. Only the split include fails.
  • The probe line shows property checks are active on the same object in both runs.

Versions

v0.11.1 and dev-main @ 8c3b01d (2026-10-05), PHP 8.3, opcache.enable_cli=0.

Context

Found in a large codebase where the framework's base field classes live outside the checked paths and module code assigns $field->entity: about 30 violations per module run, all this one shape. I guess the property type is resolved from the docblock without the declaring class's template list when that class was not parsed, so the template name falls through to class resolution. Treating an unresolvable template as its bound (or mixed when unbound) would match PHPStan.

Activity

  1. self-assigned this
    on Oct 6, 2026
  2. linked a pull request that will close this issueInternal improvements 37 #106on Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions