Passed
Push — main ( 7dc7cf...2fdc69 )
by Pranjal
02:49
created

Database::exec()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace Scrawler\Arca;
5
use Doctrine\DBAL\Types\Type;
6
use Dunglas\DoctrineJsonOdm\Serializer;
7
use Dunglas\DoctrineJsonOdm\Type\JsonDocumentType;
8
use Symfony\Component\Serializer\Encoder\JsonEncoder;
9
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
10
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
11
use Symfony\Component\Serializer\Normalizer\UidNormalizer;
12
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
13
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
14
15
16
/**
17
 * 
18
 * Class that manages all interaction with database
19
 */
20
class Database
21
{
22
    /**
23
     * Store the instance of current connection
24
     * @var \Scrawler\Arca\Connection
25
     */
26
    private Connection $connection;
27
    /**
28
     * When $isFrozen is set to true tables are not updated/created
29
     * @var bool
30
     */
31
    private bool $isFroozen = false;
32
    
33
34
    /**
35
     * Create a new Database instance
36
     * @param \Scrawler\Arca\Connection $connection
37
     */
38
    public function __construct(Connection $connection)
39
    {
40
        $this->connection = $connection;
41
        $this->registerEvents();
42
        $this->registerJsonDocumentType();
43
44
    }
45
46
    private function registerJsonDocumentType(): void
47
    {
48
        if (!Type::hasType('json_document')) {
49
            Type::addType('json_document', JsonDocumentType::class);
50
            //@phpstan-ignore-next-line
51
            Type::getType('json_document')->setSerializer(
0 ignored issues
show
Bug introduced by
The method setSerializer() does not exist on Doctrine\DBAL\Types\Type. It seems like you code against a sub-type of Doctrine\DBAL\Types\Type such as Dunglas\DoctrineJsonOdm\Type\JsonDocumentType. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

51
            Type::getType('json_document')->/** @scrutinizer ignore-call */ setSerializer(
Loading history...
52
                new Serializer([new BackedEnumNormalizer(), new UidNormalizer(), new DateTimeNormalizer(), new ArrayDenormalizer(), new ObjectNormalizer()], [new JsonEncoder()])
53
            );
54
        }
55
    }
56
57
    /**
58
     * Register events
59
     * @return void
60
     */
61
    public function registerEvents(): void
62
    {
63
        Event::subscribeTo('__arca.model.save.'.$this->connection->getConnectionId(), function ($model) {
64
            return $this->save($model);
65
        });
66
        Event::subscribeTo('__arca.model.delete.'.$this->connection->getConnectionId(), function ($model) {
67
            return $this->delete($model);
68
        });
69
    }
70
71
    /**
72
     * Executes an SQL query and returns the number of row affected
73
     *
74
     * @param string $sql
75
     * @param array<mixed> $params
76
     * @return int|numeric-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment int|numeric-string at position 2 could not be parsed: Unknown type name 'numeric-string' at position 2 in int|numeric-string.
Loading history...
77
     */
78
    public function exec(string $sql, array $params=array()): int|string
79
    {
80
        return  $this->connection->executeStatement($sql, $params);
0 ignored issues
show
Bug introduced by
The method executeStatement() does not exist on Scrawler\Arca\Connection. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

80
        return  $this->connection->/** @scrutinizer ignore-call */ executeStatement($sql, $params);
Loading history...
81
    }
82
83
    /**
84
     * Returns array of data from SQL select statement
85
     *
86
     * @param string $sql
87
     * @param array<mixed> $params
88
     * @return array<int,array<string,mixed>>
89
     */
90
    public function getAll(string $sql, array $params=[]): array
91
    {
92
        return  $this->connection->executeQuery($sql, $params)->fetchAllAssociative();
0 ignored issues
show
Bug introduced by
The method executeQuery() does not exist on Scrawler\Arca\Connection. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

92
        return  $this->connection->/** @scrutinizer ignore-call */ executeQuery($sql, $params)->fetchAllAssociative();
Loading history...
93
    }
94
95
    /**
96
     * Creates model from name
97
     *
98
     * @param string $name
99
     * @return \Scrawler\Arca\Model
100
     */
101
    public function create(string $name) : Model
102
    {
103
        return $this->connection->getModelManager()->create($name);
104
    }
105
106
    /**
107
     * Save model into database
108
     *
109
     * @param \Scrawler\Arca\Model $model
110
     * @return mixed returns int for id and string for uuid
111
     */
112
    public function save(\Scrawler\Arca\Model $model) : mixed
113
    {
114
        if ($model->hasForeign('oto')) {
115
            $this->saveForeignOto($model);
116
        }
117
        
118
        $this->createTables($model);
119
        $this->connection->beginTransaction();
0 ignored issues
show
Bug introduced by
The method beginTransaction() does not exist on Scrawler\Arca\Connection. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

119
        $this->connection->/** @scrutinizer ignore-call */ 
120
                           beginTransaction();
Loading history...
120
121
        try {
122
            $id = $this->createRecords($model);
123
            $model->set('id',$id);
124
            $this->connection->commit();
0 ignored issues
show
Bug introduced by
The method commit() does not exist on Scrawler\Arca\Connection. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

124
            $this->connection->/** @scrutinizer ignore-call */ 
125
                               commit();
Loading history...
125
        } catch (\Exception $e) {
126
            $this->connection->rollBack();
0 ignored issues
show
Bug introduced by
The method rollBack() does not exist on Scrawler\Arca\Connection. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

126
            $this->connection->/** @scrutinizer ignore-call */ 
127
                               rollBack();
Loading history...
127
            throw $e;
128
        }
129
        
130
        if ($model->hasForeign('otm')) {
131
            $this->saveForeignOtm($model, $id);
132
        }
133
134
        if ($model->hasForeign('mtm')) {
135
            $this->saveForeignMtm($model, $id);
136
        }
137
138
        $model->cleanModel();
139
        $model->setLoaded();
140
141
        return $id;
142
    }
143
144
    /**
145
     * Create tables
146
     * @param \Scrawler\Arca\Model $model
147
     * @return void
148
     */
149
    private function createTables($model): void
150
    {
151
        if (!$this->isFroozen) {
152
            $table = $this->connection->getTableManager()->createTable($model);
153
            $this->connection->getTableManager()->saveOrUpdateTable($model->getName(), $table);
154
        }
155
    }
156
157
    /**
158
     * Create records
159
     * @param \Scrawler\Arca\Model $model
160
     * @return mixed
161
     */
162
    private function createRecords(Model $model) : mixed
163
    {
164
165
        if ($model->isLoaded()) {
166
            return $this->connection->getRecordManager()->update($model);
167
        }
168
169
        if($model->hasIdError()){
170
            throw new Exception\InvalidIdException();
171
        }
172
173
        return $this->connection->getRecordManager()->insert($model);
174
    }
175
176
177
    /**
178
     * Save One to One related model into database
179
     * @param Model $model
180
     * @return void
181
     */
182
    private function saveForeignOto(\Scrawler\Arca\Model $model): void
183
    {
184
        foreach ($model->getForeignModels('oto') as $foreign) {
185
            $this->createTables($foreign);
186
        }
187
188
        $this->connection->beginTransaction();
189
        try {
190
            foreach ($model->getForeignModels('oto') as $foreign) {
191
                $id = $this->createRecords($foreign);
192
                $foreign->cleanModel();
193
                $foreign->setLoaded();
194
                $name = $foreign->getName().'_id';
195
                $model->$name = $id;
196
            }
197
            $this->connection->commit();
198
        } catch (\Exception $e) {
199
            $this->connection->rollBack();
200
            throw $e;
201
        }
202
    }
203
204
205
    /**
206
     * Save One to Many related model into database
207
     * @param Model $model
208
     * @param mixed $id
209
     * @return void
210
     */
211
    private function saveForeignOtm(\Scrawler\Arca\Model $model, mixed $id): void
212
    {
213
        foreach ($model->getForeignModels('otm') as $foreign) {
214
                $key = $model->getName().'_id';
215
                $foreign->$key = $id;
216
                $this->createTables($foreign);
217
        }
218
        $this->connection->beginTransaction();
219
        try {
220
            foreach ($model->getForeignModels('otm') as $foreign) {
221
                    $this->createRecords($foreign);
222
                    $foreign->cleanModel();
223
                    $foreign->setLoaded();
224
            }
225
            $this->connection->commit();
226
        } catch (\Exception $e) {
227
            $this->connection->rollBack();
228
            throw $e;
229
        }
230
    }
231
232
233
    /**
234
     * Save Many to Many related model into database
235
     * @param Model $model
236
     * @param mixed $id
237
     * @return void
238
     */
239
    private function saveForeignMtm(\Scrawler\Arca\Model $model, mixed $id): void
240
    {
241
        foreach ($model->getForeignModels('mtm') as $foreign) {
242
                $model_id = $model->getName().'_id';
243
                $foreign_id = $foreign->getName().'_id';
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Traversable. It seems like you code against a sub-type of Traversable such as SimpleXMLElement or Scrawler\Arca\Model or Pest\Arch\Layer or RectorPrefix202312\Nette\Utils\Html or RectorPrefix202312\Nette\Utils\ArrayList or PHPMD\RuleSet or PHPUnit\Architecture\Elements\Layer\Layer or SimpleXMLElement or RectorPrefix202312\Nette\Iterators\CachingIterator or SimpleXMLElement or SimpleXMLIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

243
                $foreign_id = $foreign->/** @scrutinizer ignore-call */ getName().'_id';
Loading history...
244
                $relational_table = $this->create($model->getName().'_'.$foreign->getName());
245
                if ($this->isUsingUUID()) {
246
                    $relational_table->$model_id = "";
247
                    $relational_table->$foreign_id = "";
248
                } else {
249
                    $relational_table->$model_id = 0;
250
                    $relational_table->$foreign_id = 0;
251
                }
252
                $this->createTables($relational_table);
253
                $this->createTables($foreign);
254
        }
255
        $this->connection->beginTransaction();
256
        try {
257
            foreach ($model->getForeignModels('mtm') as $foreign) {
258
                    $rel_id = $this->createRecords($foreign);
259
                    $foreign->cleanModel();
260
                    $foreign->setLoaded();
261
                    $model_id = $model->getName().'_id';
262
                    $foreign_id = $foreign->getName().'_id';
263
                    $relational_table = $this->create($model->getName().'_'.$foreign->getName());
264
                    $relational_table->$model_id = $id;
265
                    $relational_table->$foreign_id = $rel_id;
266
                    $this->createRecords($relational_table);
267
            }
268
            $this->connection->commit();
269
        } catch (\Exception $e) {
270
            $this->connection->rollBack();
271
            throw $e;
272
        }
273
    }
274
275
    /**
276
     * Delete record from database
277
     *
278
     * @param \Scrawler\Arca\Model $model
279
     * @return mixed
280
     */
281
    public function delete(\Scrawler\Arca\Model $model) : mixed
282
    {
283
        return $this->connection->getRecordManager()->delete($model);
284
    }
285
286
    /**
287
     * Get collection of all records from table
288
     * @param string $table
289
     * @return Collection
290
     */
291
    public function get(string $table) : Collection
292
    {
293
       
294
        return $this->connection->getRecordManager()->getAll($table);
295
    }
296
297
    /**
298
     * Get single record
299
     * @param string $table
300
     * @param mixed $id
301
     * @return Model
302
     */
303
    public function getOne(string $table, mixed $id) : Model|null
304
    {
305
        return $this->connection->getRecordManager()->getById($table, $id);
306
    }
307
308
    /**
309
     * Returns QueryBuilder to build query for finding data
310
     * Eg: db()->find('user')->where('active = 1')->get();
311
     *
312
     * @param string $name
313
     * @return QueryBuilder
314
     */
315
    public function find(string $name) : QueryBuilder
316
    {
317
        return $this->connection->getRecordManager()->find($name);
318
    }
319
320
    /**
321
     * Freezes table for production
322
     * @return void
323
     */
324
    public function freeze() : void
325
    {
326
        $this->isFroozen = true;
327
    }
328
329
    /**
330
     * Helper function to unfreeze table
331
     * @return void
332
     */
333
    public function unfreeze() : void
334
    {
335
        $this->isFroozen = false;
336
    }
337
338
    /**
339
     * Checks if database is currently using uuid rather than id
340
     * @return bool
341
     */
342
    public function isUsingUUID() : bool
343
    {
344
        return $this->connection->isUsingUUID();
345
    }
346
347
    /**
348
     * Returns the current connection
349
     * @return Connection
350
     */
351
    public function getConnection() : Connection
352
    {
353
        return $this->connection;
354
    }
355
356
    /**
357
     * Check if tables exist
358
     * @param array<int,string> $tables
359
     * @return bool
360
     */
361
    public function tablesExist(array $tables) : bool
362
    {
363
364
        return $this->connection->getSchemaManager()->tablesExist($tables);
365
    }
366
367
    /**
368
     * Check if table exists
369
     * @param string $table
370
     * @return bool
371
     */
372
    public function tableExists(string $table) : bool
373
    {
374
      
375
        return $this->connection->getSchemaManager()->tableExists($table);
376
    }
377
}
378