├── .gitignore ├── README.md ├── composer.json └── src ├── DependencyInjection ├── Configuration.php └── GraphQLMakerExtension.php ├── GraphQLMakerBundle.php ├── Maker ├── CustomMaker.php ├── MakeGraphQLConnection.php ├── MakeGraphQLMutation.php ├── MakeGraphQLQuery.php ├── MakeGraphQLResolver.php └── MakeGraphQLType.php ├── Resources ├── config │ └── makers.xml └── skeleton │ ├── Connection.types.tpl.php │ ├── Input.types.tpl.php │ ├── Mutation.fragment.tpl.php │ ├── Mutation.tpl.php │ ├── Mutation.yaml.tpl.php │ ├── Payload.types.tpl.php │ ├── Query.fragment.tpl.php │ ├── Query.yaml.tpl.php │ ├── Resolver.tpl.php │ ├── Type.fields.fragment.tpl.php │ └── Type.types.tpl.php └── Utils ├── Str.php └── Validator.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | phpunit.xml 3 | vendor/ 4 | .php_cs.cache 5 | /.idea 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Maker Bundle 2 | Bundle to easily create GraphQL types for [Overblog GraphQL Bundle](https://github.com/overblog/GraphQLBundle) by using the new [Symfony Maker component](https://github.com/symfony/maker-bundle) 3 | 4 | ## Installation 5 | 6 | ```bash 7 | $ composer require liinkiing/graphql-maker-bundle 8 | ``` 9 | 10 | If you use **Symfony flex**, it will be automatically register under the `bundles.php` file. 11 | Otherwise, register the bundle manually 12 | 13 | ```php 14 | // AppKernel.php 15 | class AppKernel extends Kernel 16 | { 17 | public function registerBundles() 18 | { 19 | $bundles = [ 20 | // ... 21 | new Liinkiing\GraphQLMakerBundle\GraphQLMakerBundle(), 22 | ]; 23 | 24 | // ... 25 | } 26 | } 27 | ``` 28 | 29 | ## Configuration 30 | By default, no configuration is needed. It uses **convention over configuration**, but if you wanna customize the behaviour, 31 | you can add a config file `config/packages/dev/graphql_maker.yaml` : 32 | 33 | ```yaml 34 | graphql_maker: 35 | root_namespace: App\GraphQL # Customize the root namespace where PHP mutations and resolver will be 36 | schemas: # You can also define, for any schemas if you use many, a custom out directory for types files 37 | public: 38 | out_dir: '%kernel.project_dir%/config/graphql/public/types' 39 | internal: 40 | out_dir: '%kernel.project_dir%/config/graphql/internal/types' 41 | preview: 42 | out_dir: '%kernel.project_dir%/config/graphql/preview/types' 43 | ``` 44 | 45 | ## Usage 46 | Currently, you can generate: 47 | - type 48 | - connection 49 | - query 50 | - mutation 51 | 52 | ```bash 53 | $ bin/console make:graphql:type [--schema] 54 | $ bin/console make:graphql:connection [--schema] 55 | $ bin/console make:graphql:query [--schema] 56 | $ bin/console make:graphql:mutation [--schema] 57 | $ bin/console make:graphql:resolver 58 | ``` 59 | 60 | Then, you will be asked some questions to generate what you asked, *à la Maker* 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Bundle to easily create GraphQL types for Overblog GraphQLBundle", 3 | "name": "liinkiing/graphql-maker-bundle", 4 | "type": "symfony-bundle", 5 | "license": "MIT", 6 | "keywords": ["generator", "code generator", "scaffolding", "scaffold", "graphql", "graphql types"], 7 | "authors": [ 8 | { 9 | "name": "Omar Jbara " 10 | } 11 | ], 12 | "config": { 13 | "preferred-install": "dist", 14 | "sort-packages": true 15 | }, 16 | "require": { 17 | "php": "^7.0.8", 18 | "symfony/config": "^3.4|^4.0|^5.0", 19 | "symfony/console": "^3.4|^4.0|^5.0", 20 | "symfony/dependency-injection": "^3.4|^4.0|^5.0", 21 | "symfony/maker-bundle": "^1.11.0", 22 | "symfony/http-kernel": "^3.4|^4.0|^5.0", 23 | "overblog/graphql-bundle": "^0.11.0|^0.12.0|^0.13.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { "Liinkiing\\GraphQLMakerBundle\\": "src/" } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | createRootNode($treeBuilder, self::NAME); 23 | $rootNode 24 | ->children() 25 | ->scalarNode('root_namespace') 26 | ->defaultValue('App\\GraphQL') 27 | ->validate() 28 | ->ifTrue(function (string $value) { 29 | return substr($value, -1) === '\\'; 30 | }) 31 | ->thenInvalid('%s does not seems to be a valid namespace') 32 | ->end() 33 | ->end(); 34 | 35 | $rootNode 36 | ->children() 37 | ->scalarNode('out_dir') 38 | ->defaultValue(self::DEFAULT_OUT_DIR) 39 | ->end() 40 | ->end(); 41 | 42 | $rootNode->append($this->schemasSection()); 43 | 44 | 45 | return $treeBuilder; 46 | } 47 | 48 | /** 49 | * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition 50 | */ 51 | private function schemasSection() 52 | { 53 | $treeBuilder = new TreeBuilder(self::SCHEMA_SECTION_NAME); 54 | $node = $this->createRootNode($treeBuilder, self::SCHEMA_SECTION_NAME); 55 | $node 56 | ->performNoDeepMerging() 57 | ->arrayPrototype() 58 | ->children() 59 | ->scalarNode('out_dir') 60 | ->defaultValue(self::DEFAULT_OUT_DIR) 61 | ; 62 | 63 | return $node; 64 | } 65 | 66 | /** 67 | * @param TreeBuilder $treeBuilder 68 | * @param string $type 69 | * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition|\Symfony\Component\Config\Definition\Builder\NodeDefinition 70 | */ 71 | private function createRootNode(TreeBuilder $treeBuilder, string $name, string $type = 'array') 72 | { 73 | if (method_exists($treeBuilder, 'getRootNode')) { 74 | $rootNode = $treeBuilder->getRootNode(); 75 | } else { 76 | // BC layer for symfony/config 4.1 and older 77 | $rootNode = $treeBuilder->root($name, $type); 78 | } 79 | 80 | return $rootNode; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DependencyInjection/GraphQLMakerExtension.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class GraphQLMakerExtension extends Extension 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function load(array $configs, ContainerBuilder $container): void 19 | { 20 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 21 | $loader->load('makers.xml'); 22 | 23 | $configuration = $this->getConfiguration($configs, $container); 24 | $config = $this->processConfiguration($configuration, $configs); 25 | 26 | foreach ($container->getDefinitions() as $definition) { 27 | if ($definition->hasTag('maker.graphql_command')) { 28 | $definition->replaceArgument( 29 | 0, 30 | // Make sure we have two trailing slashes, to prevent issues in YAML to FQCN conversion 31 | // for `resolve` value in generated queries and mutations 32 | str_replace('\\', '\\\\', $config['root_namespace']) 33 | ); 34 | $definition->replaceArgument( 35 | 1, 36 | $config['out_dir'] 37 | ); 38 | 39 | $definition->replaceArgument( 40 | 2, 41 | $config['schemas'] 42 | ); 43 | } 44 | } 45 | } 46 | 47 | public function getAlias(): string 48 | { 49 | return Configuration::NAME; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/GraphQLMakerBundle.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class GraphQLMakerBundle extends Bundle 13 | { 14 | 15 | public function getContainerExtension(): ?ExtensionInterface 16 | { 17 | if (!$this->extension instanceof ExtensionInterface) { 18 | $this->extension = new GraphQLMakerExtension(); 19 | } 20 | 21 | return $this->extension; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Maker/CustomMaker.php: -------------------------------------------------------------------------------- 1 | rootNamespace = $rootNamespace; 30 | $this->rootDir = $rootDir; 31 | $this->outdir = $outdir; 32 | $this->schemas = $schemas; 33 | } 34 | 35 | protected function printAvailableTypes(): void 36 | { 37 | $this->io->writeln('Available types (excluding custom scalars)'); 38 | foreach (self::AVAILABLE_FIELD_TYPES as $FIELD_TYPE) { 39 | $this->io->writeln(" - $FIELD_TYPE (or [$FIELD_TYPE])"); 40 | } 41 | $this->io->writeln(''); 42 | } 43 | 44 | protected function createNameBasedOnSchema(?string $schema, string $name): string 45 | { 46 | $result = $schema ? ucfirst($schema) : ''; 47 | $result .= ucfirst($name); 48 | 49 | return $result; 50 | } 51 | 52 | protected function getSchemaOutDir(?string $schema): ?string 53 | { 54 | if (!$schema || !isset($this->schemas[$schema])) { 55 | return null; 56 | } 57 | return $this->schemas[$schema]['out_dir']; 58 | } 59 | 60 | protected function askForNextField(bool $isFirstField): ?array 61 | { 62 | $this->io->writeln(''); 63 | 64 | if ($isFirstField) { 65 | $questionText = 'New field name (press to stop adding fields)'; 66 | } else { 67 | $questionText = 'Add another field? Enter the field name (or press to stop adding fields)'; 68 | } 69 | 70 | $name = $this->io->ask($questionText); 71 | 72 | if (!$name) { 73 | return null; 74 | } 75 | 76 | $defaultType = self::AVAILABLE_FIELD_TYPES[0]; 77 | $snakeCasedField = Str::asSnakeCase($name); 78 | 79 | if ('id' === substr($snakeCasedField, -2)) { 80 | $defaultType = self::AVAILABLE_FIELD_TYPES[4]; 81 | } elseif (0 === strpos($snakeCasedField, 'is_')) { 82 | $defaultType = self::AVAILABLE_FIELD_TYPES[3]; 83 | } elseif (0 === strpos($snakeCasedField, 'has_')) { 84 | $defaultType = self::AVAILABLE_FIELD_TYPES[3]; 85 | } 86 | 87 | [$type, $nullable] = $this->askFieldType($defaultType); 88 | $description = $this->askQuestion('Field description?', "I am the $name description"); 89 | 90 | return compact('name', 'type', 'description', 'nullable'); 91 | } 92 | 93 | /** 94 | * @param string $question 95 | * @param mixed|null $default 96 | * @param array|null $autocompleterValues 97 | * @param callable|null $validator 98 | * @return mixed 99 | */ 100 | protected function askQuestion(string $question, $default = null, ?array $autocompleterValues = null, ?callable $validator = null) 101 | { 102 | $question = new Question($question, $default); 103 | if ($validator) { 104 | $question->setValidator($validator); 105 | } 106 | if ($autocompleterValues) { 107 | $question->setAutocompleterValues($autocompleterValues); 108 | } 109 | return $this->io->askQuestion($question); 110 | } 111 | 112 | /** 113 | * @param string $question 114 | * @param mixed|null $default 115 | * @param callable|null $validator 116 | * @return mixed 117 | */ 118 | protected function askConfirmationQuestion(string $question, $default = true, ?callable $validator = null) 119 | { 120 | $question = new ConfirmationQuestion($question, $default); 121 | if ($validator) { 122 | $question->setValidator($validator); 123 | } 124 | return $this->io->askQuestion($question); 125 | } 126 | 127 | protected function writelnSpaced($message): void 128 | { 129 | $this->io->writeln(''); 130 | $this->io->writeln($message); 131 | $this->io->writeln(''); 132 | } 133 | 134 | public function parseTemplate(string $templatePath, array $parameters = []): string 135 | { 136 | ob_start(); 137 | extract($parameters, EXTR_SKIP); 138 | include $templatePath; 139 | 140 | return ob_get_clean(); 141 | } 142 | 143 | protected function askFieldType(string $defaultType): array 144 | { 145 | $type = null; 146 | $allValidTypes = self::AVAILABLE_FIELD_TYPES; 147 | while (null === $type) { 148 | $question = new Question('Field type (enter ? to see all types)', $defaultType); 149 | $question->setAutocompleterValues($allValidTypes); 150 | $type = str_replace('!', '', $this->io->askQuestion($question)); 151 | 152 | if ('?' === $type) { 153 | $this->printAvailableTypes(); 154 | $this->io->writeln(''); 155 | 156 | $type = null; 157 | } 158 | } 159 | $nullable = $this->io->confirm('Can this field be nullable', false); 160 | 161 | return [$type, $nullable]; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Maker/MakeGraphQLConnection.php: -------------------------------------------------------------------------------- 1 | schemaOutDir ?? $this->outdir) . DIRECTORY_SEPARATOR . $name . '.yaml'; 22 | } 23 | 24 | /** 25 | * Return the command name for your maker (e.g. make:report). 26 | * 27 | * @return string 28 | */ 29 | public static function getCommandName(): string 30 | { 31 | return 'make:graphql:connection'; 32 | } 33 | 34 | /** 35 | * Configure the command: set description, input arguments, options, etc. 36 | * 37 | * By default, all arguments will be asked interactively. If you want 38 | * to avoid that, use the $inputConfig->setArgumentAsNonInteractive() method. 39 | * 40 | * @param Command $command 41 | * @param InputConfiguration $inputConfig 42 | */ 43 | public function configureCommand(Command $command, InputConfiguration $inputConfig): void 44 | { 45 | $command 46 | ->setDescription('Creates a new relay-compliant GraphQL connection') 47 | ->addArgument('name', InputArgument::REQUIRED, sprintf('Choose a connection name (e.g. PostConnection)')) 48 | ->addOption('schema', 's', InputOption::VALUE_OPTIONAL, 'Specify your GraphQL schema (e.g internal, preview, public)') 49 | ; 50 | } 51 | 52 | /** 53 | * Configure any library dependencies that your maker requires. 54 | * 55 | * @param DependencyBuilder $dependencies 56 | */ 57 | public function configureDependencies(DependencyBuilder $dependencies): void 58 | { 59 | $dependencies 60 | ->addClassDependency(OverblogGraphQLBundle::class, 'overblog/graphql-bundle'); 61 | } 62 | 63 | /** 64 | * Called after normal code generation: allows you to do anything. 65 | * 66 | * @param InputInterface $input 67 | * @param ConsoleStyle $io 68 | * @param Generator $generator 69 | */ 70 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 71 | { 72 | $this->io = $io; 73 | $this->schemaOutDir = $this->getSchemaOutDir($input->getOption('schema')); 74 | $name = $this->createNameBasedOnSchema($this->schemaOutDir, $input->getArgument('name')); 75 | 76 | if (!Str::hasSuffix($name, 'Connection')) { 77 | $name .= 'Connection'; 78 | } 79 | if ($name) { 80 | $nodeType = $this->askQuestion( 81 | 'What is your connection node type? (the related type in which the connection refers to)' 82 | ); 83 | $nodeTypeNullable = $this->askConfirmationQuestion( 84 | 'Is your node type nullable?', false 85 | ); 86 | $hasFields = $this->askConfirmationQuestion( 87 | 'Do you want to add custom fields to your connection?', false 88 | ); 89 | 90 | $isFirstField = true; 91 | $fields = []; 92 | if ($hasFields) { 93 | while (true) { 94 | $newField = $this->askForNextField($isFirstField); 95 | $fields[] = $newField; 96 | $isFirstField = false; 97 | if (null === $newField) { 98 | $fields = array_filter($fields); 99 | break; 100 | } 101 | } 102 | } 103 | 104 | $generator->generateFile( 105 | $this->getTargetPath($name), 106 | __DIR__ . '/../Resources/skeleton/Connection.types.tpl.php', 107 | [ 108 | 'name' => $name, 109 | 'nodeType' => $nodeType, 110 | 'nodeTypeNullable' => $nodeTypeNullable, 111 | 'hasFields' => $hasFields, 112 | 'fields' => $fields 113 | ] 114 | ); 115 | 116 | $generator->writeChanges(); 117 | $this->writeSuccessMessage($io); 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/Maker/MakeGraphQLMutation.php: -------------------------------------------------------------------------------- 1 | schemaOutDir ??$this->outdir) . DIRECTORY_SEPARATOR . $this->filename; 39 | } 40 | 41 | private function getInputTargetPath(string $name): string 42 | { 43 | return ($this->schemaOutDir ??$this->outdir) . DIRECTORY_SEPARATOR . "$name.types.yaml"; 44 | } 45 | 46 | private function getPayloadTargetPath(string $name): string 47 | { 48 | return ($this->schemaOutDir ??$this->outdir) . DIRECTORY_SEPARATOR . "$name.types.yaml"; 49 | } 50 | 51 | 52 | /** 53 | * Return the command name for your maker (e.g. make:report). 54 | * 55 | * @return string 56 | */ 57 | public static function getCommandName(): string 58 | { 59 | return 'make:graphql:mutation'; 60 | } 61 | 62 | /** 63 | * Configure the command: set description, input arguments, options, etc. 64 | * 65 | * By default, all arguments will be asked interactively. If you want 66 | * to avoid that, use the $inputConfig->setArgumentAsNonInteractive() method. 67 | * 68 | * @param Command $command 69 | * @param InputConfiguration $inputConfig 70 | */ 71 | public function configureCommand(Command $command, InputConfiguration $inputConfig): void 72 | { 73 | $command 74 | ->setDescription('Creates a new GraphQL mutation') 75 | ->addArgument('name', InputArgument::REQUIRED, sprintf('Choose a mutation name (e.g. addPost)')) 76 | ->addOption('schema', 's', InputOption::VALUE_OPTIONAL, 'Specify your GraphQL schema (e.g internal, preview, public)') 77 | ; 78 | } 79 | 80 | /** 81 | * Configure any library dependencies that your maker requires. 82 | * 83 | * @param DependencyBuilder $dependencies 84 | */ 85 | public function configureDependencies(DependencyBuilder $dependencies): void 86 | { 87 | $dependencies 88 | ->addClassDependency(OverblogGraphQLBundle::class, 'overblog/graphql-bundle'); 89 | } 90 | 91 | /** 92 | * Called after normal code generation: allows you to do anything. 93 | * 94 | * @param InputInterface $input 95 | * @param ConsoleStyle $io 96 | * @param Generator $generator 97 | * @throws \Exception 98 | */ 99 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 100 | { 101 | $this->io = $io; 102 | $schema = $input->getOption('schema'); 103 | $this->schemaOutDir = $this->getSchemaOutDir($schema); 104 | 105 | if (!file_exists($this->getTargetPath())) { 106 | $this->filename = 'Mutation.types.yml'; 107 | if (!file_exists($this->getTargetPath())) { 108 | $this->filename = 'Mutation.types.yaml'; 109 | $this->firstTime = true; 110 | } 111 | } 112 | $mutationName = $input->getArgument('name'); 113 | 114 | if ($mutationName) { 115 | $description = $this->askQuestion( 116 | 'What is your mutation description?', 117 | "I am the $mutationName description" 118 | ); 119 | $hasAccess = $this->askConfirmationQuestion( 120 | 'Do you want to add custom access for this mutation?', false 121 | ); 122 | $access = null; 123 | if ($hasAccess) { 124 | $this->printAccessExpressionsExamples(); 125 | $access = $this->askQuestion( 126 | 'Please type your access expression', 127 | null, 128 | null, 129 | [Validator::class, 'notBlank'] 130 | ); 131 | } 132 | 133 | $content = $this->firstTime ? 134 | $this->parseTemplate($this->mutationYamlTemplatePath, compact('schema')) : 135 | file_get_contents($this->getTargetPath()); 136 | 137 | $inputName = $this->createNameBasedOnSchema($schema, $mutationName. 'Input'); 138 | $payloadName = $this->createNameBasedOnSchema($schema, $mutationName. 'Payload'); 139 | $rootNamespace = Str::normalizeNamespace($this->rootNamespace); 140 | 141 | $content .= $this->parseTemplate( 142 | $this->mutationTemplatePath, 143 | compact('access', 'hasAccess', 'rootNamespace', 'mutationName', 'description', 'inputName', 'payloadName') 144 | ); 145 | $generator->dumpFile( 146 | $this->getTargetPath(), 147 | $content 148 | ); 149 | 150 | $this->writelnSpaced("Now, let's configure $inputName!"); 151 | $this->generateMutationInput($generator, $inputName, $mutationName); 152 | 153 | $this->writelnSpaced("Now, let's configure $payloadName!"); 154 | $this->generateMutationPayload($generator, $payloadName, $mutationName); 155 | $fcn = $rootNamespace . "\\Mutation\\" . ucfirst($mutationName) . 'Mutation'; 156 | $generatePhpFiles = $this->askConfirmationQuestion( 157 | "Do you want to generate the PHP mutation $fcn" 158 | ); 159 | 160 | if ($generatePhpFiles) { 161 | $generator->generateClass( 162 | $fcn, 163 | $this->phpMutationTemplatePath 164 | ); 165 | } 166 | 167 | $generator->writeChanges(); 168 | $this->writeSuccessMessage($io); 169 | } 170 | 171 | } 172 | 173 | private function printAccessExpressionsExamples() 174 | { 175 | $this->io->writeln('Custom access expressions examples'); 176 | foreach (self::ACCESS_EXAMPLES as $ACCESS) { 177 | $this->io->writeln(" - $ACCESS"); 178 | } 179 | $this->io->writeln(''); 180 | $this->io->comment('More informations here: ' . self::EXPRESSION_LANGUAGE_DOC_URL . ''); 181 | } 182 | 183 | protected function generateMutationInput(Generator $generator, string $inputName, $name): void 184 | { 185 | $inputDescription = $this->askQuestion( 186 | "$inputName - What is your mutation input description?", 187 | "Input of $name mutation" 188 | ); 189 | 190 | $this->io->writeln("Now, let's add some fields to the $inputName"); 191 | 192 | $isFirstField = true; 193 | $inputFields = []; 194 | while (true) { 195 | $newField = $this->askForNextField($isFirstField); 196 | $inputFields[] = $newField; 197 | $isFirstField = false; 198 | if (null === $newField) { 199 | $inputFields = array_filter($inputFields); 200 | break; 201 | } 202 | } 203 | 204 | $generator->generateFile( 205 | $this->getInputTargetPath($inputName), 206 | $this->inputTemplatePath, 207 | compact('inputName', 'inputFields', 'inputDescription') 208 | ); 209 | } 210 | 211 | protected function generateMutationPayload(Generator $generator, string $payloadName, $name): void 212 | { 213 | $payloadDescription = $this->askQuestion( 214 | "$payloadName - What is your mutation payload description?", 215 | "Payload of $name mutation" 216 | ); 217 | 218 | $this->io->writeln("Now, let's add some fields to the $payloadName"); 219 | 220 | $isFirstField = true; 221 | $payloadFields = []; 222 | while (true) { 223 | $newField = $this->askForNextField($isFirstField); 224 | $payloadFields[] = $newField; 225 | $isFirstField = false; 226 | if (null === $newField) { 227 | $payloadFields = array_filter($payloadFields); 228 | break; 229 | } 230 | } 231 | 232 | $generator->generateFile( 233 | $this->getPayloadTargetPath($payloadName), 234 | $this->payloadTemplatePath, 235 | compact('payloadName', 'payloadFields', 'payloadDescription') 236 | ); 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /src/Maker/MakeGraphQLQuery.php: -------------------------------------------------------------------------------- 1 | schemaOutDir ?? $this->outdir) . DIRECTORY_SEPARATOR . $this->createNameBasedOnSchema($schema, $this->filename); 28 | } 29 | 30 | 31 | /** 32 | * Return the command name for your maker (e.g. make:report). 33 | * 34 | * @return string 35 | */ 36 | public static function getCommandName(): string 37 | { 38 | return 'make:graphql:query'; 39 | } 40 | 41 | /** 42 | * Configure the command: set description, input arguments, options, etc. 43 | * 44 | * By default, all arguments will be asked interactively. If you want 45 | * to avoid that, use the $inputConfig->setArgumentAsNonInteractive() method. 46 | * 47 | * @param Command $command 48 | * @param InputConfiguration $inputConfig 49 | */ 50 | public function configureCommand(Command $command, InputConfiguration $inputConfig): void 51 | { 52 | $command 53 | ->setDescription('Creates a new GraphQL query') 54 | ->addArgument('name', InputArgument::REQUIRED, sprintf('Choose a query name (e.g. allPosts)')) 55 | ->addOption('schema', 's', InputOption::VALUE_OPTIONAL, 'Specify your GraphQL schema (e.g internal, preview, public)') 56 | ; 57 | } 58 | 59 | /** 60 | * Configure any library dependencies that your maker requires. 61 | * 62 | * @param DependencyBuilder $dependencies 63 | */ 64 | public function configureDependencies(DependencyBuilder $dependencies): void 65 | { 66 | $dependencies 67 | ->addClassDependency(OverblogGraphQLBundle::class, 'overblog/graphql-bundle'); 68 | } 69 | 70 | /** 71 | * Called after normal code generation: allows you to do anything. 72 | * 73 | * @param InputInterface $input 74 | * @param ConsoleStyle $io 75 | * @param Generator $generator 76 | * @throws \Exception 77 | */ 78 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 79 | { 80 | $this->io = $io; 81 | $schema = $input->getOption('schema'); 82 | $this->schemaOutDir = $this->getSchemaOutDir($schema); 83 | $name = $input->getArgument('name'); 84 | 85 | if (!file_exists($this->getTargetPath($schema))) { 86 | $this->filename = 'Query.types.yml'; 87 | if (!file_exists($this->getTargetPath($schema))) { 88 | $this->filename = 'Query.types.yaml'; 89 | $this->firstTime = true; 90 | } 91 | } 92 | 93 | if ($name) { 94 | $description = $this->askQuestion( 95 | 'What is your query description?', 96 | "Get $name" 97 | ); 98 | [$type, $nullable] = $this->askFieldType(self::AVAILABLE_FIELD_TYPES[0]); 99 | 100 | $rootNamespace = Str::normalizeNamespace($this->rootNamespace); 101 | 102 | $fcn = Str::normalizeNamespace($this->rootNamespace."\\Resolver\\Query\\" . ucfirst($name) . 'Resolver'); 103 | $generatePhpFiles = $this->askConfirmationQuestion( 104 | "Do you want to generate the PHP resolver $fcn" 105 | ); 106 | 107 | $content = $this->firstTime ? 108 | $this->parseTemplate($this->yamlTemplatePath, compact('schema')) : 109 | file_get_contents($this->getTargetPath($schema)); 110 | 111 | $content .= $this->parseTemplate( 112 | $this->templatePath, 113 | compact('name', 'description', 'rootNamespace', 'type', 'nullable', 'generatePhpFiles') 114 | ); 115 | $generator->dumpFile( 116 | $this->getTargetPath($schema), 117 | $content 118 | ); 119 | 120 | 121 | if ($generatePhpFiles) { 122 | $generator->generateClass( 123 | $fcn, 124 | $this->phpResolverTemplatePath 125 | ); 126 | } 127 | 128 | $generator->writeChanges(); 129 | $this->writeSuccessMessage($io); 130 | } 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/Maker/MakeGraphQLResolver.php: -------------------------------------------------------------------------------- 1 | setArgumentAsNonInteractive() method. 36 | * 37 | * @param Command $command 38 | * @param InputConfiguration $inputConfig 39 | */ 40 | public function configureCommand(Command $command, InputConfiguration $inputConfig): void 41 | { 42 | $command 43 | ->setDescription('Creates a new GraphQL PHP resolver') 44 | ->addArgument('name', InputArgument::REQUIRED, sprintf('Choose a resolver name (e.g. UserPosts)')) 45 | ; 46 | } 47 | 48 | /** 49 | * Configure any library dependencies that your maker requires. 50 | * 51 | * @param DependencyBuilder $dependencies 52 | */ 53 | public function configureDependencies(DependencyBuilder $dependencies): void 54 | { 55 | $dependencies 56 | ->addClassDependency(OverblogGraphQLBundle::class, 'overblog/graphql-bundle'); 57 | } 58 | 59 | /** 60 | * Called after normal code generation: allows you to do anything. 61 | * 62 | * @param InputInterface $input 63 | * @param ConsoleStyle $io 64 | * @param Generator $generator 65 | * @throws \Exception 66 | */ 67 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 68 | { 69 | $this->io = $io; 70 | $name = $input->getArgument('name'); 71 | 72 | if ($name) { 73 | $name = str_replace('Resolver', '', $name); 74 | $fcn = Str::normalizeNamespace($this->rootNamespace."\\Resolver\\" . ucfirst($name) . 'Resolver'); 75 | $generator->generateClass( 76 | $fcn, 77 | $this->phpResolverTemplatePath 78 | ); 79 | 80 | $generator->writeChanges(); 81 | $this->writeSuccessMessage($io); 82 | } 83 | 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/Maker/MakeGraphQLType.php: -------------------------------------------------------------------------------- 1 | schemaOutDir ?? $this->outdir).DIRECTORY_SEPARATOR.$name; 34 | } 35 | 36 | /** 37 | * Configure the command: set description, input arguments, options, etc. 38 | * 39 | * By default, all arguments will be asked interactively. If you want 40 | * to avoid that, use the $inputConfig->setArgumentAsNonInteractive() method. 41 | * 42 | * @param Command $command 43 | * @param InputConfiguration $inputConfig 44 | */ 45 | public function configureCommand(Command $command, InputConfiguration $inputConfig): void 46 | { 47 | $command 48 | ->setDescription('Creates a new GraphQL type') 49 | ->addArgument('name', InputArgument::REQUIRED, sprintf('Choose a type name (e.g. Post)')) 50 | ->addOption('schema', 's', InputOption::VALUE_OPTIONAL, 'Specify your GraphQL schema (e.g internal, preview, public)') 51 | ; 52 | } 53 | 54 | /** 55 | * Configure any library dependencies that your maker requires. 56 | * 57 | * @param DependencyBuilder $dependencies 58 | */ 59 | public function configureDependencies(DependencyBuilder $dependencies): void 60 | { 61 | $dependencies 62 | ->addClassDependency(OverblogGraphQLBundle::class, 'overblog/graphql-bundle'); 63 | } 64 | 65 | /** 66 | * Called after normal code generation: allows you to do anything. 67 | * 68 | * @param InputInterface $input 69 | * @param ConsoleStyle $io 70 | * @param Generator $generator 71 | */ 72 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 73 | { 74 | $this->io = $io; 75 | $schema = $input->getOption('schema'); 76 | $this->schemaOutDir = $this->getSchemaOutDir($schema); 77 | $name = ucfirst($input->getArgument('name')); 78 | 79 | if ($name) { 80 | $firstTime = !file_exists($this->getTargetPath($name.'.types.yaml')) 81 | && !file_exists($this->getTargetPath($name.'.types.yml')); 82 | $filename = $schema ? ucfirst($schema) : ''; 83 | $filename .= "$name.types.yaml"; 84 | if (file_exists($this->getTargetPath($name.'.types.yml'))) { 85 | $filename = "$name.types.yml"; 86 | } 87 | if ($firstTime) { 88 | $type = $this->askQuestion( 89 | 'What is your object type? (e.g. object, interface, enum)', 90 | 'object', 91 | self::AVAILABLE_OBJECT_TYPES 92 | ); 93 | $description = $this->askQuestion( 94 | 'What is your type description?', 95 | "I am the $name description!" 96 | ); 97 | $inherits = $this->askQuestion( 98 | 'Does your type inherits any other types? (e.g. Comment, Video or leave it blank for none)' 99 | ); 100 | $interfaces = $this->askQuestion( 101 | 'Does your type have any interfaces? (e.g. Node, Commentable or leave it blank for none)' 102 | ); 103 | 104 | $hasFields = $this->askConfirmationQuestion('Do you want to add fields?'); 105 | } else { 106 | $this->io->writeln("The $name type already exists! Let's add some fields."); 107 | $hasFields = true; 108 | } 109 | 110 | $isFirstField = true; 111 | $fields = []; 112 | if ($hasFields) { 113 | while (true) { 114 | $newField = $this->askForNextField($isFirstField); 115 | $fields[] = $newField; 116 | $isFirstField = false; 117 | if (null === $newField) { 118 | $fields = array_filter($fields); 119 | break; 120 | } 121 | } 122 | } 123 | 124 | $content = $firstTime ? 125 | $this->parseTemplate( 126 | $this->typeTemplatePath, 127 | [ 128 | 'schema' => $schema, 129 | 'name' => $name, 130 | 'type' => $type, 131 | 'description' => $description, 132 | 'hasFields' => $hasFields, 133 | 'fields' => $fields, 134 | 'inherits' => array_filter( 135 | array_map('trim', explode(', ', $inherits ?? '')), 136 | function ($item) { return $item !== ''; }), 137 | 'interfaces' => array_filter( 138 | array_map('trim', explode(', ', $interfaces ?? '')), 139 | function ($item) { return $item !== ''; }) ] 140 | 141 | ) : 142 | file_get_contents($this->getTargetPath($filename)); 143 | 144 | $content .= $this->parseTemplate( 145 | $this->typeFieldsTemplatePath, 146 | compact('fields') 147 | ); 148 | 149 | $generator->dumpFile( 150 | $this->getTargetPath($filename), 151 | $content 152 | ); 153 | 154 | $generator->writeChanges(); 155 | $this->writeSuccessMessage($io); 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/Resources/config/makers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | %kernel.project_dir% 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | %kernel.project_dir% 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | %kernel.project_dir% 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | %kernel.project_dir% 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | %kernel.project_dir% 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Connection.types.tpl.php: -------------------------------------------------------------------------------- 1 | : 2 | type: relay-connection 3 | config: 4 | 5 | nodeType: '' 6 | 7 | connectionFields: 8 | totalCount: 9 | type: 'Int!' 10 | description: 'Return the number of items of the connection' 11 | 0) { ?> 12 | 13 | : 14 | type: '' 15 | description: 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Input.types.tpl.php: -------------------------------------------------------------------------------- 1 | : 2 | type: relay-mutation-input 3 | config: 4 | 5 | description: 6 | 7 | 0) { ?> 8 | fields: 9 | 10 | : 11 | type: '' 12 | description: 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Mutation.fragment.tpl.php: -------------------------------------------------------------------------------- 1 | : 2 | description: '' 3 | 4 | access: "" 5 | 6 | builder: 'Relay::Mutation' 7 | builderConfig: 8 | inputType: 9 | payloadType: 10 | mutateAndGetPayload: '@=mutation("\\Mutation\\", [value])' 11 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Mutation.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | use Overblog\GraphQLBundle\Definition\Argument; 6 | use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface; 7 | 8 | class implements MutationInterface 9 | { 10 | 11 | // public function __construct() 12 | // { 13 | // If you wanna inject some service dependencies, do it here 14 | // } 15 | 16 | public function __invoke(Argument $args) 17 | { 18 | // Add your logic when the mutation is called here (e.g database calls and updates). 19 | // $args is generally an object passed in your Mutation.types.yaml and contains arguments of your mutation. 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Mutation.yaml.tpl.php: -------------------------------------------------------------------------------- 1 | Mutation: 2 | type: object 3 | config: 4 | name: Mutation 5 | fields: 6 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Payload.types.tpl.php: -------------------------------------------------------------------------------- 1 | : 2 | type: relay-mutation-payload 3 | config: 4 | 5 | description: 6 | 7 | 0) { ?> 8 | fields: 9 | 10 | : 11 | type: '' 12 | description: 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Query.fragment.tpl.php: -------------------------------------------------------------------------------- 1 | : 2 | description: '' 3 | 4 | type: '' 5 | 6 | #If you want to use a Relay connection 7 | #argsBuilder: Relay::Connection 8 | resolve: '@=resolver("\\Resolver\\Query\\", [args])' 9 | #args: 10 | #Put here your custom args for you query (if any) 11 | #first: 12 | #type: Int 13 | #defaultValue: 30 14 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Query.yaml.tpl.php: -------------------------------------------------------------------------------- 1 | Query: 2 | type: object 3 | config: 4 | name: Query 5 | fields: 6 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Resolver.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace ; 4 | 5 | use Overblog\GraphQLBundle\Definition\Argument; 6 | use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; 7 | 8 | class implements ResolverInterface 9 | { 10 | 11 | // public function __construct() 12 | // { 13 | // If you wanna inject some service dependencies, do it here 14 | // } 15 | 16 | public function __invoke(Argument $args) 17 | { 18 | // Add your logic on how to fetch your data (e.g database calls). 19 | // $args can be an object passed in your Query.types.yaml and contains arguments of your query. 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Type.fields.fragment.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | : 3 | type: '' 4 | description: 5 | 6 | -------------------------------------------------------------------------------- /src/Resources/skeleton/Type.types.tpl.php: -------------------------------------------------------------------------------- 1 | : 2 | type: 3 | 0) { ?> 4 | inherits: [] 5 | 6 | config: 7 | name: 8 | 0) { ?> 9 | interfaces: [] 10 | 11 | 12 | description: 13 | 14 | 15 | fields: 16 | id: 17 | type: 'ID!' 18 | description: 'The id' 19 | 20 | 21 | fields: 22 | 23 | -------------------------------------------------------------------------------- /src/Utils/Str.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Liinkiing\GraphQLMakerBundle\Utils; 13 | 14 | use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException; 15 | use Symfony\Bundle\MakerBundle\Str; 16 | 17 | /** 18 | * @author Javier Eguiluz 19 | * @author Ryan Weaver 20 | * 21 | * @internal 22 | */ 23 | final class Validator 24 | { 25 | public static function validateClassName(string $className, string $errorMessage = ''): string 26 | { 27 | // remove potential opening slash so we don't match on it 28 | $pieces = explode('\\', ltrim($className, '\\')); 29 | $shortClassName = Str::getShortClassName($className); 30 | 31 | $reservedKeywords = ['__halt_compiler', 'abstract', 'and', 'array', 32 | 'as', 'break', 'callable', 'case', 'catch', 'class', 33 | 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 34 | 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 35 | 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 36 | 'exit', 'extends', 'final', 'for', 'foreach', 'function', 37 | 'global', 'goto', 'if', 'implements', 'include', 38 | 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 39 | 'list', 'namespace', 'new', 'or', 'print', 'private', 40 | 'protected', 'public', 'require', 'require_once', 'return', 41 | 'static', 'switch', 'throw', 'trait', 'try', 'unset', 42 | 'use', 'var', 'while', 'xor', 43 | 'int', 'float', 'bool', 'string', 'true', 'false', 'null', 'void', 44 | 'iterable', 'object', '__file__', '__line__', '__dir__', '__function__', '__class__', 45 | '__method__', '__namespace__', '__trait__', 'self', 'parent', 46 | ]; 47 | 48 | foreach ($pieces as $piece) { 49 | if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $piece)) { 50 | $errorMessage = $errorMessage ?: sprintf('"%s" is not valid as a PHP class name (it must start with a letter or underscore, followed by any number of letters, numbers, or underscores)', $className); 51 | 52 | throw new RuntimeCommandException($errorMessage); 53 | } 54 | 55 | if (\in_array(strtolower($shortClassName), $reservedKeywords, true)) { 56 | throw new RuntimeCommandException(sprintf('"%s" is a reserved keyword and thus cannot be used as class name in PHP.', $shortClassName)); 57 | } 58 | } 59 | 60 | // return original class name 61 | return $className; 62 | } 63 | 64 | public static function notBlank(string $value = null): string 65 | { 66 | if (null === $value || '' === $value) { 67 | throw new RuntimeCommandException('This value cannot be blank'); 68 | } 69 | 70 | return $value; 71 | } 72 | 73 | public static function validateLength($length) 74 | { 75 | if (!$length) { 76 | return $length; 77 | } 78 | 79 | $result = filter_var($length, FILTER_VALIDATE_INT, [ 80 | 'options' => ['min_range' => 1], 81 | ]); 82 | 83 | if (false === $result) { 84 | throw new RuntimeCommandException(sprintf('Invalid length "%s".', $length)); 85 | } 86 | 87 | return $result; 88 | } 89 | 90 | public static function validatePrecision($precision) 91 | { 92 | if (!$precision) { 93 | return $precision; 94 | } 95 | 96 | $result = filter_var($precision, FILTER_VALIDATE_INT, [ 97 | 'options' => ['min_range' => 1, 'max_range' => 65], 98 | ]); 99 | 100 | if (false === $result) { 101 | throw new RuntimeCommandException(sprintf('Invalid precision "%s".', $precision)); 102 | } 103 | 104 | return $result; 105 | } 106 | 107 | public static function validateScale($scale) 108 | { 109 | if (!$scale) { 110 | return $scale; 111 | } 112 | 113 | $result = filter_var($scale, FILTER_VALIDATE_INT, [ 114 | 'options' => ['min_range' => 0, 'max_range' => 30], 115 | ]); 116 | 117 | if (false === $result) { 118 | throw new RuntimeCommandException(sprintf('Invalid scale "%s".', $scale)); 119 | } 120 | 121 | return $result; 122 | } 123 | 124 | public static function validateBoolean($value) 125 | { 126 | if ('yes' === $value) { 127 | return true; 128 | } 129 | 130 | if ('no' === $value) { 131 | return false; 132 | } 133 | 134 | if (null === $valueAsBool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) { 135 | throw new RuntimeCommandException(sprintf('Invalid bool value "%s".', $value)); 136 | } 137 | 138 | return $valueAsBool; 139 | } 140 | 141 | public static function classExists(string $className, string $errorMessage = ''): string 142 | { 143 | self::notBlank($className); 144 | 145 | if (!class_exists($className)) { 146 | $errorMessage = $errorMessage ?: sprintf('Class "%s" doesn\'t exists. Please enter existing full class name', $className); 147 | 148 | throw new RuntimeCommandException($errorMessage); 149 | } 150 | 151 | return $className; 152 | } 153 | 154 | public static function classDoesNotExist($className): string 155 | { 156 | self::notBlank($className); 157 | 158 | if (class_exists($className)) { 159 | throw new RuntimeCommandException(sprintf('Class "%s" already exists', $className)); 160 | } 161 | 162 | return $className; 163 | } 164 | 165 | } 166 | --------------------------------------------------------------------------------