Laravel Türkiye Discord Kanalı Forumda kod paylaşılırken dikkat edilmesi gerekenler!Birlikte proje geliştirmek ister misiniz?

Selamlar

Service sınıfları uygulamamızın koşullarını gerçekleştirmek, uygulama dışı erişimler, uygulama içi bağımlılıkları soyutlamak vb. gibi durumlarda kullandığımız sınıflar.
Bu konuda aklıma takılan özel bir durum var. Daha çok kavramsal bir konu; Repository sınıfları.
Repository sınıfları database işlemlerini gerçekleştirdiğimiz depolarımız olarak nitelemek yanlış olmaz.

Bazı kod örneklerinde her repository için bir service sınıf uygulandığını gördüm. Örnek olarak:


class ProductController extends Controller{
        public function index(ProductService $productService) {
              $product = $productService->getAllProducts();
              return view("product.index", compact("product"));
         }
 }

class ProductService{
        public function getAllProducts(ProductRepository $productRepository) {
              return $productRepository->getAllProducts();
        }
}

class ProductRepository {
        public function getAllProducts() {
            return  \App\Models\Product::orderBy('created_at', 'asc')->get()
        }
}

Temelde uygulanan örnek bu şekilde, burada ProductService e ihtiyaç var mı? Doğrudan Repository e bir service sınıfı diyerek Controller da DI yapmamamızda ki sakınca nedir. Bu konuda kabul edilen fikir nedir?

Burada araya sırf Repositoryden alabileceğim bilgi için ekstradan bir service sınıfı tanımlamışım gibi geliyor.

Bu konuda ki fikirlerinizi merak ediyorum.

  • mgsmus bunu yanıtladı.
  • capodecina Bu verdiğiniz örnek çok doğru değil. Şöyle olabilir:

    class ProductService
    {
        public function __construct(
            protected ProductRepository $repository,
        )
        {}
    
        public function getRepository(): ProductRepository
        {
            return $this->repository;
        }
    }
    class ProductController extends Controller
    {
            public function __construct(
                protected ProductService $productService,
            )
            {}
            
            public function index() 
            {
                  $product = $this->productService
                      ->getRepository()
                      ->getAllProducts();
                  
                  return view("product.index", compact("product"));
             }
     }

    Servisin rolü ise şöyle olabilir:

    class ProductService
    {
        public function __construct(
            protected ProductRepository $repository,
        )
        {}
    
        public function getRepository(): ProductRepository
        {
            return $this->repository;
        }
    
        public function createProduct(array $data, ?User $authUser = null): Product
        {
            if($authUser instanceof User) {
                $data['created_by'] = $authUser->id;
            }
            
            $product = $this->getRepository()
                   ->getBuilder()
                   ->create($data);
    
            ProductCreated::dispatch($product, $authUser);
    
            return $product;
        }
    }
    class ProductController extends Controller
    {
            public function __construct(
                protected ProductService $productService,
            )
            {}
            
            public function index() 
            {
                  $product = $this->productService
                      ->getRepository()
                      ->getAllProducts();
                  
                  return view("product.index", compact("product"));
             }
    
            public function store(StoreProductRequest $request)
            {
                $product = $this->productService
                    ->createProduct($request->validated(), $request->user());
    
                return view('product.show', compact('product'));
            }
     }

    capodecina Bu verdiğiniz örnek çok doğru değil. Şöyle olabilir:

    class ProductService
    {
        public function __construct(
            protected ProductRepository $repository,
        )
        {}
    
        public function getRepository(): ProductRepository
        {
            return $this->repository;
        }
    }
    class ProductController extends Controller
    {
            public function __construct(
                protected ProductService $productService,
            )
            {}
            
            public function index() 
            {
                  $product = $this->productService
                      ->getRepository()
                      ->getAllProducts();
                  
                  return view("product.index", compact("product"));
             }
     }

    Servisin rolü ise şöyle olabilir:

    class ProductService
    {
        public function __construct(
            protected ProductRepository $repository,
        )
        {}
    
        public function getRepository(): ProductRepository
        {
            return $this->repository;
        }
    
        public function createProduct(array $data, ?User $authUser = null): Product
        {
            if($authUser instanceof User) {
                $data['created_by'] = $authUser->id;
            }
            
            $product = $this->getRepository()
                   ->getBuilder()
                   ->create($data);
    
            ProductCreated::dispatch($product, $authUser);
    
            return $product;
        }
    }
    class ProductController extends Controller
    {
            public function __construct(
                protected ProductService $productService,
            )
            {}
            
            public function index() 
            {
                  $product = $this->productService
                      ->getRepository()
                      ->getAllProducts();
                  
                  return view("product.index", compact("product"));
             }
    
            public function store(StoreProductRequest $request)
            {
                $product = $this->productService
                    ->createProduct($request->validated(), $request->user());
    
                return view('product.show', compact('product'));
            }
     }

      mgsmus

      Kesinlikle yanlış yazmışım, içeriğini size referans vereceğim bu verdiğiniz örnek üzerine arada ki ProductService ihtiyaç var mı?
      Repositoryi doğrudan controller da kullanmamızda ki sakınca nedir.

      Ayrıca ProductService şuan repository üzerinde işlemler yapabilmeyi sağlıyor. DB üzerinden çekilen dataları manüpüle
      edebilir belirli bir duruma bağlı olarak, peki bir API isteği gerçekleştirmem gereken durumlarda veya uygulamamın business logiclerinin işletilmesini yine bu ProductService de mi gerçekleştirmeliyim?

      Product Insert işleminde; API üzerinden bir yere istek attığım, dönen sonuca göre Repositoryden belirli id li ürünleri çektiğim sonrasında da bunlarla kendi uygulama logiclerimi işlettiğim durumda ProductService God Class diye tabir edilen bir yapıya dönmeyecek midir?

      Burada ki ayrımı hep dışarıyla bağlantı kuran(API), uygulama durumlarımı yöneten ve işleten, Async işlemler için producer service gibi ayrı olması gerektini düşünüyordum.

      Özetle bir Service sınıfının kapsamı tam olarak ne olmalı nerede duracak bu Service sınıfının gelişmesi gibi sorularım var.

        capodecina

        capodecina bu verdiğiniz örnek üzerine arada ki ProductService ihtiyaç var mı?

        Bir uygulama temel olarak ikiye ayrılır; iş (business) ve uygulama (application). Burada servis, iş katmanına ait işlemleri gerçekleştirmeniz için; repository ise sizin uygulama katmanında veritabanı üzerinde ve/veya modelin üstüne inşa edilmiş bir yapı. Repository ürün oluşturmaz, çünkü o katmana girdiğinizde artık ürün diye bir şey yoktur, o katmanda o yapı bir girdidir (entry). Ona, işinize uygun anlamı yüklediğiniz yer servistir. Elbette uygulama katmanında da servisler olur ama genellikle Manager, Finder vs gibi isimlendirilir.

        capodecina Repositoryi doğrudan controller da kullanmamızda ki sakınca nedir.

        Bir sakınca yok ama portatif kod yazmamış olursunuz. ProductServis'i her yerde kullanabilirsiniz, gördüğünüz gibi kesin bir bağlılığı yok. Ürünü bir controller ile gelen bir istek de oluşturabilir, kuyruktaki bir Job da oluşturabilir, konsolda çalışan bir Command da oluşturabilir. Bu durumda ya ortak kullanmanız gereken bir servis olacak ya da Action Pattern kullanıp yapılan işleri tek bir iş yapan ama her yerde çağırabileceğiniz küçük sınıflara böleceksiniz, CreateProduct::handle($data, $user) gibi ama burada da Poltergeist (ya da Gipsy Wagon) dediğimiz anti-pattern'ın oluşma durumu olabilir. Nested ve/veya gereksiz obje kullanımından kaçınmanız gerekecek.

        capodecina Ayrıca ProductService şuan repository üzerinde işlemler yapabilmeyi sağlıyor. DB üzerinden çekilen dataları manüpüle
        edebilir belirli bir duruma bağlı olarak, peki bir API isteği gerçekleştirmem gereken durumlarda veya uygulamamın business logiclerinin işletilmesini yine bu ProductService de mi gerçekleştirmeliyim?

        Servis bu iş için iyi bir yer. Burada şöyle bir durum daha var, genellike direkt servis değil onu gerçeklediği interface enjekte edilir, örneğin ProductServiceInterface. Böylece Laravel'in service container özelliğe ile Inversion of Control ilkesi sağlanmış olur ve tek bir noktadan altını (gerçek sınıfı) değiştirebileceğiniz bir yapı elde edersiniz. Şu konuda bir şeyler anlatmışım: https://laravel.gen.tr/d/5835-crud-islemleri

        capodecina Product Insert işleminde; API üzerinden bir yere istek attığım, dönen sonuca göre Repositoryden belirli id li ürünleri çektiğim sonrasında da bunlarla kendi uygulama logiclerimi işlettiğim durumda ProductService God Class diye tabir edilen bir yapıya dönmeyecek midir?

        Servisleri daha küçük servislere bölerek servisler üzerindeki yükü azaltabilirsiniz, sürekli kullanılmayan yerleri direkt repository'den çıkıp gerekirse ileride servise alabilirsiniz ama servisler arası veri aktarımında kullandığınız yapı Poltergeist oluşmasına da sebep olabilir dikkatli olmanız lazım.

        capodecina Özetle bir Service sınıfının kapsamı tam olarak ne olmalı nerede duracak bu Service sınıfının gelişmesi gibi sorularım var.

        Sınıfın kapsamı bağlamıdır. ProductService isimli bir sınıfınız varsa bu sınıfın demek ki bağlamı üründür ve ürün ile ilgili temel görevleri yapacak demektir. Eğer bağlamın dışına çıkarsa God olur; bağlamın temel tüm gereksinimlerini sağlamazsa yetersiz olur, çok fazla sınıfla objeler kullanarak haberleşirse Poltergeist oluşur. Her 10 işlemde 1 kere kullanılan bir işi o servise koymazsınız; her ürün oluşturmanızda gerekli olan bir işi servise alırsınız vs...

        Eğer her şeyi iki parağrafta anlatabilseydik felsefeye gerek olmazdı. Bu soruların kesin kabul görmüş cevapları yok, herkes farklı görüşte ve kod yazılırken savunulan yapının bile dışına çıkılan zamanlar olur. Her uygulamanın kaosa dönüştüğü yerleri olur, mükemmel bir uygulama ütopiktir, o yüzden versiyonlama diye bir kavram vardır. Benim tavsiyem Ockham'ın usturası prensibinden hareket ederek basitliğin peşinden koşmanız ki bazen basit olan anti-pattern de olabilir; sadece çok belirgin yanlışlardan uzak durun.

        • KKylo

            Seviye 2
          • Düzenlendi

          Peki bir proje halihazırda Eloquent kullanırken Repository Pattern peşinden gitmek ne kadar doğrudur?

          Bir de ben Service sınıflarında genellikle constructor kullanıyorum. Bu nedenle Controller sınıfında işlem yaparken Service sınıfından bir metoda ihtiyaç duyduğumda her seferinde yeni bir nesne oluşturmak durumunda kalıyorum. Bunun bir dezavantajı ya da daha iyi bir yolu var mıdır (statik metot hariç)?

            Kylo Eloquent ile Repository Pattern ayrı şeyler. Repository içinde sadece DB ile de işlem yapabilirsiniz. Ayrıca bir çok yerde ortak kullanacağınız sorguları bir yerde toplamanız gerekiyor. Bu da repository kullanmak için ideal bir senaryo.

            Kylo Bir de ben Service sınıflarında genellikle constructor kullanıyorum. Bu nedenle Controller sınıfında işlem yaparken Service sınıfından bir metoda ihtiyaç duyduğumda her seferinde yeni bir nesne oluşturmak durumunda kalıyorum. Bunun bir dezavantajı ya da daha iyi bir yolu var mıdır (statik metot hariç)?

            https://laravel.com/docs/9.x/container#binding

            • Kylo bunu yanıtladı.
              • KKylo

                  Seviye 2
                • Düzenlendi

                mgsmus Eloquent ile Repository Pattern ayrı şeyler.

                Evet farkındayım ama şu yazıya bir göz atabilir misiniz:

                https://adelf.tech/2019/useless-eloquent-repositories

                Diğer soruma gelince, şu şekilde basit bir Controller sınıfı için her seferinde yeni bir Service oluşturmamak adına nasıl bir binding yapılabilir? (Kodu basit tutmak adına return tiplerini void olarak bıraktım)

                class UserController
                {
                	public function changePassword(ChangePasswordRequest $request, User $user): void
                    {
                		$userService = new UserService($user);
                		
                		$userService->changePassword(
                			$request->only(['old_password', 'new_password'])
                		);
                	}
                	
                	public function changeEmail(ChangeEmailRequest $request, User $user): void
                    {
                		$userService = new UserService($user);
                		
                		$userService->changeEmail($request->input('email'));
                	}
                }
                  • mgsmus

                    Seviye 1382
                  • Düzenlendi

                  Kylo Ben bu konuda repository örneği vermedim ama koda bakarsanız eğer getBuilder() şeklinde yöntem görürsünüz. Bu yöntem makalede anlatılan şeyi aşmak için. Şöyle mesela:

                  use App\Models\User;
                  use Illuminate\Database\Eloquent\Builder;
                  
                  class UserRepository
                  {
                      protected Builder $builder;
                  
                      public function getBuilder(): Builder
                      {
                          return $this->builder ?? User::query();
                      }
                  
                      public function setBuilder(Builder $builder): UserRepository
                      {
                          $this->builder = $builder;
                  
                          return $this;
                      }
                  
                      public function getUsersByRecentMembership(string $membershipType): Collection
                      {
                          return $this->getBuilder()
                              ->where(function ($query) {
                                  $query->select('type')
                                      ->from('membership')
                                      ->whereColumn('membership.user_id', 'users.id')
                                      ->orderByDesc('membership.start_date')
                                      ->limit(1);
                              }, $membershipType)
                              ->get();
                      }
                  }

                  Ben repository'yi UserRepository::getUsersByRecentMembership() gibi sorguları yapabilmek için kullanıyorum. Eloquent ile farklı derken anlatmak istediğim Eloquent'in üzerine aynı işi yapan bir katman olmadığı idi. Makaledeki gibi şeylere ihtiyacım olsa zaten:

                  $user = $this->userRepository
                      ->getBuilder()
                      ->findOrFail($id);

                  şeklinde yapabilirim.

                  Diğer sorunuzda ise şöyle ilerleyebilirsiniz:

                  public function changePassword(ChangePasswordRequest $request, User $user): void
                  {		
                      $this->userService
                          ->changePassword($user, $request->input('new_password'));
                  }

                  yani

                  class UserService
                  {
                      public function changePassword(User $user, string $password): User
                      {
                          $user->password = bcrypt($password);
                          $user->save();
                  
                          PasswordChanged::dispatch($user);
                  
                          return $user;
                      }
                  }
                  • Kylo bunu yanıtladı.

                    mgsmus Cevaplarınız için çok teşekkürler, çok faydalı bilgiler veriyorsunuz. İzin verirseniz konuyla ilgili birkaç sorum daha olacak, UserService sınıfı için __construct kullanmak yerine User'i parametre olarak kullanmayı tavsiye etmişsiniz ama anlayamadığım nokta bu şekilde yaptığımda zaten changePassword metodu statik metot gibi bir hâle geliyor.

                    UserService sınıfında Useri parametre olarak alan 10'dan fazla metodum varsa, sizce Service sınıfı için __construct kullanmak mı yoksa dediğiniz şekilde bütün metotlara parametre vermek mi daha doğru olur?

                    1. Eğer __construct kullan derseniz Controller sınıfında da bir __construct tanımlayıp buradan her Controller metodunun erişebileceği bir UserService nesnesi oluşturmanın yolu var mıdır (oluru yok gibi duruyor ama yine de sorayım dedim)? Örneğin (telefondan yazıyorum):
                    class UserController
                    {
                    	private $userService;
                    
                    	public function __construct(User $user, UserService $userService): void
                        {
                        // istekten gelen $user'i (varsa) burada yakalamanın bir yolu var mı?
                    		$this->userService = new UserService($user);
                    	}
                    	
                    	public function changePassword(ChangePasswordRequest $request): void
                        {
                    		$this->userService->changePassword(
                    			$request->only(['old_password', 'new_password'])
                    		);
                    	}
                    1. Eğer parametrelerden git derseniz UserService metotlarını statik yapmakla yapmamak arasındaki fark nedir?

                      Kylo UserService sınıfını kurucu yöntem içine enjekte etmeniz daha iyi ama onlar Auth yüklenmeden çağrıldığı için orada giriş yapmış kullanıcıya erişemezsiniz. Orada yapabileceğiniz şeyler sınırlı.

                      OOP içerisinde static yöntem kullanmanın bazı dezavantajları var;

                      • Diyelim ki UserService::changePassword() şeklinde kullandınız. Bunu kullandığınız yere bunu çivi gibi çakmış oluyorsunuz. Bunu artık kolay değiştirmenin yolu yok. Yöntemin yaptığı işi değiştirseniz kullandığınız her yerde değişecek. Ayrı bir yöntem yazsanız kullandığınız her yerde değiştirmeniz gerekecek. Yani refactor yapmadan olmayacak.
                      • Diğer bir nokta da, diyelim ki bunun bir controller yöntemi içerisinde kullandınız, bunun nerede kullanıldığını bilmenizin yolu yok. Halbuki enjekte ettiğinizde service container sayesinde bunu bilebiliyorsunuz ve değiştirebiliyorsunuz.
                      • Dependency Injection olarak Interface Injection kullanamazsınız, bu da service container aracılığıyla Inversion of Control prensibinden mahrum kalacağınız anlamına geliyor. Şu yazıma bakın.
                      • Bir controller yöntemi düşünün, bir iş yaptıkan sonra bir event fırlatıyor, o eventı dinleyen bir listener ise başka bir işlem yapıyor ve yöntem bir http yanıt dönüyor. Bu yanıtı Laravel'de event ve listener'ı gerçekten tetiklemeden test edebilirsiniz çünkü bağlı gereksinimleri mocklayabilir ya da değiştirebilirsiniz. UserService::changePassword() kullandığınızda ise bağlı olan bir şey olmadığı için test sırasında bu kod hep çalışır, çalışmasın diyemezsiniz.

                      Başka dezavantajları da vardır, araştırabilirsiniz.

                      Bana göre service container olan bir yapıda static yöntem kullanmaya gerek yok. İlla kullanacağım diyorsanız Laravel'in buna bir çözümü var:
                      https://laravel.com/docs/9.x/facades