Laravel 8'de Rest API Anotasyonları

PHP 8 ile birlikte attribute desteğinin gelmesiyle birlikte meta-data ekleme işlemleri nihayet built-in bir duruma gelmiş oldu. Daha önceleri Symfony Anotation kütüphaneleri ile yorum şeklinde (PHP-Doc) çözüm sağlanabiliyordu.

C#, Java dillerinden de aşina olunduğu haliyle anotation ya da attribute bir çok işi meta yoluyla zahmetsizce yapabiliyor. Örneğin Java Spring framework'üne ait şu örneği inceleyelim :

  @GetMapping("/employees/{id}")
  Employee one(@PathVariable Long id) {

    return repository.findById(id)
      .orElseThrow(() -> new EmployeeNotFoundException(id));
  }

Bu kod bloğu ile derleyiciye kısaca şunu anlatmış olduk

  1. Res API isteği olarak GET metodu gelirse
  2. Endpoint adı employees/x şeklinde yapılırsa

Mapleme işlemi başlatılmış olacak. Bu şekilde meta aktarımını kısaca yapmış oluyoruz.

Bu yaklaşımı PHP 8 öncesi şu şekilde yapabiliyorduk :

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog", name="blog_list")
     */
    public function list()
    {
        // ...
    }
}

Symfony doctrine/annotations kütüphanesi yardımıyla PHP-Doc şeklinde anotasyon bilfirimi yapabiliyoruz. Ancak görüldüğü üzere iki sistem arasında dağlar kadar fark var. Bunun en büyük nedeni yorum üzerinden yaptığınız işlemlerin de-facto bir şekilde ele alınıyor olması. Örneğin pure PHP yazacağım derseniz bu yaklaşımı kullanmak için bir kaç yoldan geçmeniz gerekiyor.

  1. Composer kur
  2. Symfony annotation paketini ekle

PHP 8 öncesi kendi anotasyonunuzu yazmak için de yine aynı tür paketlerin kurulumu gerekiyordu. Bu akışın kullanıma hazır (out of the box) bir şekilde yapılması PHP 8 ile mümkün bir hale geldi. Ancak unutmayalım Rest API türü işlemler için yine de kendi attribute akışınızı yazmanız gerekiyor.

Anotasyonlar, XML veya JSON şeması biçiminde ayrı bir tanımda yazılmak yerine, bu meta verileri düzenlemenin kolay ve yönetilebilir bir yolunu sağlar.

PHP 8 ile birlikte hayatımıza giren attribute desteği sayesinde Laravel özelinde kullandığımız api.php dosyasındaki yakşalımları daha kolay bir seviyeye taşımak mümkün hale geldi. Bunun için spatie/laravel-route-attributes paketinden yararlanacağız. Detaylı bilgi için https://github.com/spatie/laravel-route-attributes adresinden yararlanabilirsiniz.

Önce, kurulumla başlayalım

1- composer require spatie/laravel-route-attributes

2- php artisan vendor:publish --provider="Spatie\RouteAttributes\RouteAttributesServiceProvider" --tag="config"

3- Config dosyasına da bir gözatabilirsiniz (config/route-attributes.php)

<?php

return [
    /*
     *  Automatic registration of routes will only happen if this setting is `true`
     */
    'enabled' => true,

    /*
     * Controllers in these directories that have routing attributes
     * will automatically be registered.
     */
    'directories' => [
        app_path('Http/Controllers'),
    ],

    /**
     * This middleware will be applied to all routes.
     */
    'middleware' => [
        \Illuminate\Routing\Middleware\SubstituteBindings::class
    ]
];

ve hazırız.

Bir GET metodu örneği şıu şekilde yapılabilir:

use Spatie\RouteAttributes\Attributes\Get;

class UserController
{
    #[Get('users')]
    public function getUsers()
    {

    }
}

Nasıl harika değil mi :)

Deklare ettiğimiz anotasyon Laravel tarafından otomatik olarak şu şekilde register edilir:

Route::get('users', [UserController::class, 'getUsers']);

Bu peketle birlikte aşağıdaki tüm HTTP metodlarını kullanabilirsiniz :

#[Spatie\RouteAttributes\Attributes\Post('my-uri')]
#[Spatie\RouteAttributes\Attributes\Put('my-uri')]
#[Spatie\RouteAttributes\Attributes\Patch('my-uri')]
#[Spatie\RouteAttributes\Attributes\Delete('my-uri')]
#[Spatie\RouteAttributes\Attributes\Options('my-uri')]

Geldik asıl konuya :) Peki nasıl middleware ekleyeceğiz ? Haklısınız authentication ve authorization konusu çok önemli. Laravel'de standartta yaptığımız işlemleri bu paket ile yapabiliyoruz.

Tüm HTTP metodları middleware class'ı veya class dizisi kabul edebilir. Örneğin;

use Spatie\RouteAttributes\Attributes\Get;

class UserController
{
    #[Get('users', middleware: RoleMiddleware::class)]
    public function getUsers()
    {

    }
}

Yukarıdaki kod öbeği şuna dönüşür :

Route::get('users', [UserController::class, 'users'])->middleware(RoleMiddleware::class);

Bir sınıfın tüm metodlarına middleware uygulamak için Middleware özelliğini kullanabilirsiniz. Bunu, bir metoda anotasyon uygulayarak çoğaltabilirsiniz.

use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Middleware;

#[Middleware(EnsureHasTokenMiddleware::class)]
class UserController
{
    #[Get('users')]
    public function getUsers()
    {
       //...
    }

    #[Get('users/{id}', middleware: RoleMiddleware::class)]
    public function getUser(int $id)
    {
       //...
    }
}

Bu pakette maalesef middeware önceliği konusu yer almıyor. Şöyle ki, class üzerinde tanımladığınız genel-kapsar bir middleware, methodlarda yeralan middleware'ler tarafından ezilemiyor aksine bir önceki örnekte olduğu gibi çoğaltılıyor.

Şöyle bir logic güzel olurdu :

#[Middleware(EnsureHasTokenMiddleware::class)]
class UserController
{
    #[Get('users')]
    public function getUsers()
    {
       //...
    }

    #[Get('users/{id}', middleware: AnonymousMiddleware::class)]
    public function everybodyCanAccessHere(int $id)
    {
       //...
    }
}

Class özelinde token alınmasını zorunlu kıldık ancak everybodyCanAccessHere metoduna herkesin erişimini mümkün kıldık. Ancak dediğim gibi bu yaklaşım şu aşamada pakette yer almıyor. Belki bir PR ile ben katkı yaparım :)

Son olarak prefix özelliğinden bahsetmek gerekirse kurgu şu şekilde işliyor:

use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Prefix;

#[Prefix('users')]
class UserController
{
    #[Get('/)]
    public function getUsers()
    {
      //...
    }

    #[GET('{id}')]
    public function getUser(int $id)
    {
      //...
    }
}

Bu sayede her seferinde users anahtar kelimesini kullanmak zorunda kalmıyoruz. Bu kod parçacağı arkaplanda şuna dönüşüyor :

Route::get('users', [UserController::class, 'getUsers']);
Route::post('users/{id}', [UserController::class, 'getUser']);

Bir yazının daha sonuna geldik, okuduğunuz için teşekkürler.

KeepCoding :-)

No Comments Yet