xref: /plugin/struct/meta/QueryBuilder.php (revision 4566427492fa9f2a317c68458001b38a472b1b68)
18c551fd7SAndreas Gohr<?php
28c551fd7SAndreas Gohr
38c551fd7SAndreas Gohrnamespace dokuwiki\plugin\struct\meta;
48c551fd7SAndreas Gohr
58c551fd7SAndreas Gohr/**
68c551fd7SAndreas Gohr * Class QueryBuilder
78c551fd7SAndreas Gohr * @package dokuwiki\plugin\struct\meta
88c551fd7SAndreas Gohr */
98c551fd7SAndreas Gohrclass QueryBuilder {
108c551fd7SAndreas Gohr
118c551fd7SAndreas Gohr    /** @var array placeholder -> values */
128c551fd7SAndreas Gohr    protected $values = array();
138c551fd7SAndreas Gohr    /** @var array (alias -> statement */
148c551fd7SAndreas Gohr    protected $select = array();
158c551fd7SAndreas Gohr    /** @var array (alias -> statement) */
168c551fd7SAndreas Gohr    protected $from = array();
172f68434dSAndreas Gohr    /** @var array (alias -> "table"|"join") keeps how tables were added, as table or join */
182f68434dSAndreas Gohr    protected $type = array();
198c551fd7SAndreas Gohr    /** @var QueryBuilderWhere */
208c551fd7SAndreas Gohr    protected $where;
2183cb959bSAndreas Gohr    /** @var  string[] */
222f68434dSAndreas Gohr    protected $orderby = array();
2383cb959bSAndreas Gohr    /** @var  string[] */
242f68434dSAndreas Gohr    protected $groupby = array();
258c551fd7SAndreas Gohr
268c551fd7SAndreas Gohr    /**
278c551fd7SAndreas Gohr     * QueryBuilder constructor.
288c551fd7SAndreas Gohr     */
298c551fd7SAndreas Gohr    public function __construct() {
308c551fd7SAndreas Gohr        $this->where = new QueryBuilderWhere();
318c551fd7SAndreas Gohr    }
328c551fd7SAndreas Gohr
338c551fd7SAndreas Gohr    /**
348c551fd7SAndreas Gohr     * Adds a column to select
358c551fd7SAndreas Gohr     *
368c551fd7SAndreas Gohr     * If the alias already exists, the current statement for that alias will be overwritten.
378c551fd7SAndreas Gohr     *
388c551fd7SAndreas Gohr     * @param string $tablealias The table to select from
398c551fd7SAndreas Gohr     * @param string $column The column to select
408c551fd7SAndreas Gohr     * @param string $alias Under whichname to slect the column. blank for column name
418c551fd7SAndreas Gohr     */
428c551fd7SAndreas Gohr    public function addSelectColumn($tablealias, $column, $alias = '') {
438c551fd7SAndreas Gohr        if($alias === '') $alias = $column;
44aa1f5074SAndreas Gohr        if(!isset($this->from[$tablealias])) {
45aa1f5074SAndreas Gohr            throw new StructException('Table Alias does not exist');
46aa1f5074SAndreas Gohr        }
479ee8a979SAndreas Gohr        $this->select[$alias] = "$tablealias.$column";
488c551fd7SAndreas Gohr    }
498c551fd7SAndreas Gohr
508c551fd7SAndreas Gohr    /**
519ee8a979SAndreas Gohr     * Add a new column selection statement
528c551fd7SAndreas Gohr     *
539ee8a979SAndreas Gohr     * Basically the same as @see addSelectColumn() but accepts any statement. This is useful to
548c551fd7SAndreas Gohr     * select things like fixed strings or more complex function calls, but the correctness will not
558c551fd7SAndreas Gohr     * be checked.
568c551fd7SAndreas Gohr     *
578c551fd7SAndreas Gohr     * If the alias already exists, the current statement for that alias will be overwritten.
588c551fd7SAndreas Gohr     *
598c551fd7SAndreas Gohr     * @param string $statement
608c551fd7SAndreas Gohr     * @param string $alias
618c551fd7SAndreas Gohr     */
628c551fd7SAndreas Gohr    public function addSelectStatement($statement, $alias) {
639ee8a979SAndreas Gohr        $this->select[$alias] = $statement;
649ee8a979SAndreas Gohr    }
659ee8a979SAndreas Gohr
669ee8a979SAndreas Gohr    /**
679ee8a979SAndreas Gohr     * Return an already defined column selection statement based on the alias
689ee8a979SAndreas Gohr     *
699ee8a979SAndreas Gohr     * @param string $alias
709ee8a979SAndreas Gohr     * @return string
719ee8a979SAndreas Gohr     * @throws StructException when the alias does not exist
729ee8a979SAndreas Gohr     */
739ee8a979SAndreas Gohr    public function getSelectStatement($alias) {
749ee8a979SAndreas Gohr        if(!isset($this->select[$alias])) {
759ee8a979SAndreas Gohr            throw new StructException('No such select alias');
769ee8a979SAndreas Gohr        }
779ee8a979SAndreas Gohr
789ee8a979SAndreas Gohr        return $this->select[$alias];
798c551fd7SAndreas Gohr    }
808c551fd7SAndreas Gohr
818c551fd7SAndreas Gohr    /**
828c551fd7SAndreas Gohr     * Adds the the table to the FROM statement part
838c551fd7SAndreas Gohr     *
848c551fd7SAndreas Gohr     * @param string $table the table to add
858c551fd7SAndreas Gohr     * @param string $alias alias for the table, blank for table name
868c551fd7SAndreas Gohr     */
878c551fd7SAndreas Gohr    public function addTable($table, $alias = '') {
888c551fd7SAndreas Gohr        if($alias === '') $alias = $table;
89aa1f5074SAndreas Gohr        if(isset($this->from[$alias])) {
90aa1f5074SAndreas Gohr            throw new StructException('Table Alias exists');
91aa1f5074SAndreas Gohr        }
928c551fd7SAndreas Gohr        $this->from[$alias] = "$table AS $alias";
932f68434dSAndreas Gohr        $this->type[$alias] = 'table';
948c551fd7SAndreas Gohr    }
958c551fd7SAndreas Gohr
968c551fd7SAndreas Gohr    /**
978c551fd7SAndreas Gohr     * Adds a LEFT JOIN clause to the FROM statement part, sorted at the correct spot
988c551fd7SAndreas Gohr     *
998c551fd7SAndreas Gohr     * @param string $leftalias the alias of the left table you're joining on, has to exist already
1008c551fd7SAndreas Gohr     * @param string $righttable the right table to be joined
1018c551fd7SAndreas Gohr     * @param string $rightalias an alias for the right table, blank for table name
1028c551fd7SAndreas Gohr     * @param string $onclause the ON clause condition the join is based on
1038c551fd7SAndreas Gohr     */
1048c551fd7SAndreas Gohr    public function addLeftJoin($leftalias, $righttable, $rightalias, $onclause) {
1058c551fd7SAndreas Gohr        if($rightalias === '') $rightalias = $righttable;
106aa1f5074SAndreas Gohr        if(!isset($this->from[$leftalias])) {
107aa1f5074SAndreas Gohr            throw new StructException('Table Alias does not exist');
108aa1f5074SAndreas Gohr        }
109aa1f5074SAndreas Gohr        if(isset($this->from[$rightalias])) {
110aa1f5074SAndreas Gohr            throw new StructException('Table Alias already exists');
111aa1f5074SAndreas Gohr        }
1128c551fd7SAndreas Gohr
1138c551fd7SAndreas Gohr        $pos = array_search($leftalias, array_keys($this->from));
1148c551fd7SAndreas Gohr        $statement = "LEFT OUTER JOIN $righttable AS $rightalias ON $onclause";
1158c551fd7SAndreas Gohr        $this->from = $this->array_insert($this->from, array($rightalias => $statement), $pos + 1);
1162f68434dSAndreas Gohr        $this->type[$rightalias] = 'join';
1178c551fd7SAndreas Gohr    }
1188c551fd7SAndreas Gohr
1198c551fd7SAndreas Gohr    /**
1208c551fd7SAndreas Gohr     * Returns the current WHERE filters and allows to set new ones
1218c551fd7SAndreas Gohr     *
1228c551fd7SAndreas Gohr     * @return QueryBuilderWhere
1238c551fd7SAndreas Gohr     */
1242f68434dSAndreas Gohr    public function filters() {
1258c551fd7SAndreas Gohr        return $this->where;
1268c551fd7SAndreas Gohr    }
1278c551fd7SAndreas Gohr
1288c551fd7SAndreas Gohr    /**
12983cb959bSAndreas Gohr     * Add an ORDER BY clause
13083cb959bSAndreas Gohr     *
13183cb959bSAndreas Gohr     * @param string $sort a single sorting condition
13283cb959bSAndreas Gohr     */
13383cb959bSAndreas Gohr    public function addOrderBy($sort) {
13483cb959bSAndreas Gohr        $this->orderby[] = $sort;
13583cb959bSAndreas Gohr    }
13683cb959bSAndreas Gohr
13783cb959bSAndreas Gohr    /**
13883cb959bSAndreas Gohr     * Add an GROUP BY clause
13983cb959bSAndreas Gohr     *
1402f68434dSAndreas Gohr     * @param string $tablealias
1412f68434dSAndreas Gohr     * @param string $column
14283cb959bSAndreas Gohr     */
1432f68434dSAndreas Gohr    public function addGroupByColumn($tablealias, $column) {
144aa1f5074SAndreas Gohr        if(!isset($this->from[$tablealias])) {
145aa1f5074SAndreas Gohr            throw new StructException('Table Alias does not exist');
146aa1f5074SAndreas Gohr        }
1472f68434dSAndreas Gohr        $this->groupby[] = "$tablealias.$column";
1482f68434dSAndreas Gohr    }
1492f68434dSAndreas Gohr
1502f68434dSAndreas Gohr    /**
1512f68434dSAndreas Gohr     * Add an GROUP BY clause
1522f68434dSAndreas Gohr     *
1532f68434dSAndreas Gohr     * Like @see addGroupByColumn but accepts an arbitrary statement
1542f68434dSAndreas Gohr     *
1552f68434dSAndreas Gohr     * @param string $statement a single grouping clause
1562f68434dSAndreas Gohr     */
1572f68434dSAndreas Gohr    public function addGroupByStatement($statement) {
1582f68434dSAndreas Gohr        $this->groupby[] = $statement;
15983cb959bSAndreas Gohr    }
16083cb959bSAndreas Gohr
16183cb959bSAndreas Gohr    /**
1628c551fd7SAndreas Gohr     * Adds a value to the statement
1638c551fd7SAndreas Gohr     *
164df30dbf7SAndreas Gohr     * This function returns the name of the placeholder you have to use in your statement, so whenever
165df30dbf7SAndreas Gohr     * you need to use a user value in a statement, call this first, then add the statement through the
166df30dbf7SAndreas Gohr     * other functions using the returned placeholder.
1678c551fd7SAndreas Gohr     *
1688c551fd7SAndreas Gohr     * @param mixed $value
1698c551fd7SAndreas Gohr     * @return string
1708c551fd7SAndreas Gohr     */
1718c551fd7SAndreas Gohr    public function addValue($value) {
1728c551fd7SAndreas Gohr        static $count = 0;
1738c551fd7SAndreas Gohr        $count++;
1748c551fd7SAndreas Gohr
1758c551fd7SAndreas Gohr        $placeholder = ":!!val$count!!:"; // sqlite plugin does not support named parameters, yet so we have simulate it
1768c551fd7SAndreas Gohr        $this->values[$placeholder] = $value;
1778c551fd7SAndreas Gohr        return $placeholder;
1788c551fd7SAndreas Gohr    }
1798c551fd7SAndreas Gohr
1808c551fd7SAndreas Gohr    /**
1818c551fd7SAndreas Gohr     * Creates a new table alias that has not been used before
1828c551fd7SAndreas Gohr     *
1838c551fd7SAndreas Gohr     * @return string
1848c551fd7SAndreas Gohr     */
1858c551fd7SAndreas Gohr    public function generateTableAlias() {
1868c551fd7SAndreas Gohr        static $count = 0;
1878c551fd7SAndreas Gohr        $count++;
1888c551fd7SAndreas Gohr        return "T$count";
1898c551fd7SAndreas Gohr    }
1908c551fd7SAndreas Gohr
1918c551fd7SAndreas Gohr    /**
1928c551fd7SAndreas Gohr     * Returns the complete SQL statement and the values to apply
1938c551fd7SAndreas Gohr     *
19483cb959bSAndreas Gohr     * @return array ($sql, $vals)
1958c551fd7SAndreas Gohr     */
1968c551fd7SAndreas Gohr    public function getSQL() {
1972f68434dSAndreas Gohr        // FROM needs commas only for tables, not joins
1982f68434dSAndreas Gohr        $from = '';
1992f68434dSAndreas Gohr        foreach($this->from as $alias => $statement) {
2002f68434dSAndreas Gohr            if($this->type[$alias] == 'table' && $from) {
2012f68434dSAndreas Gohr                $from .= ",\n";
2022f68434dSAndreas Gohr            } else {
2032f68434dSAndreas Gohr                $from .= "\n";
2042f68434dSAndreas Gohr            }
2052f68434dSAndreas Gohr
2062f68434dSAndreas Gohr            $from .= $statement;
2072f68434dSAndreas Gohr        }
2082f68434dSAndreas Gohr
2099ee8a979SAndreas Gohr        // prepare aliases for the select columns
2109ee8a979SAndreas Gohr        $selects = array();
2119ee8a979SAndreas Gohr        foreach($this->select as $alias => $select) {
2129ee8a979SAndreas Gohr            $selects[] = "$select AS $alias";
2139ee8a979SAndreas Gohr        }
2149ee8a979SAndreas Gohr
2158c551fd7SAndreas Gohr        $sql =
2169ee8a979SAndreas Gohr            ' SELECT ' . join(",\n", $selects) . "\n" .
2172f68434dSAndreas Gohr            '   FROM ' . $from . "\n" .
2188c551fd7SAndreas Gohr            '  WHERE ' . $this->where->toSQL() . "\n";
2198c551fd7SAndreas Gohr
22083cb959bSAndreas Gohr        if($this->groupby) {
22183cb959bSAndreas Gohr            $sql .=
22283cb959bSAndreas Gohr                'GROUP BY ' . join(",\n", $this->groupby) . "\n";
22383cb959bSAndreas Gohr        }
22483cb959bSAndreas Gohr
2252f68434dSAndreas Gohr        if($this->orderby) {
2262f68434dSAndreas Gohr            $sql .=
2272f68434dSAndreas Gohr                'ORDER BY ' . join(",\n", $this->orderby) . "\n";
2282f68434dSAndreas Gohr        }
2292f68434dSAndreas Gohr
2308c551fd7SAndreas Gohr        return $this->fixPlaceholders($sql);
2318c551fd7SAndreas Gohr    }
2328c551fd7SAndreas Gohr
2338c551fd7SAndreas Gohr    /**
2348c551fd7SAndreas Gohr     * Replaces the named placeholders with ? placeholders
2358c551fd7SAndreas Gohr     *
2368c551fd7SAndreas Gohr     * Until the sqlite plugin can use named placeholder properly
2378c551fd7SAndreas Gohr     *
2388c551fd7SAndreas Gohr     * @param string $sql
2398c551fd7SAndreas Gohr     * @return array
2408c551fd7SAndreas Gohr     */
2418c551fd7SAndreas Gohr    protected function fixPlaceholders($sql) {
2428c551fd7SAndreas Gohr        $vals = array();
2438c551fd7SAndreas Gohr
2448c551fd7SAndreas Gohr        while(preg_match('/(:!!val\d+!!:)/', $sql, $m)) {
2458c551fd7SAndreas Gohr            $pl = $m[1];
2462b5df26eSAndreas Gohr
247*45664274SAndreas Gohr            if(!array_key_exists($pl, $this->values)) {
248aa1f5074SAndreas Gohr                throw new StructException('Placeholder not found');
249aa1f5074SAndreas Gohr            }
2502b5df26eSAndreas Gohr
2512b5df26eSAndreas Gohr            $sql = preg_replace("/$pl/", '?', $sql, 1);
2528c551fd7SAndreas Gohr            $vals[] = $this->values[$pl];
2538c551fd7SAndreas Gohr        }
2548c551fd7SAndreas Gohr
2558c551fd7SAndreas Gohr        return array($sql, $vals);
2568c551fd7SAndreas Gohr    }
2578c551fd7SAndreas Gohr
2588c551fd7SAndreas Gohr    /**
2598c551fd7SAndreas Gohr     * Insert an array into another array at a given position in an associative array
2608c551fd7SAndreas Gohr     *
2618c551fd7SAndreas Gohr     * @param array $array The initial array
2628c551fd7SAndreas Gohr     * @param array $pairs The array to insert
2638c551fd7SAndreas Gohr     * @param string $key_pos The position at which to insert
2648c551fd7SAndreas Gohr     * @link https://gist.github.com/scribu/588429 simplified
2658c551fd7SAndreas Gohr     * @return array
2668c551fd7SAndreas Gohr     */
2678c551fd7SAndreas Gohr    protected function array_insert($array, $pairs, $key_pos) {
2688c551fd7SAndreas Gohr        $result = array_slice($array, 0, $key_pos);
2698c551fd7SAndreas Gohr        $result = array_merge($result, $pairs);
2708c551fd7SAndreas Gohr        $result = array_merge($result, array_slice($array, $key_pos));
2718c551fd7SAndreas Gohr        return $result;
2728c551fd7SAndreas Gohr    }
2738c551fd7SAndreas Gohr}
2748c551fd7SAndreas Gohr
275