Object Calisthenics com Maybe
Object Calisthenics é um conjunto de nove regras de Jeff Bay para escrever código que continua legível conforme cresce. Várias delas são difíceis de seguir em PHP puro — até você ter os tipos certos. As primitivas do Maybe tornam a maioria delas o jeito natural de escrever, não uma disciplina extra que você precisa lembrar.
Este guia vai regra por regra: o que ela pede e como Option, Result, Schema e DTO ajudam a chegar lá. A própria biblioteca segue essas regras (veja src/), então o código que você copia da documentação já lê assim.
1. Um nível de indentação por método
if/foreach aninhados são onde os métodos ficam ilegíveis. match() colapsa uma decisão de dois ramos em uma única expressão, e andThen() substitui a escada de checagens de sucesso aninhadas.
// Antes — dois níveis, e crescendo
function price(?Order $order): int
{
if ($order !== null) {
if ($order->isPaid()) {
return $order->total();
}
}
return 0;
}
// Depois — um nível
function price(Order $order): int
{
return $order->paidTotal()->unwrapOr(0);
}2. Não use else
else quase sempre significa "duas coisas acontecendo aqui". Option e Result te dão exatamente dois ramos com nomes, tratados em um só lugar:
$label = $user->match(
fn (User $u): string => $u->name(),
fn (): string => 'guest'
);Na validação, andThen() encadeia o caminho feliz e faz curto-circuito nas falhas — sem else, sem escada de early-returns.
3. Envolva todos os primitivos e strings
Um string $email ou array $payload cru não carrega regra nenhuma. Schema faz o parse do input não confiável em dados validados na fronteira, e um DTO dá a esses dados um nome e um tipo:
final class SignupData extends DTO
{
public static function schema(): ObjectSchema
{
return Schema::shape([
'email' => Schema::string()->trimmed()->regex('/^[^@\s]+@[^@\s]+\.[^@\s]+$/'),
'age' => Schema::int()->min(18),
]);
}
// ...
}
$result = SignupData::fromArray($request); // Result<SignupData, ValidationErrorBag>Depois da fronteira você passa um SignupData, nunca um array cru.
4. Coleções de primeira classe
Uma classe que envolve um array deve envolver só esse array. O próprio ValidationErrorBag do Maybe é o exemplo: guarda a lista de erros e nada mais, e é iterável e contável em vez de expor o array cru.
foreach ($errors as $error) {
echo $error->describedAs();
}5. Um ponto por linha — com uma exceção deliberada
A regra mira a Lei de Deméter: não alcance através de um objeto para chegar a outro ($order->customer()->address()->zip()). Isso continua valendo evitar.
Mas uma cadeia fluente do Maybe não é isso — cada ->map()->andThen()->match() retorna um novo objeto do mesmo tipo, então você nunca alcança através de colaboradores não relacionados. O Maybe abraça essas cadeias de propósito; elas são o núcleo legível da biblioteca, e este guia as trata como a exceção sancionada à regra 5.
$invoice = loadUser($id)
->andThen(fn (User $u): Result => charge($u))
->map(fn (Charge $c): string => $c->reference())
->unwrapOr('unpaid');6. Não abrevie
Nada específico da biblioteca aqui, mas tipos expressivos removem a pressão de abreviar: você escreve unwrapOr('guest'), não um truque terso de null-coalescing que implora por um comentário // ....
7. Mantenha as entidades pequenas
Tipos pequenos compõem. O Maybe te empurra para muitos objetos pequenos e de propósito único — um schema por campo, um DTO por fronteira, um value object por conceito — em vez de um god-service com cem ramos.
8. No máximo duas variáveis de instância
Essa é a regra que mais código falha, e a correção é sempre a mesma: envolva campos relacionados em um value object. Os internos do schema do Maybe mostram o padrão — StringSchema guarda um TextLength e um TextFormat (dois objetos) em vez de quatro escalares soltos; DateSchema guarda um formato e um DateBounds.
Quando a sua classe quiser um terceiro campo, esse é o sinal de que dois deles pertencem juntos.
9. Sem getters, sem setters
"Tell, Don't Ask": objetos devem fazer coisas, não entregar seus internos para outro inspecionar. ValidationError se renderiza (describedAs()) e se re-parenteia (underField()) — nunca expõe ->path() ou ->message(). A única exceção aceita é a fronteira de serialização: toArray() existe justamente para transformar um objeto em array cru na borda (uma resposta JSON), e em nenhum outro lugar.
// Ask (evite): puxar campos para formatar em outro lugar
$line = $error->path() . ': ' . $error->message(); // esses getters não existem
// Tell (faça): o erro sabe se descrever
$line = $error->describedAs();
// Fronteira de serialização (ok): transformar o bag em corpo de resposta
return json_encode(['errors' => $errors->toArray()]);Option::unwrap() e Result::unwrap() não são getters nesse sentido — são o núcleo intencional e checado desses tipos (e você geralmente deve preferir match()/unwrapOr() a eles, de qualquer forma).
As duas exceções, ditas claramente
Aplicar as regras estritamente ainda deixa dois pontos onde uma leitura purista estaria errada para este domínio:
- Cadeias fluentes (regra 5): permitidas e encorajadas — retornos do mesmo tipo não são violação de Deméter.
toArray()(regra 9): a única fronteira de serialização sancionada; toda outra leitura de internos foi removida.
Todo o resto em src/ segue as regras como escritas. Se você usa um assistente de IA para escrever código com o Maybe, aponte-o para o llms.txt — ele codifica essas mesmas regras para o código gerado herdá-las.