# PSR-14 Events: Sisteme Müdahale Etme

TYPO3 v13'te PSR-14 event sistemi ile core davranışlarına müdahale etmeyi kapsar.  
Hooks sistemi TYPO3 v12'den itibaren deprecated edilmiş, v13'te tamamen PSR-14 Events kullanılmaktadır.

---

## PSR-14 Events Nedir?

PSR-14, PHP-FIG tarafından tanımlanan bir event dispatcher standardıdır.  
TYPO3 Core, belirli noktalarda event dispatch ederek dışarıdan müdahale imkânı tanır.

```
Çağıran Sınıf       Event           Event Listeners
─────────────       ─────           ───────────────
SomeClass ──────► dispatch() ──────► ListenerA
              │  (MyEvent)          ListenerB
              │                     ListenerC
              └─── $event (değiştirilmiş olabilir)
```

**Eski yaklaşım (hooks — artık kullanılmaz):**
```php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'][] =
    MyClass::class . '->myMethod';
```

**Yeni yaklaşım (PSR-14 Attribute):**
```php
#[AsEventListener(identifier: 'my-extension/my-listener')]
final class MyListener
{
    public function __invoke(SomeEvent $event): void { ... }
}
```

---

## Kayıt Yöntemleri

TYPO3 v13'te iki farklı kayıt yöntemi mevcuttur.

### Yöntem 1: PHP Attribute (Önerilen)

`Configuration/Services.yaml` dosyasında `autoconfigure: true` varsa (varsayılan),  
`#[AsEventListener]` attribute yeterlidir — ek ayar gerekmez.

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'testproject/my-listener',       // Benzersiz tanımlayıcı
    before: 'anotherExt/some-listener',           // Bu listener'dan önce çalış
    after: 'thirdExt/another-listener',           // Bu listener'dan sonra çalış
)]
final readonly class MyListener
{
    public function __invoke(BeforeMailerSentMessageEvent $event): void
    {
        // ...
    }
}
```

### Yöntem 2: Services.yaml (Attribute kullanmak istemiyorsanız)

```yaml
# Configuration/Services.yaml
services:
  Gfdhfhgfgg\Testproject\EventListener\MyListener:
    tags:
      - name: event.listener
        identifier: 'testproject/my-listener'
        event: 'TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent'
        method: handleEvent   # __invoke yerine belirtilen method
        before: 'anotherExt/some-listener'
        after: 'thirdExt/another-listener'
```

### Yöntem 3: Tek Sınıf, Birden Fazla Event

Bir sınıfın birden fazla event dinlemesi için `method` parametresi kullanılır:

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;

final class MailEventListener
{
    #[AsEventListener(identifier: 'testproject/before-mail')]
    public function onBeforeSend(BeforeMailerSentMessageEvent $event): void
    {
        // Göndermeden önce
    }

    #[AsEventListener(identifier: 'testproject/after-mail')]
    public function onAfterSend(AfterMailerSentMessageEvent $event): void
    {
        // Gönderdikten sonra
    }
}
```

---

## Proje Uygulamaları (testproject)

Bu bölümde projeye gerçek anlamda uygulanmış listener örnekleri yer almaktadır.

### 1. Mail BCC Listener

**Dosya:** `packages/testproject/Classes/EventListener/MailBccListener.php`

Her giden e-postaya otomatik BCC ekler. Konfigürasyon `additional.php` üzerinden yapılır.

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\EventListener;

use Symfony\Component\Mime\Address;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;

#[AsEventListener(
    identifier: 'testproject/mail-bcc-listener',
)]
final readonly class MailBccListener
{
    public function __invoke(BeforeMailerSentMessageEvent $event): void
    {
        $bccAddress = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['testproject']['mailBcc'] ?? '';

        if (empty($bccAddress)) {
            return;
        }

        $message = $event->getMessage();

        // Zaten BCC'de yoksa ekle
        $existingBcc = $message->getBcc();
        foreach ($existingBcc as $bcc) {
            if ($bcc->getAddress() === $bccAddress) {
                return;
            }
        }

        $message->addBcc(new Address($bccAddress));
    }
}
```

**`config/system/additional.php`'e eklenecek:**
```php
// BCC adresini etkinleştirmek için:
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['testproject']['mailBcc'] = 'log@ornek.com';
```

---

### 2. Mail Sent Logger Listener

**Dosya:** `packages/testproject/Classes/EventListener/MailSentLoggerListener.php`

Gönderilen e-postaları TYPO3 log sistemine kaydeder.  
`after: 'testproject/mail-bcc-listener'` ile BCC eklendikten sonra çalışır.

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\EventListener;

use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Mail\Event\AfterMailerSentMessageEvent;
use TYPO3\CMS\Core\Utility\GeneralUtility;

#[AsEventListener(
    identifier: 'testproject/mail-sent-logger',
    after: 'testproject/mail-bcc-listener',
)]
final class MailSentLoggerListener
{
    private LoggerInterface $logger;

    public function __construct()
    {
        $this->logger = GeneralUtility::makeInstance(LogManager::class)
            ->getLogger(__CLASS__);
    }

    public function __invoke(AfterMailerSentMessageEvent $event): void
    {
        $message = $event->getMessage();

        $toAddresses = array_map(
            fn(\Symfony\Component\Mime\Address $a) => $a->getAddress(),
            $message->getTo()
        );

        $this->logger->info('E-posta gönderildi', [
            'to'      => implode(', ', $toAddresses),
            'subject' => $message->getSubject(),
        ]);
    }
}
```

---

## Kendi Event'ini Oluşturma

Kendi extension event'ini tanımlamak ve dispatch etmek için:

### Adım 1 — Event Sınıfı

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\Event;

/**
 * İçerik elementi kaydedildiğinde fırlatılan event.
 * readonly özellikler immutable, diğerleri listener'lar tarafından değiştirilebilir.
 */
final class ContentElementSavedEvent
{
    public function __construct(
        private readonly string $table,
        private readonly int    $uid,
        private string          $action = 'save',   // mutable — değiştirilebilir
    ) {}

    public function getTable(): string  { return $this->table; }
    public function getUid(): int       { return $this->uid; }
    public function getAction(): string { return $this->action; }

    public function setAction(string $action): void
    {
        $this->action = $action;
    }
}
```

### Adım 2 — Event'i Dispatch Et

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\Service;

use Psr\EventDispatcher\EventDispatcherInterface;
use Gfdhfhgfgg\Testproject\Event\ContentElementSavedEvent;

final class ContentService
{
    public function __construct(
        private readonly EventDispatcherInterface $eventDispatcher,
    ) {}

    public function saveContent(string $table, int $uid): void
    {
        // ... kaydetme işlemi ...

        $event = new ContentElementSavedEvent($table, $uid);
        $this->eventDispatcher->dispatch($event);

        // Listener'lar $event'i değiştirmiş olabilir
        $action = $event->getAction();
    }
}
```

### Adım 3 — Event'i Dinle

```php
<?php

declare(strict_types=1);

namespace Gfdhfhgfgg\Testproject\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use Gfdhfhgfgg\Testproject\Event\ContentElementSavedEvent;

#[AsEventListener(identifier: 'testproject/content-saved')]
final class ContentElementSavedListener
{
    public function __invoke(ContentElementSavedEvent $event): void
    {
        if ($event->getTable() === 'tx_news_domain_model_news') {
            // Haber kaydedildi: Önbelleği temizle, bildirim gönder vb.
        }
    }
}
```

---

## StoppableEvent (Zincir Durdurmak)

Bir listener, sonraki listener'ların çalışmasını engelleyebilir:

```php
<?php

use Psr\EventDispatcher\StoppableEventInterface;

final class ContentElementSavedEvent implements StoppableEventInterface
{
    private bool $stopped = false;

    public function isPropagationStopped(): bool
    {
        return $this->stopped;
    }

    public function stopPropagation(): void
    {
        $this->stopped = true;
    }
}
```

Listener içinde kullanımı:
```php
public function __invoke(ContentElementSavedEvent $event): void
{
    if ($this->shouldStop()) {
        $event->stopPropagation(); // Sonraki listener'lar çalışmaz
        return;
    }
    // ...
}
```

---

## Yaygın TYPO3 Core Event'leri

### Frontend Event'leri (`TYPO3\CMS\Frontend\Event\`)

| Event | Açıklama |
|-------|----------|
| `BeforePageIsResolvedEvent` | Sayfa çözümlenmeden önce |
| `AfterPageWithRootLineHasBeenResolvedEvent` | Rootline ile sayfa çözümlendi |
| `AfterCacheableContentIsGeneratedEvent` | Cache'lenebilir içerik oluşturuldu |
| `AfterCachedPageIsPersistedEvent` | Sayfa cache'e yazıldıktan sonra |
| `ModifyHrefLangTagsEvent` | hreflang etiketlerini değiştir |
| `ModifyPageLinkConfigurationEvent` | Sayfa link yapılandırmasını değiştir |
| `ModifyTypoScriptConfigEvent` | TypoScript config değiştirilmeden önce |

### Backend Event'leri (`TYPO3\CMS\Backend\`)

| Event | Sınıf Namespace Sonu |
|-------|---------------------|
| `AfterBackendPageRenderEvent` | `View\Event\` |
| `ModifyDatabaseQueryForRecordListingEvent` | `RecordList\Event\` |
| `AfterPageTreeItemsPreparedEvent` | `Tree\Repository\Event\` |
| `AfterSectionMarkupGeneratedEvent` | `View\Event\` |

### Mail Event'leri (`TYPO3\CMS\Core\Mail\Event\`)

| Event | Açıklama |
|-------|----------|
| `BeforeMailerSentMessageEvent` | E-posta gönderilmeden **önce** — mesaj değiştirilebilir |
| `AfterMailerSentMessageEvent` | E-posta gönderildikten **sonra** — loglama için ideal |

### Core Domain Event'leri (`TYPO3\CMS\Core\Domain\Event\`)

| Event | Açıklama |
|-------|----------|
| `RecordCreationEvent` | Kayıt oluşturulurken özellikleri değiştir |

### Resource / FAL Event'leri (`TYPO3\CMS\Core\Resource\Event\`)

| Event | Açıklama |
|-------|----------|
| `AfterFileAddedEvent` | Dosya eklendikten sonra |
| `AfterFileDeletedEvent` | Dosya silindikten sonra |
| `AfterFileMovedEvent` | Dosya taşındıktan sonra |
| `AfterFileRenamedEvent` | Dosya yeniden adlandırıldıktan sonra |
| `BeforeFileAddedEvent` | Dosya eklenmeden önce — iptale olanak tanır |
| `GeneratePublicUrlForResourceEvent` | Kaynak için public URL oluşturma |

---

## Listener Sıralama (before / after)

Birden fazla listener aynı event'i dinliyorsa çalışma sırası  
`before` ve `after` parametreleriyle kontrol edilir:

```php
// A listener'ı B'den önce, C'den sonra çalışır:
#[AsEventListener(
    identifier: 'my-ext/listener-a',
    before: 'other-ext/listener-b',
    after: 'another-ext/listener-c',
)]
```

```
Çalışma sırası: listener-c  →  listener-a  →  listener-b
```

> **Not:** `before` ve `after` değerleri virgülle ayrılarak birden fazla identifier belirtilebilir:  
> `before: 'ext-a/listener-1, ext-b/listener-2'`

---

## Tüm Event'leri Listeleme

```bash
# DI Container dump dosyasından event listener'ları çıkar
ddev exec grep -r "AsEventListener\|event.listener" packages/ --include="*.php" --include="*.yaml" -l

# Kayıtlı listener sınıflarını sınıf ismiyle filtrele
ddev exec grep -r "AsEventListener" packages/ --include="*.php" -l

# Symfony DI container'ını inspect et (container dump oluşturur)
ddev exec vendor/bin/typo3 cache:warmup

# Oluşturulan DI dump dosyasından EventListener girişlerini bul
ddev exec grep -r "event.listener" var/cache/code/di/ 2>/dev/null | head -20
```

---

## Pratik Senaryo Örnekleri

### Dosya Yükleme Kısıtlaması

```php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\Event\BeforeFileAddedEvent;

#[AsEventListener(identifier: 'testproject/restrict-file-types')]
final class RestrictFileTypesListener
{
    public function __invoke(BeforeFileAddedEvent $event): void
    {
        $fileName = $event->getFileName();
        $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));

        // SVG dosyalarını yalnızca belirli klasöre izin ver
        if ($ext === 'svg' && !str_contains($event->getTargetFolder()->getIdentifier(), '/images/')) {
            throw new \RuntimeException('SVG dosyaları yalnızca /images/ klasörüne yüklenebilir.');
        }
    }
}
```

### News Cache Temizleme

```php
use TYPO3\CMS\Core\Attribute\AsEventListener;
use Gfdhfhgfgg\Testproject\Event\ContentElementSavedEvent;

#[AsEventListener(identifier: 'testproject/news-cache-clear')]
final class NewsCacheClearListener
{
    public function __construct(
        private readonly \TYPO3\CMS\Core\Cache\CacheManager $cacheManager,
    ) {}

    public function __invoke(ContentElementSavedEvent $event): void
    {
        if ($event->getTable() === 'tx_news_domain_model_news') {
            $this->cacheManager->flushCachesByTag('tx_news_news_' . $event->getUid());
        }
    }
}
```

---

## Services.yaml Mevcut Durum

```yaml
# packages/testproject/Configuration/Services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true   # AsEventListener attribute'u otomatik tanır

  Gfdhfhgfgg\Testproject\:
    resource: '../Classes/*'   # Classes/ altındaki her PHP sınıfı dahil

  # PSR-14 Event Listeners
  # autoconfigure:true + #[AsEventListener] attribute ile otomatik kayıt yapılır.
  # EventListener/ klasörüne eklenen her yeni listener otomatik olarak devreye girer.
```

---

## Kaynaklar

- [TYPO3 PSR-14 Events — Core API](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Events/)
- [Event List — All Core Events](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Events/EventList/)
- [PHP-FIG PSR-14 Standard](https://www.php-fig.org/psr/psr-14/)
- [TYPO3 v13 Migration: Hooks → Events](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Hooks/Index.html)

