]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/IO/CsvIn.php
Avoid count in loops
[galette.git] / galette / lib / Galette / IO / CsvIn.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * CSV imports
7 *
8 * PHP version 5
9 *
10 * Copyright © 2013-2014 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 2013-2014 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 Available since 0.7.6dev - 2013-08-27
35 */
36
37 namespace Galette\IO;
38
39 use Analog\Analog;
40 use Galette\Core\Db;
41 use Galette\Core\Preferences;
42 use Galette\Core\History;
43 use Galette\Entity\Adherent;
44 use Galette\Entity\ImportModel;
45 use Galette\Entity\FieldsConfig;
46 use Galette\Entity\Status;
47 use Galette\Entity\Title;
48 use Galette\Repository\Titles;
49 use Galette\IO\FileTrait;
50 use Galette\Repository\Members;
51
52 /**
53 * CSV imports
54 *
55 * @category IO
56 * @name Csv
57 * @package Galette
58 * @author Johan Cwiklinski <johan@x-tnd.be>
59 * @copyright 2013-2014 The Galette Team
60 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
61 * @link http://galette.tuxfamily.org
62 * @since Available since 0.7.6dev - 2013-08-27
63 */
64
65 class CsvIn extends Csv implements FileInterface
66 {
67 use FileTrait;
68
69 const DEFAULT_DIRECTORY = GALETTE_IMPORTS_PATH;
70 const DATA_IMPORT_ERROR = -10;
71
72 protected $extensions = array('csv', 'txt');
73
74 private $_fields;
75 private $_default_fields = array(
76 'nom_adh',
77 'prenom_adh',
78 'ddn_adh',
79 'adresse_adh',
80 'cp_adh',
81 'ville_adh',
82 'pays_adh',
83 'tel_adh',
84 'gsm_adh',
85 'email_adh',
86 'url_adh',
87 'prof_adh',
88 'pseudo_adh',
89 'societe_adh',
90 'login_adh',
91 'date_crea_adh',
92 'id_statut',
93 'info_public_adh',
94 'info_adh'
95 );
96
97 private $_dryrun = true;
98
99 private $_members_fields;
100 private $_members_fields_cats;
101 private $_required;
102 private $statuses;
103 private $titles;
104 private $langs;
105 private $emails;
106 private $zdb;
107 private $preferences;
108 private $history;
109
110 /**
111 * Default constructor
112 *
113 * @param Db $zdb Database
114 */
115 public function __construct(Db $zdb)
116 {
117 $this->zdb = $zdb;
118 $this->init(
119 self::DEFAULT_DIRECTORY,
120 $this->extensions,
121 array(
122 'csv' => 'text/csv',
123 'txt' => 'text/plain'
124 ),
125 2048
126 );
127
128 parent::__construct(self::DEFAULT_DIRECTORY);
129 }
130
131 /**
132 * Load fields list from database or from default values
133 *
134 * @return void
135 */
136 private function loadFields()
137 {
138 //at last, we got the defaults
139 $this->_fields = $this->_default_fields;
140
141 $model = new ImportModel();
142 //we go with default fields if model cannot be loaded
143 if ($model->load()) {
144 $this->_fields = $model->getFields();
145 }
146 }
147
148 /**
149 * Get default fields
150 *
151 * @return array
152 */
153 public function getDefaultFields()
154 {
155 return $this->_default_fields;
156 }
157
158 /**
159 * Import members from CSV file
160 *
161 * @param Db $zdb Database instance
162 * @param Preferences $preferences Preferences instance
163 * @param History $history History instance
164 * @param string $filename CSV filename
165 * @param array $members_fields Members fields
166 * @param array $members_fields_cats Members fields categories
167 * @param boolean $dryrun Run in dry run mode (do not store in database)
168 *
169 * @return boolean
170 */
171 public function import(
172 Db $zdb,
173 Preferences $preferences,
174 History $history,
175 $filename,
176 array $members_fields,
177 array $members_fields_cats,
178 $dryrun
179 ) {
180 if (
181 !file_exists(self::DEFAULT_DIRECTORY . '/' . $filename)
182 || !is_readable(self::DEFAULT_DIRECTORY . '/' . $filename)
183 ) {
184 Analog::log(
185 'File ' . $filename . ' does not exists or cannot be read.',
186 Analog::ERROR
187 );
188 return false;
189 }
190
191 $this->zdb = $zdb;
192 $this->preferences = $preferences;
193 $this->history = $history;
194 if ($dryrun === false) {
195 $this->_dryrun = false;
196 }
197
198 $this->loadFields();
199 $this->_members_fields = $members_fields;
200 $this->_members_fields_cats = $members_fields_cats;
201
202 if (!$this->check($filename)) {
203 return self::INVALID_FILE;
204 }
205
206 if (!$this->storeMembers($filename)) {
207 return self::DATA_IMPORT_ERROR;
208 }
209
210 return true;
211 }
212
213 /**
214 * Check if input file meet requirements
215 *
216 * @param string $filename File name
217 *
218 * @return boolean
219 */
220 private function check($filename)
221 {
222 $handle = fopen(self::DEFAULT_DIRECTORY . '/' . $filename, 'r');
223 if (!$handle) {
224 Analog::log(
225 'File ' . $filename . ' cannot be open!',
226 Analog::ERROR
227 );
228 $this->addError(
229 str_replace(
230 '%filename',
231 $filename,
232 _T('File %filename cannot be open!')
233 )
234 );
235 return false;
236 }
237
238 $cnt_fields = count($this->_fields);
239
240 //check required fields
241 $fc = new FieldsConfig(
242 $this->zdb,
243 Adherent::TABLE,
244 $this->_members_fields,
245 $this->_members_fields_cats
246 );
247 $config_required = $fc->getRequired();
248 $this->_required = array();
249
250 foreach (array_keys($config_required) as $field) {
251 if (in_array($field, $this->_fields)) {
252 $this->_required[$field] = $field;
253 }
254 }
255
256 $member = new Adherent($this->zdb);
257 $dfields = [];
258 $member->setDependencies(
259 $this->preferences,
260 $this->_members_fields,
261 $this->history
262 );
263
264 $row = 0;
265 while (
266 ($data = fgetcsv(
267 $handle,
268 1000,
269 self::DEFAULT_SEPARATOR,
270 self::DEFAULT_QUOTE
271 )) !== false
272 ) {
273 //check fields count
274 $count = count($data);
275 if ($count != $cnt_fields) {
276 $this->addError(
277 str_replace(
278 array('%should_count', '%count', '%row'),
279 array($cnt_fields, $count, $row),
280 _T("Fields count mismatch... There should be %should_count fields and there are %count (row %row)")
281 )
282 );
283 return false;
284 }
285
286 if ($row > 0) {
287 //header line is the first one. Here comes data
288 $col = 0;
289 $errors = [];
290 foreach ($data as $column) {
291 $column = trim($column);
292
293 //check required fields
294 if (
295 in_array($this->_fields[$col], $this->_required)
296 && empty($column)
297 ) {
298 $this->addError(
299 str_replace(
300 array('%field', '%row'),
301 array($this->_fields[$col], $row),
302 _T("Field %field is required, but missing in row %row")
303 )
304 );
305 return false;
306 }
307
308 //check for statuses
309 //if missing, set default one; if not check it does exists
310 if ($this->_fields[$col] == Status::PK) {
311 if (empty($column)) {
312 $column = Status::DEFAULT_STATUS;
313 } else {
314 if ($this->statuses === null) {
315 //load existing status
316 $status = new Status($this->zdb);
317 $this->statuses = $status->getList();
318 }
319 if (!isset($this->statuses[$column])) {
320 $this->addError(
321 str_replace(
322 '%status',
323 $column,
324 _T("Status %status does not exists!")
325 )
326 );
327 return false;
328 }
329 }
330 }
331
332 //check for title
333 if ($this->_fields[$col] == 'titre_adh' && !empty($column)) {
334 if ($this->titles === null) {
335 //load existing titles
336 $this->titles = Titles::getList($this->zdb);
337 }
338 if (!isset($this->titles[$column])) {
339 $this->addError(
340 str_replace(
341 '%title',
342 $column,
343 _T("Title %title does not exists!")
344 )
345 );
346 return false;
347 }
348 }
349
350 //check for email unicity
351 if ($this->_fields[$col] == 'email_adh' && !empty($column)) {
352 if ($this->emails === null) {
353 //load existing emails
354 $this->emails = Members::getEmails($this->zdb);
355 }
356 if (isset($this->emails[$column])) {
357 $existing = $this->emails[$column];
358 $extra = ($existing == -1 ?
359 _T("from another member in import") : str_replace('%id_adh', $existing, _T("from member %id_adh"))
360 );
361 $this->addError(
362 str_replace(
363 ['%address', '%extra'],
364 [$column, $extra],
365 _T("Email address %address is already used! (%extra)")
366 )
367 );
368 return false;
369 } else {
370 //add email to list
371 $this->emails[$column] = -1;
372 }
373 }
374
375 //check for language
376 if ($this->_fields[$col] == 'pref_lang') {
377 if ($this->langs === null) {
378 //load existing titles
379 global $i18n;
380 $this->langs = $i18n->getArrayList();
381 }
382 if (empty($column)) {
383 $column = $this->preferences->pref_lang;
384 } else {
385 if (!isset($this->langs[$column])) {
386 $this->addError(
387 str_replace(
388 '%title',
389 $column,
390 _T("Lang %lang does not exists!")
391 )
392 );
393 return false;
394 }
395 }
396 }
397
398 //passwords
399 if ($this->_fields[$col] == 'mdp_adh' && !empty($column)) {
400 $this->_fields['mdp_adh2'] = $column;
401 }
402
403 if (substr($this->_fields[$col], 0, strlen('dynfield_')) === 'dynfield_') {
404 //dynamic field, keep to check later
405 $dfields[$this->_fields[$col] . '_1'] = $column;
406 } else {
407 //standard field
408 $member->validate($this->_fields[$col], $column, $this->_fields);
409 }
410 $errors = $member->getErrors();
411 if (count($errors)) {
412 foreach ($errors as $error) {
413 $this->addError($error);
414 }
415 return false;
416 }
417
418 $col++;
419 }
420
421 //check dynamic fields
422 $errcnt = count($errors);
423 $member->dynamicsValidate($dfields);
424 $errors = $member->getErrors();
425 if (count($errors) > $errcnt) {
426 $lcnt = ($errcnt > 0 ? $errcnt - 1 : 0);
427 $cnt_err = count($errors);
428 for ($i = $lcnt; $i < $cnt_err; ++$i) {
429 $this->addError($errors[$i]);
430 }
431 return false;
432 }
433 }
434
435 $row++;
436 }
437 fclose($handle);
438
439 if (!($row > 1)) {
440 //no data in file, just headers line
441 $this->addError(
442 _T("File is empty!")
443 );
444 return false;
445 } else {
446 return true;
447 }
448 }
449
450 /**
451 * Store members in database
452 *
453 * @param string $filename CSV filename
454 *
455 * @return boolean
456 */
457 private function storeMembers($filename)
458 {
459 $handle = fopen(self::DEFAULT_DIRECTORY . '/' . $filename, 'r');
460
461 $row = 0;
462
463 try {
464 $this->zdb->connection->beginTransaction();
465 while (
466 ($data = fgetcsv(
467 $handle,
468 1000,
469 self::DEFAULT_SEPARATOR,
470 self::DEFAULT_QUOTE
471 )) !== false
472 ) {
473 if ($row > 0) {
474 $col = 0;
475 $values = array();
476 foreach ($data as $column) {
477 if (substr($this->_fields[$col], 0, strlen('dynfield_')) === 'dynfield_') {
478 //dynamic field, keep to check later
479 $values[str_replace('dynfield_', 'info_field_', $this->_fields[$col] . '_1')] = $column;
480 $col++;
481 continue;
482 }
483
484 $values[$this->_fields[$col]] = $column;
485 if ($this->_fields[$col] === 'societe_adh') {
486 $values['is_company'] = true;
487 }
488 //check for booleans
489 if (
490 ($this->_fields[$col] == 'bool_admin_adh'
491 || $this->_fields[$col] == 'bool_exempt_adh'
492 || $this->_fields[$col] == 'bool_display_info'
493 || $this->_fields[$col] == 'activite_adh')
494 && ($column == null || trim($column) == '')
495 ) {
496 $values[$this->_fields[$col]] = 0; //defaults to 0 as in Adherent
497 }
498
499 if ($this->_fields[$col] == Status::PK && empty(trim($column))) {
500 $values[Status::PK] = Status::DEFAULT_STATUS;
501 }
502
503 $col++;
504 }
505 //import member itself
506 $member = new Adherent($this->zdb);
507 $member->setDependencies(
508 $this->preferences,
509 $this->_members_fields,
510 $this->history
511 );
512 //check for empty creation date
513 if (isset($values['date_crea_adh']) && trim($values['date_crea_adh']) === '') {
514 unset($values['date_crea_adh']);
515 }
516 if (isset($values['mdp_adh'])) {
517 $values['mdp_adh2'] = $values['mdp_adh'];
518 }
519
520 $valid = $member->check($values, $this->_required, null);
521 if ($valid === true) {
522 if ($this->_dryrun === false) {
523 $store = $member->store();
524 if ($store !== true) {
525 $this->addError(
526 str_replace(
527 array('%row', '%name'),
528 array($row, $member->sname),
529 _T("An error occurred storing member at row %row (%name):")
530 )
531 );
532 return false;
533 }
534 }
535 } else {
536 $this->addError(
537 str_replace(
538 array('%row', '%name'),
539 array($row, $member->sname),
540 _T("An error occurred storing member at row %row (%name):")
541 )
542 );
543 if (is_array($valid)) {
544 foreach ($valid as $e) {
545 $this->addError($e);
546 }
547 }
548 return false;
549 }
550 }
551 $row++;
552 }
553 $this->zdb->connection->commit();
554 return true;
555 } catch (\Exception $e) {
556 $this->zdb->connection->rollBack();
557 $this->addError($e->getMessage());
558 }
559
560 return false;
561 }
562
563 /**
564 * Return textual error message
565 *
566 * @param int $code The error code
567 *
568 * @return string Localized message
569 */
570 public function getErrorMessage($code)
571 {
572 $error = null;
573 switch ($code) {
574 case self::DATA_IMPORT_ERROR:
575 $error = _T("An error occurred while importing members");
576 break;
577 }
578
579 if ($error === null) {
580 $error = $this->getErrorMessageFromCode($code);
581 }
582
583 return $error;
584 }
585 }