From e029cc58e2249b45dcec9c804f8795bc486f1692 Mon Sep 17 00:00:00 2001 From: Vasil Butorin Date: Sun, 19 Jul 2026 21:22:39 +0300 Subject: [PATCH] v1 --- README.md | 48 ++++- composer.json | 19 ++ src/Firebird/FirebirdConnection.php | 112 ++++++++++++ src/Firebird/FirebirdConnector.php | 64 +++++++ src/Firebird/FirebirdGrammar.php | 221 ++++++++++++++++++++++++ src/Firebird/FirebirdQueryBuilder.php | 43 +++++ src/Firebird/FirebirdQueryProcessor.php | 20 +++ 7 files changed, 524 insertions(+), 3 deletions(-) create mode 100644 composer.json create mode 100644 src/Firebird/FirebirdConnection.php create mode 100644 src/Firebird/FirebirdConnector.php create mode 100644 src/Firebird/FirebirdGrammar.php create mode 100644 src/Firebird/FirebirdQueryBuilder.php create mode 100644 src/Firebird/FirebirdQueryProcessor.php diff --git a/README.md b/README.md index a4fc742..4da954c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,45 @@ -# Laravel-Firebird - -Firebird connector for Laravel \ No newline at end of file +# 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 \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..55c2a7a --- /dev/null +++ b/composer.json @@ -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/" + } + } +} diff --git a/src/Firebird/FirebirdConnection.php b/src/Firebird/FirebirdConnection.php new file mode 100644 index 0000000..39747c0 --- /dev/null +++ b/src/Firebird/FirebirdConnection.php @@ -0,0 +1,112 @@ +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(); + } +} \ No newline at end of file diff --git a/src/Firebird/FirebirdConnector.php b/src/Firebird/FirebirdConnector.php new file mode 100644 index 0000000..567ccc0 --- /dev/null +++ b/src/Firebird/FirebirdConnector.php @@ -0,0 +1,64 @@ +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); + }); + } +} \ No newline at end of file diff --git a/src/Firebird/FirebirdGrammar.php b/src/Firebird/FirebirdGrammar.php new file mode 100644 index 0000000..1bd178c --- /dev/null +++ b/src/Firebird/FirebirdGrammar.php @@ -0,0 +1,221 @@ +', '<=', '>=', '<>', '!=', + '!<', '!>', '~<', '~>', '^<', '^>', '~=', '^=', + '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); + } +} \ No newline at end of file diff --git a/src/Firebird/FirebirdQueryBuilder.php b/src/Firebird/FirebirdQueryBuilder.php new file mode 100644 index 0000000..96bb37f --- /dev/null +++ b/src/Firebird/FirebirdQueryBuilder.php @@ -0,0 +1,43 @@ + 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); + } +} \ No newline at end of file diff --git a/src/Firebird/FirebirdQueryProcessor.php b/src/Firebird/FirebirdQueryProcessor.php new file mode 100644 index 0000000..61ca3d8 --- /dev/null +++ b/src/Firebird/FirebirdQueryProcessor.php @@ -0,0 +1,20 @@ + ((object) $result)->column_name, $results); + } +} \ No newline at end of file