Passed
Push — master ( 7cdedb...e314df )
by Carlos C
57s queued 10s
created

Repository   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 278
Duplicated Lines 0 %

Test Coverage

Coverage 98.86%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 118
c 7
b 0
f 0
dl 0
loc 278
ccs 87
cts 88
cp 0.9886
rs 10
wmc 29

14 Methods

Rating   Name   Duplication   Size   Complexity  
A catalogName() 0 7 2
A query() 0 5 1
A queryRowsInField() 0 12 2
A statement() 0 22 4
A queryById() 0 11 2
A queryRow() 0 6 2
A createSatCatalogosNotFoundException() 0 18 3
A queryRowByFields() 0 8 2
A existsId() 0 7 1
A queryByIds() 0 3 1
A escapeName() 0 3 1
A __construct() 0 3 1
A queryValue() 0 6 2
A queryRowsByFields() 0 19 5
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 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_PRODUCTOS_SERVICIOS = 'cfdi_productos_servicios';
27
28
    public const CFDI_CODIGOS_POSTALES = 'cfdi_codigos_postales';
29
30
    public const CFDI_IMPUESTOS = 'cfdi_impuestos';
31
32
    public const CFDI_FORMAS_PAGO = 'cfdi_formas_pago';
33
34
    public const CFDI_METODOS_PAGO = 'cfdi_metodos_pago';
35
36
    public const CFDI_MONEDAS = 'cfdi_monedas';
37
38
    public const CFDI_PAISES = 'cfdi_paises';
39
40
    public const CFDI_REGIMENES_FISCALES = 'cfdi_regimenes_fiscales';
41
42
    public const CFDI_TIPOS_RELACIONES = 'cfdi_tipos_relaciones';
43
44
    public const CFDI_USOS_CFDI = 'cfdi_usos_cfdi';
45
46
    public const CFDI_TIPOS_FACTORES = 'cfdi_tipos_factores';
47
48
    public const CFDI_NUMEROS_PEDIMENTO_ADUANA = 'cfdi_numeros_pedimento_aduana';
49
50
    public const CFDI_REGLAS_TASA_CUOTA = 'cfdi_reglas_tasa_cuota';
51
52
    public const CFDI_PATENTES_ADUANALES = 'cfdi_patentes_aduanales';
53
54
    public const CFDI_TIPOS_COMPROBANTES = 'cfdi_tipos_comprobantes';
55
56
    public const CATALOGS = [
57
        self::CFDI_ADUANAS,
58
        self::CFDI_CLAVES_UNIDADES,
59
        self::CFDI_PRODUCTOS_SERVICIOS,
60
        self::CFDI_CODIGOS_POSTALES,
61
        self::CFDI_IMPUESTOS,
62
        self::CFDI_FORMAS_PAGO,
63
        self::CFDI_METODOS_PAGO,
64
        self::CFDI_MONEDAS,
65
        self::CFDI_PAISES,
66
        self::CFDI_REGIMENES_FISCALES,
67
        self::CFDI_TIPOS_RELACIONES,
68
        self::CFDI_USOS_CFDI,
69
        self::CFDI_TIPOS_FACTORES,
70
        self::CFDI_NUMEROS_PEDIMENTO_ADUANA,
71
        self::CFDI_REGLAS_TASA_CUOTA,
72
        self::CFDI_PATENTES_ADUANALES,
73
        self::CFDI_TIPOS_COMPROBANTES,
74
    ];
75
76 44
    public function __construct(PDO $pdo)
77
    {
78 44
        $this->pdo = $pdo;
79 44
    }
80
81
    /**
82
     * @param string $catalog
83
     * @param string $id
84
     * @return array<string, mixed>
85
     */
86 25
    public function queryById(string $catalog, string $id): array
87
    {
88
        $sql = 'select *'
89 25
            . ' from ' . $this->catalogName($catalog)
90 24
            . ' where (id = :id);';
91 24
        $data = $this->queryRow($sql, ['id' => $id]);
92 21
        if (! count($data)) {
93 3
            throw $this->createSatCatalogosNotFoundException($catalog, ['id' => $id]);
94
        }
95
96 18
        return $data;
97
    }
98
99
    /**
100
     * @param string $catalog
101
     * @param string[] $ids
102
     * @return array<array<string, mixed>>
103
     */
104 1
    public function queryByIds(string $catalog, array $ids): array
105
    {
106 1
        return $this->queryRowsInField($catalog, 'id', $ids);
107
    }
108
109
    /**
110
     * @param string $catalog
111
     * @param string $fieldName
112
     * @param mixed[] $values
113
     * @return array<int, array<string, mixed>>
114
     */
115 1
    public function queryRowsInField(string $catalog, string $fieldName, array $values): array
116
    {
117 1
        $values = array_values($values);
118 1
        $questionMarks = implode(',', array_fill(0, count($values), '?'));
119
        $sql = 'select *'
120 1
            . ' from ' . $this->catalogName($catalog)
121 1
            . ' where ' . $this->escapeName($fieldName) . ' IN (' . $questionMarks . ')'
122 1
            . ';';
123 1
        $stmt = $this->query($sql, $values);
124 1
        $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
125
126 1
        return (is_array($data)) ? $data : [];
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
127
    }
128
129
    /**
130
     * @param string $catalog
131
     * @param array<string, mixed> $values
132
     * @param int $limit
133
     * @param bool $exactSearch
134
     * @return array<int, array<string, mixed>>
135
     */
136 9
    public function queryRowsByFields(string $catalog, array $values, int $limit = 0, bool $exactSearch = true): array
137
    {
138 9
        $keys = array_keys($values);
139 9
        $operator = ($exactSearch) ? '=' : 'like';
140
        $sql = 'select *'
141 9
            . ' from ' . $this->catalogName($catalog)
142 9
            . call_user_func(function (array $keys, string $operator): string {
143 9
                if (count($keys)) {
144 9
                    return ' where ' . implode(' and ', array_map(function ($field) use ($operator) {
145 9
                        return '(' . $this->escapeName($field) . ' ' . $operator . ' :' . $field . ')';
146 9
                    }, $keys));
147
                }
148
                return '';
149 9
            }, $keys, $operator)
150 9
            . (($limit > 0) ? ' limit ' . $limit : '')
151 9
            . ';';
152 9
        $stmt = $this->query($sql, $values);
153 9
        $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
154 9
        return (is_array($data)) ? $data : [];
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
155
    }
156
157
    /**
158
     * @param string $catalog
159
     * @param array<string, mixed> $values
160
     * @return array<string, mixed>
161
     */
162 5
    public function queryRowByFields(string $catalog, array $values): array
163
    {
164 5
        $data = $this->queryRowsByFields($catalog, $values, 1);
165 5
        if (1 !== count($data)) {
166 3
            throw $this->createSatCatalogosNotFoundException($catalog, $values);
167
        }
168
169 2
        return $data[0];
170
    }
171
172
    /**
173
     * @param string $catalog
174
     * @param array<string, mixed> $values
175
     * @return SatCatalogosNotFoundException
176
     */
177 6
    private function createSatCatalogosNotFoundException(string $catalog, array $values): SatCatalogosNotFoundException
178
    {
179 6
        $valuesCount = count($values);
180 6
        $keys = array_keys($values);
181 6
        if ($valuesCount > 1) {
182 1
            $exMessage = sprintf(
183 1
                'Cannot found %s using (%s) with values (%s)',
184
                $catalog,
185 1
                implode(', ', $keys),
186 1
                implode(', ', $values)
187
            );
188 5
        } elseif (1 === $valuesCount) {
189 4
            $exMessage = sprintf('Cannot found %s using %s \'%s\'', $catalog, $keys[0], $values[$keys[0]]);
190
        } else {
191 1
            $exMessage = sprintf('Cannot found any %s without filter', $catalog);
192
        }
193
194 6
        return new SatCatalogosNotFoundException($exMessage);
195
    }
196
197 2
    public function existsId(string $catalog, string $id): bool
198
    {
199
        $sql = 'select count(*) '
200 2
            . ' from ' . $this->catalogName($catalog)
201 2
            . ' where (id = :id);';
202 2
        $value = $this->queryValue($sql, ['id' => $id], 0);
203 2
        return (1 === (int) $value);
204
    }
205
206 36
    public function escapeName(string $name): string
207
    {
208 36
        return '"' . str_replace('"', '""', $name) . '"';
209
    }
210
211 37
    public function catalogName(string $catalog): string
212
    {
213 37
        if (! in_array($catalog, self::CATALOGS, true)) {
214 1
            throw new SatCatalogosLogicException("The catalog name $catalog is not recognized by the repository");
215
        }
216
217 36
        return $this->escapeName($catalog);
218
    }
219
220
    /**
221
     * Execute a sql statement, it will use the preparedStatements cache, set the arguments and throw an exception
222
     * with the corresponding message (if working on silent mode)
223
     * @param string $query
224
     * @param mixed[] $arguments
225
     * @return PDOStatement
226
     */
227 36
    private function query(string $query, array $arguments = []): PDOStatement
228
    {
229 36
        $statement = $this->statement($query);
230 33
        $statement->execute($arguments);
231 33
        return $statement;
232
    }
233
234
    /**
235
     * Get one and only one value after executing a query.
236
     * NOTICE: Do not use this function for boolean values
237
     *
238
     * @param string $query
239
     * @param mixed[] $arguments
240
     * @param mixed $defaultValue
241
     * @return mixed
242
     */
243 2
    private function queryValue(string $query, array $arguments = [], $defaultValue = null)
244
    {
245 2
        $stmt = $this->query($query, $arguments);
246 2
        $value = $stmt->fetchColumn(0);
247
248 2
        return (false !== $value) ? $value : $defaultValue;
249
    }
250
251
    /**
252
     * @param string $query
253
     * @param mixed[] $arguments
254
     * @return array<string, mixed>
255
     */
256 24
    private function queryRow(string $query, array $arguments = []): array
257
    {
258 24
        $stmt = $this->query($query, $arguments);
259 21
        $values = $stmt->fetch(PDO::FETCH_ASSOC);
260
261 21
        return (is_array($values)) ? $values : [];
262
    }
263
264
    /**
265
     * Cache or create a prepared statement
266
     *
267
     * @param string $query
268
     * @return PDOStatement
269
     */
270 36
    private function statement(string $query): PDOStatement
271
    {
272 36
        $statement = $this->statements[$query] ?? null;
273 36
        if ($statement instanceof PDOStatement) {
274 6
            return $statement;
275
        }
276
277
        try {
278
            /**
279
             * @noinspection PhpUsageOfSilenceOperatorInspection
280
             * @var PDOStatement|false $statement phpstan does not know that prepare can return FALSE
281
             */
282 36
            $statement = @$this->pdo->prepare($query);
283 1
        } catch (PDOException $exception) {
284 1
            throw new LogicException("Cannot prepare the statement: $query", 0, $exception);
285
        }
286 35
        if (false === $statement) {
0 ignored issues
show
introduced by
The condition false === $statement is always false.
Loading history...
287 2
            throw new LogicException("Cannot prepare the statement: $query");
288
        }
289 33
        $this->statements[$query] = $statement;
290
291 33
        return $statement;
292
    }
293
}
294