1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync\Db;
4:
5: use Salient\Contract\Core\Provider\ProviderInterface;
6: use Salient\Contract\Sync\SyncDefinitionInterface;
7: use Salient\Contract\Sync\SyncEntityInterface;
8: use Salient\Core\Exception\MethodNotImplementedException;
9: use Salient\Core\Facade\Cache;
10: use Salient\Core\SqlQuery;
11: use Salient\Db\DbConnector;
12: use Salient\Sync\Exception\SyncEntityNotFoundException;
13: use Salient\Sync\Exception\UnreachableBackendException;
14: use Salient\Sync\AbstractSyncProvider;
15: use Salient\Utility\Arr;
16: use Salient\Utility\Get;
17: use Salient\Utility\Str;
18: use ADOConnection;
19: use ADODB_Exception;
20:
21: /**
22: * Base class for providers with traditional database backends
23: */
24: abstract class DbSyncProvider extends AbstractSyncProvider
25: {
26: private DbConnector $DbConnector;
27: private ADOConnection $Db;
28:
29: /**
30: * Specify how to connect to the backend
31: *
32: * The {@see DbConnector} returned will be cached for the lifetime of the
33: * {@see DbSyncProvider} instance.
34: */
35: abstract protected function getDbConnector(): DbConnector;
36:
37: /**
38: * @inheritDoc
39: */
40: public function getBackendIdentifier(): array
41: {
42: $connector = $this->dbConnector();
43:
44: if ($connector->Dsn !== null) {
45: /** @todo Implement DSN parsing */
46: throw new MethodNotImplementedException(
47: static::class,
48: __FUNCTION__,
49: ProviderInterface::class
50: );
51: }
52:
53: return Arr::trim([
54: Str::lower((string) $connector->Hostname),
55: (string) $connector->Port,
56: Str::lower((string) $connector->Database),
57: Str::lower((string) $connector->Schema),
58: ], null, false);
59: }
60:
61: /**
62: * @inheritDoc
63: */
64: final public function getDefinition(string $entity): SyncDefinitionInterface
65: {
66: return $this->getDbDefinition($entity);
67: }
68:
69: /**
70: * Override to implement sync operations by returning a DbSyncDefinition
71: * object for the given entity
72: *
73: * @template TEntity of SyncEntityInterface
74: *
75: * @param class-string<TEntity> $entity
76: * @return DbSyncDefinition<TEntity,$this>
77: */
78: protected function getDbDefinition(string $entity): DbSyncDefinition
79: {
80: return $this->builderFor($entity)->build();
81: }
82:
83: /**
84: * Get a new DbSyncDefinitionBuilder for an entity
85: *
86: * @template TEntity of SyncEntityInterface
87: *
88: * @param class-string<TEntity> $entity
89: * @return DbSyncDefinitionBuilder<TEntity,$this>
90: */
91: final protected function builderFor(string $entity): DbSyncDefinitionBuilder
92: {
93: return DbSyncDefinition::build()
94: ->entity($entity)
95: ->provider($this);
96: }
97:
98: /**
99: * Get a DbConnector instance to open connections to the backend
100: */
101: final public function dbConnector(): DbConnector
102: {
103: return $this->DbConnector
104: ?? ($this->DbConnector = $this->getDbConnector());
105: }
106:
107: /**
108: * Get a connection to the backend
109: */
110: final public function getDb(): ADOConnection
111: {
112: return $this->Db
113: ?? ($this->Db = $this->dbConnector()->getConnection());
114: }
115:
116: /**
117: * Get a SqlQuery instance to prepare queries for the backend
118: */
119: protected function getSqlQuery(ADOConnection $db): SqlQuery
120: {
121: return new SqlQuery(fn(string $name): string => $db->Param($name));
122: }
123:
124: /**
125: * @inheritDoc
126: */
127: public function checkHeartbeat(int $ttl = 300)
128: {
129: $key = implode(':', [
130: static::class,
131: __FUNCTION__,
132: Get::hash(implode("\0", $this->getBackendIdentifier())),
133: ]);
134:
135: if (Cache::get($key) === null) {
136: try {
137: $this->dbConnector()->getConnection(5);
138: } catch (ADODB_Exception $ex) {
139: throw new UnreachableBackendException(
140: $this,
141: $ex->getMessage(),
142: $ex,
143: );
144: }
145: Cache::set($key, true, $ttl);
146: }
147:
148: return $this;
149: }
150:
151: /**
152: * Get the first row in a recordset, or throw a SyncEntityNotFoundException
153: *
154: * @param array<array<string,mixed>> $rows The recordset retrieved from the
155: * backend.
156: * @param class-string<SyncEntityInterface> $entity The requested entity.
157: * @param int|string $id The identifier of the requested entity.
158: * @return array<string,mixed>
159: */
160: protected function first(array $rows, string $entity, $id): array
161: {
162: $row = array_shift($rows);
163: if ($row === null) {
164: throw new SyncEntityNotFoundException($this, $entity, $id);
165: }
166:
167: return $row;
168: }
169: }
170: