PHP 8.0: tipos avançados, match, nullsafe e union types
O PHP 8.0 trouxe o JIT compiler, union types em parâmetros e retornos, a expressão match (mais segura que switch) e o operador nullsafe ?-> para encadear acesso a objetos anuláveis sem if/else verbosos.
// Union types — múltiplos tipos aceitos no mesmo parâmetro
function processPayment(int|string $orderId, float|Decimal $amount): bool
{
// ...
return true;
}
// Match expression — retorna valor, comparação estrita (===), sem fall-through
$label = match($status) {
"paid" => "Pago",
"pending" => "Aguardando",
"cancelled" => "Cancelado",
default => throw new InvalidArgumentException("Status inválido: $status"),
};
// Nullsafe operator — para em null silenciosamente
$city = $user?->address?->city?->name; // null se qualquer ponto for null
$zip = $order?->customer?->address?->zipCode ?? "CEP não informado";
// Named arguments — ordem não importa
function createOrder(string $customerId, float $total, string $status = "pending"): Order {}
$order = createOrder(
status: "paid",
total: 199.90,
customerId: "cst-123",
);
// Constructor property promotion — elimina boilerplate
class Order
{
public function __construct(
public readonly string $id,
public readonly string $customerId,
public float $total,
public string $status = "pending",
) {}
}
// Attributes (#[...]) — substituto dos docblocks para metadados
#[Route('/api/orders', methods: ['POST'])]
#[Middleware('auth')]
class OrderController {}PHP 8.1: Enums com métodos e interfaces, readonly properties, Fibers
O PHP 8.1 introduziu Enums nativos (backed e pure), a palavra-chave readonly para propriedades imutáveis após a construção e Fibers — uma primitiva de concorrência cooperativa semelhante a coroutines.
// Backed Enum — valor string ou int associado
enum OrderStatus: string
{
case Pending = "pending";
case Paid = "paid";
case Cancelled = "cancelled";
// Método no enum — lógica acoplada ao tipo
public function label(): string
{
return match($this) {
self::Pending => "Aguardando pagamento",
self::Paid => "Pago",
self::Cancelled => "Cancelado",
};
}
// Transições válidas
public function canTransitionTo(self $next): bool
{
return match($this) {
self::Pending => in_array($next, [self::Paid, self::Cancelled]),
default => false,
};
}
}
// Enum implementando interface
interface HasColor {
public function color(): string;
}
enum Priority: int implements HasColor
{
case Low = 1;
case Medium = 2;
case High = 3;
public function color(): string
{
return match($this) {
self::Low => "#green",
self::Medium => "#yellow",
self::High => "#red",
};
}
}
// Readonly properties — imutáveis após __construct
class Money
{
public function __construct(
public readonly float $amount,
public readonly string $currency,
) {}
}
$price = new Money(99.90, "BRL");
$price->amount = 0; // TypeError: Cannot modify readonly property
// Intersection types — parâmetro deve implementar ambas as interfaces
function audit(Loggable&Serializable $entity): void {}
// Fibers — coroutine nativa (base do async em ReactPHP/AMPHP)
$fiber = new Fiber(function(): void {
$valor = Fiber::suspend("primeiro suspend");
echo "Recebeu: $valor\n";
Fiber::suspend("segundo suspend");
});
$resultado1 = $fiber->start(); // "primeiro suspend"
$resultado2 = $fiber->resume("olá"); // "Recebeu: olá", retorna "segundo suspend"PHP 8.2: readonly classes e DNF types
O PHP 8.2 expandiu readonly para classes inteiras — todas as propriedades declaradas ficam automaticamente readonly. DNF types (Disjunctive Normal Form) permitem combinações de union e intersection types.
// Readonly class — todas as propriedades são readonly automaticamente
readonly class OrderCreatedEvent
{
public function __construct(
public string $orderId,
public string $customerId,
public float $total,
public \DateTime $occurredAt,
) {}
}
$event = new OrderCreatedEvent("ord-1", "cst-1", 99.90, new \DateTime());
$event->orderId = "outro"; // TypeError
// Readonly class ainda pode ter propriedades não-tipadas (sem readonly implícito)
// Clone de readonly class — permite modificar propriedades no clone
$newEvent = clone $event; // cria cópia (PHP 8.3+ permite with syntax)
// DNF types — combinações de union e intersection
// (Countable&Serializable)|null — implementa ambas OU é null
function process((Countable&Serializable)|null $data): void
{
if ($data === null) return;
echo count($data); // Countable disponível
}
// null, false, true como tipos standalone (8.2)
function findFirst(array $items): mixed|false
{
return empty($items) ? false : $items[0];
}
function isFeatureEnabled(string $name): true
{
// retorno garantido como true (lança exceção se feature não existir)
return FeatureFlag::get($name) ?? throw new RuntimeException("Feature não encontrada");
}PHP 8.3: typed class constants, json_validate, #[Override]
O PHP 8.3 refinamentos no sistema de tipos: constantes de classe agora podem ser tipadas, json_validate() evita decode desnecessário para validar JSON e o atributo #[Override] detecta em compile time se um método sobrescreve algo que não existe.
// Typed class constants — garantia em compile time
class Config
{
const int MAX_RETRIES = 3;
const string APP_VERSION = "2.1.0";
const float TAX_RATE = 0.10;
const array ENVIRONMENTS = ["local", "staging", "production"];
}
// Interface com constante tipada
interface HasVersion
{
const string VERSION = "1.0";
}
// json_validate — valida sem decodificar (mais eficiente)
$payload = file_get_contents("php://input");
if (!json_validate($payload)) {
http_response_code(400);
echo json_encode(["error" => "JSON inválido"]);
exit;
}
// Só decodifica se for válido
$data = json_decode($payload, associative: true);
// #[Override] — erro se o método não sobrescreve nada na classe pai
class BaseRepository
{
public function findById(string $id): ?Model { /* ... */ return null; }
}
class OrderRepository extends BaseRepository
{
#[\Override]
public function findById(string $id): ?Order { /* ... */ return null; }
#[\Override]
public function findByEmal(string $email): ?Order { /* ERRO em análise estática */
return null;
}
}
// Readonly properties em clones (8.3)
readonly class Point
{
public function __construct(
public float $x,
public float $y,
) {}
// Clone com valor diferente
public function withX(float $x): static
{
return new static($x, $this->y);
}
}PHP avançado: traits — conflitos de método, abstract traits, late static binding
Traits são mecanismos de reuso horizontal de código — não são classes nem interfaces. Quando dois traits têm métodos com o mesmo nome, o conflito é resolvido explicitamente com insteadof e as.
trait Timestampable
{
private ?\DateTime $createdAt = null;
private ?\DateTime $updatedAt = null;
public function touch(): void
{
$now = new \DateTime();
$this->createdAt ??= $now;
$this->updatedAt = $now;
}
public function getCreatedAt(): ?\DateTime { return $this->createdAt; }
}
trait SoftDeletable
{
private ?\DateTime $deletedAt = null;
public function softDelete(): void { $this->deletedAt = new \DateTime(); }
public function isDeleted(): bool { return $this->deletedAt !== null; }
public function restore(): void { $this->deletedAt = null; }
}
// Usando dois traits com método em conflito
trait LoggableA { public function log(): void { echo "A"; } }
trait LoggableB { public function log(): void { echo "B"; } }
class Order
{
use LoggableA, LoggableB {
LoggableA::log insteadof LoggableB; // usa A, ignora B
LoggableB::log as logB; // renomeia B para logB
}
use Timestampable, SoftDeletable;
}
// Abstract trait — obriga a classe que usa a implementar
trait Validatable
{
abstract protected function rules(): array;
public function validate(array $data): bool
{
foreach ($this->rules() as $field => $rule) {
if ($rule === "required" && empty($data[$field])) return false;
}
return true;
}
}
// Late Static Binding — self vs static
class BaseModel
{
public static function create(array $data): static
{
return new static($data); // static = classe chamada, não BaseModel
}
public static function tableName(): string
{
return strtolower((new \ReflectionClass(static::class))->getShortName()) . "s";
}
}
class Order extends BaseModel {}
$order = Order::create($data); // retorna Order, não BaseModel
echo Order::tableName(); // "orders"PHP avançado: generators e yield from
Generators permitem iterar sobre sequências sem armazenar tudo em memória. yield suspende a execução e retorna um valor; yield from delega para outro generator ou iterável. send() permite comunicação bidirecional.
// Generator básico — processa arquivo CSV linha por linha
function readCsvLines(string $path): \Generator
{
$handle = fopen($path, "r");
try {
// pula header
fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
yield $row; // suspende aqui, retorna $row para o foreach
}
} finally {
fclose($handle); // garantido mesmo em exceções
}
}
foreach (readCsvLines("pedidos.csv") as $row) {
processRow($row); // apenas uma linha em memória por vez
}
// yield from — delega para outro generator
function allOrders(): \Generator
{
yield from readCsvLines("pedidos-2023.csv");
yield from readCsvLines("pedidos-2024.csv");
yield new \ArrayIterator([["ord-999", "extra"]]);
}
// Generator com chave => valor
function indexedOrders(string $path): \Generator
{
foreach (readCsvLines($path) as $row) {
yield $row[0] => $row; // ID como chave
}
}
// send() — comunicação bidirecional com o generator
function logger(): \Generator
{
while (true) {
$message = yield; // aguarda send()
if ($message === null) return;
file_put_contents("app.log", date("Y-m-d H:i:s") . " $message\n", FILE_APPEND);
}
}
$log = logger();
$log->current(); // inicializa (avança até o primeiro yield)
$log->send("Pedido criado: ord-123");
$log->send("Pagamento processado: pay-456");PHP avançado: closures — bind, bindTo, call, arrow functions
Closures em PHP são objetos da classe Closure. Podem capturar variáveis do escopo externo com use. bind() e bindTo() permitem executar uma closure como se fosse um método de outro objeto — útil para testes e frameworks.
// Closure com use — captura variável do escopo externo
$multiplier = 3;
$triple = function(float $n) use ($multiplier): float {
return $n * $multiplier; // captura por valor (cópia)
};
// Captura por referência
$total = 0;
$accumulate = function(float $value) use (&$total): void {
$total += $value; // modifica a variável original
};
array_walk($items, $accumulate);
// Arrow function (PHP 7.4+) — captura automaticamente (por valor, sem use)
$tax = 0.1;
$withTax = fn(float $price): float => $price * (1 + $tax);
// Arrow functions aninhadas
$discount = fn(float $pct) => fn(float $price): float => $price * (1 - $pct);
$half = $discount(0.5);
echo $half(100); // 50.0
// Closure::bind — cria nova closure com $this e escopo alterados
class Order
{
private string $secret = "order-secret";
}
$readSecret = Closure::bind(
fn() => $this->secret, // closure que usa $this
new Order(), // objeto que será o $this
Order::class // escopo para acessar private
);
echo $readSecret(); // "order-secret"
// Closure::call — mais conciso que bind para uso único
$result = (fn() => $this->secret)->call(new Order());
// Closures como estratégia (Strategy pattern)
class PriceCalculator
{
private array $discountStrategies = [];
public function addDiscount(string $name, \Closure $fn): void
{
$this->discountStrategies[$name] = $fn;
}
public function apply(string $strategy, float $price): float
{
return ($this->discountStrategies[$strategy] ?? fn($p) => $p)($price);
}
}PHP avançado: SPL data structures e array functions essenciais
A SPL oferece estruturas de dados eficientes para casos onde arrays simples são inadequados. As funções nativas de array do PHP são poderosas e evitam loops explícitos desnecessários.
// SplStack — pilha LIFO
$stack = new \SplStack();
$stack->push("primeiro");
$stack->push("segundo");
echo $stack->top(); // "segundo"
echo $stack->pop(); // "segundo"
// SplQueue — fila FIFO
$queue = new \SplQueue();
$queue->enqueue("tarefa-1");
$queue->enqueue("tarefa-2");
echo $queue->dequeue(); // "tarefa-1"
// SplMinHeap — fila de prioridade (menor valor = maior prioridade)
$heap = new \SplMinHeap();
$heap->insert(["priority" => 3, "task" => "baixa"]);
$heap->insert(["priority" => 1, "task" => "urgente"]);
$heap->insert(["priority" => 2, "task" => "normal"]);
echo $heap->extract()["task"]; // "urgente"
// Array functions essenciais
$orders = [
["id" => "a", "total" => 150.0, "status" => "paid"],
["id" => "b", "total" => 80.0, "status" => "pending"],
["id" => "c", "total" => 220.0, "status" => "paid"],
];
// array_map — transforma cada elemento
$totalsWithTax = array_map(fn($o) => $o["total"] * 1.1, $orders);
// array_filter — filtra (preserva chaves por padrão)
$paid = array_values(array_filter($orders, fn($o) => $o["status"] === "paid"));
// array_reduce — acumula em um único valor
$total = array_reduce($orders, fn($carry, $o) => $carry + $o["total"], 0.0);
// array_column — extrai coluna de array multidimensional
$ids = array_column($orders, "id"); // ["a","b","c"]
$indexed = array_column($orders, null, "id"); // indexado por id
// array_chunk — divide em pedaços
$batches = array_chunk($orders, 2);
// usort — ordena com comparador personalizado
usort($orders, fn($a, $b) => $b["total"] <=> $a["total"]); // decrescenteLaravel: rotas — resource, route model binding, grupos, named routes, constraints
O sistema de rotas do Laravel é expressivo e poderoso. Route::resource gera todas as rotas CRUD com um comando. Route model binding resolve automaticamente models pelo ID passado na URL.
// routes/api.php
// Resource routes — gera: index, store, show, update, destroy
Route::apiResource("orders", OrderController::class);
// Resource parcial
Route::apiResource("products", ProductController::class)->only(["index", "show"]);
Route::apiResource("drafts", DraftController::class)->except(["destroy"]);
// Route model binding — Laravel resolve Order automaticamente; 404 se não existir
Route::get("/orders/{order}", [OrderController::class, "show"]);
// O parâmetro {order} deve corresponder ao tipo da variável no método do controller
// Named routes — gera URLs sem hardcode
Route::get("/orders/{order}", [OrderController::class, "show"])->name("orders.show");
$url = route("orders.show", ["order" => $orderId]); // http://app.test/orders/ord-123
// Grupos com prefix, middleware e namespace
Route::prefix("api/v1")
->middleware(["auth:sanctum", "throttle:60,1"])
->name("api.")
->group(function () {
Route::apiResource("orders", OrderController::class);
Route::apiResource("payments", PaymentController::class);
Route::post("/orders/{order}/confirm", [OrderController::class, "confirm"])
->name("orders.confirm");
});
// Constraints — valida o parâmetro na rota
Route::get("/orders/{id}", [OrderController::class, "show"])
->whereUuid("id"); // valida formato UUID
Route::get("/products/{slug}", [ProductController::class, "show"])
->where("slug", "[a-z0-9-]+");
// Rota com múltiplos parâmetros tipados
Route::get("/users/{user}/orders/{order}", [UserOrderController::class, "show"])
->whereNumber("user"); // user deve ser numéricoLaravel: Middleware — criação, grupos, terminable middleware
Middleware intercepta requisições antes de chegarem ao controller ou respostas antes de chegarem ao cliente. terminable middleware executa após a resposta ser enviada — ideal para logging e analytics.
// Criação: php artisan make:middleware EnsureOrderBelongsToUser
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureOrderBelongsToUser
{
public function handle(Request $request, Closure $next, string $guard = "default"): mixed
{
$order = $request->route("order");
if ($order->user_id !== $request->user()->id) {
return response()->json(["error" => "Acesso negado"], 403);
}
return $next($request);
}
}
// Registro em bootstrap/app.php (Laravel 11+)
$app->withMiddleware(function (\Illuminate\Foundation\Configuration\Middleware $middleware) {
$middleware->alias([
"order.owner" => EnsureOrderBelongsToUser::class,
]);
$middleware->appendToGroup("api", [
\App\Http\Middleware\ForceJsonResponse::class,
]);
});
// Uso em rota
Route::put("/orders/{order}", [OrderController::class, "update"])
->middleware("order.owner");
// Terminable middleware — roda após a resposta ser enviada
class LogRequestMiddleware
{
public function handle(Request $request, Closure $next): mixed
{
return $next($request);
}
// Chamado automaticamente após a resposta ser enviada
public function terminate(Request $request, \Illuminate\Http\Response $response): void
{
\Log::info("Requisição concluída", [
"url" => $request->fullUrl(),
"method" => $request->method(),
"status" => $response->getStatusCode(),
"time" => microtime(true) - LARAVEL_START,
]);
}
}Laravel: Controllers — resource controllers, invokable, dependency injection
Controllers coordenam requisições. Invokable controllers são ideais para ações únicas. A injeção de dependência via construtor ou parâmetro de método é resolvida automaticamente pelo service container.
// Resource controller — php artisan make:controller OrderController --api
namespace App\Http\Controllers;
use App\Http\Requests\StoreOrderRequest;
use App\Http\Requests\UpdateOrderRequest;
use App\Http\Resources\OrderResource;
use App\Models\Order;
use App\Services\OrderService;
class OrderController extends Controller
{
// Injeção de dependência — resolvida automaticamente pelo container
public function __construct(private readonly OrderService $service) {}
public function index(): \Illuminate\Http\Resources\Json\AnonymousResourceCollection
{
$orders = Order::query()
->where("user_id", auth()->id())
->with(["items", "customer"])
->paginate(20);
return OrderResource::collection($orders);
}
public function store(StoreOrderRequest $request): OrderResource
{
$order = $this->service->create($request->validated());
return (new OrderResource($order))->response()->setStatusCode(201);
}
public function show(Order $order): OrderResource // route model binding
{
$this->authorize("view", $order); // policy
return new OrderResource($order->load("items"));
}
public function update(UpdateOrderRequest $request, Order $order): OrderResource
{
$this->authorize("update", $order);
$this->service->update($order, $request->validated());
return new OrderResource($order->fresh());
}
public function destroy(Order $order): \Illuminate\Http\Response
{
$this->authorize("delete", $order);
$order->delete();
return response()->noContent(); // 204
}
}
// Invokable controller — ação única
// php artisan make:controller ConfirmOrderController --invokable
class ConfirmOrderController extends Controller
{
public function __invoke(Order $order, OrderService $service): OrderResource
{
$this->authorize("confirm", $order);
$confirmed = $service->confirm($order);
return new OrderResource($confirmed);
}
}
// Registro: Route::post("/orders/{order}/confirm", ConfirmOrderController::class);Laravel: Request — validação, FormRequest, custom rules, after hooks
FormRequest separa a lógica de validação do controller. Custom rules são classes reutilizáveis. after hooks permitem validações cruzadas entre campos depois que as regras individuais passaram.
// php artisan make:request StoreOrderRequest
class StoreOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can("create", Order::class);
}
public function rules(): array
{
return [
"customer_id" => ["required", "ulid", "exists:customers,id"],
"items" => ["required", "array", "min:1", "max:50"],
"items.*.product_id" => ["required", "ulid", "exists:products,id"],
"items.*.qty" => ["required", "integer", "min:1", "max:999"],
"coupon_code" => ["nullable", "string", Rule::exists("coupons", "code")->where("active", true)],
"notes" => ["nullable", "string", "max:500"],
];
}
public function messages(): array
{
return [
"items.required" => "O pedido deve ter pelo menos 1 item.",
"items.*.qty.min" => "A quantidade mínima por item é 1.",
];
}
// After hook — validação cruzada após regras individuais
public function after(): array
{
return [
function (\Illuminate\Validation\Validator $validator) {
if ($this->stockService()->hasStock($this->items) === false) {
$validator->errors()->add("items", "Um ou mais itens estão fora de estoque.");
}
},
];
}
}
// Custom Rule — php artisan make:rule ValidCpf
class ValidCpf implements \Illuminate\Contracts\Validation\ValidationRule
{
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
if (!$this->isValidCpf($value)) {
$fail("O :attribute não é um CPF válido.");
}
}
private function isValidCpf(string $cpf): bool
{
$cpf = preg_replace("/[^0-9]/", "", $cpf);
return strlen($cpf) === 11 && !preg_match("/^(\d)\1{10}$/", $cpf);
}
}
// Uso: "cpf" => ["required", new ValidCpf]Laravel: Responses — JSON, download, redirect, macros
O Laravel oferece helpers para todos os tipos de resposta HTTP. response()->json() define headers corretos automaticamente. Macros estendem o objeto Response com métodos customizados reutilizáveis.
// JSON response
return response()->json(["data" => $order, "message" => "Pedido criado"], 201);
// Com headers customizados
return response()->json($data, 200, ["X-Request-Id" => $requestId]);
// API Resource — transforma models (php artisan make:resource OrderResource)
class OrderResource extends \Illuminate\Http\Resources\Json\JsonResource
{
public function toArray(Request $request): array
{
return [
"id" => $this->id,
"total" => number_format($this->total, 2, ".", ""),
"status" => $this->status->value,
"status_label" => $this->status->label(),
"items" => OrderItemResource::collection($this->whenLoaded("items")),
"customer" => new CustomerResource($this->whenLoaded("customer")),
"created_at" => $this->created_at->toISOString(),
];
}
}
// File download
return response()->download(storage_path("invoices/{$orderId}.pdf"), "fatura.pdf");
// Stream de arquivo (sem carregar na memória)
return response()->streamDownload(function () use ($order) {
echo $this->pdfService->generateStream($order);
}, "fatura-{$order->id}.pdf");
// Redirect
return redirect()->route("orders.show", $order);
return redirect()->back()->withErrors(["msg" => "Erro ao processar"]);
return redirect()->away("https://payment-gateway.com/checkout");
// Response macro — estende o objeto Response globalmente
// Em um ServiceProvider:
\Illuminate\Support\Facades\Response::macro("success", function ($data, int $status = 200) {
return Response::json(["success" => true, "data" => $data], $status);
});
Response::macro("error", function (string $message, int $status = 400) {
return Response::json(["success" => false, "error" => $message], $status);
});
// Uso: return response()->success($order, 201);
// Uso: return response()->error("Pedido não encontrado", 404);Laravel: Blade — diretivas, components, slots, stacks, @once
Blade é a engine de templates do Laravel. Diretivas @ cobrem condicionais, loops e includes. Components são blocos reutilizáveis com props. Stacks permitem injetar conteúdo em seções definidas no layout.
{{-- Layout: resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head>
<title>@yield("title", "App")</title>
@stack("styles")
</head>
<body>
@yield("content")
@stack("scripts")
</body>
</html>
{{-- View que usa o layout --}}
@extends("layouts.app")
@section("title", "Pedidos")
@section("content")
@foreach($orders as $order)
<x-order-card :order="$order" :showDetails="true" />
@endforeach
@forelse($orders as $order)
<p>{{ $order->id }}</p>
@empty
<p>Nenhum pedido encontrado.</p>
@endforelse
@endsection
@push("scripts")
<script src="orders.js"></script>
@endpush
{{-- Component: resources/views/components/order-card.blade.php --}}
{{-- php artisan make:component OrderCard --}}
@props(["order", "showDetails" => false])
<div class="order-card">
<h3>{{ $order->id }}</h3>
<span>{{ $order->status->label() }}</span>
@if($showDetails)
{{ $slot }}
@endif
{{-- Named slots --}}
@if($header->isNotEmpty())
<header>{{ $header }}</header>
@endif
{{ $slot }}
</div>
{{-- Uso com named slots --}}
<x-order-card :order="$order">
<x-slot:header>Pedido especial</x-slot:header>
<p>Detalhes do pedido...</p>
</x-order-card>
{{-- @once — renderiza apenas uma vez mesmo em loops --}}
@once
@push("styles")
<link rel="stylesheet" href="order-card.css">
@endpush
@endonceLaravel: Eloquent avançado — scopes, accessors/mutators, observers
Scopes encapsulam condições de query reutilizáveis. Accessors e mutators (PHP 8 style com Attribute) transformam valores na leitura e escrita. Observers reagem a eventos do model sem poluir o model com hooks.
class Order extends Model
{
protected $fillable = ["customer_id", "total", "status", "notes"];
protected $casts = [
"total" => "decimal:2",
"status" => OrderStatus::class,
"metadata" => "array",
];
// Local scope — adiciona condição à query
public function scopePending(\Illuminate\Database\Eloquent\Builder $query): void
{
$query->where("status", OrderStatus::Pending);
}
public function scopeForUser(\Illuminate\Database\Eloquent\Builder $query, int $userId): void
{
$query->where("user_id", $userId);
}
// Uso: Order::pending()->forUser(1)->get()
// Accessor/Mutator (PHP 8 style — Attribute class)
protected function total(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn(string $value) => new Money((float) $value, "BRL"),
set: fn(Money $value) => $value->amount,
);
}
protected function formattedTotal(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn() => "R$ " . number_format($this->getRawOriginal("total"), 2, ",", "."),
)->shouldCache();
}
}
// Global scope — aplicado automaticamente em todas as queries
class ActiveScope implements \Illuminate\Database\Eloquent\Scope
{
public function apply(\Illuminate\Database\Eloquent\Builder $builder, Model $model): void
{
$builder->where("active", true);
}
}
// Observer — php artisan make:observer OrderObserver --model=Order
class OrderObserver
{
public function created(Order $order): void
{
\App\Events\OrderCreated::dispatch($order);
}
public function updated(Order $order): void
{
if ($order->wasChanged("status")) {
\App\Jobs\NotifyStatusChange::dispatch($order);
}
}
public function deleting(Order $order): void
{
if ($order->status !== OrderStatus::Cancelled) {
throw new \RuntimeException("Só pedidos cancelados podem ser removidos.");
}
}
}
// Registro em ServiceProvider ou AppServiceProvider
Order::observe(OrderObserver::class);Laravel: Eloquent relationships completo
Os relacionamentos do Eloquent cobrem todos os padrões comuns. Eager loading com with() evita o problema N+1. Relacionamentos polimórficos permitem que um model pertença a vários outros tipos.
class User extends Model
{
public function profile(): HasOne { return $this->hasOne(Profile::class); }
public function orders(): HasMany { return $this->hasMany(Order::class); }
public function roles(): BelongsToMany { return $this->belongsToMany(Role::class)->withTimestamps(); }
public function comments(): MorphMany { return $this->morphMany(Comment::class, "commentable"); }
}
class Order extends Model
{
public function user(): BelongsTo { return $this->belongsTo(User::class); }
public function items(): HasMany { return $this->hasMany(OrderItem::class); }
public function invoice(): HasOne { return $this->hasOne(Invoice::class); }
public function comments(): MorphMany { return $this->morphMany(Comment::class, "commentable"); }
// hasManyThrough — produtos de um pedido através dos itens
public function products(): HasManyThrough
{
return $this->hasManyThrough(Product::class, OrderItem::class,
"order_id", // FK em OrderItem
"id", // FK em Product
"id", // PK em Order
"product_id" // FK local em OrderItem
);
}
}
// Polimórfico — Comment pode pertencer a Order, Product, User
class Comment extends Model
{
public function commentable(): MorphTo { return $this->morphTo(); }
}
// Eager loading — evita N+1
$orders = Order::with([
"user:id,name,email", // select específico
"items.product", // aninhado
"invoice",
])->where("status", "paid")->get();
// Eager loading condicional
$orders->load("comments"); // carrega depois de já ter o resultado
// Atach/detach/sync em BelongsToMany
$user->roles()->attach($roleId, ["assigned_at" => now()]);
$user->roles()->detach($roleId);
$user->roles()->sync([$roleId1, $roleId2]); // remove os outros
// withCount — conta relacionamentos sem carregar
$users = User::withCount("orders")->get();
echo $users->first()->orders_count;
// withAggregate / withSum / withAvg
$users = User::withSum("orders", "total")->get();
echo $users->first()->orders_sum_total;Laravel: Query Builder — selects, joins, subqueries, raw, chunking
O Query Builder oferece uma interface fluente para SQL sem escrever strings cruas. Para casos complexos, DB::raw() e selectRaw() permitem SQL nativo de forma controlada.
use Illuminate\Support\Facades\DB;
// Selects e condições
$orders = DB::table("orders")
->select("id", "total", "status", DB::raw("DATE(created_at) as date"))
->where("status", "paid")
->whereBetween("total", [100, 500])
->whereIn("user_id", [1, 2, 3])
->whereNull("deleted_at")
->orderByDesc("created_at")
->limit(20)
->get();
// Joins
$data = DB::table("orders as o")
->join("users as u", "o.user_id", "=", "u.id")
->leftJoin("invoices as i", "o.id", "=", "i.order_id")
->select("o.id", "u.name", "o.total", "i.number as invoice")
->get();
// Subquery
$topCustomers = DB::table("users")
->whereIn("id", function ($query) {
$query->select("user_id")
->from("orders")
->groupBy("user_id")
->havingRaw("SUM(total) > ?", [1000]);
})
->get();
// selectRaw para aggregates
$stats = DB::table("orders")
->selectRaw("status, COUNT(*) as count, SUM(total) as revenue, AVG(total) as avg_ticket")
->groupBy("status")
->get();
// Chunking — processa registros em lotes sem estourar a memória
DB::table("orders")->orderBy("id")->chunk(500, function ($orders) {
foreach ($orders as $order) {
processOrder($order);
}
});
// chunkById — mais eficiente (usa índice do id, não OFFSET)
Order::chunkById(500, function ($orders) {
$orders->each(fn($order) => dispatch(new ProcessOrder($order)));
});
// lazy() — cursor para iterar sem chunk manual
foreach (Order::lazy() as $order) {
processOrder($order); // apenas um model em memória por vez (cursor no driver)
}Laravel: Migrations e Schema Builder
Migrations são controle de versão para o banco. O Schema Builder suporta todos os tipos de coluna do MySQL/Postgres/SQLite. Soft deletes e foreign keys são tratados com helpers.
// php artisan make:migration create_orders_table
return new class extends \Illuminate\Database\Migrations\Migration
{
public function up(): void
{
\Illuminate\Support\Facades\Schema::create("orders", function (\Illuminate\Database\Schema\Blueprint $table) {
$table->ulid("id")->primary();
$table->ulid("user_id");
$table->ulid("customer_id");
$table->decimal("total", 10, 2);
$table->string("status", 20)->default("pending");
$table->string("notes", 500)->nullable();
$table->json("metadata")->nullable();
$table->timestamps();
$table->softDeletes(); // adiciona deleted_at
// Índices
$table->index("status");
$table->index(["user_id", "status"]); // índice composto
$table->index(["created_at"]); // para ordenação
// Foreign keys
$table->foreign("user_id")->references("id")->on("users")->cascadeOnDelete();
$table->foreign("customer_id")->references("id")->on("customers")->restrictOnDelete();
});
}
public function down(): void
{
\Illuminate\Support\Facades\Schema::dropIfExists("orders");
}
};
// Alterar tabela existente
\Illuminate\Support\Facades\Schema::table("orders", function (\Illuminate\Database\Schema\Blueprint $table) {
$table->string("currency", 3)->default("BRL")->after("total");
$table->dropColumn("notes");
$table->renameColumn("metadata", "extra_data");
$table->index("currency");
});
// Tipos de coluna mais usados
// $table->id() → BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
// $table->uuid("id") → CHAR(36) UUID
// $table->ulid("id") → CHAR(26) ULID (ordenável por tempo)
// $table->string("name", 100)
// $table->text / longText
// $table->integer / bigInteger / unsignedBigInteger
// $table->decimal("price", 10, 2)
// $table->boolean("active")
// $table->enum("status", ["a","b"]) (prefira string + constraint)
// $table->json("data")
// $table->timestamp("published_at")->nullable()Laravel: Jobs e Queues — batches, chains, rate limiting, unique jobs
Jobs encapsulam trabalho background. ShouldQueue o envia para a fila. Chains executam jobs em série; Batches em paralelo com callback de conclusão. Rate limiting evita sobrecarga em APIs externas.
// Job básico — php artisan make:job ProcessOrderPayment
class ProcessOrderPayment implements \Illuminate\Contracts\Queue\ShouldQueue
{
use \Illuminate\Foundation\Bus\Dispatchable,
\Illuminate\Queue\InteractsWithQueue,
\Illuminate\Queue\Queueable,
\Illuminate\Queue\SerializesModels;
public int $tries = 3;
public int $backoff = 60; // segundos entre tentativas (exponencial disponível também)
public int $timeout = 120;
public function __construct(private Order $order) {}
public function handle(\App\Services\PaymentGateway $gateway): void
{
$result = $gateway->charge($this->order->total, $this->order->customer_id);
$this->order->update(["status" => "paid", "payment_id" => $result->id]);
}
public function failed(\Throwable $e): void
{
\Log::error("Pagamento falhou: {$this->order->id}", ["error" => $e->getMessage()]);
$this->order->update(["status" => "payment_failed"]);
}
}
// Dispatch
ProcessOrderPayment::dispatch($order);
ProcessOrderPayment::dispatch($order)->onQueue("payments")->delay(now()->addMinutes(2));
// Chain — executa em série, para se algum falhar
\Illuminate\Support\Facades\Bus::chain([
new ValidateOrder($order),
new ReserveStock($order),
new ProcessOrderPayment($order),
new SendConfirmationEmail($order),
])->catch(fn(\Throwable $e) => $order->markAsFailed($e->getMessage()))->dispatch();
// Batch — executa em paralelo com progresso e callback
$batch = \Illuminate\Support\Facades\Bus::batch([
new ExportOrders("2024-01"),
new ExportOrders("2024-02"),
new ExportOrders("2024-03"),
])
->then(fn($batch) => GenerateReport::dispatch($batch->id))
->catch(fn($batch, \Throwable $e) => \Log::error("Batch falhou", ["id" => $batch->id]))
->finally(fn($batch) => cache()->forget("export_running"))
->dispatch();
// Unique job — previne duplicatas na fila
class SendDailyReport implements \Illuminate\Contracts\Queue\ShouldQueue,
\Illuminate\Contracts\Queue\ShouldBeUnique
{
public int $uniqueFor = 3600; // único por 1 hora
public function uniqueId(): string { return "daily_report_" . date("Y-m-d"); }
}
// Rate limiting — em AppServiceProvider
\Illuminate\Support\Facades\Queue::before(function (\Illuminate\Queue\Events\JobProcessing $event) {
\Illuminate\Support\Facades\RateLimiter::attempt(
"stripe-api",
100, // tentativas por minuto
fn() => true,
);
});Laravel: Events e Listeners — sync vs queued listeners
Events e Listeners implementam o Observer pattern de forma elegante. Listeners podem ser síncronos (executam na requisição) ou queued (delegam para fila), sem alterar o código do Event.
// Event — php artisan make:event OrderPlaced
class OrderPlaced
{
use \Illuminate\Foundation\Events\Dispatchable,
\Illuminate\Queue\SerializesModels;
public function __construct(public readonly Order $order) {}
}
// Listener síncrono — php artisan make:listener UpdateInventory --event=OrderPlaced
class UpdateInventory
{
public function handle(OrderPlaced $event): void
{
foreach ($event->order->items as $item) {
Product::where("id", $item->product_id)->decrement("stock", $item->qty);
}
}
}
// Listener queued — executa em background
class SendOrderConfirmationEmail implements \Illuminate\Contracts\Queue\ShouldQueue
{
public string $queue = "notifications";
public int $delay = 5; // segundos de atraso
public function handle(OrderPlaced $event): void
{
\Illuminate\Support\Facades\Mail::to($event->order->user->email)
->send(new \App\Mail\OrderConfirmed($event->order));
}
}
// Registro em EventServiceProvider (ou discovery automático no Laravel 11+)
protected $listen = [
OrderPlaced::class => [
UpdateInventory::class,
SendOrderConfirmationEmail::class,
\App\Listeners\NotifyWarehouse::class,
],
];
// Dispatch do evento
OrderPlaced::dispatch($order);
event(new OrderPlaced($order)); // equivalente
// Subscriber — agrupa múltiplos listeners numa classe
class OrderEventSubscriber
{
public function subscribe(\Illuminate\Events\Dispatcher $events): array
{
return [
OrderPlaced::class => "handleOrderPlaced",
OrderCancelled::class => "handleOrderCancelled",
];
}
public function handleOrderPlaced(OrderPlaced $event): void { /* ... */ }
public function handleOrderCancelled(OrderCancelled $event): void { /* ... */ }
}Laravel: Service Container — binding, singletons, contextual binding, tags
O service container é o coração do Laravel. Resolve dependências automaticamente por meio de reflexão, mas também aceita bindings explícitos para interfaces e configurações especiais.
// AppServiceProvider.php — registra bindings no container
// Binding de interface → implementação concreta
$this->app->bind(
\App\Contracts\PaymentGateway::class,
\App\Services\StripeGateway::class
);
// Singleton — mesma instância em toda a requisição
$this->app->singleton(\App\Services\CurrencyConverter::class, function ($app) {
return new \App\Services\CurrencyConverter(
apiKey: config("services.exchange.key"),
cache: $app->make(\Illuminate\Cache\Repository::class),
);
});
// Binding com configuração por ambiente
$this->app->bind(\App\Contracts\Storage::class, function ($app) {
return app()->isProduction()
? new \App\Services\S3Storage(config("filesystems.disks.s3"))
: new \App\Services\LocalStorage(storage_path("app"));
});
// Contextual binding — injeta implementação diferente por classe
$this->app->when(\App\Http\Controllers\OrderController::class)
->needs(\App\Contracts\PaymentGateway::class)
->give(\App\Services\StripeGateway::class);
$this->app->when(\App\Console\Commands\ProcessRefunds::class)
->needs(\App\Contracts\PaymentGateway::class)
->give(\App\Services\PayPalGateway::class);
// Tags — agrupa múltiplas implementações
$this->app->bind(\App\Notifications\EmailNotifier::class, /* ... */);
$this->app->bind(\App\Notifications\SmsNotifier::class, /* ... */);
$this->app->bind(\App\Notifications\PushNotifier::class, /* ... */);
$this->app->tag([
\App\Notifications\EmailNotifier::class,
\App\Notifications\SmsNotifier::class,
\App\Notifications\PushNotifier::class,
], "notifiers");
// Resolve todos os notifiers
$notifiers = $this->app->tagged("notifiers");
foreach ($notifiers as $notifier) {
$notifier->send($message);
}Laravel: Facades e Service Providers
Facades fornecem uma interface estática para serviços registrados no container — Cache::get() é mais legível que app(Repository::class)->get(). Service Providers são os pontos de extensão do framework.
// Facade customizada
// 1. Criar a classe de serviço
class OrderMetrics
{
public function averageTicket(\Carbon\Carbon $since): float
{
return \DB::table("orders")
->where("created_at", ">=", $since)
->where("status", "paid")
->avg("total") ?? 0.0;
}
}
// 2. Criar a Facade
class OrderMetricsFacade extends \Illuminate\Support\Facades\Facade
{
protected static function getFacadeAccessor(): string
{
return "order.metrics"; // chave no container
}
}
// 3. Service Provider
class OrderMetricsServiceProvider extends \Illuminate\Support\ServiceProvider
{
public function register(): void
{
$this->app->singleton("order.metrics", fn() => new OrderMetrics());
}
public function boot(): void
{
// Aqui você registra routes, views, commands, policies...
$this->loadRoutesFrom(__DIR__ . "/../routes/api.php");
$this->loadMigrationsFrom(__DIR__ . "/../database/migrations");
$this->publishes([
__DIR__ . "/../config/order-metrics.php" => config_path("order-metrics.php"),
], "order-metrics-config");
}
}
// 4. Uso
use App\Facades\OrderMetricsFacade as Metrics;
$avg = Metrics::averageTicket(now()->subMonth());Laravel: Testing — feature tests, unit tests, mocking, database
O Laravel tem infraestrutura completa para testes. RefreshDatabase recria o banco antes de cada teste (mais lento); DatabaseTransactions faz rollback (mais rápido, incompatível com múltiplas conexões).
// Feature test — testa uma funcionalidade completa
class CreateOrderTest extends \Illuminate\Foundation\Testing\TestCase
{
use \Illuminate\Foundation\Testing\RefreshDatabase;
public function test_authenticated_user_can_create_order(): void
{
$user = \App\Models\User::factory()->create();
$products = \App\Models\Product::factory(3)->create(["stock" => 10]);
$response = $this->actingAs($user, "sanctum")
->postJson("/api/orders", [
"customer_id" => \App\Models\Customer::factory()->create()->id,
"items" => $products->map(fn($p) => ["product_id" => $p->id, "qty" => 2])->toArray(),
]);
$response->assertStatus(201)
->assertJsonStructure(["id", "total", "status", "items"])
->assertJsonPath("status", "pending");
$this->assertDatabaseHas("orders", ["user_id" => $user->id, "status" => "pending"]);
$this->assertDatabaseCount("order_items", 3);
}
public function test_unauthenticated_user_cannot_create_order(): void
{
$this->postJson("/api/orders", [])->assertUnauthorized();
}
}
// Unit test — testa lógica isolada
class OrderServiceTest extends \Illuminate\Foundation\Testing\TestCase
{
use \Illuminate\Foundation\Testing\DatabaseTransactions;
public function test_calculates_total_with_discount(): void
{
// Mock do repositório
$repo = \Mockery::mock(\App\Repositories\OrderRepository::class);
$repo->shouldReceive("save")->once()->andReturnUsing(fn($order) => $order);
$service = new \App\Services\OrderService($repo);
$order = $service->create([
"items" => [["price" => 100, "qty" => 2]],
"coupon_code" => "DESCONTO10",
]);
$this->assertEquals(180.0, $order->total);
}
// Faker via factories
public function test_order_factory_creates_valid_order(): void
{
$order = \App\Models\Order::factory()
->for(\App\Models\User::factory())
->has(\App\Models\OrderItem::factory(3))
->paid()
->create();
$this->assertEquals("paid", $order->status->value);
$this->assertCount(3, $order->items);
}
}
// Artisan command para testes
// php artisan test --filter CreateOrderTest
// php artisan test --coverageLaravel: Sanctum e Passport — tokens, SPA auth
Sanctum é a solução leve para tokens de API e SPA authentication via cookies. Passport é OAuth2 completo para casos que precisam de grants, scopes e tokens de terceiros.
// Sanctum — tokens de API
// Instalação: composer require laravel/sanctum
// Migrar: php artisan migrate
// Criar token pessoal
$user = \App\Models\User::find(1);
$token = $user->createToken("token-do-app", ["orders:read", "orders:write"]);
echo $token->plainTextToken; // só aparece uma vez
// Usar nas rotas
Route::middleware("auth:sanctum")->group(function () {
Route::apiResource("orders", OrderController::class);
});
// Verificar habilidades (scopes)
$request->user()->tokenCan("orders:write");
// Revogar tokens
$user->currentAccessToken()->delete(); // token atual
$user->tokens()->delete(); // todos os tokens
// SPA Authentication (cookie-based) — para frontend no mesmo domínio
// config/sanctum.php → stateful_domains: ["localhost", "app.com"]
// Login para SPA
Route::post("/login", function (\Illuminate\Http\Request $request) {
$credentials = $request->validate([
"email" => "required|email",
"password" => "required",
]);
if (!\Illuminate\Support\Facades\Auth::attempt($credentials)) {
throw \Illuminate\Validation\ValidationException::withMessages([
"email" => ["Credenciais inválidas."],
]);
}
$request->session()->regenerate();
return response()->json(["user" => auth()->user()]);
});
// Passport — OAuth2 completo
// Instalação: composer require laravel/passport
// Migrar: php artisan passport:install
// User model: use HasApiTokens do Passport
// Config em AuthServiceProvider:
\Laravel\Passport\Passport::tokensExpireIn(now()->addDays(15));
\Laravel\Passport\Passport::refreshTokensExpireIn(now()->addDays(30));
\Laravel\Passport\Passport::personalAccessTokensExpireIn(now()->addMonths(6));Artisan: comandos úteis e criação de comandos customizados
O Artisan é o CLI do Laravel. Além dos comandos embutidos, você pode criar commands que aproveitam o container, filas e o agendador.
# Scaffolding
php artisan make:model Order -mfcs # model + migration + factory + controller + seeder
php artisan make:controller OrderController --api --model=Order
php artisan make:request StoreOrderRequest
php artisan make:resource OrderResource
php artisan make:job ProcessPayment
php artisan make:event OrderPlaced
php artisan make:listener SendConfirmation --event=OrderPlaced
php artisan make:observer OrderObserver --model=Order
php artisan make:policy OrderPolicy --model=Order
php artisan make:middleware EnsureOwnership
php artisan make:rule ValidCpf
# Banco de dados
php artisan migrate
php artisan migrate:fresh --seed # recria tudo + seeds (cuidado em produção)
php artisan migrate:rollback --step=2
php artisan db:seed --class=OrderSeeder
# Filas
php artisan queue:work redis --queue=payments,default --tries=3
php artisan queue:failed # lista jobs falhos
php artisan queue:retry all # reprocessa todos os falhos
# Cache e otimização
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan optimize # roda todos acima
php artisan optimize:clear
# Utilitários
php artisan route:list --path=orders # filtra rotas
php artisan tinker # REPL interativo
php artisan schedule:run # roda tarefas agendadas (cron)// Comando customizado — php artisan make:command ExportOrders
class ExportOrders extends \Illuminate\Console\Command
{
protected $signature = "orders:export {--since= : Data de início (Y-m-d)} {--format=csv : csv ou json}";
protected $description = "Exporta pedidos para arquivo";
public function handle(\App\Services\OrderExporter $exporter): int
{
$since = $this->option("since") ? \Carbon\Carbon::parse($this->option("since")) : now()->subMonth();
$format = $this->option("format");
$bar = $this->output->createProgressBar(Order::where("created_at", ">=", $since)->count());
Order::where("created_at", ">=", $since)
->chunkById(100, function ($orders) use ($exporter, $bar, $format) {
$exporter->export($orders, $format);
$bar->advance($orders->count());
});
$bar->finish();
$this->newLine();
$this->info("Exportação concluída!");
return self::SUCCESS; // 0
}
}Versões — PHP 7.4 → 8.3
| Versão | Ano | Status | Principais novidades |
|---|---|---|---|
| 7.4 | 2019 | EOL | Typed properties (public int $id); arrow functions (fn($x) => $x * 2); preloading; spread operator em arrays; covariant return types; FFI |
| 8.0 | 2020 | EOL | JIT compiler; named arguments; match expression; nullsafe operator (?->); union types (int|string); constructor property promotion; Attributes (#[...]); str_contains/starts_with/ends_with; throw como expressão |
| 8.1 | 2021 | Security fixes | Enums (backed e pure); readonly properties; Fibers (coroutines nativas); never return type; intersection types (A&B); array_is_list(); new em initializers; readonly em parâmetros promoted |
| 8.2 | 2022 | Active | Readonly classes; DNF types ((A&B)|C); null, false, true como tipos standalone; Deprecated attribute; melhoria em Random extension |
| 8.3 | 2023 | Active | Typed class constants (const int MAX = 100); json_validate(); #[Override] attribute; readonly em propriedades de classe anônima; gc_status() com mais informações; mb_str_pad() |