• Czas czytania ~3 min
  • 06.03.2023

Lunety Laravel Global są świetne, ale nie widzę, aby były często używane. Zamiast tego widzę, że wiele lokalnych zakresów jest wykorzystywanych do osiągnięcia tego samego. Przy odpowiedniej implementacji globalnych zakresów kod i bezpieczeństwo zostałyby znacznie ulepszone. Pozwólcie, że zilustruję to prostym przykładem.

The local scope way

W naszej bazie kodu mamy model transakcji, który przechowuje transakcje dla naszych użytkowników. Jeśli chcemy pobrać transakcję dla zalogowanego użytkownika z bazy danych, możemy to zrobić w następujący sposób:

$transactions = Transaction::where('user_id', auth()->id())->get();

Ponieważ używalibyśmy tego często w całej bazie kodu, sensowne byłoby utworzenie zakresu lokalnego w modelu transakcji w następujący sposób:


namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Transaction extends Model
{
    public function scopeForLoggedInUser($query): void
    {
        $query->where('user_id', auth()->id());
    }
}

W tym zakresie lokalnym możemy wykonać zapytanie w następujący sposób:

$transactions = Transaction::forLoggedInUser()->get();

To jest ładna refaktoryzacja DRY (Don't Repeat Yourself), która trochę go oczyszcza.

A question you should ask yourself

Lokalne zakresy są niesamowite, używam ich tam, gdzie mogę, aby kod był SUCHY. Ale nauczyłem się zadawać sobie to pytanie, gdy tworzę zakres lokalny: "Czy większość zapytań dla tego modelu będzie korzystać z tego zakresu lokalnego".

Jeśli odpowiedź brzmi "nie", zachowaj zakres lokalny. Używanie go ma sens.

When the answer is yes

Taki byłby sens przy rozważaniu zasięgu globalnego. Zakres globalny jest zawsze stosowany do wszystkich zapytań danego modelu. Zakres globalny można utworzyć, po prostu tworząc klasę, która implementuje Illuminate\Database\Eloquent\Scope.

<?php

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TransactionsForLoggedInUserScope implements Scope
{

    public function apply(Builder $builder, Model $model)
    {
        $builder->where('user_id', auth()->id());
    }
}

Następnie należy poinformować model o zakresie globalnym:

<?php

namespace App\Models;

use App\Models\Scopes\TransactionsForLoggedInUserScope;
use Illuminate\Database\Eloquent\Model;

class Transaction extends Model
{
    

    protected static function booted(): void
    {
        static::addGlobalScope(new TransactionsForLoggedInUserScope());
    }
}

Jeśli teraz otrzymasz wszystkie transakcje, zwróci tylko transakcje zalogowanego użytkownika.

Transaction::all();

Removing a global scope

Istnieją pewne możliwe scenariusze, w których chcesz utworzyć zapytanie transakcyjne bez zastosowania zakresu globalnego. Na przykład w przeglądzie dla administratorów lub w niektórych obliczeniach statystyk globalnych. Można to zrobić za pomocą metody withoutGlobalScope:

Transaction::withoutGlobalScope(TransactionsForLoggedInUserScope::class)->get();

Anonymous Global Scopes

Istnieje również sposób na utworzenie zakresu globalnego bez użycia dodatkowego pliku. Zamiast wskazywać na klasę TransactionsForLoggedInUserScope, możesz dołączyć zapytanie w następujący sposób:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Transaction extends Model
{
    

    protected static function booted(): void
    {
        static::addGlobalScope('for_logged_in_users', function (Builder $builder) {
            $builder->where('user_id', auth()->id());
        });
    }
}

Osobiście nie lubię anonimowych zakresów globalnych, ponieważ może to rozdęć model, jeśli zapytanie jest nieco złożone. Aby zachować spójność w całej bazie kodu, zawsze używam globalnych zakresów plików zewnętrznych, nawet jeśli zapytanie jest tak proste, jak w naszym przykładzie.

The downside of global scopes

Nie będę Cię okłamywać, jeśli pracujesz na bazie kodu o globalnych zakresach i nie jesteś ich świadomy, możesz wpaść w sytuacje, w których nic nie ma już sensu. To sprawiło, że pewnego (bardzo złego) dnia 😂 zakwestionowałem moje wybory zawodowe. Otrzymasz zupełnie nieoczekiwane wyniki podczas majsterkowania. Jeśli zdarzyło się to raz, nauczyłeś się, że dobrze jest mieć metodę withoutGlobalScopes w pasku narzędzi podczas pracy w bazie kodu, której nie znasz do końca.

Security

Jeśli bezpieczeństwo aplikacji zależy od zasięgu globalnego, niebezpieczne jest, gdy Ty lub inny deweloper wprowadzisz w niej zmiany w przyszłości. Wyobraźmy sobie w naszym przykładzie, że ktoś usunie lub zmieni globalny zakres. Wtedy wszystkie wyniki dla transakcji użytkownika byłyby całkowicie błędne! Dlatego tak ważne jest wdrożenie testów dla globalnych zakresów, aby to nie mogło się zdarzyć. Typowy test (PEST) dla naszego przykładu wyglądałby następująco:

it("should only get the transactions for the logged-in user", function (){
    $user = User::factory()->create();
    $otherUser = User::factory()->create();

    $transaction_1 = Transaction::factory()->create(['user_id' => $user->id]);
    $transaction_2 = Transaction::factory()->create(['user_id' => $otherUser->id]);

    $this->actingAs($user);
    expect(Transaction::all())->toHaveCount(1)
        ->and($transaction_1->is(Transaction::first()));
});

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297