uepg/laravel-sybase
Laravel Sybase adds Sybase ASE database support to Laravel, providing a driver connector and query builder so Eloquent and the DB facade work with Sybase via PDO dblib. Configure hosts, credentials, charset, and options in config/database.php.
What this package is: uepg/laravel-sybase adds a sybase connection driver to Laravel's database layer (SAP ASE / Sybase ASE) so you can use the query builder, DB::connection(), and Eloquent against legacy ASE servers without hand-rolling PDO code. Version 6.x targets modern Laravel on PHP 8.x.
Prerequisites (the part that actually blocks people):
ext-pdo_dblib plus FreeTDS (or the vendor Sybase client libraries) installed on the host/container. Confirm with php -m | grep dblib and tsql -H host -p 5000 -U user before touching Laravel config — most "the package doesn't work" reports are a FreeTDS install problem.Install and wire up:
composer require uepg/laravel-sybase:^6.2
Then add a connection block in config/database.php (keep your existing default connection untouched):
'sybase' => [
'driver' => 'sybase',
'host' => env('DB_SYBASE_HOST'),
'port' => env('DB_SYBASE_PORT', '5000'),
'database' => env('DB_SYBASE_DATABASE'),
'username' => env('DB_SYBASE_USERNAME'),
'password' => env('DB_SYBASE_PASSWORD'),
'charset' => env('DB_SYBASE_CHARSET', 'utf8'),
'appname' => env('DB_SYBASE_APPNAME', 'Laravel'),
],
Where to look first:
config/database.php — the connection array above is the whole contract.vendor/uepg/laravel-sybase/src/ — read SybaseConnector, SybaseConnection, and SybaseGrammar. The grammar file is your source of truth for which builder methods actually compile on ASE.vendor/uepg/laravel-sybase/README.md for any version-specific config keys.First use case: a read-only reporting screen against the legacy ASE box. Use the non-default connection explicitly:
$rows = DB::connection('sybase')
->table('dbo.customers')
->select(['cust_id', 'cust_name', 'created_dt'])
->limit(25)
->get();
That single call proves the extension, FreeTDS, credentials, charset, and grammar all line up. Do it before you write a single model.
Always name the connection; never make Sybase the default. Legacy ASE is almost always a secondary system. Keep MySQL/Postgres as default and reach for DB::connection('sybase') or Model::on('sybase'). This keeps migrate, RefreshDatabase, and queue/session tables working normally.
Eloquent models for legacy tables need de-Laravelizing:
class LegacyCustomer extends Model
{
protected $connection = 'sybase';
protected $table = 'dbo.customers';
protected $primaryKey = 'cust_id';
public $timestamps = false; // almost always true for legacy schemas
public $incrementing = false; // non-identity keys
protected $guarded = [];
protected $casts = ['created_dt' => 'datetime', 'balance' => 'float'];
}
If a table has a composite key, drop Eloquent and stay on the query builder — a half-working model is worse than none.
Hide the database behind a repository interface. The rest of your app should depend on CustomerRepository, with a SybaseCustomerRepository and a fake/array implementation for tests. This is the single highest-leverage pattern here: it lets you write the Laravel-side features normally and swap the legacy source later without touching controllers.
Multiple ASE servers/databases: duplicate the block with a distinct key (sybase_reporting) and pass the name through the repository constructor. There is no cross-database join across ASE and your primary DB — land data in a local staging table in batched insertOrIgnore/upsert calls, then join locally with real Laravel SQL.
Large extracts: prefer cursor(), lazy(), or chunkById() over get(). And use simplePaginate() — paginate() fires a count(*) that can be brutal on an unindexed legacy table.
Read-only by convention, not by accident. Give the Sybase login read-only grants at the database level. It prevents a future ->delete() from being a career event and documents the intent.
Testing: bind a fake repository in the container for feature tests, and gate any test that touches the real ASE behind @group sybase / markTestSkipped() so CI without FreeTDS still runs green.
The 6.2 change is an encoding fix — pin ^6.2 if you store non-ASCII text. Prior behavior could mangle accented/CJK data on both read and write (classic é instead of é, or truncation at a converted byte boundary). Upgrading from 6.0/6.1 is low risk: the changelog is a single commit, no breaking changes, no config key removals. Verify on upgrade with:
DB::connection('sybase')->select("SELECT 'Grüße — 東京' AS t");
If that round-trips, encoding is healthy.
Charset exists in two places and both must agree. The charset in config/database.php and the FreeTDS client charset in freetds.conf (or TDSVER/FREETDSCONF env). Mismatched settings cause double conversion — mojibake in, mojibake out, and it looks like a package bug. utf8 in config + UTF-8 in FreeTDS is the pairing you want.
Schema/migrations are not a reliable path. Treat ASE as an external schema owned by someone else. Manage it out-of-band; don't plan on Schema::connection('sybase')->create().
String semantics differ from MySQL: char/varchar comparison pads with spaces, so '' can behave like a single space. Call trim() on read, and never validate "empty" against a raw fetched value.
upsert / ON DUPLICATE KEY won't translate. Use updateOrInsert() or an explicit UPDATE-then-INSERT inside DB::connection('sybase')->transaction(...). Verify savepoints before nesting transactions — nested transaction support depends on the grammar.
Binds can be sent as strings by pdo_dblib, which triggers implicit-conversion errors or index-ignoring scans on numeric columns. Cast in PHP ((int) $id) before binding, or use whereRaw('cust_id = ?', [(int) $id]).
Dates: ASE datetime often arrives as a non-ISO string. Explicit $casts (or Carbon::parse() with a known format) prevents the InvalidFormatException class of bug.
Debugging toolkit:
DB::connection('sybase')->listen(fn ($q) => logger($q->sql, $q->bindings));->dump() / ->dd() on the builder for the compiled SQL — the fastest way to see how the grammar rewrote your limit/offset.TDSDUMP=/tmp/tds.log to trace the FreeTDS protocol layer when connections fail below Laravel.Extension point: if you need session options (SET ANSI_NULLS ON, quoted_identifier, lock timeouts), the clean hook is DB::extend('sybase_ro', fn ($config, $name) => ...) in a service provider that wraps the package connector and issues the setup statements post-connect. Prefer that over editing vendor code.
Connection lifetime: long-lived queue workers will hold idle ASE connections that the server may reap. Add a timeout in options and expect occasional Could not connect on the first job after idle — retry once in the repository rather than assuming a fatal error.
How can I help you explore Laravel packages today?