{$app.name|escxml}
+| {$model.name} | +{trans 'Add'} | +{trans 'Change'} | +
|---|
From: avl %1$s%2$s %3$s%4$s
';
+ else
+ $str = '
';
+ }
+ $row[$h['name']] = $str;
+ }
+ $this->LinksRow(&$row, &$o);
+
+
+ return $row;
+ }
+
+ protected function LinksRow(&$row, &$o){
+ if (method_exists($this,'list_display_links')){
+ $links_display = $this->list_display_links();
+ }else{
+ $links_display = null;
+ $i = 1;
+ }
+ foreach($row as $name=>&$v){
+ if ($links_display){
+ if (array_search($name, $links_display)!==false)
+ $v = ''.$v.'';
+ }else{
+ if ($i==1)
+ $v = ''.$v.'';
+ $i++;
+ }
+ }
+ }
+
+ protected function UrlForResult(&$o){
+ return $o->__get($this->qe->getTable()->getIdentifier()).'/';
+ }
+
+ // Views Function
+ public function AddItem($request, $lapp, $lmodel){
+ $model = new $this->modelName();
+ if ($request->method == 'POST'){
+ $form = IPF_Shortcuts::GetFormForModel($model,$request->POST,array('user_fields'=>$this->fields()));
+ $this->_setupAddForm(&$form);
+ if ($form->isValid()) {
+ $item = $form->save();
+ AdminLog::logAction($request, $item, AdminLog::ADDITION);
+ $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel));
+ return new IPF_HTTP_Response_Redirect($url);
+ }
+ }
+ else{
+ $form = IPF_Shortcuts::GetFormForModel($model,null,array('user_fields'=>$this->fields()));
+ $this->_setupAddForm(&$form);
+
+ }
+ $context = array(
+ 'page_title'=>'Add '.$this->modelName,
+ 'classname'=>$this->modelName,
+ 'form'=>$form,
+ 'lapp'=>$lapp,
+ 'lmodel'=>$lmodel,
+ );
+ return IPF_Shortcuts::RenderToResponse('admin/add.html', $context, $request);
+ }
+
+ public function EditItem($request, $lapp, $lmodel, $o){
+ if ($request->method == 'POST'){
+ $form = IPF_Shortcuts::GetFormForModel($o,$request->POST,array('user_fields'=>$this->fields()));
+ $this->_setupEditForm(&$form);
+ if ($form->isValid()) {
+ $item = $form->save();
+ AdminLog::logAction($request, $item, AdminLog::CHANGE);
+ $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel));
+ return new IPF_HTTP_Response_Redirect($url);
+ }
+ }
+ else{
+ $form = IPF_Shortcuts::GetFormForModel($o,$o->getData(),array('user_fields'=>$this->fields()));
+ $this->_setupEditForm(&$form);
+ }
+ $context = array(
+ 'page_title'=>'Edit '.$this->modelName,
+ 'classname'=>$this->modelName,
+ 'object'=>$o,
+ 'form'=>$form,
+ 'lapp'=>$lapp,
+ 'lmodel'=>$lmodel,
+ );
+ return IPF_Shortcuts::RenderToResponse('admin/change.html', $context, $request);
+ }
+
+ public function DeleteItem($request, $lapp, $lmodel, $o){
+ if ($request->method == 'POST'){
+ AdminLog::logAction($request, $o, AdminLog::DELETION);
+ $o->delete();
+ $url = IPF_HTTP_URL_urlForView('IPF_Admin_Views_ListItems', array($lapp, $lmodel));
+ return new IPF_HTTP_Response_Redirect($url);
+ }
+ $context = array(
+ 'page_title'=>'Delete '.$this->modelName,
+ 'classname'=>$this->modelName,
+ 'object'=>$o,
+ 'form'=>$form,
+ 'lapp'=>$lapp,
+ 'lmodel'=>$lmodel,
+ 'affected'=>array(),
+ );
+ return IPF_Shortcuts::RenderToResponse('admin/delete.html', $context, $request);
+ }
+
+ public function ListItems($request){
+ $this->ListItemsQuery();
+ $this->qe = $this->q->execute();
+ $this->ListItemsHeader();
+ //print_r($this->qe->getTable()->getIdentifier());
+ $context = array(
+ 'page_title'=>$this->modelName.' List',
+ 'header'=>$this->header,
+ 'classname'=>$this->modelName,
+ 'objects'=>$this->qe,
+ 'classname'=>$this->modelName,
+ );
+ return IPF_Shortcuts::RenderToResponse('admin/items.html', $context, $request);
+ }
+}
diff --git a/ipf/admin/models.yml b/ipf/admin/models.yml
new file mode 100644
index 0000000..0e27a3e
--- /dev/null
+++ b/ipf/admin/models.yml
@@ -0,0 +1,29 @@
+AdminLog:
+ tableName: admin_log
+ actAs:
+ Timestampable:
+ updated:
+ disabled: true
+ columns:
+ username: string(32)
+ user_id: integer
+ object_id: integer
+ object_class: string(200)
+ object_repr: string(200)
+ action_flag: integer
+ change_message: string(200)
+
+ indexes:
+ idx_object_id:
+ fields: object_id
+ idx_object_class:
+ fields: object_class
+ idx_created_at:
+ fields: created_at
+ idx_action_flag:
+ fields: action_flag
+
+ options:
+ type: INNODB
+ collate: utf8_unicode_ci
+ charset: utf8
diff --git a/ipf/admin/models/AdminLog.php b/ipf/admin/models/AdminLog.php
new file mode 100644
index 0000000..f84c33d
--- /dev/null
+++ b/ipf/admin/models/AdminLog.php
@@ -0,0 +1,44 @@
+username = $request->user->username;
+ $log->user_id = $request->user->id;
+ $log->object_id = $object->id;
+ $log->object_class = get_class($object);
+ $log->object_repr = (string)$object;
+ $log->action_flag = $action_flag;
+ $log->change_message = $message;
+ $log->save();
+ }
+
+ public function is_addition(){
+ if ($this->action_flag==AdminLog::ADDITION)
+ return true;
+ return false;
+ }
+
+ public function is_change(){
+ if ($this->action_flag==AdminLog::CHANGE)
+ return true;
+ return false;
+ }
+
+ public function is_deletion(){
+ if ($this->action_flag==AdminLog::DELETION)
+ return true;
+ return false;
+ }
+
+ public function GetAdminUrl(){
+ return IPF_HTTP_URL_urlForView('IPF_Admin_Views_Index').IPF_Utils::appLabelByModel($this->object_class).'/'.strtolower($this->object_class).'/'.$this->object_id.'/';
+ }
+
+}
+
diff --git a/ipf/admin/models/_generated/BaseAdminLog.php b/ipf/admin/models/_generated/BaseAdminLog.php
new file mode 100644
index 0000000..e7114fc
--- /dev/null
+++ b/ipf/admin/models/_generated/BaseAdminLog.php
@@ -0,0 +1,34 @@
+setTableName('admin_log');
+ $this->hasColumn('username', 'string', 32, array('type' => 'string', 'length' => '32'));
+ $this->hasColumn('user_id', 'integer', null, array('type' => 'integer'));
+ $this->hasColumn('object_id', 'integer', null, array('type' => 'integer'));
+ $this->hasColumn('object_class', 'string', 200, array('type' => 'string', 'length' => '200'));
+ $this->hasColumn('object_repr', 'string', 200, array('type' => 'string', 'length' => '200'));
+ $this->hasColumn('action_flag', 'integer', null, array('type' => 'integer'));
+ $this->hasColumn('change_message', 'string', 200, array('type' => 'string', 'length' => '200'));
+
+
+ $this->index('idx_object_id', array('fields' => 'object_id'));
+ $this->index('idx_object_class', array('fields' => 'object_class'));
+ $this->index('idx_created_at', array('fields' => 'created_at'));
+ $this->index('idx_action_flag', array('fields' => 'action_flag'));
+ $this->option('type', 'INNODB');
+ $this->option('collate', 'utf8_unicode_ci');
+ $this->option('charset', 'utf8');
+ }
+
+ public function setUp()
+ {
+ $timestampable0 = new IPF_ORM_Template_Timestampable(array('updated' => array('disabled' => true)));
+ $this->actAs($timestampable0);
+ }
+}
\ No newline at end of file
diff --git a/ipf/admin/templates/admin/add.html b/ipf/admin/templates/admin/add.html
new file mode 100644
index 0000000..66dc26b
--- /dev/null
+++ b/ipf/admin/templates/admin/add.html
@@ -0,0 +1,25 @@
+{extends "admin/base.html"}
+
+{block breadcrumbs} Home » {$classname} » {$page_title}{/block}
+
+{block content}
+
+{$page_title}
+
+
+
+{$page_title}
+
+
+
+{$page_title}
+
+
+
+{$page_title}
+
+
+
+{$page_title}
+ {$app.name|escxml}
+
+ {foreach $app.models as $model}
+
+ {/foreach}
+
+
+ {/foreach}
+ {$model.name}
+ {trans 'Add'}
+ {trans 'Change'}
+
+{$page_title}
+
+
+
+
+
+ {foreach $header as $h}
+
+
+
+ {foreach $objects as $o}
+ {$h.title}
+ {/foreach}
+
+ {foreach $o.ModelAdmin().ListRow($o) as $v}
+
+ {/foreach}
+
+ {$v|safe}
+ {/foreach}
+ {$page_title}
+
+
+
+
| PHP | +'.$o($e->getFile()).', line '.$o($e->getLine()).' | +
|---|---|
| URI | +'.$o($_SERVER['REQUEST_METHOD'].' '. + $_SERVER['REQUEST_URI']).' | +
| Arg | +Name | +Value | +
|---|---|---|
| '.$o($k).' | +'.$o($name).' | +
+ '.highlight_string(print_r($v,true), true).'
+ |
+
'.$clean($line).''.
+ $clean($line).'';
+ foreach ($req_headers as $req_h_name => $req_h_val) {
+ $out .= $o($req_h_name.': '.$req_h_val);
+ $out .= '
';
+ }
+ $out .= '
No headers.
'; + } + $req_body = file_get_contents('php://input'); + if ( strlen( $req_body ) > 0 ) { + $out .=' +
+ '.$o($req_body).'
+
| Variable | +Value | +
|---|---|
| '.$o($k).' | +
+ '.$o(print_r($v,TRUE)).'
+ |
+
No data
'; + } + } + $out .= ' + +';
+ foreach ( $resp_headers as $resp_h ) {
+ $out .= $o($resp_h);
+ $out .= '
';
+ }
+ $out .= '
No headers.
'; + } + $out .= ' +"; + $r[] = "IPF_ORM_Manager"; + $r[] = "Connections : ".count($this->_connections); + $r[] = ""; + return implode("\n",$r); + } +} diff --git a/ipf/orm/null.php b/ipf/orm/null.php new file mode 100644 index 0000000..20e813d --- /dev/null +++ b/ipf/orm/null.php @@ -0,0 +1,13 @@ +loadData($path); + } + + static public function dump($array, $type = 'xml', $path = null) + { + $parser = self::getParser($type); + return $parser->dumpData($array, $path); + } + + public function doLoad($path) + { + ob_start(); + if ( ! file_exists($path)) { + $contents = $path; + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'dparser_' . microtime(); + + file_put_contents($path, $contents); + } + include($path); + $contents = iconv("UTF-8", "UTF-8", ob_get_clean()); + return $contents; + } + + public function doDump($data, $path = null) + { + if ($path !== null) { + return file_put_contents($path, $data); + } else { + return $data; + } + } +} \ No newline at end of file diff --git a/ipf/orm/parser/spyc.php b/ipf/orm/parser/spyc.php new file mode 100644 index 0000000..953d2b4 --- /dev/null +++ b/ipf/orm/parser/spyc.php @@ -0,0 +1,637 @@ +id = $nodeId; + } +} + +class IPF_ORM_Parser_Spyc { + + function YAMLLoad($input) { + $spyc = new IPF_ORM_Spyc; + return $spyc->load($input); + } + + function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new IPF_ORM_Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + function load($input) { + if ( ! empty($input) && (strpos($input, "\n") === false) + && file_exists($input)) { + $yaml = file($input); + } else { + $yaml = explode("\n",$input); + } + $base = new IPF_ORM_Parser_Spyc_YamlNode (1); + $base->indent = 0; + $this->_lastIndent = 0; + $this->_lastNode = $base->id; + $this->_inBlock = false; + $this->_isInline = false; + $this->_nodeId = 2; + + foreach ($yaml as $linenum => $line) { + $ifchk = trim($line); + if (preg_match('/^(\t)+(\w+)/', $line)) { + $err = 'ERROR: Line '. ($linenum + 1) .' in your input YAML begins'. + ' with a tab. YAML only recognizes spaces. Please reformat.'; + die($err); + } + + if ($this->_inBlock === false && empty($ifchk)) { + continue; + } elseif ($this->_inBlock == true && empty($ifchk)) { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] .= "\n"; + } elseif ($ifchk{0} != '#' && substr($ifchk,0,3) != '---') { + $node = new IPF_ORM_Parser_Spyc_YamlNode ($this->_nodeId); + $this->_nodeId++; + + $node->indent = $this->_getIndent($line); + + // Check where the node lies in the hierarchy + if ($this->_lastIndent == $node->indent) { + // If we're in a block, add the text to the parent's data + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } else { + // The current node's parent is the same as the previous node's + if (isset($this->_allNodes[$this->_lastNode])) { + $node->parent = $this->_allNodes[$this->_lastNode]->parent; + } + } + } elseif ($this->_lastIndent < $node->indent) { + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } elseif ($this->_inBlock === false) { + // The current node's parent is the previous node + $node->parent = $this->_lastNode; + + // If the value of the last node's data was > or | we need to + // start blocking i.e. taking in all lines as a text value until + // we drop our indent. + $parent =& $this->_allNodes[$node->parent]; + $this->_allNodes[$node->parent]->children = true; + if (is_array($parent->data)) { + $chk = ''; + if (isset ($parent->data[key($parent->data)])) + $chk = $parent->data[key($parent->data)]; + if ($chk === '>') { + $this->_inBlock = true; + $this->_blockEnd = ' '; + $parent->data[key($parent->data)] = + str_replace('>','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line).' '; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } elseif ($chk === '|') { + $this->_inBlock = true; + $this->_blockEnd = "\n"; + $parent->data[key($parent->data)] = + str_replace('|','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line)."\n"; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } + } + } + } elseif ($this->_lastIndent > $node->indent) { + // Any block we had going is dead now + if ($this->_inBlock === true) { + $this->_inBlock = false; + if ($this->_blockEnd = "\n") { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] = + trim($last->data[key($last->data)]); + } + } + + // We don't know the parent of the node so we have to find it + // foreach ($this->_allNodes as $n) { + foreach ($this->_indentSort[$node->indent] as $n) { + if ($n->indent == $node->indent) { + $node->parent = $n->parent; + } + } + } + + if ($this->_inBlock === false) { + // Set these properties with information from our current node + $this->_lastIndent = $node->indent; + // Set the last node + $this->_lastNode = $node->id; + // Parse the YAML line and return its data + $node->data = $this->_parseLine($line); + // Add the node to the master list + $this->_allNodes[$node->id] = $node; + // Add a reference to the parent list + $this->_allParent[intval($node->parent)][] = $node->id; + // Add a reference to the node in an indent array + $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; + // Add a reference to the node in a References array if this node + // has a YAML reference in it. + if ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + ( ! is_array($node->data[key($node->data)])) ) + && + ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) + || + (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } elseif ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + (is_array($node->data[key($node->data)])) ) + ) { + // Incomplete reference making code. Ugly, needs cleaned up. + foreach ($node->data[key($node->data)] as $d) { + if ( !is_array($d) && + ( (preg_match('/^&([^ ]+)/',$d)) + || + (preg_match('/^\*([^ ]+)/',$d)) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } + } + } + } + } + } + unset($node); + + // Here we travel through node-space and pick out references (& and *) + $this->_linkReferences(); + + // Build the PHP array out of node-space + $trunk = $this->_buildArray(); + return $trunk; + } + + function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,0); + } + return $string; + } + + var $_haveRefs; + var $_allNodes; + var $_allParent; + var $_lastIndent; + var $_lastNode; + var $_inBlock; + var $_isInline; + var $_dumpIndent; + var $_dumpWordWrap; + + var $_nodeId; + + function _yamlize($key,$value,$indent) { + if (is_array($value)) { + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key,NULL,$indent); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif ( ! is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key,$value,$indent); + } + return $string; + } + + function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,$indent); + } + return $string; + } else { + return false; + } + } + + function _dumpNode($key,$value,$indent) { + // do some folding here, for blocks + if (strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + if (is_bool($value)) { + $value = ($value) ? "true" : "false"; + } + + $spaces = str_repeat(' ',$indent); + + if (is_int($key)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // It's mapped + $string = $spaces.$key.': '.$value."\n"; + } + return $string; + } + + function _doLiteralBlock($value,$indent) { + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + if ($this->_dumpWordWrap === 0) { + return $value; + } + + if (strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } + return $value; + } + + function _getIndent($line) { + preg_match('/^\s{1,}/',$line,$match); + if ( ! empty($match[0])) { + $indent = substr_count($match[0],' '); + } else { + $indent = 0; + } + return $indent; + } + + function _parseLine($line) { + $line = trim($line); + + if(!preg_match("/\\\#/", $line)) { + $line = trim(preg_replace('/#.*$/', '', $line)); + } + + $array = array(); + + if (preg_match('/^-(.*):$/',$line)) { + // It's a mapped sequence + $key = trim(substr(substr($line,1),0,-1)); + $array[$key] = ''; + } elseif ($line[0] == '-' && substr($line,0,3) != '---') { + // It's a list item but not a new stream + if (strlen($line) > 1) { + $value = trim(substr($line,1)); + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + $array[] = $value; + } else { + $array[] = array(); + } + } elseif (preg_match('/^(.+):/',$line,$key)) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':',$line); + $key = trim($explode[0]); + array_shift($explode); + $value = trim(implode(':',$explode)); + } + + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if (empty($key)) { + $array[] = $value; + } else { + $array[$key] = $value; + } + } + return $array; + } + + function _toType($value) { + if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { + $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); + $value = preg_replace('/\\\\"/','"',$value); + } elseif (preg_match('/^\\[(.*)\\]$/',$value,$matches)) { + // Inline Sequence + + // Take out strings sequences and mappings + $explode = empty($matches[1]) ? array() : $this->_inlineEscape($matches[1]); + + // Propogate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + } elseif (strpos($value,': ')!==false && !preg_match('/^{(.+)/',$value)) { + // It's a map + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + $value = array($key => $value); + } elseif (preg_match("/{(.+)}$/",$value,$matches)) { + // Inline Mapping + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $array = array(); + foreach ($explode as $v) { + $array = $array + $this->_toType($v); + } + $value = $array; + } elseif (strtolower($value) == 'null' or $value == '' or $value == '~') { + $value = NULL; + } elseif (preg_match ('/^[0-9]+$/', $value)) { + // Cheeky change for compartibility with PHP < 4.2.0 + $raw = $value; + $value = (int) $value; + + if ((string) $value != (string) $raw) { + $value = (string) $raw; + } + } elseif (in_array(strtolower($value), + array('true', 'on', '+', 'yes', 'y'))) { + $value = true; + } elseif (in_array(strtolower($value), + array('false', 'off', '-', 'no', 'n'))) { + $value = false; + } elseif (is_numeric($value)) { + $raw = $value; + $value = (float) $value; + + if ((string) $value != (string) $raw) { + $value = (string) $raw; + } + } else { + // Just a normal string, right? + $value = trim(preg_replace('/#(.+)$/','',$value)); + } + + return $value; + } + + function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + $saved_strings = array(); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + // Check for sequences + if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) { + $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); + $seqs = $seqs[0]; + } + + // Check for mappings + if (preg_match_all('/{(.+)}/U',$inline,$maps)) { + $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); + $maps = $maps[0]; + } + + $explode = explode(', ',$inline); + + + // Re-add the sequences + if ( ! empty($seqs)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $explode[$key] = str_replace('YAMLSeq',$seqs[$i],$value); + ++$i; + } + } + } + + // Re-add the mappings + if ( ! empty($maps)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + $explode[$key] = str_replace('YAMLMap',$maps[$i],$value); + ++$i; + } + } + } + + // Re-add the strings + if ( ! empty($saved_strings)) { + $i = 0; + foreach ($explode as $key => $value) { + while (strpos($value,'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$i],$value, 1); + ++$i; + $value = $explode[$key]; + } + } + } + + return $explode; + } + + function _buildArray() { + $trunk = array(); + + if ( ! isset($this->_indentSort[0])) { + return $trunk; + } + + foreach ($this->_indentSort[0] as $n) { + if (empty($n->parent)) { + $this->_nodeArrayizeData($n); + // Check for references and copy the needed data to complete them. + $this->_makeReferences($n); + // Merge our data with the big array we're building + $trunk = $this->_array_kmerge($trunk,$n->data); + } + } + + return $trunk; + } + + function _linkReferences() { + if (is_array($this->_haveRefs)) { + foreach ($this->_haveRefs as $node) { + if ( ! empty($node->data)) { + $key = key($node->data); + // If it's an array, don't check. + if (is_array($node->data[$key])) { + foreach ($node->data[$key] as $k => $v) { + $this->_linkRef($node,$key,$k,$v); + } + } else { + $this->_linkRef($node,$key); + } + } + } + } + return true; + } + + function _linkRef(&$n,$key,$k = NULL,$v = NULL) { + if (empty($k) && empty($v)) { + // Look for &refs + if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key] = + substr($n->data[$key],strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } elseif ( ! empty($k) && !empty($v)) { + if (preg_match('/^&([^ ]+)/',$v,$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key][$k] = + substr($v,strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } + } + + function _gatherChildren($nid) { + $return = array(); + $node =& $this->_allNodes[$nid]; + if (is_array ($this->_allParent[$node->id])) { + foreach ($this->_allParent[$node->id] as $nodeZ) { + $z =& $this->_allNodes[$nodeZ]; + // We found a child + $this->_nodeArrayizeData($z); + // Check for references + $this->_makeReferences($z); + // Merge with the big array we're returning + // The big array being all the data of the children of our parent node + $return = $this->_array_kmerge($return,$z->data); + } + } + return $return; + } + + function _nodeArrayizeData(&$node) { + if (is_array($node->data) && $node->children == true) { + // This node has children, so we need to find them + $childs = $this->_gatherChildren($node->id); + // We've gathered all our children's data and are ready to use it + $key = key($node->data); + $key = empty($key) ? 0 : $key; + // If it's an array, add to it of course + if (isset ($node->data[$key])) { + if (is_array($node->data[$key])) { + $node->data[$key] = $this->_array_kmerge($node->data[$key],$childs); + } else { + $node->data[$key] = $childs; + } + } else { + $node->data[$key] = $childs; + } + } elseif ( ! is_array($node->data) && $node->children == true) { + // Same as above, find the children of this node + $childs = $this->_gatherChildren($node->id); + $node->data = array(); + $node->data[] = $childs; + } + + // We edited $node by reference, so just return true + return true; + } + + function _makeReferences(&$z) { + // It is a reference + if (isset($z->ref)) { + $key = key($z->data); + // Copy the data to this object for easy retrieval later + $this->ref[$z->ref] =& $z->data[$key]; + // It has a reference + } elseif (isset($z->refKey)) { + if (isset($this->ref[$z->refKey])) { + $key = key($z->data); + // Copy the data from this object to make the node a real reference + $z->data[$key] =& $this->ref[$z->refKey]; + } + } + return true; + } + + function _array_kmerge($arr1,$arr2) { + if( ! is_array($arr1)) $arr1 = array(); + if( ! is_array($arr2)) $arr2 = array(); + + $keys = array_merge(array_keys($arr1),array_keys($arr2)); + $vals = array_merge(array_values($arr1),array_values($arr2)); + $ret = array(); + foreach($keys as $key) { + list($unused,$val) = each($vals); + if (isset($ret[$key]) and is_int($key)) $ret[] = $val; else $ret[$key] = $val; + } + return $ret; + } +} \ No newline at end of file diff --git a/ipf/orm/parser/yml.php b/ipf/orm/parser/yml.php new file mode 100644 index 0000000..bd6e0a4 --- /dev/null +++ b/ipf/orm/parser/yml.php @@ -0,0 +1,19 @@ +dump($array, false, false); + return $this->doDump($data, $path); + } + + public function loadData($path) + { + $contents = $this->doLoad($path); + $spyc = new IPF_ORM_Parser_Spyc(); + $array = $spyc->load($contents); + return $array; + } +} diff --git a/ipf/orm/query.php b/ipf/orm/query.php new file mode 100644 index 0000000..29fb618 --- /dev/null +++ b/ipf/orm/query.php @@ -0,0 +1,1494 @@ +_pendingJoinConditions = array(); + $this->_pendingSubqueries = array(); + $this->_pendingFields = array(); + $this->_neededTables = array(); + $this->_expressionMap = array(); + $this->_subqueryAliases = array(); + $this->_needsSubquery = false; + $this->_isLimitSubqueryUsed = false; + } + + public function createSubquery() + { + $class = get_class($this); + $obj = new $class(); + + // copy the aliases to the subquery + $obj->copyAliases($this); + + // this prevents the 'id' being selected, re ticket #307 + $obj->isSubquery(true); + + return $obj; + } + + protected function _addPendingJoinCondition($componentAlias, $joinCondition) + { + $this->_pendingJoins[$componentAlias] = $joinCondition; + } + + public function addEnumParam($key, $table = null, $column = null) + { + $array = (isset($table) || isset($column)) ? array($table, $column) : array(); + + if ($key === '?') { + $this->_enumParams[] = $array; + } else { + $this->_enumParams[$key] = $array; + } + } + + public function getEnumParams() + { + return $this->_enumParams; + } + + public function getDql() + { + $q = ''; + $q .= ( ! empty($this->_dqlParts['select']))? 'SELECT ' . implode(', ', $this->_dqlParts['select']) : ''; + $q .= ( ! empty($this->_dqlParts['from']))? ' FROM ' . implode(' ', $this->_dqlParts['from']) : ''; + $q .= ( ! empty($this->_dqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_dqlParts['where']) : ''; + $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : ''; + $q .= ( ! empty($this->_dqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_dqlParts['having']) : ''; + $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : ''; + $q .= ( ! empty($this->_dqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_dqlParts['limit']) : ''; + $q .= ( ! empty($this->_dqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_dqlParts['offset']) : ''; + + return $q; + } + + + public function fetchArray($params = array()) { + return $this->execute($params, IPF_ORM::HYDRATE_ARRAY); + } + + public function fetchOne($params = array(), $hydrationMode = null) + { + $collection = $this->execute($params, $hydrationMode); + + if (count($collection) === 0) { + return false; + } + + if ($collection instanceof IPF_ORM_Collection) { + return $collection->getFirst(); + } else if (is_array($collection)) { + return array_shift($collection); + } + + return false; + } + + public function isSubquery($bool = null) + { + if ($bool === null) { + return $this->_isSubquery; + } + + $this->_isSubquery = (bool) $bool; + return $this; + } + + public function getAggregateAlias($dqlAlias) + { + return $this->getSqlAggregateAlias($dqlAlias); + } + + public function getSqlAggregateAlias($dqlAlias) + { + if (isset($this->_aggregateAliasMap[$dqlAlias])) { + // mark the expression as used + $this->_expressionMap[$dqlAlias][1] = true; + + return $this->_aggregateAliasMap[$dqlAlias]; + } else if ( ! empty($this->_pendingAggregates)) { + $this->processPendingAggregates(); + + return $this->getSqlAggregateAlias($dqlAlias); + } else { + throw new IPF_ORM_Exception('Unknown aggregate alias: ' . $dqlAlias); + } + } + + public function getDqlPart($queryPart) + { + if ( ! isset($this->_dqlParts[$queryPart])) { + throw new IPF_ORM_Exception('Unknown query part ' . $queryPart); + } + + return $this->_dqlParts[$queryPart]; + } + + public function contains($dql) + { + return stripos($this->getDql(), $dql) === false ? false : true; + } + + public function processPendingFields($componentAlias) + { + $tableAlias = $this->getTableAlias($componentAlias); + $table = $this->_queryComponents[$componentAlias]['table']; + + if ( ! isset($this->_pendingFields[$componentAlias])) { + if ($this->_hydrator->getHydrationMode() != IPF_ORM::HYDRATE_NONE) { + if ( ! $this->_isSubquery && $componentAlias == $this->getRootAlias()) { + throw new IPF_ORM_Exception("The root class of the query (alias $componentAlias) " + . " must have at least one field selected."); + } + } + return; + } + + // At this point we know the component is FETCHED (either it's the base class of + // the query (FROM xyz) or its a "fetch join"). + + // Check that the parent join (if there is one), is a "fetch join", too. + if (isset($this->_queryComponents[$componentAlias]['parent'])) { + $parentAlias = $this->_queryComponents[$componentAlias]['parent']; + if (is_string($parentAlias) && ! isset($this->_pendingFields[$parentAlias]) + && $this->_hydrator->getHydrationMode() != IPF_ORM::HYDRATE_NONE) { + throw new IPF_ORM_Exception("The left side of the join between " + . "the aliases '$parentAlias' and '$componentAlias' must have at least" + . " the primary key field(s) selected."); + } + } + + $fields = $this->_pendingFields[$componentAlias]; + + // check for wildcards + if (in_array('*', $fields)) { + $fields = $table->getFieldNames(); + } else { + // only auto-add the primary key fields if this query object is not + // a subquery of another query object + if ( ! $this->_isSubquery || $this->_hydrator->getHydrationMode() === IPF_ORM::HYDRATE_NONE) { + $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields)); + } + } + + $sql = array(); + foreach ($fields as $fieldName) { + $columnName = $table->getColumnName($fieldName); + if (($owner = $table->getColumnOwner($columnName)) !== null && + $owner !== $table->getComponentName()) { + + $parent = $this->_conn->getTable($owner); + $columnName = $parent->getColumnName($fieldName); + $parentAlias = $this->getTableAlias($componentAlias . '.' . $parent->getComponentName()); + $sql[] = $this->_conn->quoteIdentifier($parentAlias . '.' . $columnName) + . ' AS ' + . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); + } else { + $columnName = $table->getColumnName($fieldName); + $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $columnName) + . ' AS ' + . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); + } + } + + $this->_neededTables[] = $tableAlias; + + return implode(', ', $sql); + } + + public function parseSelectField($field) + { + $terms = explode('.', $field); + + if (isset($terms[1])) { + $componentAlias = $terms[0]; + $field = $terms[1]; + } else { + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $fields = $terms[0]; + } + + $tableAlias = $this->getTableAlias($componentAlias); + $table = $this->_queryComponents[$componentAlias]['table']; + + + // check for wildcards + if ($field === '*') { + $sql = array(); + + foreach ($table->getColumnNames() as $field) { + $sql[] = $this->parseSelectField($componentAlias . '.' . $field); + } + + return implode(', ', $sql); + } else { + $name = $table->getColumnName($field); + + $this->_neededTables[] = $tableAlias; + + return $this->_conn->quoteIdentifier($tableAlias . '.' . $name) + . ' AS ' + . $this->_conn->quoteIdentifier($tableAlias . '__' . $name); + } + } + + public function getExpressionOwner($expr) + { + if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') { + preg_match_all("/[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[\.[a-z0-9]+]*/i", $expr, $matches); + + $match = current($matches); + + if (isset($match[0])) { + $terms = explode('.', $match[0]); + + return $terms[0]; + } + } + return $this->getRootAlias(); + + } + + public function parseSelect($dql) + { + $refs = $this->_tokenizer->sqlExplode($dql, ','); + + $pos = strpos(trim($refs[0]), ' '); + $first = substr($refs[0], 0, $pos); + + // check for DISTINCT keyword + if ($first === 'DISTINCT') { + $this->_sqlParts['distinct'] = true; + + $refs[0] = substr($refs[0], ++$pos); + } + + $parsedComponents = array(); + + foreach ($refs as $reference) { + $reference = trim($reference); + + if (empty($reference)) { + continue; + } + + $terms = $this->_tokenizer->sqlExplode($reference, ' '); + + $pos = strpos($terms[0], '('); + + if (count($terms) > 1 || $pos !== false) { + $expression = array_shift($terms); + $alias = array_pop($terms); + + if ( ! $alias) { + $alias = substr($expression, 0, $pos); + } + + $componentAlias = $this->getExpressionOwner($expression); + $expression = $this->parseClause($expression); + + $tableAlias = $this->getTableAlias($componentAlias); + + $index = count($this->_aggregateAliasMap); + + $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); + + $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; + + $this->_aggregateAliasMap[$alias] = $sqlAlias; + $this->_expressionMap[$alias][0] = $expression; + + $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; + + $this->_neededTables[] = $tableAlias; + } else { + $e = explode('.', $terms[0]); + + if (isset($e[1])) { + $componentAlias = $e[0]; + $field = $e[1]; + } else { + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $field = $e[0]; + } + + $this->_pendingFields[$componentAlias][] = $field; + } + } + } + + public function parseClause($clause) + { + $clause = trim($clause); + + if (is_numeric($clause)) { + return $clause; + } + + $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/', '<', '>', '=', '>=', '<=')); + + $str = ''; + foreach ($terms as $term) { + $pos = strpos($term[0], '('); + + if ($pos !== false) { + $name = substr($term[0], 0, $pos); + $term[0] = $this->parseFunctionExpression($term[0]); + } else { + if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") { + + if (strpos($term[0], '.') !== false) { + if ( ! is_numeric($term[0])) { + $e = explode('.', $term[0]); + + $field = array_pop($e); + + if ($this->getType() === IPF_ORM_Query::SELECT) { + $componentAlias = implode('.', $e); + + if (empty($componentAlias)) { + $componentAlias = $this->getRootAlias(); + } + + $this->load($componentAlias); + + // check the existence of the component alias + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception('Unknown component alias ' . $componentAlias); + } + + $table = $this->_queryComponents[$componentAlias]['table']; + + $def = $table->getDefinitionOf($field); + + // get the actual field name from alias + $field = $table->getColumnName($field); + + // check column existence + if ( ! $def) { + throw new IPF_ORM_Exception('Unknown column ' . $field); + } + + if (isset($def['owner'])) { + $componentAlias = $componentAlias . '.' . $def['owner']; + } + + $tableAlias = $this->getTableAlias($componentAlias); + + // build sql expression + $term[0] = $this->_conn->quoteIdentifier($tableAlias) + . '.' + . $this->_conn->quoteIdentifier($field); + } else { + // build sql expression + $field = $this->getRoot()->getColumnName($field); + $term[0] = $this->_conn->quoteIdentifier($field); + } + } + } else { + if ( ! empty($term[0]) && + ! in_array(strtoupper($term[0]), self::$_keywords) && + ! is_numeric($term[0])) { + + $componentAlias = $this->getRootAlias(); + + $found = false; + + if ($componentAlias !== false && + $componentAlias !== null) { + $table = $this->_queryComponents[$componentAlias]['table']; + + // check column existence + if ($table->hasField($term[0])) { + $found = true; + + $def = $table->getDefinitionOf($term[0]); + + // get the actual column name from field name + $term[0] = $table->getColumnName($term[0]); + + + if (isset($def['owner'])) { + $componentAlias = $componentAlias . '.' . $def['owner']; + } + + $tableAlias = $this->getTableAlias($componentAlias); + + if ($this->getType() === IPF_ORM_Query::SELECT) { + // build sql expression + $term[0] = $this->_conn->quoteIdentifier($tableAlias) + . '.' + . $this->_conn->quoteIdentifier($term[0]); + } else { + // build sql expression + $term[0] = $this->_conn->quoteIdentifier($term[0]); + } + } else { + $found = false; + } + } + + if ( ! $found) { + $term[0] = $this->getSqlAggregateAlias($term[0]); + } + } + } + } + } + + $str .= $term[0] . $term[1]; + } + return $str; + } + + public function parseIdentifierReference($expr) + { + } + + public function parseFunctionExpression($expr) + { + $pos = strpos($expr, '('); + + $name = substr($expr, 0, $pos); + + if ($name === '') { + return $this->parseSubquery($expr); + } + + $argStr = substr($expr, ($pos + 1), -1); + + $args = array(); + // parse args + + foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $arg) { + $args[] = $this->parseClause($arg); + } + + // convert DQL function to its RDBMS specific equivalent + try { + $expr = call_user_func_array(array($this->_conn->expression, $name), $args); + } catch (IPF_ORM_Exception $e) { + throw new IPF_ORM_Exception('Unknown function ' . $name . '.'); + } + + return $expr; + } + + public function parseSubquery($subquery) + { + $trimmed = trim($this->_tokenizer->bracketTrim($subquery)); + + // check for possible subqueries + if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { + // parse subquery + $trimmed = $this->createSubquery()->parseDqlQuery($trimmed)->getQuery(); + } else { + // parse normal clause + $trimmed = $this->parseClause($trimmed); + } + + return '(' . $trimmed . ')'; + } + + public function processPendingSubqueries() + { + foreach ($this->_pendingSubqueries as $value) { + list($dql, $alias) = $value; + + $subquery = $this->createSubquery(); + + $sql = $subquery->parseDqlQuery($dql, false)->getQuery(); + + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $tableAlias = $this->getTableAlias($componentAlias); + + $sqlAlias = $tableAlias . '__' . count($this->_aggregateAliasMap); + + $this->_sqlParts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias); + + $this->_aggregateAliasMap[$alias] = $sqlAlias; + $this->_queryComponents[$componentAlias]['agg'][] = $alias; + } + $this->_pendingSubqueries = array(); + } + + public function processPendingAggregates() + { + // iterate trhough all aggregates + foreach ($this->_pendingAggregates as $aggregate) { + list ($expression, $components, $alias) = $aggregate; + + $tableAliases = array(); + + // iterate through the component references within the aggregate function + if ( ! empty ($components)) { + foreach ($components as $component) { + + if (is_numeric($component)) { + continue; + } + + $e = explode('.', $component); + + $field = array_pop($e); + $componentAlias = implode('.', $e); + + // check the existence of the component alias + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception('Unknown component alias ' . $componentAlias); + } + + $table = $this->_queryComponents[$componentAlias]['table']; + + $field = $table->getColumnName($field); + + // check column existence + if ( ! $table->hasColumn($field)) { + throw new IPF_ORM_Exception('Unknown column ' . $field); + } + + $sqlTableAlias = $this->getSqlTableAlias($componentAlias); + + $tableAliases[$sqlTableAlias] = true; + + // build sql expression + + $identifier = $this->_conn->quoteIdentifier($sqlTableAlias . '.' . $field); + $expression = str_replace($component, $identifier, $expression); + } + } + + if (count($tableAliases) !== 1) { + $componentAlias = reset($this->_tableAliasMap); + $tableAlias = key($this->_tableAliasMap); + } + + $index = count($this->_aggregateAliasMap); + $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); + + $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; + + $this->_aggregateAliasMap[$alias] = $sqlAlias; + $this->_expressionMap[$alias][0] = $expression; + + $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; + + $this->_neededTables[] = $tableAlias; + } + // reset the state + $this->_pendingAggregates = array(); + } + + protected function _buildSqlQueryBase() + { + switch ($this->_type) { + case self::DELETE: + $q = 'DELETE FROM '; + break; + case self::UPDATE: + $q = 'UPDATE '; + break; + case self::SELECT: + $distinct = ($this->_sqlParts['distinct']) ? 'DISTINCT ' : ''; + $q = 'SELECT ' . $distinct . implode(', ', $this->_sqlParts['select']) . ' FROM '; + break; + } + return $q; + } + + protected function _buildSqlFromPart() + { + $q = ''; + foreach ($this->_sqlParts['from'] as $k => $part) { + if ($k === 0) { + $q .= $part; + continue; + } + + // preserve LEFT JOINs only if needed + // Check if it's JOIN, if not add a comma separator instead of space + if (!preg_match('/\bJOIN\b/i', $part) && !isset($this->_pendingJoinConditions[$k])) { + $q .= ', ' . $part; + } else { + $e = explode(' ', $part); + + if (substr($part, 0, 9) === 'LEFT JOIN') { + $aliases = array_merge($this->_subqueryAliases, + array_keys($this->_neededTables)); + + if ( ! in_array($e[3], $aliases) && + ! in_array($e[2], $aliases) && + + ! empty($this->_pendingFields)) { + continue; + } + + } + + if (isset($this->_pendingJoinConditions[$k])) { + $parser = new IPF_ORM_JoinCondition($this, $this->_tokenizer); + + if (strpos($part, ' ON ') !== false) { + $part .= ' AND '; + } else { + $part .= ' ON '; + } + $part .= $parser->parse($this->_pendingJoinConditions[$k]); + + unset($this->_pendingJoinConditions[$k]); + } + + $tableAlias = trim($e[3], '"'); + $componentAlias = $this->getComponentAlias($tableAlias); + + $string = $this->getInheritanceCondition($componentAlias); + + if ($string) { + $q .= ' ' . $part . ' AND ' . $string; + } else { + $q .= ' ' . $part; + } + } + + $this->_sqlParts['from'][$k] = $part; + } + return $q; + } + + public function getSqlQuery($params = array()) + { + if ($this->_state !== self::STATE_DIRTY) { + return $this->_sql; + } + + // reset the state + if ( ! $this->isSubquery()) { + $this->_queryComponents = array(); + $this->_pendingAggregates = array(); + $this->_aggregateAliasMap = array(); + } + $this->reset(); + + // invoke the preQuery hook + $this->_preQuery(); + + // process the DQL parts => generate the SQL parts. + // this will also populate the $_queryComponents. + foreach ($this->_dqlParts as $queryPartName => $queryParts) { + $this->_processDqlQueryPart($queryPartName, $queryParts); + } + $this->_state = self::STATE_CLEAN; + + $params = $this->convertEnums($params); + + // Proceed with the generated SQL + + if (empty($this->_sqlParts['from'])) { + return false; + } + + $needsSubQuery = false; + $subquery = ''; + $map = reset($this->_queryComponents); + $table = $map['table']; + $rootAlias = key($this->_queryComponents); + + if ( ! empty($this->_sqlParts['limit']) && $this->_needsSubquery && + $table->getAttribute(IPF_ORM::ATTR_QUERY_LIMIT) == IPF_ORM::LIMIT_RECORDS) { + $this->_isLimitSubqueryUsed = true; + $needsSubQuery = true; + } + + $sql = array(); + if ( ! empty($this->_pendingFields)) { + foreach ($this->_queryComponents as $alias => $map) { + $fieldSql = $this->processPendingFields($alias); + if ( ! empty($fieldSql)) { + $sql[] = $fieldSql; + } + } + } + if ( ! empty($sql)) { + array_unshift($this->_sqlParts['select'], implode(', ', $sql)); + } + + $this->_pendingFields = array(); + + // build the basic query + $q = $this->_buildSqlQueryBase(); + $q .= $this->_buildSqlFromPart(); + + if ( ! empty($this->_sqlParts['set'])) { + $q .= ' SET ' . implode(', ', $this->_sqlParts['set']); + } + + $string = $this->getInheritanceCondition($this->getRootAlias()); + + // apply inheritance to WHERE part + if ( ! empty($string)) { + if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') { + $this->_sqlParts['where'][] = $string; + } else { + $this->_sqlParts['where'][] = '(' . $string . ')'; + } + } + + $modifyLimit = true; + if ( ! empty($this->_sqlParts['limit']) || ! empty($this->_sqlParts['offset'])) { + if ($needsSubQuery) { + $subquery = $this->getLimitSubquery(); + // what about composite keys? + $idColumnName = $table->getColumnName($table->getIdentifier()); + switch (strtolower($this->_conn->getDriverName())) { + case 'mysql': + // mysql doesn't support LIMIT in subqueries + $list = $this->_conn->execute($subquery, $params)->fetchAll(IPF_ORM::FETCH_COLUMN); + $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list)); + break; + case 'pgsql': + // pgsql needs special nested LIMIT subquery + $subquery = 'SELECT ipf_orm_subquery_alias.' . $idColumnName . ' FROM (' . $subquery . ') AS ipf_orm_subquery_alias'; + break; + } + + $field = $this->getSqlTableAlias($rootAlias) . '.' . $idColumnName; + + // only append the subquery if it actually contains something + if ($subquery !== '') { + array_unshift($this->_sqlParts['where'], $this->_conn->quoteIdentifier($field) . ' IN (' . $subquery . ')'); + } + + $modifyLimit = false; + } + } + + $q .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; + $q .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; + $q .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']): ''; + $q .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; + + if ($modifyLimit) { + $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']); + } + + // return to the previous state + if ( ! empty($string)) { + array_pop($this->_sqlParts['where']); + } + if ($needsSubQuery) { + array_shift($this->_sqlParts['where']); + } + $this->_sql = $q; + + return $q; + } + + public function getLimitSubquery() + { + $map = reset($this->_queryComponents); + $table = $map['table']; + $componentAlias = key($this->_queryComponents); + + // get short alias + $alias = $this->getTableAlias($componentAlias); + // what about composite keys? + $primaryKey = $alias . '.' . $table->getColumnName($table->getIdentifier()); + + // initialize the base of the subquery + $subquery = 'SELECT DISTINCT ' . $this->_conn->quoteIdentifier($primaryKey); + + $driverName = $this->_conn->getAttribute(IPF_ORM::ATTR_DRIVER_NAME); + + // pgsql needs the order by fields to be preserved in select clause + if ($driverName == 'pgsql') { + foreach ($this->_sqlParts['orderby'] as $part) { + $part = trim($part); + $e = $this->_tokenizer->bracketExplode($part, ' '); + $part = trim($e[0]); + + if (strpos($part, '.') === false) { + continue; + } + + // don't add functions + if (strpos($part, '(') !== false) { + continue; + } + + // don't add primarykey column (its already in the select clause) + if ($part !== $primaryKey) { + $subquery .= ', ' . $part; + } + } + } + + if ($driverName == 'mysql' || $driverName == 'pgsql') { + foreach ($this->_expressionMap as $dqlAlias => $expr) { + if (isset($expr[1])) { + $subquery .= ', ' . $expr[0] . ' AS ' . $this->_aggregateAliasMap[$dqlAlias]; + } + } + } + + $subquery .= ' FROM'; + + foreach ($this->_sqlParts['from'] as $part) { + // preserve LEFT JOINs only if needed + if (substr($part, 0, 9) === 'LEFT JOIN') { + $e = explode(' ', $part); + + if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where'])) { + continue; + } + } + + $subquery .= ' ' . $part; + } + + // all conditions must be preserved in subquery + $subquery .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; + $subquery .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; + $subquery .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']) : ''; + + $subquery .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; + + // add driver specific limit clause + $subquery = $this->_conn->modifyLimitSubquery($table, $subquery, $this->_sqlParts['limit'], $this->_sqlParts['offset']); + + $parts = $this->_tokenizer->quoteExplode($subquery, ' ', "'", "'"); + + foreach ($parts as $k => $part) { + if (strpos($part, ' ') !== false) { + continue; + } + + $part = str_replace(array('"', "'", '`'), "", $part); + + if ($this->hasSqlTableAlias($part)) { + $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewSqlTableAlias($part)); + continue; + } + + if (strpos($part, '.') === false) { + continue; + } + + preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m); + + foreach ($m[0] as $match) { + $e = explode('.', $match); + + // Rebuild the original part without the newly generate alias and with quoting reapplied + $e2 = array(); + foreach ($e as $k2 => $v2) { + $e2[$k2] = $this->_conn->quoteIdentifier($v2); + } + $match = implode('.', $e2); + + // Generate new table alias + $e[0] = $this->generateNewSqlTableAlias($e[0]); + + // Requote the part with the newly generated alias + foreach ($e as $k2 => $v2) { + $e[$k2] = $this->_conn->quoteIdentifier($v2); + } + + $replace = implode('.' , $e); + + // Replace the original part with the new part with new sql table alias + $parts[$k] = str_replace($match, $replace, $parts[$k]); + } + } + + if ($driverName == 'mysql' || $driverName == 'pgsql') { + foreach ($parts as $k => $part) { + if (strpos($part, "'") !== false) { + continue; + } + if (strpos($part, '__') == false) { + continue; + } + + preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m); + + foreach ($m[0] as $match) { + $e = explode('__', $match); + $e[0] = $this->generateNewTableAlias($e[0]); + + $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]); + } + } + } + + $subquery = implode(' ', $parts); + return $subquery; + } + + public function parseDqlQuery($query, $clear = true) + { + if ($clear) { + $this->clear(); + } + + $query = trim($query); + $query = str_replace("\r", "\n", str_replace("\r\n", "\n", $query)); + $query = str_replace("\n", ' ', $query); + + $parts = $this->_tokenizer->tokenizeQuery($query); + + foreach ($parts as $partName => $subParts) { + $subParts = trim($subParts); + $partName = strtolower($partName); + switch ($partName) { + case 'create': + $this->_type = self::CREATE; + break; + case 'insert': + $this->_type = self::INSERT; + break; + case 'delete': + $this->_type = self::DELETE; + break; + case 'select': + $this->_type = self::SELECT; + $this->_addDqlQueryPart($partName, $subParts); + break; + case 'update': + $this->_type = self::UPDATE; + $partName = 'from'; + case 'from': + $this->_addDqlQueryPart($partName, $subParts); + break; + case 'set': + $this->_addDqlQueryPart($partName, $subParts, true); + break; + case 'group': + case 'order': + $partName .= 'by'; + case 'where': + case 'having': + case 'limit': + case 'offset': + $this->_addDqlQueryPart($partName, $subParts); + break; + } + } + + return $this; + } + + public function load($path, $loadFields = true) + { + if (isset($this->_queryComponents[$path])) { + return $this->_queryComponents[$path]; + } + + $e = $this->_tokenizer->quoteExplode($path, ' INDEXBY '); + + $mapWith = null; + if (count($e) > 1) { + $mapWith = trim($e[1]); + + $path = $e[0]; + } + + // parse custom join conditions + $e = explode(' ON ', $path); + + $joinCondition = ''; + + if (count($e) > 1) { + $joinCondition = $e[1]; + $overrideJoin = true; + $path = $e[0]; + } else { + $e = explode(' WITH ', $path); + + if (count($e) > 1) { + $joinCondition = $e[1]; + $path = $e[0]; + } + $overrideJoin = false; + } + + $tmp = explode(' ', $path); + $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null; + + $e = preg_split("/[.:]/", $tmp[0], -1); + + $fullPath = $tmp[0]; + $prevPath = ''; + $fullLength = strlen($fullPath); + + if (isset($this->_queryComponents[$e[0]])) { + $table = $this->_queryComponents[$e[0]]['table']; + $componentAlias = $e[0]; + + $prevPath = $parent = array_shift($e); + } + + foreach ($e as $key => $name) { + // get length of the previous path + $length = strlen($prevPath); + + // build the current component path + $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; + + $delimeter = substr($fullPath, $length, 1); + + // if an alias is not given use the current path as an alias identifier + if (strlen($prevPath) === $fullLength && isset($originalAlias)) { + $componentAlias = $originalAlias; + } else { + $componentAlias = $prevPath; + } + + // if the current alias already exists, skip it + if (isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception("Duplicate alias '$componentAlias' in query."); + } + + if ( ! isset($table)) { + // process the root of the path + + $table = $this->loadRoot($name, $componentAlias); + } else { + $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; + + $relation = $table->getRelation($name); + $localTable = $table; + + $table = $relation->getTable(); + $this->_queryComponents[$componentAlias] = array('table' => $table, + 'parent' => $parent, + 'relation' => $relation, + 'map' => null); + if ( ! $relation->isOneToOne()) { + $this->_needsSubquery = true; + } + + $localAlias = $this->getTableAlias($parent, $table->getTableName()); + $foreignAlias = $this->getTableAlias($componentAlias, $relation->getTable()->getTableName()); + + $foreignSql = $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) + . ' ' + . $this->_conn->quoteIdentifier($foreignAlias); + + $map = $relation->getTable()->inheritanceMap; + + if ( ! $loadFields || ! empty($map) || $joinCondition) { + $this->_subqueryAliases[] = $foreignAlias; + } + + if ($relation instanceof IPF_ORM_Relation_Association) { + $asf = $relation->getAssociationTable(); + + $assocTableName = $asf->getTableName(); + + if ( ! $loadFields || ! empty($map) || $joinCondition) { + $this->_subqueryAliases[] = $assocTableName; + } + + $assocPath = $prevPath . '.' . $asf->getComponentName(); + + $this->_queryComponents[$assocPath] = array('parent' => $prevPath, 'relation' => $relation, 'table' => $asf); + + $assocAlias = $this->getTableAlias($assocPath, $asf->getTableName()); + + $queryPart = $join . $assocTableName . ' ' . $assocAlias; + + $queryPart .= ' ON ' . $localAlias + . '.' + . $localTable->getColumnName($localTable->getIdentifier()) // what about composite keys? + . ' = ' + . $assocAlias . '.' . $relation->getLocal(); + + if ($relation->isEqual()) { + // equal nest relation needs additional condition + $queryPart .= ' OR ' . $localAlias + . '.' + . $table->getColumnName($table->getIdentifier()) + . ' = ' + . $assocAlias . '.' . $relation->getForeign(); + } + + $this->_sqlParts['from'][] = $queryPart; + + $queryPart = $join . $foreignSql; + + if ( ! $overrideJoin) { + $queryPart .= $this->buildAssociativeRelationSql($relation, $assocAlias, $foreignAlias, $localAlias); + } + } else { + $queryPart = $this->buildSimpleRelationSql($relation, $foreignAlias, $localAlias, $overrideJoin, $join); + } + + $queryPart .= $this->buildInheritanceJoinSql($table->getComponentName(), $componentAlias); + + $this->_sqlParts['from'][$componentAlias] = $queryPart; + if ( ! empty($joinCondition)) { + $this->_pendingJoinConditions[$componentAlias] = $joinCondition; + } + } + if ($loadFields) { + + $restoreState = false; + // load fields if necessary + if ($loadFields && empty($this->_dqlParts['select'])) { + $this->_pendingFields[$componentAlias] = array('*'); + } + } + $parent = $prevPath; + } + + $table = $this->_queryComponents[$componentAlias]['table']; + + return $this->buildIndexBy($componentAlias, $mapWith); + } + + protected function buildSimpleRelationSql(IPF_ORM_Relation $relation, $foreignAlias, $localAlias, $overrideJoin, $join) + { + $queryPart = $join . $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) + . ' ' + . $this->_conn->quoteIdentifier($foreignAlias); + + if ( ! $overrideJoin) { + $queryPart .= ' ON ' + . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocal()) + . ' = ' + . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeign()); + } + + return $queryPart; + } + + protected function buildIndexBy($componentAlias, $mapWith = null) + { + $table = $this->_queryComponents[$componentAlias]['table']; + + $indexBy = null; + + if (isset($mapWith)) { + $terms = explode('.', $mapWith); + + if (isset($terms[1])) { + $indexBy = $terms[1]; + } + } elseif ($table->getBoundQueryPart('indexBy') !== null) { + $indexBy = $table->getBoundQueryPart('indexBy'); + } + + if ($indexBy !== null) { + if ( ! $table->hasColumn($table->getColumnName($indexBy))) { + throw new IPF_ORM_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist."); + } + + $this->_queryComponents[$componentAlias]['map'] = $indexBy; + } + + return $this->_queryComponents[$componentAlias]; + } + + + protected function buildAssociativeRelationSql(IPF_ORM_Relation $relation, $assocAlias, $foreignAlias, $localAlias) + { + $table = $relation->getTable(); + + $queryPart = ' ON '; + + if ($relation->isEqual()) { + $queryPart .= '('; + } + + $localIdentifier = $table->getColumnName($table->getIdentifier()); + + $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) + . ' = ' + . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeign()); + + if ($relation->isEqual()) { + $queryPart .= ' OR ' + . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) + . ' = ' + . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocal()) + . ') AND ' + . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) + . ' != ' + . $this->_conn->quoteIdentifier($localAlias . '.' . $localIdentifier); + } + + return $queryPart; + } + + public function loadRoot($name, $componentAlias) + { + // get the connection for the component + $manager = IPF_ORM_Manager::getInstance(); + if ($manager->hasConnectionForComponent($name)) { + $this->_conn = $manager->getConnectionForComponent($name); + } + + $table = $this->_conn->getTable($name); + $tableName = $table->getTableName(); + + // get the short alias for this table + $tableAlias = $this->getTableAlias($componentAlias, $tableName); + // quote table name + $queryPart = $this->_conn->quoteIdentifier($tableName); + + if ($this->_type === self::SELECT) { + $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias); + } + + $this->_tableAliasMap[$tableAlias] = $componentAlias; + + $queryPart .= $this->buildInheritanceJoinSql($name, $componentAlias); + + $this->_sqlParts['from'][] = $queryPart; + + $this->_queryComponents[$componentAlias] = array('table' => $table, 'map' => null); + + return $table; + } + + public function buildInheritanceJoinSql($name, $componentAlias) + { + // get the connection for the component + $manager = IPF_ORM_Manager::getInstance(); + if ($manager->hasConnectionForComponent($name)) { + $this->_conn = $manager->getConnectionForComponent($name); + } + + $table = $this->_conn->getTable($name); + $tableName = $table->getTableName(); + + // get the short alias for this table + $tableAlias = $this->getTableAlias($componentAlias, $tableName); + + $queryPart = ''; + + foreach ($table->getOption('joinedParents') as $parent) { + $parentTable = $this->_conn->getTable($parent); + + $parentAlias = $componentAlias . '.' . $parent; + + // get the short alias for the parent table + $parentTableAlias = $this->getTableAlias($parentAlias, $parentTable->getTableName()); + + $queryPart .= ' LEFT JOIN ' . $this->_conn->quoteIdentifier($parentTable->getTableName()) + . ' ' . $this->_conn->quoteIdentifier($parentTableAlias) . ' ON '; + + //IPF_ORM::dump($table->getIdentifier()); + foreach ((array) $table->getIdentifier() as $identifier) { + $column = $table->getColumnName($identifier); + + $queryPart .= $this->_conn->quoteIdentifier($tableAlias) + . '.' . $this->_conn->quoteIdentifier($column) + . ' = ' . $this->_conn->quoteIdentifier($parentTableAlias) + . '.' . $this->_conn->quoteIdentifier($column); + } + } + + return $queryPart; + } + + public function getCountQuery() + { + // triggers dql parsing/processing + $this->getQuery(); // this is ugly + + // initialize temporary variables + $where = $this->_sqlParts['where']; + $having = $this->_sqlParts['having']; + $groupby = $this->_sqlParts['groupby']; + $map = reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + $tableAlias = $this->getTableAlias($componentAlias); + $table = $map['table']; + $idColumnNames = $table->getIdentifierColumnNames(); + + // build the query base + $q = 'SELECT COUNT(DISTINCT ' . $this->_conn->quoteIdentifier($tableAlias) + . '.' . implode( + ' || ' . $this->_conn->quoteIdentifier($tableAlias) . '.', + $this->_conn->quoteMultipleIdentifier($idColumnNames) + ) . ') AS num_results'; + + foreach ($this->_sqlParts['select'] as $field) { + if (strpos($field, '(') !== false) { + $q .= ', ' . $field; + } + } + + $q .= ' FROM ' . $this->_buildSqlFromPart(); + + // append column aggregation inheritance (if needed) + $string = $this->getInheritanceCondition($this->getRootAlias()); + + if ( ! empty($string)) { + $where[] = $string; + } + + // append conditions + $q .= ( ! empty($where)) ? ' WHERE ' . implode(' AND ', $where) : ''; + + if ( ! empty($groupby)) { + // Maintain existing groupby + $q .= ' GROUP BY ' . implode(', ', $groupby); + } else { + // Default groupby to primary identifier. Database defaults to this internally + // This is required for situations where the user has aggregate functions in the select part + // Without the groupby it fails + $q .= ' GROUP BY ' . $this->_conn->quoteIdentifier($tableAlias) + . '.' . implode( + ', ' . $this->_conn->quoteIdentifier($tableAlias) . '.', + $this->_conn->quoteMultipleIdentifier($idColumnNames) + ); + } + + $q .= ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having): ''; + + return $q; + } + + public function count($params = array()) + { + $q = $this->getCountQuery(); + + if ( ! is_array($params)) { + $params = array($params); + } + + $params = array_merge($this->_params['join'], $this->_params['where'], $this->_params['having'], $params); + + $params = $this->convertEnums($params); + + $results = $this->getConnection()->fetchAll($q, $params); + + if (count($results) > 1) { + $count = count($results); + } else { + if (isset($results[0])) { + $results[0] = array_change_key_case($results[0], CASE_LOWER); + $count = $results[0]['num_results']; + } else { + $count = 0; + } + } + + return (int) $count; + } + + public function query($query, $params = array(), $hydrationMode = null) + { + $this->parseDqlQuery($query); + return $this->execute($params, $hydrationMode); + } + + public function copy(IPF_ORM_Query $query = null) + { + if ( ! $query) { + $query = $this; + } + + $new = clone $query; + + return $new; + } + + public function __clone() + { + $this->_parsers = array(); + } + + public function free() + { + $this->reset(); + $this->_parsers = array(); + $this->_dqlParts = array(); + $this->_enumParams = array(); + } + + public function serialize() + { + $vars = get_object_vars($this); + } + + public function unserialize($serialized) + { + } +} diff --git a/ipf/orm/query/abstract.php b/ipf/orm/query/abstract.php new file mode 100644 index 0000000..00533c2 --- /dev/null +++ b/ipf/orm/query/abstract.php @@ -0,0 +1,1058 @@ + array(), + 'where' => array(), + 'set' => array(), + 'having' => array()); + + protected $_resultCache; + protected $_expireResultCache = false; + protected $_resultCacheTTL; + + protected $_queryCache; + protected $_expireQueryCache = false; + protected $_queryCacheTTL; + + protected $_conn; + + protected $_sqlParts = array( + 'select' => array(), + 'distinct' => false, + 'forUpdate' => false, + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + + protected $_dqlParts = array( + 'from' => array(), + 'select' => array(), + 'forUpdate' => false, + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => array(), + 'offset' => array(), + ); + + protected $_queryComponents = array(); + protected $_type = self::SELECT; + protected $_hydrator; + protected $_tokenizer; + protected $_parser; + protected $_tableAliasSeeds = array(); + protected $_options = array( + 'fetchMode' => IPF_ORM::FETCH_RECORD + ); + protected $_enumParams = array(); + + protected $_isLimitSubqueryUsed = false; + protected $_pendingSetParams = array(); + protected $_components; + protected $_preQueried = false; + + public function __construct(IPF_ORM_Connection $connection = null, + IPF_ORM_Hydrator_Abstract $hydrator = null) + { + if ($connection === null) { + $connection = IPF_ORM_Manager::getInstance()->getCurrentConnection(); + } + if ($hydrator === null) { + $hydrator = new IPF_ORM_Hydrator(); + } + $this->_conn = $connection; + $this->_hydrator = $hydrator; + $this->_tokenizer = new IPF_ORM_Query_Tokenizer(); + $this->_resultCacheTTL = $this->_conn->getAttribute(IPF_ORM::ATTR_RESULT_CACHE_LIFESPAN); + $this->_queryCacheTTL = $this->_conn->getAttribute(IPF_ORM::ATTR_QUERY_CACHE_LIFESPAN); + } + + public function setOption($name, $value) + { + if ( ! isset($this->_options[$name])) { + throw new IPF_ORM_Exception('Unknown option ' . $name); + } + $this->_options[$name] = $value; + } + + public function hasTableAlias($sqlTableAlias) + { + return $this->hasSqlTableAlias($sqlTableAlias); + } + + public function hasSqlTableAlias($sqlTableAlias) + { + return (isset($this->_tableAliasMap[$sqlTableAlias])); + } + + public function getTableAliases() + { + return $this->getTableAliasMap(); + } + + public function getTableAliasMap() + { + return $this->_tableAliasMap; + } + + public function getQueryPart($part) + { + return $this->getSqlQueryPart($part); + } + + public function getSqlQueryPart($part) + { + if ( ! isset($this->_sqlParts[$part])) { + throw new IPF_ORM_Exception('Unknown SQL query part ' . $part); + } + return $this->_sqlParts[$part]; + } + + public function setQueryPart($name, $part) + { + return $this->setSqlQueryPart($name, $part); + } + + public function setSqlQueryPart($name, $part) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + + if ($name !== 'limit' && $name !== 'offset') { + if (is_array($part)) { + $this->_sqlParts[$name] = $part; + } else { + $this->_sqlParts[$name] = array($part); + } + } else { + $this->_sqlParts[$name] = $part; + } + + return $this; + } + + public function addQueryPart($name, $part) + { + return $this->addSqlQueryPart($name, $part); + } + + public function addSqlQueryPart($name, $part) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + if (is_array($part)) { + $this->_sqlParts[$name] = array_merge($this->_sqlParts[$name], $part); + } else { + $this->_sqlParts[$name][] = $part; + } + return $this; + } + + public function removeQueryPart($name) + { + return $this->removeSqlQueryPart($name); + } + + public function removeSqlQueryPart($name) + { + if ( ! isset($this->_sqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + + if ($name == 'limit' || $name == 'offset') { + $this->_sqlParts[$name] = false; + } else { + $this->_sqlParts[$name] = array(); + } + + return $this; + } + + public function removeDqlQueryPart($name) + { + if ( ! isset($this->_dqlParts[$name])) { + throw new IPF_ORM_Exception('Unknown query part ' . $name); + } + + if ($name == 'limit' || $name == 'offset') { + $this->_dqlParts[$name] = false; + } else { + $this->_dqlParts[$name] = array(); + } + + return $this; + } + + public function getParams($params = array()) + { + return array_merge($this->_params['join'], $this->_params['set'], $this->_params['where'], $this->_params['having'], $params); + } + + public function setParams(array $params = array()) { + $this->_params = $params; + } + + public function setView(IPF_ORM_View $view) + { + $this->_view = $view; + } + + public function getView() + { + return $this->_view; + } + + public function isLimitSubqueryUsed() + { + return $this->_isLimitSubqueryUsed; + } + + public function convertEnums($params) + { + $table = $this->getRoot(); + + // $position tracks the position of the parameter, to ensure we're converting + // the right parameter value when simple ? placeholders are used. + // This only works because SET is only allowed in update statements and it's + // the first place where parameters can occur.. see issue #935 + $position = 0; + foreach ($this->_pendingSetParams as $fieldName => $value) { + $e = explode('.', $fieldName); + $fieldName = isset($e[1]) ? $e[1]:$e[0]; + if ($table->getTypeOf($fieldName) == 'enum') { + $value = $value === '?' ? $position : $value; + $this->addEnumParam($value, $table, $fieldName); + } + ++$position; + } + $this->_pendingSetParams = array(); + + foreach ($this->_enumParams as $key => $values) { + if (isset($params[$key])) { + if ( ! empty($values)) { + $params[$key] = $values[0]->enumIndex($values[1], $params[$key]); + } + } + } + + return $params; + } + + public function getInheritanceCondition($componentAlias) + { + $map = $this->_queryComponents[$componentAlias]['table']->inheritanceMap; + + // No inheritance map so lets just return + if (empty($map)) { + return; + } + + $tableAlias = $this->getSqlTableAlias($componentAlias); + + if ($this->_type !== IPF_ORM_Query::SELECT) { + $tableAlias = ''; + } else { + $tableAlias .= '.'; + } + + $field = key($map); + $value = current($map); + $identifier = $this->_conn->quoteIdentifier($tableAlias . $field); + + return $identifier . ' = ' . $this->_conn->quote($value);; + } + + public function getTableAlias($componentAlias, $tableName = null) + { + return $this->getSqlTableAlias($componentAlias, $tableName); + } + + public function getSqlTableAlias($componentAlias, $tableName = null) + { + $alias = array_search($componentAlias, $this->_tableAliasMap); + + if ($alias !== false) { + return $alias; + } + + if ($tableName === null) { + throw new IPF_ORM_Exception("Couldn't get short alias for " . $componentAlias); + } + + return $this->generateTableAlias($componentAlias, $tableName); + } + + public function generateNewTableAlias($oldAlias) + { + return $this->generateNewSqlTableAlias($oldAlias); + } + + public function generateNewSqlTableAlias($oldAlias) + { + if (isset($this->_tableAliasMap[$oldAlias])) { + // generate a new alias + $name = substr($oldAlias, 0, 1); + $i = ((int) substr($oldAlias, 1)); + + if ($i == 0) { + $i = 1; + } + + $newIndex = ($this->_tableAliasSeeds[$name] + $i); + + return $name . $newIndex; + } + + return $oldAlias; + } + + public function getTableAliasSeed($sqlTableAlias) + { + return $this->getSqlTableAliasSeed($sqlTableAlias); + } + + public function getSqlTableAliasSeed($sqlTableAlias) + { + if ( ! isset($this->_tableAliasSeeds[$sqlTableAlias])) { + return 0; + } + return $this->_tableAliasSeeds[$sqlTableAlias]; + } + + public function hasAliasDeclaration($componentAlias) + { + return isset($this->_queryComponents[$componentAlias]); + } + + public function getAliasDeclaration($componentAlias) + { + return $this->getQueryComponent($componentAlias); + } + + public function getQueryComponent($componentAlias) + { + if ( ! isset($this->_queryComponents[$componentAlias])) { + throw new IPF_ORM_Exception('Unknown component alias ' . $componentAlias); + } + + return $this->_queryComponents[$componentAlias]; + } + + public function copyAliases(IPF_ORM_Query_Abstract $query) + { + $this->_tableAliasMap = $query->_tableAliasMap; + $this->_queryComponents = $query->_queryComponents; + $this->_tableAliasSeeds = $query->_tableAliasSeeds; + return $this; + } + + public function getRootAlias() + { + if ( ! $this->_queryComponents) { + $this->getSql(); + } + reset($this->_queryComponents); + + return key($this->_queryComponents); + } + + public function getRootDeclaration() + { + $map = reset($this->_queryComponents); + return $map; + } + + public function getRoot() + { + $map = reset($this->_queryComponents); + + if ( ! isset($map['table'])) { + throw new IPF_ORM_Exception('Root component not initialized.'); + } + + return $map['table']; + } + + public function generateTableAlias($componentAlias, $tableName) + { + return $this->generateSqlTableAlias($componentAlias, $tableName); + } + + public function generateSqlTableAlias($componentAlias, $tableName) + { + preg_match('/([^_])/', $tableName, $matches); + $char = strtolower($matches[0]); + + $alias = $char; + + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + + while (isset($this->_tableAliasMap[$alias])) { + if ( ! isset($this->_tableAliasSeeds[$alias])) { + $this->_tableAliasSeeds[$alias] = 1; + } + $alias = $char . ++$this->_tableAliasSeeds[$alias]; + } + + $this->_tableAliasMap[$alias] = $componentAlias; + + return $alias; + } + + public function getComponentAlias($sqlTableAlias) + { + if ( ! isset($this->_tableAliasMap[$sqlTableAlias])) { + throw new IPF_ORM_Exception('Unknown table alias ' . $sqlTableAlias); + } + return $this->_tableAliasMap[$sqlTableAlias]; + } + + protected function _execute($params) + { + $params = $this->_conn->convertBooleans($params); + + if ( ! $this->_view) { + if ($this->_queryCache || $this->_conn->getAttribute(IPF_ORM::ATTR_QUERY_CACHE)) { + $queryCacheDriver = $this->getQueryCacheDriver(); + // calculate hash for dql query + $dql = $this->getDql(); + $hash = md5($dql . 'IPF_ORM_QUERY_CACHE_SALT'); + $cached = $queryCacheDriver->fetch($hash); + if ($cached) { + $query = $this->_constructQueryFromCache($cached); + } else { + $query = $this->getSqlQuery($params); + $serializedQuery = $this->getCachedForm($query); + $queryCacheDriver->save($hash, $serializedQuery, $this->getQueryCacheLifeSpan()); + } + } else { + $query = $this->getSqlQuery($params); + } + $params = $this->convertEnums($params); + } else { + $query = $this->_view->getSelectSql(); + } + + if ($this->isLimitSubqueryUsed() && + $this->_conn->getAttribute(IPF_ORM::ATTR_DRIVER_NAME) !== 'mysql') { + $params = array_merge($params, $params); + } + + if ($this->_type !== self::SELECT) { + return $this->_conn->exec($query, $params); + } + + $stmt = $this->_conn->execute($query, $params); + return $stmt; + } + + public function execute($params = array(), $hydrationMode = null) + { + $this->_preQuery(); + + if ($hydrationMode !== null) { + $this->_hydrator->setHydrationMode($hydrationMode); + } + + $params = $this->getParams($params); + + if ($this->_resultCache && $this->_type == self::SELECT) { + $cacheDriver = $this->getResultCacheDriver(); + + $dql = $this->getDql(); + // calculate hash for dql query + $hash = md5($dql . var_export($params, true)); + + $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); + + if ($cached === false) { + // cache miss + $stmt = $this->_execute($params); + $this->_hydrator->setQueryComponents($this->_queryComponents); + $result = $this->_hydrator->hydrateResultSet($stmt, $this->_tableAliasMap); + + $cached = $this->getCachedForm($result); + $cacheDriver->save($hash, $cached, $this->getResultCacheLifeSpan()); + } else { + $result = $this->_constructQueryFromCache($cached); + } + } else { + $stmt = $this->_execute($params); + + if (is_integer($stmt)) { + $result = $stmt; + } else { + $this->_hydrator->setQueryComponents($this->_queryComponents); + $result = $this->_hydrator->hydrateResultSet($stmt, $this->_tableAliasMap); + } + } + + return $result; + } + + protected function _getDqlCallback() + { + $callback = false; + if ( ! empty($this->_dqlParts['from'])) { + switch ($this->_type) { + case self::DELETE: + $callback = array( + 'callback' => 'preDqlDelete', + 'const' => IPF_ORM_Event::RECORD_DQL_DELETE + ); + break; + case self::UPDATE: + $callback = array( + 'callback' => 'preDqlUpdate', + 'const' => IPF_ORM_Event::RECORD_DQL_UPDATE + ); + break; + case self::SELECT: + $callback = array( + 'callback' => 'preDqlSelect', + 'const' => IPF_ORM_Event::RECORD_DQL_SELECT + ); + break; + } + } + + return $callback; + } + + protected function _preQuery() + { + if ( ! $this->_preQueried && IPF_ORM_Manager::getInstance()->getAttribute('use_dql_callbacks')) { + $this->_preQueried = true; + + $callback = $this->_getDqlCallback(); + + // if there is no callback for the query type, then we can return early + if ( ! $callback) { + return; + } + + $copy = $this->copy(); + $copy->getSqlQuery(); + + foreach ($copy->getQueryComponents() as $alias => $component) { + $table = $component['table']; + $record = $table->getRecordInstance(); + + // Trigger preDql*() callback event + $params = array('component' => $component, 'alias' => $alias); + $event = new IPF_ORM_Event($record, $callback['const'], $this, $params); + + $record->$callback['callback']($event); + $table->getRecordListener()->$callback['callback']($event); + } + } + + // Invoke preQuery() hook on IPF_ORM_Query for child classes which implement this hook + $this->preQuery(); + } + + public function preQuery() + { + } + + protected function _constructQueryFromCache($cached) + { + $cached = unserialize($cached); + $this->_tableAliasMap = $cached[2]; + $customComponent = $cached[0]; + + $queryComponents = array(); + $cachedComponents = $cached[1]; + foreach ($cachedComponents as $alias => $components) { + $e = explode('.', $components[0]); + if (count($e) === 1) { + $queryComponents[$alias]['table'] = $this->_conn->getTable($e[0]); + } else { + $queryComponents[$alias]['parent'] = $e[0]; + $queryComponents[$alias]['relation'] = $queryComponents[$e[0]]['table']->getRelation($e[1]); + $queryComponents[$alias]['table'] = $queryComponents[$alias]['relation']->getTable(); + } + if (isset($components[1])) { + $queryComponents[$alias]['agg'] = $components[1]; + } + if (isset($components[2])) { + $queryComponents[$alias]['map'] = $components[2]; + } + } + $this->_queryComponents = $queryComponents; + + return $customComponent; + } + + public function getCachedForm($customComponent = null) + { + $componentInfo = array(); + + foreach ($this->getQueryComponents() as $alias => $components) { + if ( ! isset($components['parent'])) { + $componentInfo[$alias][] = $components['table']->getComponentName(); + } else { + $componentInfo[$alias][] = $components['parent'] . '.' . $components['relation']->getAlias(); + } + if (isset($components['agg'])) { + $componentInfo[$alias][] = $components['agg']; + } + if (isset($components['map'])) { + $componentInfo[$alias][] = $components['map']; + } + } + + return serialize(array($customComponent, $componentInfo, $this->getTableAliasMap())); + } + + public function addSelect($select) + { + return $this->_addDqlQueryPart('select', $select, true); + } + + public function addTableAlias($tableAlias, $componentAlias) + { + return $this->addSqlTableAlias($tableAlias, $componentAlias); + } + + public function addSqlTableAlias($sqlTableAlias, $componentAlias) + { + $this->_tableAliasMap[$sqlTableAlias] = $componentAlias; + return $this; + } + + public function addFrom($from) + { + return $this->_addDqlQueryPart('from', $from, true); + } + + public function addWhere($where, $params = array()) + { + if (is_array($params)) { + $this->_params['where'] = array_merge($this->_params['where'], $params); + } else { + $this->_params['where'][] = $params; + } + return $this->_addDqlQueryPart('where', $where, true); + } + + public function whereIn($expr, $params = array(), $not = false) + { + $params = (array) $params; + + // if there's no params, return (else we'll get a WHERE IN (), invalid SQL) + if (!count($params)) + return $this; + + $a = array(); + foreach ($params as $k => $value) { + if ($value instanceof IPF_ORM_Expression) { + $value = $value->getSql(); + unset($params[$k]); + } else { + $value = '?'; + } + $a[] = $value; + } + + $this->_params['where'] = array_merge($this->_params['where'], $params); + + $where = $expr . ($not === true ? ' NOT ':'') . ' IN (' . implode(', ', $a) . ')'; + + return $this->_addDqlQueryPart('where', $where, true); + } + + public function whereNotIn($expr, $params = array()) + { + return $this->whereIn($expr, $params, true); + } + + public function addGroupBy($groupby) + { + return $this->_addDqlQueryPart('groupby', $groupby, true); + } + + public function addHaving($having, $params = array()) + { + if (is_array($params)) { + $this->_params['having'] = array_merge($this->_params['having'], $params); + } else { + $this->_params['having'][] = $params; + } + return $this->_addDqlQueryPart('having', $having, true); + } + + public function addOrderBy($orderby) + { + return $this->_addDqlQueryPart('orderby', $orderby, true); + } + + public function select($select) + { + return $this->_addDqlQueryPart('select', $select); + } + + public function distinct($flag = true) + { + $this->_sqlParts['distinct'] = (bool) $flag; + return $this; + } + + public function forUpdate($flag = true) + { + $this->_sqlParts[self::FOR_UPDATE] = (bool) $flag; + return $this; + } + + public function delete() + { + $this->_type = self::DELETE; + return $this; + } + + public function update($update) + { + $this->_type = self::UPDATE; + return $this->_addDqlQueryPart('from', $update); + } + + public function set($key, $value, $params = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->set($k, '?', array($v)); + } + return $this; + } else { + if ($params !== null) { + if (is_array($params)) { + $this->_params['set'] = array_merge($this->_params['set'], $params); + } else { + $this->_params['set'][] = $params; + } + } + + $this->_pendingSetParams[$key] = $value; + + return $this->_addDqlQueryPart('set', $key . ' = ' . $value, true); + } + } + + public function from($from) + { + return $this->_addDqlQueryPart('from', $from); + } + + public function innerJoin($join, $params = array()) + { + if (is_array($params)) { + $this->_params['join'] = array_merge($this->_params['join'], $params); + } else { + $this->_params['join'][] = $params; + } + + return $this->_addDqlQueryPart('from', 'INNER JOIN ' . $join, true); + } + + public function leftJoin($join, $params = array()) + { + if (is_array($params)) { + $this->_params['join'] = array_merge($this->_params['join'], $params); + } else { + $this->_params['join'][] = $params; + } + + return $this->_addDqlQueryPart('from', 'LEFT JOIN ' . $join, true); + } + + public function groupBy($groupby) + { + return $this->_addDqlQueryPart('groupby', $groupby); + } + + public function where($where, $params = array()) + { + $this->_params['where'] = array(); + if (is_array($params)) { + $this->_params['where'] = $params; + } else { + $this->_params['where'][] = $params; + } + + return $this->_addDqlQueryPart('where', $where); + } + + public function having($having, $params = array()) + { + $this->_params['having'] = array(); + if (is_array($params)) { + $this->_params['having'] = $params; + } else { + $this->_params['having'][] = $params; + } + + return $this->_addDqlQueryPart('having', $having); + } + + public function orderBy($orderby) + { + return $this->_addDqlQueryPart('orderby', $orderby); + } + + public function limit($limit) + { + return $this->_addDqlQueryPart('limit', $limit); + } + + public function offset($offset) + { + return $this->_addDqlQueryPart('offset', $offset); + } + + public function getSql($params = array()) + { + return $this->getSqlQuery($params); + } + + protected function clear() + { + $this->_sqlParts = array( + 'select' => array(), + 'distinct' => false, + 'forUpdate' => false, + 'from' => array(), + 'set' => array(), + 'join' => array(), + 'where' => array(), + 'groupby' => array(), + 'having' => array(), + 'orderby' => array(), + 'limit' => false, + 'offset' => false, + ); + } + + public function setHydrationMode($hydrationMode) + { + $this->_hydrator->setHydrationMode($hydrationMode); + return $this; + } + + public function getAliasMap() + { + return $this->_queryComponents; + } + + public function getQueryComponents() + { + return $this->_queryComponents; + } + + public function getParts() + { + return $this->getSqlParts(); + } + + public function getSqlParts() + { + return $this->_sqlParts; + } + + public function getType() + { + return $this->_type; + } + + public function useCache($driver = true, $timeToLive = null) + { + return $this->useResultCache($driver, $timeToLive); + } + + public function useResultCache($driver = true, $timeToLive = null) + { + if ($driver !== null && $driver !== true && ! ($driver instanceOf IPF_ORM_Cache_Interface)) { + $msg = 'First argument should be instance of IPF_ORM_Cache_Interface or null.'; + throw new IPF_ORM_Exception($msg); + } + $this->_resultCache = $driver; + + return $this->setResultCacheLifeSpan($timeToLive); + } + + public function useQueryCache(IPF_ORM_Cache_Interface $driver, $timeToLive = null) + { + $this->_queryCache = $driver; + return $this->setQueryCacheLifeSpan($timeToLive); + } + + public function expireCache($expire = true) + { + return $this->expireResultCache($expire); + } + + public function expireResultCache($expire = true) + { + $this->_expireResultCache = true; + return $this; + } + + public function expireQueryCache($expire = true) + { + $this->_expireQueryCache = true; + return $this; + } + + public function setCacheLifeSpan($timeToLive) + { + return $this->setResultCacheLifeSpan($timeToLive); + } + + public function setResultCacheLifeSpan($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_resultCacheTTL = $timeToLive; + + return $this; + } + + public function getResultCacheLifeSpan() + { + return $this->_resultCacheTTL; + } + + public function setQueryCacheLifeSpan($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_queryCacheTTL = $timeToLive; + + return $this; + } + + public function getQueryCacheLifeSpan() + { + return $this->_queryCacheTTL; + } + + public function getCacheDriver() + { + return $this->getResultCacheDriver(); + } + + public function getResultCacheDriver() + { + if ($this->_resultCache instanceof IPF_ORM_Cache_Interface) { + return $this->_resultCache; + } else { + return $this->_conn->getResultCacheDriver(); + } + } + + public function getQueryCacheDriver() + { + if ($this->_queryCache instanceof IPF_ORM_Cache_Interface) { + return $this->_queryCache; + } else { + return $this->_conn->getQueryCacheDriver(); + } + } + + public function getConnection() + { + return $this->_conn; + } + + protected function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + { + if ($append) { + $this->_dqlParts[$queryPartName][] = $queryPart; + } else { + $this->_dqlParts[$queryPartName] = array($queryPart); + } + + $this->_state = IPF_ORM_Query::STATE_DIRTY; + return $this; + } + + protected function _processDqlQueryPart($queryPartName, $queryParts) + { + $this->removeSqlQueryPart($queryPartName); + + if (is_array($queryParts) && ! empty($queryParts)) { + foreach ($queryParts as $queryPart) { + $parser = $this->_getParser($queryPartName); + $sql = $parser->parse($queryPart); + if (isset($sql)) { + if ($queryPartName == 'limit' || $queryPartName == 'offset') { + $this->setSqlQueryPart($queryPartName, $sql); + } else { + $this->addSqlQueryPart($queryPartName, $sql); + } + } + } + } + } + + protected function _getParser($name) + { + if ( ! isset($this->_parsers[$name])) { + $class = 'IPF_ORM_Query_' . ucwords(strtolower($name)); + + //IPF_ORM::autoload($class); + + if ( ! class_exists($class)) { + throw new IPF_ORM_Exception('Unknown parser ' . $name); + } + + $this->_parsers[$name] = new $class($this, $this->_tokenizer); + } + + return $this->_parsers[$name]; + } + + abstract public function getSqlQuery($params = array()); + + abstract public function parseDqlQuery($query); + + public function parseQuery($query) + { + return $this->parseDqlQuery($query); + } + + public function getQuery($params = array()) + { + return $this->getSqlQuery($params); + } +} diff --git a/ipf/orm/query/check.php b/ipf/orm/query/check.php new file mode 100644 index 0000000..d2d521b --- /dev/null +++ b/ipf/orm/query/check.php @@ -0,0 +1,101 @@ +getCurrentConnection() + ->getTable($table); + } + $this->table = $table; + $this->_tokenizer = new IPF_ORM_Query_Tokenizer(); + } + + public function getTable() + { + return $this->table; + } + + public function parse($dql) + { + $this->sql = $this->parseClause($dql); + } + + public function parseClause($dql) + { + $parts = $this->_tokenizer->sqlExplode($dql, ' AND '); + + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $ret[] = $this->parseSingle($part); + } + + $r = implode(' AND ', $ret); + } else { + $parts = $this->_tokenizer->quoteExplode($dql, ' OR '); + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $ret[] = $this->parseClause($part); + } + + $r = implode(' OR ', $ret); + } else { + $ret = $this->parseSingle($dql); + return $ret; + } + } + return '(' . $r . ')'; + } + + public function parseSingle($part) + { + $e = explode(' ', $part); + + $e[0] = $this->parseFunction($e[0]); + + switch ($e[1]) { + case '>': + case '<': + case '=': + case '!=': + case '<>': + + break; + default: + throw new IPF_ORM_Exception('Unknown operator ' . $e[1]); + } + + return implode(' ', $e); + } + public function parseFunction($dql) + { + if (($pos = strpos($dql, '(')) !== false) { + $func = substr($dql, 0, $pos); + $value = substr($dql, ($pos + 1), -1); + + $expr = $this->table->getConnection()->expression; + + if ( ! method_exists($expr, $func)) { + throw new IPF_ORM_Exception('Unknown function ' . $func); + } + + $func = $expr->$func($value); + } + return $func; + } + + public function getSql() + { + return $this->sql; + } +} \ No newline at end of file diff --git a/ipf/orm/query/condition.php b/ipf/orm/query/condition.php new file mode 100644 index 0000000..5a93f51 --- /dev/null +++ b/ipf/orm/query/condition.php @@ -0,0 +1,71 @@ +_tokenizer->bracketExplode($str, array(' \&\& ', ' AND '), '(', ')'); + + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $part = $this->_tokenizer->bracketTrim($part, '(', ')'); + $ret[] = $this->parse($part); + } + $r = implode(' AND ', $ret); + } else { + + $parts = $this->_tokenizer->bracketExplode($str, array(' \|\| ', ' OR '), '(', ')'); + if (count($parts) > 1) { + $ret = array(); + foreach ($parts as $part) { + $part = $this->_tokenizer->bracketTrim($part, '(', ')'); + $ret[] = $this->parse($part); + } + $r = implode(' OR ', $ret); + } else { + // Fix for #710 + if (substr($parts[0],0,1) == '(' && substr($parts[0], -1) == ')') { + return $this->parse(substr($parts[0], 1, -1)); + } else { + // Processing NOT here + if (strtoupper(substr($parts[0], 0, 4)) === 'NOT ') { + $r = 'NOT ('.$this->parse(substr($parts[0], 4)).')'; + } else { + return $this->load($parts[0]); + } + } + } + } + + return '(' . $r . ')'; + } + + public function parseLiteralValue($value) + { + // check that value isn't a string + if (strpos($value, '\'') === false) { + // parse booleans + $value = $this->query->getConnection() + ->dataDict->parseBoolean($value); + + $a = explode('.', $value); + + if (count($a) > 1) { + // either a float or a component.. + + if ( ! is_numeric($a[0])) { + // a component found + $field = array_pop($a); + $reference = implode('.', $a); + $value = $this->query->getTableAlias($reference). '.' . $field; + } + } + } else { + // string literal found + } + return $value; + } +} \ No newline at end of file diff --git a/ipf/orm/query/filter.php b/ipf/orm/query/filter.php new file mode 100644 index 0000000..65b0917 --- /dev/null +++ b/ipf/orm/query/filter.php @@ -0,0 +1,12 @@ +_filters[] = $filter; + } + + public function get($key) + { + if ( ! isset($this->_filters[$key])) { + throw new IPF_ORM_Exception('Unknown filter ' . $key); + } + return $this->_filters[$key]; + } + + public function set($key, IPF_ORM_Query_Filter $listener) + { + $this->_filters[$key] = $listener; + } + + public function preQuery(IPF_ORM_Query $query) + { + foreach ($this->_filters as $filter) { + $filter->preQuery($query); + } + } + + public function postQuery(IPF_ORM_Query $query) + { + foreach ($this->_filters as $filter) { + $filter->postQuery($query); + } + } +} \ No newline at end of file diff --git a/ipf/orm/query/filter/interface.php b/ipf/orm/query/filter/interface.php new file mode 100644 index 0000000..09c104f --- /dev/null +++ b/ipf/orm/query/filter/interface.php @@ -0,0 +1,7 @@ +_tokenizer->bracketExplode($str, 'JOIN'); + + $from = $return ? array() : null; + + $operator = false; + + switch (trim($parts[0])) { + case 'INNER': + $operator = ':'; + case 'LEFT': + array_shift($parts); + break; + } + + $last = ''; + + foreach ($parts as $k => $part) { + $part = trim($part); + + if (empty($part)) { + continue; + } + + $e = explode(' ', $part); + + if (end($e) == 'INNER' || end($e) == 'LEFT') { + $last = array_pop($e); + } + $part = implode(' ', $e); + + foreach ($this->_tokenizer->bracketExplode($part, ',') as $reference) { + $reference = trim($reference); + $e = explode(' ', $reference); + $e2 = explode('.', $e[0]); + + if ($operator) { + $e[0] = array_shift($e2) . $operator . implode('.', $e2); + } + + if ($return) { + $from[] = $e; + } else { + $table = $this->query->load(implode(' ', $e)); + } + } + + $operator = ($last == 'INNER') ? ':' : '.'; + } + return $from; + } +} diff --git a/ipf/orm/query/groupby.php b/ipf/orm/query/groupby.php new file mode 100644 index 0000000..824f11c --- /dev/null +++ b/ipf/orm/query/groupby.php @@ -0,0 +1,15 @@ +query->parseClause($reference); + } + return implode(', ', $r); + } +} diff --git a/ipf/orm/query/having.php b/ipf/orm/query/having.php new file mode 100644 index 0000000..3fe3b6d --- /dev/null +++ b/ipf/orm/query/having.php @@ -0,0 +1,58 @@ +_tokenizer->bracketExplode($func, ',', '(', ')'); + + foreach ($params as $k => $param) { + $params[$k] = $this->parseAggregateFunction($param); + } + + $funcs = $name . '(' . implode(', ', $params) . ')'; + + return $funcs; + + } else { + if ( ! is_numeric($func)) { + $a = explode('.', $func); + + if (count($a) > 1) { + $field = array_pop($a); + $reference = implode('.', $a); + $map = $this->query->load($reference, false); + $field = $map['table']->getColumnName($field); + $func = $this->query->getTableAlias($reference) . '.' . $field; + } else { + $field = end($a); + $func = $this->query->getAggregateAlias($field); + } + return $func; + } else { + return $func; + } + } + } + + final public function load($having) + { + $tokens = $this->_tokenizer->bracketExplode($having, ' ', '(', ')'); + $part = $this->parseAggregateFunction(array_shift($tokens)); + $operator = array_shift($tokens); + $value = implode(' ', $tokens); + $part .= ' ' . $operator . ' ' . $value; + // check the RHS for aggregate functions + if (strpos($value, '(') !== false) { + $value = $this->parseAggregateFunction($value); + } + return $part; + } +} diff --git a/ipf/orm/query/joincondition.php b/ipf/orm/query/joincondition.php new file mode 100644 index 0000000..94d969b --- /dev/null +++ b/ipf/orm/query/joincondition.php @@ -0,0 +1,126 @@ +_tokenizer->sqlExplode($condition); + + if (count($e) > 2) { + $expr = new IPF_ORM_Expression($e[0], $this->query->getConnection()); + $e[0] = $expr->getSql(); + + $operator = $e[1]; + + if (substr(trim($e[2]), 0, 1) != '(') { + $expr = new IPF_ORM_Expression($e[2], $this->query->getConnection()); + $e[2] = $expr->getSql(); + } + + // We need to check for agg functions here + $hasLeftAggExpression = preg_match('/(.*)\(([^\)]*)\)([\)]*)/', $e[0], $leftMatches); + + if ($hasLeftAggExpression) { + $e[0] = $leftMatches[2]; + } + + $hasRightAggExpression = preg_match('/(.*)\(([^\)]*)\)([\)]*)/', $e[2], $rightMatches); + + if ($hasRightAggExpression) { + $e[2] = $rightMatches[2]; + } + + $a = explode('.', $e[0]); + $field = array_pop($a); + $reference = implode('.', $a); + $value = $e[2]; + + $conn = $this->query->getConnection(); + $alias = $this->query->getTableAlias($reference); + $map = $this->query->getAliasDeclaration($reference); + $table = $map['table']; + // check if value is enumerated value + $enumIndex = $table->enumIndex($field, trim($value, "'")); + + if (false !== $enumIndex && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $enumIndex = $conn->quote($enumIndex, 'text'); + } + + // FIX: Issues with "(" XXX ")" + if ($hasRightAggExpression) { + $value = '(' . $value . ')'; + } + + if (substr($value, 0, 1) == '(') { + // trim brackets + $trimmed = $this->_tokenizer->bracketTrim($value); + + if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { + // subquery found + $q = $this->query->createSubquery(); + + // Change due to bug "(" XXX ")" + //$value = '(' . $q->parseQuery($trimmed)->getQuery() . ')'; + $value = $q->parseQuery($trimmed)->getQuery(); + } elseif (substr($trimmed, 0, 4) == 'SQL:') { + // Change due to bug "(" XXX ")" + //$value = '(' . substr($trimmed, 4) . ')'; + $value = substr($trimmed, 4); + } else { + // simple in expression found + $e = $this->_tokenizer->sqlExplode($trimmed, ','); + + $value = array(); + foreach ($e as $part) { + $index = $table->enumIndex($field, trim($part, "'")); + + if (false !== $index && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $index = $conn->quote($index, 'text'); + } + + if ($index !== false) { + $value[] = $index; + } else { + $value[] = $this->parseLiteralValue($part); + } + } + + // Change due to bug "(" XXX ")" + //$value = '(' . implode(', ', $value) . ')'; + $value = implode(', ', $value); + } + } else { + if ($enumIndex !== false) { + $value = $enumIndex; + } else { + $value = $this->parseLiteralValue($value); + } + } + + switch ($operator) { + case '<': + case '>': + case '=': + case '!=': + if ($enumIndex !== false) { + $value = $enumIndex; + } + default: + $leftExpr = (($hasLeftAggExpression) ? $leftMatches[1] . '(' : '') + . $alias . '.' . $field + . (($hasLeftAggExpression) ? $leftMatches[3] . ')' : '') ; + + $rightExpr = (($hasRightAggExpression) ? $rightMatches[1] . '(' : '') + . $value + . (($hasRightAggExpression) ? $rightMatches[3] . ')' : '') ; + + $condition = $leftExpr . ' ' . $operator . ' ' . $rightExpr; + } + + } + + return $condition; + } +} diff --git a/ipf/orm/query/limit.php b/ipf/orm/query/limit.php new file mode 100644 index 0000000..f0cd5ae --- /dev/null +++ b/ipf/orm/query/limit.php @@ -0,0 +1,9 @@ +query->parseClause($r); + + $ret[] = $r; + } + return $ret; + } +} diff --git a/ipf/orm/query/parser.php b/ipf/orm/query/parser.php new file mode 100644 index 0000000..71d6f1b --- /dev/null +++ b/ipf/orm/query/parser.php @@ -0,0 +1,5 @@ +query = $query; + if ( ! $tokenizer) { + $tokenizer = new IPF_ORM_Query_Tokenizer(); + } + $this->_tokenizer = $tokenizer; + } + + public function getQuery() + { + return $this->query; + } +} diff --git a/ipf/orm/query/registry.php b/ipf/orm/query/registry.php new file mode 100644 index 0000000..f35ff42 --- /dev/null +++ b/ipf/orm/query/registry.php @@ -0,0 +1,39 @@ +_queries[$key] = $query; + } else { + // namespace found + $e = explode('/', $key); + + $this->_queries[$e[0]][$e[1]] = $query; + } + } + + public function get($key, $namespace = null) + { + if (isset($namespace)) { + if ( ! isset($this->_queries[$namespace][$key])) { + throw new IPF_ORM_Exception('A query with the name ' . $namespace . '/' . $key . ' does not exist.'); + } + $query = $this->_queries[$namespace][$key]; + } else { + if ( ! isset($this->_queries[$key])) { + throw new IPF_ORM_Exception('A query with the name ' . $key . ' does not exist.'); + } + $query = $this->_queries[$key]; + } + + if ( ! ($query instanceof IPF_ORM_Query)) { + $query = IPF_ORM_Query::create()->parseQuery($query); + } + + return $query; + } +} \ No newline at end of file diff --git a/ipf/orm/query/select.php b/ipf/orm/query/select.php new file mode 100644 index 0000000..0994a68 --- /dev/null +++ b/ipf/orm/query/select.php @@ -0,0 +1,9 @@ +query->parseSelect($dql); + } +} \ No newline at end of file diff --git a/ipf/orm/query/set.php b/ipf/orm/query/set.php new file mode 100644 index 0000000..5e26e3b --- /dev/null +++ b/ipf/orm/query/set.php @@ -0,0 +1,27 @@ +_tokenizer->sqlExplode($dql, ' '); + foreach ($terms as $term) { + preg_match_all("/[a-z0-9_]+\.[a-z0-9_]+[\.[a-z0-9]+]*/i", $term, $m); + + if (isset($m[0])) { + foreach ($m[0] as $part) { + $e = explode('.', trim($part)); + $field = array_pop($e); + + $reference = implode('.', $e); + + $alias = $this->query->getTableAlias($reference); + $map = $this->query->getAliasDeclaration($reference); + + $dql = str_replace($part, $map['table']->getColumnName($field), $dql); + } + } + } + return $dql; + } +} \ No newline at end of file diff --git a/ipf/orm/query/tokenizer.php b/ipf/orm/query/tokenizer.php new file mode 100644 index 0000000..f9a7d13 --- /dev/null +++ b/ipf/orm/query/tokenizer.php @@ -0,0 +1,235 @@ +sqlExplode($query, ' '); + + foreach ($tokens as $index => $token) { + $token = trim($token); + switch (strtolower($token)) { + case 'delete': + case 'update': + case 'select': + case 'set': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $p = $token; + //$parts[$token] = array(); + $parts[$token] = ''; + break; + case 'order': + case 'group': + $i = ($index + 1); + if (isset($tokens[$i]) && strtolower($tokens[$i]) === 'by') { + $p = $token; + $parts[$token] = ''; + //$parts[$token] = array(); + } else { + $parts[$p] .= "$token "; + //$parts[$p][] = $token; + } + break; + case 'by': + continue; + default: + if ( ! isset($p)) { + throw new IPF_ORM_Exception( + "Couldn't tokenize query. Encountered invalid token: '$token'."); + } + + $parts[$p] .= "$token "; + //$parts[$p][] = $token; + } + } + return $parts; + } + + public function bracketTrim($str, $e1 = '(', $e2 = ')') + { + if (substr($str, 0, 1) === $e1 && substr($str, -1) === $e2) { + return substr($str, 1, -1); + } else { + return $str; + } + } + + public function bracketExplode($str, $d = ' ', $e1 = '(', $e2 = ')') + { + if (is_array($d)) { + $a = preg_split('#('.implode('|', $d).')#i', $str); + $d = stripslashes($d[0]); + } else { + $a = explode($d, $str); + } + + $i = 0; + $term = array(); + foreach($a as $key=>$val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + $s1 = substr_count($term[$i], $e1); + $s2 = substr_count($term[$i], $e2); + + if ($s1 == $s2) { + $i++; + } + } else { + $term[$i] .= $d . trim($val); + $c1 = substr_count($term[$i], $e1); + $c2 = substr_count($term[$i], $e2); + + if ($c1 == $c2) { + $i++; + } + } + } + return $term; + } + + public function quoteExplode($str, $d = ' ') + { + if (is_array($d)) { + $a = preg_split('/('.implode('|', $d).')/', $str); + $d = stripslashes($d[0]); + } else { + $a = explode($d, $str); + } + + $i = 0; + $term = array(); + foreach ($a as $key => $val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + + if ( ! (substr_count($term[$i], "'") & 1)) { + $i++; + } + } else { + $term[$i] .= $d . trim($val); + + if ( ! (substr_count($term[$i], "'") & 1)) { + $i++; + } + } + } + return $term; + } + + public function sqlExplode($str, $d = ' ', $e1 = '(', $e2 = ')') + { + if ($d == ' ') { + $d = array(' ', '\s'); + } + if (is_array($d)) { + $d = array_map('preg_quote', $d); + + if (in_array(' ', $d)) { + $d[] = '\s'; + } + + $split = '#(' . implode('|', $d) . ')#'; + + $str = preg_split($split, $str); + $d = stripslashes($d[0]); + } else { + $str = explode($d, $str); + } + + $i = 0; + $term = array(); + + foreach ($str as $key => $val) { + if (empty($term[$i])) { + $term[$i] = trim($val); + + $s1 = substr_count($term[$i], $e1); + $s2 = substr_count($term[$i], $e2); + + if (strpos($term[$i], '(') !== false) { + if ($s1 == $s2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1)) { + $i++; + } + } + } else { + $term[$i] .= $d . trim($val); + $c1 = substr_count($term[$i], $e1); + $c2 = substr_count($term[$i], $e2); + + if (strpos($term[$i], '(') !== false) { + if ($c1 == $c2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1)) { + $i++; + } + } + } + } + return $term; + } + + public function clauseExplode($str, array $d, $e1 = '(', $e2 = ')') + { + if (is_array($d)) { + $d = array_map('preg_quote', $d); + + if (in_array(' ', $d)) { + $d[] = '\s'; + } + + $split = '#(' . implode('|', $d) . ')#'; + + $str = preg_split($split, $str, -1, PREG_SPLIT_DELIM_CAPTURE); + } + + $i = 0; + $term = array(); + + foreach ($str as $key => $val) { + if ($key & 1) { + if (isset($term[($i - 1)]) && ! is_array($term[($i - 1)])) { + $term[($i - 1)] = array($term[($i - 1)], $val); + } + continue; + } + if (empty($term[$i])) { + $term[$i] = $val; + } else { + $term[$i] .= $str[($key - 1)] . $val; + } + + $c1 = substr_count($term[$i], $e1); + $c2 = substr_count($term[$i], $e2); + + if (strpos($term[$i], '(') !== false) { + if ($c1 == $c2) { + $i++; + } + } else { + if ( ! (substr_count($term[$i], "'") & 1) && + ! (substr_count($term[$i], "\"") & 1)) { + $i++; + } + } + } + + if (isset($term[$i - 1])) { + $term[$i - 1] = array($term[$i - 1], ''); + } + + return $term; + } +} diff --git a/ipf/orm/query/where.php b/ipf/orm/query/where.php new file mode 100644 index 0000000..1f51bbc --- /dev/null +++ b/ipf/orm/query/where.php @@ -0,0 +1,143 @@ +_tokenizer->bracketTrim(trim($where)); + $conn = $this->query->getConnection(); + $terms = $this->_tokenizer->sqlExplode($where); + + if (count($terms) > 1) { + if (substr($where, 0, 6) == 'EXISTS') { + return $this->parseExists($where, true); + } elseif (substr($where, 0, 10) == 'NOT EXISTS') { + return $this->parseExists($where, false); + } + } + + if (count($terms) < 3) { + $terms = $this->_tokenizer->sqlExplode($where, array('=', '<', '<>', '>', '!=')); + } + + if (count($terms) > 1) { + $first = array_shift($terms); + $value = array_pop($terms); + $operator = trim(substr($where, strlen($first), -strlen($value))); + $table = null; + $field = null; + + if (strpos($first, "'") === false && strpos($first, '(') === false) { + // normal field reference found + $a = explode('.', $first); + + $field = array_pop($a); + $reference = implode('.', $a); + + if (empty($reference)) { + $map = $this->query->getRootDeclaration(); + + $alias = $this->query->getTableAlias($this->query->getRootAlias()); + $table = $map['table']; + } else { + $map = $this->query->load($reference, false); + + $alias = $this->query->getTableAlias($reference); + $table = $map['table']; + } + } + $first = $this->query->parseClause($first); + + $sql = $first . ' ' . $operator . ' ' . $this->parseValue($value, $table, $field); + + return $sql; + } else { + return $where; + } + } + + public function parseValue($value, IPF_ORM_Table $table = null, $field = null) + { + $conn = $this->query->getConnection(); + + if (substr($value, 0, 1) == '(') { + // trim brackets + $trimmed = $this->_tokenizer->bracketTrim($value); + + if (substr($trimmed, 0, 4) == 'FROM' || + substr($trimmed, 0, 6) == 'SELECT') { + + // subquery found + $q = new IPF_ORM_Query(); + $value = '(' . $this->query->createSubquery()->parseQuery($trimmed, false)->getQuery() . ')'; + + } elseif (substr($trimmed, 0, 4) == 'SQL:') { + $value = '(' . substr($trimmed, 4) . ')'; + } else { + // simple in expression found + $e = $this->_tokenizer->sqlExplode($trimmed, ','); + + $value = array(); + + $index = false; + + foreach ($e as $part) { + if (isset($table) && isset($field)) { + $index = $table->enumIndex($field, trim($part, "'")); + + if (false !== $index && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $index = $conn->quote($index, 'text'); + } + } + + if ($index !== false) { + $value[] = $index; + } else { + $value[] = $this->parseLiteralValue($part); + } + } + + $value = '(' . implode(', ', $value) . ')'; + } + } else if (substr($value, 0, 1) == ':' || $value === '?') { + // placeholder found + if (isset($table) && isset($field) && $table->getTypeOf($field) == 'enum') { + $this->query->addEnumParam($value, $table, $field); + } else { + $this->query->addEnumParam($value, null, null); + } + } else { + $enumIndex = false; + if (isset($table) && isset($field)) { + // check if value is enumerated value + $enumIndex = $table->enumIndex($field, trim($value, "'")); + + if (false !== $enumIndex && $conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) { + $enumIndex = $conn->quote($enumIndex, 'text'); + } + } + + if ($enumIndex !== false) { + $value = $enumIndex; + } else { + $value = $this->parseLiteralValue($value); + } + } + return $value; + } + + public function parseExists($where, $negation) + { + $operator = ($negation) ? 'EXISTS' : 'NOT EXISTS'; + + $pos = strpos($where, '('); + + if ($pos == false) { + throw new IPF_ORM_Exception('Unknown expression, expected a subquery with () -marks'); + } + + $sub = $this->_tokenizer->bracketTrim(substr($where, $pos)); + + return $operator . ' (' . $this->query->createSubquery()->parseQuery($sub, false)->getQuery() . ')'; + } +} diff --git a/ipf/orm/rawsql.php b/ipf/orm/rawsql.php new file mode 100644 index 0000000..4177d05 --- /dev/null +++ b/ipf/orm/rawsql.php @@ -0,0 +1,230 @@ +_parseSelectFields($queryPart); + return $this; + } + if ( ! isset($this->parts[$queryPartName])) { + $this->_sqlParts[$queryPartName] = array(); + } + + if ( ! $append) { + $this->_sqlParts[$queryPartName] = array($queryPart); + } else { + $this->_sqlParts[$queryPartName][] = $queryPart; + } + return $this; + } + + protected function _addDqlQueryPart($queryPartName, $queryPart, $append = false) + { + return $this->parseQueryPart($queryPartName, $queryPart, $append); + } + + private function _parseSelectFields($queryPart){ + preg_match_all('/{([^}{]*)}/U', $queryPart, $m); + $this->fields = $m[1]; + $this->_sqlParts['select'] = array(); + } + + public function parseDqlQuery($query) + { + $this->_parseSelectFields($query); + $this->clear(); + + $tokens = $this->_tokenizer->sqlExplode($query, ' '); + + $parts = array(); + foreach ($tokens as $key => $part) { + $partLowerCase = strtolower($part); + switch ($partLowerCase) { + case 'select': + case 'from': + case 'where': + case 'limit': + case 'offset': + case 'having': + $type = $partLowerCase; + if ( ! isset($parts[$partLowerCase])) { + $parts[$partLowerCase] = array(); + } + break; + case 'order': + case 'group': + $i = $key + 1; + if (isset($tokens[$i]) && strtolower($tokens[$i]) === 'by') { + $type = $partLowerCase . 'by'; + $parts[$type] = array(); + } else { + //not a keyword so we add it to the previous type + $parts[$type][] = $part; + } + break; + case 'by': + continue; + default: + //not a keyword so we add it to the previous type. + if ( ! isset($parts[$type][0])) { + $parts[$type][0] = $part; + } else { + // why does this add to index 0 and not append to the + // array. If it had done that one could have used + // parseQueryPart. + $parts[$type][0] .= ' '.$part; + } + } + } + + $this->_sqlParts = $parts; + $this->_sqlParts['select'] = array(); + + return $this; + } + + public function getSqlQuery($params = array()) + { + $select = array(); + + foreach ($this->fields as $field) { + $e = explode('.', $field); + if ( ! isset($e[1])) { + throw new IPF_ORM_Exception('All selected fields in Sql query must be in format tableAlias.fieldName'); + } + // try to auto-add component + if ( ! $this->hasSqlTableAlias($e[0])) { + try { + $this->addComponent($e[0], ucwords($e[0])); + } catch (IPF_ORM_Exception $exception) { + throw new IPF_ORM_Exception('The associated component for table alias ' . $e[0] . ' couldn\'t be found.'); + } + } + + $componentAlias = $this->getComponentAlias($e[0]); + + if ($e[1] == '*') { + foreach ($this->_queryComponents[$componentAlias]['table']->getColumnNames() as $name) { + $field = $e[0] . '.' . $name; + + $select[$componentAlias][$field] = $field . ' AS ' . $e[0] . '__' . $name; + } + } else { + $field = $e[0] . '.' . $e[1]; + $select[$componentAlias][$field] = $field . ' AS ' . $e[0] . '__' . $e[1]; + } + } + + // force-add all primary key fields + + foreach ($this->getTableAliasMap() as $tableAlias => $componentAlias) { + $map = $this->_queryComponents[$componentAlias]; + + foreach ((array) $map['table']->getIdentifierColumnNames() as $key) { + $field = $tableAlias . '.' . $key; + + if ( ! isset($this->_sqlParts['select'][$field])) { + $select[$componentAlias][$field] = $field . ' AS ' . $tableAlias . '__' . $key; + } + } + } + + // first add the fields of the root component + reset($this->_queryComponents); + $componentAlias = key($this->_queryComponents); + + $q = 'SELECT ' . implode(', ', $select[$componentAlias]); + unset($select[$componentAlias]); + + foreach ($select as $component => $fields) { + if ( ! empty($fields)) { + $q .= ', ' . implode(', ', $fields); + } + } + + $string = $this->getInheritanceCondition($this->getRootAlias()); + if ( ! empty($string)) { + $this->_sqlParts['where'][] = $string; + } + $copy = $this->_sqlParts; + unset($copy['select']); + + $q .= ( ! empty($this->_sqlParts['from']))? ' FROM ' . implode(' ', $this->_sqlParts['from']) : ''; + $q .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_sqlParts['where']) : ''; + $q .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; + $q .= ( ! empty($this->_sqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']) : ''; + $q .= ( ! empty($this->_sqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; + $q .= ( ! empty($this->_sqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_sqlParts['limit']) : ''; + $q .= ( ! empty($this->_sqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_sqlParts['offset']) : ''; + + if ( ! empty($string)) { + array_pop($this->_sqlParts['where']); + } + return $q; + } + + public function getFields() + { + return $this->fields; + } + + public function addComponent($tableAlias, $path) + { + $tmp = explode(' ', $path); + $originalAlias = (count($tmp) > 1) ? end($tmp) : null; + + $e = explode('.', $tmp[0]); + + $fullPath = $tmp[0]; + $fullLength = strlen($fullPath); + + $table = null; + + $currPath = ''; + + if (isset($this->_queryComponents[$e[0]])) { + $table = $this->_queryComponents[$e[0]]['table']; + + $currPath = $parent = array_shift($e); + } + + foreach ($e as $k => $component) { + // get length of the previous path + $length = strlen($currPath); + + // build the current component path + $currPath = ($currPath) ? $currPath . '.' . $component : $component; + + $delimeter = substr($fullPath, $length, 1); + + // if an alias is not given use the current path as an alias identifier + if (strlen($currPath) === $fullLength && isset($originalAlias)) { + $componentAlias = $originalAlias; + } else { + $componentAlias = $currPath; + } + if ( ! isset($table)) { + $conn = IPF_ORM_Manager::getInstance() + ->getConnectionForComponent($component); + + $table = $conn->getTable($component); + $this->_queryComponents[$componentAlias] = array('table' => $table); + } else { + $relation = $table->getRelation($component); + + $this->_queryComponents[$componentAlias] = array('table' => $relation->getTable(), + 'parent' => $parent, + 'relation' => $relation); + } + $this->addSqlTableAlias($tableAlias, $componentAlias); + + $parent = $currPath; + } + + return $this; + } +} \ No newline at end of file diff --git a/ipf/orm/record.php b/ipf/orm/record.php new file mode 100644 index 0000000..325e42d --- /dev/null +++ b/ipf/orm/record.php @@ -0,0 +1,1266 @@ +_table = $table; + $exists = ( ! $isNewEntry); + } else { + // get the table of this class + $class = get_class($this); + $this->_table = IPF_ORM::getTable($class); + $exists = false; + } + + // Check if the current connection has the records table in its registry + // If not this record is only used for creating table definition and setting up + // relations. + if ( ! $this->_table->getConnection()->hasTable($this->_table->getComponentName())) { + return; + } + + $this->_oid = self::$_index; + + self::$_index++; + + // get the data array + $this->_data = $this->_table->getData(); + + // get the column count + $count = count($this->_data); + + $this->_values = $this->cleanData($this->_data); + + $this->prepareIdentifiers($exists); + + if ( ! $exists) { + if ($count > count($this->_values)) { + $this->_state = IPF_ORM_Record::STATE_TDIRTY; + } else { + $this->_state = IPF_ORM_Record::STATE_TCLEAN; + } + + // set the default values for this record + $this->assignDefaultValues(); + } else { + $this->_state = IPF_ORM_Record::STATE_CLEAN; + + if ($count < $this->_table->getColumnCount()) { + $this->_state = IPF_ORM_Record::STATE_PROXY; + } + } + + $repository = $this->_table->getRepository(); + $repository->add($this); + + $this->construct(); + } + + public static function _index() + { + return self::$_index; + } + + public function setUp(){} + public function construct(){} + + public function getOid() + { + return $this->_oid; + } + + public function oid() + { + return $this->_oid; + } + + public function isValid() + { + if ( ! $this->_table->getAttribute(IPF_ORM::ATTR_VALIDATE)) { + return true; + } + // Clear the stack from any previous errors. + $this->getErrorStack()->clear(); + + // Run validation process + $validator = new IPF_ORM_Validator(); + $validator->validateRecord($this); + $this->validate(); + if ($this->_state == self::STATE_TDIRTY || $this->_state == self::STATE_TCLEAN) { + $this->validateOnInsert(); + } else { + $this->validateOnUpdate(); + } + + return $this->getErrorStack()->count() == 0 ? true : false; + } + + protected function validate(){} + + protected function validateOnUpdate(){} + + protected function validateOnInsert(){} + + public function preSerialize($event){} + + public function postSerialize($event){} + + public function preUnserialize($event){} + + public function postUnserialize($event){} + + public function preSave($event){} + + public function postSave($event){} + + public function preDelete($event){} + + public function postDelete($event){} + + public function preUpdate($event){} + + public function postUpdate($event){} + + public function preInsert($event){} + + public function postInsert($event){} + + public function preDqlSelect($event){} + + public function preDqlUpdate($event){} + + public function preDqlDelete($event){} + + public function getErrorStack(){ + if ( ! $this->_errorStack) { + $this->_errorStack = new IPF_ORM_Validator_ErrorStack(get_class($this)); + } + + return $this->_errorStack; + } + + public function errorStack($stack = null) + { + if ($stack !== null) { + if ( ! ($stack instanceof IPF_ORM_Validator_ErrorStack)) { + throw new IPF_ORM_Exception('Argument should be an instance of IPF_ORM_Validator_ErrorStack.'); + } + $this->_errorStack = $stack; + } else { + return $this->getErrorStack(); + } + } + + public function assignDefaultValues($overwrite = false) + { + if ( ! $this->_table->hasDefaultValues()) { + return false; + } + foreach ($this->_data as $column => $value) { + $default = $this->_table->getDefaultValueOf($column); + + if ($default === null) { + continue; + } + + if ($value === self::$_null || $overwrite) { + $this->_data[$column] = $default; + $this->_modified[] = $column; + $this->_state = IPF_ORM_Record::STATE_TDIRTY; + } + } + } + + public function cleanData(&$data) + { + $tmp = $data; + $data = array(); + + foreach ($this->getTable()->getFieldNames() as $fieldName) { + if (isset($tmp[$fieldName])) { + $data[$fieldName] = $tmp[$fieldName]; + } else if (array_key_exists($fieldName, $tmp)) { + $data[$fieldName] = self::$_null; + } else if (!isset($this->_data[$fieldName])) { + $data[$fieldName] = self::$_null; + } + unset($tmp[$fieldName]); + } + + return $tmp; + } + + public function hydrate(array $data) + { + $this->_values = array_merge($this->_values, $this->cleanData($data)); + $this->_data = array_merge($this->_data, $data); + $this->prepareIdentifiers(true); + } + + private function prepareIdentifiers($exists = true) + { + switch ($this->_table->getIdentifierType()) { + case IPF_ORM::IDENTIFIER_AUTOINC: + case IPF_ORM::IDENTIFIER_SEQUENCE: + case IPF_ORM::IDENTIFIER_NATURAL: + $name = $this->_table->getIdentifier(); + if (is_array($name)) { + $name = $name[0]; + } + if ($exists) { + if (isset($this->_data[$name]) && $this->_data[$name] !== self::$_null) { + $this->_id[$name] = $this->_data[$name]; + } + } + break; + case IPF_ORM::IDENTIFIER_COMPOSITE: + $names = $this->_table->getIdentifier(); + + foreach ($names as $name) { + if ($this->_data[$name] === self::$_null) { + $this->_id[$name] = null; + } else { + $this->_id[$name] = $this->_data[$name]; + } + } + break; + } + } + + public function serialize() + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::RECORD_SERIALIZE); + + $this->preSerialize($event); + + $vars = get_object_vars($this); + + unset($vars['_references']); + unset($vars['_table']); + unset($vars['_errorStack']); + unset($vars['_filter']); + unset($vars['_node']); + + $name = $this->_table->getIdentifier(); + $this->_data = array_merge($this->_data, $this->_id); + + foreach ($this->_data as $k => $v) { + if ($v instanceof IPF_ORM_Record && $this->_table->getTypeOf($k) != 'object') { + unset($vars['_data'][$k]); + } elseif ($v === self::$_null) { + unset($vars['_data'][$k]); + } else { + switch ($this->_table->getTypeOf($k)) { + case 'array': + case 'object': + $vars['_data'][$k] = serialize($vars['_data'][$k]); + break; + case 'gzip': + $vars['_data'][$k] = gzcompress($vars['_data'][$k]); + break; + case 'enum': + $vars['_data'][$k] = $this->_table->enumIndex($k, $vars['_data'][$k]); + break; + } + } + } + + $str = serialize($vars); + + $this->postSerialize($event); + + return $str; + } + + public function unserialize($serialized) + { + $event = new IPF_ORM_Event($this, IPF_ORM_Event::RECORD_UNSERIALIZE); + + $this->preUnserialize($event); + + $manager = IPF_ORM_Manager::getInstance(); + $connection = $manager->getConnectionForComponent(get_class($this)); + + $this->_oid = self::$_index; + self::$_index++; + + $this->_table = $connection->getTable(get_class($this)); + + $array = unserialize($serialized); + + foreach($array as $k => $v) { + $this->$k = $v; + } + + foreach ($this->_data as $k => $v) { + switch ($this->_table->getTypeOf($k)) { + case 'array': + case 'object': + $this->_data[$k] = unserialize($this->_data[$k]); + break; + case 'gzip': + $this->_data[$k] = gzuncompress($this->_data[$k]); + break; + case 'enum': + $this->_data[$k] = $this->_table->enumValue($k, $this->_data[$k]); + break; + + } + } + + $this->_table->getRepository()->add($this); + + $this->cleanData($this->_data); + + $this->prepareIdentifiers($this->exists()); + + $this->postUnserialize($event); + } + + public function state($state = null) + { + if ($state == null) { + return $this->_state; + } + + $err = false; + if (is_integer($state)) { + if ($state >= 1 && $state <= 6) { + $this->_state = $state; + } else { + $err = true; + } + } else if (is_string($state)) { + $upper = strtoupper($state); + + $const = 'IPF_ORM_Record::STATE_' . $upper; + if (defined($const)) { + $this->_state = constant($const); + } else { + $err = true; + } + } + + if ($this->_state === IPF_ORM_Record::STATE_TCLEAN || + $this->_state === IPF_ORM_Record::STATE_CLEAN) { + $this->_modified = array(); + } + + if ($err) { + throw new IPF_ORM_Exception('Unknown record state ' . $state); + } + } + + public function refresh($deep = false) + { + $id = $this->identifier(); + if ( ! is_array($id)) { + $id = array($id); + } + if (empty($id)) { + return false; + } + $id = array_values($id); + + if ($deep) { + $query = $this->getTable()->createQuery(); + foreach (array_keys($this->_references) as $name) { + $query->leftJoin(get_class($this) . '.' . $name); + } + $query->where(implode(' = ? AND ', $this->getTable()->getIdentifierColumnNames()) . ' = ?'); + $this->clearRelated(); + $record = $query->fetchOne($id); + } else { + // Use FETCH_ARRAY to avoid clearing object relations + $record = $this->getTable()->find($id, IPF_ORM::HYDRATE_ARRAY); + if ($record) { + $this->hydrate($record); + } + } + + if ($record === false) { + throw new IPF_ORM_Exception('Failed to refresh. Record does not exist.'); + } + + $this->_modified = array(); + + $this->prepareIdentifiers(); + + $this->_state = IPF_ORM_Record::STATE_CLEAN; + + return $this; + } + + public function refreshRelated($name = null) + { + if (is_null($name)) { + foreach ($this->_table->getRelations() as $rel) { + $this->_references[$rel->getAlias()] = $rel->fetchRelatedFor($this); + } + } else { + $rel = $this->_table->getRelation($name); + $this->_references[$name] = $rel->fetchRelatedFor($this); + } + } + + public function clearRelated() + { + $this->_references = array(); + } + + public function getTable() + { + return $this->_table; + } + + public function getData() + { + return $this->_data; + } + + public function rawGet($fieldName) + { + if ( ! isset($this->_data[$fieldName])) { + throw new IPF_ORM_Exception('Unknown property '. $fieldName); + } + if ($this->_data[$fieldName] === self::$_null) { + return null; + } + + return $this->_data[$fieldName]; + } + + public function load() + { + // only load the data from database if the IPF_ORM_Record is in proxy state + if ($this->_state == IPF_ORM_Record::STATE_PROXY) { + $this->refresh(); + $this->_state = IPF_ORM_Record::STATE_CLEAN; + return true; + } + return false; + } + + public function get($fieldName, $load = true) + { + $value = self::$_null; + + if (isset($this->_data[$fieldName])) { + // check if the value is the IPF_ORM_Null object located in self::$_null) + if ($this->_data[$fieldName] === self::$_null && $load) { + $this->load(); + } + if ($this->_data[$fieldName] === self::$_null) { + $value = null; + } else { + $value = $this->_data[$fieldName]; + } + return $value; + } + + if (isset($this->_values[$fieldName])) { + return $this->_values[$fieldName]; + } + + try { + if ( ! isset($this->_references[$fieldName]) && $load) { + $rel = $this->_table->getRelation($fieldName); + $this->_references[$fieldName] = $rel->fetchRelatedFor($this); + } + return $this->_references[$fieldName]; + } catch (IPF_ORM_Exception_Table $e) { + foreach ($this->_table->getFilters() as $filter) { + if (($value = $filter->filterGet($this, $fieldName, $value)) !== null) { + return $value; + } + } + } + } + + public function mapValue($name, $value) + { + $this->_values[$name] = $value; + } + + public function set($fieldName, $value, $load = true) + { + if (isset($this->_data[$fieldName])) { + $type = $this->_table->getTypeOf($fieldName); + if ($value instanceof IPF_ORM_Record) { + $id = $value->getIncremented(); + + if ($id !== null && $type !== 'object') { + $value = $id; + } + } + + if ($load) { + $old = $this->get($fieldName, $load); + } else { + $old = $this->_data[$fieldName]; + } + + if ($this->_isValueModified($type, $old, $value)) { + if ($value === null) { + $value = self::$_null; + } + + $this->_data[$fieldName] = $value; + $this->_modified[] = $fieldName; + switch ($this->_state) { + case IPF_ORM_Record::STATE_CLEAN: + $this->_state = IPF_ORM_Record::STATE_DIRTY; + break; + case IPF_ORM_Record::STATE_TCLEAN: + $this->_state = IPF_ORM_Record::STATE_TDIRTY; + break; + } + } + } else { + try { + $this->coreSetRelated($fieldName, $value); + } catch (IPF_ORM_Exception_Table $e) { + foreach ($this->_table->getFilters() as $filter) { + if (($value = $filter->filterSet($this, $fieldName, $value)) !== null) { + break; + } + } + } + } + + return $this; + } + + protected function _isValueModified($type, $old, $new) + { + if ($type == 'boolean' && (is_bool($old) || is_numeric($old)) && (is_bool($new) || is_numeric($new)) && $old == $new) { + return false; + } else { + return $old !== $new; + } + } + + public function coreSetRelated($name, $value) + { + $rel = $this->_table->getRelation($name); + + if ($value === null) { + $value = self::$_null; + } + + // one-to-many or one-to-one relation + if ($rel instanceof IPF_ORM_Relation_ForeignKey || $rel instanceof IPF_ORM_Relation_LocalKey) { + if ( ! $rel->isOneToOne()) { + // one-to-many relation found + if ( ! ($value instanceof IPF_ORM_Collection)) { + throw new IPF_ORM_Exception("Couldn't call IPF_ORM::set(), second argument should be an instance of IPF_ORM_Collection when setting one-to-many references."); + } + if (isset($this->_references[$name])) { + $this->_references[$name]->setData($value->getData()); + return $this; + } + } else { + if ($value !== self::$_null) { + $relatedTable = $value->getTable(); + $foreignFieldName = $relatedTable->getFieldName($rel->getForeign()); + $localFieldName = $this->_table->getFieldName($rel->getLocal()); + + // one-to-one relation found + if ( ! ($value instanceof IPF_ORM_Record)) { + throw new IPF_ORM_Exception("Couldn't call IPF_ORM::set(), second argument should be an instance of IPF_ORM_Record or IPF_ORM_Null when setting one-to-one references."); + } + if ($rel instanceof IPF_ORM_Relation_LocalKey) { + if ( ! empty($foreignFieldName) && $foreignFieldName != $value->getTable()->getIdentifier()) { + $this->set($localFieldName, $value->rawGet($foreignFieldName), false); + } else { + $this->set($localFieldName, $value, false); + } + } else { + $value->set($foreignFieldName, $this, false); + } + } + } + + } else if ($rel instanceof IPF_ORM_Relation_Association) { + // join table relation found + if ( ! ($value instanceof IPF_ORM_Collection)) { + throw new IPF_ORM_Exception("Couldn't call IPF_ORM::set(), second argument should be an instance of IPF_ORM_Collection when setting many-to-many references."); + } + } + + $this->_references[$name] = $value; + } + + public function contains($fieldName) + { + if (isset($this->_data[$fieldName])) { + // this also returns true if the field is a IPF_ORM_Null. + // imho this is not correct behavior. + return true; + } + if (isset($this->_id[$fieldName])) { + return true; + } + if (isset($this->_values[$fieldName])) { + return true; + } + if (isset($this->_references[$fieldName]) && + $this->_references[$fieldName] !== self::$_null) { + + return true; + } + return false; + } + + public function __unset($name) + { + if (isset($this->_data[$name])) { + $this->_data[$name] = array(); + } else if (isset($this->_references[$name])) { + if ($this->_references[$name] instanceof IPF_ORM_Record) { + $this->_pendingDeletes[] = $this->$name; + $this->_references[$name] = self::$_null; + } elseif ($this->_references[$name] instanceof IPF_ORM_Collection) { + $this->_pendingDeletes[] = $this->$name; + $this->_references[$name]->setData(array()); + } + } + } + + public function getPendingDeletes() + { + return $this->_pendingDeletes; + } + + public function save(IPF_ORM_Connection $conn = null) + { + if ($conn === null) { + $conn = $this->_table->getConnection(); + } + $conn->unitOfWork->saveGraph($this); + } + + public function trySave(IPF_ORM_Connection $conn = null) { + try { + $this->save($conn); + return true; + } catch (IPF_ORM_Exception_Validator $ignored) { + return false; + } + } + + public function replace(IPF_ORM_Connection $conn = null) + { + if ($conn === null) { + $conn = $this->_table->getConnection(); + } + + if ($this->exists()) { + return $this->save(); + } else { + $identifier = (array) $this->getTable()->getIdentifier(); + return $conn->replace($this->_table, $this->toArray(), $identifier); + } + } + + public function getModified() + { + $a = array(); + + foreach ($this->_modified as $k => $v) { + $a[$v] = $this->_data[$v]; + } + return $a; + } + + public function modifiedFields() + { + $a = array(); + + foreach ($this->_modified as $k => $v) { + $a[$v] = $this->_data[$v]; + } + return $a; + } + + public function getPrepared(array $array = array()) + { + $a = array(); + + if (empty($array)) { + $modifiedFields = $this->_modified; + } + + foreach ($modifiedFields as $field) { + $type = $this->_table->getTypeOf($field); + + if ($this->_data[$field] === self::$_null) { + $a[$field] = null; + continue; + } + + switch ($type) { + case 'array': + case 'object': + $a[$field] = serialize($this->_data[$field]); + break; + case 'gzip': + $a[$field] = gzcompress($this->_data[$field],5); + break; + case 'boolean': + $a[$field] = $this->getTable()->getConnection()->convertBooleans($this->_data[$field]); + break; + case 'enum': + $a[$field] = $this->_table->enumIndex($field, $this->_data[$field]); + break; + default: + if ($this->_data[$field] instanceof IPF_ORM_Record) { + $a[$field] = $this->_data[$field]->getIncremented(); + if ($a[$field] !== null) { + $this->_data[$field] = $a[$field]; + } + } else { + $a[$field] = $this->_data[$field]; + } + /** TODO: + if ($this->_data[$v] === null) { + throw new IPF_ORM_Record_Exception('Unexpected null value.'); + } + */ + } + } + $map = $this->_table->inheritanceMap; + foreach ($map as $k => $v) { + $k = $this->_table->getFieldName($k); + $old = $this->get($k, false); + + if ((string) $old !== (string) $v || $old === null) { + $a[$k] = $v; + $this->_data[$k] = $v; + } + } + + return $a; + } + + public function count() + { + return count($this->_data); + } + + public function columnCount() + { + return $this->count(); + } + + public function toArray($deep = true, $prefixKey = false) + { + if ($this->_state == self::STATE_LOCKED) { + return false; + } + + $stateBeforeLock = $this->_state; + $this->_state = self::STATE_LOCKED; + + $a = array(); + + foreach ($this as $column => $value) { + if ($value === self::$_null || is_object($value)) { + $value = null; + } + + $a[$column] = $value; + } + + if ($this->_table->getIdentifierType() == IPF_ORM::IDENTIFIER_AUTOINC) { + $i = $this->_table->getIdentifier(); + $a[$i] = $this->getIncremented(); + } + + if ($deep) { + foreach ($this->_references as $key => $relation) { + if (! $relation instanceof IPF_ORM_Null) { + $a[$key] = $relation->toArray($deep, $prefixKey); + } + } + } + + // [FIX] Prevent mapped IPF_ORM_Records from being displayed fully + foreach ($this->_values as $key => $value) { + if ($value instanceof IPF_ORM_Record) { + $a[$key] = $value->toArray($deep, $prefixKey); + } else { + $a[$key] = $value; + } + } + + $this->_state = $stateBeforeLock; + + return $a; + } + + public function merge($data, $deep = true) + { + if ($data instanceof $this) { + $array = $data->toArray($deep); + } else if (is_array($data)) { + $array = $data; + } + + return $this->fromArray($array, $deep); + } + + public function fromArray(array $array, $deep = true) + { + $refresh = false; + foreach ($array as $key => $value) { + if ($key == '_identifier') { + $refresh = true; + $this->assignIdentifier((array) $value); + continue; + } + + if ($deep && $this->getTable()->hasRelation($key)) { + $this->$key->fromArray($value, $deep); + } else if ($this->getTable()->hasField($key)) { + $this->set($key, $value); + } + } + + if ($refresh) { + $this->refresh(); + } + } + + public function synchronizeWithArray(array $array, $deep = true) + { + $refresh = false; + foreach ($array as $key => $value) { + if ($key == '_identifier') { + $refresh = true; + $this->assignIdentifier((array) $value); + continue; + } + + if ($deep && $this->getTable()->hasRelation($key)) { + $this->get($key)->synchronizeWithArray($value); + } else if ($this->getTable()->hasField($key)) { + $this->set($key, $value); + } + } + + // eliminate relationships missing in the $array + foreach ($this->_references as $name => $obj) { + if ( ! isset($array[$name])) { + unset($this->$name); + } + } + + if ($refresh) { + $this->refresh(); + } + } + + public function exportTo($type, $deep = true) + { + if ($type == 'array') { + return $this->toArray($deep); + } else { + return IPF_ORM_Parser::dump($this->toArray($deep, true), $type); + } + } + + public function importFrom($type, $data) + { + if ($type == 'array') { + return $this->fromArray($data); + } else { + return $this->fromArray(IPF_ORM_Parser::load($data, $type)); + } + } + + public function exists() + { + return ($this->_state !== IPF_ORM_Record::STATE_TCLEAN && + $this->_state !== IPF_ORM_Record::STATE_TDIRTY); + } + + public function isModified() + { + return ($this->_state === IPF_ORM_Record::STATE_DIRTY || + $this->_state === IPF_ORM_Record::STATE_TDIRTY); + } + + public function hasRelation($fieldName) + { + if (isset($this->_data[$fieldName]) || isset($this->_id[$fieldName])) { + return true; + } + return $this->_table->hasRelation($fieldName); + } + + public function getIterator() + { + return new IPF_ORM_Record_Iterator($this); + } + + public function delete(IPF_ORM_Connection $conn = null) + { + if ($conn == null) { + $conn = $this->_table->getConnection(); + } + return $conn->unitOfWork->delete($this); + } + + public function copy($deep = false) + { + $data = $this->_data; + + if ($this->_table->getIdentifierType() === IPF_ORM::IDENTIFIER_AUTOINC) { + $id = $this->_table->getIdentifier(); + + unset($data[$id]); + } + + $ret = $this->_table->create($data); + $modified = array(); + + foreach ($data as $key => $val) { + if ( ! ($val instanceof IPF_ORM_Null)) { + $ret->_modified[] = $key; + } + } + + if ($deep) { + foreach ($this->_references as $key => $value) { + if ($value instanceof IPF_ORM_Collection) { + foreach ($value as $record) { + $ret->{$key}[] = $record->copy($deep); + } + } else if($value instanceof IPF_ORM_Record) { + $ret->set($key, $value->copy($deep)); + } + } + } + + return $ret; + } + + public function assignIdentifier($id = false) + { + if ($id === false) { + $this->_id = array(); + $this->_data = $this->cleanData($this->_data); + $this->_state = IPF_ORM_Record::STATE_TCLEAN; + $this->_modified = array(); + } elseif ($id === true) { + $this->prepareIdentifiers(true); + $this->_state = IPF_ORM_Record::STATE_CLEAN; + $this->_modified = array(); + } else { + if (is_array($id)) { + foreach ($id as $fieldName => $value) { + $this->_id[$fieldName] = $value; + $this->_data[$fieldName] = $value; + } + } else { + $name = $this->_table->getIdentifier(); + $this->_id[$name] = $id; + $this->_data[$name] = $id; + } + $this->_state = IPF_ORM_Record::STATE_CLEAN; + $this->_modified = array(); + } + } + + public function identifier() + { + return $this->_id; + } + + final public function getIncremented() + { + $id = current($this->_id); + if ($id === false) { + return null; + } + + return $id; + } + + public function getLast() + { + return $this; + } + + public function hasReference($name) + { + return isset($this->_references[$name]); + } + + public function reference($name) + { + if (isset($this->_references[$name])) { + return $this->_references[$name]; + } + } + + public function obtainReference($name) + { + if (isset($this->_references[$name])) { + return $this->_references[$name]; + } + throw new IPF_ORM_Exception("Unknown reference $name"); + } + + public function getReferences() + { + return $this->_references; + } + + final public function setRelated($alias, IPF_ORM_Access $coll) + { + $this->_references[$alias] = $coll; + } + + public function loadReference($name) + { + $rel = $this->_table->getRelation($name); + $this->_references[$name] = $rel->fetchRelatedFor($this); + } + + public function call($callback, $column) + { + $args = func_get_args(); + array_shift($args); + + if (isset($args[0])) { + $fieldName = $args[0]; + $args[0] = $this->get($fieldName); + + $newvalue = call_user_func_array($callback, $args); + + $this->_data[$fieldName] = $newvalue; + } + return $this; + } + + public function getNode() + { + if ( ! $this->_table->isTree()) { + return false; + } + + if ( ! isset($this->_node)) { + $this->_node = IPF_ORM_Node::factory($this, + $this->getTable()->getOption('treeImpl'), + $this->getTable()->getOption('treeOptions') + ); + } + + return $this->_node; + } + + public function unshiftFilter(IPF_ORM_Record_Filter $filter) + { + return $this->_table->unshiftFilter($filter); + } + + public function unlink($alias, $ids = array()) + { + $ids = (array) $ids; + + $q = new IPF_ORM_Query(); + + $rel = $this->getTable()->getRelation($alias); + + if ($rel instanceof IPF_ORM_Relation_Association) { + $q->delete() + ->from($rel->getAssociationTable()->getComponentName()) + ->where($rel->getLocal() . ' = ?', array_values($this->identifier())); + + if (count($ids) > 0) { + $q->whereIn($rel->getForeign(), $ids); + } + + $q->execute(); + + } else if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + $q->update($rel->getTable()->getComponentName()) + ->set($rel->getForeign(), '?', array(null)) + ->addWhere($rel->getForeign() . ' = ?', array_values($this->identifier())); + + if (count($ids) > 0) { + $q->whereIn($rel->getTable()->getIdentifier(), $ids); + } + + $q->execute(); + } + if (isset($this->_references[$alias])) { + foreach ($this->_references[$alias] as $k => $record) { + if (in_array(current($record->identifier()), $ids)) { + $this->_references[$alias]->remove($k); + } + } + $this->_references[$alias]->takeSnapshot(); + } + return $this; + } + + public function link($alias, $ids) + { + $ids = (array) $ids; + + if ( ! count($ids)) { + return $this; + } + + $identifier = array_values($this->identifier()); + $identifier = array_shift($identifier); + + $rel = $this->getTable()->getRelation($alias); + + if ($rel instanceof IPF_ORM_Relation_Association) { + + $modelClassName = $rel->getAssociationTable()->getComponentName(); + $localFieldName = $rel->getLocalFieldName(); + $localFieldDef = $rel->getAssociationTable()->getColumnDefinition($localFieldName); + if ($localFieldDef['type'] == 'integer') { + $identifier = (integer) $identifier; + } + $foreignFieldName = $rel->getForeignFieldName(); + $foreignFieldDef = $rel->getAssociationTable()->getColumnDefinition($foreignFieldName); + if ($foreignFieldDef['type'] == 'integer') { + for ($i = 0; $i < count($ids); $i++) { + $ids[$i] = (integer) $ids[$i]; + } + } + foreach ($ids as $id) { + $record = new $modelClassName; + $record[$localFieldName] = $identifier; + $record[$foreignFieldName] = $id; + $record->save(); + } + + } else if ($rel instanceof IPF_ORM_Relation_ForeignKey) { + + $q = new IPF_ORM_Query(); + + $q->update($rel->getTable()->getComponentName()) + ->set($rel->getForeign(), '?', array_values($this->identifier())); + + if (count($ids) > 0) { + $q->whereIn($rel->getTable()->getIdentifier(), $ids); + } + + $q->execute(); + + } else if ($rel instanceof IPF_ORM_Relation_LocalKey) { + + $q = new IPF_ORM_Query(); + + $q->update($this->getTable()->getComponentName()) + ->set($rel->getLocalFieldName(), '?', $ids); + + if (count($ids) > 0) { + $q->whereIn($rel->getTable()->getIdentifier(), array_values($this->identifier())); + } + + $q->execute(); + + } + + return $this; + } + + public function __call($method, $args) + { + if (($template = $this->_table->getMethodOwner($method)) !== false) { + $template->setInvoker($this); + return call_user_func_array(array($template, $method), $args); + } + + foreach ($this->_table->getTemplates() as $template) { + if (method_exists($template, $method)) { + $template->setInvoker($this); + $this->_table->setMethodOwner($method, $template); + + return call_user_func_array(array($template, $method), $args); + } + } + + throw new IPF_ORM_Exception(sprintf('Unknown method %s::%s', get_class($this), $method)); + } + + public function deleteNode() + { + $this->getNode()->delete(); + } + + public function free($deep = false) + { + if ($this->_state != self::STATE_LOCKED) { + $this->_state = self::STATE_LOCKED; + + $this->_table->getRepository()->evict($this->_oid); + $this->_table->removeRecord($this); + $this->_data = array(); + $this->_id = array(); + + if ($deep) { + foreach ($this->_references as $name => $reference) { + if ( ! ($reference instanceof IPF_ORM_Null)) { + $reference->free($deep); + } + } + } + + $this->_references = array(); + } + } + + public function toString() + { + return IPF_ORM::dump(get_object_vars($this)); + } + + public function __toString() + { + return (string) $this->_oid; + } + + public function ModelAdmin(){ + $cn = get_class($this); + if (isset(IPF_Admin_Model::$models[$cn])) + return IPF_Admin_Model::$models[$cn]; + return null; + } + + function SetFromFormData($cleaned_values) + { + foreach ($cleaned_values as $key=>$val) { + $this->$key = $val; + } + } +} \ No newline at end of file diff --git a/ipf/orm/record/abstract.php b/ipf/orm/record/abstract.php new file mode 100644 index 0000000..ffbe2c2 --- /dev/null +++ b/ipf/orm/record/abstract.php @@ -0,0 +1,192 @@ +_table; + } + + public function addListener($listener, $name = null) + { + $this->_table->addRecordListener($listener, $name = null); + return $this; + } + + public function getListener() + { + return $this->_table->getRecordListener(); + } + + public function setListener($listener) + { + $this->_table->setRecordListener($listener); + return $this; + } + + public function index($name, array $definition = array()) + { + if ( ! $definition) { + return $this->_table->getIndex($name); + } else { + return $this->_table->addIndex($name, $definition); + } + } + public function setAttribute($attr, $value) + { + $this->_table->setAttribute($attr, $value); + } + public function setTableName($tableName) + { + $this->_table->setTableName($tableName); + } + public function setInheritanceMap($map) + { + $this->_table->setOption('inheritanceMap', $map); + } + + public function setSubclasses($map) + { + if (isset($map[get_class($this)])) { + $this->_table->setOption('inheritanceMap', $map[get_class($this)]); + return; + } + $this->_table->setOption('subclasses', array_keys($map)); + } + + public function attribute($attr, $value) + { + if ($value == null) { + if (is_array($attr)) { + foreach ($attr as $k => $v) { + $this->_table->setAttribute($k, $v); + } + } else { + return $this->_table->getAttribute($attr); + } + } else { + $this->_table->setAttribute($attr, $value); + } + } + + public function option($name, $value = null) + { + if ($value === null) { + if (is_array($name)) { + foreach ($name as $k => $v) { + $this->_table->setOption($k, $v); + } + } else { + return $this->_table->getOption($name); + } + } else { + $this->_table->setOption($name, $value); + } + } + + public function ownsOne() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::ONE_COMPOSITE); + + return $this; + } + + public function ownsMany() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::MANY_COMPOSITE); + return $this; + } + + public function hasOne() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::ONE_AGGREGATE); + return $this; + } + + public function hasMany() + { + $this->_table->bind(func_get_args(), IPF_ORM_Relation::MANY_AGGREGATE); + return $this; + } + + public function hasColumn($name, $type, $length = 2147483647, $options = "") + { + $this->_table->setColumn($name, $type, $length, $options); + } + + public function hasColumns(array $definitions) + { + foreach ($definitions as $name => $options) { + $this->hasColumn($name, $options['type'], $options['length'], $options); + } + } + + public function loadTemplate($template, array $options = array()) + { + $this->actAs($template, $options); + } + + public function bindQueryParts(array $queryParts) + { + $this->_table->bindQueryParts($queryParts); + return $this; + } + + public function loadGenerator(IPF_ORM_Record_Generator $generator) + { + $generator->initialize($this->_table); + $this->_table->addGenerator($generator, get_class($generator)); + } + + public function actAs($tpl, array $options = array()) + { + if ( ! is_object($tpl)) { + $className = 'IPF_ORM_Template_' . $tpl; + + if (class_exists($className, true)) { + $tpl = new $className($options); + } else if (class_exists($tpl, true)) { + $tpl = new $tpl($options); + } else { + throw new IPF_ORM_Record_Exception('Could not load behavior named: "' . $tpl . '"'); + } + } + + if ( ! ($tpl instanceof IPF_ORM_Template)) { + throw new IPF_ORM_Record_Exception('Loaded behavior class is not an istance of IPF_ORM_Template.'); + } + + $className = get_class($tpl); + + $this->_table->addTemplate($className, $tpl); + + $tpl->setTable($this->_table); + $tpl->setUp(); + $tpl->setTableDefinition(); + + return $this; + } + + public function check($constraint, $name = null) + { + if (is_array($constraint)) { + foreach ($constraint as $name => $def) { + $this->_table->addCheckConstraint($def, $name); + } + } else { + $this->_table->addCheckConstraint($constraint, $name); + } + return $this; + } +} diff --git a/ipf/orm/record/filter.php b/ipf/orm/record/filter.php new file mode 100644 index 0000000..ee65088 --- /dev/null +++ b/ipf/orm/record/filter.php @@ -0,0 +1,19 @@ +_table = $table; + } + + public function getTable() + { + return $this->_table; + } + + abstract public function filterSet(IPF_ORM_Record $record, $name, $value); + abstract public function filterGet(IPF_ORM_Record $record, $name); +} \ No newline at end of file diff --git a/ipf/orm/record/filter/compound.php b/ipf/orm/record/filter/compound.php new file mode 100644 index 0000000..289136d --- /dev/null +++ b/ipf/orm/record/filter/compound.php @@ -0,0 +1,58 @@ +_aliases = $aliases; + } + public function init() + { + // check that all aliases exist + foreach ($this->_aliases as $alias) { + $this->_table->getRelation($alias); + } + } + + public function filterSet(IPF_ORM_Record $record, $name, $value) + { + foreach ($this->_aliases as $alias) { + if ( ! $record->exists()) { + if (isset($record[$alias][$name])) { + $record[$alias][$name] = $value; + + return $record; + } + } else { + // we do not want to execute N + 1 queries here, hence we cannot use get() + if (($ref = $record->reference($alias)) !== null) { + if (isset($ref[$name])) { + $ref[$name] = $value; + } + + return $record; + } + } + } + } + + public function filterGet(IPF_ORM_Record $record, $name) + { + foreach ($this->_aliases as $alias) { + if ( ! $record->exists()) { + if (isset($record[$alias][$name])) { + return $record[$alias][$name]; + } + } else { + // we do not want to execute N + 1 queries here, hence we cannot use get() + if (($ref = $record->reference($alias)) !== null) { + if (isset($ref[$name])) { + return $ref[$name]; + } + } + } + } + } +} \ No newline at end of file diff --git a/ipf/orm/record/filter/standard.php b/ipf/orm/record/filter/standard.php new file mode 100644 index 0000000..d76c5c6 --- /dev/null +++ b/ipf/orm/record/filter/standard.php @@ -0,0 +1,14 @@ +record = $record; + parent::__construct($record->getData()); + } + + public static function initNullObject(IPF_ORM_Null $null) + { + self::$null = $null; + } + + public function current() + { + $value = parent::current(); + + if ($value === self::$null) { + return null; + } else { + return $value; + } + } +} \ No newline at end of file diff --git a/ipf/orm/record/listener.php b/ipf/orm/record/listener.php new file mode 100644 index 0000000..03a31e7 --- /dev/null +++ b/ipf/orm/record/listener.php @@ -0,0 +1,22 @@ +_listeners[] = $listener; + } else { + $this->_listeners[$name] = $listener; + } + } + + public function get($key) + { + if ( ! isset($this->_listeners[$key])) { + return null; + } + return $this->_listeners[$key]; + } + + public function set($key, $listener) + { + $this->_listeners[$key] = $listener; + } + + public function preSerialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preSerialize($event); + } + } + + public function postSerialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preSerialize($event); + } + } + + public function preUnserialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preUnserialize($event); + } + } + + public function postUnserialize(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postUnserialize($event); + } + } + + public function preDqlSelect(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDqlSelect($event); + } + } + + public function preSave(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preSave($event); + } + } + + public function postSave(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postSave($event); + } + } + + public function preDqlDelete(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDqlDelete($event); + } + } + + public function preDelete(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDelete($event); + } + } + + public function postDelete(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postDelete($event); + } + } + + public function preDqlUpdate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preDqlUpdate($event); + } + } + + public function preUpdate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preUpdate($event); + } + } + + public function postUpdate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postUpdate($event); + } + } + + public function preInsert(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preInsert($event); + } + } + + public function postInsert(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postInsert($event); + } + } + + public function preHydrate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->preHydrate($event); + } + } + + public function postHydrate(IPF_ORM_Event $event) + { + foreach ($this->_listeners as $listener) { + $listener->postHydrate($event); + } + } +} diff --git a/ipf/orm/record/listener/interface.php b/ipf/orm/record/listener/interface.php new file mode 100644 index 0000000..d3e98aa --- /dev/null +++ b/ipf/orm/record/listener/interface.php @@ -0,0 +1,19 @@ + true, + 'foreign' => true, + 'local' => true, + 'class' => true, + 'type' => true, + 'table' => true, + 'localTable' => true, + 'name' => null, + 'refTable' => null, + 'onDelete' => null, + 'onUpdate' => null, + 'deferred' => null, + 'deferrable' => null, + 'constraint' => null, + 'equal' => false, + 'cascade' => array(), // application-level cascades + 'owningSide' => false, // whether this is the owning side + ); + + public function __construct(array $definition) + { + $def = array(); + foreach ($this->definition as $key => $val) { + if ( ! isset($definition[$key]) && $val) { + throw new IPF_ORM_Exception($key . ' is required!'); + } + if (isset($definition[$key])) { + $def[$key] = $definition[$key]; + } else { + $def[$key] = $this->definition[$key]; + } + } + $this->definition = $def; + } + + public function hasConstraint() + { + return ($this->definition['constraint'] || + ($this->definition['onUpdate']) || + ($this->definition['onDelete'])); + } + public function isDeferred() + { + return $this->definition['deferred']; + } + + public function isDeferrable() + { + return $this->definition['deferrable']; + } + public function isEqual() + { + return $this->definition['equal']; + } + + public function offsetExists($offset) + { + return isset($this->definition[$offset]); + } + + public function offsetGet($offset) + { + if (isset($this->definition[$offset])) { + return $this->definition[$offset]; + } + + return null; + } + + public function offsetSet($offset, $value) + { + if (isset($this->definition[$offset])) { + $this->definition[$offset] = $value; + } + } + + public function offsetUnset($offset) + { + $this->definition[$offset] = false; + } + + public function toArray() + { + return $this->definition; + } + + final public function getAlias() + { + return $this->definition['alias']; + } + + final public function getType() + { + return $this->definition['type']; + } + + public function isCascadeDelete() + { + return in_array('delete', $this->definition['cascade']); + } + + final public function getTable() + { + return IPF_ORM_Manager::getInstance() + ->getConnectionForComponent($this->definition['class']) + ->getTable($this->definition['class']); + } + + final public function getClass() + { + return $this->definition['class']; + } + + final public function getLocal() + { + return $this->definition['local']; + } + + final public function getLocalFieldName() + { + return $this->definition['localTable']->getFieldName($this->definition['local']); + } + + final public function getForeign() + { + return $this->definition['foreign']; + } + + final public function getForeignFieldName() + { + return $this->definition['table']->getFieldName($this->definition['foreign']); + } + + final public function isComposite() + { + return ($this->definition['type'] == IPF_ORM_Relation::ONE_COMPOSITE || + $this->definition['type'] == IPF_ORM_Relation::MANY_COMPOSITE); + } + + final public function isOneToOne() + { + return ($this->definition['type'] == IPF_ORM_Relation::ONE_AGGREGATE || + $this->definition['type'] == IPF_ORM_Relation::ONE_COMPOSITE); + } + + public function getRelationDql($count) + { + $component = $this->getTable()->getComponentName(); + + $dql = 'FROM ' . $component + . ' WHERE ' . $component . '.' . $this->definition['foreign'] + . ' IN (' . substr(str_repeat('?, ', $count), 0, -2) . ')'; + + return $dql; + } + + abstract public function fetchRelatedFor(IPF_ORM_Record $record); + + public function __toString() + { + $r[] = "
";
+ foreach ($this->definition as $k => $v) {
+ if (is_object($v)) {
+ $v = 'Object(' . get_class($v) . ')';
+ }
+ $r[] = $k . ' : ' . $v;
+ }
+ $r[] = "";
+ return implode("\n", $r);
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/relation/association.php b/ipf/orm/relation/association.php
new file mode 100644
index 0000000..4a1958d
--- /dev/null
+++ b/ipf/orm/relation/association.php
@@ -0,0 +1,48 @@
+definition['refTable'];
+ }
+ public function getAssociationTable()
+ {
+ return $this->definition['refTable'];
+ }
+
+ public function getRelationDql($count, $context = 'record')
+ {
+ $table = $this->definition['refTable'];
+ $component = $this->definition['refTable']->getComponentName();
+
+ switch ($context) {
+ case "record":
+ $sub = substr(str_repeat("?, ", $count),0,-2);
+ $dql = 'FROM ' . $this->getTable()->getComponentName();
+ $dql .= '.' . $component;
+ $dql .= ' WHERE ' . $this->getTable()->getComponentName()
+ . '.' . $component . '.' . $this->definition['local'] . ' IN (' . $sub . ')';
+ break;
+ case "collection":
+ $sub = substr(str_repeat("?, ", $count),0,-2);
+ $dql = 'FROM ' . $component . '.' . $this->getTable()->getComponentName();
+ $dql .= ' WHERE ' . $component . '.' . $this->definition['local'] . ' IN (' . $sub . ')';
+ break;
+ }
+
+ return $dql;
+ }
+
+ public function fetchRelatedFor(IPF_ORM_Record $record)
+ {
+ $id = $record->getIncremented();
+ if (empty($id) || ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) {
+ $coll = new IPF_ORM_Collection($this->getTable());
+ } else {
+ $coll = $this->getTable()->getConnection()->query($this->getRelationDql(1), array($id));
+ }
+ $coll->setReference($record, $this);
+ return $coll;
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/relation/foreignkey.php b/ipf/orm/relation/foreignkey.php
new file mode 100644
index 0000000..8830a4b
--- /dev/null
+++ b/ipf/orm/relation/foreignkey.php
@@ -0,0 +1,56 @@
+getTable();
+ foreach ((array) $this->definition['local'] as $local) {
+ $value = $record->get($localTable->getFieldName($local));
+ if (isset($value)) {
+ $id[] = $value;
+ }
+ }
+ if ($this->isOneToOne()) {
+ if ( ! $record->exists() || empty($id) ||
+ ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) {
+
+ $related = $this->getTable()->create();
+ } else {
+ $dql = 'FROM ' . $this->getTable()->getComponentName()
+ . ' WHERE ' . $this->getCondition();
+
+ $coll = $this->getTable()->getConnection()->query($dql, $id);
+ $related = $coll[0];
+ }
+
+ $related->set($related->getTable()->getFieldName($this->definition['foreign']),
+ $record, false);
+ } else {
+
+ if ( ! $record->exists() || empty($id) ||
+ ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) {
+
+ $related = new IPF_ORM_Collection($this->getTable());
+ } else {
+ $query = $this->getRelationDql(1);
+ $related = $this->getTable()->getConnection()->query($query, $id);
+ }
+ $related->setReference($record, $this);
+ }
+ return $related;
+ }
+
+ public function getCondition($alias = null)
+ {
+ if ( ! $alias) {
+ $alias = $this->getTable()->getComponentName();
+ }
+ $conditions = array();
+ foreach ((array) $this->definition['foreign'] as $foreign) {
+ $conditions[] = $alias . '.' . $foreign . ' = ?';
+ }
+ return implode(' AND ', $conditions);
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/relation/localkey.php b/ipf/orm/relation/localkey.php
new file mode 100644
index 0000000..ebe8e66
--- /dev/null
+++ b/ipf/orm/relation/localkey.php
@@ -0,0 +1,38 @@
+getTable()->getFieldName($this->definition['local']);
+ $id = $record->get($localFieldName);
+
+ if (is_null($id) || ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) {
+ $related = $this->getTable()->create();
+ } else {
+ $dql = 'FROM ' . $this->getTable()->getComponentName()
+ . ' WHERE ' . $this->getCondition();
+
+ $related = $this->getTable()
+ ->getConnection()
+ ->query($dql, array($id))
+ ->getFirst();
+
+ if ( ! $related || empty($related)) {
+ $related = $this->getTable()->create();
+ }
+ }
+
+ $record->set($localFieldName, $related, false);
+
+ return $related;
+ }
+
+ public function getCondition($alias = null)
+ {
+ if ( ! $alias) {
+ $alias = $this->getTable()->getComponentName();
+ }
+ return $alias . '.' . $this->definition['foreign'] . ' = ?';
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/relation/nest.php b/ipf/orm/relation/nest.php
new file mode 100644
index 0000000..f23f90a
--- /dev/null
+++ b/ipf/orm/relation/nest.php
@@ -0,0 +1,84 @@
+definition['table']->getIdentifierColumnNames();
+ $identifier = array_pop($identifierColumnNames);
+ $sub = 'SELECT '.$this->definition['foreign']
+ . ' FROM '.$this->definition['refTable']->getTableName()
+ . ' WHERE '.$this->definition['local']
+ . ' = ?';
+
+ $sub2 = 'SELECT '.$this->definition['local']
+ . ' FROM '.$this->definition['refTable']->getTableName()
+ . ' WHERE '.$this->definition['foreign']
+ . ' = ?';
+
+ $dql = 'FROM ' . $this->definition['table']->getComponentName()
+ . '.' . $this->definition['refTable']->getComponentName()
+ . ' WHERE ' . $this->definition['table']->getComponentName()
+ . '.' . $identifier
+ . ' IN (' . $sub . ')'
+ . ' || ' . $this->definition['table']->getComponentName()
+ . '.' . $identifier
+ . ' IN (' . $sub2 . ')';
+ break;
+ case 'collection':
+ $sub = substr(str_repeat('?, ', $count),0,-2);
+ $dql = 'FROM '.$this->definition['refTable']->getComponentName()
+ . '.' . $this->definition['table']->getComponentName()
+ . ' WHERE '.$this->definition['refTable']->getComponentName()
+ . '.' . $this->definition['local'] . ' IN (' . $sub . ')';
+ };
+
+ return $dql;
+ }
+
+ public function fetchRelatedFor(IPF_ORM_Record $record)
+ {
+ $id = $record->getIncremented();
+
+
+ if (empty($id) || ! $this->definition['table']->getAttribute(IPF_ORM::ATTR_LOAD_REFERENCES)) {
+ return new IPF_ORM_Collection($this->getTable());
+ } else {
+ $q = new IPF_ORM_RawSql($this->getTable()->getConnection());
+
+ $assocTable = $this->getAssociationFactory()->getTableName();
+ $tableName = $record->getTable()->getTableName();
+ $identifierColumnNames = $record->getTable()->getIdentifierColumnNames();
+ $identifier = array_pop($identifierColumnNames);
+
+ $sub = 'SELECT ' . $this->getForeign()
+ . ' FROM ' . $assocTable
+ . ' WHERE ' . $this->getLocal()
+ . ' = ?';
+
+ $condition[] = $tableName . '.' . $identifier . ' IN (' . $sub . ')';
+ $joinCondition[] = $tableName . '.' . $identifier . ' = ' . $assocTable . '.' . $this->getForeign();
+
+ if ($this->definition['equal']) {
+ $sub2 = 'SELECT ' . $this->getLocal()
+ . ' FROM ' . $assocTable
+ . ' WHERE ' . $this->getForeign()
+ . ' = ?';
+
+ $condition[] = $tableName . '.' . $identifier . ' IN (' . $sub2 . ')';
+ $joinCondition[] = $tableName . '.' . $identifier . ' = ' . $assocTable . '.' . $this->getLocal();
+ }
+ $q->select('{'.$tableName.'.*}, {'.$assocTable.'.*}')
+ ->from($tableName . ' INNER JOIN ' . $assocTable . ' ON ' . implode(' OR ', $joinCondition))
+ ->where(implode(' OR ', $condition));
+ $q->addComponent($tableName, $record->getTable()->getComponentName());
+ $q->addComponent($assocTable, $record->getTable()->getComponentName(). '.' . $this->getAssociationFactory()->getComponentName());
+
+ $params = ($this->definition['equal']) ? array($id, $id) : array($id);
+
+ return $q->execute($params);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/relation/parser.php b/ipf/orm/relation/parser.php
new file mode 100644
index 0000000..e15228e
--- /dev/null
+++ b/ipf/orm/relation/parser.php
@@ -0,0 +1,390 @@
+_table = $table;
+ }
+
+ public function getTable()
+ {
+ return $this->_table;
+ }
+
+ public function getPendingRelation($name)
+ {
+ if ( ! isset($this->_pending[$name])) {
+ throw new IPF_ORM_Exception('Unknown pending relation ' . $name);
+ }
+
+ return $this->_pending[$name];
+ }
+
+ public function getPendingRelations()
+ {
+ return $this->_pending;
+ }
+
+ public function unsetPendingRelations($name)
+ {
+ unset($this->_pending[$name]);
+ }
+
+ public function hasRelation($name)
+ {
+ if ( ! isset($this->_pending[$name]) && ! isset($this->_relations[$name])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function bind($name, $options = array())
+ {
+ $e = explode(' as ', $name);
+ $name = $e[0];
+ $alias = isset($e[1]) ? $e[1] : $name;
+
+ if ( ! isset($options['type'])) {
+ throw new IPF_ORM_Exception('Relation type not set.');
+ }
+
+ if ($this->hasRelation($alias)) {
+ unset($this->relations[$alias]);
+ unset($this->_pending[$alias]);
+ }
+
+ $this->_pending[$alias] = array_merge($options, array('class' => $name, 'alias' => $alias));
+
+ return $this->_pending[$alias];
+ }
+
+ public function getRelation($alias, $recursive = true)
+ {
+ if (isset($this->_relations[$alias])) {
+ return $this->_relations[$alias];
+ }
+
+ if (isset($this->_pending[$alias])) {
+ $def = $this->_pending[$alias];
+ $identifierColumnNames = $this->_table->getIdentifierColumnNames();
+ $idColumnName = array_pop($identifierColumnNames);
+
+ // check if reference class name exists
+ // if it does we are dealing with association relation
+ if (isset($def['refClass'])) {
+ $def = $this->completeAssocDefinition($def);
+ $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName()));
+
+ if ( ! isset($this->_pending[$def['refClass']]) &&
+ ! isset($this->_relations[$def['refClass']])) {
+
+ $parser = $def['refTable']->getRelationParser();
+ if ( ! $parser->hasRelation($this->_table->getComponentName())) {
+ $parser->bind($this->_table->getComponentName(),
+ array('type' => IPF_ORM_Relation::ONE,
+ 'local' => $def['local'],
+ 'foreign' => $idColumnName,
+ 'localKey' => true,
+ ));
+ }
+
+ if ( ! $this->hasRelation($def['refClass'])) {
+ $this->bind($def['refClass'], array('type' => IPF_ORM_Relation::MANY,
+ 'foreign' => $def['local'],
+ 'local' => $idColumnName));
+ }
+ }
+ if (in_array($def['class'], $localClasses)) {
+ $rel = new IPF_ORM_Relation_Nest($def);
+ } else {
+ $rel = new IPF_ORM_Relation_Association($def);
+ }
+ } else {
+ // simple foreign key relation
+ $def = $this->completeDefinition($def);
+
+ if (isset($def['localKey'])) {
+ $rel = new IPF_ORM_Relation_LocalKey($def);
+
+ // Automatically index foreign keys which are not primary
+ $foreign = (array) $def['foreign'];
+ foreach ($foreign as $fk) {
+ if ( ! $rel->getTable()->isIdentifier($fk)) {
+ $rel->getTable()->addIndex($fk, array('fields' => array($fk)));
+ }
+ }
+ } else {
+ $rel = new IPF_ORM_Relation_ForeignKey($def);
+ }
+ }
+ if (isset($rel)) {
+ // unset pending relation
+ unset($this->_pending[$alias]);
+
+ $this->_relations[$alias] = $rel;
+ return $rel;
+ }
+ }
+ if ($recursive) {
+ $this->getRelations();
+
+ return $this->getRelation($alias, false);
+ } else {
+ throw new IPF_ORM_Exception('Unknown relation alias ' . $alias);
+ }
+ }
+
+ public function getRelations()
+ {
+ foreach ($this->_pending as $k => $v) {
+ $this->getRelation($k);
+ }
+
+ return $this->_relations;
+ }
+
+ public function getImpl($template)
+ {
+ $conn = $this->_table->getConnection();
+
+ if (in_array('IPF_ORM_Template', class_parents($template))) {
+ $impl = $this->_table->getImpl($template);
+
+ if ($impl === null) {
+ throw new IPF_ORM_Exception("Couldn't find concrete implementation for template " . $template);
+ }
+ } else {
+ $impl = $template;
+ }
+
+ return $conn->getTable($impl);
+ }
+
+ public function completeAssocDefinition($def)
+ {
+ $conn = $this->_table->getConnection();
+ $def['table'] = $this->getImpl($def['class']);
+ $def['localTable'] = $this->_table;
+ $def['class'] = $def['table']->getComponentName();
+ $def['refTable'] = $this->getImpl($def['refClass']);
+
+ $id = $def['refTable']->getIdentifierColumnNames();
+
+ if (count($id) > 1) {
+ if ( ! isset($def['foreign'])) {
+ // foreign key not set
+ // try to guess the foreign key
+
+ $def['foreign'] = ($def['local'] === $id[0]) ? $id[1] : $id[0];
+ }
+ if ( ! isset($def['local'])) {
+ // foreign key not set
+ // try to guess the foreign key
+
+ $def['local'] = ($def['foreign'] === $id[0]) ? $id[1] : $id[0];
+ }
+ } else {
+
+ if ( ! isset($def['foreign'])) {
+ // foreign key not set
+ // try to guess the foreign key
+
+ $columns = $this->getIdentifiers($def['table']);
+
+ $def['foreign'] = $columns;
+ }
+ if ( ! isset($def['local'])) {
+ // local key not set
+ // try to guess the local key
+ $columns = $this->getIdentifiers($this->_table);
+
+ $def['local'] = $columns;
+ }
+ }
+ return $def;
+ }
+
+ public function getIdentifiers(IPF_ORM_Table $table)
+ {
+ $componentNameToLower = strtolower($table->getComponentName());
+ if (is_array($table->getIdentifier())) {
+ $columns = array();
+ foreach ((array) $table->getIdentifierColumnNames() as $identColName) {
+ $columns[] = $componentNameToLower . '_' . $identColName;
+ }
+ } else {
+ $columns = $componentNameToLower . '_' . $table->getColumnName(
+ $table->getIdentifier());
+ }
+
+ return $columns;
+ }
+
+ public function guessColumns(array $classes, IPF_ORM_Table $foreignTable)
+ {
+ $conn = $this->_table->getConnection();
+
+ foreach ($classes as $class) {
+ try {
+ $table = $conn->getTable($class);
+ } catch (IPF_ORM_Exception $e) {
+ continue;
+ }
+ $columns = $this->getIdentifiers($table);
+ $found = true;
+
+ foreach ((array) $columns as $column) {
+ if ( ! $foreignTable->hasColumn($column)) {
+ $found = false;
+ break;
+ }
+ }
+ if ($found) {
+ break;
+ }
+ }
+
+ if ( ! $found) {
+ throw new IPF_ORM_Exception("Couldn't find columns.");
+ }
+
+ return $columns;
+ }
+
+ public function completeDefinition($def)
+ {
+ $conn = $this->_table->getConnection();
+ $def['table'] = $this->getImpl($def['class']);
+ $def['localTable'] = $this->_table;
+ $def['class'] = $def['table']->getComponentName();
+
+ $foreignClasses = array_merge($def['table']->getOption('parents'), array($def['class']));
+ $localClasses = array_merge($this->_table->getOption('parents'), array($this->_table->getComponentName()));
+
+ $localIdentifierColumnNames = $this->_table->getIdentifierColumnNames();
+ $localIdentifierCount = count($localIdentifierColumnNames);
+ $localIdColumnName = array_pop($localIdentifierColumnNames);
+ $foreignIdentifierColumnNames = $def['table']->getIdentifierColumnNames();
+ $foreignIdColumnName = array_pop($foreignIdentifierColumnNames);
+
+ if (isset($def['local'])) {
+ if ( ! isset($def['foreign'])) {
+ // local key is set, but foreign key is not
+ // try to guess the foreign key
+
+ if ($def['local'] === $localIdColumnName) {
+ $def['foreign'] = $this->guessColumns($localClasses, $def['table']);
+ } else {
+ // the foreign field is likely to be the
+ // identifier of the foreign class
+ $def['foreign'] = $foreignIdColumnName;
+ $def['localKey'] = true;
+ }
+ } else {
+ if ($localIdentifierCount == 1) {
+ if ($def['local'] == $localIdColumnName && isset($def['owningSide'])
+ && $def['owningSide'] === true) {
+ $def['localKey'] = true;
+ } else if (($def['local'] !== $localIdColumnName && $def['type'] == IPF_ORM_Relation::ONE)) {
+ $def['localKey'] = true;
+ }
+ } else if ($localIdentifierCount > 1) {
+ // It's a composite key and since 'foreign' can not point to a composite
+ // key currently, we know that 'local' must be the foreign key.
+ $def['localKey'] = true;
+ }
+ }
+ } else {
+ if (isset($def['foreign'])) {
+ // local key not set, but foreign key is set
+ // try to guess the local key
+ if ($def['foreign'] === $foreignIdColumnName) {
+ $def['localKey'] = true;
+ try {
+ $def['local'] = $this->guessColumns($foreignClasses, $this->_table);
+ } catch (IPF_ORM_Exception $e) {
+ $def['local'] = $localIdColumnName;
+ }
+ } else {
+ $def['local'] = $localIdColumnName;
+ }
+ } else {
+ // neither local or foreign key is being set
+ // try to guess both keys
+
+ $conn = $this->_table->getConnection();
+
+ // the following loops are needed for covering inheritance
+ foreach ($localClasses as $class) {
+ $table = $conn->getTable($class);
+ $identifierColumnNames = $table->getIdentifierColumnNames();
+ $idColumnName = array_pop($identifierColumnNames);
+ $column = strtolower($table->getComponentName())
+ . '_' . $idColumnName;
+
+ foreach ($foreignClasses as $class2) {
+ $table2 = $conn->getTable($class2);
+ if ($table2->hasColumn($column)) {
+ $def['foreign'] = $column;
+ $def['local'] = $idColumnName;
+ return $def;
+ }
+ }
+ }
+
+ foreach ($foreignClasses as $class) {
+ $table = $conn->getTable($class);
+ $identifierColumnNames = $table->getIdentifierColumnNames();
+ $idColumnName = array_pop($identifierColumnNames);
+ $column = strtolower($table->getComponentName())
+ . '_' . $idColumnName;
+
+ foreach ($localClasses as $class2) {
+ $table2 = $conn->getTable($class2);
+ if ($table2->hasColumn($column)) {
+ $def['foreign'] = $idColumnName;
+ $def['local'] = $column;
+ $def['localKey'] = true;
+ return $def;
+ }
+ }
+ }
+
+ // auto-add columns and auto-build relation
+ $columns = array();
+ foreach ((array) $this->_table->getIdentifierColumnNames() as $id) {
+ // ?? should this not be $this->_table->getComponentName() ??
+ $column = strtolower($table->getComponentName())
+ . '_' . $id;
+
+ $col = $this->_table->getColumnDefinition($id);
+ $type = $col['type'];
+ $length = $col['length'];
+
+ unset($col['type']);
+ unset($col['length']);
+ unset($col['autoincrement']);
+ unset($col['sequence']);
+ unset($col['primary']);
+
+ $def['table']->setColumn($column, $type, $length, $col);
+
+ $columns[] = $column;
+ }
+ if (count($columns) > 1) {
+ $def['foreign'] = $columns;
+ } else {
+ $def['foreign'] = $columns[0];
+ }
+ $def['local'] = $localIdColumnName;
+ }
+ }
+ return $def;
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/sequence.php b/ipf/orm/sequence.php
new file mode 100644
index 0000000..77ce93b
--- /dev/null
+++ b/ipf/orm/sequence.php
@@ -0,0 +1,21 @@
+warnings[] = 'database does not support getting current
+ sequence value, the sequence value was incremented';
+ return $this->nextId($seqName);
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/sequence/mysql.php b/ipf/orm/sequence/mysql.php
new file mode 100644
index 0000000..4293095
--- /dev/null
+++ b/ipf/orm/sequence/mysql.php
@@ -0,0 +1,52 @@
+conn->quoteIdentifier($seqName, true);
+ $seqcolName = $this->conn->quoteIdentifier($this->conn->getAttribute(IPF_ORM::ATTR_SEQCOL_NAME), true);
+ $query = 'INSERT INTO ' . $sequenceName . ' (' . $seqcolName . ') VALUES (NULL)';
+
+ try {
+
+ $this->conn->exec($query);
+
+ } catch(IPF_ORM_Exception $e) {
+ if ($onDemand && $e->getPortableCode() == IPF_ORM::ERR_NOSUCHTABLE) {
+ // Since we are creating the sequence on demand
+ // we know the first id = 1 so initialize the
+ // sequence at 2
+ try {
+ $result = $this->conn->export->createSequence($seqName, 2);
+ } catch(IPF_ORM_Exception $e) {
+ throw new IPF_ORM_Exception('on demand sequence ' . $seqName . ' could not be created');
+ }
+ // First ID of a newly created sequence is 1
+ return 1;
+ }
+ throw $e;
+ }
+
+ $value = $this->lastInsertId();
+
+ if (is_numeric($value)) {
+ $query = 'DELETE FROM ' . $sequenceName . ' WHERE ' . $seqcolName . ' < ' . $value;
+ $this->conn->exec($query);
+ }
+ return $value;
+ }
+
+ public function lastInsertId($table = null, $field = null)
+ {
+ return $this->conn->getDbh()->lastInsertId();
+ }
+
+ public function currId($seqName)
+ {
+ $sequenceName = $this->conn->quoteIdentifier($seqName, true);
+ $seqcolName = $this->conn->quoteIdentifier($this->conn->getAttribute(IPF_ORM::ATTR_SEQCOL_NAME), true);
+ $query = 'SELECT MAX(' . $seqcolName . ') FROM ' . $sequenceName;
+ return (int) $this->conn->fetchOne($query);
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/table.php b/ipf/orm/table.php
new file mode 100644
index 0000000..05ea8d5
--- /dev/null
+++ b/ipf/orm/table.php
@@ -0,0 +1,1405 @@
+ null,
+ 'tableName' => null,
+ 'sequenceName' => null,
+ 'inheritanceMap' => array(),
+ 'enumMap' => array(),
+ 'type' => null,
+ 'charset' => null,
+ 'collation' => null,
+ 'treeImpl' => null,
+ 'treeOptions' => array(),
+ 'indexes' => array(),
+ 'parents' => array(),
+ 'joinedParents' => array(),
+ 'queryParts' => array(),
+ 'versioning' => null,
+ 'subclasses' => array(),
+ );
+
+ protected $_tree;
+ protected $_parser;
+
+ protected $_templates = array();
+ protected $_filters = array();
+ protected $_generators = array();
+ protected $_invokedMethods = array();
+ protected $record;
+
+ public function __construct($name, IPF_ORM_Connection $conn, $initDefinition = false)
+ {
+ $this->_conn = $conn;
+
+ $this->setParent($this->_conn);
+
+ $this->_options['name'] = $name;
+ $this->_parser = new IPF_ORM_Relation_Parser($this);
+
+ if ($initDefinition) {
+ $this->record = $this->initDefinition();
+
+ $this->initIdentifier();
+
+ $this->record->setUp();
+
+ // if tree, set up tree
+ if ($this->isTree()) {
+ $this->getTree()->setUp();
+ }
+ } else {
+ if ( ! isset($this->_options['tableName'])) {
+ $this->setTableName(IPF_ORM_Inflector::tableize($this->_options['name']));
+ }
+ }
+ $this->_filters[] = new IPF_ORM_Record_Filter_Standard();
+ $this->_repository = new IPF_ORM_Table_Repository($this);
+ }
+
+ public function initDefinition()
+ {
+ $name = $this->_options['name'];
+ if ( ! class_exists($name) || empty($name)) {
+ throw new IPF_ORM_Exception("Couldn't find class " . $name);
+ }
+ $record = new $name($this);
+
+ $names = array();
+
+ $class = $name;
+
+ // get parent classes
+
+ do {
+ if ($class === 'IPF_ORM_Record') {
+ break;
+ }
+
+ $name = $class;
+ $names[] = $name;
+ } while ($class = get_parent_class($class));
+
+ if ($class === false) {
+ throw new IPF_ORM_Exception('Class "' . $name . '" must be a child class of IPF_ORM_Record');
+ }
+
+ // reverse names
+ $names = array_reverse($names);
+ // save parents
+ array_pop($names);
+ $this->_options['parents'] = $names;
+
+ // create database table
+ if (method_exists($record, 'setTableDefinition')) {
+ $record->setTableDefinition();
+ // get the declaring class of setTableDefinition method
+ $method = new ReflectionMethod($this->_options['name'], 'setTableDefinition');
+ $class = $method->getDeclaringClass();
+
+ } else {
+ $class = new ReflectionClass($class);
+ }
+
+ $this->_options['joinedParents'] = array();
+
+ foreach (array_reverse($this->_options['parents']) as $parent) {
+
+ if ($parent === $class->getName()) {
+ continue;
+ }
+ $ref = new ReflectionClass($parent);
+
+ if ($ref->isAbstract()) {
+ continue;
+ }
+ $parentTable = $this->_conn->getTable($parent);
+
+ $found = false;
+ $parentColumns = $parentTable->getColumns();
+
+ foreach ($parentColumns as $columnName => $definition) {
+ if ( ! isset($definition['primary']) || $definition['primary'] === false) {
+ if (isset($this->_columns[$columnName])) {
+ $found = true;
+ break;
+ } else {
+ if ( ! isset($parentColumns[$columnName]['owner'])) {
+ $parentColumns[$columnName]['owner'] = $parentTable->getComponentName();
+ }
+
+ $this->_options['joinedParents'][] = $parentColumns[$columnName]['owner'];
+ }
+ } else {
+ unset($parentColumns[$columnName]);
+ }
+ }
+
+ if ($found) {
+ continue;
+ }
+
+ foreach ($parentColumns as $columnName => $definition) {
+ $fullName = $columnName . ' as ' . $parentTable->getFieldName($columnName);
+ $this->setColumn($fullName, $definition['type'], $definition['length'], $definition, true);
+ }
+
+ break;
+ }
+
+ $this->_options['joinedParents'] = array_values(array_unique($this->_options['joinedParents']));
+
+ $this->_options['declaringClass'] = $class;
+
+ // set the table definition for the given tree implementation
+ if ($this->isTree()) {
+ $this->getTree()->setTableDefinition();
+ }
+
+ $this->columnCount = count($this->_columns);
+
+ if ( ! isset($this->_options['tableName'])) {
+ $this->setTableName(IPF_ORM_Inflector::tableize($class->getName()));
+ }
+
+ return $record;
+ }
+
+ public function initIdentifier()
+ {
+ switch (count($this->_identifier)) {
+ case 0:
+ if ( ! empty($this->_options['joinedParents'])) {
+ $root = current($this->_options['joinedParents']);
+
+ $table = $this->_conn->getTable($root);
+
+ $this->_identifier = $table->getIdentifier();
+
+ $this->_identifierType = ($table->getIdentifierType() !== IPF_ORM::IDENTIFIER_AUTOINC)
+ ? $table->getIdentifierType() : IPF_ORM::IDENTIFIER_NATURAL;
+
+ // add all inherited primary keys
+ foreach ((array) $this->_identifier as $id) {
+ $definition = $table->getDefinitionOf($id);
+
+ // inherited primary keys shouldn't contain autoinc
+ // and sequence definitions
+ unset($definition['autoincrement']);
+ unset($definition['sequence']);
+
+ // add the inherited primary key column
+ $fullName = $id . ' as ' . $table->getFieldName($id);
+ $this->setColumn($fullName, $definition['type'], $definition['length'],
+ $definition, true);
+ }
+ } else {
+ $definition = array('type' => 'integer',
+ 'length' => 20,
+ 'autoincrement' => true,
+ 'primary' => true);
+ $this->setColumn('id', $definition['type'], $definition['length'], $definition, true);
+ $this->_identifier = 'id';
+ $this->_identifierType = IPF_ORM::IDENTIFIER_AUTOINC;
+ }
+ $this->columnCount++;
+ break;
+ case 1:
+ foreach ($this->_identifier as $pk) {
+ $e = $this->getDefinitionOf($pk);
+
+ $found = false;
+
+ foreach ($e as $option => $value) {
+ if ($found) {
+ break;
+ }
+
+ $e2 = explode(':', $option);
+
+ switch (strtolower($e2[0])) {
+ case 'autoincrement':
+ case 'autoinc':
+ if ($value !== false) {
+ $this->_identifierType = IPF_ORM::IDENTIFIER_AUTOINC;
+ $found = true;
+ }
+ break;
+ case 'seq':
+ case 'sequence':
+ $this->_identifierType = IPF_ORM::IDENTIFIER_SEQUENCE;
+ $found = true;
+
+ if (is_string($value)) {
+ $this->_options['sequenceName'] = $value;
+ } else {
+ if (($sequence = $this->getAttribute(IPF_ORM::ATTR_DEFAULT_SEQUENCE)) !== null) {
+ $this->_options['sequenceName'] = $sequence;
+ } else {
+ $this->_options['sequenceName'] = $this->_conn->formatter->getSequenceName($this->_options['tableName']);
+ }
+ }
+ break;
+ }
+ }
+ if ( ! isset($this->_identifierType)) {
+ $this->_identifierType = IPF_ORM::IDENTIFIER_NATURAL;
+ }
+ }
+
+ $this->_identifier = $pk;
+
+ break;
+ default:
+ $this->_identifierType = IPF_ORM::IDENTIFIER_COMPOSITE;
+ }
+ }
+
+ public function getColumnOwner($columnName)
+ {
+ if (isset($this->_columns[$columnName]['owner'])) {
+ return $this->_columns[$columnName]['owner'];
+ } else {
+ return $this->getComponentName();
+ }
+ }
+
+ public function getRecordInstance()
+ {
+ if ( ! $this->record) {
+ $this->record = new $this->_options['name'];
+ }
+ return $this->record;
+ }
+
+ public function isInheritedColumn($columnName)
+ {
+ return (isset($this->_columns[$columnName]['owner']));
+ }
+
+ public function isIdentifier($fieldName)
+ {
+ return ($fieldName === $this->getIdentifier() ||
+ in_array($fieldName, (array) $this->getIdentifier()));
+ }
+
+ public function isIdentifierAutoincrement()
+ {
+ return $this->getIdentifierType() === IPF_ORM::IDENTIFIER_AUTOINC;
+ }
+
+ public function isIdentifierComposite()
+ {
+ return $this->getIdentifierType() === IPF_ORM::IDENTIFIER_COMPOSITE;
+ }
+
+ public function getMethodOwner($method)
+ {
+ return (isset($this->_invokedMethods[$method])) ?
+ $this->_invokedMethods[$method] : false;
+ }
+
+ public function setMethodOwner($method, $class)
+ {
+ $this->_invokedMethods[$method] = $class;
+ }
+
+ public function getTemplates()
+ {
+ return $this->_templates;
+ }
+
+ public function export()
+ {
+ $this->_conn->export->exportTable($this);
+ }
+
+ public function getExportableFormat($parseForeignKeys = true)
+ {
+ $columns = array();
+ $primary = array();
+
+ foreach ($this->getColumns() as $name => $definition) {
+
+ if (isset($definition['owner'])) {
+ continue;
+ }
+
+ switch ($definition['type']) {
+ case 'enum':
+ if (isset($definition['default'])) {
+ $definition['default'] = $this->enumIndex($name, $definition['default']);
+ }
+ break;
+ case 'boolean':
+ if (isset($definition['default'])) {
+ $definition['default'] = $this->getConnection()->convertBooleans($definition['default']);
+ }
+ break;
+ }
+ $columns[$name] = $definition;
+
+ if (isset($definition['primary']) && $definition['primary']) {
+ $primary[] = $name;
+ }
+ }
+
+ $options['foreignKeys'] = isset($this->_options['foreignKeys']) ?
+ $this->_options['foreignKeys'] : array();
+
+ if ($parseForeignKeys && $this->getAttribute(IPF_ORM::ATTR_EXPORT)
+ & IPF_ORM::EXPORT_CONSTRAINTS) {
+
+ $constraints = array();
+
+ $emptyIntegrity = array('onUpdate' => null,
+ 'onDelete' => null);
+
+ foreach ($this->getRelations() as $name => $relation) {
+ $fk = $relation->toArray();
+ $fk['foreignTable'] = $relation->getTable()->getTableName();
+
+ if ($relation->getTable() === $this && in_array($relation->getLocal(), $primary)) {
+ if ($relation->hasConstraint()) {
+ throw new IPF_ORM_Exception("Badly constructed integrity constraints.");
+ }
+ continue;
+ }
+
+ $integrity = array('onUpdate' => $fk['onUpdate'],
+ 'onDelete' => $fk['onDelete']);
+
+ if ($relation instanceof IPF_ORM_Relation_LocalKey) {
+ $def = array('local' => $relation->getLocal(),
+ 'foreign' => $relation->getForeign(),
+ 'foreignTable' => $relation->getTable()->getTableName());
+
+ if (($key = array_search($def, $options['foreignKeys'])) === false) {
+ $options['foreignKeys'][] = $def;
+ $constraints[] = $integrity;
+ } else {
+ if ($integrity !== $emptyIntegrity) {
+ $constraints[$key] = $integrity;
+ }
+ }
+ }
+ }
+
+ foreach ($constraints as $k => $def) {
+ $options['foreignKeys'][$k] = array_merge($options['foreignKeys'][$k], $def);
+ }
+ }
+
+ $options['primary'] = $primary;
+
+ return array('tableName' => $this->getOption('tableName'),
+ 'columns' => $columns,
+ 'options' => array_merge($this->getOptions(), $options));
+ }
+
+ public function getRelationParser()
+ {
+ return $this->_parser;
+ }
+
+ public function __get($option)
+ {
+ if (isset($this->_options[$option])) {
+ return $this->_options[$option];
+ }
+ return null;
+ }
+
+ public function __isset($option)
+ {
+ return isset($this->_options[$option]);
+ }
+
+ public function getOptions()
+ {
+ return $this->_options;
+ }
+
+ public function setOptions($options)
+ {
+ foreach ($options as $key => $value) {
+ $this->setOption($key, $value);
+ }
+ }
+
+ public function addForeignKey(array $definition)
+ {
+ $this->_options['foreignKeys'][] = $definition;
+ }
+
+ public function addCheckConstraint($definition, $name)
+ {
+ if (is_string($name)) {
+ $this->_options['checks'][$name] = $definition;
+ } else {
+ $this->_options['checks'][] = $definition;
+ }
+
+ return $this;
+ }
+
+ public function addIndex($index, array $definition)
+ {
+ $this->_options['indexes'][$index] = $definition;
+ }
+
+ public function getIndex($index)
+ {
+ if (isset($this->_options['indexes'][$index])) {
+ return $this->_options['indexes'][$index];
+ }
+
+ return false;
+ }
+
+ public function bind($args, $type)
+ {
+ $options = array();
+ $options['type'] = $type;
+
+ if ( ! isset($args[1])) {
+ $args[1] = array();
+ }
+
+ // the following is needed for backwards compatibility
+ if (is_string($args[1])) {
+ if ( ! isset($args[2])) {
+ $args[2] = array();
+ } elseif (is_string($args[2])) {
+ $args[2] = (array) $args[2];
+ }
+
+ $classes = array_merge($this->_options['parents'], array($this->getComponentName()));
+
+ $e = explode('.', $args[1]);
+ if (in_array($e[0], $classes)) {
+ if ($options['type'] >= IPF_ORM_Relation::MANY) {
+ $options['foreign'] = $e[1];
+ } else {
+ $options['local'] = $e[1];
+ }
+ } else {
+ $e2 = explode(' as ', $args[0]);
+ if ($e[0] !== $e2[0] && ( ! isset($e2[1]) || $e[0] !== $e2[1])) {
+ $options['refClass'] = $e[0];
+ }
+
+ $options['foreign'] = $e[1];
+ }
+
+ $options = array_merge($args[2], $options);
+ } else {
+ $options = array_merge($args[1], $options);
+ }
+
+ $this->_parser->bind($args[0], $options);
+ }
+
+ public function hasRelation($alias)
+ {
+ return $this->_parser->hasRelation($alias);
+ }
+
+ public function getRelation($alias, $recursive = true)
+ {
+ return $this->_parser->getRelation($alias, $recursive);
+ }
+
+ public function getRelations()
+ {
+ return $this->_parser->getRelations();
+ }
+
+ public function createQuery($alias = '')
+ {
+ if ( ! empty($alias)) {
+ $alias = ' ' . trim($alias);
+ }
+ return IPF_ORM_Query::create($this->_conn)->from($this->getComponentName() . $alias);
+ }
+
+ public function getRepository()
+ {
+ return $this->_repository;
+ }
+
+ public function setOption($name, $value)
+ {
+ switch ($name) {
+ case 'name':
+ case 'tableName':
+ break;
+ case 'enumMap':
+ case 'inheritanceMap':
+ case 'index':
+ case 'treeOptions':
+ if ( ! is_array($value)) {
+ throw new IPF_ORM_Exception($name . ' should be an array.');
+ }
+ break;
+ }
+ $this->_options[$name] = $value;
+ }
+
+ public function getOption($name)
+ {
+ if (isset($this->_options[$name])) {
+ return $this->_options[$name];
+ }
+ return null;
+ }
+
+ public function getColumnName($fieldName)
+ {
+ // FIX ME: This is being used in places where an array is passed, but it should not be an array
+ // For example in places where IPF_ORM should support composite foreign/primary keys
+ $fieldName = is_array($fieldName) ? $fieldName[0]:$fieldName;
+
+ if (isset($this->_columnNames[$fieldName])) {
+ return $this->_columnNames[$fieldName];
+ }
+
+ return strtolower($fieldName);
+ }
+
+ public function getColumnDefinition($columnName)
+ {
+ if ( ! isset($this->_columns[$columnName])) {
+ return false;
+ }
+ return $this->_columns[$columnName];
+ }
+
+ public function getFieldName($columnName)
+ {
+ if (isset($this->_fieldNames[$columnName])) {
+ return $this->_fieldNames[$columnName];
+ }
+ return $columnName;
+ }
+ public function setColumns(array $definitions)
+ {
+ foreach ($definitions as $name => $options) {
+ $this->setColumn($name, $options['type'], $options['length'], $options);
+ }
+ }
+
+ public function setColumn($name, $type, $length = null, $options = array(), $prepend = false)
+ {
+ if (is_string($options)) {
+ $options = explode('|', $options);
+ }
+
+ foreach ($options as $k => $option) {
+ if (is_numeric($k)) {
+ if ( ! empty($option)) {
+ $options[$option] = true;
+ }
+ unset($options[$k]);
+ }
+ }
+
+ // extract column name & field name
+ if (stripos($name, ' as '))
+ {
+ if (strpos($name, ' as')) {
+ $parts = explode(' as ', $name);
+ } else {
+ $parts = explode(' AS ', $name);
+ }
+
+ if (count($parts) > 1) {
+ $fieldName = $parts[1];
+ } else {
+ $fieldName = $parts[0];
+ }
+
+ $name = strtolower($parts[0]);
+ } else {
+ $fieldName = $name;
+ $name = strtolower($name);
+ }
+
+ $name = trim($name);
+ $fieldName = trim($fieldName);
+
+ if ($prepend) {
+ $this->_columnNames = array_merge(array($fieldName => $name), $this->_columnNames);
+ $this->_fieldNames = array_merge(array($name => $fieldName), $this->_fieldNames);
+ } else {
+ $this->_columnNames[$fieldName] = $name;
+ $this->_fieldNames[$name] = $fieldName;
+ }
+
+ if ($length == null) {
+ switch ($type) {
+ case 'string':
+ case 'clob':
+ case 'float':
+ case 'integer':
+ case 'array':
+ case 'object':
+ case 'blob':
+ case 'gzip':
+ // use php int max
+ $length = 2147483647;
+ break;
+ case 'boolean':
+ $length = 1;
+ case 'date':
+ // YYYY-MM-DD ISO 8601
+ $length = 10;
+ case 'time':
+ // HH:NN:SS+00:00 ISO 8601
+ $length = 14;
+ case 'timestamp':
+ // YYYY-MM-DDTHH:MM:SS+00:00 ISO 8601
+ $length = 25;
+ break;
+ }
+ }
+
+ $options['type'] = $type;
+ $options['length'] = $length;
+
+ if ($prepend) {
+ $this->_columns = array_merge(array($name => $options), $this->_columns);
+ } else {
+ $this->_columns[$name] = $options;
+ }
+
+ if (isset($options['primary']) && $options['primary']) {
+ if (isset($this->_identifier)) {
+ $this->_identifier = (array) $this->_identifier;
+ }
+ if ( ! in_array($fieldName, $this->_identifier)) {
+ $this->_identifier[] = $fieldName;
+ }
+ }
+ if (isset($options['default'])) {
+ $this->hasDefaultValues = true;
+ }
+ }
+
+ public function hasDefaultValues()
+ {
+ return $this->hasDefaultValues;
+ }
+
+ public function getDefaultValueOf($fieldName)
+ {
+ $columnName = $this->getColumnName($fieldName);
+ if ( ! isset($this->_columns[$columnName])) {
+ throw new IPF_ORM_Exception("Couldn't get default value. Column ".$columnName." doesn't exist.");
+ }
+ if (isset($this->_columns[$columnName]['default'])) {
+ return $this->_columns[$columnName]['default'];
+ } else {
+ return null;
+ }
+ }
+
+ public function getIdentifier()
+ {
+ return $this->_identifier;
+ }
+
+ public function getIdentifierType()
+ {
+ return $this->_identifierType;
+ }
+
+ public function hasColumn($columnName)
+ {
+ return isset($this->_columns[strtolower($columnName)]);
+ }
+
+ public function hasField($fieldName)
+ {
+ return isset($this->_columnNames[$fieldName]);
+ }
+
+ public function setConnection(IPF_ORM_Connection $conn)
+ {
+ $this->_conn = $conn;
+
+ $this->setParent($this->_conn);
+
+ return $this;
+ }
+
+ public function getConnection()
+ {
+ return $this->_conn;
+ }
+
+ public function create(array $array = array())
+ {
+ $record = new $this->_options['name']($this, true);
+ $record->fromArray($array);
+
+ return $record;
+ }
+
+ public function find($id, $hydrationMode = null)
+ {
+ if (is_null($id)) {
+ return false;
+ }
+
+ $id = is_array($id) ? array_values($id) : array($id);
+
+ $q = $this->createQuery();
+ $q->where(implode(' = ? AND ', (array) $this->getIdentifier()) . ' = ?', $id)
+ ->limit(1);
+ $res = $q->fetchOne(array(), $hydrationMode);
+ $q->free();
+
+ return $res;
+ }
+
+ public function findAll($hydrationMode = null)
+ {
+ return $this->createQuery()->execute(array(), $hydrationMode);
+ }
+
+ public function findBySql($dql, $params = array(), $hydrationMode = null)
+ {
+ return $this->createQuery()->where($dql)->execute($params, $hydrationMode);
+ }
+
+ public function findByDql($dql, $params = array(), $hydrationMode = null)
+ {
+ $parser = new IPF_ORM_Query($this->_conn);
+ $component = $this->getComponentName();
+ $query = 'FROM ' . $component . ' WHERE ' . $dql;
+
+ return $parser->query($query, $params, $hydrationMode);
+ }
+
+ public function execute($queryKey, $params = array(), $hydrationMode = IPF_ORM::HYDRATE_RECORD)
+ {
+ return IPF_ORM_Manager::getInstance()
+ ->getQueryRegistry()
+ ->get($queryKey, $this->getComponentName())
+ ->execute($params, $hydrationMode);
+ }
+
+ public function executeOne($queryKey, $params = array(), $hydrationMode = IPF_ORM::HYDRATE_RECORD)
+ {
+ return IPF_ORM_Manager::getInstance()
+ ->getQueryRegistry()
+ ->get($queryKey, $this->getComponentName())
+ ->fetchOne($params, $hydrationMode);
+ }
+
+ public function clear()
+ {
+ $this->_identityMap = array();
+ }
+
+ public function addRecord(IPF_ORM_Record $record)
+ {
+ $id = implode(' ', $record->identifier());
+
+ if (isset($this->_identityMap[$id])) {
+ return false;
+ }
+
+ $this->_identityMap[$id] = $record;
+
+ return true;
+ }
+
+ public function removeRecord(IPF_ORM_Record $record)
+ {
+ $id = implode(' ', $record->identifier());
+
+ if (isset($this->_identityMap[$id])) {
+ unset($this->_identityMap[$id]);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function getRecord()
+ {
+ if ( ! empty($this->_data)) {
+ $identifierFieldNames = $this->getIdentifier();
+
+ if ( ! is_array($identifierFieldNames)) {
+ $identifierFieldNames = array($identifierFieldNames);
+ }
+
+ $found = false;
+ foreach ($identifierFieldNames as $fieldName) {
+ if ( ! isset($this->_data[$fieldName])) {
+ // primary key column not found return new record
+ $found = true;
+ break;
+ }
+ $id[] = $this->_data[$fieldName];
+ }
+
+ if ($found) {
+ $recordName = $this->getComponentName();
+ $record = new $recordName($this, true);
+ $this->_data = array();
+ return $record;
+ }
+
+
+ $id = implode(' ', $id);
+
+ if (isset($this->_identityMap[$id])) {
+ $record = $this->_identityMap[$id];
+ $record->hydrate($this->_data);
+ } else {
+ $recordName = $this->getComponentName();
+ $record = new $recordName($this);
+ $this->_identityMap[$id] = $record;
+ }
+ $this->_data = array();
+ } else {
+ $recordName = $this->getComponentName();
+ $record = new $recordName($this, true);
+ }
+
+ return $record;
+ }
+
+ public function getClassnameToReturn()
+ {
+ if ( ! isset($this->_options['subclasses'])) {
+ return $this->_options['name'];
+ }
+ foreach ($this->_options['subclasses'] as $subclass) {
+ $table = $this->_conn->getTable($subclass);
+ $inheritanceMap = $table->getOption('inheritanceMap');
+ $nomatch = false;
+ foreach ($inheritanceMap as $key => $value) {
+ if ( ! isset($this->_data[$key]) || $this->_data[$key] != $value) {
+ $nomatch = true;
+ break;
+ }
+ }
+ if ( ! $nomatch) {
+ return $table->getComponentName();
+ }
+ }
+ return $this->_options['name'];
+ }
+
+ final public function getProxy($id = null)
+ {
+ if ($id !== null) {
+ $identifierColumnNames = $this->getIdentifierColumnNames();
+ $query = 'SELECT ' . implode(', ', (array) $identifierColumnNames)
+ . ' FROM ' . $this->getTableName()
+ . ' WHERE ' . implode(' = ? && ', (array) $identifierColumnNames) . ' = ?';
+ $query = $this->applyInheritance($query);
+
+ $params = array_merge(array($id), array_values($this->_options['inheritanceMap']));
+
+ $this->_data = $this->_conn->execute($query, $params)->fetch(PDO::FETCH_ASSOC);
+
+ if ($this->_data === false)
+ return false;
+ }
+ return $this->getRecord();
+ }
+
+ final public function applyInheritance($where)
+ {
+ if ( ! empty($this->_options['inheritanceMap'])) {
+ $a = array();
+ foreach ($this->_options['inheritanceMap'] as $field => $value) {
+ $a[] = $this->getColumnName($field) . ' = ?';
+ }
+ $i = implode(' AND ', $a);
+ $where .= ' AND ' . $i;
+ }
+ return $where;
+ }
+
+ public function count()
+ {
+ $a = $this->_conn->execute('SELECT COUNT(1) FROM ' . $this->_options['tableName'])->fetch(IPF_ORM::FETCH_NUM);
+ return current($a);
+ }
+
+ public function getQueryObject()
+ {
+ $graph = new IPF_ORM_Query($this->getConnection());
+ $graph->load($this->getComponentName());
+ return $graph;
+ }
+
+ public function getEnumValues($fieldName)
+ {
+ $columnName = $this->getColumnName($fieldName);
+ if (isset($this->_columns[$columnName]['values'])) {
+ return $this->_columns[$columnName]['values'];
+ } else {
+ return array();
+ }
+ }
+
+ public function enumValue($fieldName, $index)
+ {
+ if ($index instanceof IPF_ORM_Null) {
+ return $index;
+ }
+
+ $columnName = $this->getColumnName($fieldName);
+ if ( ! $this->_conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)
+ && isset($this->_columns[$columnName]['values'][$index])
+ ) {
+ return $this->_columns[$columnName]['values'][$index];
+ }
+
+ return $index;
+ }
+
+ public function enumIndex($fieldName, $value)
+ {
+ $values = $this->getEnumValues($fieldName);
+
+ $index = array_search($value, $values);
+ if ($index === false || !$this->_conn->getAttribute(IPF_ORM::ATTR_USE_NATIVE_ENUM)) {
+ return $index;
+ }
+ return $value;
+ }
+
+ public function validateField($fieldName, $value, IPF_ORM_Record $record = null)
+ {
+ if ($record instanceof IPF_ORM_Record) {
+ $errorStack = $record->getErrorStack();
+ } else {
+ $record = $this->create();
+ $errorStack = new IPF_ORM_Validator_ErrorStack($this->getOption('name'));
+ }
+
+ if ($value === self::$_null) {
+ $value = null;
+ } else if ($value instanceof IPF_ORM_Record) {
+ $value = $value->getIncremented();
+ }
+
+ $dataType = $this->getTypeOf($fieldName);
+
+ // Validate field type, if type validation is enabled
+ if ($this->getAttribute(IPF_ORM::ATTR_VALIDATE) & IPF_ORM::VALIDATE_TYPES) {
+ if ( ! IPF_ORM_Validator::isValidType($value, $dataType)) {
+ $errorStack->add($fieldName, 'type');
+ }
+ if ($dataType == 'enum') {
+ $enumIndex = $this->enumIndex($fieldName, $value);
+ if ($enumIndex === false) {
+ $errorStack->add($fieldName, 'enum');
+ }
+ }
+ }
+
+ // Validate field length, if length validation is enabled
+ if ($this->getAttribute(IPF_ORM::ATTR_VALIDATE) & IPF_ORM::VALIDATE_LENGTHS) {
+ if ( ! IPF_ORM_Validator::validateLength($value, $dataType, $this->getFieldLength($fieldName))) {
+ $errorStack->add($fieldName, 'length');
+ }
+ }
+
+ // Run all custom validators
+ foreach ($this->getFieldValidators($fieldName) as $validatorName => $args) {
+ if ( ! is_string($validatorName)) {
+ $validatorName = $args;
+ $args = array();
+ }
+
+ $validator = IPF_ORM_Validator::getValidator($validatorName);
+ $validator->invoker = $record;
+ $validator->field = $fieldName;
+ $validator->args = $args;
+ if ( ! $validator->validate($value)) {
+ $errorStack->add($fieldName, $validator);
+ }
+ }
+
+ return $errorStack;
+ }
+
+ public function getColumnCount()
+ {
+ return $this->columnCount;
+ }
+
+ public function getColumns()
+ {
+ return $this->_columns;
+ }
+
+ public function removeColumn($fieldName)
+ {
+ if ( ! $this->hasField($fieldName)) {
+ return false;
+ }
+
+ $columnName = $this->getColumnName($fieldName);
+ unset($this->_columnNames[$fieldName], $this->_fieldNames[$columnName], $this->_columns[$columnName]);
+ $this->columnCount = count($this->_columns);
+ return true;
+ }
+
+ public function getColumnNames(array $fieldNames = null)
+ {
+ if ($fieldNames === null) {
+ return array_keys($this->_columns);
+ } else {
+ $columnNames = array();
+ foreach ($fieldNames as $fieldName) {
+ $columnNames[] = $this->getColumnName($fieldName);
+ }
+ return $columnNames;
+ }
+ }
+
+ public function getIdentifierColumnNames()
+ {
+ return $this->getColumnNames((array) $this->getIdentifier());
+ }
+
+ public function getFieldNames()
+ {
+ return array_values($this->_fieldNames);
+ }
+
+ public function getDefinitionOf($fieldName)
+ {
+ $columnName = $this->getColumnName($fieldName);
+ return $this->getColumnDefinition($columnName);
+ }
+
+ public function getTypeOf($fieldName)
+ {
+ return $this->getTypeOfColumn($this->getColumnName($fieldName));
+ }
+
+ public function getTypeOfColumn($columnName)
+ {
+ return isset($this->_columns[$columnName]) ? $this->_columns[$columnName]['type'] : false;
+ }
+
+ public function setData(array $data)
+ {
+ $this->_data = $data;
+ }
+
+ public function getData()
+ {
+ return $this->_data;
+ }
+
+ public function prepareValue($fieldName, $value, $typeHint = null)
+ {
+ if ($value === self::$_null) {
+ return self::$_null;
+ } else if ($value === null) {
+ return null;
+ } else {
+ $type = is_null($typeHint) ? $this->getTypeOf($fieldName) : $typeHint;
+
+ switch ($type) {
+ case 'integer':
+ case 'string';
+ // don't do any casting here PHP INT_MAX is smaller than what the databases support
+ break;
+ case 'enum':
+ return $this->enumValue($fieldName, $value);
+ break;
+ case 'boolean':
+ return (boolean) $value;
+ break;
+ case 'array':
+ case 'object':
+ if (is_string($value)) {
+ $value = empty($value) ? null:unserialize($value);
+
+ if ($value === false) {
+ throw new IPF_ORM_Exception('Unserialization of ' . $fieldName . ' failed.');
+ }
+ return $value;
+ }
+ break;
+ case 'gzip':
+ $value = gzuncompress($value);
+
+ if ($value === false) {
+ throw new IPF_ORM_Exception('Uncompressing of ' . $fieldName . ' failed.');
+ }
+ return $value;
+ break;
+ }
+ }
+ return $value;
+ }
+
+ public function getTree()
+ {
+ if (isset($this->_options['treeImpl'])) {
+ if ( ! $this->_tree) {
+ $options = isset($this->_options['treeOptions']) ? $this->_options['treeOptions'] : array();
+ $this->_tree = IPF_ORM_Tree::factory($this,
+ $this->_options['treeImpl'],
+ $options
+ );
+ }
+ return $this->_tree;
+ }
+ return false;
+ }
+
+ public function getComponentName()
+ {
+ return $this->_options['name'];
+ }
+
+ public function getTableName()
+ {
+ return $this->_options['tableName'];
+ }
+
+ public function setTableName($tableName)
+ {
+ $this->setOption('tableName', $this->_conn->formatter->getTableName($tableName));
+ }
+
+ public function isTree()
+ {
+ return ( ! is_null($this->_options['treeImpl'])) ? true : false;
+ }
+
+ public function getTemplate($template)
+ {
+ if ( ! isset($this->_templates[$template])) {
+ throw new IPF_ORM_Exception('Template ' . $template . ' not loaded');
+ }
+
+ return $this->_templates[$template];
+ }
+
+ public function hasTemplate($template)
+ {
+ return isset($this->_templates[$template]);
+ }
+
+ public function addTemplate($template, IPF_ORM_Template $impl)
+ {
+ $this->_templates[$template] = $impl;
+
+ return $this;
+ }
+
+ public function getGenerators()
+ {
+ return $this->_generators;
+ }
+
+ public function getGenerator($generator)
+ {
+ if ( ! isset($this->_generators[$generator])) {
+ throw new IPF_ORM_Exception('Generator ' . $generator . ' not loaded');
+ }
+
+ return $this->_generators[$generator];
+ }
+
+ public function hasGenerator($generator)
+ {
+ return isset($this->_generators[$generator]);
+ }
+
+ public function addGenerator(IPF_ORM_Record_Generator $generator, $name = null)
+ {
+ if ($name === null) {
+ $this->_generators[] = $generator;
+ } else {
+ $this->_generators[$name] = $generator;
+ }
+ return $this;
+ }
+
+ public function bindQueryParts(array $queryParts)
+ {
+ $this->_options['queryParts'] = $queryParts;
+
+ return $this;
+ }
+
+ public function bindQueryPart($queryPart, $value)
+ {
+ $this->_options['queryParts'][$queryPart] = $value;
+
+ return $this;
+ }
+
+ public function getFieldValidators($fieldName)
+ {
+ $validators = array();
+ $columnName = $this->getColumnName($fieldName);
+ // this loop is a dirty workaround to get the validators filtered out of
+ // the options, since everything is squeezed together currently
+ foreach ($this->_columns[$columnName] as $name => $args) {
+ if (empty($name)
+ || $name == 'primary'
+ || $name == 'protected'
+ || $name == 'autoincrement'
+ || $name == 'default'
+ || $name == 'values'
+ || $name == 'sequence'
+ || $name == 'zerofill'
+ || $name == 'owner'
+ || $name == 'scale'
+ || $name == 'type'
+ || $name == 'length'
+ || $name == 'fixed') {
+ continue;
+ }
+ if ($name == 'notnull' && isset($this->_columns[$columnName]['autoincrement'])) {
+ continue;
+ }
+ // skip it if it's explicitly set to FALSE (i.e. notnull => false)
+ if ($args === false) {
+ continue;
+ }
+ $validators[$name] = $args;
+ }
+
+ return $validators;
+ }
+
+ public function getFieldLength($fieldName)
+ {
+ return $this->_columns[$this->getColumnName($fieldName)]['length'];
+ }
+
+ public function getBoundQueryPart($queryPart)
+ {
+ if ( ! isset($this->_options['queryParts'][$queryPart])) {
+ return null;
+ }
+
+ return $this->_options['queryParts'][$queryPart];
+ }
+
+ public function unshiftFilter(IPF_ORM_Record_Filter $filter)
+ {
+ $filter->setTable($this);
+
+ $filter->init();
+
+ array_unshift($this->_filters, $filter);
+
+ return $this;
+ }
+
+ public function getFilters()
+ {
+ return $this->_filters;
+ }
+
+ public function __toString()
+ {
+ return IPF_ORM_Utils::getTableAsString($this);
+ }
+
+ protected function findBy($fieldName, $value, $hydrationMode = null)
+ {
+ return $this->createQuery()->where($fieldName . ' = ?', array($value))->execute(array(), $hydrationMode);
+ }
+
+ protected function findOneBy($fieldName, $value, $hydrationMode = null)
+ {
+ $results = $this->createQuery()
+ ->where($fieldName . ' = ?',array($value))
+ ->limit(1)
+ ->execute(array(), $hydrationMode);
+
+ if (is_array($results) && isset($results[0])) {
+ return $results[0];
+ } else if ($results instanceof IPF_ORM_Collection && $results->count() > 0) {
+ return $results->getFirst();
+ } else {
+ return false;
+ }
+ }
+
+ protected function _resolveFindByFieldName($name)
+ {
+ $fieldName = IPF_ORM_Inflector::tableize($name);
+ if ($this->hasColumn($name) || $this->hasField($name)) {
+ return $this->getFieldName($this->getColumnName($name));
+ } else if ($this->hasColumn($fieldName) || $this->hasField($fieldName)) {
+ return $this->getFieldName($this->getColumnName($fieldName));
+ } else {
+ return false;
+ }
+ }
+
+ public function __call($method, $arguments)
+ {
+ if (substr($method, 0, 6) == 'findBy') {
+ $by = substr($method, 6, strlen($method));
+ $method = 'findBy';
+ } else if (substr($method, 0, 9) == 'findOneBy') {
+ $by = substr($method, 9, strlen($method));
+ $method = 'findOneBy';
+ }
+
+ if (isset($by)) {
+ if ( ! isset($arguments[0])) {
+ throw new IPF_ORM_Exception('You must specify the value to findBy');
+ }
+
+ $fieldName = $this->_resolveFindByFieldName($by);
+ $hydrationMode = isset($arguments[1]) ? $arguments[1]:null;
+ if ($this->hasField($fieldName)) {
+ return $this->$method($fieldName, $arguments[0], $hydrationMode);
+ } else if ($this->hasRelation($by)) {
+ $relation = $this->getRelation($by);
+
+ if ($relation['type'] === IPF_ORM_Relation::MANY) {
+ throw new IPF_ORM_Exception('Cannot findBy many relationship.');
+ }
+
+ return $this->$method($relation['local'], $arguments[0], $hydrationMode);
+ } else {
+ throw new IPF_ORM_Exception('Cannot find by: ' . $by . '. Invalid column or relationship alias.');
+ }
+ }
+
+ throw new IPF_ORM_Exception(sprintf('Unknown method %s::%s', get_class($this), $method));
+ }
+}
diff --git a/ipf/orm/table/repository.php b/ipf/orm/table/repository.php
new file mode 100644
index 0000000..1fd1557
--- /dev/null
+++ b/ipf/orm/table/repository.php
@@ -0,0 +1,77 @@
+table = $table;
+ }
+
+ public function getTable()
+ {
+ return $this->table;
+ }
+
+ public function add(IPF_ORM_Record $record)
+ {
+ $oid = $record->getOID();
+
+ if (isset($this->registry[$oid])) {
+ return false;
+ }
+ $this->registry[$oid] = $record;
+
+ return true;
+ }
+
+ public function get($oid)
+ {
+ if ( ! isset($this->registry[$oid])) {
+ throw new IPF_ORM_Exception("Unknown object identifier");
+ }
+ return $this->registry[$oid];
+ }
+
+ public function count()
+ {
+ return count($this->registry);
+ }
+
+ public function evict($oid)
+ {
+ if ( ! isset($this->registry[$oid])) {
+ return false;
+ }
+ unset($this->registry[$oid]);
+ return true;
+ }
+
+ public function evictAll()
+ {
+ $evicted = 0;
+ foreach ($this->registry as $oid=>$record) {
+ if ($this->evict($oid)) {
+ $evicted++;
+ }
+ }
+ return $evicted;
+ }
+
+ public function getIterator()
+ {
+ return new ArrayIterator($this->registry);
+ }
+
+ public function contains($oid)
+ {
+ return isset($this->registry[$oid]);
+ }
+
+ public function loadAll()
+ {
+ $this->table->findAll();
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/template.php b/ipf/orm/template.php
new file mode 100644
index 0000000..3de6e4d
--- /dev/null
+++ b/ipf/orm/template.php
@@ -0,0 +1,58 @@
+_table = $table;
+ }
+
+ public function getTable()
+ {
+ return $this->_table;
+ }
+
+ public function setInvoker(IPF_ORM_Record $invoker)
+ {
+ $this->_invoker = $invoker;
+ }
+
+ public function getInvoker()
+ {
+ return $this->_invoker;
+ }
+
+ public function addChild(IPF_ORM_Template $template)
+ {
+ $this->_plugin->addChild($template);
+
+ return $this;
+ }
+
+ public function getPlugin()
+ {
+ return $this->_plugin;
+ }
+
+ public function get($name)
+ {
+ throw new IPF_ORM_Exception("Templates doesn't support accessors.");
+ }
+
+ public function set($name, $value)
+ {
+ throw new IPF_ORM_Exception("Templates doesn't support accessors.");
+ }
+
+ public function setUp()
+ {
+
+ }
+
+ public function setTableDefinition()
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/template/listener/timestampable.php b/ipf/orm/template/listener/timestampable.php
new file mode 100644
index 0000000..6905d7b
--- /dev/null
+++ b/ipf/orm/template/listener/timestampable.php
@@ -0,0 +1,48 @@
+_options = $options;
+ }
+
+ public function preInsert(IPF_ORM_Event $event)
+ {
+ if( ! $this->_options['created']['disabled']) {
+ $createdName = $this->_options['created']['name'];
+ $event->getInvoker()->$createdName = $this->getTimestamp('created');
+ }
+
+ if( ! $this->_options['updated']['disabled'] && $this->_options['updated']['onInsert']) {
+ $updatedName = $this->_options['updated']['name'];
+ $event->getInvoker()->$updatedName = $this->getTimestamp('updated');
+ }
+ }
+
+ public function preUpdate(IPF_ORM_Event $event)
+ {
+ if( ! $this->_options['updated']['disabled']) {
+ $updatedName = $this->_options['updated']['name'];
+ $event->getInvoker()->$updatedName = $this->getTimestamp('updated');
+ }
+ }
+
+ public function getTimestamp($type)
+ {
+ $options = $this->_options[$type];
+
+ if ($options['expression'] !== false && is_string($options['expression'])) {
+ return new IPF_ORM_Expression($options['expression']);
+ } else {
+ if ($options['type'] == 'date') {
+ return date($options['format'], time());
+ } else if ($options['type'] == 'timestamp') {
+ return date($options['format'], time());
+ } else {
+ return time();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/template/timestampable.php b/ipf/orm/template/timestampable.php
new file mode 100644
index 0000000..f39a19b
--- /dev/null
+++ b/ipf/orm/template/timestampable.php
@@ -0,0 +1,34 @@
+ array('name' => 'created_at',
+ 'type' => 'timestamp',
+ 'format' => 'Y-m-d H:i:s',
+ 'disabled' => false,
+ 'expression' => false,
+ 'options' => array()),
+ 'updated' => array('name' => 'updated_at',
+ 'type' => 'timestamp',
+ 'format' => 'Y-m-d H:i:s',
+ 'disabled' => false,
+ 'expression' => false,
+ 'onInsert' => true,
+ 'options' => array()));
+
+ public function __construct(array $options = array())
+ {
+ $this->_options = IPF_ORM_Utils::arrayDeepMerge($this->_options, $options);
+ }
+
+ public function setTableDefinition()
+ {
+ if( ! $this->_options['created']['disabled']) {
+ $this->hasColumn($this->_options['created']['name'], $this->_options['created']['type'], null, $this->_options['created']['options']);
+ }
+ if( ! $this->_options['updated']['disabled']) {
+ $this->hasColumn($this->_options['updated']['name'], $this->_options['updated']['type'], null, $this->_options['updated']['options']);
+ }
+ $this->addListener(new IPF_ORM_Template_Listener_Timestampable($this->_options));
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/transaction.php b/ipf/orm/transaction.php
new file mode 100644
index 0000000..b4d08d6
--- /dev/null
+++ b/ipf/orm/transaction.php
@@ -0,0 +1,279 @@
+_collections[] = $coll;
+
+ return $this;
+ }
+
+ public function getState()
+ {
+ switch ($this->_nestingLevel) {
+ case 0:
+ return IPF_ORM_Transaction::STATE_SLEEP;
+ break;
+ case 1:
+ return IPF_ORM_Transaction::STATE_ACTIVE;
+ break;
+ default:
+ return IPF_ORM_Transaction::STATE_BUSY;
+ }
+ }
+
+ public function addInvalid(IPF_ORM_Record $record)
+ {
+ if (in_array($record, $this->invalid, true)) {
+ return false;
+ }
+ $this->invalid[] = $record;
+ return true;
+ }
+
+ public function getInvalid()
+ {
+ return $this->invalid;
+ }
+
+ public function getTransactionLevel()
+ {
+ return $this->_nestingLevel;
+ }
+
+ public function getInternalTransactionLevel()
+ {
+ return $this->_internalNestingLevel;
+ }
+
+ public function beginTransaction($savepoint = null)
+ {
+ $this->conn->connect();
+ $listener = $this->conn->getAttribute(IPF_ORM::ATTR_LISTENER);
+
+
+ if ( ! is_null($savepoint)) {
+ $this->savePoints[] = $savepoint;
+
+ $event = new IPF_ORM_Event($this, IPF_ORM_Event::SAVEPOINT_CREATE);
+
+ $listener->preSavepointCreate($event);
+
+ if ( ! $event->skipOperation) {
+ $this->createSavePoint($savepoint);
+ }
+
+ $listener->postSavepointCreate($event);
+ } else {
+ if ($this->_nestingLevel == 0) {
+ $event = new IPF_ORM_Event($this, IPF_ORM_Event::TX_BEGIN);
+
+ $listener->preTransactionBegin($event);
+
+ if ( ! $event->skipOperation) {
+ try {
+ $this->_doBeginTransaction();
+ } catch (Exception $e) {
+ throw new IPF_ORM_Exception($e->getMessage());
+ }
+ }
+ $listener->postTransactionBegin($event);
+ }
+ }
+
+ $level = ++$this->_nestingLevel;
+
+ return $level;
+ }
+
+ public function commit($savepoint = null)
+ {
+ if ($this->_nestingLevel == 0) {
+ throw new IPF_ORM_Exception("Commit failed. There is no active transaction.");
+ }
+
+ $this->conn->connect();
+
+ $listener = $this->conn->getAttribute(IPF_ORM::ATTR_LISTENER);
+
+ if ( ! is_null($savepoint)) {
+ $this->_nestingLevel -= $this->removeSavePoints($savepoint);
+
+ $event = new IPF_ORM_Event($this, IPF_ORM_Event::SAVEPOINT_COMMIT);
+
+ $listener->preSavepointCommit($event);
+
+ if ( ! $event->skipOperation) {
+ $this->releaseSavePoint($savepoint);
+ }
+
+ $listener->postSavepointCommit($event);
+ } else {
+
+ if ($this->_nestingLevel == 1 || $this->_internalNestingLevel == 1) {
+ if ( ! empty($this->invalid)) {
+ if ($this->_internalNestingLevel == 1) {
+ // transaction was started by IPF_ORM, so we are responsible
+ // for a rollback
+ $this->rollback();
+ $tmp = $this->invalid;
+ $this->invalid = array();
+ throw new IPF_ORM_Exception_Validator($tmp);
+ }
+ }
+ if ($this->_nestingLevel == 1) {
+ // take snapshots of all collections used within this transaction
+ foreach ($this->_collections as $coll) {
+ $coll->takeSnapshot();
+ }
+ $this->_collections = array();
+
+ $event = new IPF_ORM_Event($this, IPF_ORM_Event::TX_COMMIT);
+
+ $listener->preTransactionCommit($event);
+ if ( ! $event->skipOperation) {
+ $this->_doCommit();
+ }
+ $listener->postTransactionCommit($event);
+ }
+ }
+
+ if ($this->_nestingLevel > 0) {
+ $this->_nestingLevel--;
+ }
+ if ($this->_internalNestingLevel > 0) {
+ $this->_internalNestingLevel--;
+ }
+ }
+
+ return true;
+ }
+
+ public function rollback($savepoint = null)
+ {
+ if ($this->_nestingLevel == 0) {
+ throw new IPF_ORM_Exception("Rollback failed. There is no active transaction.");
+ }
+
+ $this->conn->connect();
+
+ if ($this->_internalNestingLevel > 1 || $this->_nestingLevel > 1) {
+ $this->_internalNestingLevel--;
+ $this->_nestingLevel--;
+ return false;
+ }
+
+ $listener = $this->conn->getAttribute(IPF_ORM::ATTR_LISTENER);
+
+ if ( ! is_null($savepoint)) {
+ $this->_nestingLevel -= $this->removeSavePoints($savepoint);
+
+ $event = new IPF_ORM_Event($this, IPF_ORM_Event::SAVEPOINT_ROLLBACK);
+
+ $listener->preSavepointRollback($event);
+
+ if ( ! $event->skipOperation) {
+ $this->rollbackSavePoint($savepoint);
+ }
+
+ $listener->postSavepointRollback($event);
+ } else {
+ $event = new IPF_ORM_Event($this, IPF_ORM_Event::TX_ROLLBACK);
+
+ $listener->preTransactionRollback($event);
+
+ if ( ! $event->skipOperation) {
+ $this->_nestingLevel = 0;
+ $this->_internalNestingLevel = 0;
+ try {
+ $this->_doRollback();
+ } catch (Exception $e) {
+ throw new IPF_ORM_Exception($e->getMessage());
+ }
+ }
+
+ $listener->postTransactionRollback($event);
+ }
+
+ return true;
+ }
+
+ protected function createSavePoint($savepoint)
+ {
+ throw new IPF_ORM_Exception('Savepoints not supported by this driver.');
+ }
+
+ protected function releaseSavePoint($savepoint)
+ {
+ throw new IPF_ORM_Exception('Savepoints not supported by this driver.');
+ }
+
+ protected function rollbackSavePoint($savepoint)
+ {
+ throw new IPF_ORM_Exception('Savepoints not supported by this driver.');
+ }
+
+ protected function _doRollback()
+ {
+ $this->conn->getDbh()->rollback();
+ }
+
+ protected function _doCommit()
+ {
+ $this->conn->getDbh()->commit();
+ }
+
+ protected function _doBeginTransaction()
+ {
+ $this->conn->getDbh()->beginTransaction();
+ }
+
+ private function removeSavePoints($savepoint)
+ {
+ $this->savePoints = array_values($this->savePoints);
+
+ $found = false;
+ $i = 0;
+
+ foreach ($this->savePoints as $key => $sp) {
+ if ( ! $found) {
+ if ($sp === $savepoint) {
+ $found = true;
+ }
+ }
+ if ($found) {
+ $i++;
+ unset($this->savePoints[$key]);
+ }
+ }
+
+ return $i;
+ }
+
+ public function setIsolation($isolation)
+ {
+ throw new IPF_ORM_Exception('Transaction isolation levels not supported by this driver.');
+ }
+
+ public function getIsolation()
+ {
+ throw new IPF_ORM_Exception('Fetching transaction isolation level not supported by this driver.');
+ }
+
+ public function beginInternalTransaction($savepoint = null)
+ {
+ $this->_internalNestingLevel++;
+ return $this->beginTransaction($savepoint);
+ }
+}
diff --git a/ipf/orm/transaction/mysql.php b/ipf/orm/transaction/mysql.php
new file mode 100644
index 0000000..20597ff
--- /dev/null
+++ b/ipf/orm/transaction/mysql.php
@@ -0,0 +1,47 @@
+conn->execute($query);
+ }
+
+ protected function releaseSavePoint($savepoint)
+ {
+ $query = 'RELEASE SAVEPOINT ' . $savepoint;
+
+ return $this->conn->execute($query);
+ }
+
+ protected function rollbackSavePoint($savepoint)
+ {
+ $query = 'ROLLBACK TO SAVEPOINT ' . $savepoint;
+
+ return $this->conn->execute($query);
+ }
+
+ public function setIsolation($isolation)
+ {
+ switch ($isolation) {
+ case 'READ UNCOMMITTED':
+ case 'READ COMMITTED':
+ case 'REPEATABLE READ':
+ case 'SERIALIZABLE':
+ break;
+ default:
+ throw new IPF_ORM_Exception('Isolation level ' . $isolation . ' is not supported.');
+ }
+
+ $query = 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $isolation;
+
+ return $this->conn->execute($query);
+ }
+
+ public function getIsolation()
+ {
+ return $this->conn->fetchOne('SELECT @@tx_isolation');
+ }
+}
\ No newline at end of file
diff --git a/ipf/orm/utils.php b/ipf/orm/utils.php
new file mode 100644
index 0000000..5afd7da
--- /dev/null
+++ b/ipf/orm/utils.php
@@ -0,0 +1,195 @@
+';
+ $r[] = 'IPF_ORM_Connection object';
+ $r[] = 'State : ' . IPF_ORM_Utils::getConnectionStateAsString($connection->transaction->getState());
+ $r[] = 'Open Transactions : ' . $connection->transaction->getTransactionLevel();
+ $r[] = 'Table in memory : ' . $connection->count();
+ $r[] = 'Driver name : ' . $connection->getAttribute(IPF_ORM::ATTR_DRIVER_NAME);
+ $r[] = "";
+ return implode("\n",$r).""; + $r[] = "Component : ".$table->getComponentName(); + $r[] = "Table : ".$table->getTableName(); + $r[] = ""; + + return implode("\n",$r)."
"; + $r[] = get_class($collection); + $r[] = 'data : ' . IPF_ORM::dump($collection->getData(), false); + //$r[] = 'snapshot : ' . IPF_ORM::dump($collection->getSnapshot()); + $r[] = ""; + return implode("\n",$r); + } + + +} + diff --git a/ipf/orm/validator.php b/ipf/orm/validator.php new file mode 100644 index 0000000..0fd7340 --- /dev/null +++ b/ipf/orm/validator.php @@ -0,0 +1,98 @@ +getTable(); + + // if record is transient all fields will be validated + // if record is persistent only the modified fields will be validated + $fields = $record->exists() ? $record->getModified():$record->getData(); + foreach ($fields as $fieldName => $value) { + $table->validateField($fieldName, $value, $record); + } + } + + public static function validateLength($value, $type, $maximumLength) + { + if ($type == 'timestamp' || $type == 'integer' || $type == 'enum') { + return true; + } else if ($type == 'array' || $type == 'object') { + $length = strlen(serialize($value)); + } else { + $length = strlen($value); + } + if ($length > $maximumLength) { + return false; + } + return true; + } + + public function hasErrors() + { + return (count($this->stack) > 0); + } + + public static function isValidType($var, $type) + { + if ($var instanceof IPF_ORM_Expression) { + return true; + } else if ($var === null) { + return true; + } else if (is_object($var)) { + return $type == 'object'; + } + + switch ($type) { + case 'float': + case 'double': + case 'decimal': + return (string)$var == strval(floatval($var)); + case 'integer': + return (string)$var == strval(intval($var)); + case 'string': + return is_string($var) || is_numeric($var); + case 'blob': + case 'clob': + case 'gzip': + return is_string($var); + case 'array': + return is_array($var); + case 'object': + return is_object($var); + case 'boolean': + return is_bool($var) || (is_numeric($var) && ($var == 0 || $var == 1)); + case 'timestamp': + $validator = self::getValidator('timestamp'); + return $validator->validate($var); + case 'time': + $validator = self::getValidator('time'); + return $validator->validate($var); + case 'date': + $validator = self::getValidator('date'); + return $validator->validate($var); + case 'enum': + return is_string($var) || is_int($var); + default: + return false; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/country.php b/ipf/orm/validator/country.php new file mode 100644 index 0000000..2536f15 --- /dev/null +++ b/ipf/orm/validator/country.php @@ -0,0 +1,260 @@ + 'Andorra', + 'ae' => 'United Arab Emirates', + 'af' => 'Afghanistan', + 'ag' => 'Antigua and Barbuda', + 'ai' => 'Anguilla', + 'al' => 'Albania', + 'am' => 'Armenia', + 'an' => 'Netherlands Antilles', + 'ao' => 'Angola', + 'aq' => 'Antarctica', + 'ar' => 'Argentina', + 'as' => 'American Samoa', + 'at' => 'Austria', + 'au' => 'Australia', + 'aw' => 'Aruba', + 'az' => 'Azerbaijan', + 'ba' => 'Bosnia Hercegovina', + 'bb' => 'Barbados', + 'bd' => 'Bangladesh', + 'be' => 'Belgium', + 'bf' => 'Burkina Faso', + 'bg' => 'Bulgaria', + 'bh' => 'Bahrain', + 'bi' => 'Burundi', + 'bj' => 'Benin', + 'bm' => 'Bermuda', + 'bn' => 'Brunei Darussalam', + 'bo' => 'Bolivia', + 'br' => 'Brazil', + 'bs' => 'Bahamas', + 'bt' => 'Bhutan', + 'bv' => 'Bouvet Island', + 'bw' => 'Botswana', + 'by' => 'Belarus (Byelorussia)', + 'bz' => 'Belize', + 'ca' => 'Canada', + 'cc' => 'Cocos Islands', + 'cd' => 'Congo, The Democratic Republic of the', + 'cf' => 'Central African Republic', + 'cg' => 'Congo', + 'ch' => 'Switzerland', + 'ci' => 'Ivory Coast', + 'ck' => 'Cook Islands', + 'cl' => 'Chile', + 'cm' => 'Cameroon', + 'cn' => 'China', + 'co' => 'Colombia', + 'cr' => 'Costa Rica', + 'cs' => 'Czechoslovakia', + 'cu' => 'Cuba', + 'cv' => 'Cape Verde', + 'cx' => 'Christmas Island', + 'cy' => 'Cyprus', + 'cz' => 'Czech Republic', + 'de' => 'Germany', + 'dj' => 'Djibouti', + 'dk' => 'Denmark', + 'dm' => 'Dominica', + 'do' => 'Dominican Republic', + 'dz' => 'Algeria', + 'ec' => 'Ecuador', + 'ee' => 'Estonia', + 'eg' => 'Egypt', + 'eh' => 'Western Sahara', + 'er' => 'Eritrea', + 'es' => 'Spain', + 'et' => 'Ethiopia', + 'fi' => 'Finland', + 'fj' => 'Fiji', + 'fk' => 'Falkland Islands', + 'fm' => 'Micronesia', + 'fo' => 'Faroe Islands', + 'fr' => 'France', + 'fx' => 'France, Metropolitan FX', + 'ga' => 'Gabon', + 'gb' => 'United Kingdom (Great Britain)', + 'gd' => 'Grenada', + 'ge' => 'Georgia', + 'gf' => 'French Guiana', + 'gh' => 'Ghana', + 'gi' => 'Gibraltar', + 'gl' => 'Greenland', + 'gm' => 'Gambia', + 'gn' => 'Guinea', + 'gp' => 'Guadeloupe', + 'gq' => 'Equatorial Guinea', + 'gr' => 'Greece', + 'gs' => 'South Georgia and the South Sandwich Islands', + 'gt' => 'Guatemala', + 'gu' => 'Guam', + 'gw' => 'Guinea-bissau', + 'gy' => 'Guyana', + 'hk' => 'Hong Kong', + 'hm' => 'Heard and McDonald Islands', + 'hn' => 'Honduras', + 'hr' => 'Croatia', + 'ht' => 'Haiti', + 'hu' => 'Hungary', + 'id' => 'Indonesia', + 'ie' => 'Ireland', + 'il' => 'Israel', + 'in' => 'India', + 'io' => 'British Indian Ocean Territory', + 'iq' => 'Iraq', + 'ir' => 'Iran', + 'is' => 'Iceland', + 'it' => 'Italy', + 'jm' => 'Jamaica', + 'jo' => 'Jordan', + 'jp' => 'Japan', + 'ke' => 'Kenya', + 'kg' => 'Kyrgyzstan', + 'kh' => 'Cambodia', + 'ki' => 'Kiribati', + 'km' => 'Comoros', + 'kn' => 'Saint Kitts and Nevis', + 'kp' => 'North Korea', + 'kr' => 'South Korea', + 'kw' => 'Kuwait', + 'ky' => 'Cayman Islands', + 'kz' => 'Kazakhstan', + 'la' => 'Laos', + 'lb' => 'Lebanon', + 'lc' => 'Saint Lucia', + 'li' => 'Lichtenstein', + 'lk' => 'Sri Lanka', + 'lr' => 'Liberia', + 'ls' => 'Lesotho', + 'lt' => 'Lithuania', + 'lu' => 'Luxembourg', + 'lv' => 'Latvia', + 'ly' => 'Libya', + 'ma' => 'Morocco', + 'mc' => 'Monaco', + 'md' => 'Moldova Republic', + 'mg' => 'Madagascar', + 'mh' => 'Marshall Islands', + 'mk' => 'Macedonia, The Former Yugoslav Republic of', + 'ml' => 'Mali', + 'mm' => 'Myanmar', + 'mn' => 'Mongolia', + 'mo' => 'Macau', + 'mp' => 'Northern Mariana Islands', + 'mq' => 'Martinique', + 'mr' => 'Mauritania', + 'ms' => 'Montserrat', + 'mt' => 'Malta', + 'mu' => 'Mauritius', + 'mv' => 'Maldives', + 'mw' => 'Malawi', + 'mx' => 'Mexico', + 'my' => 'Malaysia', + 'mz' => 'Mozambique', + 'na' => 'Namibia', + 'nc' => 'New Caledonia', + 'ne' => 'Niger', + 'nf' => 'Norfolk Island', + 'ng' => 'Nigeria', + 'ni' => 'Nicaragua', + 'nl' => 'Netherlands', + 'no' => 'Norway', + 'np' => 'Nepal', + 'nr' => 'Nauru', + 'nt' => 'Neutral Zone', + 'nu' => 'Niue', + 'nz' => 'New Zealand', + 'om' => 'Oman', + 'pa' => 'Panama', + 'pe' => 'Peru', + 'pf' => 'French Polynesia', + 'pg' => 'Papua New Guinea', + 'ph' => 'Philippines', + 'pk' => 'Pakistan', + 'pl' => 'Poland', + 'pm' => 'St. Pierre and Miquelon', + 'pn' => 'Pitcairn', + 'pr' => 'Puerto Rico', + 'pt' => 'Portugal', + 'pw' => 'Palau', + 'py' => 'Paraguay', + 'qa' => 'Qatar', + 're' => 'Reunion', + 'ro' => 'Romania', + 'ru' => 'Russia', + 'rw' => 'Rwanda', + 'sa' => 'Saudi Arabia', + 'sb' => 'Solomon Islands', + 'sc' => 'Seychelles', + 'sd' => 'Sudan', + 'se' => 'Sweden', + 'sg' => 'Singapore', + 'sh' => 'St. Helena', + 'si' => 'Slovenia', + 'sj' => 'Svalbard and Jan Mayen Islands', + 'sk' => 'Slovakia (Slovak Republic)', + 'sl' => 'Sierra Leone', + 'sm' => 'San Marino', + 'sn' => 'Senegal', + 'so' => 'Somalia', + 'sr' => 'Suriname', + 'st' => 'Sao Tome and Principe', + 'sv' => 'El Salvador', + 'sy' => 'Syria', + 'sz' => 'Swaziland', + 'tc' => 'Turks and Caicos Islands', + 'td' => 'Chad', + 'tf' => 'French Southern Territories', + 'tg' => 'Togo', + 'th' => 'Thailand', + 'tj' => 'Tajikistan', + 'tk' => 'Tokelau', + 'tm' => 'Turkmenistan', + 'tn' => 'Tunisia', + 'to' => 'Tonga', + 'tp' => 'East Timor', + 'tr' => 'Turkey', + 'tt' => 'Trinidad, Tobago', + 'tv' => 'Tuvalu', + 'tw' => 'Taiwan', + 'tz' => 'Tanzania', + 'ua' => 'Ukraine', + 'ug' => 'Uganda', + 'uk' => 'United Kingdom', + 'um' => 'United States Minor Islands', + 'us' => 'United States of America', + 'uy' => 'Uruguay', + 'uz' => 'Uzbekistan', + 'va' => 'Vatican City', + 'vc' => 'Saint Vincent, Grenadines', + 've' => 'Venezuela', + 'vg' => 'Virgin Islands (British)', + 'vi' => 'Virgin Islands (USA)', + 'vn' => 'Viet Nam', + 'vu' => 'Vanuatu', + 'wf' => 'Wallis and Futuna Islands', + 'ws' => 'Samoa', + 'ye' => 'Yemen', + 'yt' => 'Mayotte', + 'yu' => 'Yugoslavia', + 'za' => 'South Africa', + 'zm' => 'Zambia', + 'zr' => 'Zaire', + 'zw' => 'Zimbabwe'); + + public static function getCountries() + { + return self::$countries; + } + + public function validate($value) + { + $value = strtolower($value); + return isset(self::$countries[$value]); + } +} \ No newline at end of file diff --git a/ipf/orm/validator/creditcard.php b/ipf/orm/validator/creditcard.php new file mode 100644 index 0000000..f4a59f6 --- /dev/null +++ b/ipf/orm/validator/creditcard.php @@ -0,0 +1,45 @@ + 'visa', + "/^5[12345]\d{14}$/" => 'mastercard', + "/^3[47]\d{13}$/" => 'amex', + "/^6011\d{12}$/" => 'discover', + "/^30[012345]\d{11}$/" => 'diners', + "/^3[68]\d{12}$/" => 'diners', + ); + foreach ($card_regexes as $regex => $type) { + if (preg_match($regex, $value)) { + $cardType = $type; + break; + } + } + if ( ! $cardType) { + return false; + } + /* mod 10 checksum algorithm */ + $revcode = strrev($value); + $checksum = 0; + for ($i = 0; $i < strlen($revcode); $i++) { + $currentNum = intval($revcode[$i]); + if ($i & 1) { /* Odd position */ + $currentNum *= 2; + } + /* Split digits and add. */ + $checksum += $currentNum % 10; + if ($currentNum > 9) { + $checksum += 1; + } + } + if ($checksum % 10 == 0) { + return true; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/date.php b/ipf/orm/validator/date.php new file mode 100644 index 0000000..b0916a9 --- /dev/null +++ b/ipf/orm/validator/date.php @@ -0,0 +1,17 @@ +_args[$arg])) { + return $this->_args[$arg]; + } + return null; + } + + public function __isset($arg) + { + return isset($this->_args[$arg]); + } + + public function __set($arg, $value) + { + $this->_args[$arg] = $value; + + return $this; + } + + public function getArg($arg) + { + if ( ! isset($this->_args[$arg])) { + throw new IPF_ORM_Exception_Validator('Unknown option ' . $arg); + } + return $this->_args[$arg]; + } + + public function setArg($arg, $value) + { + $this->_args[$arg] = $value; + + return $this; + } + + public function getArgs() + { + return $this->_args; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/email.php b/ipf/orm/validator/email.php new file mode 100644 index 0000000..6e07505 --- /dev/null +++ b/ipf/orm/validator/email.php @@ -0,0 +1,11 @@ +_className = $className; + } + + public function add($invalidFieldName, $errorCode = 'general') + { + // FIXME: In the future the error stack should contain nothing but validator objects + if (is_object($errorCode) && strpos(get_class($errorCode), 'IPF_ORM_Validator_') !== false) { + $validator = $errorCode; + $this->_validators[$invalidFieldName][] = $validator; + $className = get_class($errorCode); + $errorCode = strtolower(substr($className, strlen('IPF_ORM_Validator_'), strlen($className))); + } + + $this->_errors[$invalidFieldName][] = $errorCode; + } + + public function remove($fieldName) + { + if (isset($this->_errors[$fieldName])) { + unset($this->_errors[$fieldName]); + } + } + + public function get($fieldName) + { + return isset($this->_errors[$fieldName]) ? $this->_errors[$fieldName] : null; + } + + public function set($fieldName, $errorCode) + { + $this->add($fieldName, $errorCode); + } + + public function contains($fieldName) + { + return array_key_exists($fieldName, $this->_errors); + } + + public function clear() + { + $this->_errors = array(); + } + + public function getIterator() + { + return new ArrayIterator($this->_errors); + } + + public function toArray() + { + return $this->_errors; + } + + public function count() + { + return count($this->_errors); + } + + public function getClassname() + { + return $this->_className; + } + + public function getValidators() + { + return $this->_validators; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/future.php b/ipf/orm/validator/future.php new file mode 100644 index 0000000..b075f38 --- /dev/null +++ b/ipf/orm/validator/future.php @@ -0,0 +1,43 @@ +args) && isset($this->args['timezone'])) { + switch (strtolower($this->args['timezone'])) { + case 'gmt': + $now = gmdate("U") - date("Z"); + break; + default: + $now = getdate(); + break; + } + } else { + $now = getdate(); + } + + if ($now['year'] > $e[0]) { + return false; + } else if ($now['year'] == $e[0]) { + if ($now['mon'] > $e[1]) { + return false; + } else if ($now['mon'] == $e[1]) { + return $now['mday'] < $e[2]; + } else { + return true; + } + } else { + return true; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/htmlcolor.php b/ipf/orm/validator/htmlcolor.php new file mode 100644 index 0000000..59cf78d --- /dev/null +++ b/ipf/orm/validator/htmlcolor.php @@ -0,0 +1,12 @@ +args) && strlen($value) < $this->args) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/nospace.php b/ipf/orm/validator/nospace.php new file mode 100644 index 0000000..b9f326c --- /dev/null +++ b/ipf/orm/validator/nospace.php @@ -0,0 +1,9 @@ +args) && isset($this->args['timezone'])) { + switch (strtolower($this->args['timezone'])) { + case 'gmt': + $now = gmdate("U") - date("Z"); + break; + default: + $now = getdate(); + break; + } + } else { + $now = getdate(); + } + + if ($now['year'] < $e[0]) { + return false; + } else if ($now['year'] == $e[0]) { + if ($now['mon'] < $e[1]) { + return false; + } else if ($now['mon'] == $e[1]) { + return $now['mday'] > $e[2]; + } else { + return true; + } + } else { + return true; + } + } +} \ No newline at end of file diff --git a/ipf/orm/validator/range.php b/ipf/orm/validator/range.php new file mode 100644 index 0000000..2a895d3 --- /dev/null +++ b/ipf/orm/validator/range.php @@ -0,0 +1,15 @@ +args[0]) && $value < $this->args[0]) { + return false; + } + if (isset($this->args[1]) && $value > $this->args[1]) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/readonly.php b/ipf/orm/validator/readonly.php new file mode 100644 index 0000000..dac58f8 --- /dev/null +++ b/ipf/orm/validator/readonly.php @@ -0,0 +1,10 @@ +invoker->getModified(); + return array_key_exists($this->field, $modified) ? false : true; + } +} diff --git a/ipf/orm/validator/regexp.php b/ipf/orm/validator/regexp.php new file mode 100644 index 0000000..3981f13 --- /dev/null +++ b/ipf/orm/validator/regexp.php @@ -0,0 +1,25 @@ +args)) { + return true; + } + if (is_array($this->args)) { + foreach ($this->args as $regexp) { + if ( ! preg_match($regexp, $value)) { + return false; + } + } + return true; + } else { + if (preg_match($this->args, $value)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/time.php b/ipf/orm/validator/time.php new file mode 100644 index 0000000..7c90fd4 --- /dev/null +++ b/ipf/orm/validator/time.php @@ -0,0 +1,27 @@ += 0 && $hr <= 23 && $min >= 0 && $min <= 59 && $sec >= 0 && $sec <= 59; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/timestamp.php b/ipf/orm/validator/timestamp.php new file mode 100644 index 0000000..b214308 --- /dev/null +++ b/ipf/orm/validator/timestamp.php @@ -0,0 +1,30 @@ +validate($date)) { + return false; + } + + if ( ! $timeValidator->validate($time)) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/ipf/orm/validator/unique.php b/ipf/orm/validator/unique.php new file mode 100644 index 0000000..8f03404 --- /dev/null +++ b/ipf/orm/validator/unique.php @@ -0,0 +1,35 @@ +invoker->getTable(); + $pks = $table->getIdentifier(); + + if ( is_array($pks) ) { + $pks = join(',', $pks); + } + + $sql = 'SELECT ' . $pks . ' FROM ' . $table->getTableName() . ' WHERE ' . $this->field . ' = ?'; + + $values = array(); + $values[] = $value; + + // If the record is not new we need to add primary key checks because its ok if the + // unique value already exists in the database IF the record in the database is the same + // as the one that is validated here. + $state = $this->invoker->state(); + if ( ! ($state == IPF_ORM_Record::STATE_TDIRTY || $state == IPF_ORM_Record::STATE_TCLEAN)) { + foreach ((array) $table->getIdentifier() as $pk) { + $sql .= " AND {$pk} != ?"; + $values[] = $this->invoker->$pk; + } + } + + $stmt = $table->getConnection()->getDbh()->prepare($sql); + $stmt->execute($values); + + return ( ! is_array($stmt->fetch())); + } +} \ No newline at end of file diff --git a/ipf/orm/validator/unsigned.php b/ipf/orm/validator/unsigned.php new file mode 100644 index 0000000..7f6cf1e --- /dev/null +++ b/ipf/orm/validator/unsigned.php @@ -0,0 +1,14 @@ + true, + 'AL' => true, + 'AR' => true, + 'AZ' => true, + 'CA' => true, + 'CO' => true, + 'CT' => true, + 'DC' => true, + 'DE' => true, + 'FL' => true, + 'GA' => true, + 'HI' => true, + 'IA' => true, + 'ID' => true, + 'IL' => true, + 'IN' => true, + 'KS' => true, + 'KY' => true, + 'LA' => true, + 'MA' => true, + 'MD' => true, + 'ME' => true, + 'MI' => true, + 'MN' => true, + 'MO' => true, + 'MS' => true, + 'MT' => true, + 'NC' => true, + 'ND' => true, + 'NE' => true, + 'NH' => true, + 'NJ' => true, + 'NM' => true, + 'NV' => true, + 'NY' => true, + 'OH' => true, + 'OK' => true, + 'OR' => true, + 'PA' => true, + 'PR' => true, + 'RI' => true, + 'SC' => true, + 'SD' => true, + 'TN' => true, + 'TX' => true, + 'UT' => true, + 'VA' => true, + 'VI' => true, + 'VT' => true, + 'WA' => true, + 'WI' => true, + 'WV' => true, + 'WY' => true + ); + public function getStates() + { + return self::$states; + } + + public function validate($value) + { + return isset(self::$states[$value]); + } +} \ No newline at end of file diff --git a/ipf/project.php b/ipf/project.php new file mode 100644 index 0000000..932c290 --- /dev/null +++ b/ipf/project.php @@ -0,0 +1,94 @@ +apps[$appname] = null; + } + } + + private function __clone(){ + } + + private function appClassName($name){ + return $name.'_App'; + } + + public function appList(){ + return $this->apps; + } + + // Lazy Application Loader + public function getApp($name){ + if (!array_key_exists($name,$this->apps)) + throw new IPF_Exception_Panic("Application \"$name\" not found"); + if ($this->apps[$name]==null){ + $className = $this->appClassName($name); + $this->apps[$name] = new $className(); + } + return $this->apps[$name]; + } + + public function checkApps(){ + foreach( $this->apps as $appname=>&$app) + $this->getApp($appname); + } + + public function generateModels(){ + foreach( $this->apps as $appname=>&$app) + $this->getApp($appname)->generateModels(); + } + + public function createTablesFromModels(){ + foreach( $this->apps as $appname=>&$app) + $this->getApp($appname)->createTablesFromModels(); + } + + public function generateSql(){ + $sql = ''; + foreach( $this->apps as $appname=>&$app) + $sql .= $this->getApp($appname)->generateSql(); + $sql .= "\n\n"; + return $sql; + } + + private function loadModels(){ + foreach( $this->apps as $appname=>&$app){ + $this->getApp($appname)->loadModels(); + } + } + + private function cli(){ + $cli = new IPF_Cli(); + $cli->run(); + } + + public function run(){ + $dsn = IPF::get('dsn'); + if ($dsn=='') + throw new IPF_Exception_Panic('Specify dsn in config file'); + IPF_ORM_Manager::connection($dsn); + $this->loadModels(); + if (php_sapi_name()=='cli'){ + $this->cli(); + return; + } + IPF_ORM_Manager::getInstance()->setAttribute(IPF_ORM::ATTR_VALIDATE, IPF_ORM::VALIDATE_ALL); + $this->router = new IPF_Router(); + $this->router->dispatch(IPF_HTTP_URL::getAction()); + } +} \ No newline at end of file diff --git a/ipf/router.php b/ipf/router.php new file mode 100644 index 0000000..a8c7a4f --- /dev/null +++ b/ipf/router.php @@ -0,0 +1,82 @@ +processRequest($req); + if ($response !== false) { + // $response is a response + $response->render($req->method != 'HEAD' and !defined('IN_UNIT_TESTS')); + $skip = true; + break; + } + } + } + if ($skip === false) { + $response = IPF_Router::match($req); + if (!empty($req->response_vary_on)) { + $response->headers['Vary'] = $req->response_vary_on; + } + $middleware = array_reverse($middleware); + foreach ($middleware as $mw) { + if (method_exists($mw, 'processResponse')) { + $response = $mw->processResponse($req, $response); + } + } + //var_dump($response); + $response->render($req->method != 'HEAD'); + } + return array($req, $response); + } + + public static function match($req) + { + foreach (IPF::get('urls') as $url) { + $prefix = $url['prefix']; + foreach ($url['urls'] as $suburl){ + $regex = $prefix.$suburl['regex']; + //print "Regex = $regex