* #84 test data dump from CREST login * updated "credits" dialog (Google+ link) fixed login form layout * updated Cortex Data-Mapper * - #84 CREST Login (WIP) - New CREST controller - Database restructuring - improved type-casting for some controller functions - New login process - Fixed some bugs during the setup process (/setup root) - Added CREST request caching by response headers * pathfinder-84 [Feature Request] CREST Pilot Tracking, many smaller Bugfixes * pathfinder-84 [Feature Request] added develop JS files * closed #121 fixed wormhole signature type caching * closed #120 removed map-loading animation for larger maps (same behaviour as IGB) * closed #119 fixed wormhole signature id count * closed #114 Added check for already existing system when adding a new one. (fixed PDO 'duplicate entry' error) * closed #112 fixed DataTables error for missing "status" data (signature table) * closed #111 fixed convertDataToUTC(); client side date transformation * closed #109 fixed system TrueSec rounding * closed #103 fixed system updated timestamp in getData() * fixed CSS class for secStatus in Routes module * closed #121 fixed wormhole signature type caching * changed dateTime format from German to US format fixed some minor bugs in signatureTable module * closed #81 fixed "signature type" overwriting by "signature reader" update * closed #106 added new signature_types form C5/6 wormholes (gas/ore) * closed #129 fixed parameter hinting * closed #131 new "route search" algorithm, added current map systems to live search, added refresh/update functionality for each found route, added bulk route refresh function, added "meta map" route search (search on multiple maps), added route "filters" (restrict search on "stargates", "wormholes", "jumpbridges"), added route "filter" for wormholes (reduced/critical wormholes) closed #89 fixed "loop connections" on same system #84 added error messages for "invalid" CREST "Client ID" added "bootboxjs" (customized styled checkboxes/radio buttons) CSS only "Font Awesome" version upgrade 4.4.0 -> 4.61 "Bootbox.js" version upgrade 4.3.0 -> 4.4.0 fixed "system dialog" (added responsive layout) * closed #134 fixed db column type DT_INT (8 bytes) to DT_BIGINT * closed #138 added new cookie based login * closed #137 fixed javascript errors on trying to establish an "invalid" connection * - #84, #138 improved "character selection" on login page (expired cookies are deleted, character panel layout improvements) - added new "Server info panel" to the login page - added new cronjob to delete expired cookie authentication data * #138 enables character switching between characters which have same user * - PHP Framework upgrade 3.5.0 -> 3.5.1 (fixes some issues with CREST cURL caching, and SESSION management) - #138 added "cookie logout" to "logout" menu entry * - updated "feature page" with new feature descriptions and label - added some new images to the "feature gallery" - removed "beta" status from "magnetizing" feature on map menu - hide "server status" panel on "mobile" breakpoint * - #138 clear character authentication data on sold characters * closed #142 added custom "onsuspect()" session handler * #142 do not log suspect if no file is defined in pathfinder.ini * #142 added NullSec Data/Relic sites to C1/2/3 wormholes as signature option * #144 fixed "Character not found" warning * #144 fixed "Character not found" warning * closed #144 fixed broken routes panel in IGB * updated README.md for upcoming release * #147 response header validation * #149 changed comment for 'BASE' framework var * fixed map import * - added minimal SDE dump (EVE Online: Citadel) - #147 improved CREST API error logging (WIP) - improved SSO controller (removed access_token from public endpoints) * closed #154 added alliance maps to CREST API * - updated Gulp build dependencies - increased CREST timeout from 3s -> 4s - added "Accept" Headers for some CREST endpoints * cloased #147 * - closed #153 added character verification check for getAll(); Signatures Ajax endpoint * - updated README.md (added Slack developer chat information) * Bugfix frig holes (#159) * added missing frigate wormholes and fixed Q003 destination in shattered wormholes * changed C7 to 0.0 for Q003 * - fixed broken "graph" data for system * added a "failover" system for bad crest requests (HTTP status 5xx,.. ) * Red Gaint => Red Giant (#161) * closed #163 added CREST endpoint support for "waypoints" * fixed typo * closed #160 fixed tooltip container * - added new features to login page * closes #154 added alliance map support * fixed XML path for cronjobs * fixed a bug with inactive "private" maps * closes #175 added alternative environment configuration * - v1.0.0 build
2633 lines
81 KiB
PHP
2633 lines
81 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Cortex - a general purpose mapper for the PHP Fat-Free Framework
|
|
*
|
|
* The contents of this file are subject to the terms of the GNU General
|
|
* Public License Version 3.0. You may not use this file except in
|
|
* compliance with the license. Any of the license terms and conditions
|
|
* can be waived if you get permission from the copyright holder.
|
|
*
|
|
* crafted by __ __ __
|
|
* |__| |--.| |--.-----.-----.
|
|
* | | < | <| -__|-- __|
|
|
* |__|__|__||__|__|_____|_____|
|
|
*
|
|
* Copyright (c) 2016 by ikkez
|
|
* Christian Knuth <mail@ikkez.de>
|
|
* https://github.com/ikkez/F3-Sugar/
|
|
*
|
|
* @package DB
|
|
* @version 1.4.1
|
|
* @date 29.01.2016
|
|
* @since 24.04.2012
|
|
*/
|
|
|
|
namespace DB;
|
|
use DB\SQL\Schema;
|
|
|
|
class Cortex extends Cursor {
|
|
|
|
protected
|
|
// config
|
|
$db, // DB object [ \DB\SQL, \DB\Jig, \DB\Mongo ]
|
|
$table, // selected table, string
|
|
$fluid, // fluid sql schema mode, boolean
|
|
$fieldConf, // field configuration, array
|
|
$ttl, // default mapper schema ttl
|
|
$rel_ttl, // default mapper rel ttl
|
|
$primary, // SQL table primary key
|
|
// behaviour
|
|
$smartLoading, // intelligent lazy eager loading, boolean
|
|
$standardiseID, // return standardized '_id' field for SQL when casting
|
|
// internals
|
|
$dbsType, // mapper engine type [jig, sql, mongo]
|
|
$fieldsCache, // relation field cache
|
|
$saveCsd, // mm rel save cascade
|
|
$collection, // collection
|
|
$relFilter, // filter for loading related models
|
|
$hasCond, // IDs of records the next find should have
|
|
$whitelist, // restrict to these fields
|
|
$relWhitelist, // restrict relations to these fields
|
|
$grp_stack, // stack of group conditions
|
|
$countFields, // relational counter buffer
|
|
$preBinds, // bind values to be prepended to $filter
|
|
$vFields, // virtual fields buffer
|
|
$_ttl; // rel_ttl overwrite
|
|
|
|
/** @var Cursor */
|
|
protected $mapper;
|
|
|
|
/** @var CortexQueryParser */
|
|
protected $queryParser;
|
|
|
|
static
|
|
$init = false; // just init without mapper
|
|
|
|
const
|
|
// special datatypes
|
|
DT_SERIALIZED = 'SERIALIZED',
|
|
DT_JSON = 'JSON',
|
|
|
|
// error messages
|
|
E_ARRAY_DATATYPE = 'Unable to save an Array in field %s. Use DT_SERIALIZED or DT_JSON.',
|
|
E_CONNECTION = 'No valid DB Connection given.',
|
|
E_NO_TABLE = 'No table specified.',
|
|
E_UNKNOWN_DB_ENGINE = 'This unknown DB system is not supported: %s',
|
|
E_FIELD_SETUP = 'No field setup defined',
|
|
E_UNKNOWN_FIELD = 'Field %s does not exist in %s.',
|
|
E_INVALID_RELATION_OBJECT = 'You can only save hydrated mapper objects',
|
|
E_NULLABLE_COLLISION = 'Unable to set NULL to the NOT NULLABLE field: %s',
|
|
E_WRONG_RELATION_CLASS = 'Relations only works with Cortex objects',
|
|
E_MM_REL_VALUE = 'Invalid value for many field "%s". Expecting null, split-able string, hydrated mapper object, or array of mapper objects.',
|
|
E_MM_REL_CLASS = 'Mismatching m:m relation config from class `%s` to `%s`.',
|
|
E_MM_REL_FIELD = 'Mismatching m:m relation keys from `%s` to `%s`.',
|
|
E_REL_CONF_INC = 'Incomplete relation config for `%s`. Linked key is missing.',
|
|
E_MISSING_REL_CONF = 'Cannot create related model. Specify a model name or relConf array.',
|
|
E_HAS_COND = 'Cannot use a "has"-filter on a non-bidirectional relation field';
|
|
|
|
/**
|
|
* init the ORM, based on given DBS
|
|
* @param null|object $db
|
|
* @param string $table
|
|
* @param null|bool $fluid
|
|
* @param int $ttl
|
|
*/
|
|
public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0)
|
|
{
|
|
if (!is_null($fluid))
|
|
$this->fluid = $fluid;
|
|
if (!is_object($this->db=(is_string($db=($db?:$this->db))?\Base::instance()->get($db):$db)))
|
|
trigger_error(self::E_CONNECTION);
|
|
if ($this->db instanceof Jig)
|
|
$this->dbsType = 'jig';
|
|
elseif ($this->db instanceof SQL)
|
|
$this->dbsType = 'sql';
|
|
elseif ($this->db instanceof Mongo)
|
|
$this->dbsType = 'mongo';
|
|
if ($table)
|
|
$this->table = $table;
|
|
if ($this->dbsType != 'sql')
|
|
$this->primary = '_id';
|
|
elseif (!$this->primary)
|
|
$this->primary = 'id';
|
|
$this->table = $this->getTable();
|
|
if (!$this->table)
|
|
trigger_error(self::E_NO_TABLE);
|
|
$this->ttl = $ttl ?: 60;
|
|
if (!$this->rel_ttl)
|
|
$this->rel_ttl = 0;
|
|
$this->_ttl = $this->rel_ttl ?: 0;
|
|
if (static::$init == TRUE) return;
|
|
if ($this->fluid)
|
|
static::setup($this->db,$this->table,array());
|
|
$this->initMapper();
|
|
}
|
|
|
|
/**
|
|
* create mapper instance
|
|
*/
|
|
public function initMapper()
|
|
{
|
|
switch ($this->dbsType) {
|
|
case 'jig':
|
|
$this->mapper = new Jig\Mapper($this->db, $this->table);
|
|
break;
|
|
case 'sql':
|
|
$this->mapper = new SQL\Mapper($this->db, $this->table, $this->whitelist,
|
|
($this->fluid)?0:$this->ttl);
|
|
break;
|
|
case 'mongo':
|
|
$this->mapper = new Mongo\Mapper($this->db, $this->table);
|
|
break;
|
|
default:
|
|
trigger_error(sprintf(self::E_UNKNOWN_DB_ENGINE,$this->dbsType));
|
|
}
|
|
$this->queryParser = CortexQueryParser::instance();
|
|
$this->reset();
|
|
$this->clearFilter();
|
|
$f3 = \Base::instance();
|
|
$this->smartLoading = $f3->exists('CORTEX.smartLoading') ?
|
|
$f3->get('CORTEX.smartLoading') : TRUE;
|
|
$this->standardiseID = $f3->exists('CORTEX.standardiseID') ?
|
|
$f3->get('CORTEX.standardiseID') : TRUE;
|
|
if(!empty($this->fieldConf))
|
|
foreach($this->fieldConf as &$conf) {
|
|
$conf=static::resolveRelationConf($conf);
|
|
unset($conf);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* get fields or set whitelist / blacklist of fields
|
|
* @param array $fields
|
|
* @param bool $exclude
|
|
* @return array
|
|
*/
|
|
public function fields(array $fields=array(), $exclude=false)
|
|
{
|
|
if ($fields)
|
|
// collect restricted fields for related mappers
|
|
foreach($fields as $i=>$val)
|
|
if(is_int(strpos($val,'.'))) {
|
|
list($key, $relField) = explode('.',$val,2);
|
|
$this->relWhitelist[$key][(int)$exclude][] = $relField;
|
|
unset($fields[$i]);
|
|
$fields[] = $key;
|
|
}
|
|
$fields = array_unique($fields);
|
|
$schema = $this->whitelist ?: $this->mapper->fields();
|
|
if (!$schema && !$this->dbsType != 'sql' && $this->dry()) {
|
|
$schema = $this->load()->mapper->fields();
|
|
$this->reset();
|
|
}
|
|
if (!$this->whitelist && $this->fieldConf)
|
|
$schema=array_unique(array_merge($schema,array_keys($this->fieldConf)));
|
|
if (!$fields || empty($fields))
|
|
return $schema;
|
|
elseif ($exclude) {
|
|
$this->whitelist=array_diff($schema,$fields);
|
|
} else
|
|
$this->whitelist=$fields;
|
|
$id=$this->dbsType=='sql'?$this->primary:'_id';
|
|
if(!in_array($id,$this->whitelist))
|
|
$this->whitelist[]=$id;
|
|
$this->initMapper();
|
|
return $this->whitelist;
|
|
}
|
|
|
|
/**
|
|
* set model definition
|
|
* config example:
|
|
* array('title' => array(
|
|
* 'type' => \DB\SQL\Schema::DT_TEXT,
|
|
* 'default' => 'new record title',
|
|
* 'nullable' => true
|
|
* )
|
|
* '...' => ...
|
|
* )
|
|
* @param array $config
|
|
*/
|
|
function setFieldConfiguration(array $config)
|
|
{
|
|
$this->fieldConf = $config;
|
|
$this->reset();
|
|
}
|
|
|
|
/**
|
|
* returns model field conf array
|
|
* @return array|null
|
|
*/
|
|
public function getFieldConfiguration()
|
|
{
|
|
return $this->fieldConf;
|
|
}
|
|
|
|
/**
|
|
* kick start to just fetch the config
|
|
* @return array
|
|
*/
|
|
static public function resolveConfiguration()
|
|
{
|
|
static::$init=true;
|
|
$self = new static();
|
|
static::$init=false;
|
|
$conf = array (
|
|
'table'=>$self->getTable(),
|
|
'fieldConf'=>$self->getFieldConfiguration(),
|
|
'db'=>$self->db,
|
|
'fluid'=>$self->fluid,
|
|
'primary'=>$self->primary,
|
|
);
|
|
unset($self);
|
|
return $conf;
|
|
}
|
|
|
|
/**
|
|
* give this model a reference to the collection it is part of
|
|
* @param CortexCollection $cx
|
|
*/
|
|
public function addToCollection($cx) {
|
|
$this->collection = $cx;
|
|
}
|
|
|
|
/**
|
|
* returns the collection where this model lives in
|
|
* @return CortexCollection
|
|
*/
|
|
protected function getCollection()
|
|
{
|
|
return ($this->collection && $this->smartLoading)
|
|
? $this->collection : false;
|
|
}
|
|
|
|
/**
|
|
* returns model table name
|
|
* @return string
|
|
*/
|
|
public function getTable()
|
|
{
|
|
if (!$this->table && ($this->fluid || static::$init))
|
|
$this->table = strtolower(get_class($this));
|
|
return $this->table;
|
|
}
|
|
|
|
/**
|
|
* setup / update table schema
|
|
* @static
|
|
* @param $db
|
|
* @param $table
|
|
* @param $fields
|
|
* @return bool
|
|
*/
|
|
static public function setup($db=null, $table=null, $fields=null)
|
|
{
|
|
/** @var Cortex $self */
|
|
$self = get_called_class();
|
|
if (is_null($db) || is_null($table) || is_null($fields))
|
|
$df = $self::resolveConfiguration();
|
|
if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db)))
|
|
trigger_error(self::E_CONNECTION);
|
|
if (strlen($table=$table?:$df['table'])==0)
|
|
trigger_error(self::E_NO_TABLE);
|
|
if (is_null($fields))
|
|
if (!empty($df['fieldConf']))
|
|
$fields = $df['fieldConf'];
|
|
elseif(!$df['fluid']) {
|
|
trigger_error(self::E_FIELD_SETUP);
|
|
return false;
|
|
} else
|
|
$fields = array();
|
|
if ($db instanceof SQL) {
|
|
$schema = new Schema($db);
|
|
// prepare field configuration
|
|
if (!empty($fields))
|
|
foreach($fields as $key => &$field) {
|
|
// fetch relation field types
|
|
$field = static::resolveRelationConf($field);
|
|
// check m:m relation
|
|
if (array_key_exists('has-many', $field)) {
|
|
// m:m relation conf [class,to-key,from-key]
|
|
if (is_array($relConf = $field['has-many'])) {
|
|
$rel = $relConf[0]::resolveConfiguration();
|
|
// check if foreign conf matches m:m
|
|
if (array_key_exists($relConf[1],$rel['fieldConf'])
|
|
&& !is_null($rel['fieldConf'][$relConf[1]])
|
|
&& $relConf['hasRel'] == 'has-many') {
|
|
// compute mm table name
|
|
$mmTable = isset($relConf[2]) ? $relConf[2] :
|
|
static::getMMTableName(
|
|
$rel['table'], $relConf[1], $table, $key,
|
|
$rel['fieldConf'][$relConf[1]]['has-many']);
|
|
if (!in_array($mmTable,$schema->getTables())) {
|
|
$mmt = $schema->createTable($mmTable);
|
|
$mmt->addColumn($relConf[1])->type($relConf['relFieldType']);
|
|
$mmt->addColumn($key)->type($field['type']);
|
|
$index = array($relConf[1],$key);
|
|
sort($index);
|
|
$mmt->addIndex($index);
|
|
$mmt->build();
|
|
}
|
|
}
|
|
}
|
|
unset($fields[$key]);
|
|
continue;
|
|
}
|
|
// skip virtual fields with no type
|
|
if (!array_key_exists('type', $field)) {
|
|
unset($fields[$key]);
|
|
continue;
|
|
}
|
|
// transform array fields
|
|
if (in_array($field['type'], array(self::DT_JSON, self::DT_SERIALIZED)))
|
|
$field['type']=$schema::DT_TEXT;
|
|
// defaults values
|
|
if (!array_key_exists('nullable', $field))
|
|
$field['nullable'] = true;
|
|
unset($field);
|
|
}
|
|
if (!in_array($table, $schema->getTables())) {
|
|
// create table
|
|
$table = $schema->createTable($table);
|
|
foreach ($fields as $field_key => $field_conf)
|
|
$table->addColumn($field_key, $field_conf);
|
|
if(isset($df) && $df['primary'] != 'id') {
|
|
$table->addColumn($df['primary'])->type_int();
|
|
$table->primary($df['primary']);
|
|
}
|
|
$table->build();
|
|
} else {
|
|
// add missing fields
|
|
$table = $schema->alterTable($table);
|
|
$existingCols = $table->getCols();
|
|
foreach ($fields as $field_key => $field_conf)
|
|
if (!in_array($field_key, $existingCols))
|
|
$table->addColumn($field_key, $field_conf);
|
|
// remove unused fields
|
|
// foreach ($existingCols as $col)
|
|
// if (!in_array($col, array_keys($fields)) && $col!='id')
|
|
// $table->dropColumn($col);
|
|
$table->build();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* erase all model data, handle with care
|
|
* @param null $db
|
|
* @param null $table
|
|
*/
|
|
static public function setdown($db=null, $table=null)
|
|
{
|
|
$self = get_called_class();
|
|
if (is_null($db) || is_null($table))
|
|
$df = $self::resolveConfiguration();
|
|
if (!is_object($db=(is_string($db=($db?:$df['db']))?\Base::instance()->get($db):$db)))
|
|
trigger_error(self::E_CONNECTION);
|
|
if (strlen($table=strtolower($table?:$df['table']))==0)
|
|
trigger_error(self::E_NO_TABLE);
|
|
if (isset($df) && !empty($df['fieldConf']))
|
|
$fields = $df['fieldConf'];
|
|
else
|
|
$fields = array();
|
|
$deletable = array();
|
|
$deletable[] = $table;
|
|
foreach ($fields as $key => $field) {
|
|
$field = static::resolveRelationConf($field);
|
|
if (array_key_exists('has-many',$field)) {
|
|
if (!is_array($relConf = $field['has-many']))
|
|
continue;
|
|
$rel = $relConf[0]::resolveConfiguration();
|
|
// check if foreign conf matches m:m
|
|
if (array_key_exists($relConf[1],$rel['fieldConf']) && !is_null($relConf[1])
|
|
&& key($rel['fieldConf'][$relConf[1]]) == 'has-many') {
|
|
// compute mm table name
|
|
$deletable[] = isset($relConf[2]) ? $relConf[2] :
|
|
static::getMMTableName(
|
|
$rel['table'], $relConf[1], $table, $key,
|
|
$rel['fieldConf'][$relConf[1]]['has-many']);
|
|
}
|
|
}
|
|
}
|
|
|
|
if($db instanceof Jig) {
|
|
/** @var Jig $db */
|
|
$dir = $db->dir();
|
|
foreach ($deletable as $item)
|
|
if(file_exists($dir.$item))
|
|
unlink($dir.$item);
|
|
} elseif($db instanceof SQL) {
|
|
/** @var SQL $db */
|
|
$schema = new Schema($db);
|
|
$tables = $schema->getTables();
|
|
foreach ($deletable as $item)
|
|
if(in_array($item, $tables))
|
|
$schema->dropTable($item);
|
|
} elseif($db instanceof Mongo) {
|
|
/** @var Mongo $db */
|
|
foreach ($deletable as $item)
|
|
$db->selectCollection($item)->drop();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* computes the m:m table name
|
|
* @param string $ftable foreign table
|
|
* @param string $fkey foreign key
|
|
* @param string $ptable own table
|
|
* @param string $pkey own key
|
|
* @param null|array $fConf foreign conf [class,key]
|
|
* @return string
|
|
*/
|
|
static protected function getMMTableName($ftable, $fkey, $ptable, $pkey, $fConf=null)
|
|
{
|
|
if ($fConf) {
|
|
list($fclass, $pfkey) = $fConf;
|
|
$self = get_called_class();
|
|
// check for a matching config
|
|
if (!is_int(strpos($fclass, $self)))
|
|
trigger_error(sprintf(self::E_MM_REL_CLASS, $fclass, $self));
|
|
if ($pfkey != $pkey)
|
|
trigger_error(sprintf(self::E_MM_REL_FIELD,
|
|
$fclass.'.'.$pfkey, $self.'.'.$pkey));
|
|
}
|
|
$mmTable = array($ftable.'__'.$fkey, $ptable.'__'.$pkey);
|
|
natcasesort($mmTable);
|
|
$return = strtolower(str_replace('\\', '_', implode('_mm_', $mmTable)));
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* get mm table name from config
|
|
* @param array $conf own relation config
|
|
* @param string $key relation field
|
|
* @param null|array $fConf optional foreign config
|
|
* @return string
|
|
*/
|
|
protected function mmTable($conf, $key, $fConf=null)
|
|
{
|
|
if (!isset($conf['refTable'])) {
|
|
// compute mm table name
|
|
$mmTable = isset($conf[2]) ? $conf[2] :
|
|
static::getMMTableName($conf['relTable'],
|
|
$conf['relField'], $this->table, $key, $fConf);
|
|
$this->fieldConf[$key]['has-many']['refTable'] = $mmTable;
|
|
} else
|
|
$mmTable = $conf['refTable'];
|
|
return $mmTable;
|
|
}
|
|
|
|
/**
|
|
* resolve relation field types
|
|
* @param $field
|
|
* @return mixed
|
|
*/
|
|
protected static function resolveRelationConf($field)
|
|
{
|
|
if (array_key_exists('belongs-to-one', $field)) {
|
|
// find primary field definition
|
|
if (!is_array($relConf = $field['belongs-to-one']))
|
|
$relConf = array($relConf, '_id');
|
|
// set field type
|
|
if ($relConf[1] == '_id')
|
|
$field['type'] = Schema::DT_INT4;
|
|
else {
|
|
// find foreign field type
|
|
$fc = $relConf[0]::resolveConfiguration();
|
|
$field['belongs-to-one']['relPK'] = $fc['primary'];
|
|
$field['type'] = $fc['fieldConf'][$relConf[1]]['type'];
|
|
}
|
|
$field['nullable'] = true;
|
|
$field['relType'] = 'belongs-to-one';
|
|
}
|
|
elseif (array_key_exists('belongs-to-many', $field)){
|
|
$field['type'] = self::DT_JSON;
|
|
$field['nullable'] = true;
|
|
$field['relType'] = 'belongs-to-many';
|
|
}
|
|
elseif (array_key_exists('has-many', $field)){
|
|
$field['relType'] = 'has-many';
|
|
if (!isset($field['type']))
|
|
$field['type'] = Schema::DT_INT;
|
|
$relConf = $field['has-many'];
|
|
if(!is_array($relConf))
|
|
return $field;
|
|
$rel = $relConf[0]::resolveConfiguration();
|
|
if(array_key_exists('has-many',$rel['fieldConf'][$relConf[1]])) {
|
|
$field['has-many']['hasRel'] = 'has-many';
|
|
$field['has-many']['relTable'] = $rel['table'];
|
|
$field['has-many']['relField'] = $relConf[1];
|
|
$field['has-many']['relFieldType'] = isset($rel['fieldConf'][$relConf[1]]['type']) ?
|
|
$rel['fieldConf'][$relConf[1]]['type'] : Schema::DT_INT;
|
|
$field['has-many']['relPK'] = isset($relConf[3])?$relConf[3]:$rel['primary'];
|
|
} else {
|
|
$field['has-many']['hasRel'] = 'belongs-to-one';
|
|
$toConf=$rel['fieldConf'][$relConf[1]]['belongs-to-one'];
|
|
if (is_array($toConf))
|
|
$field['has-many']['relField'] = $toConf[1];
|
|
}
|
|
} elseif(array_key_exists('has-one', $field))
|
|
$field['relType'] = 'has-one';
|
|
return $field;
|
|
}
|
|
|
|
/**
|
|
* Return an array of result arrays matching criteria
|
|
* @param null $filter
|
|
* @param array $options
|
|
* @param int $ttl
|
|
* @param int $rel_depths
|
|
* @return array
|
|
*/
|
|
public function afind($filter = NULL, array $options = NULL, $ttl = 0, $rel_depths = 1)
|
|
{
|
|
$result = $this->find($filter, $options, $ttl);
|
|
return $result ? $result->castAll($rel_depths): NULL;
|
|
}
|
|
|
|
/**
|
|
* Return an array of objects matching criteria
|
|
* @param array|null $filter
|
|
* @param array|null $options
|
|
* @param int $ttl
|
|
* @return CortexCollection
|
|
*/
|
|
public function find($filter = NULL, array $options = NULL, $ttl = 0)
|
|
{
|
|
$sort=false;
|
|
if ($this->dbsType!='sql') {
|
|
if (!empty($this->countFields))
|
|
// see if reordering is needed
|
|
foreach($this->countFields as $counter) {
|
|
if ($options && isset($options['order']) &&
|
|
preg_match('/count_'.$counter.'\h+(asc|desc)/i',$options['order'],$match))
|
|
$sort=true;
|
|
}
|
|
if ($sort) {
|
|
// backup slice settings
|
|
if (isset($options['limit'])) {
|
|
$limit = $options['limit'];
|
|
unset($options['limit']);
|
|
}
|
|
if (isset($options['offset'])) {
|
|
$offset = $options['offset'];
|
|
unset($options['offset']);
|
|
}
|
|
}
|
|
}
|
|
$this->_ttl=$ttl?:$this->rel_ttl;
|
|
$result = $this->filteredFind($filter,$options,$ttl);
|
|
if (empty($result))
|
|
return false;
|
|
foreach($result as &$record) {
|
|
$record = $this->factory($record);
|
|
unset($record);
|
|
}
|
|
if (!empty($this->countFields))
|
|
// add counter for NoSQL engines
|
|
foreach($this->countFields as $counter)
|
|
foreach($result as &$mapper) {
|
|
$cr=$mapper->get($counter);
|
|
$mapper->virtual('count_'.$counter,$cr?count($cr):null);
|
|
unset($mapper);
|
|
}
|
|
$cc = new CortexCollection();
|
|
$cc->setModels($result);
|
|
if($sort) {
|
|
$cc->orderBy($options['order']);
|
|
$cc->slice(isset($offset)?$offset:0,isset($limit)?$limit:NULL);
|
|
}
|
|
$this->clearFilter();
|
|
return $cc;
|
|
}
|
|
|
|
/**
|
|
* wrapper for custom find queries
|
|
* @param array $filter
|
|
* @param array $options
|
|
* @param int $ttl
|
|
* @param bool $count
|
|
* @return array|false array of underlying cursor objects
|
|
*/
|
|
protected function filteredFind($filter = NULL, array $options = NULL, $ttl = 0, $count=false)
|
|
{
|
|
if ($this->grp_stack) {
|
|
if ($this->dbsType == 'mongo') {
|
|
$group = array(
|
|
'keys' => $this->grp_stack['keys'],
|
|
'reduce' => 'function (obj, prev) {'.$this->grp_stack['reduce'].'}',
|
|
'initial' => $this->grp_stack['initial'],
|
|
'finalize' => $this->grp_stack['finalize'],
|
|
);
|
|
if ($options && isset($options['group'])) {
|
|
if(is_array($options['group']))
|
|
$options['group'] = array_merge($options['group'],$group);
|
|
else {
|
|
$keys = explode(',',$options['group']);
|
|
$keys = array_combine($keys,array_fill(0,count($keys),1));
|
|
$group['keys'] = array_merge($group['keys'],$keys);
|
|
$options['group'] = $group;
|
|
}
|
|
} else
|
|
$options = array('group'=>$group);
|
|
}
|
|
if($this->dbsType == 'sql') {
|
|
if ($options && isset($options['group']))
|
|
$options['group'].= ','.$this->grp_stack;
|
|
else
|
|
$options['group'] = $this->grp_stack;
|
|
}
|
|
// Jig can't group yet, but pending enhancement https://github.com/bcosca/fatfree/pull/616
|
|
}
|
|
if ($this->dbsType == 'sql' && !$count) {
|
|
$m_refl=new \ReflectionObject($this->mapper);
|
|
$m_ad_prop=$m_refl->getProperty('adhoc');
|
|
$m_ad_prop->setAccessible(true);
|
|
$m_refl_adhoc=$m_ad_prop->getValue($this->mapper);
|
|
$m_ad_prop->setAccessible(false);
|
|
unset($m_ad_prop,$m_refl);
|
|
}
|
|
$hasJoin = array();
|
|
if ($this->hasCond) {
|
|
foreach($this->hasCond as $key => $hasCond) {
|
|
$addToFilter = null;
|
|
if ($deep = is_int(strpos($key,'.'))) {
|
|
$key = rtrim($key,'.');
|
|
$hasCond = array(null,null);
|
|
}
|
|
list($has_filter,$has_options) = $hasCond;
|
|
$type = $this->fieldConf[$key]['relType'];
|
|
$fromConf = $this->fieldConf[$key][$type];
|
|
switch($type) {
|
|
case 'has-one':
|
|
case 'has-many':
|
|
if (!is_array($fromConf))
|
|
trigger_error(sprintf(self::E_REL_CONF_INC, $key));
|
|
$id = $this->dbsType == 'sql' ? $this->primary : '_id';
|
|
if ($type=='has-many' && isset($fromConf['relField'])
|
|
&& $fromConf['hasRel'] == 'belongs-to-one')
|
|
$id=$fromConf['relField'];
|
|
// many-to-many
|
|
if ($type == 'has-many' && $fromConf['hasRel'] == 'has-many') {
|
|
if (!$deep && $this->dbsType == 'sql'
|
|
&& !isset($has_options['limit']) && !isset($has_options['offset'])) {
|
|
$hasJoin = array_merge($hasJoin,
|
|
$this->_hasJoinMM_sql($key,$hasCond,$filter,$options));
|
|
$options['group'] = (isset($options['group'])?$options['group'].',':'').
|
|
$this->db->quotekey($this->table.'.'.$this->primary);
|
|
$groupFields = explode(',', preg_replace('/"/','',$options['group']));
|
|
// all non-aggregated fields need to be present in the GROUP BY clause
|
|
if (isset($m_refl_adhoc) && preg_match('/sybase|dblib|odbc|sqlsrv/i',$this->db->driver()))
|
|
foreach (array_diff($this->mapper->fields(),array_keys($m_refl_adhoc)) as $field)
|
|
if (!in_array($this->table.'.'.$field,$groupFields))
|
|
$options['group'] .= ', '.$this->db->quotekey($this->table.'.'.$field);
|
|
}
|
|
elseif ($result = $this->_hasRefsInMM($key,$has_filter,$has_options,$ttl))
|
|
$addToFilter = array($id.' IN ?', $result);
|
|
} // *-to-one
|
|
elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl))
|
|
$addToFilter = array($id.' IN ?', $result);
|
|
break;
|
|
// one-to-*
|
|
case 'belongs-to-one':
|
|
if (!$deep && $this->dbsType == 'sql'
|
|
&& !isset($has_options['limit']) && !isset($has_options['offset'])) {
|
|
if (!is_array($fromConf))
|
|
$fromConf = array($fromConf, '_id');
|
|
$rel = $fromConf[0]::resolveConfiguration();
|
|
if ($this->dbsType == 'sql' && $fromConf[1] == '_id')
|
|
$fromConf[1] = $rel['primary'];
|
|
$hasJoin[] = $this->_hasJoin_sql($key,$rel['table'],$hasCond,$filter,$options);
|
|
} elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl))
|
|
$addToFilter = array($key.' IN ?', $result);
|
|
break;
|
|
default:
|
|
trigger_error(self::E_HAS_COND);
|
|
}
|
|
if (isset($result) && !isset($addToFilter))
|
|
return false;
|
|
elseif (isset($addToFilter)) {
|
|
if (!$filter)
|
|
$filter = array('');
|
|
if (!empty($filter[0]))
|
|
$filter[0] .= ' and ';
|
|
$cond = array_shift($addToFilter);
|
|
if ($this->dbsType=='sql')
|
|
$cond = $this->_sql_quoteCondition($cond,$this->db->quotekey($this->table));
|
|
$filter[0] .= '('.$cond.')';
|
|
$filter = array_merge($filter, $addToFilter);
|
|
}
|
|
}
|
|
$this->hasCond = null;
|
|
}
|
|
$filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf);
|
|
if ($this->dbsType=='sql') {
|
|
$qtable = $this->db->quotekey($this->table);
|
|
if (isset($options['order']) && $this->db->driver() == 'pgsql')
|
|
// PostgreSQLism: sort NULL values to the end of a table
|
|
$options['order'] = preg_replace('/\h+DESC/i',' DESC NULLS LAST',$options['order']);
|
|
if (!empty($hasJoin)) {
|
|
// assemble full sql query
|
|
$adhoc='';
|
|
if ($count)
|
|
$sql = 'SELECT COUNT(*) AS '.$this->db->quotekey('rows').' FROM '.$qtable;
|
|
else {
|
|
if (!empty($this->preBinds)) {
|
|
$crit = array_shift($filter);
|
|
$filter = array_merge($this->preBinds,$filter);
|
|
array_unshift($filter,$crit);
|
|
}
|
|
if (!empty($m_refl_adhoc))
|
|
foreach ($m_refl_adhoc as $key=>$val)
|
|
$adhoc.=', '.$val['expr'].' AS '.$key;
|
|
$sql = 'SELECT '.$qtable.'.*'.$adhoc.' FROM '.$qtable;
|
|
}
|
|
$sql .= ' '.implode(' ',$hasJoin).' WHERE '.$filter[0];
|
|
if (!$count) {
|
|
if (isset($options['group']))
|
|
$sql .= ' GROUP BY '.$this->_sql_quoteCondition($options['group'], $this->table);
|
|
if (isset($options['order']))
|
|
$sql .= ' ORDER BY '.$options['order'];
|
|
if (preg_match('/mssql|sqlsrv|odbc/', $this->db->driver()) &&
|
|
(isset($options['limit']) || isset($options['offset']))) {
|
|
$ofs=isset($options['offset'])?(int)$options['offset']:0;
|
|
$lmt=isset($options['limit'])?(int)$options['limit']:0;
|
|
if (strncmp($this->db->version(),'11',2)>=0) {
|
|
// SQL Server 2012
|
|
if (!isset($options['order']))
|
|
$sql.=' ORDER BY '.$this->db->quotekey($this->primary);
|
|
$sql.=' OFFSET '.$ofs.' ROWS'.($lmt?' FETCH NEXT '.$lmt.' ROWS ONLY':'');
|
|
} else {
|
|
// SQL Server 2008
|
|
$order=(!isset($options['order']))
|
|
?($this->db->quotekey($this->table.'.'.$this->primary)):$options['order'];
|
|
$sql=str_replace('SELECT','SELECT '.($lmt>0?'TOP '.($ofs+$lmt):'').' ROW_NUMBER() '.
|
|
'OVER (ORDER BY '.$order.') AS rnum,',$sql);
|
|
$sql='SELECT * FROM ('.$sql.') x WHERE rnum > '.($ofs);
|
|
}
|
|
} else {
|
|
if (isset($options['limit']))
|
|
$sql.=' LIMIT '.(int)$options['limit'];
|
|
if (isset($options['offset']))
|
|
$sql.=' OFFSET '.(int)$options['offset'];
|
|
}
|
|
}
|
|
unset($filter[0]);
|
|
$result = $this->db->exec($sql, $filter, $ttl);
|
|
if ($count)
|
|
return $result[0]['rows'];
|
|
foreach ($result as &$record) {
|
|
// factory new mappers
|
|
$mapper = clone($this->mapper);
|
|
$mapper->reset();
|
|
// TODO: refactor this. Reflection can be removed for F3 >= v3.4.1
|
|
$mapper->query= array($record);
|
|
$m_adhoc = empty($adhoc) ? array() : $m_refl_adhoc;
|
|
foreach ($record as $key=>$val)
|
|
if (isset($m_refl_adhoc[$key]))
|
|
$m_adhoc[$key]['value']=$val;
|
|
else
|
|
$mapper->set($key, $val);
|
|
if (!empty($adhoc)) {
|
|
$refl = new \ReflectionObject($mapper);
|
|
$prop = $refl->getProperty('adhoc');
|
|
$prop->setAccessible(true);
|
|
$prop->setValue($mapper,$m_adhoc);
|
|
$prop->setAccessible(false);
|
|
}
|
|
$record = $mapper;
|
|
unset($record, $mapper);
|
|
}
|
|
return $result;
|
|
} elseif (!empty($this->preBinds) && !$count) {
|
|
// bind values to adhoc queries
|
|
if (!$filter)
|
|
// we (PDO) need any filter to bind values
|
|
$filter = array('1=1');
|
|
$crit = array_shift($filter);
|
|
$filter = array_merge($this->preBinds,$filter);
|
|
array_unshift($filter,$crit);
|
|
}
|
|
}
|
|
return ($count)
|
|
? $this->mapper->count($filter,$ttl)
|
|
: $this->mapper->find($filter,$this->queryParser->prepareOptions($options,$this->dbsType),$ttl);
|
|
}
|
|
|
|
/**
|
|
* Retrieve first object that satisfies criteria
|
|
* @param null $filter
|
|
* @param array $options
|
|
* @param int $ttl
|
|
* @return Cortex
|
|
*/
|
|
public function load($filter = NULL, array $options = NULL, $ttl = 0)
|
|
{
|
|
$this->reset();
|
|
$this->_ttl=$ttl?:$this->rel_ttl;
|
|
$res = $this->filteredFind($filter, $options, $ttl);
|
|
if ($res) {
|
|
$this->mapper->query = $res;
|
|
$this->first();
|
|
} else
|
|
$this->mapper->reset();
|
|
$this->emit('load');
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* add has-conditional filter to next find call
|
|
* @param string $key
|
|
* @param array $filter
|
|
* @param null $options
|
|
* @return $this
|
|
*/
|
|
public function has($key, $filter, $options = null) {
|
|
if (is_string($filter))
|
|
$filter=array($filter);
|
|
if (is_int(strpos($key,'.'))) {
|
|
list($key,$fkey) = explode('.',$key,2);
|
|
if (!isset($this->hasCond[$key.'.']))
|
|
$this->hasCond[$key.'.'] = array();
|
|
$this->hasCond[$key.'.'][$fkey] = array($filter,$options);
|
|
} else {
|
|
if (!isset($this->fieldConf[$key]))
|
|
trigger_error(sprintf(self::E_UNKNOWN_FIELD,$key,get_called_class()));
|
|
if (!isset($this->fieldConf[$key]['relType']))
|
|
trigger_error(self::E_HAS_COND);
|
|
$this->hasCond[$key] = array($filter,$options);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* return IDs of records that has a linkage to this mapper
|
|
* @param string $key relation field
|
|
* @param array $filter condition for foreign records
|
|
* @param array $options filter options for foreign records
|
|
* @param int $ttl
|
|
* @return array|false
|
|
*/
|
|
protected function _hasRefsIn($key, $filter, $options, $ttl = 0)
|
|
{
|
|
$type = $this->fieldConf[$key]['relType'];
|
|
$fieldConf = $this->fieldConf[$key][$type];
|
|
// one-to-many shortcut
|
|
$rel = $this->getRelFromConf($fieldConf,$key);
|
|
$hasSet = $rel->find($filter, $options, $ttl);
|
|
if (!$hasSet)
|
|
return false;
|
|
$hasSetByRelId = array_unique($hasSet->getAll($fieldConf[1], true));
|
|
return empty($hasSetByRelId) ? false : $hasSetByRelId;
|
|
}
|
|
|
|
/**
|
|
* return IDs of own mappers that match the given relation filter on pivot tables
|
|
* @param string $key
|
|
* @param array $filter
|
|
* @param array $options
|
|
* @param int $ttl
|
|
* @return array|false
|
|
*/
|
|
protected function _hasRefsInMM($key, $filter, $options, $ttl=0)
|
|
{
|
|
$fieldConf = $this->fieldConf[$key]['has-many'];
|
|
$rel = $this->getRelInstance($fieldConf[0],null,$key,true);
|
|
$hasSet = $rel->find($filter,$options,$ttl);
|
|
$result = false;
|
|
if ($hasSet) {
|
|
$hasIDs = $hasSet->getAll('_id',true);
|
|
$mmTable = $this->mmTable($fieldConf,$key);
|
|
$pivot = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable));
|
|
$pivotSet = $pivot->find(array($key.' IN ?',$hasIDs),null,$ttl);
|
|
if ($pivotSet)
|
|
$result = array_unique($pivotSet->getAll($fieldConf['relField'],true));
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* build query for SQL pivot table join and merge conditions
|
|
*/
|
|
protected function _hasJoinMM_sql($key, $hasCond, &$filter, &$options)
|
|
{
|
|
$fieldConf = $this->fieldConf[$key]['has-many'];
|
|
$hasJoin = array();
|
|
$mmTable = $this->mmTable($fieldConf,$key);
|
|
$hasJoin[] = $this->_sql_left_join($this->primary,$this->table,$fieldConf['relField'],$mmTable);
|
|
$hasJoin[] = $this->_sql_left_join($key,$mmTable,$fieldConf['relPK'],$fieldConf['relTable']);
|
|
$this->_sql_mergeRelCondition($hasCond,$fieldConf['relTable'],$filter,$options);
|
|
return $hasJoin;
|
|
}
|
|
|
|
/**
|
|
* build query for single SQL table join and merge conditions
|
|
*/
|
|
protected function _hasJoin_sql($key, $table, $cond, &$filter, &$options)
|
|
{
|
|
$relConf = $this->fieldConf[$key]['belongs-to-one'];
|
|
$relModel = is_array($relConf)?$relConf[0]:$relConf;
|
|
$rel = $this->getRelInstance($relModel,null,$key);
|
|
$fkey = is_array($this->fieldConf[$key]['belongs-to-one']) ?
|
|
$this->fieldConf[$key]['belongs-to-one'][1] : $rel->primary;
|
|
$query = $this->_sql_left_join($key,$this->table,$fkey,$table);
|
|
$this->_sql_mergeRelCondition($cond,$table,$filter,$options);
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* assemble SQL join query string
|
|
*/
|
|
protected function _sql_left_join($skey,$sTable,$fkey,$fTable)
|
|
{
|
|
$skey = $this->db->quotekey($skey);
|
|
$sTable = $this->db->quotekey($sTable);
|
|
$fkey = $this->db->quotekey($fkey);
|
|
$fTable = $this->db->quotekey($fTable);
|
|
return 'LEFT JOIN '.$fTable.' ON '.$sTable.'.'.$skey.' = '.$fTable.'.'.$fkey;
|
|
}
|
|
|
|
/**
|
|
* merge condition of relation with current condition
|
|
* @param array $cond condition of related model
|
|
* @param string $table table of related model
|
|
* @param array $filter current filter to merge with
|
|
* @param array $options current options to merge with
|
|
*/
|
|
protected function _sql_mergeRelCondition($cond, $table, &$filter, &$options)
|
|
{
|
|
$table = $this->db->quotekey($table);
|
|
if (!empty($cond[0])) {
|
|
$whereClause = '('.array_shift($cond[0]).')';
|
|
$whereClause = $this->_sql_quoteCondition($whereClause,$table);
|
|
if (!$filter)
|
|
$filter = array($whereClause);
|
|
elseif (!empty($filter[0]))
|
|
$filter[0] = '('.$this->_sql_quoteCondition($filter[0],
|
|
$this->db->quotekey($this->table)).') and '.$whereClause;
|
|
$filter = array_merge($filter, $cond[0]);
|
|
}
|
|
if ($cond[1] && isset($cond[1]['group'])) {
|
|
$hasGroup = preg_replace('/(\w+)/i', $table.'.$1', $cond[1]['group']);
|
|
$options['group'] .= ','.$hasGroup;
|
|
}
|
|
}
|
|
|
|
protected function _sql_quoteCondition($cond, $table)
|
|
{
|
|
$db = $this->db;
|
|
if (preg_match('/[`\'"\[\]]/i',$cond))
|
|
return $cond;
|
|
return preg_replace_callback('/\w+/i',function($match) use($table,$db) {
|
|
if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[0]))
|
|
return $match[0];
|
|
return $table.'.'.$db->quotekey($match[0]);
|
|
}, $cond);
|
|
}
|
|
|
|
/**
|
|
* add filter for loading related models
|
|
* @param string $key
|
|
* @param array $filter
|
|
* @param array $option
|
|
* @return $this
|
|
*/
|
|
public function filter($key,$filter=null,$option=null)
|
|
{
|
|
if (is_int(strpos($key,'.'))) {
|
|
list($key,$fkey) = explode('.',$key,2);
|
|
if (!isset($this->relFilter[$key.'.']))
|
|
$this->relFilter[$key.'.'] = array();
|
|
$this->relFilter[$key.'.'][$fkey] = array($filter,$option);
|
|
} else
|
|
$this->relFilter[$key] = array($filter,$option);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* removes one or all relation filter
|
|
* @param null|string $key
|
|
*/
|
|
public function clearFilter($key = null)
|
|
{
|
|
if (!$key)
|
|
$this->relFilter = array();
|
|
elseif(isset($this->relFilter[$key]))
|
|
unset($this->relFilter[$key]);
|
|
}
|
|
|
|
/**
|
|
* merge the relation filter to the query criteria if it exists
|
|
* @param string $key
|
|
* @param array $crit
|
|
* @return array
|
|
*/
|
|
protected function mergeWithRelFilter($key,$crit)
|
|
{
|
|
if (array_key_exists($key, $this->relFilter) &&
|
|
!empty($this->relFilter[$key][0]))
|
|
$crit=$this->mergeFilter(array($this->relFilter[$key][0],$crit));
|
|
return $crit;
|
|
}
|
|
|
|
/**
|
|
* merge multiple filters
|
|
* @param $filters
|
|
* @param string $glue
|
|
* @return array
|
|
*/
|
|
public function mergeFilter($filters,$glue='and') {
|
|
$crit = array();
|
|
$params = array();
|
|
if ($filters) {
|
|
foreach($filters as $filter) {
|
|
$crit[] = array_shift($filter);
|
|
$params = array_merge($params,$filter);
|
|
}
|
|
array_unshift($params,'( '.implode(' ) '.$glue.' ( ',$crit).' )');
|
|
}
|
|
return $params;
|
|
}
|
|
|
|
/**
|
|
* returns the option condition for a relation filter, if defined
|
|
* @param string $key
|
|
* @return array null
|
|
*/
|
|
protected function getRelFilterOption($key)
|
|
{
|
|
return (array_key_exists($key, $this->relFilter) &&
|
|
!empty($this->relFilter[$key][1]))
|
|
? $this->relFilter[$key][1] : null;
|
|
}
|
|
|
|
/**
|
|
* Delete object/s and reset ORM
|
|
* @param $filter
|
|
* @return bool
|
|
*/
|
|
public function erase($filter = null)
|
|
{
|
|
$filter = $this->queryParser->prepareFilter($filter, $this->dbsType);
|
|
if (!$filter) {
|
|
if ($this->emit('beforeerase')===false)
|
|
return false;
|
|
if ($this->fieldConf) {
|
|
foreach($this->fieldConf as $field => $conf)
|
|
if (isset($conf['has-many']) &&
|
|
$conf['has-many']['hasRel']=='has-many')
|
|
$this->set($field,null);
|
|
$this->save();
|
|
}
|
|
$this->mapper->erase();
|
|
$this->emit('aftererase');
|
|
} elseif($filter)
|
|
$this->mapper->erase($filter);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Save mapped record
|
|
* @return mixed
|
|
**/
|
|
function save()
|
|
{
|
|
// update changed collections
|
|
$fields = $this->fieldConf;
|
|
if ($fields)
|
|
foreach($fields as $key=>$conf)
|
|
if (!empty($this->fieldsCache[$key]) && $this->fieldsCache[$key] instanceof CortexCollection
|
|
&& $this->fieldsCache[$key]->hasChanged())
|
|
$this->set($key,$this->fieldsCache[$key]->getAll('_id',true));
|
|
// perform event & save operations
|
|
if ($new = $this->dry()) {
|
|
if ($this->emit('beforeinsert')===false)
|
|
return false;
|
|
$result=$this->insert();
|
|
} else {
|
|
if ($this->emit('beforeupdate')===false)
|
|
return false;
|
|
$result=$this->update();
|
|
}
|
|
// m:m save cascade
|
|
if (!empty($this->saveCsd)) {
|
|
foreach($this->saveCsd as $key => $val) {
|
|
if($fields[$key]['relType'] == 'has-many') {
|
|
$relConf = $fields[$key]['has-many'];
|
|
$mmTable = $this->mmTable($relConf,$key);
|
|
$rel = $this->getRelInstance(null, array('db'=>$this->db, 'table'=>$mmTable));
|
|
$id = $this->get($relConf['relPK'],true);
|
|
// delete all refs
|
|
if (is_null($val))
|
|
$rel->erase(array($relConf['relField'].' = ?', $id));
|
|
// update refs
|
|
elseif (is_array($val)) {
|
|
$rel->erase(array($relConf['relField'].' = ?', $id));
|
|
foreach($val as $v) {
|
|
$rel->set($key,$v);
|
|
$rel->set($relConf['relField'],$id);
|
|
$rel->save();
|
|
$rel->reset();
|
|
}
|
|
}
|
|
unset($rel);
|
|
} elseif($fields[$key]['relType'] == 'has-one') {
|
|
$val->save();
|
|
}
|
|
}
|
|
$this->saveCsd = array();
|
|
}
|
|
$this->emit($new?'afterinsert':'afterupdate');
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Count records that match criteria
|
|
* @param null $filter
|
|
* @param int $ttl
|
|
* @return mixed
|
|
*/
|
|
public function count($filter = NULL, $ttl = 60)
|
|
{
|
|
$has=$this->hasCond;
|
|
$count=$this->filteredFind($filter,null,$ttl,true);
|
|
$this->hasCond=$has;
|
|
return $count;
|
|
}
|
|
|
|
/**
|
|
* Count records that are currently loaded
|
|
* @return int
|
|
*/
|
|
public function loaded() {
|
|
return count($this->mapper->query);
|
|
}
|
|
|
|
/**
|
|
* add a virtual field that counts occurring relations
|
|
* @param $key
|
|
*/
|
|
public function countRel($key) {
|
|
if (isset($this->fieldConf[$key])){
|
|
// one-to-one, one-to-many
|
|
if ($this->fieldConf[$key]['relType'] == 'belongs-to-one') {
|
|
if ($this->dbsType == 'sql') {
|
|
$this->set('count_'.$key,'count('.$key.')');
|
|
$this->grp_stack=(!$this->grp_stack)?$key:$this->grp_stack.','.$key;
|
|
} elseif ($this->dbsType == 'mongo')
|
|
$this->_mongo_addGroup(array(
|
|
'keys'=>array($key=>1),
|
|
'reduce' => 'prev.count_'.$key.'++;',
|
|
"initial" => array("count_".$key => 0)
|
|
));
|
|
else
|
|
trigger_error('Cannot add direct relational counter.');
|
|
} elseif($this->fieldConf[$key]['relType'] == 'has-many') {
|
|
$relConf=$this->fieldConf[$key]['has-many'];
|
|
if ($relConf['hasRel']=='has-many') {
|
|
// many-to-many
|
|
if ($this->dbsType == 'sql') {
|
|
$mmTable = $this->mmTable($relConf,$key);
|
|
$filter = array($this->db->quotekey($mmTable).'.'.$this->db->quotekey($relConf['relField'])
|
|
.' = '.$this->db->quotekey($this->table).'.'.$this->db->quotekey($this->primary));
|
|
$from=$mmTable;
|
|
if (array_key_exists($key, $this->relFilter) &&
|
|
!empty($this->relFilter[$key][0])) {
|
|
$options=array();
|
|
$from = $mmTable.' '.$this->_sql_left_join($key,$mmTable,$relConf['relPK'],$relConf['relTable']);
|
|
$relFilter = $this->relFilter[$key];
|
|
$this->_sql_mergeRelCondition($relFilter,$relConf['relTable'],$filter,$options);
|
|
}
|
|
$filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf);
|
|
$crit = array_shift($filter);
|
|
if (count($filter)>0)
|
|
$this->preBinds+=$filter;
|
|
$this->set('count_'.$key,'(select count('.$mmTable.'.'.$relConf['relField'].') from '.$from.
|
|
' where '.$crit.' group by '.$mmTable.'.'.$relConf['relField'].')');
|
|
} else {
|
|
// count rel
|
|
$this->countFields[]=$key;
|
|
}
|
|
} elseif($this->fieldConf[$key]['has-many']['hasRel']=='belongs-to-one') {
|
|
// many-to-one
|
|
if ($this->dbsType == 'sql') {
|
|
$fConf=$relConf[0]::resolveConfiguration();
|
|
$fTable=$this->db->quotekey($fConf['table']);
|
|
$fKey=$this->db->quotekey($fConf['primary']);
|
|
$rKey=$this->db->quotekey($relConf[1]);
|
|
$pKey=$this->db->quotekey($this->primary);
|
|
$table=$this->db->quotekey($this->table);
|
|
$crit = $fTable.'.'.$rKey.' = '.$table.'.'.$pKey;
|
|
$filter = $this->mergeWithRelFilter($key,array($crit));
|
|
$filter = $this->queryParser->prepareFilter($filter,$this->dbsType,$this->fieldConf);
|
|
$crit = array_shift($filter);
|
|
if (count($filter)>0)
|
|
$this->preBinds+=$filter;
|
|
$this->set('count_'.$key,'(select count('.$fTable.'.'.$fKey.') from '.$fTable.' where '.
|
|
$crit.' group by '.$fTable.'.'.$rKey.')');
|
|
} else {
|
|
// count rel
|
|
$this->countFields[]=$key;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* merge mongo group options array
|
|
* @param $opt
|
|
*/
|
|
protected function _mongo_addGroup($opt){
|
|
if (!$this->grp_stack)
|
|
$this->grp_stack = array('keys'=>array(),'initial'=>array(),'reduce'=>'','finalize'=>'');
|
|
if (isset($opt['keys']))
|
|
$this->grp_stack['keys']+=$opt['keys'];
|
|
if (isset($opt['reduce']))
|
|
$this->grp_stack['reduce'].=$opt['reduce'];
|
|
if (isset($opt['initial']))
|
|
$this->grp_stack['initial']+=$opt['initial'];
|
|
if (isset($opt['finalize']))
|
|
$this->grp_stack['finalize'].=$opt['finalize'];
|
|
}
|
|
|
|
/**
|
|
* update a given date or time field with the current time
|
|
* @param string $key
|
|
*/
|
|
public function touch($key) {
|
|
if (isset($this->fieldConf[$key])
|
|
&& isset($this->fieldConf[$key]['type'])) {
|
|
$type = $this->fieldConf[$key]['type'];
|
|
$date = ($this->dbsType=='sql' && preg_match('/mssql|sybase|dblib|odbc|sqlsrv/',
|
|
$this->db->driver())) ? 'Ymd' : 'Y-m-d';
|
|
if ($type == Schema::DT_DATETIME || Schema::DT_TIMESTAMP)
|
|
$this->set($key,date($date.' H:i:s'));
|
|
elseif ($type == Schema::DT_DATE)
|
|
$this->set($key,date($date));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bind value to key
|
|
* @return mixed
|
|
* @param $key string
|
|
* @param $val mixed
|
|
*/
|
|
function set($key, $val)
|
|
{
|
|
$fields = $this->fieldConf;
|
|
unset($this->fieldsCache[$key]);
|
|
// pre-process if field config available
|
|
if (!empty($fields) && isset($fields[$key]) && is_array($fields[$key])) {
|
|
// handle relations
|
|
if (isset($fields[$key]['belongs-to-one'])) {
|
|
// one-to-many, one-to-one
|
|
if (is_null($val))
|
|
$val = NULL;
|
|
elseif (is_object($val) &&
|
|
!($this->dbsType=='mongo' && $val instanceof \MongoId)) {
|
|
// fetch fkey from mapper
|
|
if (!$val instanceof Cortex || $val->dry())
|
|
trigger_error(self::E_INVALID_RELATION_OBJECT);
|
|
else {
|
|
$relConf = $fields[$key]['belongs-to-one'];
|
|
$rel_field = (is_array($relConf) ? $relConf[1] : '_id');
|
|
$val = $val->get($rel_field,true);
|
|
}
|
|
} elseif ($this->dbsType == 'mongo' && !$val instanceof \MongoId)
|
|
$val = new \MongoId($val);
|
|
} elseif (isset($fields[$key]['has-one'])){
|
|
$relConf = $fields[$key]['has-one'];
|
|
if (is_null($val)) {
|
|
$val = $this->get($key);
|
|
$val->set($relConf[1],NULL);
|
|
} else {
|
|
if (!$val instanceof Cortex) {
|
|
$rel = $this->getRelInstance($relConf[0],null,$key);
|
|
$rel->load(array('_id = ?', $val));
|
|
$val = $rel;
|
|
}
|
|
$val->set($relConf[1], $this->_id);
|
|
}
|
|
$this->saveCsd[$key] = $val;
|
|
return $val;
|
|
} elseif (isset($fields[$key]['belongs-to-many'])) {
|
|
// many-to-many, unidirectional
|
|
$fields[$key]['type'] = self::DT_JSON;
|
|
$relConf = $fields[$key]['belongs-to-many'];
|
|
$rel_field = (is_array($relConf) ? $relConf[1] : '_id');
|
|
$val = $this->getForeignKeysArray($val, $rel_field, $key);
|
|
}
|
|
elseif (isset($fields[$key]['has-many'])) {
|
|
// many-to-many, bidirectional
|
|
$relConf = $fields[$key]['has-many'];
|
|
if ($relConf['hasRel'] == 'has-many') {
|
|
// custom setter
|
|
$val = $this->emit('set_'.$key, $val);
|
|
$val = $this->getForeignKeysArray($val,'_id',$key);
|
|
$this->saveCsd[$key] = $val; // array of keys
|
|
$this->fieldsCache[$key] = $val;
|
|
return $val;
|
|
} elseif ($relConf['hasRel'] == 'belongs-to-one') {
|
|
// TODO: many-to-one, bidirectional, inverse way
|
|
trigger_error("not implemented");
|
|
}
|
|
}
|
|
// convert array content
|
|
if (is_array($val) && $this->dbsType == 'sql')
|
|
if ($fields[$key]['type'] == self::DT_SERIALIZED)
|
|
$val = serialize($val);
|
|
elseif ($fields[$key]['type'] == self::DT_JSON)
|
|
$val = json_encode($val);
|
|
else
|
|
trigger_error(sprintf(self::E_ARRAY_DATATYPE, $key));
|
|
// add nullable polyfill
|
|
if ($val === NULL && ($this->dbsType == 'jig' || $this->dbsType == 'mongo')
|
|
&& array_key_exists('nullable', $fields[$key]) && $fields[$key]['nullable'] === false)
|
|
trigger_error(sprintf(self::E_NULLABLE_COLLISION,$key));
|
|
// MongoId shorthand
|
|
if ($this->dbsType == 'mongo' && !$val instanceof \MongoId) {
|
|
if ($key == '_id')
|
|
$val = new \MongoId($val);
|
|
elseif (preg_match('/INT/i',$fields[$key]['type'])
|
|
&& !isset($fields[$key]['relType']))
|
|
$val = (int) $val;
|
|
}
|
|
if (preg_match('/BOOL/i',$fields[$key]['type'])) {
|
|
$val = !$val || $val==='false' ? false : (bool) $val;
|
|
if ($this->dbsType == 'sql')
|
|
$val = (int) $val;
|
|
}
|
|
}
|
|
// fluid SQL
|
|
if ($this->fluid && $this->dbsType == 'sql') {
|
|
$schema = new Schema($this->db);
|
|
$table = $schema->alterTable($this->table);
|
|
// add missing field
|
|
if (!in_array($key,$table->getCols())) {
|
|
// determine data type
|
|
if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type']))
|
|
$type = $this->fieldConf[$key]['type'];
|
|
elseif (is_int($val)) $type = $schema::DT_INT;
|
|
elseif (is_double($val)) $type = $schema::DT_DOUBLE;
|
|
elseif (is_float($val)) $type = $schema::DT_FLOAT;
|
|
elseif (is_bool($val)) $type = $schema::DT_BOOLEAN;
|
|
elseif (date('Y-m-d H:i:s', strtotime($val)) == $val) $type = $schema::DT_DATETIME;
|
|
elseif (date('Y-m-d', strtotime($val)) == $val) $type = $schema::DT_DATE;
|
|
elseif (\UTF::instance()->strlen($val)<=255) $type = $schema::DT_VARCHAR256;
|
|
else $type = $schema::DT_TEXT;
|
|
$table->addColumn($key)->type($type);
|
|
$table->build();
|
|
// update mapper fields
|
|
$newField = $table->getCols(true);
|
|
$newField = $newField[$key];
|
|
$refl = new \ReflectionObject($this->mapper);
|
|
$prop = $refl->getProperty('fields');
|
|
$prop->setAccessible(true);
|
|
$fields = $prop->getValue($this->mapper);
|
|
$fields[$key] = $newField + array('value'=>NULL,'changed'=>NULL);
|
|
$prop->setValue($this->mapper,$fields);
|
|
}
|
|
}
|
|
// custom setter
|
|
$val = $this->emit('set_'.$key, $val);
|
|
return $this->mapper->set($key, $val);
|
|
}
|
|
|
|
/**
|
|
* call custom field handlers
|
|
* @param $event
|
|
* @param $val
|
|
* @return mixed
|
|
*/
|
|
protected function emit($event, $val=null)
|
|
{
|
|
if (isset($this->trigger[$event])) {
|
|
if (preg_match('/^[sg]et_/',$event)) {
|
|
$val = (is_string($f=$this->trigger[$event])
|
|
&& preg_match('/^[sg]et_/',$f))
|
|
? call_user_func(array($this,$event),$val)
|
|
: \Base::instance()->call($f,array($this,$val));
|
|
} else
|
|
$val = \Base::instance()->call($this->trigger[$event],array($this,$val));
|
|
} elseif (preg_match('/^[sg]et_/',$event) && method_exists($this,$event)) {
|
|
$this->trigger[] = $event;
|
|
$val = call_user_func(array($this,$event),$val);
|
|
}
|
|
return $val;
|
|
}
|
|
|
|
/**
|
|
* Define a custom field setter
|
|
* @param $key
|
|
* @param $func
|
|
*/
|
|
public function onset($key,$func) {
|
|
$this->trigger['set_'.$key] = $func;
|
|
}
|
|
|
|
/**
|
|
* Define a custom field getter
|
|
* @param $key
|
|
* @param $func
|
|
*/
|
|
public function onget($key,$func) {
|
|
$this->trigger['get_'.$key] = $func;
|
|
}
|
|
|
|
/**
|
|
* virtual mapper field setter
|
|
* @param string $key
|
|
* @param mixed|callback $val
|
|
* @return mixed|null
|
|
*/
|
|
public function virtual($key,$val) {
|
|
$this->vFields[$key]=$val;
|
|
}
|
|
|
|
/**
|
|
* Retrieve contents of key
|
|
* @return mixed
|
|
* @param string $key
|
|
* @param bool $raw
|
|
*/
|
|
function &get($key,$raw = false)
|
|
{
|
|
// handle virtual fields
|
|
if (isset($this->vFields[$key])) {
|
|
$out = (is_callable($this->vFields[$key]))
|
|
? call_user_func($this->vFields[$key], $this) : $this->vFields[$key];
|
|
return $out;
|
|
}
|
|
$fields = $this->fieldConf;
|
|
$id = $this->primary;
|
|
if ($key == '_id' && $this->dbsType == 'sql')
|
|
$key = $id;
|
|
if ($this->whitelist && !in_array($key,$this->whitelist)) {
|
|
$out = null;
|
|
return $out;
|
|
}
|
|
if ($raw) {
|
|
$out = $this->exists($key) ? $this->mapper->{$key} : NULL;
|
|
return $out;
|
|
}
|
|
if (!empty($fields) && isset($fields[$key]) && is_array($fields[$key])
|
|
&& !array_key_exists($key,$this->fieldsCache)) {
|
|
// load relations
|
|
if (isset($fields[$key]['belongs-to-one'])) {
|
|
// one-to-X, bidirectional, direct way
|
|
if (!$this->exists($key) || is_null($this->mapper->{$key}))
|
|
$this->fieldsCache[$key] = null;
|
|
else {
|
|
// get config for this field
|
|
$relConf = $fields[$key]['belongs-to-one'];
|
|
// fetch related model
|
|
$rel = $this->getRelFromConf($relConf,$key);
|
|
// am i part of a result collection?
|
|
if ($cx = $this->getCollection()) {
|
|
// does the collection has cached results for this key?
|
|
if (!$cx->hasRelSet($key)) {
|
|
// build the cache, find all values of current key
|
|
$relKeys = array_unique($cx->getAll($key,true));
|
|
// find related models
|
|
$crit = array($relConf[1].' IN ?', $relKeys);
|
|
$relSet = $rel->find($this->mergeWithRelFilter($key, $crit),
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
// cache relSet, sorted by ID
|
|
$cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL);
|
|
}
|
|
// get a subset of the preloaded set
|
|
$result = $cx->getSubset($key,(string) $this->get($key,true));
|
|
$this->fieldsCache[$key] = $result ? $result[0] : NULL;
|
|
} else {
|
|
$crit = array($relConf[1].' = ?', $this->get($key, true));
|
|
$crit = $this->mergeWithRelFilter($key, $crit);
|
|
$this->fieldsCache[$key] = $rel->findone($crit,
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
}
|
|
}
|
|
}
|
|
elseif (($type = isset($fields[$key]['has-one']))
|
|
|| isset($fields[$key]['has-many'])) {
|
|
$type = $type ? 'has-one' : 'has-many';
|
|
$fromConf = $fields[$key][$type];
|
|
if (!is_array($fromConf))
|
|
trigger_error(sprintf(self::E_REL_CONF_INC, $key));
|
|
$rel = $this->getRelInstance($fromConf[0],null,$key,true);
|
|
$relFieldConf = $rel->getFieldConfiguration();
|
|
$relType = isset($relFieldConf[$fromConf[1]]['belongs-to-one']) ?
|
|
'belongs-to-one' : 'has-many';
|
|
// one-to-*, bidirectional, inverse way
|
|
if ($relType == 'belongs-to-one') {
|
|
$toConf = $relFieldConf[$fromConf[1]]['belongs-to-one'];
|
|
if(!is_array($toConf))
|
|
$toConf = array($toConf, $id);
|
|
if ($toConf[1] != $id && (!$this->exists($toConf[1])
|
|
|| is_null($this->mapper->get($toConf[1]))))
|
|
$this->fieldsCache[$key] = null;
|
|
elseif($cx = $this->getCollection()) {
|
|
// part of a result set
|
|
if(!$cx->hasRelSet($key)) {
|
|
// emit eager loading
|
|
$relKeys = $cx->getAll($toConf[1],true);
|
|
$crit = array($fromConf[1].' IN ?', $relKeys);
|
|
$relSet = $rel->find($this->mergeWithRelFilter($key,$crit),
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
$cx->setRelSet($key, $relSet ? $relSet->getBy($fromConf[1],true) : NULL);
|
|
}
|
|
$result = $cx->getSubset($key, array($this->get($toConf[1])));
|
|
$this->fieldsCache[$key] = $result ? (($type == 'has-one')
|
|
? $result[0][0] : CortexCollection::factory($result[0])) : NULL;
|
|
} else {
|
|
$crit = array($fromConf[1].' = ?', $this->get($toConf[1],true));
|
|
$crit = $this->mergeWithRelFilter($key, $crit);
|
|
$opt = $this->getRelFilterOption($key);
|
|
$this->fieldsCache[$key] = (($type == 'has-one')
|
|
? $rel->findone($crit,$opt,$this->_ttl)
|
|
: $rel->find($crit,$opt,$this->_ttl)) ?: NULL;
|
|
}
|
|
}
|
|
// many-to-many, bidirectional
|
|
elseif ($relType == 'has-many') {
|
|
$toConf = $relFieldConf[$fromConf[1]]['has-many'];
|
|
$mmTable = $this->mmTable($fromConf,$key,$toConf);
|
|
// create mm table mapper
|
|
if (!$this->get($id,true)) {
|
|
$this->fieldsCache[$key] = null;
|
|
return $this->fieldsCache[$key];
|
|
}
|
|
$id = $toConf['relPK'];
|
|
$rel = $this->getRelInstance(null,array('db'=>$this->db,'table'=>$mmTable));
|
|
if ($cx = $this->getCollection()) {
|
|
if (!$cx->hasRelSet($key)) {
|
|
// get IDs of all results
|
|
$relKeys = $cx->getAll($id,true);
|
|
// get all pivot IDs
|
|
$mmRes = $rel->find(array($fromConf['relField'].' IN ?', $relKeys),null,$this->_ttl);
|
|
if (!$mmRes)
|
|
$cx->setRelSet($key, NULL);
|
|
else {
|
|
$pivotRel = array();
|
|
$pivotKeys = array();
|
|
foreach($mmRes as $model) {
|
|
$val = $model->get($key,true);
|
|
$pivotRel[ (string) $model->get($fromConf['relField'])][] = $val;
|
|
$pivotKeys[] = $val;
|
|
}
|
|
// cache pivot keys
|
|
$cx->setRelSet($key.'_pivot', $pivotRel);
|
|
// preload all rels
|
|
$pivotKeys = array_unique($pivotKeys);
|
|
$fRel = $this->getRelInstance($fromConf[0],null,$key,true);
|
|
$crit = array($toConf['relPK'].' IN ?', $pivotKeys);
|
|
$relSet = $fRel->find($this->mergeWithRelFilter($key, $crit),
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
$cx->setRelSet($key, $relSet ? $relSet->getBy($id) : NULL);
|
|
unset($fRel);
|
|
}
|
|
}
|
|
// fetch subset from preloaded rels using cached pivot keys
|
|
$fkeys = $cx->getSubset($key.'_pivot', array($this->get($id)));
|
|
$this->fieldsCache[$key] = $fkeys ?
|
|
CortexCollection::factory($cx->getSubset($key, $fkeys[0])) : NULL;
|
|
} // no collection
|
|
else {
|
|
// find foreign keys
|
|
$results = $rel->find(
|
|
array($fromConf['relField'].' = ?', $this->get($fromConf['relPK'],true)),null,$this->_ttl);
|
|
if(!$results)
|
|
$this->fieldsCache[$key] = NULL;
|
|
else {
|
|
$fkeys = $results->getAll($key,true);
|
|
// create foreign table mapper
|
|
unset($rel);
|
|
$rel = $this->getRelInstance($fromConf[0],null,$key,true);
|
|
// load foreign models
|
|
$filter = array($toConf['relPK'].' IN ?', $fkeys);
|
|
$filter = $this->mergeWithRelFilter($key, $filter);
|
|
$this->fieldsCache[$key] = $rel->find($filter,
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elseif (isset($fields[$key]['belongs-to-many'])) {
|
|
// many-to-many, unidirectional
|
|
$fields[$key]['type'] = self::DT_JSON;
|
|
$result = !$this->exists($key) ? null :$this->mapper->get($key);
|
|
if ($this->dbsType == 'sql')
|
|
$result = json_decode($result, true);
|
|
if (!is_array($result))
|
|
$this->fieldsCache[$key] = $result;
|
|
else {
|
|
// create foreign table mapper
|
|
$relConf = $fields[$key]['belongs-to-many'];
|
|
$rel = $this->getRelFromConf($relConf,$key);
|
|
$fkeys = array();
|
|
foreach ($result as $el)
|
|
$fkeys[] = is_int($el)||ctype_digit($el)?(int)$el:(string)$el;
|
|
// if part of a result set
|
|
if ($cx = $this->getCollection()) {
|
|
if (!$cx->hasRelSet($key)) {
|
|
// find all keys
|
|
$relKeys = ($cx->getAll($key,true));
|
|
if ($this->dbsType == 'sql'){
|
|
foreach ($relKeys as &$val) {
|
|
$val = substr($val, 1, -1);
|
|
unset($val);
|
|
}
|
|
$relKeys = json_decode('['.implode(',',$relKeys).']');
|
|
} else
|
|
$relKeys = call_user_func_array('array_merge', $relKeys);
|
|
// get related models
|
|
$crit = array($relConf[1].' IN ?', array_unique($relKeys));
|
|
$relSet = $rel->find($this->mergeWithRelFilter($key, $crit),
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
// cache relSet, sorted by ID
|
|
$cx->setRelSet($key, $relSet ? $relSet->getBy($relConf[1]) : NULL);
|
|
}
|
|
// get a subset of the preloaded set
|
|
$this->fieldsCache[$key] = CortexCollection::factory($cx->getSubset($key, $fkeys));
|
|
} else {
|
|
// load foreign models
|
|
$filter = array($relConf[1].' IN ?', $fkeys);
|
|
$filter = $this->mergeWithRelFilter($key, $filter);
|
|
$this->fieldsCache[$key] = $rel->find($filter,
|
|
$this->getRelFilterOption($key),$this->_ttl);
|
|
}
|
|
}
|
|
}
|
|
// resolve array fields
|
|
elseif (isset($fields[$key]['type'])) {
|
|
if ($this->dbsType == 'sql') {
|
|
if ($fields[$key]['type'] == self::DT_SERIALIZED)
|
|
$this->fieldsCache[$key] = unserialize($this->mapper->{$key});
|
|
elseif ($fields[$key]['type'] == self::DT_JSON)
|
|
$this->fieldsCache[$key] = json_decode($this->mapper->{$key},true);
|
|
}
|
|
if ($this->exists($key) && preg_match('/BOOL/i',$fields[$key]['type'])) {
|
|
$this->fieldsCache[$key] = (bool) $this->mapper->{$key};
|
|
}
|
|
}
|
|
}
|
|
// fetch cached value, if existing
|
|
$val = array_key_exists($key,$this->fieldsCache) ? $this->fieldsCache[$key]
|
|
: (($this->exists($key)) ? $this->mapper->{$key} : null);
|
|
if ($this->dbsType == 'mongo' && $val instanceof \MongoId) {
|
|
// conversion to string makes further processing in template, etc. much easier
|
|
$val = (string) $val;
|
|
}
|
|
// custom getter
|
|
$out = $this->emit('get_'.$key, $val);
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* find the ID values of given relation collection
|
|
* @param $val string|array|object|bool
|
|
* @param $rel_field string
|
|
* @param $key string
|
|
* @return array|Cortex|null|object
|
|
*/
|
|
protected function getForeignKeysArray($val, $rel_field, $key)
|
|
{
|
|
if (is_null($val))
|
|
return NULL;
|
|
if (is_object($val) && $val instanceof CortexCollection)
|
|
$val = $val->expose();
|
|
elseif (is_string($val))
|
|
// split-able string of collection IDs
|
|
$val = \Base::instance()->split($val);
|
|
elseif (!is_array($val) && !(is_object($val)
|
|
&& ($val instanceof Cortex && !$val->dry())))
|
|
trigger_error(sprintf(self::E_MM_REL_VALUE, $key));
|
|
// hydrated mapper as collection
|
|
if (is_object($val)) {
|
|
$nval = array();
|
|
while (!$val->dry()) {
|
|
$nval[] = $val->get($rel_field,true);
|
|
$val->next();
|
|
}
|
|
$val = $nval;
|
|
}
|
|
elseif (is_array($val)) {
|
|
// array of single hydrated mappers, raw ID value or mixed
|
|
$isMongo = ($this->dbsType == 'mongo');
|
|
foreach ($val as &$item) {
|
|
if (is_object($item) &&
|
|
!($isMongo && $item instanceof \MongoId)) {
|
|
if (!$item instanceof Cortex || $item->dry())
|
|
trigger_error(self::E_INVALID_RELATION_OBJECT);
|
|
else $item = $item->get($rel_field,true);
|
|
}
|
|
if ($isMongo && $rel_field == '_id' && is_string($item))
|
|
$item = new \MongoId($item);
|
|
if (is_numeric($item))
|
|
$item = (int) $item;
|
|
unset($item);
|
|
}
|
|
}
|
|
return $val;
|
|
}
|
|
|
|
/**
|
|
* creates and caches related mapper objects
|
|
* @param string $model
|
|
* @param array $relConf
|
|
* @param string $key
|
|
* @param bool $pushFilter
|
|
* @return Cortex
|
|
*/
|
|
protected function getRelInstance($model=null,$relConf=null,$key='',$pushFilter=false)
|
|
{
|
|
if (!$model && !$relConf)
|
|
trigger_error(self::E_MISSING_REL_CONF);
|
|
$relConf = $model ? $model::resolveConfiguration() : $relConf;
|
|
$relName = ($model?:'Cortex').'\\'.$relConf['db']->uuid().
|
|
'\\'.$relConf['table'].'\\'.$key;
|
|
if (\Registry::exists($relName)) {
|
|
$rel = \Registry::get($relName);
|
|
$rel->reset();
|
|
} else {
|
|
$rel = $model ? new $model : new Cortex($relConf['db'], $relConf['table']);
|
|
if (!$rel instanceof Cortex)
|
|
trigger_error(self::E_WRONG_RELATION_CLASS);
|
|
\Registry::set($relName, $rel);
|
|
}
|
|
// restrict fields of related mapper
|
|
if(!empty($key) && isset($this->relWhitelist[$key])) {
|
|
if (isset($this->relWhitelist[$key][0]))
|
|
$rel->fields($this->relWhitelist[$key][0],false);
|
|
if (isset($this->relWhitelist[$key][1]))
|
|
$rel->fields($this->relWhitelist[$key][1],true);
|
|
}
|
|
if ($pushFilter && !empty($key)) {
|
|
if (isset($this->relFilter[$key.'.'])) {
|
|
foreach($this->relFilter[$key.'.'] as $fkey=>$conf)
|
|
$rel->filter($fkey,$conf[0],$conf[1]);
|
|
}
|
|
if (isset($this->hasCond[$key.'.'])) {
|
|
foreach($this->hasCond[$key.'.'] as $fkey=>$conf)
|
|
$rel->has($fkey,$conf[0],$conf[1]);
|
|
}
|
|
}
|
|
return $rel;
|
|
}
|
|
|
|
/**
|
|
* get relation model from config
|
|
* @param $fieldConf
|
|
* @param $key
|
|
* @return Cortex
|
|
*/
|
|
protected function getRelFromConf(&$fieldConf, $key) {
|
|
if (!is_array($fieldConf))
|
|
$fieldConf = array($fieldConf, '_id');
|
|
$rel = $this->getRelInstance($fieldConf[0],null,$key,true);
|
|
if($this->dbsType=='sql' && $fieldConf[1] == '_id')
|
|
$fieldConf[1] = $rel->primary;
|
|
return $rel;
|
|
}
|
|
|
|
/**
|
|
* returns a clean/dry model from a relation
|
|
* @param string $key
|
|
* @return Cortex
|
|
*/
|
|
public function rel($key)
|
|
{
|
|
$rt = $this->fieldConf[$key]['relType'];
|
|
$rc = $this->fieldConf[$key][$rt];
|
|
if (!is_array($rc))
|
|
$rc = array($rc,'_id');
|
|
return $this->getRelInstance($rc[0],null,$key);
|
|
}
|
|
|
|
/**
|
|
* Return fields of mapper object as an associative array
|
|
* @return array
|
|
* @param bool|Cortex $obj
|
|
* @param int|array $rel_depths depths to resolve relations
|
|
*/
|
|
public function cast($obj = NULL, $rel_depths = 1)
|
|
{
|
|
$fields = $this->mapper->cast( ($obj) ? $obj->mapper : null );
|
|
if (!empty($this->vFields))
|
|
foreach(array_keys($this->vFields) as $key)
|
|
$fields[$key]=$this->get($key);
|
|
if (is_int($rel_depths))
|
|
$rel_depths = array('*'=>$rel_depths-1);
|
|
elseif (is_array($rel_depths))
|
|
$rel_depths['*'] = isset($rel_depths['*'])?--$rel_depths['*']:-1;
|
|
if (!empty($this->fieldConf)) {
|
|
$fields += array_fill_keys(array_keys($this->fieldConf),NULL);
|
|
if($this->whitelist)
|
|
$fields = array_intersect_key($fields, array_flip($this->whitelist));
|
|
$mp = $obj ? : $this;
|
|
foreach ($fields as $key => &$val) {
|
|
// post process configured fields
|
|
if (isset($this->fieldConf[$key]) && is_array($this->fieldConf[$key])) {
|
|
// handle relations
|
|
$rd = isset($rel_depths[$key]) ? $rel_depths[$key] : $rel_depths['*'];
|
|
if ((is_array($rd) || $rd >= 0) && $type=preg_grep('/[belongs|has]-(to-)*[one|many]/',
|
|
array_keys($this->fieldConf[$key]))) {
|
|
$relType=current($type);
|
|
// cast relations
|
|
$val = (($relType == 'belongs-to-one' || $relType == 'belongs-to-many')
|
|
&& !$mp->exists($key)) ? NULL : $mp->get($key);
|
|
if ($val instanceof Cortex)
|
|
$val = $val->cast(null, $rd);
|
|
elseif ($val instanceof CortexCollection)
|
|
$val = $val->castAll($rd);
|
|
}
|
|
// extract array fields
|
|
elseif (isset($this->fieldConf[$key]['type'])) {
|
|
if ($this->dbsType == 'sql') {
|
|
if ($this->fieldConf[$key]['type'] == self::DT_SERIALIZED)
|
|
$val=unserialize($mp->mapper->{$key});
|
|
elseif ($this->fieldConf[$key]['type'] == self::DT_JSON)
|
|
$val=json_decode($mp->mapper->{$key}, true);
|
|
}
|
|
if ($this->exists($key)
|
|
&& preg_match('/BOOL/i',$this->fieldConf[$key]['type'])) {
|
|
$val = (bool) $mp->mapper->{$key};
|
|
}
|
|
}
|
|
}
|
|
if ($this->dbsType == 'mongo' && $key == '_id')
|
|
$val = (string) $val;
|
|
if ($this->dbsType == 'sql' && $key == 'id' && $this->standardiseID) {
|
|
$fields['_id'] = $val;
|
|
unset($fields[$key]);
|
|
}
|
|
unset($val);
|
|
}
|
|
}
|
|
// custom getter
|
|
foreach ($fields as $key => &$val) {
|
|
$val = $this->emit('get_'.$key, $val);
|
|
unset($val);
|
|
}
|
|
return $fields;
|
|
}
|
|
|
|
/**
|
|
* cast a related collection of mappers
|
|
* @param string $key field name
|
|
* @param int $rel_depths depths to resolve relations
|
|
* @return array array of associative arrays
|
|
*/
|
|
function castField($key, $rel_depths=0)
|
|
{
|
|
if (!$key)
|
|
return NULL;
|
|
$mapper_arr = $this->get($key);
|
|
if(!$mapper_arr)
|
|
return NULL;
|
|
$out = array();
|
|
foreach ($mapper_arr as $mp)
|
|
$out[] = $mp->cast(null,$rel_depths);
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* wrap result mapper
|
|
* @param Cursor|array $mapper
|
|
* @return Cortex
|
|
*/
|
|
protected function factory($mapper)
|
|
{
|
|
if (is_array($mapper)) {
|
|
$mp = clone($this->mapper);
|
|
$mp->reset();
|
|
$cx = $this->factory($mp);
|
|
$cx->copyfrom($mapper);
|
|
} else {
|
|
$cx = clone($this);
|
|
$cx->reset(false);
|
|
$cx->mapper = $mapper;
|
|
}
|
|
$cx->emit('load');
|
|
return $cx;
|
|
}
|
|
|
|
public function dry() {
|
|
return $this->mapper->dry();
|
|
}
|
|
|
|
/**
|
|
* hydrate the mapper from hive key or given array
|
|
* @param string|array $key
|
|
* @param callback|array|string $fields
|
|
* @return NULL
|
|
*/
|
|
public function copyfrom($key, $fields = null)
|
|
{
|
|
$f3 = \Base::instance();
|
|
$srcfields = is_array($key) ? $key : $f3->get($key);
|
|
if ($fields)
|
|
if (is_callable($fields))
|
|
$srcfields = $fields($srcfields);
|
|
else {
|
|
if (is_string($fields))
|
|
$fields = $f3->split($fields);
|
|
$srcfields = array_intersect_key($srcfields, array_flip($fields));
|
|
}
|
|
foreach ($srcfields as $key => $val) {
|
|
if (isset($this->fieldConf[$key]) && isset($this->fieldConf[$key]['type'])) {
|
|
if ($this->fieldConf[$key]['type'] == self::DT_JSON && is_string($val))
|
|
$val = json_decode($val);
|
|
elseif ($this->fieldConf[$key]['type'] == self::DT_SERIALIZED && is_string($val))
|
|
$val = unserialize($val);
|
|
}
|
|
$this->set($key, $val);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* copy mapper values into hive key
|
|
* @param string $key the hive key to copy into
|
|
* @param int $relDepth the depth of relations to resolve
|
|
* @return NULL|void
|
|
*/
|
|
public function copyto($key, $relDepth=0) {
|
|
\Base::instance()->set($key, $this->cast(null,$relDepth));
|
|
}
|
|
|
|
public function skip($ofs = 1)
|
|
{
|
|
$this->reset(false);
|
|
if ($this->mapper->skip($ofs))
|
|
return $this;
|
|
else
|
|
$this->reset(false);
|
|
}
|
|
|
|
public function first()
|
|
{
|
|
$this->reset(false);
|
|
$this->mapper->first();
|
|
return $this;
|
|
}
|
|
|
|
public function last()
|
|
{
|
|
$this->reset(false);
|
|
$this->mapper->last();
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* reset and re-initialize the mapper
|
|
* @param bool $mapper
|
|
* @return NULL|void
|
|
*/
|
|
public function reset($mapper = true)
|
|
{
|
|
if ($mapper)
|
|
$this->mapper->reset();
|
|
$this->fieldsCache=array();
|
|
$this->saveCsd=array();
|
|
$this->countFields=array();
|
|
$this->preBinds=array();
|
|
$this->grp_stack=null;
|
|
// set default values
|
|
if (($this->dbsType == 'jig' || $this->dbsType == 'mongo')
|
|
&& !empty($this->fieldConf))
|
|
foreach($this->fieldConf as $field_key => $field_conf)
|
|
if (array_key_exists('default',$field_conf)) {
|
|
$val = ($field_conf['default'] === \DB\SQL\Schema::DF_CURRENT_TIMESTAMP)
|
|
? date('Y-m-d H:i:s') : $field_conf['default'];
|
|
$this->set($field_key, $val);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* check if a certain field exists in the mapper or
|
|
* or is a virtual relation field
|
|
* @param string $key
|
|
* @param bool $relField
|
|
* @return bool
|
|
*/
|
|
function exists($key, $relField = false) {
|
|
if (!$this->dry() && $key == '_id') return true;
|
|
return $this->mapper->exists($key) ||
|
|
($relField && isset($this->fieldConf[$key]['relType']));
|
|
}
|
|
|
|
/**
|
|
* clear any mapper field or relation
|
|
* @param string $key
|
|
* @return NULL|void
|
|
*/
|
|
function clear($key) {
|
|
unset($this->fieldsCache[$key]);
|
|
if (isset($this->fieldConf[$key]['relType']))
|
|
$this->set($key,null);
|
|
$this->mapper->clear($key);
|
|
}
|
|
|
|
function insert() {
|
|
$res = $this->mapper->insert();
|
|
if (is_array($res))
|
|
$res = $this->mapper;
|
|
if (is_object($res))
|
|
$res = $this->factory($res);
|
|
return is_int($res) ? $this : $res;
|
|
}
|
|
|
|
function update() {
|
|
$res = $this->mapper->update();
|
|
if (is_array($res))
|
|
$res = $this->mapper;
|
|
if (is_object($res))
|
|
$res = $this->factory($res);
|
|
return is_int($res) ? $this : $res;
|
|
}
|
|
|
|
function dbtype() {
|
|
return $this->mapper->dbtype();
|
|
}
|
|
|
|
public function __destruct() {
|
|
unset($this->mapper);
|
|
}
|
|
|
|
public function __clone() {
|
|
$this->mapper = clone($this->mapper);
|
|
}
|
|
|
|
function getiterator() {
|
|
// return new \ArrayIterator($this->cast(null,false));
|
|
return new \ArrayIterator(array());
|
|
}
|
|
}
|
|
|
|
|
|
class CortexQueryParser extends \Prefab {
|
|
|
|
const
|
|
E_BRACKETS = 'Invalid query: unbalanced brackets found',
|
|
E_INBINDVALUE = 'Bind value for IN operator must be a populated array',
|
|
E_ENGINEERROR = 'Engine not supported',
|
|
E_MISSINGBINDKEY = 'Named bind parameter `%s` does not exist in filter arguments';
|
|
|
|
protected
|
|
$queryCache = array();
|
|
|
|
/**
|
|
* converts the given filter array to fit the used DBS
|
|
*
|
|
* example filter:
|
|
* array('text = ? AND num = ?','bar',5)
|
|
* array('num > ? AND num2 <= ?',5,10)
|
|
* array('num1 > num2')
|
|
* array('text like ?','%foo%')
|
|
* array('(text like ? OR text like ?) AND num != ?','foo%','%bar',23)
|
|
*
|
|
* @param array $cond
|
|
* @param string $engine
|
|
* @param null $fieldConf
|
|
* @return array|bool|null
|
|
*/
|
|
public function prepareFilter($cond, $engine,$fieldConf=null)
|
|
{
|
|
if (is_null($cond)) return $cond;
|
|
if (is_string($cond))
|
|
$cond = array($cond);
|
|
$f3 = \Base::instance();
|
|
$cacheHash = $f3->hash($f3->stringify($cond)).'.'.$engine;
|
|
if (isset($this->queryCache[$cacheHash]))
|
|
// load from memory
|
|
return $this->queryCache[$cacheHash];
|
|
elseif ($f3->exists('CORTEX.queryParserCache')
|
|
&& ($ttl = (int) $f3->get('CORTEX.queryParserCache'))) {
|
|
$cache = \Cache::instance();
|
|
// load from cache
|
|
if ($f3->get('CACHE') && $ttl && ($cached = $cache->exists($cacheHash, $ncond))
|
|
&& $cached[0] + $ttl > microtime(TRUE)) {
|
|
$this->queryCache[$cacheHash] = $ncond;
|
|
return $ncond;
|
|
}
|
|
}
|
|
$where = array_shift($cond);
|
|
$args = $cond;
|
|
$where = str_replace(array('&&', '||'), array('AND', 'OR'), $where);
|
|
// prepare IN condition
|
|
$where = preg_replace('/\bIN\b\s*\(\s*(\?|:\w+)?\s*\)/i', 'IN $1', $where);
|
|
switch ($engine) {
|
|
case 'jig':
|
|
$ncond = $this->_jig_parse_filter($where, $args);
|
|
break;
|
|
case 'mongo':
|
|
$parts = $this->splitLogical($where);
|
|
if (is_int(strpos($where, ':')))
|
|
list($parts, $args) = $this->convertNamedParams($parts, $args);
|
|
foreach ($parts as &$part) {
|
|
$part = $this->_mongo_parse_relational_op($part, $args, $fieldConf);
|
|
unset($part);
|
|
}
|
|
$ncond = $this->_mongo_parse_logical_op($parts);
|
|
break;
|
|
case 'sql':
|
|
// preserve identifier
|
|
$where = preg_replace('/(?!\B)_id/', 'id', $where);
|
|
$parts = $this->splitLogical($where);
|
|
// ensure positional bind params
|
|
if (is_int(strpos($where, ':')))
|
|
list($parts, $args) = $this->convertNamedParams($parts, $args);
|
|
$ncond = array();
|
|
foreach ($parts as &$part) {
|
|
// enhanced IN handling
|
|
if (is_int(strpos($part, '?'))) {
|
|
$val = array_shift($args);
|
|
if (is_int($pos = strpos($part, 'IN ?'))) {
|
|
if (!is_array($val) || empty($val))
|
|
trigger_error(self::E_INBINDVALUE);
|
|
$bindMarks = str_repeat('?,', count($val) - 1).'?';
|
|
$part = substr($part, 0, $pos).'IN ('.$bindMarks.')';
|
|
$ncond = array_merge($ncond, $val);
|
|
} else
|
|
$ncond[] = $val;
|
|
}
|
|
unset($part);
|
|
}
|
|
array_unshift($ncond, array_reduce($parts,function($out,$part){
|
|
return $out.((!$out||in_array($part,array('(',')'))
|
|
||preg_match('/\($/',$out))?'':' ').$part;
|
|
},''));
|
|
break;
|
|
default:
|
|
trigger_error(self::E_ENGINEERROR);
|
|
}
|
|
$this->queryCache[$cacheHash] = $ncond;
|
|
if(isset($ttl) && $f3->get('CACHE')) {
|
|
// save to cache
|
|
$cache = \Cache::instance();
|
|
$cache->set($cacheHash,$ncond,$ttl);
|
|
}
|
|
return $ncond;
|
|
}
|
|
|
|
/**
|
|
* split where criteria string into logical chunks
|
|
* @param $cond
|
|
* @return array
|
|
*/
|
|
protected function splitLogical($cond)
|
|
{
|
|
return preg_split('/\s*((?<!\()\)|\((?!\))|\bAND\b|\bOR\b)\s*/i', $cond, -1,
|
|
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
|
|
}
|
|
|
|
/**
|
|
* converts named parameter filter to positional
|
|
* @param $parts
|
|
* @param $args
|
|
* @return array
|
|
*/
|
|
protected function convertNamedParams($parts, $args)
|
|
{
|
|
if (empty($args)) return array($parts, $args);
|
|
$params = array();
|
|
$pos = 0;
|
|
foreach ($parts as &$part) {
|
|
if (preg_match('/:\w+/i', $part, $match)) {
|
|
if (!isset($args[$match[0]]))
|
|
trigger_error(sprintf(self::E_MISSINGBINDKEY,
|
|
$match[0]));
|
|
$part = str_replace($match[0], '?', $part);
|
|
$params[] = $args[$match[0]];
|
|
} elseif (is_int(strpos($part, '?')))
|
|
$params[] = $args[$pos++];
|
|
unset($part);
|
|
}
|
|
return array($parts, $params);
|
|
}
|
|
|
|
/**
|
|
* convert filter array to jig syntax
|
|
* @param $where
|
|
* @param $args
|
|
* @return array
|
|
*/
|
|
protected function _jig_parse_filter($where, $args)
|
|
{
|
|
$parts = $this->splitLogical($where);
|
|
if (is_int(strpos($where, ':')))
|
|
list($parts, $args) = $this->convertNamedParams($parts, $args);
|
|
$ncond = array();
|
|
foreach ($parts as &$part) {
|
|
if (in_array(strtoupper($part), array('AND', 'OR')))
|
|
continue;
|
|
// prefix field names
|
|
$part = preg_replace('/([a-z_-]+)/i', '@$1', $part, -1, $count);
|
|
// value comparison
|
|
if (is_int(strpos($part, '?'))) {
|
|
$val = array_shift($args);
|
|
preg_match('/(@\w+)/i', $part, $match);
|
|
$skipVal=false;
|
|
// find like operator
|
|
if (is_int(strpos($upart = strtoupper($part), ' @LIKE '))) {
|
|
if ($not = is_int($npos = strpos($upart, '@NOT')))
|
|
$pos = $npos;
|
|
$val = $this->_likeValueToRegEx($val);
|
|
$part = ($not ? '!' : '').'preg_match(?,'.$match[0].')';
|
|
} // find IN operator
|
|
elseif (is_int($pos = strpos($upart, ' @IN '))) {
|
|
if ($not = is_int($npos = strpos($upart, '@NOT')))
|
|
$pos = $npos;
|
|
$part = ($not ? '!' : '').'in_array('.substr($part, 0, $pos).
|
|
',array(\''.implode('\',\'', $val).'\'))';
|
|
$skipVal=true;
|
|
}
|
|
// add existence check
|
|
$part = ($val===null && !$skipVal)
|
|
? '(array_key_exists(\''.ltrim($match[0],'@').'\',$_row) && '.$part.')'
|
|
: '(isset('.$match[0].') && '.$part.')';
|
|
if (!$skipVal)
|
|
$ncond[] = $val;
|
|
} elseif ($count >= 1) {
|
|
// field comparison
|
|
preg_match_all('/(@\w+)/i', $part, $matches);
|
|
$chks = array();
|
|
foreach ($matches[0] as $field)
|
|
$chks[] = 'isset('.$field.')';
|
|
$part = '('.implode(' && ',$chks).' && ('.$part.'))';
|
|
}
|
|
unset($part);
|
|
}
|
|
array_unshift($ncond, implode(' ', $parts));
|
|
return $ncond;
|
|
}
|
|
|
|
/**
|
|
* find and wrap logical operators AND, OR, (, )
|
|
* @param $parts
|
|
* @return array
|
|
*/
|
|
protected function _mongo_parse_logical_op($parts)
|
|
{
|
|
$b_offset = 0;
|
|
$ncond = array();
|
|
$child = array();
|
|
for ($i = 0, $max = count($parts); $i < $max; $i++) {
|
|
$part = $parts[$i];
|
|
if ($part == '(') {
|
|
// add sub-bracket to parse array
|
|
if ($b_offset > 0)
|
|
$child[] = $part;
|
|
$b_offset++;
|
|
} elseif ($part == ')') {
|
|
$b_offset--;
|
|
// found closing bracket
|
|
if ($b_offset == 0) {
|
|
$ncond[] = ($this->_mongo_parse_logical_op($child));
|
|
$child = array();
|
|
} elseif ($b_offset < 0)
|
|
trigger_error(self::E_BRACKETS);
|
|
else
|
|
// add sub-bracket to parse array
|
|
$child[] = $part;
|
|
} // add to parse array
|
|
elseif ($b_offset > 0)
|
|
$child[] = $part;
|
|
// condition type
|
|
elseif (!is_array($part)) {
|
|
if (strtoupper($part) == 'AND')
|
|
$add = true;
|
|
elseif (strtoupper($part) == 'OR')
|
|
$or = true;
|
|
} else // skip
|
|
$ncond[] = $part;
|
|
}
|
|
if ($b_offset > 0)
|
|
trigger_error(self::E_BRACKETS);
|
|
if (isset($add))
|
|
return array('$and' => $ncond);
|
|
elseif (isset($or))
|
|
return array('$or' => $ncond);
|
|
else
|
|
return $ncond[0];
|
|
}
|
|
|
|
/**
|
|
* find and convert relational operators
|
|
* @param $part
|
|
* @param $args
|
|
* @param null $fieldConf
|
|
* @return array|null
|
|
*/
|
|
protected function _mongo_parse_relational_op($part, &$args, $fieldConf=null)
|
|
{
|
|
if (is_null($part))
|
|
return $part;
|
|
if (preg_match('/\<\=|\>\=|\<\>|\<|\>|\!\=|\=\=|\=|like|not like|in|not in/i', $part, $match)) {
|
|
$var = is_int(strpos($part, '?')) ? array_shift($args) : null;
|
|
$exp = explode($match[0], $part);
|
|
$key = trim($exp[0]);
|
|
// unbound value
|
|
if (is_numeric($exp[1]))
|
|
$var = $exp[1];
|
|
// field comparison
|
|
elseif (!is_int(strpos($exp[1], '?')))
|
|
return array('$where' => 'this.'.$key.' '.$match[0].' this.'.trim($exp[1]));
|
|
$upart = strtoupper($match[0]);
|
|
// MongoID shorthand
|
|
if ($key == '_id' || (isset($fieldConf[$key]) && isset($fieldConf[$key]['relType']))) {
|
|
if (is_array($var))
|
|
foreach ($var as &$id) {
|
|
if (!$id instanceof \MongoId)
|
|
$id = new \MongoId($id);
|
|
unset($id);
|
|
}
|
|
elseif(!$var instanceof \MongoId)
|
|
$var = new \MongoId($var);
|
|
}
|
|
// find LIKE operator
|
|
if (in_array($upart, array('LIKE','NOT LIKE'))) {
|
|
$rgx = $this->_likeValueToRegEx($var);
|
|
$var = new \MongoRegex($rgx);
|
|
if ($upart == 'NOT LIKE')
|
|
$var = array('$not' => $var);
|
|
} // find IN operator
|
|
elseif (in_array($upart, array('IN','NOT IN'))) {
|
|
$var = array(($upart=='NOT IN')?'$nin':'$in' => array_values($var));
|
|
} // translate operators
|
|
elseif (!in_array($match[0], array('==', '='))) {
|
|
$opr = str_replace(array('<>', '<', '>', '!', '='),
|
|
array('$ne', '$lt', '$gt', '$n', 'e'), $match[0]);
|
|
$var = array($opr => (strtolower($var) == 'null') ? null :
|
|
(is_object($var) ? $var : (is_numeric($var) ? $var + 0 : $var)));
|
|
}
|
|
return array($key => $var);
|
|
}
|
|
return $part;
|
|
}
|
|
|
|
/**
|
|
* @param string $var
|
|
* @return string
|
|
*/
|
|
protected function _likeValueToRegEx($var)
|
|
{
|
|
$lC = substr($var, -1, 1);
|
|
// %var% -> /var/
|
|
if ($var[0] == '%' && $lC == '%')
|
|
$var = substr($var, 1, -1);
|
|
// var% -> /^var/
|
|
elseif ($lC == '%')
|
|
$var = '^'.substr($var, 0, -1);
|
|
// %var -> /var$/
|
|
elseif ($var[0] == '%')
|
|
$var = substr($var, 1).'$';
|
|
return '/'.$var.'/iu';
|
|
}
|
|
|
|
/**
|
|
* convert options array syntax to given engine type
|
|
*
|
|
* example:
|
|
* array('order'=>'location') // default direction is ASC
|
|
* array('order'=>'num1 desc, num2 asc')
|
|
*
|
|
* @param array $options
|
|
* @param string $engine
|
|
* @return array|null
|
|
*/
|
|
public function prepareOptions($options, $engine)
|
|
{
|
|
if (empty($options) || !is_array($options))
|
|
return null;
|
|
switch ($engine) {
|
|
case 'jig':
|
|
if (array_key_exists('order', $options))
|
|
$options['order'] = str_replace(array('asc', 'desc'),
|
|
array('SORT_ASC', 'SORT_DESC'), strtolower($options['order']));
|
|
break;
|
|
case 'mongo':
|
|
if (array_key_exists('order', $options)) {
|
|
$sorts = explode(',', $options['order']);
|
|
$sorting = array();
|
|
foreach ($sorts as $sort) {
|
|
$sp = explode(' ', trim($sort));
|
|
$sorting[$sp[0]] = (array_key_exists(1, $sp) &&
|
|
strtoupper($sp[1]) == 'DESC') ? -1 : 1;
|
|
}
|
|
$options['order'] = $sorting;
|
|
}
|
|
if (array_key_exists('group', $options) && is_string($options['group'])) {
|
|
$keys = explode(',',$options['group']);
|
|
$options['group']=array('keys'=>array(),'initial'=>array(),
|
|
'reduce'=>'function (obj, prev) {}','finalize'=>'');
|
|
$keys = array_combine($keys,array_fill(0,count($keys),1));
|
|
$options['group']['keys']=$keys;
|
|
$options['group']['initial']=$keys;
|
|
}
|
|
break;
|
|
}
|
|
return $options;
|
|
}
|
|
}
|
|
|
|
class CortexCollection extends \ArrayIterator {
|
|
|
|
protected
|
|
$relSets = array(),
|
|
$pointer = 0,
|
|
$changed = false,
|
|
$cid;
|
|
|
|
const
|
|
E_UnknownCID = 'This Collection does not exist: %s',
|
|
E_SubsetKeysValue = '$keys must be an array or split-able string, but %s was given.';
|
|
|
|
public function __construct() {
|
|
$this->cid = uniqid('cortex_collection_');
|
|
parent::__construct();
|
|
}
|
|
|
|
//! Prohibit cloning to ensure an existing relation cache
|
|
private function __clone() { }
|
|
|
|
/**
|
|
* set a collection of models
|
|
* @param $models
|
|
*/
|
|
function setModels($models,$init=true) {
|
|
array_map(array($this,'add'),$models);
|
|
if ($init)
|
|
$this->changed = false;
|
|
}
|
|
|
|
/**
|
|
* add single model to collection
|
|
* @param $model
|
|
*/
|
|
function add(Cortex $model) {
|
|
$model->addToCollection($this);
|
|
$this->append($model);
|
|
}
|
|
|
|
public function offsetSet($i, $val) {
|
|
$this->changed=true;
|
|
parent::offsetSet($i,$val);
|
|
}
|
|
|
|
public function hasChanged() {
|
|
return $this->changed;
|
|
}
|
|
|
|
/**
|
|
* get a related collection
|
|
* @param $key
|
|
* @return null
|
|
*/
|
|
public function getRelSet($key) {
|
|
return (isset($this->relSets[$key])) ? $this->relSets[$key] : null;
|
|
}
|
|
|
|
/**
|
|
* set a related collection for caching it for the lifetime of this collection
|
|
* @param $key
|
|
* @param $set
|
|
*/
|
|
public function setRelSet($key,$set) {
|
|
$this->relSets[$key] = $set;
|
|
}
|
|
|
|
/**
|
|
* check if a related collection exists in runtime cache
|
|
* @param $key
|
|
* @return bool
|
|
*/
|
|
public function hasRelSet($key) {
|
|
return array_key_exists($key,$this->relSets);
|
|
}
|
|
|
|
public function expose() {
|
|
return $this->getArrayCopy();
|
|
}
|
|
|
|
/**
|
|
* get an intersection from a cached relation-set, based on given keys
|
|
* @param string $prop
|
|
* @param array|string $keys
|
|
* @return array
|
|
*/
|
|
public function getSubset($prop,$keys) {
|
|
if (is_string($keys))
|
|
$keys = \Base::instance()->split($keys);
|
|
if (!is_array($keys))
|
|
trigger_error(sprintf(self::E_SubsetKeysValue,gettype($keys)));
|
|
if (!$this->hasRelSet($prop) || !($relSet = $this->getRelSet($prop)))
|
|
return null;
|
|
foreach ($keys as &$key) {
|
|
if ($key instanceof \MongoId)
|
|
$key = (string) $key;
|
|
unset($key);
|
|
}
|
|
return array_values(array_intersect_key($relSet, array_flip($keys)));
|
|
}
|
|
|
|
/**
|
|
* returns all values of a specified property from all models
|
|
* @param string $prop
|
|
* @param bool $raw
|
|
* @return array
|
|
*/
|
|
public function getAll($prop, $raw = false)
|
|
{
|
|
$out = array();
|
|
foreach ($this->getArrayCopy() as $model) {
|
|
if ($model instanceof Cortex && $model->exists($prop,true)) {
|
|
$val = $model->get($prop, $raw);
|
|
if (!empty($val))
|
|
$out[] = $val;
|
|
} elseif($raw)
|
|
$out[] = $model;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* cast all contained mappers to a nested array
|
|
* @param int|array $rel_depths depths to resolve relations
|
|
* @return array
|
|
*/
|
|
public function castAll($rel_depths=1) {
|
|
$out = array();
|
|
foreach ($this->getArrayCopy() as $model)
|
|
$out[] = $model->cast(null,$rel_depths);
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* return all models keyed by a specified index key
|
|
* @param string $index
|
|
* @param bool $nested
|
|
* @return array
|
|
*/
|
|
public function getBy($index, $nested = false)
|
|
{
|
|
$out = array();
|
|
foreach ($this->getArrayCopy() as $model)
|
|
if ($model->exists($index)) {
|
|
$val = $model->get($index, true);
|
|
if (!empty($val))
|
|
if($nested) $out[(string) $val][] = $model;
|
|
else $out[(string) $val] = $model;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* re-assort the current collection using a sql-like syntax
|
|
* @param $cond
|
|
*/
|
|
public function orderBy($cond){
|
|
$cols=\Base::instance()->split($cond);
|
|
$this->uasort(function($val1,$val2) use($cols) {
|
|
foreach ($cols as $col) {
|
|
$parts=explode(' ',$col,2);
|
|
$order=empty($parts[1])?'ASC':$parts[1];
|
|
$col=$parts[0];
|
|
list($v1,$v2)=array($val1[$col],$val2[$col]);
|
|
if ($out=strnatcmp($v1,$v2)*
|
|
((strtoupper($order)=='ASC')*2-1))
|
|
return $out;
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* slice the collection
|
|
* @param $offset
|
|
* @param null $limit
|
|
*/
|
|
public function slice($offset,$limit=null) {
|
|
$this->rewind();
|
|
$i=0;
|
|
$del=array();
|
|
while ($this->valid()) {
|
|
if ($i < $offset)
|
|
$del[]=$this->key();
|
|
elseif ($i >= $offset && $limit && $i >= ($offset+$limit))
|
|
$del[]=$this->key();
|
|
$i++;
|
|
$this->next();
|
|
}
|
|
foreach ($del as $ii)
|
|
unset($this[$ii]);
|
|
}
|
|
|
|
static public function factory($records) {
|
|
$cc = new self();
|
|
$cc->setModels($records);
|
|
return $cc;
|
|
}
|
|
|
|
} |