Skip to content

Feat: add PHP 8.4 lazy ghost mapper + fix asymmetric visibility in ClassPropertiesExtractor#552

Open
Shelamkoff wants to merge 8 commits intocycle:2.xfrom
Shelamkoff:2.x
Open

Feat: add PHP 8.4 lazy ghost mapper + fix asymmetric visibility in ClassPropertiesExtractor#552
Shelamkoff wants to merge 8 commits intocycle:2.xfrom
Shelamkoff:2.x

Conversation

@Shelamkoff
Copy link

Summary

Fixes #551 — Cycle ORM v2 incompatible with PHP 8.4 asymmetric visibility.

This PR adds two fixes:

  1. Hot-fix: ClassPropertiesExtractor misclassifies private(set) / protected(set) properties as PUBLIC_CLASS because isPublic() returns true for the read visibility. ClosureHydrator then skips Closure::bind() and the fallback @$entity->$prop = $value silently fails. The fix checks isPrivateSet() / isProtectedSet() (PHP 8.4) before classifying.

  2. Lazy ghost mapper: LazyGhostMapper + LazyGhostEntityFactory — a drop-in alternative to Mapper + ProxyEntityFactory using PHP 8.4 native ReflectionClass::newLazyGhost(). Solves the asymmetric visibility bug, final class limitation, and (array) cast issues with private(set) property names.

Changes

Bug fix: ClassPropertiesExtractor

// Before:
$class = $property->isPublic() ? PropertyMap::PUBLIC_CLASS : $className;

// After:
$class = $property->isPublic() && !$this->hasRestrictedSet($property)
    ? PropertyMap::PUBLIC_CLASS
    : $className;

New files

File Description
src/Mapper/LazyGhost/LazyGhostEntityFactory.php Entity factory using newLazyGhost(), setRawValueWithoutLazyInitialization(), WeakMap for pending refs
src/Mapper/LazyGhostMapper.php Mapper delegating to LazyGhostEntityFactory (same structure as Mapper)

Usage

use Cycle\ORM\Mapper\LazyGhostMapper;

$schema = new Schema([
    'user' => [
        SchemaInterface::ENTITY => User::class,
        SchemaInterface::MAPPER => LazyGhostMapper::class,
        // ...
    ],
]);

Why lazy ghosts over proxies

Problem ProxyEntityFactory LazyGhostEntityFactory
private(set) hydration isPublic() misclassifies → silent failure setRawValueWithoutLazyInitialization() bypasses all visibility
final classes eval() subclass → RuntimeException Ghost IS the original class
Data extraction (array) cast mangles private(set) names ReflectionProperty::getValue()
get_class() Returns proxy class name Returns real entity class

Test plan

  • ClassPropertiesExtractorTest — 5 tests: public, private(set), protected(set), private properties + relation with asymmetric visibility
  • LazyGhostEntityFactoryTest — 16 tests: create, upgrade, extractData, extractRelations, extractAll, lazy initializer resolution
  • Verify existing tests still pass

Shelamkoff and others added 8 commits February 23, 2026 19:14
Enhance visibility check for properties to account for restricted setters.
Implement LazyGhostEntityFactory for PHP 8.4 compatibility.
This class serves as a drop-in replacement for the Mapper, utilizing PHP 8.4 lazy ghost objects for entity creation and hydration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cycle ORM v2 incompatible with PHP 8.4 asymmetric visibility

2 participants