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