Search by

kariricode / property-inspector

growthcodeoficial

Attribute-based property analysis and inspection for the KaririCode Framework, enabling dynamic validation, normalization, and processing of object properties via PHP 8.4+ attributes and reflection.

Package info

github.com/KaririCode-PHP/property-inspector

Homepage

Issues

pkg:composer/kariricode/property-inspector

Statistics

Installs: 1 134

Dependents: 4

Suggesters: 0

Stars: 0

v2.0.0 2026-03-03 18:29 UTC

README

PHP 8.4+ License: MIT PHPStan level max Tests ARFA KaririCode Framework

Attribute-based property analysis and inspection for the KaririCode Framework —
multi-pass pipelines, reflection caching and reflection-based property mutation, PHP 8.4+.

Installation · Quick Start · Features · Pipeline · Architecture

The Problem

PHP reflection is boilerplate-heavy, error-prone, and slow when repeated across object graphs:

// The old way: raw reflection on every request
$ref = new ReflectionClass($user);
foreach ($ref->getProperties() as $prop) {
    $attrs = $prop->getAttributes(Validate::class);
    foreach ($attrs as $attr) {
        $prop->setAccessible(true); // deprecated in PHP 8.4
        $value = $prop->getValue($user);
        // now what? where does the result go? how do you write it back?
    }
}

No caching, no mutation abstraction, no error isolation, no handler contract — just raw loops you repeat in every project.

The Solution

use KaririCode\PropertyInspector\AttributeAnalyzer;
use KaririCode\PropertyInspector\Utility\PropertyInspector;
use KaririCode\PropertyInspector\Utility\PropertyAccessor;

// 1. Configure which attribute to scan for
$analyzer  = new AttributeAnalyzer(Validate::class);
$inspector = new PropertyInspector($analyzer);

// 2. Inspect — results cached after first call per class
$handler = new MyValidationHandler();
$inspector->inspect($user, $handler);

// 3. Read processed values and errors
$values = $handler->getProcessedPropertyValues();
$errors = $handler->getProcessingResultErrors();

// 4. Write back changed values via PropertyAccessor
$accessor = new PropertyAccessor($user, 'email');
$accessor->setValue(strtolower($accessor->getValue()));

Requirements

Requirement Version
PHP 8.4 or higher
kariricode/contract ^2.8
kariricode/exception ^1.2

Installation

composer require kariricode/property-inspector

Quick Start

Define an attribute, an entity, a handler — and inspect:

<?php

declare(strict_types=1);

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

use Attribute;
use KaririCode\PropertyInspector\AttributeAnalyzer;
use KaririCode\PropertyInspector\Contract\PropertyAttributeHandler;
use KaririCode\PropertyInspector\Utility\PropertyInspector;

// 1. Define a custom attribute
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Validate
{
    public function __construct(public readonly array $rules = []) {}
}

// 2. Define an entity with annotated properties
final class User
{
    public function __construct(
        #[Validate(['required', 'min:3'])]
        public string $name = '',

        #[Validate(['required', 'email'])]
        public string $email = '',

        #[Validate(['required', 'min:18'])]
        public int $age = 0,
    ) {}
}

// 3. Implement a handler
final class ValidationHandler implements PropertyAttributeHandler
{
    private array $processed = [];
    private array $errors    = [];

    public function handleAttribute(string $propertyName, object $attribute, mixed $value): mixed
    {
        $this->processed[$propertyName] = $value;

        if ($attribute instanceof Validate) {
            foreach ($attribute->rules as $rule) {
                if ($rule === 'required' && ($value === '' || $value === null)) {
                    $this->errors[$propertyName]['required'] = 'Field is required';
                }
                if (str_starts_with($rule, 'min:')) {
                    $min = (int) substr($rule, 4);
                    if (is_string($value) && strlen($value) < $min) {
                        $this->errors[$propertyName]['min'] = "Min {$min} chars required";
                    }
                    if (is_int($value) && $value < $min) {
                        $this->errors[$propertyName]['min'] = "Must be at least {$min}";
                    }
                }
            }
        }

        return $value;
    }

    public function getProcessedPropertyValues(): array { return $this->processed; }
    public function getProcessingResultMessages(): array { return []; }
    public function getProcessingResultErrors(): array   { return $this->errors; }
}

// 4. Run the pipeline
$user      = new User(name: 'Walmir', email: 'walmir@kariricode.org', age: 30);
$analyzer  = new AttributeAnalyzer(Validate::class);
$inspector = new PropertyInspector($analyzer);
$handler   = new ValidationHandler();

$inspector->inspect($user, $handler);

var_dump($handler->getProcessedPropertyValues());
// ['name' => 'Walmir', 'email' => 'walmir@kariricode.org', 'age' => 30]

var_dump($handler->getProcessingResultErrors());
// [] — all good

Features

Reflection Caching

AttributeAnalyzer caches reflection metadata after the first analysis per class. Subsequent calls for the same class — even with different object instances — skip ReflectionClass entirely:

$analyzer = new AttributeAnalyzer(Validate::class);
$inspector = new PropertyInspector($analyzer);

// First call: reflection + cache build
$inspector->inspect($user1, $handler1);

// Subsequent calls: metadata from cache — zero reflection overhead
$inspector->inspect($user2, $handler2);
$inspector->inspect($user3, $handler3);

// Force re-analysis when needed (e.g., after metadata change)
$analyzer->clearCache();

Multi-Pass Inspection

Run multiple independent passes over the same object with different attribute types:

// Pass 1: sanitize
$sanitizeInspector = new PropertyInspector(new AttributeAnalyzer(Sanitize::class));
$sanitizeHandler   = new TrimLowercaseHandler();
$sanitizeInspector->inspect($user, $sanitizeHandler);

// Apply sanitized values back to the object
foreach ($sanitizeHandler->getProcessedPropertyValues() as $prop => $value) {
    (new PropertyAccessor($user, $prop))->setValue($value);
}

// Pass 2: validate on sanitized data
$validateInspector = new PropertyInspector(new AttributeAnalyzer(Validate::class));
$validateHandler   = new ValidationHandler();
$validateInspector->inspect($user, $validateHandler);

$errors = $validateHandler->getProcessingResultErrors(); // [] if clean

PropertyAccessor — Safe Property Mutation

Read and write any property (public, protected, private) without setAccessible boilerplate:

use KaririCode\PropertyInspector\Utility\PropertyAccessor;

$accessor = new PropertyAccessor($user, 'email');

$current = $accessor->getValue();           // read
$accessor->setValue(strtolower($current));  // write (no setAccessible needed)

Attribute Polymorphism

AttributeAnalyzer uses ReflectionAttribute::IS_INSTANCEOF — it matches attribute hierarchies, not just exact class names:

// Matches Validate + any subclass of Validate
$analyzer = new AttributeAnalyzer(Validate::class);

Isolated Error Handling

All exceptions from reflection or handler code are wrapped in PropertyInspectionException — your calling code only needs to catch one type:

use KaririCode\PropertyInspector\Exception\PropertyInspectionException;

try {
    $inspector->inspect($user, $handler);
} catch (PropertyInspectionException $e) {
    // ReflectionException, TypeError, Error — all caught and re-wrapped
    // $e->getCode(): 2501–2506, see SPEC-001 §5
}

The message still includes the cause message; do not show it to end users (SECURITY.md).

The Inspection Pipeline

$inspector->inspect($object, $handler)
        │
        ▼
AttributeAnalyzer::analyzeObject($object)
  ├── Check class cache
  ├── If miss: ReflectionClass → getProperties()
  │       └── foreach property:
  │               getAttributes($attributeClass, IS_INSTANCEOF)
  │               newInstance() → cache [{attributes, property}]
  └── extractValues($object): [{value, attributes}]
        │
        ▼
foreach property → foreach attribute:
    $handler->handleAttribute($propertyName, $attribute, $value)
        │
        ▼
return $handler  (accumulates processed values + errors)

Architecture

Source layout

src/
├── AttributeAnalyzer.php      Core analyzer — reflection + cache + attribute extraction
├── Contract/
│   ├── AttributeAnalyzer.php       Interface: analyzeObject · clearCache
│   ├── PropertyAttributeHandler.php Interface: handleAttribute · getProcessed* · getErrors
│   ├── PropertyChangeApplier.php   Interface: applyChanges
│   └── PropertyInspector.php       Interface: inspect
├── Exception/
│   └── PropertyInspectionException.php  Named factory methods per failure mode
└── Utility/
    ├── PropertyAccessor.php   Safe property read/write (private, protected, public)
    └── PropertyInspector.php  Orchestrator: delegates analysis → handler

Key design decisions

Decision Rationale ADR
Reflection cache per class One ReflectionClass call per type, not per instance —
Remove setAccessible No-op since PHP 8.1 ADR-001
clearCache() on interface Callers typed against the contract can bound the cache ADR-002
Wrapped exception hierarchy Callers catch PropertyInspectionException, not reflection internals ADR-003
No double-wrapping Analyzer codes reach the caller intact ADR-004
Code 2506 Attribute-constructor failures are wrapped too ADR-005
Copy-on-write applier (proposed) Readonly DPOs cannot be written in place ADR-006
Toolchain in the repository The Specification prevails over the Devkit ADR-007
object parameters The domain is any object ADR-008
3.0 alignment (proposed) Contract exception family, §18 API, §31 coupling ADR-009
Handler-returned values Handler decides the processed value — supports chaining and transformation —

Specifications

Spec Covers
SPEC-001 Pipeline, contracts, invariants, error codes

Index of all documents: docs/README.md.

Integration with the KaririCode Ecosystem

Published dependents on Packagist (checked 2026-10-02):

Package Requires
kariricode/sanitizer 1.2.3 ^2.0
kariricode/validator 1.0.0 ^1.0

Spec V4.3.3 §31 routes coupling between components through kariricode/contract. The move of these contracts there is planned in ADR-009.

Project Stats

Metric Value
PHP source files 8 (531 lines)
Runtime dependencies 2 (contract · exception)
Tests 67 (unit 47 · integration 5 · conformance 15)
Line coverage 100% (pcov)
Mutation score MSI 100% (36 of 36 killed)
PHPStan / Psalm level max / errorLevel 1, no baseline
Spec / ARFA / ASVS V4.3.3 / 1.3 Level 1 / 5.0 Level 1

Measured locally on 2026-10-27; details in QD-V433.

Contributing

git clone https://github.com/KaririCode-Framework/kariricode-property-inspector.git
cd kariricode-property-inspector
composer install
composer quality     # must pass before opening a PR
composer infection   # MSI >= 80% (needs pcov or Xdebug)

Rules and conventions: CONTRIBUTING.md. Security: SECURITY.md. Changes: CHANGELOG.md.

License

MIT License © Walmir Silva

Part of the KaririCode Framework ecosystem.

kariricode.org · GitHub · Packagist · Issues