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.
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.
- 🏗️ 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
- PHP 8.3+
- Extensões: ds, json, mongodb, pdo, swoole
- Hyperf 3.1+
- Docker 25+ (para desenvolvimento)
- Docker Compose 2.23+
composer require devitools/serendipityRegistre o ConfigProvider no seu config/config.php:
<?phpreturn [
'providers' => [
Serendipity\ConfigProvider::class,
],
];Configure as dependências em config/autoload/dependencies.php:
<?phpreturn [
\Constructo\Contract\Reflect\TypesFactory::class => \Serendipity\Hyperf\Support\HyperfTypesFactory::class,
\Constructo\Contract\Reflect\SpecsFactory::class => \Serendipity\Hyperf\Support\HyperfSpecsFactory::class,
];Crie entidades robustas com validação automática e serialização inteligente:
<?phpuseConstructo\Support\Reflective\Attribute\Managed;
useConstructo\Support\Reflective\Attribute\Pattern;
useConstructo\Type\Timestamp;
class Game extends GameCommand
{
publicfunction__construct(
#[Managed('id')]
publicreadonlystring$id,
#[Managed('timestamp')]
publicreadonlyTimestamp$createdAt,
#[Managed('timestamp')]
publicreadonlyTimestamp$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,
);
}
}Trabalhe com coleções type-safe que garantem integridade dos dados:
<?phpuseConstructo\Type\Collection;
/** * @extends Collection<Feature> */class FeatureCollection extends Collection
{
publicfunctioncurrent(): Feature
{
return$this->validate($this->datum());
}
protectedfunctionvalidate(mixed$datum): Feature
{
return ($datuminstanceof Feature)
? $datum
: throw$this->exception(Feature::class, $datum);
}
}Sistema de validação integrado com Hyperf que suporta regras complexas:
<?phpuseSerendipity\Presentation\Input;
finalclass HealthInput extends Input
{
publicfunctionrules(): array
{
return [
'message' => 'sometimes|string|max:255',
'level' => 'required|in:debug,info,warning,error',
'metadata' => 'array',
];
}
}Crie actions limpas com injeção automática de dependências:
<?phpreadonlyclass HealthAction
{
publicfunction__invoke(HealthInput$input): array
{
return [
'method' => $input->getMethod(),
'message' => $input->value('message', 'Sistema funcionando perfeitamente!'),
'timestamp' => time(),
'status' => 'healthy'
];
}
}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
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
<?phpnamespaceApp\Presentation\Action;
useApp\Presentation\Input\ProcessLeadInput;
useApp\Application\Service\LeadProcessorService;
readonlyclass ProcessLeadAction
{
publicfunction__construct(
privateLeadProcessorService$processor
) {}
publicfunction__invoke(ProcessLeadInput$input): array
{
$result = $this->processor->process($input->validated());
return [
'success' => true,
'data' => $result->toArray(),
];
}
}<?phpnamespaceApp\Domain\Entity;
useConstructo\Support\Reflective\Attribute\Managed;
useConstructo\Support\Reflective\Attribute\Pattern;
useDateTime;
readonlyclass User
{
publicfunction__construct(
#[Managed('id')]
publicint$id,
#[Pattern('/^[a-zA-Z\s]{2,100}$/')]
publicstring$name,
publicDateTime$birthDate,
publicbool$isActive = true,
publicarray$tags = [],
) {
}
publicfunctiongetAge(): int
{
return$this->birthDate->diff(newDateTime())->y;
}
publicfunctionisAdult(): bool
{
return$this->getAge() >= 18;
}
publicfunctionaddTag(string$tag): array
{
return [...$this->tags, $tag];
}
}<?phpnamespaceApp\Presentation\Input;
useSerendipity\Presentation\Input;
finalclass CreateUserInput extends Input
{
publicfunctionrules(): 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',
];
}
publicfunctionmessages(): 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',
];
}
}<?phpnamespaceApp\Presentation\Action;
useApp\Domain\Entity\User;
useApp\Presentation\Input\CreateUserInput;
useApp\Domain\Service\UserService;
useDateTime;
usePsr\Log\LoggerInterface;
readonlyclass CreateUserAction
{
publicfunction__construct(
privateUserService$userService,
privateLoggerInterface$logger
) {}
publicfunction__invoke(CreateUserInput$input): array
{
$userData = $input->validated();
$user = newUser(
id: 0, // Será preenchido pelo banco
name: $userData['name'],
birthDate: newDateTime($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,
],
];
}
}<?phpnamespaceApp\Domain\Service;
useApp\Domain\Entity\User;
useApp\Domain\Repository\UserRepositoryInterface;
useApp\Infrastructure\Service\PasswordHashService;
readonlyclass UserService
{
publicfunction__construct(
privateUserRepositoryInterface$userRepository,
privatePasswordHashService$passwordService
) {}
publicfunctioncreate(User$user, string$password): User
{
// Validações de negócioif (!$user->isAdult()) {
thrownew \DomainException('Usuário deve ser maior de idade');
}
if (count($user->tags) > 10) {
thrownew \DomainException('Usuário não pode ter mais de 10 tags');
}
// Hash da senha$hashedPassword = $this->passwordService->hash($password);
// Persistir no bancoreturn$this->userRepository->save($user, $hashedPassword);
}
publicfunctionupdateTags(int$userId, array$newTags): User
{
$user = $this->userRepository->findById($userId);
if (!$user) {
thrownew \DomainException('Usuário não encontrado');
}
if (count($newTags) > 10) {
thrownew \DomainException('Usuário não pode ter mais de 10 tags');
}
return$this->userRepository->updateTags($userId, $newTags);
}
}<?phpnamespaceApp\Domain\Repository;
useApp\Domain\Entity\User;
interface UserRepositoryInterface
{
publicfunctionsave(User$user, string$hashedPassword): User;
publicfunctionfindById(int$id): ?User;
publicfunctionfindByEmail(string$email): ?User;
publicfunctionupdateTags(int$userId, array$tags): User;
publicfunctionfindActiveUsers(): array;
publicfunctionfindUsersByTag(string$tag): array;
}<?phpnamespaceApp\Infrastructure\Repository;
useApp\Domain\Entity\User;
useApp\Domain\Repository\UserRepositoryInterface;
useHyperf\Database\ConnectionInterface;
useDateTime;
readonlyclass UserRepository implements UserRepositoryInterface
{
publicfunction__construct(
privateConnectionInterface$connection
) {}
publicfunctionsave(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(),
]);
returnnewUser(
id: $id,
name: $user->name,
birthDate: $user->birthDate,
isActive: $user->isActive,
tags: $user->tags
);
}
publicfunctionfindById(int$id): ?User
{
$userData = $this->connection
->table('users')
->where('id', $id)
->first();
if (!$userData) {
returnnull;
}
returnnewUser(
id: $userData->id,
name: $userData->name,
birthDate: newDateTime($userData->birth_date),
isActive: (bool) $userData->is_active,
tags: json_decode($userData->tags, true) ?? []
);
}
publicfunctionfindByEmail(string$email): ?User
{
$userData = $this->connection
->table('users')
->where('email', $email)
->first();
if (!$userData) {
returnnull;
}
returnnewUser(
id: $userData->id,
name: $userData->name,
birthDate: newDateTime($userData->birth_date),
isActive: (bool) $userData->is_active,
tags: json_decode($userData->tags, true) ?? []
);
}
publicfunctionupdateTags(int$userId, array$tags): User
{
$this->connection
->table('users')
->where('id', $userId)
->update([
'tags' => json_encode($tags),
'updated_at' => now(),
]);
return$this->findById($userId);
}
publicfunctionfindActiveUsers(): array
{
$users = $this->connection
->table('users')
->where('is_active', true)
->get();
return$users->map(fn($userData) => newUser(
id: $userData->id,
name: $userData->name,
birthDate: newDateTime($userData->birth_date),
isActive: true,
tags: json_decode($userData->tags, true) ?? []
))->toArray();
}
publicfunctionfindUsersByTag(string$tag): array
{
$users = $this->connection
->table('users')
->whereJsonContains('tags', $tag)
->get();
return$users->map(fn($userData) => newUser(
id: $userData->id,
name: $userData->name,
birthDate: newDateTime($userData->birth_date),
isActive: (bool) $userData->is_active,
tags: json_decode($userData->tags, true) ?? []
))->toArray();
}
}<?phpnamespaceApp\Domain\Collection;
useConstructo\Type\Collection;
useApp\Domain\Entity\User;
/** * @extends Collection<User> */class UserCollection extends Collection
{
publicfunctioncurrent(): User
{
return$this->validate($this->datum());
}
protectedfunctionvalidate(mixed$datum): User
{
return ($datuminstanceof User)
? $datum
: throw$this->exception(User::class, $datum);
}
publicfunctiongetActiveUsers(): UserCollection
{
returnnewself(
array_filter($this->items, fn(User$user) => $user->isActive)
);
}
publicfunctiongetAdultUsers(): UserCollection
{
returnnewself(
array_filter($this->items, fn(User$user) => $user->isAdult())
);
}
publicfunctiongetUsersByTag(string$tag): UserCollection
{
returnnewself(
array_filter($this->items, fn(User$user) => in_array($tag, $user->tags))
);
}
publicfunctiongetAverageAge(): float
{
if ($this->count() === 0) {
return0;
}
$totalAge = array_sum(
array_map(fn(User$user) => $user->getAge(), $this->items)
);
return$totalAge / $this->count();
}
}Serendipity fornece ferramentas robustas para testes:
<?phpuseSerendipity\Testing\TestCase;
useApp\Domain\Entity\User;
useApp\Presentation\Input\CreateUserInput;
useApp\Presentation\Action\CreateUserAction;
useDateTime;
class CreateUserActionTest extends TestCase
{
publicfunctiontestCreateUserSuccess(): void
{
$input = newCreateUserInput([
'name' => 'João Silva',
'birth_date' => '1990-05-15',
'email' => 'joao@example.com',
'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']);
}
publicfunctiontestCreateUserValidationFails(): void
{
$this->expectException(\Hyperf\Validation\ValidationException::class);
$input = newCreateUserInput([
'name' => '', // Nome vazio'birth_date' => '2020-01-01', // Menor de idade'email' => 'email-invalido', // Email inválido'password' => '123', // Senha muito curta
]);
$input->validated();
}
publicfunctiontestUserEntityMethods(): void
{
$user = newUser(
id: 1,
name: 'Maria Santos',
birthDate: newDateTime('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
{
publicfunctiontestCreateUserWithBusinessRules(): void
{
$userService = $this->container()->get(\App\Domain\Service\UserService::class);
$user = newUser(
id: 0,
name: 'Pedro Costa',
birthDate: newDateTime('1992-08-10'),
isActive: true,
tags: ['backend']
);
$result = $userService->create($user, 'senhaSegura123');
$this->assertInstanceOf(User::class, $result);
$this->assertGreaterThan(0, $result->id);
}
publicfunctiontestCreateMinorUserFails(): void
{
$this->expectException(\DomainException::class);
$this->expectExceptionMessage('Usuário deve ser maior de idade');
$userService = $this->container()->get(\App\Domain\Service\UserService::class);
$minorUser = newUser(
id: 0,
name: 'Criança',
birthDate: newDateTime('2020-01-01'),
isActive: true,
tags: []
);
$userService->create($minorUser, 'senha123');
}
publicfunctiontestUpdateTagsSuccess(): void
{
$userService = $this->container()->get(\App\Domain\Service\UserService::class);
// Mock do usuário existente$existingUser = newUser(
id: 1,
name: 'Ana Silva',
birthDate: newDateTime('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
{
publicfunctiontestUserCollectionFilters(): void
{
$users = [
newUser(1, 'João', newDateTime('1990-01-01'), true, ['php']),
newUser(2, 'Maria', newDateTime('2010-01-01'), true, ['js']), // MenornewUser(3, 'Pedro', newDateTime('1985-01-01'), false, ['python']), // InativonewUser(4, 'Ana', newDateTime('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);
}
publicfunctiontestEmptyCollectionAverageAge(): void
{
$collection = new \App\Domain\Collection\UserCollection([]);
$this->assertEquals(0, $collection->getAverageAge());
}
}<?php$this->logger->info('Lead processado com sucesso', [
'lead_id' => $leadId,
'source' => $source,
'processing_time_ms' => $processingTime,
'memory_usage' => memory_get_usage(true),
]);<?php// Integração com sistemas de métricasuseHyperf\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(),
]);Configure schemas personalizados em config/autoload/schema.php:
<?phpreturn [
'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',
],
],
];<?phpuseSerendipity\Hyperf\Middleware\AbstractMiddleware;
usePsr\Http\Message\ServerRequestInterface;
usePsr\Http\Message\ResponseInterface;
usePsr\Http\Server\RequestHandlerInterface;
class LeadValidationMiddleware extends AbstractMiddleware
{
publicfunctionprocess(
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)) {
thrownewInvalidArgumentException('Email inválido');
}
return$handler->handle($request);
}
}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:clearFork o projeto, crie uma branch para sua feature, commit suas mudanças, push para a branch e abra um Pull Request.
- Siga PSR-12 para código PHP
- Use tipagem forte sempre que possível
- Implemente testes para novas funcionalidades
- Documente mudanças no README
Este projeto está licenciado sob a Licença MIT - veja o arquivo LICENSE para detalhes.
Serendipity - Descobrindo o potencial completo do Hyperf através de componentes elegantes e poderosos.