fix resolving of customfield record values
[tine20] / tine20 / Tinebase / Model / Filter / CustomField.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Filter
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Philipp Schuele <p.schuele@metaways.de>
10  */
11
12 /**
13  * Tinebase_Model_Filter_CustomField
14  * 
15  * filters by given customfield name/value
16  * 
17  * a custom field filter is constructed like this:
18  * 
19  *  array(
20  *     'field' => 'customfield', 
21  *     'operator' => 'contains', 
22  *     'value' => array('cfId' => '1234', 'value' => 'searchstring')
23  *  ),
24  * 
25  * @package     Tinebase
26  * @subpackage  Filter
27  *
28  * @refactor this has some issues with the different cf types
29  */
30 class Tinebase_Model_Filter_CustomField extends Tinebase_Model_Filter_Abstract
31 {
32     /**
33      * the filter used for querying the customfields table
34      * 
35      * @var Tinebase_Model_Filter_Abstract
36      */
37     protected $_subFilter = NULL;
38     
39     /**
40      * possible operators
41      * 
42      * @var array
43      */
44     protected $_operators = NULL;
45     
46     /**
47      * the customfield record
48      * 
49      * @var Tinebase_Model_CustomField_Config
50      */
51     protected $_cfRecord  = NULL;
52     
53     /**
54      * get a new single filter action
55      *
56      * @param string|array $_fieldOrData
57      * @param string $_operator
58      * @param mixed  $_value    
59      * @param array  $_options
60      */
61     public function __construct($_fieldOrData, $_operator = NULL, $_value = NULL, array $_options = array())
62     {
63         // no legacy handling
64         if(!is_array($_fieldOrData)) {
65             throw new Tinebase_Exception_InvalidArgument('$_fieldOrDatamust be an array!');
66         }
67
68         $valueFilter = null;
69         $be = new Tinebase_CustomField_Config();
70         $this->_cfRecord = $be->get($_fieldOrData['value']['cfId']);
71         $type = $this->_cfRecord->definition['type'];
72         if ($type == 'date' || $type == 'datetime') {
73             $this->_subFilter = new Tinebase_Model_CustomField_ValueFilter(array());
74             $this->_subFilter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'customfield_id', 'operator' => 'equals', 'value' => $_fieldOrData['value']['cfId'])));
75             $valueFilter = new Tinebase_Model_Filter_Date(array('field' => 'value', 'operator' => $_fieldOrData['operator'], 'value' => $_fieldOrData['value']['value']));
76             $this->_subFilter->addFilter($valueFilter);
77         } elseif ($type == 'integer') {
78             $valueFilter = new Tinebase_Model_Filter_Int($_fieldOrData, $_operator, $_value, $_options);
79         } else if ($type == 'record' && is_array($_fieldOrData['value']['value'])) {
80             $modelName = Tinebase_CustomField::getModelNameFromDefinition($this->_cfRecord->definition);
81             $this->_subFilterController = Tinebase_Core::getApplicationInstance($modelName);
82             $this->_subFilter = Tinebase_Model_Filter_FilterGroup::getFilterForModel($modelName);
83         } else if ($type == 'records') {
84             // TODO support recordset
85             throw new Tinebase_Exception_NotImplemented('filter for records type not implemented yet');
86         } else {
87             $valueFilter = new Tinebase_Model_Filter_Text($_fieldOrData, $_operator, $_value, $_options);
88         }
89
90         if ($valueFilter) {
91             $this->_valueFilter = $valueFilter;
92             $this->_operators = $valueFilter->getOperators();
93             $this->_opSqlMap = $this->_valueFilter->getOpSqlMap();
94         } else {
95             $this->_operators = array('AND', 'OR');
96             $this->_opSqlMap = array();
97         }
98         parent::__construct($_fieldOrData, $_operator, $_value, $_options);
99     }
100     
101     /**
102      * set options 
103      *
104      * @param  array $_options
105      */
106     protected function _setOptions(array $_options)
107     {
108         $_options['idProperty'] = isset($_options['idProperty']) ? $_options['idProperty'] : 'id';
109         
110         $this->_options = $_options;
111     }
112     
113     /**
114      * appends sql to given select statement
115      *
116      * @param  Zend_Db_Select                $_select
117      * @param  Tinebase_Backend_Sql_Abstract $_backend
118      * @throws Tinebase_Exception_UnexpectedValue
119      */
120     public function appendFilterSql($_select, $_backend)
121     {
122         // don't take empty filter into account
123         if (     empty($this->_value)          || ! is_array($this->_value)    || ! isset($this->_value['cfId'])  || empty($this->_value['cfId']) 
124             || ! isset($this->_value['value'])) 
125         {
126             return;
127         } else if ($this->_cfRecord->definition['type'] !== 'record' && $this->_operator == 'in') {
128             throw new Tinebase_Exception_UnexpectedValue('Operator "in" not supported.');
129         }
130         
131         // make sure $correlationName is a string
132         $correlationName = Tinebase_Record_Abstract::generateUID(30);
133         
134         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
135             ' Adding custom field filter: ' . print_r($this->_value, true));
136         
137         $db = Tinebase_Core::getDb();
138         $idProperty = $db->quoteIdentifier($this->_options['idProperty']);
139         
140         // per left join we add a customfield column named as the customfield and filter this joined column
141         // NOTE: we name the column we join like the customfield, to be able to join multiple customfield criteria (multiple invocations of this function)
142         $what = array($correlationName => SQL_TABLE_PREFIX . 'customfield');
143         $on = $db->quoteIdentifier("{$correlationName}.record_id")      . " = $idProperty AND " 
144             . $db->quoteIdentifier("{$correlationName}.customfield_id") . " = " . $db->quote($this->_value['cfId']);
145         $_select->joinLeft($what, $on, array());
146
147         $valueIdentifier = $db->quoteIdentifier("{$correlationName}.value");
148         $value = $this->_value['value'];
149         $operator = $this->_operator;
150
151         switch ($this->_cfRecord->definition['type']) {
152             case 'date':
153             case 'datetime':
154                 $customfields = Tinebase_CustomField::getInstance()->search($this->_subFilter);
155                 if ($customfields->count()) {
156                     $where = $db->quoteInto($idProperty . ' IN (?) ', $customfields->record_id);
157                 } else {
158                     $where = '1=2';
159                 }
160                 break;
161             default:
162                 if ($this->_cfRecord->definition['type'] === 'record' && is_array($value)) {
163                     $this->_subFilter->setFromArray($value);
164                     $ids = $this->_subFilterController->search($this->_subFilter, null, /*relations */
165                         false, /* only ids */
166                         true);
167                     if (count($ids)) {
168                         $where = $db->quoteInto($valueIdentifier . ' IN (?) ', $ids);
169                     } else {
170                         $where = '1=2';
171                     }
172                 } else if (! $value) {
173                     $where = $db->quoteInto($valueIdentifier. ' IS NULL OR ' . $valueIdentifier . ' = ?', $value);
174                 } else {
175                     $value = $this->_replaceWildcards($value);
176                     if (($this->_cfRecord->definition['type'] == 'keyField' || $this->_cfRecord->definition['type'] == 'record')
177                         && $operator == 'not') {
178                         $where = $db->quoteInto($valueIdentifier . ' IS NULL OR ' . $valueIdentifier . $this->_opSqlMap[$operator]['sqlop'], $value);
179                     } else {
180                         $where = $db->quoteInto($valueIdentifier . $this->_opSqlMap[$operator]['sqlop'], $value);
181                     }
182                 }
183         }
184         $_select->where($where);
185     }
186     
187     /**
188      * returns array with the filter settings of this filter
189      *
190      * @param  bool $valueToJson resolve value for json api?
191      * @return array
192      */
193     public function toArray($valueToJson = false)
194     {
195         $result = parent::toArray($valueToJson);
196         if (strtolower($this->_cfRecord->definition['type']) == 'record') {
197             try {
198                 $modelName = Tinebase_CustomField::getModelNameFromDefinition($this->_cfRecord->definition);
199                 $controller = Tinebase_Core::getApplicationInstance($modelName);
200                 if (is_string($result['value']['value'])) {
201                     $result['value']['value'] = $controller->get($result['value']['value'])->toArray();
202                 } else if (is_array($result['value']['value'])) {
203                     //  this is very bad - @refactor
204                     foreach ($result['value']['value'] as $key => $subfilter) {
205                         if (isset($result['value']['value'][$key]['value']) && is_string($result['value']['value'][$key]['value']))
206                         $result['value']['value'][$key]['value'] = $controller->get($result['value']['value'][$key]['value'])->toArray();
207                     }
208
209                 } else {
210                     // TODO do we need to do something in this case?
211                 }
212             } catch (Exception $e) {
213                 if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Error resolving custom field record: ' . $e->getMessage());
214             }
215         }
216         
217         return $result;
218     }
219 }