Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
php: ['7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
name: PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v3
Expand All @@ -32,7 +32,7 @@ jobs:
# see https://github.com/shivammathur/setup-php
- uses: shivammathur/setup-php@v2
with:
php-version: 7.1
php-version: '7.1'
coverage: none
- run: composer update --no-progress --prefer-lowest
- run: composer phpstan
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/vendor/
/.idea/
/.*.cache
/tests-report-html/
/composer.lock
Expand Down
4 changes: 4 additions & 0 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@

$config = new Config();
return $config->setRules([
'@PSR12' => true,
'@PSR12:risky' => true,
'@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true,
'@PHP71Migration' => true,
'@PHP71Migration:risky' => true,
'array_syntax' => ['syntax' => 'short'],
'php_unit_test_class_requires_covers' => false,
'backtick_to_shell_exec' => true,
Expand Down
13 changes: 9 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,23 @@
},
"autoload": {
"psr-4": {
"MC\\": "lib/MC"
"MC\\": "lib/MC/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests"
"Tests\\": "tests/"
}
},
"scripts": {
"phpstan": "@php phpstan analyse -l max lib tests examples .php-cs-fixer.php",
"phpstan": "@php phpstan analyse -l max -c phpstan.neon lib tests examples .php-cs-fixer.php",
"php-cs-fixer": "@php php-cs-fixer fix --allow-risky=yes",
"php-cs-fixer-dry-run": "@php php-cs-fixer fix --dry-run --allow-risky=yes",
"phpunit": "@php phpunit"
"phpunit": "@php phpunit",
"test": [
"@php-cs-fixer-dry-run",
"@phpstan",
"@phpunit"
]
}
}
4 changes: 2 additions & 2 deletions examples/callback_fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function most_common(array $row): ?string
'pill' => ['field' => 'pill', 'type' => 'number'],
'iud' => ['field' => 'iud', 'type' => 'number'],
'condom' => ['field' => 'condom', 'type' => 'number'],
'sterile_total' => ['field' => 'steril_total', 'type' => 'number'],
'sterile_total' => ['field' => 'sterile_total', 'type' => 'number'],
'other_modern' => ['field' => 'other_modern', 'type' => 'number'],
'traditional' => ['field' => 'traditional', 'type' => 'number'],
'most_common' => [
Expand All @@ -54,7 +54,7 @@ function most_common(array $row): ?string
<html lang="en">
<head>
<title>Callback fields visualization example</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load('visualization', '1', {'packages': ['table']});
google.setOnLoadCallback(function() {
Expand Down
2 changes: 1 addition & 1 deletion examples/simple.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
exit;
}
?>
<html>
<html lang="en">
<head>
<title>Simple single-table visualization example</title>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
Expand Down
93 changes: 66 additions & 27 deletions lib/MC/Google/Visualization.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|string[]>,
* from?: string,
* where?: null|array<array{type: string, value: string}>,
* groupby?: string[],
* pivot?: string[],
* orderby?: string[],
* limit?: string,
* offset?: string,
* label?: string
* }
* @phpstan-type Entity array{
* table: string,
* fields: array<string, FieldSpec>,
* joins: string[],
* where?: string
* }
*/
class Visualization
{
Expand All @@ -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<string, Entity>
*/
protected $entities = [];

Expand All @@ -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<string, string>
*/
protected $defaultFormat = [
'date' => 'm/d/Y',
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -199,22 +224,22 @@ 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<string, float|int|string> $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 {
if (!$this->db instanceof PDO) {
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);
Expand All @@ -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;
Expand All @@ -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<string, mixed> $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<string, mixed> the metadata array from merging the query with the entity table definitions
*
* @throws Visualization_QueryError
* @throws Visualization_Error
Expand Down Expand Up @@ -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<string, mixed> 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
Expand All @@ -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;

Expand All @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -784,13 +815,15 @@ public function parseQuery(string $str): array
case 'limit':
$limit = $token->getValues();
$limit = $limit[1];
assert(null !== $limit);
$query['limit'] = $limit;

break;

case 'offset':
$offset = $token->getValues();
$offset = $offset[1];
assert(null !== $offset);
$query['offset'] = $offset;

break;
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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.'"');
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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<array{type: string, value: string}> $where the collector array of tokens that make up the where clause
*/
protected function parseWhereTokens(Token $token, array &$where = null): void
{
Expand Down
8 changes: 4 additions & 4 deletions lib/MC/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
10 changes: 4 additions & 6 deletions lib/MC/Parser/Def.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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
*/
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down
Loading