# SMS Modülü Yazma

https://dev.wisecp.com/tr/sms-modulu-yazma

SMS modülü, metin mesajını bir geçide teslim eden sürücüdür. Mail'den farklı olarak üç iş yapar: bildirim, uluslararası gönderim ve bazen fiyat ile teslim raporlaması.

## Genel Bakış

Taban sınıf yoktur. Sözleşme Mail gibi ördek tiplemesine dayanır: çekirdek, dizin adını taşıyan sınıfı yükler ve bilinen metotları çağırır.

İki config anahtarı sürücü gösterir: `modules/sms` bildirimler, `modules/sms-intl` uluslararası trafik için. Yurt içi geçit yabancı numaraları ikincisine devreder, bu yüzden iki kova tutulur.

Üçüncü yol modülündür: `controllers/` dosyalarınız sürücüyü kaydedilmemiş kimlik bilgileriyle kurabilir.

## Ön Koşullar

- `coremio/modules/SMS/` altına yazma erişimi.
- Geçit kimlik bilgileri ve genelde kayıtlı bir gönderici kimliği.
- Geçidiniz yurt içi mi, uluslararası mı? Yetenek işaretlerini bu belirler.
- [Mail Modülü Yazma](https://dev.wisecp.com/tr/mail-modulu-yazma).

## Yapı

Dizin, dosya ve sınıf adı aynı dizedir, ad alanı yoktur.

```bash
coremio/modules/SMS/Acme/
├── Acme.php          sürücü sınıfı, adı Acme, ad alanı yok
├── config.php        künye (uluslararası bayrağıyla) + kaydedilmiş kimlik bilgileri
├── Source/class.php  isteğe bağlı geçit istemcisi, kurucu tarafından dahil edilir
├── lang/en.php
├── lang/tr.php
└── logo.png          isteğe bağlı, config meta.logo ile gösterilir
```

## Adım Adım

### Sürücü Sınıfını Kurma

1. `coremio/modules/SMS/Acme/Acme.php` dosyasını açıp `class Acme` tanımlayın.
2. Yetenek işaretlerini public özellik olarak bildirin.
3. Yapıcıda kayıtlı config'i verilen diziyle birleştirin; verilen üstün gelir.
4. `body()`, `title()`, `AddNumber()` metotlarını her biri `$this` döndürecek şekilde uygulayın.
5. `submit()` uygulayın: yurt içi kovayı gönderin, ötekini devredin.
6. `getTitle()`, `getBody()`, `getNumbers()`, `getError()` uygulayın.

### Gönderici Kimliği ve Numaralar

1. Göndericiyi yapıcıda config'ten doldurun.
2. `AddNumber()` üç biçimi kabul etmeli: numara, ülke kodlu numara, dizi.
3. Dikey çizgi taşıyan değer `ülkeKodu|numara` demektir.
4. Devir yasaklanmadıysa sonucu ülke koduna göre kovaya yönlendirin.
5. Her iki kovayı da `body()` içinde sıfırlayın.

### Ayarlar ve Raporlar

1. Gizli alanlar, etkinleştirme kutusu ve kimlik alanlarıyla `page_settings()` ekleyin.
2. `controller_save()` ekleyin: değişenleri `config.php` içine yazın, `modules/sms` çevirin.
3. Kredi bakiyesi için `getBalance()`; çekirdek çağırmaz.
4. Toplu gönderim kimliği için `getReportID()` ve `getReport()` uygulayın.
5. `get_prices()` yalnız uluslararası geçitler içindir.

## Referans

### Çekirdek Neyi Çağırır

Dağıtıcı mesajı kurar, sonra alıcıları gezer.

| Metot | Çekirdek ne zaman çağırır | Ne döndürmeli |
| --- | --- | --- |
| `__construct($external_config = [])` | Gönderim başına bir kez | hiçbir şey |
| `body($text, $template, $variables, $lang, $user)` | İlk sırada; kovaları sıfırlar | `$this` |
| `use_otp()` | Tek kullanımlık kod şablonlarında, `body()`'den sonra. Sunulduğunda. | `$this` |
| `title($arg)` | Gönderici kimliği değiştirilirse | `$this` |
| `AddNumber($arg, $cc)` | Alıcı başına, ya da bir diziyle | `$this` |
| `submit($isthis = false)` | Alıcılar girildikten sonra | başarıda doğru değer, hatada false |
| `getTitle()` | Başarılı gönderimden sonra | dize |
| `getBody()` | Başarılı gönderimden sonra | dize |
| `getNumbers()` | Başarılı gönderimden sonra | iki kova, tek dizi |
| `getError()` | Yanlış değer dönen gönderimden sonra | hata metni |
| `numbers_reset()` | Kendi `body()` metodunuz | okunmaz |
| `getReportID()`, `getReport($id)`, `get_prices()` | Sunulduğunda | aşağıya bakın |
| `getBalance()` | Çekirdek çağırmaz | kendi biçiminiz |

Harf durumuna dikkat: dağıtıcı `addNumber()`, sürücüler `AddNumber()` yazar. PHP ikisini de aynı metoda çözer.

### Metot İmzaları

```php
public function __construct($external_config = []);

// HER İKİ alıcı kovasını sıfırlar, sonra şablon verildiyse onu işler.
// $template "group/name" biçimindedir, örn. "user/gsm-activation"; false ise $text nihaidir.
public function body($text = '', $template = false, $variables = [], $lang = '', $user = 0);

// Cihazda görünen gönderici kimliği / originator.
public function title($arg = '');

// SIRA TUZAĞI: ÖNCE numara, SONRA ülke kodu gelir.
// $arg şunları kabul eder: "5551234567", "90|5551234567", ya da ikisinden birinin dizisi.
// $cc yalnız $arg bir dizi değilken dikkate alınır.
public function AddNumber($arg = 0, $cc = null);

// $isthis = true boolean yerine sürücüyü döndürür.
public function submit($isthis = false);

public function getTitle();
public function getBody();
public function getNumbers();
public function getError();
public function numbers_reset();

// İsteğe bağlı, özellik özellik. Çekirdek her birini çağırmadan önce method_exists() ile yoklar.
public function getReportID();                // son gönderimin ürettiği toplu gönderim kimliği
public function getReport($id = 0);           // bir toplu gönderim için teslim raporu
public function get_prices();                 // uluslararası fiyat listesi, aşağıya bakın
public function use_otp(bool $on = true);     // sağlayıcının OTP rotası; $this döndürür

// Modüle özgü konvansiyon: çekirdek bunu hiç çağırmaz. Yalnız kendi
// controllers/*.php ve pages/*.php dosyalarınızdan ulaşılır, yani argüman listesi sizindir.
public function getBalance();                 // kalan kredi, ya da $error dolu olarak false
```

İki dönüş biçimi ada göre okunur:

```php
// getReport(): üç adlandırılmış kova, her biri ham satırlar ve bir sayaç taşır.
// Rapor okuyucu ayrıca 'delivered' / 'sending' / 'failed' adlarını takma ad olarak kabul eder.
return [
    'waiting'   => ['data' => $waitingRows,   'count' => count($waitingRows)],
    'conducted' => ['data' => $deliveredRows, 'count' => count($deliveredRows)],
    'erroneous' => ['data' => $failedRows,    'count' => count($failedRows)],
];

// get_prices(): ülke kodu => para birimi kodu => mesaj başına maliyet.
// Config'in supported-currencies anahtarı birini belirtmedikçe kullanılabilir ilk para birimi kazanır.
return [
    'TR' => ['EUR' => 0.0180],
    'DE' => ['EUR' => 0.0640, 'USD' => 0.0700],
    'US' => ['USD' => 0.0075],
];
```

`get_prices()` maliyetleri birincil para birimine çevrilir, `sms/profit-rate` marjı eklenir ve ülke haritası yeniden yazılır.

### Yetenek İşaretleri

Bunlar metot değil public özelliktir. Çekirdek onları hem örnekte hem config'te okur, ikisi uyuşmalıdır.

- **$international**: Geçit ülke dışına teslim edebiliyorsa true.
- **$prevent_transmission_to_intl**: Yabancı numaraları yurt içi kovada tutar; iki metotta da buna uyun.
- **$otp**: Tek kullanımlık kod gönderimini işaretler. `body()` sıfırlar, çekirdek `use_otp()` ile açar; `submit()` rotayı buna göre seçer.
- **$error**: `getError()` olmasına rağmen doğrudan okunur.
- **meta.international**: Aynı cevabın `config.php` hâli; seçici buna bakar. Özellik ile meta uyuşmalıdır.

### Config Anahtarları

- **meta.name**: Görünen ad; dil dosyasındaki `name` anahtarı üstün gelir.
- **meta.poweredby**: Geçit markası.
- **origin**: Kayıtlı gönderici kimliği; `title()` bundan doldurulur.
- **supported-currencies**: `get_prices()` birkaç para birimi sunarsa tercih listesi. Boşsa ilki alınır.
- **Crypt::encode()**: Şifreli saklanır: `Crypt::encode($v, Config::get("crypt/user"))` ile yazın, `Crypt::decode()` ile okuyun.

### Hangi Sürücü Çalışır

Üçüncü çözümlemeyi yanlış yapmak, müşterinin hesabı yerine operatörün geçit hesabıyla gönderir.

| Yol | Örnek nasıl kurulur | Kullanılan kimlik bilgileri |
| --- | --- | --- |
| Bildirimler | Adsız `Modules::Load("SMS")`, sonra `new $smsModule()` | kayıtlı config |
| Uluslararası gönderim | `Modules::getInstance("SMS", Config::get("modules/sms-intl"))` | kayıtlı config |
| Modülün kendi sayfaları | `new Modulunuz($external)` | formdan gelen değerler, kayıtlı config üzerine |

Yapıcının `$external_config` taşıma sebebi budur; çekirdek onu geçmez. Fabrikanın üçüncü argümanı yapıcı için konumsal argüman listesidir.

## Örnek

Kendi ülkesini kapsayan ve yabancı numaraları devreden bir geçit.

"Kendi ülke kodu" geçidinizin hizmet verdiği ülkedir; config'inizden okunur. Karşılaştırmadan önce baştaki `+` işaretini temizleyin ve operatöre dikkat edin: `!=` ile `'90'` ve `'+90'` eşit sayılır, `!==` ile sayılmaz.

```php
<?php
defined('CORE_FOLDER') OR exit('You can not get in here!');

class Acme
{
    public $international = false;
    public $prevent_transmission_to_intl = false;
    public $otp = false;
    public $error = null;
    public $lang = [];
    public $config = [];

    private $title = '';
    private $body = '';
    private $numbers = [];
    private $numbers_intl = [];

    public function __construct($external_config = [])
    {
        $this->lang   = Modules::Lang('SMS', __CLASS__);
        $this->config = array_merge(Modules::Config('SMS', __CLASS__) ?: [], $external_config);
        $this->title  = (string) ($this->config['origin'] ?? '');
    }

    public function title($arg = '')
    {
        $this->title = (string) $arg;
        return $this;
    }

    public function body($text = '', $template = false, $variables = [], $lang = '', $user = 0)
    {
        // Dağıtıcı alıcı başına tek örneği yeniden kullanır; bu olmadan kovalar büyür.
        $this->numbers_reset();

        if ($template) {
            $look = View::notifications('sms', $template, $text, $variables, $lang, $user);
            if ($look !== false && isset($look['content'])) {
                if (isset($look['title'])) $this->title($look['title']);
                $text = $look['content'];
            }
        }

        $this->body = (string) $text;
        return $this;
    }

    public function use_otp(bool $on = true)
    {
        // Çekirdek bunu kod şablonlarında body()'den sonra çağırır; son sözü modülün kendi ayarı verir.
        $this->otp = $on && (bool) ($this->config['otp'] ?? false);
        return $this;
    }

    public function AddNumber($arg = 0, $cc = null)
    {
        if (!is_array($arg)) $arg = $cc ? [$cc . '|' . $arg] : [$arg];

        foreach ($arg as $num) {
            if (!str_contains((string) $num, '|')) {
                $this->numbers[] = Filter::numbers((string) $num);
                continue;
            }

            // Geçidin kendi ülkesi, config'inden okunur — asla sabit yazılmaz.
            $home = ltrim((string) ($this->config['home_cc'] ?? ''), '+');

            [$ccPart, $numPart] = explode('|', (string) $num, 2);
            $ccPart = ltrim($ccPart, '+') ?: $home;
            $full   = $ccPart . Filter::numbers($numPart);

            if (!$this->prevent_transmission_to_intl && $home !== '' && $ccPart !== $home) $this->numbers_intl[] = $full;
            else $this->numbers[] = $full;
        }

        return $this;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function getBody()
    {
        return $this->body;
    }

    public function getNumbers()
    {
        return array_merge($this->numbers, $this->numbers_intl);
    }

    public function getError()
    {
        return $this->error;
    }

    public function numbers_reset()
    {
        $this->otp          = false;
        $this->numbers      = [];
        $this->numbers_intl = [];
        return true;
    }

    public function submit($isthis = false)
    {
        if (Validation::isEmpty($this->body)) {
            $this->error = 'Message content can not be left blank!';
            return false;
        }

        if (!$this->numbers && !$this->numbers_intl) {
            $this->error = 'Enter the phone number to be sent.';
            return false;
        }

        $send = false;

        // Yabancı numaralar modules/sms-intl anahtarını hangi sürücü tutuyorsa ona gider.
        if (!$this->prevent_transmission_to_intl && $this->numbers_intl) {
            $intl = (string) Config::get("modules/sms-intl");
            if ($intl !== '' && $intl !== 'none') {
                $peer = Modules::getInstance('SMS', $intl);
                if ($peer) {
                    $send = $peer->body($this->getBody())->AddNumber($this->numbers_intl)->submit();

                    // Yurt içi numara kalmadı, yani karşı sürücünün sonucu sonucun tamamıdır.
                    if (!$this->numbers) {
                        $this->error = $peer->getError();
                        return $isthis ? $this : $send;
                    }
                }
            }
        }

        if ($this->numbers) {
            $response = Utility::HttpRequest([
                'url'  => 'https://api.example.com/sms/send',
                'type' => 'POST',
                'data' => [
                    'user'    => $this->config['username'] ?? '',
                    'pass'    => Crypt::decode($this->config['password'] ?? '', Config::get("crypt/user")),
                    'header'  => $this->title,
                    'message' => $this->body,
                    'to'      => implode(',', $this->numbers),
                    'otp'     => $this->otp ? 1 : 0,
                ],
            ]);

            $decoded = Utility::jdecode((string) $response, true) ?: [];
            $send    = (string) ($decoded['code'] ?? '') === '00';

            // Dağıtıcı yanlış değer dönüşünden sonra hatayı okur; fırlatmaları yakalamaz.
            if (!$send) $this->error = $decoded['message'] ?? 'Acme refused the batch.';
        }

        return $isthis ? $this : $send;
    }
}
```

```php
Modules::Load("SMS");
$smsModule = Config::get("modules/sms");
$sms = $smsModule && $smsModule !== 'none' ? new $smsModule() : false;

// $phone "number|countryCode|lang" olarak gelir, yani parçalar konuma göre açılır.
foreach ($adminContacts['phones'] as $phone) {
    $parse   = explode("|", (string) $phone);
    $aLang   = $parse[2] ?? $localLang;
    $sendSms = $sms->body($body, $templatePath, $variables, $aLang);

    if (isset($parse[1])) $sendSms->addNumber($parse[0], $parse[1]);
    else $sendSms->addNumber($parse[0]);

    $sendSms = $sendSms->submit();

    if ($sendSms) LogManager::Sms_Log(0, $reason, $sms->getTitle(), $sms->getBody(), implode(",", $sms->getNumbers()));
    else $errors['sms'][$phone] = $sms->getError();
}
```

## Tuzaklar

> **AddNumber: önce numara, sonra ülke kodu**
> 
> Birleşik biçim bunun tersidir, `ülkeKodu|numara`; kayıtlı kişi `numara|ülkeKodu|dil` olarak açılır. Yer değiştirmek makul görünen ama düşen bir numara üretir.

> **body() kovaları sıfırlamalıdır**
> 
> Mail alıcıları `subject()` içinde sıfırlar; SMS `body()` içinde. Sıfırlama olmazsa ikinci alıcı, içinde birinci alıcı da olan bir gönderim alır.

> **İşaret, para hareket etmeden önce kontrol edilir**
> 
> Müşteri paneli gönderimi fiyatlar, sürücünün `$international` özelliği true değilse reddeder, sonra bakiyeden düşer. Desteği yalnız `config.php` içinde iddia eden sürücü düşer.

> **submit() hatayı false döndürerek bildirir**
> 
> Bildirim döngüsü try/catch içinde değildir ve yanlış değer dönen çağrıdan sonra `getError()` okur. Ayar controller'larınız fırlatabilir.

> **OTP işaretini şablon adından açmayın**
> 
> Bildirim dağıtıcısı `body()`'yi şablon adı vermeden çağırır, yani şablon adına bakan bir koşul hiç çalışmaz. Kod şablonunu çekirdek seçer ve `use_otp()` çağırır; sürücü yalnız kendi ayarını birleştirir.

> **Kum havuzu sürücüsüyle test edin**
> 
> SampleSMS her şeyi kabul eder ve her mesajı `temp/sample-sms/` altına metin dosyası olarak yazar.

## İlgili Makaleler

- [Mail Modülü Yazma](https://dev.wisecp.com/tr/mail-modulu-yazma)
- [SMS Şablonu Yazma](https://dev.wisecp.com/tr/sms-sablonu-yazma)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Zamanlanmış Görev Ekleme](https://dev.wisecp.com/tr/zamanlanmis-gorev-ekleme)
- [Kullanıcı Girdisini Filtreleme](https://dev.wisecp.com/tr/kullanici-girdisini-filtreleme)
