diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile
new file mode 100644
index 0000000..a3a7de4
--- /dev/null
+++ b/.docker/php/Dockerfile
@@ -0,0 +1,25 @@
+ARG PHP_VERSION=8.3
+
+FROM php:${PHP_VERSION}-alpine
+
+# Install system dependencies
+RUN apk update && apk add --no-cache \
+ $PHPIZE_DEPS \
+ linux-headers \
+ zlib-dev \
+ libmemcached-dev \
+ cyrus-sasl-dev
+
+RUN pecl install xdebug redis memcached \
+ && docker-php-ext-enable xdebug redis memcached
+
+# Copy custom PHP configuration
+COPY .docker/php/kariricode-php.ini /usr/local/etc/php/conf.d/
+
+# Instalação do Composer
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+RUN apk del --purge $PHPIZE_DEPS && rm -rf /var/cache/apk/*
+
+# Mantém o contêiner ativo sem fazer nada
+CMD tail -f /dev/null
diff --git a/.docker/php/kariricode-php.ini b/.docker/php/kariricode-php.ini
new file mode 100644
index 0000000..9e90446
--- /dev/null
+++ b/.docker/php/kariricode-php.ini
@@ -0,0 +1,14 @@
+[PHP]
+memory_limit = 256M
+upload_max_filesize = 50M
+post_max_size = 50M
+date.timezone = America/Sao_Paulo
+
+[Xdebug]
+; zend_extension=xdebug.so
+xdebug.mode=debug
+xdebug.start_with_request=yes
+xdebug.client_host=host.docker.internal
+xdebug.client_port=9003
+xdebug.log=/tmp/xdebug.log
+xdebug.idekey=VSCODE
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e461630
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,3 @@
+KARIRI_APP_ENV=develop
+KARIRI_PHP_VERSION=8.3
+KARIRI_PHP_PORT=9003
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..31f41b6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,17 @@
+/.docker export-ignore
+/.github export-ignore
+/.vscode export-ignore
+/tests export-ignore
+/vendor export-ignore
+/.env export-ignore
+/.env.example export-ignore
+/.gitignore export-ignore
+/.php-cs-fixer.php export-ignore
+/.phpcs-cache export-ignore
+/docker-compose.yml export-ignore
+/phpcs.xml export-ignore
+/phpinsights.php export-ignore
+/phpstan.neon export-ignore
+/phpunit.xml export-ignore
+/psalm.xml export-ignore
+/Makefile export-ignore
\ No newline at end of file
diff --git a/.github/workflows/kariri-ci-cd.yml b/.github/workflows/kariri-ci-cd.yml
new file mode 100644
index 0000000..bd9f272
--- /dev/null
+++ b/.github/workflows/kariri-ci-cd.yml
@@ -0,0 +1,72 @@
+name: Kariri CI Pipeline
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ setup-and-lint:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ php: ["8.3"]
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Cache Composer dependencies
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-composer-
+
+ - name: Set up PHP ${{ matrix.php }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: mbstring, xml
+ tools: composer:v2, php-cs-fixer, phpunit
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Validate composer.json
+ run: composer validate
+
+ - name: Coding Standards Check
+ run: vendor/bin/php-cs-fixer fix --dry-run --diff
+
+ unit-tests:
+ needs: setup-and-lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Download Composer Cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-composer-
+
+ - name: Set up PHP ${{ matrix.php }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: mbstring, xml
+ tools: composer:v2, php-cs-fixer, phpunit
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Run PHPUnit Tests
+ run: XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text
+
+ - name: Security Check
+ run: vendor/bin/security-checker security:check
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5e5baad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,66 @@
+# Arquivos de configuração do sistema
+/.idea/
+*.sublime-project
+*.sublime-workspace
+/.phpunit.result.cache
+/.php_cs.cache
+/.php_cs.dist.cache
+/phpstan.neon.dist
+/phpstan.neon.cache
+/.phpstan.result.cache
+/.phpcs-cache
+
+# Dependências
+/vendor/
+/node_modules/
+
+# Arquivos específicos do sistema operacional
+.DS_Store
+Thumbs.db
+
+# Arquivos de build e compilação
+/build/
+/dist/
+*.log
+*.tlog
+*.tmp
+*.temp
+
+# Arquivos e pastas de ambientes virtuais
+.env
+
+# Arquivos de cache
+/cache/
+*.cache
+*.class
+
+# Arquivos de log
+*.log
+*.sql
+*.sqlite
+
+# Pasta de testes que não devem ser incluídas no repositório
+coverage/
+coverage*
+
+# Arquivos de pacotes
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# Outros arquivos e pastas
+*.swp
+*~
+._*
+temp/
+tmp/
+.vscode/launch.json
+.vscode/extensions.json
+tests/lista_de_arquivos.php
+tests/lista_de_arquivos_test.php
+lista_de_arquivos.txt
+lista_de_arquivos_tests.txt
+add_static_to_providers.php
diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
new file mode 100644
index 0000000..c3a51bb
--- /dev/null
+++ b/.php-cs-fixer.php
@@ -0,0 +1,69 @@
+in(__DIR__ . '/src')
+ ->in(__DIR__ . '/tests')
+ ->exclude('var')
+ ->exclude('config')
+ ->exclude('vendor');
+
+return (new PhpCsFixer\Config())
+ ->setParallelConfig(new PhpCsFixer\Runner\Parallel\ParallelConfig(4, 20))
+ ->setRules([
+ '@PSR12' => true,
+ '@Symfony' => true,
+ 'full_opening_tag' => false,
+ 'phpdoc_var_without_name' => false,
+ 'phpdoc_to_comment' => false,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'concat_space' => ['spacing' => 'one'],
+ 'binary_operator_spaces' => [
+ 'default' => 'single_space',
+ 'operators' => [
+ '=' => 'single_space',
+ '=>' => 'single_space',
+ ],
+ ],
+ 'blank_line_before_statement' => [
+ 'statements' => ['return']
+ ],
+ 'cast_spaces' => ['space' => 'single'],
+ 'class_attributes_separation' => [
+ 'elements' => [
+ 'const' => 'none',
+ 'method' => 'one',
+ 'property' => 'none'
+ ]
+ ],
+ 'declare_equal_normalize' => ['space' => 'none'],
+ 'function_typehint_space' => true,
+ 'lowercase_cast' => true,
+ 'no_unused_imports' => true,
+ 'not_operator_with_successor_space' => true,
+ 'ordered_imports' => true,
+ 'phpdoc_align' => ['align' => 'left'],
+ 'phpdoc_no_alias_tag' => ['replacements' => ['type' => 'var', 'link' => 'see']],
+ 'phpdoc_order' => true,
+ 'phpdoc_scalar' => true,
+ 'single_quote' => true,
+ 'standardize_not_equals' => true,
+ 'trailing_comma_in_multiline' => ['elements' => ['arrays']],
+ 'trim_array_spaces' => true,
+ 'space_after_semicolon' => true,
+ 'no_spaces_inside_parenthesis' => true,
+ 'no_whitespace_before_comma_in_array' => true,
+ 'whitespace_after_comma_in_array' => true,
+ 'visibility_required' => ['elements' => ['const', 'method', 'property']],
+ 'multiline_whitespace_before_semicolons' => [
+ 'strategy' => 'no_multi_line',
+ ],
+ 'method_chaining_indentation' => true,
+ 'class_definition' => [
+ 'single_item_single_line' => false,
+ 'multi_line_extends_each_single_line' => true,
+ ],
+ 'not_operator_with_successor_space' => false
+ ])
+ ->setRiskyAllowed(true)
+ ->setFinder($finder)
+ ->setUsingCache(false);
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..38f7f80
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "[php]": {
+ "editor.defaultFormatter": "junstyle.php-cs-fixer"
+ },
+ "php-cs-fixer.executablePath": "${workspaceFolder}/vendor/bin/php-cs-fixer",
+ "php-cs-fixer.onsave": true,
+ "php-cs-fixer.rules": "@PSR12",
+ "php-cs-fixer.config": ".php_cs.dist",
+ "php-cs-fixer.formatHtml": true
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..33f418c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,174 @@
+# Initial configurations
+PHP_SERVICE := kariricode-property-inspector
+DC := docker-compose
+
+# Command to execute commands inside the PHP container
+EXEC_PHP := $(DC) exec -T php
+
+# Icons
+CHECK_MARK := ✅
+WARNING := ⚠️
+INFO := ℹ️
+
+# Colors
+RED := \033[0;31m
+GREEN := \033[0;32m
+YELLOW := \033[1;33m
+NC := \033[0m # No Color
+
+# Check if Docker is installed
+CHECK_DOCKER := @command -v docker > /dev/null 2>&1 || { echo >&2 "${YELLOW}${WARNING} Docker is not installed. Aborting.${NC}"; exit 1; }
+# Check if Docker Compose is installed
+CHECK_DOCKER_COMPOSE := @command -v docker-compose > /dev/null 2>&1 || { echo >&2 "${YELLOW}${WARNING} Docker Compose is not installed. Aborting.${NC}"; exit 1; }
+# Function to check if the container is running
+CHECK_CONTAINER_RUNNING := @docker ps | grep $(PHP_SERVICE) > /dev/null 2>&1 || { echo >&2 "${YELLOW}${WARNING} The container $(PHP_SERVICE) is not running. Run 'make up' to start it.${NC}"; exit 1; }
+# Check if the .env file exists
+CHECK_ENV := @test -f .env || { echo >&2 "${YELLOW}${WARNING} .env file not found. Run 'make setup-env' to configure.${NC}"; exit 1; }
+
+## setup-env: Copy .env.example to .env if the latter does not exist
+setup-env:
+ @test -f .env || (cp .env.example .env && echo "${GREEN}${CHECK_MARK} .env file created successfully from .env.example${NC}")
+
+check-environment:
+ @echo "${GREEN}${INFO} Checking Docker, Docker Compose, and .env file...${NC}"
+ $(CHECK_DOCKER)
+ $(CHECK_DOCKER_COMPOSE)
+ $(CHECK_ENV)
+
+check-container-running:
+ $(CHECK_CONTAINER_RUNNING)
+
+## up: Start all services in the background
+up: check-environment
+ @echo "${GREEN}${INFO} Starting services...${NC}"
+ @$(DC) up -d
+ @echo "${GREEN}${CHECK_MARK} Services are up!${NC}"
+
+## down: Stop and remove all containers
+down: check-environment
+ @echo "${YELLOW}${INFO} Stopping and removing services...${NC}"
+ @$(DC) down
+ @echo "${GREEN}${CHECK_MARK} Services stopped and removed!${NC}"
+
+## build: Build Docker images
+build: check-environment
+ @echo "${YELLOW}${INFO} Building services...${NC}"
+ @$(DC) build
+ @echo "${GREEN}${CHECK_MARK} Services built!${NC}"
+
+## logs: Show container logs
+logs: check-environment
+ @echo "${YELLOW}${INFO} Container logs:${NC}"
+ @$(DC) logs
+
+## re-build: Rebuild and restart containers
+re-build: check-environment
+ @echo "${YELLOW}${INFO} Stopping and removing current services...${NC}"
+ @$(DC) down
+ @echo "${GREEN}${INFO} Rebuilding services...${NC}"
+ @$(DC) build
+ @echo "${GREEN}${INFO} Restarting services...${NC}"
+ @$(DC) up -d
+ @echo "${GREEN}${CHECK_MARK} Services rebuilt and restarted successfully!${NC}"
+ @$(DC) logs
+
+## shell: Access the shell of the PHP container
+shell: check-environment check-container-running
+ @echo "${GREEN}${INFO} Accessing the shell of the PHP container...${NC}"
+ @$(DC) exec php sh
+
+## composer-install: Install Composer dependencies. Use make composer-install [PKG="[vendor/package [version]]"] [DEV="--dev"]
+composer-install: check-environment check-container-running
+ @echo "${GREEN}${INFO} Installing Composer dependencies...${NC}"
+ @if [ -z "$(PKG)" ]; then \
+ $(EXEC_PHP) composer install; \
+ else \
+ $(EXEC_PHP) composer require $(PKG) $(DEV); \
+ fi
+ @echo "${GREEN}${CHECK_MARK} Composer operation completed!${NC}"
+
+## composer-remove: Remove Composer dependencies. Usage: make composer-remove PKG="vendor/package"
+composer-remove: check-environment check-container-running
+ @if [ -z "$(PKG)" ]; then \
+ echo "${RED}${WARNING} You must specify a package to remove. Usage: make composer-remove PKG=\"vendor/package\"${NC}"; \
+ else \
+ $(EXEC_PHP) composer remove $(PKG); \
+ echo "${GREEN}${CHECK_MARK} Package $(PKG) removed successfully!${NC}"; \
+ fi
+
+## composer-update: Update Composer dependencies
+composer-update: check-environment check-container-running
+ @echo "${GREEN}${INFO} Updating Composer dependencies...${NC}"
+ $(EXEC_PHP) composer update
+ @echo "${GREEN}${CHECK_MARK} Dependencies updated!${NC}"
+
+## test: Run tests
+test: check-environment check-container-running
+ @echo "${GREEN}${INFO} Running tests...${NC}"
+ $(EXEC_PHP) ./vendor/bin/phpunit --testdox --colors=always tests
+ @echo "${GREEN}${CHECK_MARK} Tests completed!${NC}"
+
+## test-file: Run tests on a specific class. Usage: make test-file FILE=[file]
+test-file: check-environment check-container-running
+ @echo "${GREEN}${INFO} Running test for class $(FILE)...${NC}"
+ $(EXEC_PHP) ./vendor/bin/phpunit --testdox --colors=always tests/$(FILE)
+ @echo "${GREEN}${CHECK_MARK} Test for class $(FILE) completed!${NC}"
+
+## coverage: Run test coverage with visual formatting
+coverage: check-environment check-container-running
+ @echo "${GREEN}${INFO} Analyzing test coverage...${NC}"
+ XDEBUG_MODE=coverage $(EXEC_PHP) ./vendor/bin/phpunit --coverage-text --colors=always tests | ccze -A
+
+## coverage-html: Run test coverage and generate HTML report
+coverage-html: check-environment check-container-running
+ @echo "${GREEN}${INFO} Analyzing test coverage and generating HTML report...${NC}"
+ XDEBUG_MODE=coverage $(EXEC_PHP) ./vendor/bin/phpunit --coverage-html ./coverage-report-html tests
+ @echo "${GREEN}${INFO} Test coverage report generated in ./coverage-report-html${NC}"
+
+## run-script: Run a PHP script. Usage: make run-script SCRIPT="path/to/script.php"
+run-script: check-environment check-container-running
+ @echo "${GREEN}${INFO} Running script: $(SCRIPT)...${NC}"
+ $(EXEC_PHP) php $(SCRIPT)
+ @echo "${GREEN}${CHECK_MARK} Script executed!${NC}"
+
+## cs-check: Run PHP_CodeSniffer to check code style
+cs-check: check-environment check-container-running
+ @echo "${GREEN}${INFO} Checking code style...${NC}"
+ $(EXEC_PHP) ./vendor/bin/php-cs-fixer fix --dry-run --diff
+ @echo "${GREEN}${CHECK_MARK} Code style check completed!${NC}"
+
+## cs-fix: Run PHP CS Fixer to fix code style
+cs-fix: check-environment check-container-running
+ @echo "${GREEN}${INFO} Fixing code style with PHP CS Fixer...${NC}"
+ $(EXEC_PHP) ./vendor/bin/php-cs-fixer fix
+ @echo "${GREEN}${CHECK_MARK} Code style fixed!${NC}"
+
+## security-check: Check for security vulnerabilities in dependencies
+security-check: check-environment check-container-running
+ @echo "${GREEN}${INFO} Checking for security vulnerabilities with Security Checker...${NC}"
+ $(EXEC_PHP) ./vendor/bin/security-checker security:check
+ @echo "${GREEN}${CHECK_MARK} Security check completed!${NC}"
+
+## quality: Run all quality commands
+quality: check-environment check-container-running cs-check test security-check
+ @echo "${GREEN}${CHECK_MARK} All quality commands executed!${NC}"
+
+## help: Show initial setup steps and available commands
+help:
+ @echo "${GREEN}Initial setup steps for configuring the project:${NC}"
+ @echo "1. ${YELLOW}Initial environment setup:${NC}"
+ @echo " ${GREEN}${CHECK_MARK} Copy the environment file:${NC} make setup-env"
+ @echo " ${GREEN}${CHECK_MARK} Start the Docker containers:${NC} make up"
+ @echo " ${GREEN}${CHECK_MARK} Install Composer dependencies:${NC} make composer-install"
+ @echo "2. ${YELLOW}Development:${NC}"
+ @echo " ${GREEN}${CHECK_MARK} Access the PHP container shell:${NC} make shell"
+ @echo " ${GREEN}${CHECK_MARK} Run a PHP script:${NC} make run-script SCRIPT=\"script_name.php\""
+ @echo " ${GREEN}${CHECK_MARK} Run the tests:${NC} make test"
+ @echo "3. ${YELLOW}Maintenance:${NC}"
+ @echo " ${GREEN}${CHECK_MARK} Update Composer dependencies:${NC} make composer-update"
+ @echo " ${GREEN}${CHECK_MARK} Clear the application cache:${NC} make cache-clear"
+ @echo " ${RED}${WARNING} Stop and remove all Docker containers:${NC} make down"
+ @echo "\n${GREEN}Available commands:${NC}"
+ @sed -n 's/^##//p' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ": "}; {printf "${YELLOW}%-30s${NC} %s\n", $$1, $$2}'
+
+.PHONY: setup-env up down build logs re-build shell composer-install composer-remove composer-update test test-file coverage coverage-html run-script cs-check cs-fix security-check quality help
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..417779f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,213 @@
+# KaririCode Framework: PropertyInspector Component
+
+[](README.md) [](README.pt-br.md)
+
+  
+
+A powerful and flexible component for inspecting and processing object properties based on custom attributes in the KaririCode Framework, providing advanced features for property validation, sanitization, and analysis in PHP applications.
+
+## Table of Contents
+
+- [Features](#features)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Basic Usage](#basic-usage)
+ - [Advanced Usage](#advanced-usage)
+- [Integration with Other KaririCode Components](#integration-with-other-kariricode-components)
+- [Development and Testing](#development-and-testing)
+- [License](#license)
+- [Support and Community](#support-and-community)
+- [Acknowledgements](#acknowledgements)
+
+## Features
+
+- Easy inspection and processing of object properties based on custom attributes
+- Support for both validation and sanitization of property values
+- Flexible attribute handling through custom attribute handlers
+- Seamless integration with other KaririCode components (Serializer, Validator, Normalizer)
+- Extensible architecture allowing custom attributes and handlers
+- Built on top of the KaririCode\Contract interfaces for maximum flexibility
+
+## Installation
+
+The PropertyInspector component can be easily installed via Composer, which is the recommended dependency manager for PHP projects.
+
+To install the PropertyInspector component in your project, run the following command in your terminal:
+
+```bash
+composer require kariricode/property-inspector
+```
+
+This command will automatically add PropertyInspector to your project and install all necessary dependencies.
+
+### Requirements
+
+- PHP 8.1 or higher
+- Composer
+
+### Manual Installation
+
+If you prefer not to use Composer, you can download the source code directly from the [GitHub repository](https://github.com/KaririCode-Framework/kariricode-property-inspector) and include it manually in your project. However, we strongly recommend using Composer for easier dependency management and updates.
+
+After installation, you can start using PropertyInspector in your PHP project immediately. Make sure to include the Composer autoloader in your script:
+
+```php
+require_once 'vendor/autoload.php';
+```
+
+## Usage
+
+### Basic Usage
+
+1. Define your custom attributes and entity:
+
+```php
+use Attribute;
+
+#[Attribute(Attribute::TARGET_PROPERTY)]
+class Validate
+{
+ public function __construct(public readonly array $rules) {}
+}
+
+#[Attribute(Attribute::TARGET_PROPERTY)]
+class Sanitize
+{
+ public function __construct(public readonly string $method) {}
+}
+
+class User
+{
+ public function __construct(
+ #[Validate(['required', 'string', 'min:3'])]
+ #[Sanitize('trim')]
+ public string $name,
+ #[Validate(['required', 'email'])]
+ #[Sanitize('lowercase')]
+ public string $email,
+ #[Validate(['required', 'integer', 'min:18'])]
+ public int $age
+ ) {}
+}
+```
+
+2. Create a custom attribute handler:
+
+```php
+use KaririCode\PropertyInspector\Contract\PropertyAttributeHandler;
+
+class CustomAttributeHandler implements PropertyAttributeHandler
+{
+ public function handleAttribute(object $object, string $propertyName, object $attribute, mixed $value): ?string
+ {
+ if ($attribute instanceof Validate) {
+ return $this->validate($propertyName, $value, $attribute->rules);
+ }
+ if ($attribute instanceof Sanitize) {
+ return $this->sanitize($value, $attribute->method);
+ }
+ return null;
+ }
+
+ // Implement validate and sanitize methods...
+}
+```
+
+3. Use the PropertyInspector:
+
+```php
+use KaririCode\PropertyInspector\AttributeAnalyzer;
+use KaririCode\PropertyInspector\PropertyInspector;
+
+$attributeAnalyzer = new AttributeAnalyzer(Validate::class);
+$propertyInspector = new PropertyInspector($attributeAnalyzer);
+$handler = new CustomAttributeHandler();
+
+$user = new User('John Doe', 'john@example.com', 25);
+
+$results = $propertyInspector->inspect($user, $handler);
+```
+
+### Advanced Usage
+
+You can create more complex validation and sanitization rules, and even combine the PropertyInspector with other components like the ProcessorPipeline for more advanced processing flows.
+
+## Integration with Other KaririCode Components
+
+The PropertyInspector component is designed to work seamlessly with other KaririCode components:
+
+- **KaririCode\Serializer**: Use PropertyInspector to validate and sanitize data before serialization.
+- **KaririCode\Validator**: Integrate custom validation logic with PropertyInspector attributes.
+- **KaririCode\Normalizer**: Use PropertyInspector to normalize object properties based on attributes.
+
+## Development and Testing
+
+For development and testing purposes, this package uses Docker and Docker Compose to ensure consistency across different environments. A Makefile is provided for convenience.
+
+### Prerequisites
+
+- Docker
+- Docker Compose
+- Make (optional, but recommended for easier command execution)
+
+### Development Setup
+
+1. Clone the repository:
+
+ ```bash
+ git clone https://github.com/KaririCode-Framework/kariricode-property-inspector.git
+ cd kariricode-property-inspector
+ ```
+
+2. Set up the environment:
+
+ ```bash
+ make setup-env
+ ```
+
+3. Start the Docker containers:
+
+ ```bash
+ make up
+ ```
+
+4. Install dependencies:
+ ```bash
+ make composer-install
+ ```
+
+### Available Make Commands
+
+- `make up`: Start all services in the background
+- `make down`: Stop and remove all containers
+- `make build`: Build Docker images
+- `make shell`: Access the PHP container shell
+- `make test`: Run tests
+- `make coverage`: Run test coverage with visual formatting
+- `make cs-fix`: Run PHP CS Fixer to fix code style
+- `make quality`: Run all quality commands (cs-check, test, security-check)
+
+For a full list of available commands, run:
+
+```bash
+make help
+```
+
+## License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+## Support and Community
+
+- **Documentation**: [https://kariricode.org/docs/property-inspector](https://kariricode.org/docs/property-inspector)
+- **Issue Tracker**: [GitHub Issues](https://github.com/KaririCode-Framework/kariricode-property-inspector/issues)
+- **Community**: [KaririCode Club Community](https://kariricode.club)
+
+## Acknowledgements
+
+- The KaririCode Framework team and contributors.
+- Inspired by attribute-based programming and reflection patterns in modern PHP applications.
+
+---
+
+Built with ❤️ by the KaririCode team. Empowering developers to create more robust and flexible PHP applications.
diff --git a/README.pt-br.md b/README.pt-br.md
new file mode 100644
index 0000000..0e949d7
--- /dev/null
+++ b/README.pt-br.md
@@ -0,0 +1,213 @@
+# Framework KaririCode: Componente PropertyInspector
+
+[](README.md) [](README.pt-br.md)
+
+  
+
+Um componente poderoso e flexível para inspecionar e processar propriedades de objetos baseado em atributos personalizados no Framework KaririCode, fornecendo recursos avançados para validação, sanitização e análise de propriedades em aplicações PHP.
+
+## Índice
+
+- [Características](#características)
+- [Instalação](#instalação)
+- [Uso](#uso)
+ - [Uso Básico](#uso-básico)
+ - [Uso Avançado](#uso-avançado)
+- [Integração com Outros Componentes KaririCode](#integração-com-outros-componentes-kariricode)
+- [Desenvolvimento e Testes](#desenvolvimento-e-testes)
+- [Licença](#licença)
+- [Suporte e Comunidade](#suporte-e-comunidade)
+- [Agradecimentos](#agradecimentos)
+
+## Características
+
+- Fácil inspeção e processamento de propriedades de objetos baseados em atributos personalizados
+- Suporte para validação e sanitização de valores de propriedades
+- Manipulação flexível de atributos através de manipuladores de atributos personalizados
+- Integração perfeita com outros componentes KaririCode (Serializer, Validator, Normalizer)
+- Arquitetura extensível permitindo atributos e manipuladores personalizados
+- Construído sobre as interfaces KaririCode\Contract para máxima flexibilidade
+
+## Instalação
+
+O componente PropertyInspector pode ser facilmente instalado via Composer, que é o gerenciador de dependências recomendado para projetos PHP.
+
+Para instalar o componente PropertyInspector em seu projeto, execute o seguinte comando no seu terminal:
+
+```bash
+composer require kariricode/property-inspector
+```
+
+Este comando adicionará automaticamente o PropertyInspector ao seu projeto e instalará todas as dependências necessárias.
+
+### Requisitos
+
+- PHP 8.1 ou superior
+- Composer
+
+### Instalação Manual
+
+Se preferir não usar o Composer, você pode baixar o código-fonte diretamente do [repositório GitHub](https://github.com/KaririCode-Framework/kariricode-property-inspector) e incluí-lo manualmente em seu projeto. No entanto, recomendamos fortemente o uso do Composer para facilitar o gerenciamento de dependências e atualizações.
+
+Após a instalação, você pode começar a usar o PropertyInspector em seu projeto PHP imediatamente. Certifique-se de incluir o autoloader do Composer em seu script:
+
+```php
+require_once 'vendor/autoload.php';
+```
+
+## Uso
+
+### Uso Básico
+
+1. Defina seus atributos personalizados e entidade:
+
+```php
+use Attribute;
+
+#[Attribute(Attribute::TARGET_PROPERTY)]
+class Validate
+{
+ public function __construct(public readonly array $rules) {}
+}
+
+#[Attribute(Attribute::TARGET_PROPERTY)]
+class Sanitize
+{
+ public function __construct(public readonly string $method) {}
+}
+
+class Usuario
+{
+ public function __construct(
+ #[Validate(['required', 'string', 'min:3'])]
+ #[Sanitize('trim')]
+ public string $nome,
+ #[Validate(['required', 'email'])]
+ #[Sanitize('lowercase')]
+ public string $email,
+ #[Validate(['required', 'integer', 'min:18'])]
+ public int $idade
+ ) {}
+}
+```
+
+2. Crie um manipulador de atributos personalizado:
+
+```php
+use KaririCode\PropertyInspector\Contract\PropertyAttributeHandler;
+
+class ManipuladorAtributoPersonalizado implements PropertyAttributeHandler
+{
+ public function handleAttribute(object $object, string $propertyName, object $attribute, mixed $value): ?string
+ {
+ if ($attribute instanceof Validate) {
+ return $this->validar($propertyName, $value, $attribute->rules);
+ }
+ if ($attribute instanceof Sanitize) {
+ return $this->sanitizar($value, $attribute->method);
+ }
+ return null;
+ }
+
+ // Implemente os métodos validar e sanitizar...
+}
+```
+
+3. Use o PropertyInspector:
+
+```php
+use KaririCode\PropertyInspector\AttributeAnalyzer;
+use KaririCode\PropertyInspector\PropertyInspector;
+
+$analisadorAtributos = new AttributeAnalyzer(Validate::class);
+$inspetorPropriedades = new PropertyInspector($analisadorAtributos);
+$manipulador = new ManipuladorAtributoPersonalizado();
+
+$usuario = new Usuario('João Silva', 'joao@exemplo.com', 25);
+
+$resultados = $inspetorPropriedades->inspect($usuario, $manipulador);
+```
+
+### Uso Avançado
+
+Você pode criar regras de validação e sanitização mais complexas e até combinar o PropertyInspector com outros componentes como o ProcessorPipeline para fluxos de processamento mais avançados.
+
+## Integração com Outros Componentes KaririCode
+
+O componente PropertyInspector é projetado para trabalhar perfeitamente com outros componentes KaririCode:
+
+- **KaririCode\Serializer**: Use o PropertyInspector para validar e sanitizar dados antes da serialização.
+- **KaririCode\Validator**: Integre lógica de validação personalizada com atributos do PropertyInspector.
+- **KaririCode\Normalizer**: Use o PropertyInspector para normalizar propriedades de objetos baseadas em atributos.
+
+## Desenvolvimento e Testes
+
+Para fins de desenvolvimento e teste, este pacote usa Docker e Docker Compose para garantir consistência em diferentes ambientes. Um Makefile é fornecido para conveniência.
+
+### Pré-requisitos
+
+- Docker
+- Docker Compose
+- Make (opcional, mas recomendado para facilitar a execução de comandos)
+
+### Configuração de Desenvolvimento
+
+1. Clone o repositório:
+
+ ```bash
+ git clone https://github.com/KaririCode-Framework/kariricode-property-inspector.git
+ cd kariricode-property-inspector
+ ```
+
+2. Configure o ambiente:
+
+ ```bash
+ make setup-env
+ ```
+
+3. Inicie os contêineres Docker:
+
+ ```bash
+ make up
+ ```
+
+4. Instale as dependências:
+ ```bash
+ make composer-install
+ ```
+
+### Comandos Make Disponíveis
+
+- `make up`: Inicia todos os serviços em segundo plano
+- `make down`: Para e remove todos os contêineres
+- `make build`: Constrói imagens Docker
+- `make shell`: Acessa o shell do contêiner PHP
+- `make test`: Executa os testes
+- `make coverage`: Executa a cobertura de testes com formatação visual
+- `make cs-fix`: Executa o PHP CS Fixer para corrigir o estilo do código
+- `make quality`: Executa todos os comandos de qualidade (cs-check, test, security-check)
+
+Para uma lista completa de comandos disponíveis, execute:
+
+```bash
+make help
+```
+
+## Licença
+
+Este projeto está licenciado sob a Licença MIT - veja o arquivo [LICENSE](LICENSE) para detalhes.
+
+## Suporte e Comunidade
+
+- **Documentação**: [https://kariricode.org/docs/property-inspector](https://kariricode.org/docs/property-inspector)
+- **Rastreador de Problemas**: [GitHub Issues](https://github.com/KaririCode-Framework/kariricode-property-inspector/issues)
+- **Comunidade**: [Comunidade KaririCode Club](https://kariricode.club)
+
+## Agradecimentos
+
+- A equipe do Framework KaririCode e colaboradores.
+- Inspirado em padrões de programação baseada em atributos e reflexão em aplicações PHP modernas.
+
+---
+
+Construído com ❤️ pela equipe KaririCode. Capacitando desenvolvedores para criar aplicações PHP mais robustas e flexíveis.
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..4aa7385
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,52 @@
+{
+ "name": "kariricode/property-inspector",
+ "description": "A robust and flexible data sanitization component for PHP, part of the KaririCode Framework, utilizing configurable processors and native functions.",
+ "keywords": [
+ "kariri-code",
+ "property-inspector",
+ "attribute",
+ "reflection",
+ "validation",
+ "normalization",
+ "inspection",
+ "object-properties",
+ "dynamic-analysis",
+ "metadata",
+ "framework",
+ "php8"
+ ],
+ "version": "1.0.0",
+ "homepage": "https://kariricode.org",
+ "type": "library",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Walmir Silva",
+ "email": "community@kariricode.org"
+ }
+ ],
+ "require": {
+ "php": "^8.3"
+ },
+ "autoload": {
+ "psr-4": {
+ "KaririCode\\PropertyInspector\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "KaririCode\\PropertyInspector\\Tests\\": "tests"
+ }
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.51",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^11.0",
+ "squizlabs/php_codesniffer": "^3.9",
+ "enlightn/security-checker": "^2.0"
+ },
+ "support": {
+ "issues": "https://github.com/KaririCode-Framework/kariricode-property-inspector/issues",
+ "source": "https://github.com/KaririCode-Framework/kariricode-property-inspector"
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..45bd296
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,15 @@
+services:
+ php:
+ container_name: kariricode-property-inspector
+ build:
+ context: .
+ dockerfile: .docker/php/Dockerfile
+ args:
+ PHP_VERSION: ${KARIRI_PHP_VERSION}
+ environment:
+ XDEBUG_MODE: coverage
+ volumes:
+ - .:/app
+ working_dir: /app
+ ports:
+ - "${KARIRI_PHP_PORT}:9003"
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..07143a4
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+ src/
+ tests/
+
+
+ vendor/*
+ config/*
+ tests/bootstrap.php
+ tests/object-manager.php
+
+
diff --git a/phpinsights.php b/phpinsights.php
new file mode 100644
index 0000000..5df088e
--- /dev/null
+++ b/phpinsights.php
@@ -0,0 +1,60 @@
+ 'symfony',
+ 'exclude' => [
+ 'src/Migrations',
+ 'src/Kernel.php',
+ ],
+ 'add' => [],
+ 'remove' => [
+ \PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff::class,
+ \NunoMaduro\PhpInsights\Domain\Sniffs\ForbiddenSetterSniff::class,
+ \SlevomatCodingStandard\Sniffs\Commenting\UselessFunctionDocCommentSniff::class,
+ \SlevomatCodingStandard\Sniffs\Commenting\DocCommentSpacingSniff::class,
+ \SlevomatCodingStandard\Sniffs\Classes\SuperfluousInterfaceNamingSniff::class,
+ \SlevomatCodingStandard\Sniffs\Classes\SuperfluousExceptionNamingSniff::class,
+ \SlevomatCodingStandard\Sniffs\ControlStructures\DisallowYodaComparisonSniff::class,
+ \NunoMaduro\PhpInsights\Domain\Insights\ForbiddenTraits::class,
+ \NunoMaduro\PhpInsights\Domain\Insights\ForbiddenNormalClasses::class,
+ \SlevomatCodingStandard\Sniffs\Classes\SuperfluousTraitNamingSniff::class,
+ \SlevomatCodingStandard\Sniffs\Classes\ForbiddenPublicPropertySniff::class,
+ \NunoMaduro\PhpInsights\Domain\Insights\CyclomaticComplexityIsHigh::class,
+ \NunoMaduro\PhpInsights\Domain\Insights\ForbiddenDefineFunctions::class,
+ \NunoMaduro\PhpInsights\Domain\Insights\ForbiddenFinalClasses::class,
+ \NunoMaduro\PhpInsights\Domain\Insights\ForbiddenGlobals::class,
+ \PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff::class,
+ \SlevomatCodingStandard\Sniffs\TypeHints\ReturnTypeHintSniff::class,
+ \SlevomatCodingStandard\Sniffs\Commenting\InlineDocCommentDeclarationSniff::class,
+ \SlevomatCodingStandard\Sniffs\Classes\ModernClassNameReferenceSniff::class,
+ \PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\UselessOverridingMethodSniff::class,
+ \SlevomatCodingStandard\Sniffs\TypeHints\DeclareStrictTypesSniff::class,
+ \SlevomatCodingStandard\Sniffs\TypeHints\ParameterTypeHintSniff::class,
+ \SlevomatCodingStandard\Sniffs\TypeHints\PropertyTypeHintSniff::class,
+ \SlevomatCodingStandard\Sniffs\Arrays\TrailingArrayCommaSniff::class
+ ],
+ 'config' => [
+ \PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff::class => [
+ 'lineLimit' => 120,
+ 'absoluteLineLimit' => 160,
+ ],
+ \SlevomatCodingStandard\Sniffs\Commenting\InlineDocCommentDeclarationSniff::class => [
+ 'exclude' => [
+ 'src/Exception/BaseException.php',
+ ],
+ ],
+ \SlevomatCodingStandard\Sniffs\ControlStructures\AssignmentInConditionSniff::class => [
+ 'enabled' => false,
+ ],
+ ],
+ 'requirements' => [
+ 'min-quality' => 80,
+ 'min-complexity' => 50,
+ 'min-architecture' => 75,
+ 'min-style' => 95,
+ 'disable-security-check' => false,
+ ],
+ 'threads' => null
+];
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..c3392e9
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,7 @@
+parameters:
+ level: max
+ paths:
+ - src
+ - tests
+ ignoreErrors:
+ - '#Method .* has parameter \$.* with no value type specified in iterable type array.#'
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..ba8e7af
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+ src
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..f0c90a3
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AttributeAnalyzer.php b/src/AttributeAnalyzer.php
new file mode 100644
index 0000000..07c7f0d
--- /dev/null
+++ b/src/AttributeAnalyzer.php
@@ -0,0 +1,55 @@
+getProperties() as $property) {
+ $propertyResult = $this->analyzeProperty($object, $property);
+ if (null !== $propertyResult) {
+ $results[$property->getName()] = $propertyResult;
+ }
+ }
+
+ return $results;
+ } catch (\ReflectionException $e) {
+ throw new PropertyInspectionException('Failed to analyze object: ' . $e->getMessage(), 0, $e);
+ } catch (\Error $e) {
+ throw new PropertyInspectionException('An error occurred during object analysis: ' . $e->getMessage(), 0, $e);
+ }
+ }
+
+ private function analyzeProperty(object $object, \ReflectionProperty $property): ?array
+ {
+ $attributes = $property->getAttributes($this->attributeClass, \ReflectionAttribute::IS_INSTANCEOF);
+ if (empty($attributes)) {
+ return null;
+ }
+
+ $property->setAccessible(true);
+ $propertyValue = $property->getValue($object);
+
+ return [
+ 'value' => $propertyValue,
+ 'attributes' => array_map(
+ static fn (\ReflectionAttribute $attr): object => $attr->newInstance(),
+ $attributes
+ ),
+ ];
+ }
+}
diff --git a/src/Contract/AttributeAnalyzer.php b/src/Contract/AttributeAnalyzer.php
new file mode 100644
index 0000000..b6745cc
--- /dev/null
+++ b/src/Contract/AttributeAnalyzer.php
@@ -0,0 +1,19 @@
+}> An associative array with the analysis results
+ */
+ public function analyzeObject(object $object): array;
+}
diff --git a/src/Contract/PropertyAttributeHandler.php b/src/Contract/PropertyAttributeHandler.php
new file mode 100644
index 0000000..b27b5fb
--- /dev/null
+++ b/src/Contract/PropertyAttributeHandler.php
@@ -0,0 +1,20 @@
+> The inspection results
+ */
+ public function inspect(object $object, PropertyAttributeHandler $handler): array;
+}
diff --git a/src/Exception/PropertyInspectionException.php b/src/Exception/PropertyInspectionException.php
new file mode 100644
index 0000000..a6618d6
--- /dev/null
+++ b/src/Exception/PropertyInspectionException.php
@@ -0,0 +1,9 @@
+attributeAnalyzer->analyzeObject($object);
+ $handledResults = [];
+
+ foreach ($analysisResults as $propertyName => $propertyData) {
+ foreach ($propertyData['attributes'] as $attribute) {
+ $result = $handler->handleAttribute($object, $propertyName, $attribute, $propertyData['value']);
+ if (null !== $result) {
+ $handledResults[$propertyName][] = $result;
+ }
+ }
+ }
+
+ return $handledResults;
+ } catch (\ReflectionException $e) {
+ throw new PropertyInspectionException('Failed to analyze object: ' . $e->getMessage(), 0, $e);
+ } catch (\Exception $e) {
+ throw new PropertyInspectionException('An error occurred during object analysis: ' . $e->getMessage(), 0, $e);
+ } catch (\Error $e) {
+ throw new PropertyInspectionException('An error occurred during object analysis: ' . $e->getMessage(), 0, $e);
+ }
+ }
+}
diff --git a/tests/AttributeAnalyzerTest.php b/tests/AttributeAnalyzerTest.php
new file mode 100644
index 0000000..2a41957
--- /dev/null
+++ b/tests/AttributeAnalyzerTest.php
@@ -0,0 +1,122 @@
+analyzer = new AttributeAnalyzer(TestAttribute::class);
+ }
+
+ public function testAnalyzeObject(): void
+ {
+ $object = new TestObject();
+ $result = $this->analyzer->analyzeObject($object);
+
+ $this->assertArrayHasKey('testProperty', $result);
+ $this->assertEquals('test value', $result['testProperty']['value']);
+ $this->assertInstanceOf(TestAttribute::class, $result['testProperty']['attributes'][0]);
+ }
+
+ public function testAnalyzeObjectWithNoAttributes(): void
+ {
+ $object = new class {
+ public string $propertyWithoutAttribute = 'no attribute';
+ };
+
+ $result = $this->analyzer->analyzeObject($object);
+
+ $this->assertEmpty($result);
+ }
+
+ public function testAnalyzeObjectWithPrivateProperty(): void
+ {
+ $object = new TestObject();
+ $result = $this->analyzer->analyzeObject($object);
+
+ $this->assertArrayNotHasKey('privateProperty', $result);
+ }
+
+ public function testReflectionExceptionThrownDuringAnalyzeObject(): void
+ {
+ // Define a fake attribute class for testing
+ $attributeClass = 'FakeAttributeClass';
+
+ // Create the AttributeAnalyzer with the fake attribute class
+ $analyzer = new AttributeAnalyzer($attributeClass);
+
+ // Simulate an object that will trigger a ReflectionException
+ $object = new class {
+ private $inaccessibleProperty;
+
+ public function __construct()
+ {
+ // Simulating an inaccessible property that will cause ReflectionException
+ $this->inaccessibleProperty = null;
+ }
+ };
+
+ // We expect a PropertyInspectionException due to ReflectionException
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('An error occurred during object analysis: Class "FakeAttributeClass" not found');
+
+ // Execute the analyzeObject method, which should trigger the exception
+ $analyzer->analyzeObject($object);
+ }
+
+ public function testErrorThrownDuringAnalyzeProperty(): void
+ {
+ // Define a fake attribute class for testing
+ $attributeClass = 'FakeAttributeClass';
+
+ // Create the AttributeAnalyzer with the fake attribute class
+ $analyzer = new AttributeAnalyzer($attributeClass);
+
+ // Simulate an object that will trigger an Error during property analysis
+ $object = new class {
+ private $errorProperty;
+
+ public function __construct()
+ {
+ // Simulating an error in the property that will cause an Error during reflection
+ $this->errorProperty = null;
+ }
+ };
+
+ // Mock Reflection to throw an error during attribute analysis
+ $reflectionPropertyMock = $this->createMock(\ReflectionProperty::class);
+ $reflectionPropertyMock->method('getAttributes')
+ ->willThrowException(new \Error('Simulated Error'));
+
+ // We expect a PropertyInspectionException due to the Error
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('An error occurred during object analysis: Class "FakeAttributeClass" not found');
+
+ // Execute the analyzeObject method, which should trigger the exception
+ $analyzer->analyzeObject($object);
+ }
+}
diff --git a/tests/Exception/ReflectionExceptionTest.php b/tests/Exception/ReflectionExceptionTest.php
new file mode 100644
index 0000000..2309ff8
--- /dev/null
+++ b/tests/Exception/ReflectionExceptionTest.php
@@ -0,0 +1,96 @@
+createMock(AttributeAnalyzerInterface::class);
+ $handler = $this->createMock(PropertyAttributeHandler::class);
+ $inspector = new PropertyInspector($attributeAnalyzer);
+
+ $object = new \stdClass();
+
+ $attributeAnalyzer->method('analyzeObject')
+ ->willThrowException(new \ReflectionException('Simulated ReflectionException'));
+
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('Failed to analyze object: Simulated ReflectionException');
+
+ $inspector->inspect($object, $handler);
+ }
+
+ public function testReflectionExceptionThrownDuringAnalyzeObject(): void
+ {
+ $attributeAnalyzer = $this->createMock(AttributeAnalyzerInterface::class);
+ $object = new \stdClass();
+
+ $attributeAnalyzer->method('analyzeObject')
+ ->willThrowException(new PropertyInspectionException('Failed to analyze property: Class "FakeAttributeClass" not found'));
+
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('Failed to analyze property: Class "FakeAttributeClass" not found');
+
+ $attributeAnalyzer->analyzeObject($object);
+ }
+
+ public function testReflectionExceptionInAnalyzeObject(): void
+ {
+ $attributeAnalyzer = $this->createMock(AttributeAnalyzerInterface::class);
+ $object = new \stdClass();
+
+ $attributeAnalyzer->method('analyzeObject')
+ ->willThrowException(new \ReflectionException('Test ReflectionException'));
+
+ $inspector = new PropertyInspector($attributeAnalyzer);
+ $handler = $this->createMock(PropertyAttributeHandler::class);
+
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('Failed to analyze object: Test ReflectionException');
+
+ $inspector->inspect($object, $handler);
+ }
+
+ public function testErrorInAnalyzeObject(): void
+ {
+ $attributeAnalyzer = $this->createMock(AttributeAnalyzerInterface::class);
+ $object = new \stdClass();
+
+ $attributeAnalyzer->method('analyzeObject')
+ ->willThrowException(new \Error('Test Error'));
+
+ $inspector = new PropertyInspector($attributeAnalyzer);
+ $handler = $this->createMock(PropertyAttributeHandler::class);
+
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('An error occurred during object analysis: Test Error');
+
+ $inspector->inspect($object, $handler);
+ }
+
+ public function testErrorThrownDuringInspection(): void
+ {
+ $attributeAnalyzer = $this->createMock(AttributeAnalyzerInterface::class);
+ $handler = $this->createMock(PropertyAttributeHandler::class);
+ $inspector = new PropertyInspector($attributeAnalyzer);
+
+ $object = new \stdClass();
+
+ $attributeAnalyzer->method('analyzeObject')
+ ->willThrowException(new \Error('Simulated Error'));
+
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('An error occurred during object analysis: Simulated Error');
+
+ $inspector->inspect($object, $handler);
+ }
+}
diff --git a/tests/PropertyInspectorTest.php b/tests/PropertyInspectorTest.php
new file mode 100644
index 0000000..561c337
--- /dev/null
+++ b/tests/PropertyInspectorTest.php
@@ -0,0 +1,77 @@
+analyzer = $this->createMock(AttributeAnalyzer::class);
+ $this->inspector = new PropertyInspector($this->analyzer);
+ }
+
+ public function testInspect(): void
+ {
+ $object = new \stdClass();
+ $mockHandler = $this->createMock(PropertyAttributeHandler::class);
+
+ $this->analyzer->expects($this->once())
+ ->method('analyzeObject')
+ ->with($object)
+ ->willReturn([
+ 'property1' => [
+ 'value' => 'value1',
+ 'attributes' => [new \stdClass()],
+ ],
+ ]);
+
+ $mockHandler->expects($this->once())
+ ->method('handleAttribute')
+ ->willReturn('handled result');
+
+ $result = $this->inspector->inspect($object, $mockHandler);
+
+ $this->assertEquals(['property1' => ['handled result']], $result);
+ }
+
+ public function testInspectWithNoResults(): void
+ {
+ $object = new \stdClass();
+ $mockHandler = $this->createMock(PropertyAttributeHandler::class);
+
+ $this->analyzer->expects($this->once())
+ ->method('analyzeObject')
+ ->willReturn([]);
+
+ $result = $this->inspector->inspect($object, $mockHandler);
+
+ $this->assertEmpty($result);
+ }
+
+ public function testInspectWithAnalyzerException(): void
+ {
+ $object = new \stdClass();
+ $mockHandler = $this->createMock(PropertyAttributeHandler::class);
+
+ $this->analyzer->expects($this->once())
+ ->method('analyzeObject')
+ ->willThrowException(new PropertyInspectionException('Test exception'));
+
+ $this->expectException(PropertyInspectionException::class);
+ $this->expectExceptionMessage('An error occurred during object analysis: Test exception');
+
+ $this->inspector->inspect($object, $mockHandler);
+ }
+}
diff --git a/tests/application.php b/tests/application.php
new file mode 100644
index 0000000..2f8e4b9
--- /dev/null
+++ b/tests/application.php
@@ -0,0 +1,181 @@
+ $this->validate($propertyName, $value, $attribute->rules),
+ $attribute instanceof Sanitize => $this->sanitize($propertyName, $value, $attribute->method),
+ default => null,
+ };
+ }
+
+ private function validate(string $propertyName, mixed $value, array $rules): ?string
+ {
+ $errors = array_filter(array_map(
+ fn ($rule) => $this->applyValidationRule($propertyName, $value, $rule),
+ $rules
+ ));
+
+ return empty($errors) ? null : implode(' ', $errors);
+ }
+
+ private function applyValidationRule(string $propertyName, mixed $value, string $rule): ?string
+ {
+ return match (true) {
+ 'required' === $rule && empty($value) => "$propertyName is required.",
+ 'string' === $rule && !is_string($value) => "$propertyName must be a string.",
+ str_starts_with($rule, 'min:') => $this->validateMinRule($propertyName, $value, $rule),
+ 'email' === $rule && !filter_var($value, FILTER_VALIDATE_EMAIL) => "$propertyName must be a valid email address.",
+ 'integer' === $rule && !is_int($value) => "$propertyName must be an integer.",
+ default => null,
+ };
+ }
+
+ private function validateMinRule(string $propertyName, mixed $value, string $rule): ?string
+ {
+ $minValue = (int) substr($rule, 4);
+
+ return match (true) {
+ is_string($value) && strlen($value) < $minValue => "$propertyName must be at least $minValue characters long.",
+ is_int($value) && $value < $minValue => "$propertyName must be at least $minValue.",
+ default => null,
+ };
+ }
+
+ private function sanitize(string $propertyName, mixed $value, string $method): string
+ {
+ return match ($method) {
+ 'trim' => trim($value),
+ 'lowercase' => strtolower($value),
+ default => (string) $value,
+ };
+ }
+}
+
+function runApplication(): void
+{
+ $attributeAnalyzer = new AttributeAnalyzer(Validate::class);
+ $propertyInspector = new PropertyInspector($attributeAnalyzer);
+ $handler = new CustomAttributeHandler();
+
+ // Scenario 1: Valid User
+ $validUser = new User(' WaLmir Silva ', 'WALMIR.SILVA@EXAMPLE.COM', 25);
+ processUser($propertyInspector, $handler, $validUser, 'Scenario 1: Valid User');
+
+ // Scenario 2: Invalid User (Age below 18)
+ $underageUser = new User('Walmir Silva', 'walmir@example.com', 16);
+ processUser($propertyInspector, $handler, $underageUser, 'Scenario 2: Underage User');
+
+ // Scenario 3: Invalid User (Empty name and invalid email)
+ $invalidUser = new User('', 'invalid-email', 30);
+ processUser($propertyInspector, $handler, $invalidUser, 'Scenario 3: Invalid User Data');
+
+ // Scenario 4: Non-existent Attribute (to trigger an exception)
+ try {
+ $invalidAttributeAnalyzer = new AttributeAnalyzer('NonExistentAttribute');
+ $invalidPropertyInspector = new PropertyInspector($invalidAttributeAnalyzer);
+ $invalidPropertyInspector->inspect($validUser, $handler);
+ } catch (PropertyInspectionException $e) {
+ echo "\nScenario 4: Non-existent Attribute\n";
+ echo 'Error: ' . $e->getMessage() . "\n";
+ }
+}
+
+function processUser(PropertyInspector $inspector, PropertyAttributeHandler $handler, User $user, string $scenario): void
+{
+ echo "\n$scenario\n";
+ echo 'Original User: ' . json_encode($user) . "\n";
+
+ try {
+ $results = $inspector->inspect($user, $handler);
+ displayResults($results);
+
+ if (empty($results)) {
+ sanitizeUser($user);
+ displaySanitizedUser($user);
+ } else {
+ echo "Validation failed. User was not sanitized.\n";
+ }
+ } catch (PropertyInspectionException $e) {
+ echo "An error occurred during property inspection: {$e->getMessage()}\n";
+ }
+}
+
+function displayResults(array $results): void
+{
+ if (empty($results)) {
+ echo "All properties are valid.\n";
+
+ return;
+ }
+
+ echo "Validation Results:\n";
+ foreach ($results as $propertyName => $propertyResults) {
+ echo "Property: $propertyName\n";
+ foreach ($propertyResults as $result) {
+ if (null !== $result) {
+ echo " - $result\n";
+ }
+ }
+ }
+}
+
+function sanitizeUser(User $user): void
+{
+ $user->name = trim($user->name);
+ $user->email = strtolower($user->email);
+}
+
+function displaySanitizedUser(User $user): void
+{
+ echo "Sanitized User:\n";
+ echo json_encode($user) . "\n";
+}
+
+// Run the application
+runApplication();