Kategoriler
Teknik

TrimStrings Başınıza Nasıl Bela Olabilir

Laravel, PHP dünyasında popüler bir framework ve bir çok geliştiriciye kolaylık sağlamak için harika araçlar sunuyor. Ancak bazen, bu kolaylıklar beklenmedik sorunlara da yol açabiliyor. Bu yazıda, Laravel’deki TrimStrings middleware’inin nasıl bir problem yaratabileceğinden ve bunu nasıl çözebileceğimizden bahsedeceğim.

TrimStrings Middleware Nedir ve Ne İşe Yarar?

TrimStrings middleware’i, Laravel uygulamalarında form verileri gibi kullanıcıdan gelen isteklerdeki boşlukları otomatik olarak temizlemek için kullanılır. Bu, özellikle form inputlarında kullanıcıların yanlışlıkla başında veya sonunda boşluk bıraktığı durumlarda faydalıdır. Örneğin, bir kullanıcı formda e-posta adresini girerken " [email protected] " gibi başında veya sonunda boşluk bırakabilir. TrimStrings middleware’i bu tür boşlukları kırpar ve sadece "[email protected]" değerini alır.

Bu özellik, gereksiz boşluklardan kaynaklanan hataları önlemek ve daha temiz veri işlemek için oldukça yararlıdır. Ancak, her zaman olduğu gibi, bazı özel durumlarda bu varsayılan davranış istenmeyen sonuçlara yol açabilir.

Olay Nasıl Gelişti?

Brezilya merkezli bir ödeme sağlayıcı ile entegre çalıştığımız bir projede, ödeme sonuçlarını almak ve doğrulamak için bir callback yapısı kullanıyoruz. Ödeme sağlayıcısı, işlem sonucunu bir POST isteği ile sunucumuza gönderiyor ve biz de bu isteğin doğruluğunu kontrol etmek için bir signature/hash doğrulaması yapıyoruz.

Bu doğrulama işlemi oldukça basit bir mantığa dayanıyor:

  1. Sağlayıcıdan gelen veriler alınıyor.
  2. Tüm veriler bir string olarak birleştiriliyor.
  3. Bu string, sağlayıcının bize verdiği secret key ile SHA256 algoritması kullanılarak hash’leniyor.
  4. Bu hash, sağlayıcıdan gelen hash ile karşılaştırılıyor. Eğer aynı ise işlem başarılı sayılıyor, değilse başarısız.

Problemi Nasıl Fark Edildi?

Başlangıçta, gelen bazı valid isteklerin neden reddedildiğini anlamak zor oldu. Ancak, Nginx loglarını incelediğimizde, gelen istekteki full_name parametresinde sondaki boşlukların korunarak gönderildiğini fark ettik. Buna rağmen, bizim sunucumuzda bu boşluklar kırpılmıştı ve bu durum hash doğrulamasında hataya yol açıyordu. İşte o an, TrimStrings middleware’inin bu soruna neden olduğu anlaşıldı.

Çözüm Ne?

Bu tür durumlarla karşılaşmamak için, belirli rotalar veya istekler için TrimStrings middleware’ini devre dışı bırakmak gerekiyor. Laravel 8 ile birlikte sunulan TrimStrings::skipWhen metodu, bu duruma özel bir çözüm sunuyor.

Aşağıda bir provider aracılığıyla bu çözümün nasıl uygulanabileceğini görebilirsiniz:

use Illuminate\Foundation\Http\Middleware\TrimStrings;
use Illuminate\Http\Request;

// ...

TrimStrings::skipWhen(function (Request $request) {
    return $request->is('api/v1/integrations/foo-provider/callback');
});

Bu kod parçası, belirli bir rota için TrimStrings middleware’ini devre dışı bırakıyor. Yani api/v1/integrations/foo-provider/callback rotasından gelen istekler için boşluk kırpılması işlemi yapılmıyor ve böylece hash doğrulama işlemi sorunsuz bir şekilde gerçekleştiriliyor.

Sonuç

Laravel’in sunduğu varsayılan özellikler genellikle işleri kolaylaştırırken, belirli senaryolarda beklenmedik sonuçlar doğurabiliyor. Bu yüzden, proje geliştirme sürecinde kullanılan araçların işleyişine dikkat etmek ve potansiyel etkilerini iyi değerlendirmek çok önemli. TrimStrings middleware’i, çoğu durumda yararlı bir araç olmasına rağmen, bu gibi senaryolarda başınıza bela olabilir. Neyse ki, TrimStrings::skipWhen gibi esnek çözümlerle bu tür sorunların önüne geçmek mümkün.

Kategoriler
Teknik

PHP’de Özyinelemeli Closure Kullanımı

Bazen bir çözümü gerçekleştirmek için özyinelemeli bir fonksiyon veya metot kullanma ihtiyacımız olabiliyor. Bu ihtiyaç, benim bir kategoriye ait öğeleri ve o kategorinin alt kategorisine ait öğeleri de içine alacak şekilde Eloquent ORM’de listelemem gerektiğinde ortaya çıktı.

Bunun için, kategori modeline alt kategorilerinin ID’sini getiren bir metot ekledim. İşte bu metodun örneği:

<?php

declare(strict_types=1);

namespace App\Models;

// things...

class Category
{
    public function categories()
    {
        return $this->hasMany(Category::class);
    }

    public function getChildCategoryIDs(): array
    {
        $ids = [];

        $extract = function (Category $category, array &$ids = []) use (&$extract): void {
            $ids[] = $category->id;

            if (isset($category->categories) && is_iterable($category->categories)) {
                foreach ($category->categories as $category) {
                    $extract($category, $ids);
                }
            }
        }

        $extract($this);

        return $ids;
    }
}

Closure Kullanımı: Neden?

Bu örnekte, özyinelemeyi gerçekleştirmek için sınıf içerisinde bir metot oluşturabilirdim. Ancak bu yaklaşımın, metot sayısını artıracağı ve kodun karmaşıklığını potansiyel olarak artırabileceği göz önünde bulundurulduğunda, tüm işlemleri tek bir metot içinde gerçekleştirmeye karar verdim ve Closure’u tercih ettim.

Kategori Yapısının Sebepleri

Bu kategori yapısını, ekstra bir Laravel/PHP paketi kullanmadan ve veritabanı tablo yapısını basit tutmak amacıyla oluşturdum. Alternatif olarak, Laravel’de hiyerarşik kategori paketi olan Baum’u kullanabilirdim, ancak bu durumda ek bir bağımlılığın getireceği potansiyel karmaşıklığı tercih etmedim.

Bu yaklaşımın kısa vadede hızlı ve basit bir çözüm sağladığını, ancak uzun vadede ve artan web trafiğiyle birlikte sorgu optimizasyonu ihtiyacını doğurabileceğini unutmamak önemli. Ancak, bu potansiyel sorun, önbellek kullanımıyla çözülebilir.

Çalışma Şekli:

<?php

public function show(Category $category)
{
    $category->load('categories.categories.categories');
    $ids = $category->getChildCategoryIDs();
    $products = Product::whereIn('category_id', $ids)->get();

    return view('products.index', compact('category', 'products'));
}

CategoryController içerisindeki show metodu, belirli bir kategoriyi ve o kategorinin ürünlerini listeleyen bir işlem gerçekleştirir. ‘Lazy load’ yöntemi kullanarak, hedef kategorinin alt kategorilerini (burada üç dal alıyoruz) elde ederiz ve model içinde tanımladığımız getChildCategoryIDs metodu ile bu alt kategorilerin ID’lerini alırız. Son olarak, bu ID’lere sahip tüm ürünlerin bir listesini WHERE IN sorgusu ile elde ederiz.

Kategoriler
Teknik

Spatie Async Paketiyle Laravel Eloquent ORM’i Kullanmak

Bir servisten yüklü miktarda veri çekip veritabanına işlemem gerekiyordu. Veriyi Guzzle’ın havuz özelliğiyle asenkron şekilde çekip veriyi işlemek içinse Spatie Async paketi kullanmam gerekti. Şu şekilde yol aldım.

Paketi composer ile kuralım:

composer require spatie/async

Kullanımı:

use SpatieAsyncPool;

$pool = Pool::create();

foreach ($things as $thing) {
    $pool->add(function () use ($thing) {
        // Do a thing
    })->then(function ($output) {
        // Handle success
    })->catch(function (Throwable $exception) {
        // Handle exception
    });
}

$pool->wait();

Yukarıdaki örnekte varsayılan ayarlamalar kullanıldı. Aşağıdaki örnekte havuza ekleyeceğimiz öğeleri Task sınıfını kullanarak ekleyip, ayarlamaları kendimiz yapacağız.

use SpatieAsyncPool;

$pool = Pool::create()
    ->concurrency(10)
    ->autoload(__DIR__ . '/vendor/autoload.php');

$pool->add(new FooTask(1));
$pool->add(new FooTask(2));
$pool->add(new FooTask(3));

$pool->wait();

Task örneği ise şöyle olacak:

class FooTask extends SpatieAsyncTask
{
    public function configure()
    {
        $app = require __DIR__ . '/../../bootstrap/app.php';
        $app->make(IlluminateContractsConsoleKernel::class)->bootstrap();
    }

    public function run()
    {
        return AppItem::create([
            'foo' => 'bar'
        ]);
    }
}

Burada önemli olan nokta configure() metodunda gereklilikleri ihtiyaçları çağırmak.

Kolay gelsin.

Kategoriler
Teknik

Laravel’de PHPUnit Testinde ‘Class env does not exist’ Hatası

Laravel (5.8)’de test yazarken

php artisan make:test FooTest

ile testi oluşturup

$response = $this->json('POST', '/api/v1/foo/bar', ['param1' => 'value1']);
$response->assertOk()->assertJsonCounts(1, 'reports');

şeklinde talep testi yapabiliyoruz.

Fakat eğer uygulamanızda Telescope kullanıyorsanız şu hatayı almanız muhtemel:

[message] => Class env does not exist
[exception] => ReflectionException
[file] => /.../vendor/laravel/framework/src/Illuminate/Container/Container.php
[line] => 794

Bu durumda phpunit.xml dosyanıza <env name="TELESCOPE_ENABLED" value="false"/> satırını eklerseniz, Telescope’u test sırasında pasif duruma getirmiş olursunuz. Böylelikle hatayı almayacaksınız.

Kolay gelsin.

Kategoriler
Teknik

Eloquent ORM’de Erişimci ve Mutatörler

Erişimciler (Accessors)

Erişimciler, veritabanından aldığımız bilgileri/verileri önceden tanımlı olarak biçimlendirmemize olanak tanır. İki tane örnek verelim. Öznitelikleri türkçe olarak örneklendireceğim (elbette ki veritabanını tasarlarken ingilizce kullanmak gerekiyor)

Veritabanımızdaki tabloda ad ve soyad adlı iki adet sütun olduğunu varsayalım. Normal şartlarda veritabanındaki ad soyad bilgisini görüntülemek istediğimizde

Ad Soyad: {{ mb_ucfirst($uye->ad) . ' ' . mb_ucfirst($uye->soyad) }}

şeklinde kullanırız. Ama aşağıdaki gibi bir metodu, modelimize tanımlarsak, işimizi, daha kullanışlı hale getirebiliriz.

public function getAdSoyadAttribute()
{
    return mb_ucfirst($this->ad) . ' ' . mb_ucfirst($this->soyad);
}

Artık şöyle kullanılabilir: Ad Soyad: {{ $uye->ad_soyad }}

Önceden tanımladığımız metod, bize veri işlemede yardımcı oluyor. Benzer bir örneği para birimi için verebiliriz. Veritabanımızda fiyat adlı bir sütun olsun ve içeriği ondalık sayı olarak tutsun. 1325.00 olan bir değeri normal şartlarda görüntülemek istediğimizde ondalık hali yerine şunu göreceğiz: 1325. Şimdi bunu biçimlendirelim.

/**
 * @return string
 */
public function getFiyatAttribute()
{
    return number_format((float) $this->fiyat, 2, ',', '.') . ' TL';
}

Şimdi, veritabanındaki fiyat değerini görüntülemek istediğimizde TL’ye uygun biçimde görünecektir. Yani 1.325,00 TL şeklinde.

 Mutatörler (Mutators)

Erişimcilerin aksine mutatörler gösterilen veriyi işlemek yerine gelen/alınan veriyi işlemekte bize yardımcı olurlar.

Aşağıdaki örnekte 1.325,00 TL olarak gelen veriyi veritabanına kaydetmeden önce 1325.00 haline çevireceğiz.

/**
 * @param string $deger
 * @return void
 */
public function setFiyatAttribute($deger)
{
    $this->attributes['fiyat'] = (float) str_replace(
        [' TL', '.', ','], 
        [null, null, '.'], 
        $deger
    );
}

Biraz daha basitleştirirsek, gelen şifre değerini veritabanına kaydetmeden evvel md5 ile şifrelenmiş haliyle kaydetmek isteyelim. (Ama tabii siz bunu yapmayın, bcrypt ile saklayın)

/**
 * @param string $deger
 * @return void
 */
public function setSifreAttribute($deger)
{
    $this->attributes['sifre'] = md5($deger);
}

Zaman Mutatörleri

Eloquent, varsayılan olarak created_at ve updated_at sütunlarını Carbon (tarih/saat sınıfı) örneğine dönüştürür. $dates sınıf değişkeni üzerine zaman özniteliği adını (aşağıdaki örnekte silinme_tarihi ve yorum_tarihi) ekleyerek geçerli kılabiliyoruz.

class Gonderi extends Model
{
    protected $dates = [
        'created_at',
        'updated_at',
        'silinme_tarihi', // deleted_at
        'yorum_tarihi', // commented_at
    ];
}

 Tarih Formatları

Varsayılan olarak tarih formatı Y-m-d H:i:s olan formatını değiştirmek için kullanılır. $dateFormat sınıf değişkeni aracılığıyla değiştirilir.

class Gonderi extends Model
{
    protected $dateFormat = 'U'; // Unix zaman damgası cinsine döndürür
}

Öznitelik Biçimlendirme

$casts sınıf değişkeni sayesinde model özniteliklerini biçimlendirebiliyoruz (cast). Bu özellik oldukça kullanışlı. Örneğin, vertabanında bir kullanıcının yönetici olup olmadığını yonetici_mi sütunu ile belirliyoruz. MySQL’de boolean veri türü olmadığı için işimizi görecek veri türü Tiny Integer oluyor. Eğer yönetici ise 1 değilse 0 değerini alacağını varsayalım. Veriyi elde etmek istediğimizde yonetici_mi‘in 1 değilde, true olarak biçimlendirmek istiyoruz. Şunu yapmak yeterli geliyor:

class Uye extends Model
{
  protected $casts = [
    'yonetici_mi' => 'boolean',
    'son_giris' => 'timestamp',
    'fiyat' => 'float',
    'yas' => 'integer'
  ]
}

$casts dizisinin ilk elemanında değeri boolean veri türüne biçimlendirmesini söyledik. Diğer elemanlarda da farklı işlemler yaptık. son_giris unix zaman damgasına biçimlenecek. fiyat ondalık sayı tipine biçimlenecek. yas sayı tipine biçimlenecek. Desteklenen biçimlerse şu şekilde: integer, real, float, double, string, boolean, object, array, collection, date, datetime, ve timestamp.

 Dizi ve JSON Biçimlenirme

Dizi biçimlendirme, JSON olarak depolanan veri tiplerinde oldukça kullanışlıdır (MySQL’in 5.7 sürümü JSON veri tipini destekliyor, önceki sürümler için TEXT kullanılabilir).

class Uye extends Model
{
  protected $casts = ['bilgiler' => 'array'];
}

$uye = Uye::findOrFail($id);

$bilgiler = $uye->bilgiler;
$bilgiler['yas'] = 25;
$bilgiler['sehir'] = 'İstanbul';
$uye->bilgiler = $bilgiler;

$uye->save();

Kolay gelsin.


Kaynakça

  1. https://laravel.com/docs/5.6/eloquent-mutators