Uygulamanın dizin yapısı

Nette Framework projelerinde anlaşılır ve ölçeklenebilir bir dizin yapısı nasıl tasarlanır? Size kodunuzu düzenlemenize yardım edecek kanıtlanmış uygulamaları göstereceğiz. Şunları öğreneceksiniz:

  • uygulamayı dizinlere mantıklı biçimde nasıl yapılandıracağınızı
  • yapıyı, proje büyüdükçe iyi ölçeklenecek şekilde nasıl tasarlayacağınızı
  • olası alternatiflerin neler olduğunu ve avantajlarıyla dezavantajlarını

Nette Framework'ün kendisinin belirli bir yapıyı dayatmadığını belirtmek önemli. Her ihtiyaca ve tercihe kolayca uyarlanabilecek şekilde tasarlanmıştır.

Temel proje yapısı

Nette Framework sabit bir dizin yapısı dayatmasa da, Web Project biçiminde kanıtlanmış bir varsayılan düzen vardır:

web-project/
├── app/              ← uygulama dizini
├── assets/           ← SCSS, JS dosyaları, görseller..., alternatif olarak resources/
├── bin/              ← komut satırı betikleri
├── config/           ← yapılandırma
├── log/              ← günlüklenen hatalar
├── temp/             ← geçici dosyalar, önbellek
├── tests/            ← testler
├── vendor/           ← Composer ile 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ırın veya taşıyın. Sonra yalnızca Bootstrap.php ve gerekirse composer.json içindeki dizin göreli yollarını düzeltmeniz yeterli. Başka bir şey gerekmez, karmaşık bir yeniden yapılandırma yok, sabitlerde değişiklik yok. Nette akıllı otomatik algılamaya sahiptir ve uygulamanın konumunu, temel URL'si dahil, kendiliğinden tanır.

Kodu düzenleme ilkeleri

Yeni bir projeyi ilk kez incelediğinizde hızlıca yolunuzu bulabilmelisiniz. app/Model/ dizinine tıkladığınızı ve şu yapıyı gördüğünüzü düşünün:

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

Bundan yalnızca projenin bazı servisler, repository'ler ve varlıklar kullandığını öğrenirsiniz. Uygulamanın asıl amacı hakkında hiçbir şey öğrenemezsiniz.

Farklı bir yaklaşıma bakalım: alan adına göre düzenleme:

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

Burada durum farklı: ilk bakışta bunun bir e-ticaret sitesi olduğu bellidir. Dizin adlarının kendisi uygulamanın neler yapabildiğini ele verir: ödemelerle, siparişlerle ve ürünlerle çalışıyor.

İlk yaklaşım (sınıf tipine göre düzenleme) pratikte birkaç sorun getirir: mantıksal olarak ilişkili kod farklı klasörlere dağılır ve aralarında gidip gelmeniz gerekir. Bu yüzden alan adına göre düzenleyeceğiz.

İsim alanları

Dizin yapısının uygulamadaki isim alanlarına karşılık gelmesi âdettendir. Yani dosyaların fiziksel konumu isim alanlarıyla uyuşur. Örneğin app/Model/Product/ProductRepository.php konumundaki bir sınıfın isim alanı App\Model\Product olmalıdır. Bu ilke kodda gezinmeye yardım eder ve otomatik yüklemeyi basitleştirir.

Adlarda tekil mi çoğul mu

Uygulamanın ana dizinlerinde tekil kullandığımıza dikkat edin: app, config, log, temp, www. Aynısı uygulamanın içi için de geçerlidir: Model, Core, Presentation. Bunun nedeni her birinin tek bir bütünlüklü kavramı temsil etmesidir.

Benzer şekilde app/Model/Product, ürünlerle ilgili her şeyi temsil eder. Ona Products demiyoruz, çünkü ürünlerle dolu bir klasör değil (öyle olsaydı nokia.php, samsung.php gibi dosyalar içerirdi). Ürünlerle çalışmaya yarayan sınıfları içeren bir isim alanıdır: ProductRepository.php, ProductService.php.

app/Tasks klasörü çoğuldur, çünkü ayrı çalıştırılabilir betiklerden oluşan bir küme içerir: CleanupTask.php, ImportTask.php. Her biri bağımsız bir birimdir.

Tutarlılık için şunu öneririz:

  • İşlevsel bir birimi temsil eden isim alanlarında tekil (birden fazla varlıkla çalışsa bile)
  • Bağımsız birim koleksiyonlarında çoğul
  • Kararsız kaldığınızda ya da düşünmek istemediğinizde tekili seçin

Genel dizin www/

Bu dizin, web'den erişilebilen tek dizindir (document-root). www/ yerine sıkça public/ adıyla karşılaşabilirsiniz; bu yalnızca bir uzlaşım meselesidir ve uygulamanın işleyişini etkilemez. Dizin şunları içerir:

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

Uygulamanın güvenliği için document-root'un doğru yapılandırılmış olması çok önemlidir.

node_modules/ klasörünü asla bu dizine koymayın; çalıştırılabilir olabilecek binlerce dosya içerir ve herkese açık olmamalıdır.

Uygulama dizini app/

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

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

Bootstrap.php, ortamı hazırlayan, yapılandırmayı yükleyen ve DI konteynerini oluşturan uygulama başlatma sınıfıdır.

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

Presenter'lar ve şablonlar

Uygulamanın sunum bölümü app/Presentation dizinindedir. Alternatifi daha kısa olan app/UI'dır. Burası tüm presenter'ların, şablonlarının ve ilişkili yardımcı sınıfların yeridir.

Bu katmanı alan adına göre düzenleriz. E-ticaret sitesini, blogu ve API'yi birleştiren karmaşık bir projede yapı şöyle görünürdü:

app/Presentation/
├── Shop/              ← e-ticaret frontend'i
│   ├── 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 yapıyı kullanırdık:

app/Presentation/
├── Front/             ← sitenin frontend'i
│   ├── 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ül denir. Teknik olarak bunlar, uygulamayı mantıksal olarak bölmeye yarayan sıradan dizinlerdir.

Presenter içeren her klasör, presenter dosyasının kendisini ve şablonlarını barındırır. Ö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ındadır (bkz. Presenter mapping):

namespace App\Presentation\Admin\Dashboard;

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

Admin modülündeki Dashboard presenter'ına uygulamada iki nokta üst üste gösterimiyle Admin:Dashboard diye başvururuz. Onun default eylemine ise Admin:Dashboard:default denir. İç içe modüllerde birden fazla iki nokta üst üste kullanırız, örneğin Shop:Order:Detail:default.

Yapının esnek biçimde gelişmesi

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

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

Zamanla daha fazla besleme tipi eklenir ve onlar için daha fazla mantık gerekir… Sorun değil! Export/ klasörü kolayca bir modüle dönüşür:

Export/
├── Sitemap/
│   ├── SitemapPresenter.php
│   └── sitemap.latte
└── Feed/
	├── FeedPresenter.php
	├── amazon.latte         ← Amazon için besleme
	└── ebay.latte           ← eBay için besleme

Bu dönüşüm tamamen pürüzsüzdür: yalnızca yeni alt klasörler oluşturun, kodu onlara bölün ve bağlantıları güncelleyin (örneğin Export:feed'den Export:Feed:amazon'a). Bu sayede yapıyı gerektikçe kademeli olarak genişletebiliriz; iç içelik düzeyi hiçbir şekilde sınırlı değildir.

Örneğin yönetim panelinde sipariş yönetimiyle ilgili OrderDetail, OrderEdit, OrderDispatch gibi birçok presenter'ınız varsa, daha iyi düzen için Order adlı bir modül (klasör) oluşturabilirsiniz; bu modül Detail, Edit, Dispatch ve diğer presenter'ların (klasörlerini) içerecektir.

Şablonların konumu

Önceki örneklerde şablonların doğrudan presenter'ın bulunduğu klasörde olduğunu gördük:

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

Bu konum pratikte en kullanışlısı olduğunu kanıtlar: ilişkili tüm dosyalar elinizin altındadır.

Alternatif olarak şablonları bir templates/ alt klasörüne koyabilirsiniz. Nette her iki seçeneği de destekler. Şablonları tamamen Presentation/ klasörünün dışına bile koyabilirsiniz. Şablon konumu seçenekleriyle ilgili her şeyi Şablon arama bölümünde bulabilirsiniz.

Yardımcı sınıflar ve bileşenler

Presenter'lar ve şablonlar sıklıkla başka yardımcı dosyalarla birlikte gelir. Onları kapsamlarına göre mantıklı biçimde yerleştiririz:

1. Doğrudan presenter'ın yanına, o presenter'a özgü bileşenler söz konusuysa:

Product/
├── ProductPresenter.php
├── ProductGrid.php        ← ürün listelemesi bileşeni
└── FilterForm.php         ← filtreleme formu

2. Modül için – alfabetik olarak başta yer aldığı için elverişli olan Accessory klasörünü kullanmanızı öneririz:

Front/
├── Accessory/
│   ├── NavbarControl.php    ← frontend bileşenleri
│   └── TemplateFilters.php
├── Product/
└── Cart/

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

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

Alternatif olarak LatteExtension.php veya TemplateFilters.php gibi yardımcı sınıfları app/Core/Latte/ altyapı klasörüne koyabilirsiniz. Bileşenleri de app/Components içine. Seçim ekip uzlaşımlarına bağlıdır.

Model – uygulamanın kalbi

Model, uygulamanın tüm iş mantığını içerir. Onu düzenleme kuralı yine aynıdır: alan adına göre yapılandırmak:

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

Model'de genellikle şu sınıf tipleriyle karşılaşırsınız:

Facade'lar: uygulamadaki belirli bir alana giden ana giriş noktasını temsil ederler. Bir orkestra şefi gibi davranır, tam kullanım senaryolarını (“sipariş oluştur” veya “ödemeyi işle”) gerçekleştirmek için çeşitli servisler arasındaki iş birliğini koordine ederler. Facade, bu orkestrasyon katmanının altında uygulama ayrıntılarını uygulamanın geri kalanından gizler ve böylece ilgili alanla çalışmak için temiz bir arayüz sunar.

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

Servisler: bir alan içindeki belirli iş operasyonlarına odaklanır. Tüm kullanım senaryolarını yöneten facade'ların aksine, bir servis somut bir iş mantığını (fiyat hesaplaması veya ödeme işleme gibi) uygular. Servisler genellikle durumsuzdur ve daha karmaşık işlemler için yapı taşı olarak facade'lar tarafından ya da daha basit görevler için doğrudan uygulamanın başka bölümleri tarafından kullanılabilir.

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

Repository'ler: veri deposuyla, genellikle bir veritabanıyla, tüm iletişimi üstlenirler. Görevleri varlıkları yüklemek, kaydetmek ve onları aramaya yarayan metotları uygulamaktır. Bir repository, uygulamanın geri kalanını veritabanının uygulama ayrıntılarından korur ve veriyle çalışmak için nesne yönelimli bir arayüz sunar.

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

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

Varlıklar: uygulamadaki başlıca iş kavramlarını temsil eden, kendi kimliği olan ve zamanla değişen nesneler. Genellikle bir ORM (Nette Database Explorer veya Doctrine gibi) ile 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,
		]);
	}
}

Value Object'ler: kendi kimliği olmayan değerleri temsil eden değişmez nesneler; örneğin bir para tutarı veya bir e-posta adresi. Aynı değerlere sahip iki value object örneği özdeş sayılır.

Altyapı kodu

Core/ klasörü (ya da alternatif olarak Infrastructure/) uygulamanın teknik temelinin yuvasıdır. Altyapı kodu genellikle ş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/                ← önbellek katmanı
│   └── FullPageCache.php
└── Integration/          ← dış servislerle entegrasyon
	├── Slack/
	└── Stripe/

Daha küçük projelerde doğal olarak düz bir yapı yeterlidir:

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

Bu, şöyle bir koddur:

  • Teknik altyapıyı üstlenir (yönlendirme, günlükleme, önbellekleme)
  • Dış 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 bir e-ticaret sitesinde de blogda da aynı çalışır.

Belirli bir sınıfın buraya mı yoksa model'e mi ait olduğunu merak mı ediyorsunuz? Temel fark şudur: Core/ içindeki kod:

  • Alan hakkında hiçbir şey bilmez (ürünler, siparişler, makaleler)
  • Genellikle başka bir projeye taşınabilir
  • “Nasıl çalıştığını” çözer (bir e-posta nasıl gönderilir), “ne yaptığını” değil (hangi e-posta gönderilir)

Daha iyi anlamak için bir örnek:

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

Komut betikleri

Uygulamaların sıklıkla olağan HTTP isteklerinin dışında işler yapması gerekir; ister arka planda veri işleme, ister bakım, ister düzenli görevler olsun. Çalıştırmak için bin/ dizinindeki basit betikler kullanılır, asıl uygulama mantığı ise app/Tasks/ (veya app/Commands/) içine konur.

Örnek:

app/Tasks/
├── Maintenance/               ← bakım betikleri
│   ├── CleanupCommand.php     ← eski verilerin silinmesi
│   └── DbOptimizeCommand.php  ← veritabanı iyileştirmesi
├── Integration/               ← dış sistemlerle entegrasyon
│   ├── ImportProducts.php     ← tedarikçi sisteminden içe aktarım
│   └── SyncOrders.php         ← siparişlerin eşitlenmesi
└── Scheduled/                 ← düzenli görevler
	├── NewsletterCommand.php  ← bültenlerin gönderimi
	└── ReminderCommand.php    ← müşteri bildirimleri

Model'e ne, komut betiklerine ne ait? Örneğin tek bir e-posta gönderme mantığı model'in parçasıdır, binlerce e-postanın toplu gönderimi ise Tasks/'a aittir.

Görevler genellikle komut satırından veya cron ile çalıştırılır: bin/ içindeki betik, bootConsoleApplication() metoduyla DI konteynerini oluşturur ve gereken servisi ondan çeker. Bir HTTP isteğiyle de çalıştırılabilirler, ama güvenlik göz önünde bulundurulmalıdır. Görevi çalıştıran presenter'ın güvenliği sağlanmalıdır; örneğin yalnızca oturum açmış kullanıcılar için ya da güçlü bir belirteç ve izin verilen IP adreslerinden erişimle. Uzun süren görevlerde betiğin zaman sınırını artırmak ve oturumu kilitlememek için session_write_close() kullanmak gerekir.

Olası diğer dizinler

Sözü geçen temel dizinlerin yanı sıra, projenin ihtiyaçlarına göre başka özel klasörler ekleyebilirsiniz. En yaygınlarına ve kullanımlarına bakalım:

app/
├── Api/              ← sunum katmanından bağımsız API mantığı
├── Database/         ← migration betikleri ve test verisi için seeder'lar
├── Components/       ← tüm uygulamada paylaşılan görsel bileşenler
├── Event/            ← olay güdümlü bir mimari kullanılıyorsa yararlı
├── Mail/             ← e-posta şablonları ve ilgili mantık
└── Utils/            ← yardımcı sınıflar

Uygulama genelindeki 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 listelemeleri için bileşenler
│   └── DataGrid.php
└── Navigation/           ← gezinme öğeleri
	├── Breadcrumbs.php
	└── Menu.php

Daha karmaşık mantığa sahip bileşenler buraya aittir. Bileşenleri birden fazla proje arasında paylaşmak isterseniz, onları ayrı bir Composer paketine çıkarmanız yerinde olur.

app/Mail dizinine e-posta iletişiminin yönetimini koyabilirsiniz:

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

Presenter mapping

Mapping, sınıf adının presenter adından türetilme kurallarını tanımlar. Onları yapılandırmada application › mapping anahtarı altında belirtiriz.

Bu sayfada presenter'ları app/Presentation (veya app/UI) klasörüne koyduğumuzu gösterdik. Nette Application 3.3'ten beri bu, yapılandırılması gerekmeyen varsayılan uzlaşımdır. Farklı bir yapı kullanıyorsanız veya mapping'i açıkça belirtmek istiyorsanız, varsayılan ayar şu satıra karşılık gelir:

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

Mapping nasıl çalışır? Daha iyi anlamak için önce modülsüz bir uygulama düşünelim. 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şlensin. Bu, şu yapılandırmayla sağlanır:

application:
	mapping: App\Presentation\*Presenter

Mapping, App\Presentation\*Presenter maskesindeki yıldızın presenter adı Home ile değiştirilmesiyle çalışır ve sonuçta App\Presentation\HomePresenter sınıf adı elde edilir. Basit!

Ancak bu ve diğer bölümlerdeki örneklerde gördüğünüz gibi, presenter sınıflarını aynı adlı alt dizinlere koyuyoruz; örneğin Home presenter'ı App\Presentation\Home\HomePresenter sınıfına eşlenir. Bunu çift yıldız ** kullanarak sağlarız (Nette Application 3.2.3 gerektirir):

application:
	mapping: App\Presentation\**Presenter

Şimdi presenter'ları modüllere eşlemeye geçiyoruz. Her modül için özel bir mapping 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, Api:OAuth presenter'ı ise App\Api\OAuthPresenter sınıfına eşlenir.

Front ve Admin modüllerinin mapping deseni benzer olduğundan ve büyük olasılıkla böyle daha çok modül olacağından, onların yerine geçen genel bir kural oluşturmak mümkündür. Sınıf maskesine modül için yeni bir yıldız eklenir:

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

Bu, daha derin iç içe dizin yapılarında da çalışır; örneğin Admin:User:Edit presenter'ında yıldızlı bölüm her modül düzeyi için yinelenir ve sonuçta App\Presentation\Admin\User\Edit\EditPresenter sınıfı elde edilir.

Alternatif bir yazım, dize yerine üç bölümden oluşan bir dizi kullanmaktır. Yukarıda gösterilen örnekler için bu yazım öncekiyle eşdeğerdir:

application:
	mapping:
		*: [App\Presentation, *, **Presenter]
		Api: [App\Api, '', *Presenter]
versiyon: 4.x