v1
This commit is contained in:
@@ -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
|
||||||
@@ -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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user