Skip to content

Commit baacea1

Browse files
committed
Initial commit
1 parent 09e886f commit baacea1

35 files changed

Lines changed: 1042 additions & 0 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.idea/
2+
composer.lock
3+
vendor/
4+
.phpunit.result.cache

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "comphp/dbengine",
3+
"type": "library",
4+
"description": "Provides unified access to database engines regardless of the underlying connectivity",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "timothy.mcclatchey",
9+
"email": "timothy@commonphp.org"
10+
}
11+
],
12+
"autoload": {
13+
"psr-4": {
14+
"CommonPHP\\DatabaseEngine\\": "src/"
15+
}
16+
},
17+
"autoload-dev": {
18+
"psr-4": {
19+
"CommonPHP\\Tests\\": "tests/"
20+
}
21+
},
22+
"require": {
23+
"php": "^8.3",
24+
"psr/container": "^2.0"
25+
},
26+
"require-dev": {
27+
"phpunit/phpunit": "^10.5.9"
28+
}
29+
}

src/ConnectionManager.php

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
/**
4+
* Manages database connections and provides a unified interface for executing queries.
5+
* This class serves as a central point for managing multiple database connections,
6+
* facilitating the execution of queries and type conversion through registered type converters.
7+
*
8+
* @package CommonPHP
9+
* @subpackage DatabaseManager
10+
* @author Timothy McClatchey <timothy@commonphp.org>
11+
* @copyright 2024 CommonPHP.org
12+
* @license http://opensource.org/licenses/MIT MIT License
13+
* @noinspection PhpUnused
14+
*/
15+
16+
namespace CommonPHP\DatabaseEngine;
17+
18+
use CommonPHP\DatabaseEngine\Contracts\BuildableQueryContract;
19+
use CommonPHP\DatabaseEngine\Contracts\ConnectorContract;
20+
use CommonPHP\DatabaseEngine\Exceptions\ColumnNotFoundException;
21+
use CommonPHP\DatabaseEngine\Exceptions\ConnectionAlreadyDefinedException;
22+
use CommonPHP\DatabaseEngine\Exceptions\ConnectionNotRegisteredException;
23+
use CommonPHP\DatabaseEngine\Exceptions\DatabaseEngineException;
24+
use CommonPHP\DatabaseEngine\Exceptions\EmptyConnectionNameException;
25+
use CommonPHP\DatabaseEngine\Exceptions\ResultNotReadException;
26+
use CommonPHP\DatabaseEngine\Support\TypeConversionProvider;
27+
use CommonPHP\DatabaseEngine\TypeConverters\BoolTypeConverter;
28+
use CommonPHP\DatabaseEngine\TypeConverters\DateTimeTypeConverter;
29+
use CommonPHP\DatabaseEngine\TypeConverters\FloatTypeConverter;
30+
use CommonPHP\DatabaseEngine\TypeConverters\IntTypeConverter;
31+
use CommonPHP\DatabaseEngine\TypeConverters\MixedTypeConverter;
32+
use CommonPHP\DatabaseEngine\TypeConverters\StringTypeConverter;
33+
use DateTime;
34+
35+
final class ConnectionManager
36+
{
37+
/**
38+
* @var ConnectorContract[] Holds the registered database connections.
39+
*/
40+
private array $connections = [];
41+
42+
/**
43+
* Provides type conversion services for database values.
44+
*
45+
* @var TypeConversionProvider
46+
*/
47+
public readonly TypeConversionProvider $typeConversionProvider;
48+
49+
/**
50+
* Initializes the ConnectionManager with an optional TypeConversionProvider.
51+
* Registers default type converters if not already supported by the provided TypeConversionProvider.
52+
*
53+
* @param TypeConversionProvider|null $typeConversionProvider The type conversion provider to use. A new instance is created if null is passed.
54+
*/
55+
public function __construct(?TypeConversionProvider $typeConversionProvider = null)
56+
{
57+
if ($typeConversionProvider === null)
58+
{
59+
$typeConversionProvider = new TypeConversionProvider();
60+
}
61+
if (!$typeConversionProvider->supportsType('bool')) $typeConversionProvider->register('bool', new BoolTypeConverter());
62+
if (!$typeConversionProvider->supportsType(DateTime::class)) $typeConversionProvider->register(DateTime::class, new DateTimeTypeConverter());
63+
if (!$typeConversionProvider->supportsType('float')) $typeConversionProvider->register('float', new FloatTypeConverter());
64+
if (!$typeConversionProvider->supportsType('int')) $typeConversionProvider->register('int', new IntTypeConverter());
65+
if (!$typeConversionProvider->supportsType('mixed')) $typeConversionProvider->register('mixed', new MixedTypeConverter());
66+
if (!$typeConversionProvider->supportsType('string')) $typeConversionProvider->register('string', new StringTypeConverter());
67+
$this->typeConversionProvider = $typeConversionProvider;
68+
}
69+
70+
/**
71+
* Retrieves a registered connector by name.
72+
*
73+
* @param string $connectionName The name of the connection to retrieve.
74+
* @return ConnectorContract The requested connector.
75+
* @throws ConnectionNotRegisteredException If the connection name is not registered.
76+
*/
77+
public function with(string $connectionName): ConnectorContract
78+
{
79+
$name = trim(strtolower($connectionName));
80+
if (!isset($this->connections[$name]))
81+
{
82+
throw new ConnectionNotRegisteredException($connectionName);
83+
}
84+
return $this->connections[$name];
85+
}
86+
87+
/**
88+
* Registers a new database connection under a specified name.
89+
*
90+
* @param string $connectionName The name to register the connection under.
91+
* @param ConnectorContract $connection The connector instance to register.
92+
* @throws EmptyConnectionNameException If the provided connection name is empty.
93+
* @throws ConnectionAlreadyDefinedException If a connection with the given name is already registered.
94+
*/
95+
public function register(string $connectionName, ConnectorContract $connection): void
96+
{
97+
$name = trim(strtolower($connectionName));
98+
if ($name == '')
99+
{
100+
throw new EmptyConnectionNameException();
101+
}
102+
if (isset($this->connections[$name]))
103+
{
104+
throw new ConnectionAlreadyDefinedException($connectionName);
105+
}
106+
$this->connections[$name] = $connection;
107+
}
108+
109+
/**
110+
* Retrieves the last insert ID from the specified connection.
111+
*
112+
* @param string $connectionName The name of the connection to retrieve the last insert ID from.
113+
* @return string|int The last insert ID as a string or integer, depending on the database.
114+
* @throws ConnectionNotRegisteredException
115+
*/
116+
public function getLastInsertId(string $connectionName): string|int
117+
{
118+
return $this->with($connectionName)->getLastInsertId();
119+
}
120+
121+
/**
122+
* Executes a non-query (e.g., INSERT, UPDATE, DELETE) using the specified connection.
123+
*
124+
* @param string $connectionName The name of the connection to use for execution.
125+
* @param Query|BuildableQueryContract $query The query to execute.
126+
* @return int The number of rows affected by the query.
127+
* @throws ConnectionNotRegisteredException
128+
*/
129+
public function executeNonQuery(string $connectionName, Query|BuildableQueryContract $query): int
130+
{
131+
return $this->execute($connectionName, $query)->getRowCount();
132+
}
133+
134+
/**
135+
* Executes a query that returns a single scalar value using the specified connection.
136+
*
137+
* @template T
138+
* @param string $connectionName The name of the connection to use for execution.
139+
* @param Query|BuildableQueryContract $query The query to execute.
140+
* @param class-string<T> $expectedType The expected return type of the query result.
141+
* @return T The scalar value returned by the query, converted to the specified type.
142+
* @throws ColumnNotFoundException
143+
* @throws ConnectionNotRegisteredException
144+
* @throws DatabaseEngineException
145+
* @throws ResultNotReadException
146+
*/
147+
public function executeScalar(string $connectionName, Query|BuildableQueryContract $query, string $expectedType = 'mixed'): mixed
148+
{
149+
$scalar = null;
150+
$result = $this->execute($connectionName, $query);
151+
if ($result->read() && $result->getColumnCount() > 0)
152+
{
153+
$scalar = $result->getValue(0, $expectedType);
154+
}
155+
return $scalar;
156+
}
157+
158+
/**
159+
* Executes a query using the specified connection and returns the result set.
160+
*
161+
* @param string $connectionName The name of the connection to use for execution.
162+
* @param Query|BuildableQueryContract $query The query to execute.
163+
* @return Result The result set of the executed query.
164+
* @throws ConnectionNotRegisteredException
165+
*/
166+
public function execute(string $connectionName, Query|BuildableQueryContract $query): Result
167+
{
168+
return $this->with($connectionName)->execute($this->typeConversionProvider, $query);
169+
}
170+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Contracts;
4+
5+
use BackedEnum;
6+
use CommonPHP\DatabaseEngine\Exceptions\EnumDoesNotExistException;
7+
use CommonPHP\DatabaseEngine\Exceptions\TypeDecodingFailedException;
8+
use CommonPHP\DatabaseEngine\Exceptions\TypeEncodingFailedException;
9+
use CommonPHP\DatabaseEngine\Support\TypeConversionProvider;
10+
use UnitEnum;
11+
12+
class AbstractEnumTypeConverter implements TypeConverterContract
13+
{
14+
/** @var string|UnitEnum|BackedEnum */
15+
private string|UnitEnum|BackedEnum $enumClass;
16+
17+
public function __construct(string $enumClass)
18+
{
19+
if (!enum_exists($enumClass))
20+
{
21+
throw new EnumDoesNotExistException($enumClass);
22+
}
23+
$this->enumClass = $enumClass;
24+
}
25+
26+
#[\Override] final function encode(mixed $data): mixed
27+
{
28+
$typeName = TypeConversionProvider::getTypeOf($data);
29+
if ($typeName !== $this->enumClass)
30+
{
31+
throw new TypeEncodingFailedException($data, $this->enumClass);
32+
}
33+
return $data->name;
34+
}
35+
36+
#[\Override] final function decode(mixed $data): mixed
37+
{
38+
foreach (($this->enumClass)::cases() as $case)
39+
{
40+
if ($case->name == $data)
41+
{
42+
return $case;
43+
}
44+
}
45+
throw new TypeDecodingFailedException($data, $this->enumClass);
46+
}
47+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Contracts;
4+
5+
use CommonPHP\DatabaseEngine\Query;
6+
7+
interface BuildableQueryContract
8+
{
9+
function build(): Query;
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Contracts;
4+
5+
use CommonPHP\DatabaseEngine\Query;
6+
use CommonPHP\DatabaseEngine\Result;
7+
use CommonPHP\DatabaseEngine\Support\TypeConversionProvider;
8+
9+
interface ConnectorContract
10+
{
11+
function getLastInsertId(): string|int;
12+
function execute(TypeConversionProvider $typeConversionProvider, Query|BuildableQueryContract $query): Result;
13+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Contracts;
4+
5+
interface TypeConverterContract
6+
{
7+
function encode(mixed $data): mixed;
8+
function decode(mixed $data): mixed;
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Exceptions;
4+
5+
use Throwable;
6+
7+
class ColumnNotFoundException extends DatabaseEngineException
8+
{
9+
public function __construct(string $name, array $names, int $code = 0, ?Throwable $previous = null)
10+
{
11+
parent::__construct('The column `'.$name.'` is not defined in the result ('.implode(', ', $names).')', $code, $previous);
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Exceptions;
4+
5+
use Throwable;
6+
7+
class ConnectionAlreadyDefinedException extends DatabaseEngineException
8+
{
9+
public function __construct(string $name, int $code = 0, ?Throwable $previous = null)
10+
{
11+
parent::__construct('Could not use the connection name `'.$name.'` because it already exists', $code, $previous);
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace CommonPHP\DatabaseEngine\Exceptions;
4+
5+
use Throwable;
6+
7+
class ConnectionNotRegisteredException extends DatabaseEngineException
8+
{
9+
public function __construct(string $name, int $code = 0, ?Throwable $previous = null)
10+
{
11+
parent::__construct('There is no connection with the name `'.$name.'`', $code, $previous);
12+
}
13+
}

0 commit comments

Comments
 (0)