1: <?php declare(strict_types=1);
2:
3: namespace Salient\Db;
4:
5: use Salient\Contract\Core\Entity\Readable;
6: use Salient\Core\Concern\ReadsProtectedProperties;
7: use Salient\Utility\Env;
8: use Salient\Utility\Format;
9: use Salient\Utility\Get;
10: use Salient\Utility\Reflect;
11: use ADOConnection;
12: use RuntimeException;
13: use UnexpectedValueException;
14:
15: /**
16: * Creates connections to databases
17: *
18: * @property-read string $Name
19: * @property-read DbDriver::* $Driver
20: * @property-read string|null $Dsn
21: * @property-read string|null $Hostname
22: * @property-read int|null $Port
23: * @property-read string|null $Username
24: * @property-read string|null $Password
25: * @property-read string|null $Database
26: * @property-read string|null $Schema
27: */
28: final class DbConnector implements Readable
29: {
30: use ReadsProtectedProperties;
31:
32: /** @var string */
33: protected $Name;
34: /** @var DbDriver::* */
35: protected $Driver;
36: /** @var string|null */
37: protected $Dsn;
38: /** @var string|null */
39: protected $Hostname;
40: /** @var int|null */
41: protected $Port;
42: /** @var string|null */
43: protected $Username;
44: /** @var string|null */
45: protected $Password;
46: /** @var string|null */
47: protected $Database;
48: /** @var string|null */
49: protected $Schema;
50: /** @var string */
51: private $AdodbDriver;
52:
53: /**
54: * Creates a new DbConnector object
55: *
56: * @param string $name The connection name used in the following environment
57: * variables:
58: * - `<name>_driver`: ignored if `$driver` is set, otherwise required
59: * - `<name>_dsn`: if set, other values may be ignored
60: * - `<name>_hostname`
61: * - `<name>_port`: do not set if `<name>_hostname` specifies a port number
62: * - `<name>_username`
63: * - `<name>_password`
64: * - `<name>_database`
65: * - `<name>_schema`
66: * @param DbDriver::*|null $driver If `null`, the environment variable
67: * `<name>_driver` is used.
68: */
69: public function __construct(string $name, ?int $driver = null)
70: {
71: $driver ??= Get::arrayKey(Env::get("{$name}_driver"));
72: /** @var (int&DbDriver::*)|string $driver */
73: if (is_string($driver)) {
74: /** @var (int&DbDriver::*) */
75: $driver = Reflect::getConstantValue(DbDriver::class, $driver, true);
76: }
77:
78: $this->Name = $name;
79: $this->Driver = $driver;
80: $this->Dsn = Env::getNullable("{$name}_dsn", null);
81: $this->Hostname = Env::getNullable("{$name}_hostname", null);
82: $this->Port = Env::getNullableInt("{$name}_port", null);
83: $this->Username = Env::getNullable("{$name}_username", null);
84: $this->Password = Env::getNullable("{$name}_password", null);
85: $this->Database = Env::getNullable("{$name}_database", null);
86: $this->Schema = Env::getNullable("{$name}_schema", null);
87:
88: $this->AdodbDriver = DbDriver::toAdodbDriver($driver);
89: }
90:
91: /**
92: * @param array<string,string|int|bool|null> $attributes
93: */
94: private function getConnectionString(array $attributes, bool $enclose = true): string
95: {
96: $parts = [];
97: foreach ($attributes as $keyword => $value) {
98: if (is_bool($value)) {
99: $value = Format::bool($value);
100: } else {
101: $value = (string) $value;
102: }
103: if (($enclose && strpos($value, '}') !== false)
104: || (!$enclose && strpos($value, ';') !== false)) {
105: throw new UnexpectedValueException(sprintf(
106: 'Illegal character in attribute: %s',
107: $keyword,
108: ));
109: }
110: $parts[] = sprintf(
111: $enclose ? '%s={%s}' : '%s=%s',
112: $keyword,
113: $value,
114: );
115: }
116:
117: return implode(';', $parts);
118: }
119:
120: public function getConnection(int $timeout = 15): ADOConnection
121: {
122: $db = $this->throwOnFailure(
123: ADONewConnection($this->AdodbDriver),
124: 'Error connecting to database',
125: );
126:
127: $db->SetFetchMode(ADODB_FETCH_ASSOC);
128:
129: switch ($this->Driver) {
130: case DbDriver::DB2:
131: if (!Env::has('DB2CODEPAGE')) {
132: // 1208 = UTF-8 encoding of Unicode
133: Env::set('DB2CODEPAGE', '1208');
134: }
135:
136: if ($this->Dsn !== null) {
137: $db->Connect($this->Dsn);
138: } else {
139: $db->Connect(
140: $this->getConnectionString([
141: 'driver' => Env::get('odbc_db2_driver', 'Db2'),
142: 'hostname' => $this->Hostname,
143: 'protocol' => 'tcpip',
144: 'port' => $this->Port,
145: 'database' => $this->Database,
146: 'uid' => $this->Username,
147: 'pwd' => $this->Password,
148: 'connecttimeout' => $timeout,
149: ], false)
150: );
151: }
152:
153: if ($this->Schema !== null) {
154: $db->Execute(
155: 'SET SCHEMA = ' . $db->Param('schema'),
156: ['schema' => $this->Schema]
157: );
158: }
159: break;
160:
161: case DbDriver::MSSQL:
162: $db->setConnectionParameter('CharacterSet', 'UTF-8');
163: $db->setConnectionParameter(
164: 'TrustServerCertificate',
165: Env::getBool('mssql_trust_server_certificate', false),
166: );
167: $db->setConnectionParameter('LoginTimeout', $timeout);
168: // No break
169: default:
170: $db->Connect(
171: (string) $this->Hostname,
172: (string) $this->Username,
173: (string) $this->Password,
174: (string) $this->Database,
175: );
176: break;
177: }
178:
179: return $db;
180: }
181:
182: /**
183: * @template T
184: *
185: * @param T $result
186: * @param string|int|float ...$args
187: * @return (T is false ? never : T)
188: * @phpstan-param T|false $result
189: * @phpstan-return ($result is false ? never : T)
190: */
191: private static function throwOnFailure($result, string $message, ...$args)
192: {
193: if ($result === false) {
194: throw new RuntimeException(sprintf($message, ...$args));
195: }
196:
197: return $result;
198: }
199: }
200: