448 lines
12 KiB
PHP
448 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Vvveb
|
|
*
|
|
* Copyright (C) 2022 Ziadin Givan
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
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, "<?php\nreturn " . var_export(AST::toArray($document), true) . ";\n");
|
|
} else {
|
|
$document = AST::fromArray(require $cacheFilename); // fromArray() is a lazy operation as well
|
|
}
|
|
|
|
//$typeConfigDecorator = function () {};
|
|
//$schema = BuildSchema::build($document);//, $typeConfigDecorator);
|
|
$this->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;
|
|
}
|
|
}
|