Laravel orWhere ihmali scope ile çözümü
Laravel orWhere ihmali scope ile çözümü

Bir projede logic sorguları zincirlerken bir gün bug ile karşılaştık. Bütün profillerde olmaması gereken bir link bulunuyordu. Sebebi de aslında çok basit; links() ilişkisi çağırıldıktan sonra timer kullanılıp kullanılmamasına göre 2 durum ortaya çıkması gerekiyordu. İlk durumda status=true olanlar gelmeliydi, 2. durumda ise istenilen zaman diliminde aktif olmalıydı fakat aşağıdaki kodda çok basit ama büyük sorun oluşturacak ihmal var.

                
public function activeLinks(): HasMany { return $this ->links() ->where([ 'timer' => false, 'status' => true ]) ->orWhere(function($query){ $query ->whereTimer(true) ->where('start_date', '<=', now()) ->where('end_date', '>=' now()); }); }

Laravel dökümanında söylenine göre orWhere her zaman grup içerisine alınmalıdır. Örneğin yukarıdaki kodda çıktı böyledir: 

select * from links where user_id = :user_id and (timer = false and status = true) or (timer = true and start_date <= :start_date and end_date >= end_date);

where AA and BB or CC şeklinde olduğu için ne olursa olsun CC koşulunu sağlayan satırlar da geliyor yani yukarıdaki örnekte sadece kullanıcıya ait linkler gelmesi gerekirken başkalarına ait timer aktif olan linkler de sonuca eklenmiş oldu. Olması gereken where AA and (BB or CC) şeklindedir.

Sorunu gidermek için aşağıdaki gibi where function içerisine alıp grupladık fakat göründüğü gibi kodun okunması biraz zorlaştı.

                
public function activeLinks(): HasMany { return $this ->links() ->where(function($q1){ $q1->where([ 'timer' => false, 'status' => true ]) ->orWhere(function($q2){ $q2 ->whereTimer(true) ->where('start_date', '<=', now()) ->where('end_date', '>=' now()); }) }); }

İşte burda çözüm için scope devreye giriyor. Scope sayesinde önceden tanımlı koşullar oluşturup bunu global olarak veya sadece istediğimizde sorgularımıza ekleyebiliyoruz. Status ve Timer için 2 farklı status oluşturduk. Scope tanımlamak için aşağıdaki gibi model içerisinde scope adının başına 'scope' yazmanız gerekir.

                
/** Scope a query to only include active link by status. */ public function scopeStatusTrue(Builder $query): Builder { return $query->where([ 'timer' => false, 'status' => true ]); } /** Scope a query to only include active link by time.*/ public function scopeOnTime(Builder $query): Builder { return $query ->where('timer', true) ->where('start_date', '<=', now()) ->where('end_date', '>=', now()); }

Ve bunu var olan sorgumuza dahil ettik. Ne kadar da basit görünüyor değil mi :)

                
/** * Active links of the user. * * @return HasMany */ public function activeLinks(): HasMany { return $this ->links() ->where(fn($query) => $query ->StatusTrue() ->orWhere ->OnTime()); }

Bu hata ile ilgili değil fakat global scope nasıl kullanılır merak ediyorsanız laravel dökümanını ve örneklerini inceleyebilirsiniz. https://laravel.com/docs/8.x/eloquent#global-scopes