Evaluate.php 5.7 KB
Newer Older
Carsten  Rose's avatar
Carsten Rose committed
1
2
3
4
5
6
7
8
9
10
11
<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 1/12/16
 * Time: 4:36 PM
 */

namespace qfq;

use qfq;
12
use qfq\Store;
Carsten  Rose's avatar
Carsten Rose committed
13
14
15
16

require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Database.php');

Carsten  Rose's avatar
Carsten Rose committed
17
18
19
20
/**
 * Class Evaluate
 * @package qfq
 */
Carsten  Rose's avatar
Carsten Rose committed
21
22
23
24
25
26
27
28
class Evaluate {
    private $store = null;
    private $db = null;
    private $startDelimiter = '';
    private $startDelimiterLength = 0;
    private $endDelimiter = '';
    private $endDelimiterLength = 0;
    private $sqlKeywords = array('SELECT ', 'INSERT ', 'DELETE ', 'UPDATE ', 'SHOW ');
29
30

//    private $debugStack = array();
Carsten  Rose's avatar
Carsten Rose committed
31
32
33
34
35
36
37
38
39
40
41
42


    public function __construct(Store $store, Database $db, $startDelimiter = '{{', $endDelimiter = '}}') {
        $this->store = $store;
        $this->db = $db;
        $this->startDelimiter = $startDelimiter;
        $this->startDelimiterLength = strlen($startDelimiter);
        $this->endDelimiter = $endDelimiter;
        $this->endDelimiterLength = strlen($endDelimiter);
    }

    /**
43
     * Evaluate a whole array or a array of arrays.
44
45
46
47
     *
     * @param $tokenArray
     * @return mixed
     */
48
    public function parseArray($tokenArray, &$debugStack = array()) {
49
50
51
        $arr = array();

        foreach ($tokenArray as $key => $value) {
52
53
54
            if (is_array($value)) {
                $arr[] = $this->parseArray($value);
            } else {
55
                $arr[$key] = $this->parse($value, 0, $debugStack);
56
            }
57
58
59
60
61
62
        }

        return $arr;
    }

    /**
63
     * Recursive evaluation of 'line'. Constant string, Variables or SQL Query or all of them.
64
65
     *
     * Token to replace have to be enclosed by '{{' and '}}'
66
     *
Carsten  Rose's avatar
Carsten Rose committed
67
     * @param $line
68
69
     * @param int $recursion
     * @return array|mixed|null|string
70
     * @throws UserException
Carsten  Rose's avatar
Carsten Rose committed
71
     */
72
    public function parse($line, $recursion = 0, &$debugStack = array(), &$foundInStore = '') {
73
        $flagTokenReplaced = false;
Carsten  Rose's avatar
Carsten Rose committed
74
75

        if ($recursion > 4) {
Carsten  Rose's avatar
Carsten Rose committed
76
            throw new qfq\UserException("Recursion too deep ($recursion). Line: $line", ERROR_RECURSION_TOO_DEEP);
Carsten  Rose's avatar
Carsten Rose committed
77
78
        }

79
        $result = $line;
Carsten  Rose's avatar
Carsten Rose committed
80

81
82
        $debugIndent = str_repeat(' ', $recursion);
        $debugLocal[] = $debugIndent . "#Parse: '$result'";
Carsten  Rose's avatar
Carsten Rose committed
83

84
        $posFirstClose = strpos($result, $this->endDelimiter);
85

86
87
88
89
        while ($posFirstClose !== false) {
            $flagTokenReplaced = true;

            $posMatchOpen = strrpos(substr($result, 0, $posFirstClose), $this->startDelimiter);
Carsten  Rose's avatar
Carsten Rose committed
90
            if ($posMatchOpen === false) {
91
                throw new \qfq\UserException("Missing open delimiter: $result", ERROR_MISSING_OPEN_DELIMITER);
Carsten  Rose's avatar
Carsten Rose committed
92
93
            }

94
95
96
            $pre = substr($result, 0, $posMatchOpen);
            $post = substr($result, $posFirstClose + $this->endDelimiterLength);
            $match = substr($result, $posMatchOpen + $this->startDelimiterLength, $posFirstClose - $posMatchOpen - $this->startDelimiterLength);
Carsten  Rose's avatar
Carsten Rose committed
97

98
            $evaluated = $this->substitute($match, $foundInStore);
99
            $debugLocal[] = $debugIndent . "#Replace: '$match'";
Carsten  Rose's avatar
Carsten Rose committed
100
101
102

            // If an array is returned, break everything and return this assoc array.
            if (is_array($evaluated)) {
103
                $result = $evaluated;
104
                $debugLocal[] = $debugIndent . "#By: 'array(" . count($result) . ")'";
105
                break;
Carsten  Rose's avatar
Carsten Rose committed
106
107
            }

108
109
            $debugLocal[] = $debugIndent . "#By: '$evaluated'";

Carsten  Rose's avatar
Carsten Rose committed
110
111
            // More to substitute in the new evaluated result? Start recursion just with the new result..
            if (strpos($evaluated, $this->endDelimiter) !== false) {
112
                $evaluated = $this->parse($evaluated, $recursion + 1, $debugLocal, $foundInStore);
113
            }  
Carsten  Rose's avatar
Carsten Rose committed
114

115
116
117
            $result = $pre . $evaluated . $post;
            $posFirstClose = strpos($result, $this->endDelimiter);
        }
Carsten  Rose's avatar
Carsten Rose committed
118

119
120
121
        if ($flagTokenReplaced === true) {
            $debugLocal[] = $debugIndent . "#Final: " . is_array($result) ?  "array(" . count($result) .")" : "'$result'" ;
            $debugStack = $debugLocal;
Carsten  Rose's avatar
Carsten Rose committed
122
123
        }

124
        return $result;
Carsten  Rose's avatar
Carsten Rose committed
125
126
127
128
129
    }

    /**
     * Tries to substitute $token.
     * Token might be
130
131
     *   a) a SQL statement to fire
     *   b) fetch from a store. Syntax: 'form', 'form:C', 'form:SC0', 'form:S:ALNUMX'
132
     * The token have to be _without_ Delimiter '{{' / '}}'
Carsten  Rose's avatar
Carsten Rose committed
133
134
135
     * If neither a) or b) match, return the token itself, surrounded by single ticks, to emphase that substition failed.
     *
     * @param $token
Carsten  Rose's avatar
Carsten Rose committed
136
     * @param string $foundInStore
Carsten  Rose's avatar
Carsten Rose committed
137
     * @return array|mixed|null|string
Carsten  Rose's avatar
Carsten Rose committed
138
     * @throws CodeException
139
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
140
     */
141
    public function substitute($token, &$foundInStore = '') {
Carsten  Rose's avatar
Carsten Rose committed
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
        $sqlMode = ROW_IMPLODE_ALL;

        $token = trim($token);

        // just to extract the first token: check if this is a SQL Statement
        $arr = explode(' ', $token, 2);

        if ($token[0] === '!') {
            $token = substr($token, 1);
            $arr[0] = substr($arr[0], 1);
            $sqlMode = ROW_REGULAR;
        }

        // SQL Statement?
        if (in_array(strtoupper($arr[0] . ' '), $this->sqlKeywords)) {
            return $this->db->sql($token, $sqlMode);
        }

Carsten  Rose's avatar
Carsten Rose committed
160
        // explode for: <key>:<store priority>:<sanitize class>
Carsten  Rose's avatar
Carsten Rose committed
161
162
163
164
165
166
167
168
        $arr = explode(':', $token, 3);
        if (!isset($arr[1]))
            $arr[1] = null;
        if (!isset($arr[2]))
            $arr[2] = null;


        // search for value in stores
169
        $value = $this->store->getVar($arr[0], $arr[1], $arr[2], $foundInStore);
Carsten  Rose's avatar
Carsten Rose committed
170

Carsten  Rose's avatar
Carsten Rose committed
171
        // nothing replaced: put ticks around, to sanitize strings for SQL statements. Nothing to substitute is not a wished situation.
Carsten  Rose's avatar
Carsten Rose committed
172
173
        return ($value === false) ? "'" . $token . "'" : $value;
    }
174
175
176
177

    public function getDebug() {
        return '<pre>' . implode("\n", $this->debugStack) . '</pre>';
    }
178
}