Completed
Push — master ( 73c8dd...7be29b )
by Ondřej
03:37
created

SqlPatternRecipeMacros::toSql()   C

Complexity

Conditions 9
Paths 12

Size

Total Lines 51
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 51
rs 6.2727
cc 9
eloc 33
nc 12
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Ivory\Query;
3
4
use Ivory\Exception\InvalidStateException;
5
use Ivory\Exception\NoDataException;
6
use Ivory\Exception\UndefinedTypeException;
7
use Ivory\Lang\SqlPattern\SqlPattern;
8
use Ivory\Lang\SqlPattern\SqlPatternPlaceholder;
9
use Ivory\Type\ITypeDictionary;
10
use Ivory\Utils\StringUtils;
11
12
trait SqlPatternRecipeMacros
13
{
14
    /** @var SqlPattern */
15
    private $sqlPattern;
16
    /** @var array map: parameter name or position => supplied value */
17
    private $params;
18
    /** @var bool[] map: name of parameter which has not been set any value yet => <tt>true</tt> value */
19
    private $unsatisfiedParams;
20
21
22
    /**
23
     * Creates an SQL recipe from an SQL string.
24
     *
25
     * No parameter substitution is performed on the string - it is used as is.
26
     *
27
     * @param string $sql SQL string
28
     * @return static
29
     */
30
    public static function fromSql(string $sql): self
31
    {
32
        $sqlPattern = new SqlPattern($sql, [], []);
33
        return new static($sqlPattern, []);
34
    }
35
36
    /**
37
     * Creates a new recipe from an SQL pattern.
38
     *
39
     * Values for all positional parameters required by the pattern must be given.
40
     *
41
     * Example:
42
     * <pre>
43
     * <?php
44
     * $recipe = new SqlRecipe(
45
     *   'SELECT *, %s:status FROM person WHERE role = %d AND email = %s',
46
     *   4, '[email protected]'
47
     * );
48
     * // results in "SELECT * FROM person WHERE role = 4 AND email = '[email protected]'"
49
     * </pre>
50
     *
51
     * Performance considerations: parsing the SQL pattern, if given as a string, is done by the parser obtained by
52
     * {@link \Ivory\Ivory::getSqlPatternParser()}. Depending on Ivory configuration, the parser will cache the results
53
     * and reuse them for the same pattern next time.
54
     *
55
     * @param string|SqlPattern $sqlPattern
56
     * @param array ...$positionalParameters
57
     * @return static
58
     * @throws \InvalidArgumentException when the number of provided positional parameters differs from the number of
59
     *                                     positional parameters required by the pattern
60
     */
61
    public static function fromPattern($sqlPattern, ...$positionalParameters): self
62
    {
63
        if (!$sqlPattern instanceof SqlPattern) {
64
            $parser = \Ivory\Ivory::getSqlPatternParser();
65
            $sqlPattern = $parser->parse($sqlPattern);
66
        }
67
68
        if (count($sqlPattern->getPositionalPlaceholders()) != count($positionalParameters)) {
69
            throw new \InvalidArgumentException(sprintf(
70
                'The SQL pattern requires %d positional parameters, %d given.',
71
                count($sqlPattern->getPositionalPlaceholders()),
72
                count($positionalParameters)
73
            ));
74
        }
75
76
        return new static($sqlPattern, $positionalParameters);
77
    }
78
79
    /**
80
     * Creates an SQL recipe from one or more fragments, each with its own positional parameters.
81
     *
82
     * Each fragment must be immediately followed by values for all positional parameters it requires. Then, another
83
     * fragment may follow. As the very last argument, a map of values for named parameters may optionally be given (or
84
     * {@link SqlRecipe::setParams()} may be used to set them later).
85
     *
86
     * The fragments get concatenated to form the resulting SQL pattern. A single space is added between each two
87
     * fragments the latter of which does not start with whitespace.
88
     *
89
     * Named parameters are shared among fragments. In other words, if two fragments use the same named parameter,
90
     * specifying the parameter by {@link setParam()} will substitute the same value to both fragments.
91
     *
92
     * Example:
93
     * <pre>
94
     * <?php
95
     * $recipe = SqlRecipe::fromFragments(
96
     *   'SELECT * FROM person WHERE role = %d', 4, 'AND email = %s', '[email protected]'
97
     * );
98
     * // results in "SELECT * FROM person WHERE role = 4 AND email = '[email protected]'"
99
     * </pre>
100
     *
101
     * Performance considerations: parsing the SQL pattern, if given as a string, is done by the parser obtained by
102
     * {@link \Ivory\Ivory::getSqlPatternParser()}. Depending on Ivory configuration, the parser will cache the results
103
     * and reuse them for the same pattern next time.
104
     *
105
     * @internal Ivory design note: The single space added between each two fragments aspires to be more practical than
106
     * a mere concatenation, which would require the user to specify spaces where the next fragment immediately
107
     * continued with the query.
108
     *
109
     * @param string|SqlPattern $fragment
110
     * @param array ...$fragmentsAndPositionalParams
111
     *                                  further fragments (each of which is either a <tt>string</tt> or an
112
     *                                    {@link SqlPattern} object) and values of their parameters;
113
     *                                  the very last argument may be a map of values for named parameters to set
114
     *                                    immediately
115
     *
116
     * @return static
117
     * @throws \InvalidArgumentException when any fragment is not followed by the exact number of parameter values it
118
     *                                     requires
119
     */
120
    public static function fromFragments($fragment, ...$fragmentsAndPositionalParams): self
121
    {
122
        $overallSqlTorso = '';
123
        $overallPosPlaceholders = [];
124
        $overallNamedPlaceholderMap = [];
125
        $overallPosParams = [];
126
127
        $namedParamValues = [];
128
129
        $curFragment = $fragment;
130
        $curFragmentNum = 1;
131
        $argsProcessed = 0;
132
        do {
133
            // process the fragment
134
            if (!$curFragment instanceof SqlPattern) {
135
                if (is_string($curFragment)) {
136
                    $parser = \Ivory\Ivory::getSqlPatternParser();
137
                    $curFragment = $parser->parse($curFragment);
138
                } elseif (
139
                    (is_array($curFragment) || $curFragment instanceof \Traversable) && // PHP 7.1: is_iterable()
140
                    $argsProcessed > 0 &&
141
                    !array_key_exists($argsProcessed, $fragmentsAndPositionalParams)
142
                ) {
143
                    $namedParamValues = $curFragment;
144
                    break;
145
                } else {
146
                    $ord = StringUtils::englishOrd($curFragmentNum);
147
                    throw new \InvalidArgumentException("Invalid type of $ord fragment. Isn't it a misplaced parameter value?");
148
                }
149
            }
150
151
            // add to the overall pattern
152
            if ($argsProcessed > 0 && !preg_match('~^\s~', $curFragment->getSqlTorso())) {
153
                $overallSqlTorso .= ' ';
154
            }
155
            $sqlTorsoOffset = strlen($overallSqlTorso);
156
            $overallSqlTorso .= $curFragment->getSqlTorso();
157 View Code Duplication
            foreach ($curFragment->getPositionalPlaceholders() as $plcHdr) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158
                $overallPlcHdr = new SqlPatternPlaceholder(
159
                    $sqlTorsoOffset + $plcHdr->getOffset(),
160
                    count($overallPosPlaceholders),
161
                    $plcHdr->getTypeName(),
162
                    $plcHdr->isTypeNameQuoted(),
163
                    $plcHdr->getSchemaName(),
164
                    $plcHdr->isSchemaNameQuoted()
165
                );
166
                $overallPosPlaceholders[] = $overallPlcHdr;
167
            }
168
            foreach ($curFragment->getNamedPlaceholderMap() as $name => $occurrences) {
169
                /** @var SqlPatternPlaceholder[] $occurrences */
170
                if (!isset($overallNamedPlaceholderMap[$name])) {
171
                    $overallNamedPlaceholderMap[$name] = [];
172
                }
173 View Code Duplication
                foreach ($occurrences as $plcHdr) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
                    $overallPlcHdr = new SqlPatternPlaceholder(
175
                        $sqlTorsoOffset + $plcHdr->getOffset(),
176
                        $name,
177
                        $plcHdr->getTypeName(),
178
                        $plcHdr->isTypeNameQuoted(),
179
                        $plcHdr->getSchemaName(),
180
                        $plcHdr->isSchemaNameQuoted()
181
                    );
182
                    $overallNamedPlaceholderMap[$name][] = $overallPlcHdr;
183
                }
184
            }
185
186
            // values of parameters
187
            $plcHdrCnt = count($curFragment->getPositionalPlaceholders());
188
            $posParams = array_slice($fragmentsAndPositionalParams, $argsProcessed, $plcHdrCnt);
189
            if (count($posParams) == $plcHdrCnt) {
190
                $overallPosParams = array_merge($overallPosParams, $posParams);
191
            } else {
192
                $ord = StringUtils::englishOrd($curFragmentNum);
193
                throw new \InvalidArgumentException("Not enough positional parameters for the $ord fragment");
194
            }
195
196
            $curFragmentNum++;
197
            $argsProcessed += count($posParams);
198
199
            $curFragment =& $fragmentsAndPositionalParams[$argsProcessed];
200
            $argsProcessed++;
201
        } while (isset($curFragment));
202
203
        $overallPattern = new SqlPattern($overallSqlTorso, $overallPosPlaceholders, $overallNamedPlaceholderMap);
204
205
        $recipe = new static($overallPattern, $overallPosParams);
206
        $recipe->setParams($namedParamValues);
207
        return $recipe;
208
    }
209
210
    final private function __construct(SqlPattern $sqlPattern, array $positionalParameters)
211
    {
212
        $this->sqlPattern = $sqlPattern;
213
        $this->params = $positionalParameters;
214
        $this->unsatisfiedParams = array_fill_keys(array_keys($sqlPattern->getNamedPlaceholderMap()), true);
215
    }
216
217
    public function setParam($nameOrPosition, $value)
218
    {
219
        if (isset($this->unsatisfiedParams[$nameOrPosition])) {
220
            unset($this->unsatisfiedParams[$nameOrPosition]);
221
        } elseif (!array_key_exists($nameOrPosition, $this->params)) {
222
            throw new \InvalidArgumentException("The SQL pattern does not have parameter '$nameOrPosition'");
223
        }
224
225
        $this->params[$nameOrPosition] = $value;
226
        return $this;
227
    }
228
229
    public function setParams($paramMap)
230
    {
231
        foreach ($paramMap as $nameOrPosition => $value) {
232
            $this->setParam($nameOrPosition, $value);
233
        }
234
        return $this;
235
    }
236
237
    public function getSqlPattern(): SqlPattern
238
    {
239
        return $this->sqlPattern;
240
    }
241
242
    public function getParams(): array
243
    {
244
        return $this->params;
245
    }
246
247
    /**
248
     * @param ITypeDictionary $typeDictionary
249
     * @return string
250
     * @throws InvalidStateException when values for one or more named parameters has not been set
251
     * @throws UndefinedTypeException when some of the types used in the pattern are not defined
252
     */
253
    public function toSql(ITypeDictionary $typeDictionary): string
254
    {
255
        if ($this->unsatisfiedParams) {
256
            $names = array_keys($this->unsatisfiedParams);
257
            if (count($names) == 1) {
258
                $msg = sprintf('Value for parameter "%s" has not been set.', $names[0]);
259
            } else {
260
                $msg = sprintf(
261
                    'Values for parameters %s and "%s" have not been set.',
262
                    array_map(function ($s) { return "\"$s\""; }, array_slice($names, 0, -1))
263
                );
264
            }
265
            throw new InvalidStateException($msg);
266
        }
267
268
        $gen = $this->sqlPattern->generateSql();
269
        while ($gen->valid()) {
270
            /** @var SqlPatternPlaceholder $placeholder */
271
            $placeholder = $gen->current();
272
            assert(
273
                array_key_exists($placeholder->getNameOrPosition(), $this->params),
274
                new NoDataException("Value for parameter {$placeholder->getNameOrPosition()} not set.")
275
            );
276
277
            $value = $this->params[$placeholder->getNameOrPosition()];
278
279
            if ($placeholder->getTypeName() !== null) {
280
                $typeName = $placeholder->getTypeName();
281
                if (!$placeholder->isTypeNameQuoted()) {
282
                    $typeName = mb_strtolower($typeName); // OPT: SqlPatternPlaceholder might also store the lower-case name, which might be cached
283
                }
284
                $schemaName = $placeholder->getSchemaName();
285
                if ($schemaName !== null) {
286
                    if (!$placeholder->isSchemaNameQuoted()) {
287
                        $schemaName = mb_strtolower($schemaName); // OPT: SqlPatternPlaceholder might also store the lower-case name, which might be cached
288
                    }
289
                } elseif ($placeholder->isTypeNameQuoted()) {
290
                    $schemaName = false;
291
                }
292
293
                $converter = $typeDictionary->requireTypeByName($typeName, $schemaName);
294
            } else {
295
                $converter = $typeDictionary->requireTypeByValue($value);
296
            }
297
            $serializedValue = $converter->serializeValue($value);
298
299
            $gen->send($serializedValue);
300
        }
301
302
        return $gen->getReturn();
303
    }
304
}
305