]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/IO/CsvOut.php
9c03bd675751768edd10b2dd6b672184cb91f40b
[galette.git] / galette / lib / Galette / IO / CsvOut.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * CSV exports
7 *
8 * PHP version 5
9 *
10 * Copyright © 2009-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
18 *
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
26 *
27 * @category IO
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Disponible depuis la Release 0.7alpha - 2009-02-09
35 */
36
37 namespace Galette\IO;
38
39 use ArrayObject;
40 use Laminas\Db\ResultSet\ResultSet;
41 use Symfony\Component\Yaml\Yaml;
42 use Throwable;
43 use Analog\Analog;
44 use Laminas\Db\Adapter\Adapter;
45
46 /**
47 * CSV exports
48 *
49 * @category IO
50 * @name Csv
51 * @package Galette
52 * @author Johan Cwiklinski <johan@x-tnd.be>
53 * @copyright 2009-2023 The Galette Team
54 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
55 * @link http://galette.tuxfamily.org
56 * @since Disponible depuis la Release 0.7alpha - 2009-02-09
57 */
58
59 class CsvOut extends Csv
60 {
61 public const DEFAULT_DIRECTORY = GALETTE_EXPORTS_PATH;
62
63 private $rs;
64 private $parameted_path;
65 private $legacy_parameted_file = 'exports.xml';
66 private $parameted_file = 'exports.yaml';
67
68 /**
69 * Default constructor
70 */
71 public function __construct()
72 {
73 parent::__construct(self::DEFAULT_DIRECTORY);
74 $this->parameted_path = GALETTE_CONFIG_PATH;
75 $this->parameted_file = $this->parameted_path . $this->parameted_file;
76 $this->legacy_parameted_file = $this->parameted_path . $this->legacy_parameted_file;
77 }
78
79 /**
80 * Export Array result set to CSV
81 *
82 * @param ResultSet $rs Results as an array
83 * @param string $separator The CSV separator (either '\t', ';' or ','
84 * are accepted)
85 * @param string $quote how does fields should be quoted
86 * @param bool $titles does export shows column titles or not.
87 * Defaults to false.
88 * @param resource|false $file export to a file on disk. A file pointer
89 * should be passed here. Defaults to false.
90 *
91 * @return string CSV result
92 */
93 public function export($rs, $separator, $quote, $titles = false, $file = false)
94 {
95 //switch back to the default separator if not in accepted_separators array
96 if (!in_array($separator, $this->accepted_separators)) {
97 $separator = self::DEFAULT_SEPARATOR;
98 }
99 //switch back to the default quote if not in accepted_quotes array
100 if (!in_array($quote, $this->accepted_quotes)) {
101 $quote = self::DEFAULT_QUOTE;
102 }
103
104 $this->result = '';
105 $results = [];
106 if (count($rs) > 0) {
107 foreach ($rs as $row) {
108 $results[] = $row;
109 }
110 }
111 $this->separator = $separator;
112 $this->quote = $quote;
113 //dubbing quote for escaping
114 $this->escaped = $quote . $quote;
115 $this->file = $file;
116 $this->current_line = 0;
117
118 $fields = array();
119 if ($titles && !is_array($titles)) {
120 $row = $results[0];
121 foreach (array_keys((array)$row) as $field) {
122 $fields[] = $this->quote . str_replace(
123 $this->quote,
124 $this->escaped,
125 $field
126 ) . $this->quote;
127 }
128 $this->result .= implode($this->separator, $fields) . self::NEWLINE;
129 } elseif ($titles && is_array($titles) && count($titles) > 1) {
130 foreach ($titles as $field) {
131 $field = str_replace(
132 array(':', '&nbsp;'),
133 '',
134 $field
135 );
136 $fields[] = $this->quote . str_replace(
137 $this->quote,
138 $this->escaped,
139 $field
140 ) . $this->quote;
141 }
142 $this->result .= implode($this->separator, $fields) . self::NEWLINE;
143 }
144
145 foreach ($results as $row) {
146 $elts = array();
147
148 if (is_array($row) || is_object($row)) {
149 foreach ($row as $v) {
150 $elts[] = $this->quote . str_replace(
151 $this->quote,
152 $this->escaped,
153 $v ?? ''
154 ) . $this->quote;
155 }
156
157 $this->result .= implode($this->separator, $elts) . self::NEWLINE;
158
159 $this->current_line += 1;
160
161 $this->write();
162 }
163 }
164 $this->write(true);
165 return $this->result;
166 }
167
168 /**
169 * Write export.
170 * If a file is defined, export will be outputted into it.
171 * If not, it will be returned
172 *
173 * @param bool $last true if we write the latest line
174 *
175 * @return void
176 */
177 private function write($last = false)
178 {
179 if (
180 $last && $this->file
181 || !$last && $this->file
182 && ($this->current_line % self::BUFLINES) == 0
183 ) {
184 if ($this->file === true) {
185 echo $this->result;
186 } else {
187 fwrite($this->file, $this->result);
188 }
189 $this->result = '';
190 }
191 }
192
193 /**
194 * Retrieve parameted export name
195 *
196 * @param string $id Parameted export identifier
197 *
198 * @return ?string
199 */
200 public function getParamedtedExportName($id)
201 {
202 //check first in YAML configuration file
203 $data = Yaml::parseFile($this->parameted_file);
204 foreach ($data as $export) {
205 if (!isset($export['inactive']) || $export['inactive']) {
206 $keys = array_keys($export);
207 $anid = array_shift($keys);
208 if ($anid == $id) {
209 return $export['name'];
210 }
211 }
212 }
213
214 //if id has not been found, look for it in legacy XML configuration file
215 if (file_exists($this->legacy_parameted_file)) {
216 $xml = simplexml_load_file($this->legacy_parameted_file);
217 $xpath = $xml->xpath(
218 '/exports/export[@id=\'' . $id . '\'][1]/@name'
219 );
220 return (string)$xpath[0];
221 }
222
223 return null;
224 }
225
226 /**
227 * Get al list of all parameted exports
228 *
229 * @return array
230 */
231 public function getParametedExports()
232 {
233 $parameted = [];
234
235 //first, load legacy config; if exists
236 if (file_exists($this->legacy_parameted_file)) {
237 $xml = simplexml_load_file($this->legacy_parameted_file);
238
239 foreach ($xml->export as $export) {
240 if (!($export['inactive'] == 'inactive')) {
241 $id = (string)$export['id'];
242 $parameted[$id] = array(
243 'id' => $id,
244 'name' => (string)$export['name'],
245 'description' => (string)$export['description']
246 );
247 }
248 }
249 }
250
251 //then, load config from YAML file
252 $data = Yaml::parseFile($this->parameted_file);
253 foreach ($data as $export) {
254 if (!isset($export['inactive']) || $export['inactive']) {
255 $keys = array_keys($export);
256 $id = array_shift($keys);
257 $parameted[$id] = [
258 'id' => $id,
259 'name' => $export['name'],
260 'description' => $export['description']
261 ];
262 }
263 }
264
265 return $parameted;
266 }
267
268 /**
269 * Run selected export parameted as XML
270 *
271 * @param string $id export's id to run
272 *
273 * @return string|int filename used or error code
274 */
275 private function runXmlParametedExport($id)
276 {
277 global $zdb;
278
279 $xml = simplexml_load_file($this->legacy_parameted_file);
280
281 $xpath = $xml->xpath(
282 '/exports/export[@id=\'' . $id . '\'][not(@inactive)][1]'
283 );
284 $export = $xpath[0];
285
286 try {
287 $results = $zdb->db->query(
288 str_replace('galette_', PREFIX_DB, $export->query),
289 Adapter::QUERY_MODE_EXECUTE
290 );
291
292 $filename = self::DEFAULT_DIRECTORY . $export['filename'];
293
294 $fp = fopen($filename, 'w');
295 if ($fp) {
296 $separator = ($export->separator)
297 ? $export->separator
298 : self::DEFAULT_SEPARATOR;
299 $quote = ($export->quote) ? $export->quote : self::DEFAULT_QUOTE;
300 if ($export->headers->none) {
301 //No title
302 $title = false;
303 } else {
304 $xpath = $export->xpath('headers/header');
305 if (count($xpath) == 0) {
306 //show titles
307 $title = true;
308 } else {
309 //titles from array
310 foreach ($xpath as $header) {
311 $title[] = (string)$header;
312 }
313 }
314 }
315
316 $this->export($results, $separator, $quote, $title, $fp);
317 fclose($fp);
318 } else {
319 Analog::log(
320 'File ' . $filename . ' is not writeable.',
321 Analog::ERROR
322 );
323 return self::FILE_NOT_WRITABLE;
324 }
325 return $export['filename'];
326 } catch (Throwable $e) {
327 Analog::log(
328 'An error occurred while exporting | ' . $e->getMessage(),
329 Analog::ERROR
330 );
331 return self::DB_ERROR;
332 }
333 }
334
335 /**
336 * Run selected export parameted as YAML
337 *
338 * @param string $id export's id to run
339 *
340 * @return string|int|false filename used, error code or failure
341 */
342 private function runYamlParametedExport($id)
343 {
344 global $zdb;
345
346 $export = [];
347 $data = Yaml::parseFile($this->parameted_file);
348 foreach ($data as $anexport) {
349 if (!isset($anexport['inactive']) || $anexport['inactive']) {
350 $keys = array_keys($anexport);
351 $anid = array_shift($keys);
352 if ($anid == $id) {
353 $export = $anexport;
354 }
355 }
356 }
357
358 if ($export['inactive'] ?? false) {
359 return false;
360 }
361
362 try {
363 $results = $zdb->db->query(
364 str_replace('galette_', PREFIX_DB, $export['query']),
365 Adapter::QUERY_MODE_EXECUTE
366 );
367
368 $filename = self::DEFAULT_DIRECTORY . $export['filename'];
369
370 $fp = fopen($filename, 'w');
371 if ($fp) {
372 $separator = $export['separator'] ?? self::DEFAULT_SEPARATOR;
373 $quote = $export['quote'] ?? self::DEFAULT_QUOTE;
374 $title = [];
375 if (isset($export['headers'])) {
376 if ($export['headers'] === false) {
377 //No title
378 $title = false;
379 } else {
380 foreach ($export['headers'] as $header) {
381 $title[] = (string)$header;
382 }
383 }
384 }
385
386 $this->export($results, $separator, $quote, $title, $fp);
387 fclose($fp);
388 } else {
389 Analog::log(
390 'File ' . $filename . ' is not writeable.',
391 Analog::ERROR
392 );
393 return self::FILE_NOT_WRITABLE;
394 }
395 return $export['filename'];
396 } catch (Throwable $e) {
397 Analog::log(
398 'An error occurred while exporting | ' . $e->getMessage(),
399 Analog::ERROR
400 );
401 return self::DB_ERROR;
402 }
403 }
404
405 /**
406 * Run selected export
407 *
408 * @param string $id export's id to run
409 *
410 * @return string filename used
411 */
412 public function runParametedExport($id)
413 {
414 //try first to run from YAML configuration file
415 return $this->runYamlParametedExport($id);
416
417 //if nothing has been run yet, look into legacy XML configuration file
418 if (file_exists($this->legacy_parameted_file)) {
419 return $this->runXmlParametedExport($id);
420 }
421 }
422 }