diff --git a/lib/MC/Google/Visualization.php b/lib/MC/Google/Visualization.php index 02a139e..fa269d7 100644 --- a/lib/MC/Google/Visualization.php +++ b/lib/MC/Google/Visualization.php @@ -22,7 +22,32 @@ * * @see \Tests\VisualizationTest * - * @phpstan-type FieldSpec array{callback?:callable,field?:string,extra?:array,fields?:string[],sort_field?:string,type?:string,join?:string} + * @phpstan-type FieldSpec array{ + * type: string, + * field?: string, + * fields?: string[], + * callback?: callable, + * extra?: mixed[], + * sort_field?: string, + * join?: string + * } + * @phpstan-type QueryParsed array{ + * select?: array, + * from?: string, + * where?: null|array, + * groupby?: string[], + * pivot?: string[], + * orderby?: string[], + * limit?: string, + * offset?: string, + * label?: string + * } + * @phpstan-type Entity array{ + * table: string, + * fields: array, + * joins: string[], + * where?: string + * } */ class Visualization { @@ -37,7 +62,7 @@ class Visualization /** * The entity schema that defines which tables are exposed to visualization clients, along with their fields, joins, and callbacks. * - * @var array + * @var array */ protected $entities = []; @@ -60,7 +85,7 @@ class Visualization /** * If a format string is not provided by the query, these will be used to format values by default. * - * @var array + * @var array */ protected $defaultFormat = [ 'date' => 'm/d/Y', @@ -156,8 +181,8 @@ public function setDefaultFormat(string $type, string $format): void * Handle the entire request, pulling the query from the $_GET variables and printing the results directly * if not specified otherwise. * - * @param bool $echo print response and set header - * @param null|array $queryParams query parameters + * @param bool $echo print response and set header + * @param null|array{tq:string, tqx:string, responseHandler?:string} $queryParams query parameters * * @return string the javascript response * @@ -199,14 +224,14 @@ public function handleRequest(bool $echo = true, ?array $queryParams = null): st /** * Handle a specific query. Use this if you want to gather the query parameters yourself instead of using handleRequest(). * - * @param string $query the visualization query to parse and execute - * @param array $params all extra params sent along with the query - must include at least "reqId" key + * @param string $query the visualization query to parse and execute + * @param array $params all extra params sent along with the query - must include at least "reqId" key * * @return string the javascript response */ public function handleQuery(string $query, array $params): string { - $reqId = null; + $reqId = -1; $response = ''; try { @@ -214,7 +239,7 @@ public function handleQuery(string $query, array $params): string throw new Visualization_Error('You must pass a PDO connection to the MC Google Visualization Server if you want to let the server handle the entire request'); } - $reqId = $params['reqId']; + $reqId = (int) $params['reqId']; $queryParsed = $this->parseQuery($query); $meta = $this->generateMetadata($queryParsed); $sql = $this->generateSQL($meta); @@ -236,13 +261,13 @@ public function handleQuery(string $query, array $params): string $response .= $this->getSuccessClose(); } catch (Visualization_Error $visualizationError) { - $response .= $this->handleError($reqId, $visualizationError->getMessage(), $params['responseHandler'], $visualizationError->type, $visualizationError->summary); + $response .= $this->handleError($reqId, $visualizationError->getMessage(), (string) $params['responseHandler'], $visualizationError->type, $visualizationError->summary); } catch (PDOException $pdoException) { - $response .= $this->handleError($reqId, $pdoException->getMessage(), $params['responseHandler'], 'invalid_query', 'Invalid Query - PDO exception'); + $response .= $this->handleError($reqId, $pdoException->getMessage(), (string) $params['responseHandler'], 'invalid_query', 'Invalid Query - PDO exception'); } catch (ParseError $parseError) { - $response .= $this->handleError($reqId, $parseError->getMessage(), $params['responseHandler'], 'invalid_query', 'Invalid Query - Parse Error'); + $response .= $this->handleError($reqId, $parseError->getMessage(), (string) $params['responseHandler'], 'invalid_query', 'Invalid Query - Parse Error'); } catch (Exception $exception) { - $response .= $this->handleError($reqId, $exception->getMessage(), $params['responseHandler']); + $response .= $this->handleError($reqId, $exception->getMessage(), (string) $params['responseHandler']); } return $response; @@ -251,9 +276,11 @@ public function handleQuery(string $query, array $params): string /** * Given the results of parseQuery(), introspect against the entity definitions provided and return the metadata array used to generate the SQL. * - * @param array $query the visualization query broken up into sections + * @param array $query the visualization query broken up into sections * - * @return array the metadata array from merging the query with the entity table definitions + * @phpstan-param QueryParsed $query + * + * @return array the metadata array from merging the query with the entity table definitions * * @throws Visualization_QueryError * @throws Visualization_Error @@ -704,7 +731,9 @@ public function getGrammar(): Def * * @param string $str the query string to parse * - * @return array the parsed query as an array, keyed by each part of the query (select, from, where, groupby, pivot, orderby, limit, offset, label, format, options + * @return array the parsed query as an array, keyed by each part of the query (select, from, where, groupby, pivot, orderby, limit, offset, label, format, options + * + * @phpstan-return QueryParsed * * @throws ParseError * @throws Visualization_QueryError @@ -727,8 +756,10 @@ public function parseQuery(string $str): array break; case 'from': - $vals = $token->getValues(); - $query['from'] = $vals[1]; + $from = $token->getValues(); + $from = $from[1]; + assert(null !== $from); + $query['from'] = $from; break; @@ -744,7 +775,7 @@ public function parseQuery(string $str): array $groupby = $token->getValues(); array_shift($groupby); array_shift($groupby); - $query['groupby'] = $groupby; + $query['groupby'] = array_filter($groupby); break; @@ -754,7 +785,7 @@ public function parseQuery(string $str): array } $pivot = $token->getValues(); array_shift($pivot); - $query['pivot'] = $pivot; + $query['pivot'] = array_filter($pivot); break; @@ -784,6 +815,7 @@ public function parseQuery(string $str): array case 'limit': $limit = $token->getValues(); $limit = $limit[1]; + assert(null !== $limit); $query['limit'] = $limit; break; @@ -791,6 +823,7 @@ public function parseQuery(string $str): array case 'offset': $offset = $token->getValues(); $offset = $offset[1]; + assert(null !== $offset); $query['offset'] = $offset; break; @@ -803,6 +836,7 @@ public function parseQuery(string $str): array $count = count($labels); for ($i = 0; $i < $count; $i += 2) { $field = $labels[$i]; + assert(null !== $labels[$i + 1]); $label = trim($labels[$i + 1], '\'"'); $queryLabels[$field] = $label; } @@ -818,6 +852,7 @@ public function parseQuery(string $str): array $count = count($formats); for ($i = 0; $i < $count; $i += 2) { $field = $formats[$i]; + assert(null !== $formats[$i + 1]); $queryFormats[$field] = trim($formats[$i + 1], '\'"'); } $query['formats'] = $queryFormats; @@ -1290,15 +1325,18 @@ protected function getFieldSQL(string $name, array $spec, bool $alias = false, s /** * Recursively process the dependant fields for callback entity fields. * - * @param array $field the spec array for the field to add (must have a "callback" key) - * @param array $entity the spec array for the entity to pull other fields from - * @param array $meta the query metadata array to append the results + * @param array $fieldSpec the spec array for the field to add (must have a "callback" key) + * @param array $entity the spec array for the entity to pull other fields from + * @param array $meta the query metadata array to append the results + * + * @phpstan-param FieldSpec $fieldSpec + * @phpstan-param Entity $entity * * @throws Visualization_Error */ - protected function addDependantCallbackFields(array $field, array $entity, array &$meta): void + protected function addDependantCallbackFields(array $fieldSpec, array $entity, array &$meta): void { - foreach ($field['fields'] as $dependant) { + foreach ($fieldSpec['fields'] ?? [] as $dependant) { if (!isset($entity['fields'][$dependant])) { throw new Visualization_Error('Unknown callback required field "'.$dependant.'"'); } @@ -1335,6 +1373,7 @@ protected function parseFieldTokens(Token $token, array &$fields = null): void if ($token->hasChildren()) { if ('function' === $token->name) { $field = $token->getValues(); + assert(null !== $field[0]); $field[0] = strtolower($field[0]); $fields[] = $field; } else { @@ -1350,8 +1389,8 @@ protected function parseFieldTokens(Token $token, array &$fields = null): void /** * Helper method for the query parser to recursively scan and flatten the where clause's conditions. * - * @param Token $token the token or token group to parse - * @param null|array $where the collector array of tokens that make up the where clause + * @param Token $token the token or token group to parse + * @param null|array $where the collector array of tokens that make up the where clause */ protected function parseWhereTokens(Token $token, array &$where = null): void { diff --git a/lib/MC/Parser.php b/lib/MC/Parser.php index 0282d93..291ad9b 100644 --- a/lib/MC/Parser.php +++ b/lib/MC/Parser.php @@ -32,17 +32,17 @@ class Parser /** * Return a Set with the function arguments as the subexpressions. */ - public function set(): Set + public function set(Def ...$args): Set { - return new Set(func_get_args()); + return new Set($args); } /** * Return a OneOf with the function arguments as the possible expressions. */ - public function oneOf(): OneOf + public function oneOf(Def ...$args): OneOf { - return new OneOf(func_get_args()); + return new OneOf($args); } /** diff --git a/lib/MC/Parser/Def.php b/lib/MC/Parser/Def.php index cd9f1b4..fe94492 100644 --- a/lib/MC/Parser/Def.php +++ b/lib/MC/Parser/Def.php @@ -28,7 +28,7 @@ public function parse(string $str): Token $str = ltrim($str); [$loc, $tok] = $this->parsePart($str, 0); - if ($loc !== strlen($str)) { + if ((null === $tok) || ($loc !== strlen($str))) { throw new ParseError('An error occurred: "'.substr($str, $loc).'"', $str, $loc); } @@ -38,7 +38,7 @@ public function parse(string $str): Token /** * Parse a string, cleaning up whitespace when we're done. * - * @return array A two-item array of the string location where parsing stopped, and the MC_Token instance that matches the grammar conditions + * @return array{int, null|Token} A two-item array of the string location where parsing stopped and the MC_Token instance that matches the grammar conditions * * @throws ParseError */ @@ -61,7 +61,7 @@ public function parsePart(string $str, int $loc): array * @param string $str the string to parse * @param int $loc the index to start parsing * - * @return array A two-item array of the string location where parsing stopped, and the MC_Token instance that matches the grammar conditions + * @return array{int, null|Token} A two-item array of the string location where parsing stopped, and the Token instance that matches the grammar conditions * * @throws ParseError */ @@ -103,11 +103,9 @@ public function suppress(): self /** * Return a token instance, copying over this Def's name and flagging suppression. * - * @param mixed $value - * * @return null|Token the new token, or null if the token should be suppressed */ - public function token($value): ?Token + public function token(?string $value): ?Token { if ($this->suppress) { return null; diff --git a/lib/MC/Parser/Def/OneOf.php b/lib/MC/Parser/Def/OneOf.php index 164bbc0..4e2c11b 100644 --- a/lib/MC/Parser/Def/OneOf.php +++ b/lib/MC/Parser/Def/OneOf.php @@ -15,6 +15,9 @@ class OneOf extends Def /** @var Def[] */ public $exprs = []; + /** + * @param Def[] $exprs + */ public function __construct(array $exprs = []) { $this->exprs = $exprs; diff --git a/lib/MC/Parser/Def/Set.php b/lib/MC/Parser/Def/Set.php index 9d8c840..6f1320f 100644 --- a/lib/MC/Parser/Def/Set.php +++ b/lib/MC/Parser/Def/Set.php @@ -11,11 +11,11 @@ */ class Set extends Def { - /** @var array */ + /** @var Def[] */ public $exprs = []; /** - * Set constructor. + * @param Def[] $exprs */ public function __construct(array $exprs = []) { @@ -31,7 +31,7 @@ public function _parse(string $str, int $loc): array $res = $this->tokenGroup(); foreach ($this->exprs as $expr) { [$loc, $toks] = $expr->parsePart($str, $loc); - if ((null !== $toks) && (!is_array($toks) || (count($toks) > 0))) { + if (null !== $toks) { $res->append($toks); } } diff --git a/lib/MC/Parser/Token.php b/lib/MC/Parser/Token.php index a199a96..d0ea72b 100644 --- a/lib/MC/Parser/Token.php +++ b/lib/MC/Parser/Token.php @@ -12,25 +12,29 @@ class Token /** @var null|string */ public $name; - /** @var mixed */ + /** @var null|string */ public $value; /** * Token constructor. - * - * @param mixed $value */ - public function __construct($value, ?string $name) + public function __construct(?string $value, ?string $name) { $this->value = $value; $this->name = $name; } + /** + * @return array + */ public function getValues(): array { return [$this->value]; } + /** + * @return array + */ public function getNameValues(): array { return [[$this->name, $this->value]]; diff --git a/lib/MC/Parser/Token/Group.php b/lib/MC/Parser/Token/Group.php index 2aa0fb3..341936d 100644 --- a/lib/MC/Parser/Token/Group.php +++ b/lib/MC/Parser/Token/Group.php @@ -14,10 +14,7 @@ class Group extends Token implements Countable /** @var Token[] */ public $subtoks = []; - /** - * @param null|string $name - */ - public function __construct($name) + public function __construct(?string $name) { parent::__construct(null, $name); } @@ -43,6 +40,9 @@ public function count(): int return count($this->subtoks); } + /** + * @return array + */ public function getValues(): array { $values = []; @@ -53,6 +53,9 @@ public function getValues(): array return $values; } + /** + * @return array + */ public function getNameValues(): array { $values = []; diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php index 1756644..d787d34 100644 --- a/tests/ExampleTest.php +++ b/tests/ExampleTest.php @@ -86,6 +86,30 @@ public function testQuerySimple(): void self::assertStringEqualsFile(__DIR__.'/result2.js', $output); } + /** + * @throws Visualization_Error + */ + public function testQueryError(): void + { + $db = new PDO('sqlite:'.__DIR__.'/../examples/example.db'); + $vis = new Visualization($db); + $vis->addEntity('countries', [ + 'fields' => [ + 'id' => ['field' => 'id', 'type' => 'number'], + 'name' => ['field' => 'name', 'type' => 'text'], + ], + ]); + + $parameters = [ + 'tq' => 'select id, name from', + 'tqx' => 'reqId:2', + ]; + + $output = $vis->handleRequest(false, $parameters); + + self::assertSame('google.visualization.Query.setResponse({version:"0.5",reqId:"2",status:"error",errors:[{reason:"invalid_query",message:"Invalid Query - Parse Error",detailed_message:"An error occurred: \"from\""}]});', $output); + } + /** * @throws Visualization_Error */ diff --git a/tests/VisualizationTest.php b/tests/VisualizationTest.php index a168214..493ba68 100644 --- a/tests/VisualizationTest.php +++ b/tests/VisualizationTest.php @@ -359,6 +359,9 @@ public function testIsNull(): void self::assertSame('SELECT id AS id FROM orders WHERE (product IS NOT NULL)', $sql); } + /** + * @param array $row + */ public static function callbackTest(array $row): string { return 'callback-'.$row['field'];