2020-11-27 12:32:38 +01:00
< ? php
2020-11-30 14:56:12 +01:00
/**
* Generic importer to feed data to cerebrate from JSON or CSV .
2021-09-24 13:11:39 +02:00
*
2020-11-30 14:56:12 +01:00
* - JSON configuration file must have the `format` key which can either take the value `json` or `csv`
* - If `csv` is provided , the file must contains the header .
* - If `json` is provided , a `mapping` key on how to reach each fields using the cakephp4 ' s Hash syntax must be provided .
* - The mapping is done in the following way :
* - The key is the field name
* - The value
* - Can either be the string representing the path from which to get the value
2021-09-24 13:11:39 +02:00
* - Or a JSON containg the `path` , the optional `override` parameter specifying if the existing data should be overriden
2020-11-30 14:56:12 +01:00
* and an optional `massage` function able to alter the data .
* - Example
* {
* " name " : " data. { n}.team-name " ,
* " uuid " : {
* " path " : " data. { n}.team-name " , // a path MUST always be provided
* " override " : false , // If the value already exists in the database, do not override it
* " massage " : " genUUID " // The function genUUID will be called on every piece of data
* },
*
* - The optional primary key argument provides a way to make import replayable . It can typically be used when an ID or UUID is not provided in the source file but can be replaced by something else ( e . g . team - name or other type of unique data ) .
2021-09-24 13:11:39 +02:00
*
2020-11-30 14:56:12 +01:00
*/
2020-11-27 12:32:38 +01:00
namespace App\Command ;
use Cake\Console\Command ;
use Cake\Console\Arguments ;
use Cake\Console\ConsoleIo ;
use Cake\Console\ConsoleOptionParser ;
use Cake\Filesystem\File ;
use Cake\Utility\Hash ;
use Cake\Utility\Text ;
use Cake\Validation\Validator ;
use Cake\Http\Client ;
class ImporterCommand extends Command
{
protected $modelClass = 'Organisations' ;
2020-11-30 13:47:48 +01:00
private $fieldsNoOverride = [];
2020-11-30 14:56:12 +01:00
private $format = 'json' ;
2021-03-10 09:22:47 +01:00
private $noMetaTemplate = false ;
private $autoYes = false ;
private $updateOnly = false ;
2020-11-27 12:32:38 +01:00
protected function buildOptionParser ( ConsoleOptionParser $parser ) : ConsoleOptionParser
{
$parser -> setDescription ( 'Import data based on the provided configuration file.' );
$parser -> addArgument ( 'config' , [
2020-11-30 14:56:12 +01:00
'help' => 'JSON configuration file path for the importer.' ,
2020-11-27 12:32:38 +01:00
'required' => true
]);
$parser -> addArgument ( 'source' , [
'help' => 'The source that should be imported. Can be either a file on the disk or an valid URL.' ,
'required' => true
]);
2020-11-30 15:59:01 +01:00
$parser -> addOption ( 'primary_key' , [
'short' => 'p' ,
2020-11-30 13:47:48 +01:00
'help' => 'To avoid duplicates, entries having the value specified by the primary key will be updated instead of inserted. Leave empty if only insertion should be done' ,
2020-11-27 12:32:38 +01:00
'default' => null ,
]);
2020-11-30 15:59:01 +01:00
$parser -> addOption ( 'model_class' , [
'short' => 'm' ,
'help' => 'The target cerebrate model for the import' ,
'default' => 'Organisations' ,
'choices' => [ 'Organisations' , 'Individuals' , 'AuthKeys' ]
2021-03-10 09:22:47 +01:00
]);
$parser -> addOption ( 'yes' , [
'short' => 'y' ,
'help' => 'Automatically assume yes to any prompts' ,
'default' => false ,
'boolean' => true
]);
$parser -> addOption ( 'update-only' , [
'short' => 'u' ,
'help' => 'Only update existing record. No new record will be created. primary_key MUST be supplied' ,
'default' => false ,
'boolean' => true
2020-11-30 15:59:01 +01:00
]);
2020-11-27 12:32:38 +01:00
return $parser ;
}
public function execute ( Arguments $args , ConsoleIo $io )
{
$this -> io = $io ;
$configPath = $args -> getArgument ( 'config' );
$source = $args -> getArgument ( 'source' );
2020-11-30 15:59:01 +01:00
$primary_key = $args -> getOption ( 'primary_key' );
$model_class = $args -> getOption ( 'model_class' );
if ( ! is_null ( $model_class )) {
$this -> modelClass = $model_class ;
}
2021-03-10 09:22:47 +01:00
$this -> autoYes = $args -> getOption ( 'yes' );
$this -> updateOnly = $args -> getOption ( 'update-only' );
if ( $this -> updateOnly && is_null ( $primary_key )) {
$io -> error ( 'A `primary_key` must be supplied when using `--update-only` mode.' );
die ( 1 );
}
2020-11-27 12:32:38 +01:00
$table = $this -> modelClass ;
$this -> loadModel ( $table );
$config = $this -> getConfigFromFile ( $configPath );
2020-11-30 13:47:48 +01:00
$this -> processConfig ( $config );
2021-03-10 09:22:47 +01:00
$sourceData = $this -> getDataFromSource ( $source , $config );
2020-11-27 12:32:38 +01:00
$data = $this -> extractData ( $this -> { $table }, $config , $sourceData );
2020-11-30 13:47:48 +01:00
$entities = $this -> marshalData ( $this -> { $table }, $data , $config , $primary_key );
2021-02-26 10:38:10 +01:00
$entitiesSample = array_slice ( $entities , 0 , min ( 5 , count ( $entities )));
2020-11-27 12:32:38 +01:00
$ioTable = $this -> transformEntitiesIntoTable ( $entitiesSample );
$io -> helper ( 'Table' ) -> output ( $ioTable );
2020-11-30 13:47:48 +01:00
2021-03-10 09:22:47 +01:00
if ( $this -> autoYes ) {
2020-11-27 12:32:38 +01:00
$this -> saveData ( $this -> { $table }, $entities );
2021-03-10 09:22:47 +01:00
} else {
$selection = $io -> askChoice ( 'A sample of the data you about to be saved is provided above. Would you like to proceed?' , [ 'Y' , 'N' ], 'N' );
if ( $selection == 'Y' ) {
$this -> saveData ( $this -> { $table }, $entities );
}
2020-11-27 12:32:38 +01:00
}
}
2020-11-30 13:47:48 +01:00
private function marshalData ( $table , $data , $config , $primary_key = null )
2020-11-27 12:32:38 +01:00
{
2021-02-26 10:38:10 +01:00
$this -> loadModel ( 'MetaTemplates' );
2020-11-30 13:47:48 +01:00
$this -> loadModel ( 'MetaFields' );
2020-11-27 12:32:38 +01:00
$entities = [];
if ( is_null ( $primary_key )) {
2021-10-20 11:44:23 +02:00
$entities = $table -> newEntities ( $data , [
'accessibleFields' => ( $table -> newEmptyEntity ()) -> getAccessibleFieldForNew ()
]);
2020-11-27 12:32:38 +01:00
} else {
foreach ( $data as $i => $item ) {
2020-11-30 15:36:13 +01:00
$entity = null ;
if ( isset ( $item [ $primary_key ])) {
$query = $table -> find ( 'all' )
2023-04-06 17:59:22 +02:00
-> where ([ " $primary_key " => $item [ $primary_key ]]);
2020-11-30 15:36:13 +01:00
$entity = $query -> first ();
}
2020-11-30 13:47:48 +01:00
if ( is_null ( $entity )) {
2021-03-10 09:22:47 +01:00
if ( ! $this -> updateOnly ) {
$entity = $table -> newEmptyEntity ();
}
2020-11-30 13:47:48 +01:00
} else {
2020-11-30 14:56:12 +01:00
$this -> lockAccess ( $entity );
2020-11-27 12:32:38 +01:00
}
2021-03-10 09:22:47 +01:00
if ( ! is_null ( $entity )) {
2021-10-20 11:44:23 +02:00
$entity = $table -> patchEntity ( $entity , $item , [
'accessibleFields' => $entity -> getAccessibleFieldForNew ()
]);
2021-03-10 09:22:47 +01:00
$entities [] = $entity ;
}
2020-11-27 12:32:38 +01:00
}
}
$hasErrors = false ;
2021-03-10 09:22:47 +01:00
if ( ! $this -> noMetaTemplate ) {
$metaTemplate = $this -> MetaTemplates -> find ()
-> where ([ 'uuid' => $config [ 'metaTemplateUUID' ]])
2023-03-31 13:55:48 +02:00
-> order ([ 'version' => 'DESC' ])
2021-03-10 09:22:47 +01:00
-> first ();
if ( ! is_null ( $metaTemplate )) {
$metaTemplateFieldsMapping = $this -> MetaTemplates -> MetaTemplateFields -> find ( 'list' , [
'keyField' => 'field' ,
'valueField' => 'id'
]) -> where ([ 'meta_template_id' => $metaTemplate -> id ]) -> toArray ();
} else {
2021-09-24 13:11:39 +02:00
$this -> io -> error ( " Unknown template for UUID { $config [ 'metaTemplateUUID' ] } " );
2021-03-10 09:22:47 +01:00
die ( 1 );
}
2021-02-26 10:38:10 +01:00
}
2021-03-10 09:22:47 +01:00
2020-11-27 16:22:54 +01:00
foreach ( $entities as $i => $entity ) {
2020-11-27 12:32:38 +01:00
if ( $entity -> hasErrors ()) {
$hasErrors = true ;
$this -> io -> error ( json_encode ([ 'entity' => $entity , 'errors' => $entity -> getErrors ()], JSON_PRETTY_PRINT ));
2020-11-30 13:47:48 +01:00
} else {
2021-03-10 09:22:47 +01:00
if ( ! $this -> noMetaTemplate && ! is_null ( $metaTemplate )) {
2021-02-26 10:38:10 +01:00
$metaFields = [];
foreach ( $entity [ 'metaFields' ] as $fieldName => $fieldValue ) {
$metaEntity = null ;
if ( ! $entity -> isNew ()) {
$query = $this -> MetaFields -> find ( 'all' ) -> where ([
'parent_id' => $entity -> id ,
'field' => $fieldName ,
'meta_template_id' => $metaTemplate -> id
]);
$metaEntity = $query -> first ();
}
if ( is_null ( $metaEntity )) {
$metaEntity = $this -> MetaFields -> newEmptyEntity ();
$metaEntity -> field = $fieldName ;
2023-01-17 09:28:27 +01:00
$metaEntity -> scope = $table -> getBehavior ( 'MetaFields' ) -> getScope ();
2021-02-26 10:38:10 +01:00
$metaEntity -> meta_template_id = $metaTemplate -> id ;
2023-03-31 13:55:48 +02:00
$metaEntity -> meta_template_directory_id = $metaTemplate -> meta_template_directory_id ;
2021-02-26 10:38:10 +01:00
if ( isset ( $metaTemplateFieldsMapping [ $fieldName ])) { // a meta field template must exists
$metaEntity -> meta_template_field_id = $metaTemplateFieldsMapping [ $fieldName ];
} else {
$hasErrors = true ;
2021-09-24 13:11:39 +02:00
$this -> io -> error ( " Field $fieldName is unknown for template { $metaTemplate -> name } " );
2021-02-26 10:38:10 +01:00
break ;
}
}
if ( $this -> canBeOverriden ( $metaEntity )) {
$metaEntity -> value = $fieldValue ;
}
$metaFields [] = $metaEntity ;
2020-11-30 13:47:48 +01:00
}
2021-02-26 10:38:10 +01:00
$entities [ $i ] -> metaFields = $metaFields ;
2020-11-30 13:47:48 +01:00
}
2020-11-27 12:32:38 +01:00
}
}
if ( ! $hasErrors ) {
$this -> io -> verbose ( 'No validation errors' );
2020-11-30 15:36:13 +01:00
} else {
$this -> io -> error ( 'Validation errors, please fix before importing' );
die ( 1 );
2020-11-27 12:32:38 +01:00
}
return $entities ;
}
private function saveData ( $table , $entities )
{
2020-11-27 16:22:54 +01:00
$this -> loadModel ( 'MetaFields' );
2020-11-27 12:32:38 +01:00
$this -> io -> verbose ( 'Saving data' );
2020-11-30 15:36:13 +01:00
$progress = $this -> io -> helper ( 'Progress' );
2020-11-27 16:22:54 +01:00
$entities = $table -> saveMany ( $entities );
if ( $entities === false ) {
2020-11-27 12:32:38 +01:00
$this -> io -> error ( 'Error while saving data' );
}
2020-11-27 16:22:54 +01:00
$this -> io -> verbose ( 'Saving meta fields' );
2020-11-30 15:41:36 +01:00
$this -> io -> out ( '' );
2020-11-30 15:36:13 +01:00
$progress -> init ([
2020-11-30 15:41:36 +01:00
'total' => count ( $entities ),
'length' => 20
2020-11-30 15:36:13 +01:00
]);
2020-11-27 16:22:54 +01:00
foreach ( $entities as $i => $entity ) {
2021-03-10 09:22:47 +01:00
if ( ! $this -> noMetaTemplate ) {
$this -> saveMetaFields ( $entity );
}
2020-11-30 15:36:13 +01:00
$progress -> increment ( 1 );
$progress -> draw ();
2020-11-30 13:47:48 +01:00
}
2020-11-30 15:41:36 +01:00
$this -> io -> out ( '' );
2020-11-30 13:47:48 +01:00
}
private function saveMetaFields ( $entity )
{
2020-11-30 15:41:36 +01:00
foreach ( $entity -> metaFields as $i => $metaEntity ) {
2020-11-30 13:47:48 +01:00
$metaEntity -> parent_id = $entity -> id ;
2023-01-17 09:28:27 +01:00
$metaEntity -> setNew ( true );
2020-11-30 15:36:13 +01:00
if ( $metaEntity -> hasErrors () || is_null ( $metaEntity -> value )) {
2021-02-26 10:38:10 +01:00
$this -> io -> error ( json_encode ([ 'entity' => $metaEntity , 'errors' => $metaEntity -> getErrors ()], JSON_PRETTY_PRINT ));
2020-11-30 15:41:36 +01:00
unset ( $entity -> metaFields [ $i ]);
2020-11-27 16:22:54 +01:00
}
}
2020-11-30 15:41:36 +01:00
$entity -> metaFields = $this -> MetaFields -> saveMany ( $entity -> metaFields );
if ( $entity -> metaFields === false ) {
$this -> io -> error ( 'Error while saving meta data' );
2020-11-30 13:47:48 +01:00
}
2020-11-27 12:32:38 +01:00
}
private function extractData ( $table , $config , $source )
{
$this -> io -> verbose ( 'Extracting data' );
2020-11-30 14:56:12 +01:00
$defaultFields = array_flip ( $table -> getSchema () -> columns ());
if ( $this -> format == 'json' ) {
$data = $this -> extractDataFromJSON ( $defaultFields , $config , $source );
} else if ( $this -> format == 'csv' ) {
$data = $this -> extractDataFromCSV ( $defaultFields , $config , $source );
} else {
$this -> io -> error ( 'Cannot extract data: Invalid file format' );
die ( 1 );
}
return $data ;
}
private function extractDataFromJSON ( $defaultFields , $config , $source )
{
2020-11-27 12:32:38 +01:00
$data = [];
2020-11-30 14:56:12 +01:00
foreach ( $config [ 'mapping' ] as $key => $fieldConfig ) {
2020-11-27 12:32:38 +01:00
$values = null ;
2020-11-27 16:48:14 +01:00
if ( ! is_array ( $fieldConfig )) {
$fieldConfig = [ 'path' => $fieldConfig ];
}
2020-11-27 12:32:38 +01:00
if ( ! empty ( $fieldConfig [ 'path' ])) {
$values = Hash :: extract ( $source , $fieldConfig [ 'path' ]);
}
if ( ! empty ( $fieldConfig [ 'massage' ])) {
$values = array_map ( " self:: { $fieldConfig [ 'massage' ] } " , $values );
}
if ( isset ( $defaultFields [ $key ])) {
2023-01-17 09:28:27 +01:00
$data [ $key ] = array_map ( 'trim' , $values );
2020-11-27 12:32:38 +01:00
} else {
2023-01-17 09:28:27 +01:00
$data [ 'metaFields' ][ $key ] = array_map ( 'trim' , $values );
2020-11-27 12:32:38 +01:00
}
}
return $this -> invertArray ( $data );
}
2020-11-30 14:56:12 +01:00
private function extractDataFromCSV ( $defaultFields , $config , $source )
2023-01-17 09:28:27 +01:00
{
$csvData = $this -> csvToAssociativeArray ( $source );
return $this -> extractDataFromJSON ( $defaultFields , $config , $csvData );
}
private function csvToAssociativeArray ( $source ) : array
2020-11-30 14:56:12 +01:00
{
$rows = array_map ( 'str_getcsv' , explode ( PHP_EOL , $source ));
if ( count ( $rows [ 0 ]) != count ( $rows [ 1 ])) {
$this -> io -> error ( 'Error while parsing source data. CSV doesn\'t have the same number of columns' );
die ( 1 );
}
2023-01-17 09:28:27 +01:00
$csvData = [];
$headers = array_shift ( $rows );
foreach ( $rows as $row ) {
if ( count ( $headers ) == count ( $row )) {
$csvData [] = array_combine ( $headers , $row );
2020-11-30 14:56:12 +01:00
}
}
2023-01-17 09:28:27 +01:00
return $csvData ;
2020-11-30 14:56:12 +01:00
}
private function lockAccess ( & $entity )
2020-11-30 13:47:48 +01:00
{
foreach ( $this -> fieldsNoOverride as $fieldName ) {
$entity -> setAccess ( $fieldName , false );
}
}
2020-11-30 14:56:12 +01:00
private function canBeOverriden ( $metaEntity )
2020-11-30 13:47:48 +01:00
{
return ! in_array ( $metaEntity -> field , $this -> fieldsNoOverride );
}
2021-03-10 09:22:47 +01:00
private function getDataFromSource ( $source , $config )
2020-11-27 12:32:38 +01:00
{
$data = $this -> getDataFromFile ( $source );
if ( $data === false ) {
2021-03-10 09:22:47 +01:00
$data = $this -> getDataFromURL ( $source , $config );
2020-11-27 12:32:38 +01:00
}
return $data ;
}
2021-03-10 09:22:47 +01:00
private function getDataFromURL ( $url , $config )
2020-11-27 12:32:38 +01:00
{
$validator = new Validator ();
$validator
-> requirePresence ( 'url' )
-> notEmptyString ( 'url' , 'Please provide a valid source' )
-> url ( 'url' );
$errors = $validator -> validate ([ 'url' => $url ]);
if ( ! empty ( $errors )) {
$this -> io -> error ( json_encode ( Hash :: extract ( $errors , '{s}' ), JSON_PRETTY_PRINT ));
die ( 1 );
}
$http = new Client ();
$this -> io -> verbose ( 'Downloading file' );
2021-03-10 09:22:47 +01:00
$httpConfig = [
'headers' => ! empty ( $config [ 'sourceHeaders' ]) ? $config [ 'sourceHeaders' ] : []
];
$query = [];
$response = $http -> get ( $url , $query , $httpConfig );
2020-11-30 14:56:12 +01:00
if ( $this -> format == 'json' ) {
return $response -> getJson ();
} else if ( $this -> format == 'csv' ) {
return $response -> getStringBody ();
} else {
$this -> io -> error ( 'Cannot parse source data: Invalid file format' );
}
2020-11-27 12:32:38 +01:00
}
private function getDataFromFile ( $path )
{
$file = new File ( $path );
if ( $file -> exists ()) {
$this -> io -> verbose ( 'Reading file' );
$data = $file -> read ();
$file -> close ();
if ( ! empty ( $data )) {
2020-11-30 14:56:12 +01:00
if ( $this -> format == 'json' ) {
$data = json_decode ( $data , true );
if ( is_null ( $data )) {
$this -> io -> error ( 'Error while parsing the source file' );
die ( 1 );
}
return $data ;
} else if ( $this -> format == 'csv' ) {
return $data ;
} else {
$this -> io -> error ( 'Cannot parse source data: Invalid file format' );
2020-11-27 12:32:38 +01:00
}
}
}
return false ;
}
private function getConfigFromFile ( $configPath )
{
$file = new File ( $configPath );
if ( $file -> exists ()) {
$config = $file -> read ();
$file -> close ();
if ( ! empty ( $config )) {
$config = json_decode ( $config , true );
if ( is_null ( $config )) {
$this -> io -> error ( 'Error while parsing the configuration file' );
die ( 1 );
}
return $config ;
} else {
$this -> io -> error ( 'Configuration file cound not be read' );
}
} else {
$this -> io -> error ( 'Configuration file not found' );
}
}
2020-11-30 13:47:48 +01:00
private function processConfig ( $config )
{
2020-11-30 14:56:12 +01:00
if ( empty ( $config [ 'mapping' ])) {
$this -> io -> error ( 'Error while parsing the configuration file, mapping missing' );
die ( 1 );
}
2021-03-10 09:22:47 +01:00
if ( empty ( $config [ 'metaTemplateUUID' ])) {
$this -> io -> warning ( 'No `metaTemplateUUID` provided. No meta fields will be created.' );
$this -> noMetaTemplate = true ;
}
2020-11-30 14:56:12 +01:00
if ( ! empty ( $config [ 'format' ])) {
$this -> format = $config [ 'format' ];
}
2020-11-30 13:47:48 +01:00
$this -> fieldsNoOverride = [];
2020-11-30 14:56:12 +01:00
foreach ( $config [ 'mapping' ] as $fieldName => $fieldConfig ) {
2020-11-30 13:47:48 +01:00
if ( is_array ( $fieldConfig )) {
if ( isset ( $fieldConfig [ 'override' ]) && $fieldConfig [ 'override' ] === false ) {
$this -> fieldsNoOverride [] = $fieldName ;
}
}
}
}
2020-11-27 12:32:38 +01:00
private function transformResultSetsIntoTable ( $result , $header = [])
{
$table = [[]];
if ( ! empty ( $result )) {
$tableHeader = empty ( $header ) ? array_keys ( $result [ 0 ]) : $header ;
$tableContent = [];
foreach ( $result as $item ) {
if ( empty ( $header )) {
$tableContent [] = array_map ( 'strval' , array_values ( $item ));
} else {
$row = [];
foreach ( $tableHeader as $key ) {
$row [] = ( string ) $item [ $key ];
}
$tableContent [] = $row ;
}
}
$table = array_merge ([ $tableHeader ], $tableContent );
}
return $table ;
}
private function transformEntitiesIntoTable ( $entities , $header = [])
{
$table = [[]];
if ( ! empty ( $entities )) {
$tableHeader = empty ( $header ) ? array_keys ( Hash :: flatten ( $entities [ 0 ] -> toArray ())) : $header ;
2020-11-27 16:22:54 +01:00
$tableHeader = array_filter ( $tableHeader , function ( $name ) {
return ! in_array ( 'metaFields' , explode ( '.' , $name ));
});
2020-11-30 14:56:12 +01:00
if ( ! empty ( $entities [ 0 ] -> metaFields )) {
foreach ( $entities [ 0 ] -> metaFields as $metaField ) {
$tableHeader [] = " metaFields. $metaField->field " ;
}
2020-11-27 16:22:54 +01:00
}
2020-11-27 12:32:38 +01:00
$tableContent = [];
foreach ( $entities as $entity ) {
$row = [];
foreach ( $tableHeader as $key ) {
2023-01-17 09:28:27 +01:00
if ( in_array ( $key , $entity -> getVirtual ())) {
continue ;
}
2020-11-27 12:32:38 +01:00
$subKeys = explode ( '.' , $key );
if ( in_array ( 'metaFields' , $subKeys )) {
2020-11-30 13:47:48 +01:00
$found = false ;
foreach ( $entity -> metaFields as $metaField ) {
if ( $metaField -> field == $subKeys [ 1 ]) {
$row [] = ( string ) $metaField -> value ;
$found = true ;
break ;
}
}
if ( ! $found ) {
$row [] = '' ;
}
2020-11-27 12:32:38 +01:00
} else {
$row [] = ( string ) $entity [ $key ];
}
}
$tableContent [] = $row ;
}
$table = array_merge ([ $tableHeader ], $tableContent );
}
return $table ;
}
private function invertArray ( $data )
{
$inverted = [];
foreach ( $data as $key => $values ) {
if ( $key == 'metaFields' ) {
foreach ( $values as $metaKey => $metaValues ) {
foreach ( $metaValues as $i => $metaValue ) {
$inverted [ $i ][ 'metaFields' ][ $metaKey ] = $metaValue ;
}
}
} else {
foreach ( $values as $i => $value ) {
$inverted [ $i ][ $key ] = $value ;
}
}
}
return $inverted ;
}
private function genUUID ( $value )
{
return Text :: uuid ();
}
2021-02-26 10:38:10 +01:00
private function nullToEmptyString ( $value )
{
return is_null ( $value ) ? '' : $value ;
}
2021-09-24 13:11:39 +02:00
}