Overview

Namespaces

  • LeanMapperQuery
    • Exception

Classes

  • LeanMapperQuery\Caller
  • LeanMapperQuery\Entity
  • LeanMapperQuery\Query

Interfaces

  • LeanMapperQuery\ICaster
  • LeanMapperQuery\IQuery

Exceptions

  • LeanMapperQuery\Exception\Exception
  • LeanMapperQuery\Exception\InvalidArgumentException
  • LeanMapperQuery\Exception\InvalidMethodCallException
  • LeanMapperQuery\Exception\InvalidRelationshipException
  • LeanMapperQuery\Exception\InvalidStateException
  • LeanMapperQuery\Exception\MemberAccessException
  • LeanMapperQuery\Exception\NonExistingMethodException
  • LeanMapperQuery\Exception\NotImplementedException
  • Overview
  • Namespace
  • Class
  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

/**
 * This file is part of the LeanMapperQuery extension
 * for the Lean Mapper library (http://leanmapper.com)
 * Copyright (c) 2013 Michal Bohuslávek
 */

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;

/**
 * @author Michal Bohuslávek
 *
 * @method Query where($cond)
 * @method Query orderBy($field)
 * @method Query asc(bool $asc = TRUE)
 * @method Query desc(bool $desc = TRUE)
 * @method Query limit(int $limit)
 * @method Query offset(int $offset)
 */
class Query implements IQuery, \Iterator
{
    /** @var string */
    private static $defaultPlaceholder = '?';

    /** @var string */
    private static $variablePatternFirstLetter = '[a-zA-Z_\x7f-\xff]';

    /** @var string */
    private static $variablePatternOtherLetters = '[a-zA-Z0-9_\x7f-\xff]';

    /** @var string */
    private static $typeFlagName = 'type';

    /**
     * Placeholders transformation table.
     * @var array
     */
    private static $placeholders = [
        'string' => '%s',
        'boolean' => '%b',
        'integer' => '%i',
        'float' => '%f',
        'DateTime' => '%t',
        'Date' => '%d',
    ];

    ////////////////////////////////////////////////////

    /** @var string|NULL */
    private $castedEntityClass = NULL;

    /** @var string */
    protected $sourceTableName;

    /** @var Fluent|NULL */
    private $fluent = NULL;

    /** @var IMapper */
    protected $mapper;

    /**
     * Whether to use dumb replacing of placeholders globally.
     * @var boolean
     */
    protected $replacePlaceholders = FALSE;

    /** @var array */
    private $queue = [];

    /** @var array */
    private $limitQueue = [];

    /** @var array */
    private $tablesAliases;

    /** @var array|NULL */
    private $possibleJoin = NULL;

    /** @var array */
    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];
    }

    /**
     * Returns TRUE if the $targetTable is already joined.
     * @param  string $currentTable
     * @param  string $targetTable
     * @param  string $viaColumn
     * @param  string $alias
     * @return bool
     */
    private function getTableAlias($currentTable, $targetTable, $viaColumn, &$globalKey, &$alias)
    {
        // Tables can be joined via different columns from the same table,
        // or from different tables via column with the same name.
        $localKey = $targetTable . '_' . $viaColumn;
        $globalKey = $currentTable . '_' . $localKey;
        if (array_key_exists($globalKey, $this->tablesAliases)) {
            $alias = $this->tablesAliases[$globalKey];
            return TRUE;
        }
        // Find the tiniest unique table alias.
        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;
        }
    }

    /**
     * Dismisses the join and returns alternative table
     * and column names.
     * @return array(string, string)
     */
    private function dismissJoin()
    {
        $this->possibleJoin = NULL;
        return $this->joinAlternative;
    }

    /**
     * @return bool
     */
    private function pendingJoin()
    {
        return $this->possibleJoin !== NULL;
    }

    private function joinRelatedTable($currentTable, $referencingColumn, $targetTable, $targetTablePrimaryKey, $filters = [], $joinImmediately = TRUE)
    {
        // Join if not already joined.
        if (!$this->getTableAlias($currentTable, $targetTable, $referencingColumn, $globalKey, $alias)) {
            if (empty($filters)) {
                // Do simple join.
                // In few cases there is no need to do join immediately -> register join
                // to decide later.
                $this->registerJoin($currentTable, $referencingColumn, $targetTable, $targetTablePrimaryKey, $globalKey, $alias);
                $joinImmediately && $this->triggerJoin();
            } else {
                // Join sub-query due to applying implicit filters.
                $subFluent = new Fluent($this->fluent->getConnection());
                $subFluent->select('%n.*', $targetTable)->from($targetTable);

                // Apply implicit filters.
                $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();
            // Join table.
            $targetTableAlias = $this->joinRelatedTable($currentTableAlias, $referencingColumn, $targetTable, $targetTablePrimaryKey, $implicitFilters, FALSE);

        } elseif ($relationship instanceof Relationship\BelongsTo) { // BelongsToOne, BelongsToMany
            $targetTable = $relationship->getTargetTable();
            $sourceTablePrimaryKey = $this->mapper->getPrimaryKey($currentTable);
            $referencingColumn = $relationship->getColumnReferencingSourceTable();
            // Join table.
            $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);
            // Join tables.
            // Don't apply filters on relationship table.
            $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));
        }
    }

    /**
     * Parses given statement. Basically it replaces '@foo' to
     * '[table_name].[foo]' and performs automatic joins.
     * @param  string $statement
     * @param  bool   $replacePlaceholders
     * @return string
     * @throws InvalidArgumentException
     * @throws MemberAccessException
     * @throws InvalidStateException
     */
    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++) {
            // Do one more loop due to succesfuly translating
            // properties attached to the end of the statement.
            $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()) {
                            // If the last property also has relationship replace with primary key field value.
                            // NOTE: Traversing to a related entity is necessary even for the HasOne and HasMany
                            //  relationships if there are implicit filters to be applied.
                            $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()) {
                            // There is a pending join that does not need to be done. Primary key value
                            // is already known from referencing table.
                            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 {
                    // Dumb replacing of the placeholder.
                    // NOTE: Placeholders are replaced by the type of last found property.
                    // It is stupid as it doesn't work for all kinds of SQL statements.
                    $output .= $this->replacePlaceholder($property);
                }
            } else {
                if ($ch === '"' && $switches["'"] === FALSE) {
                    $switches['"'] = !$switches['"'];
                } elseif ($ch === "'" && $switches['"'] === FALSE) {
                    $switches["'"] = !$switches["'"];
                }
                $output .= $ch;
            }
        }
        return $output;
    }

    /**
     * Returns fluent if it is available. It is supposed
     * to be used within own command<name> methods.
     * @throws InvalidStateException
     * @return Fluent
     */
    protected function getFluent()
    {
        if ($this->fluent === NULL) {
            throw new InvalidStateException('getFluent() method could be only called within command<name>() methods.');
        }
        return $this->fluent;
    }

    ////////////////////////////////////////////////////

    /**
     * @param  string
     * @return self
     */
    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;
    }

    /**
     * @inheritdoc
     * @throws     InvalidArgumentException
     */
    public function applyQuery(Fluent $fluent, IMapper $mapper)
    {
        return $this->apply($fluent, $mapper);
    }

    /**
     * @inheritdoc
     * @throws     InvalidArgumentException
     */
    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)
    {
        // NOTE:
        // $fluent is expected to have called method Fluent::from
        // with pure table name as an argument. For example:
        //   $fluent->from('author');
        //
        // So something like
        //   $fluent->from('[author]');
        // is not supported. If a Fluent::from method is called multiple
        // times, the table name from the first call is used as
        // the source table.
        //
        // The advantage of this way is that there is no need to explicitly
        // specify $tableName when calling Query::applyQuery anymore.
        $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) { // complicated from clause
            $subFluent = clone $fluent;
            // Reset fluent.
            foreach (array_keys(\Dibi\Fluent::$separators) as $separator) {
                $fluent->removeClause($separator);
            }
            // If there are some joins, enwrap the original fluent to enable
            // accessing columns from joined tables.
            $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);
            }
        }

        // Add source table name to tables aliases list to avoid error
        // when joining to itself.
        $this->tablesAliases = [$this->sourceTableName];

        foreach (array_merge($this->queue, $this->limitQueue) as $call) {
            list($method, $args) = $call;
            call_user_func_array([$this, $method], $args);
        }

        // Reset fluent.
        $this->fluent = NULL;
        return $fluent;
    }

    /**
     * @return bool
     */
    public function junctionQueryNeeded()
    {
        return !empty($this->limitQueue);
    }

    /**
     * Enqueues command.
     * @param  string $name Command name
     * @param  array  $args
     * @return self
     * @throws NonExistingMethodException
     */
    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;
    }

    /////////////// basic commands //////////////////////

    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";
            }
            // Only first argument is parsed. Other arguments will be maintained
            // as parameters.
            $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));
                // FIXME: Column name could be specified in the entity instead of mapper provided by 'getEntityField' function.
                $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);
    }

    //////////////////// Iterator //////////////////////

    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.");
    }

}
mibk/leanmapperquery cl4 API documentation API documentation generated by ApiGen