1.4 Estruturas de controle
TL;DR Prefira guard clauses (return cedo) em vez de if aninhado. match em vez de switch (PHP 8.0). while quando você não sabe quantas iterações, for para contador, foreach para arrays. Depois de foreach com &$var, sempre unset($var). break sai do loop, continue pula a iteração. Evite exit/die em controllers. Sempre use declare(strict_types=1).
Conteúdo
- if, elseif, else
- switch vs match
- while, do-while
- for
- foreach
- break, continue
- return, exit, die
- declare (strict_types)
- Recapitulando
- Exercícios práticos
if, elseif, else
O que é: Execução condicional de código.
Como funciona:
$age = 25;
if ($age < 18) {
echo 'Menor de idade';
} elseif ($age < 65) {
echo 'Adulto';
} else {
echo 'Aposentado';
}
// Sem chaves (não recomendado)
if ($isActive)
echo 'Ativo';
// Com chaves (recomendado)
if ($isActive) {
echo 'Ativo';
}
// Sintaxe alternativa (para templates)
<?php if ($user->isAdmin()): ?>
<div>Painel do administrador</div>
<?php endif; ?>Quando usar: Qualquer checagem condicional.
Exemplo prático:
// Checar permissão
public function update(Request $request, Post $post)
{
if (!auth()->check()) {
abort(401, 'Autenticação necessária');
}
if (!Gate::allows('update', $post)) {
abort(403, 'Sem permissão para editar');
}
$post->update($request->validated());
return response()->json($post);
}
// Guard clauses (saída antecipada)
public function process(?User $user)
{
// BOM (guard clause)
if ($user === null) {
return;
}
if (!$user->isActive()) {
throw new InactiveUserException();
}
// Lógica principal
$this->doSomething($user);
}
// RUIM (aninhamento profundo)
public function processBad(?User $user)
{
if ($user !== null) {
if ($user->isActive()) {
// Lógica principal no 3º nível de aninhamento
$this->doSomething($user);
}
}
}Na entrevista:
"if-elseif-else para condições. Prefiro guard clauses (return cedo) em vez de if aninhado. Em template eu uso a sintaxe alternativa (if: ... endif;)."
switch vs match
O que é: Escolha múltipla com base em um valor.
Como funciona:
// switch (jeito antigo)
$status = 'active';
switch ($status) {
case 'active':
$message = 'Ativo';
break;
case 'pending':
$message = 'Pendente';
break;
case 'blocked':
$message = 'Bloqueado';
break;
default:
$message = 'Desconhecido';
}
// ⚠️ Sem break — executa todos os case abaixo (fall-through)
switch ($role) {
case 'admin':
$permissions[] = 'delete';
case 'editor':
$permissions[] = 'edit';
case 'viewer':
$permissions[] = 'view';
break;
}
// Se $role = 'editor' → $permissions = ['edit', 'view']
// match (PHP 8.0+)
$message = match($status) {
'active' => 'Ativo',
'pending' => 'Pendente',
'blocked' => 'Bloqueado',
default => 'Desconhecido',
};
// Vários valores
$httpCategory = match($statusCode) {
200, 201, 204 => 'success',
400, 401, 403, 404 => 'client_error',
500, 502, 503 => 'server_error',
default => 'unknown',
};Quando usar:
match— sempre que der (PHP 8.0+)switch— código legado ou lógica pesada dentro do case
Exemplo prático:
// Tratar status HTTP
$result = match($response->status()) {
200 => $response->json(),
201 => ['message' => 'Created', 'id' => $response->json('id')],
400 => throw new BadRequestException($response->body()),
401 => throw new UnauthorizedException(),
404 => throw new NotFoundException(),
default => throw new HttpException($response->status()),
};
// Permissões
$canEdit = match(true) {
$user->isAdmin() => true,
$user->owns($post) && !$post->isPublished() => true,
default => false,
};
// Enum (PHP 8.1)
enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Delivered = 'delivered';
case Cancelled = 'cancelled';
}
$badge = match($order->status) {
OrderStatus::Pending => 'badge-warning',
OrderStatus::Paid => 'badge-info',
OrderStatus::Shipped => 'badge-primary',
OrderStatus::Delivered => 'badge-success',
OrderStatus::Cancelled => 'badge-danger',
};Na entrevista:
"switch é escolha múltipla com break. match (PHP 8.0) é o switch melhorado: comparação estrita, devolve valor, não precisa de break. Prefiro match quando vou devolver um valor."
while, do-while
O que é: Loops com condição.
Como funciona:
// while — checa ANTES de executar
$i = 0;
while ($i < 5) {
echo $i;
$i++;
}
// Imprime: 01234
// do-while — checa DEPOIS de executar (roda pelo menos 1 vez)
$i = 10;
do {
echo $i;
$i++;
} while ($i < 5);
// Imprime: 10 (mesmo com condição false, rodou 1 vez)
// Loop infinito
while (true) {
$job = $queue->pop();
if ($job === null) {
break; // Sai do loop
}
$job->handle();
}Quando usar:
while— quando você não sabe quantas iterações vêmdo-while— quando precisa rodar pelo menos 1 vezfor/foreach— quando você já sabe a quantidade
Exemplo prático:
// Ler arquivo grande linha a linha
$handle = fopen('large-file.csv', 'r');
while (($line = fgets($handle)) !== false) {
$data = str_getcsv($line);
// Processa a linha
$this->processRow($data);
}
fclose($handle);
// Queue worker
while (true) {
$job = $this->queue->pop();
if ($job === null) {
usleep(100000); // 100ms
continue;
}
try {
$job->handle();
$this->queue->acknowledge($job);
} catch (\Exception $e) {
$this->queue->reject($job);
$this->logger->error($e);
}
}
// Retry
$attempts = 0;
$maxAttempts = 3;
while ($attempts < $maxAttempts) {
try {
$result = $this->apiClient->request();
break; // Sucesso — sai
} catch (ApiException $e) {
$attempts++;
if ($attempts >= $maxAttempts) {
throw $e;
}
sleep(2); // Espera antes de tentar de novo
}
}Na entrevista:
"while checa a condição ANTES de executar, do-while DEPOIS (pelo menos 1 vez). Uso para retry, ler arquivo linha a linha, queue worker."
for
O que é: Loop com contador.
Como funciona:
// for (inicialização; condição; incremento)
for ($i = 0; $i < 5; $i++) {
echo $i;
}
// Imprime: 01234
// Ordem inversa
for ($i = 5; $i > 0; $i--) {
echo $i;
}
// Imprime: 54321
// Passo 2
for ($i = 0; $i <= 10; $i += 2) {
echo $i;
}
// Imprime: 0246810
// Várias variáveis
for ($i = 0, $j = 10; $i < $j; $i++, $j--) {
echo "$i-$j ";
}
// Imprime: 0-10 1-9 2-8 3-7 4-6
// Loop infinito
for (;;) {
// Roda para sempre
if ($shouldStop) {
break;
}
}Quando usar: Quando você precisa de contador ou já sabe quantas iterações são.
Exemplo prático:
// Gerar números de página
$totalPages = 10;
$currentPage = 5;
for ($i = 1; $i <= $totalPages; $i++) {
if ($i === $currentPage) {
echo "<span class='active'>$i</span> ";
} else {
echo "<a href='?page=$i'>$i</a> ";
}
}
// Processamento em batch
$total = User::count(); // 100 000
$batchSize = 1000;
for ($offset = 0; $offset < $total; $offset += $batchSize) {
$users = User::skip($offset)->take($batchSize)->get();
foreach ($users as $user) {
// Processa
$this->processUser($user);
}
// Libera memória
unset($users);
}
// Gerar intervalo de datas
$start = new DateTime('2024-01-01');
$end = new DateTime('2024-01-31');
$dates = [];
for ($date = clone $start; $date <= $end; $date->modify('+1 day')) {
$dates[] = $date->format('Y-m-d');
}Na entrevista:
"for é loop com contador. Uso para batch, gerar intervalo, quando preciso do índice. Para array eu prefiro foreach."
foreach
O que é: Iteração em arrays e objetos.
Como funciona:
$users = ['João', 'Pedro', 'Maria'];
// Só valores
foreach ($users as $user) {
echo $user;
}
// Chave + valor
$ages = ['João' => 25, 'Pedro' => 30];
foreach ($ages as $name => $age) {
echo "$name: $age anos";
}
// Alterar por referência
$numbers = [1, 2, 3];
foreach ($numbers as &$number) {
$number *= 2;
}
unset($number); // ⚠️ IMPORTANTE! Limpar a referência
var_dump($numbers); // [2, 4, 6]
// Erro sem unset
foreach ($numbers as &$number) {
$number *= 2;
}
// Esqueceu unset($number)
foreach ($numbers as $number) {
// $number ainda é referência ao último elemento!
// O último elemento será sobrescrito
}Quando usar: Iterar array, collection, objeto com Iterator.
Exemplo prático:
// Eloquent Collection
$users = User::where('is_active', true)->get();
foreach ($users as $user) {
echo $user->name;
}
// Alterar array por referência
$data = [
['name' => 'Produto 1', 'price' => 100],
['name' => 'Produto 2', 'price' => 200],
];
foreach ($data as &$item) {
$item['price'] *= 1.1; // Aumenta o preço em 10%
}
unset($item);
// Laravel Collection (melhor usar map)
$discounted = collect($data)->map(function ($item) {
$item['price'] *= 0.9; // Desconto de 10%
return $item;
});
// Agrupar
$usersByDepartment = [];
foreach ($users as $user) {
$usersByDepartment[$user->department_id][] = $user;
}
// Laravel Collection (melhor groupBy)
$grouped = $users->groupBy('department_id');Na entrevista:
"foreach itera array e collection. Para alterar elemento eu uso &$var, mas DEPOIS do loop é obrigatório unset(). No Laravel eu prefiro os métodos da Collection (map, filter, groupBy)."
break, continue
O que é: Controle da execução do loop.
Como funciona:
// break — sai do loop
for ($i = 0; $i < 10; $i++) {
if ($i === 5) {
break; // Sai quando i = 5
}
echo $i;
}
// Imprime: 01234
// continue — pula a iteração atual
for ($i = 0; $i < 10; $i++) {
if ($i % 2 === 0) {
continue; // Pula os pares
}
echo $i;
}
// Imprime: 13579
// break com nível (sai de loops aninhados)
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
if ($i === 1 && $j === 1) {
break 2; // Sai dos DOIS loops
}
echo "$i-$j ";
}
}
// Imprime: 0-0 0-1 0-2 1-0Quando usar:
break— sair quando a condição bate (achou o resultado, deu erro)continue— pular o item (filtro)
Exemplo prático:
// Buscar usuário
$found = null;
foreach ($users as $user) {
if ($user->email === $searchEmail) {
$found = $user;
break; // Achou — sai
}
}
// Melhor com Collection
$found = $users->firstWhere('email', $searchEmail);
// Validação com saída antecipada
public function validate(array $data): array
{
$errors = [];
foreach ($data as $field => $value) {
if (empty($value)) {
$errors[$field] = 'Campo obrigatório';
continue; // Pula as outras checagens deste campo
}
if ($field === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field] = 'Email inválido';
}
}
return $errors;
}
// Processamento em batch com erros
foreach ($items as $item) {
try {
$this->process($item);
} catch (ProcessException $e) {
Log::error("Erro ao processar: {$e->getMessage()}");
continue; // Pula o item com erro, segue o processamento
}
}Na entrevista:
"break sai do loop, continue pula a iteração. break 2 sai de loops aninhados. Uso break para sair cedo quando acho o resultado, continue para pular item com erro."
return, exit, die
O que é: Interrompe a função ou o script.
Como funciona:
// return — sai da função e devolve valor
function findUser(int $id): ?User
{
$user = User::find($id);
if ($user === null) {
return null; // Saída antecipada
}
return $user;
}
// exit / die — para o script por completo
if (!auth()->check()) {
exit('Autenticação necessária'); // Para a execução
}
// exit() = die() (aliases)
die('Erro fatal');
// Com código de saída (para CLI)
exit(0); // Sucesso
exit(1); // ErroQuando usar:
return— sempre para sair da funçãoexit/die— só em erro crítico (não em produção!)
Exemplo prático:
// RUIM (não use exit no controller)
public function show(int $id)
{
$user = User::find($id);
if ($user === null) {
exit('User not found'); // ❌ Ruim
}
return view('user.show', compact('user'));
}
// BOM (use abort ou exceção)
public function show(int $id)
{
$user = User::findOrFail($id); // 404 se não achar
return view('user.show', compact('user'));
}
// Guard clauses (return cedo)
public function update(Request $request, Post $post)
{
if (!Gate::allows('update', $post)) {
return response()->json(['error' => 'Forbidden'], 403);
}
$post->update($request->validated());
return response()->json($post);
}
// Comando CLI
public function handle()
{
if (!$this->confirm('Apagar todos os dados?')) {
$this->info('Cancelado');
return 0; // Código de sucesso
}
try {
DB::table('logs')->truncate();
return 0;
} catch (\Exception $e) {
$this->error($e->getMessage());
return 1; // Código de erro
}
}Na entrevista:
"return sai da função. exit/die param o script (não uso em produção, só em erro crítico). Prefiro guard clauses (return cedo) em vez de if aninhado."
declare (strict_types)
O que é: Declara diretivas para o PHP.
Como funciona:
<?php
declare(strict_types=1);
// strict_types — tipagem estrita
function add(int $a, int $b): int
{
return $a + $b;
}
add(5, 10); // OK
add(5, '10'); // ❌ TypeError (sem strict_types converte '10' → 10)
// Sem strict_types (padrão)
add(5, '10'); // OK, '10' → 10 (conversão automática)
add(5, 'abc'); // ❌ TypeError ('abc' não dá para converter para int)Quando usar:Sempre use declare(strict_types=1) no começo do arquivo.
Exemplo prático:
<?php
declare(strict_types=1);
namespace App\Services;
class OrderService
{
public function create(int $userId, float $amount): Order
{
// $userId e $amount com tipo estrito
return Order::create([
'user_id' => $userId,
'amount' => $amount,
]);
}
}
// Sem strict_types
$service->create('5', '100.50'); // OK (converte para int e float)
// Com strict_types
$service->create('5', '100.50'); // ❌ TypeError
// Correto
$service->create(5, 100.50); // ✅ OKNa entrevista:
"declare(strict_types=1) liga a tipagem estrita. Sem isso o PHP converte sozinho ('5' → 5). Sempre uso strict_types para o tipo não me surpreender."
Recapitulando
Condições:
if-elseif-else— qualquer condiçãomatch(PHP 8.0) — no lugar de switch- Guard clauses (return cedo) — no lugar de if aninhado
Loops:
for— quando precisa de contadorforeach— array e collectionwhile— quando você não sabe quantas iteraçõesdo-while— roda pelo menos 1 vez
Controle:
break— sai do loopcontinue— pula a iteraçãoreturn— sai da funçãoexit/die— para o script (não use!)
Importante na entrevista:
matchvsswitch(PHP 8.0)- Guard clauses (saída antecipada)
&$varno foreach +unset()obrigatóriodeclare(strict_types=1)— use sempre- Evite
exit/dieem controllers
Exercícios práticos
Exercício 1: Guard Clauses vs if aninhado
Enunciado: Refatore if aninhados para guard clauses.
Solução
<?php
// ❌ RUIM (aninhamento profundo)
function processOrder(?Order $order, ?User $user): void
{
if ($order !== null) {
if ($user !== null) {
if ($user->isActive()) {
if ($order->isPending()) {
if ($user->hasEnoughBalance($order->total)) {
// Lógica principal no 5º nível de aninhamento
$order->process();
$user->deductBalance($order->total);
} else {
throw new InsufficientBalanceException();
}
} else {
throw new InvalidOrderStatusException();
}
} else {
throw new InactiveUserException();
}
} else {
throw new UserNotFoundException();
}
} else {
throw new OrderNotFoundException();
}
}
// ✅ BOM (guard clauses)
function processOrderRefactored(?Order $order, ?User $user): void
{
// Saídas antecipadas
if ($order === null) {
throw new OrderNotFoundException();
}
if ($user === null) {
throw new UserNotFoundException();
}
if (!$user->isActive()) {
throw new InactiveUserException();
}
if (!$order->isPending()) {
throw new InvalidOrderStatusException();
}
if (!$user->hasEnoughBalance($order->total)) {
throw new InsufficientBalanceException();
}
// Lógica principal no 1º nível
$order->process();
$user->deductBalance($order->total);
}Pontos-chave:
- Guard clauses diminuem o aninhamento
- Lógica principal fica no nível de cima
- Mais fácil de ler e manter
- Condição de erro fica explícita
Exercício 2: Loop com break e continue
Enunciado: Processe um array de pedidos pulando itens e parando no limite.
Solução
<?php
function processOrders(array $orders, int $maxAmount): array
{
$processed = [];
$totalAmount = 0;
foreach ($orders as $order) {
// Pula pedidos cancelados
if ($order['status'] === 'cancelled') {
continue;
}
// Pula pedidos com erro
if (empty($order['items'])) {
Log::warning("Pedido #{$order['id']} sem itens");
continue;
}
// Para se atingir o limite
if ($totalAmount + $order['total'] > $maxAmount) {
Log::info("Atingiu o limite máximo");
break;
}
// Processa o pedido
try {
$this->processOrder($order);
$processed[] = $order['id'];
$totalAmount += $order['total'];
} catch (ProcessException $e) {
Log::error("Falha ao processar pedido #{$order['id']}: {$e->getMessage()}");
continue; // Pula o pedido que falhou
}
}
return [
'processed' => $processed,
'total_amount' => $totalAmount,
'count' => count($processed),
];
}
// Exemplo de uso
$orders = [
['id' => 1, 'status' => 'pending', 'total' => 1000, 'items' => ['A', 'B']],
['id' => 2, 'status' => 'cancelled', 'total' => 500, 'items' => ['C']],
['id' => 3, 'status' => 'pending', 'total' => 1500, 'items' => []],
['id' => 4, 'status' => 'pending', 'total' => 2000, 'items' => ['D']],
];
$result = processOrders($orders, 5000);
// processed: [1, 4], total_amount: 3000Pontos-chave:
continuepula a iteração atualbreaksai do loop de vez- Serve para processamento em batch com limite
- Loga o erro sem parar o processo inteiro
Exercício 3: Generator para dados grandes
Enunciado: Leia um CSV grande linha a linha.
Solução
<?php
// ❌ RUIM (carrega o arquivo inteiro na memória)
function readCsvBad(string $filePath): array
{
$data = [];
$handle = fopen($filePath, 'r');
while (($line = fgets($handle)) !== false) {
$data[] = str_getcsv($line);
}
fclose($handle);
return $data; // Arquivo inteiro na memória!
}
// ✅ BOM (generator, economiza memória)
function readCsv(string $filePath): Generator
{
$handle = fopen($filePath, 'r');
// Pula o cabeçalho
fgets($handle);
while (($line = fgets($handle)) !== false) {
$row = str_getcsv($line);
// Pula linhas vazias
if (empty($row[0])) {
continue;
}
yield $row; // Devolve uma linha por vez
}
fclose($handle);
}
// Uso
function importUsers(string $csvPath): array
{
$imported = [];
$errors = [];
$count = 0;
foreach (readCsv($csvPath) as $index => $row) {
try {
[$name, $email, $age] = $row;
// Validação
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Linha $index: email inválido";
continue;
}
// Importa
$user = User::create([
'name' => $name,
'email' => $email,
'age' => (int) $age,
]);
$imported[] = $user->id;
$count++;
// Para depois de 1000 registros
if ($count >= 1000) {
Log::info('Atingiu o limite de importação');
break;
}
} catch (\Exception $e) {
$errors[] = "Linha $index: {$e->getMessage()}";
continue;
}
}
return [
'imported' => $imported,
'count' => $count,
'errors' => $errors,
];
}Pontos-chave:
- Generator (
yield) não carrega o arquivo inteiro na memória continuepula linha inválidabreaklimita quantos registros processar- Serve para importar arquivo grande
Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate