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ı çözerApp\Model\OrderMailer
– siparişlerle ilgili e-postaları göndermek içinMailerFactory
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]