This commit is contained in:
Your Name 2021-07-26 19:46:18 +02:00
parent e7a49138bb
commit aae17f10a6
818 changed files with 70695 additions and 16408 deletions

View File

@ -0,0 +1,7 @@
/ci/ export-ignore
/docs/ export-ignore
/test/ export-ignore
/.gitignore export-ignore
/.gitlab-ci.yml export-ignore
/composer.lock export-ignore
/phpunit.xml export-ignore

View File

@ -0,0 +1,13 @@
Copyright (c) 2018 Hayden Pierce
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,126 @@
ClassFinder
===========
A dead simple utility to identify classes in a given namespace.
This package is an improved implementation of an [answer on Stack Overflow](https://stackoverflow.com/a/40229665/3000068)
and provides additional features with less configuration required.
Requirements
------------
* Application is using Composer.
* Classes can be autoloaded with Composer.
* PHP >= 5.3.0
Installing
----------
Installing is done by requiring it with Composer.
```
$ composer require haydenpierce/class-finder
```
No other installation methods are currently supported.
Supported Autoloading Methods
--------------------------------
| Method | Supported | with `ClassFinder::RECURSIVE_MODE` |
| ---------- | --------- | ---------------------------------- |
| PSR-4 | ✔️ | ✔️ |
| PSR-0 | ❌️* | ❌️* |
| Classmap | ✔️ | ✔️ |
| Files | ✔️^ | ❌️** |
\^ Experimental.
\* Planned.
\** Not planned. Open an issue if you need this feature.
Examples
--------
**Standard Mode**
```
<?php
require_once __DIR__ . '/vendor/autoload.php';
$classes = ClassFinder::getClassesInNamespace('TestApp1\Foo');
/**
* array(
* 'TestApp1\Foo\Bar',
* 'TestApp1\Foo\Baz',
* 'TestApp1\Foo\Foo'
* )
*/
var_dump($classes);
```
**Recursive Mode** *(in v0.3-beta)*
```
<?php
require_once __DIR__ . '/vendor/autoload.php';
$classes = ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE);
/**
* array(
* 'TestApp1\Foo\Bar',
* 'TestApp1\Foo\Baz',
* 'TestApp1\Foo\Foo',
* 'TestApp1\Foo\Box\Bar',
* 'TestApp1\Foo\Box\Baz',
* 'TestApp1\Foo\Box\Foo',
* 'TestApp1\Foo\Box\Lon\Bar',
* 'TestApp1\Foo\Box\Lon\Baz',
* 'TestApp1\Foo\Box\Lon\Foo',
* )
*/
var_dump($classes);
```
Documentation
-------------
[Changelog](docs/changelog.md)
**Exceptions**:
* [Files could not locate PHP](docs/exceptions/filesCouldNotLocatePHP.md)
* [Files exec not available](docs/exceptions/filesExecNotAvailable.md)
* [Missing composer.json](docs/exceptions/missingComposerConfig.md)
**Internals**
* [How Testing Works](docs/testing.md)
* [Continuous Integration Notes](docs/ci.md)
Future Work
-----------
> **WARNING**: Before 1.0.0, expect that bug fixes _will not_ be backported to older versions. Backwards incompatible changes
may be introduced in minor 0.X.Y versions, where X changes.
* `psr0` support
* Additional features:
Various ideas:
* ~~`ClassFinder::getClassesInNamespace('TestApp1\Foo', ClassFinder::RECURSIVE_MODE)`.
Providing classes multiple namespaces deep.~~ (included v0.3-beta)
* `ClassFinder::getClassesImplementingInterface('TestApp1\Foo', 'TestApp1\FooInterface', ClassFinder::RECURSIVE_MODE)`.
Filtering classes to only classes that implement a namespace.
* `ClassFinder::debugRenderReport('TestApp1\Foo\Baz')`
Guidance for solving "class not found" errors resulting from typos in namespaces, missing directories, etc. Would print
an HTML report. Not intended for production use, but debugging.

View File

@ -0,0 +1,27 @@
{
"name": "haydenpierce/class-finder",
"description" : "A library that can provide of a list of classes in a given namespace",
"type": "library",
"license": "MIT",
"version": "0.4.3",
"authors": [
{
"name": "Hayden Pierce",
"email": "hayden@haydenpierce.com"
}
],
"require": {
"php": ">=5.3",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "~9.0",
"mikey179/vfsstream": "^1.6"
},
"autoload": {
"psr-4": {
"HaydenPierce\\ClassFinder\\": "src/",
"HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit"
}
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace HaydenPierce\ClassFinder;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class AppConfig
{
/** @var string */
private $appRoot;
public function __construct()
{
$this->appRoot = $this->findAppRoot();
}
/**
* @return string
*/
private function findAppRoot()
{
if ($this->appRoot) {
$appRoot = $this->appRoot;
} else {
$workingDirectory = str_replace('\\', '/', __DIR__);
$workingDirectory = str_replace('/vendor/haydenpierce/class-finder/src', '', $workingDirectory);
$directoryPathPieces = explode('/', $workingDirectory);
$appRoot = null;
do {
$path = implode('/', $directoryPathPieces) . '/composer.json';
if (file_exists($path)) {
$appRoot = implode('/', $directoryPathPieces) . '/';
} else {
array_pop($directoryPathPieces);
}
} while (is_null($appRoot) && count($directoryPathPieces) > 0);
}
$this->throwIfInvalidAppRoot($appRoot);
$this->appRoot= $appRoot;
return $this->appRoot;
}
/**
* @param string $appRoot
* @return void
* @throws ClassFinderException
*/
private function throwIfInvalidAppRoot($appRoot)
{
if (!file_exists($appRoot . '/composer.json')) {
throw new ClassFinderException(sprintf("Could not locate composer.json. You can get around this by setting ClassFinder::\$appRoot manually. See '%s' for details.",
'https://gitlab.com/hpierce1102/ClassFinder/blob/master/docs/exceptions/missingComposerConfig.md'
));
}
}
/**
* @return string
*/
public function getAppRoot()
{
if ($this->appRoot === null) {
$this->appRoot = $this->findAppRoot();
}
$this->throwIfInvalidAppRoot($this->appRoot);
return $this->appRoot;
}
/**
* @param string $appRoot
* @return void
*/
public function setAppRoot($appRoot)
{
$this->appRoot = $appRoot;
}
}

View File

@ -0,0 +1,208 @@
<?php
namespace HaydenPierce\ClassFinder;
use HaydenPierce\ClassFinder\Classmap\ClassmapEntryFactory;
use HaydenPierce\ClassFinder\Classmap\ClassmapFinder;
use HaydenPierce\ClassFinder\Files\FilesEntryFactory;
use HaydenPierce\ClassFinder\Files\FilesFinder;
use HaydenPierce\ClassFinder\PSR4\PSR4Finder;
use HaydenPierce\ClassFinder\PSR4\PSR4NamespaceFactory;
class ClassFinder
{
const STANDARD_MODE = 1;
const RECURSIVE_MODE = 2;
/** @var AppConfig */
private static $config;
/** @var PSR4Finder */
private static $psr4;
/** @var ClassmapFinder */
private static $classmap;
/** @var FilesFinder */
private static $files;
/** @var boolean */
private static $useFilesSupport = false;
/** @var boolean */
private static $usePSR4Support = true;
/** @var boolean */
private static $useClassmapSupport = true;
/**
* @return void
*/
private static function initialize()
{
if (!(self::$config instanceof AppConfig)) {
self::$config = new AppConfig();
}
if (!(self::$psr4 instanceof PSR4Finder)) {
$PSR4Factory = new PSR4NamespaceFactory(self::$config);
self::$psr4 = new PSR4Finder($PSR4Factory);
}
if (!(self::$classmap instanceof ClassmapFinder)) {
$classmapFactory = new ClassmapEntryFactory(self::$config);
self::$classmap = new ClassmapFinder($classmapFactory);
}
if (!(self::$files instanceof FilesFinder) && self::$useFilesSupport) {
$filesFactory = new FilesEntryFactory(self::$config);
self::$files = new FilesFinder($filesFactory);
}
}
/**
* Identify classes in a given namespace.
*
* @param string $namespace
* @param int $options
* @return string[]
*
* @throws \Exception
*/
public static function getClassesInNamespace($namespace, $options = self::STANDARD_MODE)
{
self::initialize();
$findersWithNamespace = self::findersWithNamespace($namespace);
$classes = array_reduce($findersWithNamespace, function($carry, FinderInterface $finder) use ($namespace, $options){
return array_merge($carry, $finder->findClasses($namespace, $options));
}, array());
return array_unique($classes);
}
/**
* Check if a given namespace contains any classes.
*
* @param string $namespace
* @return bool
*/
public static function namespaceHasClasses($namespace)
{
self::initialize();
return count(self::findersWithNamespace($namespace)) > 0;
}
/**
* @param string $appRoot
* @return void
*/
public static function setAppRoot($appRoot)
{
self::initialize();
self::$config->setAppRoot($appRoot);
}
/**
* @return void
*/
public static function enableExperimentalFilesSupport()
{
self::$useFilesSupport = true;
}
/**
* @return void
*/
public static function disableExperimentalFilesSupport()
{
self::$useFilesSupport = false;
}
/**
* @return void
*/
public static function enablePSR4Support()
{
self::$usePSR4Support = true;
}
/**
* @return void
*/
public static function disablePSR4Support()
{
self::$usePSR4Support = false;
}
/**
* @return void
*/
public static function enableClassmapSupport()
{
self::$useClassmapSupport = true;
}
/**
* @return void
*/
public static function disableClassmapSupport()
{
self::$useClassmapSupport = false;
}
/**
* @return FinderInterface[]
*/
private static function getSupportedFinders()
{
$supportedFinders = array();
/*
* This is done for testing. For some tests, allowing PSR4 classes contaminates the test results. This could also be
* disabled for performance reasons (less finders in use means less work), but most people probably won't do that.
*/
if (self::$usePSR4Support) {
$supportedFinders[] = self::$psr4;
}
/*
* This is done for testing. For some tests, allowing classmap classes contaminates the test results. This could also be
* disabled for performance reasons (less finders in use means less work), but most people probably won't do that.
*/
if (self::$useClassmapSupport) {
$supportedFinders[] = self::$classmap;
}
/*
* Files support is tucked away behind a flag because it will need to use some kind of shell access via exec, or
* system.
*
* #1 Many environments (such as shared space hosts) may not allow these functions, and attempting to call
* these functions will blow up.
* #2 I've heard of performance issues with calling these functions.
* #3 Files support probably doesn't benefit most projects.
* #4 Using exec() or system() is against many PHP developers' religions.
*/
if (self::$useFilesSupport) {
$supportedFinders[] = self::$files;
}
return $supportedFinders;
}
/**
* @param string $namespace
* @return FinderInterface[]
*/
private static function findersWithNamespace($namespace)
{
$findersWithNamespace = array_filter(self::getSupportedFinders(), function (FinderInterface $finder) use ($namespace) {
return $finder->isNamespaceKnown($namespace);
});
return $findersWithNamespace;
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace HaydenPierce\ClassFinder\Classmap;
use HaydenPierce\ClassFinder\ClassFinder;
class ClassmapEntry
{
/** @var string */
private $className;
/**
* @param string $fullyQualifiedClassName
*/
public function __construct($fullyQualifiedClassName)
{
$this->className = $fullyQualifiedClassName;
}
/**
* @param string $namespace
* @return bool
*/
public function knowsNamespace($namespace)
{
return strpos($this->className, $namespace) !== false;
}
/**
* @param string $namespace
* @return bool
*/
public function matches($namespace, $options)
{
if ($options === ClassFinder::RECURSIVE_MODE) {
return $this->doesMatchAnyNamespace($namespace);
} else {
return $this->doesMatchDirectNamespace($namespace);
}
}
/**
* @return string
*/
public function getClassName()
{
return $this->className;
}
/**
* Checks if the class is a child or subchild of the given namespace.
*
* @param $namespace
* @return bool
*/
private function doesMatchAnyNamespace($namespace)
{
return strpos($this->getClassName(),$namespace) === 0;
}
/**
* Checks if the class is a DIRECT child of the given namespace.
*
* @param string $namespace
* @return bool
*/
private function doesMatchDirectNamespace($namespace)
{
$classNameFragments = explode('\\', $this->getClassName());
array_pop($classNameFragments);
$classNamespace = implode('\\', $classNameFragments);
$namespace = trim($namespace, '\\');
return $namespace === $classNamespace;
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace HaydenPierce\ClassFinder\Classmap;
use HaydenPierce\ClassFinder\AppConfig;
class ClassmapEntryFactory
{
/** @var AppConfig */
private $appConfig;
public function __construct(AppConfig $appConfig)
{
$this->appConfig = $appConfig;
}
/**
* @return ClassmapEntry[]
*/
public function getClassmapEntries()
{
// Composer will compile user declared mappings to autoload_classmap.php. So no additional work is needed
// to fetch user provided entries.
$classmap = require($this->appConfig->getAppRoot() . 'vendor/composer/autoload_classmap.php');
// if classmap has no entries return empty array
if(count($classmap) == 0) {
return array();
}
$classmapKeys = array_keys($classmap);
return array_map(function($index) use ($classmapKeys){
return new ClassmapEntry($classmapKeys[$index]);
}, range(0, count($classmap) - 1));
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace HaydenPierce\ClassFinder\Classmap;
use HaydenPierce\ClassFinder\FinderInterface;
class ClassmapFinder implements FinderInterface
{
/** @var ClassmapEntryFactory */
private $factory;
public function __construct(ClassmapEntryFactory $factory)
{
$this->factory = $factory;
}
/**
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace)
{
$classmapEntries = $this->factory->getClassmapEntries();
foreach($classmapEntries as $classmapEntry) {
if ($classmapEntry->knowsNamespace($namespace)) {
return true;
}
}
return false;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options)
{
$classmapEntries = $this->factory->getClassmapEntries();
$matchingEntries = array_filter($classmapEntries, function(ClassmapEntry $entry) use ($namespace, $options) {
return $entry->matches($namespace, $options);
});
return array_map(function(ClassmapEntry $entry) {
return $entry->getClassName();
}, $matchingEntries);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace HaydenPierce\ClassFinder\Exception;
class ClassFinderException extends \Exception
{
}

View File

@ -0,0 +1,101 @@
<?php
namespace HaydenPierce\ClassFinder\Files;
class FilesEntry
{
/** @var string */
private $file;
/** @var string */
private $php;
/**
* @param string $fileToInclude
* @param string $php
*/
public function __construct($fileToInclude, $php)
{
$this->file = $this->normalizePath($fileToInclude);
$this->php = $php;
}
/**
* @param string $namespace
* @return bool
*/
public function knowsNamespace($namespace)
{
$classes = $this->getClassesInFile();
foreach($classes as $class) {
if (strpos($class, $namespace) !== false) {
return true;
};
}
return false;
}
/**
* Gets a list of classes that belong to the given namespace.
*
* @param string $namespace
* @return string[]
*/
public function getClasses($namespace)
{
$classes = $this->getClassesInFile();
return array_values(array_filter($classes, function($class) use ($namespace) {
$classNameFragments = explode('\\', $class);
array_pop($classNameFragments);
$classNamespace = implode('\\', $classNameFragments);
$namespace = trim($namespace, '\\');
return $namespace === $classNamespace;
}));
}
/**
* Dynamically execute files and check for defined classes.
*
* This is where the real magic happens. Since classes in a randomly included file could contain classes in any namespace,
* (or even multiple namespaces!) we must execute the file and check for newly defined classes. This has a potential
* downside that files being executed will execute their side effects - which may be undesirable. However, Composer
* will require these files anyway - so hopefully causing those side effects isn't that big of a deal.
*
* @return array
*/
private function getClassesInFile()
{
// get_declared_classes() returns a bunch of classes that are built into PHP. So we need a control here.
$script = "var_export(get_declared_classes());";
exec($this->php . " -r \"$script\"", $output);
$classes = 'return ' . implode('', $output) . ';';
$initialClasses = eval($classes);
// clear the exec() buffer.
unset($output);
// This brings in the new classes. so $classes here will include the PHP defaults and the newly defined classes
$script = "require_once '{$this->file}'; var_export(get_declared_classes());";
exec($this->php . ' -r "' . $script . '"', $output);
$classes = 'return ' . implode('', $output) . ';';
$allClasses = eval($classes);
return array_diff($allClasses, $initialClasses);
}
/**
* TODO: Similar to PSR4Namespace::normalizePath. Maybe we refactor?
* @param string $path
* @return string
*/
private function normalizePath($path)
{
$path = str_replace('\\', '/', $path);
return $path;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace HaydenPierce\ClassFinder\Files;
use HaydenPierce\ClassFinder\AppConfig;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class FilesEntryFactory
{
/** @var AppConfig */
private $appConfig;
public function __construct(AppConfig $appConfig)
{
$this->appConfig = $appConfig;
}
/**
* @return FilesEntry[]
*/
public function getFilesEntries()
{
$files = require($this->appConfig->getAppRoot() . 'vendor/composer/autoload_files.php');
$phpPath = $this->findPHP();
$filesKeys = array_values($files);
return array_map(function($index) use ($filesKeys, $phpPath){
return new FilesEntry($filesKeys[$index], $phpPath);
}, range(0, count($files) - 1));
}
/**
* Locates the PHP interrupter.
*
* If PHP 5.4 or newer is used, the PHP_BINARY value is used.
* Otherwise we attempt to find it from shell commands.
*
* @return string
* @throws ClassFinderException
*/
private function findPHP()
{
if (defined("PHP_BINARY")) {
// PHP_BINARY was made available in PHP 5.4
$php = PHP_BINARY;
} else {
$isHostWindows = strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
if ($isHostWindows) {
exec('where php', $output);
$php = $output[0];
} else {
exec('which php', $output);
$php = $output[0];
}
}
if (!isset($php)) {
throw new ClassFinderException(sprintf(
'Could not locate PHP interrupter. See "%s" for details.',
'https://gitlab.com/hpierce1102/ClassFinder/blob/master/docs/exceptions/filesCouldNotLocatePHP.md'
));
}
return $php;
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace HaydenPierce\ClassFinder\Files;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
use HaydenPierce\ClassFinder\FinderInterface;
class FilesFinder implements FinderInterface
{
/** @var FilesEntryFactory */
private $factory;
/**
* @param FilesEntryFactory $factory
* @throws ClassFinderException
*/
public function __construct(FilesEntryFactory $factory)
{
$this->factory = $factory;
if (!function_exists('exec')) {
throw new ClassFinderException(sprintf(
'FilesFinder requires that exec() is available. Check your php.ini to see if it is disabled. See "%s" for details.',
'https://gitlab.com/hpierce1102/ClassFinder/blob/master/docs/exceptions/filesExecNotAvailable.md'
));
}
}
/**
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace)
{
$filesEntries = $this->factory->getFilesEntries();
foreach($filesEntries as $filesEntry) {
if ($filesEntry->knowsNamespace($namespace)) {
return true;
}
}
return false;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options)
{
$filesEntries = $this->factory->getFilesEntries();
return array_reduce($filesEntries, function($carry, FilesEntry $entry) use ($namespace){
return array_merge($carry, $entry->getClasses($namespace));
}, array());
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace HaydenPierce\ClassFinder;
interface FinderInterface
{
/**
* Find classes in a given namespace.
*
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options);
/**
* Check if a given namespace is known.
*
* A namespace is "known" if a Finder can determine that the autoloader can create classes from that namespace.
*
* For instance:
* If given a classmap for "TestApp1\Foo\Bar\Baz", the namespace "TestApp1\Foo" is known, even if nothing loads
* from that namespace directly. It is known because classes that include that namespace are known.
*
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace);
}

View File

@ -0,0 +1,106 @@
<?php
namespace HaydenPierce\ClassFinder\PSR4;
use HaydenPierce\ClassFinder\ClassFinder;
use HaydenPierce\ClassFinder\FinderInterface;
class PSR4Finder implements FinderInterface
{
/** @var PSR4NamespaceFactory */
private $factory;
public function __construct(PSR4NamespaceFactory $factory)
{
$this->factory = $factory;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options)
{
if ($options === ClassFinder::RECURSIVE_MODE) {
$applicableNamespaces = $this->findAllApplicableNamespaces($namespace);
}
if (empty($applicableNamespaces)) {
$bestNamespace = $this->findBestPSR4Namespace($namespace);
$applicableNamespaces = array($bestNamespace);
}
return array_reduce($applicableNamespaces, function($carry, $psr4NamespaceOrNull) use ($namespace, $options) {
if ($psr4NamespaceOrNull instanceof PSR4Namespace) {
$classes = $psr4NamespaceOrNull->findClasses($namespace, $options);
} else {
$classes = array();
}
return array_merge($carry, $classes);
}, array());
}
/**
* @param string $namespace
* @return bool
*/
public function isNamespaceKnown($namespace)
{
$composerNamespaces = $this->factory->getPSR4Namespaces();
foreach($composerNamespaces as $psr4Namespace) {
if ($psr4Namespace->knowsNamespace($namespace)) {
return true;
}
}
return false;
}
/**
* @param string $namespace
* @return PSR4Namespace[]
*/
private function findAllApplicableNamespaces($namespace)
{
$composerNamespaces = $this->factory->getPSR4Namespaces();
return array_filter($composerNamespaces, function(PSR4Namespace $potentialNamespace) use ($namespace){
return $potentialNamespace->isAcceptableNamespaceRecursiveMode($namespace);
});
}
/**
* @param string $namespace
* @return PSR4Namespace
*/
private function findBestPSR4Namespace($namespace)
{
$composerNamespaces = $this->factory->getPSR4Namespaces();
$acceptableNamespaces = array_filter($composerNamespaces, function(PSR4Namespace $potentialNamespace) use ($namespace){
return $potentialNamespace->isAcceptableNamespace($namespace);
});
$carry = new \stdClass();
$carry->highestMatchingSegments = 0;
$carry->bestNamespace = null;
/** @var PSR4Namespace $bestNamespace */
$bestNamespace = array_reduce($acceptableNamespaces, function ($carry, PSR4Namespace $potentialNamespace) use ($namespace) {
$matchingSegments = $potentialNamespace->countMatchingNamespaceSegments($namespace);
if ($matchingSegments > $carry->highestMatchingSegments) {
$carry->highestMatchingSegments = $matchingSegments;
$carry->bestNamespace = $potentialNamespace;
}
return $carry;
}, $carry);
return $bestNamespace->bestNamespace;
}
}

View File

@ -0,0 +1,302 @@
<?php
namespace HaydenPierce\ClassFinder\PSR4;
use HaydenPierce\ClassFinder\ClassFinder;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class PSR4Namespace
{
/** @var string */
private $namespace;
/** @var string[] */
private $directories;
/** @var PSR4Namespace[] */
private $directSubnamespaces;
/**
* @param string $namespace
* @param string[] $directories
*/
public function __construct($namespace, $directories)
{
$this->namespace = $namespace;
$this->directories = $directories;
}
/**
* @param string $namespace
* @return bool
*/
public function knowsNamespace($namespace)
{
$numberOfSegments = count(explode('\\', $namespace));
$matchingSegments = $this->countMatchingNamespaceSegments($namespace);
if ($matchingSegments === 0) {
// Provided namespace doesn't map to anything registered.
return false;
} elseif ($numberOfSegments <= $matchingSegments) {
// This namespace is a superset of the provided namespace. Namespace is known.
return true;
} else {
// This namespace is a subset of the provided namespace. We must resolve the remaining segments to a directory.
$relativePath = substr($namespace, strlen($this->namespace));
foreach ($this->directories as $directory) {
$path = $this->normalizePath($directory, $relativePath);
if (is_dir($path)) {
return true;
}
}
return false;
}
}
/**
* Determines how many namespace segments match the internal namespace. This is useful because multiple namespaces
* may technically match a registered namespace root, but one of the matches may be a better match. Namespaces that
* match, but are not _the best_ match are incorrect matches. TestApp1\\ is **not** the best match when searching for
* namespace TestApp1\\Multi\\Foo if TestApp1\\Multi was explicitly registered.
*
* PSR4Namespace $a;
* $a->namespace = "TestApp1\\";
* $a->countMatchingNamespaceSegments("TestApp1\\Multi") -> 1, TestApp1 matches.
*
* PSR4Namespace $b;
* $b->namespace = "TestApp1\\Multi";
* $b->countMatchingNamespaceSegments("TestApp1\\Multi") -> 2, TestApp1\\Multi matches
*
* PSR4Namespace $c;
* $c->namespace = "HaydenPierce\\Foo\\Bar";
* $c->countMatchingNamespaceSegments("TestApp1\\Multi") -> 0, No matches.
*
* @param string $namespace
* @return int
*/
public function countMatchingNamespaceSegments($namespace)
{
$namespaceFragments = explode('\\', $namespace);
$undefinedNamespaceFragments = array();
while($namespaceFragments) {
$possibleNamespace = implode('\\', $namespaceFragments) . '\\';
if(strpos($this->namespace, $possibleNamespace) !== false){
return count(explode('\\', $possibleNamespace)) - 1;
}
array_unshift($undefinedNamespaceFragments, array_pop($namespaceFragments));
}
return 0;
}
/**
* @param string $namespace
* @return bool
*/
public function isAcceptableNamespace($namespace)
{
$namespaceSegments = count(explode('\\', $this->namespace)) - 1;
$matchingSegments = $this->countMatchingNamespaceSegments($namespace);
return $namespaceSegments === $matchingSegments;
}
/**
* @param string $namespace
* @return bool
*/
public function isAcceptableNamespaceRecursiveMode($namespace)
{
// Remove prefix backslash (TODO: review if we do this eariler).
$namespace = ltrim($namespace, '\\');
return strpos($this->namespace, $namespace) === 0;
}
/**
* Used to identify subnamespaces.
*
* @return string[]
*/
public function findDirectories()
{
$self = $this;
$directories = array_reduce($this->directories, function($carry, $directory) use ($self){
$path = $self->normalizePath($directory, '');
$realDirectory = realpath($path);
if ($realDirectory !== false) {
return array_merge($carry, array($realDirectory));
} else {
return $carry;
}
}, array());
$arraysOfClasses = array_map(function($directory) use ($self) {
$files = scandir($directory);
return array_map(function($file) use ($directory, $self) {
return $self->normalizePath($directory, $file);
}, $files);
}, $directories);
$potentialDirectories = array_reduce($arraysOfClasses, function($carry, $arrayOfClasses) {
return array_merge($carry, $arrayOfClasses);
}, array());
// Remove '.' and '..' directories
$potentialDirectories = array_filter($potentialDirectories, function($potentialDirectory) {
$segments = explode('/', $potentialDirectory);
$lastSegment = array_pop($segments);
return $lastSegment !== '.' && $lastSegment !== '..';
});
$confirmedDirectories = array_filter($potentialDirectories, function($potentialDirectory) {
return is_dir($potentialDirectory);
});
return $confirmedDirectories;
}
/**
* @param string $namespace
* @param int $options
* @return string[]
*/
public function findClasses($namespace, $options = ClassFinder::STANDARD_MODE)
{
$relativePath = substr($namespace, strlen($this->namespace));
$self = $this;
$directories = array_reduce($this->directories, function($carry, $directory) use ($relativePath, $namespace, $self){
$path = $self->normalizePath($directory, $relativePath);
$realDirectory = realpath($path);
if ($realDirectory !== false) {
return array_merge($carry, array($realDirectory));
} else {
return $carry;
}
}, array());
$arraysOfClasses = array_map(function($directory) {
return scandir($directory);
}, $directories);
$potentialClassFiles = array_reduce($arraysOfClasses, function($carry, $arrayOfClasses) {
return array_merge($carry, $arrayOfClasses);
}, array());
$potentialClasses = array_map(function($file) use ($namespace){
return $namespace . '\\' . str_replace('.php', '', $file);
}, $potentialClassFiles);
if ($options == ClassFinder::RECURSIVE_MODE) {
return $this->getClassesFromListRecursively($namespace);
} else {
return array_filter($potentialClasses, function($potentialClass) {
if (function_exists($potentialClass)) {
// For some reason calling class_exists() on a namespace'd function raises a Fatal Error (tested PHP 7.0.8)
// Example: DeepCopy\deep_copy
return false;
} else {
return class_exists($potentialClass);
}
});
}
}
/**
* @return string[]
*/
private function getDirectClassesOnly()
{
$self = $this;
$directories = array_reduce($this->directories, function($carry, $directory) use ($self){
$path = $self->normalizePath($directory, '');
$realDirectory = realpath($path);
if ($realDirectory !== false) {
return array_merge($carry, array($realDirectory));
} else {
return $carry;
}
}, array());
$arraysOfClasses = array_map(function($directory) {
return scandir($directory);
}, $directories);
$potentialClassFiles = array_reduce($arraysOfClasses, function($carry, $arrayOfClasses) {
return array_merge($carry, $arrayOfClasses);
}, array());
$selfNamespace = $this->namespace; // PHP 5.3 BC
$potentialClasses = array_map(function($file) use ($self, $selfNamespace) {
return $selfNamespace . str_replace('.php', '', $file);
}, $potentialClassFiles);
return array_filter($potentialClasses, function($potentialClass) {
if (function_exists($potentialClass)) {
// For some reason calling class_exists() on a namespace'd function raises a Fatal Error (tested PHP 7.0.8)
// Example: DeepCopy\deep_copy
return false;
} else {
return class_exists($potentialClass);
}
});
}
/**
* @param string $namespace
* @return string[]
*/
public function getClassesFromListRecursively($namespace)
{
$initialClasses = strpos( $this->namespace, $namespace) !== false ? $this->getDirectClassesOnly() : array();
return array_reduce($this->getDirectSubnamespaces(), function($carry, PSR4Namespace $subNamespace) use ($namespace) {
return array_merge($carry, $subNamespace->getClassesFromListRecursively($namespace));
}, $initialClasses);
}
/**
* Join an absolute path and a relative path in a platform agnostic way.
*
* This method is also extracted so that it can be turned into a vfs:// stream URL for unit testing.
*
* @param string $directory
* @param string $relativePath
* @return mixed
*/
public function normalizePath($directory, $relativePath)
{
$path = str_replace('\\', '/', $directory . '/' . $relativePath);
return $path;
}
/**
* @return PSR4Namespace[]
*/
public function getDirectSubnamespaces()
{
return $this->directSubnamespaces;
}
/**
* @param PSR4Namespace[] $directSubnamespaces
*/
public function setDirectSubnamespaces($directSubnamespaces)
{
$this->directSubnamespaces = $directSubnamespaces;
}
/**
* @return mixed
*/
public function getNamespace()
{
return trim($this->namespace, '\\');
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace HaydenPierce\ClassFinder\PSR4;
use HaydenPierce\ClassFinder\AppConfig;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class PSR4NamespaceFactory
{
/** @var AppConfig */
private $appConfig;
public function __construct(AppConfig $appConfig)
{
$this->appConfig = $appConfig;
}
/**
* @return string[]
*/
public function getPSR4Namespaces()
{
$namespaces = $this->getUserDefinedPSR4Namespaces();
$vendorNamespaces = require($this->appConfig->getAppRoot() . 'vendor/composer/autoload_psr4.php');
$namespaces = array_merge($vendorNamespaces, $namespaces);
// There's some wackiness going on here for PHP 5.3 compatibility.
$names = array_keys($namespaces);
$directories = array_values($namespaces);
$self = $this;
$namespaces = array_map(function($index) use ($self, $names, $directories) {
return $self->createNamespace($names[$index], $directories[$index]);
},range(0, count($namespaces) - 1));
return $namespaces;
}
/**
* @return string[]
*/
private function getUserDefinedPSR4Namespaces()
{
$appRoot = $this->appConfig->getAppRoot();
$composerJsonPath = $appRoot . 'composer.json';
$composerConfig = json_decode(file_get_contents($composerJsonPath));
if (!isset($composerConfig->autoload)) {
return array();
}
//Apparently PHP doesn't like hyphens, so we use variable variables instead.
$psr4 = "psr-4";
return (array)$composerConfig->autoload->$psr4;
}
/**
* Creates a namespace from composer_psr4.php and composer.json autoload.psr4 items.
*
* @param string $namespace
* @param string[] $directories
* @return PSR4Namespace
* @throws ClassFinderException
*/
public function createNamespace($namespace, $directories)
{
if (is_string($directories)) {
// This is an acceptable format according to composer.json
$directories = array($directories);
} elseif (is_array($directories)) {
// composer_psr4.php seems to put everything in this format
} else {
throw new ClassFinderException('Unknown PSR4 definition.');
}
$self = $this;
$appConfig = $this->appConfig;
$directories = array_map(function($directory) use ($self, $appConfig) {
if ($self->isAbsolutePath($directory)) {
return $directory;
} else {
return $appConfig->getAppRoot() . $directory;
}
}, $directories);
$directories = array_filter(array_map(function($directory) {
return realpath($directory);
}, $directories));
$psr4Namespace = new PSR4Namespace($namespace, $directories);
$subNamespaces = $this->getSubnamespaces($psr4Namespace);
$psr4Namespace->setDirectSubnamespaces($subNamespaces);
return $psr4Namespace;
}
/**
* @param PSR4Namespace $psr4Namespace
* @return PSR4Namespace[]
*/
private function getSubnamespaces(PSR4Namespace $psr4Namespace)
{
// Scan it's own directories.
$directories = $psr4Namespace->findDirectories();
$self = $this;
$subnamespaces = array_map(function($directory) use ($self, $psr4Namespace){
$segments = explode('/', $directory);
$subnamespaceSegment = array_pop($segments);
$namespace = $psr4Namespace->getNamespace() . "\\" . $subnamespaceSegment . "\\";
return $self->createNamespace($namespace, $directory);
}, $directories);
return $subnamespaces;
}
/**
* Check if a path is absolute.
*
* Mostly this answer https://stackoverflow.com/a/38022806/3000068
* A few changes: Changed exceptions to be ClassFinderExceptions, removed some ctype dependencies,
* updated the root prefix regex to handle Window paths better.
*
* @param string $path
* @return bool
* @throws ClassFinderException
*/
public function isAbsolutePath($path) {
if (!is_string($path)) {
$mess = sprintf('String expected but was given %s', gettype($path));
throw new ClassFinderException($mess);
}
// Optional wrapper(s).
$regExp = '%^(?<wrappers>(?:[[:print:]]{2,}://)*)';
// Optional root prefix.
$regExp .= '(?<root>(?:[[:alpha:]]:[/\\\\]|/)?)';
// Actual path.
$regExp .= '(?<path>(?:[[:print:]]*))$%';
$parts = array();
if (!preg_match($regExp, $path, $parts)) {
$mess = sprintf('Path is NOT valid, was given %s', $path);
throw new ClassFinderException($mess);
}
if ('' !== $parts['root']) {
return true;
}
return false;
}
}

5
vendor/laragraph/utils/.styleci.yml vendored Normal file
View File

@ -0,0 +1,5 @@
preset: laravel
risky: true
enabled:
- declare_strict_types
- unalign_double_arrow

13
vendor/laragraph/utils/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,13 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 1.0.0
### Added
- Add `RequestParser` to convert an incoming HTTP request to one or more `OperationParams`

16
vendor/laragraph/utils/LICENSE vendored Normal file
View File

@ -0,0 +1,16 @@
The MIT License (MIT)
Copyright (c) 2019 Benedikt Franke
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

39
vendor/laragraph/utils/README.md vendored Normal file
View File

@ -0,0 +1,39 @@
# laragraph/utils
[![CI Status](https://github.com/laragraph/utils/workflows/Continuous%20Integration/badge.svg)](https://github.com/laragraph/utils/actions)
[![codecov](https://codecov.io/gh/laragraph/utils/branch/master/graph/badge.svg)](https://codecov.io/gh/laragraph/utils)
[![StyleCI](https://github.styleci.io/repos/228471198/shield?branch=master)](https://github.styleci.io/repos/228471198)
[![Latest Stable Version](https://poser.pugx.org/laragraph/utils/v/stable)](https://packagist.org/packages/laragraph/utils)
[![Total Downloads](https://poser.pugx.org/laragraph/utils/downloads)](https://packagist.org/packages/laragraph/utils)
Utilities for using GraphQL with Laravel
## Installation
Install through composer
```bash
composer require laragraph/utils
```
## Usage
This package holds basic utilities that are useful for building a GraphQL server with Laravel.
If you want to build an application, we recommend using a full framework that integrates the
primitives within this package:
- SDL-first: [Lighthouse](https://github.com/nuwave/lighthouse)
- Code-first: [graphql-laravel](https://github.com/rebing/graphql-laravel)
## Changelog
See [`CHANGELOG.md`](CHANGELOG.md).
## Contributing
See [`CONTRIBUTING.md`](.github/CONTRIBUTING.md).
## License
This package is licensed using the MIT License.

55
vendor/laragraph/utils/composer.json vendored Normal file
View File

@ -0,0 +1,55 @@
{
"name": "laragraph/utils",
"type": "library",
"description": "Utilities for using GraphQL with Laravel",
"homepage": "https://github.com/laragraph/utils",
"license": "MIT",
"authors": [
{
"name": "Benedikt Franke",
"email": "benedikt@franke.tech"
}
],
"require": {
"php": "^7.2 || ^8.0",
"illuminate/contracts": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/http": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"thecodingmachine/safe": "^1.1",
"webonyx/graphql-php": "^0.13.2 || ^14"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.11",
"infection/infection": "~0.20",
"jangregor/phpstan-prophecy": "^0.8.1",
"orchestra/testbench": "3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4 || ^5 || ^6",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12.57",
"phpstan/phpstan-deprecation-rules": "^0.12.5",
"phpstan/phpstan-strict-rules": "^0.12.5",
"phpunit/phpunit": "^7.5 || ^8.5",
"thecodingmachine/phpstan-safe-rule": "^1.0"
},
"config": {
"preferred-install": "dist",
"sort-packages": true
},
"autoload": {
"psr-4": {
"Laragraph\\Utils\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Laragraph\\Utils\\Tests\\": "tests/"
},
"files": [
"vendor/symfony/var-dumper/Resources/functions/dump.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"support": {
"issues": "https://github.com/laragraph/utils/issues",
"source": "https://github.com/laragraph/utils"
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Laragraph\Utils;
use GraphQL\Server\Helper;
use GraphQL\Server\OperationParams;
use GraphQL\Server\RequestError;
use GraphQL\Utils\Utils;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class RequestParser
{
/**
* @var \GraphQL\Server\Helper
*/
protected $helper;
public function __construct()
{
$this->helper = new Helper();
}
/**
* Converts an incoming HTTP request to one or more OperationParams.
*
* @return \GraphQL\Server\OperationParams|array<int, \GraphQL\Server\OperationParams>
*
* @throws \GraphQL\Server\RequestError
*/
public function parseRequest(Request $request)
{
$method = $request->getMethod();
$bodyParams = [];
/** @var array<string, mixed> $queryParams */
$queryParams = $request->query();
if ($method === 'POST') {
/**
* Never null, since Symfony defaults to application/x-www-form-urlencoded.
*
* @var string $contentType
*/
$contentType = $request->header('Content-Type');
if (stripos($contentType, 'application/json') !== false) {
/** @var string $content */
$content = $request->getContent();
$bodyParams = \Safe\json_decode($content, true);
if (! is_array($bodyParams)) {
throw new RequestError(
'GraphQL Server expects JSON object or array, but got '.
Utils::printSafeJson($bodyParams)
);
}
} elseif (stripos($contentType, 'application/graphql') !== false) {
/** @var string $content */
$content = $request->getContent();
$bodyParams = ['query' => $content];
} elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
/** @var array<string, mixed> $bodyParams */
$bodyParams = $request->post();
} elseif (stripos($contentType, 'multipart/form-data') !== false) {
$bodyParams = $this->inlineFiles($request);
} else {
throw new RequestError('Unexpected content type: '.Utils::printSafeJson($contentType));
}
}
return $this->helper->parseRequestParams($method, $bodyParams, $queryParams);
}
/**
* Inline file uploads given through a multipart request.
*
* @param \Illuminate\Http\Request $request
* @return array<mixed>
*/
protected function inlineFiles(Request $request): array
{
/** @var string|null $mapParam */
$mapParam = $request->post('map');
if ($mapParam === null) {
throw new RequestError(
'Could not find a valid map, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
);
}
/** @var string|null $operationsParam */
$operationsParam = $request->post('operations');
if ($operationsParam === null) {
throw new RequestError(
'Could not find valid operations, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
);
}
/** @var array<string, mixed>|array<int, array<string, mixed>> $operations */
$operations = \Safe\json_decode($operationsParam, true);
/** @var array<int|string, array<int, string>> $map */
$map = \Safe\json_decode($mapParam, true);
foreach ($map as $fileKey => $operationsPaths) {
/** @var array<string> $operationsPaths */
$file = $request->file((string) $fileKey);
/** @var string $operationsPath */
foreach ($operationsPaths as $operationsPath) {
Arr::set($operations, $operationsPath, $file);
}
}
return $operations;
}
}

View File

@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace Laragraph\Utils\Tests\Unit;
use GraphQL\Server\RequestError;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Laragraph\Utils\RequestParser;
use Orchestra\Testbench\TestCase;
use Safe\Exceptions\JsonException;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class RequestParserTest extends TestCase
{
public function testGetWithQuery(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest('GET', ['query' => $query]);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostWithJson(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/json'],
\Safe\json_encode(['query' => $query])
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostWithQueryApplicationGraphQL(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/graphql'],
$query
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostWithRegularForm(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
['query' => $query],
[],
['Content-Type' => 'application/x-www-form-urlencoded']
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostDefaultsToRegularForm(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
['query' => $query]
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testNonSensicalContentType(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'foobar']
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
public function testNoQuery(): void
{
$request = $this->makeRequest('GET');
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame(null, $params->query);
}
public function testInvalidJson(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/json'],
'this is not valid json'
);
$parser = new RequestParser();
$this->expectException(JsonException::class);
$parser->parseRequest($request);
}
public function testNonArrayJson(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/json'],
'"this should be a map with query, variables, etc."'
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
public function testMultipartFormRequest(): void
{
$file = UploadedFile::fake()->create('image.jpg', 500);
$request = $this->makeRequest(
'POST',
[
'operations' => /** @lang JSON */ '
{
"query": "mutation Upload($file: Upload!) { upload(file: $file) }",
"variables": {
"file": null
}
}
',
'map' => /** @lang JSON */ '
{
"0": ["variables.file"]
}
',
],
[
'0' => $file,
],
[
'Content-Type' => 'multipart/form-data',
]
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame('mutation Upload($file: Upload!) { upload(file: $file) }', $params->query);
$variables = $params->variables;
self::assertNotNull($variables);
/** @var array<string, mixed> $variables */
self::assertSame($file, $variables['file']);
}
public function testMultipartFormWithoutMap(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
[
'Content-Type' => 'multipart/form-data',
]
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
public function testMultipartFormWithoutOperations(): void
{
$request = $this->makeRequest(
'POST',
[
'map' => /** @lang JSON */ '
{
"0": ["variables.file"]
}
',
],
[],
[
'Content-Type' => 'multipart/form-data',
]
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
/**
* @param string $method
* @param array<mixed> $parameters
* @param array<mixed> $files
* @param array<mixed> $headers
* @param string|resource|null $content
* @return \Illuminate\Http\Request
*/
public function makeRequest(string $method, array $parameters = [], array $files = [], array $headers = [], $content = null): Request
{
$symfonyRequest = SymfonyRequest::create(
'http://foo.bar/graphql',
$method,
$parameters,
[],
$files,
$this->transformHeadersToServerVars($headers),
$content
);
return Request::createFromBase($symfonyRequest);
}
}

View File

@ -1,6 +1,6 @@
<div align="center">
<a href="https://www.lighthouse-php.com">
<img src="logo.png" alt=lighthouse-logo" width="150" height="150">
<img src="./logo.png" alt=lighthouse-logo" width="150" height="150">
</a>
</div>
@ -8,50 +8,59 @@
# Lighthouse
[![Build Status](https://travis-ci.org/nuwave/lighthouse.svg?branch=master)](https://travis-ci.org/nuwave/lighthouse)
[![codecov](https://codecov.io/gh/nuwave/lighthouse/branch/master/graph/badge.svg)](https://codecov.io/gh/nuwave/lighthouse)
[![Continuous Integration](https://github.com/nuwave/lighthouse/workflows/Continuous%20Integration/badge.svg)](https://github.com/nuwave/lighthouse/actions)
[![Code Coverage](https://codecov.io/gh/nuwave/lighthouse/branch/master/graph/badge.svg)](https://codecov.io/gh/nuwave/lighthouse)
[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
[![StyleCI](https://github.styleci.io/repos/59965104/shield?branch=master)](https://github.styleci.io/repos/59965104)
[![Packagist](https://img.shields.io/packagist/dt/nuwave/lighthouse.svg)](https://packagist.org/packages/nuwave/lighthouse)
[![GitHub license](https://img.shields.io/github/license/nuwave/lighthouse.svg)](https://github.com/nuwave/lighthouse/blob/master/LICENSE)
[![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://join.slack.com/t/lighthouse-php/shared_invite/enQtMzc1NzQwNTUxMjk3LWI1ZDQ1YWM1NmM2MmQ0NTU0NGNjZWFkMTJhY2VjMDAwZmMyZDFlZTc1Mjc3ZGY0MWM1Y2Q5MWNjYmJmYWJkYmU)
[![StyleCI](https://github.styleci.io/repos/59965104/shield?branch=master&style=flat)](https://github.styleci.io/repos/59965104)
[![Packagist](https://img.shields.io/packagist/dt/nuwave/lighthouse.svg)](https://packagist.org/packages/nuwave/lighthouse)
[![Latest Stable Version](https://poser.pugx.org/nuwave/lighthouse/v/stable)](https://packagist.org/packages/nuwave/lighthouse)
[![GitHub license](https://img.shields.io/github/license/nuwave/lighthouse.svg)](https://github.com/nuwave/lighthouse/blob/master/LICENSE)
[![Ask on Stack Overflow](https://img.shields.io/badge/StackOverflow-ask-orange.svg)](https://stackoverflow.com/questions/tagged/laravel-lighthouse)
[![Get on Slack](https://img.shields.io/badge/Slack-join-blueviolet.svg)](https://join.slack.com/t/lighthouse-php/shared_invite/zt-4sm280w1-wu21r94f3kLRRtBXRbXVfw)
**A framework for serving GraphQL from Laravel**
**GraphQL Server for Laravel**
</div>
Lighthouse is a PHP package that allows you to serve a GraphQL endpoint from your
Laravel application. It greatly reduces the boilerplate required to create a schema,
it integrates well with any Laravel project, and it's highly customizable
giving you full control over your data.
Lighthouse is a GraphQL framework that integrates with your Laravel application.
It takes the best ideas of both and combines them to solve common tasks with ease
and offer flexibility when you need it.
## [Documentation](https://lighthouse-php.com/)
## Documentation
The documentation lives at [lighthouse-php.com](https://lighthouse-php.com/).
If you like reading plain markdown, you can also find the source files in the [docs folder](/docs).
The site includes the latest docs for each major version of Lighthouse.
You can find docs for specific versions by looking at the contents of [/docs/master](/docs/master)
at that point in the git history: `https://github.com/nuwave/lighthouse/tree/<SPECIFIC-TAG>/docs/master`.
## Get started
If you have an existing Laravel project, all you really need
to get up and running is a few steps:
1. Install via `composer require nuwave/lighthouse`
2. Publish the default schema `php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider" --tag=schema`
3. Use something like [GraphQL Playground](https://github.com/mll-lab/laravel-graphql-playground) to explore your GraphQL endpoint
Check out [the docs](https://lighthouse-php.com/) to learn more.
A chinese translation is available at [lighthouse-php.cn](http://lighthouse-php.cn/) and is maintained
over at https://github.com/haxibiao/lighthouse.
## Get involved
We welcome contributions of any kind.
- Have a question? [Use the laravel-lighthouse tag on Stackoverflow](https://stackoverflow.com/questions/tagged/laravel-lighthouse)
- Talk to other users? [Hop into Slack](https://join.slack.com/t/lighthouse-php/shared_invite/enQtMzc1NzQwNTUxMjk3LWI1ZDQ1YWM1NmM2MmQ0NTU0NGNjZWFkMTJhY2VjMDAwZmMyZDFlZTc1Mjc3ZGY0MWM1Y2Q5MWNjYmJmYWJkYmU)
- Have a question? [Use the laravel-lighthouse tag on Stack Overflow](https://stackoverflow.com/questions/tagged/laravel-lighthouse)
- Talk to other users? [Hop into Slack](https://join.slack.com/t/lighthouse-php/shared_invite/zt-4sm280w1-wu21r94f3kLRRtBXRbXVfw)
- Found a bug? [Report a bug](https://github.com/nuwave/lighthouse/issues/new?template=bug_report.md)
- Need a feature? [Open a feature request](https://github.com/nuwave/lighthouse/issues/new?template=feature_request.md)
- Want to improve Lighthouse? [Read our contribution guidelines](https://github.com/nuwave/lighthouse/blob/master/.github/CONTRIBUTING.md)
- Have an idea? [Propose a feature](https://github.com/nuwave/lighthouse/issues/new?template=feature_proposal.md)
- Want to improve Lighthouse? [Read our contribution guidelines](https://github.com/nuwave/lighthouse/blob/master/CONTRIBUTING.md)
## Changelog
All notable changes to this project are documented in [`CHANGELOG.md`](CHANGELOG.md).
## Upgrade Guide
When upgrading between major versions of Lighthouse, consider [`UPGRADE.md`](UPGRADE.md).
## Contributing
We welcome contributions of any kind, see how in [`CONTRIBUTING.md`](CONTRIBUTING.md).
## Security Vulnerabilities
If you discover a security vulnerability within Lighthouse,
please email Benedikt Franke via [benedikt@franke.tech](mailto:benedikt@franke.tech).
please email Benedikt Franke via [benedikt@franke.tech](mailto:benedikt@franke.tech)
or visit https://tidelift.com/security.

122
vendor/nuwave/lighthouse/_ide_helper.php vendored Normal file
View File

@ -0,0 +1,122 @@
<?php
namespace Illuminate\Foundation\Testing {
class TestResponse
{
/**
* Asserts that the response contains an error from a given category.
*
* @param string $category The name of the expected error category.
* @return $this
*/
public function assertGraphQLErrorCategory(string $category): self
{
return $this;
}
/**
* Assert that the returned result contains exactly the given validation keys.
*
* @param array $keys The validation keys the result should have.
* @return $this
*/
public function assertGraphQLValidationKeys(array $keys): self
{
return $this;
}
/**
* Assert that a given validation error is present in the response.
*
* @param string $key The validation key that should be present.
* @param string $message The expected validation message.
* @return $this
*/
public function assertGraphQLValidationError(string $key, string $message): self
{
return $this;
}
/**
* Assert that no validation errors are present in the response.
*
* @return $this
*/
public function assertGraphQLValidationPasses(): self
{
return $this;
}
}
}
namespace Illuminate\Testing {
class TestResponse
{
/**
* Assert the response contains an error with the given message.
*
* @param string $message The expected error message.
* @return $this
*/
public function assertGraphQLErrorMessage(string $message): self
{
return $this;
}
/**
* Assert the response contains an error from the given category.
*
* @param string $category The name of the expected error category.
* @return $this
*/
public function assertGraphQLErrorCategory(string $category): self
{
return $this;
}
/**
* Assert the returned result contains exactly the given validation keys.
*
* @param array $keys The validation keys the result should have.
* @return $this
*/
public function assertGraphQLValidationKeys(array $keys): self
{
return $this;
}
/**
* Assert a given validation error is present in the response.
*
* @param string $key The validation key that should be present.
* @param string $message The expected validation message.
* @return $this
*/
public function assertGraphQLValidationError(string $key, string $message): self
{
return $this;
}
/**
* Assert no validation errors are present in the response.
*
* @return $this
*/
public function assertGraphQLValidationPasses(): self
{
return $this;
}
}
}
namespace GraphQL\Type\Definition {
class ResolveInfo
{
/**
* We monkey patch this onto here to pass it down the resolver chain.
*
* @var \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet
*/
public $argumentSet;
}
}

View File

@ -1,15 +1,14 @@
{
"name": "nuwave/lighthouse",
"description": "Lighthouse is a schema first GraphQL package for Laravel applications.",
"type": "library",
"description": "A framework for serving GraphQL from Laravel",
"keywords": [
"api",
"graphql",
"laravel",
"laravel-graphql"
],
"license": "MIT",
"homepage": "https://lighthouse-php.com",
"license": "MIT",
"authors": [
{
"name": "Christopher Moore",
@ -22,38 +21,71 @@
"homepage": "https://franke.tech"
}
],
"support": {
"issues": "https://github.com/nuwave/lighthouse/issues",
"source": "https://github.com/nuwave/lighthouse"
},
"require": {
"php": ">= 7.1",
"php": ">= 7.2",
"ext-json": "*",
"illuminate/contracts": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"illuminate/http": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"illuminate/pagination": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"illuminate/routing": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"illuminate/support": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"illuminate/validation": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"webonyx/graphql-php": "^0.13.2"
"haydenpierce/class-finder": "^0.4",
"illuminate/auth": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/bus": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/contracts": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/http": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/pagination": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/queue": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/routing": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/support": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/validation": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"laragraph/utils": "^1",
"thecodingmachine/safe": "^1",
"webonyx/graphql-php": "^14.7"
},
"require-dev": {
"bensampo/laravel-enum": "^1.22",
"laravel/lumen-framework": "5.5.*|5.6.*|5.7.*|5.8.*|^6.0",
"laravel/scout": "^4.0",
"mll-lab/graphql-php-scalars": "^2.1",
"mockery/mockery": "^1.0",
"orchestra/database": "3.5.*|3.6.*|3.7.*|3.8.*|3.9.*",
"orchestra/testbench": "3.5.*|3.6.*|3.7.*|3.8.*|3.9.*",
"phpbench/phpbench": "@dev",
"pusher/pusher-php-server": "^3.2",
"haydenpierce/class-finder": "^0.3.3"
"bensampo/laravel-enum": "^1.28.3 || ^2 || ^3",
"ergebnis/composer-normalize": "^2.2.2",
"finwe/phpstan-faker": "^0.1.0",
"laravel/framework": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"laravel/legacy-factories": "^1",
"laravel/lumen-framework": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"laravel/scout": "^7 || ^8",
"mll-lab/graphql-php-scalars": "^4",
"mockery/mockery": "^1",
"nunomaduro/larastan": "^0.6 || ^0.7",
"orchestra/testbench": "3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4 || ^5 || ^6",
"phpbench/phpbench": "1.0.0-alpha4",
"phpstan/phpstan": "0.12.89",
"phpstan/phpstan-mockery": "^0.12.5",
"phpstan/phpstan-phpunit": "^0.12.17",
"phpunit/phpunit": "^7.5 || ^8.4 || ^9",
"predis/predis": "^1.1",
"pusher/pusher-php-server": "^4 || ^5",
"rector/rector": "^0.9",
"thecodingmachine/phpstan-safe-rule": "^1",
"vimeo/psalm": "^4.7"
},
"suggest": {
"bensampo/laravel-enum": "Convenient enum definitions that can easily be registered in your Schema",
"laravel/scout": "Required for the @search directive",
"mll-lab/graphql-php-scalars": "Useful scalar types, required for @whereConstraints",
"mll-lab/graphql-php-scalars": "Useful scalar types, required for @whereConditions",
"mll-lab/laravel-graphql-playground": "GraphQL IDE for better development workflow - integrated with Laravel",
"bensampo/laravel-enum": "Convenient enum definitions that can easily be registered in your Schema"
"pusher/pusher-php-server": "Required when using the Pusher Subscriptions driver"
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"aliases": {
"graphql": "Nuwave\\Lighthouse\\GraphQL"
},
"providers": [
"Nuwave\\Lighthouse\\LighthouseServiceProvider",
"Nuwave\\Lighthouse\\GlobalId\\GlobalIdServiceProvider",
"Nuwave\\Lighthouse\\OrderBy\\OrderByServiceProvider",
"Nuwave\\Lighthouse\\Pagination\\PaginationServiceProvider",
"Nuwave\\Lighthouse\\Scout\\ScoutServiceProvider",
"Nuwave\\Lighthouse\\SoftDeletes\\SoftDeletesServiceProvider",
"Nuwave\\Lighthouse\\Validation\\ValidationServiceProvider"
]
}
},
"autoload": {
"psr-4": {
@ -67,24 +99,15 @@
}
},
"scripts": {
"test": "phpunit --colors=always",
"test:unit": "phpunit --colors=always --testsuite Unit",
"test:integration": "phpunit --colors=always --testsuite Integration",
"bench": "phpbench run",
"rector": "rector process -v src/ tests/",
"stan": "phpstan analyse --memory-limit 2048M",
"bench": "phpbench run"
"test": "phpunit --colors=always",
"test:integration": "phpunit --colors=always --testsuite Integration",
"test:unit": "phpunit --colors=always --testsuite Unit"
},
"extra": {
"laravel": {
"providers": [
"Nuwave\\Lighthouse\\LighthouseServiceProvider",
"Nuwave\\Lighthouse\\SoftDeletes\\SoftDeletesServiceProvider"
],
"aliases": {
"graphql": "Nuwave\\Lighthouse\\GraphQL"
}
}
},
"config": {
"sort-packages": true
"support": {
"issues": "https://github.com/nuwave/lighthouse/issues",
"source": "https://github.com/nuwave/lighthouse"
}
}

View File

@ -1,240 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Route Configuration
|--------------------------------------------------------------------------
|
| Controls the HTTP route that your GraphQL server responds to.
| You may set `route` => false, to disable the default route
| registration and take full control.
|
*/
'route' => [
/*
* The URI the endpoint responds to, e.g. mydomain.com/graphql.
*/
'uri' => 'graphql',
/*
* Lighthouse creates a named route for convenient URL generation and redirects.
*/
'name' => 'graphql',
/*
*
* Beware that middleware defined here runs before the GraphQL execution phase,
* so you have to take extra care to return spec-compliant error responses.
* To apply middleware on a field level, use the @middleware directive.
*/
'middleware' => [
\Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
],
],
/*
|--------------------------------------------------------------------------
| Schema Declaration
|--------------------------------------------------------------------------
|
| This is a path that points to where your GraphQL schema is located
| relative to the app path. You should define your entire GraphQL
| schema in this file (additional files may be imported).
|
*/
'schema' => [
'register' => base_path('graphql/schema.graphql'),
],
/*
|--------------------------------------------------------------------------
| Schema Cache
|--------------------------------------------------------------------------
|
| A large part of schema generation consists of parsing and AST manipulation.
| This operation is very expensive, so it is highly recommended to enable
| caching of the final schema to optimize performance of large schemas.
|
*/
'cache' => [
'enable' => env('LIGHTHOUSE_CACHE_ENABLE', true),
'key' => env('LIGHTHOUSE_CACHE_KEY', 'lighthouse-schema'),
'ttl' => env('LIGHTHOUSE_CACHE_TTL', null),
],
/*
|--------------------------------------------------------------------------
| Namespaces
|--------------------------------------------------------------------------
|
| These are the default namespaces where Lighthouse looks for classes
| that extend functionality of the schema. You may pass either a string
| or an array, they are tried in order and the first match is used.
|
*/
'namespaces' => [
'models' => ['App', 'App\\Models'],
'queries' => 'App\\GraphQL\\Queries',
'mutations' => 'App\\GraphQL\\Mutations',
'subscriptions' => 'App\\GraphQL\\Subscriptions',
'interfaces' => 'App\\GraphQL\\Interfaces',
'unions' => 'App\\GraphQL\\Unions',
'scalars' => 'App\\GraphQL\\Scalars',
'directives' => ['App\\GraphQL\\Directives'],
],
/*
|--------------------------------------------------------------------------
| Security
|--------------------------------------------------------------------------
|
| Control how Lighthouse handles security related query validation.
| This configures the options from http://webonyx.github.io/graphql-php/security/
|
*/
'security' => [
'max_query_complexity' => \GraphQL\Validator\Rules\QueryComplexity::DISABLED,
'max_query_depth' => \GraphQL\Validator\Rules\QueryDepth::DISABLED,
'disable_introspection' => \GraphQL\Validator\Rules\DisableIntrospection::DISABLED,
],
/*
|--------------------------------------------------------------------------
| Pagination
|--------------------------------------------------------------------------
|
| Limits the maximum "count" that users may pass as an argument
| to fields that are paginated with the @paginate directive.
| A setting of "null" means the count is unrestricted.
|
*/
'paginate_max_count' => null,
/*
|--------------------------------------------------------------------------
| Pagination Amount Argument
|--------------------------------------------------------------------------
|
| Set the name to use for the generated argument on paginated fields
| that controls how many results are returned.
| This setting will be removed in v5.
|
*/
'pagination_amount_argument' => 'first',
/*
|--------------------------------------------------------------------------
| Debug
|--------------------------------------------------------------------------
|
| Control the debug level as described in http://webonyx.github.io/graphql-php/error-handling/
| Debugging is only applied if the global Laravel debug config is set to true.
|
*/
'debug' => \GraphQL\Error\Debug::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\Debug::INCLUDE_TRACE,
/*
|--------------------------------------------------------------------------
| Error Handlers
|--------------------------------------------------------------------------
|
| Register error handlers that receive the Errors that occur during execution
| and handle them. You may use this to log, filter or format the errors.
| The classes must implement \Nuwave\Lighthouse\Execution\ErrorHandler
|
*/
'error_handlers' => [
\Nuwave\Lighthouse\Execution\ExtensionErrorHandler::class,
],
/*
|--------------------------------------------------------------------------
| Global ID
|--------------------------------------------------------------------------
|
| The name that is used for the global id field on the Node interface.
| When creating a Relay compliant server, this must be named "id".
|
*/
'global_id_field' => 'id',
/*
|--------------------------------------------------------------------------
| Batched Queries
|--------------------------------------------------------------------------
|
| GraphQL query batching means sending multiple queries to the server in one request,
| You may set this flag to either process or deny batched queries.
|
*/
'batched_queries' => true,
/*
|--------------------------------------------------------------------------
| Transactional Mutations
|--------------------------------------------------------------------------
|
| Sets default setting for transactional mutations.
| You may set this flag to have @create|@update mutations transactional or not.
|
*/
'transactional_mutations' => true,
/*
|--------------------------------------------------------------------------
| GraphQL Subscriptions
|--------------------------------------------------------------------------
|
| Here you can define GraphQL subscription "broadcasters" and "storage" drivers
| as well their required configuration options.
|
*/
'subscriptions' => [
/*
* Determines if broadcasts should be queued by default.
*/
'queue_broadcasts' => env('LIGHTHOUSE_QUEUE_BROADCASTS', true),
/*
* Default subscription storage.
*
* Any Laravel supported cache driver options are available here.
*/
'storage' => env('LIGHTHOUSE_SUBSCRIPTION_STORAGE', 'redis'),
/*
* Default subscription broadcaster.
*/
'broadcaster' => env('LIGHTHOUSE_BROADCASTER', 'pusher'),
/*
* Subscription broadcasting drivers with config options.
*/
'broadcasters' => [
'log' => [
'driver' => 'log',
],
'pusher' => [
'driver' => 'pusher',
'routes' => \Nuwave\Lighthouse\Subscriptions\SubscriptionRouter::class.'@pusher',
'connection' => 'pusher',
],
],
],
];

29
vendor/nuwave/lighthouse/rector.php vendored Normal file
View File

@ -0,0 +1,29 @@
<?php
use Rector\Core\Configuration\Option;
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedParameterRector;
use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set(Option::SETS, [
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::PHPUNIT_EXCEPTION,
SetList::PHPUNIT_SPECIFIC_METHOD,
SetList::PHPUNIT_YIELD_DATA_PROVIDER,
]);
$parameters->set(Option::SKIP, [
// Does not fit autoloading standards
__DIR__.'/tests/database/migrations',
// Gets stuck on WhereConditionsBaseDirective for some reason
__DIR__.'/src/WhereConditions',
// Having unused parameters can increase clarity, e.g. in event handlers
RemoveUnusedParameterRector::class,
]);
};

View File

@ -0,0 +1,79 @@
<?php
namespace Nuwave\Lighthouse\ClientDirectives;
use GraphQL\Executor\Values;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\GraphQL;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
/**
* Provides information about where client directives
* were placed in the query and what arguments were given to them.
*
* TODO implement accessors for other locations https://spec.graphql.org/draft/#ExecutableDirectiveLocation
*/
class ClientDirective
{
/**
* @var string
*/
protected $name;
/**
* @var \GraphQL\Type\Definition\Directive|null
*/
protected $definition;
public function __construct(string $name)
{
$this->name = $name;
}
/**
* Get the given values for a client directive.
*
* This returns an array of the given arguments for all field nodes.
* The number of items in the returned result will always be equivalent
* to the number of field nodes, each having one of the following values:
* - When a field node does not have the directive on it: null
* - When the directive is present but has no arguments: []
* - When the directive is present with arguments: an associative array
*
* @return array<array<string, mixed>|null>
*/
public function forField(ResolveInfo $resolveInfo): array
{
$directive = $this->definition();
$arguments = [];
foreach ($resolveInfo->fieldNodes as $fieldNode) {
$arguments [] = Values::getDirectiveValues($directive, $fieldNode, $resolveInfo->variableValues);
}
return $arguments;
}
/**
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
protected function definition(): Directive
{
if ($this->definition !== null) {
return $this->definition;
}
/** @var \Nuwave\Lighthouse\Schema\SchemaBuilder $schemaBuilder */
$schemaBuilder = app(SchemaBuilder::class);
$schema = $schemaBuilder->schema();
$definition = $schema->getDirective($this->name);
if ($definition === null) {
throw new DefinitionException("Missing a schema definition for the client directive $this->name");
}
return $this->definition = $definition;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\Command;
use Nuwave\Lighthouse\Schema\AST\ASTBuilder;
class CacheCommand extends Command
{
protected $name = 'lighthouse:cache';
protected $description = 'Compile the GraphQL schema and cache it.';
public function handle(ASTBuilder $builder): void
{
$builder->documentAST();
$this->info('GraphQL schema cache created.');
}
}

View File

@ -3,33 +3,45 @@
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Cache\Factory as CacheFactory;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Filesystem\Filesystem;
use Nuwave\Lighthouse\Exceptions\UnknownCacheVersionException;
class ClearCacheCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
* TODO remove once we require Laravel 6 which allows $this->call(ClearCacheCommand::class).
*/
protected $signature = 'lighthouse:clear-cache';
const NAME = 'lighthouse:clear-cache';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear the cache for the GraphQL AST.';
protected $name = self::NAME;
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @return void
*/
public function handle(Repository $cache): void
protected $description = 'Clear the GraphQL schema cache.';
public function handle(ConfigRepository $config): void
{
$cache->forget(config('lighthouse.cache.key'));
$version = $config->get('lighthouse.cache.version', 1);
switch ($version) {
case 1:
/** @var \Illuminate\Contracts\Cache\Factory $cacheFactory */
$cacheFactory = app(CacheFactory::class);
$cacheFactory
->store($config->get('lighthouse.cache.store'))
->forget($config->get('lighthouse.cache.key'));
break;
case 2:
/** @var \Illuminate\Filesystem\Filesystem $filesystem */
$filesystem = app(Filesystem::class);
$path = $config->get('lighthouse.cache.path')
?? base_path('bootstrap/cache/lighthouse-schema.php');
$filesystem->delete($path);
break;
default:
throw new UnknownCacheVersionException($version);
}
$this->info('GraphQL AST schema cache deleted.');
}

View File

@ -0,0 +1,228 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Nuwave\Lighthouse\Support\Contracts\ArgBuilderDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgDirective;
use Nuwave\Lighthouse\Support\Contracts\ArgDirectiveForArray;
use Nuwave\Lighthouse\Support\Contracts\ArgManipulator;
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
use Nuwave\Lighthouse\Support\Contracts\ArgTransformerDirective;
use Nuwave\Lighthouse\Support\Contracts\FieldManipulator;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\FieldResolver;
use Nuwave\Lighthouse\Support\Contracts\TypeExtensionManipulator;
use Nuwave\Lighthouse\Support\Contracts\TypeManipulator;
use Nuwave\Lighthouse\Support\Contracts\TypeMiddleware;
use Nuwave\Lighthouse\Support\Contracts\TypeResolver;
use Symfony\Component\Console\Input\InputOption;
class DirectiveCommand extends LighthouseGeneratorCommand
{
const ARGUMENT_INTERFACES = [
ArgTransformerDirective::class,
ArgBuilderDirective::class,
ArgResolver::class,
ArgManipulator::class,
];
const FIELD_INTERFACES = [
FieldResolver::class,
FieldMiddleware::class,
FieldManipulator::class,
];
const TYPE_INTERFACES = [
TypeManipulator::class,
TypeMiddleware::class,
TypeResolver::class,
TypeExtensionManipulator::class,
];
protected $name = 'lighthouse:directive';
protected $description = 'Create a class for a custom schema directive.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Directive';
/**
* The required imports.
*
* @var \Illuminate\Support\Collection<string>
*/
protected $imports;
/**
* The implemented interfaces.
*
* @var \Illuminate\Support\Collection<string>
*/
protected $implements;
/**
* The method stubs.
*
* @var \Illuminate\Support\Collection<string>
*/
protected $methods;
protected function getNameInput(): string
{
return parent::getNameInput().'Directive';
}
protected function namespaceConfigKey(): string
{
return 'directives';
}
/**
* @param string $name
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function buildClass($name): string
{
$this->imports = new Collection();
$this->implements = new Collection();
$this->methods = new Collection();
$stub = parent::buildClass($name);
$forType = $this->option('type');
$forField = $this->option('field');
$forArgument = $this->option('argument');
if (! $forType && ! $forField && ! $forArgument) {
throw new \Exception('Must specify at least one of: --type, --field or --argument');
}
if ($forType) {
$this->askForInterfaces(self::TYPE_INTERFACES);
}
if ($forField) {
$this->askForInterfaces(self::FIELD_INTERFACES);
}
if ($forArgument) {
// Arg directives always either implement ArgDirective or ArgDirectiveForArray.
if ($this->confirm('Will your argument directive apply to a list of items?')) {
$this->implementInterface(ArgDirectiveForArray::class);
} else {
$this->implementInterface(ArgDirective::class);
}
$this->askForInterfaces(self::ARGUMENT_INTERFACES);
}
$stub = str_replace(
'{{ imports }}',
$this->imports
->filter()
->unique()
->implode("\n"),
$stub
);
$stub = str_replace(
'{{ methods }}',
$this->methods->implode("\n"),
$stub
);
return str_replace(
'{{ implements }}',
$this->implements->implode(', '),
$stub
);
}
/**
* Ask the user if the directive should implement any of the given interfaces.
*
* @param array<class-string> $interfaces
*/
protected function askForInterfaces(array $interfaces): void
{
foreach ($interfaces as $interface) {
if ($this->confirm("Should the directive implement the {$this->shortName($interface)} middleware?")) {
$this->implementInterface($interface);
}
}
}
/**
* @param class-string $interface
*/
protected function shortName(string $interface): string
{
return Str::afterLast($interface, '\\');
}
/**
* @param class-string $interface
*/
protected function implementInterface(string $interface): void
{
$shortName = $this->shortName($interface);
$this->implements->push($shortName);
$this->imports->push("use {$interface};");
if ($imports = $this->interfaceImports($shortName)) {
$imports = explode("\n", $imports);
$this->imports->push(...$imports);
}
if ($methods = $this->interfaceMethods($shortName)) {
$this->methods->push($methods);
}
}
protected function getStub(): string
{
return __DIR__.'/stubs/directive.stub';
}
protected function interfaceMethods(string $interface): ?string
{
return $this->getFileIfExists(
__DIR__.'/stubs/directives/'.Str::snake($interface).'_methods.stub'
);
}
protected function interfaceImports(string $interface): ?string
{
return $this->getFileIfExists(
__DIR__.'/stubs/directives/'.Str::snake($interface).'_imports.stub'
);
}
protected function getFileIfExists(string $path): ?string
{
if (! $this->files->exists($path)) {
return null;
}
return $this->files->get($path);
}
/**
* @return array<int, array<int, mixed>>
*/
protected function getOptions(): array
{
return [
['type', null, InputOption::VALUE_NONE, 'Create a directive that can be applied to types.'],
['field', null, InputOption::VALUE_NONE, 'Create a directive that can be applied to fields.'],
['argument', null, InputOption::VALUE_NONE, 'Create a directive that can be applied to arguments.'],
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Nuwave\Lighthouse\Console;
use Symfony\Component\Console\Input\InputOption;
abstract class FieldGeneratorCommand extends LighthouseGeneratorCommand
{
protected function getStub(): string
{
$stub = $this->option('full')
? 'field_full'
: 'field_simple';
return __DIR__."/stubs/{$stub}.stub";
}
/**
* @return array<int, array<int, mixed>>
*/
protected function getOptions(): array
{
return [
['full', 'F', InputOption::VALUE_NONE, 'Include the seldom needed resolver arguments $context and $resolveInfo'],
];
}
}

View File

@ -2,88 +2,91 @@
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\Command;
use GraphQL\Type\Definition\Type;
use GraphQL\Utils\SchemaPrinter;
use HaydenPierce\ClassFinder\ClassFinder;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Schema\DirectiveNamespacer;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Nuwave\Lighthouse\Support\Contracts\Directive;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use HaydenPierce\ClassFinder\Exception\ClassFinderException;
class IdeHelperCommand extends Command
{
const GENERATED_NOTICE = <<<'SDL'
public const OPENING_PHP_TAG = /** @lang GraphQL */ "<?php\n";
public const GENERATED_NOTICE = /** @lang GraphQL */ <<<'GRAPHQL'
# File generated by "php artisan lighthouse:ide-helper".
# Do not edit this file directly.
# This file should be ignored by git.
# This file should be ignored by git as it can be autogenerated.
SDL;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lighthouse:ide-helper';
GRAPHQL;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Gather all schema directive definitions and write them to a file.';
protected $name = 'lighthouse:ide-helper';
/**
* Execute the console command.
*
* @param \Nuwave\Lighthouse\Schema\DirectiveNamespacer $directiveNamespaces
* @return int
*/
public function handle(DirectiveNamespacer $directiveNamespaces): int
protected $description = 'Create IDE helper files to improve type checking and autocompletion.';
public function handle(DirectiveLocator $directiveLocator, TypeRegistry $typeRegistry): int
{
if (! class_exists('HaydenPierce\ClassFinder\ClassFinder')) {
$this->error(
"This command requires haydenpierce/class-finder. Install it by running:\n"
."\n"
." composer require --dev haydenpierce/class-finder\n"
);
$this->schemaDirectiveDefinitions($directiveLocator);
$this->programmaticTypes($typeRegistry);
$this->phpIdeHelper();
return 1;
}
$directiveClasses = $this->scanForDirectives(
$directiveNamespaces->gather()
);
$schema = $this->buildSchemaString($directiveClasses);
$filePath = static::filePath();
file_put_contents($filePath, $schema);
$this->info("Wrote schema directive definitions to $filePath.");
$this->info("\nIt is recommended to add them to your .gitignore file.");
return 0;
}
/**
* Create and write schema directive definitions to a file.
*/
protected function schemaDirectiveDefinitions(DirectiveLocator $directiveLocator): void
{
$schema = /** @lang GraphQL */ <<<'GRAPHQL'
"""
Placeholder type for various directives such as `@orderBy`.
Will be replaced by a generated type during schema manipulation.
"""
scalar _
GRAPHQL;
$directiveClasses = $this->scanForDirectives(
$directiveLocator->namespaces()
);
foreach ($directiveClasses as $directiveClass) {
$definition = $this->define($directiveClass);
$schema .= /** @lang GraphQL */ <<<GRAPHQL
# Directive class: $directiveClass
$definition
GRAPHQL;
}
$filePath = static::schemaDirectivesPath();
\Safe\file_put_contents($filePath, self::GENERATED_NOTICE.$schema);
$this->info("Wrote schema directive definitions to $filePath.");
}
/**
* Scan the given namespaces for directive classes.
*
* @param string[] $directiveNamespaces
* @return string[]
* @param array<string> $directiveNamespaces
* @return array<string, class-string<\Nuwave\Lighthouse\Support\Contracts\Directive>>
*/
protected function scanForDirectives(array $directiveNamespaces): array
{
$directives = [];
foreach ($directiveNamespaces as $directiveNamespace) {
try {
$classesInNamespace = ClassFinder::getClassesInNamespace($directiveNamespace);
} catch (ClassFinderException $classFinderException) {
// TODO remove if https://gitlab.com/hpierce1102/ClassFinder/merge_requests/16 is merged
// The ClassFinder throws if no classes are found. Since we can not know
// in advance if the user has defined custom directives, this behaviour is problematic.
continue;
}
/** @var array<class-string> $classesInNamespace */
$classesInNamespace = ClassFinder::getClassesInNamespace($directiveNamespace);
foreach ($classesInNamespace as $class) {
$reflection = new \ReflectionClass($class);
@ -94,10 +97,7 @@ SDL;
if (! is_a($class, Directive::class, true)) {
continue;
}
/** @var \Nuwave\Lighthouse\Support\Contracts\Directive $instance */
$instance = app($class);
$name = $instance->name();
$name = DirectiveLocator::directiveName($class);
// The directive was already found, so we do not add it twice
if (isset($directives[$name])) {
@ -112,42 +112,77 @@ SDL;
}
/**
* @param string[] $directiveClasses
* @return string
* @param class-string<\Nuwave\Lighthouse\Support\Contracts\Directive> $directiveClass
* @throws \Nuwave\Lighthouse\Exceptions\DefinitionException
*/
protected function buildSchemaString(array $directiveClasses): string
protected function define(string $directiveClass): string
{
$schema = self::GENERATED_NOTICE;
$definition = $directiveClass::definition();
foreach ($directiveClasses as $name => $directiveClass) {
$definition = $this->define($name, $directiveClass);
// Throws if the definition is invalid
ASTHelper::extractDirectiveDefinition($definition);
$schema .= "\n"
."# Directive class: $directiveClass\n"
.$definition."\n";
}
return $schema;
return trim($definition);
}
protected function define(string $name, string $directiveClass): string
{
if (is_a($directiveClass, DefinedDirective::class, true)) {
/** @var DefinedDirective $directiveClass */
$definition = $directiveClass::definition();
// This operation throws if the schema definition is invalid
PartialParser::directiveDefinition($definition);
return trim($definition);
} else {
return '# Add a proper definition by implementing '.DefinedDirective::class."\n"
."directive @{$name}";
}
}
public static function filePath(): string
public static function schemaDirectivesPath(): string
{
return base_path().'/schema-directives.graphql';
}
protected function programmaticTypes(TypeRegistry $typeRegistry): void
{
// Users may register types programmatically, e.g. in service providers
// In order to allow referencing those in the schema, it is useful to print
// those types to a helper schema, excluding types the user defined in the schema
$types = new Collection($typeRegistry->resolvedTypes());
$filePath = static::programmaticTypesPath();
if ($types->isEmpty() && file_exists($filePath)) {
\Safe\unlink($filePath);
return;
}
$schema = $types
->map(function (Type $type): string {
return SchemaPrinter::printType($type);
})
->implode("\n");
\Safe\file_put_contents($filePath, self::GENERATED_NOTICE.$schema);
$this->info("Wrote definitions for programmatically registered types to $filePath.");
}
public static function programmaticTypesPath(): string
{
return base_path().'/programmatic-types.graphql';
}
protected function phpIdeHelper(): void
{
$filePath = static::phpIdeHelperPath();
$contents = \Safe\file_get_contents(__DIR__.'/../../_ide_helper.php');
\Safe\file_put_contents($filePath, $this->withGeneratedNotice($contents));
$this->info("Wrote PHP definitions to $filePath.");
}
public static function phpIdeHelperPath(): string
{
return base_path().'/_lighthouse_ide_helper.php';
}
protected function withGeneratedNotice(string $phpContents): string
{
return substr_replace(
$phpContents,
self::OPENING_PHP_TAG.self::GENERATED_NOTICE,
0,
strlen(self::OPENING_PHP_TAG)
);
}
}

View File

@ -4,43 +4,17 @@ namespace Nuwave\Lighthouse\Console;
class InterfaceCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:interface';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a GraphQL interface type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Interface';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
protected function namespaceConfigKey(): string
{
return config('lighthouse.namespaces.interfaces');
return 'interfaces';
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/typeResolver.stub';

View File

@ -3,6 +3,7 @@
namespace Nuwave\Lighthouse\Console;
use Illuminate\Console\GeneratorCommand;
use InvalidArgumentException;
abstract class LighthouseGeneratorCommand extends GeneratorCommand
{
@ -12,11 +13,83 @@ abstract class LighthouseGeneratorCommand extends GeneratorCommand
* As a typical workflow would be to write the schema first and then copy-paste
* a field name to generate a class for it, we uppercase it so the user does
* not run into unnecessary errors. You're welcome.
*
* @return string
*/
protected function getNameInput(): string
{
return ucfirst(trim($this->argument('name')));
$name = $this->argument('name');
if (! is_string($name)) {
throw new InvalidArgumentException('You must the name for the class to generate.');
}
return ucfirst(trim($name));
}
/**
* @param string $rootNamespace
*/
protected function getDefaultNamespace($rootNamespace): string
{
$namespaces = config('lighthouse.namespaces.'.$this->namespaceConfigKey());
return static::commonNamespace((array) $namespaces);
}
/**
* Get the config key that holds the default namespaces for the class.
*/
abstract protected function namespaceConfigKey(): string;
/**
* Find the common namespace of a list of namespaces.
*
* For example, ['App\\Foo\\A', 'App\\Foo\\B'] would return 'App\\Foo'.
*
* @param array<string> $namespaces
*/
public static function commonNamespace(array $namespaces): string
{
if ($namespaces === []) {
throw new InvalidArgumentException(
'A default namespace is required for code generation.'
);
}
if (count($namespaces) === 1) {
return reset($namespaces);
}
// Save the first namespace
$preferredNamespaceFallback = reset($namespaces);
// If the strings are sorted, any prefix common to all strings
// will be common to the sorted first and last strings.
// All the strings in the middle can be ignored.
\Safe\sort($namespaces);
$firstParts = explode('\\', reset($namespaces));
$lastParts = explode('\\', end($namespaces));
$matching = [];
foreach ($firstParts as $i => $part) {
// We ran out of elements to compare, so we reached the maximum common length
if (! isset($lastParts[$i])) {
break;
}
// We found an element that differs
if ($lastParts[$i] !== $part) {
break;
}
$matching [] = $part;
}
// We could not determine a common part of the configured namespaces,
// so we just assume the user will prefer the first one in the list.
if ($matching === []) {
return $preferredNamespaceFallback;
}
return implode('\\', $matching);
}
}

View File

@ -2,47 +2,16 @@
namespace Nuwave\Lighthouse\Console;
class MutationCommand extends LighthouseGeneratorCommand
class MutationCommand extends FieldGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:mutation';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a single field on the root Mutation type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Mutation';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
protected function namespaceConfigKey(): string
{
return config('lighthouse.namespaces.mutations');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/field.stub';
return 'mutations';
}
}

View File

@ -2,53 +2,62 @@
namespace Nuwave\Lighthouse\Console;
use Nuwave\Lighthouse\GraphQL;
use Illuminate\Console\Command;
use GraphQL\Type\Introspection;
use GraphQL\Type\Schema;
use GraphQL\Utils\SchemaPrinter;
use Illuminate\Cache\Repository;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
class PrintSchemaCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = '
lighthouse:print-schema
{--W|write : Write the output to a file}
';
public const GRAPHQL_FILENAME = 'lighthouse-schema.graphql';
public const JSON_FILENAME = 'lighthouse-schema.json';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Compile the final GraphQL schema and print the result.';
protected $signature = <<<'SIGNATURE'
lighthouse:print-schema
{--W|write : Write the output to a file}
{--json : Output JSON instead of GraphQL SDL}
SIGNATURE;
/**
* Execute the console command.
*
* @param \Illuminate\Cache\Repository $cache
* @param \Illuminate\Contracts\Filesystem\Filesystem $storage
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function handle(Repository $cache, Filesystem $storage, GraphQL $graphQL): void
protected $description = 'Compile the GraphQL schema and print the result.';
public function handle(Filesystem $storage, SchemaBuilder $schemaBuilder): void
{
// Clear the cache so this always gets the current schema
$cache->forget(config('lighthouse.cache.key'));
$this->callSilent(ClearCacheCommand::NAME);
$schema = SchemaPrinter::doPrint(
$graphQL->prepSchema()
);
$schema = $schemaBuilder->schema();
if ($this->option('json')) {
$filename = self::JSON_FILENAME;
$schemaString = $this->toJson($schema);
} else {
$filename = self::GRAPHQL_FILENAME;
$schemaString = SchemaPrinter::doPrint($schema);
}
if ($this->option('write')) {
$storage->put('lighthouse-schema.graphql', $schema);
$this->info('Wrote schema to the default file storage (usually storage/app) as "lighthouse-schema.graphql".');
$storage->put($filename, $schemaString);
$this->info('Wrote schema to the default file storage (usually storage/app) as "'.$filename.'".');
} else {
$this->info($schema);
$this->info($schemaString);
}
}
protected function toJson(Schema $schema): string
{
$introspectionResult = Introspection::fromSchema($schema);
if ($introspectionResult === null) {
throw new \Exception(<<<'MESSAGE'
Did not receive a valid introspection result.
Check if your schema is correct with:
php artisan lighthouse:validate-schema
MESSAGE
);
}
return \Safe\json_encode($introspectionResult);
}
}

View File

@ -2,47 +2,16 @@
namespace Nuwave\Lighthouse\Console;
class QueryCommand extends LighthouseGeneratorCommand
class QueryCommand extends FieldGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:query';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a single field on the root Query type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Query';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
protected function namespaceConfigKey(): string
{
return config('lighthouse.namespaces.queries');
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/field.stub';
return 'queries';
}
}

View File

@ -4,43 +4,17 @@ namespace Nuwave\Lighthouse\Console;
class ScalarCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:scalar';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a GraphQL scalar type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Scalar';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
protected function namespaceConfigKey(): string
{
return config('lighthouse.namespaces.scalars');
return 'scalars';
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/scalar.stub';

View File

@ -4,43 +4,17 @@ namespace Nuwave\Lighthouse\Console;
class SubscriptionCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:subscription';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a single field on the root Subscription type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Subscription';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
protected function namespaceConfigKey(): string
{
return config('lighthouse.namespaces.subscriptions');
return 'subscriptions';
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/subscription.stub';

View File

@ -4,43 +4,17 @@ namespace Nuwave\Lighthouse\Console;
class UnionCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:union';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a class for a GraphQL union type.';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Union';
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace): string
protected function namespaceConfigKey(): string
{
return config('lighthouse.namespaces.unions');
return 'unions';
}
/**
* Get the stub file for the generator.
*
* @return string
*/
protected function getStub(): string
{
return __DIR__.'/stubs/typeResolver.stub';

View File

@ -2,38 +2,52 @@
namespace Nuwave\Lighthouse\Console;
use Nuwave\Lighthouse\GraphQL;
use GraphQL\Type\Schema;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher;
use Nuwave\Lighthouse\Events\ValidateSchema;
use Nuwave\Lighthouse\Schema\DirectiveLocator;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
use Nuwave\Lighthouse\Schema\FallbackTypeNodeConverter;
use Nuwave\Lighthouse\Schema\SchemaBuilder;
use Nuwave\Lighthouse\Schema\TypeRegistry;
class ValidateSchemaCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lighthouse:validate-schema';
/**
* The console command description.
*
* @var string
*/
protected $name = 'lighthouse:validate-schema';
protected $description = 'Validate the GraphQL schema definition.';
/**
* Execute the console command.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function handle(Repository $cache, GraphQL $graphQL): void
{
public function handle(
EventsDispatcher $eventsDispatcher,
SchemaBuilder $schemaBuilder,
DirectiveLocator $directiveLocator,
TypeRegistry $typeRegistry
): void {
// Clear the cache so this always validates the current schema
$cache->forget(config('lighthouse.cache.key'));
$this->call(ClearCacheCommand::NAME);
$graphQL->prepSchema()->assertValid();
$originalSchema = $schemaBuilder->schema();
$schemaConfig = $originalSchema->getConfig();
// We add schema directive definitions only here, since it is very slow
$directiveFactory = new DirectiveFactory(
new FallbackTypeNodeConverter($typeRegistry)
);
foreach ($directiveLocator->definitions() as $directiveDefinition) {
// TODO consider a solution that feels less hacky
if ($directiveDefinition->name->value !== 'deprecated') {
$schemaConfig->directives [] = $directiveFactory->handle($directiveDefinition);
}
}
$schema = new Schema($schemaConfig);
$schema->assertValid();
// Allow plugins to do their own schema validations
$eventsDispatcher->dispatch(
new ValidateSchema($schema)
);
$this->info('The defined schema is valid.');
}

View File

@ -0,0 +1,40 @@
<?php
namespace Nuwave\Lighthouse\Console;
class ValidatorCommand extends LighthouseGeneratorCommand
{
/**
* The name of the console command.
*
* @var string
*/
protected $name = 'lighthouse:validator';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create a validator class';
/**
* The type of class being generated.
*
* @var string
*/
protected $type = 'Validator';
protected function namespaceConfigKey(): string
{
return 'validators';
}
/**
* Get the stub file for the generator.
*/
protected function getStub(): string
{
return __DIR__.'/stubs/validator.stub';
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace DummyNamespace;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
{{ imports }}
class DummyClass extends BaseDirective implements {{ implements }}
{
// TODO implement the directive https://lighthouse-php.com/master/custom-directives/getting-started.html
{{ methods }}}

View File

@ -0,0 +1,11 @@
/**
* Add additional constraints to the builder based on the given argument value.
*
* @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
* @param mixed $value
* @return \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder
*/
public function handleBuilder($builder, $value)
{
// TODO implement the arg builder
}

View File

@ -0,0 +1,4 @@
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputValueDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;

View File

@ -0,0 +1,17 @@
/**
* Manipulate the AST.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\InputValueDefinitionNode $argDefinition
* @param \GraphQL\Language\AST\FieldDefinitionNode $parentField
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @return void
*/
public function manipulateArgDefinition(
DocumentAST &$documentAST,
InputValueDefinitionNode &$argDefinition,
FieldDefinitionNode &$parentField,
ObjectTypeDefinitionNode &$parentType
) {
// TODO implement the arg manipulator
}

View File

@ -0,0 +1,9 @@
/**
* @param mixed $root The result of the parent resolver.
* @param mixed|\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet|array<\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet> $value The slice of arguments that belongs to this nested resolver.
* @return mixed
*/
public function __invoke($root, $value)
{
// TODO implement the arg resolver
}

View File

@ -0,0 +1,10 @@
/**
* Apply transformations on the value of an argument given to a field.
*
* @param mixed $argumentValue
* @return mixed
*/
public function transform($argumentValue)
{
// TODO implement the arg transformer
}

View File

@ -0,0 +1,3 @@
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;

View File

@ -0,0 +1,15 @@
/**
* Manipulate the AST based on a field definition.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\FieldDefinitionNode $fieldDefinition
* @param \GraphQL\Language\AST\ObjectTypeDefinitionNode $parentType
* @return void
*/
public function manipulateFieldDefinition(
DocumentAST &$documentAST,
FieldDefinitionNode &$fieldDefinition,
ObjectTypeDefinitionNode &$parentType
) {
// TODO implement the field manipulator
}

View File

@ -0,0 +1,2 @@
use Closure;
use Nuwave\Lighthouse\Schema\Values\FieldValue;

View File

@ -0,0 +1,11 @@
/**
* Wrap around the final field resolver.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue, Closure $next)
{
// TODO implement the field middleware
}

View File

@ -0,0 +1 @@
use Nuwave\Lighthouse\Schema\Values\FieldValue;

View File

@ -0,0 +1,13 @@
/**
* Set a field resolver on the FieldValue.
*
* This must call $fieldValue->setResolver() before returning
* the FieldValue.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function resolveField(FieldValue $fieldValue)
{
// TODO implement the field resolver
}

View File

@ -0,0 +1,2 @@
use GraphQL\Language\AST\TypeExtensionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;

View File

@ -0,0 +1,11 @@
/**
* Apply manipulations from a type extension node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeExtensionNode $typeExtension
* @return void
*/
public function manipulateTypeExtension(DocumentAST &$documentAST, TypeExtensionNode &$typeExtension)
{
// TODO implement the type extension manipulator
}

View File

@ -0,0 +1,2 @@
use GraphQL\Language\AST\TypeDefinitionNode;
use Nuwave\Lighthouse\Schema\AST\DocumentAST;

View File

@ -0,0 +1,11 @@
/**
* Apply manipulations from a type definition node.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @param \GraphQL\Language\AST\TypeDefinitionNode $typeDefinition
* @return void
*/
public function manipulateTypeDefinition(DocumentAST &$documentAST, TypeDefinitionNode &$typeDefinition)
{
// TODO implement the type manipulator
}

View File

@ -0,0 +1,2 @@
use Closure;
use Nuwave\Lighthouse\Schema\Values\TypeValue;

View File

@ -0,0 +1,11 @@
/**
* Handle a type AST as it is converted to an executable type.
*
* @param \Nuwave\Lighthouse\Schema\Values\TypeValue $value
* @param \Closure $next
* @return \GraphQL\Type\Definition\Type
*/
public function handleNode(TypeValue $value, Closure $next)
{
// TODO implement the type middleware
}

View File

@ -0,0 +1 @@
use Nuwave\Lighthouse\Schema\Values\TypeValue;

View File

@ -0,0 +1,10 @@
/**
* Resolve a type AST to a GraphQL Type.
*
* @param \Nuwave\Lighthouse\Schema\Values\TypeValue $value
* @return \GraphQL\Type\Definition\Type
*/
public function resolveNode(TypeValue $value)
{
// TODO implement the type resolver
}

View File

@ -1,23 +0,0 @@
<?php
namespace DummyNamespace;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class DummyClass
{
/**
* Return a value for the field.
*
* @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
* @param mixed[] $args The arguments that were passed into the field.
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context Arbitrary data that is shared between all fields of a single query.
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
* @return mixed
*/
public function __invoke($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
{
// TODO implement the resolver
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace DummyNamespace;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class DummyClass
{
/**
* Return a value for the field.
*
* @param @param null $root Always null, since this field has no parent.
* @param array<string, mixed> $args The field arguments passed by the client.
* @param \Nuwave\Lighthouse\Support\Contracts\GraphQLContext $context Shared between all fields.
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo Metadata for advanced query resolution.
* @return mixed
*/
public function __invoke($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
{
// TODO implement the resolver
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace DummyNamespace;
class DummyClass
{
/**
* @param null $_
* @param array<string, mixed> $args
*/
public function __invoke($_, array $args)
{
// TODO implement the resolver
}
}

View File

@ -5,7 +5,7 @@ namespace DummyNamespace;
use GraphQL\Type\Definition\ScalarType;
/**
* Read more about scalars here http://webonyx.github.io/graphql-php/type-system/scalar-types/
* Read more about scalars here https://webonyx.github.io/graphql-php/type-definitions/scalars
*/
class DummyClass extends ScalarType
{
@ -45,7 +45,7 @@ class DummyClass extends ScalarType
* }
*
* @param \GraphQL\Language\AST\Node $valueNode
* @param mixed[]|null $variables
* @param array<string, mixed>|null $variables
* @return mixed
*/
public function parseLiteral($valueNode, ?array $variables = null)

View File

@ -5,6 +5,7 @@ namespace DummyNamespace;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Schema\TypeRegistry;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class DummyClass
{
@ -15,12 +16,6 @@ class DummyClass
*/
protected $typeRegistry;
/**
* Constructor.
*
* @param \Nuwave\Lighthouse\Schema\TypeRegistry $typeRegistry
* @return void
*/
public function __construct(TypeRegistry $typeRegistry)
{
$this->typeRegistry = $typeRegistry;

View File

@ -0,0 +1,20 @@
<?php
namespace DummyNamespace;
use Nuwave\Lighthouse\Validation\Validator;
class DummyClass extends Validator
{
/**
* Return the validation rules.
*
* @return array<string, array<mixed>>
*/
public function rules(): array
{
return [
// TODO Add your validation rules
];
}
}

View File

@ -3,14 +3,13 @@
namespace Nuwave\Lighthouse\Defer;
use Closure;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Support\Arr;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\GraphQL;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Symfony\Component\HttpFoundation\Response;
use Nuwave\Lighthouse\Schema\AST\PartialParser;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
use Nuwave\Lighthouse\Support\Contracts\CanStreamResponse;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
use Symfony\Component\HttpFoundation\Response;
class Defer implements CreatesResponse
{
@ -25,161 +24,134 @@ class Defer implements CreatesResponse
protected $graphQL;
/**
* @var mixed[]
* @var \Nuwave\Lighthouse\Events\StartExecution
*/
protected $result = [];
protected $startExecution;
/**
* @var mixed[]
* A map from paths to deferred resolvers.
*
* @var array<string, \Closure(): mixed>
*/
protected $deferred = [];
/**
* @var mixed[]
* Paths resolved during the current nesting of defers.
*
* @var array<int, mixed>
*/
protected $resolved = [];
/**
* @var bool
* The entire result of resolving the query up until the current nesting.
*
* @var array<string, mixed>
*/
protected $acceptFurtherDeferring = true;
protected $result = [];
/**
* Should further deferring happen?
*
* @var bool
*/
protected $shouldDeferFurther = true;
/**
* Are we currently streaming deferred results?
*
* @var bool
*/
protected $isStreaming = false;
/**
* @var int
* @var float|int
*/
protected $maxExecutionTime = 0;
/**
* @var int
*/
protected $maxNestedFields = 0;
/**
* @param \Nuwave\Lighthouse\Support\Contracts\CanStreamResponse $stream
* @param \Nuwave\Lighthouse\GraphQL $graphQL
* @return void
*/
public function __construct(CanStreamResponse $stream, GraphQL $graphQL)
public function __construct(CanStreamResponse $stream, GraphQL $graphQL, ConfigRepository $config)
{
$this->stream = $stream;
$this->graphQL = $graphQL;
$this->maxNestedFields = config('lighthouse.defer.max_nested_fields', 0);
$executionTime = $config->get('lighthouse.defer.max_execution_ms', 0);
if ($executionTime > 0) {
$this->maxExecutionTime = microtime(true) + $executionTime * 1000;
}
$this->maxNestedFields = $config->get('lighthouse.defer.max_nested_fields', 0);
}
/**
* Set the tracing directive on all fields of the query to enable tracing them.
*
* @param \Nuwave\Lighthouse\Events\ManipulateAST $manipulateAST
* @return void
*/
public function handleManipulateAST(ManipulateAST $manipulateAST): void
public function handleStartExecution(StartExecution $startExecution): void
{
ASTHelper::attachDirectiveToObjectTypeFields(
$manipulateAST->documentAST,
PartialParser::directive('@deferrable')
);
$manipulateAST->documentAST->setDirectiveDefinition(
PartialParser::directiveDefinition('
"""
Use this directive on expensive or slow fields to resolve them asynchronously.
Must not be placed upon:
- Non-Nullable fields
- Mutation root fields
"""
directive @defer(if: Boolean = true) on FIELD
')
);
}
/**
* @return bool
*/
public function isStreaming(): bool
{
return $this->isStreaming;
$this->startExecution = $startExecution;
}
/**
* Register deferred field.
*
* @param \Closure $resolver
* @param string $path
* @return mixed
* @param \Closure(): mixed $resolver
* @return mixed The data if it is already available.
*/
public function defer(Closure $resolver, string $path)
{
if ($data = Arr::get($this->result, "data.{$path}")) {
$data = $this->getData($path);
if ($data !== null) {
return $data;
}
if ($this->isDeferred($path) || ! $this->acceptFurtherDeferring) {
// If we have been here before, now is the time to resolve this field
$deferredResolver = $this->deferred[$path] ?? null;
if ($deferredResolver) {
return $this->resolve($deferredResolver, $path);
}
if (! $this->shouldDeferFurther) {
return $this->resolve($resolver, $path);
}
$this->deferred[$path] = $resolver;
return null;
}
/**
* @param \Closure $originalResolver
* @param string $path
* @return mixed
* @return mixed The data at the path
*/
public function findOrResolve(Closure $originalResolver, string $path)
protected function getData(string $path)
{
if (! $this->hasData($path)) {
if (isset($this->deferred[$path])) {
unset($this->deferred[$path]);
}
return $this->resolve($originalResolver, $path);
}
return Arr::get($this->result, "data.{$path}");
}
/**
* Resolve field with data or resolver.
*
* @param \Closure $originalResolver
* @param string $path
* @return mixed
* @param \Closure(): mixed $resolver
* @return mixed The loaded data
*/
public function resolve(Closure $originalResolver, string $path)
protected function resolve(Closure $resolver, string $path)
{
$isDeferred = $this->isDeferred($path);
$resolver = $isDeferred
? $this->deferred[$path]
: $originalResolver;
if ($isDeferred) {
$this->resolved[] = $path;
unset($this->deferred[$path]);
}
unset($this->deferred[$path]);
$this->resolved [] = $path;
return $resolver();
}
/**
* @param string $path
* @return bool
* @param \Closure(): mixed $originalResolver
* @return mixed The loaded data
*/
public function isDeferred(string $path): bool
public function findOrResolve(Closure $originalResolver, string $path)
{
return isset($this->deferred[$path]);
if ($this->hasData($path)) {
return $this->getData($path);
}
return $originalResolver();
}
/**
* @param string $path
* @return bool
*/
public function hasData(string $path): bool
protected function hasData(string $path): bool
{
return Arr::has($this->result, "data.{$path}");
}
@ -187,30 +159,26 @@ directive @defer(if: Boolean = true) on FIELD
/**
* Return either a final response or a stream of responses.
*
* @param mixed[] $result
* @param array<string, mixed> $result
* @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\StreamedResponse
*/
public function createResponse(array $result): Response
{
if (empty($this->deferred)) {
if (! $this->hasRemainingDeferred()) {
return response($result);
}
$this->result = $result;
$this->isStreaming = true;
return response()->stream(
function () use ($result): void {
function (): void {
$this->stream();
$nested = 1;
$this->result = $result;
$this->isStreaming = true;
$this->stream->stream($result, [], empty($this->deferred));
if ($executionTime = config('lighthouse.defer.max_execution_ms', 0)) {
$this->maxExecutionTime = microtime(true) + ($executionTime * 1000);
}
// TODO: Allow nested_levels to be set in config to break out of loop early.
while (
count($this->deferred)
&& ! $this->executionTimeExpired()
$this->hasRemainingDeferred()
&& ! $this->maxExecutionTimeReached()
&& ! $this->maxNestedFieldsResolved($nested)
) {
$nested++;
@ -218,48 +186,40 @@ directive @defer(if: Boolean = true) on FIELD
}
// We've hit the max execution time or max nested levels of deferred fields.
$this->shouldDeferFurther = false;
// We process remaining deferred fields, but are no longer allowing additional
// fields to be deferred.
if (count($this->deferred)) {
$this->acceptFurtherDeferring = false;
if ($this->hasRemainingDeferred()) {
$this->executeDeferred();
}
},
200,
[
// TODO: Allow headers to be set in config
'X-Accel-Buffering' => 'no',
'Content-Type' => 'multipart/mixed; boundary="-"',
]
);
}
/**
* @param int $time
* @return void
*/
public function setMaxExecutionTime(int $time): void
protected function hasRemainingDeferred(): bool
{
$this->maxExecutionTime = $time;
return count($this->deferred) > 0;
}
protected function stream(): void
{
$this->stream->stream(
$this->result,
$this->resolved,
! $this->hasRemainingDeferred()
);
}
/**
* Override max nested fields.
*
* @param int $max
* @return void
* Check if we reached the maximum execution time.
*/
public function setMaxNestedFields(int $max): void
{
$this->maxNestedFields = $max;
}
/**
* Check if the maximum execution time has expired.
*
* @return bool
*/
protected function executionTimeExpired(): bool
protected function maxExecutionTimeReached(): bool
{
if ($this->maxExecutionTime === 0) {
return false;
@ -270,9 +230,6 @@ directive @defer(if: Boolean = true) on FIELD
/**
* Check if the maximum number of nested field has been resolved.
*
* @param int $nested
* @return bool
*/
protected function maxNestedFieldsResolved(int $nested): bool
{
@ -280,26 +237,32 @@ directive @defer(if: Boolean = true) on FIELD
return false;
}
return $nested >= $this->maxNestedFields;
return $this->maxNestedFields <= $nested;
}
/**
* Execute deferred fields.
*
* @return void
*/
protected function executeDeferred(): void
{
$this->result = app()->call(
[$this->graphQL, 'executeRequest']
$executionResult = $this->graphQL->executeQuery(
$this->startExecution->query,
$this->startExecution->context,
$this->startExecution->variables,
null,
$this->startExecution->operationName
);
$this->stream->stream(
$this->result,
$this->resolved,
empty($this->deferred)
);
$this->result = $this->graphQL->serializable($executionResult);
$this->stream();
$this->resolved = [];
}
public function setMaxExecutionTime(float $time): void
{
$this->maxExecutionTime = $time;
}
public function setMaxNestedFields(int $max): void
{
$this->maxNestedFields = $max;
}
}

View File

@ -2,43 +2,65 @@
namespace Nuwave\Lighthouse\Defer;
use Illuminate\Support\ServiceProvider;
use GraphQL\Language\Parser;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Nuwave\Lighthouse\Events\ManipulateAST;
use Nuwave\Lighthouse\Schema\Factories\DirectiveFactory;
use Nuwave\Lighthouse\Events\RegisterDirectiveNamespaces;
use Nuwave\Lighthouse\Events\StartExecution;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Support\Contracts\CreatesResponse;
class DeferServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @param \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory $directiveFactory
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @return void
*/
public function boot(DirectiveFactory $directiveFactory, Dispatcher $dispatcher): void
public function register(): void
{
$directiveFactory->addResolved(
DeferrableDirective::NAME,
DeferrableDirective::class
$this->app->singleton(Defer::class);
$this->app->singleton(CreatesResponse::class, Defer::class);
}
public function boot(Dispatcher $dispatcher): void
{
$dispatcher->listen(
RegisterDirectiveNamespaces::class,
static function (): string {
return __NAMESPACE__;
}
);
$dispatcher->listen(
ManipulateAST::class,
Defer::class.'@handleManipulateAST'
function (ManipulateAST $manipulateAST): void {
$this->handleManipulateAST($manipulateAST);
}
);
$dispatcher->listen(
StartExecution::class,
Defer::class.'@handleStartExecution'
);
}
/**
* Register any application services.
*
* @return void
* Set the tracing directive on all fields of the query to enable tracing them.
*/
public function register(): void
public function handleManipulateAST(ManipulateAST $manipulateAST): void
{
$this->app->singleton(Defer::class);
ASTHelper::attachDirectiveToObjectTypeFields(
$manipulateAST->documentAST,
Parser::constDirective(/** @lang GraphQL */ '@deferrable')
);
$this->app->singleton(CreatesResponse::class, Defer::class);
$manipulateAST->documentAST->setDirectiveDefinition(
Parser::directiveDefinition(/** @lang GraphQL */ '
"""
Use this directive on expensive or slow fields to resolve them asynchronously.
Must not be placed upon:
- Non-Nullable fields
- Mutation root fields
"""
directive @defer(if: Boolean = true) on FIELD
')
);
}
}

View File

@ -4,31 +4,32 @@ namespace Nuwave\Lighthouse\Defer;
use Closure;
use GraphQL\Error\Error;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Language\AST\NonNullTypeNode;
use Nuwave\Lighthouse\Schema\AST\ASTHelper;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use GraphQL\Language\AST\TypeNode;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\ClientDirectives\ClientDirective;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
use Nuwave\Lighthouse\Schema\RootType;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\DefinedDirective;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
class DeferrableDirective extends BaseDirective implements DefinedDirective, FieldMiddleware
class DeferrableDirective extends BaseDirective implements FieldMiddleware
{
const NAME = 'deferrable';
const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD = 'The @defer directive cannot be used on a root mutation field.';
const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD = 'The @defer directive cannot be used on a Non-Nullable field.';
public const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD = 'The @defer directive cannot be used on a root mutation field.';
public const THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD = 'The @defer directive cannot be used on a Non-Nullable field.';
public const DEFER_DIRECTIVE_NAME = 'defer';
public static function definition(): string
{
return /* @lang GraphQL */ <<<'SDL'
return /** @lang GraphQL */ <<<'GRAPHQL'
"""
Do not use this directive directly, it is automatically added to the schema
when using the defer extension.
"""
directive @deferrable on FIELD_DEFINITION
SDL;
GRAPHQL;
}
/**
@ -36,32 +37,11 @@ SDL;
*/
protected $defer;
/**
* @param \Nuwave\Lighthouse\Defer\Defer $defer
* @return void
*/
public function __construct(Defer $defer)
{
$this->defer = $defer;
}
/**
* Name of the directive.
*
* @return string
*/
public function name(): string
{
return self::NAME;
}
/**
* Resolve the field directive.
*
* @param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* @param \Closure $next
* @return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue, Closure $next): FieldValue
{
$previousResolver = $fieldValue->getResolver();
@ -78,9 +58,7 @@ SDL;
return $this->defer->defer($wrappedResolver, $path);
}
return $this->defer->isStreaming()
? $this->defer->findOrResolve($wrappedResolver, $path)
: $previousResolver($root, $args, $context, $resolveInfo);
return $this->defer->findOrResolve($wrappedResolver, $path);
}
);
@ -90,49 +68,56 @@ SDL;
/**
* Determine if field should be deferred.
*
* @param \GraphQL\Language\AST\TypeNode $fieldType
* @param \GraphQL\Type\Definition\ResolveInfo $resolveInfo
* @return bool
*
* @throws \GraphQL\Error\Error
*/
protected function shouldDefer(TypeNode $fieldType, ResolveInfo $resolveInfo): bool
{
foreach ($resolveInfo->fieldNodes as $fieldNode) {
$deferDirective = ASTHelper::directiveDefinition($fieldNode, 'defer');
if (! $deferDirective) {
return false;
}
$defers = (new ClientDirective(self::DEFER_DIRECTIVE_NAME))->forField($resolveInfo);
if ($resolveInfo->parentType->name === 'Mutation') {
if ($this->anyFieldHasDefer($defers)) {
if ($resolveInfo->parentType->name === RootType::MUTATION) {
throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_ROOT_MUTATION_FIELD);
}
if (! ASTHelper::directiveArgValue($deferDirective, 'if', true)) {
return false;
if ($fieldType instanceof NonNullTypeNode) {
throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD);
}
}
$skipDirective = ASTHelper::directiveDefinition($fieldNode, 'skip');
if (
$skipDirective
&& ASTHelper::directiveArgValue($skipDirective, 'if') === true
) {
return false;
}
$includeDirective = ASTHelper::directiveDefinition($fieldNode, 'include');
if (
$includeDirective
&& ASTHelper::directiveArgValue($includeDirective, 'if') === false
) {
// Following the semantics of Apollo:
// All declarations of a field have to contain @defer for the field to be deferred
foreach ($defers as $defer) {
if ($defer === null || $defer === [Directive::IF_ARGUMENT_NAME => false]) {
return false;
}
}
if ($fieldType instanceof NonNullTypeNode) {
throw new Error(self::THE_DEFER_DIRECTIVE_CANNOT_BE_USED_ON_A_NON_NULLABLE_FIELD);
$skips = (new ClientDirective(Directive::SKIP_NAME))->forField($resolveInfo);
foreach ($skips as $skip) {
if ($skip === [Directive::IF_ARGUMENT_NAME => true]) {
return false;
}
}
return true;
$includes = (new ClientDirective(Directive::INCLUDE_NAME))->forField($resolveInfo);
return ! in_array(
[Directive::IF_ARGUMENT_NAME => false],
$includes,
true
);
}
/**
* @param array<array<string, mixed>|null> $defers
*/
protected function anyFieldHasDefer(array $defers): bool
{
foreach ($defers as $defer) {
if ($defer !== null) {
return true;
}
}
return false;
}
}

View File

@ -5,9 +5,8 @@ namespace Nuwave\Lighthouse\Events;
/**
* Fires after a query was resolved.
*
* Listeners of this event may return an instance of
* @see \Nuwave\Lighthouse\Execution\ExtensionsResponse
* that is then added to the response.
* Listeners may return a @see \Nuwave\Lighthouse\Execution\ExtensionsResponse
* to include in the response.
*/
class BuildExtensionsResponse
{

View File

@ -19,12 +19,6 @@ class BuildSchemaString
*/
public $userSchema;
/**
* BuildSchemaString constructor.
*
* @param string $userSchema
* @return void
*/
public function __construct(string $userSchema)
{
$this->userSchema = $userSchema;

View File

@ -0,0 +1,32 @@
<?php
namespace Nuwave\Lighthouse\Events;
use GraphQL\Executor\ExecutionResult;
use Illuminate\Support\Carbon;
/**
* Fires after resolving a single operation.
*/
class EndExecution
{
/**
* The result of resolving a single operation.
*
* @var \GraphQL\Executor\ExecutionResult
*/
public $result;
/**
* The point in time when the result was ready.
*
* @var \Illuminate\Support\Carbon
*/
public $moment;
public function __construct(ExecutionResult $result)
{
$this->result = $result;
$this->moment = Carbon::now();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Nuwave\Lighthouse\Events;
/**
* Fires after resolving all operations.
*/
class EndOperationOrOperations
{
/**
* The result of either a single or multiple operations.
*
* @var array<string, mixed>|array<int, array<string, mixed>>
*/
public $resultOrResults;
/**
* @param array<string, mixed>|array<int, array<string, mixed>> $resultOrResults
*/
public function __construct(array $resultOrResults)
{
$this->resultOrResults = $resultOrResults;
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Nuwave\Lighthouse\Events;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\Response;
/**
* Fires right after building the HTTP response in the GraphQLController.
*
* Can be used for logging or for measuring and monitoring
* the time a request takes to resolve.
*
* @see \Nuwave\Lighthouse\Support\Http\Controllers\GraphQLController
*/
class EndRequest
{
/**
* The response that is about to be sent to the client.
*
* @var \Symfony\Component\HttpFoundation\Response
*/
public $response;
/**
* The point in time when the response was ready.
*
* @var \Illuminate\Support\Carbon
*/
public $moment;
public function __construct(Response $response)
{
$this->response = $response;
$this->moment = Carbon::now();
}
}

View File

@ -21,12 +21,6 @@ class ManipulateAST
*/
public $documentAST;
/**
* BuildSchemaString constructor.
*
* @param \Nuwave\Lighthouse\Schema\AST\DocumentAST $documentAST
* @return void
*/
public function __construct(DocumentAST &$documentAST)
{
$this->documentAST = $documentAST;

View File

@ -19,12 +19,6 @@ class ManipulateResult
*/
public $result;
/**
* ManipulateResult constructor.
*
* @param \GraphQL\Executor\ExecutionResult $result
* @return void
*/
public function __construct(ExecutionResult &$result)
{
$this->result = $result;

View File

@ -8,7 +8,7 @@ namespace Nuwave\Lighthouse\Events;
* Listeners may return one or more strings that are used as the base
* namespace for locating directives.
*
* @see \Nuwave\Lighthouse\Schema\Factories\DirectiveFactory
* @see \Nuwave\Lighthouse\Schema\DirectiveLocator::namespaces()
*/
class RegisterDirectiveNamespaces
{

View File

@ -2,30 +2,61 @@
namespace Nuwave\Lighthouse\Events;
use Carbon\Carbon;
use GraphQL\Language\AST\DocumentNode;
use Illuminate\Support\Carbon;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
/**
* Fires right before resolving an individual query.
* Fires right before resolving a single operation.
*
* Might happen multiple times in a single request if
* query batching is used.
* Might happen multiple times in a single request if batching is used.
*/
class StartExecution
{
/**
* The client given parsed query string.
*
* @var \GraphQL\Language\AST\DocumentNode
*/
public $query;
/**
* The client given variables, neither validated nor transformed.
*
* @var array<string, mixed>|null
*/
public $variables;
/**
* The client given operation name.
*
* @var string|null
*/
public $operationName;
/**
* The context for the operation.
*
* @var \Nuwave\Lighthouse\Support\Contracts\GraphQLContext
*/
public $context;
/**
* The point in time when the query execution started.
*
* @var \Carbon\Carbon
* @var \Illuminate\Support\Carbon
*/
public $moment;
/**
* StartRequest constructor.
*
* @return void
* @param array<string, mixed>|null $variables
*/
public function __construct()
public function __construct(DocumentNode $query, ?array $variables, ?string $operationName, GraphQLContext $context)
{
$this->query = $query;
$this->variables = $variables;
$this->operationName = $operationName;
$this->context = $context;
$this->moment = Carbon::now();
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Nuwave\Lighthouse\Events;
/**
* Fires after receiving the parsed operation (single query) or operations (batched query).
*/
class StartOperationOrOperations
{
/**
* One or multiple parsed GraphQL operations.
*
* @var \GraphQL\Server\OperationParams|array<int, \GraphQL\Server\OperationParams>
*/
public $operationOrOperations;
/**
* @param \GraphQL\Server\OperationParams|array<int, \GraphQL\Server\OperationParams> $operationOrOperations
*/
public function __construct($operationOrOperations)
{
$this->operationOrOperations = $operationOrOperations;
}
}

View File

@ -2,8 +2,8 @@
namespace Nuwave\Lighthouse\Events;
use Carbon\Carbon;
use Nuwave\Lighthouse\Execution\GraphQLRequest;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
/**
* Fires right after a request reaches the GraphQLController.
@ -16,26 +16,20 @@ use Nuwave\Lighthouse\Execution\GraphQLRequest;
class StartRequest
{
/**
* GraphQL request instance.
* The request sent from the client.
*
* @var \Nuwave\Lighthouse\Execution\GraphQLRequest
* @var \Illuminate\Http\Request
*/
public $request;
/**
* The point in time when the request started.
*
* @var \Carbon\Carbon
* @var \Illuminate\Support\Carbon
*/
public $moment;
/**
* StartRequest constructor.
*
* @param \Nuwave\Lighthouse\Execution\GraphQLRequest $request
* @return void
*/
public function __construct(GraphQLRequest $request)
public function __construct(Request $request)
{
$this->request = $request;
$this->moment = Carbon::now();

View File

@ -0,0 +1,25 @@
<?php
namespace Nuwave\Lighthouse\Events;
use GraphQL\Type\Schema;
/**
* Dispatched when php artisan lighthouse:validate-schema is called.
*
* Listeners should throw a descriptive error if the schema is wrong.
*/
class ValidateSchema
{
/**
* The final schema to validate.
*
* @var \GraphQL\Type\Schema
*/
public $schema;
public function __construct(Schema $schema)
{
$this->schema = $schema;
}
}

View File

@ -6,35 +6,21 @@ use Illuminate\Auth\AuthenticationException as IlluminateAuthenticationException
class AuthenticationException extends IlluminateAuthenticationException implements RendersErrorsExtensions
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public const MESSAGE = 'Unauthenticated.';
public const CATEGORY = 'authentication';
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'authentication';
return self::CATEGORY;
}
/**
* Return the content that is put in the "extensions" part
* of the returned error.
*
* @return array
* @return array<string, array<string>>
*/
public function extensionsContent(): array
{

View File

@ -7,30 +7,13 @@ use Illuminate\Auth\Access\AuthorizationException as IlluminateAuthorizationExce
class AuthorizationException extends IlluminateAuthorizationException implements ClientAware
{
/**
* @var string
*/
const CATEGORY = 'authorization';
public const CATEGORY = 'authorization';
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return self::CATEGORY;

View File

@ -5,27 +5,18 @@ namespace Nuwave\Lighthouse\Exceptions;
use Exception;
use GraphQL\Error\ClientAware;
/**
* Thrown when the schema definition or related code is wrong.
*
* This signals a developer error, so we do not show this exception to the user.
*/
class DefinitionException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return false;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'schema';

View File

@ -7,25 +7,11 @@ use GraphQL\Error\ClientAware;
class DirectiveException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function isClientSafe(): bool
{
return false;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'schema';

View File

@ -0,0 +1,10 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Exception;
class FederationException extends Exception
{
//
}

View File

@ -6,30 +6,20 @@ use GraphQL\Error\Error;
class GenericException extends Error
{
/**
* The category.
*
* @var string
*/
protected $category = 'generic';
/**
* Set the contents that will be rendered under the "extensions" key of the error response.
*
* @param mixed $extensions
* @param array<string, mixed> $extensions
* @return $this
*/
public function setExtensions($extensions): self
public function setExtensions(array $extensions): self
{
$this->extensions = (array) $extensions;
$this->extensions = $extensions;
return $this;
}
/**
* Set the category that will be rendered under the "extensions" key of the error response.
*
* @param string $category
* @return $this
*/
public function setCategory(string $category): self

View File

@ -4,28 +4,31 @@ namespace Nuwave\Lighthouse\Exceptions;
use Exception;
use GraphQL\Error\ClientAware;
use GraphQL\Error\SyntaxError;
use GraphQL\Language\Source;
class ParseException extends Exception implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
* @return bool
*/
public function __construct(SyntaxError $error)
{
$message = $error->getMessage();
$source = $error->getSource();
$positions = $error->getPositions();
if ($source instanceof Source && count($positions) > 0) {
$position = $positions[0];
$message .= ', near: '.\Safe\substr($source->body, max(0, $position - 50), 100);
}
parent::__construct($message);
}
public function isClientSafe(): bool
{
return false;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
* @return string
*/
public function getCategory(): string
{
return 'schema';

View File

@ -0,0 +1,30 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use GraphQL\Error\ClientAware;
use RuntimeException;
/**
* Thrown when the user has reached the rate limit for a field.
*/
class RateLimitException extends RuntimeException implements ClientAware
{
public const MESSAGE = 'Rate limit exceeded. Please try later.';
public const CATEGORY = 'rate-limit';
public function __construct()
{
parent::__construct(self::MESSAGE);
}
public function isClientSafe(): bool
{
return true;
}
public function getCategory(): string
{
return self::CATEGORY;
}
}

View File

@ -16,7 +16,7 @@ interface RendersErrorsExtensions extends ClientAware
* Return the content that is put in the "extensions" part
* of the returned error.
*
* @return array
* @return array<string, mixed>
*/
public function extensionsContent(): array;
}

View File

@ -1,35 +0,0 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use InvalidArgumentException;
use GraphQL\Error\ClientAware;
class SubscriptionException extends InvalidArgumentException implements ClientAware
{
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @api
*
* @return bool
*/
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* Value "graphql" is reserved for errors produced by query parsing or validation, do not use it.
*
* @api
*
* @return string
*/
public function getCategory(): string
{
return 'subscription';
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Nuwave\Lighthouse\Exceptions;
use Exception;
class UnknownCacheVersionException extends Exception
{
/**
* @param mixed $version Should be int, but could be something else
*/
public function __construct($version)
{
parent::__construct("Expected lighthouse.cache.version to be 1 or 2, got: {$version}.");
}
}

View File

@ -2,36 +2,62 @@
namespace Nuwave\Lighthouse\Exceptions;
class ValidationException extends \Illuminate\Validation\ValidationException implements RendersErrorsExtensions
use Exception;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException as LaravelValidationException;
class ValidationException extends Exception implements RendersErrorsExtensions
{
const CATEGORY = 'validation';
/**
* Returns true when exception message is safe to be displayed to a client.
*
* @return bool
* @var \Illuminate\Contracts\Validation\Validator
*/
public function isClientSafe()
protected $validator;
public function __construct(string $message, Validator $validator)
{
parent::__construct($message);
$this->validator = $validator;
}
public static function fromLaravel(LaravelValidationException $laravelException): self
{
return new static(
$laravelException->getMessage(),
$laravelException->validator
);
}
/**
* Instantiate from a plain array of messages.
*
* @see \Illuminate\Validation\ValidationException::withMessages()
*
* @param array<string, string|array<string>> $messages
*/
public static function withMessages(array $messages): self
{
return static::fromLaravel(
LaravelValidationException::withMessages($messages)
);
}
public function isClientSafe(): bool
{
return true;
}
/**
* Returns string describing a category of the error.
*
* @return string
*/
public function getCategory()
public function getCategory(): string
{
return 'validation';
return self::CATEGORY;
}
/**
* Return the content that is put in the "extensions" part
* of the returned error.
*
* @return array
*/
public function extensionsContent(): array
{
return ['validation' => $this->errors()];
return [
self::CATEGORY => $this->validator->errors()->messages(),
];
}
}

View File

@ -0,0 +1,209 @@
<?php
namespace Nuwave\Lighthouse\Execution\Arguments;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Nuwave\Lighthouse\Exceptions\DefinitionException;
use Nuwave\Lighthouse\Support\Contracts\ArgResolver;
use Nuwave\Lighthouse\Support\Utils;
use ReflectionClass;
use ReflectionNamedType;
class ArgPartitioner
{
/**
* Partition the arguments into nested and regular.
*
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
* @return array<\Nuwave\Lighthouse\Execution\Arguments\ArgumentSet>
*/
public static function nestedArgResolvers(ArgumentSet $argumentSet, $root): array
{
$model = $root instanceof Model
? new \ReflectionClass($root)
: null;
foreach ($argumentSet->arguments as $name => $argument) {
static::attachNestedArgResolver($name, $argument, $model);
}
return static::partition(
$argumentSet,
static function (string $name, Argument $argument): bool {
return $argument->resolver !== null;
}
);
}
/**
* Extract all the arguments that correspond to a relation of a certain type on the model.
*
* For example, if the args input looks like this:
*
* [
* 'name' => 'Ralf',
* 'comments' =>
* ['foo' => 'Bar'],
* ]
*
* and the model has a method "comments" that returns a HasMany relationship,
* the result will be:
* [
* [
* 'comments' =>
* ['foo' => 'Bar'],
* ],
* [
* 'name' => 'Ralf',
* ]
* ]
*
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
* @return array{0: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet, 1: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet}
*/
public static function relationMethods(
ArgumentSet $argumentSet,
Model $model,
string $relationClass
): array {
$modelReflection = new ReflectionClass($model);
[$relations, $remaining] = static::partition(
$argumentSet,
static function (string $name) use ($modelReflection, $relationClass): bool {
return static::methodReturnsRelation($modelReflection, $name, $relationClass);
}
);
$nonNullRelations = new ArgumentSet();
$nonNullRelations->arguments = array_filter(
$relations->arguments,
static function (Argument $argument): bool {
return null !== $argument->value;
}
);
return [$nonNullRelations, $remaining];
}
/**
* Attach a nested argument resolver to an argument.
*
* @param \Nuwave\Lighthouse\Execution\Arguments\Argument $argument
*/
protected static function attachNestedArgResolver(string $name, Argument &$argument, ?ReflectionClass $model): void
{
$resolverDirective = $argument->directives->first(
Utils::instanceofMatcher(ArgResolver::class)
);
if ($resolverDirective) {
$argument->resolver = $resolverDirective;
return;
}
if (isset($model)) {
$isRelation = static function (string $relationClass) use ($model, $name): bool {
return static::methodReturnsRelation($model, $name, $relationClass);
};
if (
$isRelation(HasOne::class)
|| $isRelation(MorphOne::class)
) {
$argument->resolver = new ResolveNested(new NestedOneToOne($name));
return;
}
if (
$isRelation(HasMany::class)
|| $isRelation(MorphMany::class)
) {
$argument->resolver = new ResolveNested(new NestedOneToMany($name));
return;
}
if (
$isRelation(BelongsToMany::class)
|| $isRelation(MorphToMany::class)
) {
$argument->resolver = new ResolveNested(new NestedManyToMany($name));
return;
}
}
}
/**
* Partition arguments based on a predicate.
*
* The predicate will be called for each argument within the ArgumentSet
* with the following parameters:
* 1. The name of the argument
* 2. The argument itself
*
* Returns an array of two new ArgumentSet instances:
* - the first one contains all arguments for which the predicate matched
* - the second one contains all arguments for which the predicate did not match
*
* @param \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet $argumentSet
* @return array{0: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet, 1: \Nuwave\Lighthouse\Execution\Arguments\ArgumentSet}
*/
public static function partition(ArgumentSet $argumentSet, \Closure $predicate): array
{
$matched = new ArgumentSet();
$notMatched = new ArgumentSet();
foreach ($argumentSet->arguments as $name => $argument) {
if ($predicate($name, $argument)) {
$matched->arguments[$name] = $argument;
} else {
$notMatched->arguments[$name] = $argument;
}
}
return [
$matched,
$notMatched,
];
}
/**
* Does a method on the model return a relation of the given class?
*/
public static function methodReturnsRelation(
ReflectionClass $modelReflection,
string $name,
string $relationClass
): bool {
if (! $modelReflection->hasMethod($name)) {
return false;
}
$relationMethodCandidate = $modelReflection->getMethod($name);
$returnType = $relationMethodCandidate->getReturnType();
if ($returnType === null) {
return false;
}
if (! $returnType instanceof ReflectionNamedType) {
return false;
}
if (! class_exists($returnType->getName())) {
throw new DefinitionException('Class '.$returnType->getName().' does not exist, did you forget to import the Eloquent relation class?');
}
return is_a($returnType->getName(), $relationClass, true);
}
}

Some files were not shown because too many files have changed in this diff Show More