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

“Mikro” Ufak PHP Framework

composer require yidemir/mikro

komutuyla ilgili dizine projeyi dahil ettiğiniz taktirde fonksiyonlar otomatik olarak işlenecektir.

 Özellikler

  • Basit şifreleme/şifre çözme
  • Veritabanı fonksiyonelliği (oluştur, oku, güncelle, sil, sorgula)
  • Sayfalama (dizi sayfalama/veritabanı ilintili sayfalama)
  • Basit talep işleme (request)
  • Yanıt (response) işlemleri (html, json, yönlendirme)
  • Rotacı (rota metodları, gruplama, kaynak işleme)
  • Doğrulama (basit validasyon işlemleri, form validasyonu)
  • Görünüm (view) işleme (Basit ama kullanışlı gövde ve blok işlemleri)

 Neden ihtiyaç duyayım?

Orta çaplı ya da büyük bir projede, projeden bağımsız servis geliştirmek için idealdir.

 Nasıl kullanırım?

Evvela bir index.php sayfası oluşturalım ve yazmaya başlayalım:

require __DIR__ . '/vendor/autoload.php';

view\path(__DIR__ . '/views'); // görünüm dosyalarının bulunacağı dizin
crypt\secret('foobar'); // şifreleme kullanılacaksa belirlenmesi gereken gizli anahtar

route\get('/', function() {
    $posts = db\table('posts')->get('where comment_count=? and is_published=1', [5]);
    return response\json($posts);
});

route\get('/show/:id', function($id) {
    $post = db\table('posts')->find($id);
    return response\json($post);
});

route\post('/', function() {
    $values = request\all();
    $validator = validator\validate($values, [
        'title' => 'required|maxlen:255',
        'body' => 'required|minlen:5',
        'created_at' => 'required|time',
        'is_published' => 'nullable'
    ]);

    if ($validator->fails) {
        return response\json([
            'message' => 'Form verileri geçersiz',
            'errors' => $validator->errors,
            'status' => 422
        ], 422);
    }

    db\table('posts')->insert($validator->values);
    return response\json([
        'message' => 'Gönderi başarıyla eklendi',
        'code' => 200
    ]);
});

Veya daha karmaşık bir uygulama yazacaksanız rota gruplama ve rota kaynağı oluşturma özelliğini kullanabilirsiniz.

route\get('/', 'AppControllersHomeController@index');

route\group([
    'path' => '/admin',
    'namespace' => 'AppControllersAdmin\',
    'middleware' => ['check_admin_callback']
], function() {
    route\get('/', 'DashboardController@index');
    route\resource('/posts', 'PostController');
    route\resource('/categories', 'CategoryController');
});

Daha detaylı bilgiler için kaynak kodlarına göz atabilir ve examples dizinini inceleyebilirsiniz. Dökümantasyon için henüz hazır değil.

İyi çalışmalar