Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
117 / 117
100.00% covered (success)
100.00%
14 / 14
CRAP
100.00% covered (success)
100.00%
1 / 1
Data
100.00% covered (success)
100.00%
117 / 117
100.00% covered (success)
100.00%
14 / 14
61
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setDivide
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDivide
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSql
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onConflict
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 onDuplicateKeyUpdate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isSerialized
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serialize
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
20
 writeToFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 streamToFile
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
23
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatConflicts
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Db\Sql;
15
16use Pop\Db\Adapter\AbstractAdapter;
17
18/**
19 * Data class to output data to a valid SQL file
20 *
21 * @category   Pop
22 * @package    Pop\Db
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://www.popphp.org/license     New BSD License
26 * @version    6.5.0
27 */
28class Data extends AbstractSql
29{
30
31    /**
32     * Database table
33     * @var string
34     */
35    protected string $table = 'pop_db_data';
36
37    /**
38     * Divide INSERT groups by # (0 creates one big INSERT statement, 1 creates an INSERT statement per row)
39     * @var int
40     */
41    protected int $divide = 1;
42
43    /**
44     * Conflict key for UPSERT
45     * @var ?string
46     */
47    protected ?string $conflictKey = null;
48
49    /**
50     * Conflict columns for UPSERT
51     * @var array
52     */
53    protected array $conflictColumns = [];
54
55    /**
56     * SQL string
57     * @var ?string
58     */
59    protected ?string $sql = null;
60
61    /**
62     * Constructor
63     *
64     * Instantiate the SQL object
65     *
66     * @param  AbstractAdapter $db
67     * @param  string          $table
68     * @param  int             $divide
69     */
70    public function __construct(AbstractAdapter $db, string $table = 'pop_db_data', int $divide = 1)
71    {
72        parent::__construct($db);
73        $this->setDivide($divide);
74        $this->setTable($table);
75    }
76
77    /**
78     * Set the INSERT divide
79     *
80     * @param  int $divide
81     * @return Data
82     */
83    public function setDivide(int $divide): Data
84    {
85        $this->divide = $divide;
86        return $this;
87    }
88
89    /**
90     * Get the INSERT divide
91     *
92     * @return int
93     */
94    public function getDivide(): int
95    {
96        return $this->divide;
97    }
98
99    /**
100     * Set the database table
101     *
102     * @param  string $table
103     * @return Data
104     */
105    public function setTable(string $table): Data
106    {
107        $this->table = $table;
108        return $this;
109    }
110
111    /**
112     * Get the database table
113     *
114     * @return string
115     */
116    public function getTable(): string
117    {
118        return $this->table;
119    }
120
121    /**
122     * Get SQL string
123     *
124     * @return ?string
125     */
126    public function getSql(): ?string
127    {
128        return $this->sql;
129    }
130
131    /**
132     * Set what to do on a insert conflict (UPSERT - PostgreSQL & SQLite)
133     *
134     * @param  array   $columns
135     * @param  ?string $key
136     * @return Data
137     */
138    public function onConflict(array $columns, ?string $key = null): Data
139    {
140        $this->conflictColumns = $columns;
141        $this->conflictKey     = $key;
142        return $this;
143    }
144
145    /**
146     * Set columns to handle duplicates/conflicts (UPSERT - MySQL-ism)
147     *
148     * @param  array $columns
149     * @return Data
150     */
151    public function onDuplicateKeyUpdate(array $columns): Data
152    {
153        $this->onConflict($columns);
154        return $this;
155    }
156
157    /**
158     * Check if data was serialized into SQL
159     *
160     * @return bool
161     */
162    public function isSerialized(): bool
163    {
164        return ($this->sql !== null);
165    }
166
167    /**
168     * Serialize the data into INSERT statements
169     *
170     * @param  array $data
171     * @param  mixed $omit
172     * @param  bool  $nullEmpty
173     * @param  bool  $forceQuote
174     * @return ?string
175     */
176    public function serialize(array $data, mixed $omit = null, bool $nullEmpty = false, bool $forceQuote = false): ?string
177    {
178        if ($omit !== null) {
179            $omit = (!is_array($omit)) ? [$omit] : $omit;
180        }
181
182        $this->sql = '';
183        $table     = $this->quoteId($this->table);
184        $columns   = array_keys(reset($data));
185
186        if (!empty($omit)) {
187            foreach ($omit as $o) {
188                if (in_array($o, $columns)) {
189                    unset($columns[array_search($o, $columns)]);
190                }
191            }
192        }
193
194        $columns  = array_map([$this, 'quoteId'], $columns);
195        $insert   = "INSERT INTO " . $table . " (" . implode(', ', $columns) . ") VALUES" . PHP_EOL;
196        $onUpdate = $this->formatConflicts();
197
198        foreach ($data as $i => $row) {
199            if (!empty($omit)) {
200                foreach ($omit as $o) {
201                    if (isset($row[$o])) {
202                        unset($row[$o]);
203                    }
204                }
205            }
206            $value = "(" . implode(', ', array_map(function($value) use ($forceQuote) {
207                    return $this->quote($value, $forceQuote);
208                }, $row)) . ")";
209            if ($nullEmpty) {
210                $value = str_replace(["('',", " '', ", ", '')"], ["(NULL,", ' NULL, ', ', NULL)'], $value);
211            }
212
213            switch ($this->divide) {
214                case 0:
215                    if ($i == 0) {
216                        $this->sql .= $insert;
217                    }
218                    $this->sql .= $value;
219                    $this->sql .= ($i == (count($data) - 1)) ? $onUpdate . ';' : ',';
220                    $this->sql .= PHP_EOL;
221                    break;
222                case 1:
223                    $this->sql .= $insert . $value . $onUpdate . ';' . PHP_EOL;
224                    break;
225                default:
226                    if (($i % $this->divide) == 0) {
227                        $this->sql .= $insert . $value . (($i == (count($data) - 1)) ? $onUpdate . ';' : ',') . PHP_EOL;
228                    } else {
229                        $this->sql .= $value;
230                        $this->sql .= (((($i + 1) % $this->divide) == 0) || ($i == (count($data) - 1))) ? $onUpdate . ';' : ',';
231                        $this->sql .= PHP_EOL;
232                    }
233            }
234        }
235
236        return $this->sql;
237    }
238
239    /**
240     * Output SQL to a file
241     *
242     * @param  string  $to
243     * @param  ?string $header
244     * @param  ?string $footer
245     * @return void
246     */
247    public function writeToFile(string $to, ?string $header = null, ?string $footer = null): void
248    {
249        file_put_contents($to, $header . $this->sql . $footer);
250    }
251
252    /**
253     * Serialize the data into INSERT statements
254     *
255     * @param  array   $data
256     * @param  ?string $to
257     * @param  mixed   $omit
258     * @param  bool    $nullEmpty
259     * @param  ?string $header
260     * @param  ?string $footer
261     * @return void
262     */
263    public function streamToFile(
264        array $data, ?string $to, mixed $omit = null, bool $nullEmpty = false, ?string $header = null, ?string $footer = null
265    ): void
266    {
267        if (!file_exists($to)) {
268            touch($to);
269        }
270
271        $handle = fopen($to, 'a');
272
273        if ($header !== null) {
274            fwrite($handle, $header);
275        }
276
277        if ($omit !== null) {
278            $omit = (!is_array($omit)) ? [$omit] : $omit;
279        }
280
281        $table    = $this->quoteId($this->table);
282        $columns  = array_keys(reset($data));
283
284        if (!empty($omit)) {
285            foreach ($omit as $o) {
286                if (in_array($o, $columns)) {
287                    unset($columns[array_search($o, $columns)]);
288                }
289            }
290        }
291
292        $columns  = array_map([$this, 'quoteId'], $columns);
293        $insert   = "INSERT INTO " . $table . " (" . implode(', ', $columns) . ") VALUES" . PHP_EOL;
294        $onUpdate = $this->formatConflicts();
295
296        foreach ($data as $i => $row) {
297            if (!empty($omit)) {
298                foreach ($omit as $o) {
299                    if (isset($row[$o])) {
300                        unset($row[$o]);
301                    }
302                }
303            }
304            $value = "(" . implode(', ', array_map([$this, 'quote'], $row)) . ")";
305            if ($nullEmpty) {
306                $value = str_replace(["'', ", ", '')"], ['NULL, ', ', NULL)'], $value);
307            }
308
309            switch ($this->divide) {
310                case 0:
311                    if ($i == 0) {
312                        fwrite($handle, $insert);
313                    }
314                    fwrite($handle, $value);
315                    fwrite($handle, ($i == (count($data) - 1)) ? $onUpdate . ';' : ',');
316                    fwrite($handle, PHP_EOL);
317                    break;
318                case 1:
319                    fwrite($handle, $insert . $value . ';' . PHP_EOL);
320                    break;
321                default:
322                    if (($i % $this->divide) == 0) {
323                        fwrite($handle, $insert . $value . (($i == (count($data) - 1)) ? $onUpdate . ';' : ',') . PHP_EOL);
324                    } else {
325                        fwrite($handle, $value);
326                        fwrite($handle, ((((($i + 1) % $this->divide) == 0) || ($i == (count($data) - 1))) ? $onUpdate . ';' : ','));
327                        fwrite($handle, PHP_EOL);
328                    }
329            }
330        }
331
332
333        if ($footer !== null) {
334            fwrite($handle, $footer);
335        }
336
337        fclose($handle);
338    }
339
340    /**
341     * __toString magic method
342     *
343     * @return string
344     */
345    public function __toString(): string
346    {
347        return $this->sql;
348    }
349
350    /**
351     * Method to format conflicts (UPSERT)
352     *
353     * @return string
354     */
355    protected function formatConflicts(): string
356    {
357        $onUpdate = '';
358
359        if (!empty($this->conflictColumns)) {
360            $updates = [];
361            switch ($this->dbType) {
362                case self::MYSQL:
363                    foreach ($this->conflictColumns as $conflictColumn) {
364                        $updates[] = $this->quoteId($conflictColumn) . ' = VALUES(' . $conflictColumn .')';
365                    }
366                    $onUpdate = PHP_EOL . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
367                    break;
368                case self::SQLITE:
369                case self::PGSQL:
370                    foreach ($this->conflictColumns as $conflictColumn) {
371                        $updates[] = $this->quoteId($conflictColumn) . ' = excluded.' . $conflictColumn;
372                    }
373                    $onUpdate = PHP_EOL . ' ON CONFLICT (' . $this->quoteId($this->conflictKey) . ') DO UPDATE SET '
374                        . implode(', ', $updates);
375                    break;
376            }
377        }
378
379        return $onUpdate;
380    }
381
382}