This commit is contained in:
Vasil Butorin
2026-07-19 21:22:39 +03:00
parent 3221e7b5f5
commit e029cc58e2
7 changed files with 524 additions and 3 deletions
+45 -3
View File
@@ -1,3 +1,45 @@
# Laravel-Firebird # Firebird connector for Laravel
Firebird connector for Laravel ## Usage
### 1. Install
Use `composer require codefort/firebird` to install the project
### 2. Register
Register `CodeFort\Firebird\FirebirdConnector` with `AppServiceProvider`
File `app\Providers\AppServiceProvider.php`:
```php
use CodeFort\Firebird\FirebirdConnector;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
FirebirdConnector::register($this->app);
...
}
}
```
### 3. Configure connection
File `config\database.php`
```php
'database' => [
'driver' => 'firebird',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', ''),
'database' => env('DB_NAME', ''),
'username' => env('DB_USER', 'SYSDBA'),
'password' => env('DB_PASS', 'masterkey'),
'charset' => 'utf8',
// 'options' => [ PDO::ATTR_AUTOCOMMIT => false ],
],
```
Settings autocommit option to false will require to start transaction explicitly even for read access
+19
View File
@@ -0,0 +1,19 @@
{
"name": "codefort/laravel-firebird",
"description": "Firebird connector for Laravel",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Vasil Butorin",
"email": "vasil.butorin@gmail.com"
}
],
"minimum-stability": "stable",
"require": {},
"autoload": {
"psr-4": {
"CodeFort\\": "src/"
}
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace CodeFort\Firebird;
use Illuminate\Database\Connection as DatabaseConnection;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionException;
class FirebirdConnection extends DatabaseConnection
{
/**
* Get the server version for the connection.
*/
public function getServerVersion(): string
{
$version = $this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION);
return Str::match('/(?<=LI-V)\d+\.\d+\.\d+/', $version);
}
/**
* Get the default query grammar instance.
*
* @throws ReflectionException
*/
protected function getDefaultQueryGrammar(): Grammar|FirebirdGrammar
{
$grammar = $this->safeConstruct(FirebirdGrammar::class, $this);
if (method_exists($grammar, 'setConnection')) {
$grammar = $grammar->setConnection($this);
}
if (method_exists($this, 'withTablePrefix')) {
return $this->withTablePrefix($grammar);
}
return $grammar;
}
/**
* Get the default post processor instance.
*/
protected function getDefaultPostProcessor(): Processor|FirebirdQueryProcessor
{
return new FirebirdQueryProcessor;
}
/**
* Get a schema builder instance for the connection.
*/
public function getSchemaBuilder(): Builder|FirebirdSchemaBuilder
{
if (is_null($this->schemaGrammar)) {
$this->useDefaultSchemaGrammar();
}
return new FirebirdSchemaBuilder($this);
}
/**
* Get the default schema grammar instance.
*
* @throws ReflectionException
*/
protected function getDefaultSchemaGrammar(): null|\Illuminate\Database\Schema\Grammars\Grammar|FirebirdSchemaGrammar
{
$grammar = $this->safeConstruct(FirebirdSchemaGrammar::class, $this);
// $grammar = new FirebirdSchemaGrammar($this);
if (method_exists($grammar, 'setConnection')) {
$grammar = $grammar->setConnection($this);
}
if (method_exists($this, 'withTablePrefix')) {
return $this->withTablePrefix($grammar);
}
return $grammar;
}
/**
* Get a new query builder instance.
*/
public function query(): FirebirdQueryBuilder|\Illuminate\Database\Query\Builder
{
return new FirebirdQueryBuilder(
$this, $this->getQueryGrammar(), $this->getPostProcessor()
);
}
/**
* Execute a stored procedure.
*/
public function executeProcedure(string $procedure, array $bindings = []): Collection
{
return $this->query()->procedure($procedure, $bindings)->get();
}
/**
* @throws ReflectionException
*/
protected function safeConstruct(string $className, mixed ...$args)
{
$ref = new ReflectionClass($className);
$ctor = $ref->getConstructor();
if ($ctor && $ctor->getNumberOfParameters() > 0) {
return $ref->newInstanceArgs($args);
}
return $ref->newInstance();
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
namespace CodeFort\Firebird;
use PDO;
use Exception;
use Illuminate\Database\Connection;
use Illuminate\Database\Connectors\Connector;
use Illuminate\Database\Connectors\ConnectorInterface;
class FirebirdConnector extends Connector implements ConnectorInterface
{
/**
* Establish a database connection.
*
* @throws Exception
*/
public function connect(array $config): PDO
{
$dsn = $this->getDsn($config);
$options = $this->getOptions($config);
return $this->createConnection($dsn, $config, $options);
}
/**
* Create a DSN string from the configuration.
*/
protected function getDsn(array $config): string
{
if (!isset($config['host']) || !isset($config['database'])) {
trigger_error('Cannot connect to Firebird Database, at least host and database required');
return '';
}
$dsn = "firebird:dbname={$config['host']}";
if ($port = ($config['port'] ?? false)) {
$dsn .= "/".$port;
}
$dsn .= ":{$config['database']};";
if (isset($config['role'])) {
$dsn .= "role={$config['role']};";
}
if (isset($config['charset'])) {
$dsn .= "charset={$config['charset']};";
}
return $dsn;
}
public static function register($app)
{
$app->bind('db.connector.firebird', fn() => new static);
Connection::resolverFor('firebird', function ($connection, $database, $tablePrefix, $config) {
return new FirebirdConnection($connection, $database, $tablePrefix, $config);
});
}
}
+221
View File
@@ -0,0 +1,221 @@
<?php
namespace CodeFort\Firebird;
use Illuminate\Support\Str;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\Grammars\Grammar;
class FirebirdGrammar extends Grammar
{
/**
* The components that make up a select clause.
*
* @var string[]
*/
protected $selectComponents = [
'aggregate',
'columns',
'from',
'joins',
'wheres',
'groups',
'havings',
'orders',
'offset',
'limit',
'lock',
];
/**
* All the available clause operators.
*
* @var array
*
* @link https://ib-aid.com/download/docs/firebird-language-reference-2.5/fblangref25-commons-predicates.html
*/
protected $operators = [
'=', '<', '>', '<=', '>=', '<>', '!=',
'!<', '!>', '~<', '~>', '^<', '^>', '~=', '^=',
'like', 'not like', 'between', 'not between',
'containing', 'not containing', 'starting with', 'not starting with',
'similar to', 'not similar to', 'is distinct from', 'is not distinct from',
];
protected function wrapValue($value)
{
return $value;
}
/**
* Compile the "select *" portion of the query.
*
* @param array $columns
*/
protected function compileColumns(Builder $query, $columns): ?string
{
// See superclass.
if (!is_null($query->aggregate)) {
return null;
}
$select = 'select ';
// Before Firebird v3, the syntax used to limit and offset rows is
// "select first [int] skip [int] * from table". Laravel's query builder
// doesn't natively support inserting components between "select" and
// the column names, so compile the limit and offset here.
if (isset($query->limit) && $usesLegacyLimitAndOffset ??= $this->usesLegacyLimitAndOffset()) {
$select .= $this->compileLegacyLimit($query, $query->limit).' ';
}
if (isset($query->offset) && $usesLegacyLimitAndOffset ??= $this->usesLegacyLimitAndOffset()) {
$select .= $this->compileLegacyOffset($query, $query->offset).' ';
}
if ($query->distinct) {
if (is_array($query->distinct)) {
throw new RuntimeException('This database engine does not support distinct on specific columns.');
}
$select .= 'distinct ';
}
return $select.$this->columnize($columns);
}
/**
* Compile the "limit" portions of the query for legacy versions of Firebird.
*/
protected function compileLegacyLimit(Builder $query, int|string $limit): string
{
return 'first '.(int) $limit;
}
/**
* Compile the "offset" portions of the query for legacy versions of Firebird.
*/
protected function compileLegacyOffset(Builder $query, int|string $offset): string
{
return 'skip '.(int) $offset;
}
/**
* Compile the random statement into SQL.
*
* @param string $seed
*/
public function compileRandom($seed): string
{
return 'rand()';
}
/**
* Wrap a union subquery in parentheses.
*
* @param string $sql
*/
protected function wrapUnion($sql): string
{
return 'select * from ('.$sql.')';
}
/**
* Determine if the database uses the legacy limit and offset syntax.
*/
public function usesLegacyLimitAndOffset(): bool
{
if (config('database.firebird.legacy_limit_and_offset', true)) return true;
return version_compare($this->connection->getServerVersion(), '3.0.0', '<');
}
/**
* Compile the "limit" portions of the query.
*
* @param int $limit
*/
protected function compileLimit(Builder $query, $limit): string
{
if ($this->usesLegacyLimitAndOffset()) {
return '';
}
return 'fetch first '.(int) $limit.' rows only';
}
/**
* Compile the "offset" portions of the query.
*
* @param int $offset
*/
protected function compileOffset(Builder $query, $offset): string
{
if ($this->usesLegacyLimitAndOffset()) {
return '';
}
return 'offset '.(int) $offset.' rows';
}
/**
* Compile a date based where clause.
*
* @param string $type
* @param array $where
*/
protected function dateBasedWhere($type, Builder $query, $where): string
{
$condition = ($type === 'date' || $type === 'time')
? 'cast('.$this->wrap($where['column']).' as '.$type.') '
: 'extract('.$type.' from '.$this->wrap($where['column']).') ';
$condition .= $where['operator'].' '.$this->parameter($where['value']);
return $condition;
}
/**
* Compile the select clause for a stored procedure.
*/
public function compileProcedure(Builder $query, string $procedure, array $values = []): string
{
$procedure = $this->wrap($procedure);
return $procedure.' ('.$this->parameterize($values).')';
}
/**
* Compile an aggregated select clause.
*
* @param array $aggregate
*/
protected function compileAggregate(Builder $query, $aggregate): string
{
// Wrap `aggregate` in double quotes to ensure the result set returns the
// column name as a lowercase string. This resolves compatibility with
// the framework's paginator.
return Str::replaceLast(
'as aggregate', 'as "aggregate"', parent::compileAggregate($query, $aggregate)
);
}
/**
* Compile a "lateral join" clause.
*/
public function compileJoinLateral($join, string $expression): string
{
return trim("{$join->type} join lateral {$expression} on true");
}
public function whereDate(Builder $query, $where): string
{
return $this->dateBasedWhere('date', $query, $where);
}
public function whereTime(Builder $query, $where): string
{
return $this->dateBasedWhere('time', $query, $where);
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace CodeFort\Firebird;
use Illuminate\Database\Query\Builder as QueryBuilder;
class FirebirdQueryBuilder extends QueryBuilder
{
/**
* Determine if any rows exist for the current query.
*/
public function exists(): bool
{
return parent::count() > 0;
}
/**
* Set the stored procedure which the query is targeting.
*
* @return QueryBuilder|FirebirdQueryBuilder
*/
public function procedure(string $procedure, array $bindings = []): QueryBuilder|static
{
$expression = $this->grammar->compileProcedure($this, $procedure, $bindings);
$this->fromRaw($expression, $this->cleanBindings($bindings));
return $this;
}
/**
* Alias to set the stored procedure which the query is targeting.
*
* @return FirebirdQueryBuilder|QueryBuilder
*
* @deprecated This method is deprecated and will be removed in a future
* release. Use the `procedure` method instead.
*/
public function fromProcedure(string $procedure, array $bindings = []): FirebirdQueryBuilder|QueryBuilder|static
{
return $this->procedure($procedure, $bindings);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace CodeFort\Firebird;
use Illuminate\Database\Query\Processors\Processor;
class FirebirdQueryProcessor extends Processor
{
/**
* Process the results of a column listing query.
*
* @deprecated Will be removed in a future Laravel version.
*
* @param array $results
*/
public function processColumnListing($results): array
{
return array_map(fn ($result) => ((object) $result)->column_name, $results);
}
}