Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
install-args: ['']
php-version: ['8.2']
php-version: ['8.2', '8.3', '8.4']
fail-fast: false
steps:
# Cancel previous runs of the same branch
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"symfony/yaml": "^6.4 || ^7",
"beberlei/porpaginas": "^1.2 || ^2.0",
"symfony/phpunit-bridge": "^6.4 || ^7",
"phpstan/phpstan": "^1.8",
"phpstan/phpstan": "^2",
"phpstan/phpstan-symfony": "^2.0",
"composer/package-versions-deprecated": "^1.8",
"composer/semver": "^3.4"
},
Expand All @@ -47,7 +48,7 @@
"phpdocumentor/type-resolver": "<1.4"
},
"scripts": {
"phpstan": "phpstan analyse -c phpstan.neon --level=7 --no-progress"
"phpstan": "phpstan analyse -c phpstan.neon --no-progress"
},
"suggest": {
"symfony/security-bundle": "To use #[Logged] or #[Right] attributes"
Expand Down
14 changes: 8 additions & 6 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
includes:
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
- vendor/phpstan/phpstan-symfony/extension.neon

parameters:
level: max
tmpDir: .phpstan-cache
paths:
- .
- src
excludePaths:
- vendor
- cache
- .phpstan-cache
- tests
level: max

polluteScopeWithLoopInitialAssignments: false
polluteScopeWithAlwaysIterableForeach: false
checkAlwaysTrueCheckTypeFunctionCall: true
checkAlwaysTrueInstanceof: true
checkAlwaysTrueStrictComparison: true
checkExplicitMixedMissingReturn: true
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: true
Expand All @@ -24,3 +24,5 @@ parameters:
ignoreErrors:
# Wrong return type hint in Symfony's TreeBuilder
- '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::\w+\(\).#'
# PhpStan doesn't know bundle resolved config structure and it's pretty complex to describe it
- '#Parameter \#2 \$value of method Symfony\\Component\\DependencyInjection\\Container::setParameter\(\) expects array\|bool\|float\|int\|string\|UnitEnum\|null, mixed given\.#'
2 changes: 1 addition & 1 deletion src/Controller/GraphQL/InvalidUserPasswordException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class InvalidUserPasswordException extends GraphQLException
{
public static function create(Exception $previous = null): self
public static function create(?Exception $previous = null): self
{
return new self('The provided user / password is incorrect.', 401, $previous, ['category' => 'Security']);
}
Expand Down
5 changes: 3 additions & 2 deletions src/Controller/GraphQLiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class GraphQLiteController
*/
private $httpCodeDecider;

public function __construct(ServerConfig $serverConfig, HttpMessageFactoryInterface $httpMessageFactory = null, ?int $debug = null, ?HttpCodeDeciderInterface $httpCodeDecider = null)
public function __construct(ServerConfig $serverConfig, ?HttpMessageFactoryInterface $httpMessageFactory = null, ?int $debug = null, ?HttpCodeDeciderInterface $httpCodeDecider = null)
{
$this->serverConfig = $serverConfig;
$this->httpMessageFactory = $httpMessageFactory ?: new PsrHttpFactory(new ServerRequestFactory(), new StreamFactory(), new UploadedFileFactory(), new ResponseFactory());
Expand Down Expand Up @@ -78,7 +78,7 @@ public function handleRequest(Request $request): Response
{
$psr7Request = $this->httpMessageFactory->createRequest($request);

if (strtoupper($request->getMethod()) === "POST" && empty($psr7Request->getParsedBody())) {
if (strtoupper($request->getMethod()) === 'POST' && empty($psr7Request->getParsedBody())) {
$content = $psr7Request->getBody()->getContents();
$parsedBody = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Expand All @@ -94,6 +94,7 @@ public function handleRequest(Request $request): Response
if (class_exists(UploadMiddleware::class)) {
$uploadMiddleware = new UploadMiddleware();
$psr7Request = $uploadMiddleware->processRequest($psr7Request);
\assert($psr7Request instanceof ServerRequestInterface);
}

return $this->handlePsr7Request($psr7Request, $request);
Expand Down
65 changes: 35 additions & 30 deletions src/DependencyInjection/GraphQLiteCompilerPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
use function filter_var;
use function ini_get;
use function interface_exists;
use function strpos;

/**
* Detects controllers and types automatically and tag them.
Expand Down Expand Up @@ -199,52 +198,53 @@ public function process(ContainerBuilder $container): void
$container->getDefinition(StaticClassListTypeMapperFactory::class)->setArgument(0, $staticTypes);
}

foreach ($container->getDefinitions() as $id => $definition) {
foreach ($container->getDefinitions() as $definition) {
if ($definition->isAbstract() || $definition->getClass() === null) {
continue;
}
/**
* @var class-string $class
*/

/** @var class-string $class */
$class = $definition->getClass();
/* foreach ($controllersNamespaces as $controllersNamespace) {
if (strpos($class, $controllersNamespace) === 0) {
$definition->addTag('graphql.annotated.controller');
}
}*/

foreach ($typesNamespaces as $typesNamespace) {
if (strpos($class, $typesNamespace) === 0) {
//$definition->addTag('graphql.annotated.type');
// Set the types public
$reflectionClass = new ReflectionClass($class);
$typeAnnotation = $this->getAnnotationReader()->getTypeAnnotation($reflectionClass);
if ($typeAnnotation !== null && $typeAnnotation->isSelfType()) {
continue;
}
if ($typeAnnotation !== null || $this->getAnnotationReader()->getExtendTypeAnnotation($reflectionClass) !== null) {
\assert(\is_string($typesNamespace));

if (false === str_starts_with($class, $typesNamespace)) {
continue;
}

// Set the types public
$reflectionClass = new ReflectionClass($class);
$typeAnnotation = $this->getAnnotationReader()->getTypeAnnotation($reflectionClass);
if ($typeAnnotation !== null && $typeAnnotation->isSelfType()) {
continue;
}
if ($typeAnnotation !== null || $this->getAnnotationReader()->getExtendTypeAnnotation($reflectionClass) !== null) {
$definition->setPublic(true);
}
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$factory = $reader->getFactoryAnnotation($method);
if ($factory !== null) {
$definition->setPublic(true);
}
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$factory = $reader->getFactoryAnnotation($method);
if ($factory !== null) {
$definition->setPublic(true);
}
}
}
}
}

foreach ($controllersNamespaces as $controllersNamespace) {
\assert(\is_string($controllersNamespace));

$schemaFactory->addMethodCall('addNamespace', [ $controllersNamespace ]);
foreach ($this->getClassList($controllersNamespace) as $className => $refClass) {
foreach ($this->getClassList($controllersNamespace) as $refClass) {
$this->makePublicInjectedServices($refClass, $reader, $container, true);
}
}

foreach ($typesNamespaces as $typeNamespace) {
\assert(\is_string($typeNamespace));

$schemaFactory->addMethodCall('addNamespace', [ $typeNamespace ]);
foreach ($this->getClassList($typeNamespace) as $className => $refClass) {
foreach ($this->getClassList($typeNamespace) as $refClass) {
$this->makePublicInjectedServices($refClass, $reader, $container, false);
}
}
Expand All @@ -256,11 +256,14 @@ public function process(ContainerBuilder $container): void
$customNotMappedTypes = [];
foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attributes) {
if (isset($attributes["class"])) {
$phpClass = $attributes["class"];
if (!class_exists($phpClass)) {
\assert(\is_array($attributes));

if (isset($attributes['class']) && is_string($attributes['class'])) {
$phpClass = $attributes['class'];
if (false === class_exists($phpClass)) {
throw new \RuntimeException(sprintf('The class attribute of the graphql.output_type annotation of the %s service must point to an existing PHP class. Value passed: %s', $id, $phpClass));
}

$customTypes[$phpClass] = new Reference($id);
} else {
$customNotMappedTypes[] = new Reference($id);
Expand Down Expand Up @@ -308,6 +311,8 @@ private function registerController(string $controllerClassName, ContainerBuilde

$serviceLocatorMap = [];
foreach ($controllersList as $controller) {
\assert(\is_string($controller));

$serviceLocatorMap[$controller] = new Reference($controller);
}

Expand Down
17 changes: 15 additions & 2 deletions src/DependencyInjection/GraphQLiteExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,35 @@ public function load(array $configs, ContainerBuilder $container): void

$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config/container'));

\assert(\is_array($config['namespace']));
if (isset($config['namespace']['controllers'])) {
$controllers = $config['namespace']['controllers'];
if (!is_array($controllers)) {
$controllers = [ $controllers ];
}

$namespaceController = array_map(
function($namespace): string {
\assert(\is_string($namespace));

return rtrim($namespace, '\\');
},
$controllers
);
} else {
$namespaceController = [];
}

\assert(\is_array($config['namespace']));
if (isset($config['namespace']['types'])) {
$types = $config['namespace']['types'];
if (!is_array($types)) {
$types = [ $types ];
}
$namespaceType = array_map(
function($namespace): string {
\assert(\is_string($namespace));

return rtrim($namespace, '\\');
},
$types
Expand All @@ -65,6 +73,7 @@ function($namespace): string {
$namespaceType = [];
}

\assert(\is_array($config['security']));
$enableLogin = $config['security']['enable_login'] ?? 'auto';
$enableMe = $config['security']['enable_me'] ?? 'auto';

Expand All @@ -80,11 +89,15 @@ function($namespace): string {
$loader->load('graphqlite.xml');

$definition = $container->getDefinition(ServerConfig::class);
if (isset($config['debug'])) {
$debugCode = $this->toDebugCode($config['debug']);
if (isset($config['debug']) && \is_array($config['debug'])) {
/** @var array<string, int> $configDebug */
$configDebug = $config['debug'];

$debugCode = $this->toDebugCode($configDebug);
} else {
$debugCode = DebugFlag::RETHROW_UNSAFE_EXCEPTIONS;
}

$definition->addMethodCall('setDebugFlag', [$debugCode]);

$container->registerForAutoconfiguration(ObjectType::class)
Expand Down
23 changes: 9 additions & 14 deletions src/GraphiQL/EndpointResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,20 @@ public function __construct(RequestStack $requestStack)
$this->requestStack = $requestStack;
}

/**
* @return string
*/
public function getBySchema($name)
public function getBySchema($name): string
{
if ('default' === $name) {
$request = $this->requestStack->getCurrentRequest();
assert(!is_null($request));

return $request->getBaseUrl().'/graphql';
if ('default' !== $name) {
/** @phpstan-ignore throw.notThrowable (Missing return type in the library) */
throw GraphQLEndpointInvalidSchemaException::forSchemaAndResolver($name, self::class);
}

throw GraphQLEndpointInvalidSchemaException::forSchemaAndResolver($name, self::class);
$request = $this->requestStack->getCurrentRequest();
assert(!is_null($request));

return $request->getBaseUrl().'/graphql';
}

/**
* @return string
*/
public function getDefault()
public function getDefault(): string
{
return $this->getBySchema('default');
}
Expand Down
2 changes: 1 addition & 1 deletion src/Mappers/RequestParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class RequestParameter implements ParameterInterface
* @param array<string, mixed> $args
* @param mixed $context
*/
public function resolve(?object $source, array $args, $context, ResolveInfo $info): mixed
public function resolve(?object $source, array $args, mixed $context, ResolveInfo $info): mixed
{
if (!$context instanceof SymfonyRequestContextInterface) {
throw new GraphQLException('Cannot type-hint on a Symfony Request object in your query/mutation/field. The request context must implement SymfonyRequestContextInterface.');
Expand Down
2 changes: 1 addition & 1 deletion src/Security/AuthorizationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function __construct(?AuthorizationCheckerInterface $authorizationChecker
*
* @param mixed $subject The scope this right applies on. $subject is typically an object or a FQCN. Set $subject to "null" if the right is global.
*/
public function isAllowed(string $right, $subject = null): bool
public function isAllowed(string $right, mixed $subject = null): bool
{
if ($this->authorizationChecker === null || $this->tokenStorage === null) {
throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
Expand Down
4 changes: 3 additions & 1 deletion src/Server/ServerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

/**
* A slightly modified version of the server config: default validators are added by default when setValidators is called.
*
* @phpstan-type ValidationRulesResolveFn = callable(OperationParams, DocumentNode, string): array<ValidationRule>
*/
class ServerConfig extends \GraphQL\Server\ServerConfig
{
/**
* Set validation rules for this server, AND adds by default all the "default" validation rules provided by Webonyx
*
* @param ValidationRule[]|callable $validationRules
* @param ValidationRule[]|ValidationRulesResolveFn $validationRules
*
* @api
*/
Expand Down
20 changes: 2 additions & 18 deletions src/Types/SymfonyUserInterfaceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,17 @@

namespace TheCodingMachine\GraphQLite\Bundle\Types;

use Symfony\Component\Security\Core\Role\Role;
use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Type;
use Symfony\Component\Security\Core\User\UserInterface;
use TheCodingMachine\GraphQLite\FieldNotFoundException;

#[Type(class: UserInterface::class)]
class SymfonyUserInterfaceType
{
#[Field]
public function getUserName(UserInterface $user): string
{
// @phpstan-ignore-next-line Forward Compatibility for Symfony >=5.3
if (method_exists($user, 'getUserIdentifier')) {
return $user->getUserIdentifier();
}

// @phpstan-ignore-next-line Backward Compatibility for Symfony <5.3
if (method_exists($user, 'getUsername')) {
return $user->getUsername();
}

throw FieldNotFoundException::missingField(UserInterface::class, 'userName');
return $user->getUserIdentifier();
}

/**
Expand All @@ -36,13 +24,9 @@ public function getRoles(UserInterface $user): array
{
$roles = [];
foreach ($user->getRoles() as $role) {
// @phpstan-ignore-next-line BC for Symfony 4
if (class_exists(Role::class) && $role instanceof Role) {
$role = $role->getRole();
}

$roles[] = $role;
}

return $roles;
}
}
Loading