Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.51% covered (success)
85.51%
118 / 138
81.25% covered (success)
81.25%
13 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Data
85.51% covered (success)
85.51%
118 / 138
81.25% covered (success)
81.25%
13 / 16
86.35
0.00% covered (danger)
0.00%
0 / 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
 setForceUpdate
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isForceUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
71.43% covered (success)
71.43%
40 / 56
0.00% covered (danger)
0.00%
0 / 1
46.29
 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 (https://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@noladev.com>
7 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
8 * @license    https://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@noladev.com>
24 * @copyright  Copyright (c) 2009-2025 NOLA Interactive, LLC.
25 * @license    https://www.popphp.org/license     New BSD License
26 * @version    6.6.5
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     * Force UPDATE instead of UPSERT if conflict keys/columns are provided
57     * @var bool
58     */
59    protected bool $forceUpdate = false;
60
61    /**
62     * SQL string
63     * @var ?string
64     */
65    protected ?string $sql = null;
66
67    /**
68     * Constructor
69     *
70     * Instantiate the SQL object
71     *
72     * @param  AbstractAdapter $db
73     * @param  string          $table
74     * @param  int             $divide
75     */
76    public function __construct(AbstractAdapter $db, string $table = 'pop_db_data', int $divide = 1)
77    {
78        parent::__construct($db);
79        $this->setDivide($divide);
80        $this->setTable($table);
81    }
82
83    /**
84     * Set the INSERT divide
85     *
86     * @param  int $divide
87     * @return Data
88     */
89    public function setDivide(int $divide): Data
90    {
91        $this->divide = $divide;
92        return $this;
93    }
94
95    /**
96     * Get the INSERT divide
97     *
98     * @return int
99     */
100    public function getDivide(): int
101    {
102        return $this->divide;
103    }
104
105    /**
106     * Set force update
107     *
108     * @param  bool   $forceUpdate
109     * @param  string $conflictKey
110     * @return Data
111     */
112    public function setForceUpdate(bool $forceUpdate = true, string $conflictKey = 'id'): Data
113    {
114        $this->forceUpdate = $forceUpdate;
115        $this->conflictKey = $conflictKey;
116        return $this;
117    }
118
119    /**
120     * Is force update
121     *
122     * @return bool
123     */
124    public function isForceUpdate(): bool
125    {
126        return $this->forceUpdate;
127    }
128
129    /**
130     * Set the database table
131     *
132     * @param  string $table
133     * @return Data
134     */
135    public function setTable(string $table): Data
136    {
137        $this->table = $table;
138        return $this;
139    }
140
141    /**
142     * Get the database table
143     *
144     * @return string
145     */
146    public function getTable(): string
147    {
148        return $this->table;
149    }
150
151    /**
152     * Get SQL string
153     *
154     * @return ?string
155     */
156    public function getSql(): ?string
157    {
158        return $this->sql;
159    }
160
161    /**
162     * Set what to do on a insert conflict (UPSERT - PostgreSQL & SQLite)
163     *
164     * @param  array   $columns
165     * @param  ?string $key
166     * @return Data
167     */
168    public function onConflict(array $columns, ?string $key = null): Data
169    {
170        $this->conflictColumns = $columns;
171        $this->conflictKey     = $key;
172        return $this;
173    }
174
175    /**
176     * Set columns to handle duplicates/conflicts (UPSERT - MySQL-ism)
177     *
178     * @param  array $columns
179     * @return Data
180     */
181    public function onDuplicateKeyUpdate(array $columns): Data
182    {
183        $this->onConflict($columns);
184        return $this;
185    }
186
187    /**
188     * Check if data was serialized into SQL
189     *
190     * @return bool
191     */
192    public function isSerialized(): bool
193    {
194        return ($this->sql !== null);
195    }
196
197    /**
198     * Serialize the data into INSERT statements
199     *
200     * @param  array $data
201     * @param  mixed $omit
202     * @param  bool  $nullEmpty
203     * @param  bool  $forceQuote
204     * @return ?string
205     */
206    public function serialize(array $data, mixed $omit = null, bool $nullEmpty = false, bool $forceQuote = false): ?string
207    {
208        if ($omit !== null) {
209            $omit = (!is_array($omit)) ? [$omit] : $omit;
210        }
211
212        $this->sql = '';
213        $table     = $this->quoteId($this->table);
214        $columns   = array_keys(reset($data));
215
216        if (!empty($omit)) {
217            foreach ($omit as $o) {
218                if (in_array($o, $columns)) {
219                    unset($columns[array_search($o, $columns)]);
220                }
221            }
222        }
223
224        // Force UPDATE SQL
225        if (($this->forceUpdate) && !empty($this->conflictKey)) {
226            foreach ($data as $row) {
227                $primaryValue = $row[$this->conflictKey];
228                unset($row[$this->conflictKey]);
229
230                $update = "UPDATE " . $table . " SET ";
231                if (!empty($omit)) {
232                    foreach ($omit as $o) {
233                        if (isset($row[$o])) {
234                            unset($row[$o]);
235                        }
236                    }
237                }
238
239                $values = [];
240                foreach ($row as $key => $value) {
241                    $values[] = $this->quoteId($key) . ' = ' . $this->quote($value, $forceQuote);
242                }
243                $values = implode(', ', $values);
244
245                if ($nullEmpty) {
246                    $values = str_replace(["('',", " '', ", ", '')"], ["(NULL,", ' NULL, ', ', NULL)'], $values);
247                }
248
249                $update .= $values . " WHERE " . $this->quoteId($this->conflictKey) . ' = ' . $primaryValue . ';';
250                $this->sql .= $update .  PHP_EOL;
251            }
252        // Else, INSERT/UPSERT SQL
253        } else {
254            $columns  = array_map([$this, 'quoteId'], $columns);
255            $insert   = "INSERT INTO " . $table . " (" . implode(', ', $columns) . ") VALUES" . PHP_EOL;
256            $onUpdate = $this->formatConflicts();
257
258            foreach ($data as $i => $row) {
259                if (!empty($omit)) {
260                    foreach ($omit as $o) {
261                        if (isset($row[$o])) {
262                            unset($row[$o]);
263                        }
264                    }
265                }
266                $value = "(" . implode(', ', array_map(function($value) use ($forceQuote) {
267                        return $this->quote($value, $forceQuote);
268                    }, $row)) . ")";
269                if ($nullEmpty) {
270                    $value = str_replace(["('',", " '', ", ", '')"], ["(NULL,", ' NULL, ', ', NULL)'], $value);
271                }
272
273                switch ($this->divide) {
274                    case 0:
275                        if ($i == 0) {
276                            $this->sql .= $insert;
277                        }
278                        $this->sql .= $value;
279                        $this->sql .= ($i == (count($data) - 1)) ? $onUpdate . ';' : ',';
280                        $this->sql .= PHP_EOL;
281                        break;
282                    case 1:
283                        $this->sql .= $insert . $value . $onUpdate . ';' . PHP_EOL;
284                        break;
285                    default:
286                        if (($i % $this->divide) == 0) {
287                            $this->sql .= $insert . $value . (($i == (count($data) - 1)) ? $onUpdate . ';' : ',') . PHP_EOL;
288                        } else {
289                            $this->sql .= $value;
290                            $this->sql .= (((($i + 1) % $this->divide) == 0) || ($i == (count($data) - 1))) ? $onUpdate . ';' : ',';
291                            $this->sql .= PHP_EOL;
292                        }
293                }
294            }
295        }
296
297        return $this->sql;
298    }
299
300    /**
301     * Output SQL to a file
302     *
303     * @param  string  $to
304     * @param  ?string $header
305     * @param  ?string $footer
306     * @return void
307     */
308    public function writeToFile(string $to, ?string $header = null, ?string $footer = null): void
309    {
310        file_put_contents($to, $header . $this->sql . $footer);
311    }
312
313    /**
314     * Serialize the data into INSERT statements
315     *
316     * @param  array   $data
317     * @param  ?string $to
318     * @param  mixed   $omit
319     * @param  bool    $nullEmpty
320     * @param  ?string $header
321     * @param  ?string $footer
322     * @return void
323     */
324    public function streamToFile(
325        array $data, ?string $to, mixed $omit = null, bool $nullEmpty = false, ?string $header = null, ?string $footer = null
326    ): void
327    {
328        if (!file_exists($to)) {
329            touch($to);
330        }
331
332        $handle = fopen($to, 'a');
333
334        if ($header !== null) {
335            fwrite($handle, $header);
336        }
337
338        if ($omit !== null) {
339            $omit = (!is_array($omit)) ? [$omit] : $omit;
340        }
341
342        $table    = $this->quoteId($this->table);
343        $columns  = array_keys(reset($data));
344
345        if (!empty($omit)) {
346            foreach ($omit as $o) {
347                if (in_array($o, $columns)) {
348                    unset($columns[array_search($o, $columns)]);
349                }
350            }
351        }
352
353        $columns  = array_map([$this, 'quoteId'], $columns);
354        $insert   = "INSERT INTO " . $table . " (" . implode(', ', $columns) . ") VALUES" . PHP_EOL;
355        $onUpdate = $this->formatConflicts();
356
357        foreach ($data as $i => $row) {
358            if (!empty($omit)) {
359                foreach ($omit as $o) {
360                    if (isset($row[$o])) {
361                        unset($row[$o]);
362                    }
363                }
364            }
365            $value = "(" . implode(', ', array_map([$this, 'quote'], $row)) . ")";
366            if ($nullEmpty) {
367                $value = str_replace(["'', ", ", '')"], ['NULL, ', ', NULL)'], $value);
368            }
369
370            switch ($this->divide) {
371                case 0:
372                    if ($i == 0) {
373                        fwrite($handle, $insert);
374                    }
375                    fwrite($handle, $value);
376                    fwrite($handle, ($i == (count($data) - 1)) ? $onUpdate . ';' : ',');
377                    fwrite($handle, PHP_EOL);
378                    break;
379                case 1:
380                    fwrite($handle, $insert . $value . ';' . PHP_EOL);
381                    break;
382                default:
383                    if (($i % $this->divide) == 0) {
384                        fwrite($handle, $insert . $value . (($i == (count($data) - 1)) ? $onUpdate . ';' : ',') . PHP_EOL);
385                    } else {
386                        fwrite($handle, $value);
387                        fwrite($handle, ((((($i + 1) % $this->divide) == 0) || ($i == (count($data) - 1))) ? $onUpdate . ';' : ','));
388                        fwrite($handle, PHP_EOL);
389                    }
390            }
391        }
392
393
394        if ($footer !== null) {
395            fwrite($handle, $footer);
396        }
397
398        fclose($handle);
399    }
400
401    /**
402     * __toString magic method
403     *
404     * @return string
405     */
406    public function __toString(): string
407    {
408        return $this->sql;
409    }
410
411    /**
412     * Method to format conflicts (UPSERT)
413     *
414     * @return string
415     */
416    protected function formatConflicts(): string
417    {
418        $onUpdate = '';
419
420        if (!empty($this->conflictColumns)) {
421            $updates = [];
422            switch ($this->dbType) {
423                case self::MYSQL:
424                    foreach ($this->conflictColumns as $conflictColumn) {
425                        $updates[] = $this->quoteId($conflictColumn) . ' = VALUES(' . $conflictColumn .')';
426                    }
427                    $onUpdate = PHP_EOL . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates);
428                    break;
429                case self::SQLITE:
430                case self::PGSQL:
431                    foreach ($this->conflictColumns as $conflictColumn) {
432                        $updates[] = $this->quoteId($conflictColumn) . ' = excluded.' . $conflictColumn;
433                    }
434                    $onUpdate = PHP_EOL . ' ON CONFLICT (' . $this->quoteId($this->conflictKey) . ') DO UPDATE SET '
435                        . implode(', ', $updates);
436                    break;
437            }
438        }
439
440        return $onUpdate;
441    }
442
443}