Laravel dostarcza garść globalnych helperów, po które seniorzy sięgają regularnie, a jednak rzadko są dokumentowane inaczej niż jedną linią w oficjalnych dokumentach. tap(), rescue(), optional() i fake() - każdy z nich elegancko rozwiązuje powtarzający się problem. Wiedza o tym, kiedy ich używać (i kiedy nie), odróżnia programistów walczących z frameworkiem od tych, którzy z nim współpracują.
📋 Spis treści
🔧 app() i resolve() {#app-resolve}
app() bez argumentów zwraca instancję Application - sam kontener IoC. Z argumentem rozwiązuje bindowanie z kontenera. resolve() to bezpośredni alias.
// app/Services/ReportService.php
declare(strict_types=1);
namespace App\Services;
use Illuminate\Contracts\Foundation\Application;
class ReportService
{
public function containerInstance(): Application
{
return app(); // Illuminate\Foundation\Application
}
public function resolveService(): MailService
{
// Wszystkie trzy są identyczne w zachowaniu.
$a = app(MailService::class);
$b = resolve(MailService::class);
$c = app()->make(MailService::class);
return $a;
}
}
app(), resolve() i app()->make() to ta sama operacja. Wybierz jedno i bądź konsekwentny w całej bazie kodu. resolve() jest najbardziej ekspresywne - sygnalizuje czytelnikowi, że odbywa się wyszukiwanie w kontenerze.
Możesz przekazać parametry konstruktora, by nadpisać rozwiązanie kontenera:
// app/Services/ReportService.php
declare(strict_types=1);
namespace App\Services;
class ReportService
{
public function buildWithOverride(): PdfExporter
{
return resolve(PdfExporter::class, [
'dpi' => 300,
'format' => 'A4',
]);
}
}
Kiedy używać: kod bootstrappujący działający poza kontekstem dependency injection - domknięcia poleceń Artisan, makra, metody boot() w pakietach, gdzie nie możesz używać type hintów. W kodzie aplikacji preferuj wstrzykiwanie przez konstruktor. Używanie app() jako service lokatora wewnątrz kontrolerów i serwisów jest antywzorcem, który utrudnia testowanie i czytanie kodu.
🪝 tap() {#tap}
tap($value, callable $callback) uruchamia $callback z $value jako argumentem, a następnie zwraca $value bez zmian. Callback istnieje wyłącznie dla swoich efektów ubocznych.
Przed tap():
// app/Services/OrderService.php
declare(strict_types=1);
namespace App\Services;
use App\Models\Order;
use App\Events\OrderCreated;
class OrderService
{
public function create(array $data): Order
{
$order = Order::create($data);
event(new OrderCreated($order));
return $order;
}
}
Po tap():
// app/Services/OrderService.php
declare(strict_types=1);
namespace App\Services;
use App\Models\Order;
use App\Events\OrderCreated;
class OrderService
{
public function create(array $data): Order
{
return tap(Order::create($data), function (Order $order) {
event(new OrderCreated($order));
});
}
}
Obie wersje są funkcjonalnie identyczne. Wersja z tap() jest cenniejsza gdy jesteś w środku łańcucha i musisz wstawić efekt uboczny bez przerywania przepływu wartości zwracanej:
// app/Services/CartService.php
declare(strict_types=1);
namespace App\Services;
use App\Models\Cart;
use Illuminate\Support\Facades\Log;
class CartService
{
public function addItem(Cart $cart, int $productId, int $qty): Cart
{
return tap($cart)
->addProduct($productId, $qty)
->recalculateTotals()
->save();
// tap() bez callbacku zwraca obiekt proxy, który przekazuje
// wywołania metod do $cart i na końcu zwraca $cart.
}
}
Rzeczywiste zastosowania: logowanie przed zwróceniem modelu, dispatch eventów po zapisaniu, audyt zmian stanu w środku pipeline, aktualizacja cache bez przerywania łańcuchów zwracanych.
tap() jest też intensywnie używany w samym źródle Laravel - Illuminate\Support\Fluent, kolekcje i query builder używają go do zwracania $this z metod w stylu setterów.
tap() w pipeline kolekcji:
// app/Services/InvoiceService.php
declare(strict_types=1);
namespace App\Services;
use App\Models\Invoice;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class InvoiceService
{
public function processOverdue(): Collection
{
return Invoice::overdue()
->get()
->tap(function (Collection $invoices) {
Log::info('Przetwarzanie przeterminowanych faktur', ['count' => $invoices->count()]);
})
->each(fn (Invoice $invoice) => $invoice->markAsOverdue())
->filter(fn (Invoice $invoice) => $invoice->shouldNotify());
}
}
Collection::tap() to wersja specyficzna dla kolekcji - otrzymuje całą kolekcję, pozwala ją obserwować bez transformowania i łańcuch kontynuuje bez zmian.
🛟 rescue() {#rescue}
rescue(callable $callback, mixed $fallback = null, bool $report = true) wykonuje $callback wewnątrz try/catch. Jeśli zostanie rzucony wyjątek, opcjonalnie go raportuje i zwraca $fallback.
// app/Services/EnrichmentService.php
declare(strict_types=1);
namespace App\Services;
use App\Models\Order;
use App\Integrations\TaxApiClient;
class EnrichmentService
{
public function __construct(
private readonly TaxApiClient $taxApi,
) {}
public function enrichWithTaxData(Order $order): array
{
$taxRate = rescue(
fn () => $this->taxApi->getRateForCountry($order->country),
fallback: 0.0,
report: true,
);
return [
'id' => $order->id,
'subtotal' => $order->subtotal_cents / 100,
'tax_rate' => $taxRate,
'tax' => $order->subtotal_cents / 100 * $taxRate,
];
}
}
Jeśli API podatkowe rzuci wyjątek połączenia, odpowiedź z zamówieniem nadal zostanie zwrócona - ze stawką podatkową zero i wyjątkiem zaraportowanym do trackera błędów.
rescue() vs try/catch: argument o czytelności jest uzasadniony, ale zależy od kontekstu. Dla pojedynczego fallbacku na izolowanym wywołaniu, rescue() jest wizualnie czystsze. Dla złożonych wielokrokowych przepływów, gdzie różne typy wyjątków wymagają różnej obsługi, blok try/catch jest jaśniejszy i bezpieczniejszy.
// app/Services/CurrencyService.php
declare(strict_types=1);
namespace App\Services;
// Użyj rescue() - proste, jeden fallback, raportuj wyjątek.
public function currentRate(string $from, string $to): float
{
return rescue(
fn () => $this->api->getRate($from, $to),
fallback: $this->cachedRate($from, $to),
report: true,
);
}
// Użyj try/catch - wiele typów wyjątków, różna obsługa.
public function exchangeAndRecord(int $amountCents, string $from, string $to): int
{
try {
$rate = $this->api->getRate($from, $to);
$converted = (int) round($amountCents * $rate);
$this->recorder->log($from, $to, $rate);
return $converted;
} catch (ApiRateLimitException $e) {
throw new ServiceUnavailableException('Przekroczono limit API walut.', previous: $e);
} catch (NetworkException $e) {
Log::error('API walut niedostępne', ['exception' => $e]);
return $this->convertWithCachedRate($amountCents, $from, $to);
}
}
Ciche fallbacki: przekaż report: false gdy naprawdę nie zależy ci na błędzie - opcjonalne wzbogacenia funkcji, niekrytyczne metadane lub narzędzia deweloperskie.
// app/Http/Resources/UserResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'email' => $this->email,
'avatar' => rescue(fn () => $this->gravatar->url(), null, report: false),
];
}
}
Jeśli Gravatar jest niedostępny, pole avatar ma wartość null. Żaden wyjątek nie jest raportowany, żaden użytkownik nie odczuwa skutków. To poprawne zachowanie dla opcjonalnych danych.
🔍 optional() {#optional}
optional($value) owija wartość w proxy, które zwraca null dla każdego wywołania metody lub dostępu do właściwości, gdy sama $value jest null, zamiast rzucać błąd.
// app/Http/Resources/OrderResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'discount_value' => optional($this->discount)->formatted_value,
'discount_code' => optional($this->discount)->code,
];
}
}
Bez optional(), dostęp do $this->discount->formatted_value gdy $this->discount jest null rzuca fatalny błąd. Z optional() zwraca czysto null.
optional() vs operator null-safe ?->: PHP 8.0 wprowadził ?-> dla łańcuchowania z null-safety. Dla prostego dostępu na jednym poziomie, ?-> jest bardziej idiomatycznym wyborem:
// app/Http/Resources/OrderResource.php
declare(strict_types=1);
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
// Nowoczesne PHP - preferuj to dla prostego dostępu
'discount_value' => $this->discount?->formatted_value,
// optional() nadal wygrywa przy łańcuchach wielu wywołań na tym samym nullable
'discount' => optional($this->discount, function ($d) {
return [
'value' => $d->formatted_value,
'code' => $d->code,
'type' => $d->type->label(),
];
}),
];
}
}
optional() z callbackiem to jego druga forma: jeśli $value nie jest null, callback uruchamia się z $value i jego wartość zwracana jest używana. Jeśli $value jest null, zwracane jest null bez wywoływania callbacku.
Kiedy optional() bije ?->:
- Łańcuchowanie wielu wywołań metod na tym samym nullable bez powtarzania
?->przy każdym kroku. - Praca z magicznymi właściwościami
__get()na obiektach nie będących modelami, które?->obsługuje niespójnie. - Warunkowe budowanie struktury, gdzie chcesz ustrukturyzowane null zamiast sekwencji sprawdzeń
?->.
🎭 fake() {#fake}
fake() zwraca instancję Faker\Generator, której Laravel używa wewnętrznie w fabrykach modeli. Jest globalnie dostępna wszędzie - nie tylko w definicjach fabryk.
// database/factories/OrderFactory.php
declare(strict_types=1);
namespace Database\Factories;
use App\Enums\OrderStatus;
use App\Models\Order;
use Illuminate\Database\Eloquent\Factories\Factory;
class OrderFactory extends Factory
{
protected $model = Order::class;
public function definition(): array
{
return [
'reference' => strtoupper(fake()->bothify('ORD-????-####')),
'status' => fake()->randomElement(OrderStatus::cases()),
'subtotal_cents'=> fake()->numberBetween(500, 50000),
'currency' => fake()->randomElement(['GBP', 'EUR', 'USD']),
'email' => fake()->safeEmail(),
];
}
}
Używanie fake() poza fabrykami - seedery:
// database/seeders/ProductSeeder.php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Product;
use Illuminate\Database\Seeder;
class ProductSeeder extends Seeder
{
public function run(): void
{
collect(range(1, 50))->each(function (int $i) {
Product::create([
'name' => fake()->words(3, asText: true),
'sku' => fake()->bothify('SKU-####-??'),
'price_cents' => fake()->numberBetween(999, 99900),
'description' => fake()->paragraph(3),
]);
});
}
}
Używanie fake() w testach z realistycznymi danymi:
// tests/Feature/Api/V1/OrderControllerTest.php
declare(strict_types=1);
namespace Tests\Feature\Api\V1;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class OrderControllerTest extends TestCase
{
use RefreshDatabase;
public function test_creates_order_with_valid_payload(): void
{
$payload = [
'email' => fake()->safeEmail(),
'shipping_name' => fake()->name(),
'shipping_line1' => fake()->streetAddress(),
'shipping_city' => fake()->city(),
'shipping_post' => fake()->postcode(),
'items' => [
['product_id' => 1, 'qty' => fake()->numberBetween(1, 5)],
],
];
$this->postJson('/api/v1/orders', $payload)
->assertCreated()
->assertJsonPath('data.email', $payload['email']);
}
}
Dane specyficzne dla lokalizacji: przekaż ciąg lokalizacji do fake(), by uzyskać odpowiednie dane:
// database/seeders/CustomerSeeder.php
declare(strict_types=1);
namespace Database\Seeders;
use App\Models\Customer;
use Illuminate\Database\Seeder;
class CustomerSeeder extends Seeder
{
public function run(): void
{
// Polscy klienci do testów rynku lokalnego
collect(range(1, 20))->each(function () {
Customer::create([
'name' => fake('pl_PL')->name(),
'city' => fake('pl_PL')->city(),
'phone' => fake('pl_PL')->phoneNumber(),
]);
});
}
}
fake('pl_PL') zwraca instancję Faker skonfigurowaną dla polskiej lokalizacji - polskie nazwy miast, polskie formaty numerów telefonów, polskie wzorce imion i nazwisk.
🔲 Bonus: blank() i filled() {#blank-filled}
blank($value) zwraca true jeśli wartość jest null, pustym ciągiem, ciągiem zawierającym tylko białe znaki lub pustą tablicą. filled() jest odwrotnością.
// app/Services/SearchService.php
declare(strict_types=1);
namespace App\Services;
use App\Models\Product;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class SearchService
{
public function search(array $filters): Collection
{
return Product::query()
->when(filled($filters['query'] ?? null), fn (Builder $q, $term) =>
$q->where('name', 'like', "%{$term}%")
)
->when(filled($filters['category'] ?? null), fn (Builder $q, $cat) =>
$q->where('category_id', $cat)
)
->get();
}
}
blank() vs empty(): empty() traktuje '0' jako puste (jest fałszywe w PHP). blank() nie - blank('0') zwraca false. To praktyczna różnica:
// tabela porównawcza blank()
blank(null) // true
blank('') // true
blank(' ') // true ← kluczowa różnica od empty()
blank([]) // true
blank(0) // false ← 0 nie jest puste
blank('0') // false ← '0' nie jest puste
blank(false) // true
filled('') // false
filled('hello') // true
filled(0) // true
filled([1, 2, 3]) // true
Kiedy używać blank():
- Walidacja opcjonalnych pól formularza, gdzie użytkownik mógł przesłać ciąg ze spacjami.
- Warunkowe scope zapytań, gdzie nieustawiony filtr powinien być całkowicie pominięty.
- Wszędzie tam, gdzie
empty()niepoprawnie traktowałoby'0'lub0jako nieobecne.
✅ Podsumowanie
app()/resolve()/app()->make()są identyczne - wybierz jedno i trzymaj się go. Preferuj wstrzykiwanie przez konstruktor w kodzie aplikacji.tap($value, $fn)uruchamia efekt uboczny i zwraca oryginalną wartość bez zmian. Sięgaj po nie, gdy musisz wstawić logowanie, eventy lub wywołania audytu do łańcucha bez zrywania typu zwracanego.rescue($fn, $fallback, $report)to czytelny jednowyrażeniowy try/catch dla przypadków z jednym fallbackiem. Używajreport: falsedla opcjonalnych wzbogaceń. Preferuj pełny try/catch gdy wiele typów wyjątków wymaga innej obsługi.optional($value)/$value?->oba obsługują dostęp do nullable. Używaj?->dla prostego dostępu na jednym poziomie; używajoptional()gdy musisz wywołać wiele metod lub zbudować strukturę z nullable.fake()jest dostępne wszędzie, nie tylko w fabrykach. Używaj wariantów lokalizacyjnych jakfake('pl_PL')dla danych testowych specyficznych dla rynku.blank()toempty(), ale poprawne - traktuje białe znaki jako puste i nie traktuje0ani'0'jako pustych.
Te helpery istnieją, bo filozofia projektowania Laravel polega na tym, by typowy przypadek był łatwym przypadkiem. Konsekwentne ich używanie tworzy kod, który czytelnie komunikuje intencje i przewidywalnie obsługuje przypadki brzegowe. Efekt widać wyraźnie podczas code review: mniej hałasu, więcej sygnału.
Obserwuj mnie na LinkedIn po więcej porad Laravel i DevOps!