Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a GraphQL argument
*/
final readonly class GraphQLArgument
{
public function __construct(
public GraphQLFieldType $type,
public mixed $defaultValue = null,
public ?string $description = null
) {
}
public function toDefinition(string $name): string
{
$def = $name . ': ' . $this->type->toString();
if ($this->defaultValue !== null) {
$def .= ' = ' . json_encode($this->defaultValue);
}
return $def;
}
public function hasDefaultValue(): bool
{
return $this->defaultValue !== null;
}
}

View File

@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Executes GraphQL queries against a schema
*/
final readonly class GraphQLExecutor
{
public function __construct(
private GraphQLSchema $schema,
private GraphQLQueryParser $parser
) {
}
public function execute(
string $query,
array $variables = [],
mixed $context = null,
mixed $rootValue = null
): GraphQLResult {
try {
// Parse the query
$parsedQuery = $this->parser->parse($query);
// Execute based on operation type
$data = match ($parsedQuery->operationType) {
GraphQLOperationType::QUERY => $this->executeQuery($parsedQuery, $variables, $context, $rootValue),
GraphQLOperationType::MUTATION => $this->executeMutation($parsedQuery, $variables, $context, $rootValue),
GraphQLOperationType::SUBSCRIPTION => throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Subscriptions are not supported yet'
),
};
return new GraphQLResult(data: $data);
} catch (FrameworkException $e) {
return new GraphQLResult(
errors: [
[
'message' => $e->getMessage(),
'extensions' => [
'code' => $e->getErrorCode()->value,
],
],
]
);
} catch (\Throwable $e) {
return new GraphQLResult(
errors: [
[
'message' => 'Internal server error',
'extensions' => [
'code' => 'INTERNAL_ERROR',
],
],
]
);
}
}
private function executeQuery(
GraphQLParsedQuery $parsedQuery,
array $variables,
mixed $context,
mixed $rootValue
): array {
$result = [];
foreach ($parsedQuery->fields as $field) {
$queryField = $this->schema->getQuery($field->name);
if ($queryField === null) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
"Query field '{$field->name}' not found"
);
}
// Merge arguments with variables
$args = $this->resolveArguments($field->arguments, $variables);
// Execute resolver
$fieldResult = $queryField->resolve($rootValue, $args, $context);
// Handle sub-field selection if needed
if ($field->hasSubFields() && is_array($fieldResult)) {
$fieldResult = $this->selectSubFields($fieldResult, $field->subFields);
}
$result[$field->name] = $fieldResult;
}
return $result;
}
private function executeMutation(
GraphQLParsedQuery $parsedQuery,
array $variables,
mixed $context,
mixed $rootValue
): array {
$result = [];
foreach ($parsedQuery->fields as $field) {
$mutationField = $this->schema->getMutation($field->name);
if ($mutationField === null) {
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
"Mutation field '{$field->name}' not found"
);
}
// Merge arguments with variables
$args = $this->resolveArguments($field->arguments, $variables);
// Execute resolver
$fieldResult = $mutationField->resolve($rootValue, $args, $context);
// Handle sub-field selection if needed
if ($field->hasSubFields() && is_array($fieldResult)) {
$fieldResult = $this->selectSubFields($fieldResult, $field->subFields);
}
$result[$field->name] = $fieldResult;
}
return $result;
}
private function resolveArguments(array $fieldArgs, array $variables): array
{
$resolved = [];
foreach ($fieldArgs as $key => $value) {
// Handle variable references
if (is_string($value) && str_starts_with($value, '$')) {
$varName = substr($value, 1);
$resolved[$key] = $variables[$varName] ?? null;
} else {
$resolved[$key] = $value;
}
}
return $resolved;
}
private function selectSubFields(array $data, array $subFields): array
{
if (empty($subFields)) {
return $data;
}
// Check if it's a list of items
$isList = isset($data[0]) && is_array($data[0]);
if ($isList) {
return array_map(fn ($item) => $this->filterFields($item, $subFields), $data);
}
return $this->filterFields($data, $subFields);
}
private function filterFields(array $item, array $fields): array
{
$filtered = [];
foreach ($fields as $field) {
if (array_key_exists($field, $item)) {
$filtered[$field] = $item[$field];
}
}
return $filtered;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a GraphQL field with type and resolver
*/
final readonly class GraphQLField
{
/**
* @param array<string, GraphQLArgument> $arguments
*/
public function __construct(
public GraphQLFieldType $type,
public \Closure $resolver,
public array $arguments = [],
public ?string $description = null,
public bool $isDeprecated = false,
public ?string $deprecationReason = null
) {
}
public function toDefinition(string $name): string
{
$def = $name;
// Add arguments if any
if (! empty($this->arguments)) {
$args = [];
foreach ($this->arguments as $argName => $arg) {
$args[] = $arg->toDefinition($argName);
}
$def .= '(' . implode(', ', $args) . ')';
}
// Add return type
$def .= ': ' . $this->type->toString();
// Add deprecation if needed
if ($this->isDeprecated) {
$reason = $this->deprecationReason ?? 'No longer supported';
$def .= ' @deprecated(reason: "' . $reason . '")';
}
return $def;
}
public function resolve(mixed $root, array $args, mixed $context): mixed
{
return ($this->resolver)($root, $args, $context);
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents GraphQL field types (scalar and complex)
*/
final readonly class GraphQLFieldType
{
public function __construct(
public string $type,
public bool $isNullable = true,
public bool $isList = false,
public bool $isListNullable = true
) {
}
public function toString(): string
{
$typeStr = $this->type;
if ($this->isList) {
$typeStr = $this->isNullable ? $typeStr : $typeStr . '!';
$typeStr = '[' . $typeStr . ']';
$typeStr = $this->isListNullable ? $typeStr : $typeStr . '!';
} else {
$typeStr = $this->isNullable ? $typeStr : $typeStr . '!';
}
return $typeStr;
}
// Common scalar types
public static function string(bool $nullable = true): self
{
return new self('String', $nullable);
}
public static function int(bool $nullable = true): self
{
return new self('Int', $nullable);
}
public static function float(bool $nullable = true): self
{
return new self('Float', $nullable);
}
public static function boolean(bool $nullable = true): self
{
return new self('Boolean', $nullable);
}
public static function id(bool $nullable = false): self
{
return new self('ID', $nullable);
}
// List types
public static function listOf(string $type, bool $itemsNullable = true, bool $listNullable = true): self
{
return new self($type, $itemsNullable, true, $listNullable);
}
// Custom type
public static function custom(string $typeName, bool $nullable = true): self
{
return new self($typeName, $nullable);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* GraphQL operation types
*/
enum GraphQLOperationType: string
{
case QUERY = 'query';
case MUTATION = 'mutation';
case SUBSCRIPTION = 'subscription';
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a parsed field from a GraphQL query
*/
final readonly class GraphQLParsedField
{
/**
* @param array<string, mixed> $arguments
* @param string[] $subFields
*/
public function __construct(
public string $name,
public array $arguments = [],
public array $subFields = []
) {
}
public function hasArguments(): bool
{
return ! empty($this->arguments);
}
public function hasSubFields(): bool
{
return ! empty($this->subFields);
}
public function getArgumentCount(): int
{
return count($this->arguments);
}
public function getSubFieldCount(): int
{
return count($this->subFields);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a parsed GraphQL query
*/
final readonly class GraphQLParsedQuery
{
/**
* @param GraphQLParsedField[] $fields
* @param array<string, string> $variables
*/
public function __construct(
public GraphQLOperationType $operationType,
public array $fields,
public array $variables,
public string $rawQuery
) {
}
public function isQuery(): bool
{
return $this->operationType === GraphQLOperationType::QUERY;
}
public function isMutation(): bool
{
return $this->operationType === GraphQLOperationType::MUTATION;
}
public function isSubscription(): bool
{
return $this->operationType === GraphQLOperationType::SUBSCRIPTION;
}
public function getFieldCount(): int
{
return count($this->fields);
}
public function hasVariables(): bool
{
return ! empty($this->variables);
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
/**
* Simple GraphQL query parser for basic queries and mutations
*/
final class GraphQLQueryParser
{
public function parse(string $query): GraphQLParsedQuery
{
$query = trim($query);
// Extract operation type (query or mutation)
$operationType = $this->extractOperationType($query);
// Extract fields
$fields = $this->extractFields($query);
// Extract variables if present
$variables = $this->extractVariables($query);
return new GraphQLParsedQuery(
operationType: $operationType,
fields: $fields,
variables: $variables,
rawQuery: $query
);
}
private function extractOperationType(string $query): GraphQLOperationType
{
if (str_starts_with($query, 'query') || str_starts_with($query, '{')) {
return GraphQLOperationType::QUERY;
}
if (str_starts_with($query, 'mutation')) {
return GraphQLOperationType::MUTATION;
}
if (str_starts_with($query, 'subscription')) {
return GraphQLOperationType::SUBSCRIPTION;
}
throw FrameworkException::create(
ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
'Invalid GraphQL operation type'
);
}
private function extractFields(string $query): array
{
// Simple regex to extract fields from query
// This is a basic implementation - a real parser would be more sophisticated
preg_match('/{([^}]+)}/', $query, $matches);
if (empty($matches[1])) {
return [];
}
$fieldsStr = trim($matches[1]);
$fields = [];
// Parse each field (simplified)
$lines = explode("\n", $fieldsStr);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Extract field name and arguments
if (preg_match('/^(\w+)(?:\(([^)]*)\))?(?:\s*{([^}]+)})?/', $line, $fieldMatch)) {
$fieldName = $fieldMatch[1];
$arguments = $this->parseArguments($fieldMatch[2] ?? '');
$subFields = isset($fieldMatch[3]) ? $this->parseSubFields($fieldMatch[3]) : [];
$fields[] = new GraphQLParsedField(
name: $fieldName,
arguments: $arguments,
subFields: $subFields
);
}
}
return $fields;
}
private function parseArguments(string $argsStr): array
{
if (empty($argsStr)) {
return [];
}
$args = [];
$pairs = explode(',', $argsStr);
foreach ($pairs as $pair) {
$pair = trim($pair);
if (empty($pair)) {
continue;
}
[$key, $value] = explode(':', $pair, 2);
$key = trim($key);
$value = trim($value, ' "\'');
// Try to parse value as JSON for proper type conversion
$decodedValue = json_decode($value, true);
$args[$key] = $decodedValue !== null ? $decodedValue : $value;
}
return $args;
}
private function parseSubFields(string $subFieldsStr): array
{
$subFields = [];
$lines = explode("\n", trim($subFieldsStr));
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
$subFields[] = $line;
}
return $subFields;
}
private function extractVariables(string $query): array
{
// Extract variables from query definition
if (preg_match('/\(([^)]+)\)/', $query, $matches)) {
return $this->parseVariables($matches[1]);
}
return [];
}
private function parseVariables(string $variablesStr): array
{
$variables = [];
$pairs = explode(',', $variablesStr);
foreach ($pairs as $pair) {
$pair = trim($pair);
if (empty($pair) || ! str_starts_with($pair, '$')) {
continue;
}
// Parse variable definition like $id: ID!
if (preg_match('/\$(\w+):\s*(.+)/', $pair, $varMatch)) {
$variables[$varMatch[1]] = trim($varMatch[2]);
}
}
return $variables;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a GraphQL execution result
*/
final readonly class GraphQLResult
{
/**
* @param array<string, mixed>|null $data
* @param array<array{message: string, extensions?: array}>|null $errors
*/
public function __construct(
public ?array $data = null,
public ?array $errors = null
) {
}
public function hasErrors(): bool
{
return ! empty($this->errors);
}
public function hasData(): bool
{
return $this->data !== null;
}
public function isSuccess(): bool
{
return ! $this->hasErrors() && $this->hasData();
}
public function toArray(): array
{
$result = [];
if ($this->hasData()) {
$result['data'] = $this->data;
}
if ($this->hasErrors()) {
$result['errors'] = $this->errors;
}
return $result;
}
}

View File

@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a GraphQL schema with queries and mutations
*/
final class GraphQLSchema
{
/** @var array<string, GraphQLField> */
public private(set) array $queries = [];
/** @var array<string, GraphQLField> */
public private(set) array $mutations = [];
/** @var array<string, GraphQLType> */
public private(set) array $types = [];
public string $schemaDefinition {
get {
$schema = [];
// Add type definitions
foreach ($this->types as $name => $type) {
$schema[] = $type->toDefinition();
}
// Add Query type
if (! empty($this->queries)) {
$queryFields = [];
foreach ($this->queries as $name => $field) {
$queryFields[] = " " . $field->toDefinition($name);
}
$schema[] = "type Query {\n" . implode("\n", $queryFields) . "\n}";
}
// Add Mutation type
if (! empty($this->mutations)) {
$mutationFields = [];
foreach ($this->mutations as $name => $field) {
$mutationFields[] = " " . $field->toDefinition($name);
}
$schema[] = "type Mutation {\n" . implode("\n", $mutationFields) . "\n}";
}
return implode("\n\n", $schema);
}
}
public int $queryCount {
get => count($this->queries);
}
public int $mutationCount {
get => count($this->mutations);
}
public int $typeCount {
get => count($this->types);
}
public bool $hasQueries {
get => ! empty($this->queries);
}
public bool $hasMutations {
get => ! empty($this->mutations);
}
public function addQuery(string $name, GraphQLField $field): self
{
$this->queries[$name] = $field;
return $this;
}
public function addMutation(string $name, GraphQLField $field): self
{
$this->mutations[$name] = $field;
return $this;
}
public function addType(string $name, GraphQLType $type): self
{
$this->types[$name] = $type;
return $this;
}
public function getQuery(string $name): ?GraphQLField
{
return $this->queries[$name] ?? null;
}
public function getMutation(string $name): ?GraphQLField
{
return $this->mutations[$name] ?? null;
}
public function getType(string $name): ?GraphQLType
{
return $this->types[$name] ?? null;
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Framework\GraphQL;
/**
* Represents a GraphQL type definition
*/
final class GraphQLType
{
/** @var array<string, GraphQLField> */
public private(set) array $fields = [];
public function __construct(
public readonly string $name,
public readonly ?string $description = null,
public readonly bool $isInput = false
) {
}
public int $fieldCount {
get => count($this->fields);
}
public bool $hasFields {
get => ! empty($this->fields);
}
public function addField(string $name, GraphQLField $field): self
{
$this->fields[$name] = $field;
return $this;
}
public function getField(string $name): ?GraphQLField
{
return $this->fields[$name] ?? null;
}
public function toDefinition(): string
{
$keyword = $this->isInput ? 'input' : 'type';
$def = "{$keyword} {$this->name} {\n";
foreach ($this->fields as $fieldName => $field) {
$def .= " " . $field->toDefinition($fieldName) . "\n";
}
$def .= "}";
if ($this->description) {
$def = '"""' . "\n" . $this->description . "\n" . '"""' . "\n" . $def;
}
return $def;
}
}