Skip to content

Instantly share code, notes, and snippets.

@sasezaki
Last active January 1, 2025 09:51
Show Gist options
  • Save sasezaki/6c48c7ebcbc9c17a372e9d2680d2fc94 to your computer and use it in GitHub Desktop.
Save sasezaki/6c48c7ebcbc9c17a372e9d2680d2fc94 to your computer and use it in GitHub Desktop.
Big Query schema json to PHP's array-shape
<?php
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\Type;
use PHPStan\PhpDocParser\Printer\Printer;
require __DIR__ . '/vendor/autoload.php';
$schemaJson = <<<'JSON'
[
{
"mode": "NULLABLE",
"name": "logName",
"type": "STRING"
},
{
"fields" : [
{
"name": "first_name",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "product",
"type": "RECORD",
"mode": "NULLABLE",
"fields": [
{
"name": "id",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
{
"name": "first_name",
"type": "FLOAT",
"mode": "NULLABLE"
}
],
"mode": "NULLABLE",
"name": "jsonPayload",
"type": "RECORD"
}
]
JSON;
// class ContextTypeSpecifyingExtenion implements MethodTypeSpecifyingExtension
// {
// public function __construct(private ContextTypeProviderInterface $contextTypeProvider){}
// public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
// {
// return new SpecifiedTypes(['$context' => [$this->contextTypeProvider->getType()]]);
// }
// }
interface ContextTypeProviderInterface
{
public function getType() : Type\ArrayShapeNode;
}
final class ContextTypeFromBigQueryJsonSchemaFileProvider implements ContextTypeProviderInterface
{
private ?array $jsonPayloadFields;
public function __construct(
private string $schemaFile,
private BigQueryJsonSchemaJsonPayloadTypeConverterInterface $bigQueryJsonSchemaTypeConverter
)
{
}
public function getType() : Type\ArrayShapeNode
{
return $this->bigQueryJsonSchemaTypeConverter->toArrayShapeNode($this->getJsonPayloadFields());
}
private function getJsonPayloadFields() : array
{
if (!isset($this->jsonPayloadFields)) {
$schemaJson = file_get_contents($this->schemaFile);
$schema = json_decode($schemaJson, JSON_OBJECT_AS_ARRAY);
$jsonPayloadFields = null;
foreach($schema as $item) {
if ($item['name'] !== 'jsonPayload') {
continue;
}
$jsonPayloadFields = $item['fields'];
}
$this->jsonPayloadFields = $jsonPayloadFields;
}
return $this->jsonPayloadFields;
}
}
interface BigQueryJsonSchemaJsonPayloadTypeConverterInterface
{
public function toArrayShapeNode(array $jsonPayloadFields) : Type\ArrayShapeNode;
}
/**
*
* > The name and type fields are required. All other fields are optional.
* https://cloud.google.com/bigquery/docs/schemas?hl=en#creating_a_JSON_schema_file
*
* - type
* https://cloud.google.com/bigquery/docs/reference/rest/v2/tables?hl=en#TableFieldSchema
* > The field data type. Possible values include:
* STRING, BYTES, INTEGER (or INT64), FLOAT (or FLOAT64), ...., NUMERIC, BIGNUMERIC, JSON, RECORD (or STRUCT), RANGE
*
* - mode
* > Optional. The field mode. Possible values include NULLABLE, REQUIRED and REPEATED.
* The default value is NULLABLE.
*
* @phpstan-type schema_item = array{name: string, type: string, mode?: 'NULLABLE'|'REQUIRED'|'REPEATED'}
*/
final class StandardBigQueryJsonSchemaJsonPayloadTypeConverter implements BigQueryJsonSchemaJsonPayloadTypeConverterInterface
{
/**
* @param array{jsonPayload: list<schema_item>} $jsonPayloadFields
*/
public function toArrayShapeNode(array $jsonPayloadFields) : Type\ArrayShapeNode
{
return new Type\ArrayShapeNode(
array_map('StandardBigQueryJsonSchemaJsonPayloadTypeConverter::itemToNode', $jsonPayloadFields)
);
}
/**
* @param schema_item $item
*/
public static function itemToNode(array $item)
{
if ($item['type'] === 'RECORD' || $item['type'] === 'STRUCT') {
if ($item['mode'] === 'REPEATED') {
// todo...
}
return new Type\ArrayShapeItemNode(
new Type\IdentifierTypeNode($item['name']),
$item['mode'] === 'NULLABLE',
new Type\ArrayShapeNode(
array_map('StandardBigQueryJsonSchemaJsonPayloadTypeConverter::itemToNode', $item['fields'])
)
);
}
$optional = true;
if (isset($item['mode']) && $item['mode'] !== 'NULLABLE') {
$optional = false;
}
return new Type\ArrayShapeItemNode(
new Type\IdentifierTypeNode($item['name']),
$optional,
new Type\IdentifierTypeNode(self::convertTypeToPhpScalarType($item['type']))
);
}
/**
* @todo implement
*/
public static function convertTypeToPhpScalarType(string $type)
{
if (in_array($type, ['INTGER', 'INT64'])) {
return 'int';
}
// fallback to string
return 'string';
}
}
$contextTypeProvider = new ContextTypeFromBigQueryJsonSchemaFileProvider("data://,$schemaJson", new StandardBigQueryJsonSchemaJsonPayloadTypeConverter);
$schemaNode = $contextTypeProvider->getType();
// sample
$phpDocNode = new PhpDocNode([
new PhpDocTagNode(
'@param',
new ParamTagValueNode($schemaNode, false, '$context', 'desc')
),
]);
echo (new Printer())->print($phpDocNode);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment