Evaluate.php 5.48 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
            $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
113
                $evaluated = $this->parse($evaluated, $recursion + 1, $debugLocal);
            }  
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
132
     *   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
133
134
135
136
     * 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
137
     * @throws DbException
Carsten  Rose's avatar
Carsten Rose committed
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
     */
    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;
    }
172
173
174
175

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