Evaluate.php 5.39 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
63
        }

        return $arr;
    }

    /**
     * Recursive evaluation of 'line'.
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
73
    public function parse($line, $recursion = 0, &$debugStack = array()) {
        $flagTokenReplaced = false;
Carsten  Rose's avatar
Carsten Rose committed
74
75

        if ($recursion > 4) {
76
            throw new qfq\UserException("Recursion too deep: $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);
99
100
            $debugLocal[] = $debugIndent . "#Replace: '$match'";
            $debugLocal[] = $debugIndent . "#By: '$evaluated'";
Carsten  Rose's avatar
Carsten Rose committed
101
102
103

            // If an array is returned, break everything and return this assoc array.
            if (is_array($evaluated)) {
104
105
                $result = $evaluated;
                break;
Carsten  Rose's avatar
Carsten Rose committed
106
107
108
109
            }

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

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

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

122
        return $result;
Carsten  Rose's avatar
Carsten Rose committed
123
124
125
126
127
    }

    /**
     * Tries to substitute $token.
     * Token might be
128
129
130
     *   a) fetch from a store. Syntax: 'form', 'form:C', 'form:SC0', 'form:S:ALNUMX'
     *   b) a SQL statement to fire
     * The token have to be _without_ Delimiter '{{' / '}}'
Carsten  Rose's avatar
Carsten Rose committed
131
132
133
134
     * If neither a) or b) match, return the token itself, surrounded by single ticks, to emphase that substition failed.
     *
     * @param $token
     * @return array|mixed|null|string
135
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
     */
    public function substitute($token) {
        $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);
        }

        // explode for: <key>:<store priority>:<sanatize class>
        $arr = explode(':', $token, 3);
        if (!isset($arr[1]))
            $arr[1] = null;
        if (!isset($arr[2]))
            $arr[2] = null;


        // search for value in stores
        $value = $this->store->getVar($arr[0], $arr[1], $arr[2]);

        // nothing replaced: put ticks around, to sanatize strings for SQL statements. Nothing to substitute is not a wished situation.
        return ($value === false) ? "'" . $token . "'" : $value;
    }
170
171
172
173

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