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
.htaccessdosyası (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ı üstlenirApp\Model\OrderMailer– siparişlerle ilgili e-postaları göndermek içinMailerFactory'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]