') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - devitools/serendipity · GitHub
Skip to content

Repository files navigation

SonarQube Cloud

Reliability RatingSecurity RatingQuality Gate StatusMaintainability Rating

VulnerabilitiesBugsTechnical DebtCode Smells

CoverageDuplicated 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:

<?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,
];

🎯 Funcionalidades Principais

Entidades com Tipagem Forte

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,
);
}
}

Coleções Tipadas

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);
}
}

Validação de Input Inteligente

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',
];
}
}

Actions com Injeção de Dependência

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'
];
}
}

🏗️ 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

<?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(),
];
}
}

📋 Exemplos Práticos

Entidade User com Validação

<?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];
}
}

Input de Validação para User

<?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',
];
}
}

Action para Criação de User

<?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,
],
];
}
}

Serviço de Domínio para User

<?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);
}
}

Repositório de User

<?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;
}

Implementação do Repositório

<?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();
}
}

Coleção Tipada de Users

<?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();
}
}

🧪 Testes

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());
}
}

⚡ 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é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(),
]);

🔧 Configuração Avançada

Schema e Especificações

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',
],
],
];

Middlewares Personalizados

<?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);
}
}

📚 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.

Releases

Packages

Used by

Contributors

Languages