This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | namespace izzum\statemachine\persistence; |
||
3 | use izzum\statemachine\loader\Loader; |
||
4 | use izzum\statemachine\StateMachine; |
||
5 | use izzum\statemachine\Identifier; |
||
6 | use izzum\statemachine\loader\LoaderArray; |
||
7 | use izzum\statemachine\loader\LoaderData; |
||
8 | use izzum\statemachine\Exception; |
||
9 | use izzum\statemachine\Transition; |
||
10 | use izzum\statemachine\State; |
||
11 | |||
12 | /** |
||
13 | * A persistence adapter/loader specifically for the PHP Data Objects (PDO) |
||
14 | * extension. |
||
15 | * PDO use database drivers for different backend implementations (postgres, |
||
16 | * mysql, sqlite, MSSQL, oracle etc). |
||
17 | * By providing a DSN connection string, you can connect to different backends. |
||
18 | * |
||
19 | * This specific adapter uses the schema as defined in |
||
20 | * - /assets/sql/postgres.sql |
||
21 | * - /assets/sql/sqlite.sql |
||
22 | * - /assets/sql/mysql.sql |
||
23 | * but can be used on tables for a different database vendor as long as |
||
24 | * the table names, fields and constraints are the same. Optionally, |
||
25 | * a prefix can be set. |
||
26 | * |
||
27 | * This Adapter does double duty as a Loader since both use the |
||
28 | * same backend. You could use two seperate classes for this, but you can |
||
29 | * implement both in one class. |
||
30 | * You could use the ReaderWriterDelegator to seperate |
||
31 | * the reading of configuration and the persisting of state and transition data. |
||
32 | * |
||
33 | * |
||
34 | * If you need an adapter more specialized to your |
||
35 | * needs/framework/system you can easily write one yourself. |
||
36 | * |
||
37 | * More functionality related to a backend can be implemented on this class eg: |
||
38 | * - get the history of transitions from the history table |
||
39 | * - get the factories for machines from the machine table |
||
40 | * |
||
41 | * @link http://php.net/manual/en/pdo.drivers.php |
||
42 | * @link http://php.net/manual/en/book.pdo.php |
||
43 | * |
||
44 | * @author Rolf Vreijdenberger |
||
45 | */ |
||
46 | class PDO extends Adapter implements Loader { |
||
47 | |||
48 | /** |
||
49 | * the pdo connection string |
||
50 | * |
||
51 | * @var string |
||
52 | */ |
||
53 | private $dsn; |
||
54 | |||
55 | /** |
||
56 | * |
||
57 | * @var string |
||
58 | */ |
||
59 | private $user; |
||
60 | /** |
||
61 | * |
||
62 | * @var string |
||
63 | */ |
||
64 | private $password; |
||
65 | /** |
||
66 | * pdo options |
||
67 | * |
||
68 | * @var array |
||
69 | */ |
||
70 | private $options; |
||
71 | |||
72 | /** |
||
73 | * the locally cached connections |
||
74 | * |
||
75 | * @var \PDO |
||
76 | */ |
||
77 | private $connection; |
||
78 | |||
79 | /** |
||
80 | * table prefix |
||
81 | * |
||
82 | * @var string |
||
83 | */ |
||
84 | private $prefix = ''; |
||
85 | |||
86 | /** |
||
87 | * |
||
88 | * @param string $dsn |
||
89 | * a PDO data source name |
||
90 | * example: 'pgsql:host=localhost;port=5432;dbname=izzum' |
||
91 | * @param string $user |
||
92 | * optional, defaults to null |
||
93 | * @param string $password |
||
94 | * optional, defaults to null |
||
95 | * @param array $options |
||
96 | * optional, defaults to empty array. |
||
97 | * @link http://php.net/manual/en/pdo.connections.php |
||
98 | */ |
||
99 | public function __construct($dsn, $user = null, $password = null, $options = array()) |
||
100 | { |
||
101 | $this->dsn = $dsn; |
||
102 | $this->user = $user; |
||
103 | $this->password = $password; |
||
104 | $this->options = $options; |
||
105 | } |
||
106 | |||
107 | /** |
||
108 | * get the connection to a database via the PDO adapter. |
||
109 | * The connection retrieved will be reused if one already exists. |
||
110 | * |
||
111 | * @return \PDO |
||
112 | * @throws Exception |
||
113 | */ |
||
114 | public function getConnection() |
||
115 | { |
||
116 | try { |
||
117 | if ($this->connection === null) { |
||
118 | $this->connection = new \PDO($this->dsn, $this->user, $this->password, $this->options); |
||
119 | $this->setupConnection($this->connection); |
||
120 | } |
||
121 | return $this->connection; |
||
122 | } catch(\Exception $e) { |
||
123 | throw new Exception(sprintf("error creating PDO [%s], message: [%s]", $this->dsn, $e->getMessage()), Exception::PERSISTENCE_FAILED_TO_CONNECT); |
||
124 | } |
||
125 | } |
||
126 | |||
127 | /** |
||
128 | * set the PDO connection explicitely, useful if you want to share the |
||
129 | * PDO instance when it is created outside this class. |
||
130 | * @param \PDO $connection |
||
131 | */ |
||
132 | public function setConnection(\PDO $connection) |
||
133 | { |
||
134 | $this->connection = $connection; |
||
135 | } |
||
136 | |||
137 | protected function setupConnection(\PDO $connection) |
||
0 ignored issues
–
show
|
|||
138 | { |
||
139 | /** |
||
140 | * hook, override to: |
||
141 | * - set schema on postgresql |
||
142 | * - set PRAGMA on sqlite |
||
143 | * - etc.. |
||
144 | * whatever is the need to do a setup on, the first time |
||
145 | * you create a connection |
||
146 | * - SET UTF-8 on mysql can be done with an option in the $options |
||
147 | * constructor argument |
||
148 | */ |
||
149 | } |
||
150 | |||
151 | /** |
||
152 | * |
||
153 | * @return string the type of backend we connect to |
||
154 | */ |
||
155 | public function getType() |
||
156 | { |
||
157 | $index = strpos($this->dsn, ":"); |
||
158 | return $index ? substr($this->dsn, 0, $index) : $this->dsn; |
||
159 | } |
||
160 | |||
161 | /** |
||
162 | * set the table prefix to be used |
||
163 | * |
||
164 | * @param string $prefix |
||
165 | */ |
||
166 | final public function setPrefix($prefix) |
||
167 | { |
||
168 | $this->prefix = $prefix; |
||
169 | } |
||
170 | |||
171 | /** |
||
172 | * get the table prefix |
||
173 | * |
||
174 | * @return string |
||
175 | */ |
||
176 | final public function getPrefix() |
||
177 | { |
||
178 | return $this->prefix; |
||
179 | } |
||
180 | |||
181 | /** |
||
182 | * {@inheritDoc} |
||
183 | * This is an implemented method from the Loader interface. |
||
184 | * All other methods are actually implemented methods from the Adapter |
||
185 | * class. |
||
186 | */ |
||
187 | public function load(StateMachine $statemachine) |
||
188 | { |
||
189 | $data = $this->getLoaderData($statemachine->getContext()->getMachine()); |
||
190 | // delegate to LoaderArray |
||
191 | $loader = new LoaderArray($data); |
||
192 | $loader->load($statemachine); |
||
193 | } |
||
194 | |||
195 | /** |
||
196 | * {@inheritDoc} |
||
197 | */ |
||
198 | public function processGetState(Identifier $identifier) |
||
199 | { |
||
200 | $connection = $this->getConnection(); |
||
201 | $prefix = $this->getPrefix(); |
||
202 | try { |
||
203 | $query = 'SELECT state FROM ' . $prefix . 'statemachine_entities WHERE machine = ' . ':machine AND entity_id = :entity_id'; |
||
204 | $machine = $identifier->getMachine(); |
||
205 | $entity_id = $identifier->getEntityId(); |
||
206 | $statement = $connection->prepare($query); |
||
207 | $statement->bindParam(":machine", $machine); |
||
208 | $statement->bindParam(":entity_id", $entity_id); |
||
209 | $result = $statement->execute(); |
||
210 | if ($result === false) { |
||
211 | throw new Exception($this->getErrorInfo($statement)); |
||
212 | } |
||
213 | } catch(\Exception $e) { |
||
214 | throw new Exception(sprintf('query for getting current state failed: [%s]', $e->getMessage()), Exception::PERSISTENCE_LAYER_EXCEPTION); |
||
215 | } |
||
216 | $row = $statement->fetch(); |
||
217 | if ($row === false) { |
||
218 | throw new Exception(sprintf('no state found for [%s]. Did you "$machine->add()" it to the persistence layer?', $identifier->toString()), Exception::PERSISTENCE_LAYER_EXCEPTION); |
||
219 | return State::STATE_UNKNOWN; |
||
0 ignored issues
–
show
return \izzum\statemachine\State::STATE_UNKNOWN; does not seem to be reachable.
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed. Unreachable code is most often the result of function fx() {
try {
doSomething();
return true;
}
catch (\Exception $e) {
return false;
}
return false;
}
In the above example, the last ![]() |
|||
220 | } |
||
221 | return $row ['state']; |
||
222 | } |
||
223 | |||
224 | |||
225 | |||
226 | |||
227 | /** |
||
228 | * {@inheritDoc} |
||
229 | */ |
||
230 | public function getEntityIds($machine, $state = null) |
||
231 | { |
||
232 | $connection = $this->getConnection(); |
||
233 | $prefix = $this->getPrefix(); |
||
234 | $query = 'SELECT se.entity_id FROM ' . $prefix . 'statemachine_entities AS se |
||
235 | JOIN ' . $prefix . 'statemachine_states AS ss ON (se.state = ss.state AND |
||
236 | se.machine = ss.machine) WHERE se.machine = :machine'; |
||
237 | $output = array(); |
||
238 | try { |
||
239 | if ($state != null) { |
||
0 ignored issues
–
show
|
|||
240 | $query .= ' AND se.state = :state'; |
||
241 | } |
||
242 | $statement = $connection->prepare($query); |
||
243 | $statement->bindParam(":machine", $machine); |
||
244 | if ($state != null) { |
||
0 ignored issues
–
show
|
|||
245 | $statement->bindParam(":state", $state); |
||
246 | } |
||
247 | |||
248 | $result = $statement->execute(); |
||
249 | if ($result === false) { |
||
250 | throw new Exception($this->getErrorInfo($statement)); |
||
251 | } |
||
252 | |||
253 | $rows = $statement->fetchAll(); |
||
254 | if ($rows === false) { |
||
255 | throw new Exception("failed getting rows: " . $this->getErrorInfo($statement)); |
||
256 | } |
||
257 | |||
258 | foreach ($rows as $row) { |
||
259 | $output [] = $row ['entity_id']; |
||
260 | } |
||
261 | } catch(\Exception $e) { |
||
262 | throw new Exception($e->getMessage(), Exception::PERSISTENCE_LAYER_EXCEPTION, $e); |
||
263 | } |
||
264 | return $output; |
||
265 | } |
||
266 | |||
267 | /** |
||
268 | * {@inheritDoc} |
||
269 | */ |
||
270 | public function isPersisted(Identifier $identifier) |
||
271 | { |
||
272 | $connection = $this->getConnection(); |
||
273 | $prefix = $this->getPrefix(); |
||
274 | try { |
||
275 | $query = 'SELECT entity_id FROM ' . $prefix . 'statemachine_entities WHERE ' . 'machine = :machine AND entity_id = :entity_id'; |
||
276 | $statement = $connection->prepare($query); |
||
277 | $machine = $identifier->getMachine(); |
||
278 | $entity_id = $identifier->getEntityId(); |
||
279 | $statement->bindParam(":machine", $machine); |
||
280 | $statement->bindParam(":entity_id", $entity_id); |
||
281 | $result = $statement->execute(); |
||
282 | if ($result === false) { |
||
283 | throw new Exception($this->getErrorInfo($statement)); |
||
284 | } |
||
285 | |||
286 | $row = $statement->fetch(); |
||
287 | |||
288 | if ($row === false) { |
||
289 | return false; |
||
290 | } |
||
291 | return ($row ['entity_id'] == $identifier->getEntityId()); |
||
292 | } catch(\Exception $e) { |
||
293 | throw new Exception(sprintf('query for getting persistence info failed: [%s]', $e->getMessage()), Exception::PERSISTENCE_LAYER_EXCEPTION); |
||
294 | } |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * {@inheritDoc} |
||
299 | */ |
||
300 | protected function insertState(Identifier $identifier, $state, $message = null) |
||
301 | { |
||
302 | |||
303 | $connection = $this->getConnection(); |
||
304 | $prefix = $this->getPrefix(); |
||
305 | try { |
||
306 | $query = 'INSERT INTO ' . $prefix . 'statemachine_entities |
||
307 | (machine, entity_id, state, changetime) |
||
308 | VALUES |
||
309 | (:machine, :entity_id, :state, :timestamp)'; |
||
310 | $statement = $connection->prepare($query); |
||
311 | $machine = $identifier->getMachine(); |
||
312 | $entity_id = $identifier->getEntityId(); |
||
313 | $timestamp = $this->getTimestampForDriver(); |
||
314 | $statement->bindParam(":machine", $machine); |
||
315 | $statement->bindParam(":entity_id", $entity_id); |
||
316 | $statement->bindParam(":state", $state); |
||
317 | $statement->bindParam(":timestamp", $timestamp); |
||
318 | $result = $statement->execute(); |
||
319 | if ($result === false) { |
||
320 | throw new Exception($this->getErrorInfo($statement)); |
||
321 | } |
||
322 | } catch(\Exception $e) { |
||
323 | throw new Exception(sprintf('query for inserting state failed: [%s]', $e->getMessage()), Exception::PERSISTENCE_LAYER_EXCEPTION); |
||
324 | } |
||
325 | } |
||
326 | |||
327 | /** |
||
328 | * collects error information ready to be used as output. |
||
329 | * @param \PDOStatement $statement |
||
330 | * @return string |
||
331 | */ |
||
332 | protected function getErrorInfo(\PDOStatement $statement) |
||
333 | { |
||
334 | $info = $statement->errorInfo(); |
||
335 | $output = sprintf("%s - message: '%s'", $info [0], $info [2]); |
||
336 | return $output; |
||
337 | } |
||
338 | |||
339 | /** |
||
340 | * hook method |
||
341 | * |
||
342 | * @return number|string |
||
343 | */ |
||
344 | protected function getTimestampForDriver() |
||
345 | { |
||
346 | // yuk, seems postgres and sqlite need some different input. |
||
347 | // maybe other drivers too. so therefore this hook method. |
||
348 | if (strstr($this->dsn, 'sqlite:')) { |
||
349 | return time(); // "CURRENT_TIMESTAMP";//"DateTime('now')"; |
||
350 | } |
||
351 | // might have to be overriden for certain drivers. |
||
352 | return date("Y-m-d H:i:s", time()); // since prepared statements do not support now as binding parameter we need the time from php |
||
353 | } |
||
354 | |||
355 | /** |
||
356 | * hook method. |
||
357 | * not all drivers have the same boolean datatype. convert here. |
||
358 | * |
||
359 | * @param boolean $boolean |
||
360 | * @return boolean|int|string |
||
361 | */ |
||
362 | protected function getBooleanForDriver($boolean) |
||
363 | { |
||
364 | if (strstr($this->dsn, 'sqlite:') || strstr($this->dsn, 'mysql:')) { |
||
365 | return $boolean ? 1 : 0; |
||
366 | } |
||
367 | // might have to be overriden for certain drivers. |
||
368 | return $boolean; |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * {@inheritDoc} |
||
373 | */ |
||
374 | protected function updateState(Identifier $identifier, $state, $message = null) |
||
375 | { |
||
376 | |||
377 | $connection = $this->getConnection(); |
||
378 | $prefix = $this->getPrefix(); |
||
379 | try { |
||
380 | $query = 'UPDATE ' . $prefix . 'statemachine_entities SET state = :state, |
||
381 | changetime = :timestamp WHERE entity_id = :entity_id |
||
382 | AND machine = :machine'; |
||
383 | $statement = $connection->prepare($query); |
||
384 | $machine = $identifier->getMachine(); |
||
385 | $entity_id = $identifier->getEntityId(); |
||
386 | $timestamp = $this->getTimestampForDriver(); |
||
387 | $statement->bindParam(":machine", $machine); |
||
388 | $statement->bindParam(":entity_id", $entity_id); |
||
389 | $statement->bindParam(":state", $state); |
||
390 | $statement->bindParam(":timestamp", $timestamp); |
||
391 | $result = $statement->execute(); |
||
392 | if ($result === false) { |
||
393 | throw new Exception($this->getErrorInfo($statement)); |
||
394 | } |
||
395 | } catch(\Exception $e) { |
||
396 | throw new Exception(sprintf('query for updating state failed: [%s]', $e->getMessage()), Exception::PERSISTENCE_LAYER_EXCEPTION); |
||
397 | } |
||
398 | } |
||
399 | |||
400 | /** |
||
401 | *{@inheritDoc} |
||
402 | */ |
||
403 | public function addHistory(Identifier $identifier, $state, $message = null, $is_exception = false) |
||
404 | { |
||
405 | $connection = $this->getConnection(); |
||
406 | $prefix = $this->getPrefix(); |
||
407 | try { |
||
408 | $query = 'INSERT INTO ' . $prefix . 'statemachine_history |
||
409 | (machine, entity_id, state, message, changetime, exception) |
||
410 | VALUES |
||
411 | (:machine, :entity_id, :state, :message, :timestamp, :exception)'; |
||
412 | $statement = $connection->prepare($query); |
||
413 | $machine = $identifier->getMachine(); |
||
414 | $entity_id = $identifier->getEntityId(); |
||
415 | $timestamp = $this->getTimestampForDriver(); |
||
416 | $is_exception = $this->getBooleanForDriver($is_exception); |
||
417 | $statement->bindParam(":machine", $machine); |
||
418 | $statement->bindParam(":entity_id", $entity_id); |
||
419 | $statement->bindParam(":state", $state); |
||
420 | if($message) { |
||
421 | if(is_string($message)) { |
||
422 | $info = new \stdClass(); |
||
423 | $info->message = $message; |
||
424 | $message = $info; |
||
425 | } |
||
426 | //always json encode it so we can pass objects as the message and store it |
||
427 | $message = json_encode($message); |
||
428 | } |
||
429 | $statement->bindParam(":message", $message); |
||
430 | $statement->bindParam(":timestamp", $timestamp); |
||
431 | $statement->bindParam(":exception", $is_exception); |
||
432 | $result = $statement->execute(); |
||
433 | if ($result === false) { |
||
434 | throw new Exception($this->getErrorInfo($statement)); |
||
435 | } |
||
436 | } catch(\Exception $e) { |
||
437 | throw new Exception(sprintf('query for updating state failed: [%s]', $e->getMessage()), Exception::PERSISTENCE_LAYER_EXCEPTION); |
||
438 | } |
||
439 | } |
||
440 | |||
441 | /** |
||
442 | * get all the ordered transition and state information for a specific |
||
443 | * machine. |
||
444 | * This method is public for testing purposes |
||
445 | * |
||
446 | * @param string $machine |
||
447 | * @return [][] resultset from postgres |
||
0 ignored issues
–
show
The doc-type
[][] could not be parsed: Unknown type name "" at position 0. [(view supported doc-types)
This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types. ![]() |
|||
448 | * @throws Exception |
||
449 | */ |
||
450 | public function getTransitions($machine) |
||
451 | { |
||
452 | $connection = $this->getConnection(); |
||
453 | $prefix = $this->getPrefix(); |
||
454 | $query = 'SELECT st.machine, |
||
455 | st.state_from AS state_from, st.state_to AS state_to, |
||
456 | st.rule, st.command, |
||
457 | ss_to.type AS state_to_type, |
||
458 | ss_to.exit_command as state_to_exit_command, |
||
459 | ss_to.entry_command as state_to_entry_command, |
||
460 | ss.type AS state_from_type, |
||
461 | ss.exit_command as state_from_exit_command, |
||
462 | ss.entry_command as state_from_entry_command, |
||
463 | st.priority, |
||
464 | ss.description AS state_from_description, |
||
465 | ss_to.description AS state_to_description, |
||
466 | st.description AS transition_description, |
||
467 | st.event |
||
468 | FROM ' . $prefix . 'statemachine_transitions AS st |
||
469 | LEFT JOIN |
||
470 | ' . $prefix . 'statemachine_states AS ss |
||
471 | ON (st.state_from = ss.state AND st.machine = ss.machine) |
||
472 | LEFT JOIN |
||
473 | ' . $prefix . 'statemachine_states AS ss_to |
||
474 | ON (st.state_to = ss_to.state AND st.machine = ss_to.machine) |
||
475 | WHERE |
||
476 | st.machine = :machine |
||
477 | ORDER BY |
||
478 | st.state_from ASC, st.priority ASC, st.state_to ASC'; |
||
479 | try { |
||
480 | $statement = $connection->prepare($query); |
||
481 | $statement->bindParam(":machine", $machine); |
||
482 | $result = $statement->execute(); |
||
483 | |||
484 | if ($result === false) { |
||
485 | throw new Exception($this->getErrorInfo($statement)); |
||
486 | } |
||
487 | $rows = $statement->fetchAll(); |
||
488 | } catch(\Exception $e) { |
||
489 | throw new Exception($e->getMessage(), Exception::PERSISTENCE_LAYER_EXCEPTION, $e); |
||
490 | } |
||
491 | |||
492 | return $rows; |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * gets all data for transitions. |
||
497 | * This method is public for testing purposes |
||
498 | * |
||
499 | * @param string $machine |
||
500 | * the machine name |
||
501 | * @return Transition[] |
||
502 | */ |
||
503 | public function getLoaderData($machine) |
||
504 | { |
||
505 | $rows = $this->getTransitions($machine); |
||
506 | |||
507 | $output = array(); |
||
508 | // array for local caching of states |
||
509 | $states = array(); |
||
510 | |||
511 | foreach ($rows as $row) { |
||
512 | $state_from = $row ['state_from']; |
||
513 | $state_to = $row ['state_to']; |
||
514 | |||
515 | // create the 'from' state |
||
516 | if (isset($states [$state_from])) { |
||
517 | $from = $states [$state_from]; |
||
518 | } else { |
||
519 | $from = new State($row ['state_from'], $row ['state_from_type'], $row ['state_from_entry_command'], $row ['state_from_exit_command']); |
||
520 | $from->setDescription($row ['state_from_description']); |
||
521 | } |
||
522 | // cache the 'from' state for the next iterations |
||
523 | $states [$from->getName()] = $from; |
||
524 | |||
525 | // create the 'to' state |
||
526 | if (isset($states [$state_to])) { |
||
527 | $to = $states [$state_to]; |
||
528 | } else { |
||
529 | $to = new State($row ['state_to'], $row ['state_to_type'], $row ['state_to_entry_command'], $row ['state_to_exit_command']); |
||
530 | $to->setDescription($row ['state_to_description']); |
||
531 | } |
||
532 | // cache to 'to' state for the next iterations |
||
533 | $states [$to->getName()] = $to; |
||
534 | |||
535 | // build the transition |
||
536 | $transition = new Transition($from, $to, $row ['event'], $row ['rule'], $row ['command']); |
||
537 | $transition->setDescription($row ['transition_description']); |
||
538 | |||
539 | $output [] = $transition; |
||
540 | } |
||
541 | |||
542 | return $output; |
||
543 | } |
||
544 | |||
545 | /** |
||
546 | * do some cleanup |
||
547 | */ |
||
548 | public function __destruct() |
||
549 | { |
||
550 | $this->connection = null; |
||
551 | } |
||
552 | } |
||
553 |
This check looks from parameters that have been defined for a function or method, but which are not used in the method body.