# Schema API: Veritabanı Tablo Yönetimi (TCA)

TYPO3'te **TCA (Table Configuration Array)** ile özel veritabanı tablolarını oluşturma, backend formlarını tasarlama ve mevcut tabloları genişletme konusunu kapsar.

---

## TCA Nedir? (Basit Anlatım)

**TCA** = Veritabanı tabloları + Backend formları + Doğrulama

TYPO3'te veritabanı tablolarınızı 3 adımda tanımladığınız sistemi:

1. **SQL** → Veritabanını yaratır
2. **TCA** → Admin panelinde nasıl görüneceğini tanımlar
3. **Setup** → TYPO3'e söyleyin "tamamlandı, aktif et"

---

## 🚀 Özel Tablo Oluşturma (Adım Adım)

### Senaryo: "Ekip Üyesi" Tablosu Eklemek

Amaç: Admin panelinde ekip üyelerini (ad, e-posta, fotoğraf) yönetmek.

---

### Adım 1: SQL Dosyası Oluştur

**Dosya:** `packages/testproject/ext_tables.sql`

```sql
CREATE TABLE tx_testproject_domain_model_team (
    uid int(11) unsigned NOT NULL auto_increment PRIMARY KEY,
    pid int(11) DEFAULT '0' NOT NULL,
    tstamp int(11) unsigned DEFAULT '0' NOT NULL,
    crdate int(11) unsigned DEFAULT '0' NOT NULL,
    cruser_id int(11) unsigned DEFAULT '0' NOT NULL,
    deleted tinyint(4) unsigned DEFAULT '0' NOT NULL,
    hidden tinyint(4) unsigned DEFAULT '0' NOT NULL,
    starttime int(11) unsigned DEFAULT '0' NOT NULL,
    endtime int(11) unsigned DEFAULT '0' NOT NULL,
    
    name varchar(255) DEFAULT '' NOT NULL,
    position varchar(255) DEFAULT '' NOT NULL,
    email varchar(255) DEFAULT '' NOT NULL,
    bio mediumtext NOT NULL,
    photo int(11) unsigned DEFAULT '0' NOT NULL,
    social_linkedin varchar(255) DEFAULT '' NOT NULL,
    social_twitter varchar(255) DEFAULT '' NOT NULL,
    sorting int(11) DEFAULT '0' NOT NULL,
    
    KEY parent (pid),
    KEY deleted (deleted),
);
```

**Ne yapar?**
- `uid` = ID (birincil anahtar)
- `pid` = Parent ID (hangi sayfaya ait)
- `tstamp`, `crdate`, `cruser_id` = TYPO3 otomatik alanları
- `deleted`, `hidden` = Gösterme/gizleme
- `starttime`, `endtime` = Yayın tarihleri
- `name`, `email`, vb. = Kendi alanlarımız

```sql
CREATE TABLE tx_mysite_domain_model_team (
    name varchar(255) DEFAULT '' NOT NULL,
    position varchar(255) DEFAULT '' NOT NULL,
    email varchar(255) DEFAULT '' NOT NULL,
    bio text,
    photo int unsigned DEFAULT 0 NOT NULL,
    social_linkedin varchar(255) DEFAULT '' NOT NULL,
    social_twitter varchar(255) DEFAULT '' NOT NULL,
    sorting int DEFAULT 0 NOT NULL,
);
```

### Adım 2: Domain Model Oluştur (PHP Sınıfı)

**Dosya:** `packages/testproject/Classes/Domain/Model/Team.php`

```php
<?php

declare(strict_types=1);

namespace MySite\Testproject\Domain\Model;

use TYPO3\CMS\Extbase\Domain\Model\AbstractEntity;

class Team extends AbstractEntity
{
    protected string $name = '';
    protected string $position = '';
    protected string $email = '';
    protected string $bio = '';
    protected int $photo = 0;
    protected string $socialLinkedin = '';
    protected string $socialTwitter = '';

    public function getName(): string { return $this->name; }
    public function setName(string $name): void { $this->name = $name; }

    public function getPosition(): string { return $this->position; }
    public function setPosition(string $position): void { $this->position = $position; }

    public function getEmail(): string { return $this->email; }
    public function setEmail(string $email): void { $this->email = $email; }

    public function getBio(): string { return $this->bio; }
    public function setBio(string $bio): void { $this->bio = $bio; }

    public function getPhoto(): int { return $this->photo; }
    public function setPhoto(int $photo): void { $this->photo = $photo; }

    public function getSocialLinkedin(): string { return $this->socialLinkedin; }
    public function setSocialLinkedin(string $url): void { $this->socialLinkedin = $url; }

    public function getSocialTwitter(): string { return $this->socialTwitter; }
    public function setSocialTwitter(string $url): void { $this->socialTwitter = $url; }
}
```

**Ne yapar?**
- PHP sınıfı = Veritabanı tablosunun temsili
- `protected` = Private (dış dünyada erişilmez)
- `get/set` = Veri okuma/yazma (getter/setter)

### Adım 3: TCA Yapılandırması (Backend Tanımı)

**Dosya:** `packages/testproject/Configuration/TCA/tx_testproject_domain_model_team.php`

```php
<?php

return [
    // =========== KONTROL (Genel Ayarlar) ===========
    'ctrl' => [
        'title' => 'Ekip Üyesi',                    // Backend listede başlık
        'label' => 'name',                          // Her kaydın adı (name sütunu)
        'label_alt' => 'position',                  // Eğer name boşsa position göster
        
        // TYPO3 otomatik alanları
        'tstamp' => 'tstamp',                       // Son güncelleme tarihi
        'crdate' => 'crdate',                       // Oluşturma tarihi
        'cruser_id' => 'cruser_id',                 // Oluşturan kişi
        
        // Silme ve gizleme
        'delete' => 'deleted',                      // Fiziksel silme kolonu
        'enablecolumns' => [
            'disabled' => 'hidden',                 // Gizli/Görünür toggle
            'starttime' => 'starttime',             // Yayın başlangıç tarihi
            'endtime' => 'endtime',                 // Yayın bitiş tarihi
        ],
        
        // Arama ve sıralama
        'searchFields' => 'name,position,email',   // Arama yapılacak alanlar
        'sortby' => 'sorting',                      // Sıralama alanı
        
        // Görsel (opsiyonel)
        'iconfile' => 'EXT:testproject/Resources/Public/Icons/team.svg',
    ],

    // =========== TYPES (Sekmeler & Alan Düzeni) ===========
    'types' => [
        '1' => [
            'showitem' => '
                --div--;Genel,
                    name, position, email, bio, photo,
                --div--;Sosyal,
                    social_linkedin, social_twitter,
                --div--;Ayarlar,
                    hidden, starttime, endtime,
            ',
        ],
    ],

    // =========== COLUMNS (Alan Tanımları) ===========
    'columns' => [
        // ✏️ Ad Soyad
        'name' => [
            'label' => 'Ad Soyad',
            'config' => [
                'type' => 'input',
                'size' => 50,
                'max' => 255,
                'eval' => 'trim,required',  // Zorunlu alan
                'placeholder' => 'Örn: Ahmet Yılmaz',
            ],
        ],

        // 💼 Pozisyon
        'position' => [
            'label' => 'Pozisyon',
            'config' => [
                'type' => 'input',
                'size' => 50,
                'max' => 255,
                'eval' => 'trim',
                'placeholder' => 'Örn: Yazılım Geliştirici',
            ],
        ],

        // 📧 E-posta
        'email' => [
            'label' => 'E-posta Adresi',
            'config' => [
                'type' => 'email',
                'eval' => 'trim',
                'placeholder' => 'example@company.com',
            ],
        ],

        // 📝 Biyografi
        'bio' => [
            'label' => 'Biyografi',
            'config' => [
                'type' => 'text',
                'enableRichtext' => true,   // WYSIWYG editör
                'richtextConfiguration' => 'small',
                'rows' => 6,
            ],
        ],

        // 🖼️ Fotoğraf
        'photo' => [
            'label' => 'Profil Fotoğrafı',
            'config' => [
                'type' => 'file',
                'maxitems' => 1,                    // Sadece 1 resim
                'allowed' => 'common-image-types',  // jpg, png, gif, webp
            ],
        ],

        // 🔗 LinkedIn
        'social_linkedin' => [
            'label' => 'LinkedIn Profili',
            'config' => [
                'type' => 'link',
                'allowedTypes' => ['url'],
                'placeholder' => 'https://linkedin.com/in/...',
            ],
        ],

        // 🐦 Twitter/X
        'social_twitter' => [
            'label' => 'Twitter/X Profili',
            'config' => [
                'type' => 'link',
                'allowedTypes' => ['url'],
                'placeholder' => 'https://x.com/...',
            ],
        ],

        // 👁️ Gizli
        'hidden' => [
            'label' => 'Gizli',
            'config' => [
                'type' => 'check',
                'renderType' => 'checkboxToggle',
            ],
        ],

        // 📅 Başlangıç Tarihi
        'starttime' => [
            'label' => 'Yayın Başlangıç',
            'config' => [
                'type' => 'datetime',
            ],
        ],

        // ⏰ Bitiş Tarihi
        'endtime' => [
            'label' => 'Yayın Bitiş',
            'config' => [
                'type' => 'datetime',
            ],
        ],
    ],
];
```

**Detaylı Açıklamalar:**

| Bölüm | Açıklama | Örnek |
|-------|----------|-------|
| **`ctrl`** | Genel ayarlar | Başlık, label, ikon |
| **`types`** | Sekmeler & düzen | --div--;Sekme Adı, |
| **`columns`** | Alan tanımları | input, email, file, text |
| **`type`** | Alan tipi | `input`, `file`, `email` |
| **`config`** | Alan ayarları | `required`, `maxitems`, `max` |

### Adım 4: Veritabanını Aktif Et

```bash
# Veritabanı tablolarını oluştur ve TCA'yı yükle
ddev exec vendor/bin/typo3 extension:setup

# Cache'i temizle
ddev exec vendor/bin/typo3 cache:flush
```

**Ne olur?**
1. `ext_tables.sql` okur → tablo oluşturur
2. TCA dosyasını yükler → backend formunu oluşturur
3. TYPO3 admin panelinde "Ekip Üyesi" modülü görünür

---

### Adım 5: Backend'de Kullan

1. **Admin panele gir** → `https://typo3v13.ddev.site/yonetim`
2. **Modüller** → "Web" → "Sayfalar" seçim yap
3. İçeriğin altında "Ekip Üyesi" linkini gör
4. **Yeni Kayıt Ekle** (+)
5. Form açılır:
   - Ad Soyad: Ahmet Yılmaz
   - Pozisyon: Yazılım Geliştirici
   - E-posta: ahmet@company.com
   - Biyografi: (RTE editör)
   - Profil Fotoğrafı: (resim yükle)
6. **Kaydet** tıkla

---

## 📋 TCA Alan Türleri (v13) — Detaylı

| Tür | Kullanım | Örnek | Notlar |
|-----|----------|-------|--------|
| **input** | Tekli metin | Başlık, şirket adı | `size`, `max`, `eval` |
| **text** | Çok satır | Biyografi, tanım | `enableRichtext` → WYSYWIG |
| **email** | E-posta | Contact form | Otomatik doğrulama |
| **number** | Sayı | Fiyat, miktar | `format: decimal/integer` |
| **link** | Bağlantı | LinkedIn, web adresi | `allowedTypes: [url, page, file]` |
| **color** | Renk seçici | Brand rengi (HEX) | Renk picker UI |
| **datetime** | Tarih-Saat | Olay tarihi, yayın saati | `datetime`, `date` format |
| **select** | Açılır liste | Statü (Aktif/İnaktif) | `items` ile seçenekler |
| **radio** | Radyo tuşları | Evet/Hayır | Görsel seçim |
| **check** | Onay kutusu | Gizli/Görünür | `renderType: checkboxToggle` |
| **file** | Dosya/Resim | Profil fotoğrafı | `maxitems`, `allowed` |
| **folder** | Klasör seçici | Upload klasörü | Dosya yönetimi |
| **slug** | URL slug | `/profile/ahmet-yilmaz` | SEO friendly URL |
| **json** | JSON veri | Yapılandırma | JSON formatında |
| **category** | Kategori | Ekip → Departman | İlişkili kategoriler |
| **inline** | Alt kayıtlar (IRRE) | Ekip → Üyeler | Parent-child ilişkisi |

**Sık Kullanılan Eval Seçenekleri:**
- `required` — Zorunlu alan
- `trim` — Başında/sonundaki boşlukları sil
- `email` — E-posta doğrulaması
- `unique` — Eşsiz değer (tekrarlamaz)
- `int`, `num` — Sayısal doğrulama

---

## 🔧 Mevcut Tabloları Genişletme

### pages Tablosuna Alan Ekleme

Amaç: Her sayfaya "Hero Görseli" alanı eklemek

**Dosya:** `packages/testproject/Configuration/TCA/Overrides/pages.php`

```php
<?php

// Yeni column ekle
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns(
    'pages',
    [
        'tx_testproject_hero_image' => [
            'label' => 'Hero Görseli',
            'config' => [
                'type' => 'file',
                'maxitems' => 1,
                'allowed' => 'common-image-types',
            ],
        ],
    ]
);

// Formuyla ekle (sekmede göster)
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
    'pages',
    '--div--;Hero, tx_testproject_hero_image',
    '',
    'after:media'  // "Media" sekmesinden sonra
);
```

**SQL:** `packages/testproject/ext_tables.sql`

```sql
CREATE TABLE pages (
    tx_testproject_hero_image int unsigned DEFAULT 0 NOT NULL,
);
```

**Sonuç:** Tüm sayfalarda "Hero" sekmesi görünür, resim yükleme alanı bulunur

---

## 🚀 Pratik Örnekler

### Örnek 1: "Ürün" Tablosu

```php
'cols' => [
    'sku' => [
        'label' => 'Ürün Kodu',
        'config' => [
            'type' => 'input',
            'eval' => 'unique,required,int',
        ],
    ],
    'price' => [
        'label' => 'Fiyat (TL)',
        'config' => [
            'type' => 'number',
            'format' => 'decimal',
        ],
    ],
    'stock' => [
        'label' => 'Stok',
        'config' => [
            'type' => 'number',
            'format' => 'integer',
        ],
    ],
    'status' => [
        'label' => 'Durum',
        'config' => [
            'type' => 'select',
            'items' => [
                ['Satışta', 'active'],
                ['İndirimli', 'discounted'],
                ['Tükenmiş', 'out_of_stock'],
            ],
        ],
    ],
]
```

### Örnek 2: "Blog Yazısı" Sayfasına Alan Ekleme

```php
// pages tablosuna yazar, yayın tarihi ekle
'Configuration/TCA/Overrides/pages.php':

'tx_testproject_author' => [
    'label' => 'Yazarı',
    'config' => [
        'type' => 'select',
        'foreign_table' => 'be_users',
        'maxitems' => 1,
    ],
],
'tx_testproject_published_date' => [
    'label' => 'Yayın Tarihi',
    'config' => [
        'type' => 'datetime',
    ],
]
```

---

## 📦 Repository Pattern (Backend'de Veri Çekme)

Özel tablodan veri almanın profesyonel yolu **Repository** kullanmaktır.

### Repository Oluştur

**Dosya:** `packages/testproject/Classes/Domain/Repository/TeamRepository.php`

```php
<?php

namespace Testproject\Testproject\Domain\Repository;

use TYPO3\CMS\Extbase\Persistence\Repository;
use Testproject\Testproject\Domain\Model\Team;

class TeamRepository extends Repository
{
    /**
     * Sadece aktif üyeleri getir
     */
    public function findActive()
    {
        return $this->createQuery()
            ->matching(
                $this->query->equals('hidden', 0)
            )
            ->execute();
    }

    /**
     * Belirli departmandan üyeleri getir
     */
    public function findByDepartment($department)
    {
        return $this->createQuery()
            ->matching(
                $this->query->equals('department', $department)
            )
            ->setOrderings(['sorting' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING])
            ->execute();
    }

    /**
     * Üyeleri sırayla getir
     */
    public function findAll()
    {
        return $this->createQuery()
            ->setOrderings(['sorting' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING])
            ->execute();
    }
}
```

### Controller'da Kullan

**Dosya:** `packages/testproject/Classes/Controller/TeamController.php`

```php
<?php

namespace Testproject\Testproject\Controller;

use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use Testproject\Testproject\Domain\Repository\TeamRepository;

class TeamController extends ActionController
{
    public function __construct(
        private readonly TeamRepository $teamRepository
    ) {}

    /**
     * Tüm ekip üyelerini listele
     */
    public function listAction(): void
    {
        $teams = $this->teamRepository->findAll();
        $this->view->assign('teams', $teams);
    }

    /**
     * Belirli üyeyi görüntüle
     */
    public function showAction(Team $team): void
    {
        $this->view->assign('team', $team);
    }

    /**
     * Departmana göre filtrele
     */
    public function byDepartmentAction(string $department): void
    {
        $teams = $this->teamRepository->findByDepartment($department);
        $this->view->assign('teams', $teams);
    }
}
```

---

## 🎨 Frontend'de Göster (Fluid Template)

### Template: `list.html`

```html
<div class="team-container">
    <f:for each="{teams}" as="team">
        <div class="team-card">
            <!-- Foto -->
            <f:if condition="{team.photo}">
                <f:image image="{team.photo}" width="200c" height="200c" class="team-photo" />
            </f:if>

            <!-- İçerik -->
            <div class="team-info">
                <h3>{team.name}</h3>
                <p class="position">{team.position}</p>
                <p class="bio">{team.bio}</p>

                <!-- Sosyal Linkler -->
                <div class="social">
                    <f:if condition="{team.socialLinkedin}">
                        <a href="{team.socialLinkedin}" target="_blank" class="linkedin">
                            LinkedIn
                        </a>
                    </f:if>
                    <f:if condition="{team.socialTwitter}">
                        <a href="{team.socialTwitter}" target="_blank" class="twitter">
                            Twitter
                        </a>
                    </f:if>
                </div>

                <!-- E-posta -->
                <p class="email">
                    <a href="mailto:{team.email}">
                        {team.email}
                    </a>
                </p>
            </div>

            <!-- Detay Linki -->
            <f:link.action action="show" arguments="{team: team}" class="btn-detail">
                Detay →
            </f:link.action>
        </div>
    </f:for>
</div>
```

### Template: `show.html` (Detay)

```html
<article class="team-detail">
    <!-- Başık -->
    <header>
        <h1>{team.name}</h1>
        <p class="subtitle">{team.position}</p>
    </header>

    <!-- Foto (tam boyut) -->
    <f:if condition="{team.photo}">
        <figure class="detail-photo">
            <f:image image="{team.photo}" width="600c" height="400c" />
            <figcaption>Profil Fotoğrafı</figcaption>
        </figure>
    </f:if>

    <!-- İçerik -->
    <section class="detail-bio">
        <h2>Hakkında</h2>
        <p>{team.bio -> f:format.nl2br()}</p>
    </section>

    <!-- İletişim -->
    <section class="contact-section">
        <h3>İletişim</h3>
        <ul>
            <li><strong>E-posta:</strong> <a href="mailto:{team.email}">{team.email}</a></li>
            <f:if condition="{team.socialLinkedin}">
                <li><strong>LinkedIn:</strong> <a href="{team.socialLinkedin}" target="_blank">Profil</a></li>
            </f:if>
            <f:if condition="{team.socialTwitter}">
                <li><strong>Twitter:</strong> <a href="{team.socialTwitter}" target="_blank">{team.socialTwitter}</a></li>
            </f:if>
        </ul>
    </section>

    <!-- Geri Linki -->
    <f:link.action action="list" class="btn-back">
        ← Ekibe Dön
    </f:link.action>
</article>
```

---

## 🔍 SQL Sorgusu ile Veri Çekme (Alternatif)

Eğer Repository yerine doğrudan SQL kullanmak istersen:

```php
<?php

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class TeamService
{
    public function getAllTeams()
    {
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getConnectionForTable('tx_testproject_domain_model_team');

        $query = $connection->select(
            ['*'],  // Columns
            'tx_testproject_domain_model_team',  // Table
            ['hidden' => 0, 'deleted' => 0]  // WHERE
        );

        return $query->fetchAllAssociative();
    }

    public function getTeamsByDepartment($department)
    {
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getConnectionForTable('tx_testproject_domain_model_team');

        $query = $connection->select(
            ['*'],
            'tx_testproject_domain_model_team',
            ['hidden' => 0, 'deleted' => 0, 'department' => $department]
        );

        return $query->fetchAllAssociative();
    }
}
```

---

## ⚡ Hızlı Referans

### TCA Minimum Yapısı

```php
<?php

return [
    'ctrl' => [
        'title' => 'Başlık',
        'label' => 'name',  // Listede hangi alan görünsün
        'tstamp' => 'tstamp',
        'crdate' => 'crdate',
        'delete' => 'deleted',
        'enablecolumns' => [
            'disabled' => 'hidden',
            'starttime' => 'starttime',
            'endtime' => 'endtime',
        ],
    ],
    'types' => [
        '1' => ['showitem' => 'name, position, email, ---divider---, bio, photo'],
    ],
    'columns' => [
        'name' => ['config' => ['type' => 'input']],
        'position' => ['config' => ['type' => 'input']],
        'email' => ['config' => ['type' => 'email']],
        'bio' => ['config' => ['type' => 'text', 'enableRichtext' => true]],
        'photo' => ['config' => ['type' => 'file', 'maxitems' => 1]],
    ],
];
```

### Sık Yapılandırmalar

```php
// Zorunlu alan
'eval' => 'required'

// Düzenlemeyi kapat
'readOnly' => true

// Varsayılan değer
'default' => 'hello'

// Karakter sınırı
'max' => 50

// Satır sınırı (text)
'rows' => 10

// Dosya türü sınırlaması
'allowed' => 'common-image-types'

// Sıralama
'sorting' => 'name'
```

---

## Kaynaklar

- [TYPO3 TCA Reference](https://docs.typo3.org/m/typo3/reference-tca/main/en-us/)
- [Extbase Domain Models](https://docs.typo3.org/m/typo3/guide-extbase/main/en-us/)
- [Database API](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Database/)
- [Query Methods](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Database/Query/)
