Skip to content

devitools/serendipity

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SonarQube Cloud

Reliability Rating Security Rating Quality Gate Status Maintainability Rating

Vulnerabilities Bugs Technical Debt Code Smells

Coverage Duplicated Lines (%) Lines of Code


Serendipity

O componente que faltava no Hyperf

Serendipity é uma biblioteca PHP que estende o framework Hyperf com funcionalidades avançadas de Domain-Driven Design ( DDD), validação inteligente, serialização automática e infraestrutura robusta para aplicações de alta performance.

🍿 Visão Geral

Serendipity preenche as lacunas do ecossistema Hyperf, oferecendo uma camada de abstração poderosa que combina os melhores padrões de desenvolvimento com a performance assíncrona do Hyperf. Utilizando o Constructo como base, oferece metaprogramação avançada para resolver dependências e formatar dados de forma flexível.

Principais Características

  • 🏗️ Arquitetura DDD: Estrutura completa seguindo Domain-Driven Design
  • ⚡ Assíncrono por Padrão: Totalmente compatível com corrotinas do Hyperf
  • 🔍 Validação Inteligente: Sistema de validação baseado em atributos e regras
  • 📊 Serialização Automática: Conversão inteligente de entidades para diferentes formatos
  • 🎯 Type Safety: Tipagem forte com suporte a generics
  • 🧪 Testabilidade: Ferramentas completas para testes unitários e de integração
  • 📈 Observabilidade: Logging estruturado e monitoramento integrado

🚀 Instalação

Pré-requisitos

  • PHP 8.3+
  • Extensões: ds, json, mongodb, pdo, swoole
  • Hyperf 3.1+
  • Docker 25+ (para desenvolvimento)
  • Docker Compose 2.23+

Instalação via Composer

composer require devitools/serendipity

Configuração Básica

Registre o ConfigProvider no seu config/config.php:

<?php

return [
    'providers' => [
        Serendipity\ConfigProvider::class,
    ],
];

Configure as dependências em config/autoload/dependencies.php:

<?php

return [
    \Constructo\Contract\Reflect\TypesFactory::class => 
        \Serendipity\Hyperf\Support\HyperfTypesFactory::class,
    \Constructo\Contract\Reflect\SpecsFactory::class => 
        \Serendipity\Hyperf\Support\HyperfSpecsFactory::class,
];

🎯 Funcionalidades Principais

Entidades com Tipagem Forte

Crie entidades robustas com validação automática e serialização inteligente:

<?php

use Constructo\Support\Reflective\Attribute\Managed;
use Constructo\Support\Reflective\Attribute\Pattern;
use Constructo\Type\Timestamp;

class Game extends GameCommand
{
    public function __construct(
        #[Managed('id')]
        public readonly string $id,
        #[Managed('timestamp')]
        public readonly Timestamp $createdAt,
        #[Managed('timestamp')]
        public readonly Timestamp $updatedAt,
        #[Pattern('/^[a-zA-Z]{1,255}$/')]
        string $name,
        #[Pattern]
        string $slug,
        Timestamp $publishedAt,
        array $data,
        FeatureCollection $features,
    ) {
        parent::__construct(
            name: $name,
            slug: $slug,
            publishedAt: $publishedAt,
            data: $data,
            features: $features,
        );
    }
}

Coleções Tipadas

Trabalhe com coleções type-safe que garantem integridade dos dados:

<?php

use Constructo\Type\Collection;

/**
 * @extends Collection<Feature>
 */
class FeatureCollection extends Collection
{
    public function current(): Feature
    {
        return $this->validate($this->datum());
    }

    protected function validate(mixed $datum): Feature
    {
        return ($datum instanceof Feature)
            ? $datum
            : throw $this->exception(Feature::class, $datum);
    }
}

Validação de Input Inteligente

Sistema de validação integrado com Hyperf que suporta regras complexas:

<?php

use Serendipity\Presentation\Input;

final class HealthInput extends Input
{
    public function rules(): array
    {
        return [
            'message' => 'sometimes|string|max:255',
            'level' => 'required|in:debug,info,warning,error',
            'metadata' => 'array',
        ];
    }
}

Actions com Injeção de Dependência

Crie actions limpas com injeção automática de dependências:

<?php

readonly class HealthAction
{
    public function __invoke(HealthInput $input): array
    {
        return [
            'method' => $input->getMethod(),
            'message' => $input->value('message', 'Sistema funcionando perfeitamente!'),
            'timestamp' => time(),
            'status' => 'healthy'
        ];
    }
}

🏗️ Arquitetura de Projeto com Serendipity

Estrutura recomendada para projetos que utilizam Serendipity, baseada em projetos reais em produção:

/
├── app/                   # Código fonte da aplicação
│   ├── Application/       # Casos de uso da aplicação
│   │   ├── Exception/     # Exceções de aplicação
│   │   └── Service/       # Serviços de aplicação
│   ├── Domain/            # Lógica de negócio pura
│   │   ├── Entity/        # Entidades do domínio
│   │   ├── Enum/          # Enums do domínio
│   │   ├── Provider/      # Provedores de domínio
│   │   ├── Repository/    # Contratos de repositório
│   │   ├── Service/       # Serviços de domínio
│   │   ├── Support/       # Utilitários do domínio
│   │   └── Validator/     # Validadores de negócio
│   ├── Infrastructure/    # Implementações de infraestrutura
│   │   ├── Exception/     # Exceções de infraestrutura
│   │   ├── Parser/        # Parsers de dados
│   │   ├── Repository/    # Implementações de repositório
│   │   ├── Service/       # Serviços de infraestrutura
│   │   ├── Support/       # Utilitários de infraestrutura
│   │   └── Validator/     # Validadores de infraestrutura
│   └── Presentation/      # Camada de apresentação
│       ├── Action/        # Controllers/Actions
│       ├── Input/         # Validação de entrada
│       └── Service/       # Serviços de apresentação
├── bin/                   # Scripts executáveis
│   ├── hyperf.php         # Script principal do Hyperf
│   └── phpunit.php        # Script de testes
├── compose.yml           # Configuração principal do Docker Compose
├── composer.json         # Dependências do Composer
├── composer.lock         # Lock das dependências
├── config/               # Configurações da aplicação
│   └── autoload/         # Configurações carregadas automaticamente
├── deptrac.yaml          # Configuração de análise de dependências
├── Dockerfile            # Configuração do Docker
├── LICENSE               # Licença do projeto
├── makefile              # Comandos de desenvolvimento
├── migrations/           # Migrações do banco de dados
├── phpcs.xml            # Configuração do PHP CodeSniffer
├── phpmd.xml            # Configuração do PHP Mess Detector
├── phpstan.neon         # Configuração do PHPStan
├── phpunit.xml          # Configuração do PHPUnit
├── psalm.xml            # Configuração do Psalm
├── README.md            # Documentação principal
├── rector.php           # Configuração do Rector
├── runtime/             # Arquivos temporários e cache
├── sonar-project.properties # Configuração do SonarQube
├── storage/             # Armazenamento local
└── tests/               # Testes automatizados
    ├── Application/     # Testes de aplicação
    ├── Domain/          # Testes de domínio
    ├── Infrastructure/  # Testes de infraestrutura
    └── Presentation/    # Testes de apresentação

Organização das Camadas

Application Layer - Casos de uso e orquestração

  • Service/: Coordenam operações entre domínio e infraestrutura
  • Exception/: Exceções específicas da camada de aplicação

Domain Layer - Lógica de negócio pura

  • Entity/: Entidades principais do negócio
  • Enum/: Enumerações e constantes do domínio
  • Repository/: Interfaces para persistência
  • Service/: Regras de negócio complexas
  • Validator/: Validações de regras de negócio

Infrastructure Layer - Implementações técnicas

  • Repository/: Implementações concretas dos repositórios
  • Service/: Integrações com APIs externas
  • Parser/: Processamento e transformação de dados
  • Support/: Utilitários técnicos

Presentation Layer - Interface com o mundo externo

  • Action/: Endpoints HTTP e handlers
  • Input/: Validação e sanitização de entrada
  • Service/: Formatação de resposta

Exemplo de Estrutura de Action

<?php

namespace App\Presentation\Action;

use App\Presentation\Input\ProcessLeadInput;
use App\Application\Service\LeadProcessorService;

readonly class ProcessLeadAction
{
    public function __construct(
        private LeadProcessorService $processor
    ) {}

    public function __invoke(ProcessLeadInput $input): array
    {
        $result = $this->processor->process($input->validated());
        
        return [
            'success' => true,
            'data' => $result->toArray(),
        ];
    }
}

📋 Exemplos Práticos

Entidade User com Validação

<?php

namespace App\Domain\Entity;

use Constructo\Support\Reflective\Attribute\Managed;
use Constructo\Support\Reflective\Attribute\Pattern;
use DateTime;

readonly class User
{
    public function __construct(
        #[Managed('id')]
        public int $id,
        #[Pattern('/^[a-zA-Z\s]{2,100}$/')]
        public string $name,
        public DateTime $birthDate,
        public bool $isActive = true,
        public array $tags = [],
    ) {
    }

    public function getAge(): int
    {
        return $this->birthDate->diff(new DateTime())->y;
    }

    public function isAdult(): bool
    {
        return $this->getAge() >= 18;
    }

    public function addTag(string $tag): array
    {
        return [...$this->tags, $tag];
    }
}

Input de Validação para User

<?php

namespace App\Presentation\Input;

use Serendipity\Presentation\Input;

final class CreateUserInput extends Input
{
    public function rules(): array
    {
        return [
            'name' => 'required|string|min:2|max:100|regex:/^[a-zA-Z\s]+$/',
            'birth_date' => 'required|date|before:today',
            'is_active' => 'sometimes|boolean',
            'tags' => 'sometimes|array',
            'tags.*' => 'string|max:50',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|string|min:8|confirmed',
        ];
    }

    public function messages(): array
    {
        return [
            'name.regex' => 'O nome deve conter apenas letras e espaços',
            'birth_date.before' => 'A data de nascimento deve ser anterior a hoje',
            'email.unique' => 'Este email já está em uso',
            'password.confirmed' => 'A confirmação da senha não confere',
        ];
    }
}

Action para Criação de User

<?php

namespace App\Presentation\Action;

use App\Domain\Entity\User;
use App\Presentation\Input\CreateUserInput;
use App\Domain\Service\UserService;
use DateTime;
use Psr\Log\LoggerInterface;

readonly class CreateUserAction
{
    public function __construct(
        private UserService $userService,
        private LoggerInterface $logger
    ) {}

    public function __invoke(CreateUserInput $input): array
    {
        $userData = $input->validated();
        
        $user = new User(
            id: 0, // Será preenchido pelo banco
            name: $userData['name'],
            birthDate: new DateTime($userData['birth_date']),
            isActive: $userData['is_active'] ?? true,
            tags: $userData['tags'] ?? []
        );

        $savedUser = $this->userService->create($user, $userData['password']);

        $this->logger->info('Usuário criado com sucesso', [
            'user_id' => $savedUser->id,
            'name' => $savedUser->name,
            'is_adult' => $savedUser->isAdult(),
        ]);

        return [
            'success' => true,
            'user' => [
                'id' => $savedUser->id,
                'name' => $savedUser->name,
                'age' => $savedUser->getAge(),
                'is_adult' => $savedUser->isAdult(),
                'is_active' => $savedUser->isActive,
                'tags' => $savedUser->tags,
            ],
        ];
    }
}

Serviço de Domínio para User

<?php

namespace App\Domain\Service;

use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use App\Infrastructure\Service\PasswordHashService;

readonly class UserService
{
    public function __construct(
        private UserRepositoryInterface $userRepository,
        private PasswordHashService $passwordService
    ) {}

    public function create(User $user, string $password): User
    {
        // Validações de negócio
        if (!$user->isAdult()) {
            throw new \DomainException('Usuário deve ser maior de idade');
        }

        if (count($user->tags) > 10) {
            throw new \DomainException('Usuário não pode ter mais de 10 tags');
        }

        // Hash da senha
        $hashedPassword = $this->passwordService->hash($password);

        // Persistir no banco
        return $this->userRepository->save($user, $hashedPassword);
    }

    public function updateTags(int $userId, array $newTags): User
    {
        $user = $this->userRepository->findById($userId);
        
        if (!$user) {
            throw new \DomainException('Usuário não encontrado');
        }

        if (count($newTags) > 10) {
            throw new \DomainException('Usuário não pode ter mais de 10 tags');
        }

        return $this->userRepository->updateTags($userId, $newTags);
    }
}

Repositório de User

<?php

namespace App\Domain\Repository;

use App\Domain\Entity\User;

interface UserRepositoryInterface
{
    public function save(User $user, string $hashedPassword): User;
    
    public function findById(int $id): ?User;
    
    public function findByEmail(string $email): ?User;
    
    public function updateTags(int $userId, array $tags): User;
    
    public function findActiveUsers(): array;
    
    public function findUsersByTag(string $tag): array;
}

Implementação do Repositório

<?php

namespace App\Infrastructure\Repository;

use App\Domain\Entity\User;
use App\Domain\Repository\UserRepositoryInterface;
use Hyperf\Database\ConnectionInterface;
use DateTime;

readonly class UserRepository implements UserRepositoryInterface
{
    public function __construct(
        private ConnectionInterface $connection
    ) {}

    public function save(User $user, string $hashedPassword): User
    {
        $id = $this->connection->table('users')->insertGetId([
            'name' => $user->name,
            'email' => $user->email ?? '',
            'password' => $hashedPassword,
            'birth_date' => $user->birthDate->format('Y-m-d'),
            'is_active' => $user->isActive,
            'tags' => json_encode($user->tags),
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return new User(
            id: $id,
            name: $user->name,
            birthDate: $user->birthDate,
            isActive: $user->isActive,
            tags: $user->tags
        );
    }

    public function findById(int $id): ?User
    {
        $userData = $this->connection
            ->table('users')
            ->where('id', $id)
            ->first();

        if (!$userData) {
            return null;
        }

        return new User(
            id: $userData->id,
            name: $userData->name,
            birthDate: new DateTime($userData->birth_date),
            isActive: (bool) $userData->is_active,
            tags: json_decode($userData->tags, true) ?? []
        );
    }

    public function findByEmail(string $email): ?User
    {
        $userData = $this->connection
            ->table('users')
            ->where('email', $email)
            ->first();

        if (!$userData) {
            return null;
        }

        return new User(
            id: $userData->id,
            name: $userData->name,
            birthDate: new DateTime($userData->birth_date),
            isActive: (bool) $userData->is_active,
            tags: json_decode($userData->tags, true) ?? []
        );
    }

    public function updateTags(int $userId, array $tags): User
    {
        $this->connection
            ->table('users')
            ->where('id', $userId)
            ->update([
                'tags' => json_encode($tags),
                'updated_at' => now(),
            ]);

        return $this->findById($userId);
    }

    public function findActiveUsers(): array
    {
        $users = $this->connection
            ->table('users')
            ->where('is_active', true)
            ->get();

        return $users->map(fn($userData) => new User(
            id: $userData->id,
            name: $userData->name,
            birthDate: new DateTime($userData->birth_date),
            isActive: true,
            tags: json_decode($userData->tags, true) ?? []
        ))->toArray();
    }

    public function findUsersByTag(string $tag): array
    {
        $users = $this->connection
            ->table('users')
            ->whereJsonContains('tags', $tag)
            ->get();

        return $users->map(fn($userData) => new User(
            id: $userData->id,
            name: $userData->name,
            birthDate: new DateTime($userData->birth_date),
            isActive: (bool) $userData->is_active,
            tags: json_decode($userData->tags, true) ?? []
        ))->toArray();
    }
}

Coleção Tipada de Users

<?php

namespace App\Domain\Collection;

use Constructo\Type\Collection;
use App\Domain\Entity\User;

/**
 * @extends Collection<User>
 */
class UserCollection extends Collection
{
    public function current(): User
    {
        return $this->validate($this->datum());
    }

    protected function validate(mixed $datum): User
    {
        return ($datum instanceof User)
            ? $datum
            : throw $this->exception(User::class, $datum);
    }

    public function getActiveUsers(): UserCollection
    {
        return new self(
            array_filter($this->items, fn(User $user) => $user->isActive)
        );
    }

    public function getAdultUsers(): UserCollection
    {
        return new self(
            array_filter($this->items, fn(User $user) => $user->isAdult())
        );
    }

    public function getUsersByTag(string $tag): UserCollection
    {
        return new self(
            array_filter($this->items, fn(User $user) => in_array($tag, $user->tags))
        );
    }

    public function getAverageAge(): float
    {
        if ($this->count() === 0) {
            return 0;
        }

        $totalAge = array_sum(
            array_map(fn(User $user) => $user->getAge(), $this->items)
        );

        return $totalAge / $this->count();
    }
}

🧪 Testes

Serendipity fornece ferramentas robustas para testes:

<?php

use Serendipity\Testing\TestCase;
use App\Domain\Entity\User;
use App\Presentation\Input\CreateUserInput;
use App\Presentation\Action\CreateUserAction;
use DateTime;

class CreateUserActionTest extends TestCase
{
    public function testCreateUserSuccess(): void
    {
        $input = new CreateUserInput([
            'name' => 'João Silva',
            'birth_date' => '1990-05-15',
            'email' => '[email protected]',
            'password' => 'senha123456',
            'password_confirmation' => 'senha123456',
            'is_active' => true,
            'tags' => ['desenvolvedor', 'php'],
        ]);

        $action = $this->container()->get(CreateUserAction::class);
        $result = $action($input);

        $this->assertTrue($result['success']);
        $this->assertArrayHasKey('user', $result);
        $this->assertEquals('João Silva', $result['user']['name']);
        $this->assertTrue($result['user']['is_adult']);
        $this->assertTrue($result['user']['is_active']);
        $this->assertContains('desenvolvedor', $result['user']['tags']);
    }

    public function testCreateUserValidationFails(): void
    {
        $this->expectException(\Hyperf\Validation\ValidationException::class);

        $input = new CreateUserInput([
            'name' => '', // Nome vazio
            'birth_date' => '2020-01-01', // Menor de idade
            'email' => 'email-invalido', // Email inválido
            'password' => '123', // Senha muito curta
        ]);

        $input->validated();
    }

    public function testUserEntityMethods(): void
    {
        $user = new User(
            id: 1,
            name: 'Maria Santos',
            birthDate: new DateTime('1985-03-20'),
            isActive: true,
            tags: ['designer', 'ui-ux']
        );

        $this->assertEquals(39, $user->getAge()); // Assumindo 2024
        $this->assertTrue($user->isAdult());
        $this->assertEquals(['designer', 'ui-ux', 'frontend'], $user->addTag('frontend'));
    }
}

class UserServiceTest extends TestCase
{
    public function testCreateUserWithBusinessRules(): void
    {
        $userService = $this->container()->get(\App\Domain\Service\UserService::class);
        
        $user = new User(
            id: 0,
            name: 'Pedro Costa',
            birthDate: new DateTime('1992-08-10'),
            isActive: true,
            tags: ['backend']
        );

        $result = $userService->create($user, 'senhaSegura123');

        $this->assertInstanceOf(User::class, $result);
        $this->assertGreaterThan(0, $result->id);
    }

    public function testCreateMinorUserFails(): void
    {
        $this->expectException(\DomainException::class);
        $this->expectExceptionMessage('Usuário deve ser maior de idade');

        $userService = $this->container()->get(\App\Domain\Service\UserService::class);
        
        $minorUser = new User(
            id: 0,
            name: 'Criança',
            birthDate: new DateTime('2020-01-01'),
            isActive: true,
            tags: []
        );

        $userService->create($minorUser, 'senha123');
    }

    public function testUpdateTagsSuccess(): void
    {
        $userService = $this->container()->get(\App\Domain\Service\UserService::class);
        
        // Mock do usuário existente
        $existingUser = new User(
            id: 1,
            name: 'Ana Silva',
            birthDate: new DateTime('1988-12-05'),
            isActive: true,
            tags: ['old-tag']
        );

        $newTags = ['new-tag', 'another-tag'];
        $result = $userService->updateTags(1, $newTags);

        $this->assertInstanceOf(User::class, $result);
        $this->assertEquals($newTags, $result->tags);
    }
}

class UserCollectionTest extends TestCase
{
    public function testUserCollectionFilters(): void
    {
        $users = [
            new User(1, 'João', new DateTime('1990-01-01'), true, ['php']),
            new User(2, 'Maria', new DateTime('2010-01-01'), true, ['js']), // Menor
            new User(3, 'Pedro', new DateTime('1985-01-01'), false, ['python']), // Inativo
            new User(4, 'Ana', new DateTime('1992-01-01'), true, ['php', 'laravel']),
        ];

        $collection = new \App\Domain\Collection\UserCollection($users);

        // Teste filtro de usuários ativos
        $activeUsers = $collection->getActiveUsers();
        $this->assertCount(3, $activeUsers);

        // Teste filtro de usuários adultos
        $adultUsers = $collection->getAdultUsers();
        $this->assertCount(3, $adultUsers);

        // Teste filtro por tag
        $phpUsers = $collection->getUsersByTag('php');
        $this->assertCount(2, $phpUsers);

        // Teste média de idade
        $averageAge = $collection->getAverageAge();
        $this->assertGreaterThan(0, $averageAge);
    }

    public function testEmptyCollectionAverageAge(): void
    {
        $collection = new \App\Domain\Collection\UserCollection([]);
        $this->assertEquals(0, $collection->getAverageAge());
    }
}

⚡ Performance e Observabilidade

Logging Estruturado

<?php

$this->logger->info('Lead processado com sucesso', [
    'lead_id' => $leadId,
    'source' => $source,
    'processing_time_ms' => $processingTime,
    'memory_usage' => memory_get_usage(true),
]);

Métricas e Monitoramento

<?php

// Integração com sistemas de métricas
use Hyperf\Context\Context;

Context::set('metrics.processing_start', microtime(true));
$result = $this->processLead($input);
$duration = microtime(true) - Context::get('metrics.processing_start');

$this->logger->info('Métrica de performance', [
    'operation' => 'process_lead',
    'duration_ms' => round($duration * 1000, 2),
    'success' => $result->isSuccess(),
]);

🔧 Configuração Avançada

Schema e Especificações

Configure schemas personalizados em config/autoload/schema.php:

<?php

return [
    'specs' => [
        'lead' => [
            'id' => 'string',
            'name' => 'string',
            'email' => 'email',
            'phone' => 'string',
            'created_at' => 'timestamp',
        ],
        'quote' => [
            'id' => 'string',
            'lead_id' => 'string',
            'amount' => 'decimal',
            'status' => 'enum:pending,approved,rejected',
        ],
    ],
];

Middlewares Personalizados

<?php

use Serendipity\Hyperf\Middleware\AbstractMiddleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;

class LeadValidationMiddleware extends AbstractMiddleware
{
    public function process(
        ServerRequestInterface $request, 
        RequestHandlerInterface $handler
    ): ResponseInterface {
        // Validação específica de leads
        $body = $request->getParsedBody();
        
        if (isset($body['email']) && !filter_var($body['email'], FILTER_VALIDATE_EMAIL)) {
            throw new InvalidArgumentException('Email inválido');
        }
        
        return $handler->handle($request);
    }
}

📚 Comandos CLI

Serendipity inclui comandos úteis para desenvolvimento:

# Gerar regras de validação
php bin/hyperf.php gen:rules LeadRules

# Executar health check via CLI
php bin/hyperf.php health:check

# Processar leads em lote
php bin/hyperf.php lead:process-batch

# Limpar caches
php bin/hyperf.php cache:clear

🤝 Contribuindo

Fork o projeto, crie uma branch para sua feature, commit suas mudanças, push para a branch e abra um Pull Request.

Padrões de Desenvolvimento

  • Siga PSR-12 para código PHP
  • Use tipagem forte sempre que possível
  • Implemente testes para novas funcionalidades
  • Documente mudanças no README

📄 Licença

Este projeto está licenciado sob a Licença MIT - veja o arquivo LICENSE para detalhes.

🔗 Links Relacionados


Serendipity - Descobrindo o potencial completo do Hyperf através de componentes elegantes e poderosos.