a3caecebbd4a0e16c8792bcf57ada1066532d4bb
[tine20] / tine20 / Tinebase / Convert / Json.php
1 <?php
2 /**
3  * convert functions for records from/to json (array) format
4  * 
5  * @package     Tinebase
6  * @subpackage  Convert
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2011-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * convert functions for records from/to json (array) format
14  *
15  * @package     Tinebase
16  * @subpackage  Convert
17  */
18 class Tinebase_Convert_Json implements Tinebase_Convert_Interface
19 {
20     /**
21      * converts external format to Tinebase_Record_Abstract
22      * 
23      * @param  mixed                     $_blob   the input data to parse
24      * @param  Tinebase_Record_Abstract  $_record  update existing record
25      * @return Tinebase_Record_Abstract
26      */
27     public function toTine20Model($_blob, Tinebase_Record_Abstract $_record = NULL)
28     {
29         throw new Tinebase_Exception_NotImplemented('From json to record is not implemented yet');
30     }
31     
32     /**
33      * converts Tinebase_Record_Abstract to external format
34      * 
35      * @param  Tinebase_Record_Abstract $_record
36      * @return mixed
37      */
38     public function fromTine20Model(Tinebase_Record_Abstract $_record)
39     {
40         if (! $_record) {
41             return array();
42         }
43         
44         // for resolving we'll use recordset
45         $recordClassName = get_class($_record);
46         $records = new Tinebase_Record_RecordSet($recordClassName, array($_record));
47         $modelConfiguration = $recordClassName::getConfiguration();
48         
49         $this->_resolveBeforeToArray($records, $modelConfiguration, FALSE);
50         
51         $_record = $records->getFirstRecord();
52         $_record->setTimezone(Tinebase_Core::getUserTimezone());
53         $_record->bypassFilters = true;
54         
55         $result = $_record->toArray();
56         
57         $result = $this->_resolveAfterToArray($result, $modelConfiguration, FALSE);
58         
59         return $result;
60     }
61
62     /**
63      * resolves single record fields (Tinebase_ModelConfiguration._recordsFields)
64      * 
65      * @param Tinebase_Record_RecordSet $_records the records
66      * @param Tinebase_ModelConfiguration $modelConfig
67      */
68     protected function _resolveSingleRecordFields(Tinebase_Record_RecordSet $_records, $modelConfig = NULL)
69     {
70         if (! $modelConfig) {
71             return;
72         }
73         
74         $resolveFields = $modelConfig->recordFields;
75         
76         if ($resolveFields && is_array($resolveFields)) {
77             // don't search twice if the same recordClass gets resolved on multiple fields
78             $resolveRecords = array();
79             foreach ($resolveFields as $fieldKey => $fieldConfig) {
80                 $resolveRecords[$fieldConfig['config']['recordClassName']][] = $fieldKey;
81             }
82             
83             foreach ($resolveRecords as $foreignRecordClassName => $fields) {
84                 $foreignIds = array();
85                 $fields = (array) $fields;
86                 foreach ($fields as $field) {
87                     $idsForField = $_records->{$field};
88                     foreach ($idsForField as $key => $value) {
89                         if ($value && ! in_array($value, $foreignIds)) {
90                             $foreignIds[] = $value;
91                         }
92                     }
93                 }
94                 
95                 if (empty($foreignIds)) {
96                     continue;
97                 }
98                 
99                 $cfg = $resolveFields[$fields[0]];
100                 
101                 if ($cfg['type'] == 'user') {
102                     $foreignRecords = Tinebase_User::getInstance()->getMultiple($foreignIds);
103                 } else if ($cfg['type'] == 'container') {
104                     // TODO: resolve recursive records of records better in controller
105                     // TODO: resolve containers
106                     if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
107                         . ' No handling for container foreign records implemented');
108
109                     $foreignRecords = new Tinebase_Record_RecordSet('Tinebase_Model_Container');
110                     // $foreignRecords->addRecord(Tinebase_Container::getInstance()->get(XXX));
111                 } else {
112                     try {
113                         $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
114                         $foreignRecords = $controller->getMultiple($foreignIds);
115                     } catch (Tinebase_Exception_AccessDenied $tead) {
116                         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
117                             . ' No right to access application of record ' . $foreignRecordClassName);
118                         continue;
119                     }
120                 }
121                 
122                 if ($foreignRecords->count() === 0) {
123                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
124                         . ' No matching foreign records found (' . $foreignRecordClassName . ')');
125                     continue;
126                 }
127                 $foreignRecords->setTimezone(Tinebase_Core::getUserTimezone());
128                 $foreignRecords->convertDates = true;
129                 Tinebase_Frontend_Json_Abstract::resolveContainersAndTags($foreignRecords);
130                 $fr = $foreignRecords->getFirstRecord();
131                 if ($fr && $fr->has('notes')) {
132                     Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
133                 }
134                 
135                 foreach ($_records as $record) {
136                     foreach ($fields as $field) {
137                         $foreignId = $record->{$field};
138                         if (is_scalar($foreignId)) {
139                             $idx = $foreignRecords->getIndexById($foreignId);
140                             if (isset($idx) && $idx !== FALSE) {
141                                 $record->{$field} = $foreignRecords[$idx];
142                             } else {
143                                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
144                                     . ' No matching foreign record found for id: ' . $foreignId);
145                             }
146                         }
147                     }
148                 }
149             }
150         }
151     }
152     
153     /**
154      * resolves multiple records (fallback)
155      * 
156      * @deprecated use Tinebase_ModelConfiguration to configure your models, so this won't be used anymore 
157      * @param Tinebase_Record_RecordSet $_records the records
158      * @param array $resolveFields
159      */
160     public static function resolveMultipleIdFields($records, $resolveFields = NULL)
161     {
162         if (! $records instanceof Tinebase_Record_RecordSet || !$records->count()) {
163             return;
164         }
165         
166         $ownRecordClass = $records->getRecordClassName();
167         if ($resolveFields === NULL) {
168             $resolveFields = $ownRecordClass::getResolveForeignIdFields();
169         }
170         
171         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
172             . ' Resolving ' . $ownRecordClass . ' fields: ' . print_r($resolveFields, TRUE));
173         
174         foreach ((array) $resolveFields as $foreignRecordClassName => $fields) {
175             if ($foreignRecordClassName === 'recursive') {
176                 foreach ($fields as $field => $model) {
177                     foreach ($records->$field as $subRecords) {
178                         self::resolveMultipleIdFields($subRecords);
179                     }
180                 }
181             } else {
182                 self::_resolveForeignIdFields($records, $foreignRecordClassName, (array) $fields);
183             }
184         }
185     }
186     
187     /**
188      * resolve foreign fields for records like user ids to users, etc.
189      * 
190      * @param Tinebase_Record_RecordSet $records
191      * @param string $foreignRecordClassName
192      * @param array $fields
193      */
194     protected static function _resolveForeignIdFields($records, $foreignRecordClassName, $fields)
195     {
196         $options = (isset($fields['options']) || array_key_exists('options', $fields)) ? $fields['options'] : array();
197         $fields = (isset($fields['fields']) || array_key_exists('fields', $fields)) ? $fields['fields'] : $fields;
198         
199         $foreignIds = array();
200         foreach ($fields as $field) {
201             $foreignIds = array_unique(array_merge($foreignIds, $records->{$field}));
202         }
203         
204         try {
205             $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
206         } catch (Tinebase_Exception_AccessDenied $tead) {
207             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
208                 . ' No right to access application of record ' . $foreignRecordClassName);
209             return;
210         }
211         
212         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
213             . ' Fetching ' . $foreignRecordClassName . ' by id: ' . print_r($foreignIds, TRUE));
214         
215         if ((isset($options['ignoreAcl']) || array_key_exists('ignoreAcl', $options)) && $options['ignoreAcl']) {
216             // @todo make sure that second param of getMultiple() is $ignoreAcl
217             $foreignRecords = $controller->getMultiple($foreignIds, TRUE);
218         } else {
219             $foreignRecords = $controller->getMultiple($foreignIds);
220         }
221         
222         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
223             . ' Foreign records found: ' . print_r($foreignRecords->toArray(), TRUE));
224         
225         if (count($foreignRecords) === 0) {
226             return;
227         }
228         
229         foreach ($records as $record) {
230             foreach ($fields as $field) {
231                 if (is_scalar($record->{$field})) {
232                     $idx = $foreignRecords->getIndexById($record->{$field});
233                     if (isset($idx) && $idx !== FALSE) {
234                         $record->{$field} = $foreignRecords[$idx];
235                     } else {
236                         switch ($foreignRecordClassName) {
237                             case 'Tinebase_Model_User':
238                             case 'Tinebase_Model_FullUser':
239                                 $record->{$field} = Tinebase_User::getInstance()->getNonExistentUser();
240                                 break;
241                             default:
242                                 // skip
243                         }
244                     }
245                 }
246             }
247         }
248     }
249     
250     /**
251      * resolve multiple record fields (Tinebase_ModelConfiguration._recordsFields)
252      * 
253      * @param Tinebase_Record_RecordSet $_records
254      * @param Tinebase_ModelConfiguration $modelConfiguration
255      * @param boolean $multiple
256      * @throws Tinebase_Exception_UnexpectedValue
257      */
258     protected function _resolveMultipleRecordFields(Tinebase_Record_RecordSet $_records, $modelConfiguration = NULL, $multiple = false)
259     {
260         if (! $modelConfiguration || (! $_records->count())) {
261             return;
262         }
263         
264         if (! ($resolveFields = $modelConfiguration->recordsFields)) {
265             return;
266         }
267         
268         $ownIds = $_records->{$modelConfiguration->idProperty};
269         
270         // iterate fields to resolve
271         foreach ($resolveFields as $fieldKey => $c) {
272             $config = $c['config'];
273             
274             // resolve records, if omitOnSearch is definitively set to FALSE (by default they won't be resolved on search)
275             if ($multiple && !(isset($config['omitOnSearch']) && $config['omitOnSearch'] === FALSE)) {
276                 continue;
277             }
278
279             if (! isset($config['controllerClassName'])) {
280                 throw new Tinebase_Exception_UnexpectedValue('Controller class name needed');
281             }
282
283             // fetch the fields by the refIfField
284             $controller = $config['controllerClassName']::getInstance();
285             $filterName = $config['filterClassName'];
286             
287             $filterArray = array();
288             
289             // addFilters can be added and must be added if the same model resides in more than one records fields
290             if (isset($config['addFilters']) && is_array($config['addFilters'])) {
291                 $filterArray = $config['addFilters'];
292             }
293             
294             $filter = new $filterName($filterArray);
295             $filter->addFilter(new Tinebase_Model_Filter_Id(array('field' => $config['refIdField'], 'operator' => 'in', 'value' => $ownIds)));
296             
297             $paging = NULL;
298             if (isset($config['paging']) && is_array($config['paging'])) {
299                 $paging = new Tinebase_Model_Pagination($config['paging']);
300             }
301             
302             $foreignRecords = $controller->search($filter, $paging);
303             $foreignRecordClass = $foreignRecords->getRecordClassName();
304             $foreignRecordModelConfiguration = $foreignRecordClass::getConfiguration();
305             
306             $foreignRecords->setTimezone(Tinebase_Core::getUserTimezone());
307             $foreignRecords->convertDates = true;
308             
309             $fr = $foreignRecords->getFirstRecord();
310
311             // @todo: resolve alarms?
312             // @todo: use parts parameter?
313             if ($foreignRecordModelConfiguration->resolveRelated && $fr) {
314                 if ($fr->has('notes')) {
315                     Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
316                 }
317                 if ($fr->has('tags')) {
318                     Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($foreignRecords);
319                 }
320                 if ($fr->has('relations')) {
321                     $relations = Tinebase_Relations::getInstance()->getMultipleRelations($foreignRecordClass, 'Sql', $foreignRecords->{$fr->getIdProperty()} );
322                     $foreignRecords->setByIndices('relations', $relations);
323                 }
324                 if ($fr->has('customfields')) {
325                     Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($foreignRecords);
326                 }
327                 if ($fr->has('attachments') && Tinebase_Core::isFilesystemAvailable()) {
328                     Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($foreignRecords);
329                 }
330             }
331             
332             if ($foreignRecords->count() > 0) {
333                 foreach ($_records as $record) {
334                     $filtered = $foreignRecords->filter($config['refIdField'], $record->getId())->toArray();
335                     $filtered = $this->_resolveAfterToArray($filtered, $foreignRecordModelConfiguration, TRUE);
336                     $record->{$fieldKey} = $filtered;
337                 }
338                 
339             } else {
340                 $_records->{$fieldKey} = NULL;
341             }
342         }
343         
344     }
345     
346     /**
347      * resolves virtual fields, if a function has been defined in the field definition
348      * 
349      * @param array $resultSet
350      * @param Tinebase_ModelConfiguration $modelConfiguration
351      * @param boolean $multiple
352      */
353     protected function _resolveVirtualFields($resultSet, $modelConfiguration = NULL, $multiple = false)
354     {
355         if (! $modelConfiguration || ! ($virtualFields = $modelConfiguration->virtualFields)) {
356             return $resultSet;
357         }
358         
359         if ($modelConfiguration->resolveVFGlobally === TRUE) {
360             
361             $controller = $modelConfiguration->getControllerInstance();
362             
363             if ($multiple) {
364                 return $controller->resolveMultipleVirtualFields($resultSet);
365             }
366             return $controller->resolveVirtualFields($resultSet);
367         }
368         
369         foreach ($virtualFields as $field) {
370             // resolve virtual relation record from relations property
371             if (! $multiple && isset($field['type']) && $field['type'] == 'relation') {
372                 $fc = $field['config'];
373                 if (isset($resultSet['relations']) && (is_array($resultSet['relations']))) {
374                     foreach($resultSet['relations'] as $relation) {
375                         if (($relation['type'] == $fc['type']) && ($relation['related_model'] == ($fc['appName'] . '_Model_' . $fc['modelName']))) {
376                             $resultSet[$field['key']] = $relation['related_record'];
377                         }
378                     }
379                 }
380             // resolve virtual field by function
381             } else if ((isset($field['function']) || array_key_exists('function', $field))) {
382                 if (is_array($field['function'])) {
383                     if (count($field['function']) > 1) { // static method call
384                         $class  = $field['function'][0];
385                         $method = $field['function'][1];
386                         $resultSet = $class::$method($resultSet);
387
388                     } else { // use key as classname and value as method name
389                         $ks = array_keys($field['function']);
390                         $class  = array_pop($ks);
391                         $vs = array_values($field['function']);
392                         $method = array_pop($vs);
393                         $class = $class::getInstance();
394                         
395                         $resultSet = $class->$method($resultSet);
396                         
397                     }
398                 // if no array has been given, this should be a function name
399                 } else {
400                     $resolveFunction = $field['function'];
401                     $resultSet = $resolveFunction($resultSet);
402                 }
403             }
404         }
405         
406         return $resultSet;
407     }
408     
409     /**
410      * resolves child records before converting the record set to an array
411      * 
412      * @param Tinebase_Record_RecordSet $records
413      * @param Tinebase_ModelConfiguration $modelConfiguration
414      * @param boolean $multiple
415      */
416     protected function _resolveBeforeToArray($records, $modelConfiguration, $multiple = false)
417     {
418         Tinebase_Frontend_Json_Abstract::resolveContainersAndTags($records);
419         
420         self::resolveMultipleIdFields($records);
421         
422         // use modern record resolving, if the model was configured using Tinebase_ModelConfiguration
423         // at first, resolve all single record fields
424         if ($modelConfiguration) {
425             $this->_resolveSingleRecordFields($records, $modelConfiguration);
426         
427             // resolve all multiple records fields
428             $this->_resolveMultipleRecordFields($records, $modelConfiguration, $multiple);
429         }
430     }
431     
432     /**
433      * resolves child records after converting the record set to an array
434      * 
435      * @param array $result
436      * @param Tinebase_ModelConfiguration $modelConfiguration
437      * @param boolean $multiple
438      * 
439      * @return array
440      */
441     protected function _resolveAfterToArray($result, $modelConfiguration, $multiple = false)
442     {
443         $result = $this->_resolveVirtualFields($result, $modelConfiguration, $multiple);
444         return $result;
445     }
446     
447     /**
448      * converts Tinebase_Record_RecordSet to external format
449      * 
450      * @param Tinebase_Record_RecordSet  $_records
451      * @param Tinebase_Model_Filter_FilterGroup $_filter
452      * @param Tinebase_Model_Pagination $_pagination
453      * 
454      * @return mixed
455      */
456     public function fromTine20RecordSet(Tinebase_Record_RecordSet $_records = NULL, $_filter = NULL, $_pagination = NULL)
457     {
458         if (! $_records || count($_records) == 0) {
459             return array();
460         }
461         
462         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
463             . ' Processing ' . count($_records) . ' records ...');
464         
465         // find out if there is a modelConfiguration
466         $ownRecordClass = $_records->getRecordClassName();
467         $config = $ownRecordClass::getConfiguration();
468         
469         $this->_resolveBeforeToArray($_records, $config, TRUE);
470         
471         $_records->setTimezone(Tinebase_Core::getUserTimezone());
472         $_records->convertDates = true;
473         
474         $result = $_records->toArray();
475         
476         // resolve all virtual fields after converting to array, so we can add these properties "virtually"
477         $result = $this->_resolveAfterToArray($result, $config, TRUE);
478         
479         return $result;
480     }
481 }