CMS  Version 3.9
data_sync_manager.inc
Go to the documentation of this file.
1 <?php
14 {
15  function __construct()
16  {
17  }
18 
19  function export()
20  {
21  $xml = "\n<DataSyncMap>";
22  $xml .= SerializationManager::serialize(DataImportProfile, "ORDER BY import_profile_id");
23  $xml .= SerializationManager::serialize(DataImportFieldMapping, "ORDER BY mapping_id");
24  $xml .= "</DataSyncMap>";
25 
26  return $xml;
27  }
28 
29  function import($doc, $tx)
30  {
33  }
34 }
35 
41 interface IDataSyncHelper
42 {
44  function processRecord($record);
45  function extendForm($form);
46 }
47 
49 {
51  function countRecords($file);
54  function skipToOffset($fp, $offset = -1);
55  function getRecord($fp, $columns);
56  function closeImportFile($fp);
57 }
58 
60 {
61 
63  {
64  global $config;
65 
66  $file = $_FILES[$field]["tmp_name"];
67 
68  $fullpath = $config["uploadbase"] . DIRECTORY_SEPARATOR . basename($file);
69 
70  if (file_exists($fullpath))
71  {
72  unlink($fullpath);
73  }
74 
75  trace("Uploading $file to $fullpath", 3);
76 
77  move_uploaded_file($file, $fullpath);
78  chmod($fullpath, 0755);
79 
80  return $fullpath;
81 // $_SESSION["data_import_file"] = $fullpath;
82  }
83 
84  function countRecords($file)
85  {
86  //$fp = fopen($file, "r");
87  $fp = fopen_utf8($file);
88  $counter = -1;
89  while(fgetcsv($fp))
90  {
91  $counter++;
92  }
93 
94  fclose($fp);
95 
96  return $counter;
97  }
98 
100  {
101  trace("Reading columns for $file", 3);
102  //$fp = fopen($file, "r");
103  $fp = fopen_utf8($file);
104 
105  $fields = fgetcsv_noBOM($fp);
106  fclose($fp);
107 
108  $columns = array();
109 
110  foreach($fields as $field)
111  {
112  if ($field) $columns[] = $field;
113  }
114 
115  return $columns;
116  }
117 
118  function skipToOffset($fp, $offset = -1)
119  {
120  if ($offset == -1)
121  {
122  $offset = checkNumeric($_GET["offset"]);
123  }
124 
125  while($offset-- > 0)
126  {
127  fgetcsv($fp);
128  }
129  }
130 
132  {
133  if (!$file)
134  {
135  $file = $_SESSION["data_import_file"];
136  }
137 
138  if (!file_exists($file))
139  {
140  throw new FakoliException("Data Import File does not exist");
141  }
142 
143  trace("Opening import file $file", 3);
144 
145  //$fp = fopen($file, "r");
146  $fp = fopen_utf8($file);
147 
148  $fields = fgetcsv_noBOM($fp);
149 
150  foreach($fields as $field)
151  {
152  $columns[] = $field;
153  }
154 
155  return $fp;
156  }
157 
158  function getRecord($fp, $columns)
159  {
160  $record = fgetcsv($fp);
161 
162  if ($record)
163  {
164  $record = array_combine($columns, $record);
165  }
166 
167  return $record;
168  }
169 
170  function closeImportFile($fp)
171  {
172  fclose($fp);
173  }
174 }
175 
182 {
183  static $helpers = null;
184  static $totalRecords = 0;
185  static $fieldMappings = array();
186 
187  static $import_profile_id = 0;
188 
189  static $adapters = null;
190 
191  function DataSyncManager()
192  {
193  }
194 
196  {
198  }
199 
200  static function registerDataSyncAdapter($format, $adapter)
201  {
203  }
204 
205  static function registerCSVAdapter()
206  {
208  }
209 
210  static function getDataAdapter($format = null)
211  {
212  if (!$format)
213  {
214  $format = $_SESSION["data_import_format"];
215  if (!$format) $format = "CSV";
216  }
217 
218  if (DataSyncManager::$adapters == null)
219  {
220  ComponentManager::fireEvent("RegisterDataSyncAdapter");
221  }
222 
223  if (!array_key_exists($format, DataSyncManager::$adapters))
224  {
225  throw new FakoliException("Unknown Data Format");
226  }
227 
229  }
230 
231  static function getAvailableDataAdapters()
232  {
233  if (DataSyncManager::$adapters == null)
234  {
235  ComponentManager::fireEvent("RegisterDataSyncAdapter");
236  }
237  return array_keys(DataSyncManager::$adapters);
238  }
239 
240  static function setDefaults()
241  {
242  Settings::setDefaultValue("data_sync", "import_in_chunks", false, Boolean, "Specifies whether to limit the number of records being imported at one time.");
243  Settings::setDefaultValue("data_sync", "records_per_page", 500, Number, "The number of records to import at one time when chunked import is enabled.");
244  Settings::setDefaultValue("data_sync", "multiple_import_profiles", false, Boolean, "Turn on support for configuring multiple import profile for each sync target.");
245  }
246 
248  {
249  SerializationManager::registerHandler("data_sync", "Data Import Profiles and Field Mappings", new DataSyncSerializationHandler());
250  return true;
251  }
252 
253  static function getImportTabs()
254  {
255  $tabs = array(
256  "Upload File" => "data_import",
257  "Select Target" => "data_import_select_target",
258  "Choose Fields" => "data_import_field_mapping",
259  "Select Records" => "data_import_select"
260  );
261 
262  $tabBar = new TabBar("tabs", $tabs);
263  $tabBar->useQueryString = false;
264  return $tabBar;
265 
266  }
267 
268  static function getAdminImportTabs()
269  {
270  $tabs = array(
271  "Upload File" => "/admin/data_import",
272  "Select Target" => "/admin/data_import_select_target",
273  "Choose Fields" => "/admin/data_import_field_mapping",
274  "Select Records" => "/admin/data_import_select"
275  );
276 
277  $tabBar = new TabBar("tabs", $tabs);
278  $tabBar->useQueryString = false;
279  return $tabBar;
280 
281  }
282 
283  static function countRecords($file = "", $format = null)
284  {
285  if (!$file)
286  {
287  $file = $_SESSION["data_import_file"];
288  }
289 
291  return $adapter->countRecords($file);
292  }
293 
294  static function skipToOffset($fp, $offset = -1)
295  {
296  if ($offset == -1)
297  {
298  $offset = checkNumeric($_GET["offset"]);
299  }
300 
301  while($offset-- > 0)
302  {
303  fgetcsv($fp);
304  }
305  }
306 
307  static function getHelper($class)
308  {
309  if (array_key_exists($class, DataSyncManager::$helpers))
310  {
312  }
313 
314  return $helper;
315  }
316 
317  static function getImportColumns($class, $file = "", $format = null)
318  {
319  ComponentManager::fireEvent("RegisterSyncHelpers");
321 
322  if (!$file)
323  {
324  $file = $_SESSION["data_import_file"];
325  }
326 
327  $columns = $adapter->getImportColumns($file);
328 
330  if ($helper)
331  {
332  $columns = $helper->getAdditionalColumns($columns);
333  }
334 
335  return $columns;
336  }
337 
338  static function extendForm($form)
339  {
340  $class = get_class($form->data);
342 
343  if ($helper)
344  {
345  $helper->extendForm($form);
346  }
347  }
348 
349  static function getFieldMappings($class, $profile_id = 0)
350  {
351  if (!$class) return null;
352 
354 
355  $fieldMappings = IndexedQuery::create(DataImportFieldMapping, "WHERE class=:cl AND import_profile_id=:pr", "client_field")
356  ->bind(":cl", $class, ":pr", $profile_id)
357  ->execute();
358 
359  // Check for missing fields and create default entries
360 
361  $obj = new $class();
362  foreach($obj->getFields() as $field => $type)
363  {
364  if ($field == $obj->getPrimaryKey()) continue;
365  if (array_key_exists($field, $fieldMappings)) continue;
366 
368  $mapping->class = $class;
369  $mapping->client_field = $field;
370  $mapping->import_column = "";
371  $mapping->import_profile_id = $profile_id;
372 
373  $mapping->save();
374  }
375 
376  $fieldMappings = IndexedQuery::create(DataImportFieldMapping, "WHERE class=:cl AND import_profile_id=:pr", "client_field")
377  ->bind(":cl", $class, ":pr", $profile_id)
378  ->execute();
379 
380  // Sort field mappings
381  $filled = array();
382  $empty = array();
383  foreach($obj->getFields() as $field => $type)
384  {
385  if ($field == $obj->getPrimaryKey()) continue;
386  if (!array_key_exists($field, $fieldMappings)) continue;
387 
388  if ($fieldMappings[$field]->import_column)
389  {
390  $filled[] = $fieldMappings[$field];
391  }
392  else
393  {
394  $empty[] = $fieldMappings[$field];
395  }
396  }
397 
398  $fieldMappings = array_merge($filled, $empty);
399 
400  return $fieldMappings;
401  }
402 
403 
404  private static function getMatchingFields($class, $import_profile_id = 0)
405  {
406  $matchingFields = Query::create(DataImportFieldMapping, "WHERE class=:cl AND import_profile_id=:pr AND matching=1")
407  ->bind(":cl", $class, ":pr", $import_profile_id)
408  ->execute();
409 
410  $mf = array();
411 
412  foreach($matchingFields as $matchingField)
413  {
414  $mf[] = $matchingField->client_field;
415  }
416 
417  return $mf;
418  }
419 
420  private static function getMatchingFormat($class, $import_profile_id = 0)
421  {
422  $mf = DataSyncManager::getMatchingFields($class, $import_profile_id);
423 
424  $matchingFormat = implode("##", $mf);
425 
426  return $matchingFormat;
427  }
428 
434  private static function getExistingItemIDs($class, $import_profile_id = 0)
435  {
436  $matchingFields = DataSyncManager::getMatchingFields($class, $import_profile_id);
437 
438  $item = new $class;
439  $filter = new InclusionFilter();
440  $filter->add($item->getPrimaryKey());
441  foreach($matchingFields as $field)
442  {
443  $filter->add($field);
444  }
445 
446  $existingItems = IteratedQuery::create($class)
447  ->filter($filter)
448  ->execute();
449 
450  $existingItemIDs = array();
451 
452  foreach($existingItems as $item)
453  {
454  $existingItemIDs[$item->format($matchingFormat)] = $item->get($item->getPrimaryKey());
455  }
456 
457  return $existingItemIDs;
458  }
459 
460 
462  {
464 
465  trace("Retrieving field mapping for $class profile $import_profile_id", 3);
466 
468  IndexedQuery::create(DataImportFieldMapping, "WHERE class=:cl AND import_profile_id=:pr AND import_column != ''", "client_field")
469  ->bind(":cl", $class, ":pr", $import_profile_id)
470  ->execute();
471 
473  }
474 
476  {
477  foreach($fieldMappings as $fieldMapping)
478  {
479  if ($fieldMapping->matching) return true;
480  }
481 
482  return false;
483  }
484 
485  static function getActiveFieldMapping($class)
486  {
488  }
489 
490  static function generateFilter($fieldMapping)
491  {
492  $filter = new InclusionFilter();
493 
494  foreach($fieldMapping as $field => $mapping)
495  {
496  $filter->add($field);
497  }
498 
499  return $filter;
500  }
501 
502  private static function openImportFile($file, &$columns, $format = null)
503  {
505  $fp = $adapter->openImportFile($file, $columns);
506 
507  return $fp;
508  }
509 
510  private static function getRecord($adapter, $fp, $columns, $helper = null, $performLookups = true)
511  {
512 
513  $record = $adapter->getRecord($fp, $columns);
514 
515  if ($record)
516  {
517  if ($helper)
518  {
519  $record = $helper->processRecord($record, $performLookups);
520  }
521  }
522 
523  return $record;
524  }
525 
526  private static function populateItem($item, $record, $fieldMapping, $lastItem = null)
527  {
528  $skip = false;
529 
530  //AJG - a bit of a hack to allow SyncHelpers to specify that particular records should be skipped
531  if ($record["__skip__"] === true) return true;
532 
533  foreach($fieldMapping as $field => $mapping)
534  {
535  if (!$mapping->import_column) continue;
536 
537  $cols = $mapping->getColumns();
538 
539  $vals = array();
540 
541  foreach($cols as $col)
542  {
543  if ($record[$col])
544  {
545  $vals[] = $record[$col];
546  }
547  }
548 
549  $separator = ($item->getType($field) == Text) ? "\n" : " ";
550 
551  $value = trim(implode($separator, $vals));
552 
553  if (!$value && $mapping->grouped && $lastItem)
554  {
555  $value = $lastItem->get($field);
556  }
557 
558  if (!$value && $mapping->required)
559  {
560  trace("Required field $field not set in record. Skipping", 3);
561 
562  $skip = true;
563  break;
564  }
565 
566  $item->set($field, $value);
567  //$item->set($field, $item->formatFieldValue($field));
568  }
569 
570  if (!$skip) $item->unpack();
571 
572  return $skip;
573  }
574 
575  static function createImportItems($class, $import_profile_id = 0, $file = "", $format = null)
576  {
577  global $user;
578 
580 
581  $item = new $class();
582 
583  ComponentManager::fireEvent("RegisterSyncHelpers");
584 
586 
588 
589  $columns = array();
590 
593 
594  $fp = $adapter->openImportFile($file, $columns);
595 
596  $items = array();
597 
598  //DataSyncManager::skipToOffset($fp);
599  $isChunked = Settings::getValue("data_sync", "import_in_chunks");
600  $counter = Settings::getValue("data_sync", "records_per_page");
601  $offset = $isChunked ? checkNumeric($_GET["offset"]) : 0;
602 
603  $lastItem = null;
604 
605  $matchingFields = Query::create(DataImportFieldMapping, "WHERE class=:cl AND import_profile_id=:pr AND matching=1")
606  ->bind(":cl", $class, ":pr", $import_profile_id)
607  ->execute();
608 
609  $c = 1;
610  $matchingConstraint = "";
611 
612  foreach($matchingFields as $mf)
613  {
614  if ($matchingConstraint) $matchingConstraint .= " AND ";
615  $matchingConstraint .= "{$mf->client_field}=:{$c}";
616  $c++;
617  }
618 
619  $matchingQuery = Query::create($class, "WHERE $matchingConstraint");
620  $matchingQuery->filter = new InclusionFilter($item->getPrimaryKey());
621 
622  while($record = DataSyncManager::getRecord($adapter, $fp, $columns, $helper, ((!$isChunked) || $counter >= 0)))
623  {
624  $item = new $class();
625  $skip = false;
626 
627  $item->filter = $filter;
628 
629  $skip = DataSyncManager::populateItem($item, $record, $fieldMapping, $lastItem);
630 
631  if (!$skip)
632  {
634 
635  if ($isChunked && $offset > 0)
636  {
637  $offset--;
638  continue;
639  }
640 
641  if ($counter <= 0)
642  {
643  continue;
644  }
645 
646  if ($item->hasField("owner_id")) $item->set("owner_id", $user->get($user->getPrimaryKey()));
647 
648  $c = 1;
649 
650  foreach($matchingFields as $mf)
651  {
652  $matchingQuery->bind(":{$c}", $item->get($mf->client_field));
653  $c++;
654  }
655 
656  $match = null;
657  try
658  {
659  $match = $matchingQuery->executeSingle();
660 
661  $item->set($item->getPrimaryKey(), $match->get($match->getPrimaryKey()));
662  }
663  catch(DataNotFoundException $e)
664  {
665  }
666  catch(FakoliException $e)
667  {
668  }
669 
670  $items[] = $item;
671  $lastItem = $item;
672  $counter--;
673  }
674 
675  }
676 
677  fclose($fp);
678 
679  return $items;
680  }
681 
682 
683  static function batchImportItems($class, $import_profile_id, $file, $importMatching, $importNew, $process, $format = null)
684  {
685  trace("Batching importing $class profile $import_profile_id from $file", 3);
686 
687  global $user;
688 
691 
692  if (!$process)
693  {
694  throw new FakoliException("DataSyncManager::batchImportItems cannot be used within a browser request");
695  }
696 
697  try
698  {
699  ComponentManager::fireEvent("RegisterSyncHelpers");
700 
702 
703  $columns = array();
704 
705  $process->setProgress("Running", "Scanning file", 0);
706 
707  $numRecords = DataSyncManager::countRecords($file);
708 
709  $process->setProgress("Running", pluralize("$numRecords record")." found", 0);
710 
713 
714  $matchingFields = Query::create(DataImportFieldMapping, "WHERE class=:cl AND import_profile_id=:pr AND matching=1")
715  ->bind(":cl", $class, ":pr", $import_profile_id)
716  ->execute();
717 
718  $c = 1;
719  $matchingConstraint = "";
720 
721  foreach($matchingFields as $mf)
722  {
723  if ($matchingConstraint) $matchingConstraint .= " AND ";
724  $matchingConstraint .= "{$mf->client_field}=:{$c}";
725  $c++;
726  }
727 
728  trace("Matching Constraint: $matchingConstraint", 3);
729 
730  $item = new $class();
731 
732  $fp = $adapter->openImportFile($file, $columns);
733 
734  $lastItem = null;
735 
736  $imported = 0;
737  $skipped = 0;
738 
739  $matchingQuery = Query::create($class, "WHERE $matchingConstraint");
740  $matchingQuery->filter = new InclusionFilter($item->getPrimaryKey());
741 
742  while($record = DataSyncManager::getRecord($adapter, $fp, $columns, $helper))
743  {
744 
745  $item = new $class();
746  $skip = false;
747 
748  $item->filter = $filter;
749 
750  $skip = DataSyncManager::populateItem($item, $record, $fieldMapping, $lastItem);
751 
752  if (!$skip)
753  {
754  $c = 1;
755 
756  foreach($matchingFields as $mf)
757  {
758  trace("Matching: {$mf->client_field} = ".$item->get($mf->client_field), 3);
759  $matchingQuery->bind(":{$c}", $item->get($mf->client_field));
760  $c++;
761  }
762 
763  $match = null;
764  try
765  {
766  $match = $matchingQuery->executeSingle();
767 
768  $item->set($item->getPrimaryKey(), $match->get($match->getPrimaryKey()));
769  }
770  catch(DataNotFoundException $e)
771  {
772  // Data not found
773  }
774  catch(FakoliException $fe)
775  {
776  // Ambiguous singleton, most likely
777  }
778 
779  if (($match && !$importMatching) || (!$match && !$importNew))
780  {
781  $skipped++;
782  }
783  else
784  {
785  $item->save();
786  if (method_exists($helper, 'postProcess'))
787  {
788  $helper->postProcess($item, $record);
789  }
790  $imported++;
791  $lastItem = $item;
792  }
793  }
794  else
795  {
796  $skipped++;
797  }
798 
799  $counter = $skipped + $imported;
800 
801  $percentage = intval((100 * $counter) / $numRecords);
802 
803  $process->setProgress("Running", "Processed record $counter of $numRecords", $percentage);
804 
805  }
806 
807  fclose($fp);
808 
809  $process->setProgress("Completed", "$imported records imported, $skipped records skipped", 100);
810 
811  }
812  catch(Exception $e)
813  {
814  $process->setProgress("Error", $e->getMessage(), $percentage);
815  }
816  }
817 
818  static function onItemRow($item)
819  {
820  $val = $item->get($item->getPrimaryKey());
821  return ($val) ? "matching" : "new";
822  }
823 
824  static function isRowSelected($row)
825  {
826  return false;
827  }
828 
829  static function getSyncTargets()
830  {
831  $targets = array();
832  $targets = ComponentManager::fireEvent("RegisterSyncTargets", $targets);
833 
834  $syncTargets = array();
835  foreach($targets as $index => $classes)
836  {
837  if (!is_array($classes))
838  {
839  $syncTargets[] = $classes;
840  }
841 
842  foreach($classes as $class)
843  {
844  $syncTargets[] = $class;
845  }
846  }
847 
848  return $syncTargets;
849  }
850 
851  static function upgradeComponent($version)
852  {
854  $mgr->upgrade($version);
855  }
856 }
857 
859 {
860  static function onItemRow($mapping)
861  {
862  return ($mapping->import_column) ? "mapped" : "";
863  }
864 }
865 ?>
$form
$tabs
$filter
Definition: update.inc:44
$file
Definition: delete.inc:47
static fireEvent($event, $parameter=null, $mustBeConsumed=false)
Fire an event to all subscribers as detailed in their manifests.
openImportFile($file, &$columns)
getRecord($fp, $columns)
skipToOffset($fp, $offset=-1)
Provides a central management class for event handlers and common functionality for the data_sync com...
static registerSyncHelper($class, $helper)
static getAvailableDataAdapters()
static upgradeComponent($version)
static fieldMappingHasMatchField($fieldMappings)
static getFieldMapping($class, $import_profile_id)
static getImportColumns($class, $file="", $format=null)
static registerSerializationHandler()
static createImportItems($class, $import_profile_id=0, $file="", $format=null)
static registerDataSyncAdapter($format, $adapter)
static batchImportItems($class, $import_profile_id, $file, $importMatching, $importNew, $process, $format=null)
static generateFilter($fieldMapping)
static getHelper($class)
static extendForm($form)
static getDataAdapter($format=null)
static isRowSelected($row)
static skipToOffset($fp, $offset=-1)
static getFieldMappings($class, $profile_id=0)
static countRecords($file="", $format=null)
static onItemRow($item)
static getActiveFieldMapping($class)
Serialization handler for Link Libraries and Link Records.
FakoliException is the base exception class for all Fakoli errors.
Definition: core.inc:53
static onItemRow($mapping)
static serialize($class, $constraint="")
Serializes the specified DataItems to XML.
registerHandler($component, $title, $handler)
Registers a serialization handler for a component.
static unserialize($class, $doc, $tx, $save=true)
Instantiates DataItems from the supplied XML document and stores them in the database.
static getValue($component, $name)
Retrieve the value of the specified Setting.
Definition: settings.inc:104
static setDefaultValue($component, $name, $value, $field_type="String", $annotation="", $category="", $options="", $weight=0)
Sets the default value of the given component setting.
Definition: settings.inc:174
global $user
if(!checkRole("admin")) $c
global $config
Definition: import.inc:4
skipToOffset($fp, $offset=-1)
getRecord($fp, $columns)
uploadDataImportFile($field)
getImportColumns($file)
openImportFile($file, &$columns)
IDataSyncHelper defines the interface that application should implement when providing sync helpers t...
getAdditionalColumns($columns)
processRecord($record)
$_SESSION["useMobile"]
Definition: override.inc:7
$process
Definition: run.php:54