Skip to content

KaririCode-Framework/kariricode-property-inspector

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

56 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KaririCode PropertyInspector

PHP 8.4+ License: MIT PHPStan Level 9 Tests ARFA KaririCode Framework

Attribute-based property analysis and inspection for the KaririCode Framework —
multi-pass pipelines, reflection caching, and zero-overhead 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
}

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 Deprecated in PHP 8.1, removed in PHP 9; PropertyAccessor handles this ADR-001
clearCache() on interface Enables test isolation and dynamic class reloading ADR-002
Wrapped exception hierarchy Callers catch PropertyInspectionException, not reflection internals ADR-003
Handler-returned values Handler decides the processed value — supports chaining and transformation

Specifications

Spec Covers
SPEC-001 Full pipeline: analysis → handler → mutation

Integration with the KaririCode Ecosystem

PropertyInspector is the reflection engine used internally by other KaririCode components:

Component Role
kariricode/validator Uses PropertyInspector to discover #[Rule] attributes and dispatch to rule processors
kariricode/sanitizer Uses PropertyInspector to discover #[Sanitize] attributes and apply transformers
kariricode/normalizer Uses PropertyInspector for attribute-driven normalization passes

Any component that needs declarative, attribute-based property processing can be built on top of this pipeline.


Project Stats

Metric Value
PHP source files 7
External runtime dependencies 2 (contract · exception)
Test suite 40 tests · 96 assertions
PHPStan level 9
PHP version 8.4+
ARFA compliance 1.3
Test suites Unit + Integration
Reflection cache Per-class, per-AttributeAnalyzer instance

Contributing

git clone https://github.com/KaririCode-Framework/kariricode-property-inspector.git
cd kariricode-property-inspector
composer install
kcode init
kcode quality  # Must pass before opening a PR

License

MIT License © Walmir Silva


Part of the KaririCode Framework ecosystem.

kariricode.org · GitHub · Packagist · Issues

About

Powerful attribute-based property analysis and inspection tool for KaririCode Framework, enabling dynamic validation, normalization, and processing of object properties

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages