Repository::queryRowByFields()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpCfdi\SatCatalogos;
6
7
use LogicException;
8
use PDO;
9
use PDOException;
10
use PDOStatement;
11
use PhpCfdi\SatCatalogos\Exceptions\SatCatalogosLogicException;
12
use PhpCfdi\SatCatalogos\Exceptions\SatCatalogosNotFoundException;
13
14
class Repository
15
{
16
    /** @var PDO */
17
    private $pdo;
18
19
    /** @var array<string, PDOStatement> */
20
    private $statements = [];
21
22
    public const CFDI_ADUANAS = 'cfdi_aduanas';
23
24
    public const CFDI_CLAVES_UNIDADES = 'cfdi_claves_unidades';
25
26
    public const CFDI_CODIGOS_POSTALES = 'cfdi_codigos_postales';
27
28
    public const CFDI_FORMAS_PAGO = 'cfdi_formas_pago';
29
30
    public const CFDI_IMPUESTOS = 'cfdi_impuestos';
31
32
    public const CFDI_METODOS_PAGO = 'cfdi_metodos_pago';
33
34
    public const CFDI_MONEDAS = 'cfdi_monedas';
35
36
    public const CFDI_NUMEROS_PEDIMENTO_ADUANA = 'cfdi_numeros_pedimento_aduana';
37
38
    public const CFDI_PAISES = 'cfdi_paises';
39
40
    public const CFDI_PATENTES_ADUANALES = 'cfdi_patentes_aduanales';
41
42
    public const CFDI_PRODUCTOS_SERVICIOS = 'cfdi_productos_servicios';
43
44
    public const CFDI_REGIMENES_FISCALES = 'cfdi_regimenes_fiscales';
45
46
    public const CFDI_REGLAS_TASA_CUOTA = 'cfdi_reglas_tasa_cuota';
47
48
    public const CFDI_TIPOS_COMPROBANTES = 'cfdi_tipos_comprobantes';
49
50
    public const CFDI_TIPOS_FACTORES = 'cfdi_tipos_factores';
51
52
    public const CFDI_TIPOS_RELACIONES = 'cfdi_tipos_relaciones';
53
54
    public const CFDI_USOS_CFDI = 'cfdi_usos_cfdi';
55
56
    public const CFDI_40_ADUANAS = 'cfdi_40_aduanas';
57
58
    public const CFDI_40_CLAVES_UNIDADES = 'cfdi_40_claves_unidades';
59
60
    public const CFDI_40_CODIGOS_POSTALES = 'cfdi_40_codigos_postales';
61
62
    public const CFDI_40_COLONIAS = 'cfdi_40_colonias';
63
64
    public const CFDI_40_ESTADOS = 'cfdi_40_estados';
65
66
    public const CFDI_40_EXPORTACIONES = 'cfdi_40_exportaciones';
67
68
    public const CFDI_40_FORMAS_PAGO = 'cfdi_40_formas_pago';
69
70
    public const CFDI_40_IMPUESTOS = 'cfdi_40_impuestos';
71
72
    public const CFDI_40_LOCALIDADES = 'cfdi_40_localidades';
73
74
    public const CFDI_40_MESES = 'cfdi_40_meses';
75
76
    public const CFDI_40_METODOS_PAGO = 'cfdi_40_metodos_pago';
77
78
    public const CFDI_40_MONEDAS = 'cfdi_40_monedas';
79
80
    public const CFDI_40_MUNICIPIOS = 'cfdi_40_municipios';
81
82
    public const CFDI_40_NUMEROS_PEDIMENTO_ADUANA = 'cfdi_40_numeros_pedimento_aduana';
83
84
    public const CFDI_40_OBJETOS_IMPUESTOS = 'cfdi_40_objetos_impuestos';
85
86
    public const CFDI_40_PAISES = 'cfdi_40_paises';
87
88
    public const CFDI_40_PATENTES_ADUANALES = 'cfdi_40_patentes_aduanales';
89
90
    public const CFDI_40_PERIODICIDADES = 'cfdi_40_periodicidades';
91
92
    public const CFDI_40_PRODUCTOS_SERVICIOS = 'cfdi_40_productos_servicios';
93
94
    public const CFDI_40_REGIMENES_FISCALES = 'cfdi_40_regimenes_fiscales';
95
96
    public const CFDI_40_REGLAS_TASA_CUOTA = 'cfdi_40_reglas_tasa_cuota';
97
98
    public const CFDI_40_TIPOS_COMPROBANTES = 'cfdi_40_tipos_comprobantes';
99
100
    public const CFDI_40_TIPOS_FACTORES = 'cfdi_40_tipos_factores';
101
102
    public const CFDI_40_TIPOS_RELACIONES = 'cfdi_40_tipos_relaciones';
103
104
    public const CFDI_40_USOS_CFDI = 'cfdi_40_usos_cfdi';
105
106
    public const NOMINA_BANCOS = 'nomina_bancos';
107
108
    public const NOMINA_ESTADOS = 'nomina_estados';
109
110
    public const NOMINA_ORIGENES_RECURSOS = 'nomina_origenes_recursos';
111
112
    public const NOMINA_PERIODICIDADES_PAGOS = 'nomina_periodicidades_pagos';
113
114
    public const NOMINA_RIESGOS_PUESTOS = 'nomina_riesgos_puestos';
115
116
    public const NOMINA_TIPOS_CONTRATOS = 'nomina_tipos_contratos';
117
118
    public const NOMINA_TIPOS_DEDUCCIONES = 'nomina_tipos_deducciones';
119
120
    public const NOMINA_TIPOS_HORAS = 'nomina_tipos_horas';
121
122
    public const NOMINA_TIPOS_INCAPACIDADES = 'nomina_tipos_incapacidades';
123
124
    public const NOMINA_TIPOS_JORNADAS = 'nomina_tipos_jornadas';
125
126
    public const NOMINA_TIPOS_NOMINAS = 'nomina_tipos_nominas';
127
128
    public const NOMINA_TIPOS_OTROS_PAGOS = 'nomina_tipos_otros_pagos';
129
130
    public const NOMINA_TIPOS_PERCEPCIONES = 'nomina_tipos_percepciones';
131
132
    public const NOMINA_TIPOS_REGIMENES = 'nomina_tipos_regimenes';
133
134
    public const CATALOGS = [
135
        self::CFDI_ADUANAS,
136
        self::CFDI_CLAVES_UNIDADES,
137
        self::CFDI_CODIGOS_POSTALES,
138
        self::CFDI_FORMAS_PAGO,
139
        self::CFDI_IMPUESTOS,
140
        self::CFDI_METODOS_PAGO,
141
        self::CFDI_MONEDAS,
142
        self::CFDI_NUMEROS_PEDIMENTO_ADUANA,
143
        self::CFDI_PAISES,
144
        self::CFDI_PATENTES_ADUANALES,
145
        self::CFDI_PRODUCTOS_SERVICIOS,
146
        self::CFDI_REGIMENES_FISCALES,
147
        self::CFDI_REGLAS_TASA_CUOTA,
148
        self::CFDI_TIPOS_COMPROBANTES,
149
        self::CFDI_TIPOS_FACTORES,
150
        self::CFDI_TIPOS_RELACIONES,
151
        self::CFDI_USOS_CFDI,
152
        self::CFDI_40_ADUANAS,
153
        self::CFDI_40_CLAVES_UNIDADES,
154
        self::CFDI_40_CODIGOS_POSTALES,
155
        self::CFDI_40_COLONIAS,
156
        self::CFDI_40_ESTADOS,
157
        self::CFDI_40_EXPORTACIONES,
158
        self::CFDI_40_FORMAS_PAGO,
159
        self::CFDI_40_IMPUESTOS,
160
        self::CFDI_40_LOCALIDADES,
161
        self::CFDI_40_MESES,
162
        self::CFDI_40_METODOS_PAGO,
163
        self::CFDI_40_MONEDAS,
164
        self::CFDI_40_MUNICIPIOS,
165
        self::CFDI_40_NUMEROS_PEDIMENTO_ADUANA,
166
        self::CFDI_40_OBJETOS_IMPUESTOS,
167
        self::CFDI_40_PAISES,
168
        self::CFDI_40_PATENTES_ADUANALES,
169
        self::CFDI_40_PERIODICIDADES,
170
        self::CFDI_40_PRODUCTOS_SERVICIOS,
171
        self::CFDI_40_REGIMENES_FISCALES,
172
        self::CFDI_40_REGLAS_TASA_CUOTA,
173
        self::CFDI_40_TIPOS_COMPROBANTES,
174
        self::CFDI_40_TIPOS_FACTORES,
175
        self::CFDI_40_TIPOS_RELACIONES,
176
        self::CFDI_40_USOS_CFDI,
177
        self::NOMINA_BANCOS,
178
        self::NOMINA_ESTADOS,
179
        self::NOMINA_ORIGENES_RECURSOS,
180
        self::NOMINA_PERIODICIDADES_PAGOS,
181
        self::NOMINA_RIESGOS_PUESTOS,
182
        self::NOMINA_TIPOS_CONTRATOS,
183
        self::NOMINA_TIPOS_DEDUCCIONES,
184
        self::NOMINA_TIPOS_HORAS,
185
        self::NOMINA_TIPOS_INCAPACIDADES,
186
        self::NOMINA_TIPOS_JORNADAS,
187
        self::NOMINA_TIPOS_NOMINAS,
188
        self::NOMINA_TIPOS_OTROS_PAGOS,
189
        self::NOMINA_TIPOS_PERCEPCIONES,
190
        self::NOMINA_TIPOS_REGIMENES,
191
    ];
192
193 121
    public function __construct(PDO $pdo)
194
    {
195 121
        $this->pdo = $pdo;
196
    }
197
198
    /**
199
     * @param string $catalog
200
     * @param string $id
201
     * @return array<string, scalar>
202
     */
203 57
    public function queryById(string $catalog, string $id): array
204
    {
205 57
        $sql = 'select *'
206 57
            . ' from ' . $this->catalogName($catalog)
207 57
            . ' where (id = :id);';
208 56
        $data = $this->queryRow($sql, ['id' => $id]);
209 53
        if (! count($data)) {
210 3
            throw $this->createSatCatalogosNotFoundException($catalog, ['id' => $id]);
211
        }
212
213 50
        return $data;
214
    }
215
216
    /**
217
     * @param string $catalog
218
     * @param string[] $ids
219
     * @return array<array<string, scalar>>
220
     */
221 1
    public function queryByIds(string $catalog, array $ids): array
222
    {
223 1
        return $this->queryRowsInField($catalog, 'id', $ids);
224
    }
225
226
    /**
227
     * @param string $catalog
228
     * @param string $fieldName
229
     * @param scalar[] $values
230
     * @return array<int, array<string, scalar>>
231
     */
232 1
    public function queryRowsInField(string $catalog, string $fieldName, array $values): array
233
    {
234 1
        $values = array_values($values);
235 1
        $questionMarks = implode(',', array_fill(0, count($values), '?'));
236 1
        $sql = 'select *'
237 1
            . ' from ' . $this->catalogName($catalog)
238 1
            . ' where ' . $this->escapeName($fieldName) . ' IN (' . $questionMarks . ')'
239 1
            . ';';
240 1
        $stmt = $this->query($sql, $values);
241
        /** @var array<int, array<string, scalar>>|false $data phpstan does not know that fetchAll can return FALSE */
242 1
        $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
243
244 1
        return (is_array($data)) ? $data : [];
245
    }
246
247
    /**
248
     * @param string $catalog
249
     * @param array<string, scalar> $values
250
     * @param int $limit
251
     * @param bool $exactSearch
252
     * @return array<int, array<string, scalar>>
253
     */
254 50
    public function queryRowsByFields(string $catalog, array $values, int $limit = 0, bool $exactSearch = true): array
255
    {
256 50
        $keys = array_keys($values);
257 50
        $operator = ($exactSearch) ? '=' : 'like';
258 50
        $sql = 'select *'
259 50
            . ' from ' . $this->catalogName($catalog)
260 50
            . call_user_func(function (array $keys, string $operator): string {
261 50
                if (count($keys)) {
262 49
                    return ' where ' . implode(' and ', array_map(function ($field) use ($operator) {
263 49
                        return '(' . $this->escapeName($field) . ' ' . $operator . ' :' . $field . ')';
264 49
                    }, $keys));
265
                }
266 1
                return '';
267 50
            }, $keys, $operator)
268 50
            . (($limit > 0) ? ' limit ' . $limit : '')
269 50
            . ';';
270 50
        $stmt = $this->query($sql, $values);
271
        /** @var array<int, array<string, scalar>>|false $data phpstan does not know that fetchAll can return FALSE */
272 50
        $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
273 50
        return (is_array($data)) ? $data : [];
274
    }
275
276
    /**
277
     * @param string $catalog
278
     * @param array<string, scalar> $values
279
     * @return array<string, scalar>
280
     */
281 6
    public function queryRowByFields(string $catalog, array $values): array
282
    {
283 6
        $data = $this->queryRowsByFields($catalog, $values, 1);
284 6
        if (1 !== count($data)) {
285 3
            throw $this->createSatCatalogosNotFoundException($catalog, $values);
286
        }
287
288 3
        return $data[0];
289
    }
290
291
    /**
292
     * @param string $catalog
293
     * @param array<string, scalar> $values
294
     * @return SatCatalogosNotFoundException
295
     */
296 6
    private function createSatCatalogosNotFoundException(string $catalog, array $values): SatCatalogosNotFoundException
297
    {
298 6
        $valuesCount = count($values);
299 6
        $keys = array_keys($values);
300 6
        if ($valuesCount > 1) {
301 1
            $exMessage = sprintf(
302 1
                'Cannot found %s using (%s) with values (%s)',
303 1
                $catalog,
304 1
                implode(', ', $keys),
305 1
                implode(', ', $values),
306 1
            );
307 5
        } elseif (1 === $valuesCount) {
308 4
            $exMessage = sprintf("Cannot found %s using %s '%s'", $catalog, $keys[0], $values[$keys[0]]);
309
        } else {
310 1
            $exMessage = sprintf('Cannot found any %s without filter', $catalog);
311
        }
312
313 6
        return new SatCatalogosNotFoundException($exMessage);
314
    }
315
316 2
    public function existsId(string $catalog, string $id): bool
317
    {
318 2
        $sql = 'select count(*) '
319 2
            . ' from ' . $this->catalogName($catalog)
320 2
            . ' where (id = :id);';
321 2
        $value = $this->queryValue($sql, ['id' => $id], 0);
322 2
        return (1 === (int) $value);
323
    }
324
325 109
    public function escapeName(string $name): string
326
    {
327 109
        return '"' . str_replace('"', '""', $name) . '"';
328
    }
329
330 110
    public function catalogName(string $catalog): string
331
    {
332 110
        if (! in_array($catalog, self::CATALOGS, true)) {
333 1
            throw new SatCatalogosLogicException("The catalog name $catalog is not recognized by the repository");
334
        }
335
336 109
        return $this->escapeName($catalog);
337
    }
338
339
    /**
340
     * Execute a sql statement, it will use the preparedStatements cache, set the arguments and throw an exception
341
     * with the corresponding message (if working on silent mode)
342
     * @param string $query
343
     * @param scalar[] $arguments
344
     * @return PDOStatement
345
     */
346 109
    private function query(string $query, array $arguments = []): PDOStatement
347
    {
348 109
        $statement = $this->statement($query);
349 106
        $statement->execute($arguments);
350 106
        return $statement;
351
    }
352
353
    /**
354
     * Get one and only one value after executing a query.
355
     * NOTICE: Do not use this function for boolean values
356
     *
357
     * @param string $query
358
     * @param scalar[] $arguments
359
     * @param scalar|null $defaultValue
360
     * @return scalar|null
361
     */
362 2
    private function queryValue(string $query, array $arguments = [], $defaultValue = null)
363
    {
364 2
        $stmt = $this->query($query, $arguments);
365
        /** @var scalar|null $value phpstan does not know that fetchAll can return FALSE */
366 2
        $value = $stmt->fetchColumn(0);
367 2
        if (null === $value) {
368
            return $defaultValue;
369
        }
370 2
        return $value;
371
    }
372
373
    /**
374
     * @param string $query
375
     * @param scalar[] $arguments
376
     * @return array<string, scalar>
377
     */
378 56
    private function queryRow(string $query, array $arguments = []): array
379
    {
380 56
        $stmt = $this->query($query, $arguments);
381 53
        $values = $stmt->fetch(PDO::FETCH_ASSOC);
382
383 53
        return (is_array($values)) ? $values : [];
384
    }
385
386
    /**
387
     * Cache or create a prepared statement
388
     *
389
     * @param string $query
390
     * @return PDOStatement
391
     */
392 109
    private function statement(string $query): PDOStatement
393
    {
394 109
        $statement = $this->statements[$query] ?? null;
395 109
        if ($statement instanceof PDOStatement) {
396 10
            return $statement;
397
        }
398
399
        try {
400
            /**
401
             * @noinspection PhpUsageOfSilenceOperatorInspection
402
             * @var PDOStatement|false $statement phpstan does not know that prepare can return FALSE
403
             */
404 109
            $statement = @$this->pdo->prepare($query);
405 1
        } catch (PDOException $exception) {
406 1
            throw new LogicException("Cannot prepare the statement: $query", 0, $exception);
407
        }
408 108
        if (false === $statement) {
409 2
            throw new LogicException("Cannot prepare the statement: $query");
410
        }
411 106
        $this->statements[$query] = $statement;
412
413 106
        return $statement;
414
    }
415
}
416