<?php

/** Filtered table representation
 * @method NotORM_Result and (mixed $condition, mixed $parameters = array()) Add AND condition
 * @method NotORM_Result or (mixed $condition, mixed $parameters = array()) Add OR condition
 * @property string  sequence
 */
class NotORM_Result extends NotORM_Abstract implements Iterator, ArrayAccess, Countable, JsonSerializable{
    protected $single;
    protected $select             = array(), $conditions = array(), $where = array(), $parameters = array(), $order = array(), $limit = null, $offset = null, $group = "", $having = "", $lock = null;
    protected $union              = array(), $unionOrder = array(), $unionLimit = null, $unionOffset = null;
    protected $data, $referencing = array(), $aggregation = array(), $accessed, $access, $keys = array();

    public static $queryTimes = 0;

    /** Create table result
     *
     * @param string $table
     * @param NotORM $notORM
     * @param bool $single row
     *
     * @access protected must be public because it is called from NotORM
     */
    function __construct($table, NotORM $notORM, $single = false){
        $this->table   = $table;
        $this->notORM  = $notORM;
        $this->single  = $single;
        $this->primary = $notORM->structure->getPrimary($table);
    }

    /** Save data to cache and empty result
     */
    function __destruct(){
        if($this->notORM->cache && !$this->select && isset($this->rows)){
            $access = $this->access;
            if(is_array($access)){
                $access = array_filter($access);
            }
            $this->notORM->cache->save("$this->table;" . implode(",", $this->conditions), $access);
        }
        $this->rows = null;
        unset($this->data);
    }

    protected function limitString($limit, $offset = null){
        $return = "";
        if(isset($limit) && $this->notORM->driver != "oci" && $this->notORM->driver != "dblib" && $this->notORM->driver != "mssql" && $this->notORM->driver != "sqlsrv"){
            $return .= " LIMIT $limit";
            if($offset){
                //$return .= " OFFSET $offset";
                $return .= ",$offset";      //@dogstar 2014-10-24
            }
        }
        return $return;
    }

    protected function removeExtraDots($expression){
        return preg_replace('~(?:\\b[a-z_][a-z0-9_.:]*[.:])?([a-z_][a-z0-9_]*)[.:]([a-z_*])~i', '\\1.\\2', $expression); // rewrite tab1.tab2.col
    }

    protected function whereString(){
        $return = "";
        if($this->group){
            $return .= " GROUP BY $this->group";
        }
        if($this->having){
            $return .= " HAVING $this->having";
        }
        if($this->order){
            $return .= " ORDER BY " . implode(", ", $this->order);
        }
        $return = $this->removeExtraDots($return);

        $where = $this->where;
        if(isset($this->limit) && $this->notORM->driver == "oci"){
            $where[] = ($where ? " AND " : "") . "(" . ($this->offset ? "rownum > $this->offset AND "
                    : "") . "rownum <= " . ($this->limit + $this->offset) . ")"; //! rownum > doesn't work - requires subselect (see adminer/drivers/oracle.inc.php)
        }
        if($where){
            $return = " WHERE " . implode($where) . $return;
        }

        $return .= $this->limitString($this->limit, $this->offset);
        if(isset($this->lock)){
            $return .= ($this->lock ? " FOR UPDATE" : " LOCK IN SHARE MODE");
        }
        return $return;
    }

    /**
     * @param      $limit
     *
     * @return string
     */
    protected function topString( $limit){
        if(isset($limit) && ($this->notORM->driver == "dblib" || $this->notORM->driver == "mssql" || $this->notORM->driver == "sqlsrv")){
            return " TOP ($this->limit)"; //! offset is not supported
        }
        return "";
    }

    protected function createJoins($val){
        $return = array();
        preg_match_all('~\\b([a-z_][a-z0-9_.:]*[.:])[a-z_*]~i', $val, $matches);
        foreach($matches[1] as $names){
            $parent = $this->table;
            if($names != "$parent."){ // case-sensitive
                preg_match_all('~\\b([a-z_][a-z0-9_]*)([.:])~i', $names, $matches, PREG_SET_ORDER);
                foreach($matches as $match){
                    list(, $name, $delimiter) = $match;
                    $table         = $this->notORM->structure->getReferencedTable($name, $parent);
                    $column        = ($delimiter == ':' ? $this->notORM->structure->getPrimary($parent)
                        : $this->notORM->structure->getReferencedColumn($name, $parent));
                    $primary       = ($delimiter == ':' ? $this->notORM->structure->getReferencedColumn($parent, $table)
                        : $this->notORM->structure->getPrimary($table));
                    $return[$name] = " LEFT JOIN $table" . ($table != $name ? " AS $name"
                            : "") . " ON $parent.$column = $name.$primary"; // should use alias if the table is used on more places
                    $parent        = $name;
                }
            }
        }
        return $return;
    }

    /** Get SQL query
     * @return string
     */
    function __toString(){
        $return = "SELECT" . $this->topString($this->limit) . " ";
        $join   = $this->createJoins(implode(",", $this->conditions) . "," . implode(",", $this->select) . ",$this->group,$this->having," . implode(",", $this->order));
        if(!isset($this->rows) && $this->notORM->cache && !is_string($this->accessed)){
            $this->accessed = $this->notORM->cache->load("$this->table;" . implode(",", $this->conditions));
            $this->access   = $this->accessed;
        }
        if($this->select){
            $return .= $this->removeExtraDots(implode(", ", $this->select));
        }elseif($this->accessed){
            $return .= ($join ? "$this->table." : "") . implode(", " . ($join ? "$this->table."
                        : ""), array_keys($this->accessed));
        }else{
            $return .= ($join ? "$this->table." : "") . "*";
        }
        $return .= " FROM $this->table" . implode($join) . $this->whereString();
        if($this->union){
            $return = ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $return
                    : "($return)") . implode($this->union);
            if($this->unionOrder){
                $return .= " ORDER BY " . implode(", ", $this->unionOrder);
            }
            $return .= $this->limitString($this->unionLimit, $this->unionOffset);
            $top = $this->topString($this->unionLimit);
            if($top){
                $return = "SELECT$top * FROM ($return) t";
            }
        }
        return $return;
    }

    /**
     * 放开限制 protected -> public @scott 反馈
     *
     * @param $query        string
     * @param $parameters   array
     *
     * @return bool|PDOStatement
     */
    public function query($query, $parameters){
        $debugTrace = array();

        self::$queryTimes++;

        /**
         * 修正当参数过多时的SQLSTATE[HY093] @dogstar 2014-11-18
         */
        $parameters = array_map(array($this, 'formatValue'), $parameters);
        foreach($parameters as $key => $val){
            if(substr($key, 0, 1) == ':' && stripos($query, $key) === false){
                unset($parameters[$key]);
            }
        }

        if($this->notORM->debug){
            $debugTrace['startTime'] = microtime(true);

            if(!is_callable($this->notORM->debug)){
                $debug = "$query;";
                if($parameters){
                    $debug .= " -- " . implode(", ", array_map(array($this, 'quote'), $parameters));
                }
                $pattern = '(^' . preg_quote(dirname(__FILE__)) . '(\\.php$|[/\\\\]))'; // can be static
                foreach(debug_backtrace() as $backtrace){
                    if(isset($backtrace["file"]) && !preg_match($pattern, $backtrace["file"])){ // stop on first file outside NotORM source codes
                        break;
                    }
                }
                /** @var array $backtrace */
                //error_log("{$backtrace['file']}:{$backtrace['line']}:$debug\n", 0);
                //if ($this->notORM->debug) echo "$debug<br />\n";    //@dogstar 2014-10-31

                $debugTrace['sql'] = $debug;
            }elseif(call_user_func($this->notORM->debug, $query, $parameters) === false){
                return false;
            }
        }

        $return = $this->notORM->connection->prepare($query);

        //对于替换参数进行处理   @喵了个咪 2015-12-25
        $sum = 1;
        foreach($parameters as $key => $val){
            if(strstr($key, ':')){
                if(is_int($val)){
                    $return->bindValue($key, $val, PDO::PARAM_INT);
                }elseif(is_bool($val)){
                    $return->bindValue($key, $val, PDO::PARAM_BOOL);
                }elseif(is_null($val)){
                    $return->bindValue($key, $val, PDO::PARAM_NULL);
                }else{
                    $return->bindValue($key, $val);
                }
            }else{
                if(is_int($val)){
                    $return->bindValue($sum, $val, PDO::PARAM_INT);
                }elseif(is_bool($val)){
                    $return->bindValue($sum, $val, PDO::PARAM_BOOL);
                }elseif(is_null($val)){
                    $return->bindValue($sum, $val, PDO::PARAM_NULL);
                }else{
                    $return->bindValue($sum, $val);
                }
                $sum++;
            }
        }

        $errorMessage = null;
        if(!$return || !$return->execute()){
            // fixed non-object bug @dogstar 20180326
            if ($return) {
                $errorInfo = $return->errorInfo();
                $errorMessage = isset($errorInfo[2]) ? $errorInfo[2] : $errorMessage;
            } else {
                $errorMessage = 'the database server cannot successfully prepare the statement';
            }

            $return = false;
        }

        if($this->notORM->debug){
            $debugTrace['endTime'] = microtime(true);

            $sqlInfo = sprintf("[%s - %sms]%s",
                self::$queryTimes,
                round(($debugTrace['endTime'] - $debugTrace['startTime']) * 1000, 2),
                $debugTrace['sql']
            );

            if ($this->notORM->debugTimer && is_callable($this->notORM->debugTimer)) {
                call_user_func_array($this->notORM->debugTimer, array($sqlInfo));
            }
        }

        //显式抛出异常，以让开发同学尽早发现SQL语法问题 @dogstar 20150426
        if($return === false && $errorMessage !== null){
            throw new PDOException($errorMessage);
        }

        return $return;
    }

    protected function formatValue($val){
        if($val instanceof DateTime){
            return $val->format("Y-m-d H:i:s"); //! may be driver specific
        }
        return $val;
    }

    protected function quote($val){
        if(!isset($val)){
            return "NULL";
        }
        if(is_array($val)){ // (a, b) IN ((1, 2), (3, 4))
            return "(" . implode(", ", array_map(array($this, 'quote'), $val)) . ")";
        }
        $val = $this->formatValue($val);
        if(is_float($val)){
            return sprintf("%F", $val); // otherwise depends on setlocale()
        }
        if($val === false){
            return "0";
        }
        if(is_int($val) || $val instanceof NotORM_Literal){ // number or SQL code - for example "NOW()"
            return (string) $val;
        }
        return $this->notORM->connection->quote($val);
    }

    /**
     * 执行带结果的原生sql，可用于插入、更新、删除等执行性的语句 @dogstar 20190329
     *
     * @param $sql        string
     * @param $parameters   array
     *
     * @return int number of affected rows or false in case of an error
     */
    function executeSql($sql, $parameters = array()) {
        $return = $this->query($sql, $parameters);
        if(!$return){
            return false;
        }
        return $return->rowCount();
    }

    /** Shortcut for call_user_func_array(array($this, 'insert'), $rows)
     *
     * @param array $rows 待批量添加的数据
     * @param boolean $isIgnore 是否在插入时使用IGNORE
     *
     * @return int number of affected rows or false in case of an error
     */
    function insert_multi(array $rows, $isIgnore = false){
        if($this->notORM->freeze){
            return false;
        }
        if(!$rows){
            return 0;
        }
        $data       = reset($rows);
        $parameters = array();
        if($data instanceof NotORM_Result){
            $parameters = $data->parameters; //! other parameters
            $data       = (string) $data;
        }elseif($data instanceof Traversable){
            $data = iterator_to_array($data);
        }
        $insert = $data;
        if(is_array($data)){
            $values = array();
            foreach($rows as $value){
                if($value instanceof Traversable){
                    $value = iterator_to_array($value);
                }
                $values[] = $this->quote($value);
                foreach($value as $val){
                    if($val instanceof NotORM_Literal && $val->parameters){
                        $parameters = array_merge($parameters, $val->parameters);
                    }
                }
            }

            $quoteChar = $this->getQuoteChar();

            //! driver specific extended insert
            $insert = ($data || $this->notORM->driver == "mysql")
                ? "({$quoteChar}" . implode("{$quoteChar}, {$quoteChar}", array_keys($data)) . "{$quoteChar}) VALUES " . implode(", ", $values)
                : "DEFAULT VALUES";
        }

        // @dogstar 20181208
        $ignoreSql = $isIgnore ? 'IGNORE' : '';

        // requires empty $this->parameters
        $return = $this->query("INSERT $ignoreSql INTO $this->table $insert", $parameters);
        if(!$return){
            return false;
        }
        $this->rows = null;
        return $return->rowCount();
    }

    /** Insert row in a table
     *
     * @param mixed array($column => $value)|Traversable for single row insert or NotORM_Result|string for INSERT ... SELECT
     * @param ... used for extended insert
     *
     * @return mixed inserted NotORM_Row or false in case of an error or number of affected rows for INSERT ... SELECT
     */
    function insert($data){
        $rows   = func_get_args();
        $return = $this->insert_multi($rows);
        if(!$return){
            return false;
        }
        if(!is_array($data)){
            return $return;
        }

        // #56 postgresql无法获取新增数据的主键ID @ clov4r-连友 201608
        if ($this->notORM->driver == "pgsql") {
            if (!isset($data[$this->primary])) {
                //获取序列名称
                $pgss = $this->query("SELECT pg_get_serial_sequence('" . $this->table . "', '" . $this->primary . "') pgss", $this->parameters)->fetch();
                if (isset($pgss['pgss'])) {
                    $rs                   = $this->query("select last_value id from " . $pgss['pgss'], $this->parameters)->fetch();
                    $data[$this->primary] = $rs['id'];
                    $this->sequence       = $rs['id'];
                }
            }
        } else {
            if (!isset($data[$this->primary]) && ($id = $this->notORM->connection->lastInsertId($this->notORM->structure->getSequence($this->table)))) {
                $data[$this->primary] = $id;
            }
        }
        return new $this->notORM->rowClass($data, $this);
    }

    /** Update all rows in result set
     *
     * @param array ($column => $value)
     *
     * @return int number of affected rows or false in case of an error
     */
    function update(array $data){
        if($this->notORM->freeze){
            return false;
        }
        if(!$data){
            return 0;
        }
        $values     = array();
        $parameters = array();
        $quoteChar  = $this->getQuoteChar();
        foreach($data as $key => $val){
            // doesn't use binding because $this->parameters can be filled by ? or :name
            $values[] = "{$quoteChar}{$key}{$quoteChar} = " . $this->quote($val);
            if($val instanceof NotORM_Literal && $val->parameters){
                $parameters = array_merge($parameters, $val->parameters);
            }
        }
        if($this->parameters){
            $parameters = array_merge($parameters, $this->parameters);
        }
        // joins in UPDATE are supported only in MySQL
        $return = $this->query("UPDATE" . $this->topString($this->limit) . " $this->table SET " . implode(", ", $values) . $this->whereString(), $parameters);
        if(!$return){
            return false;
        }
        return $return->rowCount();
    }

    /**
     * 更新单个计数器，更友好的+1或-1或更新计数器的操作
     *
     * @param string $column
     * @param int/float $number
     *
     @ return int number of affected rows or false in case of an error
     */
    function updateCounter($column, $number = 1) {
        return $this->update(array($column => $this->createLiteral($column, $number)));
    }

    /**
     * 更新多个计数器
     *
     * @param array $data
     *
     @ return int number of affected rows or false in case of an error
     */
    function updateMultiCounters(array $data) {
        $updateData = array();

        // 转换
        foreach ($data as $column => $number) {
            $updateData[$column] = $this->createLiteral($column, $number);
        }

        return $this->update($updateData);
    }

    protected function createLiteral($column, $number) {
        return new NotORM_Literal(
            sprintf('%s %s %s', $column, $number >= 0 ? '+' : '-', abs($number))
        );
    }

    /**
     * 获取数据库字段的引号
     * @author dogstar 20170617
     */
    protected function getQuoteChar() {
        $allowDBs = array('mysql');
        return in_array($this->notORM->driver, $allowDBs) ? '`' : '';
    }

    /** Insert row or update if it already exists
     *
     * @param array ($column => $value)
     * @param array ($column => $value)
     * @param array ($column => $value), empty array means use $insert
     *
     * @return int number of affected rows or false in case of an error
     */
    function insert_update(array $unique, array $insert, array $update = array()){
        if(!$update){
            $update = $insert;
        }
        $insert = $unique + $insert;
        $values = "(" . implode(", ", array_keys($insert)) . ") VALUES " . $this->quote($insert);
        //! parameters
        if($this->notORM->driver == "mysql"){
            $set = array();
            if(!$update){
                $update = $unique;
            }
            $quoteChar  = $this->getQuoteChar();

            foreach($update as $key => $val){
                $set[] = "{$quoteChar}{$key}{$quoteChar} = " . $this->quote($val);
                //! parameters
            }
            return $this->insert("$values ON DUPLICATE KEY UPDATE " . implode(", ", $set));
        }else{
            $connection = $this->notORM->connection;
            $errorMode  = $connection->getAttribute(PDO::ATTR_ERRMODE);
            $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            try{
                $return = $this->insert($values);
                $connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
                return $return;
            }
            catch(PDOException $e){
                $connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode);
                if($e->getCode() == "23000" || $e->getCode() == "23505"){ // "23000" - duplicate key, "23505" unique constraint pgsql
                    if(!$update){
                        return 0;
                    }
                    $clone  = clone $this;
                    $return = $clone->where($unique)->update($update);
                    return ($return ? $return + 1 : $return);
                }
                if($errorMode == PDO::ERRMODE_EXCEPTION){
                    throw $e;
                }elseif($errorMode == PDO::ERRMODE_WARNING){
                    trigger_error("PDOStatement::execute(): " . $e->getMessage(), E_USER_WARNING); // E_WARNING is unusable
                }
            }
        }
        return 0;
    }

    /** Get last insert ID
     * @return string number
     */
    function insert_id(){
        return $this->notORM->connection->lastInsertId();
    }

    /** Delete all rows in result set
     * @return bool|int number of affected rows or false in case of an error
     * @throws Exception
     */
    function delete(){
        if($this->notORM->freeze){
            return false;
        }
        //防止误删，禁止全表的删除
        //@dogstar - 2014-10-24
        $where = $this->whereString();
        if(empty($where)){
            throw new Exception('sorry, you can not delete the whole table --dogstar');
        }

        $return = $this->query("DELETE" . $this->topString($this->limit) . " FROM $this->table" . $where, $this->parameters);
        if(!$return){
            return false;
        }
        return $return->rowCount();
    }

    /** Add select clause, more calls appends to the end
     *
     * @param string $columns for example "column, MD5(column) AS column_md5", empty string to reset previously set columns
     * @param string ...
     *
     * @return NotORM_Result fluent interface
     */
    function select($columns){
        $this->__destruct();
        if($columns != ""){
            foreach(func_get_args() as $columns){
                $this->select[] = $columns;
            }
        }else{
            $this->select = array();
        }
        return $this;
    }

    /** Add where condition, more calls appends with AND
     *
     * @param mixed string possibly containing ? or :name; or array($condition => $parameters, ...)
     * @param mixed array accepted by PDOStatement::execute or a scalar value
     * @param mixed ...
     *
     * @return NotORM_Result fluent interface
     */
    function where($condition, $parameters = array()){
        $args = func_get_args();
        return $this->whereOperator("AND", $args);
    }

    protected function whereOperator($operator, array $args){
        $condition  = $args[0];
        $parameters = (count($args) > 1 ? $args[1] : array());
        if(is_array($condition)){ // where(array("column1" => 1, "column2 > ?" => 2))
            foreach($condition as $key => $val){
                $this->where($key, $val);
            }
            return $this;
        }
        $this->__destruct();
        $this->conditions[] = "$operator $condition";
        $condition          = $this->removeExtraDots($condition);
        if(count($args) != 2 || strpbrk($condition, "?:")){ // where("column < ? OR column > ?", array(1, 2))
            if(count($args) != 2 || !is_array($parameters)){ // where("column < ? OR column > ?", 1, 2)
                $parameters = array_slice($args, 1);
            }
            $this->parameters = array_merge($this->parameters, $parameters);
        }elseif($parameters === null){ // where("column", null)
            $condition .= " IS NULL";
        }elseif($parameters instanceof NotORM_Result){ // where("column", $db->$table())
            $clone = clone $parameters;
            if(!$clone->select){
                $clone->select($this->notORM->structure->getPrimary($clone->table));
            }
            if($this->notORM->driver != "mysql"){
                if($clone instanceof NotORM_MultiResult){
                    array_shift($clone->select);
                    $clone->single();
                }
                $condition .= " IN ($clone)";
                $this->parameters = array_merge($this->parameters, $clone->parameters);
            }else{
                $in = array();
                foreach($clone as $row){
                    $row = array_values(iterator_to_array($row));
                    if($clone instanceof NotORM_MultiResult && count($row) > 1){
                        array_shift($row);
                    }
                    if(count($row) == 1){
                        $in[] = $this->quote($row[0]);
                    }else{
                        $in[] = $this->quote($row);
                    }
                }
                if($in){
                    $condition .= " IN (" . implode(", ", $in) . ")";
                }else{
                    $condition = "($condition) IS NOT NULL AND $condition IS NULL"; // $condition = "NOT id"
                }
            }
        }elseif(!is_array($parameters)){ // where("column", "x")
            $condition .= " = " . $this->quote($parameters);
        }else{ // where("column", array(1, 2))
            $condition = $this->whereIn($condition, $parameters);
        }
        $this->where[] = (preg_match('~^\)+$~', $condition)
            ? $condition : ($this->where ? " $operator "
                : "") . "($condition)");
        return $this;
    }

    protected function whereIn($condition, $parameters){
        if(!$parameters){
            $condition = "($condition) IS NOT NULL AND $condition IS NULL";
        }elseif($this->notORM->driver != "oci"){
            $column = $condition;
            $condition .= " IN " . $this->quote($parameters);
            $nulls = array_filter($parameters, 'is_null');
            if($nulls){
                $condition = "$condition OR $column IS NULL";
            }
        }else{ // http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/expressions014.htm
            $or = array();
            for($i = 0; $i < count($parameters); $i += 1000){
                $or[] = "$condition IN " . $this->quote(array_slice($parameters, $i, 1000));
            }
            $condition = implode(" OR ", $or);
        }
        return $condition;
    }

    function __call($name, array $args){
        $operator = strtoupper($name);
        switch($operator){
            case "AND":
            case "OR":
                return $this->whereOperator($operator, $args);
        }
        trigger_error("Call to undefined method NotORM_Result::$name()", E_USER_ERROR);
        return FALSE;
    }

    /** Shortcut for where()
     *
     * @param string
     * @param mixed
     * @param mixed ...
     *
     * @return NotORM_Result fluent interface
     */
    function __invoke($where, $parameters = array()){
        $args = func_get_args();
        return $this->whereOperator("AND", $args);
    }

    /** Add order clause, more calls appends to the end
     *
     * @param string $columns for example "column1, column2 DESC", empty string to reset previous order
     * @param string ...
     *
     * @return NotORM_Result fluent interface
     */
    function order($columns){
        $this->rows = null;
        if($columns != ""){
            foreach(func_get_args() as $columns){
                if($this->union){
                    $this->unionOrder[] = $columns;
                }else{
                    $this->order[] = $columns;
                }
            }
        }elseif($this->union){
            $this->unionOrder = array();
        }else{
            $this->order = array();
        }
        return $this;
    }

    /**
     * 对查询进行limit操作
     * 请注意以下两种用法，与NotORM原来的定义有所区别
     * - limit(数量)
     * - limit(开始位置，数量)
     * Set limit clause, more calls rewrite old values
     *
     * @param int
     * @param int
     *
     * @return NotORM_Result fluent interface
     */
    function limit($limit, $offset = null){
        $this->rows = null;
        if($this->union){
            $this->unionLimit  = +$limit;
            $this->unionOffset = +$offset;
        }else{
            $this->limit  = +$limit;
            $this->offset = +$offset;
        }
        return $this;
    }

    /** Set group clause, more calls rewrite old values
     *
     * @param string
     * @param string
     *
     * @return NotORM_Result fluent interface
     */
    function group($columns, $having = ""){
        $this->__destruct();
        $this->group  = $columns;
        $this->having = $having;
        return $this;
    }

    /** Set select FOR UPDATE or LOCK IN SHARE MODE
     *
     * @param bool
     *
     * @return NotORM_Result fluent interface
     */
    function lock($exclusive = true){
        $this->lock = $exclusive;
        return $this;
    }

    /**
     * @param NotORM_Result
     * @param bool
     *
     * @return NotORM_Result fluent interface
     */
    function union(NotORM_Result $result, $all = false){
        $this->union[]    = " UNION " . ($all ? "ALL "
                : "") . ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $result : "($result)");
        $this->parameters = array_merge($this->parameters, $result->parameters);
        return $this;
    }

    /** Execute aggregation function
     *
     * @param string $function
     *
     * @return string
     */
    function aggregation($function){
        $join  = $this->createJoins(implode(",", $this->conditions) . ",$function");
        $query = "SELECT $function FROM $this->table" . implode($join);
        if($this->where){
            $query .= " WHERE " . implode($this->where);
        }

        foreach($this->query($query, $this->parameters)->fetch() as $return){
            return $return;
        }
    }

    /** Count number of rows
     *
     * @param string
     *
     * @return int
     */
    function count($column = "*"){
        return $this->aggregation("COUNT($column)");
    }

    /** Return minimum value from a column
     *
     * @param string
     *
     * @return int
     */
    function min($column){
        return $this->aggregation("MIN($column)");
    }

    /** Return maximum value from a column
     *
     * @param string
     *
     * @return int
     */
    function max($column){
        return $this->aggregation("MAX($column)");
    }

    /** Return sum of values in a column
     *
     * @param string
     *
     * @return int
     */
    function sum($column){
        return $this->aggregation("SUM($column)");
    }

    /** Execute the built query
     *
     */
    protected function execute(){
        if(!isset($this->rows)){
            $result     = false;
            $exception  = null;
            $parameters = array();
            foreach(array_merge($this->select, array(
                $this,
                $this->group,
                $this->having
            ), $this->order, $this->unionOrder) as $val){
                if(($val instanceof NotORM_Literal || $val instanceof self) && $val->parameters){
                    $parameters = array_merge($parameters, $val->parameters);
                }
            }
            try{
                $result = $this->query($this->__toString(), $parameters);
            }
            catch(PDOException $exception){
                // handled later
            }
            if(!$result){
                if(!$this->select && $this->accessed){
                    $this->accessed = '';
                    $this->access   = array();
                    $result         = $this->query($this->__toString(), $parameters);
                }elseif($exception){
                    throw $exception;
                }
            }
            $this->rows = array();
            if($result){
                $result->setFetchMode(PDO::FETCH_ASSOC);
                foreach($result as $key => $row){
                    if(isset($row[$this->primary])){
                        $key = $row[$this->primary];
                        if(!is_string($this->access)){
                            $this->access[$this->primary] = true;
                        }
                    }
                    //$this->rows[$key] = new $this->notORM->rowClass($row, $this);
                    if ($this->notORM->isKeepPrimaryKeyIndex) {
                        //@dogstar 采用主键作为下标 2015-12-30
                        $this->rows[$key] = $row;
                    } else {
                        //@dogstar 改用返回数组 2014-11-01
                        $this->rows[] = $row;
                    }
                }
            }
            $this->data = $this->rows;
        }
    }

    /** Fetch next row of result
     *
     * @param string $column name to return or an empty string for the whole row
     *
     * @return mixed string or null with $column, NotORM_Row without $column, false if there is no row
     */
    function fetch($column = ''){
        // no $this->select($column) because next calls can access different columns
        $this->execute();
        $return = current($this->data);
        next($this->data);
        if($return && $column != ''){
            return $return[$column];
        }
        return $return;
    }

    /**
     * fetchRow别名，等效于NotORM_Result::fetchRow()
     * @author: dogstar 2015-04-26
     *
     * @param string $column
     *
     * @return mixed
     */
    function fetchOne($column = ''){
        return $this->fetchRow($column);
    }

    /**
     * 只查询第一行纪录，等效于NotORM_Result::fetchOne()
     * @author: dogstar 2015-04-18
     *
     * @param string $column
     *
     * @return mixed
     */
    function fetchRow($column = ''){
        $this->limit(1)->execute();
        return $this->fetch($column);
    }

    /**
     * 返回全部的数据，等效于NotORM_Result::fetchRows()
     * @author: dogstar 2014-11-01
     */
    function fetchAll(){
        $this->execute();
        return $this->data;
    }

    /**
     * fetchAll别名，等效于NotORM_Result::fetchAll()
     * @author: dogstar 2015-04-26
     */
    function fetchRows(){
        return $this->fetchAll();
    }

    /**
     * queryRows别名，等效于NotORM_Result::queryRows($sql, $parmas)
     * @author: dogstar 2015-04-26
     *
     * @param string    $sql
     * @param array     $parmas
     *
     * @return array
     */
    function queryAll($sql, $parmas = array()){
        return $this->queryRows($sql, $parmas);
    }

    /**
     * 根据SQL查询全部数据，等效于NotORM_Result::queryAll($sql, $parmas)
     *
     * @param string    $sql
     * @param array     $parmas
     *
     * @return array
     * @author: dogstar 2014-11-01
     */
    function queryRows($sql, $parmas = array()){
        $result = $this->query($sql, $parmas);

        $rows = array();
        if($result){
            $result->setFetchMode(PDO::FETCH_ASSOC);
            foreach($result as $row){
                if(isset($row[$this->primary])){
                    if(!is_string($this->access)){
                        $this->access[$this->primary] = true;
                    }
                }
                $rows[] = $row;
            }
        }

        return $rows;
    }

    /** Fetch all rows as associative array
     *
     * @param string $key
     * @param string $value column name used for an array value or an empty string for the whole row
     *
     * @return array
     */
    function fetchPairs($key, $value = ''){
        $return = array();
        $clone  = clone $this;
        if($value != ""){
            $clone->select = array();
            $clone->select("$key, $value"); // MultiResult adds its column
        }elseif($clone->select){
            array_unshift($clone->select, $key);
        }else{
            $clone->select = array("$key, $this->table.*");
        }
        foreach($clone as $row){
            //$values = array_values(iterator_to_array($row));
            //bugfixed: Argument 1 passed to iterator_to_array() must implement interface Traversable, array given
            //@Scott @dogstar 20151122
            /** @var Traversable|array $row */
            $values = ($row instanceof Traversable) ? array_values(iterator_to_array($row)) : array_values($row);
            if($value != "" && $clone instanceof NotORM_MultiResult){
                array_shift($values);
            }
            $return[(string) $values[0]] = ($value != "" ? $values[(array_key_exists(1, $values) ? 1 : 0)]
                : $row); // isset($values[1]) - fetchPairs("id", "id")
        }
        return $return;
    }

    protected function access($key, $delete = false){
        if($delete){
            if(is_array($this->access)){
                $this->access[$key] = false;
            }
            return false;
        }
        if(!isset($key)){
            $this->access = '';
        }elseif(!is_string($this->access)){
            $this->access[$key] = true;
        }
        if(!$this->select && $this->accessed && (!isset($key) || !isset($this->accessed[$key]))){
            $this->accessed = '';
            $this->rows     = null;
            return true;
        }
        return false;
    }

    protected function single(){
    }

    // Iterator implementation (not IteratorAggregate because $this->data can be changed during iteration)

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

    /** @return NotORM_Row */
    function current(){
        return $this->data[current($this->keys)];
    }

    /** @return string row ID */
    function key(){
        return current($this->keys);
    }

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

    function valid(){
        return current($this->keys) !== false;
    }

    // ArrayAccess implementation

    /** Test if row exists
     *
     * @param mixed $key
     *
     * @return bool
     *
     */
    function offsetExists($key){
        $row = $this->offsetGet($key);
        return isset($row);
    }

    /** Get specified row
     *
     * @param string $key row ID or array for where conditions
     *
     * @return NotORM_Row or null if there is no such row
     */
    function offsetGet($key){
        if($this->single && !isset($this->data)){
            $clone = clone $this;
            if(is_array($key)){
                $clone->where($key)->limit(1);
            }else{
                $clone->where($this->primary, $key);
            }
            $return = $clone->fetch();
            if($return){
                return $return;
            }
        }else{
            $this->execute();
            if(is_array($key)){
                foreach($this->data as $row){
                    foreach($key as $k => $v){
                        if((isset($v) && $row[$k] !== null ? $row[$k] != $v : $row[$k] !== $v)){
                            continue 2;
                        }
                    }
                    return $row;
                }
            }elseif(isset($this->data[$key])){
                return $this->data[$key];
            }
        }
        return NULL;
    }

    /** Mimic row
     *
     * @param string $key row ID
     * @param NotORM_Row $value
     *
     */
    function offsetSet($key, $value){
        $this->execute();
        $this->data[$key] = $value;
    }

    /** Remove row from result set
     *
     * @param string $key row ID
     *
     */
    function offsetUnset($key){
        $this->execute();
        unset($this->data[$key]);
    }

    // JsonSerializable implementation

    function jsonSerialize(){
        $this->execute();
        if($this->notORM->jsonAsArray){
            return array_values($this->data);
        }else{
            return $this->data;
        }
    }

    /**
     * 获取当前PDO连接
     * @return \PDO
     */
    function getConn()
    {
        return $this->notORM->connection;
    }

    /**
     * Execute an SQL statement and return the number of affected rows
     * @param string $query
     * @return int|false
     */
    function exec($query)
    {
        $conn = $this->getConn();
        $sql = $conn->quote($query);
        return $conn->exec($sql);
    }

    /** ------------------ 事务操作 ------------------ **/

    /**
     * 开启数据库事务
     * @return bool
     */
    public function beginTransaction()
    {
        return $this->getConn()->beginTransaction();
    }

    /**
     * 提交数据库事务
     * @return bool
     */
    public function commit()
    {
        return $this->getConn()->commit();
    }

    /**
     * 回滚数据库事务
     * @return bool
     */
    public function rollBack()
    {
        return $this->getConn()->rollBack();
    }
}


<?php

/** Representation of filtered table grouped by some column
*/
class NotORM_MultiResult extends NotORM_Result {
	private $result, $column, $active;

	/** @access protected must be public because it is called from Row */
	function __construct($table, NotORM_Result $result, $column, $active) {
		parent::__construct($table, $result->notORM);
		$this->result = $result;
		$this->column = $column;
		$this->active = $active;
	}

	/** Specify referencing column
	* @param string
	* @return NotORM_MultiResult fluent interface
	*/
	function via($column) {
		$this->column = $column;
		$this->conditions[0] = "$this->table.$column AND";
		$this->where[0] = "(" . $this->whereIn("$this->table.$column", array_keys((array) $this->result->rows)) . ")";
		return $this;
	}

	function insert_multi(array $rows, $isIgnore = false) {
		$args = array();
		foreach ($rows as $data) {
			if ($data instanceof Traversable && !$data instanceof NotORM_Result) {
				$data = iterator_to_array($data);
			}
			if (is_array($data)) {
				$data[$this->column] = $this->active;
			}
			$args[] = $data;
		}
		return parent::insert_multi($args, $isIgnore);
	}

	function insert_update(array $unique, array $insert, array $update = array()) {
		$unique[$this->column] = $this->active;
		return parent::insert_update($unique, $insert, $update);
	}

	protected function single() {
		$this->where[0] = "($this->column = " . $this->quote($this->active) . ")";
	}

	function update(array $data) {
		$where = $this->where;
		$this->single();
		$return = parent::update($data);
		$this->where = $where;
		return $return;
	}

	function delete() {
		$where = $this->where;
		$this->single();
		$return = parent::delete();
		$this->where = $where;
		return $return;
	}

	function select($columns) {
		$args = func_get_args();
		if (!$this->select) {
			array_unshift($args, "$this->table.$this->column");
		}
		return call_user_func_array(array($this, 'parent::select'), $args);
	}

	function order($columns) {
		if (!$this->order) { // improve index utilization
			$this->order[] = "$this->table.$this->column" . (preg_match('~\\bDESC$~i', $columns) ? " DESC" : "");
		}
		$args = func_get_args();
		return call_user_func_array(array($this, 'parent::order'), $args);
	}

	function aggregation($function) {
		$join = $this->createJoins(implode(",", $this->conditions) . ",$function");
		$column = ($join ? "$this->table." : "") . $this->column;
		$query = "SELECT $function, $column FROM $this->table" . implode($join);
		if ($this->where) {
			$query .= " WHERE " . implode($this->where);
		}
		$query .= " GROUP BY $column";
		$aggregation = &$this->result->aggregation[$query];
		if (!isset($aggregation)) {
			$aggregation = array();
			foreach ($this->query($query, $this->parameters) as $row) {
				$aggregation[$row[$this->column]] = $row;
			}
		}
		if (isset($aggregation[$this->active])) {
			foreach ($aggregation[$this->active] as $return) {
				return $return;
			}
		}
	}

	function count($column = "") {
		$return = parent::count($column);
		return (isset($return) ? $return : 0);
	}

	protected function execute() {
		if (!isset($this->rows)) {
			$referencing = &$this->result->referencing[$this->__toString()];
			if (!isset($referencing)) {
				if (!$this->limit || count($this->result->rows) <= 1 || $this->union) {
					parent::execute();
				} else { //! doesn't work with union
					$result = clone $this;
					$first = true;
					foreach ((array) $this->result->rows as $val) {
						if ($first) {
							$result->where[0] = "$this->column = " . $this->quote($val);
							$first = false;
						} else {
							$clone = clone $this;
							$clone->where[0] = "$this->column = " . $this->quote($val);
							$result->union($clone);
						}
					}
					$result->execute();
					$this->rows = $result->rows;
				}
				$referencing = array();
				foreach ($this->rows as $key => $row) {
					$referencing[$row[$this->column]][$key] = $row;
				}
			}
			$this->data = &$referencing[$this->active];
			if (!isset($this->data)) {
				$this->data = array();
			}
		}
	}

}


<?php
namespace Gt\Database\Query;

use Gt\Database\Connection\Driver;
use Gt\Database\Result\ResultSet;

abstract class Query {
	/** @var string Absolute path to query file on disk */
	protected $filePath;
	protected $connection;

	public function __construct(string $filePath, Driver $driver) {
		if(!is_file($filePath)) {
			throw new QueryNotFoundException($filePath);
		}

		$this->filePath = $filePath;
		$this->connection = $driver->getConnection();
	}

	public function getFilePath():string {
		return $this->filePath;
	}

	abstract public function execute(array $bindings = []):ResultSet;

	/**
	 * $bindings can either be :
	 * 1) An array of individual values for binding to the question mark placeholder,
	 * passed in as variable arguments.
	 * 2) An array containing subarrays containing key-value-pairs for binding to
	 * named placeholders.
	 *
	 * Due to the use of variable arguments on the Database and QueryCollection classes,
	 * key-value-pair bindings may be double or triple nested at this point.
	 */
	protected function flattenBindings(array $bindings):array {
		if(!isset($bindings[0])) {
			return $bindings;
		}

		if(is_object($bindings[0])
		&& method_exists($bindings[0], "toArray")) {
			$bindings = array_map(function($element) {
				if(method_exists($element, "toArray")) {
					return $element->toArray();
				}

				return $element;
			}, $bindings);
		}

		$flatArray = [];
		foreach($bindings as $i => $b) {
			while(isset($b[0])
			&& is_array($b[0])) {
				$merged = [];
				foreach($b as $innerKey => $innerValue) {
					$merged = array_merge(
						$merged,
						$innerValue
					);
				}

				$b = $merged;
			}

			if(!is_array($b)) {
				$b = [$b];
			}

			$flatArray = array_merge($flatArray, $b);
		}

		return $flatArray;
	}
}


<?php
namespace Gt\Database\Query;

use Gt\Database\Connection\Connection;
use Gt\Database\Connection\Driver;
use PDO;
use PDOException;
use PDOStatement;
use DateTime;
use Gt\Database\Result\ResultSet;

class SqlQuery extends Query {
	const SPECIAL_BINDINGS = [
		"limit",
		"offset",
		"groupBy",
		"orderBy",
	];

	public function getSql(array $bindings = []):string {
		$sql = file_get_contents($this->getFilePath());
		$sql = $this->injectSpecialBindings(
			$sql,
			$bindings
		);

		return $sql;
	}

	public function execute(array $bindings = []):ResultSet {
		$bindings = $this->flattenBindings($bindings);

		$pdo = $this->preparePdo();
		$sql = $this->getSql($bindings);
		$statement = $this->prepareStatement($pdo, $sql);
		$preparedBindings = $this->prepareBindings($bindings);
		$preparedBindings = $this->ensureParameterCharacter($preparedBindings);
		$preparedBindings = $this->removeUnusedBindings($preparedBindings, $sql);
		$lastInsertId = null;

		try {
			$statement->execute($preparedBindings);
			$lastInsertId = $pdo->lastInsertId();
		}
		catch(PDOException $exception) {
			throw new PreparedStatementException(null, 0, $exception);
		}

		return new ResultSet($statement, $lastInsertId);
	}

	public function prepareStatement(PDO $pdo, string $sql):PDOStatement {
		try {
			$statement = $pdo->prepare($sql);

			return $statement;
		}
		catch(PDOException $exception) {
			throw new PreparedStatementException(null, 0, $exception);
		}
	}

	/**
	 * Certain words are reserved for use by different SQL engines, such as "limit"
	 * and "offset", and can't be used by the driver as bound parameters. This
	 * function returns the SQL for the query after replacing the bound parameters
	 * manually using string replacement.
	 */
	public function injectSpecialBindings(
		string $sql,
		array $bindings
	):string {
		foreach(self::SPECIAL_BINDINGS as $special) {
			$specialPlaceholder = ":" . $special;

			if(!array_key_exists($special, $bindings)) {
				continue;
			}

			$replacement = $this->escapeSpecialBinding(
				$bindings[$special],
				$special
			);

			$sql = str_replace(
				$specialPlaceholder,
				$replacement,
				$sql
			);
			unset($bindings[$special]);
		}

		foreach($bindings as $key => $value) {
			if(is_array($value)) {
				$inString = "";

				foreach($value as $i => $innerValue) {
					$newKey = $key . "__" . $i;
					$keyParamString = ":$newKey";
					$inString .= "$keyParamString, ";
				}

				$inString = rtrim($inString, " ,");
				$sql = str_replace(
					":$key",
					$inString,
					$sql
				);
			}
		}

		return $sql;
	}

	public function prepareBindings(array $bindings):array {
		foreach($bindings as $key => $value) {
			if(is_bool($value)) {
				$bindings[$key] = (int)$value;
			}
			if($value instanceof DateTime) {
				$bindings[$key] = $value->format("Y-m-d H:i:s");
			}
			if(is_array($value)) {
				foreach($value as $i => $innerValue) {
					$newKey = $key . "__" . $i;
					$bindings[$newKey] = $innerValue;
				}
				unset($bindings[$key]);
			}
		}

		return $bindings;
	}

	public function ensureParameterCharacter(array $bindings):array {
		if($this->bindingsEmptyOrNonAssociative($bindings)) {
			return $bindings;
		}

		foreach($bindings as $key => $value) {
			if(substr($key, 0, 1) !== ":") {
				$bindings[":" . $key] = $value;
				unset($bindings[$key]);
			}
		}

		return $bindings;
	}

	public function removeUnusedBindings(array $bindings, string $sql):array {
		if($this->bindingsEmptyOrNonAssociative($bindings)) {
			return $bindings;
		}

		foreach($bindings as $key => $value) {
			if(!preg_match("/{$key}(\W|\$)/", $sql)) {
				unset($bindings[$key]);
			}
		}

		return $bindings;
	}

	public function bindingsEmptyOrNonAssociative(array $bindings):bool {
		return
			$bindings === []
			|| array_keys($bindings) === range(
				0,
				count($bindings) - 1);
	}

	protected function preparePdo():PDO {
		$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		return $this->connection;
	}

	protected function escapeSpecialBinding(
		string $value,
		string $type
	):string {
		$value = preg_replace(
			"/[^0-9a-z,'\"`\s]/i",
			"",
			$value
		);

// TODO: In v2 we will properly parse the different parts of the special bindings.
// See https://github.com/PhpGt/Database/issues/117
		switch($type) {
		// [GROUP BY {col_name | expr | position}, ... [WITH ROLLUP]]
		case "groupBy":
			break;

		// [ORDER BY {col_name | expr | position}
		case "orderBy":
			break;

		// [LIMIT {[offset,] row_count | row_count OFFSET offset}]
		case "limit":
			break;

		// [LIMIT {[offset,] row_count | row_count OFFSET offset}]
		case "offset":
			break;
		}

		return (string)$value;
	}
}


<?php
namespace Gt\Database\Query;

use DirectoryIterator;
use Gt\Database\Connection\Driver;
use Gt\Database\Database;

class QueryCollectionFactory {
	/** @var Driver */
	protected $driver;
	/** @var string */
	protected $basePath;
	/** @var array */
	protected $queryCollectionCache = [];

	public function __construct(Driver $driver) {
		$this->driver = $driver;
		$this->basePath = $this->driver->getBaseDirectory();
	}

	public function create(string $name):QueryCollection {
		if(!isset($this->queryCollectionCache[$name])) {
			$directoryPath = $this->locateDirectory($name);

			if(is_null($directoryPath)) {
				throw new QueryCollectionNotFoundException($name);
			}

			$this->queryCollectionCache[$name] = new QueryCollection(
				$directoryPath,
				$this->driver
			);
		}

		return $this->queryCollectionCache[$name];

	}

	public function directoryExists(string $name):bool {
		return !is_null($this->locateDirectory($name));
	}

	/**
	 * Case-insensitive attempt to match the provided directory name with a
	 * directory within the basePath.
	 * @param  string $name Name of the QueryCollection
	 * @return string       Absolute path to directory
	 */
	protected function locateDirectory(string $name):?string {
		$parts = [$name];

		foreach(Database::COLLECTION_SEPARATOR_CHARACTERS as $char) {
			if(!strstr($name, $char)) {
				continue;
			}

			$parts = explode($char, $name);
			break;
		}

		return $this->recurseLocateDirectory($parts);
	}

	protected function recurseLocateDirectory(
		array $parts,
		string $basePath = null
	):?string {
		$part = array_shift($parts);
		if(is_null($basePath)) {
			$basePath = $this->basePath;
		}

		foreach(new DirectoryIterator($basePath) as $fileInfo) {
			if($fileInfo->isDot()
			|| !$fileInfo->isDir()) {
				continue;
			}

			$basename = $fileInfo->getBasename();
			if(strtolower($part) === strtolower($basename)) {
				$realPath = $fileInfo->getRealPath();

				if(empty($parts)) {
					return $realPath;
				}

				return $this->recurseLocateDirectory(
					$parts,
					$realPath
				);
			}
		}

		return null;
	}

	protected function getDefaultBasePath():string {
		return getcwd();
	}
}


<?php
/***************************************************************
 *  Copyright notice
 *
 *  (c) 2016 AOE GmbH <dev@aoe.com>
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;

/**
 * @package HappyFeet
 * @subpackage Service_Test
 * @author Kevin Schu <kevin.schu@aoe.com>
 */
class Tx_HappyFeet_Service_Rendering extends Tx_HappyFeet_Service_Abstract
{
    /**
     * @var Tx_HappyFeet_Domain_Repository_FootnoteRepository
     */
    private $footnoteRepository;

    /**
     * @var string
     */
    private $defaultTemplate = 'Markup';

    /**
     * @param array $uids
     * @return string
     */
    public function renderFootnotes(array $uids)
    {
        if (empty($uids)) {
            return '';
        }
        $footnotes = $this->getFootnoteRepository()->getFootnotesByUids($uids);
        if (count($footnotes) < 1) {
            return '';
        }

        $templatePath = $this->getTemplatePath();

        $view = $this->createView($templatePath);
        $view->assign('footnotes', $footnotes);
        return $view->render($templatePath);
    }

    /**
     * Renders the content of a RTE field.
     *
     * @param string $richText
     * @return string
     */
    public function renderRichText($richText)
    {
        if (strlen($richText) < 1) {
            return '';
        }

        $templatePath = $this->getTemplatePathAndFilename('RichText');

        $view = $this->createView($templatePath);
        $view->assign('richText', $richText);
        return $view->render($templatePath);
    }

    /**
     * @return Tx_HappyFeet_Domain_Repository_FootnoteRepository
     */
    protected function getFootnoteRepository()
    {
        if (null === $this->footnoteRepository) {
            $this->footnoteRepository = $this->getObjectManager()->get(
                'Tx_HappyFeet_Domain_Repository_FootnoteRepository'
            );
        }
        return $this->footnoteRepository;
    }

    /**
     * @param string $template
     * @return \TYPO3\CMS\Fluid\View\StandaloneView
     */
    private function createView($template)
    {
        $view = $this->getObjectManager()->get('TYPO3\\CMS\\Fluid\\View\\StandaloneView');
        $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($template));
        return $view;
    }

    /**
     * @param string $template
     * @return string
     */
    private function getTemplatePathAndFilename($template)
    {
        return 'EXT:happy_feet/Resources/Private/Templates/Rendering/' . $template . '.html';
    }

    /**
     * checks if a alternative template is defined via typoscript setup configuration and if it exists as a file
     *
     * @return string
     */
    private function getTemplatePath()
    {
        /**
         * @var $tsfe TypoScriptFrontendController
         */
        $tsfe = $GLOBALS['TSFE'];
        if (isset($tsfe->tmpl->setup['lib.']['plugins.']['tx_happyfeet.']['view.']['template'])) {
            $templateFile = GeneralUtility::getFileAbsFileName(
                $tsfe->tmpl->setup['lib.']['plugins.']['tx_happyfeet.']['view.']['template']
            );
            if (is_file($templateFile)) {
                return $tsfe->tmpl->setup['lib.']['plugins.']['tx_happyfeet.']['view.']['template'];
            }
        }

        return $this->getTemplatePathAndFilename($this->defaultTemplate);
    }

    /**
     * @param Tx_HappyFeet_Domain_Repository_FootnoteRepository $footnoteRepository
     */
    public function setFootnoteRepository(Tx_HappyFeet_Domain_Repository_FootnoteRepository $footnoteRepository)
    {
        $this->footnoteRepository = $footnoteRepository;
    }
}


<?php

/***************************************************************
 *  Copyright notice
 *
 *  (c) 2014 AOE GmbH <dev@aoe.com>
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

/**
 * Repository for Tx_HappyFeet_Domain_Model_Footnote objects.
 *
 * @package HappyFeet
 * @subpackage Domain_Repository
 * @author Timo Fuchs <timo.fuchs@aoe.com>
 */
class Tx_HappyFeet_Domain_Repository_FootnoteRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
    /**
     * @var array
     */
    public static $uids;

    /**
     * @return void
     */
    public function initializeObject()
    {
        /** @var $defaultQuerySettings \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings */
        $defaultQuerySettings = $this->objectManager->get('TYPO3\\CMS\\Extbase\\Persistence\\Generic\\Typo3QuerySettings');
        $defaultQuerySettings->setRespectStoragePage(false);
        $defaultQuerySettings->setRespectSysLanguage(false);
        $defaultQuerySettings->setIgnoreEnableFields(false)->setIncludeDeleted(false);
        $this->setDefaultQuerySettings($defaultQuerySettings);
    }

    /**
     * Returns the smallest index which is not used.
     *
     * @return integer
     */
    public function getLowestFreeIndexNumber()
    {
        $query = $this->createQuery();
        $query->statement('SELECT index_number from ' . strtolower($this->objectType) . ' WHERE deleted=0');
        $index = 1;
        $results = $query->execute(true);
        if (false === is_array($results) || sizeof($results) < 1) {
            return $index;
        }
        $indexes = array();
        foreach ($results as $result) {
            $indexes[] = (integer)$result['index_number'];
        }
        for ($index = 1; $index <= sizeof($indexes) + 1; $index++) {
            if (false === in_array($index, $indexes)) {
                break;
            }
        }
        return $index;
    }

    /**
     * @param Tx_HappyFeet_Domain_Model_Footnote $object
     * @throws \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException
     * @return void
     */
    public function add($object)
    {
        /** @var Tx_HappyFeet_Domain_Model_Footnote $object */
        if (false === ($object instanceof Tx_HappyFeet_Domain_Model_Footnote)) {
            throw new \TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException(
                'The object given to add() was not of the type (' . $this->objectType . ') this repository manages.',
                1392911702
            );
        }
        $object->setIndexNumber($this->getLowestFreeIndexNumber());
        parent::add($object);
    }

    /**
     * @param array $uids
     * @return array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface
     */
    public function getFootnotesByUids(array $uids)
    {
        self::$uids = $uids;
        $query = $this->createQuery();
        $query->setQuerySettings($this->defaultQuerySettings);
        $query->matching($query->in('uid', $uids));
        return $this->sortFootnotesByUids($query->execute(), $uids);
    }

    /**
     * @param array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface $queryResult
     * @param $uids
     * @return mixed
     */
    public function sortFootnotesByUids($queryResult, $uids)
    {
        if ($queryResult instanceof \TYPO3\CMS\Extbase\Persistence\QueryResultInterface) {
            $queryResult = $queryResult->toArray();
        }
        usort($queryResult, 'Tx_HappyFeet_Domain_Repository_FootnoteRepository::usortFootnotesByUids');
        return $queryResult;
    }

    /**
     * @param Tx_HappyFeet_Domain_Model_Footnote $a
     * @param Tx_HappyFeet_Domain_Model_Footnote $b
     * @return integer
     */
    public static function usortFootnotesByUids(
        Tx_HappyFeet_Domain_Model_Footnote $a,
        Tx_HappyFeet_Domain_Model_Footnote $b
    ) {
        $map = array_flip(self::$uids);
        if ($map[$a->getUid()] >= $map[$b->getUid()]) {
            return 1;
        }
        return -1;
    }
}


<?php
namespace Aoe\HappyFeet\Typo3\Hook;

/*
 * Copyright notice
 *
 * (c) 2014 AOE GmbH <dev@aoe.com>
 *
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

use TYPO3\CMS\Core\DataHandling\DataHandler;

/**
 * Class which provides TCE main hooks.
 *
 * @author Timo Fuchs <timo.fuchs@aoe.com>
 */
class Tcemain extends \Tx_HappyFeet_Service_Abstract
{
    /**
     * @param string $status Operation type e.g new, update, delete.
     * @param string $table Database table on which the operation is performed.
     * @param integer $id Table record ID.
     * @param array $fieldArray Key/value pair of record fields.
     * @param DataHandler $tcemain Parent class instance from which the hook method is called.
     * @return void
     *
     * @codingStandardsIgnore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
     */
    public function processDatamap_postProcessFieldArray($status, $table, $id, array &$fieldArray, DataHandler $tcemain)
    {
        if ($table !== 'tx_happyfeet_domain_model_footnote') {
            return;
        }
        if ($status === 'new') {
            $fieldArray['index_number'] = $this->getFootnoteRepository()->getLowestFreeIndexNumber();
        }
        if ($status === 'delete') {
            $fieldArray['index_number'] = 0;
        }
    }

    /**
     * @return \Tx_HappyFeet_Domain_Repository_FootnoteRepository
     */
    protected function getFootnoteRepository()
    {
        /** @var \Tx_HappyFeet_Domain_Repository_FootnoteRepository $footnoteRepository */
        $footnoteRepository = $this->getObjectManager()->get('Tx_HappyFeet_Domain_Repository_FootnoteRepository');
        return $footnoteRepository;
    }
}


<?php

namespace Graze\Sniffs\Commenting;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;

class MissingFunctionCommentSniff implements Sniff
{
    const ERROR_CODE_RETURN = 'graze.commenting.missingFunctionComment.returnNoDoc';
    const ERROR_CODE_PARAMETER = 'graze.commenting.missingFunctionComment.parameterNoDoc';

    /**
     * Registers the tokens that this sniff wants to listen for.
     * An example return value for a sniff that wants to listen for whitespace
     * and any comments would be:
     * <code>
     *    return array(
     *            T_WHITESPACE,
     *            T_DOC_COMMENT,
     *            T_COMMENT,
     *           );
     * </code>
     *
     * @return int[]
     * @see    Tokens.php
     */
    public function register()
    {
        return [
            T_FUNCTION,
        ];
    }

    /**
     * @param File $phpcsFile
     * @param int  $stackPtr
     *
     * @return int|void
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        $find = Tokens::$methodPrefixes;
        $find[] = T_WHITESPACE;

        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
            // Inline comments might just be closing comments for
            // control structures or functions instead of function comments
            // using the wrong comment type. If there is other code on the line,
            // assume they relate to that code.
            $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true);
            if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) {
                $commentEnd = $prev;
            }
        }

        if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
            && $tokens[$commentEnd]['code'] !== T_COMMENT
        ) {
            if (array_key_exists('scope_opener', $tokens[$stackPtr])
                && $this->functionHasReturn($phpcsFile, $stackPtr)) {
                $phpcsFile->addError('Function has return keyword but no doc block', $stackPtr, static::ERROR_CODE_RETURN);
                return;
            }

            if ($this->functionHasParams($phpcsFile, $stackPtr)) {
                $phpcsFile->addError('Function has parameters but no doc block', $stackPtr, static::ERROR_CODE_PARAMETER);
            }
        }
    }

    /**
     * @param File $phpcsFile
     * @param int  $stackPtr
     *
     * @return bool
     */
    private function functionHasReturn(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();
        $start = $tokens[$stackPtr]['scope_opener'];
        $end = $tokens[$stackPtr]['scope_closer'];

        for ($i = $start; $i <= $end; $i++) {
            if ($tokens[$i]['code'] === T_CLOSURE || $tokens[$i]['code'] === T_FUNCTION) {
                // skip over closures within our function
                $i = $tokens[$i]['scope_closer'];
            }

            // if we hit a return keyword that isn't immediately followed by a semicolon, we need a doc block
            if ($tokens[$i]['code'] === T_RETURN && $tokens[$i + 1]['code'] !== T_SEMICOLON) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param File $phpcsFile
     * @param int  $stackPtr
     *
     * @return bool
     * @throws \PHP_CodeSniffer\Exceptions\TokenizerException
     */
    private function functionHasParams(File $phpcsFile, $stackPtr)
    {
        return (bool)$phpcsFile->getMethodParameters($stackPtr);
    }
}


<?php

namespace Graze\Sniffs\Commenting;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\AbstractVariableSniff;

class InlineVariableCommentSniff extends AbstractVariableSniff
{
    const ERROR_CODE = 'graze.commenting.inlineVariableComment';
    /**
     * Called to process class member vars.
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where this
     *                                        token was found.
     * @param int $stackPtr The position where the token was found.
     *
     * @return void
     */
    protected function processMemberVar(
        File $phpcsFile,
        $stackPtr
    ) {
        $tokens       = $phpcsFile->getTokens();
        $commentToken = [
            T_COMMENT,
            T_DOC_COMMENT_CLOSE_TAG,
        ];
        $commentEnd = $phpcsFile->findPrevious($commentToken, $stackPtr);
        $commentStart = $tokens[$commentEnd]['comment_opener'];

        if ($tokens[$commentEnd]['line'] === $tokens[$commentStart]['line']) {
            $phpcsFile->addError('Member variable comment should not be inline', $stackPtr, static::ERROR_CODE);
        }
    }

    /**
     * Called to process normal member vars.
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where this
     *                                        token was found.
     * @param int $stackPtr The position where the token was found.
     *
     * @return void
     */
    protected function processVariable(
        File $phpcsFile,
        $stackPtr
    ) {
    }

    /**
     * Called to process variables found in double quoted strings or heredocs.
     * Note that there may be more than one variable in the string, which will
     * result only in one call for the string or one call per line for heredocs.
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where this
     *                                        token was found.
     * @param int $stackPtr The position where the double quoted
     *                                        string was found.
     *
     * @return void
     */
    protected function processVariableInString(
        File $phpcsFile,
        $stackPtr
    ) {
    }
}


<?php

namespace Graze\Sniffs\Files;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class DoubleBlankLineSniff implements Sniff
{
    const ERROR_CODE = 'graze.files.doubleBlankLine';

    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * An example return value for a sniff that wants to listen for whitespace
     * and any comments would be:
     *
     *
     *    return array(
     *            T_WHITESPACE,
     *            T_DOC_COMMENT,
     *            T_COMMENT,
     *           );
     *
     *
     * @return int[]
     * @see    Tokens.php
     */
    public function register()
    {
        return [
            T_WHITESPACE
        ];
    }

    /**
     * Called when one of the token types that this sniff is listening for
     * is found.
     *
     * The stackPtr variable indicates where in the stack the token was found.
     * A sniff can acquire information this token, along with all the other
     * tokens within the stack by first acquiring the token stack:
     *
     *
     *    $tokens = $phpcsFile->getTokens();
     *    echo 'Encountered a '.$tokens[$stackPtr]['type'].' token';
     *    echo 'token information: ';
     *    print_r($tokens[$stackPtr]);
     *
     *
     * If the sniff discovers an anomaly in the code, they can raise an error
     * by calling addError() on the PHP_CodeSniffer_File object, specifying an error
     * message and the position of the offending token:
     *
     *
     *    $phpcsFile->addError('Encountered an error', $stackPtr);
     *
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where the
     *                                        token was found.
     * @param int $stackPtr The position in the PHP_CodeSniffer
     *                                        file's token stack where the token
     *                                        was found.
     *
     * @return void|int Optionally returns a stack pointer. The sniff will not be
     *                  called again on the current file until the returned stack
     *                  pointer is reached. Return (count($tokens) + 1) to skip
     *                  the rest of the file.
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        for ($i = 1; $i < $phpcsFile->numTokens; $i++) {
            if ($tokens[$i]['column'] === 1 && $tokens[$i]['length'] === 0) {
                $currentLineNumber = $tokens[$i]['line'];
                $nextWhitespace = $phpcsFile->findNext(T_WHITESPACE, $stackPtr);
                $nextWhitespace = $tokens[$nextWhitespace];
                if ($nextWhitespace['column'] === 1 && $nextWhitespace['length'] === 0 && $nextWhitespace['line'] - $currentLineNumber === 1) {
                    // double blank line
                    $phpcsFile->addError('Multiple blank lines detected', $stackPtr, static::ERROR_CODE);
                }
            }
        }
    }
}


<?php

namespace Graze\Sniffs\Naming;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class AbstractClassNamingSniff implements Sniff
{
    const ERROR_CODE = 'graze.naming.abstractClass';

    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * An example return value for a sniff that wants to listen for whitespace
     * and any comments would be:
     *
     *
     *    return array(
     *            T_WHITESPACE,
     *            T_DOC_COMMENT,
     *            T_COMMENT,
     *           );
     *
     *
     * @return int[]
     * @see    Tokens.php
     */
    public function register()
    {
        return [
            T_CLASS
        ];
    }

    /**
     * Called when one of the token types that this sniff is listening for
     * is found.
     *
     * The stackPtr variable indicates where in the stack the token was found.
     * A sniff can acquire information this token, along with all the other
     * tokens within the stack by first acquiring the token stack:
     *
     *
     *    $tokens = $phpcsFile->getTokens();
     *    echo 'Encountered a '.$tokens[$stackPtr]['type'].' token';
     *    echo 'token information: ';
     *    print_r($tokens[$stackPtr]);
     *
     *
     * If the sniff discovers an anomaly in the code, they can raise an error
     * by calling addError() on the PHP_CodeSniffer_File object, specifying an error
     * message and the position of the offending token:
     *
     *
     *    $phpcsFile->addError('Encountered an error', $stackPtr);
     *
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where the
     *                                        token was found.
     * @param int $stackPtr The position in the PHP_CodeSniffer
     *                                        file's token stack where the token
     *                                        was found.
     *
     * @return void|int Optionally returns a stack pointer. The sniff will not be
     *                  called again on the current file until the returned stack
     *                  pointer is reached. Return (count($tokens) + 1) to skip
     *                  the rest of the file.
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        if ($tokens[($stackPtr - 2)]['code'] === T_ABSTRACT) {
            $className = $phpcsFile->getDeclarationName($stackPtr);

            if (substr($className, 0, 8) !== 'Abstract') {
                $phpcsFile->addError('Abstract class name "%s" must be prefixed with "Abstract"', $stackPtr, static::ERROR_CODE, '', [$className]);
            }
        }
    }
}


<?php

namespace Graze\Sniffs\ControlStructures;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class IfVariableAssignmentSniff implements Sniff
{
    const ERROR_CODE = 'graze.controlStructures.ifVariableAssignment';
    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * An example return value for a sniff that wants to listen for whitespace
     * and any comments would be:
     *
     *
     *    return array(
     *            T_WHITESPACE,
     *            T_DOC_COMMENT,
     *            T_COMMENT,
     *           );
     *
     *
     * @return int[]
     * @see    Tokens.php
     */
    public function register()
    {
        return [
            T_IF
        ];
    }

    /**
     * Called when one of the token types that this sniff is listening for
     * is found.
     *
     * The stackPtr variable indicates where in the stack the token was found.
     * A sniff can acquire information this token, along with all the other
     * tokens within the stack by first acquiring the token stack:
     *
     *
     *    $tokens = $phpcsFile->getTokens();
     *    echo 'Encountered a '.$tokens[$stackPtr]['type'].' token';
     *    echo 'token information: ';
     *    print_r($tokens[$stackPtr]);
     *
     *
     * If the sniff discovers an anomaly in the code, they can raise an error
     * by calling addError() on the PHP_CodeSniffer_File object, specifying an error
     * message and the position of the offending token:
     *
     *
     *    $phpcsFile->addError('Encountered an error', $stackPtr);
     *
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where the
     *                                        token was found.
     * @param int $stackPtr The position in the PHP_CodeSniffer
     *                                        file's token stack where the token
     *                                        was found.
     *
     * @return void|int Optionally returns a stack pointer. The sniff will not be
     *                  called again on the current file until the returned stack
     *                  pointer is reached. Return (count($tokens) + 1) to skip
     *                  the rest of the file.
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        $start = $tokens[$stackPtr]['parenthesis_opener'];
        $end = $tokens[$stackPtr]['parenthesis_closer'];

        for ($i = $start; $i <= $end; $i++) {
            if ($tokens[$i]['code'] === T_EQUAL) {
                $phpcsFile->addError('Variable assigment in if statement', $stackPtr, static::ERROR_CODE);
            }
        }
    }
}


<?php

namespace Graze\Sniffs\ControlStructures;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class NestedTernarySniff implements Sniff
{
    const ERROR_CODE = 'graze.controlStructures.nestedTernary';

    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * An example return value for a sniff that wants to listen for whitespace
     * and any comments would be:
     *
     *
     *    return array(
     *            T_WHITESPACE,
     *            T_DOC_COMMENT,
     *            T_COMMENT,
     *           );
     *
     *
     * @return int[]
     * @see    Tokens.php
     */
    public function register()
    {
        return [
            T_INLINE_THEN
        ];
    }

    /**
     * Called when one of the token types that this sniff is listening for
     * is found.
     *
     * The stackPtr variable indicates where in the stack the token was found.
     * A sniff can acquire information this token, along with all the other
     * tokens within the stack by first acquiring the token stack:
     *
     *
     *    $tokens = $phpcsFile->getTokens();
     *    echo 'Encountered a '.$tokens[$stackPtr]['type'].' token';
     *    echo 'token information: ';
     *    print_r($tokens[$stackPtr]);
     *
     *
     * If the sniff discovers an anomaly in the code, they can raise an error
     * by calling addError() on the PHP_CodeSniffer_File object, specifying an error
     * message and the position of the offending token:
     *
     *
     *    $phpcsFile->addError('Encountered an error', $stackPtr);
     *
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where the
     *                                        token was found.
     * @param int $stackPtr The position in the PHP_CodeSniffer
     *                                        file's token stack where the token
     *                                        was found.
     *
     * @return void|int Optionally returns a stack pointer. The sniff will not be
     *                  called again on the current file until the returned stack
     *                  pointer is reached. Return (count($tokens) + 1) to skip
     *                  the rest of the file.
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();

        $start = $stackPtr + 1;
        $end = $phpcsFile->findNext(T_INLINE_ELSE, $stackPtr);

        for ($i = $start; $i <= $end; $i++) {
            if ($tokens[$i]['code'] === T_INLINE_THEN) {
                $phpcsFile->addError('Nested ternary operator is not allowed', $stackPtr, static::ERROR_CODE);
            }
        }
    }
}


<?php

namespace Graze\Sniffs\Naming;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

class InterfaceNamingSniff implements Sniff
{
    const ERROR_CODE = 'graze.naming.interface';

    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * An example return value for a sniff that wants to listen for whitespace
     * and any comments would be:
     *
     *
     *    return array(
     *            T_WHITESPACE,
     *            T_DOC_COMMENT,
     *            T_COMMENT,
     *           );
     *
     *
     * @return int[]
     * @see    Tokens.php
     */
    public function register()
    {
        return [
            T_INTERFACE
        ];
    }

    /**
     * Called when one of the token types that this sniff is listening for
     * is found.
     *
     * The stackPtr variable indicates where in the stack the token was found.
     * A sniff can acquire information this token, along with all the other
     * tokens within the stack by first acquiring the token stack:
     *
     *
     *    $tokens = $phpcsFile->getTokens();
     *    echo 'Encountered a '.$tokens[$stackPtr]['type'].' token';
     *    echo 'token information: ';
     *    print_r($tokens[$stackPtr]);
     *
     *
     * If the sniff discovers an anomaly in the code, they can raise an error
     * by calling addError() on the PHP_CodeSniffer_File object, specifying an error
     * message and the position of the offending token:
     *
     *
     *    $phpcsFile->addError('Encountered an error', $stackPtr);
     *
     *
     * @param File $phpcsFile The PHP_CodeSniffer file where the
     *                                        token was found.
     * @param int $stackPtr The position in the PHP_CodeSniffer
     *                                        file's token stack where the token
     *                                        was found.
     *
     * @return void|int Optionally returns a stack pointer. The sniff will not be
     *                  called again on the current file until the returned stack
     *                  pointer is reached. Return (count($tokens) + 1) to skip
     *                  the rest of the file.
     */
    public function process(File $phpcsFile, $stackPtr)
    {
        $className = $phpcsFile->getDeclarationName($stackPtr);

        if (substr($className, -9) !== 'Interface') {
            $phpcsFile->addError('Interface name "%s" must be suffixed with "Interface"', $stackPtr, static::ERROR_CODE, '', [$className]);
        }
    }
}


<?php

/**
 * @package   CronPlus
 * @author    Mte90
 * @license   GPL-3.0+
 * @link      http://mte90.net
 * @copyright 2015-2018 GPL
 */

/**
 *  * Add and remove Cron job in WordPress easily!
 */
class CronPlus {

	/**
	 * Args of the class
	 *
	 * @var     array
	 * @since   1.0.0
	 */
	private $args;

	/**
	 * Construct the class parameter
	 *
	 * @param array $args Parameters of class.
	 * @return void
	 */
	function __construct( $args ) {
		$defaults = array(
			'time' => current_time( 'timestamp' ), // If you want a specific time (unix timestamp required)
			'recurrence' => 'hourly', // Hourly,daily,twicedaily,weekly,monthly
			'name' => 'cronplus',
			'schedule' => 'schedule', // Schedule or single,
			'cb' => '',
			'multisite' => false,
			'plugin_root_file' => '',
			'run_on_creation' => false,
			'args' => array( '' ) // Args passed to the hook
		);

		$this->args = wp_parse_args( $args, $defaults );
		if ( isset( $this->args[ 'cb' ] ) && isset( $this->args[ 'name' ] ) ) {
			add_action( $this->args[ 'name' ], $this->args[ 'cb' ] );
		}
		if ( !empty( $this->args[ 'plugin_root_file' ] ) ) {
			register_deactivation_hook( $this->args[ 'plugin_root_file' ], array( $this, 'deactivate' ) );
		}
	}

	/**
	 * Schedule the event
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function schedule_event() {
		if ( $this->is_scheduled( $this->args[ 'name' ] ) ) {
			return;
		}
		if ( $this->args[ 'run_on_creation' ] ) {
			call_user_func( $this->args[ 'cb' ], $this->args[ 'args' ] );
		}
		if ( $this->args[ 'schedule' ] === 'schedule' ) {
			wp_schedule_event( $this->args[ 'time' ], $this->args[ 'recurrence' ], $this->args[ 'name' ], $this->args[ 'args' ] );
		} elseif ( $this->args[ 'schedule' ] === 'single' ) {
			wp_schedule_single_event( $this->args[ 'recurrence' ], $this->args[ 'name' ], $this->args[ 'args' ] );
		}

		// Save all the site ids where is the corn for the deactivation
		if ( is_multisite() && !wp_is_large_network() ) {
			$sites = ( array ) get_site_option( $this->args[ 'name' ] . '_sites', array() );
			$sites[] = get_current_blog_id();
			update_site_option( $this->args[ 'name' ] . '_sites', $sites );
		}

		return true;
	}

	/**
	 * Clear the schedule
	 *
	 * @since    1.0.0
	 * @return void
	 */
	public function clear_schedule_by_hook() {
		wp_clear_scheduled_hook( $this->args[ 'name' ], $this->args[ 'args' ] );
	}

	/**
	 * UnSchedule the event
	 *
	 * @since    1.0.0
	 *
	 * @param number $timestamp The timestamp to remove
	 *
	 * @return void
	 */
	public function unschedule_specific_event( $timestamp = '' ) {
		if ( empty( $timestamp ) ) {
			$timestamp = wp_next_scheduled( $this->args[ 'name' ], $this->args[ 'args' ] );
		}
		wp_unschedule_event( $timestamp, $this->args[ 'name' ], $this->args[ 'args' ] );
	}

	/**
	 * Remove all the cron on deactivation
	 *
	 * @return void
	 */
	public function deactivate() {
		$this->clear_schedule();
		if ( !is_multisite() || wp_is_large_network() ) {
			return;
		}

		$sites = ( array ) get_site_option( $this->args[ 'name' ] . '_sites', array() );

		$sites and $sites = array_diff( $sites, [ get_current_blog_id() ] );

		foreach ( $sites as $site ) {
			switch_to_blog( $site );
			$this->clear_schedule();
		}
		restore_current_blog();

		delete_site_option( $this->args[ 'name' ] . '_sites' );
	}

	/**
	 * Check if the event is scheduled
	 *
	 * @param string $name
	 * @param array $crons
	 * @return bool
	 */
	private function is_scheduled( $name ) {
		$crons = _get_cron_array();

		if ( empty( $crons ) ) {
			return false;
		}

		foreach ( $crons as $cron ) {
			if ( isset( $cron[ $name ] ) ) {
				return true;
			}
		}

		return false;
	}

}


<?php

namespace Flagception\Factory;

use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
 * Class ExpressionLanguageFactory
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Factory
 */
class ExpressionLanguageFactory
{
    /**
     * The expression language providers
     *
     * @var ExpressionFunctionProviderInterface[]
     */
    private $providers;

    /**
     * Add provider
     *
     * @param ExpressionFunctionProviderInterface $provider
     *
     * @return void
     */
    public function addProvider(ExpressionFunctionProviderInterface $provider)
    {
        $this->providers[] = $provider;
    }

    /**
     * Create expression language
     *
     * @return ExpressionLanguage
     */
    public function create()
    {
        $language = new ExpressionLanguage();

        foreach ($this->providers as $provider) {
            $language->registerProvider($provider);
        }

        return $language;
    }
}


<?php

namespace Flagception\Constraint\Provider;

use Symfony\Component\ExpressionLanguage\ExpressionFunction;
use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface;

/**
 * Class MatchProvider
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Constraint\Provider
 */
class MatchProvider implements ExpressionFunctionProviderInterface
{
    /**
     * {@inheritdoc}
     */
    public function getFunctions()
    {
        return array(
            new ExpressionFunction(
                'match',
                function ($pattern, $value) {
                    return sprintf('preg_match(%1$s, %2$s) === 1', $pattern, $value);
                },
                function ($arguments, $pattern, $value) {
                    return preg_match($pattern, $value) === 1;
                }
            ),
        );
    }
}


<?php

namespace Flagception\Activator;

use Flagception\Model\Context;

/**
 * Class EnvironmentActivator
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Activator
 */
class EnvironmentActivator implements FeatureActivatorInterface
{
    /**
     * Variable array mapping
     *
     * @var array
     */
    private $variables;

    /**
     * Force environment request
     *
     * @var boolean
     */
    private $forceRequest;

    /**
     * EnvironmentActivator constructor.
     *
     * @param array $variables
     * @param bool $forceRequest
     */
    public function __construct(array $variables = [], $forceRequest = false)
    {
        $this->variables = $variables;
        $this->forceRequest = $forceRequest;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'environment';
    }

    /**
     * {@inheritdoc}
     */
    public function isActive($name, Context $context)
    {
        if ($this->forceRequest === false && !array_key_exists($name, $this->variables)) {
            return false;
        }

        if ($this->forceRequest === true && !array_key_exists($name, $this->variables)) {
            return filter_var(getenv($name), FILTER_VALIDATE_BOOLEAN);
        }

        return filter_var(getenv($this->variables[$name]), FILTER_VALIDATE_BOOLEAN);
    }
}


<?php

namespace Flagception\Decorator;

use Flagception\Model\Context;

/**
 * Class ChainDecorator
 *
 * @author Michel Chowanski <chowanski@bestit-online.de>
 * @package Flagception\Decorator
 */
class ChainDecorator implements ContextDecoratorInterface
{
    /**
     * Ordered array of decorators
     *
     * @var ContextDecoratorInterface[]
     */
    private $bag = [];

    /**
     * Add context decorator
     *
     * @param ContextDecoratorInterface $decorator
     *
     * @return void
     */
    public function add(ContextDecoratorInterface $decorator)
    {
        $this->bag[] = $decorator;
    }

    /**
     * Get decorators bag
     *
     * @return ContextDecoratorInterface[]
     */
    public function getDecorators()
    {
        return $this->bag;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'chain';
    }

    /**
     * {@inheritdoc}
     */
    public function decorate(Context $context)
    {
        foreach ($this->bag as $decorator) {
            $context = $decorator->decorate($context);
        }

        return $context;
    }
}


<?php

namespace Flagception\Activator;

use Flagception\Constraint\ConstraintResolverInterface;
use Flagception\Model\Context;

/**
 * Class ConstraintActivator
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Activator
 */
class ConstraintActivator implements FeatureActivatorInterface
{
    /**
     * The constraint resolver
     *
     * @var ConstraintResolverInterface
     */
    private $resolver;

    /**
     * Features names and constraints
     *
     * @var array
     */
    private $features;

    /**
     * ConstraintActivator constructor.
     *
     * @param ConstraintResolverInterface $resolver
     * @param array $features
     */
    public function __construct(ConstraintResolverInterface $resolver, array $features = [])
    {
        $this->resolver = $resolver;
        $this->features = $features;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'constraint';
    }

    /**
     * {@inheritdoc}
     */
    public function isActive($name, Context $context)
    {
        if (!array_key_exists($name, $this->features)) {
            return false;
        }

        return $this->resolver->resolve($this->features[$name], $context);
    }
}


<?php

namespace Flagception\Decorator;

use Flagception\Model\Context;

/**
 * Class ArrayDecorator
 *
 * @author Michel Chowanski <chowanski@bestit-online.de>
 * @package Flagception\Decorator
 */
class ArrayDecorator implements ContextDecoratorInterface
{
    /**
     * Active features
     *
     * @var array
     */
    private $defaultValues;

    /**
     * ArrayDecorator constructor.
     *
     * @param array $defaultValues
     */
    public function __construct(array $defaultValues = [])
    {
        $this->defaultValues = $defaultValues;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'array';
    }

    /**
     * {@inheritdoc}
     */
    public function decorate(Context $context)
    {
        foreach ($this->defaultValues as $key => $value) {
            $context->add($key, $value);
        }

        return $context;
    }
}


<?php

namespace Flagception\Activator;

use DateInterval;
use Flagception\Model\Context;
use Psr\Cache\CacheItemPoolInterface as CachePool;

/**
 * Generic cache for activator
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Activator
 */
class CacheActivator implements FeatureActivatorInterface
{
    /**
     * Cache key
     */
    const CACHE_KEY = 'flagception';

    /**
     * The origin activator
     *
     * @var FeatureActivatorInterface
     */
    private $activator;

    /**
     * The cache pool
     *
     * @var CachePool
     */
    private $cachePool;

    /**
     * Time to live for cache items
     * Valid values are identical to \Psr\Cache\CacheItemInterface
     *
     * @var int|DateInterval|null
     */
    private $cacheTtl;

    /**
     * Short memory request cache
     *
     * @var bool[]
     */
    private $memory = [];

    /**
     * CacheActivator constructor.
     *
     * @param FeatureActivatorInterface $activator
     * @param CachePool $cachePool
     * @param int|DateInterval|null $cacheTtl
     */
    public function __construct(FeatureActivatorInterface $activator, CachePool $cachePool, $cacheTtl = 3600)
    {
        $this->activator = $activator;
        $this->cachePool = $cachePool;
        $this->cacheTtl = $cacheTtl;
    }

    /**
     * Get activator
     *
     * @return FeatureActivatorInterface
     */
    public function getActivator()
    {
        return $this->activator;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return $this->activator->getName();
    }

    /**
     * {@inheritdoc}
     */
    public function isActive($name, Context $context)
    {
        $hash = static::CACHE_KEY . '#' . $this->getName() . '#' . md5($name . '-' . $context->serialize());

        // Step 1: Try get from memory cache
        if (array_key_exists($hash, $this->memory)) {
            return $this->memory[$hash];
        }

        // Step 2: Try get from (optional) cache
        $cacheItem = null;
        if ($this->cachePool !== null) {
            $cacheItem = $this->cachePool->getItem($hash);

            if ($cacheItem->isHit()) {
                $this->memory[$hash] = $cacheItem->get();
                return $this->memory[$hash];
            }
        }

        // Step 3: Get from activators and save to cache
        $this->memory[$hash] = $this->activator->isActive($name, $context);

        // Write result to cache
        if ($this->cachePool !== null && $cacheItem !== null) {
            $cacheItem->set($this->memory[$hash]);
            $cacheItem->expiresAfter($this->cacheTtl);
            $this->cachePool->save($cacheItem);
        }

        return $this->memory[$hash];
    }
}


<?php

namespace Flagception\Constraint;

use Flagception\Exception\ConstraintSyntaxException;
use Flagception\Model\Context;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;

/**
 * Class ConstraintResolver
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Constraint
 */
class ConstraintResolver implements ConstraintResolverInterface
{
    /**
     * The expression language parser
     *
     * @var ExpressionLanguage
     */
    private $expressionLanguage;

    /**
     * ConstraintResolver constructor.
     *
     * @param ExpressionLanguage $expressionLanguage
     */
    public function __construct(ExpressionLanguage $expressionLanguage)
    {
        $this->expressionLanguage = $expressionLanguage;
    }

    /**
     * {@inheritdoc}
     */
    public function resolve($constraint, Context $context)
    {
        try {
            return $this->expressionLanguage->evaluate($constraint, array_merge(
                $context->all(),
                ['context' => $context]
            ));
        } catch (SyntaxError $exception) {
            throw new ConstraintSyntaxException('Feature constraint expression is invalid', 0, $exception);
        }
    }
}


<?php

namespace Flagception\Activator;

use Flagception\Model\Context;

/**
 * Class ChainActivator
 *
 * @author Michel Chowanski <michel.chowanski@bestit-online.de>
 * @package Flagception\Activator
 */
class ChainActivator implements FeatureActivatorInterface
{
    /**
     * Ordered array of feature activators
     *
     * @var FeatureActivatorInterface[]
     */
    private $bag = [];

    /**
     * Add activator
     *
     * @param FeatureActivatorInterface $activator
     *
     * @return void
     */
    public function add(FeatureActivatorInterface $activator)
    {
        $this->bag[] = $activator;
    }

    /**
     * Get activators bag
     *
     * @return FeatureActivatorInterface[]
     */
    public function getActivators()
    {
        return $this->bag;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'chain';
    }

    /**
     * {@inheritdoc}
     */
    public function isActive($name, Context $context)
    {
        foreach ($this->bag as $activator) {
            if ($activator->isActive($name, $context) === true) {
                return true;
            }
        }

        return false;
    }
}