Laravel ile Xero Bağlantısı Nasıl Yapılır?

By Codefacture7 dk okuma

Laravel ile Xero Bağlantısı Nasıl Yapılır?

 

Xero, başta Birleşik Krallık, Avustralya ve Yeni Zelanda olmak üzere çok sayıda küçük ve orta ölçekli işletmenin tercih ettiği muhasebe platformudur. Laravel ile geliştirilen uygulamalar için Xero bağlantısı; faturaların otomatik oluşturulması, kişilerin senkronize kalması ve finansal verilerin manuel iş gerektirmeden akması demektir. Laravel bu iş için mükemmel bir seçimdir: HTTP istemcisi API çağrılarını sadeleştirir, şifreli cast'ler token'ları veritabanında korur, cache kilitleri yenileme çakışmalarını önler, kuyruklar ve zamanlayıcı ise arka plan senkronizasyonunu yönetir. Bu rehberde Xero'yu Laravel ile adım adım bağlayacak, OAuth 2.0'ı ve API çağrılarını Laravel'in kendi araçlarıyla kuracak ve her aşama için kod örnekleri paylaşacağız.

 

Başlamadan Önce

İşe Xero geliştirici portalında bir uygulama oluşturarak başlayın. Web app türünü seçin ve callback adresinizi ekleyin; örneğin yerel geliştirme için http://localhost:8000/xero/callback. Portal size bir istemci kimliği (client ID) ve istemci anahtarı (client secret) verir. Geliştirme sırasında, örnek verilerle gelen ve gerçek defterlere dokunmadan rahatça test yapmanızı sağlayan Xero demo şirketine bağlanın.

Bilinmesi gereken önemli ve yeni bir değişiklik var: 2 Mart 2026 ve sonrasında oluşturulan uygulamalar artık geniş kapsamlı accounting.transactions scope'unu talep edemez. Bunun yerine accounting.invoices veya accounting.payments gibi granüler scope'ları, yalnızca uygulamanın gerçekten ihtiyaç duyduğu izinleri seçerek talep etmeleri gerekir. Mevcut uygulamaların geçiş için Eylül 2027'ye kadar süresi var. Kimlik bilgilerinizi ortam değişkenlerine ekleyin ve kodun geri kalanının bunları Laravel'in yapılandırma sistemi üzerinden okuması için config/services.php dosyasına kaydedin.

# .env
XERO_CLIENT_ID=...
XERO_CLIENT_SECRET=...
XERO_REDIRECT_URI=http://localhost:8000/xero/callback
XERO_WEBHOOK_KEY=...

// config/services.php
'xero' => [
    'client_id' => env('XERO_CLIENT_ID'),
    'client_secret' => env('XERO_CLIENT_SECRET'),
    'redirect' => env('XERO_REDIRECT_URI'),
    'webhook_key' => env('XERO_WEBHOOK_KEY'),
],

 

Bağlantıları Güvenle Saklamak

Tenant olarak adlandırılan her bağlı Xero organizasyonu, tenant kimliği ve güncel token'larla birlikte kendi kaydına ihtiyaç duyar. Xero erişim token'ları kısa ömürlüdür, yenileme token'ları ise bağlantıyı zaman içinde canlı tutar; bu yüzden ikisi de saklanmalıdır. Laravel'in encrypted cast'i, token'ları veritabanına ulaşmadan önce uygulama anahtarınızla otomatik olarak şifreler; böylece sızan bir veritabanı yedeği çalışan kimlik bilgilerini açığa çıkarmaz. Xero token'ları uzun olduğu ve şifreleme boyutlarını daha da artırdığı için text sütunlar kullanın.

// database/migrations/xxxx_create_xero_connections_table.php
Schema::create('xero_connections', function (Blueprint $table) {
    $table->id();
    $table->string('tenant_id')->unique();
    $table->string('tenant_name');
    $table->text('access_token');
    $table->text('refresh_token');
    $table->timestamp('expires_at');
    $table->timestamps();
});
// app/Models/XeroConnection.php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class XeroConnection extends Model
{
    protected $fillable = [
        'tenant_id', 'tenant_name', 'access_token', 'refresh_token', 'expires_at',
    ];

    protected function casts(): array
    {
        return [
            'access_token' => 'encrypted',
            'refresh_token' => 'encrypted',
            'expires_at' => 'datetime',
        ];
    }
}

 

OAuth 2.0 Akışını Kurmak

Yetkilendirme akışı iki action gerektirir. İlki rastgele bir state değeri üretir, bunu session'da saklar ve kullanıcıyı talep edilen izinleri inceleyip hangi organizasyonları bağlayacağını seçtiği Xero onay ekranına yönlendirir. Yenileme token'ı alabilmek için offline_access scope'u gereklidir. İkinci action callback'i karşılar: siteler arası istek sahteciliğine (CSRF) karşı state değerini doğrular, yetkilendirme kodunu token'larla takas eder, bağlı organizasyonları alır ve bağlantıyı kaydeder.

// app/Http/Controllers/XeroController.php
namespace App\Http\Controllers;

use App\Models\XeroConnection;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class XeroController extends Controller
{
    public function connect(Request $request)
    {
        $state = Str::random(40);
        $request->session()->put('xero_state', $state);

        $query = http_build_query([
            'response_type' => 'code',
            'client_id' => config('services.xero.client_id'),
            'redirect_uri' => config('services.xero.redirect'),
            'scope' => 'openid profile email accounting.contacts accounting.invoices offline_access',
            'state' => $state,
        ]);

        return redirect('https://login.xero.com/identity/connect/authorize?'.$query);
    }

    public function callback(Request $request)
    {
        abort_unless(
            $request->state && $request->state === $request->session()->pull('xero_state'),
            403
        );

        $tokens = Http::asForm()
            ->withBasicAuth(config('services.xero.client_id'), config('services.xero.client_secret'))
            ->post('https://identity.xero.com/connect/token', [
                'grant_type' => 'authorization_code',
                'code' => $request->code,
                'redirect_uri' => config('services.xero.redirect'),
            ])
            ->throw()
            ->json();

        // List the organisations (tenants) the user just authorised
        $tenant = Http::withToken($tokens['access_token'])
            ->get('https://api.xero.com/connections')
            ->throw()
            ->json(0);

        XeroConnection::updateOrCreate(
            ['tenant_id' => $tenant['tenantId']],
            [
                'tenant_name' => $tenant['tenantName'],
                'access_token' => $tokens['access_token'],
                'refresh_token' => $tokens['refresh_token'],
                'expires_at' => now()->addSeconds($tokens['expires_in']),
            ]
        );

        return redirect()->route('integrations')->with('status', 'Xero connected');
    }
}
// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/xero/connect', [XeroController::class, 'connect'])->name('xero.connect');
    Route::get('/xero/callback', [XeroController::class, 'callback'])->name('xero.callback');
});

Sadelik için bu örnek ilk bağlanan organizasyonu saklar. Kullanıcılar tek bir yetkilendirmede birden fazla organizasyon bağlayabiliyorsa bağlantı listesinin tamamında döngü kurun ve her tenant'ı ayrı ayrı saklayın.

 

API Çağrıları Yapmak ve Token'ları Yenilemek

Ayrı bir servis sınıfı tüm Xero mantığını tek bir yerde toplar. Her istekten önce erişim token'ının süresinin dolmak üzere olup olmadığını kontrol eder ve gerekirse yeniler. Yenileme yeni bir yenileme token'ı döndürdüğü için güncelleme hemen kaydedilmeli ve iki queue worker aynı bağlantıyı asla aynı anda yenilememelidir. Bunu bir cache kilidi çözer: ilk worker yenilemeyi yapar, diğerleri bekler, kaydı yeniden yükler ve yeni token'ı kullanır. Cache kilitlerinin Redis, Memcached veya database gibi kilitleri destekleyen bir cache sürücüsü gerektirdiğini unutmayın.

// app/Services/XeroClient.php
namespace App\Services;

use App\Models\XeroConnection;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class XeroClient
{
    public function __construct(private XeroConnection $connection) {}

    public function request(): PendingRequest
    {
        $this->refreshTokenIfNeeded();

        return Http::withToken($this->connection->access_token)
            ->withHeaders(['xero-tenant-id' => $this->connection->tenant_id])
            ->acceptJson()
            ->baseUrl('https://api.xero.com/api.xro/2.0')
            ->retry(3, 2000, fn ($e) => $e instanceof RequestException
                && $e->response->status() === 429);
    }

    public function createDraftInvoice(string $contactId, float $amount): array
    {
        return $this->request()
            ->post('/Invoices', [
                'Invoices' => [[
                    'Type' => 'ACCREC',
                    'Contact' => ['ContactID' => $contactId],
                    'LineAmountTypes' => 'Exclusive',
                    'Status' => 'DRAFT',
                    'LineItems' => [[
                        'Description' => 'Consulting services',
                        'Quantity' => 1,
                        'UnitAmount' => $amount,
                        'AccountCode' => '200',
                    ]],
                ]],
            ])
            ->throw()
            ->json('Invoices.0');
    }

    private function refreshTokenIfNeeded(): void
    {
        if ($this->connection->expires_at->isAfter(now()->addMinute())) {
            return;
        }

        // Only one worker may refresh at a time
        Cache::lock("xero-refresh-{$this->connection->id}", 10)->block(5, function () {
            $this->connection->refresh(); // another worker may have refreshed already

            if ($this->connection->expires_at->isAfter(now()->addMinute())) {
                return;
            }

            $tokens = Http::asForm()
                ->withBasicAuth(config('services.xero.client_id'), config('services.xero.client_secret'))
                ->post('https://identity.xero.com/connect/token', [
                    'grant_type' => 'refresh_token',
                    'refresh_token' => $this->connection->refresh_token,
                ])
                ->throw()
                ->json();

            $this->connection->update([
                'access_token' => $tokens['access_token'],
                'refresh_token' => $tokens['refresh_token'],
                'expires_at' => now()->addSeconds($tokens['expires_in']),
            ]);
        });
    }
}

Örnek bir taslak satış faturası oluşturur; işletme faturaları otomatik onaylamaya hazır olana kadar bu güvenli bir varsayılandır. Yerleşik retry mekanizması 429 limit yanıtlarını zarif biçimde ele alır ve her çağrı, Xero'ya hangi organizasyonun kullanılacağını söyleyen xero-tenant-id başlığını otomatik olarak taşır.

 

Xero Webhook'larını Almak

Uygulamanız, değişiklikleri sürekli sorgulamak yerine oluşturulan veya güncellenen kişiler ve faturalar gibi olaylar için Xero webhook'larına abone olabilir. Her istek, webhook anahtarınızla oluşturulmuş ham gövdenin HMAC-SHA256 özeti olan bir x-xero-signature başlığı taşır. Adresi ilk kaydettiğinizde Xero bir intent-to-receive doğrulaması gönderir: route'unuz doğru imzalanmış isteklere 200, yanlış imzalanmış olanlara 401 dönmelidir; aksi halde abonelik aktive edilmez.

Xero hızlı yanıt beklediği için controller yalnızca imzayı doğrular ve her olayı kuyruktaki bir job'a aktarır. Webhook içerikleri tam kayıtlar yerine değişen kaynaklara referanslar içerdiğinden, job güncel veriyi servis sınıfı üzerinden çeker. Webhook route'unun ayrıca CSRF korumasının dışında tutulması gerekir.

// app/Http/Controllers/XeroWebhookController.php
namespace App\Http\Controllers;

use App\Jobs\ProcessXeroEvent;
use Illuminate\Http\Request;

class XeroWebhookController extends Controller
{
    public function __invoke(Request $request)
    {
        $expected = base64_encode(hash_hmac(
            'sha256',
            $request->getContent(),
            config('services.xero.webhook_key'),
            true
        ));

        if (! hash_equals($expected, (string) $request->header('x-xero-signature'))) {
            return response('', 401);
        }

        // Respond fast: fetch full records later in queued jobs
        foreach ($request->json('events', []) as $event) {
            ProcessXeroEvent::dispatch($event);
        }

        return response('', 200);
    }
}

// routes/web.php
Route::post('/webhooks/xero', XeroWebhookController::class);

// bootstrap/app.php
$middleware->validateCsrfTokens(except: ['webhooks/*']);

 

Üretim İçin En İyi Uygulamalar

Xero; dakika başına ve günlük çağrı limitleri ile eşzamanlı istek limiti de dahil olmak üzere organizasyon başına istek limitleri uygular. Büyük senkronizasyonları kuyruktaki job'lara taşıyın, yalnızca değişen kayıtları çekin ve işlem hacmini limitler içinde tutmak için Laravel'in rate limiting özelliklerini veya job middleware'lerini kullanın. Zamanlayıcı, bir webhook'un kaçırmış olabileceği her şeyi yakalayan periyodik mutabakat işleri için doğal bir yerdir.

Yalnızca ihtiyaç duyduğunuz granüler scope'ları talep edin, kullanıcı Xero ayarlarından erişimi iptal ettiğinde bağlantı kopmasını düzgün ele alın ve sorunları hızla izleyebilmek için her senkronizasyonu yeterli bağlamla kayıt altına alın. Başarısız job'larda uyarı üretmek, senkronizasyon sorunlarının ay sonunda değil anında fark edilmesini sağlar.

 

Sonuç

Xero'yu Laravel ile bağlamak, framework'ün zaten sunduğu araçlarla uygulamanıza müşterilerinizin muhasebe verilerine güvenilir ve otomatik erişim kazandırır. Granüler scope'larla bir uygulama kaydederek, OAuth 2.0'ı state doğrulamasıyla kurarak, token'ları şifreli saklayıp cache kilitleriyle güvenle yenileyerek ve webhook'ları kuyruklar üzerinden işleyerek kullanım büyüdükçe güvenilir kalan bir entegrasyon kurarsınız. İster bir SaaS ürününe faturalama ekliyor ister iç finans süreçlerini otomatikleştiriyor olun, entegrasyonu doğru kurmak şarttır ve hem Xero'yu hem de yazılım mimarisini anlayan deneyimli bir ekiple çalışmak, finansal verilerinizin doğru ve güvende kalmasını sağlar.

xerolaravelxero entegrasyonumuhasebe entegrasyonuweb geliştirme

Bu yazıyı paylaş

Benzer Yazılar

Benzer yazı bulunamadı.

İlgili Hizmetimiz

Laravel Yazılım Hizmetimiz

Bu konuda profesyonel destek almak ister misiniz?

Hizmeti İncele

İletişim Formu

Bu form üzerinden tarafımıza ulaşabilirsiniz

© 2024-2026 Codefacture Yazılım A.Ş. Tüm Hakları Saklıdır
Hızlı Teklif

Ortalama Yanıt Süresi: 15 Dakika