Nesne Yönelimli Kod ve Mimari Standartlar
Modern Drupal, büyük ölçüde nesne yönelimli bir mimari üzerine kurulu. Controller'lar, plugin'ler, event subscriber'lar ve service class'ları bu mimarinin yapı taşları — ve bunların nasıl bir araya geldiği, tek bir class'ın nasıl yazıldığı kadar önem taşıyor.
1. Dependency Injection
Bir service'e ihtiyaç duyan bir class — bir logger, veritabanı bağlantısı, bir HTTP client — buna bir method'un ortasında statik bir çağrıyla ulaşmak yerine constructor üzerinden ulaşır. Bu, class'ın bağımlılıklarının tek bir yerde görünür olmasını ve bir testin gerçek service yerine bir mock kullanabilmesini sağlar.
// ❌ Yanlış: statik servis çağrısı bir method'un içine gömülmüş.
public function importAll() {
$logger = \Drupal::service('logger.factory')->get('product_import');
$logger->error('İçe aktarma başarısız oldu.');
}
// ✅ Doğru: servis constructor üzerinden enjekte edilir.
public function __construct(LoggerChannelFactoryInterface $logger_factory) {
$this->logger = $logger_factory->get('product_import');
}
public function importAll() {
$this->logger->error('İçe aktarma başarısız oldu.');
}Tek bir method'da fark küçük görünse de sonuçta bir araya geliyor: class'ın hangi bağımlılıklara ihtiyaç duyduğu constructor imzasından ilk bakışta anlaşılıyor, test için sahte bir logger kullanmak da bir container override yerine tek satırlık bir değişiklik oluyor.
2. Servis Tanımları
Bir servis, module_name.services.yml içinde bir kez tanımlanır ve Drupal'ın container'ı onu oluşturmayı ve kendi bağımlılıklarını enjekte etmeyi üstlenir. Servis ID'si modül adıyla namespace'lenir, class ise başka bir yerde type hint olarak kullanıldığında genelde implement ettiği interface üzerinden referans alınır.
services:
product_import.importer:
class: Drupal\product_import\ProductImporter
arguments: ['@logger.factory', '@http_client']Argümanlar, constructor'ın beklediği sırayla listelenen ve başında @ olan başka servis ID'leridir. Bir şeye tepki vermesi gereken bir servis — bir event, oluşturulan bir route, değiştirilen bir form — genelde başka kodlar tarafından doğrudan çağrılmak yerine aynı dosyada bir tag ile işaretlenir.
3. Plugin'ler
Block'lar, field formatter'lar, field widget'lar ve Drupal'daki daha bir dizi genişletme noktası, düz servisler yerine plugin olarak implement edilir. Bir plugin class'ı kendi ID'sini ve metadata'sını tanımlayan bir attribute taşır ve kendi plugin manager'ı tarafından otomatik olarak keşfedilir — elle bir yere kaydedilmesine gerek kalmaz.
#[Block(
id: "product_import_status_block",
admin_label: new TranslatableMarkup("Import status"),
)]
class ImportStatusBlock extends BlockBase {
public function build(): array {
return [
'#markup' => $this->t('Last import: @date', ['@date' => $this->lastImportDate()]),
];
}
}Bir plugin'in enjekte edilmiş bir servise ihtiyacı varsa, ContainerFactoryPluginInterface'i implement eder ve kendini statik bir create() method'u üzerinden oluşturur — controller'ların kullandığı yöntemin aynısı. Bir plugin'in constructor'ı uygulama kodu tarafından hiçbir zaman doğrudan çağrılmaz.
4. Event Subscriber'lar
Drupal'ın başka bir yerinde olan bir şeye tepki vermesi gereken kod — eşleşen bir route, kaydedilen bir entity, gönderilmek üzere olan bir response — bunu tetikleyen kod tarafından doğrudan çağrılmak yerine ilgili event'e abone olur. Bir subscriber, event_subscriber etiketli, EventSubscriberInterface'i implement eden düz bir servistir.
class ProductImportSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [
ProductImportEvents::IMPORT_COMPLETE => 'onImportComplete',
];
}
public function onImportComplete(ImportCompleteEvent $event): void {
// Tamamlanan içe aktarmaya tepki ver.
}
}getSubscribedEvents() kasıtlı olarak statik ve durumsuzdur — bir instance'a ihtiyaç duymadan event isimlerini method isimlerine eşler, böylece Drupal tüm subscriber'ları hiçbirini instantiate etmeden baştan bağlayabilir.
5. Controller'lar
Bir controller'ın işi bir request'i bir response'a çevirmektir, başka bir şey değil. İş mantığı controller'ın çağırdığı bir servise aittir, controller'ın kendisine değil — bu ayrım, mantığı bir Drush komutundan, bir queue worker'dan ya da bir testten de kullanılabilir tutar; bunların hiçbirinin elinde bir HTTP request yoktur.
// ❌ Yanlış: içe aktarma mantığı controller'ın içinde yaşıyor.
class ImportController extends ControllerBase {
public function run() {
$items = json_decode(file_get_contents('https://example.com/feed.json'), TRUE);
foreach ($items as $item) {
// ... yirmi satırlık içe aktarma mantığı ...
}
return new Response('OK');
}
}
// ✅ Doğru: controller enjekte edilmiş bir servise devrediyor.
class ImportController extends ControllerBase {
public function __construct(protected ProductImporterInterface $importer) {}
public static function create(ContainerInterface $container): static {
return new static($container->get('product_import.importer'));
}
public function run(): Response {
$count = $this->importer->importAll();
return new Response("Imported $count products.");
}
}6. Interface'lere Göre Programlama
Bir constructor, bugün onu implement eden somut class'a değil, bir interface'e göre type hint yazılır. Bunu sağlayan şey, ona bağımlı olan kodu hiç değiştirmeden farklı bir implementasyonun — ya da bir testte bir mock'un — onun yerine geçebilmesidir.
// ❌ Yanlış: somut bir class'a göre type hint yazılmış.
public function __construct(ProductImporter $importer) {}
// ✅ Doğru: implement ettiği interface'e göre type hint yazılmış.
public function __construct(ProductImporterInterface $importer) {}Değiştirilebilir olması amaçlanan her servis ve plugin, bugün tek bir implementasyonu olsa bile kendi implementasyonuyla birlikte bir interface de taşır. Bunun maliyeti tek bir küçük dosyadır; karşılığında ise ileride gelecek ikinci bir implementasyonun — ya da bir test double'ının — başka hiçbir yerde değişiklik gerektirmemesidir.