1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: 97: 98: 99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533: 534: 535: 536: 537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562: 563: 564: 565: 566: 567: 568: 569: 570: 571: 572: 573: 574: 575: 576: 577: 578: 579: 580: 581: 582: 583: 584: 585: 586: 587: 588: 589: 590: 591: 592: 593: 594: 595: 596: 597: 598: 599: 600: 601: 602: 603: 604: 605: 606: 607: 608: 609: 610: 611: 612: 613: 614: 615: 616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626: 627: 628: 629: 630: 631: 632: 633: 634: 635: 636: 637: 638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676: 677: 678: 679: 680: 681: 682: 683: 684: 685: 686: 687: 688: 689: 690: 691: 692: 693: 694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704: 705:
<?php
namespace LeanMapperQuery;
use LeanMapper;
use LeanMapper\Fluent;
use LeanMapper\IMapper;
use LeanMapper\ImplicitFilters;
use LeanMapper\Reflection\Property;
use LeanMapper\Relationship;
use LeanMapperQuery\Exception\InvalidArgumentException;
use LeanMapperQuery\Exception\InvalidRelationshipException;
use LeanMapperQuery\Exception\InvalidStateException;
use LeanMapperQuery\Exception\MemberAccessException;
use LeanMapperQuery\Exception\NonExistingMethodException;
use LeanMapperQuery\Exception\NotImplementedException;
class Query implements IQuery, \Iterator
{
private static $defaultPlaceholder = '?';
private static $variablePatternFirstLetter = '[a-zA-Z_\x7f-\xff]';
private static $variablePatternOtherLetters = '[a-zA-Z0-9_\x7f-\xff]';
private static $typeFlagName = 'type';
private static $placeholders = [
'string' => '%s',
'boolean' => '%b',
'integer' => '%i',
'float' => '%f',
'DateTime' => '%t',
'Date' => '%d',
];
private $castedEntityClass = NULL;
protected $sourceTableName;
private $fluent = NULL;
protected $mapper;
protected $replacePlaceholders = FALSE;
private $queue = [];
private $limitQueue = [];
private $tablesAliases;
private $possibleJoin = NULL;
private $joinAlternative = [];
private function getPropertiesByTable($tableName)
{
$entityClass = $this->mapper->getEntityClass($tableName);
return $this->getPropertiesByEntity($entityClass);
}
private function getPropertiesByEntity($entityClass)
{
$reflection = $entityClass::getReflection($this->mapper);
$properties = [];
foreach ($reflection->getEntityProperties() as $property) {
$properties[$property->getName()] = $property;
}
return [$entityClass, $properties];
}
private function getTableAlias($currentTable, $targetTable, $viaColumn, &$globalKey, &$alias)
{
$localKey = $targetTable . '_' . $viaColumn;
$globalKey = $currentTable . '_' . $localKey;
if (array_key_exists($globalKey, $this->tablesAliases)) {
$alias = $this->tablesAliases[$globalKey];
return TRUE;
}
if (!in_array($targetTable, $this->tablesAliases)) {
$alias = $targetTable;
} elseif (!in_array($localKey, $this->tablesAliases)) {
$alias = $localKey;
} else {
$alias = $globalKey;
}
return FALSE;
}
private function registerTableAlias($globalKey, $alias)
{
if (array_key_exists($globalKey, $this->tablesAliases)) {
throw new InvalidStateException("Global key '$globalKey' is already registered.");
}
$this->tablesAliases[$globalKey] = $alias;
}
private function registerJoin($currentTable, $referencingColumn, $targetTable, $targetTablePrimaryKey, $globalKey, $alias)
{
if ($this->possibleJoin !== NULL) {
throw new InvalidStateException('Cannot register new join. There is one registered already.');
}
$this->possibleJoin = func_get_args();
$this->joinAlternative = [$currentTable, $referencingColumn];
}
private function triggerJoin()
{
if ($this->possibleJoin !== NULL) {
list($currentTable, $referencingColumn, $targetTable, $targetTablePrimaryKey, $globalKey, $alias) = $this->possibleJoin;
$this->fluent->leftJoin("[$targetTable]" . ($targetTable !== $alias ? " [$alias]" : ''))
->on("[$currentTable].[$referencingColumn] = [$alias].[$targetTablePrimaryKey]");
$this->tryAddGroupBy($this->fluent, $currentTable);
$this->registerTableAlias($globalKey, $alias);
$this->possibleJoin = NULL;
}
}
private function dismissJoin()
{
$this->possibleJoin = NULL;
return $this->joinAlternative;
}
private function pendingJoin()
{
return $this->possibleJoin !== NULL;
}
private function joinRelatedTable($currentTable, $referencingColumn, $targetTable, $targetTablePrimaryKey, $filters = [], $joinImmediately = TRUE)
{
if (!$this->getTableAlias($currentTable, $targetTable, $referencingColumn, $globalKey, $alias)) {
if (empty($filters)) {
$this->registerJoin($currentTable, $referencingColumn, $targetTable, $targetTablePrimaryKey, $globalKey, $alias);
$joinImmediately && $this->triggerJoin();
} else {
$subFluent = new Fluent($this->fluent->getConnection());
$subFluent->select('%n.*', $targetTable)->from($targetTable);
$targetedArgs = [];
if ($filters instanceof ImplicitFilters) {
$targetedArgs = $filters->getTargetedArgs();
$filters = $filters->getFilters();
}
foreach ($filters as $filter) {
$args = [$filter];
if (is_string($filter) && array_key_exists($filter, $targetedArgs)) {
$args = array_merge($args, $targetedArgs[$filter]);
}
call_user_func_array([$subFluent, 'applyFilter'], $args);
}
$this->fluent->leftJoin($subFluent, "[$alias]")
->on("[$currentTable].[$referencingColumn] = [$alias].[$targetTablePrimaryKey]");
$this->tryAddGroupBy($this->fluent, $currentTable);
$this->registerTableAlias($globalKey, $alias);
}
}
return $alias;
}
private function traverseToRelatedEntity(&$currentTable, &$currentTableAlias, Property $property)
{
if (!$property->hasRelationship()) {
$entityClass = $this->mapper->getEntityClass($currentTable);
throw new InvalidRelationshipException("Property '{$property->getName()}' in entity '$entityClass' doesn't have any relationship.");
}
$implicitFilters= [];
$propertyType = $property->getType();
if (is_subclass_of($propertyType, 'LeanMapper\\Entity')) {
$caller = new Caller($this, $property);
$implicitFilters = $this->mapper->getImplicitFilters($property->getType(), $caller);
}
$relationship = $property->getRelationship();
if ($relationship instanceof Relationship\HasOne) {
$targetTable = $relationship->getTargetTable();
$targetTablePrimaryKey = $this->mapper->getPrimaryKey($targetTable);
$referencingColumn = $relationship->getColumnReferencingTargetTable();
$targetTableAlias = $this->joinRelatedTable($currentTableAlias, $referencingColumn, $targetTable, $targetTablePrimaryKey, $implicitFilters, FALSE);
} elseif ($relationship instanceof Relationship\BelongsTo) {
$targetTable = $relationship->getTargetTable();
$sourceTablePrimaryKey = $this->mapper->getPrimaryKey($currentTable);
$referencingColumn = $relationship->getColumnReferencingSourceTable();
$targetTableAlias = $this->joinRelatedTable($currentTableAlias, $sourceTablePrimaryKey, $targetTable, $referencingColumn, $implicitFilters);
} elseif ($relationship instanceof Relationship\HasMany) {
$sourceTablePrimaryKey = $this->mapper->getPrimaryKey($currentTable);
$relationshipTable = $relationship->getRelationshipTable();
$sourceReferencingColumn = $relationship->getColumnReferencingSourceTable();
$targetReferencingColumn = $relationship->getColumnReferencingTargetTable();
$targetTable = $relationship->getTargetTable();
$targetTablePrimaryKey = $this->mapper->getPrimaryKey($targetTable);
$relationshipTableAlias = $this->joinRelatedTable($currentTableAlias, $sourceTablePrimaryKey, $relationshipTable, $sourceReferencingColumn);
$targetTableAlias = $this->joinRelatedTable($relationshipTableAlias, $targetReferencingColumn, $targetTable, $targetTablePrimaryKey, $implicitFilters, FALSE);
} else {
throw new InvalidRelationshipException('Unknown relationship type. ' . get_class($relationship) . ' given.');
}
$currentTable = $targetTable;
$currentTableAlias = $targetTableAlias;
return $this->getPropertiesByTable($targetTable);
}
private function replacePlaceholder(Property $property)
{
$type = $property->getType();
if ($property->isBasicType()) {
if (array_key_exists($type, self::$placeholders)) {
return self::$placeholders[$type];
} else {
return self::$defaultPlaceholder;
}
} else {
if ($type === 'DateTime' || is_subclass_of($type, 'DateTime')) {
if ($property->hasCustomFlag(self::$typeFlagName)
&& preg_match('#^(DATE|Date|date)$#', $property->getCustomFlagValue(self::$typeFlagName))) {
return self::$placeholders['Date'];
} else {
return self::$placeholders['DateTime'];
}
} else {
return self::$defaultPlaceholder;
}
}
}
private function tryAddGroupBy(Fluent $fluent, $table)
{
$groupBy = $fluent->_export('GROUP BY');
if (empty($groupBy)) {
$fluent->groupBy('%n.%n', $table, $this->mapper->getPrimaryKey($table));
}
}
protected function parseStatement($statement, $replacePlaceholders = NULL)
{
if (!is_string($statement)) {
throw new InvalidArgumentException('Type of argument $statement is expected to be string. ' . gettype($statement) . ' given.');
}
$replacePlaceholders === NULL && $replacePlaceholders = (bool) $this->replacePlaceholders;
$rootTableName = $this->sourceTableName;
if ($this->castedEntityClass) {
list($rootEntityClass, $rootProperties) = $this->getPropertiesByEntity($this->castedEntityClass);
} else {
list($rootEntityClass, $rootProperties) = $this->getPropertiesByTable($rootTableName);
}
$switches = [
'@' => FALSE,
'"' => FALSE,
"'" => FALSE,
];
$output = '';
$property = NULL;
$firstLetter = TRUE;
for ($i = 0; $i < strlen($statement) + 1; $i++) {
$ch = isset($statement{$i}) ? $statement{$i} : '';
if ($switches['@'] === TRUE) {
if (preg_match('#^'.($firstLetter ? self::$variablePatternFirstLetter : self::$variablePatternOtherLetters).'$#', $ch)) {
$propertyName .= $ch;
$firstLetter = FALSE;
} else {
$firstLetter = TRUE;
if (!array_key_exists($propertyName, $properties)) {
throw new MemberAccessException("Entity '$entityClass' doesn't have property '$propertyName'.");
}
$property = $properties[$propertyName];
if ($ch === '.') {
$this->triggerJoin();
list($entityClass, $properties) = $this->traverseToRelatedEntity($tableName, $tableNameAlias, $property);
$propertyName = '';
} else {
if ($property->hasRelationship()) {
$this->triggerJoin();
list($entityClass, $properties) = $this->traverseToRelatedEntity($tableName, $tableNameAlias, $property);
$column = $this->mapper->getPrimaryKey($tableName);
$property = NULL;
foreach ($properties as $prop) {
if ($prop->getColumn() === $column) {
$property = $prop;
}
}
if (!$property) {
throw new InvalidStateException("Entity '$entityClass' doesn't have any field corresponding to the primary key column '$column'.");
}
} else {
$column = $property->getColumn();
if ($column === NULL) {
throw new InvalidStateException("Column not specified in property '$propertyName' from entity '$entityClass'.");
}
}
if ($column === $this->mapper->getPrimaryKey($tableName) && $this->pendingJoin()) {
list($tableNameAlias, $column) = $this->dismissJoin();
} else {
$this->triggerJoin();
}
$output .= "[$tableNameAlias].[$column]";
$switches['@'] = FALSE;
$output .= $ch;
}
}
} elseif ($ch === '@' && $switches["'"] === FALSE && $switches['"'] === FALSE) {
$switches['@'] = TRUE;
$propertyName = '';
$properties = $rootProperties;
$tableNameAlias = $tableName = $rootTableName;
$entityClass = $rootEntityClass;
} elseif ($replacePlaceholders && $ch === self::$defaultPlaceholder && $switches["'"] === FALSE && $switches['"'] === FALSE) {
if ($property === NULL) {
$output .= $ch;
} else {
$output .= $this->replacePlaceholder($property);
}
} else {
if ($ch === '"' && $switches["'"] === FALSE) {
$switches['"'] = !$switches['"'];
} elseif ($ch === "'" && $switches['"'] === FALSE) {
$switches["'"] = !$switches["'"];
}
$output .= $ch;
}
}
return $output;
}
protected function getFluent()
{
if ($this->fluent === NULL) {
throw new InvalidStateException('getFluent() method could be only called within command<name>() methods.');
}
return $this->fluent;
}
public function cast($entityClass)
{
if ($this->castedEntityClass !== NULL) {
throw new InvalidStateException("Entity class is already casted to {$this->castedEntityClass} class.");
}
$this->castedEntityClass = $entityClass;
return $this;
}
public function applyQuery(Fluent $fluent, IMapper $mapper)
{
return $this->apply($fluent, $mapper);
}
public function applyJunctionQuery(Fluent $fluent, IMapper $mapper, Relationship\HasMany $rel)
{
$relationshipTable = $rel->getRelationshipTable();
$targetReferencingColumn = $rel->getColumnReferencingTargetTable();
$targetTable = $rel->getTargetTable();
$targetPrimaryKey = $mapper->getPrimaryKey($targetTable);
$fluent = $this->apply($fluent, $mapper, $targetTable);
if ($fluent->_export('WHERE') || $fluent->_export('ORDER BY')) {
$fluent->leftJoin($targetTable)
->on("%n.%n = %n.%n", $relationshipTable, $targetReferencingColumn, $targetTable, $targetPrimaryKey);
}
return $fluent;
}
private function apply(Fluent $fluent, IMapper $mapper, $sourceTableName = NULL)
{
$fromClause = $fluent->_export('FROM');
if (count($fromClause) < 3 || $fromClause[1] !== '%n') {
throw new InvalidArgumentException('Unsupported fluent from clause. Only pure table name as an argument of \\LeanMapper\\Fluent::from method is supported.');
}
$this->sourceTableName = $sourceTableName !== NULL ? $sourceTableName : $fromClause[2];
if (count($fromClause) > 3) {
$subFluent = clone $fluent;
foreach (array_keys(\Dibi\Fluent::$separators) as $separator) {
$fluent->removeClause($separator);
}
$fluent->select('*')->from($subFluent)
->as($this->sourceTableName);
}
$this->fluent = $fluent;
$this->mapper = $mapper;
if ($this->castedEntityClass !== NULL) {
$rootEntityClass = $this->mapper->getEntityClass($this->sourceTableName);
if (!is_a($this->castedEntityClass, $rootEntityClass, TRUE)) {
throw new InvalidArgumentException("Query object is limited to {$this->castedEntityClass} entity, {$rootEntityClass} entity used.");
}
if ($mapper instanceof ICaster) {
$mapper->castTo($fluent, $this->castedEntityClass);
}
}
$this->tablesAliases = [$this->sourceTableName];
foreach (array_merge($this->queue, $this->limitQueue) as $call) {
list($method, $args) = $call;
call_user_func_array([$this, $method], $args);
}
$this->fluent = NULL;
return $fluent;
}
public function junctionQueryNeeded()
{
return !empty($this->limitQueue);
}
public function __call($name, array $args)
{
$method = 'command' . ucfirst($name);
if (!method_exists($this, $method)) {
throw new NonExistingMethodException("Command '$name' doesn't exist. To register this command there should be defined protected method " . get_called_class() . "::$method.");
}
switch ($name) {
case 'limit':
case 'offset':
$this->limitQueue[] = [$method, $args];
break;
default:
$this->queue[] = [$method, $args];
break;
}
return $this;
}
private function commandWhere($cond)
{
if (is_array($cond)) {
if (func_num_args() > 1) {
throw new InvalidArgumentException('Number of arguments is limited to 1 if the first argument is array.');
}
foreach ($cond as $key => $value) {
if (is_string($key)) {
$this->commandWhere($key, $value);
} else {
$this->commandWhere($value);
}
}
} else {
$replacePlaceholders = NULL;
$args = func_get_args();
$operators = ['=', '<>', '!=', '<=>', '<', '<=', '>', '>='];
$variablePattern = self::$variablePatternFirstLetter . self::$variablePatternOtherLetters . '*';
if (count($args) === 2
&& preg_match('#^\s*(@(?:'.$variablePattern.'|\.)*'.$variablePattern.')\s*(|'.implode('|', $operators).')\s*(?:\?\s*)?$#', $args[0], $matches)) {
$replacePlaceholders = TRUE;
$field = &$args[0];
list(, $field, $operator) = $matches;
$value = &$args[1];
$placeholder = self::$defaultPlaceholder;
if (!$operator) {
if (is_array($value)) {
$value = $this->replaceEntitiesForItsPrimaryKeyValues($value);
$operator = 'IN';
$placeholder = '%in';
} elseif ($value === NULL) {
$operator = 'IS';
$placeholder = 'NULL';
unset($args[1]);
} else {
$operator = '=';
}
}
$field .= " $operator $placeholder";
}
$statement = &$args[0];
$statement = $this->parseStatement($statement, $replacePlaceholders);
$statement = "($statement)";
$args = $this->replaceEntitiesForItsPrimaryKeyValues($args);
call_user_func_array([$this->fluent, 'where'], $args);
}
}
private function replaceEntitiesForItsPrimaryKeyValues(array $entities)
{
foreach ($entities as &$entity) {
if ($entity instanceof LeanMapper\Entity) {
$entityTable = $this->mapper->getTable(get_class($entity));
$idField = $this->mapper->getEntityField($entityTable, $this->mapper->getPrimaryKey($entityTable));
$entity = $entity->$idField;
}
}
return $entities;
}
private function commandOrderBy($field)
{
if (is_array($field)) {
foreach ($field as $key => $value) {
if (is_string($key)) {
$this->orderBy($key)->asc($value);
} else {
$this->orderBy($value);
}
}
} else {
$field = $this->parseStatement($field);
$this->fluent->orderBy($field);
}
}
private function commandAsc($asc = TRUE)
{
$this->fluent->{$asc ? 'asc' : 'desc'}();
}
private function commandDesc($desc = TRUE)
{
$this->commandAsc(!$desc);
}
private function commandLimit($limit)
{
$this->fluent->limit($limit);
}
private function commandOffset($offset)
{
$this->fluent->offset($offset);
}
public function current()
{
throw new NotImplementedException("Query object is not iterable.");
}
public function next()
{
throw new NotImplementedException("Query object is not iterable.");
}
public function rewind()
{
throw new NotImplementedException("Query object is not iterable.");
}
public function key()
{
throw new NotImplementedException("Query object is not iterable.");
}
public function valid()
{
throw new NotImplementedException("Query object is not iterable.");
}
}