1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
25:
26: namespace DB;
27: use DB\SQL\Schema;
28:
29: class Cortex extends Cursor {
30:
31: protected
32:
33: $db,
34: $table,
35: $fluid,
36: $fieldConf,
37: $ttl,
38: $rel_ttl,
39: $primary,
40:
41: $smartLoading,
42: $standardiseID,
43:
44: $dbsType,
45: $fieldsCache,
46: $saveCsd,
47: $collection,
48: $relFilter,
49: $hasCond,
50: $whitelist,
51: $relWhitelist,
52: $grp_stack,
53: $countFields,
54: $preBinds,
55: $vFields,
56: $_ttl;
57:
58:
59: protected $mapper;
60:
61:
62: protected $queryParser;
63:
64: static
65: $init = false;
66:
67: const
68:
69: DT_SERIALIZED = 'SERIALIZED',
70: DT_JSON = 'JSON',
71:
72:
73: E_ARRAY_DATATYPE = 'Unable to save an Array in field %s. Use DT_SERIALIZED or DT_JSON.',
74: E_CONNECTION = 'No valid DB Connection given.',
75: E_NO_TABLE = 'No table specified.',
76: E_UNKNOWN_DB_ENGINE = 'This unknown DB system is not supported: %s',
77: E_FIELD_SETUP = 'No field setup defined',
78: E_UNKNOWN_FIELD = 'Field %s does not exist in %s.',
79: E_INVALID_RELATION_OBJECT = 'You can only save hydrated mapper objects',
80: E_NULLABLE_COLLISION = 'Unable to set NULL to the NOT NULLABLE field: %s',
81: E_WRONG_RELATION_CLASS = 'Relations only works with Cortex objects',
82: E_MM_REL_VALUE = 'Invalid value for many field "%s". Expecting null, split-able string, hydrated mapper object, or array of mapper objects.',
83: E_MM_REL_CLASS = 'Mismatching m:m relation config from class `%s` to `%s`.',
84: E_MM_REL_FIELD = 'Mismatching m:m relation keys from `%s` to `%s`.',
85: E_REL_CONF_INC = 'Incomplete relation config for `%s`. Linked key is missing.',
86: E_MISSING_REL_CONF = 'Cannot create related model. Specify a model name or relConf array.',
87: E_HAS_COND = 'Cannot use a "has"-filter on a non-bidirectional relation field';
88:
89: 90: 91: 92: 93: 94: 95:
96: public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0)
97: {
98: if (!is_null($fluid))
99: $this->fluid = $fluid;
100: if (!is_object($this->db=(is_string($db=($db?:$this->db))?\Base::instance()->get($db):$db)))
101: trigger_error(self::E_CONNECTION);
102: if ($this->db instanceof Jig)
103: $this->dbsType = 'jig';
104: elseif ($this->db instanceof SQL)
105: $this->dbsType = 'sql';
106: elseif ($this->db instanceof Mongo)
107: $this->dbsType = 'mongo';
108: if ($table)
109: $this->table = $table;
110: if (!$this->primary || $this->dbsType != 'sql')
111: $this->primary = 'id';
112: if (!$this->table && !$this->fluid)
113: trigger_error(self::E_NO_TABLE);
114: $this->ttl = $ttl ?: 60;
115: if (!$this->rel_ttl)
116: $this->rel_ttl = 0;
117: $this->_ttl = $this->rel_ttl ?: 0;
118: if (static::$init == TRUE) return;
119: if ($this->fluid)
120: static::setup($this->db,$this->getTable(),array());
121: $this->initMapper();
122: }
123:
124: 125: 126:
127: public function initMapper()
128: {
129: switch ($this->dbsType) {
130: case 'jig':
131: $this->mapper = new Jig\Mapper($this->db, $this->table);
132: break;
133: case 'sql':
134: $this->mapper = new SQL\Mapper($this->db, $this->table, $this->whitelist,
135: ($this->fluid)?0:$this->ttl);
136: break;
137: case 'mongo':
138: $this->mapper = new Mongo\Mapper($this->db, $this->table);
139: break;
140: default:
141: trigger_error(sprintf(self::E_UNKNOWN_DB_ENGINE,$this->dbsType));
142: }
143: $this->queryParser = CortexQueryParser::instance();
144: $this->reset();
145: $this->clearFilter();
146: $f3 = \Base::instance();
147: $this->smartLoading = $f3->exists('CORTEX.smartLoading') ?
148: $f3->get('CORTEX.smartLoading') : TRUE;
149: $this->standardiseID = $f3->exists('CORTEX.standardiseID') ?
150: $f3->get('CORTEX.standardiseID') : TRUE;
151: if(!empty($this->fieldConf))
152: foreach($this->fieldConf as &$conf) {
153: $conf=static::resolveRelationConf($conf);
154: unset($conf);
155: }
156: }
157:
158: 159: 160: 161: 162: 163:
164: public function fields(array $fields=array(), $exclude=false)
165: {
166: if ($fields)
167:
168: foreach($fields as $i=>$val)
169: if(is_int(strpos($val,'.'))) {
170: list($key, $relField) = explode('.',$val,2);
171: $this->relWhitelist[$key][(int)$exclude][] = $relField;
172: unset($fields[$i]);
173: }
174: $schema = $this->whitelist ?: $this->mapper->fields();
175: if (!$schema && !$this->dbsType != 'sql' && $this->dry()) {
176: $schema = $this->load()->mapper->fields();
177: $this->reset();
178: }
179: if (!$this->whitelist && $this->fieldConf)
180: $schema=array_unique(array_merge($schema,array_keys($this->fieldConf)));
181: if (!$fields || empty($fields))
182: return $schema;
183: elseif ($exclude) {
184: $this->whitelist=array_diff($schema,$fields);
185: } else
186: $this->whitelist=$fields;
187: $id=$this->dbsType=='sql'?$this->primary:'_id';
188: if(!in_array($id,$this->whitelist))
189: $this->whitelist[]=$id;
190: $this->initMapper();
191: return $this->whitelist;
192: }
193:
194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205:
206: function setFieldConfiguration(array $config)
207: {
208: $this->fieldConf = $config;
209: $this->reset();
210: }
211:
212: 213: 214: 215:
216: public function getFieldConfiguration()
217: {
218: return $this->fieldConf;
219: }
220:
221: 222: 223: 224:
225: static public function resolveConfiguration()
226: {
227: static::$init=true;
228: $self = new static();
229: static::$init=false;
230: $conf = array (
231: 'table'=>$self->getTable(),
232: 'fieldConf'=>$self->getFieldConfiguration(),
233: 'db'=>$self->db,
234: 'fluid'=>$self->fluid,
235: 'primary'=>$self->primary,
236: );
237: unset($self);
238: return $conf;
239: }
240:
241: 242: 243: 244:
245: public function addToCollection($cx) {
246: $this->collection = $cx;
247: }
248:
249: 250: 251: 252:
253: protected function getCollection()
254: {
255: return ($this->collection && $this->smartLoading)
256: ? $this->collection : false;
257: }
258:
259: 260: 261: 262:
263: public function getTable()
264: {
265: if (!$this->table && $this->fluid)
266: $this->table = strtolower(get_class($this));
267: return $this->table;
268: }
269:
270: 271: 272: 273: 274: 275: 276: 277:
278: static public function setup($db=null, $table=null, $fields=null)
279: {
280:
281: $self = get_called_class();
282: if (is_null($db) || is_null($table) || is_null($fields))
283: $df = $self::resolveConfiguration();
284: if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db)))
285: trigger_error(self::E_CONNECTION);
286: if (strlen($table=$table?:$df['table'])==0)
287: trigger_error(self::E_NO_TABLE);
288: if (is_null($fields))
289: if (!empty($df['fieldConf']))
290: $fields = $df['fieldConf'];
291: elseif(!$df['fluid']) {
292: trigger_error(self::E_FIELD_SETUP);
293: return false;
294: } else
295: $fields = array();
296: if ($db instanceof SQL) {
297: $schema = new Schema($db);
298:
299: if (!empty($fields))
300: foreach($fields as $key => &$field) {
301:
302: $field = static::resolveRelationConf($field);
303:
304: if (array_key_exists('has-many', $field)) {
305:
306: if (!is_array($relConf = $field['has-many'])) {
307: unset($fields[$key]);
308: continue;
309: }
310: $rel = $relConf[0]::resolveConfiguration();
311:
312: if (array_key_exists($relConf[1],$rel['fieldConf'])
313: && !is_null($rel['fieldConf'][$relConf[1]])
314: && $relConf['hasRel'] == 'has-many') {
315:
316: $mmTable = isset($relConf[2]) ? $relConf[2] :
317: static::getMMTableName(
318: $rel['table'], $relConf[1], $table, $key,
319: $rel['fieldConf'][$relConf[1]]['has-many']);
320: if (!in_array($mmTable,$schema->getTables())) {
321: $mmt = $schema->createTable($mmTable);
322: $mmt->addColumn($relConf[1])->type_int();
323: $mmt->addColumn($key)->type_int();
324: $index = array($relConf[1],$key);
325: sort($index);
326: $mmt->addIndex($index);
327: $mmt->build();
328: }
329: }
330: }
331:
332: if (!array_key_exists('type', $field)) {
333: unset($fields[$key]);
334: continue;
335: }
336:
337: if (in_array($field['type'], array(self::DT_JSON, self::DT_SERIALIZED)))
338: $field['type']=$schema::DT_TEXT;
339:
340: if (!array_key_exists('nullable', $field))
341: $field['nullable'] = true;
342: unset($field);
343: }
344: if (!in_array($table, $schema->getTables())) {
345:
346: $table = $schema->createTable($table);
347: foreach ($fields as $field_key => $field_conf)
348: $table->addColumn($field_key, $field_conf);
349: if(isset($df) && $df['primary'] != 'id') {
350: $table->addColumn($df['primary'])->type_int();
351: $table->primary($df['primary']);
352: }
353: $table->build();
354: } else {
355:
356: $table = $schema->alterTable($table);
357: $existingCols = $table->getCols();
358: foreach ($fields as $field_key => $field_conf)
359: if (!in_array($field_key, $existingCols))
360: $table->addColumn($field_key, $field_conf);
361:
362:
363:
364:
365: $table->build();
366: }
367: }
368: return true;
369: }
370:
371: 372: 373: 374: 375:
376: static public function setdown($db=null, $table=null)
377: {
378: $self = get_called_class();
379: if (is_null($db) || is_null($table))
380: $df = $self::resolveConfiguration();
381: if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db)))
382: trigger_error(self::E_CONNECTION);
383: if (strlen($table=strtolower($table?:$df['table']))==0)
384: trigger_error(self::E_NO_TABLE);
385: if (isset($df) && !empty($df['fieldConf']))
386: $fields = $df['fieldConf'];
387: else
388: $fields = array();
389: $deletable = array();
390: $deletable[] = $table;
391: foreach ($fields as $key => $field) {
392: $field = static::resolveRelationConf($field);
393: if (array_key_exists('has-many',$field)) {
394: if (!is_array($relConf = $field['has-many']))
395: continue;
396: $rel = $relConf[0]::resolveConfiguration();
397:
398: if (array_key_exists($relConf[1],$rel['fieldConf']) && !is_null($relConf[1])
399: && key($rel['fieldConf'][$relConf[1]]) == 'has-many') {
400:
401: $deletable[] = isset($relConf[2]) ? $relConf[2] :
402: static::getMMTableName(
403: $rel['table'], $relConf[1], $table, $key,
404: $rel['fieldConf'][$relConf[1]]['has-many']);
405: }
406: }
407: }
408:
409: if($db instanceof Jig) {
410:
411: $dir = $db->dir();
412: foreach ($deletable as $item)
413: if(file_exists($dir.$item))
414: unlink($dir.$item);
415: } elseif($db instanceof SQL) {
416:
417: $schema = new Schema($db);
418: $tables = $schema->getTables();
419: foreach ($deletable as $item)
420: if(in_array($item, $tables))
421: $schema->dropTable($item);
422: } elseif($db instanceof Mongo) {
423:
424: foreach ($deletable as $item)
425: $db->selectCollection($item)->drop();
426: }
427: }
428:
429: 430: 431: 432: 433: 434: 435: 436: 437:
438: static protected function getMMTableName($ftable, $fkey, $ptable, $pkey, $fConf=null)
439: {
440: if ($fConf) {
441: list($fclass, $pfkey) = $fConf;
442: $self = get_called_class();
443:
444: if (!is_int(strpos($fclass, $self)))
445: trigger_error(sprintf(self::E_MM_REL_CLASS, $fclass, $self));
446: if ($pfkey != $pkey)
447: trigger_error(sprintf(self::E_MM_REL_FIELD,
448: $fclass.'.'.$pfkey, $self.'.'.$pkey));
449: }
450: $mmTable = array($ftable.'__'.$fkey, $ptable.'__'.$pkey);
451: natcasesort($mmTable);
452: $return = strtolower(str_replace('\\', '_', implode('_mm_', $mmTable)));
453: return $return;
454: }
455:
456: 457: 458: 459: 460:
461: protected static function resolveRelationConf($field)
462: {
463: if (array_key_exists('belongs-to-one', $field)) {
464:
465: if (!is_array($relConf = $field['belongs-to-one']))
466: $relConf = array($relConf, '_id');
467:
468: if ($relConf[1] == '_id')
469: $field['type'] = Schema::DT_INT4;
470: else {
471:
472: $fc = $relConf[0]::resolveConfiguration();
473: $field['belongs-to-one']['relPK'] = $fc['primary'];
474: $field['type'] = $fc['fieldConf'][$relConf[1]]['type'];
475: }
476: $field['nullable'] = true;
477: $field['relType'] = 'belongs-to-one';
478: }
479: elseif (array_key_exists('belongs-to-many', $field)){
480: $field['type'] = self::DT_JSON;
481: $field['nullable'] = true;
482: $field['relType'] = 'belongs-to-many';
483: }
484: elseif (array_key_exists('has-many', $field)){
485: $field['relType'] = 'has-many';
486: $relConf = $field['has-many'];
487: if(!is_array($relConf))
488: return $field;
489: $rel = $relConf[0]::resolveConfiguration();
490: if(array_key_exists('has-many',$rel['fieldConf'][$relConf[1]])) {
491: $field['has-many']['hasRel'] = 'has-many';
492: $field['has-many']['relTable'] = $rel['table'];
493: $field['has-many']['relField'] = $relConf[1];
494: $field['has-many']['relPK'] = $rel['primary'];
495: } else {
496: $field['has-many']['hasRel'] = 'belongs-to-one';
497: $toConf=$rel['fieldConf'][$relConf[1]]['belongs-to-one'];
498: if (is_array($toConf))
499: $field['has-many']['relField'] = $toConf[1];
500: }
501: } elseif(array_key_exists('has-one', $field))
502: $field['relType'] = 'has-one';
503: return $field;
504: }
505:
506: 507: 508: 509: 510: 511: 512: 513:
514: public function afind($filter = NULL, array $options = NULL, $ttl = 0, $rel_depths = 1)
515: {
516: $result = $this->find($filter, $options, $ttl);
517: return $result ? $result->castAll($rel_depths): NULL;
518: }
519:
520: 521: 522: 523: 524: 525: 526:
527: public function find($filter = NULL, array $options = NULL, $ttl = 0)
528: {
529: $sort=false;
530: if ($this->dbsType!='sql') {
531: if (!empty($this->countFields))
532:
533: foreach($this->countFields as $counter) {
534: if ($options && isset($options['order']) &&
535: preg_match('/count_'.$counter.'\h+(asc|desc)/i',$options['order'],$match))
536: $sort=true;
537: }
538: if ($sort) {
539:
540: if (isset($options['limit'])) {
541: $limit = $options['limit'];
542: unset($options['limit']);
543: }
544: if (isset($options['offset'])) {
545: $offset = $options['offset'];
546: unset($options['offset']);
547: }
548: }
549: }
550: $this->_ttl=$ttl?:$this->rel_ttl;
551: $result = $this->filteredFind($filter,$options,$ttl);
552: if (empty($result))
553: return false;
554: foreach($result as &$record) {
555: $record = $this->factory($record);
556: unset($record);
557: }
558: if (!empty($this->countFields))
559:
560: foreach($this->countFields as $counter)
561: foreach($result as &$mapper) {
562: $cr=$mapper->get($counter);
563: $mapper->virtual('count_'.$counter,$cr?count($cr):null);
564: unset($mapper);
565: }
566: $cc = new CortexCollection();
567: $cc->setModels($result);
568: if($sort) {
569: $cc->orderBy($options['order']);
570: $cc->slice(isset($offset)?$offset:0,isset($limit)?$limit:NULL);
571: }
572: $this->clearFilter();
573: return $cc;
574: }
575:
576: 577: 578: 579: 580: 581: 582: 583:
584: protected function filteredFind($filter = NULL, array $options = NULL, $ttl = 0, $count=false)
585: {
586: if ($this->grp_stack) {
587: if ($this->dbsType == 'mongo') {
588: $group = array(
589: 'keys' => $this->grp_stack['keys'],
590: 'reduce' => 'function (obj, prev) {'.$this->grp_stack['reduce'].'}',
591: 'initial' => $this->grp_stack['initial'],
592: 'finalize' => $this->grp_stack['finalize'],
593: );
594: if ($options && isset($options['group'])) {
595: if(is_array($options['group']))
596: $options['group'] = array_merge($options['group'],$group);
597: else {
598: $keys = explode(',',$options['group']);
599: $keys = array_combine($keys,array_fill(0,count($keys),1));
600: $group['keys'] = array_merge($group['keys'],$keys);
601: $options['group'] = $group;
602: }
603: } else
604: $options = array('group'=>$group);
605: }
606: if($this->dbsType == 'sql') {
607: if ($options && isset($options['group']))
608: $options['group'].= ','.$this->grp_stack;
609: else
610: $options['group'] = $this->grp_stack;
611: }
612:
613: }
614: if ($this->dbsType == 'sql' && !$count) {
615: $m_refl=new \ReflectionObject($this->mapper);
616: $m_ad_prop=$m_refl->getProperty('adhoc');
617: $m_ad_prop->setAccessible(true);
618: $m_refl_adhoc=$m_ad_prop->getValue($this->mapper);
619: $m_ad_prop->setAccessible(false);
620: unset($m_ad_prop,$m_refl);
621: }
622: $hasJoin = array();
623: if ($this->hasCond) {
624: foreach($this->hasCond as $key => $hasCond) {
625: $addToFilter = null;
626: list($has_filter,$has_options) = $hasCond;
627: $type = $this->fieldConf[$key]['relType'];
628: $fromConf = $this->fieldConf[$key][$type];
629: switch($type) {
630: case 'has-one':
631: case 'has-many':
632: if (!is_array($fromConf))
633: trigger_error(sprintf(self::E_REL_CONF_INC, $key));
634: $id = $this->dbsType == 'sql' ? $this->primary : '_id';
635: if ($type=='has-many' && isset($fromConf['relField'])
636: && $fromConf['hasRel'] == 'belongs-to-one')
637: $id=$fromConf['relField'];
638:
639: if ($type == 'has-many' && $fromConf['hasRel'] == 'has-many') {
640: if ($this->dbsType == 'sql'
641: && !isset($has_options['limit']) && !isset($has_options['offset'])) {
642: $hasJoin = array_merge($hasJoin,
643: $this->_hasJoinMM_sql($key,$hasCond,$filter,$options));
644: $options['group'] = (isset($options['group'])?$options['group'].',':'').
645: $this->db->quotekey($this->table.'.'.$this->primary);
646: $groupFields = explode(',', preg_replace('/"/','',$options['group']));
647:
648: if (isset($m_refl_adhoc) && preg_match('/sybase|dblib|odbc|sqlsrv/i',$this->db->driver()))
649: foreach (array_diff($this->mapper->fields(),array_keys($m_refl_adhoc)) as $field)
650: if (!in_array($this->table.'.'.$field,$groupFields))
651: $options['group'] .= ', '.$this->db->quotekey($this->table.'.'.$field);
652: }
653: elseif ($result = $this->_hasRefsInMM($key,$has_filter,$has_options,$ttl))
654: $addToFilter = array($id.' IN ?', $result);
655: }
656: elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl))
657: $addToFilter = array($id.' IN ?', $result);
658: break;
659:
660: case 'belongs-to-one':
661: if ($this->dbsType == 'sql'
662: && !isset($has_options['limit']) && !isset($has_options['offset'])) {
663: if (!is_array($fromConf))
664: $fromConf = array($fromConf, '_id');
665: $rel = $fromConf[0]::resolveConfiguration();
666: if ($this->dbsType == 'sql' && $fromConf[1] == '_id')
667: $fromConf[1] = $rel['primary'];
668: $hasJoin[] = $this->_hasJoin_sql($key,$rel['table'],$hasCond,$filter,$options);
669: } elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl))
670: $addToFilter = array($key.' IN ?', $result);
671: break;
672: default:
673: trigger_error(self::E_HAS_COND);
674: }
675: if (isset($result) && !isset($addToFilter))
676: return false;
677: elseif (isset($addToFilter)) {
678: if (!$filter)
679: $filter = array('');
680: if (!empty($filter[0]))
681: $filter[0] .= ' and ';
682: $filter[0] .= '('.array_shift($addToFilter).')';
683: $filter = array_merge($filter, $addToFilter);
684: }
685: }
686: $this->hasCond = null;
687: }
688: $filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf);
689: if ($this->dbsType=='sql') {
690: $qtable = $this->db->quotekey($this->table);
691: if (isset($options['order']) && $this->db->driver() == 'pgsql')
692:
693: $options['order'] = preg_replace('/\h+DESC/i',' DESC NULLS LAST',$options['order']);
694: if (!empty($hasJoin)) {
695:
696: $adhoc='';
697: if ($count)
698: $sql = 'SELECT COUNT(*) AS '.$this->db->quotekey('rows').' FROM '.$qtable;
699: else {
700: if (!empty($this->preBinds)) {
701: $crit = array_shift($filter);
702: $filter = array_merge($this->preBinds,$filter);
703: array_unshift($filter,$crit);
704: }
705: if (!empty($m_refl_adhoc))
706: foreach ($m_refl_adhoc as $key=>$val)
707: $adhoc.=', '.$val['expr'].' AS '.$key;
708: $sql = 'SELECT '.$qtable.'.*'.$adhoc.' FROM '.$qtable;
709: }
710: $sql .= ' '.implode(' ',$hasJoin).' WHERE '.$filter[0];
711: if (!$count) {
712: if (isset($options['group']))
713: $sql .= ' GROUP BY '.$this->_sql_quoteCondition($options['group'], $this->table);
714: if (isset($options['order']))
715: $sql .= ' ORDER BY '.$options['order'];
716: if (preg_match('/mssql|sqlsrv|odbc/', $this->db->driver()) &&
717: (isset($options['limit']) || isset($options['offset']))) {
718: $ofs=isset($options['offset'])?(int)$options['offset']:0;
719: $lmt=isset($options['limit'])?(int)$options['limit']:0;
720: if (strncmp($this->db->version(),'11',2)>=0) {
721:
722: if (!isset($options['order']))
723: $sql.=' ORDER BY '.$this->db->quotekey($this->primary);
724: $sql.=' OFFSET '.$ofs.' ROWS'.($lmt?' FETCH NEXT '.$lmt.' ROWS ONLY':'');
725: } else {
726:
727: $order=(!isset($options['order']))
728: ?($this->db->quotekey($this->table.'.'.$this->primary)):$options['order'];
729: $sql=str_replace('SELECT','SELECT '.($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '.
730: 'OVER (ORDER BY '.$order.') AS rnum,',$sql);
731: $sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs);
732: }
733: } else {
734: if (isset($options['limit']))
735: $sql.=' LIMIT '.(int)$options['limit'];
736: if (isset($options['offset']))
737: $sql.=' OFFSET '.(int)$options['offset'];
738: }
739: }
740: unset($filter[0]);
741: $result = $this->db->exec($sql, $filter, $ttl);
742: if ($count)
743: return $result[0]['rows'];
744: foreach ($result as &$record) {
745:
746: $mapper = clone($this->mapper);
747: $mapper->reset();
748:
749: $m_adhoc = empty($adhoc) ? array() : $m_refl_adhoc;
750: foreach ($record as $key=>$val)
751: if (isset($m_refl_adhoc[$key]))
752: $m_adhoc[$key]['value']=$val;
753: else
754: $mapper->set($key, $val);
755: if (!empty($adhoc)) {
756: $refl = new \ReflectionObject($mapper);
757: $prop = $refl->getProperty('adhoc');
758: $prop->setAccessible(true);
759: $prop->setValue($mapper,$m_adhoc);
760: $prop->setAccessible(false);
761: }
762: $record = $mapper;
763: unset($record, $mapper);
764: }
765: return $result;
766: } elseif (!empty($this->preBinds) && !$count) {
767:
768: if (!$filter)
769:
770: $filter = array('1=1');
771: $crit = array_shift($filter);
772: $filter = array_merge($this->preBinds,$filter);
773: array_unshift($filter,$crit);
774: }
775: }
776: return ($count)
777: ? $this->mapper->count($filter,$ttl)
778: : $this->mapper->find($filter,$this->queryParser->prepareOptions($options,$this->dbsType),$ttl);
779: }
780:
781: 782: 783: 784: 785: 786: 787:
788: public function load($filter = NULL, array $options = NULL, $ttl = 0)
789: {
790: $this->reset();
791: $this->_ttl=$ttl?:$this->rel_ttl;
792: $res = $this->filteredFind($filter, $options, $ttl);
793: if ($res) {
794: $this->mapper->query = $res;
795: $this->first();
796: } else
797: $this->mapper->reset();
798: $this->emit('load');
799: return $this;
800: }
801:
802: 803: 804: 805: 806: 807: 808:
809: public function has($key, $filter, $options = null) {
810: if (!isset($this->fieldConf[$key]))
811: trigger_error(sprintf(self::E_UNKNOWN_FIELD,$key,get_called_class()));
812: if (!isset($this->fieldConf[$key]['relType']))
813: trigger_error(self::E_HAS_COND);
814: if (is_string($filter))
815: $filter=array($filter);
816: $this->hasCond[$key] = array($filter,$options);
817: return $this;
818: }
819:
820: 821: 822: 823: 824: 825: 826: 827:
828: protected function _hasRefsIn($key, $filter, $options, $ttl = 0)
829: {
830: $type = $this->fieldConf[$key]['relType'];
831: $fieldConf = $this->fieldConf[$key][$type];
832: if (!is_array($fieldConf))
833:
834: $fieldConf = array($fieldConf, '_id');
835: $rel = $this->getRelInstance($fieldConf[0],null,$key);
836: if($this->dbsType=='sql' && $fieldConf[1] == '_id')
837: $fieldConf[1] = $rel->primary;
838: $hasSet = $rel->find($filter, $options, $ttl);
839: if (!$hasSet)
840: return false;
841: $hasSetByRelId = array_unique($hasSet->getAll($fieldConf[1], true));
842: return empty($hasSetByRelId) ? false : $hasSetByRelId;
843: }
844:
845: 846: 847: 848: 849: 850: 851: 852:
853: protected function _hasRefsInMM($key, $filter, $options, $ttl=0)
854: {
855: $fieldConf = $this->fieldConf[$key]['has-many'];
856: $rel = $this->getRelInstance($fieldConf[0],null,$key);
857: $hasSet = $rel->find($filter,$options,$ttl);
858: $result = false;
859: if ($hasSet) {
860: $hasIDs = $hasSet->getAll('_id',true);
861: if (!array_key_exists('refTable', $fieldConf)) {
862: $mmTable = isset($fieldConf[2]) ? $fieldConf[2] :
863: static::getMMTableName($fieldConf['relTable'],
864: $fieldConf['relField'], $this->getTable(), $key);
865: $this->fieldConf[$key]['has-many']['refTable'] = $mmTable;
866: } else
867: $mmTable = $fieldConf['refTable'];
868: $pivot = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable));
869: $pivotSet = $pivot->find(array($key.' IN ?',$hasIDs),null,$ttl);
870: if ($pivotSet)
871: $result = array_unique($pivotSet->getAll($fieldConf['relField'],true));
872: }
873: return $result;
874: }
875:
876: 877: 878:
879: protected function _hasJoinMM_sql($key, $hasCond, &$filter, &$options)
880: {
881: $fieldConf = $this->fieldConf[$key]['has-many'];
882: $hasJoin = array();
883: if (!array_key_exists('refTable', $fieldConf)) {
884:
885: $mmTable = isset($fieldConf[2]) ? $fieldConf[2] :
886: static::getMMTableName($fieldConf['relTable'],
887: $fieldConf['relField'], $this->getTable(), $key);
888: $this->fieldConf[$key]['has-many']['refTable'] = $mmTable;
889: } else
890: $mmTable = $fieldConf['refTable'];
891: $hasJoin[] = $this->_sql_left_join($this->primary,$this->table,$fieldConf['relField'],$mmTable);
892: $hasJoin[] = $this->_sql_left_join($key,$mmTable,$fieldConf['relPK'],$fieldConf['relTable']);
893: $this->_sql_mergeRelCondition($hasCond,$fieldConf['relTable'],$filter,$options);
894: return $hasJoin;
895: }
896:
897: 898: 899:
900: protected function _hasJoin_sql($key, $table, $cond, &$filter, &$options)
901: {
902: $relConf = $this->fieldConf[$key]['belongs-to-one'];
903: $relModel = is_array($relConf)?$relConf[0]:$relConf;
904: $rel = $this->getRelInstance($relModel,null,$key);
905: $fkey = is_array($this->fieldConf[$key]['belongs-to-one']) ?
906: $this->fieldConf[$key]['belongs-to-one'][1] : $rel->primary;
907: $query = $this->_sql_left_join($key,$this->table,$fkey,$table);
908: $this->_sql_mergeRelCondition($cond,$table,$filter,$options);
909: return $query;
910: }
911:
912: 913: 914:
915: protected function _sql_left_join($skey,$sTable,$fkey,$fTable)
916: {
917: $skey = $this->db->quotekey($skey);
918: $sTable = $this->db->quotekey($sTable);
919: $fkey = $this->db->quotekey($fkey);
920: $fTable = $this->db->quotekey($fTable);
921: return 'LEFT JOIN '.$fTable.' ON '.$sTable.'.'.$skey.' = '.$fTable.'.'.$fkey;
922: }
923:
924: 925: 926: 927: 928: 929: 930:
931: protected function _sql_mergeRelCondition($cond, $table, &$filter, &$options)
932: {
933: $table = $this->db->quotekey($table);
934: if (!empty($cond[0])) {
935: $whereClause = '('.array_shift($cond[0]).')';
936: $whereClause = $this->_sql_quoteCondition($whereClause,$table);
937: if (!$filter)
938: $filter = array($whereClause);
939: elseif (!empty($filter[0]))
940: $filter[0] = '('.$this->_sql_quoteCondition($filter[0],
941: $this->db->quotekey($this->table)).') and '.$whereClause;
942: $filter = array_merge($filter, $cond[0]);
943: }
944: if ($cond[1] && isset($cond[1]['group'])) {
945: $hasGroup = preg_replace('/(\w+)/i', $table.'.$1', $cond[1]['group']);
946: $options['group'] .= ','.$hasGroup;
947: }
948: }
949:
950: protected function _sql_quoteCondition($cond, $table)
951: {
952: $db = $this->db;
953: if (preg_match('/[`\'"\[\]]/i',$cond))
954: return $cond;
955: return preg_replace_callback('/\w+/i',function($match) use($table,$db) {
956: if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[0]))
957: return $match[0];
958: return $table.'.'.$db->quotekey($match[0]);
959: }, $cond);
960: }
961:
962: 963: 964: 965: 966: 967: 968:
969: public function filter($key,$filter=null,$option=null)
970: {
971: $this->relFilter[$key] = array($filter,$option);
972: return $this;
973: }
974:
975: 976: 977: 978:
979: public function clearFilter($key = null)
980: {
981: if (!$key)
982: $this->relFilter = array();
983: elseif(isset($this->relFilter[$key]))
984: unset($this->relFilter[$key]);
985: }
986:
987: 988: 989: 990: 991: 992:
993: protected function mergeWithRelFilter($key,$crit)
994: {
995: if (array_key_exists($key, $this->relFilter) &&
996: !empty($this->relFilter[$key][0]))
997: {
998: $filter = $this->relFilter[$key][0];
999: $crit[0] .= ' and '.array_shift($filter);
1000: $crit = array_merge($crit, $filter);
1001: }
1002: return $crit;
1003: }
1004:
1005: 1006: 1007: 1008: 1009:
1010: protected function getRelFilterOption($key)
1011: {
1012: return (array_key_exists($key, $this->relFilter) &&
1013: !empty($this->relFilter[$key][1]))
1014: ? $this->relFilter[$key][1] : null;
1015: }
1016:
1017: 1018: 1019: 1020: 1021:
1022: public function erase($filter = null)
1023: {
1024: $filter = $this->queryParser->prepareFilter($filter, $this->dbsType);
1025: if (!$filter && $this->emit('beforeerase')!==false) {
1026: if ($this->fieldConf) {
1027: foreach($this->fieldConf as $field => $conf)
1028: if (isset($conf['has-many']) &&
1029: $conf['has-many']['hasRel']=='has-many')
1030: $this->set($field,null);
1031: $this->save();
1032: }
1033: $this->mapper->erase();
1034: $this->emit('aftererase');
1035: } elseif($filter)
1036: $this->mapper->erase($filter);
1037: }
1038:
1039: 1040: 1041: 1042:
1043: function save()
1044: {
1045: if ($new = $this->dry()) {
1046: if ($this->emit('beforeinsert')===false)
1047: return false;
1048: $result=$this->insert();
1049: } else {
1050: if ($this->emit('beforeupdate')===false)
1051: return false;
1052: $result=$this->update();
1053: }
1054:
1055: $fields = $this->fieldConf;
1056: if ($fields)
1057: foreach($fields as $key=>$conf)
1058: if (!empty($this->fieldsCache[$key]) && $this->fieldsCache[$key] instanceof CortexCollection
1059: && $this->fieldsCache[$key]->hasChanged())
1060: $this->set($key,$this->fieldsCache[$key]->getAll('_id',true));
1061:
1062:
1063: if (!empty($this->saveCsd)) {
1064: foreach($this->saveCsd as $key => $val) {
1065: if($fields[$key]['relType'] == 'has-many') {
1066: $relConf = $fields[$key]['has-many'];
1067: if (!isset($relConf['refTable'])) {
1068:
1069: $mmTable = isset($relConf[2]) ? $relConf[2] :
1070: static::getMMTableName($relConf['relTable'],
1071: $relConf['relField'], $this->getTable(), $key);
1072: $this->fieldConf[$key]['has-many']['refTable'] = $mmTable;
1073: } else
1074: $mmTable = $relConf['refTable'];
1075: $rel = $this->getRelInstance(null, array('db'=>$this->db, 'table'=>$mmTable));
1076: $id = $this->get('_id',true);
1077:
1078: if (is_null($val))
1079: $rel->erase(array($relConf['relField'].' = ?', $id));
1080:
1081: elseif (is_array($val)) {
1082: $rel->erase(array($relConf['relField'].' = ?', $id));
1083: foreach($val as $v) {
1084: $rel->set($key,$v);
1085: $rel->set($relConf['relField'],$id);
1086: $rel->save();
1087: $rel->reset();
1088: }
1089: }
1090: unset($rel);
1091: } elseif($fields[$key]['relType'] == 'has-one') {
1092: $val->save();
1093: }
1094: }
1095: $this->saveCsd = array();
1096: }
1097: $this->emit($new?'afterinsert':'afterupdate');
1098: return $result;
1099: }
1100:
1101: 1102: 1103: 1104: 1105: 1106:
1107: public function count($filter = NULL, $ttl = 60)
1108: {
1109: $has=$this->hasCond;
1110: $count=$this->filteredFind($filter,null,$ttl,true);
1111: $this->hasCond=$has;
1112: return $count;
1113: }
1114:
1115: 1116: 1117: 1118:
1119: public function loaded() {
1120: return count($this->mapper->query);
1121: }
1122:
1123: 1124: 1125: 1126:
1127: public function countRel($key) {
1128: if (isset($this->fieldConf[$key])){
1129:
1130: if ($this->fieldConf[$key]['relType'] == 'belongs-to-one') {
1131: if ($this->dbsType == 'sql') {
1132: $this->set('count_'.$key,'count('.$key.')');
1133: $this->grp_stack=(!$this->grp_stack)?$key:$this->grp_stack.','.$key;
1134: } elseif ($this->dbsType == 'mongo')
1135: $this->_mongo_addGroup(array(
1136: 'keys'=>array($key=>1),
1137: 'reduce' => 'prev.count_'.$key.'++;',
1138: "initial" => array("count_".$key => 0)
1139: ));
1140: else
1141: trigger_error('Cannot add direct relational counter.');
1142: } elseif($this->fieldConf[$key]['relType'] == 'has-many') {
1143: $relConf=$this->fieldConf[$key]['has-many'];
1144: if ($relConf['hasRel']=='has-many') {
1145:
1146: if ($this->dbsType == 'sql') {
1147: if (!isset($relConf['refTable'])) {
1148:
1149: $mmTable = isset($relConf[2]) ? $relConf[2] :
1150: static::getMMTableName($relConf['relTable'],
1151: $relConf['relField'], $this->getTable(), $key);
1152: $this->fieldConf[$key]['has-many']['refTable'] = $mmTable;
1153: } else
1154: $mmTable = $relConf['refTable'];
1155: $filter = array($this->db->quotekey($mmTable).'.'.$this->db->quotekey($relConf['relField'])
1156: .' = '.$this->db->quotekey($this->getTable()).'.'.$this->db->quotekey($this->primary));
1157: $from=$mmTable;
1158: if (array_key_exists($key, $this->relFilter) &&
1159: !empty($this->relFilter[$key][0])) {
1160: $options=array();
1161: $from = $mmTable.' '.$this->_sql_left_join($key,$mmTable,$relConf['relPK'],$relConf['relTable']);
1162: $relFilter = $this->relFilter[$key];
1163: $this->_sql_mergeRelCondition($relFilter,$relConf['relTable'],$filter,$options);
1164: }
1165: $filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf);
1166: $crit = array_shift($filter);
1167: if (count($filter)>0)
1168: $this->preBinds+=$filter;
1169: $this->set('count_'.$key,'(select count('.$relConf['relField'].') from '.$from.' where '.
1170: $crit.' group by '.$mmTable.'.'.$relConf['relField'].')');
1171: } else {
1172:
1173: $this->countFields[]=$key;
1174: }
1175: } elseif($this->fieldConf[$key]['has-many']['hasRel']=='belongs-to-one') {
1176:
1177: if ($this->dbsType == 'sql') {
1178: $fConf=$relConf[0]::resolveConfiguration();
1179: $fTable=$this->db->quotekey($fConf['table']);
1180: $fKey=$this->db->quotekey($fConf['primary']);
1181: $rKey=$this->db->quotekey($relConf[1]);
1182: $pKey=$this->db->quotekey($this->primary);
1183: $table=$this->db->quotekey($this->getTable());
1184: $crit = $fTable.'.'.$rKey.' = '.$table.'.'.$pKey;
1185: $filter = $this->mergeWithRelFilter($key,array($crit));
1186: $filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf);
1187: $crit = array_shift($filter);
1188: if (count($filter)>0)
1189: $this->preBinds+=$filter;
1190: $this->set('count_'.$key,'(select count('.$fTable.'.'.$fKey.') from '.$fTable.' where '.
1191: $crit.' group by '.$fTable.'.'.$rKey.')');
1192: } else {
1193:
1194: $this->countFields[]=$key;
1195: }
1196: }
1197: }
1198: }
1199: }
1200:
1201: 1202: 1203: 1204:
1205: protected function _mongo_addGroup($opt){
1206: if (!$this->grp_stack)
1207: $this->grp_stack = array('keys'=>array(),'initial'=>array(),'reduce'=>'','finalize'=>'');
1208: if (isset($opt['keys']))
1209: $this->grp_stack['keys']+=$opt['keys'];
1210: if (isset($opt['reduce']))
1211: $this->grp_stack['reduce'].=$opt['reduce'];
1212: if (isset($opt['initial']))
1213: $this->grp_stack['initial']+=$opt['initial'];
1214: if (isset($opt['finalize']))
1215: $this->grp_stack['finalize'].=$opt['finalize'];
1216: }
1217:
1218: 1219: 1220: 1221: 1222: 1223:
1224: function set($key, $val)
1225: {
1226: $fields = $this->fieldConf;
1227: unset($this->fieldsCache[$key]);
1228:
1229: if (!empty($fields) && isset($fields[$key]) && is_array($fields[$key])) {
1230:
1231: if (isset($fields[$key]['belongs-to-one'])) {
1232:
1233: if (is_null($val))
1234: $val = NULL;
1235: elseif (is_object($val) &&
1236: !($this->dbsType=='mongo' && $val instanceof \MongoId)) {
1237:
1238: if (!$val instanceof Cortex || $val->dry())
1239: trigger_error(self::E_INVALID_RELATION_OBJECT);
1240: else {
1241: $relConf = $fields[$key]['belongs-to-one'];
1242: $rel_field = (is_array($relConf) ? $relConf[1] : '_id');
1243: $val = $val->get($rel_field,true);
1244: }
1245: } elseif ($this->dbsType == 'mongo' && !$val instanceof \MongoId)
1246: $val = new \MongoId($val);
1247: } elseif (isset($fields[$key]['has-one'])){
1248: $relConf = $fields[$key]['has-one'];
1249: if (is_null($val)) {
1250: $val = $this->get($key);
1251: $val->set($relConf[1],NULL);
1252: } else {
1253: if (!$val instanceof Cortex) {
1254: $rel = $this->getRelInstance($relConf[0],null,$key);
1255: $rel->load(array('_id = ?', $val));
1256: $val = $rel;
1257: }
1258: $val->set($relConf[1], $this->_id);
1259: }
1260: $this->saveCsd[$key] = $val;
1261: return $val;
1262: } elseif (isset($fields[$key]['belongs-to-many'])) {
1263:
1264: $fields[$key]['type'] = self::DT_JSON;
1265: $relConf = $fields[$key]['belongs-to-many'];
1266: $rel_field = (is_array($relConf) ? $relConf[1] : '_id');
1267: $val = $this->getForeignKeysArray($val, $rel_field, $key);
1268: }
1269: elseif (isset($fields[$key]['has-many'])) {
1270:
1271: $relConf = $fields[$key]['has-many'];
1272: if ($relConf['hasRel'] == 'has-many') {
1273:
1274: $val = $this->emit('set_'.$key, $val);
1275: $val = $this->getForeignKeysArray($val,'_id',$key);
1276: $this->saveCsd[$key] = $val;
1277: return $val;
1278: } elseif ($relConf['hasRel'] == 'belongs-to-one') {
1279:
1280: trigger_error("not implemented");
1281: }
1282: }
1283:
1284: if (is_array($val) && $this->dbsType == 'sql')
1285: if ($fields[$key]['type'] == self::DT_SERIALIZED)
1286: $val = serialize($val);
1287: elseif ($fields[$key]['type'] == self::DT_JSON)
1288: $val = json_encode($val);
1289: else
1290: trigger_error(sprintf(self::E_ARRAY_DATATYPE, $key));
1291:
1292: if ($val === NULL && ($this->dbsType == 'jig' || $this->dbsType == 'mongo')
1293: && array_key_exists('nullable', $fields[$key]) && $fields[$key]['nullable'] === false)
1294: trigger_error(sprintf(self::E_NULLABLE_COLLISION,$key));
1295:
1296: if ($this->dbsType == 'mongo' && !$val instanceof \MongoId) {
1297: if ($key == '_id')
1298: $val = new \MongoId($val);
1299: elseif (preg_match('/INT/i',$fields[$key]['type'])
1300: && !isset($fields[$key]['relType']))
1301: $val = (int) $val;
1302: }
1303: if (preg_match('/BOOL/i',$fields[$key]['type'])) {
1304: $val = !$val || $val==='false' ? false : (bool) $val;
1305: if ($this->dbsType == 'sql')
1306: $val = (int) $val;
1307: }
1308: }
1309:
1310: if ($this->fluid && $this->dbsType == 'sql') {
1311: $schema = new Schema($this->db);
1312: $table = $schema->alterTable($this->table);
1313:
1314: if (!in_array($key,$table->getCols())) {
1315:
1316: if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type']))
1317: $type = $this->fieldConf[$key]['type'];
1318: elseif (is_int($val)) $type = $schema::DT_INT;
1319: elseif (is_double($val)) $type = $schema::DT_DOUBLE;
1320: elseif (is_float($val)) $type = $schema::DT_FLOAT;
1321: elseif (is_bool($val)) $type = $schema::DT_BOOLEAN;
1322: elseif (date('Y-m-d H:i:s', strtotime($val)) == $val) $type = $schema::DT_DATETIME;
1323: elseif (date('Y-m-d', strtotime($val)) == $val) $type = $schema::DT_DATE;
1324: elseif (\UTF::instance()->strlen($val)<=255) $type = $schema::DT_VARCHAR256;
1325: else $type = $schema::DT_TEXT;
1326: $table->addColumn($key)->type($type);
1327: $table->build();
1328:
1329: $newField = $table->getCols(true);
1330: $newField = $newField[$key];
1331: $refl = new \ReflectionObject($this->mapper);
1332: $prop = $refl->getProperty('fields');
1333: $prop->setAccessible(true);
1334: $fields = $prop->getValue($this->mapper);
1335: $fields[$key] = $newField + array('value'=>NULL,'changed'=>NULL);
1336: $prop->setValue($this->mapper,$fields);
1337: }
1338: }
1339:
1340: $val = $this->emit('set_'.$key, $val);
1341: return $this->mapper->set($key, $val);
1342: }
1343:
1344: 1345: 1346: 1347: 1348: 1349:
1350: protected function emit($event, $val=null)
1351: {
1352: if (isset($this->trigger[$event])) {
1353: if (preg_match('/^[sg]et_/',$event)) {
1354: $val = (is_string($f=$this->trigger[$event])
1355: && preg_match('/^[sg]et_/',$f))
1356: ? call_user_func(array($this,$event),$val)
1357: : \Base::instance()->call($f,array($this,$val));
1358: } else
1359: $val = \Base::instance()->call($this->trigger[$event],array($this,$val));
1360: } elseif (preg_match('/^[sg]et_/',$event) && method_exists($this,$event)) {
1361: $this->trigger[] = $event;
1362: $val = call_user_func(array($this,$event),$val);
1363: }
1364: return $val;
1365: }
1366:
1367: 1368: 1369: 1370: 1371:
1372: public function onset($key,$func) {
1373: $this->trigger['set_'.$key] = $func;
1374: }
1375:
1376: 1377: 1378: 1379: 1380:
1381: public function onget($key,$func) {
1382: $this->trigger['get_'.$key] = $func;
1383: }
1384:
1385: 1386: 1387: 1388: 1389: 1390:
1391: public function virtual($key,$val) {
1392: $this->vFields[$key]=$val;
1393: }
1394:
1395: 1396: 1397: 1398: 1399: 1400:
1401: function &get($key,$raw = false)
1402: {
1403:
1404: if (isset($this->vFields[$key])) {
1405: $out = (is_callable($this->vFields[$key]))
1406: ? call_user_func($this->vFields[$key], $this) : $this->vFields[$key];
1407: return $out;
1408: }
1409: $fields = $this->fieldConf;
1410: $id = ($this->dbsType == 'sql') ? $this->primary : '_id';
1411: if ($key == '_id' && $this->dbsType == 'sql')
1412: $key = $id;
1413: if ($this->whitelist && !in_array($key,$this->whitelist)) {
1414: $out = null;
1415: return $out;
1416: }
1417: if ($raw) {
1418: $out = $this->exists($key) ? $this->mapper->{$key} : NULL;
1419: return $out;
1420: }
1421: if (!empty($fields) && isset($fields[$key]) && is_array($fields[$key])
1422: && !array_key_exists($key,$this->fieldsCache)) {
1423:
1424: if (isset($fields[$key]['belongs-to-one'])) {
1425:
1426: if (!$this->exists($key) || is_null($this->mapper->{$key}))
1427: $this->fieldsCache[$key] = null;
1428: else {
1429:
1430: $relConf = $fields[$key]['belongs-to-one'];
1431: if (!is_array($relConf))
1432: $relConf = array($relConf, '_id');
1433:
1434: $rel = $this->getRelInstance($relConf[0],null,$key);
1435: if ($this->dbsType == 'sql' && $relConf[1] == '_id')
1436: $relConf[1] = $rel->primary;
1437:
1438: if ($cx = $this->getCollection()) {
1439:
1440: if (!$cx->hasRelSet($key)) {
1441:
1442: $relKeys = array_unique($cx->getAll($key,true));
1443:
1444: $crit = array($relConf[1].' IN ?', $relKeys);
1445: $relSet = $rel->find($this->mergeWithRelFilter($key, $crit),
1446: $this->getRelFilterOption($key),$this->_ttl);
1447:
1448: $cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL);
1449: }
1450:
1451: $result = $cx->getSubset($key,(string) $this->get($key,true));
1452: $this->fieldsCache[$key] = $result ? $result[0] : NULL;
1453: } else {
1454: $crit = array($relConf[1].' = ?', $this->get($key, true));
1455: $crit = $this->mergeWithRelFilter($key, $crit);
1456: $this->fieldsCache[$key] = $rel->findone($crit,
1457: $this->getRelFilterOption($key),$this->_ttl);
1458: }
1459: }
1460: }
1461: elseif (($type = isset($fields[$key]['has-one']))
1462: || isset($fields[$key]['has-many'])) {
1463: $type = $type ? 'has-one' : 'has-many';
1464: $fromConf = $fields[$key][$type];
1465: if (!is_array($fromConf))
1466: trigger_error(sprintf(self::E_REL_CONF_INC, $key));
1467: $rel = $this->getRelInstance($fromConf[0],null,$key);
1468: $relFieldConf = $rel->getFieldConfiguration();
1469: $relType = key($relFieldConf[$fromConf[1]]);
1470:
1471: if ($relType == 'belongs-to-one') {
1472: $toConf = $relFieldConf[$fromConf[1]]['belongs-to-one'];
1473: if(!is_array($toConf))
1474: $toConf = array($toConf, $id);
1475: if ($toConf[1] != $id && (!$this->exists($toConf[1])
1476: || is_null($this->mapper->get($toConf[1]))))
1477: $this->fieldsCache[$key] = null;
1478: elseif($cx = $this->getCollection()) {
1479:
1480: if(!$cx->hasRelSet($key)) {
1481:
1482: $relKeys = $cx->getAll($toConf[1],true);
1483: $crit = array($fromConf[1].' IN ?', $relKeys);
1484: $relSet = $rel->find($this->mergeWithRelFilter($key,$crit),
1485: $this->getRelFilterOption($key),$this->_ttl);
1486: $cx->setRelSet($key, $relSet ? $relSet->getBy($fromConf[1],true) : NULL);
1487: }
1488: $result = $cx->getSubset($key, array($this->get($toConf[1])));
1489: $this->fieldsCache[$key] = $result ? (($type == 'has-one')
1490: ? $result[0][0] : CortexCollection::factory($result[0])) : NULL;
1491: } else {
1492: $crit = array($fromConf[1].' = ?', $this->get($toConf[1],true));
1493: $crit = $this->mergeWithRelFilter($key, $crit);
1494: $opt = $this->getRelFilterOption($key);
1495: $this->fieldsCache[$key] = (($type == 'has-one')
1496: ? $rel->findone($crit,$opt,$this->_ttl)
1497: : $rel->find($crit,$opt,$this->_ttl)) ?: NULL;
1498: }
1499: }
1500:
1501: elseif ($relType == 'has-many') {
1502: if (!array_key_exists('refTable', $fromConf)) {
1503:
1504: $toConf = $relFieldConf[$fromConf[1]]['has-many'];
1505: $mmTable = isset($fromConf[2]) ? $fromConf[2] :
1506: static::getMMTableName($fromConf['relTable'],
1507: $fromConf['relField'], $this->getTable(), $key, $toConf);
1508: $this->fieldConf[$key]['has-many']['refTable'] = $mmTable;
1509: } else
1510: $mmTable = $fromConf['refTable'];
1511:
1512: if (!$this->get($id,true)) {
1513: $this->fieldsCache[$key] = null;
1514: return $this->fieldsCache[$key];
1515: }
1516: $rel = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable));
1517: if ($cx = $this->getCollection()) {
1518: if (!$cx->hasRelSet($key)) {
1519:
1520: $relKeys = $cx->getAll($id,true);
1521:
1522: $mmRes = $rel->find(array($fromConf['relField'].' IN ?', $relKeys),null,$this->_ttl);
1523: if (!$mmRes)
1524: $cx->setRelSet($key, NULL);
1525: else {
1526: $pivotRel = array();
1527: $pivotKeys = array();
1528: foreach($mmRes as $model) {
1529: $val = $model->get($key,true);
1530: $pivotRel[ (string) $model->get($fromConf['relField'])][] = $val;
1531: $pivotKeys[] = $val;
1532: }
1533:
1534: $cx->setRelSet($key.'_pivot', $pivotRel);
1535:
1536: $pivotKeys = array_unique($pivotKeys);
1537: $fRel = $this->getRelInstance($fromConf[0],null,$key);
1538: $crit = array(($fRel->primary!='id' ? $fRel->primary : '_id').' IN ?', $pivotKeys);
1539: $relSet = $fRel->find($this->mergeWithRelFilter($key, $crit),
1540: $this->getRelFilterOption($key),$this->_ttl);
1541: $cx->setRelSet($key, $relSet ? $relSet->getBy($id) : NULL);
1542: unset($fRel);
1543: }
1544: }
1545:
1546: $fkeys = $cx->getSubset($key.'_pivot', array($this->get($id)));
1547: $this->fieldsCache[$key] = $fkeys ?
1548: CortexCollection::factory($cx->getSubset($key, $fkeys[0])) : NULL;
1549: }
1550: else {
1551:
1552: $results = $rel->find(
1553: array($fromConf['relField'].' = ?', $this->get($id,true)),null,$this->_ttl);
1554: if(!$results)
1555: $this->fieldsCache[$key] = NULL;
1556: else {
1557: $fkeys = $results->getAll($key,true);
1558:
1559: unset($rel);
1560: $rel = $this->getRelInstance($fromConf[0],null,$key);
1561:
1562: $filter = array(($rel->primary!='id' ? $rel->primary : '_id').' IN ?', $fkeys);
1563: $filter = $this->mergeWithRelFilter($key, $filter);
1564: $this->fieldsCache[$key] = $rel->find($filter,
1565: $this->getRelFilterOption($key),$this->_ttl);
1566: }
1567: }
1568: }
1569: }
1570: elseif (isset($fields[$key]['belongs-to-many'])) {
1571:
1572: $fields[$key]['type'] = self::DT_JSON;
1573: $result = !$this->exists($key) ? null :$this->mapper->get($key);
1574: if ($this->dbsType == 'sql')
1575: $result = json_decode($result, true);
1576: if (!is_array($result))
1577: $this->fieldsCache[$key] = $result;
1578: else {
1579:
1580: $relConf = $fields[$key]['belongs-to-many'];
1581: if (!is_array($relConf))
1582: $relConf = array($relConf, '_id');
1583: $rel = $this->getRelInstance($relConf[0],null,$key);
1584: if ($this->dbsType == 'sql' && $relConf[1] == '_id')
1585: $relConf[1] = $rel->primary;
1586: $fkeys = array();
1587: foreach ($result as $el)
1588: $fkeys[] = is_int($el)||ctype_digit($el)?(int)$el:(string)$el;
1589:
1590: if ($cx = $this->getCollection()) {
1591: if (!$cx->hasRelSet($key)) {
1592:
1593: $relKeys = ($cx->getAll($key,true));
1594: if ($this->dbsType == 'sql'){
1595: foreach ($relKeys as &$val) {
1596: $val = substr($val, 1, -1);
1597: unset($val);
1598: }
1599: $relKeys = json_decode('['.implode(',',$relKeys).']');
1600: } else
1601: $relKeys = call_user_func_array('array_merge', $relKeys);
1602:
1603: $crit = array($relConf[1].' IN ?', array_unique($relKeys));
1604: $relSet = $rel->find($this->mergeWithRelFilter($key, $crit),
1605: $this->getRelFilterOption($key),$this->_ttl);
1606:
1607: $cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL);
1608: }
1609:
1610: $this->fieldsCache[$key] = CortexCollection::factory($cx->getSubset($key, $fkeys));
1611: } else {
1612:
1613: $filter = array($relConf[1].' IN ?', $fkeys);
1614: $filter = $this->mergeWithRelFilter($key, $filter);
1615: $this->fieldsCache[$key] = $rel->find($filter,
1616: $this->getRelFilterOption($key),$this->_ttl);
1617: }
1618: }
1619: }
1620:
1621: elseif (isset($fields[$key]['type'])) {
1622: if ($this->dbsType == 'sql') {
1623: if ($fields[$key]['type'] == self::DT_SERIALIZED)
1624: $this->fieldsCache[$key] = unserialize($this->mapper->{$key});
1625: elseif ($fields[$key]['type'] == self::DT_JSON)
1626: $this->fieldsCache[$key] = json_decode($this->mapper->{$key},true);
1627: }
1628: if ($this->exists($key) && preg_match('/BOOL/i',$fields[$key]['type'])) {
1629: $this->fieldsCache[$key] = (bool) $this->mapper->{$key};
1630: }
1631: }
1632: }
1633:
1634: $val = array_key_exists($key,$this->fieldsCache) ? $this->fieldsCache[$key]
1635: : (($this->exists($key)) ? $this->mapper->{$key} : null);
1636: if ($this->dbsType == 'mongo' && $val instanceof \MongoId) {
1637:
1638: $val = (string) $val;
1639: }
1640:
1641: $out = $this->emit('get_'.$key, $val);
1642: return $out;
1643: }
1644:
1645: 1646: 1647: 1648: 1649: 1650: 1651:
1652: protected function getForeignKeysArray($val, $rel_field, $key)
1653: {
1654: if (is_null($val))
1655: return NULL;
1656: if (is_object($val) && $val instanceof CortexCollection)
1657: $val = $val->expose();
1658: elseif (is_string($val))
1659:
1660: $val = \Base::instance()->split($val);
1661: elseif (!is_array($val) && !(is_object($val)
1662: && ($val instanceof Cortex && !$val->dry())))
1663: trigger_error(sprintf(self::E_MM_REL_VALUE, $key));
1664:
1665: if (is_object($val)) {
1666: $nval = array();
1667: while (!$val->dry()) {
1668: $nval[] = $val->get($rel_field,true);
1669: $val->next();
1670: }
1671: $val = $nval;
1672: }
1673: elseif (is_array($val)) {
1674:
1675: $isMongo = ($this->dbsType == 'mongo');
1676: foreach ($val as &$item) {
1677: if (is_object($item) &&
1678: !($isMongo && $item instanceof \MongoId)) {
1679: if (!$item instanceof Cortex || $item->dry())
1680: trigger_error(self::E_INVALID_RELATION_OBJECT);
1681: else $item = $item->get($rel_field,true);
1682: }
1683: if ($isMongo && $rel_field == '_id' && is_string($item))
1684: $item = new \MongoId($item);
1685: if (is_numeric($item))
1686: $item = (int) $item;
1687: unset($item);
1688: }
1689: }
1690: return $val;
1691: }
1692:
1693: 1694: 1695: 1696: 1697: 1698: 1699:
1700: protected function getRelInstance($model=null,$relConf=null,$key='')
1701: {
1702: if (!$model && !$relConf)
1703: trigger_error(self::E_MISSING_REL_CONF);
1704: $relConf = $model ? $model::resolveConfiguration() : $relConf;
1705: $relName = ($model?:'Cortex').'\\'.$relConf['db']->uuid().'\\'.$relConf['table'];
1706: if (\Registry::exists($relName)) {
1707: $rel = \Registry::get($relName);
1708: $rel->reset();
1709: } else {
1710: $rel = $model ? new $model : new Cortex($relConf['db'], $relConf['table']);
1711: if (!$rel instanceof Cortex)
1712: trigger_error(self::E_WRONG_RELATION_CLASS);
1713: \Registry::set($relName, $rel);
1714: }
1715:
1716: if(!empty($key) && isset($this->relWhitelist[$key])) {
1717: if (isset($this->relWhitelist[$key][0]))
1718: $rel->fields($this->relWhitelist[$key][0],false);
1719: if (isset($this->relWhitelist[$key][1]))
1720: $rel->fields($this->relWhitelist[$key][1],true);
1721: }
1722: return $rel;
1723: }
1724:
1725: 1726: 1727: 1728: 1729: 1730:
1731: public function cast($obj = NULL, $rel_depths = 1)
1732: {
1733: $fields = $this->mapper->cast( ($obj) ? $obj->mapper : null );
1734: if (!empty($this->vFields))
1735: foreach(array_keys($this->vFields) as $key)
1736: $fields[$key]=$this->get($key);
1737: if (is_int($rel_depths))
1738: $rel_depths--;
1739: if (!empty($this->fieldConf)) {
1740: $fields += array_fill_keys(array_keys($this->fieldConf),NULL);
1741: if($this->whitelist)
1742: $fields = array_intersect_key($fields, array_flip($this->whitelist));
1743: $mp = $obj ? : $this;
1744: foreach ($fields as $key => &$val) {
1745:
1746: if (isset($this->fieldConf[$key]) && is_array($this->fieldConf[$key])) {
1747:
1748: if (($rel_depths === TRUE || (is_int($rel_depths) && $rel_depths >= 0))
1749: && $type=preg_grep('/[belongs|has]-(to-)*[one|many]/',
1750: array_keys($this->fieldConf[$key]))) {
1751: $relType=$type[0];
1752:
1753: $val = (($relType == 'belongs-to-one' || $relType == 'belongs-to-many')
1754: && !$mp->exists($key)) ? NULL : $mp->get($key);
1755: if ($val instanceof Cortex)
1756: $val = $val->cast(null, $rel_depths);
1757: elseif ($val instanceof CortexCollection)
1758: $val = $val->castAll($rel_depths);
1759: }
1760:
1761: elseif (isset($this->fieldConf[$key]['type'])) {
1762: if ($this->dbsType == 'sql') {
1763: if ($this->fieldConf[$key]['type'] == self::DT_SERIALIZED)
1764: $val=unserialize($this->mapper->{$key});
1765: elseif ($this->fieldConf[$key]['type'] == self::DT_JSON)
1766: $val=json_decode($this->mapper->{$key}, true);
1767: }
1768: if ($this->exists($key)
1769: && preg_match('/BOOL/i',$this->fieldConf[$key]['type'])) {
1770: $val = (bool) $this->mapper->{$key};
1771: }
1772: }
1773: }
1774: if ($this->dbsType == 'mongo' && $key == '_id')
1775: $val = (string) $val;
1776: if ($this->dbsType == 'sql' && $key == 'id' && $this->standardiseID) {
1777: $fields['_id'] = $val;
1778: unset($fields[$key]);
1779: }
1780: unset($val);
1781: }
1782: }
1783:
1784: foreach ($fields as $key => &$val) {
1785: $val = $this->emit('get_'.$key, $val);
1786: unset($val);
1787: }
1788: return $fields;
1789: }
1790:
1791: 1792: 1793: 1794: 1795: 1796:
1797: function castField($key, $rel_depths=0)
1798: {
1799: if (!$key)
1800: return NULL;
1801: $mapper_arr = $this->get($key);
1802: if(!$mapper_arr)
1803: return NULL;
1804: $out = array();
1805: foreach ($mapper_arr as $mp)
1806: $out[] = $mp->cast(null,$rel_depths);
1807: return $out;
1808: }
1809:
1810: 1811: 1812: 1813: 1814:
1815: protected function factory($mapper)
1816: {
1817: if (is_array($mapper)) {
1818: $mp = clone($this->mapper);
1819: $mp->reset();
1820: $cx = $this->factory($mp);
1821: $cx->copyfrom($mapper);
1822: } else {
1823: $cx = clone($this);
1824: $cx->reset(false);
1825: $cx->mapper = $mapper;
1826: }
1827: $cx->emit('load');
1828: return $cx;
1829: }
1830:
1831: public function dry() {
1832: return $this->mapper->dry();
1833: }
1834:
1835: 1836: 1837: 1838: 1839: 1840:
1841: public function copyfrom($key, $fields = null)
1842: {
1843: $f3 = \Base::instance();
1844: $srcfields = is_array($key) ? $key : $f3->get($key);
1845: if ($fields)
1846: if (is_callable($fields))
1847: $srcfields = $fields($srcfields);
1848: else {
1849: if (is_string($fields))
1850: $fields = $f3->split($fields);
1851: $srcfields = array_intersect_key($srcfields, array_flip($fields));
1852: }
1853: foreach ($srcfields as $key => $val) {
1854: if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type'])) {
1855: if ($this->fieldConf[$key]['type'] == self::DT_JSON && is_string($val))
1856: $val = json_decode($val);
1857: elseif ($this->fieldConf[$key]['type'] == self::DT_SERIALIZED && is_string($val))
1858: $val = unserialize($val);
1859: }
1860: $this->set($key, $val);
1861: }
1862: }
1863:
1864: 1865: 1866: 1867: 1868: 1869:
1870: public function copyto($key, $relDepth=0) {
1871: \Base::instance()->set($key, $this->cast(null,$relDepth));
1872: }
1873:
1874: public function skip($ofs = 1)
1875: {
1876: $this->reset(false);
1877: if ($this->mapper->skip($ofs))
1878: return $this;
1879: else
1880: $this->reset(false);
1881: }
1882:
1883: public function first()
1884: {
1885: $this->reset(false);
1886: $this->mapper->first();
1887: return $this;
1888: }
1889:
1890: public function last()
1891: {
1892: $this->reset(false);
1893: $this->mapper->last();
1894: return $this;
1895: }
1896:
1897: 1898: 1899: 1900: 1901:
1902: public function reset($mapper = true)
1903: {
1904: if ($mapper)
1905: $this->mapper->reset();
1906: $this->fieldsCache=array();
1907: $this->saveCsd=array();
1908: $this->countFields=array();
1909: $this->preBinds=array();
1910: $this->grp_stack=null;
1911:
1912: if (($this->dbsType == 'jig' || $this->dbsType == 'mongo')
1913: && !empty($this->fieldConf))
1914: foreach($this->fieldConf as $field_key => $field_conf)
1915: if (array_key_exists('default',$field_conf)) {
1916: $val = ($field_conf['default'] === \DB\SQL\Schema::DF_CURRENT_TIMESTAMP)
1917: ? date('Y-m-d H:i:s') : $field_conf['default'];
1918: $this->set($field_key, $val);
1919: }
1920: }
1921:
1922: 1923: 1924: 1925: 1926: 1927: 1928:
1929: function exists($key, $relField = false) {
1930: if (!$this->dry() && $key == '_id') return true;
1931: return $this->mapper->exists($key) ||
1932: ($relField && isset($this->fieldConf[$key]['relType']));
1933: }
1934:
1935: 1936: 1937: 1938: 1939:
1940: function clear($key) {
1941: unset($this->fieldsCache[$key]);
1942: if (isset($this->fieldConf[$key]['relType']))
1943: $this->set($key,null);
1944: $this->mapper->clear($key);
1945: }
1946:
1947: function insert() {
1948: $res = $this->mapper->insert();
1949: if (is_array($res))
1950: $res = $this->mapper;
1951: if (is_object($res))
1952: $res = $this->factory($res);
1953: return is_int($res) ? $this : $res;
1954: }
1955:
1956: function update() {
1957: $res = $this->mapper->update();
1958: if (is_array($res))
1959: $res = $this->mapper;
1960: if (is_object($res))
1961: $res = $this->factory($res);
1962: return is_int($res) ? $this : $res;
1963: }
1964:
1965: function dbtype() {
1966: return $this->mapper->dbtype();
1967: }
1968:
1969: public function __destruct() {
1970: unset($this->mapper);
1971: }
1972:
1973: public function __clone() {
1974: $this->mapper = clone($this->mapper);
1975: }
1976:
1977: function getiterator() {
1978:
1979: return new \ArrayIterator(array());
1980: }
1981: }
1982:
1983:
1984: class CortexQueryParser extends \Prefab {
1985:
1986: const
1987: E_BRACKETS = 'Invalid query: unbalanced brackets found',
1988: E_INBINDVALUE = 'Bind value for IN operator must be a populated array',
1989: E_ENGINEERROR = 'Engine not supported',
1990: E_MISSINGBINDKEY = 'Named bind parameter `%s` does not exist in filter arguments';
1991:
1992: protected
1993: $queryCache = array();
1994:
1995: 1996: 1997: 1998: 1999: 2000: 2001: 2002: 2003: 2004: 2005: 2006: 2007: 2008: 2009:
2010: public function prepareFilter($cond, $engine,$fieldConf=null)
2011: {
2012: if (is_null($cond)) return $cond;
2013: if (is_string($cond))
2014: $cond = array($cond);
2015: $f3 = \Base::instance();
2016: $cacheHash = $f3->hash($f3->stringify($cond)).'.'.$engine;
2017: if (isset($this->queryCache[$cacheHash]))
2018:
2019: return $this->queryCache[$cacheHash];
2020: elseif ($f3->exists('CORTEX.queryParserCache')
2021: && ($ttl = (int) $f3->get('CORTEX.queryParserCache'))) {
2022: $cache = \Cache::instance();
2023:
2024: if ($f3->get('CACHE') && $ttl && ($cached = $cache->exists($cacheHash, $ncond))
2025: && $cached[0] + $ttl > microtime(TRUE)) {
2026: $this->queryCache[$cacheHash] = $ncond;
2027: return $ncond;
2028: }
2029: }
2030: $where = array_shift($cond);
2031: $args = $cond;
2032: $where = str_replace(array('&&', '||'), array('AND', 'OR'), $where);
2033:
2034: $where = preg_replace('/\bIN\b\s*\(\s*(\?|:\w+)?\s*\)/i', 'IN $1', $where);
2035: switch ($engine) {
2036: case 'jig':
2037: $ncond = $this->_jig_parse_filter($where, $args);
2038: break;
2039: case 'mongo':
2040: $parts = $this->splitLogical($where);
2041: if (is_int(strpos($where, ':')))
2042: list($parts, $args) = $this->convertNamedParams($parts, $args);
2043: foreach ($parts as &$part) {
2044: $part = $this->_mongo_parse_relational_op($part, $args, $fieldConf);
2045: unset($part);
2046: }
2047: $ncond = $this->_mongo_parse_logical_op($parts);
2048: break;
2049: case 'sql':
2050:
2051: $where = preg_replace('/(?!\B)_id/', 'id', $where);
2052: $parts = $this->splitLogical($where);
2053:
2054: if (is_int(strpos($where, ':')))
2055: list($parts, $args) = $this->convertNamedParams($parts, $args);
2056: $ncond = array();
2057: foreach ($parts as &$part) {
2058:
2059: if (is_int(strpos($part, '?'))) {
2060: $val = array_shift($args);
2061: if (is_int($pos = strpos($part, 'IN ?'))) {
2062: if (!is_array($val) || empty($val))
2063: trigger_error(self::E_INBINDVALUE);
2064: $bindMarks = str_repeat('?,', count($val) - 1).'?';
2065: $part = substr($part, 0, $pos).'IN ('.$bindMarks.')';
2066: $ncond = array_merge($ncond, $val);
2067: } else
2068: $ncond[] = $val;
2069: }
2070: unset($part);
2071: }
2072: array_unshift($ncond, implode(' ', $parts));
2073: break;
2074: default:
2075: trigger_error(self::E_ENGINEERROR);
2076: }
2077: $this->queryCache[$cacheHash] = $ncond;
2078: if(isset($ttl) && $f3->get('CACHE')) {
2079:
2080: $cache = \Cache::instance();
2081: $cache->set($cacheHash,$ncond,$ttl);
2082: }
2083: return $ncond;
2084: }
2085:
2086: 2087: 2088: 2089: 2090:
2091: protected function splitLogical($cond)
2092: {
2093: return preg_split('/\s*((?<!\()\)|\((?!\))|\bAND\b|\bOR\b)\s*/i', $cond, -1,
2094: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
2095: }
2096:
2097: 2098: 2099: 2100: 2101: 2102:
2103: protected function convertNamedParams($parts, $args)
2104: {
2105: if (empty($args)) return array($parts, $args);
2106: $params = array();
2107: $pos = 0;
2108: foreach ($parts as &$part) {
2109: if (preg_match('/:\w+/i', $part, $match)) {
2110: if (!isset($args[$match[0]]))
2111: trigger_error(sprintf(self::E_MISSINGBINDKEY,
2112: $match[0]));
2113: $part = str_replace($match[0], '?', $part);
2114: $params[] = $args[$match[0]];
2115: } elseif (is_int(strpos($part, '?')))
2116: $params[] = $args[$pos++];
2117: unset($part);
2118: }
2119: return array($parts, $params);
2120: }
2121:
2122: 2123: 2124: 2125: 2126: 2127:
2128: protected function _jig_parse_filter($where, $args)
2129: {
2130: $parts = $this->splitLogical($where);
2131: if (is_int(strpos($where, ':')))
2132: list($parts, $args) = $this->convertNamedParams($parts, $args);
2133: $ncond = array();
2134: foreach ($parts as &$part) {
2135: if (in_array(strtoupper($part), array('AND', 'OR')))
2136: continue;
2137:
2138: $part = preg_replace('/([a-z_-]+)/i', '@$1', $part, -1, $count);
2139:
2140: if (is_int(strpos($part, '?'))) {
2141: $val = array_shift($args);
2142: preg_match('/(@\w+)/i', $part, $match);
2143:
2144: if (is_int(strpos($upart = strtoupper($part), ' @LIKE '))) {
2145: if ($not = is_int($npos = strpos($upart, '@NOT')))
2146: $pos = $npos;
2147: $val = $this->_likeValueToRegEx($val);
2148: $part = ($not ? '!' : '').'preg_match(?,'.$match[0].')';
2149: }
2150: else if (is_int($pos = strpos($upart, ' @IN '))) {
2151: if ($not = is_int($npos = strpos($upart, '@NOT')))
2152: $pos = $npos;
2153: $part = ($not ? '!' : '').'in_array('.substr($part, 0, $pos).
2154: ',array(\''.implode('\',\'', $val).'\'))';
2155: unset($val);
2156: }
2157:
2158: $part = '(isset('.$match[0].') && '.$part.')';
2159: if (isset($val))
2160: $ncond[] = $val;
2161: } elseif ($count >= 1) {
2162:
2163: preg_match_all('/(@\w+)/i', $part, $matches);
2164: $chks = array();
2165: foreach ($matches[0] as $field)
2166: $chks[] = 'isset('.$field.')';
2167: $part = '('.implode(' && ',$chks).' && ('.$part.'))';
2168: }
2169: unset($part);
2170: }
2171: array_unshift($ncond, implode(' ', $parts));
2172: return $ncond;
2173: }
2174:
2175: 2176: 2177: 2178: 2179:
2180: protected function _mongo_parse_logical_op($parts)
2181: {
2182: $b_offset = 0;
2183: $ncond = array();
2184: $child = array();
2185: for ($i = 0, $max = count($parts); $i < $max; $i++) {
2186: $part = $parts[$i];
2187: if ($part == '(') {
2188:
2189: if ($b_offset > 0)
2190: $child[] = $part;
2191: $b_offset++;
2192: } elseif ($part == ')') {
2193: $b_offset--;
2194:
2195: if ($b_offset == 0) {
2196: $ncond[] = ($this->_mongo_parse_logical_op($child));
2197: $child = array();
2198: } elseif ($b_offset < 0)
2199: trigger_error(self::E_BRACKETS);
2200: else
2201:
2202: $child[] = $part;
2203: }
2204: elseif ($b_offset > 0)
2205: $child[] = $part;
2206:
2207: elseif (!is_array($part)) {
2208: if (strtoupper($part) == 'AND')
2209: $add = true;
2210: elseif (strtoupper($part) == 'OR')
2211: $or = true;
2212: } else
2213: $ncond[] = $part;
2214: }
2215: if ($b_offset > 0)
2216: trigger_error(self::E_BRACKETS);
2217: if (isset($add))
2218: return array('$and' => $ncond);
2219: elseif (isset($or))
2220: return array('$or' => $ncond);
2221: else
2222: return $ncond[0];
2223: }
2224:
2225: 2226: 2227: 2228: 2229: 2230: 2231:
2232: protected function _mongo_parse_relational_op($part, &$args, $fieldConf=null)
2233: {
2234: if (is_null($part))
2235: return $part;
2236: if (preg_match('/\<\=|\>\=|\<\>|\<|\>|\!\=|\=\=|\=|like|not like|in|not in/i', $part, $match)) {
2237: $var = is_int(strpos($part, '?')) ? array_shift($args) : null;
2238: $exp = explode($match[0], $part);
2239: $key = trim($exp[0]);
2240:
2241: if (is_numeric($exp[1]))
2242: $var = $exp[1];
2243:
2244: elseif (!is_int(strpos($exp[1], '?')))
2245: return array('$where' => 'this.'.$key.' '.$match[0].' this.'.trim($exp[1]));
2246: $upart = strtoupper($match[0]);
2247:
2248: if ($key == '_id' || (isset($fieldConf[$key]) && isset($fieldConf[$key]['relType']))) {
2249: if (is_array($var))
2250: foreach ($var as &$id) {
2251: if (!$id instanceof \MongoId)
2252: $id = new \MongoId($id);
2253: unset($id);
2254: }
2255: elseif(!$var instanceof \MongoId)
2256: $var = new \MongoId($var);
2257: }
2258:
2259: if (in_array($upart, array('LIKE','NOT LIKE'))) {
2260: $rgx = $this->_likeValueToRegEx($var);
2261: $var = new \MongoRegex($rgx);
2262: if ($upart == 'NOT LIKE')
2263: $var = array('$not' => $var);
2264: }
2265: elseif (in_array($upart, array('IN','NOT IN'))) {
2266: $var = array(($upart=='NOT IN')?'$nin':'$in' => array_values($var));
2267: }
2268: elseif (!in_array($match[0], array('==', '='))) {
2269: $opr = str_replace(array('<>', '<', '>', '!', '='),
2270: array('$ne', '$lt', '$gt', '$n', 'e'), $match[0]);
2271: $var = array($opr => (strtolower($var) == 'null') ? null :
2272: (is_object($var) ? $var : (is_numeric($var) ? $var + 0 : $var)));
2273: }
2274: return array($key => $var);
2275: }
2276: return $part;
2277: }
2278:
2279: 2280: 2281: 2282:
2283: protected function _likeValueToRegEx($var)
2284: {
2285: $lC = substr($var, -1, 1);
2286:
2287: if ($var[0] == '%' && $lC == '%')
2288: $var = '/'.substr($var, 1, -1).'/';
2289:
2290: elseif ($lC == '%')
2291: $var = '/^'.substr($var, 0, -1).'/';
2292:
2293: elseif ($var[0] == '%')
2294: $var = '/'.substr($var, 1).'$/';
2295: return $var;
2296: }
2297:
2298: 2299: 2300: 2301: 2302: 2303: 2304: 2305: 2306: 2307: 2308:
2309: public function prepareOptions($options, $engine)
2310: {
2311: if (empty($options) || !is_array($options))
2312: return null;
2313: switch ($engine) {
2314: case 'jig':
2315: if (array_key_exists('order', $options))
2316: $options['order'] = str_replace(array('asc', 'desc'),
2317: array('SORT_ASC', 'SORT_DESC'), strtolower($options['order']));
2318: break;
2319: case 'mongo':
2320: if (array_key_exists('order', $options)) {
2321: $sorts = explode(',', $options['order']);
2322: $sorting = array();
2323: foreach ($sorts as $sort) {
2324: $sp = explode(' ', trim($sort));
2325: $sorting[$sp[0]] = (array_key_exists(1, $sp) &&
2326: strtoupper($sp[1]) == 'DESC') ? -1 : 1;
2327: }
2328: $options['order'] = $sorting;
2329: }
2330: if (array_key_exists('group', $options) && is_string($options['group'])) {
2331: $keys = explode(',',$options['group']);
2332: $options['group']=array('keys'=>array(),'initial'=>array(),'reduce'=>'function (obj, prev) {}','finalize'=>'');
2333: $keys = array_combine($keys,array_fill(0,count($keys),1));
2334: $options['group']['keys']=$keys;
2335: $options['group']['initial']=$keys;
2336: }
2337: break;
2338: }
2339: return $options;
2340: }
2341: }
2342:
2343: class CortexCollection extends \ArrayIterator {
2344:
2345: protected
2346: $relSets = array(),
2347: $pointer = 0,
2348: $changed = false,
2349: $cid;
2350:
2351: const
2352: E_UnknownCID = 'This Collection does not exist: %s',
2353: E_SubsetKeysValue = '$keys must be an array or split-able string, but %s was given.';
2354:
2355: public function __construct() {
2356: $this->cid = uniqid('cortex_collection_');
2357: parent::__construct();
2358: }
2359:
2360:
2361: private function __clone() { }
2362:
2363: 2364: 2365: 2366:
2367: function setModels($models,$init=true) {
2368: array_map(array($this,'add'),$models);
2369: if ($init)
2370: $this->changed = false;
2371: }
2372:
2373: 2374: 2375: 2376:
2377: function add(Cortex $model) {
2378: $model->addToCollection($this);
2379: $this->append($model);
2380: }
2381:
2382: public function offsetSet($i, $val) {
2383: $this->changed=true;
2384: parent::offsetSet($i,$val);
2385: }
2386:
2387: public function hasChanged() {
2388: return $this->changed;
2389: }
2390:
2391: 2392: 2393: 2394: 2395:
2396: public function getRelSet($key) {
2397: return (isset($this->relSets[$key])) ? $this->relSets[$key] : null;
2398: }
2399:
2400: 2401: 2402: 2403: 2404:
2405: public function setRelSet($key,$set) {
2406: $this->relSets[$key] = $set;
2407: }
2408:
2409: 2410: 2411: 2412: 2413:
2414: public function hasRelSet($key) {
2415: return array_key_exists($key,$this->relSets);
2416: }
2417:
2418: public function expose() {
2419: return $this->getArrayCopy();
2420: }
2421:
2422: 2423: 2424: 2425: 2426: 2427:
2428: public function getSubset($prop,$keys) {
2429: if (is_string($keys))
2430: $keys = \Base::instance()->split($keys);
2431: if (!is_array($keys))
2432: trigger_error(sprintf(self::E_SubsetKeysValue,gettype($keys)));
2433: if (!$this->hasRelSet($prop) || !($relSet = $this->getRelSet($prop)))
2434: return null;
2435: foreach ($keys as &$key) {
2436: if ($key instanceof \MongoId)
2437: $key = (string) $key;
2438: unset($key);
2439: }
2440: return array_values(array_intersect_key($relSet, array_flip($keys)));
2441: }
2442:
2443: 2444: 2445: 2446: 2447: 2448:
2449: public function getAll($prop, $raw = false)
2450: {
2451: $out = array();
2452: foreach ($this->getArrayCopy() as $model)
2453: if ($model->exists($prop,true)) {
2454: $val = $model->get($prop, $raw);
2455: if (!empty($val))
2456: $out[] = $val;
2457: }
2458: return $out;
2459: }
2460:
2461: 2462: 2463: 2464: 2465:
2466: public function castAll($rel_depths=1) {
2467: $out = array();
2468: foreach ($this->getArrayCopy() as $model)
2469: $out[] = $model->cast(null,$rel_depths);
2470: return $out;
2471: }
2472:
2473: 2474: 2475: 2476: 2477: 2478:
2479: public function getBy($index, $nested = false)
2480: {
2481: $out = array();
2482: foreach ($this->getArrayCopy() as $model)
2483: if ($model->exists($index)) {
2484: $val = $model->get($index, true);
2485: if (!empty($val))
2486: if($nested) $out[(string) $val][] = $model;
2487: else $out[(string) $val] = $model;
2488: }
2489: return $out;
2490: }
2491:
2492: 2493: 2494: 2495:
2496: public function orderBy($cond){
2497: $cols=\Base::instance()->split($cond);
2498: $this->uasort(function($val1,$val2) use($cols) {
2499: foreach ($cols as $col) {
2500: $parts=explode(' ',$col,2);
2501: $order=empty($parts[1])?'ASC':$parts[1];
2502: $col=$parts[0];
2503: list($v1,$v2)=array($val1[$col],$val2[$col]);
2504: if ($out=strnatcmp($v1,$v2)*
2505: ((strtoupper($order)=='ASC')*2-1))
2506: return $out;
2507: }
2508: return 0;
2509: });
2510: }
2511:
2512: 2513: 2514: 2515: 2516:
2517: public function slice($offset,$limit=null) {
2518: $this->rewind();
2519: $i=0;
2520: $del=array();
2521: while ($this->valid()) {
2522: if ($i < $offset)
2523: $del[]=$this->key();
2524: elseif ($i >= $offset && $limit && $i >= ($offset+$limit))
2525: $del[]=$this->key();
2526: $i++;
2527: $this->next();
2528: }
2529: foreach ($del as $ii)
2530: unset($this[$ii]);
2531: }
2532:
2533: static public function factory($records) {
2534: $cc = new self();
2535: $cc->setModels($records);
2536: return $cc;
2537: }
2538:
2539: }