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