Overview

Namespaces

  • LeanMapper
    • Bridges
      • Nette
        • DI
    • Exception
    • Reflection
    • Relationship

Classes

  • LeanMapper\Bridges\Nette\DI\LeanMapperExtension
  • LeanMapper\Caller
  • LeanMapper\Connection
  • LeanMapper\DataDifference
  • LeanMapper\DefaultEntityFactory
  • LeanMapper\DefaultMapper
  • LeanMapper\Entity
  • LeanMapper\EntityDataDecoder
  • LeanMapper\Events
  • LeanMapper\Filtering
  • LeanMapper\FilteringResult
  • LeanMapper\FilteringResultDecorator
  • LeanMapper\Fluent
  • LeanMapper\ImplicitFilters
  • LeanMapper\Reflection\Aliases
  • LeanMapper\Reflection\AliasesBuilder
  • LeanMapper\Reflection\AliasesParser
  • LeanMapper\Reflection\AnnotationsParser
  • LeanMapper\Reflection\EntityReflection
  • LeanMapper\Reflection\Property
  • LeanMapper\Reflection\PropertyFactory
  • LeanMapper\Reflection\PropertyFilters
  • LeanMapper\Reflection\PropertyMethods
  • LeanMapper\Reflection\PropertyPasses
  • LeanMapper\Reflection\PropertyType
  • LeanMapper\Reflection\PropertyValuesEnum
  • LeanMapper\Relationship\BelongsTo
  • LeanMapper\Relationship\BelongsToMany
  • LeanMapper\Relationship\BelongsToOne
  • LeanMapper\Relationship\HasMany
  • LeanMapper\Relationship\HasOne
  • LeanMapper\Repository
  • LeanMapper\Result
  • LeanMapper\ResultProxy
  • LeanMapper\Row

Interfaces

  • LeanMapper\IEntityFactory
  • LeanMapper\IMapper

Exceptions

  • LeanMapper\Exception\Exception
  • LeanMapper\Exception\InvalidAnnotationException
  • LeanMapper\Exception\InvalidArgumentException
  • LeanMapper\Exception\InvalidMethodCallException
  • LeanMapper\Exception\InvalidStateException
  • LeanMapper\Exception\InvalidValueException
  • LeanMapper\Exception\MemberAccessException
  • LeanMapper\Exception\UtilityClassException
  • 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:  706:  707:  708:  709:  710:  711:  712:  713:  714:  715:  716:  717:  718:  719:  720:  721:  722:  723:  724:  725:  726:  727:  728:  729:  730:  731:  732:  733:  734:  735:  736:  737:  738:  739:  740:  741:  742:  743:  744:  745:  746:  747:  748:  749:  750:  751:  752:  753:  754:  755:  756:  757:  758:  759:  760:  761:  762:  763:  764:  765:  766:  767:  768:  769:  770:  771:  772:  773:  774:  775:  776:  777:  778:  779:  780:  781:  782:  783:  784:  785:  786:  787:  788:  789:  790:  791:  792:  793:  794:  795:  796:  797:  798:  799:  800:  801:  802:  803:  804:  805:  806:  807:  808:  809:  810:  811:  812:  813:  814:  815:  816:  817:  818:  819:  820:  821:  822:  823:  824:  825:  826:  827:  828:  829:  830:  831:  832:  833:  834:  835:  836:  837:  838:  839:  840:  841:  842:  843:  844:  845:  846:  847:  848:  849:  850:  851:  852:  853:  854:  855:  856:  857:  858:  859:  860:  861:  862:  863:  864:  865:  866:  867:  868:  869:  870:  871:  872:  873:  874:  875:  876:  877:  878:  879:  880:  881:  882:  883:  884:  885:  886:  887:  888:  889:  890:  891:  892:  893:  894:  895:  896:  897:  898:  899:  900:  901:  902:  903:  904:  905:  906:  907:  908:  909:  910:  911:  912:  913:  914:  915:  916:  917:  918:  919:  920:  921:  922:  923:  924:  925:  926:  927:  928:  929:  930:  931:  932:  933:  934:  935:  936:  937:  938:  939:  940:  941:  942:  943:  944:  945:  946:  947:  948:  949:  950:  951:  952:  953:  954:  955:  956:  957:  958:  959:  960:  961:  962:  963:  964:  965:  966:  967:  968:  969:  970:  971:  972:  973:  974:  975:  976:  977:  978:  979:  980:  981:  982:  983:  984:  985:  986:  987:  988:  989:  990:  991:  992:  993:  994:  995:  996:  997:  998:  999: 1000: 1001: 1002: 1003: 1004: 1005: 1006: 1007: 1008: 1009: 1010: 1011: 1012: 1013: 1014: 1015: 1016: 1017: 1018: 1019: 1020: 1021: 1022: 1023: 1024: 1025: 1026: 1027: 1028: 1029: 1030: 1031: 1032: 1033: 1034: 1035: 1036: 1037: 1038: 1039: 1040: 1041: 1042: 1043: 1044: 1045: 1046: 1047: 1048: 1049: 1050: 1051: 1052: 1053: 1054: 1055: 1056: 1057: 1058: 1059: 1060: 1061: 1062: 1063: 1064: 1065: 1066: 1067: 1068: 1069: 1070: 1071: 1072: 1073: 1074: 1075: 1076: 1077: 1078: 1079: 1080: 1081: 1082: 1083: 1084: 1085: 1086: 1087: 1088: 1089: 1090: 1091: 1092: 1093: 1094: 1095: 1096: 1097: 1098: 1099: 1100: 1101: 1102: 1103: 1104: 1105: 1106: 1107: 1108: 1109: 1110: 1111: 1112: 1113: 1114: 1115: 1116: 1117: 
<?php

/**
 * This file is part of the Lean Mapper library (http://www.leanmapper.com)
 *
 * Copyright (c) 2013 Vojtěch Kohout (aka Tharos)
 *
 * For the full copyright and license information, please view the file
 * license.md that was distributed with this source code.
 */

namespace LeanMapper;

use ArrayAccess;
use Closure;
use Dibi\Drivers\Sqlite3Driver as DibiSqlite3Driver;
use Dibi\Row as DibiRow;
use LeanMapper\Exception\InvalidArgumentException;
use LeanMapper\Exception\InvalidMethodCallException;
use LeanMapper\Exception\InvalidStateException;

/**
 * Set of related data, heart of Lean Mapper
 *
 * @author Vojtěch Kohout
 */
class Result implements \Iterator
{

    const STRATEGY_IN = 'in';

    const STRATEGY_UNION = 'union';

    const DETACHED_ROW_ID = -1;

    const KEY_PRELOADED = 'preloaded';

    const KEY_FORCED = 'forced';

    const ERROR_MISSING_COLUMN = 1;

    /** @var Connection */
    private static $storedConnection;

    /** @var bool */
    private $isDetached;

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

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

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

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

    /** @var string */
    private $table;

    /** @var Connection */
    private $connection;

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

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

    /** @var self[] */
    private $referenced = [];

    /** @var self[] */
    private $referencing = [];

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

    /** @var ResultProxy */
    private $proxy;



    /**
     * Creates new common instance (it means persisted)
     *
     * @param \Dibi\Row|\Dibi\Row[] $data
     * @param string $table
     * @param Connection $connection
     * @param IMapper $mapper
     * @return self
     * @throws InvalidArgumentException
     */
    public static function createInstance($data, $table, Connection $connection, IMapper $mapper)
    {
        $dataArray = [];
        $primaryKey = $mapper->getPrimaryKey($table);
        if ($data instanceof DibiRow) {
            $dataArray = [isset($data->$primaryKey) ? $data->$primaryKey : self::DETACHED_ROW_ID => $data->toArray()];
        } else {
            $e = new InvalidArgumentException(
                'Invalid type of data given, only \Dibi\Row, \Dibi\Row[], ArrayAccess[] or array of arrays is supported at this moment.'
            );
            if (!is_array($data)) {
                throw $e;
            }
            if (!empty($data)) {
                $record = reset($data);
                if (!($record instanceof DibiRow) and !is_array($record) and (!$record instanceof ArrayAccess)) {
                    throw $e;
                }
            }
            foreach ($data as $record) {
                $record = (array)$record;
                if (isset($record[$primaryKey])) {
                    $dataArray[$record[$primaryKey]] = $record;
                } else {
                    $dataArray[] = $record;
                }
            }
        }
        return new self($dataArray, $table, $connection, $mapper);
    }



    /**
     * Creates new detached instance (it means non-persisted)
     *
     * @return self
     */
    public static function createDetachedInstance()
    {
        return new self;
    }



    /**
     * @param Connection $connection
     */
    public static function enableSerialization(Connection $connection)
    {
        if (self::$storedConnection === null) {
            self::$storedConnection = $connection;
        } elseif (self::$storedConnection !== $connection) {
            throw new InvalidStateException("Given connection doesn't equal to connection already present in Result.");
        }
    }



    /**
     * @param Connection $connection
     * @throws InvalidStateException
     */
    public function setConnection(Connection $connection)
    {
        if ($this->connection === null) {
            $this->connection = $connection;
        } elseif ($this->connection !== $connection) {
            throw new InvalidStateException("Given connection doesn't equal to connection already present in Result.");
        }
    }



    /**
     * @return bool
     */
    public function hasConnection()
    {
        return $this->connection !== null;
    }



    /**
     * @param IMapper $mapper
     */
    public function setMapper(IMapper $mapper)
    {
        $this->mapper = $mapper;
    }



    /**
     * @return IMapper|null
     */
    public function getMapper()
    {
        return $this->mapper;
    }



    /**
     * Creates new Row instance pointing to specific row within Result
     *
     * @param int $id
     * @throws InvalidArgumentException
     * @return Row|null
     */
    public function getRow($id = null)
    {
        if ($this->isDetached) {
            if ($id !== null) {
                throw new InvalidArgumentException('Argument $id in Result::getRow method cannot be passed when Result is in detached state.');
            }
            $id = self::DETACHED_ROW_ID;
        } elseif ($id === null) {
            throw new InvalidArgumentException('Argument $id in Result::getRow method must be passed when Result is in attached state.');
        }
        if (!isset($this->data[$id])) {
            return null;
        }
        return new Row($this, $id);
    }



    /**
     * Gets value of given column from row with given id
     *
     * @param mixed $id
     * @param string $key
     * @return mixed
     * @throws InvalidArgumentException
     */
    public function getDataEntry($id, $key)
    {
        if (!isset($this->data[$id])) {
            throw new InvalidArgumentException("Missing row with id $id.");
        }
        if ($this->isAlias($key)) {
            $key = $this->trimAlias($key);
        }
        if (!array_key_exists($key, $this->data[$id])) {
            throw new InvalidArgumentException("Missing '$key' column in row with id $id.", self::ERROR_MISSING_COLUMN);
        }
        return $this->data[$id][$key];
    }



    /**
     * Sets value of given column in row with given id
     *
     * @param mixed $id
     * @param string $key
     * @param mixed $value
     * @throws InvalidArgumentException
     */
    public function setDataEntry($id, $key, $value)
    {
        if (!isset($this->data[$id])) {
            throw new InvalidArgumentException("Missing row with ID $id.");
        }
        if (!$this->isDetached and $key === $this->mapper->getPrimaryKey($this->table)) { // mapper is always set when Result is not detached
            throw new InvalidArgumentException("ID can only be set in detached rows.");
        }
        $this->modified[$id][$key] = true;
        $this->data[$id][$key] = $value;
    }



    /**
     * Tells whether row with given id has given column
     *
     * @param mixed $id
     * @param string $column
     * @return bool
     */
    public function hasDataEntry($id, $column)
    {
        return isset($this->data[$id]) and array_key_exists($column, $this->data[$id]);
    }



    /**
     * Unsets given column in row with given id
     *
     * @param mixed $id
     * @param string $column
     * @throws InvalidArgumentException
     * @throws InvalidStateException
     */
    public function unsetDataEntry($id, $column)
    {
        if (!isset($this->data[$id])) {
            throw new InvalidArgumentException("Missing row with ID $id.");
        }
        unset($this->data[$id][$column], $this->modified[$id][$column]);
    }



    /**
     * Adds new data entry
     *
     * @param array $values
     * @throws InvalidStateException
     */
    public function addDataEntry(array $values)
    {
        if ($this->isDetached) {
            throw new InvalidStateException('Cannot add data entry to detached Result.');
        }
        $this->data[] = $values;
        $this->added[] = $values;
        $this->cleanReferencedResultsCache();
    }



    /**
     * Removes given data entry
     *
     * @param array $values
     * @throws InvalidStateException
     */
    public function removeDataEntry(array $values)
    {
        if ($this->isDetached) {
            throw new InvalidStateException('Cannot remove data entry to detached Result.');
        }
        foreach ($this->data as $key => $entry) {
            if (array_diff_assoc($values, $entry) === []) {
                $this->removed[] = $entry;
                unset($this->data[$key], $this->modified[$key]);
                break;
            }
        }
    }



    /**
     * Returns values of columns of requested row
     *
     * @param int $id
     * @return array
     */
    public function getData($id)
    {
        return isset($this->data[$id]) ? $this->data[$id] : [];
    }



    /**
     * Returns values of columns of requested row that were modified
     *
     * @param int $id
     * @return array
     */
    public function getModifiedData($id)
    {
        $result = [];
        if (isset($this->modified[$id])) {
            foreach (array_keys($this->modified[$id]) as $column) {
                $result[$column] = $this->data[$id][$column];
            }
        }
        return $result;
    }



    /**
     * Creates new DataDifference instance relevant to current Result state
     *
     * @return DataDifference
     */
    public function createDataDifference()
    {
        return new DataDifference($this->added, $this->removed);
    }



    /**
     * Tells whether requested row is in modified state
     *
     * @param int $id
     * @return bool
     */
    public function isModified($id)
    {
        return isset($this->modified[$id]) and !empty($this->modified[$id]);
    }



    /**
     * Tells whether Result is in detached state (in means non-persisted)
     *
     * @return bool
     */
    public function isDetached()
    {
        return $this->isDetached;
    }



    /**
     * Marks requested row as non-modified (isModified returns false right after this method call)
     *
     * @param int $id
     * @throws InvalidMethodCallException
     */
    public function markAsUpdated($id)
    {
        if ($this->isDetached) {
            throw new InvalidMethodCallException('Detached Result cannot be marked as updated.');
        }
        unset($this->modified[$id]);
    }



    /**
     * @param mixed $id
     * @param string $table
     * @throws InvalidStateException
     */
    public function attach($id, $table)
    {
        if (!$this->isDetached) {
            throw new InvalidStateException('Result is not in detached state.');
        }
        if ($this->connection === null) {
            throw new InvalidStateException('Missing connection.');
        }
        if ($this->mapper === null) {
            throw new InvalidStateException('Missing mapper.');
        }
        $modifiedData = $this->getModifiedData(self::DETACHED_ROW_ID);
        $this->data = [
            $id => [$this->mapper->getPrimaryKey($table) => $id] + $modifiedData,
        ];
        $this->modified = [];
        $this->table = $table;
        $this->isDetached = false;
    }



    public function cleanAddedAndRemovedMeta()
    {
        $this->added = [];
        $this->removed = [];
    }



    /**
     * Creates new Row instance pointing to requested row in referenced Result
     *
     * @param int $id
     * @param string $table
     * @param string|null $viaColumn
     * @param Filtering|null $filtering
     * @throws InvalidStateException
     * @return Row|null
     */
    public function getReferencedRow($id, $table, $viaColumn = null, Filtering $filtering = null)
    {
        if ($viaColumn === null) {
            $viaColumn = $this->mapper->getRelationshipColumn($this->table, $table);
        }
        $result = $this->getReferencedResult($table, $viaColumn, $filtering);
        $rowId = $this->getDataEntry($id, $viaColumn);
        return $rowId === null ? null : $result->getRow($rowId);
    }



    /**
     * Creates new array of Row instances pointing to requested row in referencing Result
     *
     * @param int $id
     * @param string $table
     * @param string|null $viaColumn
     * @param Filtering|null $filtering
     * @param string $strategy
     * @throws InvalidStateException
     * @return Row[]
     */
    public function getReferencingRows($id, $table, $viaColumn = null, Filtering $filtering = null, $strategy = null)
    {
        if ($viaColumn === null) {
            $viaColumn = $this->mapper->getRelationshipColumn($table, $this->table);
        }
        $referencingResult = $this->getReferencingResult($table, $viaColumn, $filtering, $strategy);
        $resultHash = spl_object_hash($referencingResult);
        if (!isset($this->index[$resultHash])) {
            $column = $this->isAlias($viaColumn) ? $this->trimAlias($viaColumn) : $viaColumn;
            $this->index[$resultHash] = [];
            foreach ($referencingResult as $key => $row) {
                $this->index[$resultHash][$row[$column]][] = new Row($referencingResult, $key);
            }
        }
        if (!isset($this->index[$resultHash][$id])) {
            return [];
        }
        return $this->index[$resultHash][$id];
    }



    /**
     * @param self $referencedResult
     * @param string $table
     * @param string $viaColumn
     */
    public function setReferencedResult(self $referencedResult, $table, $viaColumn = null)
    {
        if ($viaColumn === null) {
            $viaColumn = $this->mapper->getRelationshipColumn($table, $this->table);
        }
        $this->referenced["$table($viaColumn)#" . self::KEY_PRELOADED] = $referencedResult;
    }



    /**
     * @param Result $referencingResult
     * @param string $table
     * @param string $viaColumn
     * @param string $strategy
     */
    public function setReferencingResult(self $referencingResult, $table, $viaColumn = null, $strategy = self::STRATEGY_IN)
    {
        $strategy = $this->translateStrategy($strategy);
        if ($viaColumn === null) {
            $viaColumn = $this->mapper->getRelationshipColumn($table, $this->table);
        }
        $this->referencing["$table($viaColumn)$strategy#" . self::KEY_PRELOADED] = $referencingResult;
        unset($this->index[spl_object_hash($referencingResult)]);
    }



    /**
     * Adds new data entry to referencing Result
     *
     * @param array $values
     * @param string $table
     * @param string|null $viaColumn
     * @param Filtering|null $filtering
     * @param string|null $strategy
     */
    public function addToReferencing(array $values, $table, $viaColumn = null, Filtering $filtering = null, $strategy = self::STRATEGY_IN)
    {
        $result = $this->getReferencingResult($table, $viaColumn, $filtering, $strategy);
        $result->addDataEntry($values);
        unset($this->index[spl_object_hash($result)]);
    }



    /**
     * Remove given data entry from referencing Result
     *
     * @param array $values
     * @param string $table
     * @param string|null $viaColumn
     * @param Filtering|null $filtering
     * @param string|null $strategy
     */
    public function removeFromReferencing(array $values, $table, $viaColumn = null, Filtering $filtering = null, $strategy = self::STRATEGY_IN)
    {
        $result = $this->getReferencingResult($table, $viaColumn, $filtering, $strategy);
        $result->removeDataEntry($values);
        unset($this->index[spl_object_hash($result)]);
    }



    /**
     * @param string $table
     * @param string|null $viaColumn
     * @param Filtering|null $filtering
     * @param string|null $strategy
     * @return DataDifference
     */
    public function createReferencingDataDifference($table, $viaColumn = null, Filtering $filtering = null, $strategy = self::STRATEGY_IN)
    {
        return $this->getReferencingResult($table, $viaColumn, $filtering, $strategy)
            ->createDataDifference();
    }



    /**
     * Cleans in-memory cache with referenced results
     *
     * @param string|null $table
     * @param string|null $viaColumn
     */
    public function cleanReferencedResultsCache($table = null, $viaColumn = null)
    {
        if ($table === null or $viaColumn === null) {
            $this->referenced = [];
        } else {
            foreach ($this->referenced as $key => $value) {
                if (preg_match("~^$table\\($viaColumn\\)(#.*)?$~", $key)) {
                    unset($this->referenced[$key]);
                }
            }
        }
    }



    /**
     * Cleans in-memory cache with referencing results
     *
     * @param string|null $table
     * @param string|null $viaColumn
     */
    public function cleanReferencingResultsCache($table = null, $viaColumn = null)
    {
        if ($table === null or $viaColumn === null) {
            $this->referencing = $this->index = [];
        } else {
            foreach ($this->referencing as $key => $value) {
                $strategies = '(' . self::STRATEGY_IN . '|' . self::STRATEGY_UNION . ')';
                if (preg_match("~^$table\\($viaColumn\\)$strategies(#.*)?$~", $key)) {
                    unset($this->index[spl_object_hash($this->referencing[$key])]);
                    unset($this->referencing[$key]);
                }
            }
        }
    }



    /**
     * @param string $table
     * @param string|null $viaColumn
     * @param Filtering|null $filtering
     * @param string|null $strategy
     */
    public function cleanReferencingAddedAndRemovedMeta($table, $viaColumn = null, Filtering $filtering = null, $strategy = self::STRATEGY_IN)
    {
        $this->getReferencingResult($table, $viaColumn, $filtering, $strategy)
            ->cleanAddedAndRemovedMeta();
    }



    /**
     * @param string $proxyClass
     * @throws InvalidArgumentException
     * @return ResultProxy
     */
    public function getProxy($proxyClass)
    {
        if ($this->proxy === null) {
            $this->proxy = new $proxyClass($this);
        }
        if (!is_a($this->proxy, $proxyClass)) {
            throw new InvalidArgumentException('Inconsistent proxy class requested.');
        }
        return $this->proxy;
    }



    /**
     * @return array
     */
    public function __sleep()
    {
        if (self::$storedConnection === null and $this->connection !== null) {
            self::enableSerialization($this->connection);
        }

        return ['isDetached', 'data', 'modified', 'added', 'removed', 'table', 'mapper', 'keys', 'referenced', 'referencing', 'index', 'proxy'];
    }



    public function __wakeup()
    {
        if (self::$storedConnection !== null) {
            $this->setConnection(self::$storedConnection);
        }
    }

    //========== interface \Iterator ====================

    /**
     * @return mixed
     */
    public function current()
    {
        $key = current($this->keys);
        return $this->data[$key];
    }



    public function next()
    {
        next($this->keys);
    }



    /**
     * @return int
     */
    public function key()
    {
        return current($this->keys);
    }



    /**
     * @return bool
     */
    public function valid()
    {
        return current($this->keys) !== false;
    }



    public function rewind()
    {
        $this->keys = array_keys($this->data);
        reset($this->keys);
    }

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

    /**
     * @param array|null $data
     * @param string|null $table
     * @param Connection|null $connection
     * @param IMapper|null $mapper
     */
    private function __construct(array $data = null, $table = null, Connection $connection = null, IMapper $mapper = null)
    {
        $this->data = $data !== null ? $data : [self::DETACHED_ROW_ID => []];
        $this->table = $table;
        $this->connection = $connection;
        $this->mapper = $mapper;
        $this->isDetached = ($table === null or $connection === null or $mapper === null);
    }



    /**
     * @param string $table
     * @param string $viaColumn
     * @param Filtering|null $filtering
     * @throws InvalidArgumentException
     * @throws InvalidStateException
     * @return self
     */
    private function getReferencedResult($table, $viaColumn, Filtering $filtering = null)
    {
        if ($this->isDetached) {
            throw new InvalidStateException('Cannot get referenced Result for detached Result.');
        }
        $key = "$table($viaColumn)";
        if (isset($this->referenced[$forcedKey = $key . '#' . self::KEY_FORCED])) {
            $ids = $this->extractIds($viaColumn);
            $primaryKey = $this->mapper->getPrimaryKey($table);

            foreach ($this->referenced[$forcedKey] as $filteringResult) {
                if ($filteringResult->isValidFor($ids, $filtering->getArgs())) {
                    return $filteringResult->getResult();
                }
            }
        }
        if (isset($this->referenced[$preloadedKey = $key . '#' . self::KEY_PRELOADED])) {
            return $this->referenced[$preloadedKey];
        }
        if ($filtering === null) {
            if (!isset($this->referenced[$key])) {
                if (!isset($ids)) {
                    $ids = $this->extractIds($viaColumn);
                    $primaryKey = $this->mapper->getPrimaryKey($table);
                }
                $data = [];
                if (!empty($ids)) {
                    $data = $this->createTableSelection($table, $ids)
                        ->where('%n.%n IN %in', $table, $primaryKey, $ids)
                        ->execute()->setRowClass(null)->fetchAll();
                }
                $this->referenced[$key] = self::createInstance($data, $table, $this->connection, $this->mapper);
            }
            return $this->referenced[$key];
        }

        // $filtering !== null
        if (!isset($ids)) {
            $ids = $this->extractIds($viaColumn);
            $primaryKey = $this->mapper->getPrimaryKey($table);
        }
        $statement = $this->createTableSelection($table, $ids)->where('%n.%n IN %in', $table, $primaryKey, $ids);
        $filteringResult = $this->applyFiltering($statement, $filtering);

        if ($filteringResult instanceof FilteringResultDecorator) {
            if (!isset($this->referenced[$forcedKey])) {
                $this->referenced[$forcedKey] = [];
            }
            $this->referenced[$forcedKey][] = $filteringResult;
            return $filteringResult->getResult();
        }

        $args = $statement->_export();
        $key .= '#' . $this->calculateArgumentsHash($args);

        if (!isset($this->referenced[$key])) {
            $data = $this->connection->query($args)->setRowClass(null)->fetchAll();
            $this->referenced[$key] = self::createInstance($data, $table, $this->connection, $this->mapper);
        }
        return $this->referenced[$key];
    }



    /**
     * @param string $table
     * @param string $viaColumn
     * @param Filtering|null $filtering
     * @param string $strategy
     * @throws InvalidArgumentException
     * @throws InvalidStateException
     * @return self
     */
    private function getReferencingResult($table, $viaColumn = null, Filtering $filtering = null, $strategy = self::STRATEGY_IN)
    {
        $strategy = $this->translateStrategy($strategy);
        if ($this->isDetached) {
            throw new InvalidStateException('Cannot get referencing Result for detached Result.');
        }
        if ($viaColumn === null) {
            $viaColumn = $this->mapper->getRelationshipColumn($table, $this->table);
        }
        $key = "$table($viaColumn)$strategy";
        if (isset($this->referencing[$forcedKey = $key . '#' . self::KEY_FORCED])) {
            $ids = $this->extractIds($this->mapper->getPrimaryKey($this->table));
            foreach ($this->referencing[$forcedKey] as $filteringResult) {
                if ($filteringResult->isValidFor($ids, $filtering->getArgs())) {
                    return $filteringResult->getResult();
                }
            }
        }
        if (isset($this->referencing[$preloadedKey = $key . '#' . self::KEY_PRELOADED])) {
            return $this->referencing[$preloadedKey];
        }
        if ($strategy === self::STRATEGY_IN) {
            if ($filtering === null) {
                if (!isset($this->referencing[$key])) {
                    isset($ids) or $ids = $this->extractIds($this->mapper->getPrimaryKey($this->table));
                    $statement = $this->createTableSelection($table, $ids);
                    if ($this->isAlias($viaColumn)) {
                        $statement->where('%n IN %in', $this->trimAlias($viaColumn), $ids);
                    } else {
                        $statement->where('%n.%n IN %in', $table, $viaColumn, $ids);
                    }
                    $data = $statement->execute()->setRowClass(null)->fetchAll();
                    $this->referencing[$key] = self::createInstance($data, $table, $this->connection, $this->mapper);
                }
            } else {
                isset($ids) or $ids = $this->extractIds($this->mapper->getPrimaryKey($this->table));
                $statement = $this->createTableSelection($table, $ids);
                if ($this->isAlias($viaColumn)) {
                    $statement->where('%n IN %in', $this->trimAlias($viaColumn), $ids);
                } else {
                    $statement->where('%n.%n IN %in', $table, $viaColumn, $ids);
                }
                $filteringResult = $this->applyFiltering($statement, $filtering);

                if ($filteringResult instanceof FilteringResultDecorator) {
                    if (!isset($this->referencing[$forcedKey])) {
                        $this->referencing[$forcedKey] = [];
                    }
                    $this->referencing[$forcedKey][] = $filteringResult;
                    return $filteringResult->getResult();
                }
                $args = $statement->_export();
                $key .= '#' . $this->calculateArgumentsHash($args);

                if (!isset($this->referencing[$key])) {
                    $data = $this->connection->query($args)->setRowClass(null)->fetchAll();
                    $this->referencing[$key] = self::createInstance($data, $table, $this->connection, $this->mapper);
                }
            }
            return $this->referencing[$key];
        }

        // $strategy === self::STRATEGY_UNION
        if ($filtering === null) {
            if (!isset($this->referencing[$key])) {
                isset($ids) or $ids = $this->extractIds($this->mapper->getPrimaryKey($this->table));
                if (count($ids) === 0) {
                    $data = [];
                } else {
                    $data = $this->connection->query(
                        $this->buildUnionStrategySql($ids, $table, $viaColumn)
                    )->setRowClass(null)->fetchAll();
                }
                $this->referencing[$key] = self::createInstance($data, $table, $this->connection, $this->mapper);
            }
        } else {
            isset($ids) or $ids = $this->extractIds($this->mapper->getPrimaryKey($this->table));
            if (count($ids) === 0) {
                $this->referencing[$key] = self::createInstance([], $table, $this->connection, $this->mapper);
            } else {
                $firstStatement = $this->createTableSelection($table, [reset($ids)]);
                if ($this->isAlias($viaColumn)) {
                    $firstStatement->where('%n = ?', $this->trimAlias($viaColumn), reset($ids));
                } else {
                    $firstStatement->where('%n.%n = ?', $table, $viaColumn, reset($ids));
                }
                $filteringResult = $this->applyFiltering($firstStatement, $filtering);

                if ($filteringResult instanceof FilteringResultDecorator) {
                    if (!isset($this->referencing[$forcedKey])) {
                        $this->referencing[$forcedKey] = [];
                    }
                    $this->referencing[$forcedKey][] = $filteringResult;
                    return $filteringResult->getResult();
                }
                $args = $firstStatement->_export();
                $key .= '#' . $this->calculateArgumentsHash($args);

                if (!isset($this->referencing[$key])) {
                    $sql = $this->buildUnionStrategySql($ids, $table, $viaColumn, $filtering);
                    $data = $this->connection->query($sql)->setRowClass(null)->fetchAll();
                    $result = self::createInstance($data, $table, $this->connection, $this->mapper);
                    $this->referencing[$key] = $result;
                }
            }
        }
        return $this->referencing[$key];
    }



    /**
     * @param string $column
     * @return array
     */
    private function extractIds($column)
    {
        if ($this->isAlias($column)) {
            $column = $this->trimAlias($column);
        }
        $ids = [];
        foreach ($this->data as $data) {
            if (!isset($data[$column]) or $data[$column] === null) {
                continue;
            }
            $ids[$data[$column]] = true;
        }
        return array_keys($ids);
    }



    /**
     * @param array $ids
     * @param string $table
     * @param string $viaColumn
     * @param Filtering|null $filtering
     * @return mixed
     */
    private function buildUnionStrategySql(array $ids, $table, $viaColumn, Filtering $filtering = null)
    {
        $isAlias = $this->isAlias($viaColumn);
        if ($isAlias) {
            $viaColumn = $this->trimAlias($viaColumn);
        }
        foreach ($ids as $id) {
            $statement = $this->createTableSelection($table, [$id]);
            if ($isAlias) {
                $statement->where('%n = ?', $viaColumn, $id);
            } else {
                $statement->where('%n.%n = ?', $table, $viaColumn, $id);
            }
            if ($filtering !== null) {
                $this->applyFiltering($statement, $filtering);
            }
            if (isset($mainStatement)) {
                $mainStatement->union($statement);
            } else {
                $mainStatement = $statement;
            }
        }
        $sql = (string)$mainStatement;

        $driver = $this->connection->getDriver();
        // now we have to fix wrongly generated SQL by dibi...
        if ($driver instanceof DibiSqlite3Driver) {
            $sql = preg_replace('#(?<=UNION )\((SELECT.*?)\)(?= UNION|$)#', '$1', $sql); // (...) UNION (...) to ... UNION ...
        } else {
            $sql = preg_replace('#^(SELECT.*?)(?= UNION)#', '($1)', $sql); // ... UNION (...) to (...) UNION (...)
        }
        return $sql;
    }



    /**
     * @param string $table
     * @param array $relatedKeys
     * @return Fluent
     */
    private function createTableSelection($table, $relatedKeys = null)
    {
        $selection = $this->connection->select('%n.*', $table)->from('%n', $table);
        return $relatedKeys !== null ? $selection->setRelatedKeys($relatedKeys) : $selection;
    }



    /**
     * @param string|null $strategy
     * @throws InvalidArgumentException
     * @return string
     */
    private function translateStrategy($strategy)
    {
        if ($strategy === null) {
            $strategy = self::STRATEGY_IN;
        } else {
            if ($strategy !== self::STRATEGY_IN and $strategy !== self::STRATEGY_UNION) {
                throw new InvalidArgumentException("Unsupported SQL strategy given: '$strategy'.");
            }
        }
        return $strategy;
    }



    /**
     * @param Fluent $statement
     * @param Filtering|null $filtering
     * @return FilteringResult|null
     * @throws InvalidArgumentException
     */
    private function applyFiltering(Fluent $statement, Filtering $filtering)
    {
        $targetedArgs = $filtering->getTargetedArgs();
        foreach ($filtering->getFilters() as $filter) {
            $baseArgs = [];
            if (!($filter instanceof Closure)) {
                foreach (str_split($this->connection->getWiringSchema($filter)) as $autowiredArg) {
                    if ($autowiredArg === 'e') {
                        $baseArgs[] = $filtering->getEntity();
                    } elseif ($autowiredArg === 'p') {
                        $baseArgs[] = $filtering->getProperty();
                    }
                }
                if (isset($targetedArgs[$filter])) {
                    $baseArgs = array_merge($baseArgs, $targetedArgs[$filter]);
                }
            }
            $result = call_user_func_array([$statement, 'applyFilter'], array_merge([$filter], $baseArgs, $filtering->getArgs()));
            if ($result instanceof FilteringResult) {
                return new FilteringResultDecorator($result, $baseArgs);
            }
        }
    }



    /**
     * @param array $arguments
     * @return string
     */
    private function calculateArgumentsHash(array $arguments)
    {
        return md5(serialize($arguments));
    }



    /**
     * @param string $column
     * @return bool
     */
    private function isAlias($column)
    {
        return strncmp($column, '*', 1) === 0;
    }



    /**
     * @param string $column
     * @return string
     */
    private function trimAlias($column)
    {
        return substr($column, 1);
    }

}
tharos/leanmapper v3.1.1 API documentation API documentation generated by ApiGen