├── .gitignore ├── src ├── Enum.php ├── Variable.php ├── Attribute.php ├── FragmentContainer.php ├── FragmentDefinitionContainer.php ├── Query.php └── Container.php ├── autoload.php ├── composer.json ├── index.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /src/Enum.php: -------------------------------------------------------------------------------- 1 | name; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Attribute.php: -------------------------------------------------------------------------------- 1 | name = $name; 11 | } 12 | 13 | public function __toString() { 14 | return $this->name; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | type = $type; 11 | } 12 | 13 | protected function renderSignature() { 14 | return '... on ' . $this->type; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rdx/graphql-query", 3 | "description": "Builds GraphQL queries", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Rudie Dirkx", 9 | "email": "github@hotblocks.nl" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "autoload": { 14 | "psr-4": { "rdx\\graphqlquery\\": "src" } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/FragmentDefinitionContainer.php: -------------------------------------------------------------------------------- 1 | name = $name; 11 | $this->type = $type; 12 | } 13 | 14 | /** 15 | * Returns fragment type 16 | * @return string 17 | */ 18 | public function getType() { 19 | return $this->type; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 'int']); 10 | $query->defineFragment('userStuff', 'User'); 11 | $query->userStuff->fields('id', 'name', 'path'); 12 | $query->fields('scope', 'friends', 'viewer'); 13 | $query->friends->attribute('names', ['marc', 'jeff']); 14 | $query->friends->fields(['id', 'name', 'smallpic' => 'picture', 'picture']); 15 | $query->friends->smallpic->attribute('size', Query::variable('smallPicSize', 'int')); 16 | $query->friends->picture->alias('bigpic')->attribute('size', 50); // Alias 'picture' to 'bigpic', and add attribute 17 | $query->viewer->fields('...userStuff', 'repos'); 18 | $query->viewer->repos 19 | ->attribute('public', true) 20 | ->attribute('limit', 10) 21 | ->attribute('order', ['field' => Query::enum('STARS'), 'direction' => Query::enum('DESC')]); 22 | $query->viewer->repos->fields('id', 'path'); 23 | $query->viewer->repos->fragment('PublicRepo')->field('stars', 'popularity'); 24 | $query->viewer->repos->fragment('PrivateRepo')->fields(['status', 'popularity' => 'permissions', 'members']); 25 | $query->viewer->repos->PrivateRepo->members->fields('...userStuff'); 26 | 27 | echo "====\n"; 28 | echo $query->build(); 29 | echo "====\n"; 30 | 31 | echo "\n\n"; 32 | 33 | $query = Query::mutation(); 34 | $query->field('moveProjectCard')->attribute('input', ['cardId' => 123, 'columnId' => 456]); 35 | $query->moveProjectCard->fields('clientMutationId'); 36 | 37 | echo "====\n"; 38 | echo $query->build(); 39 | echo "====\n"; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GraphQL query builder 2 | ==== 3 | 4 | Build a query: 5 | 6 | $query = Query::query('TestQueryWithEverything'); 7 | $query->defineFragment('userStuff', 'User'); 8 | $query->userStuff->fields('id', 'name', 'path'); 9 | $query->fields('scope', 'friends', 'viewer'); 10 | $query->friends->attribute('names', ['marc', 'jeff']); 11 | $query->friends->fields('id', 'name', 'picture'); 12 | $query->friends->picture->attribute('size', 50); 13 | $query->viewer->fields('...userStuff', 'repos'); 14 | $query->viewer->repos 15 | ->attribute('public', true) 16 | ->attribute('limit', 10) 17 | ->attribute('order', ['field' => Query::enum('STARS'), 'direction' => Query::enum('DESC')]); 18 | $query->viewer->repos->fields('id', 'path'); 19 | $query->viewer->repos->fragment('PublicRepo')->fields('stars'); 20 | $query->viewer->repos->fragment('PrivateRepo')->fields('status', 'permissions', 'members'); 21 | $query->viewer->repos->PrivateRepo->members->fields('...userStuff'); 22 | 23 | Render it: 24 | 25 | $string = $query->build(); 26 | 27 | Results in: 28 | 29 | query TestQueryWithEverything { 30 | scope 31 | friends(names: ["marc","jeff"]) { 32 | id 33 | name 34 | picture(size: 50) 35 | } 36 | viewer { 37 | ...userStuff 38 | repos(public: true, limit: 10, order: {field: STARS, direction: DESC}) { 39 | ... on PublicRepo { 40 | stars 41 | } 42 | ... on PrivateRepo { 43 | status 44 | permissions 45 | members { 46 | ...userStuff 47 | } 48 | } 49 | id 50 | path 51 | } 52 | } 53 | } 54 | 55 | fragment userStuff on User { 56 | id 57 | name 58 | path 59 | } 60 | 61 | Mutations 62 | ---- 63 | 64 | Since Mutations are practically the same as Queries, it has the same exact semantics: 65 | 66 | $query = Query::mutation(); 67 | $query->field('moveProjectCard')->attribute('input', ['cardId' => 123, 'columnId' => 456]); 68 | $query->moveProjectCard->fields('clientMutationId'); 69 | 70 | makes: 71 | 72 | mutation { 73 | moveProjectCard (input: {cardId: 123, columnId: 456}) { 74 | clientMutationId 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | type = $type; 21 | $this->name = $name; 22 | $this->variables = $variables; 23 | } 24 | 25 | static function query($name = null, $variables = []) { 26 | return new static($name, $variables, $type = self::TYPE_QUERY); 27 | } 28 | 29 | static function mutation($name = null, $variables = []) { 30 | return new static($name, $variables, $type = self::TYPE_MUTATION); 31 | } 32 | 33 | public function defineFragment($name, $type) { 34 | return $this->fragmentDefinitions[$name] = new FragmentDefinitionContainer($name, $type); 35 | } 36 | 37 | public function build() { 38 | return trim($this->buildQuery() . $this->buildFragmentDefinitions()) . "\n"; 39 | } 40 | 41 | protected function buildQuery() { 42 | $signature = $this->renderSignature(); 43 | return "$this->type $signature{\n".$this->render(1)."}\n\n"; 44 | } 45 | 46 | public function setType($type) { 47 | $this->type = $type; 48 | } 49 | 50 | protected function renderSignature() { 51 | $name = $this->getName(); 52 | $variables = $this->renderVariables(); 53 | return "$name$variables"; 54 | } 55 | 56 | protected function renderVariables() { 57 | if ($this->variables) { 58 | $variables = []; 59 | foreach ($this->variables as $name => $type) { 60 | $variables[] = '$' . $name . ': ' . ucfirst($type); 61 | } 62 | 63 | return '(' . implode(', ', $variables) . ') '; 64 | } 65 | 66 | return ''; 67 | } 68 | 69 | protected function getName() { 70 | if (!$this->name && $this->variables) { 71 | $this->name = 'CustomQuery'; 72 | } 73 | 74 | return $this->name ? "{$this->name} " : ''; 75 | } 76 | 77 | protected function buildFragmentDefinitions() { 78 | $output = ''; 79 | foreach ($this->fragmentDefinitions as $name => $container) { 80 | $type = $container->getType(); 81 | 82 | $output .= "fragment $name on $type {\n"; 83 | $output .= $container->render(1); 84 | $output .= "}\n\n"; 85 | } 86 | 87 | return $output; 88 | } 89 | 90 | /** 91 | * @param $name 92 | * @return Container 93 | */ 94 | public function __get($name) { 95 | return $this->getFragmentDefinition($name) ?: parent::get($name); 96 | } 97 | 98 | /** 99 | * @param $name 100 | * @return FragmentDefinitionContainer 101 | */ 102 | public function getFragmentDefinition($name) { 103 | return $this->fragmentDefinitions[$name] ?? null; 104 | } 105 | 106 | static public function enum($name) { 107 | return new Enum($name); 108 | } 109 | 110 | static public function variable($name) { 111 | return new Variable($name); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | field = $field; 22 | $this->alias = $alias; 23 | } 24 | 25 | public function id() { 26 | return $this->alias ?: $this->field; 27 | } 28 | 29 | public function alias($alias) { 30 | $this->alias = $alias; 31 | return $this; 32 | } 33 | 34 | public function attribute($name, $value) { 35 | $this->attributes[$name] = $value; 36 | return $this; 37 | } 38 | 39 | public function attributes($attributes) { 40 | foreach ($attributes as $name => $value) { 41 | $this->attribute($name, $value); 42 | } 43 | 44 | return $this; 45 | } 46 | 47 | public function field($field, $alias = '') { 48 | return $this->fields[] = new Container($field, $alias); 49 | } 50 | 51 | public function fields(...$names) { 52 | if (is_array($names[0])) { 53 | $names = $names[0]; 54 | } 55 | 56 | foreach ($names as $alias => $name) { 57 | is_int($alias) ? $this->field($name) : $this->field($name, $alias); 58 | } 59 | 60 | return $this; 61 | } 62 | 63 | public function fragment($type) { 64 | return $this->fragments[$type] = new FragmentContainer($type); 65 | } 66 | 67 | public function fragments(...$types) { 68 | foreach ($types as $type) { 69 | $this->fragment($type); 70 | } 71 | 72 | return $this; 73 | } 74 | 75 | public function render($depth) { 76 | $indent = $this->indent($depth); 77 | $output = ''; 78 | 79 | // Fragments 80 | foreach ($this->fragments as $container) { 81 | $output .= $indent . $container->renderSignature() . $container->renderChildren($depth) . "\n"; 82 | } 83 | 84 | // Fields 85 | foreach ($this->fields as $container) { 86 | $output .= $indent . $container->renderSignature() . $container->renderChildren($depth) . "\n"; 87 | } 88 | 89 | return $output; 90 | } 91 | 92 | protected function renderSignature() { 93 | $name = $this->alias ? "{$this->alias}: {$this->field}" : $this->field; 94 | return $name . $this->renderAttributes(); 95 | } 96 | 97 | protected function renderChildren($depth) { 98 | if ($this->fragments || $this->fields) { 99 | return 100 | " {\n" . 101 | $this->render($depth + 1) . 102 | $this->indent($depth) . "}"; 103 | } 104 | 105 | return ''; 106 | } 107 | 108 | protected function renderAttributes() { 109 | if ($this->attributes) { 110 | $attributes = $this->renderAttributeValues($this->attributes); 111 | return " ($attributes)"; 112 | } 113 | 114 | return ''; 115 | } 116 | 117 | protected function renderAttributeValues($attributes) { 118 | $components = []; 119 | foreach ($attributes as $name => $value) { 120 | $components[] = $name . ': ' . $this->renderAttributeValue($value); 121 | } 122 | 123 | return implode(', ', $components); 124 | } 125 | 126 | protected function renderAttributeArrayValues($attributes) { 127 | $components = []; 128 | foreach ($attributes as $value) { 129 | $components[] = $this->renderAttributeValue($value); 130 | } 131 | 132 | return implode(', ', $components); 133 | } 134 | 135 | 136 | protected function isAssociative(array $array) { 137 | if (array() === $array) return false; 138 | return array_keys($array) !== range(0, count($array) - 1); 139 | } 140 | 141 | protected function renderAttributeValue($value) { 142 | // Enums are unquoted strings 143 | if ($value instanceof Attribute) { 144 | return (string) $value; 145 | } 146 | 147 | if (is_array($value)) { 148 | if (!$this->isAssociative($value)) { 149 | return '[' . $this->renderAttributeArrayValues($value) . ']'; 150 | } 151 | 152 | // Object arrays get another round of recursion 153 | return '{' . $this->renderAttributeValues($value) . '}'; 154 | } 155 | 156 | // All the rest is scalar 157 | return json_encode($value, JSON_UNESCAPED_UNICODE); 158 | } 159 | 160 | protected function indent($depth) { 161 | return str_repeat(' ', $depth); 162 | } 163 | 164 | public function get($name) { 165 | return $this->getField($name) ?: $this->getFragment($name) ?: null; 166 | } 167 | 168 | public function getField($name) { 169 | foreach ($this->fields as $field) { 170 | if ($field->id() == $name) { 171 | return $field; 172 | } 173 | } 174 | } 175 | 176 | public function getFragment($name) { 177 | return $this->fragments[$name] ?? null; 178 | } 179 | 180 | /** 181 | * @param $name 182 | * @return Container 183 | */ 184 | public function __get($name) { 185 | return $this->get($name); 186 | } 187 | 188 | } 189 | --------------------------------------------------------------------------------