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 + +[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](README.pt-br.md) + +![PHP](https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![PHPUnit](https://img.shields.io/badge/PHPUnit-3776AB?style=for-the-badge&logo=php&logoColor=white) + +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 + +[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](README.pt-br.md) + +![PHP](https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white) ![PHPUnit](https://img.shields.io/badge/PHPUnit-3776AB?style=for-the-badge&logo=php&logoColor=white) + +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();