diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 94e9a36..1882ae9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,7 +12,7 @@ jobs: strategy: matrix: install-args: [''] - php-version: ['8.1'] + php-version: ['8.2'] fail-fast: false steps: # Cancel previous runs of the same branch diff --git a/Tests/Fixtures/Entities/BadClass.php b/Tests/Fixtures/Entities/BadClass.php deleted file mode 100644 index 7c89188..0000000 --- a/Tests/Fixtures/Entities/BadClass.php +++ /dev/null @@ -1,8 +0,0 @@ -<?php - - -// The namespace for this class is broken. It must not impact Symfony. -class BadClass -{ - -} diff --git a/composer.json b/composer.json index 7491415..1ad1550 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,13 @@ "require" : { "php" : ">=8.1", "ext-json": "*", - "thecodingmachine/graphqlite" : "^6.0", - "thecodingmachine/graphqlite-symfony-validator-bridge" : "^6.0", + "thecodingmachine/graphqlite" : "^8", + "thecodingmachine/graphqlite-symfony-validator-bridge": "^7.1.1", "symfony/config": "^6.4 || ^7", "symfony/console": "^6.4 || ^7", "symfony/framework-bundle": "^6.4 || ^7", "symfony/validator": "^6.4 || ^7", "symfony/translation": "^6.4 || ^7", - "doctrine/annotations": "^1.13 || ^2.0.1", "symfony/psr-http-message-bridge": "^2.0 || ^7.0", "nyholm/psr7": "^1.1", "laminas/laminas-diactoros": "^2.2.2 || ^3", @@ -42,21 +41,25 @@ "composer/semver": "^3.4" }, "conflict": { - "mouf/classname-mapper": "<1.0.2", "symfony/event-dispatcher": "<4.3", "symfony/security-core": "<4.3", "symfony/routing": "<4.3", "phpdocumentor/type-resolver": "<1.4" }, "scripts": { - "phpstan": "phpstan analyse GraphQLiteBundle.php DependencyInjection/ Controller/ Resources/ Security/ -c phpstan.neon --level=7 --no-progress" + "phpstan": "phpstan analyse -c phpstan.neon --level=7 --no-progress" }, "suggest": { - "symfony/security-bundle": "To use @Logged or @Right annotations" + "symfony/security-bundle": "To use #[Logged] or #[Right] attributes" }, "autoload" : { "psr-4" : { - "TheCodingMachine\\GraphQLite\\Bundle\\" : "" + "TheCodingMachine\\GraphQLite\\Bundle\\" : "src" + } + }, + "autoload-dev" : { + "psr-4" : { + "TheCodingMachine\\GraphQLite\\Bundle\\Tests\\" : "tests" } }, "extra": { diff --git a/phpstan.neon b/phpstan.neon index fe0c558..f4dcf24 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,7 +8,7 @@ parameters: - vendor - cache - .phpstan-cache - - Tests + - tests level: max polluteScopeWithLoopInitialAssignments: false polluteScopeWithAlwaysIterableForeach: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist index cdfcc0f..97d95df 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,22 +16,17 @@ </php> <testsuites> - <testsuite name="Graphql controllers bundle Test Suite"> - <directory>./Tests/</directory> + <testsuite name="GraphQLite Bundle Test Suite"> + <directory>./tests/</directory> </testsuite> </testsuites> <filter> <whitelist> - <directory>./</directory> + <directory>./src/</directory> <exclude> - <directory>cache</directory> - <directory>build</directory> - <directory>./Resources</directory> - <directory>./Tests</directory> - <directory>./vendor</directory> - <directory>./var</directory> + <directory>./src/Resources</directory> </exclude> </whitelist> </filter> diff --git a/Command/DumpSchemaCommand.php b/src/Command/DumpSchemaCommand.php similarity index 100% rename from Command/DumpSchemaCommand.php rename to src/Command/DumpSchemaCommand.php diff --git a/Context/SymfonyGraphQLContext.php b/src/Context/SymfonyGraphQLContext.php similarity index 100% rename from Context/SymfonyGraphQLContext.php rename to src/Context/SymfonyGraphQLContext.php diff --git a/Context/SymfonyRequestContextInterface.php b/src/Context/SymfonyRequestContextInterface.php similarity index 100% rename from Context/SymfonyRequestContextInterface.php rename to src/Context/SymfonyRequestContextInterface.php diff --git a/Controller/GraphQL/InvalidUserPasswordException.php b/src/Controller/GraphQL/InvalidUserPasswordException.php similarity index 87% rename from Controller/GraphQL/InvalidUserPasswordException.php rename to src/Controller/GraphQL/InvalidUserPasswordException.php index 48bdbbc..e409002 100644 --- a/Controller/GraphQL/InvalidUserPasswordException.php +++ b/src/Controller/GraphQL/InvalidUserPasswordException.php @@ -10,6 +10,6 @@ class InvalidUserPasswordException extends GraphQLException { public static function create(Exception $previous = null): self { - return new self('The provided user / password is incorrect.', 401, $previous, 'Security'); + return new self('The provided user / password is incorrect.', 401, $previous, ['category' => 'Security']); } } diff --git a/Controller/GraphQL/LoginController.php b/src/Controller/GraphQL/LoginController.php similarity index 97% rename from Controller/GraphQL/LoginController.php rename to src/Controller/GraphQL/LoginController.php index de07ddf..08ed1f2 100644 --- a/Controller/GraphQL/LoginController.php +++ b/src/Controller/GraphQL/LoginController.php @@ -54,11 +54,7 @@ public function __construct(UserProviderInterface $userProvider, UserPasswordHas $this->eventDispatcher = $eventDispatcher; } - /** - * @Mutation() - * - * @phpstan-return TUser - */ + #[Mutation] public function login(string $userName, string $password, Request $request): UserInterface { try { @@ -95,9 +91,7 @@ public function login(string $userName, string $password, Request $request): Use return $user; } - /** - * @Mutation() - */ + #[Mutation] public function logout(Request $request): bool { $this->tokenStorage->setToken(null); diff --git a/Controller/GraphQL/MeController.php b/src/Controller/GraphQL/MeController.php similarity index 96% rename from Controller/GraphQL/MeController.php rename to src/Controller/GraphQL/MeController.php index 087832a..16c389c 100644 --- a/Controller/GraphQL/MeController.php +++ b/src/Controller/GraphQL/MeController.php @@ -18,9 +18,7 @@ public function __construct(TokenStorageInterface $tokenStorage) $this->tokenStorage = $tokenStorage; } - /** - * @Query() - */ + #[Query] public function me(): ?UserInterface { $token = $this->tokenStorage->getToken(); diff --git a/Controller/GraphQLiteController.php b/src/Controller/GraphQLiteController.php similarity index 100% rename from Controller/GraphQLiteController.php rename to src/Controller/GraphQLiteController.php index 6988566..9ef3d44 100644 --- a/Controller/GraphQLiteController.php +++ b/src/Controller/GraphQLiteController.php @@ -4,22 +4,17 @@ namespace TheCodingMachine\GraphQLite\Bundle\Controller; -use Laminas\Diactoros\ResponseFactory; -use Laminas\Diactoros\ServerRequestFactory; -use Laminas\Diactoros\StreamFactory; -use Laminas\Diactoros\UploadedFileFactory; -use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; -use TheCodingMachine\GraphQLite\Http\HttpCodeDecider; -use TheCodingMachine\GraphQLite\Http\HttpCodeDeciderInterface; -use function array_map; use GraphQL\Executor\ExecutionResult; use GraphQL\Server\ServerConfig; use GraphQL\Server\StandardServer; use GraphQL\Upload\UploadMiddleware; -use function class_exists; -use function json_decode; +use Laminas\Diactoros\ResponseFactory; +use Laminas\Diactoros\ServerRequestFactory; +use Laminas\Diactoros\StreamFactory; +use Laminas\Diactoros\UploadedFileFactory; use Psr\Http\Message\ServerRequestInterface; use RuntimeException; +use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -27,6 +22,11 @@ use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use TheCodingMachine\GraphQLite\Bundle\Context\SymfonyGraphQLContext; +use TheCodingMachine\GraphQLite\Http\HttpCodeDecider; +use TheCodingMachine\GraphQLite\Http\HttpCodeDeciderInterface; +use function array_map; +use function class_exists; +use function json_decode; /** * Listens to every single request and forward Graphql requests to Graphql Webonix standardServer. diff --git a/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php similarity index 100% rename from DependencyInjection/Configuration.php rename to src/DependencyInjection/Configuration.php diff --git a/DependencyInjection/GraphQLiteCompilerPass.php b/src/DependencyInjection/GraphQLiteCompilerPass.php similarity index 81% rename from DependencyInjection/GraphQLiteCompilerPass.php rename to src/DependencyInjection/GraphQLiteCompilerPass.php index bee4783..acc29d0 100644 --- a/DependencyInjection/GraphQLiteCompilerPass.php +++ b/src/DependencyInjection/GraphQLiteCompilerPass.php @@ -3,36 +3,26 @@ namespace TheCodingMachine\GraphQLite\Bundle\DependencyInjection; -use Doctrine\Common\Annotations\PsrCachedReader; +use Generator; use GraphQL\Server\ServerConfig; use GraphQL\Validator\Rules\DisableIntrospection; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; +use Kcs\ClassFinder\Finder\ComposerFinder; +use Psr\SimpleCache\CacheInterface; +use ReflectionClass; +use ReflectionMethod; use ReflectionNamedType; +use ReflectionParameter; use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\PhpFilesAdapter; use Symfony\Component\Cache\Psr16Cache; -use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; -use TheCodingMachine\GraphQLite\Mappers\StaticClassListTypeMapperFactory; -use Webmozart\Assert\Assert; -use function assert; -use function class_exists; -use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; -use Doctrine\Common\Annotations\AnnotationRegistry; -use Mouf\Composer\ClassNameMapper; -use Psr\SimpleCache\CacheInterface; -use ReflectionParameter; -use function filter_var; -use function function_exists; -use ReflectionClass; -use ReflectionMethod; -use function ini_get; -use function interface_exists; -use function strpos; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\User\UserInterface; use TheCodingMachine\CacheUtils\ClassBoundCache; @@ -40,7 +30,6 @@ use TheCodingMachine\CacheUtils\ClassBoundCacheContractInterface; use TheCodingMachine\CacheUtils\ClassBoundMemoryAdapter; use TheCodingMachine\CacheUtils\FileBoundCache; -use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer; use TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory; use TheCodingMachine\GraphQLite\AnnotationReader; use TheCodingMachine\GraphQLite\Annotations\Autowire; @@ -49,25 +38,32 @@ use TheCodingMachine\GraphQLite\Annotations\Query; use TheCodingMachine\GraphQLite\Bundle\Controller\GraphQL\LoginController; use TheCodingMachine\GraphQLite\Bundle\Controller\GraphQL\MeController; +use TheCodingMachine\GraphQLite\Bundle\Types\SymfonyUserInterfaceType; use TheCodingMachine\GraphQLite\GraphQLRuntimeException as GraphQLException; +use TheCodingMachine\GraphQLite\Mappers\StaticClassListTypeMapperFactory; use TheCodingMachine\GraphQLite\Mappers\StaticTypeMapper; use TheCodingMachine\GraphQLite\SchemaFactory; -use TheCodingMachine\GraphQLite\Bundle\Types\SymfonyUserInterfaceType; +use Webmozart\Assert\Assert; +use function assert; +use function class_exists; +use function filter_var; +use function ini_get; +use function interface_exists; +use function strpos; /** * Detects controllers and types automatically and tag them. */ class GraphQLiteCompilerPass implements CompilerPassInterface { - /** - * @var AnnotationReader - */ - private $annotationReader; + private ?AnnotationReader $annotationReader = null; + + private string $cacheDir; + + private ?CacheInterface $cache = null; + + private ?ClassBoundCacheContractInterface $codeCache = null; - /** - * @var string - */ - private $cacheDir; /** * You can modify the container here before it is dumped to PHP code. @@ -240,14 +236,14 @@ public function process(ContainerBuilder $container): void } foreach ($controllersNamespaces as $controllersNamespace) { - $schemaFactory->addMethodCall('addControllerNamespace', [ $controllersNamespace ]); + $schemaFactory->addMethodCall('addNamespace', [ $controllersNamespace ]); foreach ($this->getClassList($controllersNamespace) as $className => $refClass) { $this->makePublicInjectedServices($refClass, $reader, $container, true); } } foreach ($typesNamespaces as $typeNamespace) { - $schemaFactory->addMethodCall('addTypeNamespace', [ $typeNamespace ]); + $schemaFactory->addMethodCall('addNamespace', [ $typeNamespace ]); foreach ($this->getClassList($typeNamespace) as $className => $refClass) { $this->makePublicInjectedServices($refClass, $reader, $container, false); } @@ -301,19 +297,25 @@ public function process(ContainerBuilder $container): void private function registerController(string $controllerClassName, ContainerBuilder $container): void { $aggregateQueryProvider = $container->findDefinition(AggregateControllerQueryProviderFactory::class); + $controllersList = $aggregateQueryProvider->getArgument(0); if (!is_array($controllersList)){ throw new GraphQLException(sprintf('Expecting array in %s, arg #1', AggregateControllerQueryProviderFactory::class)); } + $controllersList[] = $controllerClassName; $aggregateQueryProvider->setArgument(0, $controllersList); + + $serviceLocatorMap = []; + foreach ($controllersList as $controller) { + $serviceLocatorMap[$controller] = new Reference($controller); + } + + $aggregateQueryProvider->setArgument(1, new ServiceLocatorArgument($serviceLocatorMap)); } /** * Register a method call on SchemaFactory for each tagged service, passing the service in parameter. - * - * @param string $tag - * @param string $methodName */ private function mapAdderToTag(string $tag, string $methodName, ContainerBuilder $container, Definition $schemaFactory): void { @@ -365,39 +367,33 @@ private function makePublicInjectedServices(ReflectionClass $refClass, Annotatio } /** - * @param ReflectionMethod $method - * @param ContainerBuilder $container * @return array<string, string> key = value = service name */ private function getListOfInjectedServices(ReflectionMethod $method, ContainerBuilder $container): array { + /** @var array<string, string> $services */ $services = []; - /** - * @var Autowire[] $autowireAnnotations - */ - $autowireAnnotations = $this->getAnnotationReader()->getMethodAnnotations($method, Autowire::class); - $parametersByName = null; - foreach ($autowireAnnotations as $autowire) { - $target = $autowire->getTarget(); - - if ($parametersByName === null) { - $parametersByName = self::getParametersByName($method); - } + $annotations = $this->getAnnotationReader()->getParameterAnnotationsPerParameter($method->getParameters()); + foreach ($annotations as $parameterName => $parameterAnnotations) { + $parameterAutowireAnnotation = $parameterAnnotations->getAnnotationsByType(Autowire::class); + foreach ($parameterAutowireAnnotation as $autowire) { + $id = $autowire->getIdentifier(); + if ($id !== null) { + $services[$id] = $id; + continue; + } - if (!isset($parametersByName[$target])) { - throw new GraphQLException('In method '.$method->getDeclaringClass()->getName().'::'.$method->getName().', the @Autowire annotation refers to a non existing parameter named "'.$target.'"'); - } + $parametersByName ??= self::getParametersByName($method); + if (!isset($parametersByName[$parameterName])) { + throw new \LogicException('Should not happen'); + } - $id = $autowire->getIdentifier(); - if ($id !== null) { - $services[$id] = $id; - } else { - $parameter = $parametersByName[$target]; + $parameter = $parametersByName[$parameterName]; $type = $parameter->getType(); - if ($type !== null && $type instanceof ReflectionNamedType) { + if ($type instanceof ReflectionNamedType) { $fqcn = $type->getName(); if ($container->has($fqcn)) { $services[$fqcn] = $fqcn; @@ -410,7 +406,6 @@ private function getListOfInjectedServices(ReflectionMethod $method, ContainerBu } /** - * @param ReflectionMethod $method * @return array<string, ReflectionParameter> */ private static function getParametersByName(ReflectionMethod $method): array @@ -423,33 +418,14 @@ private static function getParametersByName(ReflectionMethod $method): array } /** - * Returns a cached Doctrine annotation reader. + * Returns a GraphQLite annotation reader. * Note: we cannot get the annotation reader service in the container as we are in a compiler pass. */ private function getAnnotationReader(): AnnotationReader { - if ($this->annotationReader === null) { - // @phpstan-ignore-next-line "registerLoader exists in doctrine/annotations:v1.x" - if (method_exists(AnnotationRegistry::class, 'registerLoader')) { - AnnotationRegistry::registerLoader('class_exists'); - } - - $doctrineAnnotationReader = new DoctrineAnnotationReader(); - - if (ApcuAdapter::isSupported()) { - $doctrineAnnotationReader = new PsrCachedReader($doctrineAnnotationReader, new ApcuAdapter('graphqlite'), true); - } - - $this->annotationReader = new AnnotationReader($doctrineAnnotationReader, AnnotationReader::LAX_MODE); - } - return $this->annotationReader; + return $this->annotationReader ??= new AnnotationReader(); } - /** - * @var CacheInterface - */ - private $cache; - private function getPsr16Cache(): CacheInterface { if ($this->cache === null) { @@ -459,54 +435,33 @@ private function getPsr16Cache(): CacheInterface $this->cache = new Psr16Cache(new PhpFilesAdapter('graphqlite_bundle', 0, $this->cacheDir)); } } + return $this->cache; } - /** - * @var ClassBoundCacheContractInterface - */ - private $codeCache; - private function getCodeCache(): ClassBoundCacheContractInterface { - if ($this->codeCache === null) { - $this->codeCache = new ClassBoundCacheContract(new ClassBoundMemoryAdapter(new ClassBoundCache(new FileBoundCache($this->getPsr16Cache())))); - } - return $this->codeCache; + return $this->codeCache ??= new ClassBoundCacheContract( + new ClassBoundMemoryAdapter(new ClassBoundCache(new FileBoundCache($this->getPsr16Cache()))) + ); } /** * Returns the array of globbed classes. * Only instantiable classes are returned. * - * @return array<string,ReflectionClass<object>> Key: fully qualified class name + * @return Generator<class-string, ReflectionClass<object>, void, void> */ - private function getClassList(string $namespace, int $globTtl = 2, bool $recursive = true): array + private function getClassList(string $namespace): Generator { - $explorer = new GlobClassExplorer($namespace, $this->getPsr16Cache(), $globTtl, ClassNameMapper::createFromComposerFile(null, null, true), $recursive); - $allClasses = $explorer->getClassMap(); - $classes = []; - foreach ($allClasses as $className => $phpFile) { - if (! class_exists($className, false)) { - // Let's try to load the file if it was not imported yet. - // We are importing the file manually to avoid triggering the autoloader. - // The autoloader might trigger errors if the file does not respect PSR-4 or if the - // Symfony DebugAutoLoader is installed. (see https://github.com/thecodingmachine/graphqlite/issues/216) - require_once $phpFile; - // @phpstan-ignore-next-line Does it exist now? - if (! class_exists($className, false)) { - continue; - } - } - - $refClass = new ReflectionClass($className); - if (! $refClass->isInstantiable()) { - continue; - } - $classes[$className] = $refClass; + // dev note: this code will be broken if there's a broken class (which has mismatch in real namespace + // with PSR-4 configuration) in the configured namespace + // see also: https://github.com/alekitto/class-finder/issues/24#issuecomment-2480847327 + $finder = new ComposerFinder(); + foreach ($finder->inNamespace($namespace) as $class) { + assert($class instanceof ReflectionClass); + yield $class->getName() => $class; } - - return $classes; } } diff --git a/DependencyInjection/GraphQLiteExtension.php b/src/DependencyInjection/GraphQLiteExtension.php similarity index 97% rename from DependencyInjection/GraphQLiteExtension.php rename to src/DependencyInjection/GraphQLiteExtension.php index 8fa97ce..7142518 100644 --- a/DependencyInjection/GraphQLiteExtension.php +++ b/src/DependencyInjection/GraphQLiteExtension.php @@ -5,15 +5,15 @@ use GraphQL\Error\DebugFlag; -use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryInterface; -use function array_map; use GraphQL\Server\ServerConfig; use GraphQL\Type\Definition\ObjectType; -use function rtrim; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use TheCodingMachine\GraphQLite\Mappers\Root\RootTypeMapperFactoryInterface; +use function array_map; +use function rtrim; class GraphQLiteExtension extends Extension { @@ -43,7 +43,7 @@ public function load(array $configs, ContainerBuilder $container): void } $namespaceController = array_map( function($namespace): string { - return rtrim($namespace, '\\') . '\\'; + return rtrim($namespace, '\\'); }, $controllers ); @@ -57,7 +57,7 @@ function($namespace): string { } $namespaceType = array_map( function($namespace): string { - return rtrim($namespace, '\\') . '\\'; + return rtrim($namespace, '\\'); }, $types ); diff --git a/DependencyInjection/OverblogGraphiQLEndpointWiringPass.php b/src/DependencyInjection/OverblogGraphiQLEndpointWiringPass.php similarity index 100% rename from DependencyInjection/OverblogGraphiQLEndpointWiringPass.php rename to src/DependencyInjection/OverblogGraphiQLEndpointWiringPass.php diff --git a/GraphQLiteBundle.php b/src/GraphQLiteBundle.php similarity index 100% rename from GraphQLiteBundle.php rename to src/GraphQLiteBundle.php index b9005ab..95f9c16 100644 --- a/GraphQLiteBundle.php +++ b/src/GraphQLiteBundle.php @@ -3,13 +3,13 @@ namespace TheCodingMachine\GraphQLite\Bundle; -use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; -use TheCodingMachine\GraphQLite\Bundle\DependencyInjection\GraphQLiteExtension; -use TheCodingMachine\GraphQLite\Bundle\DependencyInjection\OverblogGraphiQLEndpointWiringPass; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; use TheCodingMachine\GraphQLite\Bundle\DependencyInjection\GraphQLiteCompilerPass; +use TheCodingMachine\GraphQLite\Bundle\DependencyInjection\GraphQLiteExtension; +use TheCodingMachine\GraphQLite\Bundle\DependencyInjection\OverblogGraphiQLEndpointWiringPass; class GraphQLiteBundle extends Bundle { diff --git a/GraphiQL/EndpointResolver.php b/src/GraphiQL/EndpointResolver.php similarity index 100% rename from GraphiQL/EndpointResolver.php rename to src/GraphiQL/EndpointResolver.php diff --git a/Mappers/RequestParameter.php b/src/Mappers/RequestParameter.php similarity index 100% rename from Mappers/RequestParameter.php rename to src/Mappers/RequestParameter.php diff --git a/Mappers/RequestParameterMiddleware.php b/src/Mappers/RequestParameterMiddleware.php similarity index 100% rename from Mappers/RequestParameterMiddleware.php rename to src/Mappers/RequestParameterMiddleware.php diff --git a/Resources/config/container/graphqlite.xml b/src/Resources/config/container/graphqlite.xml similarity index 95% rename from Resources/config/container/graphqlite.xml rename to src/Resources/config/container/graphqlite.xml index bda653f..545ec0e 100644 --- a/Resources/config/container/graphqlite.xml +++ b/src/Resources/config/container/graphqlite.xml @@ -5,10 +5,6 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd" > - <parameters> - <parameter key="graphqlite.annotations.error_mode">LAX_MODE</parameter> - </parameters> - <services> <defaults autowire="true" autoconfigure="true" public="false" /> @@ -30,19 +26,20 @@ </service> <service id="TheCodingMachine\GraphQLite\AggregateControllerQueryProviderFactory"> + <!-- Controller classes list will be generated by compiler pass --> <argument type="collection"> </argument> - <argument type="service" id="service_container"> + <!-- Service locator will be generated by compiler pass --> + <argument type="service_locator"> </argument> + <tag name="graphql.queryprovider_factory" /> </service> <service id="GraphQL\Type\Schema" alias="TheCodingMachine\GraphQLite\Schema" /> - <service id="TheCodingMachine\GraphQLite\AnnotationReader" > - <argument key="$mode">%graphqlite.annotations.error_mode%</argument> - </service> + <service id="TheCodingMachine\GraphQLite\AnnotationReader" /> <service id="TheCodingMachine\GraphQLite\Bundle\Security\AuthenticationService"> <argument type="service" id="security.token_storage" on-invalid="null" /> diff --git a/Resources/config/routes.xml b/src/Resources/config/routes.xml similarity index 100% rename from Resources/config/routes.xml rename to src/Resources/config/routes.xml diff --git a/Security/AuthenticationService.php b/src/Security/AuthenticationService.php similarity index 100% rename from Security/AuthenticationService.php rename to src/Security/AuthenticationService.php diff --git a/Security/AuthorizationService.php b/src/Security/AuthorizationService.php similarity index 100% rename from Security/AuthorizationService.php rename to src/Security/AuthorizationService.php diff --git a/Server/ServerConfig.php b/src/Server/ServerConfig.php similarity index 100% rename from Server/ServerConfig.php rename to src/Server/ServerConfig.php diff --git a/Types/SymfonyUserInterfaceType.php b/src/Types/SymfonyUserInterfaceType.php similarity index 93% rename from Types/SymfonyUserInterfaceType.php rename to src/Types/SymfonyUserInterfaceType.php index f895319..26ea2f4 100644 --- a/Types/SymfonyUserInterfaceType.php +++ b/src/Types/SymfonyUserInterfaceType.php @@ -9,14 +9,10 @@ use Symfony\Component\Security\Core\User\UserInterface; use TheCodingMachine\GraphQLite\FieldNotFoundException; -/** - * @Type(class=UserInterface::class) - */ +#[Type(class: UserInterface::class)] class SymfonyUserInterfaceType { - /** - * @Field - */ + #[Field] public function getUserName(UserInterface $user): string { // @phpstan-ignore-next-line Forward Compatibility for Symfony >=5.3 @@ -33,9 +29,9 @@ public function getUserName(UserInterface $user): string } /** - * @Field() * @return string[] */ + #[Field] public function getRoles(UserInterface $user): array { $roles = []; diff --git a/Tests/Command/DumpSchemaCommandTest.php b/tests/Command/DumpSchemaCommandTest.php similarity index 100% rename from Tests/Command/DumpSchemaCommandTest.php rename to tests/Command/DumpSchemaCommandTest.php diff --git a/Tests/Fixtures/Controller/MyException.php b/tests/Fixtures/Controller/MyException.php similarity index 100% rename from Tests/Fixtures/Controller/MyException.php rename to tests/Fixtures/Controller/MyException.php diff --git a/Tests/Fixtures/Controller/TestGraphqlController.php b/tests/Fixtures/Controller/TestGraphqlController.php similarity index 70% rename from Tests/Fixtures/Controller/TestGraphqlController.php rename to tests/Fixtures/Controller/TestGraphqlController.php index 17c5ba0..d768e61 100644 --- a/Tests/Fixtures/Controller/TestGraphqlController.php +++ b/tests/Fixtures/Controller/TestGraphqlController.php @@ -20,19 +20,16 @@ class TestGraphqlController { - - /** - * @Query() - */ + #[Query] public function test(string $foo): string { return 'echo ' .$foo; } /** - * @Query() * @return Product[] */ + #[Query] public function products(): array { return [ @@ -40,99 +37,76 @@ public function products(): array ]; } - /** - * @Query() - */ + #[Query] public function contact(): Contact { return new Contact('Mouf'); } - /** - * @Mutation() - */ + #[Mutation] public function saveProduct(Product $product): Product { return $product; } /** - * @Query() * @return Contact[] */ + #[Query] public function contacts(): ArrayResult { return new ArrayResult([new Contact('Mouf')]); } - /** - * @Query() - * @return string - */ + #[Query] public function triggerException(int $code = 0): string { throw new MyException('Boom', $code); } - /** - * @Query() - * @return string - */ + #[Query] public function triggerAggregateException(): string { $exception1 = new GraphQLException('foo', 401); - $exception2 = new GraphQLException('bar', 404, null, 'MyCat', ['field' => 'baz', 'category' => 'MyCat']); + $exception2 = new GraphQLException('bar', 404, null, ['field' => 'baz', 'category' => 'MyCat']); throw new GraphQLAggregateException([$exception1, $exception2]); } - /** - * @Query() - * @Logged() - * @FailWith(null) - * @return string - */ + #[Query] + #[Logged] + #[FailWith(null)] public function loggedQuery(): string { return 'foo'; } - /** - * @Query() - * @Right("ROLE_ADMIN") - * @FailWith(null) - * @return string - */ + #[Query] + #[Right('ROLE_ADMIN')] + #[FailWith(null)] public function withAdminRight(): string { return 'foo'; } - /** - * @Query() - * @Right("ROLE_USER") - * @FailWith(null) - * @return string - */ + #[Query] + #[Right('ROLE_USER')] + #[FailWith(null)] public function withUserRight(): string { return 'foo'; } - /** - * @Query() - * @return string - */ + #[Query] public function getUri(Request $request): string { return $request->getPathInfo(); } - /** - * @Query - * @Assertion(for="email", constraint=@Assert\Email()) - */ - public function findByMail(string $email = 'a@a.com'): string - { + #[Query] + public function findByMail( + #[Assertion(constraint: new Assert\Email())] + string $email = 'a@a.com' + ): string { return $email; } } diff --git a/Tests/Fixtures/Controller/TestPhp8GraphqlController.php b/tests/Fixtures/Controller/TestPhp8GraphqlController.php similarity index 100% rename from Tests/Fixtures/Controller/TestPhp8GraphqlController.php rename to tests/Fixtures/Controller/TestPhp8GraphqlController.php diff --git a/Tests/Fixtures/Entities/Contact.php b/tests/Fixtures/Entities/Contact.php similarity index 59% rename from Tests/Fixtures/Entities/Contact.php rename to tests/Fixtures/Entities/Contact.php index bfbbb5c..fc90f60 100644 --- a/Tests/Fixtures/Entities/Contact.php +++ b/tests/Fixtures/Entities/Contact.php @@ -1,18 +1,14 @@ <?php - namespace TheCodingMachine\GraphQLite\Bundle\Tests\Fixtures\Entities; - use stdClass; use TheCodingMachine\GraphQLite\Annotations\Field; use TheCodingMachine\GraphQLite\Annotations\Type; use TheCodingMachine\GraphQLite\Bundle\Tests\Fixtures\Controller\TestGraphqlController; use TheCodingMachine\GraphQLite\Annotations\Autowire; -/** - * @Type() - */ +#[Type] class Contact { /** @@ -25,51 +21,46 @@ public function __construct(string $name) $this->name = $name; } - /** - * @Field(name="name") - */ + #[Field(name: 'name')] public function getName(): string { return $this->name; } - /** - * @Field() - * @Autowire(for="$testService") - * @Autowire(for="$someService", identifier="someService") - * @Autowire(for="$someAlias", identifier="someAlias") - * @return string - */ - public function injectService(TestGraphqlController $testService = null, stdClass $someService = null, stdClass $someAlias = null): string - { + #[Field] + public function injectService( + #[Autowire] + TestGraphqlController $testService = null, + #[Autowire(identifier: 'someService')] + stdClass $someService = null, + #[Autowire(identifier: 'someAlias')] + stdClass $someAlias = null, + ): string { if (!$testService instanceof TestGraphqlController || $someService === null || $someAlias === null) { return 'KO'; } + return 'OK'; } - /** - * @Field(prefetchMethod="prefetchData") - */ + #[Field(prefetchMethod: 'prefetchData')] public function injectServicePrefetch($prefetchData): string { return $prefetchData; } - /** - * @Autowire(for="$someOtherService", identifier="someOtherService") - */ - public function prefetchData(iterable $iterable, stdClass $someOtherService = null) - { + public function prefetchData( + iterable $iterable, + #[Autowire(identifier: 'someOtherService')] + stdClass $someOtherService = null, + ) { if ($someOtherService === null) { return 'KO'; } return 'OK'; } - /** - * @Field() - */ + #[Field] public function getManager(): ?Contact { return null; diff --git a/Tests/Fixtures/Entities/Product.php b/tests/Fixtures/Entities/Product.php similarity index 99% rename from Tests/Fixtures/Entities/Product.php rename to tests/Fixtures/Entities/Product.php index b77e094..d33194a 100644 --- a/Tests/Fixtures/Entities/Product.php +++ b/tests/Fixtures/Entities/Product.php @@ -1,9 +1,7 @@ <?php - namespace TheCodingMachine\GraphQLite\Bundle\Tests\Fixtures\Entities; - class Product { /** diff --git a/Tests/Fixtures/Types/ContactType.php b/tests/Fixtures/Types/ContactType.php similarity index 84% rename from Tests/Fixtures/Types/ContactType.php rename to tests/Fixtures/Types/ContactType.php index 0e2c5c1..9340ac6 100644 --- a/Tests/Fixtures/Types/ContactType.php +++ b/tests/Fixtures/Types/ContactType.php @@ -9,14 +9,10 @@ use TheCodingMachine\GraphQLite\Bundle\Tests\Fixtures\Entities\Contact; -/** - * @ExtendType(class=Contact::class) - */ +#[ExtendType(class: Contact::class)] class ContactType { - /** - * @Field() - */ + #[Field] public function uppercaseName(Contact $contact): string { return strtoupper($contact->getName()); diff --git a/Tests/Fixtures/Types/ProductFactory.php b/tests/Fixtures/Types/ProductFactory.php similarity index 90% rename from Tests/Fixtures/Types/ProductFactory.php rename to tests/Fixtures/Types/ProductFactory.php index c28f853..1e38b91 100644 --- a/Tests/Fixtures/Types/ProductFactory.php +++ b/tests/Fixtures/Types/ProductFactory.php @@ -9,10 +9,7 @@ class ProductFactory { - - /** - * @Factory() - */ + #[Factory] public function buildProduct(string $name, float $price): Product { return new Product($name, $price); diff --git a/Tests/Fixtures/Types/ProductType.php b/tests/Fixtures/Types/ProductType.php similarity index 79% rename from Tests/Fixtures/Types/ProductType.php rename to tests/Fixtures/Types/ProductType.php index 94e3d8c..2aa3831 100644 --- a/Tests/Fixtures/Types/ProductType.php +++ b/tests/Fixtures/Types/ProductType.php @@ -10,16 +10,12 @@ use TheCodingMachine\GraphQLite\Bundle\Tests\Fixtures\Entities\Product; -/** - * @Type(class=Product::class) - * @SourceField(name="name") - * @SourceField(name="price") - */ +#[Type(class: Product::class)] +#[SourceField(name: 'name')] +#[SourceField(name: 'price')] class ProductType { - /** - * @Field() - */ + #[Field] public function getSeller(Product $product): ?Contact { return null; diff --git a/Tests/Fixtures/config/services.yaml b/tests/Fixtures/config/services.yaml similarity index 100% rename from Tests/Fixtures/config/services.yaml rename to tests/Fixtures/config/services.yaml diff --git a/Tests/FunctionalTest.php b/tests/FunctionalTest.php similarity index 100% rename from Tests/FunctionalTest.php rename to tests/FunctionalTest.php index 896485f..cdc90ed 100644 --- a/Tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -2,17 +2,17 @@ namespace TheCodingMachine\GraphQLite\Bundle\Tests; -use Symfony\Component\Security\Core\User\InMemoryUser; -use function json_decode; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\InMemoryUser; use TheCodingMachine\GraphQLite\Bundle\Controller\GraphQLiteController; use TheCodingMachine\GraphQLite\GraphQLRuntimeException as GraphQLException; use TheCodingMachine\GraphQLite\Schema; +use function json_decode; class FunctionalTest extends TestCase { diff --git a/Tests/GraphQLiteTestingKernel.php b/tests/GraphQLiteTestingKernel.php similarity index 92% rename from Tests/GraphQLiteTestingKernel.php rename to tests/GraphQLiteTestingKernel.php index 5ec06d1..c92ad95 100644 --- a/Tests/GraphQLiteTestingKernel.php +++ b/tests/GraphQLiteTestingKernel.php @@ -188,7 +188,7 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader) $container->loadFromExtension('graphqlite', $graphqliteConf); }); - $confDir = $this->getProjectDir().'/Tests/Fixtures/config'; + $confDir = $this->getProjectDir().'/tests/Fixtures/config'; $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); $loader->load($confDir.'/{packages}/'.$this->environment.'/**/*'.self::CONFIG_EXTS, 'glob'); @@ -199,12 +199,19 @@ public function configureContainer(ContainerBuilder $c, LoaderInterface $loader) // Note: typing is disabled because using different classes in Symfony 4 and 5 protected function configureRoutes(/*RoutingConfigurator*/ $routes) { - $routes->import(__DIR__.'/../Resources/config/routes.xml'); + $routes->import(__DIR__.'/../src/Resources/config/routes.xml'); } public function getCacheDir(): string { - return __DIR__.'/../cache/'.($this->enableSession?'withSession':'withoutSession').$this->enableLogin.($this->enableSecurity?'withSecurity':'withoutSecurity').$this->enableMe.'_'.($this->introspection?'withIntrospection':'withoutIntrospection').'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth.'_'.md5(serialize($this->controllersNamespace).'_'.md5(serialize($this->typesNamespace))); + $prefix = ($this->enableSession?'withSession':'withoutSession') + .$this->enableLogin + .($this->enableSecurity?'withSecurity':'withoutSecurity') + .$this->enableMe + .'_' + .($this->introspection?'withIntrospection':'withoutIntrospection'); + + return __DIR__.'/../cache/'.$prefix.'_'.$this->maximumQueryComplexity.'_'.$this->maximumQueryDepth.'_'.md5(serialize($this->controllersNamespace).'_'.md5(serialize($this->typesNamespace))); } public function process(ContainerBuilder $container): void diff --git a/Tests/NoSecurityBundleFixtures/Controller/EchoController.php b/tests/NoSecurityBundleFixtures/Controller/EchoController.php similarity index 88% rename from Tests/NoSecurityBundleFixtures/Controller/EchoController.php rename to tests/NoSecurityBundleFixtures/Controller/EchoController.php index 64dfe19..3521ed2 100644 --- a/Tests/NoSecurityBundleFixtures/Controller/EchoController.php +++ b/tests/NoSecurityBundleFixtures/Controller/EchoController.php @@ -1,16 +1,12 @@ <?php - namespace TheCodingMachine\GraphQLite\Bundle\Tests\NoSecurityBundleFixtures\Controller; - use TheCodingMachine\GraphQLite\Annotations\Query; class EchoController { - /** - * @Query() - */ + #[Query] public function echoMsg(string $message): string { return $message; } diff --git a/Tests/NoSecurityBundleTest.php b/tests/NoSecurityBundleTest.php similarity index 78% rename from Tests/NoSecurityBundleTest.php rename to tests/NoSecurityBundleTest.php index 46f6e13..91632fb 100644 --- a/Tests/NoSecurityBundleTest.php +++ b/tests/NoSecurityBundleTest.php @@ -2,6 +2,7 @@ namespace TheCodingMachine\GraphQLite\Bundle\Tests; +use Symfony\Component\Cache\Adapter\ApcuAdapter; use function json_decode; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; @@ -15,10 +16,16 @@ class NoSecurityBundleTest extends TestCase { public function testServiceWiring(): void { + // tech debt: for some reason when we're running full test suite + // - from APCu cache we're getting old controllers for available graphql and this fails test + if (ApcuAdapter::isSupported()) { + $apcu = new ApcuAdapter(); + $apcu->clear(); + } + $kernel = new GraphQLiteTestingKernel(true, null, false, null, true, null, null, ['TheCodingMachine\\GraphQLite\\Bundle\\Tests\\NoSecurityBundleFixtures\\Controller\\']); $kernel->boot(); $container = $kernel->getContainer(); - self::assertNotNull($container); $schema = $container->get(Schema::class); $this->assertInstanceOf(Schema::class, $schema);