. * */ namespace Vvveb\Controller; use function \Vvveb\arrayKeysToCamelCase; use function \Vvveb\arrayKeysToUnderscore; use function \Vvveb\camelToUnderscore; use function \Vvveb\controller; use function \Vvveb\isController; use function \Vvveb\isModel; use function \Vvveb\model; use GraphQL\Error\DebugFlag; use GraphQL\GraphQL; use GraphQL\Language\Parser; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use GraphQL\Utils\AST; use GraphQL\Utils\BuildSchema; use Vvveb\System\Event; class PostConnection { public $nodes; public $PageInfo = []; } #[\AllowDynamicProperties] class Index extends Base { protected $stack = []; protected $queryIndex = -1; //protected $currentQuery = -1; protected $stackIndex = 0; protected $schema = []; protected $args = []; protected $returnIsList = false; protected $queries = []; protected $mutations = []; protected $types = []; protected $transforms = [ 'post' => 'Vvveb\Transform\Post', 'product' => 'Vvveb\Transform\Product', ]; function queries() { list($this->queries) = Event::trigger(__CLASS__, __FUNCTION__, $this->queries); } function mutations() { list($this->mutations) = Event::trigger(__CLASS__, __FUNCTION__, $this->mutations); } function types() { list($this->types) = Event::trigger(__CLASS__, __FUNCTION__, $this->types); } function transforms() { list($this->transforms) = Event::trigger(__CLASS__, __FUNCTION__, $this->transforms); } function init() { $this->types(); $this->queries(); $this->mutations(); $this->transforms(); return parent::init(); } function cascadeOptions() { } function model($typeName, $isConnection = false) { $page = 1; $limit = 10; $data = []; $typeName = camelToUnderscore($typeName, '_'); $options = $this->args + $this->global; if (! isModel($typeName, 'admin')) { return $data; } $type = model($typeName); if ($type) { $transform = null; if (isset($this->transforms[$typeName])) { $class = $this->transforms[$typeName]; $transform = new $class(); } if ($this->returnIsList || $isConnection) { $data = $type->getAll($options); $data = $data[$typeName] ?? []; if ($transform) { //error_log(print_r($options, 1)); $data = $transform->getAll($data, $options); } } else { $data = $type->get($options); if ($transform) { $data = $transform->get($data, $options); } } } else { throw new \Exception('Model does not exist!'); } if ($isConnection) { $pageInfo = [ 'count' => $return['count'] ?? 0, 'page' => $page, 'limit' => $limit, 'hasNextPage' => true, 'hasPreviousPage' => true, 'startCursor' => 1, ]; $data = ['nodes' => $data, 'pageInfo' => $pageInfo]; } return $data; } function controller($typeName, $args, $rootType = 'mutation') { //check if has method if (preg_match('/^(\w+)([A-Z]\w+)/', $typeName, $matches)) { $method = $matches[1]; $class = $matches[2]; } else { $method = 'index'; $class = $typeName; } $classFile = $rootType . '/' . $class; $className = $rootType . '\\' . $class; $data = []; if (isController($classFile)) { $controller = controller($className); if ($controller) { if (method_exists($controller, $method)) { $data = $controller->$method($args + $this->global); } else { throw new \Exception('Controller method does not exist!'); } } else { throw new \Exception('Controller can\'t be loaded!'); } } else { $class = strtolower(camelToUnderscore($class,'_')); if (isModel($class)) { $class = model($class); if ($class) { if (method_exists($class, $method)) { $data = $class->$method($args + ['site_id' => 1]); } else { throw new \Exception('Model method does not exist!'); } } else { throw new \Exception('Model can\'t be loaded!'); } } else { //throw new \Exception('Model does not exist!'); } } return $data; } function defaultFieldResolver($objectValue, $args, $context, ResolveInfo $info) { $fieldName = $info->fieldName; if (is_null($fieldName)) { throw new \Exception('Could not get $fieldName from ResolveInfo'); } if (is_null($info->parentType)) { throw new \Exception('Could not get $parentType from ResolveInfo'); } $parentTypeName = $info->parentType->name; //MutationType $typeName = $info->fieldDefinition->config['type']->name ?? ''; $returnType = ''; $this->returnIsList = false; $isConnection = false; $this->args = $args; if (is_array($objectValue)) { $this->args += $objectValue; } if (GRAPHQL_CAMELCASE && is_array($this->args)) { $this->args = arrayKeysToUnderscore($this->args, '_'); } if ($info->returnType) { //method_exists($info->returnType, 'getInnermostType') if (is_a($info->returnType, 'GraphQL\Type\Definition\ListOfType')) { $returnType = $info->returnType->getInnermostType()->name; $this->returnIsList = true; } else { $returnType = $info->returnType->name ?? ''; } } /* error_log('$returnType = ' . $returnType); error_log('$objectValue = ' . print_r($objectValue, 1)); error_log('$fieldName = ' . $fieldName); error_log('$parentTypeName = ' . $parentTypeName); error_log('$typeName = ' . $typeName); error_log('$args = ' . print_r($this->args, 1)); */ $method = ''; $type = ''; $permission = $fieldName; if (preg_match('/^([a-z]+)([A-Z]\w+)/', $fieldName, $matches)) { $method = $matches[1]; $type = strtolower(camelToUnderscore($matches[2],'_')); $permission = "$type/$method"; } $this->permission($permission); $data = []; //if previous controller or model call already has the data then don't try to retrive it again if (isset($this->stack[$this->queryIndex][$this->stackIndex - 1]['data'][$fieldName])) { $data = $this->stack[$this->queryIndex][$this->stackIndex - 1]['data'][$fieldName]; } //if object already has the data then don't try to retrive it again if (isset($objectValue[$fieldName])) { $data = $objectValue[$fieldName]; } if ($parentTypeName == 'RootQueryType') { $this->queryIndex++; if (isset($this->queries[$type])) { $fn = $this->queries[$type]; $data = $fn($parentTypeName, $fieldName, $this->args, $this->returnIsList, $isConnection); } else { $data = $this->controller($fieldName, $this->args, 'query'); } } else { if ($parentTypeName == 'MutationType') { $data = []; if (isset($this->mutations[$type])) { $fn = $this->mutations[$type]; $data = $fn($parentTypeName, $fieldName, $this->args, $this->returnIsList, $isConnection); } else { //crud //if (preg_match('/^(get|add|edit|delete)([A-Z]\w+)/', $typeName, $matches)) { $data = $this->controller($fieldName, $this->args, 'mutation'); } } } if (! $data) { foreach ([$typeName, $returnType] as $type) { if (substr_compare($type, 'Type', -4 ,4) === 0) { //$typeName = strtolower(preg_replace('/Type$/', '', $typeName)); $type = substr($type, 0, -4); if (isset($this->types[$type])) { $fn = $this->types[$type]; $data = $fn($parentTypeName, $typeName, $this->args, $this->returnIsList, $isConnection); } else { $data = $this->model($type); } break; } else { if (substr_compare($type, 'Connection', -10 ,10) === 0) { //$typeName = strtolower(preg_replace('/Type$/', '', $typeName)); $isConnection = true; $type = substr($type, 0, -10); if (isset($this->types[$type])) { $fn = $this->types[$type]; $data = $fn($parentTypeName, $typeName, $this->args, $this->returnIsList, $isConnection); } else { $data = $this->model($type, true); } break; } } } } if (GRAPHQL_CAMELCASE && is_array($data)) { $data = arrayKeysToCamelCase($data); } $this->stack[$this->queryIndex][$this->stackIndex++] = [ 'model' => $typeName, 'method' => $method, //'args' => $this->args, 'data' => $data, 'objectValue' => $objectValue, ]; if ($data) { if ($isConnection) { return $data; } else { return $data; } } $property = null; if (is_array($objectValue) || $objectValue instanceof ArrayAccess) { if (isset($objectValue[$fieldName])) { $property = $objectValue[$fieldName]; } } elseif (is_object($objectValue)) { if (isset($objectValue->{$fieldName})) { $property = $objectValue->{$fieldName}; } } //error_log(print_r($property, 1)); return $property instanceof Closure ? $property($objectValue, $args, $contextValue, $info) : $property; } function index() { $rawInput = file_get_contents('php://input'); $typeConfigDecorator = function (array $typeConfig, $typeDefinitionNode) : array { $name = $typeConfig['name']; //error_log($name . ' $typeConfigDecorator'); if ($name == 'ObjectType') { //error_log(print_r($typeDefinitionNode, 1)); /* $typeDefinitionNode = new ObjectType([ 'name' => 'PostConnection', 'fields' => [ 'nodes' => [ 'type' => Type::listOf('PostType'), 'resolve' => fn () => $data['nodes'] ], 'pageInfo' => [ 'type' => 'PageInfo', 'resolve' => fn () => $data['pageInfo'] ] ] ]); */ } // ... add missing options to $typeConfig based on type $name return $typeConfig; }; $cacheFilename = DIR_STORAGE . 'model' . DS . 'cached_schema.php'; if (! file_exists($cacheFilename)) { $schemaController = new Schema(); $schema = $schemaController->schema(); //file_get_contents(DIR_APP . 'schema.gql'); $document = Parser::parse($schema); file_put_contents($cacheFilename, "schema = BuildSchema::build($document, $typeConfigDecorator); $input = json_decode($rawInput, true); $query = $input['query'] ?? ''; $variableValues = $input['variables'] ?? null; try { $result = GraphQL::executeQuery($this->schema, $query, null, null, $variableValues, null, [$this, 'defaultFieldResolver'] /*'Vvveb\Controller\defaultFieldResolver'*/); $flags = 0; if (DEBUG) { $flags = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE; } $output = $result->toArray($flags); //error_log(print_r($this->stack, 1)); } catch (\Exception $e) { $message = $e->getMessage(); if (DEBUG) { $message .= ' - ' . $e->getFile(); $message .= ':' . $e->getLine(); $message .= "\n" . $e->getTraceAsString(); } $output = [ 'errors' => [ [ 'message' => $message, ], ], ]; } return $output; } }