eamirgh / torob-laravel
Torob Products and Orders Sync integration for Laravel
Requires
- php: ^8.2
- ext-json: *
- ext-sodium: *
- guzzlehttp/guzzle: ^7.2
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-30 19:27:03 UTC
README
A Laravel package for integrating with Torob's Product and Order Sync systems, including:
- Product API v3: Inbound product synchronization endpoint with JWT EdDSA authentication (page & cursor pagination).
- Order Tracking API v1: Inbound order attribution endpoint with JWT validation.
- Action Tracking API v1: Inbound action attribution endpoint.
- Click ID (
torob_clid) Capture: Middleware and helpers to store and retrievetorob_clidacross a 7-day attribution window. - Product Update Webhook: Outbound notification client for instant product updates.
- Shop API: Outbound client to manage shop products, activation, clicks, price reports, and charge transfers.
- Shop Generator Connection API: Outbound client for platforms connecting merchant shops to Torob.
Requirements
- PHP
^8.2(including PHP 8.3 & 8.4) - Extensions:
ext-sodium,ext-json(built into PHP) - Laravel
^11.0,^12.0, or^13.0
Installation
Install the package via Composer:
composer require eamirgh/torob-laravel
Publish configuration and database migration:
php artisan vendor:publish --tag="torob-config" php artisan vendor:publish --tag="torob-migrations" php artisan migrate
Configuration
In config/torob.php (or your .env file):
# Torob Public Key for verifying inbound JWTs (Ed25519) TOROB_PUBLIC_KEY=MCowBQYDK2VwAyEAt6Mu4T0pBORY11W+QeM35UsmLO3vsf+6yKpFDEImFk0= # Expected audience claim in JWT (defaults to request host) TOROB_AUDIENCE=your-domain.com # Feature toggles TOROB_ORDER_TRACKING_ENABLED=true TOROB_ACTION_TRACKING_ENABLED=true # Model mappings for automatic Eloquent provider binding (optional) TOROB_PRODUCT_MODEL="App\Models\Product" TOROB_ORDER_MODEL="App\Models\Order" TOROB_ACTION_MODEL="App\Models\UserAction" # Outbound tokens TOROB_WEBHOOK_TOKEN=your_webhook_token TOROB_SHOP_TOKEN=your_shop_token TOROB_SHOP_GENERATOR_TOKEN=your_partner_token TOROB_SHOP_DOMAIN=yourshop.ir
Model Interface Integration
The cleanest way to integrate is having your Eloquent models implement the package interfaces. The package provides traits with sensible defaults.
Product Model (TorobProductContract)
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Torob\Laravel\Contracts\TorobProductContract; use Torob\Laravel\Traits\HasTorobProduct; class Product extends Model implements TorobProductContract { use HasTorobProduct; // Optional: override any getter to customize mapping // public function getTorobCurrentPrice(): int { ... } }
Then in .env:
TOROB_PRODUCT_MODEL="App\Models\Product"
The package automatically binds the built-in EloquentProductProvider — no custom provider code needed!
Order Model (TorobOrderContract)
namespace App\Models; use Illuminate\Database\Eloquent\Model; use Torob\Laravel\Contracts\TorobOrderContract; use Torob\Laravel\Traits\HasTorobOrder; class Order extends Model implements TorobOrderContract { use HasTorobOrder; }
In .env:
TOROB_ORDER_MODEL="App\Models\Order"
Custom Providers (Advanced)
If you need custom query logic instead of the built-in Eloquent providers, implement the provider contracts directly:
1. Implement Product Provider
Create a class implementing Torob\Laravel\Contracts\TorobProductProviderContract:
namespace App\Services\Torob; use Torob\Laravel\Contracts\TorobProductProviderContract; use Torob\Laravel\Contracts\ProductPageResult; use Torob\Laravel\Contracts\ProductCursorResult; use Torob\Laravel\DTO\TorobProduct; use App\Models\Product; class TorobProductProvider implements TorobProductProviderContract { public function getByUrls(array $urls): array { return Product::whereIn('url', $urls)->get()->map(fn ($p) => $this->map($p))->all(); } public function getByUniques(array $uniques): array { return Product::whereIn('id', $uniques)->get()->map(fn ($p) => $this->map($p))->all(); } public function getPaginated(int $page, string $sort): ProductPageResult { $query = Product::query(); // apply sort: date_added_desc, date_updated_desc, product_id_desc $paginator = $query->paginate(100, ['*'], 'page', $page); return new ProductPageResult( products: $paginator->getCollection()->map(fn ($p) => $this->map($p))->all(), total: $paginator->total(), currentPage: $paginator->currentPage(), maxPages: $paginator->lastPage() ); } public function getCursor(?string $cursor, string $sort): ProductCursorResult { // Cursor pagination with product_id_desc $products = Product::where('id', '<', $cursor ?? PHP_INT_MAX) ->orderBy('id', 'desc') ->limit(100) ->get(); $nextCursor = $products->count() === 100 ? (string) $products->last()->id : null; return new ProductCursorResult( products: $products->map(fn ($p) => $this->map($p))->all(), nextCursor: $nextCursor ); } private function map(Product $p): TorobProduct { return new TorobProduct( pageUnique: (string) $p->id, pageUrl: url("/products/{$p->slug}"), title: $p->name, currentPrice: $p->price_toman, availability: $p->is_in_stock, imageLinks: [$p->image_url], dateAdded: $p->created_at->toIso8601String(), spec: $p->specs ?? [] ); } }
Bind it in your AppServiceProvider:
use Torob\Laravel\Contracts\TorobProductProviderContract; use App\Services\Torob\TorobProductProvider; public function register(): void { $this->app->bind(TorobProductProviderContract::class, TorobProductProvider::class); }
2. Implement Order Tracking Provider
namespace App\Services\Torob; use Torob\Laravel\Contracts\TorobOrderProviderContract; use Torob\Laravel\DTO\TorobOrder; use Torob\Laravel\DTO\TorobOrderItem; use App\Models\Order; class TorobOrderProvider implements TorobOrderProviderContract { public function isOrderTrackingEnabled(): bool { return true; } public function getOrders(string $purchaseTimestampGt, int $limit): array { return Order::whereNotNull('torob_clid') ->where('created_at', '>', $purchaseTimestampGt) ->orderBy('created_at', 'asc') ->limit($limit) ->get() ->map(function ($order) { return new TorobOrder( purchaseTimestamp: $order->created_at->toIso8601ZuluString(), lastUpdatedTimestamp: $order->updated_at->toIso8601ZuluString(), torobClid: $order->torob_clid, status: $order->status === 'cancelled' ? 'cancelled' : 'completed', orderValue: $order->total_toman, products: $order->items->map(fn ($item) => new TorobOrderItem( productUrl: url("/products/{$item->product->slug}"), productPrice: $item->unit_price_toman, quantity: $item->quantity ))->all() ); }) ->all(); } }
Bind in AppServiceProvider:
$this->app->bind( \Torob\Laravel\Contracts\TorobOrderProviderContract::class, \App\Services\Torob\TorobOrderProvider::class );
3. Click ID Attribution (torob_clid) & Database Migration
Add torob_clid to your orders table using either:
Option A: Publish and run the package migration:
php artisan vendor:publish --tag="torob-migrations"
php artisan migrate
(Configurable target table in config/torob.php: 'tables.orders' => 'orders')
Option B: Use the Schema Blueprint macro in any custom migration:
Schema::table('orders', function (Blueprint $table) { $table->torobClid(); // creates indexed nullable 'torob_clid' column });
Add the middleware to your web middleware group in bootstrap/app.php:
$middleware->web(append: [ \Torob\Laravel\Http\Middleware\CaptureTorobClid::class, ]);
When an order is created, save the torob_clid:
use Torob\Laravel\Facades\Torob; $order->torob_clid = Torob::getClid($request); $order->save();
Outbound API Clients
Product Webhook Client
Notify Torob immediately when a product changes:
use Torob\Laravel\Facades\Torob; // Single product Torob::webhook()->notifyProduct( pageUrl: 'https://domain.com/product/34/', pageUnique: '12412_1' ); // Batch of products Torob::webhook()->notify([ ['page_url' => 'https://domain.com/product/34/', 'page_unique' => '12412_1'], ['page_url' => 'https://domain.com/product/35/', 'page_unique' => '12412_2'], ]);
Shop API Client
use Torob\Laravel\Facades\Torob; // Get shop summary $shop = Torob::shop()->getShop(); // Toggle products Torob::shop()->activateProducts(['12412_1', '12412_2']); Torob::shop()->deactivateProducts(['12412_3']); // Fetch clicks $clicks = Torob::shop()->getClicks(date: '2026-10-01'); // Fetch price reports and answer $reports = Torob::shop()->getPriceReports(); Torob::shop()->answerPriceReports([4521], 'claimed_to_be_corrected', 'Price updated'); // Transfer balance $preview = Torob::shop()->getChargeTransferPreview('other.ir'); $transfer = Torob::shop()->transferCharge('other.ir', 10000, 'req-uuid');
Shop Generator Connection API
For shop builders / multi-tenant platforms:
use Torob\Laravel\Facades\Torob; // 1. Request connection $conn = Torob::shopGenerator()->requestConnection('merchant.ir', 'https://builder.ir/torob/callback'); return redirect($conn['approve_url']); // 2. Handle callback $callback = Torob::shopGenerator()->parseCallback($request); if ($callback['approved']) { Torob::shopGenerator()->confirmConnection($callback['connection_id'], $callback['code']); } // 3. Disconnect Torob::shopGenerator()->disconnect($connectionId);
Documentation & Recipes
Full documentation and interactive recipes are hosted on GitHub Pages: https://eamirgh.github.io/torob-laravel/
- Complete E-Commerce Store Recipe: Real-world ProductVariant model with specs, Order checkout with
torob_clid, and TorobPay gateway handling. - Queued Real-Time Webhooks: Eloquent Observers + Laravel Queue Jobs for rate-limited product change notifications.
- Multi-Tenant Shop Generator Platform: Dynamic tenant routing, per-merchant tracking toggles, and multi-tenant Shop API calls.
Run docs locally:
npm run docs:dev
Build static documentation:
npm run docs:build
Testing
Run tests with PHPUnit:
composer test