Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
DefaultDataExportCommand
0.00% covered (danger)
0.00%
0 / 108
0.00% covered (danger)
0.00%
0 / 4
812
0.00% covered (danger)
0.00%
0 / 1
 buildOptionParser
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
156
 _exportAllTables
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 exportTableData
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2declare(strict_types=1);
3
4namespace App\Command;
5
6use Cake\Command\Command;
7use Cake\Console\Arguments;
8use Cake\Console\ConsoleIo;
9use Cake\Console\ConsoleOptionParser;
10use Cake\Datasource\ConnectionManager;
11use Cake\ORM\TableRegistry;
12use Cake\Utility\Inflector;
13use Exception;
14
15/**
16 * DefaultDataExportCommand
17 *
18 * This command allows exporting data from a selected database table (or all tables)
19 * to a JSON file. By default, it excludes common timestamp columns and generic 'id' columns
20 * (unless 'id' is part of a composite primary key).
21 * An option is provided to include all columns.
22 */
23class DefaultDataExportCommand extends Command
24{
25    /**
26     * Configures the option parser for the command.
27     *
28     * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure.
29     * @return \Cake\Console\ConsoleOptionParser The configured option parser.
30     */
31    protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
32    {
33        $parser
34            ->setDescription('Exports data from specified table(s) to JSON files.')
35            ->addOption('output', [
36                'short' => 'o',
37                'help' => 'Output directory for the exported JSON file(s).',
38                'default' => ROOT . DS . 'default_data',
39            ])
40            ->addOption('all', [
41                'short' => 'a',
42                'help' => 'Export data from all tables. Overrides interactive selection.',
43                'boolean' => true,
44            ])
45            ->addOption('include-all-columns', [
46                'short' => 'c',
47                'help' => 'Include ALL columns in the export, overriding default 
48                    exclusions (id, created, modified unless PK).',
49                'boolean' => true,
50                'default' => false,
51            ]);
52
53        return $parser;
54    }
55
56    /**
57     * Executes the command to export data.
58     *
59     * @param \Cake\Console\Arguments $args The command arguments.
60     * @param \Cake\Console\ConsoleIo $io The console io object.
61     * @return int The exit code of the command.
62     */
63    public function execute(Arguments $args, ConsoleIo $io): int
64    {
65        $outputDir = (string)$args->getOption('output');
66        $includeAllColumns = (bool)$args->getOption('include-all-columns');
67
68        if (!is_dir($outputDir)) {
69            if (!mkdir($outputDir, 0755, true) && !is_dir($outputDir)) {
70                 $io->error(sprintf('Output directory "%s" could not be created.', $outputDir));
71
72                 return Command::CODE_ERROR;
73            }
74            $io->info(sprintf('Output directory "%s" created.', $outputDir));
75        }
76
77        /** @var \Cake\Database\Connection $connection */
78        $connection = ConnectionManager::get('default');
79        $allTables = $connection->getSchemaCollection()->listTables();
80
81        $exportAllByFlag = (bool)$args->getOption('all');
82
83        if ($exportAllByFlag) {
84            if (empty($allTables)) {
85                $io->info('No tables found in the database to export via --all flag.');
86
87                return Command::CODE_SUCCESS;
88            }
89
90            return $this->_exportAllTables($outputDir, $io, $allTables, $includeAllColumns);
91        } else {
92            if (empty($allTables)) {
93                $io->warning('No tables found in the database for interactive export.');
94
95                return Command::CODE_SUCCESS;
96            }
97
98            $io->out('Available actions:');
99            $io->out('[0] Export All Tables');
100            foreach ($allTables as $index => $table) {
101                $io->out(sprintf('[%d] %s', $index + 1, $table));
102            }
103
104            $choiceStr = $io->ask('Please select an option by number:');
105
106            if (!ctype_digit($choiceStr)) {
107                $io->error('Invalid input. Please enter a number. Exiting.');
108
109                return Command::CODE_ERROR;
110            }
111            $choice = (int)$choiceStr;
112
113            if ($choice === 0) {
114                return $this->_exportAllTables($outputDir, $io, $allTables, $includeAllColumns);
115            } else {
116                $tableIndex = $choice - 1;
117                if (!isset($allTables[$tableIndex])) {
118                    $io->error('Invalid table selection. Exiting.');
119
120                    return Command::CODE_ERROR;
121                }
122                $tableName = $allTables[$tableIndex];
123                if ($this->exportTableData($tableName, $outputDir, $io, $includeAllColumns)) {
124                    return Command::CODE_SUCCESS;
125                } else {
126                    return Command::CODE_ERROR;
127                }
128            }
129        }
130    }
131
132    /**
133     * Helper method to export all tables.
134     *
135     * @param string $outputDir The output directory.
136     * @param \Cake\Console\ConsoleIo $io The console IO object.
137     * @param array $allTables List of all table names.
138     * @param bool $includeAllColumns Whether to include all columns.
139     * @return int Command exit code (CODE_SUCCESS or CODE_ERROR).
140     */
141    private function _exportAllTables(string $outputDir, ConsoleIo $io, array $allTables, bool $includeAllColumns): int
142    {
143        $io->out(sprintf('Exporting all tables to %s...', $outputDir));
144        if ($includeAllColumns) {
145            $io->info('Including ALL columns in export.');
146        }
147        $exportedCount = 0;
148        $failedCount = 0;
149
150        foreach ($allTables as $tableName) {
151            if ($this->exportTableData($tableName, $outputDir, $io, $includeAllColumns)) {
152                $exportedCount++;
153            } else {
154                $failedCount++;
155            }
156        }
157
158        if ($exportedCount > 0) {
159            $io->success(sprintf('Successfully exported data from %d table(s).', $exportedCount));
160        }
161        if ($failedCount > 0) {
162            $io->error(sprintf('Failed to export data from %d table(s). Check logs above.', $failedCount));
163
164            return Command::CODE_ERROR;
165        }
166
167        return Command::CODE_SUCCESS;
168    }
169
170    /**
171     * Exports data from a single table to a JSON file.
172     *
173     * @param string $tableName The name of the table to export.
174     * @param string $outputDir The directory to save the JSON file.
175     * @param \Cake\Console\ConsoleIo $io The console io object.
176     * @param bool $includeAllColumns Whether to include all columns.
177     * @return bool True on success, false on failure.
178     */
179    private function exportTableData(string $tableName, string $outputDir, ConsoleIo $io, bool $includeAllColumns): bool
180    {
181        $io->out(sprintf('Processing table: %s...', $tableName));
182        try {
183            $table = TableRegistry::getTableLocator()->get($tableName);
184            $query = $table->find();
185            $schema = $table->getSchema();
186            $allSchemaColumns = $schema->columns();
187
188            $selectedColumns = [];
189
190            if ($includeAllColumns) {
191                $io->out(sprintf('Table %s: Including all columns due to --include-all-columns flag.', $tableName));
192                $selectedColumns = $allSchemaColumns;
193            } else {
194                $primaryKey = (array)$schema->getPrimaryKey();
195                $columnsToPotentiallyExclude = ['id', 'created', 'modified'];
196
197                $selectedColumns = $allSchemaColumns;
198                foreach ($columnsToPotentiallyExclude as $col) {
199                    if (in_array($col, $selectedColumns) && !in_array($col, $primaryKey)) {
200                        $selectedColumns = array_diff($selectedColumns, [$col]);
201                    }
202                }
203                // $io->out(sprintf('Table %s: Exporting columns: %s', $tableName, implode(', ', $selectedColumns)));
204            }
205
206            if (empty($selectedColumns)) {
207                $io->info(sprintf(
208                    'Table "%s" has no columns to export after considering exclusions/inclusions. Skipping.',
209                    $tableName,
210                ));
211
212                return true;
213            }
214
215            $query->select(array_values($selectedColumns));
216            $data = $query->disableHydration()->all()->toArray();
217
218            $outputFile = $outputDir . DS . Inflector::underscore($tableName) . '.json';
219            $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
220
221            if ($json === false) {
222                $io->error(sprintf('Failed to encode JSON for table %s. Error: %s', $tableName, json_last_error_msg()));
223
224                return false;
225            }
226
227            if (file_put_contents($outputFile, $json) === false) {
228                $io->error(sprintf('Failed to write data for table %s to %s.', $tableName, $outputFile));
229
230                return false;
231            }
232
233            $io->success(sprintf('Data for table "%s" exported to %s', $tableName, $outputFile));
234
235            return true;
236        } catch (Exception $e) {
237            $io->error(sprintf('Error exporting table "%s": %s', $tableName, $e->getMessage()));
238
239            return false;
240        }
241    }
242}