Uygulama Dizin Yapısı

Nette Framework projeleri için anlaşılır ve ölçeklenebilir bir dizin yapısı nasıl tasarlanır? Kodunuzu düzenlemenize yardımcı olacak kanıtlanmış en iyi uygulamaları göstereceğiz. Şunları öğreneceksiniz:

  • uygulamayı dizinlere mantıksal olarak nasıl bölersiniz
  • yapıyı projenin büyümesiyle iyi ölçeklenecek şekilde nasıl tasarlarsınız
  • olası alternatifler ve avantajları veya dezavantajları nelerdir

Nette Framework'ün kendisinin herhangi bir belirli yapıya bağlı olmadığını belirtmek önemlidir. Herhangi bir ihtiyaca ve tercihe kolayca uyarlanabilecek şekilde tasarlanmıştır.

Temel Proje Yapısı

Nette Framework herhangi bir sabit dizin yapısı dikte etmese de, Web Project şeklinde kanıtlanmış bir varsayılan düzenleme vardır:

web-project/
├── app/              ← uygulama dizini
├── assets/           ← SCSS, JS dosyaları, resimler..., alternatif olarak resources/
├── bin/              ← komut satırı betikleri
├── config/           ← yapılandırma
├── log/              ← günlüğe kaydedilen hatalar
├── temp/             ← geçici dosyalar, önbellek
├── tests/            ← testler
├── vendor/           ← Composer tarafından kurulan kütüphaneler
└── www/              ← genel dizin (document-root)

Bu yapıyı ihtiyaçlarınıza göre serbestçe değiştirebilirsiniz – klasörleri yeniden adlandırabilir veya taşıyabilirsiniz. Ardından, yalnızca Bootstrap.php dosyasındaki ve muhtemelen composer.json dosyasındaki dizinlere giden göreceli yolları ayarlamanız yeterlidir. Başka hiçbir şeye gerek yoktur, karmaşık yeniden yapılandırma yok, sabitlerde değişiklik yok. Nette akıllı otomatik algılamaya sahiptir ve URL tabanı da dahil olmak üzere uygulamanın konumunu otomatik olarak tanır.

Kod Organizasyon Prensipleri

Yeni bir projeyi ilk kez incelerken, içinde hızla yönünüzü bulabilmelisiniz. app/Model/ dizinini açtığınızı ve şu yapıyı gördüğünüzü hayal edin:

app/Model/
├── Services/
├── Repositories/
└── Entities/

Bundan yalnızca projenin bazı servisler, depolar ve varlıklar kullandığını anlarsınız. Uygulamanın gerçek amacı hakkında hiçbir şey öğrenemezsiniz.

Başka bir yaklaşıma bakalım – alanlara göre organizasyon:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Burada durum farklı – ilk bakışta bunun bir e-ticaret sitesi olduğu açık. Dizin adlarının kendisi uygulamanın neler yapabildiğini ortaya koyuyor – ödemeler, siparişler ve ürünlerle çalışıyor.

İlk yaklaşım (sınıf türüne göre organizasyon) pratikte bir dizi sorun getirir: mantıksal olarak birbiriyle ilişkili kod farklı klasörlere dağılmıştır ve aralarında atlamanız gerekir. Bu nedenle alanlara göre organize edeceğiz.

İsim Alanları (Namespaces)

Dizin yapısının uygulamadaki isim alanlarıyla örtüşmesi adettendir. Bu, dosyaların fiziksel konumunun isim alanlarına karşılık geldiği anlamına gelir. Örneğin, app/Model/Product/ProductRepository.php içinde bulunan bir sınıfın App\Model\Product isim alanına sahip olması gerekir. Bu prensip, kodda gezinmeye yardımcı olur ve otomatik yüklemeyi basitleştirir.

Adlarda Tekil vs Çoğul Sayı

Uygulamanın ana dizinlerinde tekil sayı kullandığımıza dikkat edin: app, config, log, temp, www. Aynı şekilde uygulama içinde de: Model, Core, Presentation. Bunun nedeni, her birinin bütün bir kavramı temsil etmesidir.

Benzer şekilde, örneğin app/Model/Product, ürünlerle ilgili her şeyi temsil eder. Buna Products demeyiz, çünkü ürünlerle dolu bir klasör değildir (orada nokia.php, samsung.php dosyaları olurdu). Ürünlerle çalışmak için sınıflar içeren bir isim alanıdır – ProductRepository.php, ProductService.php.

app/Tasks klasörü çoğul sayıdadır çünkü bir dizi bağımsız yürütülebilir betik içerir – CleanupTask.php, ImportTask.php. Her biri bağımsız bir birimdir.

Tutarlılık için şunları kullanmanızı öneririz:

  • İşlevsel bir bütünü temsil eden isim alanı için tekil sayı (birden fazla varlıkla çalışsa bile)
  • Bağımsız birimlerin koleksiyonları için çoğul sayı
  • Emin olmadığınızda veya bunun hakkında düşünmek istemiyorsanız, tekil sayıyı seçin

Genel Dizin www/

Bu dizin, web'den erişilebilen tek dizindir (document-root olarak da bilinir). www/ yerine public/ adıyla da sıkça karşılaşabilirsiniz – bu sadece bir gelenek meselesidir ve uygulamanın işlevselliği üzerinde hiçbir etkisi yoktur. Dizin şunları içerir:

  • Uygulamanın Giriş noktası index.php
  • mod_rewrite (Apache için) kuralları içeren .htaccess dosyası
  • Statik dosyalar (CSS, JavaScript, resimler)
  • Yüklenen dosyalar

Uygulamanın doğru güvenliği için document-root'un doğru şekilde yapılandırılması esastır.

node_modules/ klasörünü asla bu dizine yerleştirmeyin – yürütülebilir olabilecek ve genel olarak erişilebilir olmaması gereken binlerce dosya içerir.

Uygulama Dizini app/

Bu, uygulama kodunu içeren ana dizindir. Temel yapı:

app/
├── Core/               ← altyapısal konular
├── Model/              ← iş mantığı
├── Presentation/       ← presenter'lar ve şablonlar
├── Tasks/              ← komut betikleri
└── Bootstrap.php       ← uygulamanın başlatma sınıfı

Bootstrap.php, ortamı başlatan, yapılandırmayı yükleyen ve DI konteynerini oluşturan uygulamanın başlangıç sınıfıdır.

Şimdi bireysel alt dizinlere daha ayrıntılı bakalım.

Presenter'lar ve Şablonlar

Uygulamanın sunum kısmı app/Presentation dizinindedir. Alternatif olarak kısa app/UI da kullanılabilir. Bu, tüm presenter'lar, şablonları ve olası yardımcı sınıflar için yerdir.

Bu katmanı alanlara göre organize ederiz. E-ticaret, blog ve API'yi birleştiren karmaşık bir projede yapı şöyle görünürdü:

app/Presentation/
├── Shop/              ← e-ticaret ön yüzü
│   ├── Product/
│   ├── Cart/
│   └── Order/
├── Blog/              ← blog
│   ├── Home/
│   └── Post/
├── Admin/             ← yönetim
│   ├── Dashboard/
│   └── Products/
└── Api/               ← API uç noktaları
	└── V1/

Buna karşılık, basit bir blog için şu bölümlemeyi kullanırdık:

app/Presentation/
├── Front/             ← web sitesi ön yüzü
│   ├── Home/
│   └── Post/
├── Admin/             ← yönetim
│   ├── Dashboard/
│   └── Posts/
├── Error/
└── Export/            ← RSS, site haritaları vb.

Home/ veya Dashboard/ gibi klasörler presenter'ları ve şablonları içerir. Front/, Admin/ veya Api/ gibi klasörlere modüller diyoruz. Teknik olarak bunlar, uygulamayı mantıksal olarak bölmek için kullanılan normal dizinlerdir.

Presenter içeren her klasör, aynı adı taşıyan bir presenter ve şablonlarını içerir. Örneğin, Dashboard/ klasörü şunları içerir:

Dashboard/
├── DashboardPresenter.php     ← presenter
└── default.latte              ← şablon

Bu dizin yapısı, sınıfların isim alanlarına yansır. Örneğin, DashboardPresenter, App\Presentation\Admin\Dashboard isim alanında bulunur (mapování presenterů bölümüne bakın):

namespace App\Presentation\Admin\Dashboard;

class DashboardPresenter extends Nette\Application\UI\Presenter
{
	// ...
}

Uygulamada Admin modülü içindeki Dashboard presenter'ına iki nokta üst üste gösterimiyle Admin:Dashboard olarak başvururuz. default eylemine ise Admin:Dashboard:default olarak başvururuz. İç içe geçmiş modüller durumunda, daha fazla iki nokta üst üste kullanırız, örneğin Shop:Order:Detail:default.

Esnek Yapı Geliştirme

Bu yapının büyük avantajlarından biri, projenin artan ihtiyaçlarına ne kadar zarif bir şekilde uyum sağladığıdır. Örnek olarak, XML beslemeleri oluşturan bölümü ele alalım. Başlangıçta basit bir formumuz var:

Export/
├── ExportPresenter.php   ← tüm dışa aktarımlar için tek bir presenter
├── sitemap.latte         ← site haritası için şablon
└── feed.latte            ← RSS beslemesi için şablon

Zamanla başka besleme türleri eklenir ve onlar için daha fazla mantığa ihtiyacımız olur… Sorun değil! Export/ klasörü basitçe bir modül haline gelir:

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── zbozi.latte         ← Zboží.cz için besleme
	└── heureka.latte       ← Heureka.cz için besleme

Bu dönüşüm tamamen sorunsuzdur – sadece yeni alt klasörler oluşturmanız, kodu bunlara bölmeniz ve bağlantıları güncellemeniz yeterlidir (örneğin, Export:feed yerine Export:Feed:zbozi). Bu sayede yapıyı ihtiyaçlara göre kademeli olarak genişletebiliriz, iç içe geçme seviyesi sınırlı değildir.

Örneğin, yönetimde sipariş yönetimiyle ilgili OrderDetail, OrderEdit, OrderDispatch vb. gibi birçok presenter'ınız varsa, daha iyi organizasyon için bu noktada (klasörler için) Detail, Edit, Dispatch ve diğer presenter'ları içerecek bir Order modülü (klasörü) oluşturabilirsiniz.

Şablonların Konumu

Önceki örneklerde, şablonların doğrudan presenter içeren klasörde bulunduğunu gördük:

Dashboard/
├── DashboardPresenter.php     ← presenter
├── DashboardTemplate.php      ← şablon için isteğe bağlı sınıf
└── default.latte              ← şablon

Bu konum pratikte en uygun olanıdır – tüm ilgili dosyalarınız hemen elinizin altındadır.

Alternatif olarak, şablonları templates/ alt klasörüne yerleştirebilirsiniz. Nette her iki seçeneği de destekler. Hatta şablonları tamamen Presentation/ klasörünün dışına bile yerleştirebilirsiniz. Şablonların yerleştirilme seçenekleri hakkında her şeyi Şablonları Bulma bölümünde bulabilirsiniz.

Yardımcı Sınıflar ve Bileşenler

Presenter'lara ve şablonlara genellikle başka yardımcı dosyalar da eşlik eder. Bunları etki alanlarına göre mantıksal olarak yerleştiririz:

1. Doğrudan presenter yanında, belirli bir presenter için özel bileşenler durumunda:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← ürün listeleme için bileşen
└── FilterForm.php         ← filtreleme için form

2. Modül için – alfabenin hemen başında düzgün bir şekilde yerleştirilecek olan Accessory klasörünü kullanmanızı öneririz:

Front/
├── Accessory/
│   ├── NavbarControl.php    ← ön yüz için bileşenler
│   └── TemplateFilters.php
├── Product/
└── Cart/

3. Tüm uygulama için – Presentation/Accessory/ içinde:

app/Presentation/
├── Accessory/
│   ├── LatteExtension.php
│   └── TemplateFilters.php
├── Front/
└── Admin/

Veya LatteExtension.php veya TemplateFilters.php gibi yardımcı sınıfları altyapısal app/Core/Latte/ klasörüne yerleştirebilirsiniz. Ve bileşenleri app/Components içine. Seçim, ekibin alışkanlıklarına bağlıdır.

Model – Uygulamanın Kalbi

Model, uygulamanın tüm iş mantığını içerir. Organizasyonu için yine kural geçerlidir – alanlara göre yapılandırırız:

app/Model/
├── Payment/                   ← ödemelerle ilgili her şey
│   ├── PaymentFacade.php      ← ana giriş noktası
│   ├── PaymentRepository.php
│   ├── Payment.php            ← varlık
├── Order/                     ← siparişlerle ilgili her şey
│   ├── OrderFacade.php
│   ├── OrderRepository.php
│   ├── Order.php
└── Shipping/                  ← kargoyla ilgili her şey

Modelde tipik olarak şu tür sınıflarla karşılaşırsınız:

Fasadlar (Facades): Uygulamadaki belirli bir alana ana giriş noktasını temsil ederler. Tam kullanım senaryolarını (use-cases) uygulamak için farklı servisler arasındaki işbirliğini koordine eden bir orkestratör görevi görürler (örneğin “sipariş oluştur” veya “ödemeyi işle”). Orkestrasyon katmanının altında, fasad uygulama ayrıntılarını uygulamanın geri kalanından gizler, böylece söz konusu alanla çalışmak için temiz bir arayüz sağlar.

class OrderFacade
{
	public function createOrder(Cart $cart): Order
	{
		// doğrulama
		// sipariş oluşturma
		// e-posta gönderme
		// istatistiklere yazma
	}
}

Servisler: Alan içindeki belirli bir iş operasyonuna odaklanırlar. Tüm kullanım senaryolarını düzenleyen bir fasadın aksine, bir servis belirli bir iş mantığını uygular (fiyat hesaplamaları veya ödeme işlemleri gibi). Servisler tipik olarak durumsuzdur ve daha karmaşık operasyonlar için yapı taşları olarak fasadlar tarafından veya daha basit görevler için doğrudan uygulamanın diğer bölümleri tarafından kullanılabilirler.

class PricingService
{
	public function calculateTotal(Order $order): Money
	{
		// fiyat hesaplama
	}
}

Depolar (Repositories): Veri deposuyla, tipik olarak veritabanıyla tüm iletişimi sağlarlar. Görevi, varlıkları yüklemek ve kaydetmek ve bunları aramak için metotlar uygulamaktır. Depo, uygulamanın geri kalanını veritabanının uygulama ayrıntılarından soyutlar ve verilerle çalışmak için nesne yönelimli bir arayüz sağlar.

class OrderRepository
{
	public function find(int $id): ?Order
	{
	}

	public function findByCustomer(int $customerId): array
	{
	}
}

Varlıklar (Entities): Uygulamadaki ana iş kavramlarını temsil eden, kendi kimlikleri olan ve zamanla değişen nesnelerdir. Tipik olarak bunlar, ORM (Nette Database Explorer veya Doctrine gibi) kullanılarak veritabanı tablolarına eşlenen sınıflardır. Varlıklar, verileriyle ilgili iş kurallarını ve doğrulama mantığını içerebilir.

// orders veritabanı tablosuna eşlenen varlık
class Order extends Nette\Database\Table\ActiveRow
{
	public function addItem(Product $product, int $quantity): void
	{
		$this->related('order_items')->insert([
			'product_id' => $product->id,
			'quantity' => $quantity,
			'unit_price' => $product->price,
		]);
	}
}

Değer Nesneleri (Value Objects): Kendi kimlikleri olmayan değerleri temsil eden değişmez nesnelerdir – örneğin bir para tutarı veya bir e-posta adresi. Aynı değerlere sahip iki değer nesnesi örneği özdeş kabul edilir.

Altyapısal Kod

Core/ (veya Infrastructure/) klasörü, uygulamanın teknik temelinin evidir. Altyapısal kod tipik olarak şunları içerir:

app/Core/
├── Router/               ← yönlendirme ve URL yönetimi
│   └── RouterFactory.php
├── Security/             ← kimlik doğrulama ve yetkilendirme
│   ├── Authenticator.php
│   └── Authorizator.php
├── Logging/              ← günlükleme ve izleme
│   ├── SentryLogger.php
│   └── FileLogger.php
├── Cache/                ← önbellekleme katmanı
│   └── FullPageCache.php
└── Integration/          ← harici servislerle entegrasyon
	├── Slack/
	└── Stripe/

Daha küçük projelerde, elbette düz bir bölümleme yeterlidir:

Core/
├── RouterFactory.php
├── Authenticator.php
└── QueueMailer.php

Bu, şu kodu ifade eder:

  • Teknik altyapıyı çözer (yönlendirme, günlükleme, önbellekleme)
  • Harici servisleri entegre eder (Sentry, Elasticsearch, Redis)
  • Tüm uygulama için temel servisleri sağlar (posta, veritabanı)
  • Çoğunlukla belirli bir alandan bağımsızdır – önbellek veya günlükleyici e-ticaret veya blog için aynı şekilde çalışır.

Belirli bir sınıfın buraya mı yoksa modele mi ait olduğundan emin değil misiniz? Anahtar fark, Core/ içindeki kodun:

  • Alan hakkında hiçbir şey bilmemesi (ürünler, siparişler, makaleler)
  • Çoğunlukla başka bir projeye taşınabilmesi
  • “Nasıl çalıştığını” (bir e-posta nasıl gönderilir) çözmesi, “ne yaptığını” (hangi e-postanın gönderileceği) değil

Daha iyi anlamak için bir örnek:

  • App\Core\MailerFactory – e-posta göndermek için sınıf örnekleri oluşturur, SMTP ayarlarını çözer
  • App\Model\OrderMailer – siparişlerle ilgili e-postaları göndermek için MailerFactory kullanır, şablonlarını bilir ve ne zaman gönderilmeleri gerektiğini bilir

Komut Betikleri

Uygulamaların genellikle normal HTTP istekleri dışında etkinlikler gerçekleştirmesi gerekir – ister arka planda veri işleme, ister bakım, ister periyodik görevler olsun. Çalıştırma için bin/ dizinindeki basit betikler kullanılır, uygulama mantığının kendisi ise app/Tasks/ (veya app/Commands/) içine yerleştirilir.

Örnek:

app/Tasks/
├── Maintenance/               ← bakım betikleri
│   ├── CleanupCommand.php     ← eski verileri silme
│   └── DbOptimizeCommand.php  ← veritabanı optimizasyonu
├── Integration/               ← harici sistemlerle entegrasyon
│   ├── ImportProducts.php     ← tedarikçi sisteminden içe aktarma
│   └── SyncOrders.php         ← sipariş senkronizasyonu
└── Scheduled/                 ← düzenli görevler
	├── NewsletterCommand.php  ← bülten gönderme
	└── ReminderCommand.php    ← müşteri bildirimleri

Modele ne aittir ve komut betiklerine ne aittir? Örneğin, tek bir e-posta gönderme mantığı modelin bir parçasıdır, binlerce e-postanın toplu gönderimi zaten Tasks/ içine aittir.

Görevler genellikle komut satırından veya cron aracılığıyla çalıştırılır. HTTP isteği aracılığıyla da çalıştırılabilirler, ancak güvenliği göz önünde bulundurmak gerekir. Görevi başlatan presenter'ın güvenliğini sağlamak gerekir, örneğin yalnızca oturum açmış kullanıcılar için veya güçlü bir belirteç ve izin verilen IP adreslerinden erişimle. Uzun görevler için betik zaman aşımını artırmak ve oturumun kilitlenmemesi için session_write_close() kullanmak gerekir.

Diğer Olası Dizinler

Bahsedilen temel dizinlere ek olarak, proje ihtiyaçlarına göre başka özel klasörler de ekleyebilirsiniz. En yaygın olanlarına ve kullanımlarına bakalım:

app/
├── Api/              ← sunum katmanından bağımsız API mantığı
├── Database/         ← test verileri için geçiş betikleri ve tohumlayıcılar
├── Components/       ← tüm uygulama genelinde paylaşılan görsel bileşenler
├── Event/            ← olay odaklı mimari kullanıyorsanız yararlıdır
├── Mail/             ← e-posta şablonları ve ilgili mantık
└── Utils/            ← yardımcı sınıflar

Uygulama genelinde presenter'larda kullanılan paylaşılan görsel bileşenler için app/Components veya app/Controls klasörünü kullanabilirsiniz:

app/Components/
├── Form/                 ← paylaşılan form bileşenleri
│   ├── SignInForm.php
│   └── UserForm.php
├── Grid/                 ← veri listeleri için bileşenler
│   └── DataGrid.php
└── Navigation/           ← gezinme öğeleri
	├── Breadcrumbs.php
	└── Menu.php

Buraya daha karmaşık mantığa sahip bileşenler aittir. Bileşenleri birden fazla proje arasında paylaşmak istiyorsanız, bunları ayrı bir composer paketine ayırmak uygundur.

E-posta iletişiminin yönetimini app/Mail dizinine yerleştirebilirsiniz:

app/Mail/
├── templates/            ← e-posta şablonları
│   ├── order-confirmation.latte
│   └── welcome.latte
└── OrderMailer.php

Presenter Eşlemesi

Eşleme, presenter adından sınıf adını türetme kurallarını tanımlar. Bunları yapılandırmada application › mapping anahtarı altında belirtiriz.

Bu sayfada, presenter'ları app/Presentation (veya app/UI) klasörüne yerleştirdiğimizi gösterdik. Bu geleneği Nette'ye yapılandırma dosyasında bildirmeliyiz. Tek bir satır yeterlidir:

application:
	mapping: App\Presentation\*\**Presenter

Eşleme nasıl çalışır? Daha iyi anlamak için önce modülsüz bir uygulama hayal edelim. Presenter sınıflarının App\Presentation isim alanına düşmesini istiyoruz, böylece Home presenter'ı App\Presentation\HomePresenter sınıfına eşlenir. Bunu şu yapılandırmayla başarırız:

application:
	mapping: App\Presentation\*Presenter

Eşleme, Home presenter adının App\Presentation\*Presenter maskesindeki yıldız işaretini değiştirmesiyle çalışır, böylece sonuçta App\Presentation\HomePresenter sınıf adını elde ederiz. Basit!

Ancak bu ve diğer bölümlerdeki örneklerde gördüğünüz gibi, presenter sınıflarını aynı adlı alt dizinlere yerleştiririz, örneğin Home presenter'ı App\Presentation\Home\HomePresenter sınıfına eşlenir. Bunu iki nokta üst üste işaretini iki katına çıkararak başarırız (Nette Application 3.2 gerektirir):

application:
	mapping: App\Presentation\**Presenter

Şimdi presenter'ları modüllere eşlemeye geçelim. Her modül için belirli bir eşleme tanımlayabiliriz:

application:
	mapping:
		Front: App\Presentation\Front\**Presenter
		Admin: App\Presentation\Admin\**Presenter
		Api: App\Api\*Presenter

Bu yapılandırmaya göre, Front:Home presenter'ı App\Presentation\Front\Home\HomePresenter sınıfına eşlenirken, Api:OAuth presenter'ı App\Api\OAuthPresenter sınıfına eşlenir.

Front ve Admin modülleri benzer bir eşleme yöntemine sahip olduğundan ve muhtemelen bu türden daha fazla modül olacağından, bunları değiştirecek genel bir kural oluşturmak mümkündür. Sınıf maskesine modül için yeni bir yıldız işareti eklenir:

application:
	mapping:
		*: App\Presentation\*\**Presenter
		Api: App\Api\*Presenter

Bu, örneğin Admin:User:Edit presenter'ı gibi daha derinlemesine iç içe geçmiş dizin yapıları için de çalışır, yıldız işaretli segment her seviye için tekrarlanır ve sonuç App\Presentation\Admin\User\Edit\EditPresenter sınıfıdır.

Alternatif bir gösterim, bir dize yerine üç segmentten oluşan bir dizi kullanmaktır. Bu gösterim öncekiyle eşdeğerdir:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
versiyon: 4.0