performance improvements for Tinebase_Convert_Json
[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::get(Tinebase_Core::USERTIMEZONE));
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
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             foreach ($resolveFields as $fieldKey => $fieldConfig) {
79                 $resolveRecords[$fieldConfig['config']['recordClassName']][] = $fieldKey;
80             }
81             
82             foreach ($resolveRecords as $foreignRecordClassName => $fields) {
83                 $foreignIds = array();
84                 $fields = (array) $fields;
85                 
86                 foreach($fields as $field) {
87                     $foreignIds = array_unique(array_merge($foreignIds, $_records->{$field}));
88                 }
89                 
90                 if (! Tinebase_Core::getUser()->hasRight(substr($foreignRecordClassName, 0, strpos($foreignRecordClassName, "_")), Tinebase_Acl_Rights_Abstract::RUN)) {
91                     continue;
92                 }
93                 
94                 $cfg = $resolveFields[$fields[0]];
95                 
96                 if ($cfg['type'] == 'user') {
97                     $foreignRecords = Tinebase_User::getInstance()->getUsers();
98                 } elseif ($cfg['type'] == 'container') {
99                     $foreignRecords = new Tinebase_Record_RecordSet('Tinebase_Model_Container');
100                     $foreignRecords->addRecord(Tinebase_Container::getInstance()->get($_id));
101                 // TODO: resolve recursive records of records better in controller
102                 // TODO: resolve containers
103                 } else {
104                     $controller = (isset($cfg['config']['controllerClassName']) || array_key_exists('controllerClassName', $cfg['config'])) ? $cfg['config']['controllerClassName']::getInstance() : Tinebase_Core::getApplicationInstance($foreignRecordClassName);
105                     $foreignRecords = $controller->getMultiple($foreignIds);
106                 }
107                 
108                 $foreignRecords->setTimezone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
109                 $foreignRecords->convertDates = true;
110                 Tinebase_Frontend_Json_Abstract::resolveContainerTagsUsers($foreignRecords);
111                 $fr = $foreignRecords->getFirstRecord();
112                 if ($fr && $fr->has('notes')) {
113                     Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
114                 }
115                 
116                 if ($foreignRecords->count()) {
117                     foreach ($_records as $record) {
118                         foreach ($fields as $field) {
119                             if (is_scalar($record->{$field})) {
120                                 $idx = $foreignRecords->getIndexById($record->{$field});
121                                 if (isset($idx) && $idx !== FALSE) {
122                                     $record->{$field} = $foreignRecords[$idx];
123                                 }
124                             }
125                         }
126                     }
127                 }
128             }
129         }
130     }
131     
132     /**
133      * resolves multiple records (fallback)
134      * 
135      * @deprecated use Tinebase_ModelConfiguration to configure your models, so this won't be used anymore 
136      * @param Tinebase_Record_RecordSet $_records the records
137      * @param array $resolveFields
138      */
139     public static function resolveMultipleIdFields($records, $resolveFields = NULL)
140     {
141         if (! $records instanceof Tinebase_Record_RecordSet || !$records->count()) {
142             return;
143         }
144         
145         $ownRecordClass = $records->getRecordClassName();
146         if ($resolveFields === NULL) {
147             $resolveFields = $ownRecordClass::getResolveForeignIdFields();
148         }
149         
150         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
151             . ' Resolving ' . $ownRecordClass . ' fields: ' . print_r($resolveFields, TRUE));
152         
153         foreach ((array) $resolveFields as $foreignRecordClassName => $fields) {
154             if ($foreignRecordClassName === 'recursive') {
155                 foreach ($fields as $field => $model) {
156                     foreach ($records->$field as $subRecords) {
157                         self::resolveMultipleIdFields($subRecords);
158                     }
159                 }
160             } else {
161                 self::_resolveForeignIdFields($records, $foreignRecordClassName, (array) $fields);
162             }
163         }
164     }
165     
166     /**
167      * resolve foreign fields for records
168      * 
169      * @param Tinebase_Record_RecordSet $records
170      * @param string $foreignRecordClassName
171      * @param array $fields
172      */
173     protected static function _resolveForeignIdFields($records, $foreignRecordClassName, $fields)
174     {
175         $options = (isset($fields['options']) || array_key_exists('options', $fields)) ? $fields['options'] : array();
176         $fields = (isset($fields['fields']) || array_key_exists('fields', $fields)) ? $fields['fields'] : $fields;
177         
178         $foreignIds = array();
179         foreach ($fields as $field) {
180             $foreignIds = array_unique(array_merge($foreignIds, $records->{$field}));
181         }
182         
183         try {
184             $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
185         } catch (Tinebase_Exception_AccessDenied $tead) {
186             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
187                 . ' Not resolving ' . $foreignRecordClassName . ' records because user has no right to run app.');
188             return;
189         }
190         
191         // 2013-07-04, ps: removing this as i don't think that this is correct here
192         // (and it breaks the tests)
193 //         if (method_exists($controller, 'modlogActive')) {
194 //             $modlogActive = $controller->modlogActive(FALSE);
195 //         }
196         
197         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
198             . ' Fetching ' . $foreignRecordClassName . ' by id: ' . print_r($foreignIds, TRUE));
199         
200         if ((isset($options['ignoreAcl']) || array_key_exists('ignoreAcl', $options)) && $options['ignoreAcl']) {
201             // @todo make sure that second param of getMultiple() is $ignoreAcl
202             $foreignRecords = $controller->getMultiple($foreignIds, TRUE);
203         } else {
204             $foreignRecords = $controller->getMultiple($foreignIds);
205         }
206         
207         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
208             . ' Foreign records found: ' . print_r($foreignRecords->toArray(), TRUE));
209         
210         if (count($foreignRecords) === 0) {
211             return;
212         }
213         
214         foreach ($records as $record) {
215             foreach ($fields as $field) {
216                 if (is_scalar($record->{$field})) {
217                     $idx = $foreignRecords->getIndexById($record->{$field});
218                     if (isset($idx) && $idx !== FALSE) {
219                         $record->{$field} = $foreignRecords[$idx];
220                     } else {
221                         switch ($foreignRecordClassName) {
222                             case 'Tinebase_Model_User':
223                             case 'Tinebase_Model_FullUser':
224                                 $record->{$field} = Tinebase_User::getInstance()->getNonExistentUser();
225                                 break;
226                             default:
227                                 // skip
228                         }
229                     }
230                 }
231             }
232         }
233     }
234     
235     /**
236      * resolve multiple record fields (Tinebase_ModelConfiguration._recordsFields)
237      * 
238      * @param Tinebase_Record_RecordSet $_records
239      * @param Tinebase_ModelConfiguration $modelConfiguration
240      * @param boolean $multiple
241      */
242     protected function _resolveMultipleRecordFields(Tinebase_Record_RecordSet $_records, $modelConfiguration = NULL, $multiple = false)
243     {
244         if (! $modelConfiguration || (! $_records->count())) {
245             return;
246         }
247         
248         if (! ($resolveFields = $modelConfiguration->recordsFields)) {
249             return;
250         }
251         
252         $ownIds = $_records->{$modelConfiguration->idProperty};
253         
254         // iterate fields to resolve
255         foreach ($resolveFields as $fieldKey => $c) {
256             $config = $c['config'];
257             
258             // resolve records, if omitOnSearch is definitively set to FALSE (by default they won't be resolved on search)
259             if ($multiple && !(isset($config['omitOnSearch']) && $config['omitOnSearch'] === FALSE)) {
260                 continue;
261             }
262             
263             // fetch the fields by the refIfField
264             $controller = isset($config['controllerClassName']) ? $config['controllerClassName']::getInstance() : Tinebase_Core::getApplicationInstance($foreignRecordClassName);
265             $filterName = $config['filterClassName'];
266             
267             $filterArray = array();
268             
269             // addFilters can be added and must be added if the same model resides in more than one records fields
270             if (isset($config['addFilters']) && is_array($config['addFilters'])) {
271                 $useaddFilters = true;
272                 $filterArray = $config['addFilters'];
273             }
274             
275             $filter = new $filterName($filterArray);
276             $filter->addFilter(new Tinebase_Model_Filter_Id(array('field' => $config['refIdField'], 'operator' => 'in', 'value' => $ownIds)));
277             
278             $paging = NULL;
279             if (isset($config['paging']) && is_array($config['paging'])) {
280                 $paging = new Tinebase_Model_Pagination($config['paging']);
281             }
282             
283             $foreignRecords = $controller->search($filter, $paging);
284             $foreignRecordClass = $foreignRecords->getRecordClassName();
285             $foreignRecordModelConfiguration = $foreignRecordClass::getConfiguration();
286             
287             $foreignRecords->setTimezone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
288             $foreignRecords->convertDates = true;
289             
290             $fr = $foreignRecords->getFirstRecord();
291
292             // @todo: resolve alarms?
293             // @todo: use parts parameter?
294             if ($foreignRecordModelConfiguration->resolveRelated && $fr) {
295                 if ($fr->has('notes')) {
296                     Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($foreignRecords);
297                 }
298                 if ($fr->has('tags')) {
299                     Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($foreignRecords);
300                 }
301                 if ($fr->has('relations')) {
302                     $relations = Tinebase_Relations::getInstance()->getMultipleRelations($foreignRecordClass, 'Sql', $foreignRecords->{$fr->getIdProperty()} );
303                     $foreignRecords->setByIndices('relations', $relations);
304                 }
305                 if ($fr->has('customfields')) {
306                     Tinebase_CustomField::getInstance()->resolveMultipleCustomfields($foreignRecords);
307                 }
308                 if ($fr->has('attachments') && Setup_Controller::getInstance()->isFilesystemAvailable()) {
309                     Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($foreignRecords);
310                 }
311             }
312             
313             if ($foreignRecords->count() > 0) {
314                 foreach ($_records as $record) {
315                     $filtered = $foreignRecords->filter($config['refIdField'], $record->getId())->toArray();
316                     $filtered = $this->_resolveAfterToArray($filtered, $foreignRecordModelConfiguration, TRUE);
317                     $record->{$fieldKey} = $filtered;
318                 }
319                 
320             } else {
321                 $_records->{$fieldKey} = NULL;
322             }
323         }
324         
325     }
326     
327     /**
328      * resolves virtual fields, if a function has been defined in the field definition
329      * 
330      * @param array $resultSet
331      * @param Tinebase_ModelConfiguration $modelConfiguration
332      * @param boolean $multiple
333      */
334     protected function _resolveVirtualFields($resultSet, $modelConfiguration = NULL, $multiple = false)
335     {
336         if (! $modelConfiguration || ! ($virtualFields = $modelConfiguration->virtualFields)) {
337             return $resultSet;
338         }
339         
340         if ($modelConfiguration->resolveVFGlobally === TRUE) {
341             
342             $controller = $modelConfiguration->getControllerInstance();
343             
344             if ($multiple) {
345                 return $controller->resolveMultipleVirtualFields($resultSet);
346             }
347             return $controller->resolveVirtualFields($resultSet);
348         }
349         
350         foreach($virtualFields as $field) {
351             // resolve virtual relation record from relations property
352             if (! $multiple && isset($field['type']) && $field['type'] == 'relation') {
353                 $fc = $field['config'];
354                 if (isset($resultSet['relations']) && (is_array($resultSet['relations']))) {
355                     foreach($resultSet['relations'] as $relation) {
356                         if (($relation['type'] == $fc['type']) && ($relation['related_model'] == ($fc['appName'] . '_Model_' . $fc['modelName']))) {
357                             $resultSet[$field['key']] = $relation['related_record'];
358                         }
359                     }
360                 }
361             // resolve virtual field by function
362             } elseif ((isset($field['function']) || array_key_exists('function', $field))) {
363                 if (is_array($field['function'])) {
364                     if (count($field['function']) > 1) { // static method call
365                         $class  = $field['function'][0];
366                         $method = $field['function'][1];
367                         $resultSet = $class::$method($resultSet);
368
369                     } else { // use key as classname and value as method name
370                         $ks = array_keys($field['function']);
371                         $class  = array_pop($ks);
372                         $vs = array_values($field['function']);
373                         $method = array_pop($vs);
374                         $class = $class::getInstance();
375                         
376                         $resultSet = $class->$method($resultSet);
377                         
378                     }
379                 // if no array has been given, this should be a function name
380                 } else {
381                     $resolveFunction = $field['function'];
382                     $resultSet = $resolveFunction($resultSet);
383                 }
384             }
385         }
386         
387         return $resultSet;
388     }
389     
390     /**
391      * resolves child records before converting the record set to an array
392      * 
393      * @param Tinebase_Record_RecordSet $records
394      * @param Tinebase_ModelConfiguration $modelConfiguration
395      * @param boolean $multiple
396      */
397     protected function _resolveBeforeToArray($records, $modelConfiguration, $multiple = false)
398     {
399         Tinebase_Frontend_Json_Abstract::resolveContainerTagsUsers($records);
400         
401         self::resolveMultipleIdFields($records);
402         
403         // use modern record resolving, if the model was configured using Tinebase_ModelConfiguration
404         // at first, resolve all single record fields
405         if ($modelConfiguration) {
406             $this->_resolveSingleRecordFields($records, $modelConfiguration);
407         
408             // resolve all multiple records fields
409             $this->_resolveMultipleRecordFields($records, $modelConfiguration, $multiple);
410         }
411     }
412     
413     /**
414      * resolves child records after converting the record set to an array
415      * 
416      * @param array $result
417      * @param Tinebase_ModelConfiguration $modelConfiguration
418      * @param boolean $multiple
419      * 
420      * @return array
421      */
422     protected function _resolveAfterToArray($result, $modelConfiguration, $multiple = false)
423     {
424         $result = $this->_resolveVirtualFields($result, $modelConfiguration, $multiple);
425         return $result;
426     }
427     
428     /**
429      * converts Tinebase_Record_RecordSet to external format
430      * 
431      * @param Tinebase_Record_RecordSet  $_records
432      * @param Tinebase_Model_Filter_FilterGroup $_filter
433      * @param Tinebase_Model_Pagination $_pagination
434      * 
435      * @return mixed
436      */
437     public function fromTine20RecordSet(Tinebase_Record_RecordSet $_records = NULL, $_filter = NULL, $_pagination = NULL)
438     {
439         if (! $_records || count($_records) == 0) {
440             return array();
441         }
442         
443         // find out if there is a modelConfiguration
444         $ownRecordClass = $_records->getRecordClassName();
445         $config = $ownRecordClass::getConfiguration();
446         
447         $this->_resolveBeforeToArray($_records, $config, TRUE);
448         
449         $_records->setTimezone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
450         $_records->convertDates = true;
451
452         $result = $_records->toArray();
453         
454         // resolve all virtual fields after converting to array, so we can add these properties "virtually"
455         $result = $this->_resolveAfterToArray($result, $config, TRUE);
456
457         return $result;
458     }
459 }