Blog

tap(), rescue(), optional(), fake() - cztery funkcje pomocnicze Laravel, z których doświadczeni programiści korzystają na co dzień, ale rzadko kiedy są wyjaśniane w kilku słowach. 🚀

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' lub 0 jako 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żywaj report: false dla 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żywaj optional() 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 jak fake('pl_PL') dla danych testowych specyficznych dla rynku.
  • blank() to empty(), ale poprawne - traktuje białe znaki jako puste i nie traktuje 0 ani '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!

Wsparcie istniejącego systemu

Potrzebujesz pomocy z działającą aplikacją?

Pomagam firmom rozwijać działające systemy, porządkować wdrożenia i dodawać nowe funkcje bez dokładania chaosu do projektu.

Komentarze (0)
Zaloguj się, aby dodać komentarz

Musisz być zalogowany, aby dodać komentarz.

Zaloguj się

Potrzebujesz kogoś, kto weźmie odpowiedzialność za kolejny krok?

Porozmawiajmy o Twoim projekcie i określmy zakres, który ma sens dla Twoich celów.