File manager - Edit - /home/opticamezl/www/newok/packages.tar
Back
container/src/Container/ServiceNotFoundException.php 0000644 00000000303 15166657206 0016716 0 ustar 00 <?php namespace YOOtheme\Container; use Psr\Container\NotFoundExceptionInterface; class ServiceNotFoundException extends \InvalidArgumentException implements NotFoundExceptionInterface {} container/src/Container/InvalidArgumentException.php 0000644 00000000305 15166657206 0016734 0 ustar 00 <?php namespace YOOtheme\Container; use Psr\Container\ContainerExceptionInterface; class InvalidArgumentException extends \InvalidArgumentException implements ContainerExceptionInterface {} container/src/Container/BadFunctionCallException.php 0000644 00000001750 15166657206 0016640 0 ustar 00 <?php namespace YOOtheme\Container; use Psr\Container\ContainerExceptionInterface; class BadFunctionCallException extends \BadFunctionCallException implements ContainerExceptionInterface { /** * Creates an exception from given callback. * * @param string|callable|object $callback * @param mixed $code * @param null|mixed $previous * * @return self */ public static function create($callback, $code = 0, $previous = null) { $function = $callback; if (is_object($callback)) { $function = get_class($callback); } elseif (is_array($callback)) { [$class, $method] = $callback; if (is_string($class)) { $function = "{$class}::{$method}"; } else { $function = get_class($class) . "@{$method}"; } } return new self("Function {$function} is not a callable", $code, $previous); } } container/src/Container/ParameterResolver.php 0000644 00000005751 15166657206 0015440 0 ustar 00 <?php namespace YOOtheme\Container; use YOOtheme\Container; use YOOtheme\Reflection; class ParameterResolver { /** * @var Container */ protected $container; /** * Constructor. */ public function __construct(Container $container) { $this->container = $container; } /** * Resolves parameters for given function. * * @param \ReflectionFunctionAbstract $function * @param array $parameters * * @return array */ public function resolve(\ReflectionFunctionAbstract $function, array $parameters = []) { if ($dependencies = $this->resolveDependencies($function, $parameters)) { $parameters = array_merge($dependencies, $parameters); } if ($function->getNumberOfRequiredParameters() > ($count = count($parameters))) { $parameter = $function->getParameters()[$count]; $declaring = $parameter->getDeclaringFunction(); throw new RuntimeException( "Can't resolve {$parameter} for " . Reflection::toString($declaring), ); } return $parameters; } /** * Resolves dependencies for given function. * * @param \ReflectionFunctionAbstract $function * @param array $parameters * * @return array */ protected function resolveDependencies( \ReflectionFunctionAbstract $function, array &$parameters = [] ) { $dependencies = []; foreach ($function->getParameters() as $parameter) { if (array_key_exists($name = "\${$parameter->name}", $parameters)) { $dependencies[] = $parameters[$name] instanceof \Closure ? $parameters[$name]() : $parameters[$name]; unset($parameters[$name]); } elseif ( ($classname = $this->resolveClassname($parameter)) && array_key_exists($classname, $parameters) ) { $dependencies[] = is_string($parameters[$classname]) ? $this->container->get($parameters[$classname]) : $parameters[$classname]; unset($parameters[$classname]); } elseif ( $classname && ($this->container->has($classname) || class_exists($classname)) ) { $dependencies[] = $this->container->get($classname); } else { break; } } return $dependencies; } /** * Resolves classname from parameter type. * * @param \ReflectionParameter $parameter * * @return string|null */ protected function resolveClassname(\ReflectionParameter $parameter) { $type = $parameter->getType(); return $type instanceof \ReflectionNamedType && !$type->isBuiltin() ? $type->getName() : null; } } container/src/Container/RuntimeException.php 0000644 00000000261 15166657206 0015267 0 ustar 00 <?php namespace YOOtheme\Container; use Psr\Container\ContainerExceptionInterface; class RuntimeException extends \RuntimeException implements ContainerExceptionInterface {} container/src/Container/Service.php 0000644 00000006436 15166657206 0013377 0 ustar 00 <?php namespace YOOtheme\Container; use YOOtheme\Container; class Service { /** * @var string */ public $class; /** * @var bool */ public $shared; /** * @var callable|null */ protected $factory; /** * @var array */ protected $arguments = []; /** * Constructor. * * @param string $class * @param bool $shared */ public function __construct($class, $shared = false) { $this->class = $class; $this->shared = $shared; } /** * Sets service class. * * @param string $class * * @return $this */ public function setClass($class) { $this->class = $class; return $this; } /** * Checks if service is shared. * * @return bool */ public function isShared() { return $this->shared; } /** * Sets service as shared. * * @param bool $shared * * @return $this */ public function setShared($shared = true) { $this->shared = (bool) $shared; return $this; } /** * Gets a service factory. * * @return callable */ public function getFactory() { return $this->factory; } /** * Sets a service factory. * * @param callable|string $factory * * @return $this */ public function setFactory($factory) { $this->factory = $factory; return $this; } /** * Sets an argument value. * * @param string $name * @param mixed $value * * @return $this */ public function setArgument($name, $value) { $this->arguments[$name] = $value; return $this; } /** * Gets arguments for given function. * * @return array */ public function getArguments() { return $this->arguments; } /** * Sets an array of arguments. * * @param array $arguments * * @return $this */ public function setArguments(array $arguments) { $this->arguments = $arguments; return $this; } /** * Resolves a new instance. * * @param Container $container * * @throws LogicException * @throws \ReflectionException * * @return object */ public function resolveInstance(Container $container) { return $this->factory ? $container->call($this->factory, $this->arguments) : $this->resolveClass($container); } /** * Resolves an instance from class. * * @param Container $container * * @throws LogicException * @throws \ReflectionException * * @return object */ protected function resolveClass(Container $container) { $class = new \ReflectionClass($this->class); if (!$class->isInstantiable()) { throw new LogicException("Can't instantiate {$this->class}"); } if (!($constructor = $class->getConstructor())) { return $class->newInstance(); } $resolver = new ParameterResolver($container); $arguments = $resolver->resolve($constructor, $this->arguments); return $class->newInstanceArgs($arguments); } } container/src/Container/LogicException.php 0000644 00000000255 15166657206 0014704 0 ustar 00 <?php namespace YOOtheme\Container; use Psr\Container\ContainerExceptionInterface; class LogicException extends \LogicException implements ContainerExceptionInterface {} container/src/Container.php 0000644 00000020575 15166657206 0011777 0 ustar 00 <?php namespace YOOtheme; use Psr\Container\ContainerInterface; use YOOtheme\Container\BadFunctionCallException; use YOOtheme\Container\InvalidArgumentException; use YOOtheme\Container\LogicException; use YOOtheme\Container\ParameterResolver; use YOOtheme\Container\RuntimeException; use YOOtheme\Container\Service; use YOOtheme\Container\ServiceNotFoundException; class Container implements ContainerInterface { /** * @var array */ protected $aliases = []; /** * @var array */ protected $services = []; /** * @var array */ protected $extenders = []; /** * @var array */ protected $instances = []; /** * @var array */ protected $resolving = []; /** * Gets a service. * * @param string $id * @param string ...$ids * * @return mixed */ public function __invoke($id, ...$ids) { return $ids ? array_map([$this, 'get'], [$id, ...$ids]) : $this->get($id); } /** * Gets a service. * * @param string $id * * @return mixed */ public function __get($id) { return $this->get($id); } /** * Checks if service exists. * * @param string $id * * @return bool */ public function __isset($id) { return $this->has($id); } /** * @inheritdoc */ public function has($id): bool { return isset($this->services[$id]) || isset($this->instances[$id]) || $this->isAlias($id); } /** * @inheritdoc */ public function get($id) { return $this->resolve($id); } /** * Sets a service instance. * * @param string $id * @param mixed $instance * * @return mixed */ public function set($id, $instance) { unset($this->aliases[$id]); return $this->instances[$id] = $instance; } /** * Adds a service definition. * * @param string $id * @param string|callable|Service $service * @param bool $shared * * @return Service */ public function add($id, $service = null, $shared = true) { if (is_string($service) || is_null($service)) { $service = new Service($service ?: $id, $shared); } elseif ($service instanceof \Closure) { $service = (new Service($id, $shared))->setFactory($service); } elseif (!$service instanceof Service) { throw new InvalidArgumentException('Service definition must be string or Closure'); } unset($this->instances[$id], $this->aliases[$id]); return $this->services[$id] = $service; } /** * Adds a callback to extend a service. * * @param string $id * @param callable $callback */ public function extend($id, callable $callback) { $id = $this->getAlias($id); if (isset($this->instances[$id])) { $extended = $callback($this->instances[$id], $this); if (isset($extended) && $extended !== $this->instances[$id]) { throw new LogicException( "Extending a resolved service {$id} must return the same instance", ); } } else { $this->extenders[$id][] = $callback; } } /** * Checks if a service is shared. * * @param string $id * * @return bool */ public function isShared($id) { return !empty($this->services[$id]->shared) || isset($this->instances[$id]); } /** * Checks if an alias exists. * * @param string $id * * @return bool */ public function isAlias($id) { return isset($this->aliases[$id]); } /** * Gets an alias. * * @param string $alias * * @throws LogicException * * @return string */ public function getAlias($alias) { if (!isset($this->aliases[$alias])) { return $alias; } if ($this->aliases[$alias] === $alias) { throw new LogicException("[{$alias}] is aliased to itself"); } return $this->getAlias($this->aliases[$alias]); } /** * Sets an alias. * * @param string $id * @param string $alias */ public function setAlias($id, $alias) { $this->aliases[$alias] = $id; } /** * Gets a callback from service@method or service::method syntax. * * @param callable|string $callback * * @return callable|null */ public function callback($callback) { if (is_string($callback)) { if (str_contains($callback, '::')) { [$service, $method] = explode('::', $callback, 2); $callback = [$this->getAlias($service), $method]; } elseif (str_contains($callback, '@')) { [$service, $method] = explode('@', $callback, 2); $callback = [$this->get($service), $method]; } elseif ($this->has($callback) || class_exists($callback)) { $callback = $this->get($callback); } } return is_callable($callback) ? $callback : null; } /** * Calls the callback with given parameters. * * @param callable|string $callback * @param array $parameters * @param bool $resolve * * @throws \Exception * * @return mixed */ public function call($callback, array $parameters = [], $resolve = true) { if (!($callable = $this->callback($callback))) { throw BadFunctionCallException::create($callback); } if ($resolve) { $resolver = new ParameterResolver($this); $function = Reflection::getFunction($callable); $parameters = $resolver->resolve($function, $parameters); } return $callable(...$parameters); } /** * Wraps the callback with optional parameter resolving. * * @param callable|string $callback * @param array $parameters * @param bool $resolve * * @return callable */ public function wrap($callback, array $parameters = [], $resolve = true) { return fn( ...$params ) => $this->call($callback, array_replace($parameters, $params), $resolve); } /** * Resolves a service from the container. * * @param string $id * * @throws \Exception * @throws \ReflectionException * * @return mixed */ public function resolve($id) { $id = $this->getAlias($id); if (isset($this->instances[$id])) { return $this->instances[$id]; } if (isset($this->resolving[$id])) { if ($this->resolving[$id] === true) { throw new RuntimeException( sprintf( 'Circular reference detected %s => %s', join(' => ', array_keys($this->resolving)), $id, ), ); } return $this->instances[$id] = $this->resolving[$id]; } $this->resolving[$id] = true; $instance = $this->resolveService($id); if ($this->isShared($id)) { $this->instances[$id] = $instance; } unset($this->resolving[$id]); return $instance; } /** * Resolves a service instance. * * @param string $id * * @throws \Exception * @throws \ReflectionException * * @return mixed */ protected function resolveService($id) { if (empty($this->services[$id]) && !class_exists($id)) { throw new ServiceNotFoundException("Service '{$id}' not found"); } $service = $this->services[$id] ?? new Service($id); $instance = $service->resolveInstance($this); $this->resolving[$id] = $this->isShared($id) ? $instance : null; foreach ($this->extenders[$id] ?? [] as $extender) { $instance = $extender($instance, $this) ?: $instance; } if (isset($this->instances[$id]) && $this->instances[$id] !== $instance) { throw new LogicException( "Extending a resolved service {$id} must return the same instance", ); } return $instance; } } builder-source/config/customizer.json 0000644 00000006647 15166657206 0014067 0 ustar 00 { "sources": { "filters": { "before": { "label": "Before", "description": "Add text before the content field." }, "after": { "label": "After", "description": "Add text after the content field." }, "search": { "label": "Search", "description": "Select a predefined search pattern or enter a custom string or regular expression to search for. The regular expression has to be enclosed between slashes. For example `my-string` or `/ab+c/`.", "type": "data-list", "options": { "URL Protocol": "/https?:\\/\\//", "Hyphen and Underscore": "/[\\-_]/" } }, "replace": { "label": "Replace", "description": "Enter the replacement string which may contain references. If left empty, the search matches will be removed." }, "limit": { "label": "Content Length", "description": "Limit the content length to a number of characters. All HTML elements will be stripped.", "type": "number", "attrs": { "placeholder": "No limit." } }, "preserve": { "type": "checkbox", "text": "Preserve words" }, "date": { "label": "Date Format", "description": "Select a predefined date format or enter a custom format.", "type": "data-list", "default": "", "options": { "Aug 6, 1999 (M j, Y)": "M j, Y", "August 06, 1999 (F d, Y)": "F d, Y", "08/06/1999 (m/d/Y)": "m/d/Y", "08.06.1999 (m.d.Y)": "m.d.Y", "6 Aug, 1999 (j M, Y)": "j M, Y", "Tuesday, Aug 06 (l, M d)": "l, M d", "15:00 (G:i)": "G:i", "3:00 pm (g:i A)": "g:i a" }, "attrs": { "placeholder": "Default" } } }, "directives": { "slice": { "fields": { "_grid": { "description": "Set the starting point and limit the number of items.", "type": "grid", "width": "1-2", "fields": { "offset": { "label": "Start", "type": "number", "default": 0, "modifier": 1, "attrs": { "min": 1, "required": true } }, "limit": { "label": "Quantity", "type": "limit", "attrs": { "placeholder": "No limit", "min": 0 } } } } } } } } } builder-source/config/builder.json 0000644 00000007517 15166657206 0013306 0 ustar 00 { "source": { "type": "fields", "fields": { "_source": { "label": "Dynamic Content", "type": "source-select", "description": "Select a content source to make its fields available for mapping. Choose between sources of the current page or query a custom source." }, "_sourceArgs": { "type": "source-query-args" }, "_sourceField": { "label": "Multiple Items Source", "type": "source-field-select", "description": "By default, fields of related sources with single items are available for mapping. Select a related source which has multiple items to map its fields.", "show": "yootheme.builder.helpers.Source.showMultipleSelectField(this.builder.path(this.node))" }, "_sourceFieldArgs": { "type": "source-field-args" }, "_sourceFieldDirectives": { "type": "source-field-directives" }, "_sourceCondition": { "type": "fields", "fields": { "_sourceConditionProp": { "label": "Dynamic Condition", "prop": "_condition", "type": "source-prop-select", "description": "Set a condition to display the element or its item depending on the content of a field." }, "_sourceConditionArgs": { "type": "source-prop-filters", "prop": "_condition", "fields": { "_grid": { "type": "grid", "width": "1-2", "fields": { "condition": { "label": "Condition", "type": "select", "default": "!!", "options": { "Is empty": "!", "Is not empty": "!!", "Is equal to": "=", "Is not equal to": "!=", "Contains": "~=", "Does not contain": "!~=", "Less than": "<", "Greater than": ">", "Starts with": "^=", "Does not start with": "!^=", "Ends with": "$=", "Does not end with": "!$=", "Matches a RegExp": "regex" }, "enable": "!show_empty" }, "condition_value": { "label": "Value", "enable": "!show_empty && $match(condition, '=|<|>|regex')" } } }, "show_empty": { "type": "checkbox", "text": "Show element only if dynamic content is empty" } } } }, "show": "yootheme.builder.helpers.Source.getSourceField(this.builder.path(this.node))" } } } } builder-source/src/Source.php 0000644 00000004746 15166657206 0012261 0 ustar 00 <?php namespace YOOtheme\Builder; use YOOtheme\Event; use YOOtheme\GraphQL\Executor\ExecutionResult; use YOOtheme\GraphQL\GraphQL; use YOOtheme\GraphQL\SchemaBuilder; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\Introspection; class Source extends SchemaBuilder { /** * @var Schema|null */ protected $schema; /** * Gets the schema. * * @return Schema */ public function getSchema() { return $this->schema ?: ($this->schema = $this->buildSchema()); } /** * Sets the schema. * * @param Schema $schema * * @return Schema */ public function setSchema(Schema $schema) { return $this->schema = $schema; } /** * Executes a query on schema. * * @param mixed $source * @param mixed $value * @param mixed $context * @param array|null $variables * @param string|null $operation * @param callable $fieldResolver * @param array $validationRules * * @return ExecutionResult */ public function query( $source, $value = null, $context = null, $variables = null, $operation = null, $fieldResolver = null, $validationRules = null ) { if (is_array($source)) { $source = AST::fromArray($source); } return GraphQL::executeQuery( $this->getSchema(), $source, $value, $context, $variables, $operation, $fieldResolver, $validationRules, ); } /** * Executes an introspection on schema. * * @param array $options * * @return ExecutionResult */ public function queryIntrospection(array $options = []) { $metadata = [ 'type' => $this->getType('Object'), 'resolve' => fn($type) => Event::emit( 'source.type.metadata|filter', $type->config['metadata'] ?? null, $type, ), ]; $options += [ '__Type' => compact('metadata'), '__Field' => compact('metadata'), '__Directive' => compact('metadata'), '__InputValue' => compact('metadata'), ]; return GraphQL::executeQuery( $this->getSchema(), Introspection::getIntrospectionQuery($options), ); } } builder-source/src/Source/Type/RequestType.php 0000644 00000006607 15166657206 0015472 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Type; use function YOOtheme\trans; use YOOtheme\Http\Request; class RequestType { /** * @return array */ public static function config() { return [ 'fields' => [ 'url' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('URL'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveUrl', ], ], 'method' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Method'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveMethod', ], ], 'scheme' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Scheme'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveScheme', ], ], 'host' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Host'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveHost', ], ], 'port' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Port'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolvePort', ], ], 'path' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Path'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolvePath', ], ], 'query' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Query'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveQuery', ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Request'), ], ]; } public static function resolveUrl(Request $request) { return (string) $request->getUri(); } public static function resolveMethod(Request $request) { return $request->getMethod(); } public static function resolveScheme(Request $request) { return $request->getUri()->getScheme(); } public static function resolveHost(Request $request) { return $request->getUri()->getHost(); } public static function resolvePort(Request $request) { return $request->getUri()->getPort(); } public static function resolvePath(Request $request) { return $request->getUri()->getPath(); } public static function resolveQuery(Request $request) { return $request->getUri()->getQuery(); } } builder-source/src/Source/Type/SiteType.php 0000644 00000006502 15166657206 0014740 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Type; use YOOtheme\Config; use YOOtheme\Http\Request; use function YOOtheme\app; use function YOOtheme\trans; class SiteType { /** * @return array */ public static function config() { return [ 'fields' => [ 'title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Site Title'), 'filters' => ['limit', 'preserve'], ], ], 'page_title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Page Title'), 'filters' => ['limit', 'preserve'], ], ], 'page_locale' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Page Locale'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolvePageLocale', ], ], 'page_url' => [ 'type' => 'String', 'args' => [ 'query' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Page URL'), 'arguments' => [ 'query' => [ 'label' => trans('Query String'), 'type' => 'checkbox', 'text' => trans('Include query string'), 'default' => false, ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolvePageUrl', ], ], 'is_guest' => [ 'type' => 'Int', 'metadata' => [ 'label' => trans('Guest User'), 'condition' => true, ], ], 'user' => [ 'type' => 'User', 'metadata' => [ 'label' => trans('Current User'), ], ], 'request' => [ 'type' => 'Request', 'metadata' => [ 'label' => trans('Request'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveRequest', ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Site'), ], ]; } public static function resolveRequest() { return app(Request::class); } public static function resolvePageUrl($obj, array $args) { $uri = static::resolveRequest()->getUri(); return $uri->getPath() . ($args['query'] ? "?{$uri->getQuery()}" : ''); } public static function resolvePageLocale() { return app(Config::class)('locale.code'); } } builder-source/src/Source/Listener/LoadBuilderConfig.php 0000644 00000002016 15166657206 0017346 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Listener; use YOOtheme\Builder\BuilderConfig; use YOOtheme\Builder\Source; use YOOtheme\Config; use YOOtheme\GraphQL\SchemaPrinter; class LoadBuilderConfig { public Config $config; public Source $source; public function __construct(Config $config, Source $source) { $this->config = $config; $this->source = $source; } /** * @param BuilderConfig $config */ public function handle($config): void { $dir = $this->config->get('image.cacheDir'); $file = "{$dir}/schema-{$this->config->get('source.id')}.gql"; $result = $this->source->queryIntrospection()->toArray(); $content = SchemaPrinter::doPrint($this->source->getSchema()); // update schema cache if (isset($result['data'])) { file_put_contents($file, $content); } elseif (is_file($file)) { unlink($file); } $config->merge(['schema' => $result['data']['__schema'] ?? $result]); } } builder-source/src/Source/Listener/OrderSourceMetadata.php 0000644 00000000600 15166657206 0017724 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Listener; class OrderSourceMetadata { public static function handle($metadata) { if (!empty($metadata['fields'])) { uasort( $metadata['fields'], fn($fieldA, $fieldB) => ($fieldA['@order'] ?? 0) - ($fieldB['@order'] ?? 0), ); } return $metadata; } } builder-source/src/Source/Listener/LoadSourceSchema.php 0000644 00000002630 15166657206 0017215 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Listener; use YOOtheme\Builder\Source; use YOOtheme\Config; use YOOtheme\Event; use YOOtheme\File; use YOOtheme\GraphQL\Error\Error; use YOOtheme\Http\Request; class LoadSourceSchema { public Config $config; public Request $request; public function __construct(Config $config, Request $request) { $this->config = $config; $this->request = $request; } /** * @param Source $source * @return bool|null */ public function handle($source): ?bool { $dir = $this->config->get('image.cacheDir'); $name = "schema-{$this->config->get('source.id')}"; $file = "{$dir}/{$name}.gql"; try { if ( $this->config->get('app.isSite') && !$this->request->getAttribute('customizer') && is_file($file) && filectime($file) > filectime(__FILE__) ) { // load schema from cache $hash = hash('crc32b', $file); $source->setSchema($source->loadSchema($file, "{$dir}/schema-{$hash}.php")); // stop event return false; } // delete invalid schema cache } catch (Error $e) { Event::emit('source.error', [$e]); File::rename($file, "{$dir}/{$name}.error.gql"); } return null; } } builder-source/src/Source/Listener/LogSourceError.php 0000644 00000002307 15166657206 0016751 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Listener; use YOOtheme\Config; use YOOtheme\GraphQL\Error\DebugFlag; use YOOtheme\GraphQL\Error\FormattedError; use YOOtheme\Metadata; class LogSourceError { public Config $config; public Metadata $metadata; public function __construct(Config $config, Metadata $metadata) { $this->config = $config; $this->metadata = $metadata; } public function handle($errors): void { if ($this->config->get('app.debug') || $this->config->get('app.isCustomizer')) { $this->metadata->set( 'script:graphql-errors', join( "\n", array_map( fn($error) => sprintf( 'console.warn(%s);', json_encode( FormattedError::createFromException( $error, DebugFlag::INCLUDE_DEBUG_MESSAGE, ), ), ), $errors, ), ), ); } } } builder-source/src/Source/Query/Node.php 0000644 00000004217 15166657206 0014244 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Query; class Node implements \JsonSerializable { /** * @var string */ public $kind; /** * @var ?string */ public $name; /** * @var ?string */ public $alias; /** * @var array */ public $children = []; /** * @var array */ public $arguments = []; /** * @var array */ public $directives = []; /** * Constructor. */ public function __construct($kind, $name, array $options = []) { $this->kind = $kind; $this->name = $name; foreach ($options as $key => $value) { $this->$key = $value; } } public function get($name) { foreach ($this->children as $child) { if ($child->name === $name) { return $child; } } return null; } public function query($name = null) { static::assertNode($this, 'Document'); return $this->children[] = new self('Query', $name); } public function field($name, array $arguments = []) { static::assertNode($this, 'Field', 'Query'); return $this->children[] = new self('Field', $name, ['arguments' => $arguments]); } public function directive($name, array $arguments = []) { static::assertNode($this, 'Field'); return $this->directives[] = new self('Directive', $name, ['arguments' => $arguments]); } public function toAST() { return AST::build($this); } public function toHash() { return hash('crc32b', json_encode($this)); } #[\ReturnTypeWillChange] public function jsonSerialize() { return array_values( array_filter([$this->kind, $this->name, $this->arguments, $this->directives]), ); } public static function document() { return new self('Document', null); } protected static function assertNode(self $node, ...$kind) { if (!in_array($node->kind, $kind, true)) { throw new \Exception('Node must be a ' . join(', ', $kind)); } } } builder-source/src/Source/Query/AST.php 0000644 00000007225 15166657206 0014010 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Query; class AST { public static function build(Node $node) { $build = [static::class, $node->kind]; return $build($node); } public static function field(Node $node) { $result = [ 'kind' => 'Field', 'name' => static::name($node->name), 'arguments' => static::arguments($node->arguments), 'directives' => array_map([static::class, 'directive'], $node->directives), ]; if ($node->alias) { $result['alias'] = static::name($node->alias); } if ($node->children) { $result['selectionSet'] = static::selections($node->children); } return $result; } public static function query(Node $node) { $result = [ 'kind' => 'OperationDefinition', 'operation' => 'query', 'selectionSet' => static::selections($node->children), 'variableDefinitions' => [], ]; if ($node->name) { $result['name'] = static::name($node->name); } return $result; } public static function document(Node $node) { return [ 'kind' => 'Document', 'definitions' => array_map([static::class, 'build'], $node->children), ]; } public static function directive(Node $node) { return [ 'kind' => 'Directive', 'name' => static::name($node->name), 'arguments' => static::arguments($node->arguments), ]; } public static function name($name) { return [ 'kind' => 'Name', 'value' => $name, ]; } public static function value($value) { switch (gettype($value)) { case 'NULL': return ['kind' => 'NullValue']; case 'string': return ['kind' => 'StringValue', 'value' => $value]; case 'boolean': return ['kind' => 'BooleanValue', 'value' => $value]; case 'integer': return ['kind' => 'IntValue', 'value' => strval($value)]; case 'double': return ['kind' => 'FloatValue', 'value' => strval($value)]; case 'array': return [ 'kind' => 'ListValue', 'values' => array_map([static::class, 'value'], $value), ]; case 'object': $fields = []; foreach (get_object_vars($value) as $key => $val) { $fields[] = static::objectField($key, $val); } return [ 'kind' => 'ObjectValue', 'fields' => $fields, ]; } } public static function objectField($name, $value) { return [ 'kind' => 'ObjectField', 'name' => static::name($name), 'value' => static::value($value), ]; } public static function arguments(array $arguments) { $result = []; foreach ($arguments as $name => $value) { $result[] = [ 'kind' => 'Argument', 'name' => static::name($name), 'value' => static::value($value), ]; } return $result; } public static function selections(array $selections) { $result = [ 'kind' => 'SelectionSet', 'selections' => [], ]; foreach ($selections as $selection) { $result['selections'][] = static::build($selection); } return $result; } } builder-source/src/Source/SourceQuery.php 0000644 00000004651 15166657206 0014542 0 ustar 00 <?php namespace YOOtheme\Builder\Source; use YOOtheme\Builder\Source\Query\Node; class SourceQuery { public const PARENT = '#parent'; /** * @var Node */ public $document; /** * Constructor. */ public function __construct(?Node $document = null) { $this->document = $document ?? Node::document(); } /** * Creates a source query. * * @param object $node * * @return Node */ public function create($node) { return $this->querySource($node->source, $this->document->query()); } /** * Query source definition. * * @param object $source * @param Node $node * * @return Node */ public function querySource($source, Node $node) { $field = $node; // add query selection if ($source->query->name !== self::PARENT) { $field = $this->queryField($source->query, $field); } // add field selection if (isset($source->query->field)) { $field = $this->queryField($source->query->field, $field); } // add source properties foreach ((array) ($source->props ?? []) as $prop) { if (!str_starts_with($prop->name, '#')) { $this->queryField($prop, $field); } } return $field; } /** * Create nested field nodes. * * @param object $field * @param Node $node * * @return Node */ public function queryField($field, Node $node) { $parts = explode('.', $field->name); $name = array_pop($parts); $arguments = (array) ($field->arguments ?? []); $directives = (array) ($field->directives ?? []); foreach ($parts as $part) { $node = is_null($_node = $node->get($part)) ? $node->field($part) : $_node; } // check if field already exists $nodeExists = $node->get($name); // create node for field $node = $node->field($name, $arguments); // add directives foreach ($directives as $directive) { $node->directive($directive->name, (array) ($directive->arguments ?? [])); } // add alias if ($nodeExists && $nodeExists->toHash() !== ($hash = $node->toHash())) { $node->alias = "{$name}_{$hash}"; $field->name .= "_{$hash}"; } return $node; } } builder-source/src/Source/SourceFilter.php 0000644 00000011167 15166657206 0014662 0 ustar 00 <?php namespace YOOtheme\Builder\Source; use YOOtheme\Arr; use YOOtheme\Str; trait SourceFilter { /** * @var array */ public $filters; /** * Constructor. * * @param array $filters */ public function __construct(array $filters = []) { $this->filters = array_merge( [ 'date' => [$this, 'applyDate'], 'limit' => [$this, 'applyLimit'], 'search' => [$this, 'applySearch'], 'before' => [$this, 'applyBefore'], 'after' => [$this, 'applyAfter'], 'condition' => [$this, 'applyCondition'], ], $filters, ); } /** * Adds a filter. * * @param string $name * @param callable $filter * @param int $offset */ public function addFilter($name, callable $filter, $offset = null) { Arr::splice($this->filters, $offset, 0, [$name => $filter]); } /** * Apply "before" filter. * * @param mixed $value * @param mixed $before * * @return string */ public function applyBefore($value, $before) { return $value != '' ? $before . $value : $value; } /** * Apply "after" filter. * * @param mixed $value * @param mixed $after * * @return string */ public function applyAfter($value, $after) { return $value != '' ? $value . $after : $value; } /** * Apply "limit" filter. * * @param mixed $value * @param mixed $limit * @param array $filters * * @return string */ public function applyLimit($value, $limit, array $filters) { if ($limit) { $value = preg_replace('/\s*<br[^<]*?\/?>\s*/', ' ', $value); $value = Str::limit( strip_tags($value), intval($limit), '…', !($filters['preserve'] ?? false), ); } return $value; } /** * Apply "date" filter. * * @param mixed $value * @param mixed $format * * @return false|string */ public function applyDate($value, $format) { if (!$value) { return $value; } if (is_string($value) && !is_numeric($value)) { $value = strtotime($value); } return date($format ?: 'd/m/Y', intval($value) ?: time()); } /** * Apply "search" filter. * * @param mixed $value * @param mixed $search * @param array $filters * * @return false|string */ public function applySearch($value, $search, array $filters) { $replace = $filters['replace'] ?? ''; if ($search && $search[0] === '/') { return @preg_replace($search, $replace, $value); } return str_replace($search, $replace, $value); } /** * Apply "condition" filter. * * @param mixed $value * @param mixed $operator * @param array $filters * * @return bool */ public function applyCondition($value, $operator, array $filters) { $propertyValue = html_entity_decode($value); $conditionValue = $filters['condition_value'] ?? ''; if ($operator === '!') { return empty($propertyValue); } if ($operator === '!!') { return !empty($propertyValue); } if ($operator === '=') { return $propertyValue == $conditionValue; } if ($operator === '!=') { return $propertyValue != $conditionValue; } if ($operator === '<') { return $propertyValue < $conditionValue; } if ($operator === '>') { return $propertyValue > $conditionValue; } if ($operator === '~=') { return str_contains($propertyValue, $conditionValue); } if ($operator === '!~=') { return !str_contains($propertyValue, $conditionValue); } if ($operator === '^=') { return str_starts_with($propertyValue, $conditionValue); } if ($operator === '!^=') { return !str_starts_with($propertyValue, $conditionValue); } if ($operator === '$=') { return str_ends_with($propertyValue, $conditionValue); } if ($operator === '!$=') { return !str_ends_with($propertyValue, $conditionValue); } if ($operator === 'regex') { return @preg_match($conditionValue, $propertyValue) > 0; } return !!$propertyValue; } } builder-source/src/Source/SourceTransform.php 0000644 00000015527 15166657206 0015414 0 ustar 00 <?php namespace YOOtheme\Builder\Source; use YOOtheme\Arr; use YOOtheme\Builder\Source; use YOOtheme\Config; use YOOtheme\Event; use function YOOtheme\app; class SourceTransform { use SourceFilter; /** * Transform callback "preload". * * @param object $node * @param array $params * * @return void */ public function preload($node, array &$params) { if ($params['context'] !== 'render') { return; } if (empty($node->source->query->name)) { return; } if ($node->source->query->name === SourceQuery::PARENT) { if (!empty($params['source'])) { $params['source']->source->children[] = $node; } if (!empty($node->source->query->field->name)) { $params['source'] = $node; } } else { $params['source'] = $node; } } /** * Transform callback "prerender". * * @param object $node * @param array $params * * @return bool|void */ public function prerender($node, array &$params) { if (isset($node->source->data)) { $params['data'] = $node->source->data; } if (empty($node->source->query->name)) { return; } if ($node->source->query->name === SourceQuery::PARENT) { // Ignore if no field is mapped if (empty($node->source->props) && empty($node->source->children)) { return; } return $this->resolveSource($node, $params); } if ($result = $this->querySource($node, $params)) { $params['data'] = $result['data'] ?? null; return $this->resolveSource($node, $params); } } /** * Create source query. * * @param object $node * * @return ?Query\Node */ public function createQuery($node) { $query = new SourceQuery(); $parent = $query->create($node); $props = !empty($node->source->props); // extend source query foreach ($node->source->children ?? [] as $child) { $props = $this->createChildQuery($query, $parent, $child) || $props; } return $props ? $query->document : null; } /** * Add child queries * * @param SourceQuery $query * @param Query\Node $parent * @param object $node * * @return bool */ protected function createChildQuery($query, $parent, $node) { $p = $query->querySource($node->source, $parent); $props = !empty($node->source->props); foreach ($node->source->children ?? [] as $child) { $props = $this->createChildQuery($query, $p, $child) || $props; } return $props; } /** * Query source data. * * @param object $node * @param array $params * * @return array|void */ public function querySource($node, array $params) { if (!($query = $this->createQuery($node))) { return; } // execute query without validation rules $result = app(Source::class)->query( $query->toAST(), $params, new \ArrayObject(), null, null, null, app(Config::class)->get('app.debug') ? null : [], ); if (!empty($result->errors)) { Event::emit('source.error', $result->errors, $node); } return $result->toArray(); } /** * Map source properties. * * @param object $node * @param array $params * * @return ?object */ public function mapSource($node, array $params) { foreach ($node->source->props ?? [] as $name => $prop) { $value = trim($this->toString(Arr::get($params, "data.{$prop->name}"))); $filters = (array) ($prop->filters ?? []); // apply value filters foreach (array_intersect_key($this->filters, $filters) as $key => $filter) { $value = $filter($value, $filters[$key], $filters, $params); } // check condition value if ($name === '_condition' && $value === false) { return null; } // set filtered value $node->props[$name] = $value; } return $node; } /** * Repeat node for each source item. * * @param object $node * @param array $params * * @return array */ public function repeatSource($node, array $params) { $nodes = []; // clone and map node for each item foreach ($params['data'] as $index => $data) { $data = (array) $data; $data['#index'] = $index; $data['#first'] = $index === 0; $data['#last'] = $index === array_key_last($params['data']); if ($clone = $this->mapSource($this->cloneNode($node), ['data' => $data] + $params)) { $clone->source = (object) ['data' => $data]; $nodes[] = $clone; } } // insert cloned nodes after current node array_splice($params['parent']->children, $params['i'] + 1, 0, $nodes); return $nodes; } /** * Resolve source data. * * @param object $node * @param array $params * * @return bool */ public function resolveSource($node, array &$params) { $name = 'data'; // add query name if ($node->source->query->name !== SourceQuery::PARENT) { $name .= ".{$node->source->query->name}"; } // add field name if (isset($node->source->query->field)) { $name .= ".{$node->source->query->field->name}"; } // get source data $params['data'] = Arr::get($params, $name); if (!empty($node->source->props->_condition->filters->show_empty)) { return !$params['data']; } if ($params['data'] && is_array($params['data'])) { if (!array_is_list($params['data'])) { return (bool) $this->mapSource($node, $params); } $this->repeatSource($node, $params); } return false; } /** * Clone node recursively. * * @param object $node * * @return object */ protected function cloneNode($node) { $clone = clone $node; // recursively clone children if (isset($node->children)) { $clone->children = array_map(fn($child) => $this->cloneNode($child), $node->children); } return $clone; } protected function toString($value) { if (is_scalar($value) || is_callable([$value, '__toString'])) { return (string) $value; } return ''; } } builder-source/src/Source/OptimizeTransform.php 0000644 00000000546 15166657206 0015747 0 ustar 00 <?php namespace YOOtheme\Builder\Source; class OptimizeTransform { /** * Transform callback. * * @param object $node * @param array $params */ public function __invoke($node, array $params) { if (empty($node->source->query) && isset($node->source->props)) { unset($node->source); } } } builder-source/updates.php 0000644 00000000334 15166657206 0011664 0 ustar 00 <?php namespace YOOtheme; return [ '3.0.0-beta.1.2' => function ($node) { if (isset($node->source->query->name) && empty($node->source->query->name)) { unset($node->source); } }, ]; builder-source/bootstrap.php 0000644 00000003305 15166657206 0012235 0 ustar 00 <?php namespace YOOtheme\Builder\Source; use YOOtheme\Application; use YOOtheme\Builder; use YOOtheme\Builder\BuilderConfig; use YOOtheme\Builder\Source; use YOOtheme\Builder\UpdateTransform; use YOOtheme\Event; use YOOtheme\GraphQL\Directive\SliceDirective; use YOOtheme\GraphQL\Plugin\ContainerPlugin; use YOOtheme\GraphQL\Type\ObjectScalarType; return [ 'events' => [ 'source.init' => [Listener\LoadSourceSchema::class => ['@handle', 50]], 'source.error' => [Listener\LogSourceError::class => '@handle'], 'source.type.metadata' => [Listener\OrderSourceMetadata::class => 'handle'], BuilderConfig::class => [Listener\LoadBuilderConfig::class => '@handle'], ], 'config' => [ BuilderConfig::class => __DIR__ . '/config/customizer.json', ], 'extend' => [ Builder::class => function (Builder $builder, $app) { $source = $app(SourceTransform::class); $builder->addTransform('preload', [$source, 'preload']); $builder->addTransform('prerender', [$source, 'prerender'], 2); // Before Placeholder Transform $builder->addTransform('presave', new OptimizeTransform()); }, UpdateTransform::class => function (UpdateTransform $update) { $update->addGlobals(require __DIR__ . '/updates.php'); }, ], 'services' => [ Source::class => function (SliceDirective $slice, ObjectScalarType $objectType) { $source = new Source([new ContainerPlugin(Application::getInstance())]); $source->setType($objectType); $source->setDirective($slice); Event::emit('source.init', $source); return $source; }, ], ]; http-server/bootstrap.php 0000644 00000001366 15166657206 0011601 0 ustar 00 <?php use YOOtheme\BodyMiddleware; use YOOtheme\CsrfMiddleware; use YOOtheme\Router; use YOOtheme\RouterMiddleware; use YOOtheme\Routes; use YOOtheme\UrlResolver; return [ 'events' => [ 'app.request' => [ BodyMiddleware::class => ['parseJson', 10], CsrfMiddleware::class => ['@handle', 10], RouterMiddleware::class => [['@handleRoute', 30], ['@handleStatus', 20]], ], 'app.error' => [RouterMiddleware::class => ['@handleError', 10]], 'url.resolve' => [UrlResolver::class => 'resolve'], ], 'aliases' => [ Routes::class => 'routes', ], 'services' => [ Routes::class => '', Router::class => '', RouterMiddleware::class => '', ], ]; http-server/src/Route.php 0000644 00000004746 15166657207 0011457 0 ustar 00 <?php namespace YOOtheme; class Route { /** * @var string */ protected $name; /** * @var string */ protected $path; /** * @var string|callable */ protected $callable; /** * @var array */ protected $methods = []; /** * @var array */ protected $attributes = []; /** * Constructor. * * @param string $path * @param string|callable $callable * @param string|string[] $methods */ public function __construct($path, $callable, $methods = []) { $this->setPath($path); $this->setMethods($methods); $this->callable = $callable; } /** * Gets the path. * * @return string */ public function getPath() { return $this->path; } /** * Sets the path. * * @param string $path * * @return $this */ public function setPath($path) { $this->path = '/' . trim($path, '/'); return $this; } /** * Gets the callable. * * @return string|callable */ public function getCallable() { return $this->callable; } /** * Gets the methods. * * @return string[] */ public function getMethods() { return $this->methods; } /** * Sets the methods. * * @param string|string[] $methods * * @return $this */ public function setMethods($methods) { $this->methods = array_map('strtoupper', (array) $methods); return $this; } /** * Gets an attribute. * * @param string $name * @param mixed $default * * @return mixed */ public function getAttribute($name, $default = null) { return $this->attributes[$name] ?? $default; } /** * Sets an attribute. * * @param string $name * @param mixed $value * * @return $this */ public function setAttribute($name, $value) { $this->attributes[$name] = $value; return $this; } /** * Gets the attributes. * * @return array */ public function getAttributes() { return $this->attributes; } /** * Sets the attributes. * * @param array $attributes * * @return $this */ public function setAttributes(array $attributes) { $this->attributes = $attributes; return $this; } } http-server/src/RouterMiddleware.php 0000644 00000003254 15166657207 0013630 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\Http\Request; use YOOtheme\Http\Response; class RouterMiddleware { public const FOUND = 1; public const NOT_FOUND = 0; public const METHOD_NOT_ALLOWED = 2; /** * @var Router */ protected $router; /** * Constructor. * * @param Router $router */ public function __construct(Router $router) { $this->router = $router; } /** * Handles the route dispatch. * * @param Request $request * @param callable $next * * @return Response */ public function handleRoute($request, callable $next) { return $next($this->router->dispatch($request)); } /** * Handles the route status. * * @param Request $request * @param callable $next * * @return Response */ public function handleStatus($request, callable $next) { $status = $request->getAttribute('routeStatus'); // Not found if ($status === static::NOT_FOUND) { $request->abort(404); } // Method not allowed if ($status === static::METHOD_NOT_ALLOWED) { $request->abort(405); } return $next($request); } /** * Handles an error. * * @param Response $response * @param \Exception $exception * * @return Response */ public function handleError($response, $exception) { if ($exception instanceof Http\Exception) { return $response->withStatus($exception->getCode(), $exception->getMessage()); } return $response->withStatus(500, $exception->getMessage()); } } http-server/src/UrlResolver.php 0000644 00000000620 15166657207 0012630 0 ustar 00 <?php namespace YOOtheme; class UrlResolver { public static function resolve(Config $config, $path, $parameters, $secure, callable $next) { $root = $config('app.rootDir'); $path = Path::resolveAlias($path); if (Path::isBasePath($root, $path)) { $path = Path::relative($root, $path); } return $next($path, $parameters, $secure); } } http-server/src/Routes.php 0000644 00000011007 15166657207 0011626 0 ustar 00 <?php namespace YOOtheme; class Routes implements \IteratorAggregate { /** * @var Route[] */ protected $index = []; /** * @var Route[] */ protected $routes = []; /** * Adds a route. * * @param string|string[] $method * @param string $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function map($method, $path, $handler, array $attributes = []) { $route = new Route($path, $handler, $method); $route->setAttributes($attributes); if ($this->index) { $this->index = []; } return $this->routes[] = $route; } /** * Adds a GET route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function get($path, $handler, array $attributes = []) { return $this->map('GET', $path, $handler, $attributes); } /** * Adds a POST route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function post($path, $handler, array $attributes = []) { return $this->map('POST', $path, $handler, $attributes); } /** * Adds a PUT route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function put($path, $handler, array $attributes = []) { return $this->map('PUT', $path, $handler, $attributes); } /** * Adds a PATCH route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function patch($path, $handler, array $attributes = []) { return $this->map('PATCH', $path, $handler, $attributes); } /** * Adds a DELETE route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function delete($path, $handler, array $attributes = []) { return $this->map('DELETE', $path, $handler, $attributes); } /** * Adds a HEAD route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function head($path, $handler, array $attributes = []) { return $this->map('HEAD', $path, $handler, $attributes); } /** * Adds a OPTIONS route. * * @param string|string[] $path * @param string|callable $handler * @param array $attributes * * @return Route */ public function options($path, $handler, array $attributes = []) { return $this->map('OPTIONS', $path, $handler, $attributes); } /** * Adds a group of routes. * * @param string $prefix * @param callable $group * * @return self */ public function group($prefix, callable $group) { $routes = new self(); $group($routes); return $this->mount($prefix, $routes); } /** * Mounts a route collection. * * @param string $prefix * @param Routes $routes * * @return $this */ public function mount($prefix, Routes $routes) { $prefix = trim($prefix, '/'); foreach ($routes as $route) { $this->routes[] = $route->setPath($prefix . $route->getPath()); } return $this; } /** * Gets a route by name. * * @param string $name * * @return Route|null */ public function getRoute($name) { $index = $this->getIndex(); return $index[$name] ?? null; } /** * Gets an index of routes. * * @return Route[] */ public function getIndex() { if (!$this->index) { foreach ($this->routes as $index => $route) { $this->index[$route->getAttribute('name', "route_{$index}")] = $route; } } return $this->index; } /** * Implements the IteratorAggregate. * * @return \ArrayIterator */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->routes); } } http-server/src/CsrfMiddleware.php 0000644 00000002302 15166657207 0013236 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\Http\Request; use YOOtheme\Http\Response; class CsrfMiddleware { /** * Current token. * * @var string */ protected $token; /** * Verify callable. * * @var callable */ protected $verify; /** * Constructor. * * @param string $token * @param ?callable $verify */ public function __construct($token, ?callable $verify = null) { $this->token = $token; $this->verify = $verify ?: [$this, 'verifyToken']; } /** * Handles CSRF token from request. * * @param Request $request * @param callable $next * * @return Response */ public function handle($request, callable $next) { $csrf = $request->getAttribute('csrf', in_array($request->getMethod(), ['POST', 'DELETE'])); if ($csrf && !($this->verify)($request->getHeaderLine('X-XSRF-Token'))) { $request->abort(401, 'Invalid CSRF token.'); } return $next($request); } /** * Verifies a CSRF token. */ public function verifyToken(string $token): bool { return $this->token === $token; } } http-server/src/BodyMiddleware.php 0000644 00000001060 15166657207 0013236 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\Http\Request; use YOOtheme\Http\Response; class BodyMiddleware { /** * Handles JSON requests. * * @param Request $request * @param callable $next * * @return Response */ public static function parseJson($request, callable $next) { if (stripos($request->getHeaderLine('Content-Type'), 'application/json') === 0) { $request = $request->withParsedBody(json_decode((string) $request->getBody(), true)); } return $next($request); } } http-server/src/HttpClientInterface.php 0000644 00000001613 15166657207 0014246 0 ustar 00 <?php namespace YOOtheme; interface HttpClientInterface { /** * Execute a GET HTTP request. * * @param string $url * @param array $options * * @return mixed */ public function get($url, $options = []); /** * Execute a POST HTTP request. * * @param string $url * @param string $data * @param array $options * * @return mixed */ public function post($url, $data = null, $options = []); /** * Execute a PUT HTTP request. * * @param string $url * @param string $data * @param array $options * * @return mixed */ public function put($url, $data = null, $options = []); /** * Execute a DELETE HTTP request. * * @param string $url * @param array $options * * @return mixed */ public function delete($url, $options = []); } http-server/src/Router.php 0000644 00000003543 15166657207 0011633 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\Http\Request; class Router { /** * @var Routes */ protected $routes; /** * Constructor. * * @param Routes $routes */ public function __construct(Routes $routes) { $this->routes = $routes; } /** * Dispatches router for a request. * * @param Request $request * * @return Request */ public function dispatch(Request $request) { $path = '/' . trim($request->getQueryParam('p', ''), '/'); foreach ($this->routes->getIndex() as $route) { if ($route->getMethods() && !in_array($request->getMethod(), $route->getMethods())) { continue; } if (preg_match($this->getPattern($route), $path, $matches)) { $params = []; foreach ($matches as $key => $value) { if (is_string($key)) { $params[$key] = urldecode($value); } } foreach ($route->getAttributes() as $name => $value) { $request = $request->withAttribute($name, $value); } return $request ->withAttribute('route', $route) ->withAttribute('routeParams', $params) ->withAttribute('routeStatus', 1); } } return $request->withAttribute('routeStatus', 0); } /** * Gets the route regex pattern. * * @param Route $route * * @return string */ protected function getPattern(Route $route) { return '#^' . preg_replace_callback( '#\{(\w+)\}#', fn($matches) => '(?P<' . $matches[1] . '>[^/]+)', $route->getPath(), ) . '$#'; } } application/src/Application.php 0000644 00000007540 15166657207 0012637 0 ustar 00 <?php namespace YOOtheme; use Psr\Http\Message\ResponseInterface; use YOOtheme\Configuration\Configuration; use YOOtheme\Http\Request; use YOOtheme\Http\Response; /** * @property Response $response * @property Request $request */ class Application extends Container { /** * @var Config */ protected $config; /** * @var array */ protected $loaders = []; /** * @var static|null */ protected static $instance; /** * Constructor. * * @param string $cache */ public function __construct($cache = null) { $this->config = new Configuration($cache); $this->set(static::class, $this); $this->setAlias(static::class, 'app'); $this->set(Config::class, $this->config); $this->setAlias(Config::class, 'config'); } /** * Gets global application. * * @param null|mixed $cache * * @return static */ public static function getInstance($cache = null) { return static::$instance ??= new static($cache); } /** * Run application. * * @param bool $send * * @return ResponseInterface */ public function run($send = true) { try { $response = Event::emit('app.request|middleware', [$this, 'handle'], $this->request); } catch (\Exception $exception) { $response = Event::emit('app.error|filter', $this->response, $exception); } return $send ? $response->send() : $response; } /** * Handles a request. * * @param Request $request * * @throws \Exception * * @return Response */ public function handle(Request $request) { $this->set(Request::class, $request); $route = $request->getAttribute('route'); $result = $this->call($route->getCallable()); if ($result instanceof Response) { return $result; } if (is_string($result) || (is_object($result) && method_exists($result, '__toString'))) { return $this->response->write((string) $result); } return $this->response; } /** * Loads a bootstrap file. * * @param string $files * * @return $this */ public function load($files) { $configs = []; foreach (File::glob($files, GLOB_NOSORT) as $file) { $configs = static::loadFile($file, $configs, ['app' => $this]); } if (isset($configs['loaders'])) { $this->loaders = array_merge($this->loaders, ...$configs['loaders']); } foreach (array_intersect_key($this->loaders, $configs) as $name => $loader) { if (is_string($loader) && class_exists($loader)) { $loader = $this->loaders[$name] = $this->resolveLoader($loader); } $loader($this, $configs[$name]); } return $this; } /** * Resolves a service instance. * * @param string $id * * @throws \Exception * @throws \ReflectionException * * @return mixed */ protected function resolveService($id) { return Hook::call([$id, 'app.resolve'], fn($id) => parent::resolveService($id), $id, $this); } /** * Resolves a loader instance. */ protected function resolveLoader(string $loader): callable { return new $loader($this); } /** * Loads a bootstrap config. */ protected static function loadFile(string $file, array $configs, array $parameters = []): array { extract($parameters, EXTR_SKIP); if (!is_array($config = require $file)) { throw new \RuntimeException("Unable to load file '{$file}'"); } foreach ($config as $key => $value) { $configs[$key][] = $value; } return $configs; } } application/src/Application/RouteLoader.php 0000644 00000000743 15166657207 0015062 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Container; class RouteLoader { /** * Load routes. * * @param Container $container * @param array $configs */ public function __invoke(Container $container, array $configs) { $container->extend('routes', static function ($routes) use ($configs) { foreach (array_merge(...$configs) as $route) { $routes->map(...$route); } }); } } application/src/Application/EventLoader.php 0000644 00000002725 15166657207 0015047 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Container; use YOOtheme\Event; use YOOtheme\EventDispatcher; class EventLoader { /** * @var EventDispatcher */ protected $dispatcher; /** * Constructor. */ public function __construct() { $this->dispatcher = Event::getDispatcher(); } /** * Load event listeners. */ public function __invoke(Container $container, array $configs) { foreach ($configs as $events) { foreach ($events as $event => $listeners) { foreach ($listeners as $class => $parameters) { $parameters = (array) $parameters; if (is_string($parameters[0])) { $parameters = [$parameters]; } foreach ($parameters as $params) { $this->addListener($container, $event, $class, ...$params); } } } } } /** * Adds a listener. */ public function addListener( Container $container, string $event, string $class, string $method, ...$params ): void { $this->dispatcher->addListener( $event, fn(...$arguments) => $container->call( $method[0] === '@' ? $class . $method : [$class, $method], $arguments, ), ...$params, ); } } application/src/Application/AliasLoader.php 0000644 00000001002 15166657207 0015002 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Container; class AliasLoader { /** * Load service aliases. * * @param Container $container * @param array $configs */ public function __invoke(Container $container, array $configs) { $config = array_merge_recursive(...$configs); foreach ($config as $id => $aliases) { foreach ((array) $aliases as $alias) { $container->setAlias($id, $alias); } } } } application/src/Application/ConfigLoader.php 0000644 00000004215 15166657207 0015167 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Config; use YOOtheme\ConfigObject; use YOOtheme\Container; use YOOtheme\Event; use YOOtheme\Hook; class ConfigLoader { /** * @var Config */ protected $config; /** * @var array */ protected $services = []; /** * Constructor. */ public function __construct(Container $container) { $this->config = $container->get(Config::class); Hook::after('app.resolve', [$this, 'loadConfig']); } /** * Load configuration. */ public function __invoke(Container $container, array $configs) { foreach ($configs as $config) { if ($config instanceof \Closure) { $config = $config($this->config, $container); } elseif (is_array($config)) { $config = $this->loadArray($config); } $this->config->add('', (array) $config); } } /** * After resolve service. * * @param mixed $service */ public function loadConfig($service, string $id) { if (!$service instanceof ConfigObject) { return; } foreach ($this->services[$id] ?? [] as $value) { $service->merge(is_string($value) ? static::loadFile($value) : $value); } Event::emit($id, $service); } protected function loadArray(array $config) { foreach ($config as $key => $value) { if (str_contains($key, '\\') && str_ends_with($key, 'Config')) { $this->services[$key][] = $value; unset($config[$key]); } } return $config; } protected static function loadFile(string $file) { $type = pathinfo($file, PATHINFO_EXTENSION); if ($type === 'php') { return require $file; } if ($type === 'ini') { return parse_ini_file($file, true, INI_SCANNER_TYPED); } if ($type === 'json') { return json_decode(file_get_contents($file), true); } throw new \RuntimeException("Unable to load config file '{$file}'"); } } application/src/Application/ExtendLoader.php 0000644 00000000711 15166657207 0015206 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Container; class ExtendLoader { /** * Load service extenders. * * @param Container $container * @param array $configs */ public function __invoke(Container $container, array $configs) { foreach ($configs as $config) { foreach ($config as $id => $callback) { $container->extend($id, $callback); } } } } application/src/Application/ServiceLoader.php 0000644 00000002123 15166657207 0015356 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Container; class ServiceLoader { /** * Load services configuration. * * @param Container $container * @param array $configs */ public function __invoke(Container $container, array $configs) { $config = array_merge(...$configs); foreach ($config as $id => $service) { if (is_array($service)) { $definition = $container->add($id); if (isset($service['class'])) { $definition->setClass($service['class']); } if (isset($service['factory'])) { $definition->setFactory($service['factory']); } if (isset($service['arguments'])) { $definition->setArguments($service['arguments']); } if (isset($service['shared'])) { $definition->setShared($service['shared']); } } else { $container->add($id, $service); } } } } application/src/Storage.php 0000644 00000004017 15166657207 0011774 0 ustar 00 <?php namespace YOOtheme; abstract class Storage implements \JsonSerializable { /** * @var array */ protected $values = []; /** * @var bool */ protected $modified = false; /** * Gets a value (shortcut). * * @param string $key * @param mixed $default * * @return mixed */ public function __invoke($key, $default = null) { return Arr::get($this->values, $key, $default); } /** * Checks if a key exists. * * @param string $key * * @return bool */ public function has($key) { return Arr::has($this->values, $key); } /** * Gets a value. * * @param string $key * @param mixed $default * * @return mixed */ public function get($key, $default = null) { return Arr::get($this->values, $key, $default); } /** * Sets a value. * * @param string $key * @param mixed $value * * @return $this */ public function set($key, $value) { Arr::set($this->values, $key, $value); $this->modified = true; return $this; } /** * Deletes a value. * * @param string $key * * @return $this */ public function del($key) { Arr::del($this->values, $key); $this->modified = true; return $this; } /** * Checks if values are modified. * * @return bool */ public function isModified() { return $this->modified; } /** * Gets values which should be serialized to JSON. * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return $this->values; } /** * Adds values from JSON. * * @param string $json * * @return $this */ protected function addJson($json) { $this->values = Arr::merge($this->values, json_decode($json, true) ?: []); return $this; } } application/bootstrap.php 0000644 00000001553 15166657207 0011620 0 ustar 00 <?php namespace YOOtheme\Application; use YOOtheme\Config; use YOOtheme\Path; use YOOtheme\Url; use YOOtheme\View; return [ 'extend' => [ Config::class => function (Config $config) { $config->addFilter( 'url', fn($value, $file) => Url::to(Path::resolve(dirname($file), $value)), ); }, View::class => function (View $view, $app) { $view->addGlobal('app', $app); $view->addGlobal('config', $app(Config::class)); }, ], 'aliases' => [ View::class => 'view', ], 'loaders' => [ 'services' => ServiceLoader::class, 'aliases' => AliasLoader::class, 'extend' => ExtendLoader::class, 'events' => EventLoader::class, 'routes' => RouteLoader::class, 'config' => ConfigLoader::class, ], ]; application/functions.php 0000644 00000000655 15166657207 0011615 0 ustar 00 <?php namespace YOOtheme; /** * Increase xdebug max nesting level. */ if ($level = ini_get('xdebug.max_nesting_level')) { ini_set('xdebug.max_nesting_level', max((int) $level, 256)); } /** * Gets a service from application. * * @param string $id * @param string ...$ids * * @return mixed */ function app($id = null, ...$ids) { $app = Application::getInstance(); return $id ? $app($id, ...$ids) : $app; } view/src/View.php 0000644 00000016572 15166657207 0007762 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\Theme\ViewHelperInterface; use YOOtheme\View\HtmlElementInterface; use YOOtheme\View\HtmlHelperInterface; /** * @method string builder($node, $params = []) * @mixin ViewHelperInterface * @mixin HtmlHelperInterface * @mixin HtmlElementInterface */ class View implements \ArrayAccess { /** * @var \SplStack */ protected $loader; /** * @var array */ protected $template = []; /** * @var array */ protected $parameters = []; /** * @var array */ protected $filters = []; /** * @var array */ protected $globals = []; /** * @var array */ protected $helpers = []; /** * @var array */ protected $functions = []; /** * @var array */ private $evalParameters; /** * Constructor. * * @param callable $loader */ public function __construct(?callable $loader = null) { $this->loader = new \SplStack(); $this->loader->push([$this, 'evaluate']); if ($loader) { $this->addLoader($loader); } $this->addFunction('e', [$this, 'escape']); } /** * Renders a template (shortcut). * * @param string $name * @param mixed $parameters * * @return string|false */ public function __invoke($name, $parameters = []) { return $this->render($name, $parameters); } /** * Handles dynamic calls to the class. * * @param string $name * @param array $args * * @return mixed */ public function __call($name, $args) { if (!isset($this->functions[($key = strtolower($name))])) { trigger_error( sprintf('Call to undefined method %s::%s()', get_class($this), $name), E_USER_ERROR, ); } return call_user_func_array($this->functions[$key], $args); } /** * Gets the global parameters. * * @return array */ public function getGlobals() { return $this->globals; } /** * Adds a global parameter. * * @param string $name * @param mixed $value * * @return $this */ public function addGlobal($name, $value) { $this->globals[$name] = $value; return $this; } /** * Adds a helper. * * @param string|callable $helper * * @return $this */ public function addHelper($helper) { if (is_callable($helper)) { $helper($this); } elseif (class_exists($helper)) { new $helper($this); } return $this; } /** * Adds a custom function. * * @param string $name * @param callable $callback * * @return View */ public function addFunction($name, callable $callback) { $this->functions[strtolower($name)] = $callback; return $this; } /** * Adds a loader callback. * * @param callable $loader * @param string $filter * * @return $this */ public function addLoader(callable $loader, $filter = null) { if (is_null($filter)) { $next = $this->loader->top(); $this->loader->push(function ($name, array $parameters = []) use ($loader, $next) { return $loader($name, $parameters, $next); }); } else { $this->filters[$filter][] = $loader; } return $this; } /** * Applies multiple functions. * * @param mixed $value * @param string $functions * * @return string */ public function apply($value, $functions) { $functions = explode('|', strtolower($functions)); return array_reduce( $functions, fn($value, $function) => call_user_func([$this, $function], $value), $value, ); } /** * Converts special characters to HTML entities. * * @param string $value * @param string $functions * * @return string */ public function escape($value, $functions = '') { $value = strval($value); if ($functions) { $value = $this->apply($value, $functions); } return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); } /** * Renders a template. * * @param string $name * @param array|callable $parameters * * @return string|callable|false */ public function render($name, $parameters = []) { if (is_callable($parameters)) { return fn() => $this->render( $name, call_user_func_array($parameters, func_get_args()) ?: [], ); } $next = $this->loader->top(); foreach ($this->filters as $filter => $loaders) { if (!Str::is($filter, $name)) { continue; } foreach ($loaders as $loader) { $next = fn($name, array $parameters = []) => $loader($name, $parameters, $next); } } return $next( $name, array_replace(end($this->parameters) ?: $this->globals, $parameters, [ '_root' => empty($this->template), ]), ); } /** * Renders current template. * * @param mixed $parameters * * @return string|false */ public function self($parameters = []) { return $this->render(end($this->template), $parameters); } /** * Evaluates a template. * * @param string $template * @param array $parameters * * @return string|false */ public function evaluate($template, array $parameters = []) { $this->template[] = $template; $this->parameters[] = $this->evalParameters = $parameters; unset($template, $parameters); extract($this->evalParameters, EXTR_SKIP); unset($this->evalParameters); $__file = end($this->template); $__dir = dirname($__file); if (is_file($__file)) { ob_start(); require $__file; $result = ob_get_clean(); } array_pop($this->template); array_pop($this->parameters); return $result ?? false; } /** * Checks if a helper is registered. * * @param string $name * * @return bool */ #[\ReturnTypeWillChange] public function offsetExists($name) { return isset($this->helpers[$name]); } /** * Gets a helper. * * @param string $name * * @return mixed */ #[\ReturnTypeWillChange] public function offsetGet($name) { if (!$this->offsetExists($name)) { throw new \InvalidArgumentException(sprintf('Undefined helper "%s"', $name)); } return $this->helpers[$name]; } /** * Sets a helper. * * @param string $name * @param object $helper */ #[\ReturnTypeWillChange] public function offsetSet($name, $helper) { $this->helpers[$name] = $helper; } /** * Removes a helper. * * @param string $name */ #[\ReturnTypeWillChange] public function offsetUnset($name) { throw new \LogicException(sprintf('You can\'t remove a helper "%s"', $name)); } } view/src/View/HtmlHelperInterface.php 0000644 00000002045 15166657207 0013635 0 ustar 00 <?php namespace YOOtheme\View; interface HtmlHelperInterface { /** * Creates an element. * * @param string $name * @param array $attrs * @param mixed $contents * * @return HtmlElement */ public function el($name, array $attrs = [], $contents = false); /** * Renders a link tag. * * @param string $title * @param string $url * @param array $attrs * * @return string */ public function link($title, $url = null, array $attrs = []); /** * Renders an image tag. * * @param array|string $url * @param array $attrs * * @return string */ public function image($url, array $attrs = []); /** * Renders a form tag. * * @param array $tags * @param array $attrs * * @return string */ public function form($tags, array $attrs = []); /** * Renders tag attributes. * * @param array $attrs * * @return string */ public function attrs(array $attrs); } view/src/View/FileLoader.php 0000644 00000000636 15166657207 0011762 0 ustar 00 <?php namespace YOOtheme\View; use YOOtheme\File; class FileLoader { protected array $resolvedPaths = []; public function __invoke($name, $parameters, $next) { if (!str_ends_with(strtolower($name), '.php')) { $name .= '.php'; } $this->resolvedPaths[$name] ??= File::find($name); return $next($this->resolvedPaths[$name] ?: $name, $parameters); } } view/src/View/SectionHelper.php 0000644 00000004272 15166657207 0012520 0 ustar 00 <?php namespace YOOtheme\View; use YOOtheme\View; class SectionHelper { /** * @var array */ protected $sections = []; /** * @var array */ protected $openSections = []; /** * Constructor. * * @param View $view */ public function __construct(View $view) { $view['sections'] = $this; $view->addFunction('section', [$this, 'get']); } /** * Gets a section. * * @param string $name * @param string|false $default * * @return string|false */ public function get($name, $default = false) { if (empty($this->sections[$name])) { return $default; } return array_reduce($this->sections[$name], function ($result, $content) { if (is_callable($content)) { $result = $content($result); } elseif (is_string($content)) { $result .= $content; } return $result; }); } /** * Sets a section value. * * @param string $name * @param string|callable $content */ public function set($name, $content) { $this->sections[$name] = [$content]; } /** * Adds a section value by appending it. * * @param string $name * @param string|callable $content */ public function add($name, $content) { $this->sections[$name][] = $content; } /** * Checks if a section exists. * * @param string $name * * @return bool */ public function exists($name) { return isset($this->sections[$name]); } /** * Starts a new section. * * @param string $name */ public function start($name) { if (ob_start()) { $this->openSections[] = $name; } } /** * Stops a section. * * @throws \LogicException */ public function stop() { if (!($name = array_pop($this->openSections))) { throw new \LogicException('Cannot stop a section without first starting one.'); } $this->sections[$name] = [ob_get_clean()]; } } view/src/View/HtmlHelper.php 0000644 00000006620 15166657207 0012017 0 ustar 00 <?php namespace YOOtheme\View; use YOOtheme\View; class HtmlHelper implements HtmlHelperInterface { /** * @var callable[][] */ public $transforms = []; /** * Constructor. * * @param View $view */ public function __construct(View $view) { $view['html'] = $this; $view->addFunction('el', [$this, 'el']); $view->addFunction('link', [$this, 'link']); $view->addFunction('image', [$this, 'image']); $view->addFunction('form', [$this, 'form']); $view->addFunction('attrs', [$this, 'attrs']); $view->addFunction('expr', [HtmlElement::class, 'expr']); $view->addFunction('tag', [HtmlElement::class, 'tag']); } /** * @inheritdoc */ public function el($name, array $attrs = [], $contents = false) { return new HtmlElement($name, $attrs, $contents, [$this, 'applyTransform']); } /** * @inheritdoc */ public function link($title, $url = null, array $attrs = []) { return "<a{$this->attrs(['href' => $url], $attrs)}>{$title}</a>"; } /** * @inheritdoc */ public function image($url, array $attrs = []) { $url = (array) $url; $path = array_shift($url); $params = $url ? '#' . http_build_query( array_map(fn($value) => is_array($value) ? implode(',', $value) : $value, $url), '', '&', ) : ''; if (empty($attrs['alt'])) { $attrs['alt'] = true; } return "<img{$this->attrs(['src' => $path . $params], $attrs)}>"; } /** * @inheritdoc */ public function form($tags, array $attrs = []) { return HtmlElement::tag( 'form', $attrs, array_map( fn($tag) => HtmlElement::tag($tag['tag'], array_diff_key($tag, ['tag' => null])), $tags, ), ); } /** * @inheritdoc */ public function attrs(array $attrs) { $params = []; if (count($args = func_get_args()) > 1) { $attrs = call_user_func_array('array_merge_recursive', $args); } if (isset($attrs[':params'])) { $params = $attrs[':params']; unset($attrs[':params']); } return HtmlElement::attrs($attrs, $params); } /** * Adds a component. * * @param string $name * @param callable $component */ public function addComponent($name, callable $component) { $this->addTransform($name, $component); } /** * Adds a transform. * * @param string $name * @param callable $transform */ public function addTransform($name, callable $transform) { $this->transforms[$name][] = $transform; } /** * Applies transform callbacks. * * @param HtmlElement $element * @param array $params * * @return string|void */ public function applyTransform(HtmlElement $element, array $params = []) { if (empty($this->transforms[$element->name])) { return; } foreach ($this->transforms[$element->name] as $transform) { if ($result = call_user_func($transform, $element, $params)) { return $result; } } } } view/src/View/HtmlElementInterface.php 0000644 00000001120 15166657207 0014000 0 ustar 00 <?php namespace YOOtheme\View; interface HtmlElementInterface { /** * Renders element tag. * * @param string $name * @param array $attrs * @param false|string|string[] $contents * @param array $params * * @return string */ public static function tag($name, $attrs = null, $contents = null, array $params = []); /** * Evaluate expression attribute. * * @param array $expressions * @param array $params * * @return string|null */ public static function expr($expressions, array $params = []); } view/src/View/StrHelper.php 0000644 00000002333 15166657207 0011660 0 ustar 00 <?php namespace YOOtheme\View; use YOOtheme\Str; use YOOtheme\View; class StrHelper extends Str { /** * Constructor. * * @param View $view */ public function __construct(View $view) { $functions = [ // native 'trim' => 'trim', 'json' => 'json_encode', 'nl2br' => 'nl2br', 'striptags' => 'strip_tags', // date 'date' => [$this, 'date'], // string util 'limit' => [$this, 'limit'], 'words' => [$this, 'words'], 'upper' => [$this, 'upper'], 'lower' => [$this, 'lower'], 'title' => [$this, 'titleCase'], ]; foreach ($functions as $name => $function) { $view->addFunction($name, $function); } } /** * Formats a date. * * @param mixed $date * @param string $format * * @return string */ public function date($date, $format = 'F j, Y H:i') { if (is_string($date)) { $date = strtotime($date); } elseif ($date instanceof \DateTime) { $date = $date->getTimestamp(); } return date($format, $date); } } view/src/View/HtmlElement.php 0000644 00000020173 15166657207 0012170 0 ustar 00 <?php namespace YOOtheme\View; use YOOtheme\Arr; class HtmlElement implements HtmlElementInterface { /** * @var string */ public $name; /** * @var array */ public $attrs; /** * @var mixed */ public $contents; /** * @var callable|null */ protected $transform; /** * Constructor. * * @param string $name * @param array $attrs * @param mixed $contents * @param callable|null $transform */ public function __construct( $name, array $attrs = [], $contents = '', ?callable $transform = null ) { $this->name = $name; $this->attrs = $attrs; $this->contents = $contents; $this->transform = $transform; } /** * Renders element shortcut. * * @see render() */ public function __toString() { return $this->render(); } /** * Render element shortcut. * * @param array $params * @param null|mixed $attrs * @param null|mixed $contents * @param null|mixed $name * * @return string * * @see render() */ public function __invoke(array $params = [], $attrs = null, $contents = null, $name = null) { return $this->render($params, $attrs, $contents, $name); } /** * Renders the element tag. * * @param array $params * @param array $attrs * @param string $contents * @param string $name * * @return string */ public function render(array $params = [], $attrs = null, $contents = null, $name = null) { $element = isset($attrs) ? $this->copy($attrs, $contents, $name) : $this; if (($transform = $this->transform) && ($result = $transform($element, $params))) { return $result; } return self::tag($element->name, $element->attrs, $element->contents, $params); } /** * Renders element closing tag. * * @return string */ public function end() { return self::isSelfClosing($this->name) ? '' : "</{$this->name}>"; } /** * Adds an attribute. * * @param string|array $name * @param mixed|null $value * * @return $this */ public function attr($name, $value = null) { $attrs = is_array($name) ? $name : [$name => $value]; $this->attrs = Arr::merge($this->attrs, $attrs); return $this; } /** * Copy instance. * * @param array|string $attrs * @param string $contents * @param string $name * * @return static */ public function copy($attrs = null, $contents = null, $name = null) { $clone = clone $this; if (is_array($attrs)) { $clone->attr($attrs); } elseif (isset($attrs)) { $contents = $attrs; } if (isset($name)) { $clone->name = $name; } if (isset($contents)) { $clone->contents = $contents; } return $clone; } /** * @inheritdoc */ public static function tag($name, $attrs = null, $contents = null, array $params = []) { $tag = $contents === false || self::isSelfClosing($name); if (is_array($attrs)) { $attrs = self::attrs($attrs, $params); } if (is_array($contents)) { $contents = join($contents); } return $tag ? "<{$name}{$attrs}>" : "<{$name}{$attrs}>{$contents}</{$name}>"; } /** * Renders tag attributes. * * @param array $attrs * @param array $params * * @return string */ public static function attrs(array $attrs, array $params = []) { $output = []; foreach ($attrs as $key => $value) { if (is_array($value)) { $value = self::expr($value, $params); } if (empty($value) && !is_numeric($value)) { continue; } if (is_numeric($key)) { $output[] = $value; } elseif ($value === true) { $output[] = $key; } elseif ($value !== '') { $output[] = sprintf( '%s="%s"', $key, htmlspecialchars($value, ENT_COMPAT, 'UTF-8', false), ); } } return $output ? ' ' . implode(' ', $output) : ''; } /** * @inheritdoc */ public static function expr($expressions, array $params = []) { $output = []; if (func_num_args() > 2) { $params = call_user_func_array('array_replace', array_slice(func_get_args(), 1)); } foreach ((array) $expressions as $expression => $condition) { if (!$condition) { continue; } if (is_int($expression)) { $expression = $condition; } if ( $expression = self::evaluateExpression( $expression, array_replace($params, (array) $condition), ) ) { $output[] = $expression; } } return $output ? join(' ', $output) : null; } /** * Checks if tag name is self-closing. * * @param string $name * * @return bool */ public static function isSelfClosing($name) { static $tags; if (is_null($tags)) { $tags = array_flip([ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr', ]); } return isset($tags[strtolower($name)]); } /** * Parse expression string. * * @param string $expression * * @return array */ protected static function parseExpression($expression) { static $expressions; if (isset($expressions[$expression])) { return $expressions[$expression]; } $optionals = []; // match all optionals $output = preg_replace_callback( '/\[((?:[^\[\]]+|(?R))*)\]/', function ($matches) use (&$optionals) { return '%' . array_push($optionals, $matches[1]) . '$s'; }, $expression, ); // match all parameters preg_match_all( '/\{\s*(@?)(!?)(\w+)\s*(?::\s*([^{}]*(?:\{(?-1)\}[^{}]*)*))?\}/', $output, $parameters, PREG_SET_ORDER, ); return $expressions[$expression] = [$output, $parameters, $optionals]; } /** * Evaluate expression string. * * @param string $expression * @param array $params * * @return string */ protected static function evaluateExpression($expression, array $params = []) { if (!str_contains($expression, '{')) { return trim($expression); } [$output, $parameters, $optionals] = self::parseExpression($expression); foreach ($parameters as $match) { [$parameter, $empty, $negate, $name] = $match; $regex = isset($match[4]) ? "/^({$match[4]})$/" : ''; $value = $params[$name] ?? ''; $result = $regex ? preg_match($regex, $value) : $value || (is_string($value) && $value !== ''); if ($result xor $negate) { $output = str_replace($parameter, $empty ? '' : $value, $output); } else { return ''; } } if ($optionals) { $args = [$output]; foreach ($optionals as $match) { $args[] = self::evaluateExpression($match, $params); } $output = call_user_func_array('sprintf', $args); } return trim($output); } } view/bootstrap.php 0000644 00000001004 15166657207 0010256 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\View\FileLoader; use YOOtheme\View\HtmlHelper; use YOOtheme\View\SectionHelper; use YOOtheme\View\StrHelper; return [ 'services' => [ View::class => function (FileLoader $loader) { $view = new View($loader); $view->addGlobal('view', $view); $view->addHelper(StrHelper::class); $view->addHelper(HtmlHelper::class); $view->addHelper(SectionHelper::class); return $view; }, ], ]; graphql/src/Directive/BindDirective.php 0000644 00000002752 15166657207 0014160 0 ustar 00 <?php namespace YOOtheme\GraphQL\Directive; use YOOtheme\Container; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\Type; class BindDirective extends Directive { /** * @var Container */ protected $container; /** * Constructor. * * @param Container $container */ public function __construct(Container $container) { parent::__construct([ 'name' => 'bind', 'args' => [ [ 'name' => 'id', 'type' => Type::string(), ], [ 'name' => 'class', 'type' => Type::string(), ], [ 'name' => 'args', 'type' => Type::string(), ], ], 'locations' => ['OBJECT', 'ENUM_VALUE', 'FIELD_DEFINITION'], ]); $this->container = $container; } /** * Register service on container. * * @param array $params */ public function __invoke(array $params) { if (!$this->container->has($params['id'])) { $service = $this->container->add($params['id']); if (isset($params['class'])) { $service->setClass($params['class']); } if (isset($params['args'])) { $service->setArguments(json_decode($params['args'], true)); } } } } graphql/src/Directive/SliceDirective.php 0000644 00000002514 15166657207 0014337 0 ustar 00 <?php namespace YOOtheme\GraphQL\Directive; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\Type; class SliceDirective extends Directive { /** * Constructor. */ public function __construct() { parent::__construct([ 'name' => 'slice', 'args' => [ [ 'name' => 'offset', 'type' => Type::int(), ], [ 'name' => 'limit', 'type' => Type::int(), ], ], 'locations' => ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], ]); } /** * Directive callback. * * @param array $params * * @return \Closure */ public function __invoke(array $params) { return function ($root, $args, $context, $info, callable $next) use ($params) { $offset = $params['offset'] ?? 0; $limit = $params['limit'] ?? null; $value = $next($root, $args, $context, $info); // TODO 2.4 no need to check for $offset && $limit ? if (is_array($value) && ($offset || $limit)) { return array_slice($value, (int) $offset, (int) $limit ?: null); } return $value; }; } } graphql/src/Directive/CallDirective.php 0000644 00000003117 15166657207 0014153 0 ustar 00 <?php namespace YOOtheme\GraphQL\Directive; use YOOtheme\Container; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\Middleware; class CallDirective extends Directive { /** * @var Container */ protected $container; /** * Constructor. * * @param Container $container */ public function __construct(Container $container) { parent::__construct([ 'name' => 'call', 'args' => [ [ 'name' => 'func', 'type' => Type::string(), ], [ 'name' => 'args', 'type' => Type::string(), ], ], 'locations' => ['ENUM_VALUE', 'FIELD_DEFINITION'], ]); $this->container = $container; } /** * Resolve value from function callback. * * @param array $params * @param Middleware $resolver * * @return \Closure|void */ public function __invoke(array $params, Middleware $resolver) { // override default resolver $resolver->setHandler($this->container->callback($params['func'])); // merge additional arguments if (isset($params['args']) && is_array($arguments = json_decode($params['args'], true))) { return fn($value, $args, $context, $info, $next) => $next( $value, $args + $arguments, $context, $info, ); } } } graphql/src/Utils/ASTHelper.php 0000644 00000006565 15166657207 0012424 0 ustar 00 <?php namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\ObjectType; class ASTHelper extends AST { public static function objectType(ObjectType $type) { $node = [ 'kind' => 'ObjectTypeDefinition', 'name' => [ 'kind' => 'Name', 'value' => $type->name, ], 'fields' => [], 'interfaces' => [], 'directives' => [], ]; if (isset($type->config['directives'])) { foreach ($type->config['directives'] as $config) { $node['directives'][] = static::directive($config); } } foreach ($type->getFields() as $field) { $field->astNode = static::field($field); } return static::fromArray($node); } public static function inputType(InputObjectType $type) { $node = [ 'kind' => 'InputObjectTypeDefinition', 'name' => [ 'kind' => 'Name', 'value' => $type->name, ], 'fields' => [], 'directives' => [], ]; foreach ($type->config['directives'] ?? [] as $config) { $node['directives'][] = static::directive($config); } foreach ($type->getFields() as $field) { $field->astNode = static::inputField($field); } return static::fromArray($node); } public static function field(FieldDefinition $field) { $node = [ 'kind' => 'FieldDefinition', 'name' => [ 'kind' => 'Name', 'value' => $field->name, ], 'arguments' => [], 'directives' => [], ]; foreach ($field->config['directives'] ?? [] as $config) { $node['directives'][] = static::directive($config); } return static::fromArray($node); } public static function inputField(InputObjectField $field) { $node = [ 'kind' => 'InputValueDefinition', 'name' => [ 'kind' => 'Name', 'value' => $field->name, ], 'directives' => [], ]; foreach ($field->config['directives'] ?? [] as $config) { $node['directives'][] = static::directive($config); } return static::fromArray($node); } public static function directive(array $config) { $directive = [ 'kind' => 'Directive', 'name' => [ 'kind' => 'Name', 'value' => $config['name'], ], ]; foreach ($config['args'] ?? [] as $name => $value) { $directive['arguments'][] = static::argument($name, $value); } return static::fromArray($directive); } public static function argument($name, $value) { $argument = [ 'kind' => 'Argument', 'name' => [ 'kind' => 'Name', 'value' => $name, ], 'value' => [ 'kind' => 'StringValue', 'value' => $value, ], ]; return static::fromArray($argument); } } graphql/src/Utils/Introspection.php 0000644 00000005764 15166657207 0013475 0 ustar 00 <?php namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Introspection as BaseIntrospection; class Introspection extends BaseIntrospection { public static function getIntrospectionQuery(array $options = []): string { $value = fn($val) => is_callable($val) ? $val() : $val; $options += ['defaults' => ['description']]; $fields = []; foreach (static::getTypes() as $name => $type) { if (isset($options[$name]) && $type instanceof ObjectType) { $type->config['fields'] = $value($type->config['fields']) + $options[$name]; $fields[$name] = join( ' ', array_merge($options['defaults'], array_keys($options[$name])), ); } else { $fields[$name] = join(' ', $options['defaults']); } } return <<<EOD query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name {$fields['__Directive']} locations args { ...InputValue } } } } fragment FullType on __Type { kind name {$fields['__Type']} fields(includeDeprecated: true) { name {$fields['__Field']} args { ...InputValue } type { ...TypeRef } } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name {$fields['__EnumValue']} } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name {$fields['__InputValue']} type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } EOD; } } graphql/src/Utils/Middleware.php 0000644 00000002732 15166657207 0012702 0 ustar 00 <?php namespace YOOtheme\GraphQL\Utils; class Middleware { /** * @var callable|null */ protected $handler; /** * @var array */ protected $stack = []; /** * Constructor. * * @param callable $handler */ public function __construct(?callable $handler = null) { $this->handler = $handler; } /** * Invokes the next middleware handler. * * @param mixed ...$arguments * * @return mixed */ public function __invoke(...$arguments) { if ($this->stack) { $arguments[] = $this; } $handler = array_shift($this->stack) ?: $this->handler; return $handler(...$arguments); } /** * Returns true if handler exists. */ public function hasHandler() { return isset($this->handler); } /** * Sets the middleware handler. * * @param callable $handler */ public function setHandler(callable $handler) { $this->handler = $handler; } /** * Unshift a middleware to the bottom of the stack. * * @param callable $middleware */ public function unshift(callable $middleware) { array_unshift($this->stack, $middleware); } /** * Push a middleware to the top of the stack. * * @param callable $middleware */ public function push(callable $middleware) { $this->stack[] = $middleware; } } graphql/src/Plugin/ContainerPlugin.php 0000644 00000006477 15166657207 0014076 0 ustar 00 <?php namespace YOOtheme\GraphQL\Plugin; use YOOtheme\Container; use YOOtheme\GraphQL\Directive\BindDirective; use YOOtheme\GraphQL\Directive\CallDirective; use YOOtheme\GraphQL\SchemaBuilder; use YOOtheme\GraphQL\Type\Definition\Type; class ContainerPlugin { /** * @var Container */ protected $container; /** * Constructor. * * @param Container $container */ public function __construct(Container $container) { $this->container = $container; } /** * Register directives. * * @param SchemaBuilder $schema */ public function onLoad(SchemaBuilder $schema) { $schema->setDirective(new BindDirective($this->container)); $schema->setDirective(new CallDirective($this->container)); } /** * Add directives on type. * * @param Type $type */ public function onLoadType(Type $type) { if ( property_exists($type, 'config') && ($extensions = $type->config['extensions'] ?? []) && ($directives = $this->getDirectives($extensions)) ) { $type->config['directives'] = array_merge( $type->config['directives'] ?? [], $directives, ); } } /** * Add directives on field. * * @param Type $type * @param array $field * * @return array */ public function onLoadField(Type $type, array $field) { $extensions = $field['extensions'] ?? []; if ($extensions && ($directives = $this->getDirectives($extensions))) { if (!isset($field['directives'])) { $field['directives'] = []; } $field['directives'] = array_merge($field['directives'], $directives); } return $field; } /** * Get directives. * * @param array $extensions * * @return array */ protected function getDirectives(array $extensions) { $directives = []; if (isset($extensions['bind'])) { foreach ($extensions['bind'] as $id => $params) { $directives[] = $this->bindDirective($id, $params); } } if (isset($extensions['call'])) { $directives[] = $this->callDirective($extensions['call']); } return $directives; } /** * Get @bind directive. * * @param string $id * @param string|array $params * * @return array */ protected function bindDirective($id, $params) { if (is_string($params)) { $params = ['class' => $params]; } if (isset($params['args'])) { $params['args'] = json_encode($params['args']); } return [ 'name' => 'bind', 'args' => array_filter(compact('id') + $params), ]; } /** * Get @call directive. * * @param string|array $params * * @return array */ protected function callDirective($params) { if (is_string($params)) { $params = ['func' => $params]; } if (isset($params['args'])) { $params['args'] = json_encode($params['args']); } return [ 'name' => 'call', 'args' => $params, ]; } } graphql/src/SchemaBuilder.php 0000644 00000030523 15166657207 0012233 0 ustar 00 <?php namespace YOOtheme\GraphQL; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Executor; use YOOtheme\GraphQL\Executor\Values; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\Parser; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ResolveInfo; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\ASTHelper; use YOOtheme\GraphQL\Utils\BuildSchema; use YOOtheme\GraphQL\Utils\Middleware; class SchemaBuilder { /** * @var callable[][] */ protected $hooks = []; /** * @var Type[] */ protected $types = []; /** * @var Type[] */ protected $loadedTypes = []; /** * @var array */ protected $directives = []; /** * Constructor. * * @param array $plugins */ public function __construct(array $plugins = []) { $this->hooks = [ 'onLoad' => [], 'onLoadType' => [], 'onLoadField' => [], ]; foreach ($plugins as $plugin) { $this->loadPlugin($plugin); } /** @phpstan-ignore-next-line */ foreach ($this->hooks['onLoad'] as $hook) { $hook($this); } } /** * @param string $file * @param string $cache * * @return Schema */ public function loadSchema($file, $cache = null) { $isCached = is_file($cache) && filectime($cache) > filectime($file); $document = $isCached ? ASTHelper::fromArray(require $cache) : Parser::parse(file_get_contents($file), ['noLocation' => true]); $result = BuildSchema::build( $document, fn(array $config) => ['resolveField' => [$this, 'resolveField']] + $config, ['assumeValid' => $isCached, 'assumeValidSDL' => $isCached], ); if (!$isCached && $cache) { file_put_contents( $cache, "<?php\n\nreturn {$this->exportValue(ASTHelper::toArray($document))};", ); } return $result; } /** * @param array $config * * @return Schema */ public function buildSchema(array $config = []) { $config = array_replace_recursive( [ 'query' => 'Query', 'mutation' => 'Mutation', 'subscription' => 'Subscription', 'directives' => $this->directives, 'typeLoader' => [$this, 'getType'], ], $config, ); if (is_string($config['query'])) { $config['query'] = $this->getType($config['query']); } if (is_string($config['mutation'])) { $config['mutation'] = $this->getType($config['mutation']); } if (is_string($config['subscription'])) { $config['subscription'] = $this->getType($config['subscription']); } return new Schema($config); } /** * @param array $config * * @return string */ public function printSchema(array $config = []) { return SchemaPrinter::doPrint($this->buildSchema($config)); } /** * @param string $name * * @return Directive */ public function getDirective($name) { return $this->directives[$name] ?? null; } /** * @param Directive $directive */ public function setDirective(Directive $directive) { $this->directives[$directive->name] = $directive; } /** * @param string $name * * @return bool */ public function hasType($name) { return isset($this->types[$name]); } /** * @param string $name * * @return Type|void */ public function getType($name) { if (empty($this->loadedTypes)) { $this->loadedTypes = Type::getStandardTypes(); } if (isset($this->loadedTypes[$name])) { return $this->loadedTypes[$name]; } if (isset($this->types[$name])) { return $this->loadType($this->loadedTypes[$name] = $this->types[$name]); } } /** * @param Type $type */ public function setType(Type $type) { /** @var NamedType $type */ $this->types[$type->name] = $type; } /** * @param array|callable $config * * @return ObjectType */ public function queryType($config = []) { return $this->objectType('Query', $config); } /** * @param string $name * @param array|callable $config * * @return InputObjectType */ public function inputType($name, $config = []) { $type = $this->types[$name] ?? new InputObjectType([ 'name' => $name, 'fields' => [], ]); if (!$type instanceof InputObjectType) { throw new InvariantViolation("Type '{$name}' must be an InputObjectType."); } return $this->types[$name] = $this->extendType($type, $config); } /** * @param string $name * @param array|callable $config * * @return ObjectType */ public function objectType($name, $config = []) { $type = $this->types[$name] ?? new ObjectType([ 'name' => $name, 'fields' => [], 'resolveField' => [$this, 'resolveField'], ]); if (!$type instanceof ObjectType) { throw new InvariantViolation("Type '{$name}' must be an ObjectType."); } return $this->types[$name] = $this->extendType($type, $config); } /** * @template T of Type * @param T $type * @param array|callable $config * @return T */ public function extendType(Type $type, $config = []) { if (is_callable($config)) { $config = $config($type, $this); } if (is_array($config) && property_exists($type, 'config')) { $type->config = array_replace_recursive($type->config, $config); } return $type; } /** * @template T of Type * @param T $type * @return T */ public function loadType(Type $type) { foreach ($this->hooks['onLoadType'] as $hook) { $hook($type, $this); } if (isset($type->config['description']) && property_exists($type, 'description')) { $type->description = $type->config['description']; } if (isset($type->config['resolveField']) && $type instanceof ObjectType) { $type->resolveFieldFn = $type->config['resolveField']; } if (isset($type->config['fields'])) { $type->config['fields'] = $this->loadFields($type, $type->config['fields']); } if ($type instanceof ObjectType) { $type->astNode = ASTHelper::objectType($type); } elseif ($type instanceof InputObjectType) { $type->astNode = ASTHelper::inputType($type); } return $type; } /** * @param mixed $value * @param mixed $args * @param mixed $context * @param ResolveInfo $info * * @return mixed */ public function resolveField($value, $args, $context, ResolveInfo $info) { $resolver = new Middleware([Executor::class, 'defaultFieldResolver']); foreach ($this->resolveDirectives($info) as $directiveNode) { $directiveDef = $this->getDirective($directiveNode->name->value); if (is_callable($directiveDef)) { $directive = $directiveDef( Values::getArgumentValues($directiveDef, $directiveNode, $info->variableValues), $resolver, ); if (is_callable($directive)) { $resolver->push($directive); } } } return $resolver($value, $args, $context, $info); } /** * @return NodeList<DirectiveNode> */ public function resolveDirectives(ResolveInfo $info) { $nodes = new NodeList([]); $field = $info->parentType->getField($info->fieldName); // type directives if (isset($info->parentType->astNode->directives)) { $nodes = $nodes->merge($info->parentType->astNode->directives); } // field directives if (isset($field->astNode->directives)) { $nodes = $nodes->merge($field->astNode->directives); } // query field directives foreach ($info->fieldNodes as $node) { if ($info->fieldName === $node->name->value) { return $nodes->merge($node->directives); } } return $nodes; } /** * @param Type $type * @param array $field * * @return array */ protected function loadField(Type $type, array $field) { $field += ['type' => null]; if (is_string($field['type'])) { $field['type'] = $this->getType($field['type']); } if (is_array($field['type'])) { $field['type'] = $this->loadModifiers($field['type']); } if (empty($field['type'])) { /** @var NamedType $type */ throw new InvariantViolation( "Field '{$field['name']}' on '{$type->name}' does not have a Type.", ); } return $field; } /** * @param Type $type * @param array $fields * * @return array */ protected function loadFields(Type $type, array $fields) { $result = []; foreach ($fields as $name => $field) { $field = $this->loadField( $type, $field + [ 'name' => lcfirst($name), 'args' => [], ], ); foreach ($field['args'] as $key => $arg) { $field['args'][$key] = $this->loadField($type, $arg); } foreach ($this->hooks['onLoadField'] as $hook) { $field = $hook($type, $field, $this); } $result[$name] = $field; } return $result; } /** * @param array $type * * @return Type|array */ protected function loadModifiers(array $type) { if (isset($type['nonNull'])) { if (is_string($type['nonNull'])) { $nonNull = $this->getType($type['nonNull']); } elseif (is_array($type['nonNull'])) { $nonNull = $this->loadModifiers($type['nonNull']); } $type = Type::nonNull($nonNull ?? Type::string()); } elseif (isset($type['listOf'])) { if (is_string($type['listOf'])) { $listOf = $this->getType($type['listOf']); } elseif (is_array($type['listOf'])) { $listOf = $this->loadModifiers($type['listOf']); } $type = Type::listOf($listOf ?? Type::string()); } return $type; } /** * @param mixed $plugin */ protected function loadPlugin($plugin) { foreach ($this->hooks as $method => &$hooks) { $hook = [$plugin, $method]; if (is_callable($hook)) { $hooks[] = $hook; } } } /** * Export a parsable string representation of a value. * * @param mixed $value * @param int $indent * * @return string */ protected function exportValue($value, $indent = 0) { if (is_array($value)) { $array = []; $assoc = array_values($value) !== $value; $indention = str_repeat(' ', $indent); $indentlast = $assoc ? "\n" . $indention : ''; foreach ($value as $key => $val) { $array[] = ($assoc ? "\n " . $indention . var_export($key, true) . ' => ' : '') . $this->exportValue($val, $indent + 1); } return '[' . join(', ', $array) . $indentlast . ']'; } return var_export($value, true); } } graphql/src/SchemaPrinter.php 0000644 00000006355 15166657207 0012276 0 ustar 00 <?php namespace YOOtheme\GraphQL; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\SchemaPrinter as BasePrinter; use YOOtheme\GraphQL\Utils\Utils; use function array_keys; use function array_map; use function array_values; use function compact; use function count; use function implode; use function sprintf; /** * Given an instance of Schema, prints it in GraphQL type language. */ class SchemaPrinter extends BasePrinter { /** * @inheritdoc */ public static function doPrint(Schema $schema, array $options = []): string { return parent::doPrint($schema, $options + compact('schema')); } /** * @inheritdoc */ public static function printIntrospectionSchema(Schema $schema, array $options = []): string { return parent::printIntrospectionSchema($schema, $options + compact('schema')); } /** * @inheritdoc */ protected static function printObject(ObjectType $type, array $options): string { $interfaces = $type->getInterfaces(); $implementedInterfaces = count($interfaces) > 0 ? ' implements ' . implode( ' & ', array_map( fn(InterfaceType $interface): string => $interface->name, $interfaces, ), ) : ''; return static::printDescription($options, $type) . sprintf( "type %s%s%s {\n%s\n}", $type->name, $implementedInterfaces, static::printDirectives($type, $options), static::printFields($options, $type), ); } /** * @inheritdoc */ protected static function printFields(array $options, $type): string { $fields = array_values($type->getFields()); return implode( "\n", array_map( fn($f, $i) => static::printDescription($options, $f, ' ', !$i) . ' ' . $f->name . static::printArgs($options, $f->args, ' ') . ': ' . $f->getType() . static::printDirectives($f, $options) . static::printDeprecated($f), $fields, array_keys($fields), ), ); } protected static function printDirectives($fieldOrType, array $options): string { $directives = []; if (isset($fieldOrType->astNode->directives)) { foreach ($fieldOrType->astNode->directives as $directive) { if (!$options['schema']->getDirective($directive->name->value)) { throw new Error( 'Unknown directive: ' . Utils::printSafe($directive->name->value) . '.', ); } $directives[] = Printer::doPrint($directive); } } return $directives ? ' ' . implode(' ', $directives) : ''; } } graphql/src/Type/ObjectScalarType.php 0000644 00000001656 15166657207 0013650 0 ustar 00 <?php namespace YOOtheme\GraphQL\Type; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Type\Definition\ScalarType; class ObjectScalarType extends ScalarType { /** * @param array $config */ public function __construct(array $config = []) { parent::__construct($config + ['name' => 'Object']); } /** * @param mixed $value * * @return mixed */ public function serialize($value) { return $value; } /** * @param mixed $value * * @return array|null */ public function parseValue($value) { return is_array($value) ? $value : null; } /** * @param Node $valueNode * @param null|array $variables */ public function parseLiteral($valueNode, ?array $variables = null) { throw new Error("Query error: Can't parse Object literal"); } } graphql/lib/GraphQL.php 0000644 00000020412 15166657207 0010775 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\ExecutionResult; use YOOtheme\GraphQL\Executor\Executor; use YOOtheme\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\Parser; use YOOtheme\GraphQL\Language\Source; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema as SchemaType; use YOOtheme\GraphQL\Validator\DocumentValidator; use YOOtheme\GraphQL\Validator\Rules\QueryComplexity; use YOOtheme\GraphQL\Validator\Rules\ValidationRule; /** * This is the primary facade for fulfilling GraphQL operations. * See [related documentation](executing-queries.md). * * @phpstan-import-type ArgsMapper from Executor * @phpstan-import-type FieldResolver from Executor * * @see \GraphQL\Tests\GraphQLTest */ class GraphQL { /** * Executes graphql query. * * More sophisticated GraphQL servers, such as those which persist queries, * may wish to separate the validation and execution phases to a static time * tooling step, and a server runtime step. * * Available options: * * schema: * The GraphQL type system to use when validating and executing a query. * source: * A GraphQL language formatted string representing the requested operation. * rootValue: * The value provided as the first argument to resolver functions on the top * level type (e.g. the query object type). * contextValue: * The context value is provided as an argument to resolver functions after * field arguments. It is used to pass shared information useful at any point * during executing this query, for example the currently logged in user and * connections to databases or other services. * If the passed object implements the `ScopedContext` interface, * its `clone()` method will be called before passing the context down to a field. * This allows passing information to child fields in the query tree without affecting sibling or parent fields. * variableValues: * A mapping of variable name to runtime value to use for all variables * defined in the requestString. * operationName: * The name of the operation to use if requestString contains multiple * possible operations. Can be omitted if requestString contains only * one operation. * fieldResolver: * A resolver function to use when one is not provided by the schema. * If not provided, the default field resolver is used (which looks for a * value on the source value with the field's name). * validationRules: * A set of rules for query validation step. Default value is all available rules. * Empty array would allow to skip query validation (may be convenient for persisted * queries which are validated before persisting and assumed valid during execution) * * @param string|DocumentNode $source * @param mixed $rootValue * @param mixed $contextValue * @param array<string, mixed>|null $variableValues * @param array<ValidationRule>|null $validationRules * * @api * * @throws \Exception * @throws InvariantViolation */ public static function executeQuery( SchemaType $schema, $source, $rootValue = null, $contextValue = null, ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, ?array $validationRules = null ): ExecutionResult { $promiseAdapter = new SyncPromiseAdapter(); $promise = self::promiseToExecute( $promiseAdapter, $schema, $source, $rootValue, $contextValue, $variableValues, $operationName, $fieldResolver, $validationRules ); return $promiseAdapter->wait($promise); } /** * Same as executeQuery(), but requires PromiseAdapter and always returns a Promise. * Useful for Async PHP platforms. * * @param string|DocumentNode $source * @param mixed $rootValue * @param mixed $context * @param array<string, mixed>|null $variableValues * @param array<ValidationRule>|null $validationRules Defaults to using all available rules * * @api * * @throws \Exception */ public static function promiseToExecute( PromiseAdapter $promiseAdapter, SchemaType $schema, $source, $rootValue = null, $context = null, ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, ?array $validationRules = null ): Promise { try { $documentNode = $source instanceof DocumentNode ? $source : Parser::parse(new Source($source, 'GraphQL')); if ($validationRules === null) { $queryComplexity = DocumentValidator::getRule(QueryComplexity::class); assert($queryComplexity instanceof QueryComplexity, 'should not register a different rule for QueryComplexity'); $queryComplexity->setRawVariableValues($variableValues); } else { foreach ($validationRules as $rule) { if ($rule instanceof QueryComplexity) { $rule->setRawVariableValues($variableValues); } } } $validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules); if ($validationErrors !== []) { return $promiseAdapter->createFulfilled( new ExecutionResult(null, $validationErrors) ); } return Executor::promiseToExecute( $promiseAdapter, $schema, $documentNode, $rootValue, $context, $variableValues, $operationName, $fieldResolver ); } catch (Error $e) { return $promiseAdapter->createFulfilled( new ExecutionResult(null, [$e]) ); } } /** * Returns directives defined in GraphQL spec. * * @throws InvariantViolation * * @return array<string, Directive> * * @api */ public static function getStandardDirectives(): array { return Directive::getInternalDirectives(); } /** * Returns types defined in GraphQL spec. * * @throws InvariantViolation * * @return array<string, ScalarType> * * @api */ public static function getStandardTypes(): array { return Type::getStandardTypes(); } /** * Replaces standard types with types from this list (matching by name). * * Standard types not listed here remain untouched. * * @param array<string, ScalarType> $types * * @api * * @throws InvariantViolation */ public static function overrideStandardTypes(array $types): void { Type::overrideStandardTypes($types); } /** * Returns standard validation rules implementing GraphQL spec. * * @return array<class-string<ValidationRule>, ValidationRule> * * @api */ public static function getStandardValidationRules(): array { return DocumentValidator::defaultRules(); } /** * Set default resolver implementation. * * @phpstan-param FieldResolver $fn * * @api */ public static function setDefaultFieldResolver(callable $fn): void { Executor::setDefaultFieldResolver($fn); } /** * Set default args mapper implementation. * * @phpstan-param ArgsMapper $fn * * @api */ public static function setDefaultArgsMapper(callable $fn): void { Executor::setDefaultArgsMapper($fn); } } graphql/lib/Validator/DocumentValidator.php 0000644 00000027767 15166657207 0015074 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\TypeInfo; use YOOtheme\GraphQL\Validator\Rules\DisableIntrospection; use YOOtheme\GraphQL\Validator\Rules\ExecutableDefinitions; use YOOtheme\GraphQL\Validator\Rules\FieldsOnCorrectType; use YOOtheme\GraphQL\Validator\Rules\FragmentsOnCompositeTypes; use YOOtheme\GraphQL\Validator\Rules\KnownArgumentNames; use YOOtheme\GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives; use YOOtheme\GraphQL\Validator\Rules\KnownDirectives; use YOOtheme\GraphQL\Validator\Rules\KnownFragmentNames; use YOOtheme\GraphQL\Validator\Rules\KnownTypeNames; use YOOtheme\GraphQL\Validator\Rules\LoneAnonymousOperation; use YOOtheme\GraphQL\Validator\Rules\LoneSchemaDefinition; use YOOtheme\GraphQL\Validator\Rules\NoFragmentCycles; use YOOtheme\GraphQL\Validator\Rules\NoUndefinedVariables; use YOOtheme\GraphQL\Validator\Rules\NoUnusedFragments; use YOOtheme\GraphQL\Validator\Rules\NoUnusedVariables; use YOOtheme\GraphQL\Validator\Rules\OneOfInputObjectsRule; use YOOtheme\GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged; use YOOtheme\GraphQL\Validator\Rules\PossibleFragmentSpreads; use YOOtheme\GraphQL\Validator\Rules\PossibleTypeExtensions; use YOOtheme\GraphQL\Validator\Rules\ProvidedRequiredArguments; use YOOtheme\GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives; use YOOtheme\GraphQL\Validator\Rules\QueryComplexity; use YOOtheme\GraphQL\Validator\Rules\QueryDepth; use YOOtheme\GraphQL\Validator\Rules\QuerySecurityRule; use YOOtheme\GraphQL\Validator\Rules\ScalarLeafs; use YOOtheme\GraphQL\Validator\Rules\SingleFieldSubscription; use YOOtheme\GraphQL\Validator\Rules\UniqueArgumentDefinitionNames; use YOOtheme\GraphQL\Validator\Rules\UniqueArgumentNames; use YOOtheme\GraphQL\Validator\Rules\UniqueDirectiveNames; use YOOtheme\GraphQL\Validator\Rules\UniqueDirectivesPerLocation; use YOOtheme\GraphQL\Validator\Rules\UniqueEnumValueNames; use YOOtheme\GraphQL\Validator\Rules\UniqueFieldDefinitionNames; use YOOtheme\GraphQL\Validator\Rules\UniqueFragmentNames; use YOOtheme\GraphQL\Validator\Rules\UniqueInputFieldNames; use YOOtheme\GraphQL\Validator\Rules\UniqueOperationNames; use YOOtheme\GraphQL\Validator\Rules\UniqueOperationTypes; use YOOtheme\GraphQL\Validator\Rules\UniqueTypeNames; use YOOtheme\GraphQL\Validator\Rules\UniqueVariableNames; use YOOtheme\GraphQL\Validator\Rules\ValidationRule; use YOOtheme\GraphQL\Validator\Rules\ValuesOfCorrectType; use YOOtheme\GraphQL\Validator\Rules\VariablesAreInputTypes; use YOOtheme\GraphQL\Validator\Rules\VariablesInAllowedPosition; /** * Implements the "Validation" section of the spec. * * Validation runs synchronously, returning an array of encountered errors, or * an empty array if no errors were encountered and the document is valid. * * A list of specific validation rules may be provided. If not provided, the * default list of rules defined by the GraphQL specification will be used. * * Each validation rule is an instance of GraphQL\Validator\Rules\ValidationRule * which returns a visitor (see the [GraphQL\Language\Visitor API](class-reference.md#graphqllanguagevisitor)). * * Visitor methods are expected to return an instance of [GraphQL\Error\Error](class-reference.md#graphqlerrorerror), * or array of such instances when invalid. * * Optionally a custom TypeInfo instance may be provided. If not provided, one * will be created from the provided schema. */ class DocumentValidator { /** @var array<string, ValidationRule> */ private static array $rules = []; /** @var array<class-string<ValidationRule>, ValidationRule> */ private static array $defaultRules; /** @var array<class-string<QuerySecurityRule>, QuerySecurityRule> */ private static array $securityRules; /** @var array<class-string<ValidationRule>, ValidationRule> */ private static array $sdlRules; private static bool $initRules = false; /** * Validate a GraphQL query against a schema. * * @param array<ValidationRule>|null $rules Defaults to using all available rules * * @throws \Exception * * @return list<Error> * * @api */ public static function validate( Schema $schema, DocumentNode $ast, ?array $rules = null, ?TypeInfo $typeInfo = null ): array { $rules ??= static::allRules(); if ($rules === []) { return []; } $typeInfo ??= new TypeInfo($schema); $context = new QueryValidationContext($schema, $ast, $typeInfo); $visitors = []; foreach ($rules as $rule) { $visitors[] = $rule->getVisitor($context); } Visitor::visit( $ast, Visitor::visitWithTypeInfo( $typeInfo, Visitor::visitInParallel($visitors) ) ); return $context->getErrors(); } /** * Returns all global validation rules. * * @throws \InvalidArgumentException * * @return array<string, ValidationRule> * * @api */ public static function allRules(): array { if (! self::$initRules) { self::$rules = array_merge( static::defaultRules(), self::securityRules(), self::$rules ); self::$initRules = true; } return self::$rules; } /** @return array<class-string<ValidationRule>, ValidationRule> */ public static function defaultRules(): array { return self::$defaultRules ??= [ ExecutableDefinitions::class => new ExecutableDefinitions(), UniqueOperationNames::class => new UniqueOperationNames(), LoneAnonymousOperation::class => new LoneAnonymousOperation(), SingleFieldSubscription::class => new SingleFieldSubscription(), KnownTypeNames::class => new KnownTypeNames(), FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), VariablesAreInputTypes::class => new VariablesAreInputTypes(), ScalarLeafs::class => new ScalarLeafs(), FieldsOnCorrectType::class => new FieldsOnCorrectType(), UniqueFragmentNames::class => new UniqueFragmentNames(), KnownFragmentNames::class => new KnownFragmentNames(), NoUnusedFragments::class => new NoUnusedFragments(), PossibleFragmentSpreads::class => new PossibleFragmentSpreads(), NoFragmentCycles::class => new NoFragmentCycles(), UniqueVariableNames::class => new UniqueVariableNames(), NoUndefinedVariables::class => new NoUndefinedVariables(), NoUnusedVariables::class => new NoUnusedVariables(), KnownDirectives::class => new KnownDirectives(), UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), KnownArgumentNames::class => new KnownArgumentNames(), UniqueArgumentNames::class => new UniqueArgumentNames(), ValuesOfCorrectType::class => new ValuesOfCorrectType(), ProvidedRequiredArguments::class => new ProvidedRequiredArguments(), VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), UniqueInputFieldNames::class => new UniqueInputFieldNames(), OneOfInputObjectsRule::class => new OneOfInputObjectsRule(), ]; } /** * @deprecated just add rules via @see DocumentValidator::addRule() * * @throws \InvalidArgumentException * * @return array<class-string<QuerySecurityRule>, QuerySecurityRule> */ public static function securityRules(): array { return self::$securityRules ??= [ DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED), QueryDepth::class => new QueryDepth(QueryDepth::DISABLED), QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED), ]; } /** @return array<class-string<ValidationRule>, ValidationRule> */ public static function sdlRules(): array { return self::$sdlRules ??= [ LoneSchemaDefinition::class => new LoneSchemaDefinition(), UniqueOperationTypes::class => new UniqueOperationTypes(), UniqueTypeNames::class => new UniqueTypeNames(), UniqueEnumValueNames::class => new UniqueEnumValueNames(), UniqueFieldDefinitionNames::class => new UniqueFieldDefinitionNames(), UniqueArgumentDefinitionNames::class => new UniqueArgumentDefinitionNames(), UniqueDirectiveNames::class => new UniqueDirectiveNames(), KnownTypeNames::class => new KnownTypeNames(), KnownDirectives::class => new KnownDirectives(), UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(), PossibleTypeExtensions::class => new PossibleTypeExtensions(), KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(), UniqueArgumentNames::class => new UniqueArgumentNames(), UniqueInputFieldNames::class => new UniqueInputFieldNames(), ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(), ]; } /** * Returns global validation rule by name. * * Standard rules are named by class name, so example usage for such rules: * * @example DocumentValidator::getRule(GraphQL\Validator\Rules\QueryComplexity::class); * * @api * * @throws \InvalidArgumentException */ public static function getRule(string $name): ?ValidationRule { return static::allRules()[$name] ?? null; } /** * Add rule to list of global validation rules. * * @api */ public static function addRule(ValidationRule $rule): void { self::$rules[$rule->getName()] = $rule; } /** * Remove rule from list of global validation rules. * * @api */ public static function removeRule(ValidationRule $rule): void { unset(self::$rules[$rule->getName()]); } /** * Validate a GraphQL document defined through schema definition language. * * @param array<ValidationRule>|null $rules * * @throws \Exception * * @return list<Error> */ public static function validateSDL( DocumentNode $documentAST, ?Schema $schemaToExtend = null, ?array $rules = null ): array { $rules ??= self::sdlRules(); if ($rules === []) { return []; } $context = new SDLValidationContext($documentAST, $schemaToExtend); $visitors = []; foreach ($rules as $rule) { $visitors[] = $rule->getSDLVisitor($context); } Visitor::visit( $documentAST, Visitor::visitInParallel($visitors) ); return $context->getErrors(); } /** * @throws \Exception * @throws Error */ public static function assertValidSDL(DocumentNode $documentAST): void { $errors = self::validateSDL($documentAST); if ($errors !== []) { throw new Error(self::combineErrorMessages($errors)); } } /** * @throws \Exception * @throws Error */ public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema): void { $errors = self::validateSDL($documentAST, $schema); if ($errors !== []) { throw new Error(self::combineErrorMessages($errors)); } } /** @param array<Error> $errors */ private static function combineErrorMessages(array $errors): string { $messages = []; foreach ($errors as $error) { $messages[] = $error->getMessage(); } return implode("\n\n", $messages); } } graphql/lib/Validator/QueryValidationContext.php 0000644 00000021012 15166657207 0016106 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\HasSelectionSet; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\VariableNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\CompositeType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InputType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\TypeInfo; /** * An instance of this class is passed as the "this" context to all validators, * allowing access to commonly useful contextual information from within a * validation rule. * * @phpstan-type VariableUsage array{node: VariableNode, type: (Type&InputType)|null, defaultValue: mixed} */ class QueryValidationContext implements ValidationContext { protected Schema $schema; protected DocumentNode $ast; /** @var list<Error> */ protected array $errors = []; private TypeInfo $typeInfo; /** @var array<string, FragmentDefinitionNode> */ private array $fragments; /** @var \SplObjectStorage<HasSelectionSet, array<int, FragmentSpreadNode>> */ private \SplObjectStorage $fragmentSpreads; /** @var \SplObjectStorage<OperationDefinitionNode, array<int, FragmentDefinitionNode>> */ private \SplObjectStorage $recursivelyReferencedFragments; /** @var \SplObjectStorage<HasSelectionSet, array<int, VariableUsage>> */ private \SplObjectStorage $variableUsages; /** @var \SplObjectStorage<HasSelectionSet, array<int, VariableUsage>> */ private \SplObjectStorage $recursiveVariableUsages; public function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo) { $this->schema = $schema; $this->ast = $ast; $this->typeInfo = $typeInfo; $this->fragmentSpreads = new \SplObjectStorage(); $this->recursivelyReferencedFragments = new \SplObjectStorage(); $this->variableUsages = new \SplObjectStorage(); $this->recursiveVariableUsages = new \SplObjectStorage(); } public function reportError(Error $error): void { $this->errors[] = $error; } /** @return list<Error> */ public function getErrors(): array { return $this->errors; } public function getDocument(): DocumentNode { return $this->ast; } public function getSchema(): Schema { return $this->schema; } /** * @throws \Exception * * @phpstan-return array<int, VariableUsage> */ public function getRecursiveVariableUsages(OperationDefinitionNode $operation): array { $usages = $this->recursiveVariableUsages[$operation] ?? null; if ($usages === null) { $usages = $this->getVariableUsages($operation); $fragments = $this->getRecursivelyReferencedFragments($operation); $allUsages = [$usages]; foreach ($fragments as $fragment) { $allUsages[] = $this->getVariableUsages($fragment); } $usages = array_merge(...$allUsages); $this->recursiveVariableUsages[$operation] = $usages; } return $usages; } /** * @param HasSelectionSet&Node $node * * @throws \Exception * * @phpstan-return array<int, VariableUsage> */ private function getVariableUsages(HasSelectionSet $node): array { if (! isset($this->variableUsages[$node])) { $usages = []; $typeInfo = new TypeInfo($this->schema); Visitor::visit( $node, Visitor::visitWithTypeInfo( $typeInfo, [ NodeKind::VARIABLE_DEFINITION => static fn () => Visitor::skipNode(), NodeKind::VARIABLE => static function (VariableNode $variable) use (&$usages, $typeInfo): void { $usages[] = [ 'node' => $variable, 'type' => $typeInfo->getInputType(), 'defaultValue' => $typeInfo->getDefaultValue(), ]; }, ] ) ); return $this->variableUsages[$node] = $usages; } return $this->variableUsages[$node]; } /** @return array<int, FragmentDefinitionNode> */ public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation): array { $fragments = $this->recursivelyReferencedFragments[$operation] ?? null; if ($fragments === null) { $fragments = []; $collectedNames = []; $nodesToVisit = [$operation]; while ($nodesToVisit !== []) { $node = array_pop($nodesToVisit); $spreads = $this->getFragmentSpreads($node); foreach ($spreads as $spread) { $fragName = $spread->name->value; if ($collectedNames[$fragName] ?? false) { continue; } $collectedNames[$fragName] = true; $fragment = $this->getFragment($fragName); if ($fragment === null) { continue; } $fragments[] = $fragment; $nodesToVisit[] = $fragment; } } $this->recursivelyReferencedFragments[$operation] = $fragments; } return $fragments; } /** * @param OperationDefinitionNode|FragmentDefinitionNode $node * * @return array<int, FragmentSpreadNode> */ public function getFragmentSpreads(HasSelectionSet $node): array { $spreads = $this->fragmentSpreads[$node] ?? null; if ($spreads === null) { $spreads = []; $setsToVisit = [$node->getSelectionSet()]; while ($setsToVisit !== []) { $set = array_pop($setsToVisit); foreach ($set->selections as $selection) { if ($selection instanceof FragmentSpreadNode) { $spreads[] = $selection; } else { assert($selection instanceof FieldNode || $selection instanceof InlineFragmentNode); $selectionSet = $selection->selectionSet; if ($selectionSet !== null) { $setsToVisit[] = $selectionSet; } } } } $this->fragmentSpreads[$node] = $spreads; } return $spreads; } public function getFragment(string $name): ?FragmentDefinitionNode { if (! isset($this->fragments)) { $fragments = []; foreach ($this->getDocument()->definitions as $statement) { if ($statement instanceof FragmentDefinitionNode) { $fragments[$statement->name->value] = $statement; } } $this->fragments = $fragments; } return $this->fragments[$name] ?? null; } public function getType(): ?Type { return $this->typeInfo->getType(); } /** @return (CompositeType&Type)|null */ public function getParentType(): ?CompositeType { return $this->typeInfo->getParentType(); } /** @return (Type&InputType)|null */ public function getInputType(): ?InputType { return $this->typeInfo->getInputType(); } /** @return (Type&InputType)|null */ public function getParentInputType(): ?InputType { return $this->typeInfo->getParentInputType(); } public function getFieldDef(): ?FieldDefinition { return $this->typeInfo->getFieldDef(); } public function getDirective(): ?Directive { return $this->typeInfo->getDirective(); } public function getArgument(): ?Argument { return $this->typeInfo->getArgument(); } } graphql/lib/Validator/ValidationContext.php 0000644 00000000666 15166657207 0015074 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Type\Schema; interface ValidationContext { public function reportError(Error $error): void; /** @return list<Error> */ public function getErrors(): array; public function getDocument(): DocumentNode; public function getSchema(): ?Schema; } graphql/lib/Validator/Rules/ValuesOfCorrectType.php 0000644 00000016652 15166657207 0016441 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\BooleanValueNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\FloatValueNode; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\ListValueNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NullValueNode; use YOOtheme\GraphQL\Language\AST\ObjectFieldNode; use YOOtheme\GraphQL\Language\AST\ObjectValueNode; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\AST\ValueNode; use YOOtheme\GraphQL\Language\AST\VariableNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\LeafType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * Value literals of correct type. * * A GraphQL document is only valid if all value literals are of the type * expected at their position. */ class ValuesOfCorrectType extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::NULL => static function (NullValueNode $node) use ($context): void { $type = $context->getInputType(); if ($type instanceof NonNull) { $typeStr = Utils::printSafe($type); $nodeStr = Printer::doPrint($node); $context->reportError( new Error( "Expected value of type \"{$typeStr}\", found {$nodeStr}.", $node ) ); } }, NodeKind::LST => function (ListValueNode $node) use ($context): ?VisitorOperation { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $parentType = $context->getParentInputType(); $type = $parentType === null ? null : Type::getNullableType($parentType); if (! $type instanceof ListOfType) { $this->isValidValueNode($context, $node); return Visitor::skipNode(); } return null; }, NodeKind::OBJECT => function (ObjectValueNode $node) use ($context): ?VisitorOperation { $type = Type::getNamedType($context->getInputType()); if (! $type instanceof InputObjectType) { $this->isValidValueNode($context, $node); return Visitor::skipNode(); } // Ensure every required field exists. $inputFields = $type->getFields(); $fieldNodeMap = []; foreach ($node->fields as $field) { $fieldNodeMap[$field->name->value] = $field; } foreach ($inputFields as $inputFieldName => $fieldDef) { if (! isset($fieldNodeMap[$inputFieldName]) && $fieldDef->isRequired()) { $fieldType = Utils::printSafe($fieldDef->getType()); $context->reportError( new Error( "Field {$type->name}.{$inputFieldName} of required type {$fieldType} was not provided.", $node ) ); } } return null; }, NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context): void { $parentType = Type::getNamedType($context->getParentInputType()); if (! $parentType instanceof InputObjectType) { return; } if ($context->getInputType() !== null) { return; } $suggestions = Utils::suggestionList( $node->name->value, array_keys($parentType->getFields()) ); $didYouMean = $suggestions === [] ? null : ' Did you mean ' . Utils::quotedOrList($suggestions) . '?'; $context->reportError( new Error( "Field \"{$node->name->value}\" is not defined by type \"{$parentType->name}\".{$didYouMean}", $node ) ); }, NodeKind::ENUM => function (EnumValueNode $node) use ($context): void { $this->isValidValueNode($context, $node); }, NodeKind::INT => function (IntValueNode $node) use ($context): void { $this->isValidValueNode($context, $node); }, NodeKind::FLOAT => function (FloatValueNode $node) use ($context): void { $this->isValidValueNode($context, $node); }, NodeKind::STRING => function (StringValueNode $node) use ($context): void { $this->isValidValueNode($context, $node); }, NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context): void { $this->isValidValueNode($context, $node); }, ]; } /** * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node * * @throws \JsonException */ protected function isValidValueNode(QueryValidationContext $context, ValueNode $node): void { // Report any error at the full type expected by the location. $locationType = $context->getInputType(); if ($locationType === null) { return; } $type = Type::getNamedType($locationType); if (! $type instanceof LeafType) { $typeStr = Utils::printSafe($type); $nodeStr = Printer::doPrint($node); $context->reportError( new Error( "Expected value of type \"{$typeStr}\", found {$nodeStr}.", $node ) ); return; } // Scalars determine if a literal value is valid via parseLiteral() which // may throw to indicate failure. try { $type->parseLiteral($node); } catch (\Throwable $error) { if ($error instanceof Error) { $context->reportError($error); } else { $typeStr = Utils::printSafe($type); $nodeStr = Printer::doPrint($node); $context->reportError( new Error( "Expected value of type \"{$typeStr}\", found {$nodeStr}; {$error->getMessage()}", $node, null, [], null, $error // Ensure a reference to the original error is maintained. ) ); } } } } graphql/lib/Validator/Rules/ScalarLeafs.php 0000644 00000003315 15166657207 0014701 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Validator\QueryValidationContext; class ScalarLeafs extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::FIELD => static function (FieldNode $node) use ($context): void { $type = $context->getType(); if ($type === null) { return; } if (Type::isLeafType(Type::getNamedType($type))) { if ($node->selectionSet !== null) { $context->reportError(new Error( static::noSubselectionAllowedMessage($node->name->value, $type->toString()), [$node->selectionSet] )); } } elseif ($node->selectionSet === null) { $context->reportError(new Error( static::requiredSubselectionMessage($node->name->value, $type->toString()), [$node] )); } }, ]; } public static function noSubselectionAllowedMessage(string $field, string $type): string { return "Field \"{$field}\" of type \"{$type}\" must not have a sub selection."; } public static function requiredSubselectionMessage(string $field, string $type): string { return "Field \"{$field}\" of type \"{$type}\" must have a sub selection."; } } graphql/lib/Validator/Rules/QueryComplexity.php 0000644 00000022235 15166657207 0015706 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Values; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\SelectionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * @phpstan-import-type ASTAndDefs from QuerySecurityRule */ class QueryComplexity extends QuerySecurityRule { protected int $maxQueryComplexity; protected int $queryComplexity; /** @var array<string, mixed> */ protected array $rawVariableValues = []; /** @var NodeList<VariableDefinitionNode> */ protected NodeList $variableDefs; /** @phpstan-var ASTAndDefs */ protected \ArrayObject $fieldNodeAndDefs; protected QueryValidationContext $context; /** @throws \InvalidArgumentException */ public function __construct(int $maxQueryComplexity) { $this->setMaxQueryComplexity($maxQueryComplexity); } public function getVisitor(QueryValidationContext $context): array { $this->queryComplexity = 0; $this->context = $context; $this->variableDefs = new NodeList([]); $this->fieldNodeAndDefs = new \ArrayObject(); return $this->invokeIfNeeded( $context, [ NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context): void { $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs( $context, $context->getParentType(), $selectionSet, null, $this->fieldNodeAndDefs ); }, NodeKind::VARIABLE_DEFINITION => function ($def): VisitorOperation { $this->variableDefs[] = $def; return Visitor::skipNode(); }, NodeKind::OPERATION_DEFINITION => [ 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context): void { $errors = $context->getErrors(); if ($errors !== []) { return; } if ($this->maxQueryComplexity === self::DISABLED) { return; } $this->queryComplexity = $this->fieldComplexity($operationDefinition->selectionSet); if ($this->queryComplexity <= $this->maxQueryComplexity) { return; } $context->reportError( new Error(static::maxQueryComplexityErrorMessage( $this->maxQueryComplexity, $this->queryComplexity )) ); }, ], ] ); } /** @throws \Exception */ protected function fieldComplexity(SelectionSetNode $selectionSet): int { $complexity = 0; foreach ($selectionSet->selections as $selection) { $complexity += $this->nodeComplexity($selection); } return $complexity; } /** @throws \Exception */ protected function nodeComplexity(SelectionNode $node): int { switch (true) { case $node instanceof FieldNode: if ($this->directiveExcludesField($node)) { return 0; } $childrenComplexity = isset($node->selectionSet) ? $this->fieldComplexity($node->selectionSet) : 0; $fieldDef = $this->fieldDefinition($node); if ($fieldDef instanceof FieldDefinition && $fieldDef->complexityFn !== null) { $fieldArguments = $this->buildFieldArguments($node); return ($fieldDef->complexityFn)($childrenComplexity, $fieldArguments); } return $childrenComplexity + 1; case $node instanceof InlineFragmentNode: return $this->fieldComplexity($node->selectionSet); case $node instanceof FragmentSpreadNode: $fragment = $this->getFragment($node); if ($fragment !== null) { return $this->fieldComplexity($fragment->selectionSet); } } return 0; } protected function fieldDefinition(FieldNode $field): ?FieldDefinition { foreach ($this->fieldNodeAndDefs[$this->getFieldName($field)] ?? [] as [$node, $def]) { if ($node === $field) { return $def; } } return null; } /** * Will the given field be executed at all, given the directives placed upon it? * * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation */ protected function directiveExcludesField(FieldNode $node): bool { foreach ($node->directives as $directiveNode) { if ($directiveNode->name->value === Directive::DEPRECATED_NAME) { return false; } [$errors, $variableValues] = Values::getVariableValues( $this->context->getSchema(), $this->variableDefs, $this->getRawVariableValues() ); if ($errors !== null && $errors !== []) { throw new Error(implode("\n\n", array_map(static fn (Error $error): string => $error->getMessage(), $errors))); } if ($directiveNode->name->value === Directive::INCLUDE_NAME) { $includeArguments = Values::getArgumentValues( Directive::includeDirective(), $directiveNode, $variableValues ); assert(is_bool($includeArguments['if']), 'ensured by query validation'); return ! $includeArguments['if']; } if ($directiveNode->name->value === Directive::SKIP_NAME) { $skipArguments = Values::getArgumentValues( Directive::skipDirective(), $directiveNode, $variableValues ); assert(is_bool($skipArguments['if']), 'ensured by query validation'); return $skipArguments['if']; } } return false; } /** @return array<string, mixed> */ public function getRawVariableValues(): array { return $this->rawVariableValues; } /** @param array<string, mixed>|null $rawVariableValues */ public function setRawVariableValues(?array $rawVariableValues = null): void { $this->rawVariableValues = $rawVariableValues ?? []; } /** * @throws \Exception * @throws Error * * @return array<string, mixed> */ protected function buildFieldArguments(FieldNode $node): array { $rawVariableValues = $this->getRawVariableValues(); $fieldDef = $this->fieldDefinition($node); /** @var array<string, mixed> $args */ $args = []; if ($fieldDef instanceof FieldDefinition) { [$errors, $variableValues] = Values::getVariableValues( $this->context->getSchema(), $this->variableDefs, $rawVariableValues ); if (is_array($errors) && $errors !== []) { throw new Error(implode("\n\n", array_map(static fn ($error) => $error->getMessage(), $errors))); } $args = Values::getArgumentValues($fieldDef, $node, $variableValues); } return $args; } public function getMaxQueryComplexity(): int { return $this->maxQueryComplexity; } public function getQueryComplexity(): int { return $this->queryComplexity; } /** * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0. * * @throws \InvalidArgumentException */ public function setMaxQueryComplexity(int $maxQueryComplexity): void { $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity); $this->maxQueryComplexity = $maxQueryComplexity; } public static function maxQueryComplexityErrorMessage(int $max, int $count): string { return "Max query complexity should be {$max} but got {$count}."; } protected function isEnabled(): bool { return $this->maxQueryComplexity !== self::DISABLED; } } graphql/lib/Validator/Rules/UniqueInputFieldNames.php 0000644 00000004750 15166657207 0016743 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\ObjectFieldNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * @phpstan-import-type VisitorArray from Visitor */ class UniqueInputFieldNames extends ValidationRule { /** @var array<string, NameNode> */ protected array $knownNames; /** @var array<array<string, NameNode>> */ protected array $knownNameStack; public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } /** @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { $this->knownNames = []; $this->knownNameStack = []; return [ NodeKind::OBJECT => [ 'enter' => function (): void { $this->knownNameStack[] = $this->knownNames; $this->knownNames = []; }, 'leave' => function (): void { $knownNames = array_pop($this->knownNameStack); assert(is_array($knownNames), 'should not happen if the visitor works correctly'); $this->knownNames = $knownNames; }, ], NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context): VisitorOperation { $fieldName = $node->name->value; if (isset($this->knownNames[$fieldName])) { $context->reportError(new Error( static::duplicateInputFieldMessage($fieldName), [$this->knownNames[$fieldName], $node->name] )); } else { $this->knownNames[$fieldName] = $node->name; } return Visitor::skipNode(); }, ]; } public static function duplicateInputFieldMessage(string $fieldName): string { return "There can be only one input field named \"{$fieldName}\"."; } } graphql/lib/Validator/Rules/LoneSchemaDefinition.php 0000644 00000003456 15166657207 0016556 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Lone schema definition. * * A GraphQL document is only valid if it contains only one schema definition. */ class LoneSchemaDefinition extends ValidationRule { public static function schemaDefinitionNotAloneMessage(): string { return 'Must provide only one schema definition.'; } public static function canNotDefineSchemaWithinExtensionMessage(): string { return 'Cannot define a new schema within a schema extension.'; } public function getSDLVisitor(SDLValidationContext $context): array { $oldSchema = $context->getSchema(); $alreadyDefined = $oldSchema === null ? false : ( $oldSchema->astNode !== null || $oldSchema->getQueryType() !== null || $oldSchema->getMutationType() !== null || $oldSchema->getSubscriptionType() !== null ); $schemaDefinitionsCount = 0; return [ NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount): void { if ($alreadyDefined) { $context->reportError(new Error(static::canNotDefineSchemaWithinExtensionMessage(), $node)); return; } if ($schemaDefinitionsCount > 0) { $context->reportError(new Error(static::schemaDefinitionNotAloneMessage(), $node)); } ++$schemaDefinitionsCount; }, ]; } } graphql/lib/Validator/Rules/QueryDepth.php 0000644 00000010223 15166657207 0014607 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Validator\QueryValidationContext; class QueryDepth extends QuerySecurityRule { /** @var array<string, bool> Fragment names which are already calculated in recursion */ protected array $calculatedFragments = []; protected int $maxQueryDepth; /** @throws \InvalidArgumentException */ public function __construct(int $maxQueryDepth) { $this->setMaxQueryDepth($maxQueryDepth); } public function getVisitor(QueryValidationContext $context): array { return $this->invokeIfNeeded( $context, [ NodeKind::OPERATION_DEFINITION => [ 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context): void { $maxDepth = $this->fieldDepth($operationDefinition); if ($maxDepth <= $this->maxQueryDepth) { return; } $context->reportError( new Error(static::maxQueryDepthErrorMessage($this->maxQueryDepth, $maxDepth)) ); }, ], ] ); } /** @param OperationDefinitionNode|FieldNode|InlineFragmentNode|FragmentDefinitionNode $node */ protected function fieldDepth(Node $node, int $depth = 0, int $maxDepth = 0): int { if ($node->selectionSet instanceof SelectionSetNode) { foreach ($node->selectionSet->selections as $childNode) { $maxDepth = $this->nodeDepth($childNode, $depth, $maxDepth); } } return $maxDepth; } protected function nodeDepth(Node $node, int $depth = 0, int $maxDepth = 0): int { switch (true) { case $node instanceof FieldNode: // node has children? if ($node->selectionSet !== null) { // update maxDepth if needed if ($depth > $maxDepth) { $maxDepth = $depth; } $maxDepth = $this->fieldDepth($node, $depth + 1, $maxDepth); } break; case $node instanceof InlineFragmentNode: $maxDepth = $this->fieldDepth($node, $depth, $maxDepth); break; case $node instanceof FragmentSpreadNode: $fragment = $this->getFragment($node); if ($fragment !== null) { $name = $fragment->name->value; if (isset($this->calculatedFragments[$name])) { return $this->maxQueryDepth + 1; } $this->calculatedFragments[$name] = true; $maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth); unset($this->calculatedFragments[$name]); } break; } return $maxDepth; } public function getMaxQueryDepth(): int { return $this->maxQueryDepth; } /** * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0. * * @throws \InvalidArgumentException */ public function setMaxQueryDepth(int $maxQueryDepth): void { $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth); $this->maxQueryDepth = $maxQueryDepth; } public static function maxQueryDepthErrorMessage(int $max, int $count): string { return "Max query depth should be {$max} but got {$count}."; } protected function isEnabled(): bool { return $this->maxQueryDepth !== self::DISABLED; } } graphql/lib/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php 0000644 00000010475 15166657207 0022210 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NonNullTypeNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * Provided required arguments on directives. * * A directive is only valid if all required (non-null without a * default value) field arguments have been provided. * * @phpstan-import-type VisitorArray from Visitor */ class ProvidedRequiredArgumentsOnDirectives extends ValidationRule { public static function missingDirectiveArgMessage(string $directiveName, string $argName, string $type): string { return "Directive \"@{$directiveName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided."; } /** @throws \Exception */ public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } /** @throws \Exception */ public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } /** * @throws \Exception * @throws \InvalidArgumentException * @throws \ReflectionException * @throws Error * @throws InvariantViolation * * @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { $requiredArgsMap = []; $schema = $context->getSchema(); $definedDirectives = $schema === null ? Directive::getInternalDirectives() : $schema->getDirectives(); foreach ($definedDirectives as $directive) { $directiveArgs = []; foreach ($directive->args as $arg) { if ($arg->isRequired()) { $directiveArgs[$arg->name] = $arg; } } $requiredArgsMap[$directive->name] = $directiveArgs; } $astDefinition = $context->getDocument()->definitions; foreach ($astDefinition as $def) { if ($def instanceof DirectiveDefinitionNode) { $arguments = $def->arguments; $requiredArgs = []; foreach ($arguments as $argument) { if ($argument->type instanceof NonNullTypeNode && ! isset($argument->defaultValue)) { $requiredArgs[$argument->name->value] = $argument; } } $requiredArgsMap[$def->name->value] = $requiredArgs; } } return [ NodeKind::DIRECTIVE => [ // Validate on leave to allow for deeper errors to appear first. 'leave' => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context): ?string { $directiveName = $directiveNode->name->value; $requiredArgs = $requiredArgsMap[$directiveName] ?? null; if ($requiredArgs === null || $requiredArgs === []) { return null; } $argNodeMap = []; foreach ($directiveNode->arguments as $arg) { $argNodeMap[$arg->name->value] = $arg; } foreach ($requiredArgs as $argName => $arg) { if (! isset($argNodeMap[$argName])) { $argType = $arg instanceof Argument ? $arg->getType()->toString() : Printer::doPrint($arg->type); $context->reportError( new Error(static::missingDirectiveArgMessage($directiveName, $argName, $argType), [$directiveNode]) ); } } return null; }, ], ]; } } graphql/lib/Validator/Rules/OneOfInputObjectsRule.php 0000644 00000006334 15166657207 0016715 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\ObjectValueNode; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * OneOf Input Objects validation rule. * * Validates that OneOf Input Objects have exactly one non-null field provided. */ class OneOfInputObjectsRule extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::OBJECT => static function (ObjectValueNode $node) use ($context): void { $type = $context->getInputType(); if ($type === null) { return; } $namedType = Type::getNamedType($type); if (! ($namedType instanceof InputObjectType) || ! $namedType->isOneOf() ) { return; } $providedFields = []; $nullFields = []; foreach ($node->fields as $fieldNode) { $fieldName = $fieldNode->name->value; $providedFields[] = $fieldName; // Check if the field value is explicitly null if ($fieldNode->value->kind === NodeKind::NULL) { $nullFields[] = $fieldName; } } $fieldCount = count($providedFields); if ($fieldCount === 0) { $context->reportError(new Error( static::oneOfInputObjectExpectedExactlyOneFieldMessage($namedType->name), [$node] )); return; } if ($fieldCount > 1) { $context->reportError(new Error( static::oneOfInputObjectExpectedExactlyOneFieldMessage($namedType->name, $fieldCount), [$node] )); return; } // At this point, $fieldCount === 1 if (count($nullFields) > 0) { // Exactly one field provided, but it's null $context->reportError(new Error( static::oneOfInputObjectFieldValueMustNotBeNullMessage($namedType->name, $nullFields[0]), [$node] )); } }, ]; } public static function oneOfInputObjectExpectedExactlyOneFieldMessage(string $typeName, ?int $providedCount = null): string { if ($providedCount === null) { return "OneOf input object '{$typeName}' must specify exactly one field."; } return "OneOf input object '{$typeName}' must specify exactly one field, but {$providedCount} fields were provided."; } public static function oneOfInputObjectFieldValueMustNotBeNullMessage(string $typeName, string $fieldName): string { return "OneOf input object '{$typeName}' field '{$fieldName}' must be non-null."; } } graphql/lib/Validator/Rules/FieldsOnCorrectType.php 0000644 00000012332 15166657207 0016407 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Type\Definition\HasFieldsType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\QueryValidationContext; class FieldsOnCorrectType extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::FIELD => function (FieldNode $node) use ($context): void { $fieldDef = $context->getFieldDef(); if ($fieldDef !== null && $fieldDef->isVisible()) { return; } $type = $context->getParentType(); if (! $type instanceof NamedType) { return; } // This isn't valid. Let's find suggestions, if any. $schema = $context->getSchema(); $fieldName = $node->name->value; // First determine if there are any suggested types to condition on. $suggestedTypeNames = $this->getSuggestedTypeNames($schema, $type, $fieldName); // If there are no suggested types, then perhaps this was a typo? $suggestedFieldNames = $suggestedTypeNames === [] ? $this->getSuggestedFieldNames($type, $fieldName) : []; // Report an error, including helpful suggestions. $context->reportError(new Error( static::undefinedFieldMessage( $node->name->value, $type->name, $suggestedTypeNames, $suggestedFieldNames ), [$node] )); }, ]; } /** * Go through all implementations of a type, as well as the interfaces * that it implements. If any of those types include the provided field, * suggest them, sorted by how often the type is referenced, starting * with interfaces. * * @throws InvariantViolation * * @return array<int, string> */ protected function getSuggestedTypeNames(Schema $schema, Type $type, string $fieldName): array { if (Type::isAbstractType($type)) { $suggestedObjectTypes = []; $interfaceUsageCount = []; foreach ($schema->getPossibleTypes($type) as $possibleType) { if (! $possibleType->hasField($fieldName)) { continue; } // This object type defines this field. $suggestedObjectTypes[] = $possibleType->name; foreach ($possibleType->getInterfaces() as $possibleInterface) { if (! $possibleInterface->hasField($fieldName)) { continue; } // This interface type defines this field. $interfaceUsageCount[$possibleInterface->name] = isset($interfaceUsageCount[$possibleInterface->name]) ? $interfaceUsageCount[$possibleInterface->name] + 1 : 0; } } // Suggest interface types based on how common they are. arsort($interfaceUsageCount); $suggestedInterfaceTypes = array_keys($interfaceUsageCount); // Suggest both interface and object types. return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes); } // Otherwise, must be an Object type, which does not have suggested types. return []; } /** * For the field name provided, determine if there are any similar field names * that may be the result of a typo. * * @throws InvariantViolation * * @return array<int, string> */ protected function getSuggestedFieldNames(Type $type, string $fieldName): array { if ($type instanceof HasFieldsType) { return Utils::suggestionList( $fieldName, $type->getFieldNames() ); } // Otherwise, must be a Union type, which does not define fields. return []; } /** * @param array<string> $suggestedTypeNames * @param array<string> $suggestedFieldNames */ public static function undefinedFieldMessage( string $fieldName, string $type, array $suggestedTypeNames, array $suggestedFieldNames ): string { $message = "Cannot query field \"{$fieldName}\" on type \"{$type}\"."; if ($suggestedTypeNames !== []) { $suggestions = Utils::quotedOrList($suggestedTypeNames); $message .= " Did you mean to use an inline fragment on {$suggestions}?"; } elseif ($suggestedFieldNames !== []) { $suggestions = Utils::quotedOrList($suggestedFieldNames); $message .= " Did you mean {$suggestions}?"; } return $message; } } graphql/lib/Validator/Rules/PossibleTypeExtensions.php 0000644 00000014524 15166657207 0017227 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Possible type extensions. * * A type extension is only valid if the type is defined and has the same kind. */ class PossibleTypeExtensions extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { $schema = $context->getSchema(); /** @var array<string, TypeDefinitionNode&Node> $definedTypes */ $definedTypes = []; foreach ($context->getDocument()->definitions as $def) { if ($def instanceof TypeDefinitionNode) { $name = $def->getName()->value; $definedTypes[$name] = $def; } } $checkTypeExtension = static function ($node) use ($context, $schema, &$definedTypes): ?VisitorOperation { $typeName = $node->name->value; $defNode = $definedTypes[$typeName] ?? null; $existingType = $schema !== null ? $schema->getType($typeName) : null; $expectedKind = null; if ($defNode !== null) { $expectedKind = self::defKindToExtKind($defNode->kind); } elseif ($existingType !== null) { $expectedKind = self::typeToExtKind($existingType); } if ($expectedKind !== null) { if ($expectedKind !== $node->kind) { $kindStr = self::extensionKindToTypeName($node->kind); $context->reportError( new Error( "Cannot extend non-{$kindStr} type \"{$typeName}\".", $defNode !== null ? [$defNode, $node] : $node, ), ); } } else { $existingTypesMap = $schema !== null ? $schema->getTypeMap() : []; $allTypeNames = [ ...array_keys($definedTypes), ...array_keys($existingTypesMap), ]; $suggestedTypes = Utils::suggestionList($typeName, $allTypeNames); $didYouMean = $suggestedTypes === [] ? '' : ' Did you mean ' . Utils::quotedOrList($suggestedTypes) . '?'; $context->reportError( new Error( "Cannot extend type \"{$typeName}\" because it is not defined.{$didYouMean}", $node->name, ), ); } return null; }; return [ NodeKind::SCALAR_TYPE_EXTENSION => $checkTypeExtension, NodeKind::OBJECT_TYPE_EXTENSION => $checkTypeExtension, NodeKind::INTERFACE_TYPE_EXTENSION => $checkTypeExtension, NodeKind::UNION_TYPE_EXTENSION => $checkTypeExtension, NodeKind::ENUM_TYPE_EXTENSION => $checkTypeExtension, NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkTypeExtension, ]; } /** @throws InvariantViolation */ private static function defKindToExtKind(string $kind): string { switch ($kind) { case NodeKind::SCALAR_TYPE_DEFINITION: return NodeKind::SCALAR_TYPE_EXTENSION; case NodeKind::OBJECT_TYPE_DEFINITION: return NodeKind::OBJECT_TYPE_EXTENSION; case NodeKind::INTERFACE_TYPE_DEFINITION: return NodeKind::INTERFACE_TYPE_EXTENSION; case NodeKind::UNION_TYPE_DEFINITION: return NodeKind::UNION_TYPE_EXTENSION; case NodeKind::ENUM_TYPE_DEFINITION: return NodeKind::ENUM_TYPE_EXTENSION; case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: return NodeKind::INPUT_OBJECT_TYPE_EXTENSION; default: throw new InvariantViolation("Unexpected definition kind: {$kind}."); } } /** @throws InvariantViolation */ private static function typeToExtKind(NamedType $type): string { switch (true) { case $type instanceof ScalarType: return NodeKind::SCALAR_TYPE_EXTENSION; case $type instanceof ObjectType: return NodeKind::OBJECT_TYPE_EXTENSION; case $type instanceof InterfaceType: return NodeKind::INTERFACE_TYPE_EXTENSION; case $type instanceof UnionType: return NodeKind::UNION_TYPE_EXTENSION; case $type instanceof EnumType: return NodeKind::ENUM_TYPE_EXTENSION; case $type instanceof InputObjectType: return NodeKind::INPUT_OBJECT_TYPE_EXTENSION; default: $unexpectedType = Utils::printSafe($type); throw new InvariantViolation("Unexpected type: {$unexpectedType}."); } } /** @throws InvariantViolation */ private static function extensionKindToTypeName(string $kind): string { switch ($kind) { case NodeKind::SCALAR_TYPE_EXTENSION: return 'scalar'; case NodeKind::OBJECT_TYPE_EXTENSION: return 'object'; case NodeKind::INTERFACE_TYPE_EXTENSION: return 'interface'; case NodeKind::UNION_TYPE_EXTENSION: return 'union'; case NodeKind::ENUM_TYPE_EXTENSION: return 'enum'; case NodeKind::INPUT_OBJECT_TYPE_EXTENSION: return 'input object'; default: throw new InvariantViolation("Unexpected extension kind: {$kind}."); } } } graphql/lib/Validator/Rules/UniqueTypeNames.php 0000644 00000004273 15166657207 0015621 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Unique type names. * * A GraphQL document is only valid if all defined types have unique names. */ class UniqueTypeNames extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { $schema = $context->getSchema(); /** @var array<string, NameNode> $knownTypeNames */ $knownTypeNames = []; $checkTypeName = static function ($node) use ($context, $schema, &$knownTypeNames): ?VisitorOperation { $typeName = $node->name->value; if ($schema !== null && $schema->getType($typeName) !== null) { $context->reportError( new Error( "Type \"{$typeName}\" already exists in the schema. It cannot also be defined in this type definition.", $node->name, ), ); return null; } if (array_key_exists($typeName, $knownTypeNames)) { $context->reportError( new Error( "There can be only one type named \"{$typeName}\".", [ $knownTypeNames[$typeName], $node->name, ] ), ); } else { $knownTypeNames[$typeName] = $node->name; } return Visitor::skipNode(); }; return [ NodeKind::SCALAR_TYPE_DEFINITION => $checkTypeName, NodeKind::OBJECT_TYPE_DEFINITION => $checkTypeName, NodeKind::INTERFACE_TYPE_DEFINITION => $checkTypeName, NodeKind::UNION_TYPE_DEFINITION => $checkTypeName, NodeKind::ENUM_TYPE_DEFINITION => $checkTypeName, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkTypeName, ]; } } graphql/lib/Validator/Rules/UniqueOperationNames.php 0000644 00000003344 15166657207 0016636 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; class UniqueOperationNames extends ValidationRule { /** @var array<string, NameNode> */ protected array $knownOperationNames; public function getVisitor(QueryValidationContext $context): array { $this->knownOperationNames = []; return [ NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context): VisitorOperation { $operationName = $node->name; if ($operationName !== null) { if (! isset($this->knownOperationNames[$operationName->value])) { $this->knownOperationNames[$operationName->value] = $operationName; } else { $context->reportError(new Error( static::duplicateOperationNameMessage($operationName->value), [$this->knownOperationNames[$operationName->value], $operationName] )); } } return Visitor::skipNode(); }, NodeKind::FRAGMENT_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(), ]; } public static function duplicateOperationNameMessage(string $operationName): string { return "There can be only one operation named \"{$operationName}\"."; } } graphql/lib/Validator/Rules/UniqueDirectivesPerLocation.php 0000644 00000006267 15166657207 0020162 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * Unique directive names per location. * * A GraphQL document is only valid if all non-repeatable directives at * a given location are uniquely named. * * @phpstan-import-type VisitorArray from Visitor */ class UniqueDirectivesPerLocation extends ValidationRule { /** @throws InvariantViolation */ public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } /** @throws InvariantViolation */ public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } /** * @throws InvariantViolation * * @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { /** @var array<string, true> $uniqueDirectiveMap */ $uniqueDirectiveMap = []; $schema = $context->getSchema(); $definedDirectives = $schema !== null ? $schema->getDirectives() : Directive::getInternalDirectives(); foreach ($definedDirectives as $directive) { if (! $directive->isRepeatable) { $uniqueDirectiveMap[$directive->name] = true; } } $astDefinitions = $context->getDocument()->definitions; foreach ($astDefinitions as $definition) { if ($definition instanceof DirectiveDefinitionNode && ! $definition->repeatable ) { $uniqueDirectiveMap[$definition->name->value] = true; } } return [ 'enter' => static function (Node $node) use ($uniqueDirectiveMap, $context): void { if (! property_exists($node, 'directives')) { return; } $knownDirectives = []; foreach ($node->directives as $directive) { $directiveName = $directive->name->value; if (isset($uniqueDirectiveMap[$directiveName])) { if (isset($knownDirectives[$directiveName])) { $context->reportError(new Error( static::duplicateDirectiveMessage($directiveName), [$knownDirectives[$directiveName], $directive] )); } else { $knownDirectives[$directiveName] = $directive; } } } }, ]; } public static function duplicateDirectiveMessage(string $directiveName): string { return "The directive \"{$directiveName}\" can only be used once at this location."; } } graphql/lib/Validator/Rules/FragmentsOnCompositeTypes.php 0000644 00000004365 15166657207 0017662 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Validator\QueryValidationContext; class FragmentsOnCompositeTypes extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context): void { if ($node->typeCondition === null) { return; } $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition); if ($type === null || Type::isCompositeType($type)) { return; } $context->reportError(new Error( static::inlineFragmentOnNonCompositeErrorMessage($type->toString()), [$node->typeCondition] )); }, NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context): void { $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition); if ($type === null || Type::isCompositeType($type)) { return; } $context->reportError(new Error( static::fragmentOnNonCompositeErrorMessage( $node->name->value, Printer::doPrint($node->typeCondition) ), [$node->typeCondition] )); }, ]; } public static function inlineFragmentOnNonCompositeErrorMessage(string $type): string { return "Fragment cannot condition on non composite type \"{$type}\"."; } public static function fragmentOnNonCompositeErrorMessage(string $fragName, string $type): string { return "Fragment \"{$fragName}\" cannot condition on non composite type \"{$type}\"."; } } graphql/lib/Validator/Rules/NoUndefinedVariables.php 0000644 00000004545 15166657207 0016556 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * A GraphQL operation is only valid if all variables encountered, both directly * and via fragment spreads, are defined by that operation. */ class NoUndefinedVariables extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { /** @var array<string, true> $variableNameDefined */ $variableNameDefined = []; return [ NodeKind::OPERATION_DEFINITION => [ 'enter' => static function () use (&$variableNameDefined): void { $variableNameDefined = []; }, 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context): void { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { $node = $usage['node']; $varName = $node->name->value; if (! isset($variableNameDefined[$varName])) { $context->reportError(new Error( static::undefinedVarMessage( $varName, $operation->name !== null ? $operation->name->value : null ), [$node, $operation] )); } } }, ], NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined): void { $variableNameDefined[$def->variable->name->value] = true; }, ]; } public static function undefinedVarMessage(string $varName, ?string $opName): string { return $opName === null ? "Variable \"\${$varName}\" is not defined by operation \"{$opName}\"." : "Variable \"\${$varName}\" is not defined."; } } graphql/lib/Validator/Rules/ProvidedRequiredArguments.php 0000644 00000004047 15166657207 0017667 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; class ProvidedRequiredArguments extends ValidationRule { /** @throws \Exception */ public function getVisitor(QueryValidationContext $context): array { $providedRequiredArgumentsOnDirectives = new ProvidedRequiredArgumentsOnDirectives(); return $providedRequiredArgumentsOnDirectives->getVisitor($context) + [ NodeKind::FIELD => [ 'leave' => static function (FieldNode $fieldNode) use ($context): ?VisitorOperation { $fieldDef = $context->getFieldDef(); if ($fieldDef === null) { return Visitor::skipNode(); } $argNodes = $fieldNode->arguments; $argNodeMap = []; foreach ($argNodes as $argNode) { $argNodeMap[$argNode->name->value] = $argNode; } foreach ($fieldDef->args as $argDef) { $argNode = $argNodeMap[$argDef->name] ?? null; if ($argNode === null && $argDef->isRequired()) { $context->reportError(new Error( static::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()->toString()), [$fieldNode] )); } } return null; }, ], ]; } public static function missingFieldArgMessage(string $fieldName, string $argName, string $type): string { return "Field \"{$fieldName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided."; } } graphql/lib/Validator/Rules/NoUnusedVariables.php 0000644 00000004331 15166657207 0016111 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Validator\QueryValidationContext; class NoUnusedVariables extends ValidationRule { /** @var array<int, VariableDefinitionNode> */ protected array $variableDefs; public function getVisitor(QueryValidationContext $context): array { $this->variableDefs = []; return [ NodeKind::OPERATION_DEFINITION => [ 'enter' => function (): void { $this->variableDefs = []; }, 'leave' => function (OperationDefinitionNode $operation) use ($context): void { $variableNameUsed = []; $usages = $context->getRecursiveVariableUsages($operation); $opName = $operation->name !== null ? $operation->name->value : null; foreach ($usages as $usage) { $node = $usage['node']; $variableNameUsed[$node->name->value] = true; } foreach ($this->variableDefs as $variableDef) { $variableName = $variableDef->variable->name->value; if (! isset($variableNameUsed[$variableName])) { $context->reportError(new Error( static::unusedVariableMessage($variableName, $opName), [$variableDef] )); } } }, ], NodeKind::VARIABLE_DEFINITION => function ($def): void { $this->variableDefs[] = $def; }, ]; } public static function unusedVariableMessage(string $varName, ?string $opName = null): string { return $opName !== null ? "Variable \"\${$varName}\" is never used in operation \"{$opName}\"." : "Variable \"\${$varName}\" is never used."; } } graphql/lib/Validator/Rules/DisableIntrospection.php 0000644 00000002701 15166657207 0016643 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Validator\QueryValidationContext; class DisableIntrospection extends QuerySecurityRule { public const ENABLED = 1; protected int $isEnabled; public function __construct(int $enabled) { $this->setEnabled($enabled); } public function setEnabled(int $enabled): void { $this->isEnabled = $enabled; } public function getVisitor(QueryValidationContext $context): array { return $this->invokeIfNeeded( $context, [ NodeKind::FIELD => static function (FieldNode $node) use ($context): void { if ($node->name->value !== '__type' && $node->name->value !== '__schema') { return; } $context->reportError(new Error( static::introspectionDisabledMessage(), [$node] )); }, ] ); } public static function introspectionDisabledMessage(): string { return 'GraphQL introspection is not allowed, but the query contained __schema or __type'; } protected function isEnabled(): bool { return $this->isEnabled !== self::DISABLED; } } graphql/lib/Validator/Rules/UniqueArgumentNames.php 0000644 00000004055 15166657207 0016460 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * @phpstan-import-type VisitorArray from Visitor */ class UniqueArgumentNames extends ValidationRule { /** @var array<string, NameNode> */ protected array $knownArgNames; public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } /** @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { $this->knownArgNames = []; return [ NodeKind::FIELD => function (): void { $this->knownArgNames = []; }, NodeKind::DIRECTIVE => function (): void { $this->knownArgNames = []; }, NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context): VisitorOperation { $argName = $node->name->value; if (isset($this->knownArgNames[$argName])) { $context->reportError(new Error( static::duplicateArgMessage($argName), [$this->knownArgNames[$argName], $node->name] )); } else { $this->knownArgNames[$argName] = $node->name; } return Visitor::skipNode(); }, ]; } public static function duplicateArgMessage(string $argName): string { return "There can be only one argument named \"{$argName}\"."; } } graphql/lib/Validator/Rules/KnownArgumentNames.php 0000644 00000004770 15166657207 0016312 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * Known argument names. * * A GraphQL field is only valid if all supplied arguments are defined by * that field. */ class KnownArgumentNames extends ValidationRule { /** @throws InvariantViolation */ public function getVisitor(QueryValidationContext $context): array { $knownArgumentNamesOnDirectives = new KnownArgumentNamesOnDirectives(); return $knownArgumentNamesOnDirectives->getVisitor($context) + [ NodeKind::ARGUMENT => static function (ArgumentNode $node) use ($context): void { $argDef = $context->getArgument(); if ($argDef !== null) { return; } $fieldDef = $context->getFieldDef(); if ($fieldDef === null) { return; } $parentType = $context->getParentType(); if (! $parentType instanceof NamedType) { return; } $context->reportError(new Error( static::unknownArgMessage( $node->name->value, $fieldDef->name, $parentType->name, Utils::suggestionList( $node->name->value, array_map( static fn (Argument $arg): string => $arg->name, $fieldDef->args ) ) ), [$node] )); }, ]; } /** @param array<string> $suggestedArgs */ public static function unknownArgMessage(string $argName, string $fieldName, string $typeName, array $suggestedArgs): string { $message = "Unknown argument \"{$argName}\" on field \"{$fieldName}\" of type \"{$typeName}\"."; if ($suggestedArgs !== []) { $suggestions = Utils::quotedOrList($suggestedArgs); $message .= " Did you mean {$suggestions}?"; } return $message; } } graphql/lib/Validator/Rules/QuerySecurityRule.php 0000644 00000015032 15166657207 0016205 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\HasFieldsType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * @see Visitor, FieldDefinition * * @phpstan-import-type VisitorArray from Visitor * * @phpstan-type ASTAndDefs \ArrayObject<string, \ArrayObject<int, array{FieldNode, FieldDefinition|null}>> */ abstract class QuerySecurityRule extends ValidationRule { public const DISABLED = 0; /** @var array<string, FragmentDefinitionNode> */ protected array $fragments = []; /** @throws \InvalidArgumentException */ protected function checkIfGreaterOrEqualToZero(string $name, int $value): void { if ($value < 0) { throw new \InvalidArgumentException("\${$name} argument must be greater or equal to 0."); } } protected function getFragment(FragmentSpreadNode $fragmentSpread): ?FragmentDefinitionNode { return $this->fragments[$fragmentSpread->name->value] ?? null; } /** @return array<string, FragmentDefinitionNode> */ protected function getFragments(): array { return $this->fragments; } /** * @phpstan-param VisitorArray $validators * * @phpstan-return VisitorArray */ protected function invokeIfNeeded(QueryValidationContext $context, array $validators): array { if (! $this->isEnabled()) { return []; } $this->gatherFragmentDefinition($context); return $validators; } abstract protected function isEnabled(): bool; protected function gatherFragmentDefinition(QueryValidationContext $context): void { // Gather all the fragment definition. // Importantly this does not include inline fragments. $definitions = $context->getDocument()->definitions; foreach ($definitions as $node) { if ($node instanceof FragmentDefinitionNode) { $this->fragments[$node->name->value] = $node; } } } /** * Given a selectionSet, adds all fields in that selection to * the passed in map of fields, and returns it at the end. * * Note: This is not the same as execution's collectFields because at static * time we do not know what object type will be used, so we unconditionally * spread in all fragments. * * @see \GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged * * @param \ArrayObject<string, true>|null $visitedFragmentNames * * @phpstan-param ASTAndDefs|null $astAndDefs * * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation * * @phpstan-return ASTAndDefs */ protected function collectFieldASTsAndDefs( QueryValidationContext $context, ?Type $parentType, SelectionSetNode $selectionSet, ?\ArrayObject $visitedFragmentNames = null, ?\ArrayObject $astAndDefs = null ): \ArrayObject { $visitedFragmentNames ??= new \ArrayObject(); $astAndDefs ??= new \ArrayObject(); foreach ($selectionSet->selections as $selection) { if ($selection instanceof FieldNode) { $fieldName = $selection->name->value; $fieldDef = null; if ($parentType instanceof HasFieldsType) { $schemaMetaFieldDef = Introspection::schemaMetaFieldDef(); $typeMetaFieldDef = Introspection::typeMetaFieldDef(); $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef(); $queryType = $context->getSchema()->getQueryType(); if ($fieldName === $schemaMetaFieldDef->name && $queryType === $parentType) { $fieldDef = $schemaMetaFieldDef; } elseif ($fieldName === $typeMetaFieldDef->name && $queryType === $parentType) { $fieldDef = $typeMetaFieldDef; } elseif ($fieldName === $typeNameMetaFieldDef->name) { $fieldDef = $typeNameMetaFieldDef; } elseif ($parentType->hasField($fieldName)) { $fieldDef = $parentType->getField($fieldName); } } $responseName = $this->getFieldName($selection); $responseContext = $astAndDefs[$responseName] ??= new \ArrayObject(); $responseContext[] = [$selection, $fieldDef]; } elseif ($selection instanceof InlineFragmentNode) { $typeCondition = $selection->typeCondition; $fragmentParentType = $typeCondition === null ? $parentType : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition); $astAndDefs = $this->collectFieldASTsAndDefs( $context, $fragmentParentType, $selection->selectionSet, $visitedFragmentNames, $astAndDefs ); } elseif ($selection instanceof FragmentSpreadNode) { $fragName = $selection->name->value; if (isset($visitedFragmentNames[$fragName])) { continue; } $visitedFragmentNames[$fragName] = true; $fragment = $context->getFragment($fragName); if ($fragment === null) { continue; } $astAndDefs = $this->collectFieldASTsAndDefs( $context, AST::typeFromAST([$context->getSchema(), 'getType'], $fragment->typeCondition), $fragment->selectionSet, $visitedFragmentNames, $astAndDefs ); } } return $astAndDefs; } protected function getFieldName(FieldNode $node): string { $fieldName = $node->name->value; return $node->alias === null ? $fieldName : $node->alias->value; } } graphql/lib/Validator/Rules/UniqueOperationTypes.php 0000644 00000004674 15166657207 0016706 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Unique operation types. * * A GraphQL document is only valid if it has only one type per operation. */ class UniqueOperationTypes extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { $schema = $context->getSchema(); $definedOperationTypes = []; $existingOperationTypes = $schema !== null ? [ 'query' => $schema->getQueryType(), 'mutation' => $schema->getMutationType(), 'subscription' => $schema->getSubscriptionType(), ] : []; /** * @param SchemaDefinitionNode|SchemaExtensionNode $node */ $checkOperationTypes = static function ($node) use ($context, &$definedOperationTypes, $existingOperationTypes): VisitorOperation { foreach ($node->operationTypes as $operationType) { $operation = $operationType->operation; $alreadyDefinedOperationType = $definedOperationTypes[$operation] ?? null; if (isset($existingOperationTypes[$operation])) { $context->reportError( new Error( "Type for {$operation} already defined in the schema. It cannot be redefined.", $operationType, ), ); } elseif ($alreadyDefinedOperationType !== null) { $context->reportError( new Error( "There can be only one {$operation} type in schema.", [$alreadyDefinedOperationType, $operationType], ), ); } else { $definedOperationTypes[$operation] = $operationType; } } return Visitor::skipNode(); }; return [ NodeKind::SCHEMA_DEFINITION => $checkOperationTypes, NodeKind::SCHEMA_EXTENSION => $checkOperationTypes, ]; } } graphql/lib/Validator/Rules/KnownArgumentNamesOnDirectives.php 0000644 00000007323 15166657207 0020626 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * Known argument names on directives. * * A GraphQL directive is only valid if all supplied arguments are defined by * that field. * * @phpstan-import-type VisitorArray from Visitor */ class KnownArgumentNamesOnDirectives extends ValidationRule { /** @param array<string> $suggestedArgs */ public static function unknownDirectiveArgMessage(string $argName, string $directiveName, array $suggestedArgs): string { $message = "Unknown argument \"{$argName}\" on directive \"@{$directiveName}\"."; if (isset($suggestedArgs[0])) { $suggestions = Utils::quotedOrList($suggestedArgs); $message .= " Did you mean {$suggestions}?"; } return $message; } /** @throws InvariantViolation */ public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } /** @throws InvariantViolation */ public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } /** * @throws InvariantViolation * * @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { $directiveArgs = []; $schema = $context->getSchema(); $definedDirectives = $schema !== null ? $schema->getDirectives() : Directive::getInternalDirectives(); foreach ($definedDirectives as $directive) { $directiveArgs[$directive->name] = array_map( static fn (Argument $arg): string => $arg->name, $directive->args ); } $astDefinitions = $context->getDocument()->definitions; foreach ($astDefinitions as $def) { if ($def instanceof DirectiveDefinitionNode) { $argNames = []; foreach ($def->arguments as $arg) { $argNames[] = $arg->name->value; } $directiveArgs[$def->name->value] = $argNames; } } return [ NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context): VisitorOperation { $directiveName = $directiveNode->name->value; if (! isset($directiveArgs[$directiveName])) { return Visitor::skipNode(); } $knownArgs = $directiveArgs[$directiveName]; foreach ($directiveNode->arguments as $argNode) { $argName = $argNode->name->value; if (! in_array($argName, $knownArgs, true)) { $suggestions = Utils::suggestionList($argName, $knownArgs); $context->reportError(new Error( static::unknownDirectiveArgMessage($argName, $directiveName, $suggestions), [$argNode] )); } } return Visitor::skipNode(); }, ]; } } graphql/lib/Validator/Rules/UniqueEnumValueNames.php 0000644 00000004754 15166657207 0016605 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Validator\SDLValidationContext; class UniqueEnumValueNames extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { /** @var array<string, array<string, EnumValueNode>> $knownValueNames */ $knownValueNames = []; /** * @param EnumTypeDefinitionNode|EnumTypeExtensionNode $enum */ $checkValueUniqueness = static function ($enum) use ($context, &$knownValueNames): VisitorOperation { $typeName = $enum->name->value; $schema = $context->getSchema(); $existingType = $schema !== null ? $schema->getType($typeName) : null; $valueNodes = $enum->values; if (! isset($knownValueNames[$typeName])) { $knownValueNames[$typeName] = []; } $valueNames = &$knownValueNames[$typeName]; foreach ($valueNodes as $valueDef) { $valueNameNode = $valueDef->name; $valueName = $valueNameNode->value; if ($existingType instanceof EnumType && $existingType->getValue($valueName) !== null) { $context->reportError(new Error( "Enum value \"{$typeName}.{$valueName}\" already exists in the schema. It cannot also be defined in this type extension.", $valueNameNode )); } elseif (isset($valueNames[$valueName])) { $context->reportError(new Error( "Enum value \"{$typeName}.{$valueName}\" can only be defined once.", [$valueNames[$valueName], $valueNameNode] )); } else { $valueNames[$valueName] = $valueNameNode; } } return Visitor::skipNode(); }; return [ NodeKind::ENUM_TYPE_DEFINITION => $checkValueUniqueness, NodeKind::ENUM_TYPE_EXTENSION => $checkValueUniqueness, ]; } } graphql/lib/Validator/Rules/CustomValidationRule.php 0000644 00000001772 15166657207 0016643 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\ValidationContext; /** * @see Node, VisitorOperation * * @phpstan-type NodeVisitorFnResult VisitorOperation|mixed|null * @phpstan-type VisitorFnResult array<string, callable(Node): NodeVisitorFnResult>|array<string, array<string, callable(Node): NodeVisitorFnResult>> * @phpstan-type VisitorFn callable(ValidationContext): VisitorFnResult */ class CustomValidationRule extends ValidationRule { /** * @var callable * * @phpstan-var VisitorFn */ protected $visitorFn; /** @phpstan-param VisitorFn $visitorFn */ public function __construct(string $name, callable $visitorFn) { $this->name = $name; $this->visitorFn = $visitorFn; } public function getVisitor(ValidationContext $context): array { return ($this->visitorFn)($context); } } graphql/lib/Validator/Rules/KnownDirectives.php 0000644 00000020126 15166657207 0015636 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Language\DirectiveLocation; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * @phpstan-import-type VisitorArray from Visitor */ class KnownDirectives extends ValidationRule { /** @throws InvariantViolation */ public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } /** @throws InvariantViolation */ public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } /** * @throws InvariantViolation * * @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { $locationsMap = []; $schema = $context->getSchema(); $definedDirectives = $schema === null ? Directive::getInternalDirectives() : $schema->getDirectives(); foreach ($definedDirectives as $directive) { $locationsMap[$directive->name] = $directive->locations; } $astDefinition = $context->getDocument()->definitions; foreach ($astDefinition as $def) { if ($def instanceof DirectiveDefinitionNode) { $locationNames = []; foreach ($def->locations as $location) { $locationNames[] = $location->value; } $locationsMap[$def->name->value] = $locationNames; } } return [ NodeKind::DIRECTIVE => function ( DirectiveNode $node, $key, $parent, $path, $ancestors ) use ( $context, $locationsMap ): void { $name = $node->name->value; $locations = $locationsMap[$name] ?? null; if ($locations === null) { $context->reportError(new Error( static::unknownDirectiveMessage($name), [$node] )); return; } $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors); if ($candidateLocation === '' || in_array($candidateLocation, $locations, true)) { return; } $context->reportError( new Error( static::misplacedDirectiveMessage($name, $candidateLocation), [$node] ) ); }, ]; } public static function unknownDirectiveMessage(string $directiveName): string { return "Unknown directive \"@{$directiveName}\"."; } /** * @param array<Node|NodeList<Node>> $ancestors * * @throws \Exception */ protected function getDirectiveLocationForASTPath(array $ancestors): string { $appliedTo = $ancestors[count($ancestors) - 1]; switch (true) { case $appliedTo instanceof OperationDefinitionNode: switch ($appliedTo->operation) { case 'query': return DirectiveLocation::QUERY; case 'mutation': return DirectiveLocation::MUTATION; case 'subscription': return DirectiveLocation::SUBSCRIPTION; } // no break, since all possible cases were handled case $appliedTo instanceof FieldNode: return DirectiveLocation::FIELD; case $appliedTo instanceof FragmentSpreadNode: return DirectiveLocation::FRAGMENT_SPREAD; case $appliedTo instanceof InlineFragmentNode: return DirectiveLocation::INLINE_FRAGMENT; case $appliedTo instanceof FragmentDefinitionNode: return DirectiveLocation::FRAGMENT_DEFINITION; case $appliedTo instanceof VariableDefinitionNode: return DirectiveLocation::VARIABLE_DEFINITION; case $appliedTo instanceof SchemaDefinitionNode: case $appliedTo instanceof SchemaExtensionNode: return DirectiveLocation::SCHEMA; case $appliedTo instanceof ScalarTypeDefinitionNode: case $appliedTo instanceof ScalarTypeExtensionNode: return DirectiveLocation::SCALAR; case $appliedTo instanceof ObjectTypeDefinitionNode: case $appliedTo instanceof ObjectTypeExtensionNode: return DirectiveLocation::OBJECT; case $appliedTo instanceof FieldDefinitionNode: return DirectiveLocation::FIELD_DEFINITION; case $appliedTo instanceof InterfaceTypeDefinitionNode: case $appliedTo instanceof InterfaceTypeExtensionNode: return DirectiveLocation::IFACE; case $appliedTo instanceof UnionTypeDefinitionNode: case $appliedTo instanceof UnionTypeExtensionNode: return DirectiveLocation::UNION; case $appliedTo instanceof EnumTypeDefinitionNode: case $appliedTo instanceof EnumTypeExtensionNode: return DirectiveLocation::ENUM; case $appliedTo instanceof EnumValueDefinitionNode: return DirectiveLocation::ENUM_VALUE; case $appliedTo instanceof InputObjectTypeDefinitionNode: case $appliedTo instanceof InputObjectTypeExtensionNode: return DirectiveLocation::INPUT_OBJECT; case $appliedTo instanceof InputValueDefinitionNode: $parentNode = $ancestors[count($ancestors) - 3]; return $parentNode instanceof InputObjectTypeDefinitionNode ? DirectiveLocation::INPUT_FIELD_DEFINITION : DirectiveLocation::ARGUMENT_DEFINITION; default: $unknownLocation = get_class($appliedTo); throw new \Exception("Unknown directive location: {$unknownLocation}."); } } public static function misplacedDirectiveMessage(string $directiveName, string $location): string { return "Directive \"{$directiveName}\" may not be used on \"{$location}\"."; } } graphql/lib/Validator/Rules/VariablesAreInputTypes.php 0000644 00000002631 15166657207 0017126 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Validator\QueryValidationContext; class VariablesAreInputTypes extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $node) use ($context): void { $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->type); // If the variable type is not an input type, return an error. if ($type === null || Type::isInputType($type)) { return; } $variableName = $node->variable->name->value; $context->reportError(new Error( static::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)), [$node->type] )); }, ]; } public static function nonInputTypeOnVarMessage(string $variableName, string $typeName): string { return "Variable \"\${$variableName}\" cannot be non-input type \"{$typeName}\"."; } } graphql/lib/Validator/Rules/LoneAnonymousOperation.php 0000644 00000003155 15166657207 0017212 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * Lone anonymous operation. * * A GraphQL document is only valid if when it contains an anonymous operation * (the query shorthand) that it contains only that one operation definition. */ class LoneAnonymousOperation extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { $operationCount = 0; return [ NodeKind::DOCUMENT => static function (DocumentNode $node) use (&$operationCount): void { $operationCount = 0; foreach ($node->definitions as $definition) { if ($definition instanceof OperationDefinitionNode) { ++$operationCount; } } }, NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use (&$operationCount, $context): void { if ($node->name !== null || $operationCount <= 1) { return; } $context->reportError( new Error(static::anonOperationNotAloneMessage(), [$node]) ); }, ]; } public static function anonOperationNotAloneMessage(): string { return 'This anonymous operation must be the only defined operation.'; } } graphql/lib/Validator/Rules/UniqueVariableNames.php 0000644 00000002767 15166657207 0016433 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Validator\QueryValidationContext; class UniqueVariableNames extends ValidationRule { /** @var array<string, NameNode> */ protected array $knownVariableNames; public function getVisitor(QueryValidationContext $context): array { $this->knownVariableNames = []; return [ NodeKind::OPERATION_DEFINITION => function (): void { $this->knownVariableNames = []; }, NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context): void { $variableName = $node->variable->name->value; if (! isset($this->knownVariableNames[$variableName])) { $this->knownVariableNames[$variableName] = $node->variable->name; } else { $context->reportError(new Error( static::duplicateVariableMessage($variableName), [$this->knownVariableNames[$variableName], $node->variable->name] )); } }, ]; } public static function duplicateVariableMessage(string $variableName): string { return "There can be only one variable named \"{$variableName}\"."; } } graphql/lib/Validator/Rules/ValidationRule.php 0000644 00000001612 15166657207 0015441 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * @phpstan-import-type VisitorArray from Visitor */ abstract class ValidationRule { protected string $name; public function getName(): string { return $this->name ?? static::class; } /** * Returns structure suitable for @see \GraphQL\Language\Visitor. * * @phpstan-return VisitorArray */ public function getVisitor(QueryValidationContext $context): array { return []; } /** * Returns structure suitable for @see \GraphQL\Language\Visitor. * * @phpstan-return VisitorArray */ public function getSDLVisitor(SDLValidationContext $context): array { return []; } } graphql/lib/Validator/Rules/NoFragmentCycles.php 0000644 00000006547 15166657207 0015736 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; class NoFragmentCycles extends ValidationRule { /** @var array<string, bool> */ protected array $visitedFrags; /** @var array<int, FragmentSpreadNode> */ protected array $spreadPath; /** @var array<string, int|null> */ protected array $spreadPathIndexByName; public function getVisitor(QueryValidationContext $context): array { // Tracks already visited fragments to maintain O(N) and to ensure that cycles // are not redundantly reported. $this->visitedFrags = []; // Array of AST nodes used to produce meaningful errors $this->spreadPath = []; // Position in the spread path $this->spreadPathIndexByName = []; return [ NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(), NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation { $this->detectCycleRecursive($node, $context); return Visitor::skipNode(); }, ]; } protected function detectCycleRecursive(FragmentDefinitionNode $fragment, QueryValidationContext $context): void { if (isset($this->visitedFrags[$fragment->name->value])) { return; } $fragmentName = $fragment->name->value; $this->visitedFrags[$fragmentName] = true; $spreadNodes = $context->getFragmentSpreads($fragment); if ($spreadNodes === []) { return; } $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath); foreach ($spreadNodes as $spreadNode) { $spreadName = $spreadNode->name->value; $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null; $this->spreadPath[] = $spreadNode; if ($cycleIndex === null) { $spreadFragment = $context->getFragment($spreadName); if ($spreadFragment !== null) { $this->detectCycleRecursive($spreadFragment, $context); } } else { $cyclePath = array_slice($this->spreadPath, $cycleIndex); $fragmentNames = []; foreach (array_slice($cyclePath, 0, -1) as $frag) { $fragmentNames[] = $frag->name->value; } $context->reportError(new Error( static::cycleErrorMessage($spreadName, $fragmentNames), $cyclePath )); } array_pop($this->spreadPath); } $this->spreadPathIndexByName[$fragmentName] = null; } /** @param array<string> $spreadNames */ public static function cycleErrorMessage(string $fragName, array $spreadNames = []): string { $via = $spreadNames === [] ? '' : ' via ' . implode(', ', $spreadNames); return "Cannot spread fragment \"{$fragName}\" within itself{$via}."; } } graphql/lib/Validator/Rules/ExecutableDefinitions.php 0000644 00000004231 15166657207 0016774 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\ExecutableDefinitionNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeExtensionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * Executable definitions. * * A GraphQL document is only valid for execution if all definitions are either * operation or fragment definitions. */ class ExecutableDefinitions extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context): VisitorOperation { foreach ($node->definitions as $definition) { if (! $definition instanceof ExecutableDefinitionNode) { if ($definition instanceof SchemaDefinitionNode || $definition instanceof SchemaExtensionNode) { $defName = 'schema'; } else { assert( $definition instanceof TypeDefinitionNode || $definition instanceof TypeExtensionNode, 'only other option' ); $defName = "\"{$definition->getName()->value}\""; } $context->reportError(new Error( static::nonExecutableDefinitionMessage($defName), [$definition] )); } } return Visitor::skipNode(); }, ]; } public static function nonExecutableDefinitionMessage(string $defName): string { return "The {$defName} definition is not executable."; } } graphql/lib/Validator/Rules/UniqueFieldDefinitionNames.php 0000644 00000007631 15166657207 0017735 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Unique field definition names. * * A GraphQL complex type is only valid if all its fields are uniquely named. */ class UniqueFieldDefinitionNames extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { $schema = $context->getSchema(); /** @var array<string, array<int, NameNode>> $knownFieldNames */ $knownFieldNames = []; $checkFieldUniqueness = static function ($node) use ($context, $schema, &$knownFieldNames): VisitorOperation { assert( $node instanceof InputObjectTypeDefinitionNode || $node instanceof InputObjectTypeExtensionNode || $node instanceof InterfaceTypeDefinitionNode || $node instanceof InterfaceTypeExtensionNode || $node instanceof ObjectTypeDefinitionNode || $node instanceof ObjectTypeExtensionNode ); $typeName = $node->name->value; $knownFieldNames[$typeName] ??= []; $fieldNames = &$knownFieldNames[$typeName]; foreach ($node->fields as $fieldDef) { $fieldName = $fieldDef->name->value; $existingType = $schema !== null ? $schema->getType($typeName) : null; if (self::hasField($existingType, $fieldName)) { $context->reportError( new Error( "Field \"{$typeName}.{$fieldName}\" already exists in the schema. It cannot also be defined in this type extension.", $fieldDef->name, ), ); } elseif (isset($fieldNames[$fieldName])) { $context->reportError( new Error( "Field \"{$typeName}.{$fieldName}\" can only be defined once.", [$fieldNames[$fieldName], $fieldDef->name], ), ); } else { $fieldNames[$fieldName] = $fieldDef->name; } } return Visitor::skipNode(); }; return [ NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkFieldUniqueness, NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkFieldUniqueness, NodeKind::INTERFACE_TYPE_DEFINITION => $checkFieldUniqueness, NodeKind::INTERFACE_TYPE_EXTENSION => $checkFieldUniqueness, NodeKind::OBJECT_TYPE_DEFINITION => $checkFieldUniqueness, NodeKind::OBJECT_TYPE_EXTENSION => $checkFieldUniqueness, ]; } /** @throws InvariantViolation */ private static function hasField(?NamedType $type, string $fieldName): bool { if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) { return $type->hasField($fieldName); } return false; } } graphql/lib/Validator/Rules/SingleFieldSubscription.php 0000644 00000003041 15166657207 0017307 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; class SingleFieldSubscription extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ($context): VisitorOperation { if ($node->operation === 'subscription') { $selections = $node->selectionSet->selections; if (count($selections) > 1) { $offendingSelections = $selections->splice(1, count($selections)); $context->reportError(new Error( static::multipleFieldsInOperation($node->name->value ?? null), $offendingSelections )); } } return Visitor::skipNode(); }, ]; } public static function multipleFieldsInOperation(?string $operationName): string { if ($operationName === null) { return 'Anonymous Subscription must select only one top level field.'; } return "Subscription \"{$operationName}\" must select only one top level field."; } } graphql/lib/Validator/Rules/OverlappingFieldsCanBeMerged.php 0000644 00000076566 15166657207 0020175 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\PairSet; use YOOtheme\GraphQL\Validator\QueryValidationContext; /** * ReasonOrReasons is recursive, but PHPStan does not support that. * * @phpstan-type ReasonOrReasons string|array<array{string, string|array<mixed>}> * @phpstan-type Conflict array{array{string, ReasonOrReasons}, array<int, FieldNode>, array<int, FieldNode>} * @phpstan-type FieldInfo array{Type|null, FieldNode, FieldDefinition|null} * @phpstan-type FieldMap array<string, array<int, FieldInfo>> */ class OverlappingFieldsCanBeMerged extends ValidationRule { /** * A memoization for when two fragments are compared "between" each other for * conflicts. Two fragments may be compared many times, so memoizing this can * dramatically improve the performance of this validator. */ protected PairSet $comparedFragmentPairs; /** * A cache for the "field map" and list of fragment names found in any given * selection set. Selection sets may be asked for this information multiple * times, so this improves the performance of this validator. * * @phpstan-var \SplObjectStorage<SelectionSetNode, array{FieldMap, array<int, string>}> */ protected \SplObjectStorage $cachedFieldsAndFragmentNames; public function getVisitor(QueryValidationContext $context): array { $this->comparedFragmentPairs = new PairSet(); $this->cachedFieldsAndFragmentNames = new \SplObjectStorage(); return [ NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context): void { $conflicts = $this->findConflictsWithinSelectionSet( $context, $context->getParentType(), $selectionSet ); foreach ($conflicts as $conflict) { [[$responseName, $reason], $fields1, $fields2] = $conflict; $context->reportError(new Error( static::fieldsConflictMessage($responseName, $reason), array_merge($fields1, $fields2) )); } }, ]; } /** * Find all conflicts found "within" a selection set, including those found * via spreading in fragments. Called when visiting each SelectionSet in the * GraphQL Document. * * @throws \Exception * * @phpstan-return array<int, Conflict> */ protected function findConflictsWithinSelectionSet( QueryValidationContext $context, ?Type $parentType, SelectionSetNode $selectionSet ): array { [$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames( $context, $parentType, $selectionSet ); $conflicts = []; // (A) Find all conflicts "within" the fields of this selection set. // Note: this is the *only place* `collectConflictsWithin` is called. $this->collectConflictsWithin( $context, $conflicts, $fieldMap ); $fragmentNamesLength = count($fragmentNames); if ($fragmentNamesLength !== 0) { // (B) Then collect conflicts between these fields and those represented by // each spread fragment name found. $comparedFragments = []; for ($i = 0; $i < $fragmentNamesLength; ++$i) { $this->collectConflictsBetweenFieldsAndFragment( $context, $conflicts, $comparedFragments, false, $fieldMap, $fragmentNames[$i] ); // (C) Then compare this fragment with all other fragments found in this // selection set to collect conflicts between fragments spread together. // This compares each item in the list of fragment names to every other item // in that same list (except for itself). for ($j = $i + 1; $j < $fragmentNamesLength; ++$j) { $this->collectConflictsBetweenFragments( $context, $conflicts, false, $fragmentNames[$i], $fragmentNames[$j] ); } } } return $conflicts; } /** * Given a selection set, return the collection of fields (a mapping of response * name to field ASTs and definitions) as well as a list of fragment names * referenced via fragment spreads. * * @throws \Exception * * @return array{FieldMap, array<int, string>} */ protected function getFieldsAndFragmentNames( QueryValidationContext $context, ?Type $parentType, SelectionSetNode $selectionSet ): array { if (! isset($this->cachedFieldsAndFragmentNames[$selectionSet])) { /** @phpstan-var FieldMap $astAndDefs */ $astAndDefs = []; /** @var array<string, bool> $fragmentNames */ $fragmentNames = []; $this->internalCollectFieldsAndFragmentNames( $context, $parentType, $selectionSet, $astAndDefs, $fragmentNames ); return $this->cachedFieldsAndFragmentNames[$selectionSet] = [$astAndDefs, array_keys($fragmentNames)]; } return $this->cachedFieldsAndFragmentNames[$selectionSet]; } /** * Algorithm:. * * Conflicts occur when two fields exist in a query which will produce the same * response name, but represent differing values, thus creating a conflict. * The algorithm below finds all conflicts via making a series of comparisons * between fields. In order to compare as few fields as possible, this makes * a series of comparisons "within" sets of fields and "between" sets of fields. * * Given any selection set, a collection produces both a set of fields by * also including all inline fragments, as well as a list of fragments * referenced by fragment spreads. * * A) Each selection set represented in the document first compares "within" its * collected set of fields, finding any conflicts between every pair of * overlapping fields. * Note: This is the *only time* that a the fields "within" a set are compared * to each other. After this only fields "between" sets are compared. * * B) Also, if any fragment is referenced in a selection set, then a * comparison is made "between" the original set of fields and the * referenced fragment. * * C) Also, if multiple fragments are referenced, then comparisons * are made "between" each referenced fragment. * * D) When comparing "between" a set of fields and a referenced fragment, first * a comparison is made between each field in the original set of fields and * each field in the the referenced set of fields. * * E) Also, if any fragment is referenced in the referenced selection set, * then a comparison is made "between" the original set of fields and the * referenced fragment (recursively referring to step D). * * F) When comparing "between" two fragments, first a comparison is made between * each field in the first referenced set of fields and each field in the the * second referenced set of fields. * * G) Also, any fragments referenced by the first must be compared to the * second, and any fragments referenced by the second must be compared to the * first (recursively referring to step F). * * H) When comparing two fields, if both have selection sets, then a comparison * is made "between" both selection sets, first comparing the set of fields in * the first selection set with the set of fields in the second. * * I) Also, if any fragment is referenced in either selection set, then a * comparison is made "between" the other set of fields and the * referenced fragment. * * J) Also, if two fragments are referenced in both selection sets, then a * comparison is made "between" the two fragments. */ /** * Given a reference to a fragment, return the represented collection of fields * as well as a list of nested fragment names referenced via fragment spreads. * * @param array<string, bool> $fragmentNames * * @phpstan-param FieldMap $astAndDefs * * @throws \Exception */ protected function internalCollectFieldsAndFragmentNames( QueryValidationContext $context, ?Type $parentType, SelectionSetNode $selectionSet, array &$astAndDefs, array &$fragmentNames ): void { foreach ($selectionSet->selections as $selection) { switch (true) { case $selection instanceof FieldNode: $fieldName = $selection->name->value; $fieldDef = null; if ( ($parentType instanceof ObjectType || $parentType instanceof InterfaceType) && $parentType->hasField($fieldName) ) { $fieldDef = $parentType->getField($fieldName); } $responseName = $selection->alias->value ?? $fieldName; $astAndDefs[$responseName] ??= []; $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef]; break; case $selection instanceof FragmentSpreadNode: $fragmentNames[$selection->name->value] = true; break; case $selection instanceof InlineFragmentNode: $typeCondition = $selection->typeCondition; $inlineFragmentType = $typeCondition === null ? $parentType : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition); $this->internalCollectFieldsAndFragmentNames( $context, $inlineFragmentType, $selection->selectionSet, $astAndDefs, $fragmentNames ); break; } } } /** * Collect all Conflicts "within" one collection of fields. * * @param array<int, Conflict> $conflicts * * @phpstan-param FieldMap $fieldMap * * @throws \Exception */ protected function collectConflictsWithin( QueryValidationContext $context, array &$conflicts, array $fieldMap ): void { // A field map is a keyed collection, where each key represents a response // name and the value at that key is a list of all fields which provide that // response name. For every response name, if there are multiple fields, they // must be compared to find a potential conflict. foreach ($fieldMap as $responseName => $fields) { // This compares every field in the list to every other field in this list // (except to itself). If the list only has one item, nothing needs to // be compared. $fieldsLength = count($fields); if ($fieldsLength <= 1) { continue; } for ($i = 0; $i < $fieldsLength; ++$i) { for ($j = $i + 1; $j < $fieldsLength; ++$j) { $conflict = $this->findConflict( $context, false, // within one collection is never mutually exclusive $responseName, $fields[$i], $fields[$j] ); if ($conflict !== null) { $conflicts[] = $conflict; } } } } } /** * Determines if there is a conflict between two particular fields, including * comparing their sub-fields. * * @param array{Type|null, FieldNode, FieldDefinition|null} $field1 * @param array{Type|null, FieldNode, FieldDefinition|null} $field2 * * @throws \Exception * * @phpstan-return Conflict|null */ protected function findConflict( QueryValidationContext $context, bool $parentFieldsAreMutuallyExclusive, string $responseName, array $field1, array $field2 ): ?array { [$parentType1, $ast1, $def1] = $field1; [$parentType2, $ast2, $def2] = $field2; // If it is known that two fields could not possibly apply at the same // time, due to the parent types, then it is safe to permit them to diverge // in aliased field or arguments used as they will not present any ambiguity // by differing. // It is known that two parent types could never overlap if they are // different Object types. Interface or Union types might overlap - if not // in the current state of the schema, then perhaps in some future version, // thus may not safely diverge. $areMutuallyExclusive = $parentFieldsAreMutuallyExclusive || ( $parentType1 !== $parentType2 && $parentType1 instanceof ObjectType && $parentType2 instanceof ObjectType ); // The return type for each field. $type1 = $def1 === null ? null : $def1->getType(); $type2 = $def2 === null ? null : $def2->getType(); if (! $areMutuallyExclusive) { // Two aliases must refer to the same field. $name1 = $ast1->name->value; $name2 = $ast2->name->value; if ($name1 !== $name2) { return [ [$responseName, "{$name1} and {$name2} are different fields"], [$ast1], [$ast2], ]; } if (! $this->sameArguments($ast1->arguments, $ast2->arguments)) { return [ [$responseName, 'they have differing arguments'], [$ast1], [$ast2], ]; } } if ( $type1 !== null && $type2 !== null && $this->doTypesConflict($type1, $type2) ) { return [ [$responseName, "they return conflicting types {$type1} and {$type2}"], [$ast1], [$ast2], ]; } // Collect and compare sub-fields. Use the same "visited fragment names" list // for both collections so fields in a fragment reference are never // compared to themselves. $selectionSet1 = $ast1->selectionSet; $selectionSet2 = $ast2->selectionSet; if ($selectionSet1 !== null && $selectionSet2 !== null) { $conflicts = $this->findConflictsBetweenSubSelectionSets( $context, $areMutuallyExclusive, Type::getNamedType($type1), $selectionSet1, Type::getNamedType($type2), $selectionSet2 ); return $this->subfieldConflicts( $conflicts, $responseName, $ast1, $ast2 ); } return null; } /** * @param NodeList<ArgumentNode> $arguments1 keep * @param NodeList<ArgumentNode> $arguments2 keep * * @throws \JsonException */ protected function sameArguments(NodeList $arguments1, NodeList $arguments2): bool { if (count($arguments1) !== count($arguments2)) { return false; } foreach ($arguments1 as $argument1) { $argument2 = null; foreach ($arguments2 as $argument) { if ($argument->name->value === $argument1->name->value) { $argument2 = $argument; break; } } if ($argument2 === null) { return false; } if (! $this->sameValue($argument1->value, $argument2->value)) { return false; } } return true; } /** @throws \JsonException */ protected function sameValue(Node $value1, Node $value2): bool { return Printer::doPrint($value1) === Printer::doPrint($value2); } /** * Two types conflict if both types could not apply to a value simultaneously. * * Composite types are ignored as their individual field types will be compared * later recursively. However, List and Non-Null types must match. */ protected function doTypesConflict(Type $type1, Type $type2): bool { if ($type1 instanceof ListOfType) { return $type2 instanceof ListOfType ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : true; } if ($type2 instanceof ListOfType) { return true; } if ($type1 instanceof NonNull) { return $type2 instanceof NonNull ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : true; } if ($type2 instanceof NonNull) { return true; } if (Type::isLeafType($type1) || Type::isLeafType($type2)) { return $type1 !== $type2; } return false; } /** * Find all conflicts found between two selection sets, including those found * via spreading in fragments. Called when determining if conflicts exist * between the sub-fields of two overlapping fields. * * @throws \Exception * * @return array<int, Conflict> */ protected function findConflictsBetweenSubSelectionSets( QueryValidationContext $context, bool $areMutuallyExclusive, ?Type $parentType1, SelectionSetNode $selectionSet1, ?Type $parentType2, SelectionSetNode $selectionSet2 ): array { $conflicts = []; [$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames( $context, $parentType1, $selectionSet1 ); [$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames( $context, $parentType2, $selectionSet2 ); // (H) First, collect all conflicts between these two collections of field. $this->collectConflictsBetween( $context, $conflicts, $areMutuallyExclusive, $fieldMap1, $fieldMap2 ); // (I) Then collect conflicts between the first collection of fields and // those referenced by each fragment name associated with the second. $fragmentNames2Length = count($fragmentNames2); if ($fragmentNames2Length !== 0) { $comparedFragments = []; for ($j = 0; $j < $fragmentNames2Length; ++$j) { $this->collectConflictsBetweenFieldsAndFragment( $context, $conflicts, $comparedFragments, $areMutuallyExclusive, $fieldMap1, $fragmentNames2[$j] ); } } // (I) Then collect conflicts between the second collection of fields and // those referenced by each fragment name associated with the first. $fragmentNames1Length = count($fragmentNames1); if ($fragmentNames1Length !== 0) { $comparedFragments = []; for ($i = 0; $i < $fragmentNames1Length; ++$i) { $this->collectConflictsBetweenFieldsAndFragment( $context, $conflicts, $comparedFragments, $areMutuallyExclusive, $fieldMap2, $fragmentNames1[$i] ); } } // (J) Also collect conflicts between any fragment names by the first and // fragment names by the second. This compares each item in the first set of // names to each item in the second set of names. for ($i = 0; $i < $fragmentNames1Length; ++$i) { for ($j = 0; $j < $fragmentNames2Length; ++$j) { $this->collectConflictsBetweenFragments( $context, $conflicts, $areMutuallyExclusive, $fragmentNames1[$i], $fragmentNames2[$j] ); } } return $conflicts; } /** * Collect all Conflicts between two collections of fields. This is similar to, * but different from the `collectConflictsWithin` function above. This check * assumes that `collectConflictsWithin` has already been called on each * provided collection of fields. This is true because this validator traverses * each individual selection set. * * @phpstan-param array<int, Conflict> $conflicts * @phpstan-param FieldMap $fieldMap1 * @phpstan-param FieldMap $fieldMap2 * * @throws \Exception */ protected function collectConflictsBetween( QueryValidationContext $context, array &$conflicts, bool $parentFieldsAreMutuallyExclusive, array $fieldMap1, array $fieldMap2 ): void { // A field map is a keyed collection, where each key represents a response // name and the value at that key is a list of all fields which provide that // response name. For any response name which appears in both provided field // maps, each field from the first field map must be compared to every field // in the second field map to find potential conflicts. foreach ($fieldMap1 as $responseName => $fields1) { if (! isset($fieldMap2[$responseName])) { continue; } $fields2 = $fieldMap2[$responseName]; $fields1Length = count($fields1); $fields2Length = count($fields2); for ($i = 0; $i < $fields1Length; ++$i) { for ($j = 0; $j < $fields2Length; ++$j) { $conflict = $this->findConflict( $context, $parentFieldsAreMutuallyExclusive, $responseName, $fields1[$i], $fields2[$j] ); if ($conflict !== null) { $conflicts[] = $conflict; } } } } } /** * Collect all conflicts found between a set of fields and a fragment reference * including via spreading in any nested fragments. * * @param array<string, true> $comparedFragments * * @phpstan-param array<int, Conflict> $conflicts * @phpstan-param FieldMap $fieldMap * * @throws \Exception */ protected function collectConflictsBetweenFieldsAndFragment( QueryValidationContext $context, array &$conflicts, array &$comparedFragments, bool $areMutuallyExclusive, array $fieldMap, string $fragmentName ): void { if (isset($comparedFragments[$fragmentName])) { return; } $comparedFragments[$fragmentName] = true; $fragment = $context->getFragment($fragmentName); if ($fragment === null) { return; } [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames( $context, $fragment ); if ($fieldMap === $fieldMap2) { return; } // (D) First collect any conflicts between the provided collection of fields // and the collection of fields represented by the given fragment. $this->collectConflictsBetween( $context, $conflicts, $areMutuallyExclusive, $fieldMap, $fieldMap2 ); // (E) Then collect any conflicts between the provided collection of fields // and any fragment names found in the given fragment. $fragmentNames2Length = count($fragmentNames2); for ($i = 0; $i < $fragmentNames2Length; ++$i) { $this->collectConflictsBetweenFieldsAndFragment( $context, $conflicts, $comparedFragments, $areMutuallyExclusive, $fieldMap, $fragmentNames2[$i] ); } } /** * Given a reference to a fragment, return the represented collection of fields * as well as a list of nested fragment names referenced via fragment spreads. * * @throws \Exception * * @phpstan-return array{FieldMap, array<int, string>} */ protected function getReferencedFieldsAndFragmentNames( QueryValidationContext $context, FragmentDefinitionNode $fragment ): array { // Short-circuit building a type from the AST if possible. if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) { return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet]; } $fragmentType = AST::typeFromAST([$context->getSchema(), 'getType'], $fragment->typeCondition); return $this->getFieldsAndFragmentNames( $context, $fragmentType, $fragment->selectionSet ); } /** * Collect all conflicts found between two fragments, including via spreading in * any nested fragments. * * @phpstan-param array<int, Conflict> $conflicts * * @throws \Exception */ protected function collectConflictsBetweenFragments( QueryValidationContext $context, array &$conflicts, bool $areMutuallyExclusive, string $fragmentName1, string $fragmentName2 ): void { // No need to compare a fragment to itself. if ($fragmentName1 === $fragmentName2) { return; } // Memoize so two fragments are not compared for conflicts more than once. if ( $this->comparedFragmentPairs->has( $fragmentName1, $fragmentName2, $areMutuallyExclusive ) ) { return; } $this->comparedFragmentPairs->add( $fragmentName1, $fragmentName2, $areMutuallyExclusive ); $fragment1 = $context->getFragment($fragmentName1); $fragment2 = $context->getFragment($fragmentName2); if ($fragment1 === null || $fragment2 === null) { return; } [$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames( $context, $fragment1 ); [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames( $context, $fragment2 ); // (F) First, collect all conflicts between these two collections of fields // (not including any nested fragments). $this->collectConflictsBetween( $context, $conflicts, $areMutuallyExclusive, $fieldMap1, $fieldMap2 ); // (G) Then collect conflicts between the first fragment and any nested // fragments spread in the second fragment. $fragmentNames2Length = count($fragmentNames2); for ($j = 0; $j < $fragmentNames2Length; ++$j) { $this->collectConflictsBetweenFragments( $context, $conflicts, $areMutuallyExclusive, $fragmentName1, $fragmentNames2[$j] ); } // (G) Then collect conflicts between the second fragment and any nested // fragments spread in the first fragment. $fragmentNames1Length = count($fragmentNames1); for ($i = 0; $i < $fragmentNames1Length; ++$i) { $this->collectConflictsBetweenFragments( $context, $conflicts, $areMutuallyExclusive, $fragmentNames1[$i], $fragmentName2 ); } } /** * Merge Conflicts between two sub-fields into a single Conflict. * * @phpstan-param array<int, Conflict> $conflicts * * @phpstan-return Conflict|null */ protected function subfieldConflicts( array $conflicts, string $responseName, FieldNode $ast1, FieldNode $ast2 ): ?array { if ($conflicts === []) { return null; } $reasons = []; foreach ($conflicts as $conflict) { $reasons[] = $conflict[0]; } $fields1 = [$ast1]; foreach ($conflicts as $conflict) { foreach ($conflict[1] as $field) { $fields1[] = $field; } } $fields2 = [$ast2]; foreach ($conflicts as $conflict) { foreach ($conflict[2] as $field) { $fields2[] = $field; } } return [ [ $responseName, $reasons, ], $fields1, $fields2, ]; } /** * @param string|array $reasonOrReasons * * @phpstan-param ReasonOrReasons $reasonOrReasons */ public static function fieldsConflictMessage(string $responseName, $reasonOrReasons): string { $reasonMessage = static::reasonMessage($reasonOrReasons); return "Fields \"{$responseName}\" conflict because {$reasonMessage}. Use different aliases on the fields to fetch both if this was intentional."; } /** * @param string|array $reasonOrReasons * * @phpstan-param ReasonOrReasons $reasonOrReasons */ public static function reasonMessage($reasonOrReasons): string { if (is_array($reasonOrReasons)) { $reasons = array_map( static function (array $reason): string { [$responseName, $subReason] = $reason; $reasonMessage = static::reasonMessage($subReason); return "subfields \"{$responseName}\" conflict because {$reasonMessage}"; }, $reasonOrReasons ); return implode(' and ', $reasons); } return $reasonOrReasons; } } graphql/lib/Validator/Rules/KnownTypeNames.php 0000644 00000006753 15166657207 0015454 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NamedTypeNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeSystemDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeSystemExtensionNode; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\QueryValidationContext; use YOOtheme\GraphQL\Validator\SDLValidationContext; use YOOtheme\GraphQL\Validator\ValidationContext; /** * Known type names. * * A GraphQL document is only valid if referenced types (specifically * variable definitions and fragment conditions) are defined by the type schema. * * @phpstan-import-type VisitorArray from \GraphQL\Language\Visitor */ class KnownTypeNames extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return $this->getASTVisitor($context); } public function getSDLVisitor(SDLValidationContext $context): array { return $this->getASTVisitor($context); } /** @phpstan-return VisitorArray */ public function getASTVisitor(ValidationContext $context): array { /** @var array<int, string> $definedTypes */ $definedTypes = []; foreach ($context->getDocument()->definitions as $def) { if ($def instanceof TypeDefinitionNode) { $definedTypes[] = $def->getName()->value; } } return [ NodeKind::NAMED_TYPE => static function (NamedTypeNode $node, $_1, $parent, $_2, $ancestors) use ($context, $definedTypes): void { $typeName = $node->name->value; $schema = $context->getSchema(); if (in_array($typeName, $definedTypes, true)) { return; } if ($schema !== null && $schema->hasType($typeName)) { return; } $definitionNode = $ancestors[2] ?? $parent; $isSDL = $definitionNode instanceof TypeSystemDefinitionNode || $definitionNode instanceof TypeSystemExtensionNode; if ($isSDL && in_array($typeName, Type::BUILT_IN_TYPE_NAMES, true)) { return; } $existingTypesMap = $schema !== null ? $schema->getTypeMap() : []; $typeNames = [ ...array_keys($existingTypesMap), ...$definedTypes, ]; $context->reportError(new Error( static::unknownTypeMessage( $typeName, Utils::suggestionList( $typeName, $isSDL ? [...Type::BUILT_IN_TYPE_NAMES, ...$typeNames] : $typeNames ) ), [$node] )); }, ]; } /** @param array<string> $suggestedTypes */ public static function unknownTypeMessage(string $type, array $suggestedTypes): string { $message = "Unknown type \"{$type}\"."; if ($suggestedTypes !== []) { $suggestionList = Utils::quotedOrList($suggestedTypes); $message .= " Did you mean {$suggestionList}?"; } return $message; } } graphql/lib/Validator/Rules/NoUnusedFragments.php 0000644 00000004375 15166657207 0016137 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; class NoUnusedFragments extends ValidationRule { /** @var array<int, OperationDefinitionNode> */ protected array $operationDefs; /** @var array<int, FragmentDefinitionNode> */ protected array $fragmentDefs; public function getVisitor(QueryValidationContext $context): array { $this->operationDefs = []; $this->fragmentDefs = []; return [ NodeKind::OPERATION_DEFINITION => function ($node): VisitorOperation { $this->operationDefs[] = $node; return Visitor::skipNode(); }, NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def): VisitorOperation { $this->fragmentDefs[] = $def; return Visitor::skipNode(); }, NodeKind::DOCUMENT => [ 'leave' => function () use ($context): void { $fragmentNameUsed = []; foreach ($this->operationDefs as $operation) { foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) { $fragmentNameUsed[$fragment->name->value] = true; } } foreach ($this->fragmentDefs as $fragmentDef) { $fragName = $fragmentDef->name->value; if (! isset($fragmentNameUsed[$fragName])) { $context->reportError(new Error( static::unusedFragMessage($fragName), [$fragmentDef] )); } } }, ], ]; } public static function unusedFragMessage(string $fragName): string { return "Fragment \"{$fragName}\" is never used."; } } graphql/lib/Validator/Rules/KnownFragmentNames.php 0000644 00000002054 15166657207 0016264 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Validator\QueryValidationContext; class KnownFragmentNames extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::FRAGMENT_SPREAD => static function (FragmentSpreadNode $node) use ($context): void { $fragmentName = $node->name->value; $fragment = $context->getFragment($fragmentName); if ($fragment !== null) { return; } $context->reportError(new Error( static::unknownFragmentMessage($fragmentName), [$node->name] )); }, ]; } public static function unknownFragmentMessage(string $fragName): string { return "Unknown fragment \"{$fragName}\"."; } } graphql/lib/Validator/Rules/UniqueArgumentDefinitionNames.php 0000644 00000005636 15166657207 0020477 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Unique argument definition names. * * A GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments. * A GraphQL Directive is only valid if all its arguments are uniquely named. */ class UniqueArgumentDefinitionNames extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { $checkArgUniquenessPerField = static function ($node) use ($context): VisitorOperation { assert( $node instanceof InterfaceTypeDefinitionNode || $node instanceof InterfaceTypeExtensionNode || $node instanceof ObjectTypeDefinitionNode || $node instanceof ObjectTypeExtensionNode ); foreach ($node->fields as $fieldDef) { self::checkArgUniqueness("{$node->name->value}.{$fieldDef->name->value}", $fieldDef->arguments, $context); } return Visitor::skipNode(); }; return [ NodeKind::DIRECTIVE_DEFINITION => static fn (DirectiveDefinitionNode $node): VisitorOperation => self::checkArgUniqueness("@{$node->name->value}", $node->arguments, $context), NodeKind::INTERFACE_TYPE_DEFINITION => $checkArgUniquenessPerField, NodeKind::INTERFACE_TYPE_EXTENSION => $checkArgUniquenessPerField, NodeKind::OBJECT_TYPE_DEFINITION => $checkArgUniquenessPerField, NodeKind::OBJECT_TYPE_EXTENSION => $checkArgUniquenessPerField, ]; } /** @param NodeList<InputValueDefinitionNode> $arguments */ private static function checkArgUniqueness(string $parentName, NodeList $arguments, SDLValidationContext $context): VisitorOperation { $seenArgs = []; foreach ($arguments as $argument) { $seenArgs[$argument->name->value][] = $argument; } foreach ($seenArgs as $argName => $argNodes) { if (count($argNodes) > 1) { $context->reportError( new Error( "Argument \"{$parentName}({$argName}:)\" can only be defined once.", $argNodes, ), ); } } return Visitor::skipNode(); } } graphql/lib/Validator/Rules/PossibleFragmentSpreads.php 0000644 00000014575 15166657207 0017321 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Type\Definition\AbstractType; use YOOtheme\GraphQL\Type\Definition\CompositeType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Validator\QueryValidationContext; class PossibleFragmentSpreads extends ValidationRule { public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context): void { $fragType = $context->getType(); $parentType = $context->getParentType(); if ( ! $fragType instanceof CompositeType || ! $parentType instanceof CompositeType || $this->doTypesOverlap($context->getSchema(), $fragType, $parentType) ) { return; } $context->reportError(new Error( static::typeIncompatibleAnonSpreadMessage($parentType->toString(), $fragType->toString()), [$node] )); }, NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context): void { $fragName = $node->name->value; $fragType = $this->getFragmentType($context, $fragName); $parentType = $context->getParentType(); if ( $fragType === null || $parentType === null || $this->doTypesOverlap($context->getSchema(), $fragType, $parentType) ) { return; } $context->reportError(new Error( static::typeIncompatibleSpreadMessage($fragName, $parentType->toString(), $fragType->toString()), [$node] )); }, ]; } /** * @param CompositeType&Type $fragType * @param CompositeType&Type $parentType * * @throws InvariantViolation */ protected function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType): bool { // Checking in the order of the most frequently used scenarios: // Parent type === fragment type if ($parentType === $fragType) { return true; } // Parent type is interface or union, fragment type is object type if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) { return $schema->isSubType($parentType, $fragType); } // Parent type is object type, fragment type is interface (or rather rare - union) if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) { return $schema->isSubType($fragType, $parentType); } // Both are object types: if ($parentType instanceof ObjectType && $fragType instanceof ObjectType) { return $parentType === $fragType; } // Both are interfaces // This case may be assumed valid only when implementations of two interfaces intersect // But we don't have information about all implementations at runtime // (getting this information via $schema->getPossibleTypes() requires scanning through whole schema // which is very costly to do at each request due to PHP "shared nothing" architecture) // // So in this case we just make it pass - invalid fragment spreads will be simply ignored during execution // See also https://github.com/webonyx/graphql-php/issues/69#issuecomment-283954602 if ($parentType instanceof InterfaceType && $fragType instanceof InterfaceType) { return true; // Note that there is one case when we do have information about all implementations: // When schema descriptor is defined ($schema->hasDescriptor()) // BUT we must avoid situation when some query that worked in development had suddenly stopped // working in production. So staying consistent and always validate. } // Interface within union if ($parentType instanceof UnionType && $fragType instanceof InterfaceType) { foreach ($parentType->getTypes() as $type) { if ($type->implementsInterface($fragType)) { return true; } } } if ($parentType instanceof InterfaceType && $fragType instanceof UnionType) { foreach ($fragType->getTypes() as $type) { if ($type->implementsInterface($parentType)) { return true; } } } if ($parentType instanceof UnionType && $fragType instanceof UnionType) { foreach ($fragType->getTypes() as $type) { if ($parentType->isPossibleType($type)) { return true; } } } return false; } public static function typeIncompatibleAnonSpreadMessage(string $parentType, string $fragType): string { return "Fragment cannot be spread here as objects of type \"{$parentType}\" can never be of type \"{$fragType}\"."; } /** * @throws \Exception * * @return (CompositeType&Type)|null */ protected function getFragmentType(QueryValidationContext $context, string $name): ?Type { $frag = $context->getFragment($name); if ($frag === null) { return null; } $type = AST::typeFromAST([$context->getSchema(), 'getType'], $frag->typeCondition); return $type instanceof CompositeType ? $type : null; } public static function typeIncompatibleSpreadMessage(string $fragName, string $parentType, string $fragType): string { return "Fragment \"{$fragName}\" cannot be spread here as objects of type \"{$parentType}\" can never be of type \"{$fragType}\"."; } } graphql/lib/Validator/Rules/UniqueDirectiveNames.php 0000644 00000003762 15166657207 0016620 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\SDLValidationContext; /** * Unique directive names. * * A GraphQL document is only valid if all defined directives have unique names. */ class UniqueDirectiveNames extends ValidationRule { public function getSDLVisitor(SDLValidationContext $context): array { $schema = $context->getSchema(); /** @var array<string, NameNode> $knownDirectiveNames */ $knownDirectiveNames = []; return [ NodeKind::DIRECTIVE_DEFINITION => static function ($node) use ($context, $schema, &$knownDirectiveNames): ?VisitorOperation { $directiveName = $node->name->value; if ($schema !== null && $schema->getDirective($directiveName) !== null) { $context->reportError( new Error( 'Directive "@' . $directiveName . '" already exists in the schema. It cannot be redefined.', $node->name, ), ); return null; } if (isset($knownDirectiveNames[$directiveName])) { $context->reportError( new Error( 'There can be only one directive named "@' . $directiveName . '".', [ $knownDirectiveNames[$directiveName], $node->name, ] ), ); } else { $knownDirectiveNames[$directiveName] = $node->name; } return Visitor::skipNode(); }, ]; } } graphql/lib/Validator/Rules/VariablesInAllowedPosition.php 0000644 00000011350 15166657207 0017753 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NullValueNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\ValueNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\TypeComparators; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\QueryValidationContext; class VariablesInAllowedPosition extends ValidationRule { /** * A map from variable names to their definition nodes. * * @var array<string, VariableDefinitionNode> */ protected array $varDefMap; public function getVisitor(QueryValidationContext $context): array { return [ NodeKind::OPERATION_DEFINITION => [ 'enter' => function (): void { $this->varDefMap = []; }, 'leave' => function (OperationDefinitionNode $operation) use ($context): void { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { $node = $usage['node']; $type = $usage['type']; $defaultValue = $usage['defaultValue']; $varName = $node->name->value; $varDef = $this->varDefMap[$varName] ?? null; if ($varDef === null || $type === null) { continue; } // A var type is allowed if it is the same or more strict (e.g. is // a subtype of) than the expected type. It can be more strict if // the variable type is non-null when the expected type is nullable. // If both are list types, the variable item type can be more strict // than the expected item type (contravariant). $schema = $context->getSchema(); $varType = AST::typeFromAST([$schema, 'getType'], $varDef->type); if ($varType !== null && ! $this->allowedVariableUsage($schema, $varType, $varDef->defaultValue, $type, $defaultValue)) { $context->reportError(new Error( static::badVarPosMessage($varName, $varType->toString(), $type->toString()), [$varDef, $node] )); } } }, ], NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode): void { $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode; }, ]; } /** * A var type is allowed if it is the same or more strict than the expected * type. It can be more strict if the variable type is non-null when the * expected type is nullable. If both are list types, the variable item type can * be more strict than the expected item type. */ public static function badVarPosMessage(string $varName, string $varType, string $expectedType): string { return "Variable \"\${$varName}\" of type \"{$varType}\" used in position expecting type \"{$expectedType}\"."; } /** * Returns true if the variable is allowed in the location it was found, * which includes considering if default values exist for either the variable * or the location at which it is located. * * @param ValueNode|null $varDefaultValue * @param mixed $locationDefaultValue * * @throws InvariantViolation */ protected function allowedVariableUsage(Schema $schema, Type $varType, $varDefaultValue, Type $locationType, $locationDefaultValue): bool { if ($locationType instanceof NonNull && ! $varType instanceof NonNull) { $hasNonNullVariableDefaultValue = $varDefaultValue !== null && ! $varDefaultValue instanceof NullValueNode; $hasLocationDefaultValue = Utils::undefined() !== $locationDefaultValue; if (! $hasNonNullVariableDefaultValue && ! $hasLocationDefaultValue) { return false; } $nullableLocationType = $locationType->getWrappedType(); return TypeComparators::isTypeSubTypeOf($schema, $varType, $nullableLocationType); } return TypeComparators::isTypeSubTypeOf($schema, $varType, $locationType); } } graphql/lib/Validator/Rules/UniqueFragmentNames.php 0000644 00000003115 15166657207 0016435 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator\Rules; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\Visitor; use YOOtheme\GraphQL\Language\VisitorOperation; use YOOtheme\GraphQL\Validator\QueryValidationContext; class UniqueFragmentNames extends ValidationRule { /** @var array<string, NameNode> */ protected array $knownFragmentNames; public function getVisitor(QueryValidationContext $context): array { $this->knownFragmentNames = []; return [ NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(), NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation { $fragmentName = $node->name->value; if (! isset($this->knownFragmentNames[$fragmentName])) { $this->knownFragmentNames[$fragmentName] = $node->name; } else { $context->reportError(new Error( static::duplicateFragmentNameMessage($fragmentName), [$this->knownFragmentNames[$fragmentName], $node->name] )); } return Visitor::skipNode(); }, ]; } public static function duplicateFragmentNameMessage(string $fragName): string { return "There can be only one fragment named \"{$fragName}\"."; } } graphql/lib/Validator/SDLValidationContext.php 0000644 00000001546 15166657207 0015435 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Validator; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Type\Schema; class SDLValidationContext implements ValidationContext { protected DocumentNode $ast; protected ?Schema $schema; /** @var list<Error> */ protected array $errors = []; public function __construct(DocumentNode $ast, ?Schema $schema) { $this->ast = $ast; $this->schema = $schema; } public function reportError(Error $error): void { $this->errors[] = $error; } public function getErrors(): array { return $this->errors; } public function getDocument(): DocumentNode { return $this->ast; } public function getSchema(): ?Schema { return $this->schema; } } graphql/lib/Executor/ScopedContext.php 0000644 00000000644 15166657207 0014064 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; /** * When the object passed as `$contextValue` to GraphQL execution implements this, * its `clone()` method will be called before passing the context down to a field. * This allows passing information to child fields in the query tree without affecting sibling or parent fields. */ interface ScopedContext { public function clone(): self; } graphql/lib/Executor/ExecutorImplementation.php 0000644 00000000451 15166657207 0016002 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; use YOOtheme\GraphQL\Executor\Promise\Promise; interface ExecutorImplementation { /** Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject. */ public function doExecute(): Promise; } graphql/lib/Executor/ReferenceExecutor.php 0000644 00000144323 15166657207 0014722 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Error\Warning; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\SelectionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Type\Definition\AbstractType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\LeafType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\OutputType; use YOOtheme\GraphQL\Type\Definition\ResolveInfo; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Type\SchemaValidationContext; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-import-type FieldResolver from Executor * @phpstan-import-type Path from ResolveInfo * @phpstan-import-type ArgsMapper from Executor * * @phpstan-type Fields \ArrayObject<string, \ArrayObject<int, FieldNode>> */ class ReferenceExecutor implements ExecutorImplementation { protected static \stdClass $UNDEFINED; protected ExecutionContext $exeContext; /** * @var \SplObjectStorage< * ObjectType, * \SplObjectStorage< * \ArrayObject<int, FieldNode>, * \ArrayObject< * string, * \ArrayObject<int, FieldNode> * > * > * > */ protected \SplObjectStorage $subFieldCache; /** * @var \SplObjectStorage< * FieldDefinition, * \SplObjectStorage<FieldNode, mixed> * > */ protected \SplObjectStorage $fieldArgsCache; protected FieldDefinition $schemaMetaFieldDef; protected FieldDefinition $typeMetaFieldDef; protected FieldDefinition $typeNameMetaFieldDef; protected function __construct(ExecutionContext $context) { if (! isset(static::$UNDEFINED)) { static::$UNDEFINED = Utils::undefined(); } $this->exeContext = $context; $this->subFieldCache = new \SplObjectStorage(); $this->fieldArgsCache = new \SplObjectStorage(); } /** * @param mixed $rootValue * @param mixed $contextValue * @param array<string, mixed> $variableValues * * @phpstan-param FieldResolver $fieldResolver * @phpstan-param ArgsMapper $argsMapper * * @throws \Exception */ public static function create( PromiseAdapter $promiseAdapter, Schema $schema, DocumentNode $documentNode, $rootValue, $contextValue, array $variableValues, ?string $operationName, callable $fieldResolver, ?callable $argsMapper = null // TODO make non-optional in next major release ): ExecutorImplementation { $exeContext = static::buildExecutionContext( $schema, $documentNode, $rootValue, $contextValue, $variableValues, $operationName, $fieldResolver, $argsMapper ?? Executor::getDefaultArgsMapper(), $promiseAdapter, ); if (is_array($exeContext)) { return new class($promiseAdapter->createFulfilled(new ExecutionResult(null, $exeContext))) implements ExecutorImplementation { private Promise $result; public function __construct(Promise $result) { $this->result = $result; } public function doExecute(): Promise { return $this->result; } }; } return new static($exeContext); } /** * Constructs an ExecutionContext object from the arguments passed to execute, * which we will pass throughout the other execution methods. * * @param mixed $rootValue * @param mixed $contextValue * @param array<string, mixed> $rawVariableValues * * @phpstan-param FieldResolver $fieldResolver * * @throws \Exception * * @return ExecutionContext|list<Error> */ protected static function buildExecutionContext( Schema $schema, DocumentNode $documentNode, $rootValue, $contextValue, array $rawVariableValues, ?string $operationName, callable $fieldResolver, callable $argsMapper, PromiseAdapter $promiseAdapter ) { /** @var list<Error> $errors */ $errors = []; /** @var array<string, FragmentDefinitionNode> $fragments */ $fragments = []; /** @var OperationDefinitionNode|null $operation */ $operation = null; /** @var bool $hasMultipleAssumedOperations */ $hasMultipleAssumedOperations = false; foreach ($documentNode->definitions as $definition) { switch (true) { case $definition instanceof OperationDefinitionNode: if ($operationName === null && $operation !== null) { $hasMultipleAssumedOperations = true; } if ( $operationName === null || (isset($definition->name) && $definition->name->value === $operationName) ) { $operation = $definition; } break; case $definition instanceof FragmentDefinitionNode: $fragments[$definition->name->value] = $definition; break; } } if ($operation === null) { $message = $operationName === null ? 'Must provide an operation.' : "Unknown operation named \"{$operationName}\"."; $errors[] = new Error($message); } elseif ($hasMultipleAssumedOperations) { $errors[] = new Error( 'Must provide operation name if query contains multiple operations.' ); } $variableValues = null; if ($operation !== null) { [$coercionErrors, $coercedVariableValues] = Values::getVariableValues( $schema, $operation->variableDefinitions, $rawVariableValues ); if ($coercionErrors === null) { $variableValues = $coercedVariableValues; } else { $errors = array_merge($errors, $coercionErrors); } } if ($errors !== []) { return $errors; } assert($operation instanceof OperationDefinitionNode, 'Has operation if no errors.'); assert(is_array($variableValues), 'Has variables if no errors.'); return new ExecutionContext( $schema, $fragments, $rootValue, $contextValue, $operation, $variableValues, $errors, $fieldResolver, $argsMapper, $promiseAdapter ); } /** * @throws \Exception * @throws Error */ public function doExecute(): Promise { // Return a Promise that will eventually resolve to the data described by // the "Response" section of the GraphQL specification. // // If errors are encountered while executing a GraphQL field, only that // field and its descendants will be omitted, and sibling fields will still // be executed. An execution which encounters errors will still result in a // resolved Promise. $data = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue); $result = $this->buildResponse($data); // Note: we deviate here from the reference implementation a bit by always returning promise // But for the "sync" case it is always fulfilled $promise = $this->getPromise($result); if ($promise !== null) { return $promise; } return $this->exeContext->promiseAdapter->createFulfilled($result); } /** * @param mixed $data * * @return ExecutionResult|Promise */ protected function buildResponse($data) { if ($data instanceof Promise) { return $data->then(fn ($resolved) => $this->buildResponse($resolved)); } $promiseAdapter = $this->exeContext->promiseAdapter; if ($promiseAdapter->isThenable($data)) { return $promiseAdapter->convertThenable($data) ->then(fn ($resolved) => $this->buildResponse($resolved)); } if ($data !== null) { $data = (array) $data; } return new ExecutionResult($data, $this->exeContext->errors); } /** * Implements the "Evaluating operations" section of the spec. * * @param mixed $rootValue * * @throws \Exception * * @return array<mixed>|Promise|\stdClass|null */ protected function executeOperation(OperationDefinitionNode $operation, $rootValue) { $type = $this->getOperationRootType($this->exeContext->schema, $operation); $fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject()); $path = []; $unaliasedPath = []; // Errors from sub-fields of a NonNull type may propagate to the top level, // at which point we still log the error and null the parent field, which // in this case is the entire response. // // Similar to completeValueCatchingError. try { $result = $operation->operation === 'mutation' ? $this->executeFieldsSerially($type, $rootValue, $path, $unaliasedPath, $fields, $this->exeContext->contextValue) : $this->executeFields($type, $rootValue, $path, $unaliasedPath, $fields, $this->exeContext->contextValue); $promise = $this->getPromise($result); if ($promise !== null) { return $promise->then(null, [$this, 'onError']); } return $result; } catch (Error $error) { $this->exeContext->addError($error); return null; } } /** @param mixed $error */ public function onError($error): ?Promise { if ($error instanceof Error) { $this->exeContext->addError($error); return $this->exeContext->promiseAdapter->createFulfilled(null); } return null; } /** * Extracts the root type of the operation from the schema. * * @throws \Exception * @throws Error */ protected function getOperationRootType(Schema $schema, OperationDefinitionNode $operation): ObjectType { switch ($operation->operation) { case 'query': $queryType = $schema->getQueryType(); if ($queryType === null) { throw new Error('Schema does not define the required query root type.', [$operation]); } return $queryType; case 'mutation': $mutationType = $schema->getMutationType(); if ($mutationType === null) { throw new Error('Schema is not configured for mutations.', [$operation]); } return $mutationType; case 'subscription': $subscriptionType = $schema->getSubscriptionType(); if ($subscriptionType === null) { throw new Error('Schema is not configured for subscriptions.', [$operation]); } return $subscriptionType; default: throw new Error('Can only execute queries, mutations and subscriptions.', [$operation]); } } /** * Given a selectionSet, adds all fields in that selection to * the passed in map of fields, and returns it at the end. * * CollectFields requires the "runtime type" of an object. For a field which * returns an Interface or Union type, the "runtime type" will be the actual * Object type returned by that field. * * @param \ArrayObject<string, true> $visitedFragmentNames * * @phpstan-param Fields $fields * * @throws \Exception * @throws Error * * @phpstan-return Fields */ protected function collectFields( ObjectType $runtimeType, SelectionSetNode $selectionSet, \ArrayObject $fields, \ArrayObject $visitedFragmentNames ): \ArrayObject { $exeContext = $this->exeContext; foreach ($selectionSet->selections as $selection) { switch (true) { case $selection instanceof FieldNode: if (! $this->shouldIncludeNode($selection)) { break; } $name = static::getFieldEntryKey($selection); $fields[$name] ??= new \ArrayObject(); $fields[$name][] = $selection; break; case $selection instanceof InlineFragmentNode: if ( ! $this->shouldIncludeNode($selection) || ! $this->doesFragmentConditionMatch($selection, $runtimeType) ) { break; } $this->collectFields( $runtimeType, $selection->selectionSet, $fields, $visitedFragmentNames ); break; case $selection instanceof FragmentSpreadNode: $fragName = $selection->name->value; if (isset($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) { break; } $visitedFragmentNames[$fragName] = true; if (! isset($exeContext->fragments[$fragName])) { break; } $fragment = $exeContext->fragments[$fragName]; if (! $this->doesFragmentConditionMatch($fragment, $runtimeType)) { break; } $this->collectFields( $runtimeType, $fragment->selectionSet, $fields, $visitedFragmentNames ); break; } } return $fields; } /** * Determines if a field should be included based on the @include and @skip * directives, where @skip has higher precedence than @include. * * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node * * @throws \Exception * @throws Error */ protected function shouldIncludeNode(SelectionNode $node): bool { $variableValues = $this->exeContext->variableValues; $skip = Values::getDirectiveValues( Directive::skipDirective(), $node, $variableValues ); if (isset($skip['if']) && $skip['if'] === true) { return false; } $include = Values::getDirectiveValues( Directive::includeDirective(), $node, $variableValues ); return ! isset($include['if']) || $include['if'] !== false; } /** Implements the logic to compute the key of a given fields entry. */ protected static function getFieldEntryKey(FieldNode $node): string { return $node->alias->value ?? $node->name->value; } /** * Determines if a fragment is applicable to the given type. * * @param FragmentDefinitionNode|InlineFragmentNode $fragment * * @throws \Exception */ protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type): bool { $typeConditionNode = $fragment->typeCondition; if ($typeConditionNode === null) { return true; } $conditionalType = AST::typeFromAST([$this->exeContext->schema, 'getType'], $typeConditionNode); if ($conditionalType === $type) { return true; } if ($conditionalType instanceof AbstractType) { return $this->exeContext->schema->isSubType($conditionalType, $type); } return false; } /** * Implements the "Evaluating selection sets" section of the spec for "write" mode. * * @param mixed $rootValue * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $contextValue * * @phpstan-param Fields $fields * * @return array<mixed>|Promise|\stdClass */ protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, array $unaliasedPath, \ArrayObject $fields, $contextValue) { $result = $this->promiseReduce( array_keys($fields->getArrayCopy()), function ($results, $responseName) use ($contextValue, $path, $unaliasedPath, $parentType, $rootValue, $fields) { $fieldNodes = $fields[$responseName]; assert($fieldNodes instanceof \ArrayObject, 'The keys of $fields populate $responseName'); $result = $this->resolveField( $parentType, $rootValue, $fieldNodes, $responseName, $path, $unaliasedPath, $this->maybeScopeContext($contextValue) ); if ($result === static::$UNDEFINED) { return $results; } $promise = $this->getPromise($result); if ($promise !== null) { return $promise->then(static function ($resolvedResult) use ($responseName, $results): array { $results[$responseName] = $resolvedResult; return $results; }); } $results[$responseName] = $result; return $results; }, [] ); $promise = $this->getPromise($result); if ($promise !== null) { return $result->then( static fn ($resolvedResults) => static::fixResultsIfEmptyArray($resolvedResults) ); } return static::fixResultsIfEmptyArray($result); } /** * Resolves the field on the given root value. * * In particular, this figures out the value that the field returns * by calling its resolve function, then calls completeValue to complete promises, * serialize scalars, or execute the sub-selection-set for objects. * * @param mixed $rootValue * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $contextValue * @param \ArrayObject<int, FieldNode> $fieldNodes * * @phpstan-param Path $path * @phpstan-param Path $unaliasedPath * * @throws Error * @throws InvariantViolation * * @return array<mixed>|\Throwable|mixed|null */ protected function resolveField( ObjectType $parentType, $rootValue, \ArrayObject $fieldNodes, string $responseName, array $path, array $unaliasedPath, $contextValue ) { $exeContext = $this->exeContext; $fieldNode = $fieldNodes[0]; assert($fieldNode instanceof FieldNode, '$fieldNodes is non-empty'); $fieldName = $fieldNode->name->value; $fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName); if ($fieldDef === null || ! $fieldDef->isVisible()) { return static::$UNDEFINED; } $path[] = $responseName; $unaliasedPath[] = $fieldName; $returnType = $fieldDef->getType(); // The resolve function's optional 3rd argument is a context value that // is provided to every resolve function within an execution. It is commonly // used to represent an authenticated user, or request-specific caches. // The resolve function's optional 4th argument is a collection of // information about the current execution state. $info = new ResolveInfo( $fieldDef, $fieldNodes, $parentType, $path, $exeContext->schema, $exeContext->fragments, $exeContext->rootValue, $exeContext->operation, $exeContext->variableValues, $unaliasedPath ); $resolveFn = $fieldDef->resolveFn ?? $parentType->resolveFieldFn ?? $this->exeContext->fieldResolver; $argsMapper = $fieldDef->argsMapper ?? $parentType->argsMapper ?? $this->exeContext->argsMapper; // Get the resolve function, regardless of if its result is normal // or abrupt (error). $result = $this->resolveFieldValueOrError( $fieldDef, $fieldNode, $resolveFn, $argsMapper, $rootValue, $info, $contextValue ); return $this->completeValueCatchingError( $returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue ); } /** * This method looks up the field on the given type definition. * * It has special casing for the two introspection fields, __schema * and __typename. __typename is special because it can always be * queried as a field, even in situations where no other fields * are allowed, like on a Union. __schema could get automatically * added to the query type, but that would require mutating type * definitions, which would cause issues. * * @throws InvariantViolation */ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName): ?FieldDefinition { $this->schemaMetaFieldDef ??= Introspection::schemaMetaFieldDef(); $this->typeMetaFieldDef ??= Introspection::typeMetaFieldDef(); $this->typeNameMetaFieldDef ??= Introspection::typeNameMetaFieldDef(); $queryType = $schema->getQueryType(); if ($fieldName === $this->schemaMetaFieldDef->name && $queryType === $parentType ) { return $this->schemaMetaFieldDef; } if ($fieldName === $this->typeMetaFieldDef->name && $queryType === $parentType ) { return $this->typeMetaFieldDef; } if ($fieldName === $this->typeNameMetaFieldDef->name) { return $this->typeNameMetaFieldDef; } return $parentType->findField($fieldName); } /** * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function. * Returns the result of resolveFn or the abrupt-return Error object. * * @param mixed $rootValue * @param mixed $contextValue * * @phpstan-param FieldResolver $resolveFn * * @return \Throwable|Promise|mixed */ protected function resolveFieldValueOrError( FieldDefinition $fieldDef, FieldNode $fieldNode, callable $resolveFn, callable $argsMapper, $rootValue, ResolveInfo $info, $contextValue ) { try { // Build a map of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. // @phpstan-ignore-next-line generics of SplObjectStorage are not inferred from empty instantiation $this->fieldArgsCache[$fieldDef] ??= new \SplObjectStorage(); $args = $this->fieldArgsCache[$fieldDef][$fieldNode] ??= $argsMapper(Values::getArgumentValues( $fieldDef, $fieldNode, $this->exeContext->variableValues ), $fieldDef, $fieldNode, $contextValue); return $resolveFn($rootValue, $args, $contextValue, $info); } catch (\Throwable $error) { return $error; } } /** * This is a small wrapper around completeValue which detects and logs errors * in the execution context. * * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $contextValue * @param mixed $result * * @phpstan-param Path $path * @phpstan-param Path $unaliasedPath * * @throws Error * * @return array<mixed>|Promise|\stdClass|null */ protected function completeValueCatchingError( Type $returnType, \ArrayObject $fieldNodes, ResolveInfo $info, array $path, array $unaliasedPath, $result, $contextValue ) { // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. try { $promise = $this->getPromise($result); if ($promise !== null) { $completed = $promise->then(fn (&$resolved) => $this->completeValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $resolved, $contextValue)); } else { $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue); } $promise = $this->getPromise($completed); if ($promise !== null) { return $promise->then(null, function ($error) use ($fieldNodes, $path, $unaliasedPath, $returnType): void { $this->handleFieldError($error, $fieldNodes, $path, $unaliasedPath, $returnType); }); } return $completed; } catch (\Throwable $err) { $this->handleFieldError($err, $fieldNodes, $path, $unaliasedPath, $returnType); return null; } } /** * @param mixed $rawError * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * * @throws Error */ protected function handleFieldError($rawError, \ArrayObject $fieldNodes, array $path, array $unaliasedPath, Type $returnType): void { $error = Error::createLocatedError( $rawError, $fieldNodes, $path, $unaliasedPath ); // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. if ($returnType instanceof NonNull) { throw $error; } // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. $this->exeContext->addError($error); } /** * Implements the instructions for completeValue as defined in the * "Field entries" section of the spec. * * If the field type is Non-Null, then this recursively completes the value * for the inner type. It throws a field error if that completion returns null, * as per the "Nullability" section of the spec. * * If the field type is a List, then this recursively completes the value * for the inner type on each item in the list. * * If the field type is a Scalar or Enum, ensures the completed value is a legal * value of the type by calling the `serialize` method of GraphQL type * definition. * * If the field is an abstract type, determine the runtime type of the value * and then complete based on that type. * * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. * * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $result * @param mixed $contextValue * * @throws \Throwable * @throws Error * * @return array<mixed>|mixed|Promise|null */ protected function completeValue( Type $returnType, \ArrayObject $fieldNodes, ResolveInfo $info, array $path, array $unaliasedPath, &$result, $contextValue ) { // If result is an Error, throw a located error. if ($result instanceof \Throwable) { throw $result; } // If field type is NonNull, complete for inner type, and throw field error // if result is null. if ($returnType instanceof NonNull) { $completed = $this->completeValue( $returnType->getWrappedType(), $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue ); if ($completed === null) { throw new InvariantViolation("Cannot return null for non-nullable field \"{$info->parentType}.{$info->fieldName}\"."); } return $completed; } if ($result === null) { return null; } // If field type is List, complete each item in the list with the inner type if ($returnType instanceof ListOfType) { if (! is_iterable($result)) { $resultType = gettype($result); throw new InvariantViolation("Expected field {$info->parentType}.{$info->fieldName} to return iterable, but got: {$resultType}."); } return $this->completeListValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue); } assert($returnType instanceof NamedType, 'Wrapping types should return early'); // Account for invalid schema definition when typeLoader returns different // instance than `resolveType` or $field->getType() or $arg->getType() assert( $returnType === $this->exeContext->schema->getType($returnType->name), SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name) ); if ($returnType instanceof LeafType) { return $this->completeLeafValue($returnType, $result); } if ($returnType instanceof AbstractType) { return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue); } // Field type must be and Object, Interface or Union and expect sub-selections. if ($returnType instanceof ObjectType) { return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue); } $safeReturnType = Utils::printSafe($returnType); throw new \RuntimeException("Cannot complete value of unexpected type {$safeReturnType}."); } /** @param mixed $value */ protected function isPromise($value): bool { return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value); } /** * Only returns the value if it acts like a Promise, i.e. has a "then" function, * otherwise returns null. * * @param mixed $value */ protected function getPromise($value): ?Promise { if ($value === null || $value instanceof Promise) { return $value; } $promiseAdapter = $this->exeContext->promiseAdapter; if ($promiseAdapter->isThenable($value)) { return $promiseAdapter->convertThenable($value); } return null; } /** * Similar to array_reduce(), however the reducing callback may return * a Promise, in which case reduction will continue after each promise resolves. * * If the callback does not return a Promise, then this function will also not * return a Promise. * * @param array<mixed> $values * @param Promise|mixed|null $initialValue * * @return Promise|mixed|null */ protected function promiseReduce(array $values, callable $callback, $initialValue) { return array_reduce( $values, function ($previous, $value) use ($callback) { $promise = $this->getPromise($previous); if ($promise !== null) { return $promise->then(static fn ($resolved) => $callback($resolved, $value)); } return $callback($previous, $value); }, $initialValue ); } /** * Complete a list value by completing each item in the list with the inner type. * * @param ListOfType<Type&OutputType> $returnType * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param iterable<mixed> $results * @param mixed $contextValue * * @throws Error * * @return array<mixed>|Promise|\stdClass */ protected function completeListValue( ListOfType $returnType, \ArrayObject $fieldNodes, ResolveInfo $info, array $path, array $unaliasedPath, iterable &$results, $contextValue ) { $itemType = $returnType->getWrappedType(); $i = 0; $containsPromise = false; $completedItems = []; foreach ($results as $item) { $itemPath = [...$path, $i]; $info->path = $itemPath; $itemUnaliasedPath = [...$unaliasedPath, $i]; $info->unaliasedPath = $itemUnaliasedPath; ++$i; $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $itemPath, $itemUnaliasedPath, $item, $contextValue); if (! $containsPromise && $this->getPromise($completedItem) !== null) { $containsPromise = true; } $completedItems[] = $completedItem; } return $containsPromise ? $this->exeContext->promiseAdapter->all($completedItems) : $completedItems; } /** * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible. * * @param mixed $result * * @throws \Exception * * @return mixed */ protected function completeLeafValue(LeafType $returnType, &$result) { try { return $returnType->serialize($result); } catch (\Throwable $error) { $safeReturnType = Utils::printSafe($returnType); $safeResult = Utils::printSafe($result); throw new InvariantViolation("Expected a value of type {$safeReturnType} but received: {$safeResult}. {$error->getMessage()}", 0, $error); } } /** * Complete a value of an abstract type by determining the runtime object type * of that value, then complete the value for that type. * * @param AbstractType&Type $returnType * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param array<mixed> $result * @param mixed $contextValue * * @throws \Exception * @throws Error * @throws InvariantViolation * * @return array<mixed>|Promise|\stdClass */ protected function completeAbstractValue( AbstractType $returnType, \ArrayObject $fieldNodes, ResolveInfo $info, array $path, array $unaliasedPath, &$result, $contextValue ) { $typeCandidate = $returnType->resolveType($result, $contextValue, $info); if ($typeCandidate === null) { $runtimeType = static::defaultTypeResolver($result, $contextValue, $info, $returnType); } elseif (! is_string($typeCandidate) && is_callable($typeCandidate)) { $runtimeType = $typeCandidate(); } else { $runtimeType = $typeCandidate; } $promise = $this->getPromise($runtimeType); if ($promise !== null) { return $promise->then(fn ($resolvedRuntimeType) => $this->completeObjectValue( $this->ensureValidRuntimeType( $resolvedRuntimeType, $returnType, $info, $result ), $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue )); } return $this->completeObjectValue( $this->ensureValidRuntimeType( $runtimeType, $returnType, $info, $result ), $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue ); } /** * If a resolveType function is not given, then a default resolve behavior is * used which attempts two strategies:. * * First, See if the provided value has a `__typename` field defined, if so, use * that value as name of the resolved type. * * Otherwise, test each possible type for the abstract type by calling * isTypeOf for the object being coerced, returning the first type that matches. * * @param mixed|null $value * @param mixed|null $contextValue * @param AbstractType&Type $abstractType * * @throws InvariantViolation * * @return Promise|Type|string|null */ protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType) { $typename = Utils::extractKey($value, '__typename'); if (is_string($typename)) { return $typename; } if ($abstractType instanceof InterfaceType && isset($info->schema->getConfig()->typeLoader)) { $safeValue = Utils::printSafe($value); Warning::warnOnce( "GraphQL Interface Type `{$abstractType->name}` returned `null` from its `resolveType` function for value: {$safeValue}. Switching to slow resolution method using `isTypeOf` of all possible implementations. It requires full schema scan and degrades query performance significantly. Make sure your `resolveType` function always returns a valid implementation or throws.", Warning::WARNING_FULL_SCHEMA_SCAN ); } $possibleTypes = $info->schema->getPossibleTypes($abstractType); $promisedIsTypeOfResults = []; foreach ($possibleTypes as $index => $type) { $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info); if ($isTypeOfResult === null) { continue; } $promise = $this->getPromise($isTypeOfResult); if ($promise !== null) { $promisedIsTypeOfResults[$index] = $promise; } elseif ($isTypeOfResult === true) { return $type; } } if ($promisedIsTypeOfResults !== []) { return $this->exeContext->promiseAdapter ->all($promisedIsTypeOfResults) ->then(static function ($isTypeOfResults) use ($possibleTypes): ?ObjectType { foreach ($isTypeOfResults as $index => $result) { if ($result) { return $possibleTypes[$index]; } } return null; }); } return null; } /** * Complete an Object value by executing all sub-selections. * * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $result * @param mixed $contextValue * * @throws \Exception * @throws Error * * @return array<mixed>|Promise|\stdClass */ protected function completeObjectValue( ObjectType $returnType, \ArrayObject $fieldNodes, ResolveInfo $info, array $path, array $unaliasedPath, &$result, $contextValue ) { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. $isTypeOf = $returnType->isTypeOf($result, $contextValue, $info); if ($isTypeOf !== null) { $promise = $this->getPromise($isTypeOf); if ($promise !== null) { return $promise->then(function ($isTypeOfResult) use ( $contextValue, $returnType, $fieldNodes, $path, $unaliasedPath, &$result ) { if (! $isTypeOfResult) { throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes); } return $this->collectAndExecuteSubfields( $returnType, $fieldNodes, $path, $unaliasedPath, $result, $contextValue ); }); } assert(is_bool($isTypeOf), 'Promise would return early'); if (! $isTypeOf) { throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes); } } return $this->collectAndExecuteSubfields( $returnType, $fieldNodes, $path, $unaliasedPath, $result, $contextValue ); } /** * @param \ArrayObject<int, FieldNode> $fieldNodes * @param array<mixed> $result */ protected function invalidReturnTypeError( ObjectType $returnType, $result, \ArrayObject $fieldNodes ): Error { $safeResult = Utils::printSafe($result); return new Error( "Expected value of type \"{$returnType->name}\" but got: {$safeResult}.", $fieldNodes ); } /** * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $result * @param mixed $contextValue * * @throws \Exception * @throws Error * * @return array<mixed>|Promise|\stdClass */ protected function collectAndExecuteSubfields( ObjectType $returnType, \ArrayObject $fieldNodes, array $path, array $unaliasedPath, &$result, $contextValue ) { $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes); return $this->executeFields($returnType, $result, $path, $unaliasedPath, $subFieldNodes, $contextValue); } /** * A memoized collection of relevant subfields with regard to the return * type. Memoizing ensures the subfields are not repeatedly calculated, which * saves overhead when resolving lists of values. * * @param \ArrayObject<int, FieldNode> $fieldNodes * * @throws \Exception * @throws Error * * @phpstan-return Fields */ protected function collectSubFields(ObjectType $returnType, \ArrayObject $fieldNodes): \ArrayObject { // @phpstan-ignore-next-line generics of SplObjectStorage are not inferred from empty instantiation $returnTypeCache = $this->subFieldCache[$returnType] ??= new \SplObjectStorage(); if (! isset($returnTypeCache[$fieldNodes])) { // Collect sub-fields to execute to complete this value. $subFieldNodes = new \ArrayObject(); $visitedFragmentNames = new \ArrayObject(); foreach ($fieldNodes as $fieldNode) { if (isset($fieldNode->selectionSet)) { $subFieldNodes = $this->collectFields( $returnType, $fieldNode->selectionSet, $subFieldNodes, $visitedFragmentNames ); } } $returnTypeCache[$fieldNodes] = $subFieldNodes; } return $returnTypeCache[$fieldNodes]; } /** * Implements the "Evaluating selection sets" section of the spec for "read" mode. * * @param mixed $rootValue * @param list<string|int> $path * @param list<string|int> $unaliasedPath * @param mixed $contextValue * * @phpstan-param Fields $fields * * @throws Error * @throws InvariantViolation * * @return Promise|\stdClass|array<mixed> */ protected function executeFields(ObjectType $parentType, $rootValue, array $path, array $unaliasedPath, \ArrayObject $fields, $contextValue) { $containsPromise = false; $results = []; foreach ($fields as $responseName => $fieldNodes) { $result = $this->resolveField( $parentType, $rootValue, $fieldNodes, $responseName, $path, $unaliasedPath, $this->maybeScopeContext($contextValue) ); if ($result === static::$UNDEFINED) { continue; } if (! $containsPromise && $this->isPromise($result)) { $containsPromise = true; } $results[$responseName] = $result; } // If there are no promises, we can just return the object if (! $containsPromise) { return static::fixResultsIfEmptyArray($results); } // Otherwise, results is a map from field name to the result of resolving that // field, which is possibly a promise. Return a promise that will return this // same map, but with any promises replaced with the values they resolved to. return $this->promiseForAssocArray($results); } /** * Differentiate empty objects from empty lists. * * @see https://github.com/webonyx/graphql-php/issues/59 * * @param array<mixed>|mixed $results * * @return array<mixed>|\stdClass|mixed */ protected static function fixResultsIfEmptyArray($results) { if ($results === []) { return new \stdClass(); } return $results; } /** * Transform an associative array with Promises to a Promise which resolves to an * associative array where all Promises were resolved. * * @param array<string, Promise|mixed> $assoc */ protected function promiseForAssocArray(array $assoc): Promise { $keys = array_keys($assoc); $valuesAndPromises = array_values($assoc); $promise = $this->exeContext->promiseAdapter->all($valuesAndPromises); return $promise->then(static function ($values) use ($keys) { $resolvedResults = []; foreach ($values as $i => $value) { $resolvedResults[$keys[$i]] = $value; } return static::fixResultsIfEmptyArray($resolvedResults); }); } /** * @param mixed $runtimeTypeOrName * @param AbstractType&Type $returnType * @param mixed $result * * @throws InvariantViolation */ protected function ensureValidRuntimeType( $runtimeTypeOrName, AbstractType $returnType, ResolveInfo $info, &$result ): ObjectType { $runtimeType = is_string($runtimeTypeOrName) ? $this->exeContext->schema->getType($runtimeTypeOrName) : $runtimeTypeOrName; if (! $runtimeType instanceof ObjectType) { $safeResult = Utils::printSafe($result); $notObjectType = Utils::printSafe($runtimeType); throw new InvariantViolation("Abstract type {$returnType} must resolve to an Object type at runtime for field {$info->parentType}.{$info->fieldName} with value {$safeResult}, received \"{$notObjectType}\". Either the {$returnType} type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function."); } if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) { throw new InvariantViolation("Runtime Object type \"{$runtimeType}\" is not a possible type for \"{$returnType}\"."); } assert( $this->exeContext->schema->getType($runtimeType->name) !== null, "Schema does not contain type \"{$runtimeType}\". This can happen when an object type is only referenced indirectly through abstract types and never directly through fields.List the type in the option \"types\" during schema construction, see https://webonyx.github.io/graphql-php/schema-definition/#configuration-options." ); assert( $runtimeType === $this->exeContext->schema->getType($runtimeType->name), "Schema must contain unique named types but contains multiple types named \"{$runtimeType}\". Make sure that `resolveType` function of abstract type \"{$returnType}\" returns the same type instance as referenced anywhere else within the schema (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry)." ); return $runtimeType; } /** * @param mixed $contextValue * * @return mixed */ private function maybeScopeContext($contextValue) { if ($contextValue instanceof ScopedContext) { return $contextValue->clone(); } return $contextValue; } } graphql/lib/Executor/ExecutionContext.php 0000644 00000004550 15166657207 0014612 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Type\Schema; /** * Data that must be available at all points during query execution. * * Namely, schema of the type system that is currently executing, * and the fragments defined in the query document. * * @phpstan-import-type FieldResolver from Executor * @phpstan-import-type ArgsMapper from Executor */ class ExecutionContext { public Schema $schema; /** @var array<string, FragmentDefinitionNode> */ public array $fragments; /** @var mixed */ public $rootValue; /** @var mixed */ public $contextValue; public OperationDefinitionNode $operation; /** @var array<string, mixed> */ public array $variableValues; /** * @var callable * * @phpstan-var FieldResolver */ public $fieldResolver; /** * @var callable * * @phpstan-var ArgsMapper */ public $argsMapper; /** @var list<Error> */ public array $errors; public PromiseAdapter $promiseAdapter; /** * @param array<string, FragmentDefinitionNode> $fragments * @param mixed $rootValue * @param mixed $contextValue * @param array<string, mixed> $variableValues * @param list<Error> $errors * * @phpstan-param FieldResolver $fieldResolver */ public function __construct( Schema $schema, array $fragments, $rootValue, $contextValue, OperationDefinitionNode $operation, array $variableValues, array $errors, callable $fieldResolver, callable $argsMapper, PromiseAdapter $promiseAdapter ) { $this->schema = $schema; $this->fragments = $fragments; $this->rootValue = $rootValue; $this->contextValue = $contextValue; $this->operation = $operation; $this->variableValues = $variableValues; $this->errors = $errors; $this->fieldResolver = $fieldResolver; $this->argsMapper = $argsMapper; $this->promiseAdapter = $promiseAdapter; } public function addError(Error $error): void { $this->errors[] = $error; } } graphql/lib/Executor/ExecutionResult.php 0000644 00000012267 15166657207 0014450 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; use YOOtheme\GraphQL\Error\DebugFlag; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\FormattedError; /** * Returned after [query execution](executing-queries.md). * Represents both - result of successful execution and of a failed one * (with errors collected in `errors` prop). * * Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format) * serializable array using `toArray()`. * * @phpstan-type SerializableError array{ * message: string, * locations?: array<int, array{line: int, column: int}>, * path?: array<int, int|string>, * extensions?: array<string, mixed> * } * @phpstan-type SerializableErrors list<SerializableError> * @phpstan-type SerializableResult array{ * data?: array<string, mixed>, * errors?: SerializableErrors, * extensions?: array<string, mixed> * } * @phpstan-type ErrorFormatter callable(\Throwable): SerializableError * @phpstan-type ErrorsHandler callable(list<Error> $errors, ErrorFormatter $formatter): SerializableErrors * * @see \GraphQL\Tests\Executor\ExecutionResultTest */ class ExecutionResult implements \JsonSerializable { /** * Data collected from resolvers during query execution. * * @api * * @var array<string, mixed>|null */ public ?array $data = null; /** * Errors registered during query execution. * * If an error was caused by exception thrown in resolver, $error->getPrevious() would * contain original exception. * * @api * * @var list<Error> */ public array $errors = []; /** * User-defined serializable array of extensions included in serialized result. * * @api * * @var array<string, mixed>|null */ public ?array $extensions = null; /** * @var callable|null * * @phpstan-var ErrorFormatter|null */ private $errorFormatter; /** * @var callable|null * * @phpstan-var ErrorsHandler|null */ private $errorsHandler; /** * @param array<string, mixed>|null $data * @param list<Error> $errors * @param array<string, mixed> $extensions */ public function __construct(?array $data = null, array $errors = [], array $extensions = []) { $this->data = $data; $this->errors = $errors; $this->extensions = $extensions; } /** * Define custom error formatting (must conform to http://facebook.github.io/graphql/#sec-Errors). * * Expected signature is: function (GraphQL\Error\Error $error): array * * Default formatter is "GraphQL\Error\FormattedError::createFromException" * * Expected returned value must be an array: * array( * 'message' => 'errorMessage', * // ... other keys * ); * * @phpstan-param ErrorFormatter|null $errorFormatter * * @api */ public function setErrorFormatter(?callable $errorFormatter): self { $this->errorFormatter = $errorFormatter; return $this; } /** * Define custom logic for error handling (filtering, logging, etc). * * Expected handler signature is: * fn (array $errors, callable $formatter): array * * Default handler is: * fn (array $errors, callable $formatter): array => array_map($formatter, $errors) * * @phpstan-param ErrorsHandler|null $errorsHandler * * @api */ public function setErrorsHandler(?callable $errorsHandler): self { $this->errorsHandler = $errorsHandler; return $this; } /** @phpstan-return SerializableResult */ #[\ReturnTypeWillChange] public function jsonSerialize(): array { return $this->toArray(); } /** * Converts GraphQL query result to spec-compliant serializable array using provided * errors handler and formatter. * * If debug argument is passed, output of error formatter is enriched which debugging information * ("debugMessage", "trace" keys depending on flags). * * $debug argument must sum of flags from @see \GraphQL\Error\DebugFlag * * @phpstan-return SerializableResult * * @api */ public function toArray(int $debug = DebugFlag::NONE): array { $result = []; if ($this->errors !== []) { $errorsHandler = $this->errorsHandler ?? static fn (array $errors, callable $formatter): array => array_map($formatter, $errors); /** @phpstan-var SerializableErrors */ $handledErrors = $errorsHandler( $this->errors, FormattedError::prepareFormatter($this->errorFormatter, $debug) ); // While we know that there were errors initially, they might have been discarded if ($handledErrors !== []) { $result['errors'] = $handledErrors; } } if ($this->data !== null) { $result['data'] = $this->data; } if ($this->extensions !== null && $this->extensions !== []) { $result['extensions'] = $this->extensions; } return $result; } } graphql/lib/Executor/Values.php 0000644 00000031723 15166657210 0012535 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\NullValueNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Language\AST\VariableNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Utils\Value; /** * @see ArgumentNode - force IDE import * * @phpstan-import-type ArgumentNodeValue from ArgumentNode * * @see \GraphQL\Tests\Executor\ValuesTest */ class Values { /** * Prepares an object map of variables of the correct type based on the provided * variable definitions and arbitrary input. If the input cannot be coerced * to match the variable definitions, an Error will be thrown. * * @param NodeList<VariableDefinitionNode> $varDefNodes * @param array<string, mixed> $rawVariableValues * * @throws \Exception * * @return array{array<int, Error>, null}|array{null, array<string, mixed>} */ public static function getVariableValues(Schema $schema, NodeList $varDefNodes, array $rawVariableValues): array { $errors = []; $coercedValues = []; foreach ($varDefNodes as $varDefNode) { $varName = $varDefNode->variable->name->value; $varType = AST::typeFromAST([$schema, 'getType'], $varDefNode->type); if (! Type::isInputType($varType)) { // Must use input types for variables. This should be caught during // validation, however is checked again here for safety. $typeStr = Printer::doPrint($varDefNode->type); $errors[] = new Error( "Variable \"\${$varName}\" expected value of type \"{$typeStr}\" which cannot be used as an input type.", [$varDefNode->type] ); } else { $hasValue = array_key_exists($varName, $rawVariableValues); $value = $hasValue ? $rawVariableValues[$varName] : Utils::undefined(); if (! $hasValue && ($varDefNode->defaultValue !== null)) { // If no value was provided to a variable with a default value, // use the default value. $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType); } elseif ((! $hasValue || $value === null) && ($varType instanceof NonNull)) { // If no value or a nullish value was provided to a variable with a // non-null type (required), produce an error. $safeVarType = Utils::printSafe($varType); $message = $hasValue ? "Variable \"\${$varName}\" of non-null type \"{$safeVarType}\" must not be null." : "Variable \"\${$varName}\" of required type \"{$safeVarType}\" was not provided."; $errors[] = new Error($message, [$varDefNode]); } elseif ($hasValue) { if ($value === null) { // If the explicit value `null` was provided, an entry in the coerced // values must exist as the value `null`. $coercedValues[$varName] = null; } else { // Otherwise, a non-null value was provided, coerce it to the expected // type or report an error if coercion fails. $coerced = Value::coerceInputValue($value, $varType); $coercionErrors = $coerced['errors']; if ($coercionErrors !== null) { foreach ($coercionErrors as $coercionError) { $invalidValue = $coercionError->printInvalidValue(); $inputPath = $coercionError->printInputPath(); $pathMessage = $inputPath !== null ? " at \"{$varName}{$inputPath}\"" : ''; $errors[] = new Error( "Variable \"\${$varName}\" got invalid value {$invalidValue}{$pathMessage}; {$coercionError->getMessage()}", $varDefNode, $coercionError->getSource(), $coercionError->getPositions(), $coercionError->getPath(), $coercionError, $coercionError->getExtensions() ); } } else { $coercedValues[$varName] = $coerced['value']; } } } } } return $errors === [] ? [null, $coercedValues] : [$errors, null]; } /** * Prepares an object map of argument values given a directive definition * and an AST node which may contain directives. Optionally also accepts a map * of variable values. * * If the directive does not exist on the node, returns undefined. * * @param EnumTypeDefinitionNode|EnumTypeExtensionNode|EnumValueDefinitionNode|FieldDefinitionNode|FieldNode|FragmentDefinitionNode|FragmentSpreadNode|InlineFragmentNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode|InputValueDefinitionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|OperationDefinitionNode|ScalarTypeDefinitionNode|ScalarTypeExtensionNode|SchemaExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|VariableDefinitionNode $node * @param array<string, mixed>|null $variableValues * * @throws \Exception * @throws Error * * @return array<string, mixed>|null */ public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null): ?array { $directiveDefName = $directiveDef->name; foreach ($node->directives as $directive) { if ($directive->name->value === $directiveDefName) { return self::getArgumentValues($directiveDef, $directive, $variableValues); } } return null; } /** * Prepares an object map of argument values given a list of argument * definitions and list of argument AST nodes. * * @param FieldDefinition|Directive $def * @param FieldNode|DirectiveNode $node * @param array<string, mixed>|null $variableValues * * @throws \Exception * @throws Error * * @return array<string, mixed> */ public static function getArgumentValues($def, Node $node, ?array $variableValues = null): array { if ($def->args === []) { return []; } /** @var array<string, ArgumentNodeValue> $argumentValueMap */ $argumentValueMap = []; // Might not be defined when an AST from JS is used if (isset($node->arguments)) { foreach ($node->arguments as $argumentNode) { $argumentValueMap[$argumentNode->name->value] = $argumentNode->value; } } return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node); } /** * @param FieldDefinition|Directive $def * @param array<string, ArgumentNodeValue> $argumentValueMap * @param array<string, mixed>|null $variableValues * * @throws \Exception * @throws Error * * @return array<string, mixed> */ public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null): array { /** @var array<string, mixed> $coercedValues */ $coercedValues = []; foreach ($def->args as $argumentDefinition) { $name = $argumentDefinition->name; $argType = $argumentDefinition->getType(); $argumentValueNode = $argumentValueMap[$name] ?? null; if ($argumentValueNode instanceof VariableNode) { $variableName = $argumentValueNode->name->value; $hasValue = $variableValues !== null && array_key_exists($variableName, $variableValues); $isNull = $hasValue && $variableValues[$variableName] === null; } else { $hasValue = $argumentValueNode !== null; $isNull = $argumentValueNode instanceof NullValueNode; } if (! $hasValue && $argumentDefinition->defaultValueExists()) { // If no argument was provided where the definition has a default value, // use the default value. $coercedValues[$name] = $argumentDefinition->defaultValue; } elseif ((! $hasValue || $isNull) && ($argType instanceof NonNull)) { // If no argument or a null value was provided to an argument with a // non-null type (required), produce a field error. $safeArgType = Utils::printSafe($argType); if ($isNull) { throw new Error("Argument \"{$name}\" of non-null type \"{$safeArgType}\" must not be null.", $referenceNode); } if ($argumentValueNode instanceof VariableNode) { throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was provided the variable \"\${$argumentValueNode->name->value}\" which was not provided a runtime value.", [$argumentValueNode]); } throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was not provided.", $referenceNode); } elseif ($hasValue) { assert($argumentValueNode instanceof Node); if ($argumentValueNode instanceof NullValueNode) { // If the explicit value `null` was provided, an entry in the coerced // values must exist as the value `null`. $coercedValues[$name] = null; } elseif ($argumentValueNode instanceof VariableNode) { $variableName = $argumentValueNode->name->value; // Note: This does no further checking that this variable is correct. // This assumes that this query has been validated and the variable // usage here is of the correct type. $coercedValues[$name] = $variableValues[$variableName] ?? null; } else { $coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues); if (Utils::undefined() === $coercedValue) { // Note: ValuesOfCorrectType validation should catch this before // execution. This is a runtime check to ensure execution does not // continue with an invalid argument value. $invalidValue = Printer::doPrint($argumentValueNode); throw new Error("Argument \"{$name}\" has invalid value {$invalidValue}.", [$argumentValueNode]); } $coercedValues[$name] = $coercedValue; } } } return $coercedValues; } } graphql/lib/Executor/Executor.php 0000644 00000015053 15166657210 0013072 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\ResolveInfo; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; /** * Implements the "Evaluating requests" section of the GraphQL specification. * * @phpstan-type ArgsMapper callable(array<string, mixed>, FieldDefinition, FieldNode, mixed): mixed * @phpstan-type FieldResolver callable(mixed, array<string, mixed>, mixed, ResolveInfo): mixed * @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable, callable): ExecutorImplementation * * @see \GraphQL\Tests\Executor\ExecutorTest */ class Executor { /** * @var callable * * @phpstan-var FieldResolver */ private static $defaultFieldResolver = [self::class, 'defaultFieldResolver']; /** * @var callable * * @phpstan-var ArgsMapper */ private static $defaultArgsMapper = [self::class, 'defaultArgsMapper']; private static ?PromiseAdapter $defaultPromiseAdapter; /** * @var callable * * @phpstan-var ImplementationFactory */ private static $implementationFactory = [ReferenceExecutor::class, 'create']; /** @phpstan-return FieldResolver */ public static function getDefaultFieldResolver(): callable { return self::$defaultFieldResolver; } /** * Set a custom default resolve function. * * @phpstan-param FieldResolver $fieldResolver */ public static function setDefaultFieldResolver(callable $fieldResolver): void { self::$defaultFieldResolver = $fieldResolver; } /** @phpstan-return ArgsMapper */ public static function getDefaultArgsMapper(): callable { return self::$defaultArgsMapper; } /** @phpstan-param ArgsMapper $argsMapper */ public static function setDefaultArgsMapper(callable $argsMapper): void { self::$defaultArgsMapper = $argsMapper; } public static function getDefaultPromiseAdapter(): PromiseAdapter { return self::$defaultPromiseAdapter ??= new SyncPromiseAdapter(); } /** Set a custom default promise adapter. */ public static function setDefaultPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null): void { self::$defaultPromiseAdapter = $defaultPromiseAdapter; } /** @phpstan-return ImplementationFactory */ public static function getImplementationFactory(): callable { return self::$implementationFactory; } /** * Set a custom executor implementation factory. * * @phpstan-param ImplementationFactory $implementationFactory */ public static function setImplementationFactory(callable $implementationFactory): void { self::$implementationFactory = $implementationFactory; } /** * Executes DocumentNode against given $schema. * * Always returns ExecutionResult and never throws. * All errors which occur during operation execution are collected in `$result->errors`. * * @param mixed $rootValue * @param mixed $contextValue * @param array<string, mixed>|null $variableValues * * @phpstan-param FieldResolver|null $fieldResolver * * @api * * @throws InvariantViolation */ public static function execute( Schema $schema, DocumentNode $documentNode, $rootValue = null, $contextValue = null, ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null ): ExecutionResult { $promiseAdapter = new SyncPromiseAdapter(); $result = static::promiseToExecute( $promiseAdapter, $schema, $documentNode, $rootValue, $contextValue, $variableValues, $operationName, $fieldResolver ); return $promiseAdapter->wait($result); } /** * Same as execute(), but requires promise adapter and returns a promise which is always * fulfilled with an instance of ExecutionResult and never rejected. * * Useful for async PHP platforms. * * @param mixed $rootValue * @param mixed $contextValue * @param array<string, mixed>|null $variableValues * * @phpstan-param FieldResolver|null $fieldResolver * @phpstan-param ArgsMapper|null $argsMapper * * @api */ public static function promiseToExecute( PromiseAdapter $promiseAdapter, Schema $schema, DocumentNode $documentNode, $rootValue = null, $contextValue = null, ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, ?callable $argsMapper = null ): Promise { $executor = (self::$implementationFactory)( $promiseAdapter, $schema, $documentNode, $rootValue, $contextValue, $variableValues ?? [], $operationName, $fieldResolver ?? self::$defaultFieldResolver, $argsMapper ?? self::$defaultArgsMapper, ); return $executor->doExecute(); } /** * If a resolve function is not given, then a default resolve behavior is used * which takes the property of the root value of the same name as the field * and returns it as the result, or if it's a function, returns the result * of calling that function while passing along args and context. * * @param mixed $objectLikeValue * @param array<string, mixed> $args * @param mixed $contextValue * * @return mixed */ public static function defaultFieldResolver($objectLikeValue, array $args, $contextValue, ResolveInfo $info) { $property = Utils::extractKey($objectLikeValue, $info->fieldName); return $property instanceof \Closure ? $property($objectLikeValue, $args, $contextValue, $info) : $property; } /** * @template T of array<string, mixed> * * @param T $args * * @return T */ public static function defaultArgsMapper(array $args): array { return $args; } } graphql/lib/Executor/Promise/Adapter/ReactPromiseAdapter.php 0000644 00000004600 15166657210 0020164 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor\Promise\Adapter; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use React\Promise\Promise as ReactPromise; use React\Promise\PromiseInterface as ReactPromiseInterface; use function React\Promise\all; use function React\Promise\reject; use function React\Promise\resolve; class ReactPromiseAdapter implements PromiseAdapter { public function isThenable($value): bool { return $value instanceof ReactPromiseInterface; } /** @throws InvariantViolation */ public function convertThenable($thenable): Promise { return new Promise($thenable, $this); } /** @throws InvariantViolation */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $adoptedPromise = $promise->adoptedPromise; assert($adoptedPromise instanceof ReactPromiseInterface); return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this); } /** @throws InvariantViolation */ public function create(callable $resolver): Promise { $promise = new ReactPromise($resolver); return new Promise($promise, $this); } /** @throws InvariantViolation */ public function createFulfilled($value = null): Promise { $promise = resolve($value); return new Promise($promise, $this); } /** @throws InvariantViolation */ public function createRejected(\Throwable $reason): Promise { $promise = reject($reason); return new Promise($promise, $this); } /** @throws InvariantViolation */ public function all(iterable $promisesOrValues): Promise { foreach ($promisesOrValues as &$promiseOrValue) { if ($promiseOrValue instanceof Promise) { $promiseOrValue = $promiseOrValue->adoptedPromise; } } $promisesOrValuesArray = is_array($promisesOrValues) ? $promisesOrValues : iterator_to_array($promisesOrValues); $promise = all($promisesOrValuesArray)->then(static fn ($values): array => array_map( static fn ($key) => $values[$key], array_keys($promisesOrValuesArray), )); return new Promise($promise, $this); } } graphql/lib/Executor/Promise/Adapter/AmpPromiseAdapter.php 0000644 00000010747 15166657210 0017654 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor\Promise\Adapter; use Amp\Deferred; use Amp\Failure; use Amp\Promise as AmpPromise; use Amp\Success; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use function Amp\Promise\all; class AmpPromiseAdapter implements PromiseAdapter { public function isThenable($value): bool { return $value instanceof AmpPromise; } /** @throws InvariantViolation */ public function convertThenable($thenable): Promise { return new Promise($thenable, $this); } /** @throws InvariantViolation */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $deferred = new Deferred(); $onResolve = static function (?\Throwable $reason, $value) use ($onFulfilled, $onRejected, $deferred): void { if ($reason === null && $onFulfilled !== null) { self::resolveWithCallable($deferred, $onFulfilled, $value); } elseif ($reason === null) { $deferred->resolve($value); } elseif ($onRejected !== null) { self::resolveWithCallable($deferred, $onRejected, $reason); } else { $deferred->fail($reason); } }; $adoptedPromise = $promise->adoptedPromise; assert($adoptedPromise instanceof AmpPromise); $adoptedPromise->onResolve($onResolve); return new Promise($deferred->promise(), $this); } /** @throws InvariantViolation */ public function create(callable $resolver): Promise { $deferred = new Deferred(); $resolver( static function ($value) use ($deferred): void { $deferred->resolve($value); }, static function (\Throwable $exception) use ($deferred): void { $deferred->fail($exception); } ); return new Promise($deferred->promise(), $this); } /** * @throws \Error * @throws InvariantViolation */ public function createFulfilled($value = null): Promise { $promise = new Success($value); return new Promise($promise, $this); } /** @throws InvariantViolation */ public function createRejected(\Throwable $reason): Promise { $promise = new Failure($reason); return new Promise($promise, $this); } /** * @throws \Error * @throws InvariantViolation */ public function all(iterable $promisesOrValues): Promise { /** @var array<AmpPromise<mixed>> $promises */ $promises = []; foreach ($promisesOrValues as $key => $item) { if ($item instanceof Promise) { $ampPromise = $item->adoptedPromise; assert($ampPromise instanceof AmpPromise); $promises[$key] = $ampPromise; } elseif ($item instanceof AmpPromise) { $promises[$key] = $item; } } $deferred = new Deferred(); all($promises)->onResolve(static function (?\Throwable $reason, ?array $values) use ($promisesOrValues, $deferred): void { if ($reason === null) { assert(is_array($values), 'Either $reason or $values must be passed'); $promisesOrValuesArray = is_array($promisesOrValues) ? $promisesOrValues : iterator_to_array($promisesOrValues); $resolvedValues = array_replace($promisesOrValuesArray, $values); $deferred->resolve($resolvedValues); return; } $deferred->fail($reason); }); return new Promise($deferred->promise(), $this); } /** * @template TArgument * @template TResult of AmpPromise<mixed> * * @param Deferred<TResult> $deferred * @param callable(TArgument): TResult $callback * @param TArgument $argument */ private static function resolveWithCallable(Deferred $deferred, callable $callback, $argument): void { try { $result = $callback($argument); } catch (\Throwable $exception) { $deferred->fail($exception); return; } if ($result instanceof Promise) { /** @var TResult $result */ $result = $result->adoptedPromise; } $deferred->resolve($result); } } graphql/lib/Executor/Promise/Adapter/SyncPromiseAdapter.php 0000644 00000011265 15166657210 0020047 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor\Promise\Adapter; use YOOtheme\GraphQL\Deferred; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\Utils\Utils; /** * Allows changing order of field resolution even in sync environments * (by leveraging queue of deferreds and promises). */ class SyncPromiseAdapter implements PromiseAdapter { public function isThenable($value): bool { return $value instanceof SyncPromise; } /** @throws InvariantViolation */ public function convertThenable($thenable): Promise { if (! $thenable instanceof SyncPromise) { // End-users should always use Deferred (and don't use SyncPromise directly) $deferred = Deferred::class; $safeThenable = Utils::printSafe($thenable); throw new InvariantViolation("Expected instance of {$deferred}, got {$safeThenable}"); } return new Promise($thenable, $this); } /** @throws InvariantViolation */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise { $adoptedPromise = $promise->adoptedPromise; assert($adoptedPromise instanceof SyncPromise); return new Promise($adoptedPromise->then($onFulfilled, $onRejected), $this); } /** * @throws \Exception * @throws InvariantViolation */ public function create(callable $resolver): Promise { $promise = new SyncPromise(); try { $resolver( [$promise, 'resolve'], [$promise, 'reject'] ); } catch (\Throwable $e) { $promise->reject($e); } return new Promise($promise, $this); } /** * @throws \Exception * @throws InvariantViolation */ public function createFulfilled($value = null): Promise { $promise = new SyncPromise(); return new Promise($promise->resolve($value), $this); } /** * @throws \Exception * @throws InvariantViolation */ public function createRejected(\Throwable $reason): Promise { $promise = new SyncPromise(); return new Promise($promise->reject($reason), $this); } /** * @throws \Exception * @throws InvariantViolation */ public function all(iterable $promisesOrValues): Promise { $all = new SyncPromise(); $total = is_array($promisesOrValues) ? count($promisesOrValues) : iterator_count($promisesOrValues); $count = 0; $result = []; $resolveAllWhenFinished = function () use (&$count, &$total, $all, &$result): void { if ($count === $total) { $all->resolve($result); } }; foreach ($promisesOrValues as $index => $promiseOrValue) { if ($promiseOrValue instanceof Promise) { $result[$index] = null; $promiseOrValue->then( static function ($value) use (&$result, $index, &$count, &$resolveAllWhenFinished): void { $result[$index] = $value; ++$count; $resolveAllWhenFinished(); }, [$all, 'reject'] ); } else { $result[$index] = $promiseOrValue; ++$count; } } $resolveAllWhenFinished(); return new Promise($all, $this); } /** * Synchronously wait when promise completes. * * @throws InvariantViolation * * @return mixed */ public function wait(Promise $promise) { $this->beforeWait($promise); $taskQueue = SyncPromise::getQueue(); $syncPromise = $promise->adoptedPromise; assert($syncPromise instanceof SyncPromise); while ( $syncPromise->state === SyncPromise::PENDING && ! $taskQueue->isEmpty() ) { SyncPromise::runQueue(); $this->onWait($promise); } if ($syncPromise->state === SyncPromise::FULFILLED) { return $syncPromise->result; } if ($syncPromise->state === SyncPromise::REJECTED) { throw $syncPromise->result; } throw new InvariantViolation('Could not resolve promise'); } /** Execute just before starting to run promise completion. */ protected function beforeWait(Promise $promise): void {} /** Execute while running promise completion. */ protected function onWait(Promise $promise): void {} } graphql/lib/Executor/Promise/Adapter/SyncPromise.php 0000644 00000014315 15166657210 0016545 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor\Promise\Adapter; use YOOtheme\GraphQL\Error\InvariantViolation; /** * Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode * (using queue to defer promises execution). * * Library users are not supposed to use SyncPromise class in their resolvers. * Instead, they should use @see \GraphQL\Deferred which enforces `$executor` callback in the constructor. * * Root SyncPromise without explicit `$executor` will never resolve (actually throw while trying). * The whole point of Deferred is to ensure it never happens and that any resolver creates * at least one $executor to start the promise chain. * * @phpstan-type Executor callable(): mixed */ class SyncPromise { public const PENDING = 'pending'; public const FULFILLED = 'fulfilled'; public const REJECTED = 'rejected'; public string $state = self::PENDING; /** @var mixed */ public $result; /** * Promises created in `then` method of this promise and awaiting resolution of this promise. * * @var array< * int, * array{ * self, * (callable(mixed): mixed)|null, * (callable(\Throwable): mixed)|null * } * > */ protected array $waiting = []; public static function runQueue(): void { $q = self::getQueue(); while (! $q->isEmpty()) { $task = $q->dequeue(); $task(); } } /** @param Executor|null $executor */ public function __construct(?callable $executor = null) { if ($executor === null) { return; } self::getQueue()->enqueue(function () use ($executor): void { try { $this->resolve($executor()); } catch (\Throwable $e) { $this->reject($e); } }); } /** * @param mixed $value * * @throws \Exception */ public function resolve($value): self { switch ($this->state) { case self::PENDING: if ($value === $this) { throw new \Exception('Cannot resolve promise with self'); } if (is_object($value) && method_exists($value, 'then')) { $value->then( function ($resolvedValue): void { $this->resolve($resolvedValue); }, function ($reason): void { $this->reject($reason); } ); return $this; } $this->state = self::FULFILLED; $this->result = $value; $this->enqueueWaitingPromises(); break; case self::FULFILLED: if ($this->result !== $value) { throw new \Exception('Cannot change value of fulfilled promise'); } break; case self::REJECTED: throw new \Exception('Cannot resolve rejected promise'); } return $this; } /** * @throws \Exception * * @return $this */ public function reject(\Throwable $reason): self { switch ($this->state) { case self::PENDING: $this->state = self::REJECTED; $this->result = $reason; $this->enqueueWaitingPromises(); break; case self::REJECTED: if ($reason !== $this->result) { throw new \Exception('Cannot change rejection reason'); } break; case self::FULFILLED: throw new \Exception('Cannot reject fulfilled promise'); } return $this; } /** @throws InvariantViolation */ private function enqueueWaitingPromises(): void { if ($this->state === self::PENDING) { throw new InvariantViolation('Cannot enqueue derived promises when parent is still pending'); } foreach ($this->waiting as $descriptor) { self::getQueue()->enqueue(function () use ($descriptor): void { [$promise, $onFulfilled, $onRejected] = $descriptor; if ($this->state === self::FULFILLED) { try { $promise->resolve($onFulfilled === null ? $this->result : $onFulfilled($this->result)); } catch (\Throwable $e) { $promise->reject($e); } } elseif ($this->state === self::REJECTED) { try { if ($onRejected === null) { $promise->reject($this->result); } else { $promise->resolve($onRejected($this->result)); } } catch (\Throwable $e) { $promise->reject($e); } } }); } $this->waiting = []; } /** @return \SplQueue<callable(): void> */ public static function getQueue(): \SplQueue { static $queue; return $queue ??= new \SplQueue(); } /** * @param (callable(mixed): mixed)|null $onFulfilled * @param (callable(\Throwable): mixed)|null $onRejected * * @throws InvariantViolation */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self { if ($this->state === self::REJECTED && $onRejected === null) { return $this; } if ($this->state === self::FULFILLED && $onFulfilled === null) { return $this; } $tmp = new self(); $this->waiting[] = [$tmp, $onFulfilled, $onRejected]; if ($this->state !== self::PENDING) { $this->enqueueWaitingPromises(); } return $tmp; } /** * @param callable(\Throwable): mixed $onRejected * * @throws InvariantViolation */ public function catch(callable $onRejected): self { return $this->then(null, $onRejected); } } graphql/lib/Executor/Promise/PromiseAdapter.php 0000644 00000003727 15166657210 0015636 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor\Promise; /** * Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)). */ interface PromiseAdapter { /** * Is the value a promise or a deferred of the underlying platform? * * @param mixed $value * * @api */ public function isThenable($value): bool; /** * Converts thenable of the underlying platform into GraphQL\Executor\Promise\Promise instance. * * @param mixed $thenable * * @api */ public function convertThenable($thenable): Promise; /** * Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described * in Promises/A+ specs. Then returns new wrapped instance of GraphQL\Executor\Promise\Promise. * * @api */ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise; /** * Creates a Promise from the given resolver callable. * * @param callable(callable $resolve, callable $reject): void $resolver * * @api */ public function create(callable $resolver): Promise; /** * Creates a fulfilled Promise for a value if the value is not a promise. * * @param mixed $value * * @api */ public function createFulfilled($value = null): Promise; /** * Creates a rejected promise for a reason if the reason is not a promise. * * If the provided reason is a promise, then it is returned as-is. * * @api */ public function createRejected(\Throwable $reason): Promise; /** * Given an iterable of promises (or values), returns a promise that is fulfilled when all the * items in the iterable are fulfilled. * * @param iterable<Promise|mixed> $promisesOrValues * * @api */ public function all(iterable $promisesOrValues): Promise; } graphql/lib/Executor/Promise/Promise.php 0000644 00000002112 15166657210 0014320 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Executor\Promise; use Amp\Promise as AmpPromise; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Promise\Adapter\SyncPromise; use React\Promise\PromiseInterface as ReactPromise; /** * Convenience wrapper for promises represented by Promise Adapter. */ class Promise { /** @var SyncPromise|ReactPromise<mixed>|AmpPromise<mixed> */ public $adoptedPromise; private PromiseAdapter $adapter; /** * @param mixed $adoptedPromise * * @throws InvariantViolation */ public function __construct($adoptedPromise, PromiseAdapter $adapter) { if ($adoptedPromise instanceof self) { throw new InvariantViolation('Expecting promise from adapted system, got ' . self::class); } $this->adoptedPromise = $adoptedPromise; $this->adapter = $adapter; } public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise { return $this->adapter->then($this, $onFulfilled, $onRejected); } } graphql/lib/Server/RequestError.php 0000644 00000000370 15166657210 0013402 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server; use YOOtheme\GraphQL\Error\ClientAware; class RequestError extends \Exception implements ClientAware { public function isClientSafe(): bool { return true; } } graphql/lib/Server/OperationParams.php 0000644 00000006732 15166657210 0014054 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server; /** * Structure representing parsed HTTP parameters for GraphQL operation. * * The properties in this class are not strictly typed, as this class * is only meant to serve as an intermediary representation which is * not yet validated. */ class OperationParams { /** * Id of the query (when using persisted queries). * * Valid aliases (case-insensitive): * - id * - queryId * - documentId * * @api * * @var mixed should be string|null */ public $queryId; /** * A document containing GraphQL operations and fragments to execute. * * @api * * @var mixed should be string|null */ public $query; /** * The name of the operation in the document to execute. * * @api * * @var mixed should be string|null */ public $operation; /** * Values for any variables defined by the operation. * * @api * * @var mixed should be array<string, mixed> */ public $variables; /** * Reserved for implementors to extend the protocol however they see fit. * * @api * * @var mixed should be array<string, mixed> */ public $extensions; /** * Executed in read-only context (e.g. via HTTP GET request)? * * @api */ public bool $readOnly; /** * The raw params used to construct this instance. * * @api * * @var array<string, mixed> */ public array $originalInput; /** * Creates an instance from given array. * * @param array<string, mixed> $params * * @api */ public static function create(array $params, bool $readonly = false): OperationParams { $instance = new static(); $params = array_change_key_case($params, \CASE_LOWER); $instance->originalInput = $params; $params += [ 'query' => null, 'queryid' => null, 'documentid' => null, // alias to queryid 'id' => null, // alias to queryid 'operationname' => null, 'variables' => null, 'extensions' => null, ]; foreach ($params as &$value) { if ($value === '') { $value = null; } } $instance->query = $params['query']; $instance->queryId = $params['queryid'] ?? $params['documentid'] ?? $params['id']; $instance->operation = $params['operationname']; $instance->variables = static::decodeIfJSON($params['variables']); $instance->extensions = static::decodeIfJSON($params['extensions']); $instance->readOnly = $readonly; // Apollo server/client compatibility if ( isset($instance->extensions['persistedQuery']['sha256Hash']) && $instance->queryId === null ) { $instance->queryId = $instance->extensions['persistedQuery']['sha256Hash']; } return $instance; } /** * Decodes the value if it is JSON, otherwise returns it unchanged. * * @param mixed $value * * @return mixed */ protected static function decodeIfJSON($value) { if (! is_string($value)) { return $value; } $decoded = json_decode($value, true); if (json_last_error() === \JSON_ERROR_NONE) { return $decoded; } return $value; } } graphql/lib/Server/Exception/InvalidQueryParameter.php 0000644 00000000255 15166657210 0017155 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class InvalidQueryParameter extends RequestError {} graphql/lib/Server/Exception/CannotReadBody.php 0000644 00000000246 15166657210 0015534 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class CannotReadBody extends RequestError {} graphql/lib/Server/Exception/UnexpectedContentType.php 0000644 00000000255 15166657210 0017201 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class UnexpectedContentType extends RequestError {} graphql/lib/Server/Exception/CannotParseJsonBody.php 0000644 00000000253 15166657210 0016563 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class CannotParseJsonBody extends RequestError {} graphql/lib/Server/Exception/InvalidOperationParameter.php 0000644 00000000261 15166657210 0020005 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class InvalidOperationParameter extends RequestError {} graphql/lib/Server/Exception/CannotParseVariables.php 0000644 00000000254 15166657210 0016745 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class CannotParseVariables extends RequestError {} graphql/lib/Server/Exception/FailedToDetermineOperationType.php 0000644 00000000266 15166657210 0020751 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class FailedToDetermineOperationType extends RequestError {} graphql/lib/Server/Exception/MissingContentTypeHeader.php 0000644 00000000260 15166657210 0017613 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class MissingContentTypeHeader extends RequestError {} graphql/lib/Server/Exception/MissingQueryOrQueryIdParameter.php 0000644 00000000266 15166657210 0021006 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class MissingQueryOrQueryIdParameter extends RequestError {} graphql/lib/Server/Exception/PersistedQueriesAreNotSupported.php 0000644 00000000267 15166657210 0021222 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class PersistedQueriesAreNotSupported extends RequestError {} graphql/lib/Server/Exception/HttpMethodNotSupported.php 0000644 00000000256 15166657210 0017350 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class HttpMethodNotSupported extends RequestError {} graphql/lib/Server/Exception/BatchedQueriesAreNotSupported.php 0000644 00000000265 15166657210 0020610 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class BatchedQueriesAreNotSupported extends RequestError {} graphql/lib/Server/Exception/GetMethodSupportsOnlyQueryOperation.php 0000644 00000000273 15166657210 0022111 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class GetMethodSupportsOnlyQueryOperation extends RequestError {} graphql/lib/Server/Exception/InvalidQueryIdParameter.php 0000644 00000000257 15166657210 0017434 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server\Exception; use YOOtheme\GraphQL\Server\RequestError; class InvalidQueryIdParameter extends RequestError {} graphql/lib/Server/Helper.php 0000644 00000046074 15166657210 0012172 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\FormattedError; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\ExecutionResult; use YOOtheme\GraphQL\Executor\Executor; use YOOtheme\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\GraphQL; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\Parser; use YOOtheme\GraphQL\Server\Exception\BatchedQueriesAreNotSupported; use YOOtheme\GraphQL\Server\Exception\CannotParseJsonBody; use YOOtheme\GraphQL\Server\Exception\CannotParseVariables; use YOOtheme\GraphQL\Server\Exception\CannotReadBody; use YOOtheme\GraphQL\Server\Exception\FailedToDetermineOperationType; use YOOtheme\GraphQL\Server\Exception\GetMethodSupportsOnlyQueryOperation; use YOOtheme\GraphQL\Server\Exception\HttpMethodNotSupported; use YOOtheme\GraphQL\Server\Exception\InvalidOperationParameter; use YOOtheme\GraphQL\Server\Exception\InvalidQueryIdParameter; use YOOtheme\GraphQL\Server\Exception\InvalidQueryParameter; use YOOtheme\GraphQL\Server\Exception\MissingContentTypeHeader; use YOOtheme\GraphQL\Server\Exception\MissingQueryOrQueryIdParameter; use YOOtheme\GraphQL\Server\Exception\PersistedQueriesAreNotSupported; use YOOtheme\GraphQL\Server\Exception\UnexpectedContentType; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; /** * Contains functionality that could be re-used by various server implementations. * * @see \GraphQL\Tests\Server\HelperTest */ class Helper { /** * Parses HTTP request using PHP globals and returns GraphQL OperationParams * contained in this request. For batched requests it returns an array of OperationParams. * * This function does not check validity of these params * (validation is performed separately in validateOperationParams() method). * * If $readRawBodyFn argument is not provided - will attempt to read raw request body * from `php://input` stream. * * Internally it normalizes input to $method, $bodyParams and $queryParams and * calls `parseRequestParams()` to produce actual return value. * * For PSR-7 request parsing use `parsePsrRequest()` instead. * * @throws RequestError * * @return OperationParams|array<int, OperationParams> * * @api */ public function parseHttpRequest(?callable $readRawBodyFn = null) { $method = $_SERVER['REQUEST_METHOD'] ?? null; $bodyParams = []; $urlParams = $_GET; if ($method === 'POST') { $contentType = $_SERVER['CONTENT_TYPE'] ?? null; if ($contentType === null) { throw new MissingContentTypeHeader('Missing "Content-Type" header'); } if (stripos($contentType, 'application/graphql') !== false) { $rawBody = $readRawBodyFn === null ? $this->readRawBody() : $readRawBodyFn(); $bodyParams = ['query' => $rawBody]; } elseif (stripos($contentType, 'application/json') !== false) { $rawBody = $readRawBodyFn === null ? $this->readRawBody() : $readRawBodyFn(); $bodyParams = $this->decodeJson($rawBody); $this->assertJsonObjectOrArray($bodyParams); } elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) { $bodyParams = $_POST; } elseif (stripos($contentType, 'multipart/form-data') !== false) { $bodyParams = $_POST; } else { throw new UnexpectedContentType('Unexpected content type: ' . Utils::printSafeJson($contentType)); } } return $this->parseRequestParams($method, $bodyParams, $urlParams); } /** * Parses normalized request params and returns instance of OperationParams * or array of OperationParams in case of batch operation. * * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array) * * @param array<mixed> $bodyParams * @param array<mixed> $queryParams * * @throws RequestError * * @return OperationParams|array<int, OperationParams> * * @api */ public function parseRequestParams(string $method, array $bodyParams, array $queryParams) { if ($method === 'GET') { return OperationParams::create($queryParams, true); } if ($method === 'POST') { if (isset($bodyParams[0])) { $operations = []; foreach ($bodyParams as $entry) { $operations[] = OperationParams::create($entry); } return $operations; } return OperationParams::create($bodyParams); } throw new HttpMethodNotSupported("HTTP Method \"{$method}\" is not supported"); } /** * Checks validity of OperationParams extracted from HTTP request and returns an array of errors * if params are invalid (or empty array when params are valid). * * @return list<RequestError> * * @api */ public function validateOperationParams(OperationParams $params): array { $errors = []; $query = $params->query ?? ''; $queryId = $params->queryId ?? ''; if ($query === '' && $queryId === '') { $errors[] = new MissingQueryOrQueryIdParameter('GraphQL Request must include at least one of those two parameters: "query" or "queryId"'); } if (! is_string($query)) { $errors[] = new InvalidQueryParameter( 'GraphQL Request parameter "query" must be string, but got ' . Utils::printSafeJson($params->query) ); } if (! is_string($queryId)) { $errors[] = new InvalidQueryIdParameter( 'GraphQL Request parameter "queryId" must be string, but got ' . Utils::printSafeJson($params->queryId) ); } if ($params->operation !== null && ! is_string($params->operation)) { $errors[] = new InvalidOperationParameter( 'GraphQL Request parameter "operation" must be string, but got ' . Utils::printSafeJson($params->operation) ); } if ($params->variables !== null && (! is_array($params->variables) || isset($params->variables[0]))) { $errors[] = new CannotParseVariables( 'GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got ' . Utils::printSafeJson($params->originalInput['variables']) ); } return $errors; } /** * Executes GraphQL operation with given server configuration and returns execution result * (or promise when promise adapter is different from SyncPromiseAdapter). * * @throws \Exception * @throws InvariantViolation * * @return ExecutionResult|Promise * * @api */ public function executeOperation(ServerConfig $config, OperationParams $op) { $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getDefaultPromiseAdapter(); $result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op); if ($promiseAdapter instanceof SyncPromiseAdapter) { $result = $promiseAdapter->wait($result); } return $result; } /** * Executes batched GraphQL operations with shared promise queue * (thus, effectively batching deferreds|promises of all queries at once). * * @param array<OperationParams> $operations * * @throws \Exception * @throws InvariantViolation * * @return array<int, ExecutionResult>|Promise * * @api */ public function executeBatch(ServerConfig $config, array $operations) { $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getDefaultPromiseAdapter(); $result = []; foreach ($operations as $operation) { $result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation, true); } $result = $promiseAdapter->all($result); // Wait for promised results when using sync promises if ($promiseAdapter instanceof SyncPromiseAdapter) { $result = $promiseAdapter->wait($result); } return $result; } /** * @throws \Exception * @throws InvariantViolation */ protected function promiseToExecuteOperation( PromiseAdapter $promiseAdapter, ServerConfig $config, OperationParams $op, bool $isBatch = false ): Promise { try { if ($config->getSchema() === null) { throw new InvariantViolation('Schema is required for the server'); } if ($isBatch && ! $config->getQueryBatching()) { throw new BatchedQueriesAreNotSupported('Batched queries are not supported by this server'); } $errors = $this->validateOperationParams($op); if ($errors !== []) { $locatedErrors = array_map( [Error::class, 'createLocatedError'], $errors ); return $promiseAdapter->createFulfilled( new ExecutionResult(null, $locatedErrors) ); } $doc = $op->queryId !== null ? $this->loadPersistedQuery($config, $op) : $op->query; if (! $doc instanceof DocumentNode) { $doc = Parser::parse($doc); } $operationAST = AST::getOperationAST($doc, $op->operation); if ($operationAST === null) { throw new FailedToDetermineOperationType('Failed to determine operation type'); } $operationType = $operationAST->operation; if ($operationType !== 'query' && $op->readOnly) { throw new GetMethodSupportsOnlyQueryOperation('GET supports only query operation'); } $result = GraphQL::promiseToExecute( $promiseAdapter, $config->getSchema(), $doc, $this->resolveRootValue($config, $op, $doc, $operationType), $this->resolveContextValue($config, $op, $doc, $operationType), $op->variables, $op->operation, $config->getFieldResolver(), $this->resolveValidationRules($config, $op, $doc, $operationType) ); } catch (RequestError $e) { $result = $promiseAdapter->createFulfilled( new ExecutionResult(null, [Error::createLocatedError($e)]) ); } catch (Error $e) { $result = $promiseAdapter->createFulfilled( new ExecutionResult(null, [$e]) ); } $applyErrorHandling = static function (ExecutionResult $result) use ($config): ExecutionResult { $result->setErrorsHandler($config->getErrorsHandler()); $result->setErrorFormatter( FormattedError::prepareFormatter( $config->getErrorFormatter(), $config->getDebugFlag() ) ); return $result; }; return $result->then($applyErrorHandling); } /** * @throws RequestError * * @return mixed */ protected function loadPersistedQuery(ServerConfig $config, OperationParams $operationParams) { $loader = $config->getPersistedQueryLoader(); if ($loader === null) { throw new PersistedQueriesAreNotSupported('Persisted queries are not supported by this server'); } $source = $loader($operationParams->queryId, $operationParams); // @phpstan-ignore-next-line Necessary until PHP gains function types if (! is_string($source) && ! $source instanceof DocumentNode) { $documentNode = DocumentNode::class; $safeSource = Utils::printSafe($source); throw new InvariantViolation("Persisted query loader must return query string or instance of {$documentNode} but got: {$safeSource}"); } return $source; } /** @return array<mixed>|null */ protected function resolveValidationRules( ServerConfig $config, OperationParams $params, DocumentNode $doc, string $operationType ): ?array { $validationRules = $config->getValidationRules(); if (is_callable($validationRules)) { $validationRules = $validationRules($params, $doc, $operationType); } // @phpstan-ignore-next-line unless PHP gains function types, we have to check this at runtime if ($validationRules !== null && ! is_array($validationRules)) { $safeValidationRules = Utils::printSafe($validationRules); throw new InvariantViolation("Expecting validation rules to be array or callable returning array, but got: {$safeValidationRules}"); } return $validationRules; } /** @return mixed */ protected function resolveRootValue( ServerConfig $config, OperationParams $params, DocumentNode $doc, string $operationType ) { $rootValue = $config->getRootValue(); if (is_callable($rootValue)) { $rootValue = $rootValue($params, $doc, $operationType); } return $rootValue; } /** @return mixed user defined */ protected function resolveContextValue( ServerConfig $config, OperationParams $params, DocumentNode $doc, string $operationType ) { $context = $config->getContext(); if (is_callable($context)) { $context = $context($params, $doc, $operationType); } return $context; } /** * Send response using standard PHP `header()` and `echo`. * * @param Promise|ExecutionResult|array<ExecutionResult> $result * * @api * * @throws \JsonException */ public function sendResponse($result): void { if ($result instanceof Promise) { $result->then(function ($actualResult): void { $this->emitResponse($actualResult); }); } else { $this->emitResponse($result); } } /** * @param array<mixed>|\JsonSerializable $jsonSerializable * * @throws \JsonException */ protected function emitResponse($jsonSerializable): void { header('Content-Type: application/json;charset=utf-8'); echo json_encode($jsonSerializable, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); } /** @throws RequestError */ protected function readRawBody(): string { $body = file_get_contents('php://input'); if ($body === false) { throw new CannotReadBody('Cannot not read body.'); } return $body; } /** * Converts PSR-7 request to OperationParams or an array thereof. * * @throws RequestError * * @return OperationParams|array<OperationParams> * * @api */ public function parsePsrRequest(RequestInterface $request) { if ($request->getMethod() === 'GET') { $bodyParams = []; } else { $contentType = $request->getHeader('content-type'); if (! isset($contentType[0])) { throw new MissingContentTypeHeader('Missing "Content-Type" header'); } if (stripos($contentType[0], 'application/graphql') !== false) { $bodyParams = ['query' => (string) $request->getBody()]; } elseif (stripos($contentType[0], 'application/json') !== false) { $bodyParams = $request instanceof ServerRequestInterface ? $request->getParsedBody() : $this->decodeJson((string) $request->getBody()); $this->assertJsonObjectOrArray($bodyParams); } else { if ($request instanceof ServerRequestInterface) { $bodyParams = $request->getParsedBody(); } $bodyParams ??= $this->decodeContent((string) $request->getBody()); } } parse_str(html_entity_decode($request->getUri()->getQuery()), $queryParams); return $this->parseRequestParams( $request->getMethod(), $bodyParams, $queryParams ); } /** * @throws RequestError * * @return mixed */ protected function decodeJson(string $rawBody) { $bodyParams = json_decode($rawBody, true); if (json_last_error() !== \JSON_ERROR_NONE) { throw new CannotParseJsonBody('Expected JSON object or array for "application/json" request, but failed to parse because: ' . json_last_error_msg()); } return $bodyParams; } /** @return array<mixed> */ protected function decodeContent(string $rawBody): array { parse_str($rawBody, $bodyParams); return $bodyParams; } /** * @param mixed $bodyParams * * @throws RequestError */ protected function assertJsonObjectOrArray($bodyParams): void { if (! is_array($bodyParams)) { $notArray = Utils::printSafeJson($bodyParams); throw new CannotParseJsonBody("Expected JSON object or array for \"application/json\" request, got: {$notArray}"); } } /** * Converts query execution result to PSR-7 response. * * @param Promise|ExecutionResult|array<ExecutionResult> $result * * @throws \InvalidArgumentException * @throws \JsonException * @throws \RuntimeException * * @return Promise|ResponseInterface * * @api */ public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream) { if ($result instanceof Promise) { return $result->then( fn ($actualResult): ResponseInterface => $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream) ); } return $this->doConvertToPsrResponse($result, $response, $writableBodyStream); } /** * @param ExecutionResult|array<ExecutionResult> $result * * @throws \InvalidArgumentException * @throws \JsonException * @throws \RuntimeException */ protected function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream): ResponseInterface { $writableBodyStream->write(json_encode($result, JSON_THROW_ON_ERROR)); return $response ->withHeader('Content-Type', 'application/json') ->withBody($writableBodyStream); } } graphql/lib/Server/StandardServer.php 0000644 00000012103 15166657210 0013664 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\ExecutionResult; use YOOtheme\GraphQL\Executor\Promise\Promise; use YOOtheme\GraphQL\Utils\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; /** * GraphQL server compatible with both: [express-graphql](https://github.com/graphql/express-graphql) * and [Apollo Server](https://github.com/apollographql/graphql-server). * Usage Example:. * * $server = new StandardServer([ * 'schema' => $mySchema * ]); * $server->handleRequest(); * * Or using [ServerConfig](class-reference.md#graphqlserverserverconfig) instance: * * $config = GraphQL\Server\ServerConfig::create() * ->setSchema($mySchema) * ->setContext($myContext); * * $server = new GraphQL\Server\StandardServer($config); * $server->handleRequest(); * * See [dedicated section in docs](executing-queries.md#using-server) for details. * * @see \GraphQL\Tests\Server\StandardServerTest */ class StandardServer { protected ServerConfig $config; protected Helper $helper; /** * @param ServerConfig|array<string, mixed> $config * * @api * * @throws InvariantViolation */ public function __construct($config) { if (is_array($config)) { $config = ServerConfig::create($config); } // @phpstan-ignore-next-line necessary until we can use proper union types if (! $config instanceof ServerConfig) { $safeConfig = Utils::printSafe($config); throw new InvariantViolation("Expecting valid server config, but got {$safeConfig}"); } $this->config = $config; $this->helper = new Helper(); } /** * Parses HTTP request, executes and emits response (using standard PHP `header` function and `echo`). * * When $parsedBody is not set, it uses PHP globals to parse a request. * It is possible to implement request parsing elsewhere (e.g. using framework Request instance) * and then pass it to the server. * * See `executeRequest()` if you prefer to emit the response yourself * (e.g. using the Response object of some framework). * * @param OperationParams|array<OperationParams> $parsedBody * * @api * * @throws \Exception * @throws InvariantViolation * @throws RequestError */ public function handleRequest($parsedBody = null): void { $result = $this->executeRequest($parsedBody); $this->helper->sendResponse($result); } /** * Executes a GraphQL operation and returns an execution result * (or promise when promise adapter is different from SyncPromiseAdapter). * * When $parsedBody is not set, it uses PHP globals to parse a request. * It is possible to implement request parsing elsewhere (e.g. using framework Request instance) * and then pass it to the server. * * PSR-7 compatible method executePsrRequest() does exactly this. * * @param OperationParams|array<OperationParams> $parsedBody * * @throws \Exception * @throws InvariantViolation * @throws RequestError * * @return ExecutionResult|array<int, ExecutionResult>|Promise * * @api */ public function executeRequest($parsedBody = null) { if ($parsedBody === null) { $parsedBody = $this->helper->parseHttpRequest(); } if (is_array($parsedBody)) { return $this->helper->executeBatch($this->config, $parsedBody); } return $this->helper->executeOperation($this->config, $parsedBody); } /** * Executes PSR-7 request and fulfills PSR-7 response. * * See `executePsrRequest()` if you prefer to create response yourself * (e.g. using specific JsonResponse instance of some framework). * * @throws \Exception * @throws \InvalidArgumentException * @throws \JsonException * @throws \RuntimeException * @throws InvariantViolation * @throws RequestError * * @return ResponseInterface|Promise * * @api */ public function processPsrRequest( RequestInterface $request, ResponseInterface $response, StreamInterface $writableBodyStream ) { $result = $this->executePsrRequest($request); return $this->helper->toPsrResponse($result, $response, $writableBodyStream); } /** * Executes GraphQL operation and returns execution result * (or promise when promise adapter is different from SyncPromiseAdapter). * * @throws \Exception * @throws \JsonException * @throws InvariantViolation * @throws RequestError * * @return ExecutionResult|array<int, ExecutionResult>|Promise * * @api */ public function executePsrRequest(RequestInterface $request) { $parsedBody = $this->helper->parsePsrRequest($request); return $this->executeRequest($parsedBody); } } graphql/lib/Server/ServerConfig.php 0000644 00000021234 15166657210 0013336 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Server; use YOOtheme\GraphQL\Error\DebugFlag; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\ExecutionResult; use YOOtheme\GraphQL\Executor\Promise\PromiseAdapter; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; use YOOtheme\GraphQL\Validator\Rules\ValidationRule; /** * Server configuration class. * Could be passed directly to server constructor. List of options accepted by **create** method is * [described in docs](executing-queries.md#server-configuration-options). * * Usage example: * * $config = GraphQL\Server\ServerConfig::create() * ->setSchema($mySchema) * ->setContext($myContext); * * $server = new GraphQL\Server\StandardServer($config); * * @see ExecutionResult * * @phpstan-type PersistedQueryLoader callable(string $queryId, OperationParams $operation): (string|DocumentNode) * @phpstan-type RootValueResolver callable(OperationParams $operation, DocumentNode $doc, string $operationType): mixed * @phpstan-type ValidationRulesOption array<ValidationRule>|null|callable(OperationParams $operation, DocumentNode $doc, string $operationType): array<ValidationRule> * * @phpstan-import-type ErrorsHandler from ExecutionResult * @phpstan-import-type ErrorFormatter from ExecutionResult * * @see \GraphQL\Tests\Server\ServerConfigTest */ class ServerConfig { /** * Converts an array of options to instance of ServerConfig * (or just returns empty config when array is not passed). * * @param array<string, mixed> $config * * @api * * @throws InvariantViolation */ public static function create(array $config = []): self { $instance = new static(); foreach ($config as $key => $value) { switch ($key) { case 'schema': $instance->setSchema($value); break; case 'rootValue': $instance->setRootValue($value); break; case 'context': $instance->setContext($value); break; case 'fieldResolver': $instance->setFieldResolver($value); break; case 'validationRules': $instance->setValidationRules($value); break; case 'queryBatching': $instance->setQueryBatching($value); break; case 'debugFlag': $instance->setDebugFlag($value); break; case 'persistedQueryLoader': $instance->setPersistedQueryLoader($value); break; case 'errorFormatter': $instance->setErrorFormatter($value); break; case 'errorsHandler': $instance->setErrorsHandler($value); break; case 'promiseAdapter': $instance->setPromiseAdapter($value); break; default: throw new InvariantViolation("Unknown server config option: {$key}"); } } return $instance; } private ?Schema $schema = null; /** @var mixed|callable(self, OperationParams, DocumentNode): mixed|null */ private $context; /** * @var mixed|callable * * @phpstan-var mixed|RootValueResolver */ private $rootValue; /** * @var callable|null * * @phpstan-var ErrorFormatter|null */ private $errorFormatter; /** * @var callable|null * * @phpstan-var ErrorsHandler|null */ private $errorsHandler; private int $debugFlag = DebugFlag::NONE; private bool $queryBatching = false; /** * @var array<ValidationRule>|callable|null * * @phpstan-var ValidationRulesOption */ private $validationRules; /** @var callable|null */ private $fieldResolver; private ?PromiseAdapter $promiseAdapter = null; /** * @var callable|null * * @phpstan-var PersistedQueryLoader|null */ private $persistedQueryLoader; /** @api */ public function setSchema(Schema $schema): self { $this->schema = $schema; return $this; } /** * @param mixed|callable $context * * @api */ public function setContext($context): self { $this->context = $context; return $this; } /** * @param mixed|callable $rootValue * * @phpstan-param mixed|RootValueResolver $rootValue * * @api */ public function setRootValue($rootValue): self { $this->rootValue = $rootValue; return $this; } /** * @phpstan-param ErrorFormatter $errorFormatter * * @api */ public function setErrorFormatter(callable $errorFormatter): self { $this->errorFormatter = $errorFormatter; return $this; } /** * @phpstan-param ErrorsHandler $handler * * @api */ public function setErrorsHandler(callable $handler): self { $this->errorsHandler = $handler; return $this; } /** * Set validation rules for this server. * * @param array<ValidationRule>|callable|null $validationRules * * @phpstan-param ValidationRulesOption $validationRules * * @api */ public function setValidationRules($validationRules): self { // @phpstan-ignore-next-line necessary until we can use proper union types if (! is_array($validationRules) && ! is_callable($validationRules) && $validationRules !== null) { $invalidValidationRules = Utils::printSafe($validationRules); throw new InvariantViolation("Server config expects array of validation rules or callable returning such array, but got {$invalidValidationRules}"); } $this->validationRules = $validationRules; return $this; } /** @api */ public function setFieldResolver(callable $fieldResolver): self { $this->fieldResolver = $fieldResolver; return $this; } /** * @phpstan-param PersistedQueryLoader|null $persistedQueryLoader * * @api */ public function setPersistedQueryLoader(?callable $persistedQueryLoader): self { $this->persistedQueryLoader = $persistedQueryLoader; return $this; } /** * Set response debug flags. * * @see \GraphQL\Error\DebugFlag class for a list of all available flags * * @api */ public function setDebugFlag(int $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE): self { $this->debugFlag = $debugFlag; return $this; } /** * Allow batching queries (disabled by default). * * @api */ public function setQueryBatching(bool $enableBatching): self { $this->queryBatching = $enableBatching; return $this; } /** @api */ public function setPromiseAdapter(PromiseAdapter $promiseAdapter): self { $this->promiseAdapter = $promiseAdapter; return $this; } /** @return mixed|callable */ public function getContext() { return $this->context; } /** * @return mixed|callable * * @phpstan-return mixed|RootValueResolver */ public function getRootValue() { return $this->rootValue; } public function getSchema(): ?Schema { return $this->schema; } /** @phpstan-return ErrorFormatter|null */ public function getErrorFormatter(): ?callable { return $this->errorFormatter; } /** @phpstan-return ErrorsHandler|null */ public function getErrorsHandler(): ?callable { return $this->errorsHandler; } public function getPromiseAdapter(): ?PromiseAdapter { return $this->promiseAdapter; } /** * @return array<ValidationRule>|callable|null * * @phpstan-return ValidationRulesOption */ public function getValidationRules() { return $this->validationRules; } public function getFieldResolver(): ?callable { return $this->fieldResolver; } /** @phpstan-return PersistedQueryLoader|null */ public function getPersistedQueryLoader(): ?callable { return $this->persistedQueryLoader; } public function getDebugFlag(): int { return $this->debugFlag; } public function getQueryBatching(): bool { return $this->queryBatching; } } graphql/lib/Deferred.php 0000644 00000000771 15166657210 0011217 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL; use YOOtheme\GraphQL\Executor\Promise\Adapter\SyncPromise; /** * @phpstan-import-type Executor from SyncPromise */ class Deferred extends SyncPromise { /** @param Executor $executor */ public static function create(callable $executor): self { return new self($executor); } /** @param Executor $executor */ public function __construct(callable $executor) { parent::__construct($executor); } } graphql/lib/Language/Token.php 0000644 00000005022 15166657210 0012274 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; /** * Represents a range of characters represented by a lexical token * within a Source. * * @see \GraphQL\Tests\Language\TokenTest */ class Token { // Each kind of token. public const SOF = '<SOF>'; public const EOF = '<EOF>'; public const BANG = '!'; public const DOLLAR = '$'; public const AMP = '&'; public const PAREN_L = '('; public const PAREN_R = ')'; public const SPREAD = '...'; public const COLON = ':'; public const EQUALS = '='; public const AT = '@'; public const BRACKET_L = '['; public const BRACKET_R = ']'; public const BRACE_L = '{'; public const PIPE = '|'; public const BRACE_R = '}'; public const NAME = 'Name'; public const INT = 'Int'; public const FLOAT = 'Float'; public const STRING = 'String'; public const BLOCK_STRING = 'BlockString'; public const COMMENT = 'Comment'; /** The kind of Token (see one of constants above). */ public string $kind; /** The character offset at which this Node begins. */ public int $start; /** The character offset at which this Node ends. */ public int $end; /** The 1-indexed line number on which this Token appears. */ public int $line; /** The 1-indexed column number at which this Token begins. */ public int $column; public ?string $value; /** * Tokens exist as nodes in a double-linked-list amongst all tokens * including ignored tokens. <SOF> is always the first node and <EOF> * the last. */ public ?Token $prev; public ?Token $next = null; public function __construct(string $kind, int $start, int $end, int $line, int $column, ?Token $previous = null, ?string $value = null) { $this->kind = $kind; $this->start = $start; $this->end = $end; $this->line = $line; $this->column = $column; $this->prev = $previous; $this->value = $value; } public function getDescription(): string { return $this->kind . ($this->value === null ? '' : " \"{$this->value}\""); } /** * @return array{ * kind: string, * value: string|null, * line: int, * column: int, * } */ public function toArray(): array { return [ 'kind' => $this->kind, 'value' => $this->value, 'line' => $this->line, 'column' => $this->column, ]; } } graphql/lib/Language/BlockString.php 0000644 00000012046 15166657210 0013441 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; use YOOtheme\GraphQL\Utils\Utils; /** * @see \GraphQL\Tests\Language\BlockStringTest */ class BlockString { /** * Produces the value of a block string from its parsed raw value, similar to * CoffeeScript's block string, Python's docstring trim or Ruby's strip_heredoc. * * This implements the GraphQL spec's BlockStringValue() static algorithm. */ public static function dedentBlockStringLines(string $rawString): string { $lines = Utils::splitLines($rawString); // Remove common indentation from all lines but first. $commonIndent = self::getIndentation($rawString); $linesLength = count($lines); if ($commonIndent > 0) { for ($i = 1; $i < $linesLength; ++$i) { $lines[$i] = mb_substr($lines[$i], $commonIndent); } } // Remove leading and trailing blank lines. $startLine = 0; while ($startLine < $linesLength && self::isBlank($lines[$startLine])) { ++$startLine; } $endLine = $linesLength; while ($endLine > $startLine && self::isBlank($lines[$endLine - 1])) { --$endLine; } // Return a string of the lines joined with U+000A. return implode("\n", array_slice($lines, $startLine, $endLine - $startLine)); } private static function isBlank(string $str): bool { $strLength = mb_strlen($str); for ($i = 0; $i < $strLength; ++$i) { if ($str[$i] !== ' ' && $str[$i] !== '\t') { return false; } } return true; } public static function getIndentation(string $value): int { $isFirstLine = true; $isEmptyLine = true; $indent = 0; $commonIndent = null; $valueLength = mb_strlen($value); for ($i = 0; $i < $valueLength; ++$i) { switch (Utils::charCodeAt($value, $i)) { case 13: // \r if (Utils::charCodeAt($value, $i + 1) === 10) { ++$i; // skip \r\n as one symbol } // falls through // no break case 10: // \n $isFirstLine = false; $isEmptyLine = true; $indent = 0; break; case 9: // \t case 32: // <space> ++$indent; break; default: if ( $isEmptyLine && ! $isFirstLine && ($commonIndent === null || $indent < $commonIndent) ) { $commonIndent = $indent; } $isEmptyLine = false; } } return $commonIndent ?? 0; } /** * Print a block string in the indented block form by adding a leading and * trailing blank line. However, if a block string starts with whitespace and is * a single-line, adding a leading blank line would strip that whitespace. */ public static function print(string $value): string { $escapedValue = str_replace('"""', '\\"""', $value); // Expand a block string's raw value into independent lines. $lines = Utils::splitLines($escapedValue); $isSingleLine = count($lines) === 1; // If common indentation is found we can fix some of those cases by adding leading new line $forceLeadingNewLine = count($lines) > 1; foreach ($lines as $i => $line) { if ($i === 0) { continue; } if ($line !== '' && preg_match('/^\s/', $line) !== 1) { $forceLeadingNewLine = false; } } // Trailing triple quotes just looks confusing but doesn't force trailing new line $hasTrailingTripleQuotes = preg_match('/\\\\"""$/', $escapedValue) === 1; // Trailing quote (single or double) or slash forces trailing new line $hasTrailingQuote = preg_match('/"$/', $value) === 1 && ! $hasTrailingTripleQuotes; $hasTrailingSlash = preg_match('/\\\\$/', $value) === 1; $forceTrailingNewline = $hasTrailingQuote || $hasTrailingSlash; // add leading and trailing new lines only if it improves readability $printAsMultipleLines = ! $isSingleLine || mb_strlen($value) > 70 || $forceTrailingNewline || $forceLeadingNewLine || $hasTrailingTripleQuotes; $result = ''; // Format a multi-line block quote to account for leading space. $skipLeadingNewLine = $isSingleLine && preg_match('/^\s/', $value) === 1; if (($printAsMultipleLines && ! $skipLeadingNewLine) || $forceLeadingNewLine) { $result .= "\n"; } $result .= $escapedValue; if ($printAsMultipleLines) { $result .= "\n"; } return '"""' . $result . '"""'; } } graphql/lib/Language/VisitorSkipNode.php 0000644 00000000176 15166657210 0014315 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; final class VisitorSkipNode extends VisitorOperation {} graphql/lib/Language/Visitor.php 0000644 00000043422 15166657210 0012661 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Utils\TypeInfo; use YOOtheme\GraphQL\Utils\Utils; /** * Utility for efficient AST traversal and modification. * * `visit()` will walk through an AST using a depth first traversal, calling * the visitor's enter function at each node in the traversal, and calling the * leave function after visiting that node and all of its child nodes. * * By returning different values from the `enter` and `leave` functions, the * behavior of the visitor can be altered. * - no return (`void`) or return `null`: no action * - `Visitor::skipNode()`: skips over the subtree at the current node of the AST * - `Visitor::stop()`: stop the Visitor completely * - `Visitor::removeNode()`: remove the current node * - return any other value: replace this node with the returned value * * When using `visit()` to edit an AST, the original AST will not be modified, and * a new version of the AST with the changes applied will be returned from the * visit function. * * $editedAST = Visitor::visit($ast, [ * 'enter' => function (Node $node, $key, $parent, array $path, array $ancestors) { * // ... * }, * 'leave' => function (Node $node, $key, $parent, array $path, array $ancestors) { * // ... * } * ]); * * Alternatively to providing `enter` and `leave` functions, a visitor can * instead provide functions named the same as the [kinds of AST nodes](class-reference.md#graphqllanguageastnodekind), * or enter/leave visitors at a named key, leading to four permutations of * visitor API: * * 1. Named visitors triggered when entering a node a specific kind. * * Visitor::visit($ast, [ * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) { * // enter the "ObjectTypeDefinition" node * } * ]); * * 2. Named visitors that trigger upon entering and leaving a node of * a specific kind. * * Visitor::visit($ast, [ * NodeKind::OBJECT_TYPE_DEFINITION => [ * 'enter' => function (ObjectTypeDefinitionNode $node) { * // enter the "ObjectTypeDefinition" node * } * 'leave' => function (ObjectTypeDefinitionNode $node) { * // leave the "ObjectTypeDefinition" node * } * ] * ]); * * 3. Generic visitors that trigger upon entering and leaving any node. * * Visitor::visit($ast, [ * 'enter' => function (Node $node) { * // enter any node * }, * 'leave' => function (Node $node) { * // leave any node * } * ]); * * 4. Parallel visitors for entering and leaving nodes of a specific kind. * * Visitor::visit($ast, [ * 'enter' => [ * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) { * // enter the "ObjectTypeDefinition" node * } * }, * 'leave' => [ * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) { * // leave the "ObjectTypeDefinition" node * } * ] * ]); * * @phpstan-type NodeVisitor callable(Node): (VisitorOperation|Node|NodeList<Node>|null|false|void) * @phpstan-type VisitorArray array<string, NodeVisitor>|array<string, array<string, NodeVisitor>> * * @see \GraphQL\Tests\Language\VisitorTest */ class Visitor { public const VISITOR_KEYS = [ NodeKind::NAME => [], NodeKind::DOCUMENT => ['definitions'], NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'], NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue', 'directives'], NodeKind::VARIABLE => ['name'], NodeKind::SELECTION_SET => ['selections'], NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'], NodeKind::ARGUMENT => ['name', 'value'], NodeKind::FRAGMENT_SPREAD => ['name', 'directives'], NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'], NodeKind::FRAGMENT_DEFINITION => [ 'name', // Note: fragment variable definitions are experimental and may be changed // or removed in the future. 'variableDefinitions', 'typeCondition', 'directives', 'selectionSet', ], NodeKind::INT => [], NodeKind::FLOAT => [], NodeKind::STRING => [], NodeKind::BOOLEAN => [], NodeKind::NULL => [], NodeKind::ENUM => [], NodeKind::LST => ['values'], NodeKind::OBJECT => ['fields'], NodeKind::OBJECT_FIELD => ['name', 'value'], NodeKind::DIRECTIVE => ['name', 'arguments'], NodeKind::NAMED_TYPE => ['name'], NodeKind::LIST_TYPE => ['type'], NodeKind::NON_NULL_TYPE => ['type'], NodeKind::SCHEMA_DEFINITION => ['directives', 'operationTypes'], NodeKind::OPERATION_TYPE_DEFINITION => ['type'], NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'], NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'], NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'], NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'], NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'], NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'], NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'], NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'], NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'], NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'], NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'], NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'], ]; /** * Visit the AST (see class description for details). * * @param NodeList<Node>|Node $root * @param VisitorArray $visitor * @param array<string, mixed>|null $keyMap * * @throws \Exception * * @return mixed * * @api */ public static function visit(object $root, array $visitor, ?array $keyMap = null) { $visitorKeys = $keyMap ?? self::VISITOR_KEYS; /** * @var list<array{ * inList: bool, * index: int, * keys: Node|NodeList|mixed, * edits: array<int, array{mixed, mixed}>, * }> $stack */ $stack = []; $inList = $root instanceof NodeList; $keys = [$root]; $index = -1; $edits = []; $parent = null; $path = []; $ancestors = []; do { ++$index; $isLeaving = $index === count($keys); $key = null; $node = null; $isEdited = $isLeaving && $edits !== []; if ($isLeaving) { $key = $ancestors === [] ? null : $path[count($path) - 1]; $node = $parent; $parent = array_pop($ancestors); if ($isEdited) { if ($node instanceof Node || $node instanceof NodeList) { $node = $node->cloneDeep(); } $editOffset = 0; foreach ($edits as [$editKey, $editValue]) { if ($inList) { $editKey -= $editOffset; } if ($inList && $editValue === null) { assert($node instanceof NodeList, 'Follows from $inList'); $node->splice($editKey, 1); ++$editOffset; } elseif ($node instanceof NodeList) { if ($editValue instanceof NodeList) { $node->splice($editKey, 1, $editValue); $editOffset -= count($editValue) - 1; } elseif ($editValue instanceof Node) { $node[$editKey] = $editValue; } else { $notNodeOrNodeList = Utils::printSafe($editValue); throw new \Exception("Can only add Node or NodeList to NodeList, got: {$notNodeOrNodeList}."); } } else { $node->{$editKey} = $editValue; } } } // @phpstan-ignore-next-line the stack is guaranteed to be non-empty at this point [ 'index' => $index, 'keys' => $keys, 'edits' => $edits, 'inList' => $inList, ] = array_pop($stack); } elseif ($parent === null) { $node = $root; } else { $key = $inList ? $index : $keys[$index]; $node = $parent instanceof NodeList ? $parent[$key] : $parent->{$key}; if ($node === null) { continue; } $path[] = $key; } $result = null; if (! $node instanceof NodeList) { if (! ($node instanceof Node)) { $notNode = Utils::printSafe($node); throw new \Exception("Invalid AST Node: {$notNode}."); } $visitFn = self::extractVisitFn($visitor, $node->kind, $isLeaving); if ($visitFn !== null) { $result = $visitFn($node, $key, $parent, $path, $ancestors); if ($result !== null) { if ($result instanceof VisitorStop) { break; } if ($result instanceof VisitorSkipNode) { if (! $isLeaving) { array_pop($path); } continue; } $editValue = $result instanceof VisitorRemoveNode ? null : $result; $edits[] = [$key, $editValue]; if (! $isLeaving) { if (! ($editValue instanceof Node)) { array_pop($path); continue; } $node = $editValue; } } } } if ($result === null && $isEdited) { $edits[] = [$key, $node]; } if ($isLeaving) { array_pop($path); } else { $stack[] = [ 'inList' => $inList, 'index' => $index, 'keys' => $keys, 'edits' => $edits, ]; $inList = $node instanceof NodeList; $keys = ($inList ? $node : $visitorKeys[$node->kind]) ?? []; $index = -1; $edits = []; if ($parent !== null) { $ancestors[] = $parent; } $parent = $node; } } while ($stack !== []); return $edits === [] ? $root : $edits[0][1]; } /** * Returns marker for stopping. * * @api */ public static function stop(): VisitorStop { static $stop; return $stop ??= new VisitorStop(); } /** * Returns marker for skipping the subtree at the current node. * * @api */ public static function skipNode(): VisitorSkipNode { static $skipNode; return $skipNode ??= new VisitorSkipNode(); } /** * Returns marker for removing the current node. * * @api */ public static function removeNode(): VisitorRemoveNode { static $removeNode; return $removeNode ??= new VisitorRemoveNode(); } /** * Combines the given visitors to run in parallel. * * @phpstan-param array<int, VisitorArray> $visitors * * @return VisitorArray */ public static function visitInParallel(array $visitors): array { $visitorsCount = count($visitors); $skipping = new \SplFixedArray($visitorsCount); return [ 'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) { for ($i = 0; $i < $visitorsCount; ++$i) { if ($skipping[$i] !== null) { continue; } $fn = self::extractVisitFn( $visitors[$i], $node->kind, false ); if ($fn === null) { continue; } $result = $fn(...func_get_args()); if ($result instanceof VisitorSkipNode) { $skipping[$i] = $node; } elseif ($result instanceof VisitorStop) { $skipping[$i] = $result; } elseif ($result instanceof VisitorRemoveNode) { return $result; } elseif ($result !== null) { return $result; } } return null; }, 'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) { for ($i = 0; $i < $visitorsCount; ++$i) { if ($skipping[$i] === null) { $fn = self::extractVisitFn( $visitors[$i], $node->kind, true ); if ($fn !== null) { $result = $fn(...func_get_args()); if ($result instanceof VisitorStop) { $skipping[$i] = $result; } elseif ($result instanceof VisitorRemoveNode) { return $result; } elseif ($result !== null) { return $result; } } } elseif ($skipping[$i] === $node) { $skipping[$i] = null; } } return null; }, ]; } /** * Creates a new visitor that updates TypeInfo and delegates to the given visitor. * * @phpstan-param VisitorArray $visitor * * @phpstan-return VisitorArray */ public static function visitWithTypeInfo(TypeInfo $typeInfo, array $visitor): array { return [ 'enter' => static function (Node $node) use ($typeInfo, $visitor) { $typeInfo->enter($node); $fn = self::extractVisitFn($visitor, $node->kind, false); if ($fn === null) { return null; } $result = $fn(...func_get_args()); if ($result === null) { return null; } $typeInfo->leave($node); if ($result instanceof Node) { $typeInfo->enter($result); } return $result; }, 'leave' => static function (Node $node) use ($typeInfo, $visitor) { $fn = self::extractVisitFn($visitor, $node->kind, true); $result = $fn !== null ? $fn(...func_get_args()) : null; $typeInfo->leave($node); return $result; }, ]; } /** * @phpstan-param VisitorArray $visitor * * @return (callable(Node $node, string|int|null $key, Node|NodeList<Node>|null $parent, array<int, int|string> $path, array<int, Node|NodeList<Node>> $ancestors): (VisitorOperation|Node|null))|(callable(Node): (VisitorOperation|Node|NodeList<Node>|void|false|null))|null */ protected static function extractVisitFn(array $visitor, string $kind, bool $isLeaving): ?callable { $kindVisitor = $visitor[$kind] ?? null; if (is_array($kindVisitor)) { return $isLeaving ? $kindVisitor['leave'] ?? null : $kindVisitor['enter'] ?? null; } if ($kindVisitor !== null && ! $isLeaving) { return $kindVisitor; } $specificVisitor = $isLeaving ? $visitor['leave'] ?? null : $visitor['enter'] ?? null; if (is_array($specificVisitor)) { return $specificVisitor[$kind] ?? null; } return $specificVisitor; } } graphql/lib/Language/Lexer.php 0000644 00000057031 15166657210 0012302 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; use YOOtheme\GraphQL\Error\SyntaxError; use YOOtheme\GraphQL\Utils\Utils; /** * A lexer is a stateful stream generator, it returns the next token in the Source when advanced. * Assuming the source is valid, the final returned token will be EOF, * after which the lexer will repeatedly return the same EOF token whenever called. * * Algorithm is O(N) both on memory and time. * * @phpstan-import-type ParserOptions from Parser * * @see \GraphQL\Tests\Language\LexerTest */ class Lexer { // https://spec.graphql.org/October2021/#sec-Punctuators private const TOKEN_BANG = 33; private const TOKEN_DOLLAR = 36; private const TOKEN_AMP = 38; private const TOKEN_PAREN_L = 40; private const TOKEN_PAREN_R = 41; private const TOKEN_DOT = 46; private const TOKEN_COLON = 58; private const TOKEN_EQUALS = 61; private const TOKEN_AT = 64; private const TOKEN_BRACKET_L = 91; private const TOKEN_BRACKET_R = 93; private const TOKEN_BRACE_L = 123; private const TOKEN_PIPE = 124; private const TOKEN_BRACE_R = 125; public Source $source; /** @phpstan-var ParserOptions */ public array $options; /** The previously focused non-ignored token. */ public Token $lastToken; /** The currently focused non-ignored token. */ public Token $token; /** The (1-indexed) line containing the current token. */ public int $line = 1; /** The character offset at which the current line begins. */ public int $lineStart = 0; /** Current cursor position for UTF8 encoding of the source. */ private int $position = 0; /** Current cursor position for ASCII representation of the source. */ private int $byteStreamPosition = 0; /** @phpstan-param ParserOptions $options */ public function __construct(Source $source, array $options = []) { $startOfFileToken = new Token(Token::SOF, 0, 0, 0, 0, null); $this->source = $source; $this->options = $options; $this->lastToken = $startOfFileToken; $this->token = $startOfFileToken; } /** * @throws \JsonException * @throws SyntaxError */ public function advance(): Token { $this->lastToken = $this->token; return $this->token = $this->lookahead(); } /** * @throws \JsonException * @throws SyntaxError */ public function lookahead(): Token { $token = $this->token; if ($token->kind !== Token::EOF) { do { $token = $token->next ?? ($token->next = $this->readToken($token)); } while ($token->kind === Token::COMMENT); } return $token; } /** * @throws \JsonException * @throws SyntaxError */ private function readToken(Token $prev): Token { $bodyLength = $this->source->length; $this->positionAfterWhitespace(); $position = $this->position; $line = $this->line; $col = 1 + $position - $this->lineStart; if ($position >= $bodyLength) { return new Token(Token::EOF, $bodyLength, $bodyLength, $line, $col, $prev); } // Read next char and advance string cursor: [, $code, $bytes] = $this->readChar(true); switch ($code) { case self::TOKEN_BANG: // ! return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev); case 35: // # $this->moveStringCursor(-1, -1 * $bytes); return $this->readComment($line, $col, $prev); case self::TOKEN_DOLLAR: // $ return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev); case self::TOKEN_AMP: // & return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev); case self::TOKEN_PAREN_L: // ( return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev); case self::TOKEN_PAREN_R: // ) return new Token(Token::PAREN_R, $position, $position + 1, $line, $col, $prev); case self::TOKEN_DOT: // . [, $charCode1] = $this->readChar(true); [, $charCode2] = $this->readChar(true); if ($charCode1 === self::TOKEN_DOT && $charCode2 === self::TOKEN_DOT) { return new Token(Token::SPREAD, $position, $position + 3, $line, $col, $prev); } break; case self::TOKEN_COLON: // : return new Token(Token::COLON, $position, $position + 1, $line, $col, $prev); case self::TOKEN_EQUALS: // = return new Token(Token::EQUALS, $position, $position + 1, $line, $col, $prev); case self::TOKEN_AT: // @ return new Token(Token::AT, $position, $position + 1, $line, $col, $prev); case self::TOKEN_BRACKET_L: // [ return new Token(Token::BRACKET_L, $position, $position + 1, $line, $col, $prev); case self::TOKEN_BRACKET_R: // ] return new Token(Token::BRACKET_R, $position, $position + 1, $line, $col, $prev); case self::TOKEN_BRACE_L: // { return new Token(Token::BRACE_L, $position, $position + 1, $line, $col, $prev); case self::TOKEN_PIPE: // | return new Token(Token::PIPE, $position, $position + 1, $line, $col, $prev); case self::TOKEN_BRACE_R: // } return new Token(Token::BRACE_R, $position, $position + 1, $line, $col, $prev); // A-Z case 65: case 66: case 67: case 68: case 69: case 70: case 71: case 72: case 73: case 74: case 75: case 76: case 77: case 78: case 79: case 80: case 81: case 82: case 83: case 84: case 85: case 86: case 87: case 88: case 89: case 90: // _ case 95: // a-z case 97: case 98: case 99: case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: case 108: case 109: case 110: case 111: case 112: case 113: case 114: case 115: case 116: case 117: case 118: case 119: case 120: case 121: case 122: return $this->moveStringCursor(-1, -1 * $bytes) ->readName($line, $col, $prev); // - case 45: // 0-9 case 48: case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: return $this->moveStringCursor(-1, -1 * $bytes) ->readNumber($line, $col, $prev); // " case 34: [, $nextCode] = $this->readChar(); [, $nextNextCode] = $this->moveStringCursor(1, 1) ->readChar(); if ($nextCode === 34 && $nextNextCode === 34) { return $this->moveStringCursor(-2, (-1 * $bytes) - 1) ->readBlockString($line, $col, $prev); } return $this->moveStringCursor(-2, (-1 * $bytes) - 1) ->readString($line, $col, $prev); } throw new SyntaxError($this->source, $position, $this->unexpectedCharacterMessage($code)); } /** @throws \JsonException */ private function unexpectedCharacterMessage(?int $code): string { // SourceCharacter if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) { return 'Cannot contain the invalid character ' . Utils::printCharCode($code); } if ($code === 39) { return 'Unexpected single quote character (\'), did you mean to use a double quote (")?'; } return 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.'; } /** * Reads an alphanumeric + underscore name from the source. * * [_A-Za-z][_0-9A-Za-z]* */ private function readName(int $line, int $col, Token $prev): Token { $value = ''; $start = $this->position; [$char, $code] = $this->readChar(); while ( $code !== null && ( $code === 95 // _ || ($code >= 48 && $code <= 57) // 0-9 || ($code >= 65 && $code <= 90) // A-Z || ($code >= 97 && $code <= 122) // a-z ) ) { $value .= $char; [$char, $code] = $this->moveStringCursor(1, 1)->readChar(); } return new Token( Token::NAME, $start, $this->position, $line, $col, $prev, $value ); } /** * Reads a number token from the source file, either a float * or an int depending on whether a decimal point appears. * * Int: -?(0|[1-9][0-9]*) * Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)? * * @throws \JsonException * @throws SyntaxError */ private function readNumber(int $line, int $col, Token $prev): Token { $value = ''; $start = $this->position; [$char, $code] = $this->readChar(); $isFloat = false; if ($code === 45) { // - $value .= $char; [$char, $code] = $this->moveStringCursor(1, 1)->readChar(); } // guard against leading zero's if ($code === 48) { // 0 $value .= $char; [$char, $code] = $this->moveStringCursor(1, 1)->readChar(); if ($code >= 48 && $code <= 57) { throw new SyntaxError($this->source, $this->position, 'Invalid number, unexpected digit after 0: ' . Utils::printCharCode($code)); } } else { $value .= $this->readDigits(); [$char, $code] = $this->readChar(); } if ($code === 46) { // . $isFloat = true; $this->moveStringCursor(1, 1); $value .= $char; $value .= $this->readDigits(); [$char, $code] = $this->readChar(); } if ($code === 69 || $code === 101) { // E e $isFloat = true; $value .= $char; [$char, $code] = $this->moveStringCursor(1, 1)->readChar(); if ($code === 43 || $code === 45) { // + - $value .= $char; $this->moveStringCursor(1, 1); } $value .= $this->readDigits(); } return new Token( $isFloat ? Token::FLOAT : Token::INT, $start, $this->position, $line, $col, $prev, $value ); } /** * Returns string with all digits + changes current string cursor position to point to the first char after digits. * * @throws \JsonException * @throws SyntaxError */ private function readDigits(): string { [$char, $code] = $this->readChar(); if ($code >= 48 && $code <= 57) { // 0 - 9 $value = ''; do { $value .= $char; [$char, $code] = $this->moveStringCursor(1, 1)->readChar(); } while ($code >= 48 && $code <= 57); // 0 - 9 return $value; } if ($this->position > $this->source->length - 1) { $code = null; } throw new SyntaxError($this->source, $this->position, 'Invalid number, expected digit but got: ' . Utils::printCharCode($code)); } /** * @throws \JsonException * @throws SyntaxError */ private function readString(int $line, int $col, Token $prev): Token { $start = $this->position; // Skip leading quote and read first string char: [$char, $code, $bytes] = $this->moveStringCursor(1, 1) ->readChar(); $chunk = ''; $value = ''; while ( $code !== null && $code !== 10 && $code !== 13 // not LineTerminator ) { if ($code === 34) { // Closing Quote (") $value .= $chunk; // Skip quote $this->moveStringCursor(1, 1); return new Token( Token::STRING, $start, $this->position, $line, $col, $prev, $value ); } $this->assertValidStringCharacterCode($code, $this->position); $this->moveStringCursor(1, $bytes); if ($code === 92) { // \ $value .= $chunk; [, $code] = $this->readChar(true); switch ($code) { case 34: $value .= '"'; break; case 47: $value .= '/'; break; case 92: $value .= '\\'; break; case 98: $value .= chr(8); // \b (backspace) break; case 102: $value .= "\f"; break; case 110: $value .= "\n"; break; case 114: $value .= "\r"; break; case 116: $value .= "\t"; break; case 117: $position = $this->position; [$hex] = $this->readChars(4); if (preg_match('/[0-9a-fA-F]{4}/', $hex) !== 1) { throw new SyntaxError($this->source, $position - 1, "Invalid character escape sequence: \\u{$hex}"); } $code = hexdec($hex); assert(is_int($code), 'Since only a single char is read'); // UTF-16 surrogate pair detection and handling. $highOrderByte = $code >> 8; if ($highOrderByte >= 0xD8 && $highOrderByte <= 0xDF) { [$utf16Continuation] = $this->readChars(6); if (preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation) !== 1) { throw new SyntaxError($this->source, $this->position - 5, 'Invalid UTF-16 trailing surrogate: ' . $utf16Continuation); } $surrogatePairHex = $hex . substr($utf16Continuation, 2, 4); $value .= mb_convert_encoding(pack('H*', $surrogatePairHex), 'UTF-8', 'UTF-16'); break; } $this->assertValidStringCharacterCode($code, $position - 2); $value .= Utils::chr($code); break; // null means EOF, will delegate to general handling of unterminated strings case null: continue 2; default: $chr = Utils::chr($code); throw new SyntaxError($this->source, $this->position - 1, "Invalid character escape sequence: \\{$chr}"); } $chunk = ''; } else { $chunk .= $char; } [$char, $code, $bytes] = $this->readChar(); } throw new SyntaxError($this->source, $this->position, 'Unterminated string.'); } /** * Reads a block string token from the source file. * * """("?"?(\\"""|\\(?!=""")|[^"\\]))*""" * * @throws \JsonException * @throws SyntaxError */ private function readBlockString(int $line, int $col, Token $prev): Token { $start = $this->position; // Skip leading quotes and read first string char: [$char, $code, $bytes] = $this->moveStringCursor(3, 3)->readChar(); $chunk = ''; $value = ''; while ($code !== null) { // Closing Triple-Quote (""") if ($code === 34) { // Move 2 quotes [, $nextCode] = $this->moveStringCursor(1, 1)->readChar(); [, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar(); if ($nextCode === 34 && $nextNextCode === 34) { $value .= $chunk; $this->moveStringCursor(1, 1); return new Token( Token::BLOCK_STRING, $start, $this->position, $line, $col, $prev, BlockString::dedentBlockStringLines($value) ); } // move cursor back to before the first quote $this->moveStringCursor(-2, -2); } $this->assertValidBlockStringCharacterCode($code, $this->position); $this->moveStringCursor(1, $bytes); [, $nextCode] = $this->readChar(); [, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar(); [, $nextNextNextCode] = $this->moveStringCursor(1, 1)->readChar(); // Escape Triple-Quote (\""") if ( $code === 92 && $nextCode === 34 && $nextNextCode === 34 && $nextNextNextCode === 34 ) { $this->moveStringCursor(1, 1); $value .= $chunk . '"""'; $chunk = ''; } else { // move cursor back to before the first quote $this->moveStringCursor(-2, -2); if ($code === 10) { // new line ++$this->line; $this->lineStart = $this->position; } $chunk .= $char; } [$char, $code, $bytes] = $this->readChar(); } throw new SyntaxError($this->source, $this->position, 'Unterminated string.'); } /** * @throws \JsonException * @throws SyntaxError */ private function assertValidStringCharacterCode(int $code, int $position): void { // SourceCharacter if ($code < 0x0020 && $code !== 0x0009) { $char = Utils::printCharCode($code); throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}"); } } /** * @throws \JsonException * @throws SyntaxError */ private function assertValidBlockStringCharacterCode(int $code, int $position): void { // SourceCharacter if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) { $char = Utils::printCharCode($code); throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}"); } } /** * Reads from body starting at startPosition until it finds a non-whitespace * or commented character, then places cursor to the position of that character. */ private function positionAfterWhitespace(): void { while ($this->position < $this->source->length) { [, $code, $bytes] = $this->readChar(); // Skip whitespace // tab | space | comma | BOM if ($code === 9 || $code === 32 || $code === 44 || $code === 0xFEFF) { $this->moveStringCursor(1, $bytes); } elseif ($code === 10) { // new line $this->moveStringCursor(1, $bytes); ++$this->line; $this->lineStart = $this->position; } elseif ($code === 13) { // carriage return [, $nextCode, $nextBytes] = $this->moveStringCursor(1, $bytes)->readChar(); if ($nextCode === 10) { // lf after cr $this->moveStringCursor(1, $nextBytes); } ++$this->line; $this->lineStart = $this->position; } else { break; } } } /** * Reads a comment token from the source file. * * #[\u0009\u0020-\uFFFF]* */ private function readComment(int $line, int $col, Token $prev): Token { $start = $this->position; $value = ''; $bytes = 1; do { [$char, $code, $bytes] = $this->moveStringCursor(1, $bytes)->readChar(); $value .= $char; } while ( $code !== null // SourceCharacter but not LineTerminator && ($code > 0x001F || $code === 0x0009) ); return new Token( Token::COMMENT, $start, $this->position, $line, $col, $prev, $value ); } /** * Reads next UTF8Character from the byte stream, starting from $byteStreamPosition. * * @return array{string, int|null, int} */ private function readChar(bool $advance = false, ?int $byteStreamPosition = null): array { if ($byteStreamPosition === null) { $byteStreamPosition = $this->byteStreamPosition; } $code = null; $utf8char = ''; $bytes = 0; $positionOffset = 0; if (isset($this->source->body[$byteStreamPosition])) { $ord = ord($this->source->body[$byteStreamPosition]); if ($ord < 128) { $bytes = 1; } elseif ($ord < 224) { $bytes = 2; } elseif ($ord < 240) { $bytes = 3; } else { $bytes = 4; } for ($pos = $byteStreamPosition; $pos < $byteStreamPosition + $bytes; ++$pos) { $utf8char .= $this->source->body[$pos]; } $positionOffset = 1; $code = $bytes === 1 ? $ord : Utils::ord($utf8char); } if ($advance) { $this->moveStringCursor($positionOffset, $bytes); } return [$utf8char, $code, $bytes]; } /** * Reads next $numberOfChars UTF8 characters from the byte stream. * * @return array{string, int} */ private function readChars(int $charCount): array { $result = ''; $totalBytes = 0; $byteOffset = $this->byteStreamPosition; for ($i = 0; $i < $charCount; ++$i) { [$char, $code, $bytes] = $this->readChar(false, $byteOffset); $totalBytes += $bytes; $byteOffset += $bytes; $result .= $char; } $this->moveStringCursor($charCount, $totalBytes); return [$result, $totalBytes]; } /** Moves internal string cursor position. */ private function moveStringCursor(int $positionOffset, int $byteStreamOffset): self { $this->position += $positionOffset; $this->byteStreamPosition += $byteStreamOffset; return $this; } } graphql/lib/Language/Printer.php 0000644 00000047073 15166657210 0012653 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\BooleanValueNode; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FloatValueNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\ListTypeNode; use YOOtheme\GraphQL\Language\AST\ListValueNode; use YOOtheme\GraphQL\Language\AST\NamedTypeNode; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\NonNullTypeNode; use YOOtheme\GraphQL\Language\AST\NullValueNode; use YOOtheme\GraphQL\Language\AST\ObjectFieldNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ObjectValueNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\OperationTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Language\AST\VariableNode; /** * Prints AST to string. Capable of printing GraphQL queries and Type definition language. * Useful for pretty-printing queries or printing back AST for logging, documentation, etc. * * Usage example: * * ```php * $query = 'query myQuery {someField}'; * $ast = GraphQL\Language\Parser::parse($query); * $printed = GraphQL\Language\Printer::doPrint($ast); * ``` * * @see \GraphQL\Tests\Language\PrinterTest */ class Printer { /** * Converts the AST of a GraphQL node to a string. * * Handles both executable definitions and schema definitions. * * @throws \JsonException * * @api */ public static function doPrint(Node $ast): string { return static::p($ast); } /** @throws \JsonException */ protected static function p(?Node $node): string { if ($node === null) { return ''; } switch (true) { case $node instanceof ArgumentNode: case $node instanceof ObjectFieldNode: return static::p($node->name) . ': ' . static::p($node->value); case $node instanceof BooleanValueNode: return $node->value ? 'true' : 'false'; case $node instanceof DirectiveDefinitionNode: $argStrings = []; foreach ($node->arguments as $arg) { $argStrings[] = static::p($arg); } $noIndent = true; foreach ($argStrings as $argString) { if (strpos($argString, "\n") !== false) { $noIndent = false; break; } } return static::addDescription($node->description, 'directive @' . static::p($node->name) . ($noIndent ? static::wrap('(', static::join($argStrings, ', '), ')') : static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n")) . ($node->repeatable ? ' repeatable' : '') . ' on ' . static::printList($node->locations, ' | ')); case $node instanceof DirectiveNode: return '@' . static::p($node->name) . static::wrap('(', static::printList($node->arguments, ', '), ')'); case $node instanceof DocumentNode: return static::printList($node->definitions, "\n\n") . "\n"; case $node instanceof EnumTypeDefinitionNode: return static::addDescription($node->description, static::join( [ 'enum', static::p($node->name), static::printList($node->directives, ' '), static::printListBlock($node->values), ], ' ' )); case $node instanceof EnumTypeExtensionNode: return static::join( [ 'extend enum', static::p($node->name), static::printList($node->directives, ' '), static::printListBlock($node->values), ], ' ' ); case $node instanceof EnumValueDefinitionNode: return static::addDescription( $node->description, static::join([static::p($node->name), static::printList($node->directives, ' ')], ' ') ); case $node instanceof EnumValueNode: case $node instanceof FloatValueNode: case $node instanceof IntValueNode: case $node instanceof NameNode: return $node->value; case $node instanceof FieldDefinitionNode: $argStrings = []; foreach ($node->arguments as $item) { $argStrings[] = static::p($item); } $noIndent = true; foreach ($argStrings as $argString) { if (strpos($argString, "\n") !== false) { $noIndent = false; break; } } return static::addDescription( $node->description, static::p($node->name) . ($noIndent ? static::wrap('(', static::join($argStrings, ', '), ')') : static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n)")) . ': ' . static::p($node->type) . static::wrap(' ', static::printList($node->directives, ' ')) ); case $node instanceof FieldNode: $prefix = static::wrap('', $node->alias->value ?? null, ': ') . static::p($node->name); $argsLine = $prefix . static::wrap( '(', static::printList($node->arguments, ', '), ')' ); if (strlen($argsLine) > 80) { $argsLine = $prefix . static::wrap( "(\n", static::indent( static::printList($node->arguments, "\n") ), "\n)" ); } return static::join( [ $argsLine, static::printList($node->directives, ' '), static::p($node->selectionSet), ], ' ' ); case $node instanceof FragmentDefinitionNode: // Note: fragment variable definitions are experimental and may be changed or removed in the future. return 'fragment ' . static::p($node->name) . static::wrap( '(', static::printList($node->variableDefinitions ?? new NodeList([]), ', '), ')' ) . ' on ' . static::p($node->typeCondition->name) . ' ' . static::wrap( '', static::printList($node->directives, ' '), ' ' ) . static::p($node->selectionSet); case $node instanceof FragmentSpreadNode: return '...' . static::p($node->name) . static::wrap(' ', static::printList($node->directives, ' ')); case $node instanceof InlineFragmentNode: return static::join( [ '...', static::wrap('on ', static::p($node->typeCondition->name ?? null)), static::printList($node->directives, ' '), static::p($node->selectionSet), ], ' ' ); case $node instanceof InputObjectTypeDefinitionNode: return static::addDescription($node->description, static::join( [ 'input', static::p($node->name), static::printList($node->directives, ' '), static::printListBlock($node->fields), ], ' ' )); case $node instanceof InputObjectTypeExtensionNode: return static::join( [ 'extend input', static::p($node->name), static::printList($node->directives, ' '), static::printListBlock($node->fields), ], ' ' ); case $node instanceof InputValueDefinitionNode: return static::addDescription($node->description, static::join( [ static::p($node->name) . ': ' . static::p($node->type), static::wrap('= ', static::p($node->defaultValue)), static::printList($node->directives, ' '), ], ' ' )); case $node instanceof InterfaceTypeDefinitionNode: return static::addDescription($node->description, static::join( [ 'interface', static::p($node->name), static::wrap('implements ', static::printList($node->interfaces, ' & ')), static::printList($node->directives, ' '), static::printListBlock($node->fields), ], ' ' )); case $node instanceof InterfaceTypeExtensionNode: return static::join( [ 'extend interface', static::p($node->name), static::wrap('implements ', static::printList($node->interfaces, ' & ')), static::printList($node->directives, ' '), static::printListBlock($node->fields), ], ' ' ); case $node instanceof ListTypeNode: return '[' . static::p($node->type) . ']'; case $node instanceof ListValueNode: return '[' . static::printList($node->values, ', ') . ']'; case $node instanceof NamedTypeNode: return static::p($node->name); case $node instanceof NonNullTypeNode: return static::p($node->type) . '!'; case $node instanceof NullValueNode: return 'null'; case $node instanceof ObjectTypeDefinitionNode: return static::addDescription($node->description, static::join( [ 'type', static::p($node->name), static::wrap('implements ', static::printList($node->interfaces, ' & ')), static::printList($node->directives, ' '), static::printListBlock($node->fields), ], ' ' )); case $node instanceof ObjectTypeExtensionNode: return static::join( [ 'extend type', static::p($node->name), static::wrap('implements ', static::printList($node->interfaces, ' & ')), static::printList($node->directives, ' '), static::printListBlock($node->fields), ], ' ' ); case $node instanceof ObjectValueNode: return '{ ' . static::printList($node->fields, ', ') . ' }'; case $node instanceof OperationDefinitionNode: $op = $node->operation; $name = static::p($node->name); $varDefs = static::wrap('(', static::printList($node->variableDefinitions, ', '), ')'); $directives = static::printList($node->directives, ' '); $selectionSet = static::p($node->selectionSet); // Anonymous queries with no directives or variable definitions can use // the query short form. return $name === '' && $directives === '' && $varDefs === '' && $op === 'query' ? $selectionSet : static::join([$op, static::join([$name, $varDefs]), $directives, $selectionSet], ' '); case $node instanceof OperationTypeDefinitionNode: return $node->operation . ': ' . static::p($node->type); case $node instanceof ScalarTypeDefinitionNode: return static::addDescription($node->description, static::join([ 'scalar', static::p($node->name), static::printList($node->directives, ' '), ], ' ')); case $node instanceof ScalarTypeExtensionNode: return static::join( [ 'extend scalar', static::p($node->name), static::printList($node->directives, ' '), ], ' ' ); case $node instanceof SchemaDefinitionNode: return static::join( [ 'schema', static::printList($node->directives, ' '), static::printListBlock($node->operationTypes), ], ' ' ); case $node instanceof SchemaExtensionNode: return static::join( [ 'extend schema', static::printList($node->directives, ' '), static::printListBlock($node->operationTypes), ], ' ' ); case $node instanceof SelectionSetNode: return static::printListBlock($node->selections); case $node instanceof StringValueNode: if ($node->block) { return BlockString::print($node->value); } return json_encode($node->value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); case $node instanceof UnionTypeDefinitionNode: $typesStr = static::printList($node->types, ' | '); return static::addDescription($node->description, static::join( [ 'union', static::p($node->name), static::printList($node->directives, ' '), $typesStr !== '' ? "= {$typesStr}" : '', ], ' ' )); case $node instanceof UnionTypeExtensionNode: $typesStr = static::printList($node->types, ' | '); return static::join( [ 'extend union', static::p($node->name), static::printList($node->directives, ' '), $typesStr !== '' ? "= {$typesStr}" : '', ], ' ' ); case $node instanceof VariableDefinitionNode: return '$' . static::p($node->variable->name) . ': ' . static::p($node->type) . static::wrap(' = ', static::p($node->defaultValue)) . static::wrap(' ', static::printList($node->directives, ' ')); case $node instanceof VariableNode: return '$' . static::p($node->name); } return ''; } /** * @template TNode of Node * * @param NodeList<TNode> $list * * @throws \JsonException */ protected static function printList(NodeList $list, string $separator = ''): string { $parts = []; foreach ($list as $item) { $parts[] = static::p($item); } return static::join($parts, $separator); } /** * Print each item on its own line, wrapped in an indented "{ }" block. * * @template TNode of Node * * @param NodeList<TNode> $list * * @throws \JsonException */ protected static function printListBlock(NodeList $list): string { if (count($list) === 0) { return ''; } $parts = []; foreach ($list as $item) { $parts[] = static::p($item); } return "{\n" . static::indent(static::join($parts, "\n")) . "\n}"; } /** @throws \JsonException */ protected static function addDescription(?StringValueNode $description, string $body): string { return static::join([static::p($description), $body], "\n"); } /** * If maybeString is not null or empty, then wrap with start and end, otherwise * print an empty string. */ protected static function wrap(string $start, ?string $maybeString, string $end = ''): string { if ($maybeString === null || $maybeString === '') { return ''; } return $start . $maybeString . $end; } protected static function indent(string $string): string { if ($string === '') { return ''; } return ' ' . str_replace("\n", "\n ", $string); } /** @param array<string|null> $parts */ protected static function join(array $parts, string $separator = ''): string { return implode($separator, array_filter($parts, static fn (?string $part) => $part !== '' && $part !== null)); } } graphql/lib/Language/Parser.php 0000644 00000170350 15166657210 0012457 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; use YOOtheme\GraphQL\Error\SyntaxError; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\BooleanValueNode; use YOOtheme\GraphQL\Language\AST\DefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\ExecutableDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FloatValueNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\ListTypeNode; use YOOtheme\GraphQL\Language\AST\ListValueNode; use YOOtheme\GraphQL\Language\AST\Location; use YOOtheme\GraphQL\Language\AST\NamedTypeNode; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\NonNullTypeNode; use YOOtheme\GraphQL\Language\AST\NullValueNode; use YOOtheme\GraphQL\Language\AST\ObjectFieldNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ObjectValueNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\OperationTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\SelectionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\AST\TypeExtensionNode; use YOOtheme\GraphQL\Language\AST\TypeNode; use YOOtheme\GraphQL\Language\AST\TypeSystemDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeSystemExtensionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ValueNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Language\AST\VariableNode; /** * Parses string containing GraphQL query language or [schema definition language](schema-definition-language.md) to Abstract Syntax Tree. * * @phpstan-type ParserOptions array{ * noLocation?: bool, * allowLegacySDLEmptyFields?: bool, * allowLegacySDLImplementsInterfaces?: bool, * experimentalFragmentVariables?: bool * } * * noLocation: * (By default, the parser creates AST nodes that know the location * in the source that they correspond to. This configuration flag * disables that behavior for performance or testing.) * * allowLegacySDLEmptyFields: * If enabled, the parser will parse empty fields sets in the Schema * Definition Language. Otherwise, the parser will follow the current * specification. * * This option is provided to ease adoption of the final SDL specification * and will be removed in a future major release. * * allowLegacySDLImplementsInterfaces: * If enabled, the parser will parse implemented interfaces with no `&` * character between each interface. Otherwise, the parser will follow the * current specification. * * This option is provided to ease adoption of the final SDL specification * and will be removed in a future major release. * * experimentalFragmentVariables: * (If enabled, the parser will understand and parse variable definitions * contained in a fragment definition. They'll be represented in the * `variableDefinitions` field of the FragmentDefinitionNode. * * The syntax is identical to normal, query-defined variables. For example: * * fragment A($var: Boolean = false) on T { * ... * } * * Note: this feature is experimental and may change or be removed in the * future.) * Those magic functions allow partial parsing: * * @method static NameNode name(Source|string $source, ParserOptions $options = []) * @method static ExecutableDefinitionNode|TypeSystemDefinitionNode definition(Source|string $source, ParserOptions $options = []) * @method static ExecutableDefinitionNode executableDefinition(Source|string $source, ParserOptions $options = []) * @method static OperationDefinitionNode operationDefinition(Source|string $source, ParserOptions $options = []) * @method static string operationType(Source|string $source, ParserOptions $options = []) * @method static NodeList<VariableDefinitionNode> variableDefinitions(Source|string $source, ParserOptions $options = []) * @method static VariableDefinitionNode variableDefinition(Source|string $source, ParserOptions $options = []) * @method static VariableNode variable(Source|string $source, ParserOptions $options = []) * @method static SelectionSetNode selectionSet(Source|string $source, ParserOptions $options = []) * @method static mixed selection(Source|string $source, ParserOptions $options = []) * @method static FieldNode field(Source|string $source, ParserOptions $options = []) * @method static NodeList<ArgumentNode> arguments(Source|string $source, ParserOptions $options = []) * @method static NodeList<ArgumentNode> constArguments(Source|string $source, ParserOptions $options = []) * @method static ArgumentNode argument(Source|string $source, ParserOptions $options = []) * @method static ArgumentNode constArgument(Source|string $source, ParserOptions $options = []) * @method static FragmentSpreadNode|InlineFragmentNode fragment(Source|string $source, ParserOptions $options = []) * @method static FragmentDefinitionNode fragmentDefinition(Source|string $source, ParserOptions $options = []) * @method static NameNode fragmentName(Source|string $source, ParserOptions $options = []) * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode valueLiteral(Source|string $source, ParserOptions $options = []) * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode constValueLiteral(Source|string $source, ParserOptions $options = []) * @method static StringValueNode stringLiteral(Source|string $source, ParserOptions $options = []) * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode constValue(Source|string $source, ParserOptions $options = []) * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode variableValue(Source|string $source, ParserOptions $options = []) * @method static ListValueNode array(Source|string $source, ParserOptions $options = []) * @method static ListValueNode constArray(Source|string $source, ParserOptions $options = []) * @method static ObjectValueNode object(Source|string $source, ParserOptions $options = []) * @method static ObjectValueNode constObject(Source|string $source, ParserOptions $options = []) * @method static ObjectFieldNode objectField(Source|string $source, ParserOptions $options = []) * @method static ObjectFieldNode constObjectField(Source|string $source, ParserOptions $options = []) * @method static NodeList<DirectiveNode> directives(Source|string $source, ParserOptions $options = []) * @method static NodeList<DirectiveNode> constDirectives(Source|string $source, ParserOptions $options = []) * @method static DirectiveNode directive(Source|string $source, ParserOptions $options = []) * @method static DirectiveNode constDirective(Source|string $source, ParserOptions $options = []) * @method static ListTypeNode|NamedTypeNode|NonNullTypeNode typeReference(Source|string $source, ParserOptions $options = []) * @method static NamedTypeNode namedType(Source|string $source, ParserOptions $options = []) * @method static TypeSystemDefinitionNode typeSystemDefinition(Source|string $source, ParserOptions $options = []) * @method static StringValueNode|null description(Source|string $source, ParserOptions $options = []) * @method static SchemaDefinitionNode schemaDefinition(Source|string $source, ParserOptions $options = []) * @method static OperationTypeDefinitionNode operationTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static ScalarTypeDefinitionNode scalarTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static ObjectTypeDefinitionNode objectTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static NodeList<NamedTypeNode> implementsInterfaces(Source|string $source, ParserOptions $options = []) * @method static NodeList<FieldDefinitionNode> fieldsDefinition(Source|string $source, ParserOptions $options = []) * @method static FieldDefinitionNode fieldDefinition(Source|string $source, ParserOptions $options = []) * @method static NodeList<InputValueDefinitionNode> argumentsDefinition(Source|string $source, ParserOptions $options = []) * @method static InputValueDefinitionNode inputValueDefinition(Source|string $source, ParserOptions $options = []) * @method static InterfaceTypeDefinitionNode interfaceTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static UnionTypeDefinitionNode unionTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static NodeList<NamedTypeNode> unionMemberTypes(Source|string $source, ParserOptions $options = []) * @method static EnumTypeDefinitionNode enumTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static NodeList<EnumValueDefinitionNode> enumValuesDefinition(Source|string $source, ParserOptions $options = []) * @method static EnumValueDefinitionNode enumValueDefinition(Source|string $source, ParserOptions $options = []) * @method static InputObjectTypeDefinitionNode inputObjectTypeDefinition(Source|string $source, ParserOptions $options = []) * @method static NodeList<InputValueDefinitionNode> inputFieldsDefinition(Source|string $source, ParserOptions $options = []) * @method static TypeExtensionNode typeExtension(Source|string $source, ParserOptions $options = []) * @method static SchemaExtensionNode schemaTypeExtension(Source|string $source, ParserOptions $options = []) * @method static ScalarTypeExtensionNode scalarTypeExtension(Source|string $source, ParserOptions $options = []) * @method static ObjectTypeExtensionNode objectTypeExtension(Source|string $source, ParserOptions $options = []) * @method static InterfaceTypeExtensionNode interfaceTypeExtension(Source|string $source, ParserOptions $options = []) * @method static UnionTypeExtensionNode unionTypeExtension(Source|string $source, ParserOptions $options = []) * @method static EnumTypeExtensionNode enumTypeExtension(Source|string $source, ParserOptions $options = []) * @method static InputObjectTypeExtensionNode inputObjectTypeExtension(Source|string $source, ParserOptions $options = []) * @method static DirectiveDefinitionNode directiveDefinition(Source|string $source, ParserOptions $options = []) * @method static NodeList<NameNode> directiveLocations(Source|string $source, ParserOptions $options = []) * @method static NameNode directiveLocation(Source|string $source, ParserOptions $options = []) * * @see \GraphQL\Tests\Language\ParserTest */ class Parser { /** * Given a GraphQL source, parses it into a `GraphQL\Language\AST\DocumentNode`. * * Throws `GraphQL\Error\SyntaxError` if a syntax error is encountered. * * @param Source|string $source * * @phpstan-param ParserOptions $options * * @api * * @throws \JsonException * @throws SyntaxError */ public static function parse($source, array $options = []): DocumentNode { return (new self($source, $options))->parseDocument(); } /** * Given a string containing a GraphQL value (ex. `[42]`), parse the AST for that value. * * Throws `GraphQL\Error\SyntaxError` if a syntax error is encountered. * * This is useful within tools that operate upon GraphQL Values directly and * in isolation of complete GraphQL documents. * * Consider providing the results to the utility function: `GraphQL\Utils\AST::valueFromAST()`. * * @param Source|string $source * * @phpstan-param ParserOptions $options * * @throws \JsonException * @throws SyntaxError * * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode * * @api */ public static function parseValue($source, array $options = []) { $parser = new Parser($source, $options); $parser->expect(Token::SOF); $value = $parser->parseValueLiteral(false); $parser->expect(Token::EOF); return $value; } /** * Given a string containing a GraphQL Type (ex. `[Int!]`), parse the AST for that type. * * Throws `GraphQL\Error\SyntaxError` if a syntax error is encountered. * * This is useful within tools that operate upon GraphQL Types directly and * in isolation of complete GraphQL documents. * * Consider providing the results to the utility function: `GraphQL\Utils\AST::typeFromAST()`. * * @param Source|string $source * * @phpstan-param ParserOptions $options * * @throws \JsonException * @throws SyntaxError * * @return ListTypeNode|NamedTypeNode|NonNullTypeNode * * @api */ public static function parseType($source, array $options = []) { $parser = new Parser($source, $options); $parser->expect(Token::SOF); $type = $parser->parseTypeReference(); $parser->expect(Token::EOF); return $type; } /** * Parse partial source by delegating calls to the internal parseX methods. * * @phpstan-param array{string, ParserOptions} $arguments * * @throws \JsonException * @throws SyntaxError * * @return Node|NodeList<Node> */ public static function __callStatic(string $name, array $arguments) { $parser = new Parser(...$arguments); $parser->expect(Token::SOF); switch ($name) { case 'arguments': $parsed = $parser->parseArguments(false); break; case 'valueLiteral': $parsed = $parser->parseValueLiteral(false); break; case 'array': $parsed = $parser->parseArray(false); break; case 'object': $parsed = $parser->parseObject(false); break; case 'objectField': $parsed = $parser->parseObjectField(false); break; case 'directives': $parsed = $parser->parseDirectives(false); break; case 'directive': $parsed = $parser->parseDirective(false); break; case 'constArguments': $parsed = $parser->parseArguments(true); break; case 'constValueLiteral': $parsed = $parser->parseValueLiteral(true); break; case 'constArray': $parsed = $parser->parseArray(true); break; case 'constObject': $parsed = $parser->parseObject(true); break; case 'constObjectField': $parsed = $parser->parseObjectField(true); break; case 'constDirectives': $parsed = $parser->parseDirectives(true); break; case 'constDirective': $parsed = $parser->parseDirective(true); break; default: $parsed = $parser->{'parse' . $name}(); } $parser->expect(Token::EOF); return $parsed; } private Lexer $lexer; /** * @param Source|string $source * * @phpstan-param ParserOptions $options */ public function __construct($source, array $options = []) { $sourceObj = $source instanceof Source ? $source : new Source($source); $this->lexer = new Lexer($sourceObj, $options); } /** * Returns a location object, used to identify the place in * the source that created a given parsed object. */ private function loc(Token $startToken): ?Location { if (! ($this->lexer->options['noLocation'] ?? false)) { return new Location($startToken, $this->lexer->lastToken, $this->lexer->source); } return null; } /** Determines if the next token is of a given kind. */ private function peek(string $kind): bool { return $this->lexer->token->kind === $kind; } /** * If the next token is of the given kind, return true after advancing * the parser. Otherwise, do not change the parser state and return false. * * @throws \JsonException * @throws SyntaxError */ private function skip(string $kind): bool { $match = $this->lexer->token->kind === $kind; if ($match) { $this->lexer->advance(); } return $match; } /** * If the next token is of the given kind, return that token after advancing * the parser. Otherwise, do not change the parser state and return false. * * @throws \JsonException * @throws SyntaxError */ private function expect(string $kind): Token { $token = $this->lexer->token; if ($token->kind === $kind) { $this->lexer->advance(); return $token; } throw new SyntaxError($this->lexer->source, $token->start, "Expected {$kind}, found {$token->getDescription()}"); } /** * If the next token is a keyword with the given value, advance the lexer. * Otherwise, throw an error. * * @throws \JsonException * @throws SyntaxError */ private function expectKeyword(string $value): void { $token = $this->lexer->token; if ($token->kind !== Token::NAME || $token->value !== $value) { throw new SyntaxError($this->lexer->source, $token->start, "Expected \"{$value}\", found {$token->getDescription()}"); } $this->lexer->advance(); } /** * If the next token is a given keyword, return "true" after advancing * the lexer. Otherwise, do not change the parser state and return "false". * * @throws \JsonException * @throws SyntaxError */ private function expectOptionalKeyword(string $value): bool { $token = $this->lexer->token; if ($token->kind === Token::NAME && $token->value === $value) { $this->lexer->advance(); return true; } return false; } private function unexpected(?Token $atToken = null): SyntaxError { $token = $atToken ?? $this->lexer->token; return new SyntaxError($this->lexer->source, $token->start, 'Unexpected ' . $token->getDescription()); } /** * Returns a possibly empty list of parse nodes, determined by * the parseFn. This list begins with a lex token of openKind * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * * @throws \JsonException * @throws SyntaxError * * @return NodeList<Node> */ private function any(string $openKind, callable $parseFn, string $closeKind): NodeList { $this->expect($openKind); $nodes = []; while (! $this->skip($closeKind)) { $nodes[] = $parseFn($this); } return new NodeList($nodes); } /** * Returns a non-empty list of parse nodes, determined by * the parseFn. This list begins with a lex token of openKind * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * * @template TNode of Node * * @param callable(self): TNode $parseFn * * @throws \JsonException * @throws SyntaxError * * @return NodeList<TNode> */ private function many(string $openKind, callable $parseFn, string $closeKind): NodeList { $this->expect($openKind); $nodes = [$parseFn($this)]; while (! $this->skip($closeKind)) { $nodes[] = $parseFn($this); } return new NodeList($nodes); } /** * Converts a name lex token into a name parse node. * * @throws \JsonException * @throws SyntaxError */ private function parseName(): NameNode { $token = $this->expect(Token::NAME); return new NameNode([ 'value' => $token->value, 'loc' => $this->loc($token), ]); } /** * Implements the parsing rules in the Document section. * * @throws \JsonException * @throws SyntaxError */ private function parseDocument(): DocumentNode { $start = $this->lexer->token; return new DocumentNode([ 'definitions' => $this->many( Token::SOF, fn (): DefinitionNode => $this->parseDefinition(), Token::EOF ), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError * * @return DefinitionNode&Node */ private function parseDefinition(): DefinitionNode { if ($this->peek(Token::NAME)) { switch ($this->lexer->token->value) { case 'query': case 'mutation': case 'subscription': case 'fragment': return $this->parseExecutableDefinition(); // Note: The schema definition language is an experimental addition. case 'schema': case 'scalar': case 'type': case 'interface': case 'union': case 'enum': case 'input': case 'directive': // Note: The schema definition language is an experimental addition. return $this->parseTypeSystemDefinition(); case 'extend': return $this->parseTypeSystemExtension(); } } elseif ($this->peek(Token::BRACE_L)) { return $this->parseExecutableDefinition(); } elseif ($this->peekDescription()) { // Note: The schema definition language is an experimental addition. return $this->parseTypeSystemDefinition(); } throw $this->unexpected(); } /** * @throws \JsonException * @throws SyntaxError * * @return ExecutableDefinitionNode&Node */ private function parseExecutableDefinition(): ExecutableDefinitionNode { if ($this->peek(Token::NAME)) { switch ($this->lexer->token->value) { case 'query': case 'mutation': case 'subscription': return $this->parseOperationDefinition(); case 'fragment': return $this->parseFragmentDefinition(); } } elseif ($this->peek(Token::BRACE_L)) { return $this->parseOperationDefinition(); } throw $this->unexpected(); } // Implements the parsing rules in the Operations section. /** * @throws \JsonException * @throws SyntaxError */ private function parseOperationDefinition(): OperationDefinitionNode { $start = $this->lexer->token; if ($this->peek(Token::BRACE_L)) { return new OperationDefinitionNode([ 'name' => null, 'operation' => 'query', 'variableDefinitions' => new NodeList([]), 'directives' => new NodeList([]), 'selectionSet' => $this->parseSelectionSet(), 'loc' => $this->loc($start), ]); } $operation = $this->parseOperationType(); $name = null; if ($this->peek(Token::NAME)) { $name = $this->parseName(); } return new OperationDefinitionNode([ 'name' => $name, 'operation' => $operation, 'variableDefinitions' => $this->parseVariableDefinitions(), 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->parseSelectionSet(), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseOperationType(): string { $operationToken = $this->expect(Token::NAME); switch ($operationToken->value) { case 'query': return 'query'; case 'mutation': return 'mutation'; case 'subscription': return 'subscription'; } throw $this->unexpected($operationToken); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<VariableDefinitionNode> */ private function parseVariableDefinitions(): NodeList { return $this->peek(Token::PAREN_L) ? $this->many( Token::PAREN_L, fn (): VariableDefinitionNode => $this->parseVariableDefinition(), Token::PAREN_R ) : new NodeList([]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseVariableDefinition(): VariableDefinitionNode { $start = $this->lexer->token; $var = $this->parseVariable(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); return new VariableDefinitionNode([ 'variable' => $var, 'type' => $type, 'defaultValue' => $this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null, 'directives' => $this->parseDirectives(true), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseVariable(): VariableNode { $start = $this->lexer->token; $this->expect(Token::DOLLAR); return new VariableNode([ 'name' => $this->parseName(), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseSelectionSet(): SelectionSetNode { $start = $this->lexer->token; return new SelectionSetNode( [ 'selections' => $this->many( Token::BRACE_L, fn (): SelectionNode => $this->parseSelection(), Token::BRACE_R ), 'loc' => $this->loc($start), ] ); } /** * @throws \JsonException * @throws SyntaxError * * @return SelectionNode&Node */ private function parseSelection(): SelectionNode { return $this->peek(Token::SPREAD) ? $this->parseFragment() : $this->parseField(); } /** * @throws \JsonException * @throws SyntaxError */ private function parseField(): FieldNode { $start = $this->lexer->token; $nameOrAlias = $this->parseName(); if ($this->skip(Token::COLON)) { $alias = $nameOrAlias; $name = $this->parseName(); } else { $alias = null; $name = $nameOrAlias; } return new FieldNode([ 'name' => $name, 'alias' => $alias, 'arguments' => $this->parseArguments(false), 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<ArgumentNode> */ private function parseArguments(bool $isConst): NodeList { $parseFn = $isConst ? fn (): ArgumentNode => $this->parseConstArgument() : fn (): ArgumentNode => $this->parseArgument(); return $this->peek(Token::PAREN_L) ? $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R) : new NodeList([]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseArgument(): ArgumentNode { $start = $this->lexer->token; $name = $this->parseName(); $this->expect(Token::COLON); $value = $this->parseValueLiteral(false); return new ArgumentNode([ 'name' => $name, 'value' => $value, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseConstArgument(): ArgumentNode { $start = $this->lexer->token; $name = $this->parseName(); $this->expect(Token::COLON); $value = $this->parseConstValue(); return new ArgumentNode([ 'name' => $name, 'value' => $value, 'loc' => $this->loc($start), ]); } // Implements the parsing rules in the Fragments section. /** * @throws \JsonException * @throws SyntaxError * * @return FragmentSpreadNode|InlineFragmentNode */ private function parseFragment(): SelectionNode { $start = $this->lexer->token; $this->expect(Token::SPREAD); $hasTypeCondition = $this->expectOptionalKeyword('on'); if (! $hasTypeCondition && $this->peek(Token::NAME)) { return new FragmentSpreadNode([ 'name' => $this->parseFragmentName(), 'directives' => $this->parseDirectives(false), 'loc' => $this->loc($start), ]); } return new InlineFragmentNode([ 'typeCondition' => $hasTypeCondition ? $this->parseNamedType() : null, 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->parseSelectionSet(), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseFragmentDefinition(): FragmentDefinitionNode { $start = $this->lexer->token; $this->expectKeyword('fragment'); $name = $this->parseFragmentName(); // Experimental support for defining variables within fragments changes // the grammar of FragmentDefinition: // - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet $variableDefinitions = isset($this->lexer->options['experimentalFragmentVariables']) ? $this->parseVariableDefinitions() : null; $this->expectKeyword('on'); $typeCondition = $this->parseNamedType(); return new FragmentDefinitionNode([ 'name' => $name, 'variableDefinitions' => $variableDefinitions, 'typeCondition' => $typeCondition, 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->parseSelectionSet(), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseFragmentName(): NameNode { if ($this->lexer->token->value === 'on') { throw $this->unexpected(); } return $this->parseName(); } // Implements the parsing rules in the Values section. /** * Value[Const] : * - [~Const] Variable * - IntValue * - FloatValue * - StringValue * - BooleanValue * - NullValue * - EnumValue * - ListValue[?Const] * - ObjectValue[?Const]. * * BooleanValue : one of `true` `false` * * NullValue : `null` * * EnumValue : Name but not `true`, `false` or `null` * * @throws \JsonException * @throws SyntaxError * * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode|ListValueNode|ObjectValueNode|NullValueNode */ private function parseValueLiteral(bool $isConst): ValueNode { $token = $this->lexer->token; switch ($token->kind) { case Token::BRACKET_L: return $this->parseArray($isConst); case Token::BRACE_L: return $this->parseObject($isConst); case Token::INT: $this->lexer->advance(); return new IntValueNode([ 'value' => $token->value, 'loc' => $this->loc($token), ]); case Token::FLOAT: $this->lexer->advance(); return new FloatValueNode([ 'value' => $token->value, 'loc' => $this->loc($token), ]); case Token::STRING: case Token::BLOCK_STRING: return $this->parseStringLiteral(); case Token::NAME: if ($token->value === 'true' || $token->value === 'false') { $this->lexer->advance(); return new BooleanValueNode([ 'value' => $token->value === 'true', 'loc' => $this->loc($token), ]); } if ($token->value === 'null') { $this->lexer->advance(); return new NullValueNode([ 'loc' => $this->loc($token), ]); } $this->lexer->advance(); return new EnumValueNode([ 'value' => $token->value, 'loc' => $this->loc($token), ]); case Token::DOLLAR: if (! $isConst) { return $this->parseVariable(); } break; } throw $this->unexpected(); } /** * @throws \JsonException * @throws SyntaxError */ private function parseStringLiteral(): StringValueNode { $token = $this->lexer->token; $this->lexer->advance(); return new StringValueNode([ 'value' => $token->value, 'block' => $token->kind === Token::BLOCK_STRING, 'loc' => $this->loc($token), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseConstValue(): ValueNode { return $this->parseValueLiteral(true); } /** * @throws \JsonException * @throws SyntaxError */ private function parseVariableValue(): ValueNode { return $this->parseValueLiteral(false); } /** * @throws \JsonException * @throws SyntaxError */ private function parseArray(bool $isConst): ListValueNode { $start = $this->lexer->token; $parseFn = $isConst ? fn (): ValueNode => $this->parseConstValue() : fn (): ValueNode => $this->parseVariableValue(); return new ListValueNode([ 'values' => $this->any(Token::BRACKET_L, $parseFn, Token::BRACKET_R), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseObject(bool $isConst): ObjectValueNode { $start = $this->lexer->token; $this->expect(Token::BRACE_L); $fields = []; while (! $this->skip(Token::BRACE_R)) { $fields[] = $this->parseObjectField($isConst); } return new ObjectValueNode([ 'fields' => new NodeList($fields), 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseObjectField(bool $isConst): ObjectFieldNode { $start = $this->lexer->token; $name = $this->parseName(); $this->expect(Token::COLON); return new ObjectFieldNode([ 'name' => $name, 'value' => $this->parseValueLiteral($isConst), 'loc' => $this->loc($start), ]); } // Implements the parsing rules in the Directives section. /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<DirectiveNode> */ private function parseDirectives(bool $isConst): NodeList { $directives = []; while ($this->peek(Token::AT)) { $directives[] = $this->parseDirective($isConst); } return new NodeList($directives); } /** * @throws \JsonException * @throws SyntaxError */ private function parseDirective(bool $isConst): DirectiveNode { $start = $this->lexer->token; $this->expect(Token::AT); return new DirectiveNode([ 'name' => $this->parseName(), 'arguments' => $this->parseArguments($isConst), 'loc' => $this->loc($start), ]); } // Implements the parsing rules in the Types section. /** * Handles the Type: TypeName, ListType, and NonNullType parsing rules. * * @throws \JsonException * @throws SyntaxError * * @return ListTypeNode|NamedTypeNode|NonNullTypeNode */ private function parseTypeReference(): TypeNode { $start = $this->lexer->token; if ($this->skip(Token::BRACKET_L)) { $type = $this->parseTypeReference(); $this->expect(Token::BRACKET_R); $type = new ListTypeNode([ 'type' => $type, 'loc' => $this->loc($start), ]); } else { $type = $this->parseNamedType(); } if ($this->skip(Token::BANG)) { return new NonNullTypeNode([ 'type' => $type, 'loc' => $this->loc($start), ]); } return $type; } /** * @throws \JsonException * @throws SyntaxError */ private function parseNamedType(): NamedTypeNode { $start = $this->lexer->token; return new NamedTypeNode([ 'name' => $this->parseName(), 'loc' => $this->loc($start), ]); } // Implements the parsing rules in the Type Definition section. /** * @throws \JsonException * @throws SyntaxError * * @return TypeSystemDefinitionNode&Node */ private function parseTypeSystemDefinition(): TypeSystemDefinitionNode { // Many definitions begin with a description and require a lookahead. $keywordToken = $this->peekDescription() ? $this->lexer->lookahead() : $this->lexer->token; if ($keywordToken->kind === Token::NAME) { switch ($keywordToken->value) { case 'schema': return $this->parseSchemaDefinition(); case 'scalar': return $this->parseScalarTypeDefinition(); case 'type': return $this->parseObjectTypeDefinition(); case 'interface': return $this->parseInterfaceTypeDefinition(); case 'union': return $this->parseUnionTypeDefinition(); case 'enum': return $this->parseEnumTypeDefinition(); case 'input': return $this->parseInputObjectTypeDefinition(); case 'directive': return $this->parseDirectiveDefinition(); } } throw $this->unexpected($keywordToken); } private function peekDescription(): bool { return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING); } /** * @throws \JsonException * @throws SyntaxError */ private function parseDescription(): ?StringValueNode { if ($this->peekDescription()) { return $this->parseStringLiteral(); } return null; } /** * @throws \JsonException * @throws SyntaxError */ private function parseSchemaDefinition(): SchemaDefinitionNode { $start = $this->lexer->token; $this->expectKeyword('schema'); $directives = $this->parseDirectives(true); $operationTypes = $this->many( Token::BRACE_L, fn (): OperationTypeDefinitionNode => $this->parseOperationTypeDefinition(), Token::BRACE_R ); return new SchemaDefinitionNode([ 'directives' => $directives, 'operationTypes' => $operationTypes, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseOperationTypeDefinition(): OperationTypeDefinitionNode { $start = $this->lexer->token; $operation = $this->parseOperationType(); $this->expect(Token::COLON); $type = $this->parseNamedType(); return new OperationTypeDefinitionNode([ 'operation' => $operation, 'type' => $type, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseScalarTypeDefinition(): ScalarTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('scalar'); $name = $this->parseName(); $directives = $this->parseDirectives(true); return new ScalarTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseObjectTypeDefinition(): ObjectTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('type'); $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); return new ObjectTypeDefinitionNode([ 'name' => $name, 'interfaces' => $interfaces, 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<NamedTypeNode> */ private function parseImplementsInterfaces(): NodeList { $types = []; if ($this->expectOptionalKeyword('implements')) { // Optional leading ampersand $this->skip(Token::AMP); do { $types[] = $this->parseNamedType(); } while ( $this->skip(Token::AMP) // Legacy support for the SDL? || (($this->lexer->options['allowLegacySDLImplementsInterfaces'] ?? false) && $this->peek(Token::NAME)) ); } return new NodeList($types); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<FieldDefinitionNode> */ private function parseFieldsDefinition(): NodeList { // Legacy support for the SDL? if ( ($this->lexer->options['allowLegacySDLEmptyFields'] ?? false) && $this->peek(Token::BRACE_L) && $this->lexer->lookahead()->kind === Token::BRACE_R ) { $this->lexer->advance(); $this->lexer->advance(); /** @phpstan-var NodeList<FieldDefinitionNode> $nodeList */ $nodeList = new NodeList([]); } else { /** @phpstan-var NodeList<FieldDefinitionNode> $nodeList */ $nodeList = $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, fn (): FieldDefinitionNode => $this->parseFieldDefinition(), Token::BRACE_R ) : new NodeList([]); } return $nodeList; } /** * @throws \JsonException * @throws SyntaxError */ private function parseFieldDefinition(): FieldDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $name = $this->parseName(); $args = $this->parseArgumentsDefinition(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); $directives = $this->parseDirectives(true); return new FieldDefinitionNode([ 'name' => $name, 'arguments' => $args, 'type' => $type, 'directives' => $directives, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<InputValueDefinitionNode> */ private function parseArgumentsDefinition(): NodeList { return $this->peek(Token::PAREN_L) ? $this->many( Token::PAREN_L, fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(), Token::PAREN_R ) : new NodeList([]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseInputValueDefinition(): InputValueDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $name = $this->parseName(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); $defaultValue = null; if ($this->skip(Token::EQUALS)) { $defaultValue = $this->parseConstValue(); } $directives = $this->parseDirectives(true); return new InputValueDefinitionNode([ 'name' => $name, 'type' => $type, 'defaultValue' => $defaultValue, 'directives' => $directives, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('interface'); $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); return new InterfaceTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * UnionTypeDefinition : * - Description? union Name Directives[Const]? UnionMemberTypes? * * @throws \JsonException * @throws SyntaxError */ private function parseUnionTypeDefinition(): UnionTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('union'); $name = $this->parseName(); $directives = $this->parseDirectives(true); $types = $this->parseUnionMemberTypes(); return new UnionTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, 'types' => $types, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<NamedTypeNode> */ private function parseUnionMemberTypes(): NodeList { $types = []; if ($this->skip(Token::EQUALS)) { // Optional leading pipe $this->skip(Token::PIPE); do { $types[] = $this->parseNamedType(); } while ($this->skip(Token::PIPE)); } return new NodeList($types); } /** * @throws \JsonException * @throws SyntaxError */ private function parseEnumTypeDefinition(): EnumTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('enum'); $name = $this->parseName(); $directives = $this->parseDirectives(true); $values = $this->parseEnumValuesDefinition(); return new EnumTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, 'values' => $values, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<EnumValueDefinitionNode> */ private function parseEnumValuesDefinition(): NodeList { return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, fn (): EnumValueDefinitionNode => $this->parseEnumValueDefinition(), Token::BRACE_R ) : new NodeList([]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseEnumValueDefinition(): EnumValueDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $name = $this->parseName(); $directives = $this->parseDirectives(true); return new EnumValueDefinitionNode([ 'name' => $name, 'directives' => $directives, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('input'); $name = $this->parseName(); $directives = $this->parseDirectives(true); $fields = $this->parseInputFieldsDefinition(); return new InputObjectTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start), 'description' => $description, ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<InputValueDefinitionNode> */ private function parseInputFieldsDefinition(): NodeList { return $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(), Token::BRACE_R ) : new NodeList([]); } /** * @throws \JsonException * @throws SyntaxError * * @return TypeSystemExtensionNode&Node */ private function parseTypeSystemExtension(): TypeSystemExtensionNode { $keywordToken = $this->lexer->lookahead(); if ($keywordToken->kind === Token::NAME) { switch ($keywordToken->value) { case 'schema': return $this->parseSchemaTypeExtension(); case 'scalar': return $this->parseScalarTypeExtension(); case 'type': return $this->parseObjectTypeExtension(); case 'interface': return $this->parseInterfaceTypeExtension(); case 'union': return $this->parseUnionTypeExtension(); case 'enum': return $this->parseEnumTypeExtension(); case 'input': return $this->parseInputObjectTypeExtension(); } } throw $this->unexpected($keywordToken); } /** * @throws \JsonException * @throws SyntaxError */ private function parseSchemaTypeExtension(): SchemaExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('schema'); $directives = $this->parseDirectives(true); $operationTypes = $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, fn (): OperationTypeDefinitionNode => $this->parseOperationTypeDefinition(), Token::BRACE_R ) : new NodeList([]); if (count($directives) === 0 && count($operationTypes) === 0) { $this->unexpected(); } return new SchemaExtensionNode([ 'directives' => $directives, 'operationTypes' => $operationTypes, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseScalarTypeExtension(): ScalarTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('scalar'); $name = $this->parseName(); $directives = $this->parseDirectives(true); if (count($directives) === 0) { throw $this->unexpected(); } return new ScalarTypeExtensionNode([ 'name' => $name, 'directives' => $directives, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseObjectTypeExtension(): ObjectTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('type'); $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); if ( count($interfaces) === 0 && count($directives) === 0 && count($fields) === 0 ) { throw $this->unexpected(); } return new ObjectTypeExtensionNode([ 'name' => $name, 'interfaces' => $interfaces, 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseInterfaceTypeExtension(): InterfaceTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('interface'); $name = $this->parseName(); $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); if ( count($interfaces) === 0 && count($directives) === 0 && count($fields) === 0 ) { throw $this->unexpected(); } return new InterfaceTypeExtensionNode([ 'name' => $name, 'directives' => $directives, 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), ]); } /** * UnionTypeExtension : * - extend union Name Directives[Const]? UnionMemberTypes * - extend union Name Directives[Const]. * * @throws \JsonException * @throws SyntaxError */ private function parseUnionTypeExtension(): UnionTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('union'); $name = $this->parseName(); $directives = $this->parseDirectives(true); $types = $this->parseUnionMemberTypes(); if (count($directives) === 0 && count($types) === 0) { throw $this->unexpected(); } return new UnionTypeExtensionNode([ 'name' => $name, 'directives' => $directives, 'types' => $types, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseEnumTypeExtension(): EnumTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('enum'); $name = $this->parseName(); $directives = $this->parseDirectives(true); $values = $this->parseEnumValuesDefinition(); if ( count($directives) === 0 && count($values) === 0 ) { throw $this->unexpected(); } return new EnumTypeExtensionNode([ 'name' => $name, 'directives' => $directives, 'values' => $values, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError */ private function parseInputObjectTypeExtension(): InputObjectTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('input'); $name = $this->parseName(); $directives = $this->parseDirectives(true); $fields = $this->parseInputFieldsDefinition(); if ( count($directives) === 0 && count($fields) === 0 ) { throw $this->unexpected(); } return new InputObjectTypeExtensionNode([ 'name' => $name, 'directives' => $directives, 'fields' => $fields, 'loc' => $this->loc($start), ]); } /** * DirectiveDefinition : * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations. * * @throws \JsonException * @throws SyntaxError */ private function parseDirectiveDefinition(): DirectiveDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('directive'); $this->expect(Token::AT); $name = $this->parseName(); $args = $this->parseArgumentsDefinition(); $repeatable = $this->expectOptionalKeyword('repeatable'); $this->expectKeyword('on'); $locations = $this->parseDirectiveLocations(); return new DirectiveDefinitionNode([ 'name' => $name, 'description' => $description, 'arguments' => $args, 'repeatable' => $repeatable, 'locations' => $locations, 'loc' => $this->loc($start), ]); } /** * @throws \JsonException * @throws SyntaxError * * @return NodeList<NameNode> */ private function parseDirectiveLocations(): NodeList { // Optional leading pipe $this->skip(Token::PIPE); $locations = []; do { $locations[] = $this->parseDirectiveLocation(); } while ($this->skip(Token::PIPE)); return new NodeList($locations); } /** * @throws \JsonException * @throws SyntaxError */ private function parseDirectiveLocation(): NameNode { $start = $this->lexer->token; $name = $this->parseName(); if (DirectiveLocation::has($name->value)) { return $name; } throw $this->unexpected($start); } } graphql/lib/Language/SourceLocation.php 0000644 00000001450 15166657210 0014146 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; class SourceLocation implements \JsonSerializable { public int $line; public int $column; public function __construct(int $line, int $col) { $this->line = $line; $this->column = $col; } /** @return array{line: int, column: int} */ public function toArray(): array { return [ 'line' => $this->line, 'column' => $this->column, ]; } /** @return array{line: int, column: int} */ public function toSerializableArray(): array { return $this->toArray(); } /** @return array{line: int, column: int} */ #[\ReturnTypeWillChange] public function jsonSerialize(): array { return $this->toArray(); } } graphql/lib/Language/DirectiveLocation.php 0000644 00000004305 15166657210 0014626 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; /** * Enumeration of available directive locations. */ class DirectiveLocation { public const QUERY = 'QUERY'; public const MUTATION = 'MUTATION'; public const SUBSCRIPTION = 'SUBSCRIPTION'; public const FIELD = 'FIELD'; public const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION'; public const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD'; public const INLINE_FRAGMENT = 'INLINE_FRAGMENT'; public const VARIABLE_DEFINITION = 'VARIABLE_DEFINITION'; public const EXECUTABLE_LOCATIONS = [ self::QUERY => self::QUERY, self::MUTATION => self::MUTATION, self::SUBSCRIPTION => self::SUBSCRIPTION, self::FIELD => self::FIELD, self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION, self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD, self::INLINE_FRAGMENT => self::INLINE_FRAGMENT, self::VARIABLE_DEFINITION => self::VARIABLE_DEFINITION, ]; public const SCHEMA = 'SCHEMA'; public const SCALAR = 'SCALAR'; public const OBJECT = 'OBJECT'; public const FIELD_DEFINITION = 'FIELD_DEFINITION'; public const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION'; public const IFACE = 'INTERFACE'; public const UNION = 'UNION'; public const ENUM = 'ENUM'; public const ENUM_VALUE = 'ENUM_VALUE'; public const INPUT_OBJECT = 'INPUT_OBJECT'; public const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION'; public const TYPE_SYSTEM_LOCATIONS = [ self::SCHEMA => self::SCHEMA, self::SCALAR => self::SCALAR, self::OBJECT => self::OBJECT, self::FIELD_DEFINITION => self::FIELD_DEFINITION, self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION, self::IFACE => self::IFACE, self::UNION => self::UNION, self::ENUM => self::ENUM, self::ENUM_VALUE => self::ENUM_VALUE, self::INPUT_OBJECT => self::INPUT_OBJECT, self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION, ]; public const LOCATIONS = self::EXECUTABLE_LOCATIONS + self::TYPE_SYSTEM_LOCATIONS; public static function has(string $name): bool { return isset(self::LOCATIONS[$name]); } } graphql/lib/Language/AST/ObjectFieldNode.php 0000644 00000000561 15166657210 0014626 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ObjectFieldNode extends Node { public string $kind = NodeKind::OBJECT_FIELD; public NameNode $name; /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */ public ValueNode $value; } graphql/lib/Language/AST/InputValueDefinitionNode.php 0000644 00000001153 15166657210 0016557 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class InputValueDefinitionNode extends Node { public string $kind = NodeKind::INPUT_VALUE_DEFINITION; public NameNode $name; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public TypeNode $type; /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */ public ?ValueNode $defaultValue = null; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public ?StringValueNode $description = null; } graphql/lib/Language/AST/ScalarTypeExtensionNode.php 0000644 00000000610 15166657210 0016413 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ScalarTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::SCALAR_TYPE_EXTENSION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/FragmentSpreadNode.php 0000644 00000000666 15166657210 0015364 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class FragmentSpreadNode extends Node implements SelectionNode { public string $kind = NodeKind::FRAGMENT_SPREAD; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } } graphql/lib/Language/AST/InterfaceTypeExtensionNode.php 0000644 00000001044 15166657210 0017110 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::INTERFACE_TYPE_EXTENSION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<NamedTypeNode> */ public NodeList $interfaces; /** @var NodeList<FieldDefinitionNode> */ public NodeList $fields; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/BooleanValueNode.php 0000644 00000000315 15166657210 0015025 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class BooleanValueNode extends Node implements ValueNode { public string $kind = NodeKind::BOOLEAN; public bool $value; } graphql/lib/Language/AST/TypeDefinitionNode.php 0000644 00000000626 15166657210 0015410 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type TypeDefinitionNode = ScalarTypeDefinitionNode * | ObjectTypeDefinitionNode * | InterfaceTypeDefinitionNode * | UnionTypeDefinitionNode * | EnumTypeDefinitionNode * | InputObjectTypeDefinitionNode. */ interface TypeDefinitionNode extends TypeSystemDefinitionNode { public function getName(): NameNode; } graphql/lib/Language/AST/EnumTypeDefinitionNode.php 0000644 00000001227 15166657210 0016233 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::ENUM_TYPE_DEFINITION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<EnumValueDefinitionNode> */ public NodeList $values; public ?StringValueNode $description = null; public function getName(): NameNode { return $this->name; } public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } } graphql/lib/Language/AST/InlineFragmentNode.php 0000644 00000000770 15166657210 0015360 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class InlineFragmentNode extends Node implements SelectionNode { public string $kind = NodeKind::INLINE_FRAGMENT; public ?NamedTypeNode $typeCondition = null; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public SelectionSetNode $selectionSet; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } } graphql/lib/Language/AST/TypeExtensionNode.php 0000644 00000000623 15166657210 0015271 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type TypeExtensionNode = * | ScalarTypeExtensionNode * | ObjectTypeExtensionNode * | InterfaceTypeExtensionNode * | UnionTypeExtensionNode * | EnumTypeExtensionNode * | InputObjectTypeExtensionNode;. */ interface TypeExtensionNode extends TypeSystemExtensionNode { public function getName(): NameNode; } graphql/lib/Language/AST/NonNullTypeNode.php 0000644 00000000377 15166657210 0014710 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class NonNullTypeNode extends Node implements TypeNode { public string $kind = NodeKind::NON_NULL_TYPE; /** @var NamedTypeNode|ListTypeNode */ public TypeNode $type; } graphql/lib/Language/AST/ListTypeNode.php 0000644 00000000410 15166657210 0014222 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ListTypeNode extends Node implements TypeNode { public string $kind = NodeKind::LIST_TYPE; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public TypeNode $type; } graphql/lib/Language/AST/ExecutableDefinitionNode.php 0000644 00000000370 15166657210 0016544 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type ExecutableDefinitionNode = * | OperationDefinitionNode * | FragmentDefinitionNode;. */ interface ExecutableDefinitionNode extends DefinitionNode {} graphql/lib/Language/AST/DirectiveDefinitionNode.php 0000644 00000001150 15166657210 0016376 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode { public string $kind = NodeKind::DIRECTIVE_DEFINITION; public NameNode $name; public ?StringValueNode $description = null; /** @var NodeList<InputValueDefinitionNode> */ public NodeList $arguments; public bool $repeatable; /** @var NodeList<NameNode> */ public NodeList $locations; public function __construct(array $vars) { parent::__construct($vars); $this->arguments ??= new NodeList([]); } } graphql/lib/Language/AST/DirectiveNode.php 0000644 00000000617 15166657210 0014374 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class DirectiveNode extends Node { public string $kind = NodeKind::DIRECTIVE; public NameNode $name; /** @var NodeList<ArgumentNode> */ public NodeList $arguments; public function __construct(array $vars) { parent::__construct($vars); $this->arguments ??= new NodeList([]); } } graphql/lib/Language/AST/FragmentDefinitionNode.php 0000644 00000001764 15166657210 0016236 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet { public string $kind = NodeKind::FRAGMENT_DEFINITION; public NameNode $name; /** * Note: fragment variable definitions are experimental and may be changed * or removed in the future. * * Thus, this property is the single exception where this is not always a NodeList but may be null. * * @var NodeList<VariableDefinitionNode>|null */ public ?NodeList $variableDefinitions = null; public NamedTypeNode $typeCondition; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public SelectionSetNode $selectionSet; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } public function getSelectionSet(): SelectionSetNode { return $this->selectionSet; } } graphql/lib/Language/AST/EnumValueNode.php 0000644 00000000311 15166657210 0014346 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class EnumValueNode extends Node implements ValueNode { public string $kind = NodeKind::ENUM; public string $value; } graphql/lib/Language/AST/InputObjectTypeExtensionNode.php 0000644 00000000744 15166657210 0017444 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<InputValueDefinitionNode> */ public NodeList $fields; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/ScalarTypeDefinitionNode.php 0000644 00000000675 15166657210 0016542 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::SCALAR_TYPE_DEFINITION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public ?StringValueNode $description = null; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/UnionTypeExtensionNode.php 0000644 00000000713 15166657210 0016302 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class UnionTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::UNION_TYPE_EXTENSION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<NamedTypeNode> */ public NodeList $types; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/ObjectValueNode.php 0000644 00000000372 15166657210 0014657 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ObjectValueNode extends Node implements ValueNode { public string $kind = NodeKind::OBJECT; /** @var NodeList<ObjectFieldNode> */ public NodeList $fields; } graphql/lib/Language/AST/OperationDefinitionNode.php 0000644 00000001624 15166657210 0016426 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * @phpstan-type OperationType 'query'|'mutation'|'subscription' */ class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet { public string $kind = NodeKind::OPERATION_DEFINITION; public ?NameNode $name = null; /** @var OperationType */ public string $operation; /** @var NodeList<VariableDefinitionNode> */ public NodeList $variableDefinitions; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public SelectionSetNode $selectionSet; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); $this->variableDefinitions ??= new NodeList([]); } public function getSelectionSet(): SelectionSetNode { return $this->selectionSet; } } graphql/lib/Language/AST/NodeList.php 0000644 00000007560 15166657210 0013375 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Utils\AST; /** * @template T of Node * * @phpstan-implements \ArrayAccess<array-key, T> * @phpstan-implements \IteratorAggregate<array-key, T> */ class NodeList implements \ArrayAccess, \IteratorAggregate, \Countable { /** * @var array<Node|array> * * @phpstan-var array<T|array<string, mixed>> */ private array $nodes; /** * @param array<Node|array> $nodes * * @phpstan-param array<T|array<string, mixed>> $nodes */ public function __construct(array $nodes) { $this->nodes = $nodes; } /** @param int|string $offset */ #[\ReturnTypeWillChange] public function offsetExists($offset): bool { return isset($this->nodes[$offset]); } /** * @param int|string $offset * * @phpstan-return T */ #[\ReturnTypeWillChange] public function offsetGet($offset): Node { $item = $this->nodes[$offset]; if (is_array($item)) { // @phpstan-ignore-next-line not really possible to express the correctness of this in PHP return $this->nodes[$offset] = AST::fromArray($item); } return $item; } /** * @param int|string|null $offset * @param Node|array<string, mixed> $value * * @phpstan-param T|array<string, mixed> $value * * @throws \JsonException * @throws InvariantViolation */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { if (is_array($value)) { /** @phpstan-var T $value */ $value = AST::fromArray($value); } // Happens when a Node is pushed via []= if ($offset === null) { $this->nodes[] = $value; return; } $this->nodes[$offset] = $value; } /** @param int|string $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { unset($this->nodes[$offset]); } public function getIterator(): \Traversable { foreach ($this->nodes as $key => $_) { yield $key => $this->offsetGet($key); } } public function count(): int { return count($this->nodes); } /** * Remove a portion of the NodeList and replace it with something else. * * @param T|iterable<T>|null $replacement * * @phpstan-return NodeList<T> the NodeList with the extracted elements */ public function splice(int $offset, int $length, $replacement = null): NodeList { if (is_iterable($replacement) && ! is_array($replacement)) { $replacement = iterator_to_array($replacement); } return new NodeList( array_splice($this->nodes, $offset, $length, $replacement) ); } /** * @phpstan-param iterable<array-key, T> $list * * @phpstan-return NodeList<T> */ public function merge(iterable $list): NodeList { if (! is_array($list)) { $list = iterator_to_array($list); } return new NodeList(array_merge($this->nodes, $list)); } /** Resets the keys of the stored nodes to contiguous numeric indexes. */ public function reindex(): void { $this->nodes = array_values($this->nodes); } /** * Returns a clone of this instance and all its children, except Location $loc. * * @throws \JsonException * @throws InvariantViolation * * @return static<T> */ public function cloneDeep(): self { /** @var array<T> $empty */ $empty = []; $cloned = new static($empty); foreach ($this->getIterator() as $key => $node) { $cloned[$key] = $node->cloneDeep(); } return $cloned; } } graphql/lib/Language/AST/DocumentNode.php 0000644 00000000355 15166657210 0014233 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class DocumentNode extends Node { public string $kind = NodeKind::DOCUMENT; /** @var NodeList<DefinitionNode&Node> */ public NodeList $definitions; } graphql/lib/Language/AST/SelectionNode.php 0000644 00000000301 15166657210 0014371 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode. */ interface SelectionNode {} graphql/lib/Language/AST/NodeKind.php 0000644 00000012572 15166657210 0013346 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * Holds constants of possible AST nodes. */ class NodeKind { // constants from language/kinds.js: public const NAME = 'Name'; // Document public const DOCUMENT = 'Document'; public const OPERATION_DEFINITION = 'OperationDefinition'; public const VARIABLE_DEFINITION = 'VariableDefinition'; public const VARIABLE = 'Variable'; public const SELECTION_SET = 'SelectionSet'; public const FIELD = 'Field'; public const ARGUMENT = 'Argument'; // Fragments public const FRAGMENT_SPREAD = 'FragmentSpread'; public const INLINE_FRAGMENT = 'InlineFragment'; public const FRAGMENT_DEFINITION = 'FragmentDefinition'; // Values public const INT = 'IntValue'; public const FLOAT = 'FloatValue'; public const STRING = 'StringValue'; public const BOOLEAN = 'BooleanValue'; public const ENUM = 'EnumValue'; public const NULL = 'NullValue'; public const LST = 'ListValue'; public const OBJECT = 'ObjectValue'; public const OBJECT_FIELD = 'ObjectField'; // Directives public const DIRECTIVE = 'Directive'; // Types public const NAMED_TYPE = 'NamedType'; public const LIST_TYPE = 'ListType'; public const NON_NULL_TYPE = 'NonNullType'; // Type System Definitions public const SCHEMA_DEFINITION = 'SchemaDefinition'; public const OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition'; // Type Definitions public const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition'; public const OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition'; public const FIELD_DEFINITION = 'FieldDefinition'; public const INPUT_VALUE_DEFINITION = 'InputValueDefinition'; public const INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition'; public const UNION_TYPE_DEFINITION = 'UnionTypeDefinition'; public const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition'; public const ENUM_VALUE_DEFINITION = 'EnumValueDefinition'; public const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition'; // Type Extensions public const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension'; public const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension'; public const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension'; public const UNION_TYPE_EXTENSION = 'UnionTypeExtension'; public const ENUM_TYPE_EXTENSION = 'EnumTypeExtension'; public const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension'; // Directive Definitions public const DIRECTIVE_DEFINITION = 'DirectiveDefinition'; // Type System Extensions public const SCHEMA_EXTENSION = 'SchemaExtension'; public const CLASS_MAP = [ self::NAME => NameNode::class, // Document self::DOCUMENT => DocumentNode::class, self::OPERATION_DEFINITION => OperationDefinitionNode::class, self::VARIABLE_DEFINITION => VariableDefinitionNode::class, self::VARIABLE => VariableNode::class, self::SELECTION_SET => SelectionSetNode::class, self::FIELD => FieldNode::class, self::ARGUMENT => ArgumentNode::class, // Fragments self::FRAGMENT_SPREAD => FragmentSpreadNode::class, self::INLINE_FRAGMENT => InlineFragmentNode::class, self::FRAGMENT_DEFINITION => FragmentDefinitionNode::class, // Values self::INT => IntValueNode::class, self::FLOAT => FloatValueNode::class, self::STRING => StringValueNode::class, self::BOOLEAN => BooleanValueNode::class, self::ENUM => EnumValueNode::class, self::NULL => NullValueNode::class, self::LST => ListValueNode::class, self::OBJECT => ObjectValueNode::class, self::OBJECT_FIELD => ObjectFieldNode::class, // Directives self::DIRECTIVE => DirectiveNode::class, // Types self::NAMED_TYPE => NamedTypeNode::class, self::LIST_TYPE => ListTypeNode::class, self::NON_NULL_TYPE => NonNullTypeNode::class, // Type System Definitions self::SCHEMA_DEFINITION => SchemaDefinitionNode::class, self::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class, // Type Definitions self::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class, self::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class, self::FIELD_DEFINITION => FieldDefinitionNode::class, self::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class, self::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class, self::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class, self::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class, self::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class, self::INPUT_OBJECT_TYPE_DEFINITION => InputObjectTypeDefinitionNode::class, // Type Extensions self::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class, self::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class, self::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class, self::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class, self::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class, self::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class, // Directive Definitions self::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class, ]; } graphql/lib/Language/AST/ValueNode.php 0000644 00000000461 15166657210 0013527 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type ValueNode = VariableNode * | NullValueNode * | IntValueNode * | FloatValueNode * | StringValueNode * | BooleanValueNode * | EnumValueNode * | ListValueNode * | ObjectValueNode. */ interface ValueNode {} graphql/lib/Language/AST/FloatValueNode.php 0000644 00000000313 15166657210 0014511 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class FloatValueNode extends Node implements ValueNode { public string $kind = NodeKind::FLOAT; public string $value; } graphql/lib/Language/AST/DefinitionNode.php 0000644 00000000357 15166657210 0014547 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type DefinitionNode = * | ExecutableDefinitionNode * | TypeSystemDefinitionNode * | TypeSystemExtensionNode;. */ interface DefinitionNode {} graphql/lib/Language/AST/ObjectTypeExtensionNode.php 0000644 00000001036 15166657210 0016417 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ObjectTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::OBJECT_TYPE_EXTENSION; public NameNode $name; /** @var NodeList<NamedTypeNode> */ public NodeList $interfaces; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<FieldDefinitionNode> */ public NodeList $fields; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/NullValueNode.php 0000644 00000000256 15166657210 0014364 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class NullValueNode extends Node implements ValueNode { public string $kind = NodeKind::NULL; } graphql/lib/Language/AST/NameNode.php 0000644 00000000303 15166657210 0013326 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class NameNode extends Node implements TypeNode { public string $kind = NodeKind::NAME; public string $value; } graphql/lib/Language/AST/Node.php 0000644 00000007103 15166657210 0012532 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Utils\Utils; /** * type Node = NameNode * | DocumentNode * | OperationDefinitionNode * | VariableDefinitionNode * | VariableNode * | SelectionSetNode * | FieldNode * | ArgumentNode * | FragmentSpreadNode * | InlineFragmentNode * | FragmentDefinitionNode * | IntValueNode * | FloatValueNode * | StringValueNode * | BooleanValueNode * | EnumValueNode * | ListValueNode * | ObjectValueNode * | ObjectFieldNode * | DirectiveNode * | ListTypeNode * | NonNullTypeNode. * * @see \GraphQL\Tests\Language\AST\NodeTest */ abstract class Node implements \JsonSerializable { public ?Location $loc = null; public string $kind; /** @param array<string, mixed> $vars */ public function __construct(array $vars) { Utils::assign($this, $vars); } /** * Returns a clone of this instance and all its children, except Location $loc. * * @throws \JsonException * @throws InvariantViolation * * @return static */ public function cloneDeep(): self { return static::cloneValue($this); } /** * @template TNode of Node * @template TCloneable of TNode|NodeList<TNode>|Location|string * * @phpstan-param TCloneable $value * * @throws \JsonException * @throws InvariantViolation * * @phpstan-return TCloneable */ protected static function cloneValue($value) { if ($value instanceof self) { $cloned = clone $value; foreach (get_object_vars($cloned) as $prop => $propValue) { $cloned->{$prop} = static::cloneValue($propValue); } return $cloned; } if ($value instanceof NodeList) { /** * @phpstan-var TCloneable * * @phpstan-ignore varTag.nativeType (PHPStan is strict about template types and sees NodeList<TNode> as potentially different from TCloneable) */ return $value->cloneDeep(); } return $value; } /** @throws \JsonException */ public function __toString(): string { return json_encode($this, JSON_THROW_ON_ERROR); } /** * Improves upon the default serialization by: * - excluding null values * - excluding large reference values such as @see Location::$source. * * @return array<string, mixed> */ public function jsonSerialize(): array { return $this->toArray(); } /** @return array<string, mixed> */ public function toArray(): array { return self::recursiveToArray($this); } /** @return array<string, mixed> */ private static function recursiveToArray(Node $node): array { $result = []; foreach (get_object_vars($node) as $prop => $propValue) { if ($propValue === null) { continue; } if ($propValue instanceof NodeList) { $converted = []; foreach ($propValue as $item) { $converted[] = self::recursiveToArray($item); } } elseif ($propValue instanceof Node) { $converted = self::recursiveToArray($propValue); } elseif ($propValue instanceof Location) { $converted = $propValue->toArray(); } else { $converted = $propValue; } $result[$prop] = $converted; } return $result; } } graphql/lib/Language/AST/EnumTypeExtensionNode.php 0000644 00000001142 15166657210 0016113 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class EnumTypeExtensionNode extends Node implements TypeExtensionNode { public string $kind = NodeKind::ENUM_TYPE_EXTENSION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<EnumValueDefinitionNode> */ public NodeList $values; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/ObjectTypeDefinitionNode.php 0000644 00000001123 15166657210 0016530 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::OBJECT_TYPE_DEFINITION; public NameNode $name; /** @var NodeList<NamedTypeNode> */ public NodeList $interfaces; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<FieldDefinitionNode> */ public NodeList $fields; public ?StringValueNode $description = null; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/VariableDefinitionNode.php 0000644 00000001344 15166657210 0016212 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class VariableDefinitionNode extends Node implements DefinitionNode { public string $kind = NodeKind::VARIABLE_DEFINITION; public VariableNode $variable; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public TypeNode $type; /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */ public ?ValueNode $defaultValue = null; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } } graphql/lib/Language/AST/TypeNode.php 0000644 00000000270 15166657210 0013372 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type TypeNode = NamedTypeNode * | ListTypeNode * | NonNullTypeNode. */ interface TypeNode {} graphql/lib/Language/AST/UnionTypeDefinitionNode.php 0000644 00000001000 15166657210 0016404 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::UNION_TYPE_DEFINITION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<NamedTypeNode> */ public NodeList $types; public ?StringValueNode $description = null; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/NamedTypeNode.php 0000644 00000000317 15166657210 0014341 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class NamedTypeNode extends Node implements TypeNode { public string $kind = NodeKind::NAMED_TYPE; public NameNode $name; } graphql/lib/Language/AST/OperationTypeDefinitionNode.php 0000644 00000000545 15166657210 0017271 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * @phpstan-import-type OperationType from OperationDefinitionNode */ class OperationTypeDefinitionNode extends Node { public string $kind = NodeKind::OPERATION_TYPE_DEFINITION; /** @var OperationType */ public string $operation; public NamedTypeNode $type; } graphql/lib/Language/AST/ListValueNode.php 0000644 00000000364 15166657210 0014365 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class ListValueNode extends Node implements ValueNode { public string $kind = NodeKind::LST; /** @var NodeList<ValueNode&Node> */ public NodeList $values; } graphql/lib/Language/AST/HasSelectionSet.php 0000644 00000000425 15166657210 0014702 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type DefinitionNode = OperationDefinitionNode * | FragmentDefinitionNode. */ interface HasSelectionSet { public function getSelectionSet(): SelectionSetNode; } graphql/lib/Language/AST/InputObjectTypeDefinitionNode.php 0000644 00000001247 15166657210 0017557 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<InputValueDefinitionNode> */ public NodeList $fields; public ?StringValueNode $description = null; public function getName(): NameNode { return $this->name; } public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); } } graphql/lib/Language/AST/IntValueNode.php 0000644 00000000307 15166657210 0014201 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class IntValueNode extends Node implements ValueNode { public string $kind = NodeKind::INT; public string $value; } graphql/lib/Language/AST/SelectionSetNode.php 0000644 00000000364 15166657210 0015056 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class SelectionSetNode extends Node { public string $kind = NodeKind::SELECTION_SET; /** @var NodeList<SelectionNode&Node> */ public NodeList $selections; } graphql/lib/Language/AST/InterfaceTypeDefinitionNode.php 0000644 00000001131 15166657210 0017221 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode { public string $kind = NodeKind::INTERFACE_TYPE_DEFINITION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<NamedTypeNode> */ public NodeList $interfaces; /** @var NodeList<FieldDefinitionNode> */ public NodeList $fields; public ?StringValueNode $description = null; public function getName(): NameNode { return $this->name; } } graphql/lib/Language/AST/Location.php 0000644 00000003031 15166657210 0013411 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; use YOOtheme\GraphQL\Language\Source; use YOOtheme\GraphQL\Language\Token; /** * Contains a range of UTF-8 character offsets and token references that * identify the region of the source from which the AST derived. * * @phpstan-type LocationArray array{start: int, end: int} */ class Location { /** The character offset at which this Node begins. */ public int $start; /** The character offset at which this Node ends. */ public int $end; /** The Token at which this Node begins. */ public ?Token $startToken = null; /** The Token at which this Node ends. */ public ?Token $endToken = null; /** The Source document the AST represents. */ public ?Source $source = null; public static function create(int $start, int $end): self { $tmp = new static(); $tmp->start = $start; $tmp->end = $end; return $tmp; } public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null) { $this->startToken = $startToken; $this->endToken = $endToken; $this->source = $source; if ($startToken === null || $endToken === null) { return; } $this->start = $startToken->start; $this->end = $endToken->end; } /** @return LocationArray */ public function toArray(): array { return [ 'start' => $this->start, 'end' => $this->end, ]; } } graphql/lib/Language/AST/TypeSystemDefinitionNode.php 0000644 00000000411 15166657210 0016605 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type TypeSystemDefinitionNode = * | SchemaDefinitionNode * | TypeDefinitionNode * | DirectiveDefinitionNode. */ interface TypeSystemDefinitionNode extends DefinitionNode {} graphql/lib/Language/AST/VariableNode.php 0000644 00000000315 15166657210 0014176 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class VariableNode extends Node implements ValueNode { public string $kind = NodeKind::VARIABLE; public NameNode $name; } graphql/lib/Language/AST/TypeSystemExtensionNode.php 0000644 00000000341 15166657210 0016473 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;. */ interface TypeSystemExtensionNode extends DefinitionNode {} graphql/lib/Language/AST/FieldNode.php 0000644 00000001163 15166657210 0013476 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class FieldNode extends Node implements SelectionNode { public string $kind = NodeKind::FIELD; public NameNode $name; public ?NameNode $alias = null; /** @var NodeList<ArgumentNode> */ public NodeList $arguments; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public ?SelectionSetNode $selectionSet = null; public function __construct(array $vars) { parent::__construct($vars); $this->directives ??= new NodeList([]); $this->arguments ??= new NodeList([]); } } graphql/lib/Language/AST/StringValueNode.php 0000644 00000000356 15166657210 0014721 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class StringValueNode extends Node implements ValueNode { public string $kind = NodeKind::STRING; public string $value; public bool $block = false; } graphql/lib/Language/AST/EnumValueDefinitionNode.php 0000644 00000000514 15166657210 0016364 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class EnumValueDefinitionNode extends Node { public string $kind = NodeKind::ENUM_VALUE_DEFINITION; public NameNode $name; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public ?StringValueNode $description = null; } graphql/lib/Language/AST/SchemaExtensionNode.php 0000644 00000000564 15166657210 0015554 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class SchemaExtensionNode extends Node implements TypeSystemExtensionNode { public string $kind = NodeKind::SCHEMA_EXTENSION; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<OperationTypeDefinitionNode> */ public NodeList $operationTypes; } graphql/lib/Language/AST/ArgumentNode.php 0000644 00000000657 15166657210 0014244 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; /** * @phpstan-type ArgumentNodeValue VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */ class ArgumentNode extends Node { public string $kind = NodeKind::ARGUMENT; /** @phpstan-var ArgumentNodeValue */ public ValueNode $value; public NameNode $name; } graphql/lib/Language/AST/SchemaDefinitionNode.php 0000644 00000000567 15166657210 0015673 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode { public string $kind = NodeKind::SCHEMA_DEFINITION; /** @var NodeList<DirectiveNode> */ public NodeList $directives; /** @var NodeList<OperationTypeDefinitionNode> */ public NodeList $operationTypes; } graphql/lib/Language/AST/FieldDefinitionNode.php 0000644 00000000756 15166657210 0015516 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language\AST; class FieldDefinitionNode extends Node { public string $kind = NodeKind::FIELD_DEFINITION; public NameNode $name; /** @var NodeList<InputValueDefinitionNode> */ public NodeList $arguments; /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public TypeNode $type; /** @var NodeList<DirectiveNode> */ public NodeList $directives; public ?StringValueNode $description = null; } graphql/lib/Language/VisitorStop.php 0000644 00000000172 15166657210 0013522 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; final class VisitorStop extends VisitorOperation {} graphql/lib/Language/VisitorOperation.php 0000644 00000000151 15166657210 0014532 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; abstract class VisitorOperation {} graphql/lib/Language/Source.php 0000644 00000003116 15166657210 0012456 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; class Source { public string $body; public int $length; public string $name; public SourceLocation $locationOffset; /** * A representation of source input to GraphQL. * * `name` and `locationOffset` are optional. They are useful for clients who * store GraphQL documents in source files; for example, if the GraphQL input * starts at line 40 in a file named Foo.graphql, it might be useful for name to * be "Foo.graphql" and location to be `{ line: 40, column: 0 }`. * line and column in locationOffset are 1-indexed */ public function __construct(string $body, ?string $name = null, ?SourceLocation $location = null) { $this->body = $body; $this->length = mb_strlen($body, 'UTF-8'); $this->name = $name === '' || $name === null ? 'GraphQL request' : $name; $this->locationOffset = $location ?? new SourceLocation(1, 1); } public function getLocation(int $position): SourceLocation { $line = 1; $column = $position + 1; $utfChars = json_decode('"\u2028\u2029"'); $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su'; $matches = []; preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, \PREG_OFFSET_CAPTURE); foreach ($matches[0] as $match) { ++$line; $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8')); } return new SourceLocation($line, $column); } } graphql/lib/Language/VisitorRemoveNode.php 0000644 00000000200 15166657210 0014630 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Language; final class VisitorRemoveNode extends VisitorOperation {} graphql/lib/Type/Introspection.php 0000644 00000104446 15166657210 0013264 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\GraphQL; use YOOtheme\GraphQL\Language\DirectiveLocation; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\EnumValueDefinition; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ResolveInfo; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Definition\WrappingType; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-type IntrospectionOptions array{ * descriptions?: bool, * directiveIsRepeatable?: bool, * typeIsOneOf?: bool, * } * * Available options: * - descriptions * Include descriptions in the introspection result? * Default: true * - directiveIsRepeatable * Include field `isRepeatable` for directives? * Default: false * - typeIsOneOf * Include field `isOneOf` for types? * Default: false * * @see \GraphQL\Tests\Type\IntrospectionTest */ class Introspection { public const SCHEMA_FIELD_NAME = '__schema'; public const TYPE_FIELD_NAME = '__type'; public const TYPE_NAME_FIELD_NAME = '__typename'; public const SCHEMA_OBJECT_NAME = '__Schema'; public const TYPE_OBJECT_NAME = '__Type'; public const DIRECTIVE_OBJECT_NAME = '__Directive'; public const FIELD_OBJECT_NAME = '__Field'; public const INPUT_VALUE_OBJECT_NAME = '__InputValue'; public const ENUM_VALUE_OBJECT_NAME = '__EnumValue'; public const TYPE_KIND_ENUM_NAME = '__TypeKind'; public const DIRECTIVE_LOCATION_ENUM_NAME = '__DirectiveLocation'; public const TYPE_NAMES = [ self::SCHEMA_OBJECT_NAME, self::TYPE_OBJECT_NAME, self::DIRECTIVE_OBJECT_NAME, self::FIELD_OBJECT_NAME, self::INPUT_VALUE_OBJECT_NAME, self::ENUM_VALUE_OBJECT_NAME, self::TYPE_KIND_ENUM_NAME, self::DIRECTIVE_LOCATION_ENUM_NAME, ]; /** @var array<string, mixed>|null */ protected static ?array $cachedInstances; /** * @param IntrospectionOptions $options * * @api */ public static function getIntrospectionQuery(array $options = []): string { $optionsWithDefaults = array_merge([ 'descriptions' => true, 'directiveIsRepeatable' => false, 'typeIsOneOf' => false, ], $options); $descriptions = $optionsWithDefaults['descriptions'] ? 'description' : ''; $directiveIsRepeatable = $optionsWithDefaults['directiveIsRepeatable'] ? 'isRepeatable' : ''; $typeIsOneOf = $optionsWithDefaults['typeIsOneOf'] ? 'isOneOf' : ''; return <<<GRAPHQL query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name {$descriptions} args(includeDeprecated: true) { ...InputValue } {$directiveIsRepeatable} locations } } } fragment FullType on __Type { kind name {$descriptions} {$typeIsOneOf} fields(includeDeprecated: true) { name {$descriptions} args(includeDeprecated: true) { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields(includeDeprecated: true) { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name {$descriptions} isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name {$descriptions} type { ...TypeRef } defaultValue isDeprecated deprecationReason } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } GRAPHQL; } /** * Build an introspection query from a Schema. * * Introspection is useful for utilities that care about type and field * relationships, but do not need to traverse through those relationships. * * This is the inverse of BuildClientSchema::build(). The primary use case is * outside the server context, for instance when doing schema comparisons. * * @param IntrospectionOptions $options * * @throws \Exception * @throws \JsonException * @throws InvariantViolation * * @return array<string, array<mixed>> * * @api */ public static function fromSchema(Schema $schema, array $options = []): array { $optionsWithDefaults = array_merge([ 'directiveIsRepeatable' => true, 'typeIsOneOf' => true, ], $options); $result = GraphQL::executeQuery( $schema, self::getIntrospectionQuery($optionsWithDefaults) ); $data = $result->data; if ($data === null) { $noDataResult = Utils::printSafeJson($result); throw new InvariantViolation("Introspection query returned no data: {$noDataResult}."); } return $data; } /** @param Type&NamedType $type */ public static function isIntrospectionType(NamedType $type): bool { return in_array($type->name, self::TYPE_NAMES, true); } /** @return array<string, Type&NamedType> */ public static function getTypes(): array { return [ self::SCHEMA_OBJECT_NAME => self::_schema(), self::TYPE_OBJECT_NAME => self::_type(), self::DIRECTIVE_OBJECT_NAME => self::_directive(), self::FIELD_OBJECT_NAME => self::_field(), self::INPUT_VALUE_OBJECT_NAME => self::_inputValue(), self::ENUM_VALUE_OBJECT_NAME => self::_enumValue(), self::TYPE_KIND_ENUM_NAME => self::_typeKind(), self::DIRECTIVE_LOCATION_ENUM_NAME => self::_directiveLocation(), ]; } public static function _schema(): ObjectType { return self::$cachedInstances[self::SCHEMA_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::SCHEMA_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'A GraphQL Schema defines the capabilities of a GraphQL ' . 'server. It exposes all available types and directives on ' . 'the server, as well as the entry points for query, mutation, and ' . 'subscription operations.', 'fields' => [ 'types' => [ 'description' => 'A list of all types supported by this server.', 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))), 'resolve' => static fn (Schema $schema): array => $schema->getTypeMap(), ], 'queryType' => [ 'description' => 'The type that query operations will be rooted at.', 'type' => new NonNull(self::_type()), 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getQueryType(), ], 'mutationType' => [ 'description' => 'If this server supports mutation, the type that mutation operations will be rooted at.', 'type' => self::_type(), 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getMutationType(), ], 'subscriptionType' => [ 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.', 'type' => self::_type(), 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getSubscriptionType(), ], 'directives' => [ 'description' => 'A list of all directives supported by this server.', 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))), 'resolve' => static fn (Schema $schema): array => $schema->getDirectives(), ], ], ]); } public static function _type(): ObjectType { return self::$cachedInstances[self::TYPE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::TYPE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'The fundamental unit of any GraphQL Schema is the type. There are ' . 'many kinds of types in GraphQL as represented by the `__TypeKind` enum.' . "\n\n" . 'Depending on the kind of a type, certain fields describe ' . 'information about that type. Scalar types provide no information ' . 'beyond a name and description, while Enum types provide their values. ' . 'Object and Interface types provide the fields they describe. Abstract ' . 'types, Union and Interface, provide the Object types possible ' . 'at runtime. List and NonNull types compose other types.', 'fields' => static fn (): array => [ 'kind' => [ 'type' => Type::nonNull(self::_typeKind()), 'resolve' => static function (Type $type): string { switch (true) { case $type instanceof ListOfType: return TypeKind::LIST; case $type instanceof NonNull: return TypeKind::NON_NULL; case $type instanceof ScalarType: return TypeKind::SCALAR; case $type instanceof ObjectType: return TypeKind::OBJECT; case $type instanceof EnumType: return TypeKind::ENUM; case $type instanceof InputObjectType: return TypeKind::INPUT_OBJECT; case $type instanceof InterfaceType: return TypeKind::INTERFACE; case $type instanceof UnionType: return TypeKind::UNION; default: $safeType = Utils::printSafe($type); throw new \Exception("Unknown kind of type: {$safeType}"); } }, ], 'name' => [ 'type' => Type::string(), 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType ? $type->name : null, ], 'description' => [ 'type' => Type::string(), 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType ? $type->description : null, ], 'fields' => [ 'type' => Type::listOf(Type::nonNull(self::_field())), 'args' => [ 'includeDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], 'resolve' => static function (Type $type, $args): ?array { if ($type instanceof ObjectType || $type instanceof InterfaceType) { $fields = $type->getVisibleFields(); if (! $args['includeDeprecated']) { return array_filter( $fields, static fn (FieldDefinition $field): bool => ! $field->isDeprecated() ); } return $fields; } return null; }, ], 'interfaces' => [ 'type' => Type::listOf(Type::nonNull(self::_type())), 'resolve' => static fn ($type): ?array => $type instanceof ObjectType || $type instanceof InterfaceType ? $type->getInterfaces() : null, ], 'possibleTypes' => [ 'type' => Type::listOf(Type::nonNull(self::_type())), 'resolve' => static fn ($type, $args, $context, ResolveInfo $info): ?array => $type instanceof InterfaceType || $type instanceof UnionType ? $info->schema->getPossibleTypes($type) : null, ], 'enumValues' => [ 'type' => Type::listOf(Type::nonNull(self::_enumValue())), 'args' => [ 'includeDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], 'resolve' => static function ($type, $args): ?array { if ($type instanceof EnumType) { $values = $type->getValues(); if (! $args['includeDeprecated']) { return array_filter( $values, static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated() ); } return $values; } return null; }, ], 'inputFields' => [ 'type' => Type::listOf(Type::nonNull(self::_inputValue())), 'args' => [ 'includeDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], 'resolve' => static function ($type, $args): ?array { if ($type instanceof InputObjectType) { $fields = $type->getFields(); if (! $args['includeDeprecated']) { return array_filter( $fields, static fn (InputObjectField $field): bool => ! $field->isDeprecated(), ); } return $fields; } return null; }, ], 'ofType' => [ 'type' => self::_type(), 'resolve' => static fn ($type): ?Type => $type instanceof WrappingType ? $type->getWrappedType() : null, ], 'isOneOf' => [ 'type' => Type::boolean(), 'resolve' => static fn ($type): ?bool => $type instanceof InputObjectType ? $type->isOneOf() : null, ], ], ]); } public static function _typeKind(): EnumType { return self::$cachedInstances[self::TYPE_KIND_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::TYPE_KIND_ENUM_NAME, 'isIntrospection' => true, 'description' => 'An enum describing what kind of type a given `__Type` is.', 'values' => [ 'SCALAR' => [ 'value' => TypeKind::SCALAR, 'description' => 'Indicates this type is a scalar.', ], 'OBJECT' => [ 'value' => TypeKind::OBJECT, 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', ], 'INTERFACE' => [ 'value' => TypeKind::INTERFACE, 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', ], 'UNION' => [ 'value' => TypeKind::UNION, 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.', ], 'ENUM' => [ 'value' => TypeKind::ENUM, 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.', ], 'INPUT_OBJECT' => [ 'value' => TypeKind::INPUT_OBJECT, 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.', ], 'LIST' => [ 'value' => TypeKind::LIST, 'description' => 'Indicates this type is a list. `ofType` is a valid field.', ], 'NON_NULL' => [ 'value' => TypeKind::NON_NULL, 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.', ], ], ]); } public static function _field(): ObjectType { return self::$cachedInstances[self::FIELD_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::FIELD_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'Object and Interface types are described by a list of Fields, each of ' . 'which has a name, potentially a list of arguments, and a return type.', 'fields' => static fn (): array => [ 'name' => [ 'type' => Type::nonNull(Type::string()), 'resolve' => static fn (FieldDefinition $field): string => $field->name, ], 'description' => [ 'type' => Type::string(), 'resolve' => static fn (FieldDefinition $field): ?string => $field->description, ], 'args' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), 'args' => [ 'includeDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], 'resolve' => static function (FieldDefinition $field, $args): array { $values = $field->args; if (! $args['includeDeprecated']) { return array_filter( $values, static fn (Argument $value): bool => ! $value->isDeprecated(), ); } return $values; }, ], 'type' => [ 'type' => Type::nonNull(self::_type()), 'resolve' => static fn (FieldDefinition $field): Type => $field->getType(), ], 'isDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(), ], 'deprecationReason' => [ 'type' => Type::string(), 'resolve' => static fn (FieldDefinition $field): ?string => $field->deprecationReason, ], ], ]); } public static function _inputValue(): ObjectType { return self::$cachedInstances[self::INPUT_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::INPUT_VALUE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'Arguments provided to Fields or Directives and the input fields of an ' . 'InputObject are represented as Input Values which describe their type ' . 'and optionally a default value.', 'fields' => static fn (): array => [ 'name' => [ 'type' => Type::nonNull(Type::string()), /** @param Argument|InputObjectField $inputValue */ 'resolve' => static fn ($inputValue): string => $inputValue->name, ], 'description' => [ 'type' => Type::string(), /** @param Argument|InputObjectField $inputValue */ 'resolve' => static fn ($inputValue): ?string => $inputValue->description, ], 'type' => [ 'type' => Type::nonNull(self::_type()), /** @param Argument|InputObjectField $inputValue */ 'resolve' => static fn ($inputValue): Type => $inputValue->getType(), ], 'defaultValue' => [ 'type' => Type::string(), 'description' => 'A GraphQL-formatted string representing the default value for this input value.', /** @param Argument|InputObjectField $inputValue */ 'resolve' => static function ($inputValue): ?string { if ($inputValue->defaultValueExists()) { $defaultValueAST = AST::astFromValue($inputValue->defaultValue, $inputValue->getType()); if ($defaultValueAST === null) { $inconvertibleDefaultValue = Utils::printSafe($inputValue->defaultValue); throw new InvariantViolation("Unable to convert defaultValue of argument {$inputValue->name} into AST: {$inconvertibleDefaultValue}."); } return Printer::doPrint($defaultValueAST); } return null; }, ], 'isDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), /** @param Argument|InputObjectField $inputValue */ 'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(), ], 'deprecationReason' => [ 'type' => Type::string(), /** @param Argument|InputObjectField $inputValue */ 'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason, ], ], ]); } public static function _enumValue(): ObjectType { return self::$cachedInstances[self::ENUM_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::ENUM_VALUE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'One possible value for a given Enum. Enum values are unique values, not ' . 'a placeholder for a string or numeric value. However an Enum value is ' . 'returned in a JSON response as a string.', 'fields' => [ 'name' => [ 'type' => Type::nonNull(Type::string()), 'resolve' => static fn (EnumValueDefinition $enumValue): string => $enumValue->name, ], 'description' => [ 'type' => Type::string(), 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->description, ], 'isDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(), ], 'deprecationReason' => [ 'type' => Type::string(), 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->deprecationReason, ], ], ]); } public static function _directive(): ObjectType { return self::$cachedInstances[self::DIRECTIVE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::DIRECTIVE_OBJECT_NAME, 'isIntrospection' => true, 'description' => 'A Directive provides a way to describe alternate runtime execution and ' . 'type validation behavior in a GraphQL document.' . "\n\nIn some cases, you need to provide options to alter GraphQL's " . 'execution behavior in ways field arguments will not suffice, such as ' . 'conditionally including or skipping a field. Directives provide this by ' . 'describing additional information to the executor.', 'fields' => [ 'name' => [ 'type' => Type::nonNull(Type::string()), 'resolve' => static fn (Directive $directive): string => $directive->name, ], 'description' => [ 'type' => Type::string(), 'resolve' => static fn (Directive $directive): ?string => $directive->description, ], 'isRepeatable' => [ 'type' => Type::nonNull(Type::boolean()), 'resolve' => static fn (Directive $directive): bool => $directive->isRepeatable, ], 'locations' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull( self::_directiveLocation() ))), 'resolve' => static fn (Directive $directive): array => $directive->locations, ], 'args' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), 'args' => [ 'includeDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), 'defaultValue' => false, ], ], 'resolve' => static function (Directive $directive, $args): array { $values = $directive->args; if (! $args['includeDeprecated']) { return array_filter( $values, static fn (Argument $value): bool => ! $value->isDeprecated(), ); } return $values; }, ], ], ]); } public static function _directiveLocation(): EnumType { return self::$cachedInstances[self::DIRECTIVE_LOCATION_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) 'name' => self::DIRECTIVE_LOCATION_ENUM_NAME, 'isIntrospection' => true, 'description' => 'A Directive can be adjacent to many parts of the GraphQL language, a ' . '__DirectiveLocation describes one such possible adjacencies.', 'values' => [ 'QUERY' => [ 'value' => DirectiveLocation::QUERY, 'description' => 'Location adjacent to a query operation.', ], 'MUTATION' => [ 'value' => DirectiveLocation::MUTATION, 'description' => 'Location adjacent to a mutation operation.', ], 'SUBSCRIPTION' => [ 'value' => DirectiveLocation::SUBSCRIPTION, 'description' => 'Location adjacent to a subscription operation.', ], 'FIELD' => [ 'value' => DirectiveLocation::FIELD, 'description' => 'Location adjacent to a field.', ], 'FRAGMENT_DEFINITION' => [ 'value' => DirectiveLocation::FRAGMENT_DEFINITION, 'description' => 'Location adjacent to a fragment definition.', ], 'FRAGMENT_SPREAD' => [ 'value' => DirectiveLocation::FRAGMENT_SPREAD, 'description' => 'Location adjacent to a fragment spread.', ], 'INLINE_FRAGMENT' => [ 'value' => DirectiveLocation::INLINE_FRAGMENT, 'description' => 'Location adjacent to an inline fragment.', ], 'VARIABLE_DEFINITION' => [ 'value' => DirectiveLocation::VARIABLE_DEFINITION, 'description' => 'Location adjacent to a variable definition.', ], 'SCHEMA' => [ 'value' => DirectiveLocation::SCHEMA, 'description' => 'Location adjacent to a schema definition.', ], 'SCALAR' => [ 'value' => DirectiveLocation::SCALAR, 'description' => 'Location adjacent to a scalar definition.', ], 'OBJECT' => [ 'value' => DirectiveLocation::OBJECT, 'description' => 'Location adjacent to an object type definition.', ], 'FIELD_DEFINITION' => [ 'value' => DirectiveLocation::FIELD_DEFINITION, 'description' => 'Location adjacent to a field definition.', ], 'ARGUMENT_DEFINITION' => [ 'value' => DirectiveLocation::ARGUMENT_DEFINITION, 'description' => 'Location adjacent to an argument definition.', ], 'INTERFACE' => [ 'value' => DirectiveLocation::IFACE, 'description' => 'Location adjacent to an interface definition.', ], 'UNION' => [ 'value' => DirectiveLocation::UNION, 'description' => 'Location adjacent to a union definition.', ], 'ENUM' => [ 'value' => DirectiveLocation::ENUM, 'description' => 'Location adjacent to an enum definition.', ], 'ENUM_VALUE' => [ 'value' => DirectiveLocation::ENUM_VALUE, 'description' => 'Location adjacent to an enum value definition.', ], 'INPUT_OBJECT' => [ 'value' => DirectiveLocation::INPUT_OBJECT, 'description' => 'Location adjacent to an input object type definition.', ], 'INPUT_FIELD_DEFINITION' => [ 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION, 'description' => 'Location adjacent to an input object field definition.', ], ], ]); } public static function schemaMetaFieldDef(): FieldDefinition { return self::$cachedInstances[self::SCHEMA_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::SCHEMA_FIELD_NAME, 'type' => Type::nonNull(self::_schema()), 'description' => 'Access the current type schema of this server.', 'args' => [], 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): Schema => $info->schema, ]); } public static function typeMetaFieldDef(): FieldDefinition { return self::$cachedInstances[self::TYPE_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::TYPE_FIELD_NAME, 'type' => self::_type(), 'description' => 'Request the type information of a single type.', 'args' => [ [ 'name' => 'name', 'type' => Type::nonNull(Type::string()), ], ], 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): ?Type => $info->schema->getType($args['name']), ]); } public static function typeNameMetaFieldDef(): FieldDefinition { return self::$cachedInstances[self::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([ 'name' => self::TYPE_NAME_FIELD_NAME, 'type' => Type::nonNull(Type::string()), 'description' => 'The name of the current Object type at runtime.', 'args' => [], 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): string => $info->parentType->name, ]); } public static function resetCachedInstances(): void { self::$cachedInstances = null; } } graphql/lib/Type/Validation/InputObjectCircularRefs.php 0000644 00000006570 15166657210 0017250 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Validation; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\SchemaValidationContext; class InputObjectCircularRefs { private SchemaValidationContext $schemaValidationContext; /** * Tracks already visited types to maintain O(N) and to ensure that cycles * are not redundantly reported. * * @var array<string, bool> */ private array $visitedTypes = []; /** @var array<int, InputObjectField> */ private array $fieldPath = []; /** * Position in the type path. * * @var array<string, int> */ private array $fieldPathIndexByTypeName = []; public function __construct(SchemaValidationContext $schemaValidationContext) { $this->schemaValidationContext = $schemaValidationContext; } /** * This does a straight-forward DFS to find cycles. * It does not terminate when a cycle was found but continues to explore * the graph to find all possible cycles. * * @throws InvariantViolation */ public function validate(InputObjectType $inputObj): void { if (isset($this->visitedTypes[$inputObj->name])) { return; } $this->visitedTypes[$inputObj->name] = true; $this->fieldPathIndexByTypeName[$inputObj->name] = count($this->fieldPath); $fieldMap = $inputObj->getFields(); foreach ($fieldMap as $field) { $type = $field->getType(); if ($type instanceof NonNull) { $fieldType = $type->getWrappedType(); // If the type of the field is anything else then a non-nullable input object, // there is no chance of an unbreakable cycle if ($fieldType instanceof InputObjectType) { $this->fieldPath[] = $field; if (! isset($this->fieldPathIndexByTypeName[$fieldType->name])) { $this->validate($fieldType); } else { $cycleIndex = $this->fieldPathIndexByTypeName[$fieldType->name]; $cyclePath = array_slice($this->fieldPath, $cycleIndex); $fieldNames = implode( '.', array_map( static fn (InputObjectField $field): string => $field->name, $cyclePath ) ); $fieldNodes = array_map( static fn (InputObjectField $field): ?InputValueDefinitionNode => $field->astNode, $cyclePath ); $this->schemaValidationContext->reportError( "Cannot reference Input Object \"{$fieldType->name}\" within itself through a series of non-null fields: \"{$fieldNames}\".", $fieldNodes ); } } } array_pop($this->fieldPath); } unset($this->fieldPathIndexByTypeName[$inputObj->name]); } } graphql/lib/Type/SchemaConfig.php 0000644 00000017442 15166657210 0012751 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\Type; /** * Configuration options for schema construction. * * The options accepted by the **create** method are described * in the [schema definition docs](schema-definition.md#configuration-options). * * Usage example: * * $config = SchemaConfig::create() * ->setQuery($myQueryType) * ->setTypeLoader($myTypeLoader); * * $schema = new Schema($config); * * @see Type, NamedType * * @phpstan-type MaybeLazyObjectType ObjectType|(callable(): (ObjectType|null))|null * @phpstan-type TypeLoader callable(string $typeName): ((Type&NamedType)|null) * @phpstan-type Types iterable<Type&NamedType>|(callable(): iterable<Type&NamedType>)|iterable<(callable(): Type&NamedType)>|(callable(): iterable<(callable(): Type&NamedType)>) * @phpstan-type SchemaConfigOptions array{ * query?: MaybeLazyObjectType, * mutation?: MaybeLazyObjectType, * subscription?: MaybeLazyObjectType, * types?: Types|null, * directives?: array<Directive>|null, * typeLoader?: TypeLoader|null, * assumeValid?: bool|null, * astNode?: SchemaDefinitionNode|null, * extensionASTNodes?: array<SchemaExtensionNode>|null, * } */ class SchemaConfig { /** @var MaybeLazyObjectType */ public $query; /** @var MaybeLazyObjectType */ public $mutation; /** @var MaybeLazyObjectType */ public $subscription; /** * @var iterable|callable * * @phpstan-var Types */ public $types = []; /** @var array<Directive>|null */ public ?array $directives = null; /** * @var callable|null * * @phpstan-var TypeLoader|null */ public $typeLoader; public bool $assumeValid = false; public ?SchemaDefinitionNode $astNode = null; /** @var array<SchemaExtensionNode> */ public array $extensionASTNodes = []; /** * Converts an array of options to instance of SchemaConfig * (or just returns empty config when array is not passed). * * @phpstan-param SchemaConfigOptions $options * * @throws InvariantViolation * * @api */ public static function create(array $options = []): self { $config = new static(); if ($options !== []) { if (isset($options['query'])) { $config->setQuery($options['query']); } if (isset($options['mutation'])) { $config->setMutation($options['mutation']); } if (isset($options['subscription'])) { $config->setSubscription($options['subscription']); } if (isset($options['types'])) { $config->setTypes($options['types']); } if (isset($options['directives'])) { $config->setDirectives($options['directives']); } if (isset($options['typeLoader'])) { $config->setTypeLoader($options['typeLoader']); } if (isset($options['assumeValid'])) { $config->setAssumeValid($options['assumeValid']); } if (isset($options['astNode'])) { $config->setAstNode($options['astNode']); } if (isset($options['extensionASTNodes'])) { $config->setExtensionASTNodes($options['extensionASTNodes']); } } return $config; } /** * @return MaybeLazyObjectType * * @api */ public function getQuery() { return $this->query; } /** * @param MaybeLazyObjectType $query * * @throws InvariantViolation * * @api */ public function setQuery($query): self { $this->assertMaybeLazyObjectType($query); $this->query = $query; return $this; } /** * @return MaybeLazyObjectType * * @api */ public function getMutation() { return $this->mutation; } /** * @param MaybeLazyObjectType $mutation * * @throws InvariantViolation * * @api */ public function setMutation($mutation): self { $this->assertMaybeLazyObjectType($mutation); $this->mutation = $mutation; return $this; } /** * @return MaybeLazyObjectType * * @api */ public function getSubscription() { return $this->subscription; } /** * @param MaybeLazyObjectType $subscription * * @throws InvariantViolation * * @api */ public function setSubscription($subscription): self { $this->assertMaybeLazyObjectType($subscription); $this->subscription = $subscription; return $this; } /** * @return array|callable * * @phpstan-return Types * * @api */ public function getTypes() { return $this->types; } /** * @param array|callable $types * * @phpstan-param Types $types * * @api */ public function setTypes($types): self { $this->types = $types; return $this; } /** * @return array<Directive>|null * * @api */ public function getDirectives(): ?array { return $this->directives; } /** * @param array<Directive>|null $directives * * @api */ public function setDirectives(?array $directives): self { $this->directives = $directives; return $this; } /** * @return callable|null $typeLoader * * @phpstan-return TypeLoader|null $typeLoader * * @api */ public function getTypeLoader(): ?callable { return $this->typeLoader; } /** * @phpstan-param TypeLoader|null $typeLoader * * @api */ public function setTypeLoader(?callable $typeLoader): self { $this->typeLoader = $typeLoader; return $this; } public function getAssumeValid(): bool { return $this->assumeValid; } public function setAssumeValid(bool $assumeValid): self { $this->assumeValid = $assumeValid; return $this; } public function getAstNode(): ?SchemaDefinitionNode { return $this->astNode; } public function setAstNode(?SchemaDefinitionNode $astNode): self { $this->astNode = $astNode; return $this; } /** @return array<SchemaExtensionNode> */ public function getExtensionASTNodes(): array { return $this->extensionASTNodes; } /** @param array<SchemaExtensionNode> $extensionASTNodes */ public function setExtensionASTNodes(array $extensionASTNodes): self { $this->extensionASTNodes = $extensionASTNodes; return $this; } /** * @param mixed $maybeLazyObjectType Should be MaybeLazyObjectType * * @throws InvariantViolation */ protected function assertMaybeLazyObjectType($maybeLazyObjectType): void { if ($maybeLazyObjectType instanceof ObjectType || is_callable($maybeLazyObjectType) || is_null($maybeLazyObjectType)) { return; } $notMaybeLazyObjectType = is_object($maybeLazyObjectType) ? get_class($maybeLazyObjectType) : gettype($maybeLazyObjectType); $objectTypeClass = ObjectType::class; throw new InvariantViolation("Expected instanceof {$objectTypeClass}, a callable that returns such an instance, or null, got: {$notMaybeLazyObjectType}."); } } graphql/lib/Type/Definition/UnionType.php 0000644 00000010101 15166657210 0014426 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-import-type ResolveType from AbstractType * * @phpstan-type ObjectTypeReference ObjectType|callable(): ObjectType * @phpstan-type UnionConfig array{ * name?: string|null, * description?: string|null, * types: iterable<ObjectTypeReference>|callable(): iterable<ObjectTypeReference>, * resolveType?: ResolveType|null, * astNode?: UnionTypeDefinitionNode|null, * extensionASTNodes?: array<UnionTypeExtensionNode>|null * } */ class UnionType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType { use NamedTypeImplementation; public ?UnionTypeDefinitionNode $astNode; /** @var array<UnionTypeExtensionNode> */ public array $extensionASTNodes; /** @phpstan-var UnionConfig */ public array $config; /** * Lazily initialized. * * @var array<int, ObjectType> */ private array $types; /** * Lazily initialized. * * @var array<string, bool> */ private array $possibleTypeNames; /** * @phpstan-param UnionConfig $config * * @throws InvariantViolation */ public function __construct(array $config) { $this->name = $config['name'] ?? $this->inferName(); $this->description = $config['description'] ?? $this->description ?? null; $this->astNode = $config['astNode'] ?? null; $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; $this->config = $config; } /** @throws InvariantViolation */ public function isPossibleType(Type $type): bool { if (! $type instanceof ObjectType) { return false; } if (! isset($this->possibleTypeNames)) { $this->possibleTypeNames = []; foreach ($this->getTypes() as $possibleType) { $this->possibleTypeNames[$possibleType->name] = true; } } return isset($this->possibleTypeNames[$type->name]); } /** * @throws InvariantViolation * * @return array<int, ObjectType> */ public function getTypes(): array { if (! isset($this->types)) { $this->types = []; $types = $this->config['types'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime) if (is_callable($types)) { $types = $types(); } if (! is_iterable($types)) { throw new InvariantViolation("Must provide iterable of types or a callable which returns such an iterable for Union {$this->name}."); } foreach ($types as $type) { $this->types[] = Schema::resolveType($type); } } return $this->types; } public function resolveType($objectValue, $context, ResolveInfo $info) { if (isset($this->config['resolveType'])) { return ($this->config['resolveType'])($objectValue, $context, $info); } return null; } public function assertValid(): void { Utils::assertValidName($this->name); $resolveType = $this->config['resolveType'] ?? null; // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if (isset($resolveType) && ! is_callable($resolveType)) { $notCallable = Utils::printSafe($resolveType); throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}."); } } public function astNode(): ?UnionTypeDefinitionNode { return $this->astNode; } /** @return array<UnionTypeExtensionNode> */ public function extensionASTNodes(): array { return $this->extensionASTNodes; } } graphql/lib/Type/Definition/NullableType.php 0000644 00000000460 15166657210 0015103 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /* export type GraphQLNullableType = | GraphQLScalarType | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType | GraphQLEnumType | GraphQLInputObjectType | GraphQLList<any>; */ interface NullableType {} graphql/lib/Type/Definition/StringType.php 0000644 00000003411 15166657210 0014612 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Utils\Utils; class StringType extends ScalarType { public string $name = Type::STRING; public ?string $description = 'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.'; /** @throws SerializationError */ public function serialize($value): string { $canCast = is_scalar($value) || (is_object($value) && method_exists($value, '__toString')) || $value === null; if (! $canCast) { $notStringable = Utils::printSafe($value); throw new SerializationError("String cannot represent value: {$notStringable}"); } return (string) $value; } /** @throws Error */ public function parseValue($value): string { if (! is_string($value)) { $notString = Utils::printSafeJson($value); throw new Error("String cannot represent a non string value: {$notString}"); } return $value; } /** * @throws \JsonException * @throws Error */ public function parseLiteral(Node $valueNode, ?array $variables = null): string { if ($valueNode instanceof StringValueNode) { return $valueNode->value; } $notString = Printer::doPrint($valueNode); throw new Error("String cannot represent a non string value: {$notString}", $valueNode); } } graphql/lib/Type/Definition/Deprecated.php 0000644 00000000371 15166657210 0014544 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; #[\Attribute(\Attribute::TARGET_ALL)] class Deprecated { public function __construct( public string $reason = Directive::DEFAULT_DEPRECATION_REASON, ) {} } graphql/lib/Type/Definition/UnresolvedFieldDefinition.php 0000644 00000002272 15166657210 0017611 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /** * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition * * @phpstan-type DefinitionResolver callable(): (FieldDefinition|(Type&OutputType)|UnnamedFieldDefinitionConfig) */ class UnresolvedFieldDefinition { private string $name; /** * @var callable * * @phpstan-var DefinitionResolver */ private $definitionResolver; /** @param DefinitionResolver $definitionResolver */ public function __construct(string $name, callable $definitionResolver) { $this->name = $name; $this->definitionResolver = $definitionResolver; } public function getName(): string { return $this->name; } public function resolve(): FieldDefinition { $field = ($this->definitionResolver)(); if ($field instanceof FieldDefinition) { return $field; } if ($field instanceof Type) { return new FieldDefinition([ 'name' => $this->name, 'type' => $field, ]); } return new FieldDefinition($field + ['name' => $this->name]); } } graphql/lib/Type/Definition/ResolveInfo.php 0000644 00000040424 15166657210 0014742 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Values; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; /** * Structure containing information useful for field resolution process. * * Passed as 4th argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md). * * @phpstan-import-type QueryPlanOptions from QueryPlan * * @phpstan-type Path list<string|int> */ class ResolveInfo { /** * The definition of the field being resolved. * * @api */ public FieldDefinition $fieldDefinition; /** * The name of the field being resolved. * * @api */ public string $fieldName; /** * Expected return type of the field being resolved. * * @api */ public Type $returnType; /** * AST of all nodes referencing this field in the query. * * @api * * @var \ArrayObject<int, FieldNode> */ public \ArrayObject $fieldNodes; /** * Parent type of the field being resolved. * * @api */ public ObjectType $parentType; /** * Path to this field from the very root value. When fields are aliased, the path includes aliases. * * @api * * @var list<string|int> * * @phpstan-var Path */ public array $path; /** * Path to this field from the very root value. This will never include aliases. * * @api * * @var list<string|int> * * @phpstan-var Path */ public array $unaliasedPath; /** * Instance of a schema used for execution. * * @api */ public Schema $schema; /** * AST of all fragments defined in query. * * @api * * @var array<string, FragmentDefinitionNode> */ public array $fragments = []; /** * Root value passed to query execution. * * @api * * @var mixed */ public $rootValue; /** * AST of operation definition node (query, mutation). * * @api */ public OperationDefinitionNode $operation; /** * Array of variables passed to query execution. * * @api * * @var array<string, mixed> */ public array $variableValues = []; /** * @param \ArrayObject<int, FieldNode> $fieldNodes * @param list<string|int> $path * @param array<string, FragmentDefinitionNode> $fragments * @param mixed|null $rootValue * @param array<string, mixed> $variableValues * @param list<string|int> $unaliasedPath * * @phpstan-param Path $path * @phpstan-param Path $unaliasedPath */ public function __construct( FieldDefinition $fieldDefinition, \ArrayObject $fieldNodes, ObjectType $parentType, array $path, Schema $schema, array $fragments, $rootValue, OperationDefinitionNode $operation, array $variableValues, array $unaliasedPath = [] ) { $this->fieldDefinition = $fieldDefinition; $this->fieldName = $fieldDefinition->name; $this->returnType = $fieldDefinition->getType(); $this->fieldNodes = $fieldNodes; $this->parentType = $parentType; $this->path = $path; $this->unaliasedPath = $unaliasedPath; $this->schema = $schema; $this->fragments = $fragments; $this->rootValue = $rootValue; $this->operation = $operation; $this->variableValues = $variableValues; } /** * Returns names of all fields selected in query for `$this->fieldName` up to `$depth` levels. * * Example: * { * root { * id * nested { * nested1 * nested2 { * nested3 * } * } * } * } * * Given this ResolveInfo instance is a part of root field resolution, and $depth === 1, * this method will return: * [ * 'id' => true, * 'nested' => [ * 'nested1' => true, * 'nested2' => true, * ], * ] * * This method does not consider conditional typed fragments. * Use it with care for fields of interface and union types. * * @param int $depth How many levels to include in the output beyond the first * * @return array<string, mixed> * * @api */ public function getFieldSelection(int $depth = 0): array { $fields = []; foreach ($this->fieldNodes as $fieldNode) { $selectionSet = $fieldNode->selectionSet; if ($selectionSet !== null) { $fields = array_merge_recursive( $fields, $this->foldSelectionSet($selectionSet, $depth) ); } } return $fields; } /** * Returns names and args of all fields selected in query for `$this->fieldName` up to `$depth` levels, including aliases. * * The result maps original field names to a map of selections for that field, including aliases. * For each of those selections, you can find the following keys: * - "args" contains the passed arguments for this field/alias (not on an union inline fragment) * - "type" contains the related Type instance found (will be the same for all aliases of a field) * - "selectionSet" contains potential nested fields of this field/alias (only on ObjectType). The structure is recursive from here. * - "unions" contains potential object types contained in an UnionType (only on UnionType). The structure is recursive from here and will go through the selectionSet of the object types. * * Example: * { * root { * id * nested { * nested1(myArg: 1) * nested1Bis: nested1 * } * alias1: nested { * nested1(myArg: 2, mySecondAg: "test") * } * myUnion(myArg: 3) { * ...on Nested { * nested1(myArg: 4) * } * ...on MyCustomObject { * nested3 * } * } * } * } * * Given this ResolveInfo instance is a part of root field resolution, * $depth === 1, * and fields "nested" represents an ObjectType named "Nested", * this method will return: * [ * 'id' => [ * 'id' => [ * 'args' => [], * 'type' => GraphQL\Type\Definition\IntType Object ( ... )), * ], * ], * 'nested' => [ * 'nested' => [ * 'args' => [], * 'type' => GraphQL\Type\Definition\ObjectType Object ( ... )), * 'selectionSet' => [ * 'nested1' => [ * 'nested1' => [ * 'args' => [ * 'myArg' => 1, * ], * 'type' => GraphQL\Type\Definition\StringType Object ( ... )), * ], * 'nested1Bis' => [ * 'args' => [], * 'type' => GraphQL\Type\Definition\StringType Object ( ... )), * ], * ], * ], * ], * ], * 'alias1' => [ * 'alias1' => [ * 'args' => [], * 'type' => GraphQL\Type\Definition\ObjectType Object ( ... )), * 'selectionSet' => [ * 'nested1' => [ * 'nested1' => [ * 'args' => [ * 'myArg' => 2, * 'mySecondAg' => "test", * ], * 'type' => GraphQL\Type\Definition\StringType Object ( ... )), * ], * ], * ], * ], * ], * 'myUnion' => [ * 'myUnion' => [ * 'args' => [ * 'myArg' => 3, * ], * 'type' => GraphQL\Type\Definition\UnionType Object ( ... )), * 'unions' => [ * 'Nested' => [ * 'type' => GraphQL\Type\Definition\ObjectType Object ( ... )), * 'selectionSet' => [ * 'nested1' => [ * 'nested1' => [ * 'args' => [ * 'myArg' => 4, * ], * 'type' => GraphQL\Type\Definition\StringType Object ( ... )), * ], * ], * ], * ], * 'MyCustomObject' => [ * 'type' => GraphQL\Tests\Type\TestClasses\MyCustomType Object ( ... )), * 'selectionSet' => [ * 'nested3' => [ * 'nested3' => [ * 'args' => [], * 'type' => GraphQL\Type\Definition\StringType Object ( ... )), * ], * ], * ], * ], * ], * ], * ], * ] * * @param int $depth How many levels to include in the output beyond the first * * @throws \Exception * @throws Error * @throws InvariantViolation * * @return array<string, mixed> * * @api */ public function getFieldSelectionWithAliases(int $depth = 0): array { $fields = []; foreach ($this->fieldNodes as $fieldNode) { $selectionSet = $fieldNode->selectionSet; if ($selectionSet !== null) { $field = $this->parentType->getField($fieldNode->name->value); $fieldType = $field->getType(); $fields = array_merge_recursive( $fields, $this->foldSelectionWithAlias($selectionSet, $depth, $fieldType) ); } } return $fields; } /** * @param QueryPlanOptions $options * * @throws \Exception * @throws Error * @throws InvariantViolation */ public function lookAhead(array $options = []): QueryPlan { return new QueryPlan( $this->parentType, $this->schema, $this->fieldNodes, $this->variableValues, $this->fragments, $options ); } /** @return array<string, bool> */ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array { /** @var array<string, bool> $fields */ $fields = []; foreach ($selectionSet->selections as $selection) { if ($selection instanceof FieldNode) { $fields[$selection->name->value] = $descend > 0 && $selection->selectionSet !== null ? array_merge_recursive( $fields[$selection->name->value] ?? [], $this->foldSelectionSet($selection->selectionSet, $descend - 1) ) : true; } elseif ($selection instanceof FragmentSpreadNode) { $spreadName = $selection->name->value; $fragment = $this->fragments[$spreadName] ?? null; if ($fragment === null) { continue; } $fields = array_merge_recursive( $this->foldSelectionSet($fragment->selectionSet, $descend), $fields ); } elseif ($selection instanceof InlineFragmentNode) { $fields = array_merge_recursive( $this->foldSelectionSet($selection->selectionSet, $descend), $fields ); } } return $fields; } /** * @throws \Exception * @throws Error * @throws InvariantViolation * * @return array<string> */ private function foldSelectionWithAlias(SelectionSetNode $selectionSet, int $descend, Type $parentType): array { /** @var array<string, bool> $fields */ $fields = []; if ($parentType instanceof WrappingType) { $parentType = $parentType->getInnermostType(); } foreach ($selectionSet->selections as $selection) { if ($selection instanceof FieldNode) { $fieldName = $selection->name->value; $aliasName = $selection->alias->value ?? $fieldName; if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) { continue; } assert($parentType instanceof HasFieldsType, 'ensured by query validation'); $aliasInfo = &$fields[$fieldName][$aliasName]; $fieldDef = $parentType->getField($fieldName); $aliasInfo['args'] = Values::getArgumentValues($fieldDef, $selection, $this->variableValues); $fieldType = $fieldDef->getType(); $namedFieldType = $fieldType; if ($namedFieldType instanceof WrappingType) { $namedFieldType = $namedFieldType->getInnermostType(); } $aliasInfo['type'] = $namedFieldType; if ($descend <= 0) { continue; } $nestedSelectionSet = $selection->selectionSet; if ($nestedSelectionSet === null) { continue; } if ($namedFieldType instanceof UnionType) { $aliasInfo['unions'] = $this->foldSelectionWithAlias($nestedSelectionSet, $descend, $fieldType); continue; } $aliasInfo['selectionSet'] = $this->foldSelectionWithAlias($nestedSelectionSet, $descend - 1, $fieldType); } elseif ($selection instanceof FragmentSpreadNode) { $spreadName = $selection->name->value; $fragment = $this->fragments[$spreadName] ?? null; if ($fragment === null) { continue; } $fieldType = $this->schema->getType($fragment->typeCondition->name->value); assert($fieldType instanceof Type, 'ensured by query validation'); $fields = array_merge_recursive( $this->foldSelectionWithAlias($fragment->selectionSet, $descend, $fieldType), $fields ); } elseif ($selection instanceof InlineFragmentNode) { $typeCondition = $selection->typeCondition; $fieldType = $typeCondition === null ? $parentType : $this->schema->getType($typeCondition->name->value); assert($fieldType instanceof Type, 'ensured by query validation'); if ($parentType instanceof UnionType) { assert($fieldType instanceof NamedType, 'ensured by query validation'); $fieldTypeInfo = &$fields[$fieldType->name()]; $fieldTypeInfo['type'] = $fieldType; $fieldTypeInfo['selectionSet'] = $this->foldSelectionWithAlias($selection->selectionSet, $descend, $fieldType); continue; } $fields = array_merge_recursive( $this->foldSelectionWithAlias($selection->selectionSet, $descend, $fieldType), $fields ); } } return $fields; } } graphql/lib/Type/Definition/Argument.php 0000644 00000007511 15166657210 0014271 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType) * @phpstan-type UnnamedArgumentConfig array{ * name?: string, * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } * @phpstan-type ArgumentConfig array{ * name: string, * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } * @phpstan-type ArgumentListConfig iterable<ArgumentConfig|ArgumentType>|iterable<UnnamedArgumentConfig> */ class Argument { public string $name; /** @var mixed */ public $defaultValue; public ?string $description; public ?string $deprecationReason; /** @var Type&InputType */ private Type $type; public ?InputValueDefinitionNode $astNode; /** @phpstan-var ArgumentConfig */ public array $config; /** @phpstan-param ArgumentConfig $config */ public function __construct(array $config) { $this->name = $config['name']; $this->defaultValue = $config['defaultValue'] ?? null; $this->description = $config['description'] ?? null; $this->deprecationReason = $config['deprecationReason'] ?? null; // Do nothing for type, it is lazy loaded in getType() $this->astNode = $config['astNode'] ?? null; $this->config = $config; } /** * @phpstan-param ArgumentListConfig $config * * @return array<int, self> */ public static function listFromConfig(iterable $config): array { $list = []; foreach ($config as $name => $argConfig) { if (! is_array($argConfig)) { $argConfig = ['type' => $argConfig]; } /** @phpstan-var ArgumentConfig $argConfigWithName */ $argConfigWithName = $argConfig + ['name' => $name]; $list[] = new self($argConfigWithName); } return $list; } /** @return Type&InputType */ public function getType(): Type { if (! isset($this->type)) { $this->type = Schema::resolveType($this->config['type']); } return $this->type; } public function defaultValueExists(): bool { return array_key_exists('defaultValue', $this->config); } public function isRequired(): bool { return $this->getType() instanceof NonNull && ! $this->defaultValueExists(); } public function isDeprecated(): bool { return (bool) $this->deprecationReason; } /** * @param Type&NamedType $parentType * * @throws InvariantViolation */ public function assertValid(FieldDefinition $parentField, Type $parentType): void { $error = Utils::isValidNameError($this->name); if ($error !== null) { throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}:) {$error->getMessage()}"); } $type = Type::getNamedType($this->getType()); if (! $type instanceof InputType) { $notInputType = Utils::printSafe($this->type); throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}): argument type must be Input Type but got: {$notInputType}"); } if ($this->isRequired() && $this->isDeprecated()) { throw new InvariantViolation("Required argument {$parentType->name}.{$parentField->name}({$this->name}:) cannot be deprecated."); } } } graphql/lib/Type/Definition/ObjectType.php 0000644 00000012627 15166657210 0014563 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Deferred; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Executor; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Utils\Utils; /** * Object Type Definition. * * Most GraphQL types you define will be object types. * Object types have a name, but most importantly describe their fields. * * Example: * * $AddressType = new ObjectType([ * 'name' => 'Address', * 'fields' => [ * 'street' => GraphQL\Type\Definition\Type::string(), * 'number' => GraphQL\Type\Definition\Type::int(), * 'formatted' => [ * 'type' => GraphQL\Type\Definition\Type::string(), * 'resolve' => fn (AddressModel $address): string => "{$address->number} {$address->street}", * ], * ], * ]); * * When two types need to refer to each other, or a type needs to refer to * itself in a field, you can use a function expression (aka a closure or a * thunk) to supply the fields lazily. * * Example: * * $PersonType = null; * $PersonType = new ObjectType([ * 'name' => 'Person', * 'fields' => fn (): array => [ * 'name' => GraphQL\Type\Definition\Type::string(), * 'bestFriend' => $PersonType, * ], * ]); * * @phpstan-import-type FieldResolver from Executor * @phpstan-import-type ArgsMapper from Executor * * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType * @phpstan-type ObjectConfig array{ * name?: string|null, * description?: string|null, * resolveField?: FieldResolver|null, * argsMapper?: ArgsMapper|null, * fields: (callable(): iterable<mixed>)|iterable<mixed>, * interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>, * isTypeOf?: (callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): (bool|Deferred|null))|null, * astNode?: ObjectTypeDefinitionNode|null, * extensionASTNodes?: array<ObjectTypeExtensionNode>|null * } */ class ObjectType extends Type implements OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType { use HasFieldsTypeImplementation; use NamedTypeImplementation; use ImplementingTypeImplementation; public ?ObjectTypeDefinitionNode $astNode; /** @var array<ObjectTypeExtensionNode> */ public array $extensionASTNodes; /** * @var callable|null * * @phpstan-var FieldResolver|null */ public $resolveFieldFn; /** * @var callable|null * * @phpstan-var ArgsMapper|null */ public $argsMapper; /** @phpstan-var ObjectConfig */ public array $config; /** * @phpstan-param ObjectConfig $config * * @throws InvariantViolation */ public function __construct(array $config) { $this->name = $config['name'] ?? $this->inferName(); $this->description = $config['description'] ?? null; $this->resolveFieldFn = $config['resolveField'] ?? null; $this->argsMapper = $config['argsMapper'] ?? null; $this->astNode = $config['astNode'] ?? null; $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; $this->config = $config; } /** * @param mixed $type * * @throws InvariantViolation */ public static function assertObjectType($type): self { if (! ($type instanceof self)) { $notObjectType = Utils::printSafe($type); throw new InvariantViolation("Expected {$notObjectType} to be a GraphQL Object type."); } return $type; } /** * @param mixed $objectValue The resolved value for the object type * @param mixed $context The context that was passed to GraphQL::execute() * * @return bool|Deferred|null */ public function isTypeOf($objectValue, $context, ResolveInfo $info) { return isset($this->config['isTypeOf']) ? $this->config['isTypeOf']( $objectValue, $context, $info ) : null; } /** * Validates type config and throws if one of the type options is invalid. * Note: this method is shallow, it won't validate object fields and their arguments. * * @throws Error * @throws InvariantViolation */ public function assertValid(): void { Utils::assertValidName($this->name); $isTypeOf = $this->config['isTypeOf'] ?? null; // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if (isset($isTypeOf) && ! is_callable($isTypeOf)) { $notCallable = Utils::printSafe($isTypeOf); throw new InvariantViolation("{$this->name} must provide \"isTypeOf\" as null or a callable, but got: {$notCallable}."); } foreach ($this->getFields() as $field) { $field->assertValid($this); } $this->assertValidInterfaces(); } public function astNode(): ?ObjectTypeDefinitionNode { return $this->astNode; } /** @return array<ObjectTypeExtensionNode> */ public function extensionASTNodes(): array { return $this->extensionASTNodes; } } graphql/lib/Type/Definition/WrappingType.php 0000644 00000000630 15166657210 0015133 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; interface WrappingType { /** Return the wrapped type, which may itself be a wrapping type. */ public function getWrappedType(): Type; /** * Return the innermost wrapped type, which is guaranteed to be a named type. * * @return Type&NamedType */ public function getInnermostType(): NamedType; } graphql/lib/Type/Definition/PhpEnumType.php 0000644 00000011131 15166657210 0014716 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Utils\PhpDoc; use YOOtheme\GraphQL\Utils\Utils; /** @phpstan-import-type PartialEnumValueConfig from EnumType */ class PhpEnumType extends EnumType { public const MULTIPLE_DESCRIPTIONS_DISALLOWED = 'Using more than 1 Description attribute is not supported.'; public const MULTIPLE_DEPRECATIONS_DISALLOWED = 'Using more than 1 Deprecated attribute is not supported.'; /** @var class-string<\UnitEnum> */ protected string $enumClass; /** * @param class-string<\UnitEnum> $enumClass The fully qualified class name of a native PHP enum * @param string|null $name The name the enum will have in the schema, defaults to the basename of the given class * @param string|null $description The description the enum will have in the schema, defaults to PHPDoc of the given class * @param array<EnumTypeExtensionNode>|null $extensionASTNodes * * @throws \Exception * @throws \ReflectionException */ public function __construct( string $enumClass, ?string $name = null, ?string $description = null, ?EnumTypeDefinitionNode $astNode = null, ?array $extensionASTNodes = null ) { $this->enumClass = $enumClass; $reflection = new \ReflectionEnum($enumClass); /** * @var array<string, PartialEnumValueConfig> $enumDefinitions */ $enumDefinitions = []; foreach ($reflection->getCases() as $case) { $enumDefinitions[$case->name] = [ 'value' => $case->getValue(), 'description' => $this->extractDescription($case), 'deprecationReason' => $this->deprecationReason($case), ]; } parent::__construct([ 'name' => $name ?? $this->baseName($enumClass), 'values' => $enumDefinitions, 'description' => $description ?? $this->extractDescription($reflection), 'astNode' => $astNode, 'extensionASTNodes' => $extensionASTNodes, ]); } public function serialize($value): string { if ($value instanceof $this->enumClass) { return $value->name; } if (is_a($this->enumClass, \BackedEnum::class, true)) { try { $instance = $this->enumClass::from($value); } catch (\ValueError|\TypeError $_) { $notEnumInstanceOrValue = Utils::printSafe($value); throw new SerializationError("Cannot serialize value as enum: {$notEnumInstanceOrValue}, expected instance or valid value of {$this->enumClass}."); } return $instance->name; } $notEnum = Utils::printSafe($value); throw new SerializationError("Cannot serialize value as enum: {$notEnum}, expected instance of {$this->enumClass}."); } public function parseValue($value) { // Can happen when variable values undergo a serialization cycle before execution if ($value instanceof $this->enumClass) { return $value; } return parent::parseValue($value); } /** @param class-string $class */ protected function baseName(string $class): string { $parts = explode('\\', $class); return end($parts); } /** * @param \ReflectionClassConstant|\ReflectionClass<\UnitEnum> $reflection * * @throws \Exception */ protected function extractDescription(\ReflectionClassConstant|\ReflectionClass $reflection): ?string { $attributes = $reflection->getAttributes(Description::class); if (count($attributes) === 1) { return $attributes[0]->newInstance()->description; } if (count($attributes) > 1) { throw new \Exception(self::MULTIPLE_DESCRIPTIONS_DISALLOWED); } $comment = $reflection->getDocComment(); $unpadded = PhpDoc::unpad($comment); return PhpDoc::unwrap($unpadded); } /** @throws \Exception */ protected function deprecationReason(\ReflectionClassConstant $reflection): ?string { $attributes = $reflection->getAttributes(Deprecated::class); if (count($attributes) === 1) { return $attributes[0]->newInstance()->reason; } if (count($attributes) > 1) { throw new \Exception(self::MULTIPLE_DEPRECATIONS_DISALLOWED); } return null; } } graphql/lib/Type/Definition/HasFieldsTypeImplementation.php 0000644 00000004741 15166657210 0020123 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; /** * @see HasFieldsType */ trait HasFieldsTypeImplementation { /** * Lazily initialized. * * @var array<string, FieldDefinition|UnresolvedFieldDefinition> */ private array $fields; /** @throws InvariantViolation */ private function initializeFields(): void { if (isset($this->fields)) { return; } $this->fields = FieldDefinition::defineFieldMap($this, $this->config['fields']); } /** @throws InvariantViolation */ public function getField(string $name): FieldDefinition { $field = $this->findField($name); if ($field === null) { throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\""); } return $field; } /** @throws InvariantViolation */ public function findField(string $name): ?FieldDefinition { $this->initializeFields(); if (! isset($this->fields[$name])) { return null; } $field = $this->fields[$name]; if ($field instanceof UnresolvedFieldDefinition) { return $this->fields[$name] = $field->resolve(); } return $field; } /** @throws InvariantViolation */ public function hasField(string $name): bool { $this->initializeFields(); return isset($this->fields[$name]); } /** @throws InvariantViolation */ public function getFields(): array { $this->initializeFields(); foreach ($this->fields as $name => $field) { if ($field instanceof UnresolvedFieldDefinition) { $this->fields[$name] = $field->resolve(); } } // @phpstan-ignore-next-line all field definitions are now resolved return $this->fields; } public function getVisibleFields(): array { return array_filter( $this->getFields(), fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->isVisible() ); } /** @throws InvariantViolation */ public function getFieldNames(): array { $this->initializeFields(); $visibleFieldNames = array_map( fn (FieldDefinition $fieldDefinition): string => $fieldDefinition->getName(), $this->getVisibleFields() ); return array_values($visibleFieldNames); } } graphql/lib/Type/Definition/NonNull.php 0000644 00000002177 15166657210 0014077 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Type\Schema; /** * @phpstan-type WrappedType (NullableType&Type)|callable():(NullableType&Type) */ class NonNull extends Type implements WrappingType, OutputType, InputType { /** * @var Type|callable * * @phpstan-var WrappedType */ private $wrappedType; /** * @param Type|callable $type * * @phpstan-param WrappedType $type */ public function __construct($type) { $this->wrappedType = $type; } public function toString(): string { return $this->getWrappedType()->toString() . '!'; } /** @return NullableType&Type */ public function getWrappedType(): Type { return Schema::resolveType($this->wrappedType); } public function getInnermostType(): NamedType { $type = $this->getWrappedType(); while ($type instanceof WrappingType) { $type = $type->getWrappedType(); } assert($type instanceof NamedType, 'known because we unwrapped all the way down'); return $type; } } graphql/lib/Type/Definition/LeafType.php 0000644 00000002610 15166657210 0014213 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\ValueNode; /* export type GraphQLLeafType = GraphQLScalarType | GraphQLEnumType; */ interface LeafType { /** * Serializes an internal value to include in a response. * * Should throw an exception on invalid values. * * @param mixed $value * * @throws SerializationError * * @return mixed */ public function serialize($value); /** * Parses an externally provided value (query variable) to use as an input. * * Should throw an exception with a client-friendly message on invalid values, @see ClientAware. * * @param mixed $value * * @throws Error * * @return mixed */ public function parseValue($value); /** * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input. * * Should throw an exception with a client-friendly message on invalid value nodes, @see ClientAware. * * @param ValueNode&Node $valueNode * @param array<string, mixed>|null $variables * * @throws Error * * @return mixed */ public function parseLiteral(Node $valueNode, ?array $variables = null); } graphql/lib/Type/Definition/Type.php 0000644 00000016740 15166657210 0013434 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Utils\Utils; /** * Registry of standard GraphQL types and base class for all other types. */ abstract class Type implements \JsonSerializable { public const INT = 'Int'; public const FLOAT = 'Float'; public const STRING = 'String'; public const BOOLEAN = 'Boolean'; public const ID = 'ID'; public const STANDARD_TYPE_NAMES = [ self::INT, self::FLOAT, self::STRING, self::BOOLEAN, self::ID, ]; public const BUILT_IN_TYPE_NAMES = [ ...self::STANDARD_TYPE_NAMES, ...Introspection::TYPE_NAMES, ]; /** @var array<string, ScalarType>|null */ protected static ?array $standardTypes; /** @var array<string, Type&NamedType>|null */ protected static ?array $builtInTypes; /** * Returns the registered or default standard Int type. * * @api */ public static function int(): ScalarType { return static::$standardTypes[self::INT] ??= new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** * Returns the registered or default standard Float type. * * @api */ public static function float(): ScalarType { return static::$standardTypes[self::FLOAT] ??= new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** * Returns the registered or default standard String type. * * @api */ public static function string(): ScalarType { return static::$standardTypes[self::STRING] ??= new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** * Returns the registered or default standard Boolean type. * * @api */ public static function boolean(): ScalarType { return static::$standardTypes[self::BOOLEAN] ??= new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** * Returns the registered or default standard ID type. * * @api */ public static function id(): ScalarType { return static::$standardTypes[self::ID] ??= new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct) } /** * Wraps the given type in a list type. * * @template T of Type * * @param T|callable():T $type * * @return ListOfType<T> * * @api */ public static function listOf($type): ListOfType { return new ListOfType($type); } /** * Wraps the given type in a non-null type. * * @param NonNull|(NullableType&Type)|callable():(NullableType&Type) $type * * @api */ public static function nonNull($type): NonNull { if ($type instanceof NonNull) { return $type; } return new NonNull($type); } /** * Returns all builtin in types including base scalar and introspection types. * * @return array<string, Type&NamedType> */ public static function builtInTypes(): array { return self::$builtInTypes ??= array_merge( Introspection::getTypes(), self::getStandardTypes() ); } /** * Returns all builtin scalar types. * * @return array<string, ScalarType> */ public static function getStandardTypes(): array { return [ self::INT => static::int(), self::FLOAT => static::float(), self::STRING => static::string(), self::BOOLEAN => static::boolean(), self::ID => static::id(), ]; } /** * Allows partially or completely overriding the standard types. * * @param array<ScalarType> $types * * @throws InvariantViolation */ public static function overrideStandardTypes(array $types): void { // Reset caches that might contain instances of standard types static::$builtInTypes = null; Introspection::resetCachedInstances(); Directive::resetCachedInstances(); foreach ($types as $type) { // @phpstan-ignore-next-line generic type is not enforced by PHP if (! $type instanceof ScalarType) { $typeClass = ScalarType::class; $notType = Utils::printSafe($type); throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}"); } if (! in_array($type->name, self::STANDARD_TYPE_NAMES, true)) { $standardTypeNames = implode(', ', self::STANDARD_TYPE_NAMES); $notStandardTypeName = Utils::printSafe($type->name); throw new InvariantViolation("Expecting one of the following names for a standard type: {$standardTypeNames}; got {$notStandardTypeName}"); } static::$standardTypes[$type->name] = $type; } } /** * Determines if the given type is an input type. * * @param mixed $type * * @api */ public static function isInputType($type): bool { return self::getNamedType($type) instanceof InputType; } /** * Returns the underlying named type of the given type. * * @return (Type&NamedType)|null * * @phpstan-return ($type is null ? null : Type&NamedType) * * @api */ public static function getNamedType(?Type $type): ?Type { if ($type instanceof WrappingType) { return $type->getInnermostType(); } assert($type === null || $type instanceof NamedType, 'only other option'); return $type; } /** * Determines if the given type is an output type. * * @param mixed $type * * @api */ public static function isOutputType($type): bool { return self::getNamedType($type) instanceof OutputType; } /** * Determines if the given type is a leaf type. * * @param mixed $type * * @api */ public static function isLeafType($type): bool { return $type instanceof LeafType; } /** * Determines if the given type is a composite type. * * @param mixed $type * * @api */ public static function isCompositeType($type): bool { return $type instanceof CompositeType; } /** * Determines if the given type is an abstract type. * * @param mixed $type * * @api */ public static function isAbstractType($type): bool { return $type instanceof AbstractType; } /** * Unwraps a potentially non-null type to return the underlying nullable type. * * @return Type&NullableType * * @api */ public static function getNullableType(Type $type): Type { if ($type instanceof NonNull) { return $type->getWrappedType(); } assert($type instanceof NullableType, 'only other option'); return $type; } abstract public function toString(): string; public function __toString(): string { return $this->toString(); } #[\ReturnTypeWillChange] public function jsonSerialize(): string { return $this->toString(); } } graphql/lib/Type/Definition/FieldDefinition.php 0000644 00000017456 15166657210 0015554 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Executor; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; /** * @see Executor * * @phpstan-import-type FieldResolver from Executor * @phpstan-import-type ArgsMapper from Executor * @phpstan-import-type ArgumentListConfig from Argument * * @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType) * @phpstan-type ComplexityFn callable(int, array<string, mixed>): int * @phpstan-type VisibilityFn callable(): bool * @phpstan-type FieldDefinitionConfig array{ * name: string, * type: FieldType, * resolve?: FieldResolver|null, * args?: ArgumentListConfig|null, * argsMapper?: ArgsMapper|null, * description?: string|null, * visible?: VisibilityFn|bool, * deprecationReason?: string|null, * astNode?: FieldDefinitionNode|null, * complexity?: ComplexityFn|null * } * @phpstan-type UnnamedFieldDefinitionConfig array{ * type: FieldType, * resolve?: FieldResolver|null, * args?: ArgumentListConfig|null, * argsMapper?: ArgsMapper|null, * description?: string|null, * visible?: VisibilityFn|bool, * deprecationReason?: string|null, * astNode?: FieldDefinitionNode|null, * complexity?: ComplexityFn|null * } * @phpstan-type FieldsConfig iterable<mixed>|callable(): iterable<mixed> */ /* * TODO check if newer versions of PHPStan can handle the full definition, it currently crashes when it is used * @phpstan-type EagerListEntry FieldDefinitionConfig|(Type&OutputType) * @phpstan-type EagerMapEntry UnnamedFieldDefinitionConfig|FieldDefinition * @phpstan-type FieldsList iterable<EagerListEntry|(callable(): EagerListEntry)> * @phpstan-type FieldsMap iterable<string, EagerMapEntry|(callable(): EagerMapEntry)> * @phpstan-type FieldsIterable FieldsList|FieldsMap * @phpstan-type FieldsConfig FieldsIterable|(callable(): FieldsIterable) */ class FieldDefinition { public string $name; /** @var array<int, Argument> */ public array $args; /** * Callback to transform args to value object. * * @var callable|null * * @phpstan-var ArgsMapper|null */ public $argsMapper; /** * Callback for resolving field value given parent value. * * @var callable|null * * @phpstan-var FieldResolver|null */ public $resolveFn; public ?string $description; /** * @var callable|bool * * @phpstan-var VisibilityFn|bool */ public $visible; public ?string $deprecationReason; public ?FieldDefinitionNode $astNode; /** * @var callable|null * * @phpstan-var ComplexityFn|null */ public $complexityFn; /** * Original field definition config. * * @phpstan-var FieldDefinitionConfig */ public array $config; /** @var Type&OutputType */ private Type $type; /** @param FieldDefinitionConfig $config */ public function __construct(array $config) { $this->name = $config['name']; $this->resolveFn = $config['resolve'] ?? null; $this->args = isset($config['args']) ? Argument::listFromConfig($config['args']) : []; $this->argsMapper = $config['argsMapper'] ?? null; $this->description = $config['description'] ?? null; $this->visible = $config['visible'] ?? true; $this->deprecationReason = $config['deprecationReason'] ?? null; $this->astNode = $config['astNode'] ?? null; $this->complexityFn = $config['complexity'] ?? null; $this->config = $config; } /** * @param ObjectType|InterfaceType $parentType * @param callable|iterable $fields * * @phpstan-param FieldsConfig $fields * * @throws InvariantViolation * * @return array<string, self|UnresolvedFieldDefinition> */ public static function defineFieldMap(Type $parentType, $fields): array { if (is_callable($fields)) { $fields = $fields(); } if (! is_iterable($fields)) { throw new InvariantViolation("{$parentType->name} fields must be an iterable or a callable which returns such an iterable."); } $map = []; foreach ($fields as $maybeName => $field) { if (is_array($field)) { if (! isset($field['name'])) { if (! is_string($maybeName)) { throw new InvariantViolation("{$parentType->name} fields must be an associative array with field names as keys or a function which returns such an array."); } $field['name'] = $maybeName; } // @phpstan-ignore-next-line PHPStan won't let us define the whole type $fieldDef = new self($field); } elseif ($field instanceof self) { $fieldDef = $field; } elseif (is_callable($field)) { if (! is_string($maybeName)) { throw new InvariantViolation("{$parentType->name} lazy fields must be an associative array with field names as keys."); } $fieldDef = new UnresolvedFieldDefinition($maybeName, $field); } elseif ($field instanceof Type) { // @phpstan-ignore-next-line PHPStan won't let us define the whole type $fieldDef = new self([ 'name' => $maybeName, 'type' => $field, ]); } else { $invalidFieldConfig = Utils::printSafe($field); throw new InvariantViolation("{$parentType->name}.{$maybeName} field config must be an array, but got: {$invalidFieldConfig}"); } $map[$fieldDef->getName()] = $fieldDef; } return $map; } public function getArg(string $name): ?Argument { foreach ($this->args as $arg) { if ($arg->name === $name) { return $arg; } } return null; } public function getName(): string { return $this->name; } /** @return Type&OutputType */ public function getType(): Type { return $this->type ??= Schema::resolveType($this->config['type']); } public function isVisible(): bool { if (is_bool($this->visible)) { return $this->visible; } return $this->visible = ($this->visible)(); } public function isDeprecated(): bool { return (bool) $this->deprecationReason; } /** * @param Type&NamedType $parentType * * @throws InvariantViolation */ public function assertValid(Type $parentType): void { $error = Utils::isValidNameError($this->name); if ($error !== null) { throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}"); } $type = Type::getNamedType($this->getType()); if (! $type instanceof OutputType) { $safeType = Utils::printSafe($this->type); throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Output Type but got: {$safeType}."); } // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if ($this->resolveFn !== null && ! is_callable($this->resolveFn)) { $safeResolveFn = Utils::printSafe($this->resolveFn); throw new InvariantViolation("{$parentType->name}.{$this->name} field resolver must be a function if provided, but got: {$safeResolveFn}."); } foreach ($this->args as $fieldArgument) { $fieldArgument->assertValid($this, $type); } } } graphql/lib/Type/Definition/Description.php 0000644 00000000327 15166657210 0014770 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; #[\Attribute(\Attribute::TARGET_ALL)] class Description { public function __construct( public string $description, ) {} } graphql/lib/Type/Definition/CompositeType.php 0000644 00000000317 15166657210 0015310 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /* export type GraphQLCompositeType = GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType; */ interface CompositeType {} graphql/lib/Type/Definition/EnumType.php 0000644 00000017671 15166657210 0014265 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Utils\MixedStore; use YOOtheme\GraphQL\Utils\Utils; /** * @see EnumValueDefinitionNode * * @phpstan-type PartialEnumValueConfig array{ * name?: string, * value?: mixed, * deprecationReason?: string|null, * description?: string|null, * astNode?: EnumValueDefinitionNode|null * } * @phpstan-type EnumValues iterable<string, PartialEnumValueConfig>|iterable<string, mixed>|iterable<int, string> * @phpstan-type EnumTypeConfig array{ * name?: string|null, * description?: string|null, * values: EnumValues|callable(): EnumValues, * astNode?: EnumTypeDefinitionNode|null, * extensionASTNodes?: array<EnumTypeExtensionNode>|null * } */ class EnumType extends Type implements InputType, OutputType, LeafType, NullableType, NamedType { use NamedTypeImplementation; public ?EnumTypeDefinitionNode $astNode; /** @var array<EnumTypeExtensionNode> */ public array $extensionASTNodes; /** @phpstan-var EnumTypeConfig */ public array $config; /** * Lazily initialized. * * @var array<int, EnumValueDefinition> */ private array $values; /** * Lazily initialized. * * @var MixedStore<EnumValueDefinition> */ private MixedStore $valueLookup; /** @var array<string, EnumValueDefinition> */ private array $nameLookup; /** * @phpstan-param EnumTypeConfig $config * * @throws InvariantViolation */ public function __construct(array $config) { $this->name = $config['name'] ?? $this->inferName(); $this->description = $config['description'] ?? null; $this->astNode = $config['astNode'] ?? null; $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; $this->config = $config; } /** @throws InvariantViolation */ public function getValue(string $name): ?EnumValueDefinition { if (! isset($this->nameLookup)) { $this->initializeNameLookup(); } return $this->nameLookup[$name] ?? null; } /** * @throws InvariantViolation * * @return array<int, EnumValueDefinition> */ public function getValues(): array { if (! isset($this->values)) { $this->values = []; $values = $this->config['values']; if (is_callable($values)) { $values = $values(); } // We are just assuming the config option is set correctly here, validation happens in assertValid() foreach ($values as $name => $value) { if (is_string($name)) { if (is_array($value)) { $value += ['name' => $name, 'value' => $name]; } else { $value = ['name' => $name, 'value' => $value]; } } elseif (is_string($value)) { $value = ['name' => $value, 'value' => $value]; } else { throw new InvariantViolation("{$this->name} values must be an array with value names as keys or values."); } // @phpstan-ignore-next-line assume the config matches $this->values[] = new EnumValueDefinition($value); } } return $this->values; } /** * @throws \InvalidArgumentException * @throws InvariantViolation * @throws SerializationError */ public function serialize($value) { $lookup = $this->getValueLookup(); if (isset($lookup[$value])) { return $lookup[$value]->name; } if ($value instanceof \BackedEnum) { return $value->name; } if ($value instanceof \UnitEnum) { return $value->name; } $safeValue = Utils::printSafe($value); throw new SerializationError("Cannot serialize value as enum: {$safeValue}"); } /** * @throws \InvalidArgumentException * @throws InvariantViolation * * @return MixedStore<EnumValueDefinition> */ private function getValueLookup(): MixedStore { if (! isset($this->valueLookup)) { $this->valueLookup = new MixedStore(); foreach ($this->getValues() as $value) { $this->valueLookup->offsetSet($value->value, $value); } } return $this->valueLookup; } /** * @throws Error * @throws InvariantViolation */ public function parseValue($value) { if (! is_string($value)) { $safeValue = Utils::printSafeJson($value); throw new Error("Enum \"{$this->name}\" cannot represent non-string value: {$safeValue}.{$this->didYouMean($safeValue)}"); } if (! isset($this->nameLookup)) { $this->initializeNameLookup(); } if (! isset($this->nameLookup[$value])) { throw new Error("Value \"{$value}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($value)}"); } return $this->nameLookup[$value]->value; } /** * @throws \JsonException * @throws Error * @throws InvariantViolation */ public function parseLiteral(Node $valueNode, ?array $variables = null) { if (! $valueNode instanceof EnumValueNode) { $valueStr = Printer::doPrint($valueNode); throw new Error("Enum \"{$this->name}\" cannot represent non-enum value: {$valueStr}.{$this->didYouMean($valueStr)}", $valueNode); } $name = $valueNode->value; if (! isset($this->nameLookup)) { $this->initializeNameLookup(); } if (isset($this->nameLookup[$name])) { return $this->nameLookup[$name]->value; } $valueStr = Printer::doPrint($valueNode); throw new Error("Value \"{$valueStr}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($valueStr)}", $valueNode); } /** * @throws Error * @throws InvariantViolation */ public function assertValid(): void { Utils::assertValidName($this->name); $values = $this->config['values'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime) if (! is_iterable($values) && ! is_callable($values)) { $notIterable = Utils::printSafe($values); throw new InvariantViolation("{$this->name} values must be an iterable or callable, got: {$notIterable}"); } $this->getValues(); } /** @throws InvariantViolation */ private function initializeNameLookup(): void { $this->nameLookup = []; foreach ($this->getValues() as $value) { $this->nameLookup[$value->name] = $value; } } /** @throws InvariantViolation */ protected function didYouMean(string $unknownValue): ?string { $suggestions = Utils::suggestionList( $unknownValue, array_map( static fn (EnumValueDefinition $value): string => $value->name, $this->getValues() ) ); return $suggestions === [] ? null : ' Did you mean the enum value ' . Utils::quotedOrList($suggestions) . '?'; } public function astNode(): ?EnumTypeDefinitionNode { return $this->astNode; } /** @return array<EnumTypeExtensionNode> */ public function extensionASTNodes(): array { return $this->extensionASTNodes; } } graphql/lib/Type/Definition/NamedType.php 0000644 00000001777 15166657210 0014405 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeExtensionNode; /** * export type NamedType = * | ScalarType * | ObjectType * | InterfaceType * | UnionType * | EnumType * | InputObjectType;. * * @property string $name * @property string|null $description * @property (Node&TypeDefinitionNode)|null $astNode * @property array<Node&TypeExtensionNode> $extensionASTNodes */ interface NamedType { /** @throws Error */ public function assertValid(): void; /** Is this type a built-in type? */ public function isBuiltInType(): bool; public function name(): string; public function description(): ?string; /** @return (Node&TypeDefinitionNode)|null */ public function astNode(): ?Node; /** @return array<Node&TypeExtensionNode> */ public function extensionASTNodes(): array; } graphql/lib/Type/Definition/QueryPlan.php 0000644 00000023427 15166657210 0014433 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Values; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\FragmentSpreadNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; /** * @phpstan-type QueryPlanOptions array{ * groupImplementorFields?: bool, * } */ class QueryPlan { /** * Map from type names to a list of fields referenced of that type. * * @var array<string, array<string, true>> */ private array $typeToFields = []; private Schema $schema; /** @var array<string, mixed> */ private array $queryPlan = []; /** @var array<string, mixed> */ private array $variableValues; /** @var array<string, FragmentDefinitionNode> */ private array $fragments; private bool $groupImplementorFields; /** * @param iterable<FieldNode> $fieldNodes * @param array<string, mixed> $variableValues * @param array<string, FragmentDefinitionNode> $fragments * @param QueryPlanOptions $options * * @throws \Exception * @throws Error * @throws InvariantViolation */ public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = []) { $this->schema = $schema; $this->variableValues = $variableValues; $this->fragments = $fragments; $this->groupImplementorFields = $options['groupImplementorFields'] ?? false; $this->analyzeQueryPlan($parentType, $fieldNodes); } /** @return array<string, mixed> */ public function queryPlan(): array { return $this->queryPlan; } /** @return array<int, string> */ public function getReferencedTypes(): array { return array_keys($this->typeToFields); } public function hasType(string $type): bool { return isset($this->typeToFields[$type]); } /** * TODO return array<string, true>. * * @return array<int, string> */ public function getReferencedFields(): array { $allFields = []; foreach ($this->typeToFields as $fields) { foreach ($fields as $field => $_) { $allFields[$field] = true; } } return array_keys($allFields); } public function hasField(string $field): bool { foreach ($this->typeToFields as $fields) { if (array_key_exists($field, $fields)) { return true; } } return false; } /** * TODO return array<string, true>. * * @return array<int, string> */ public function subFields(string $typename): array { return array_keys($this->typeToFields[$typename] ?? []); } /** * @param iterable<FieldNode> $fieldNodes * * @throws \Exception * @throws Error * @throws InvariantViolation */ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes): void { $queryPlan = []; $implementors = []; foreach ($fieldNodes as $fieldNode) { if ($fieldNode->selectionSet === null) { continue; } $type = Type::getNamedType( $parentType->getField($fieldNode->name->value)->getType() ); $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors); $queryPlan = $this->arrayMergeDeep($queryPlan, $subfields); } if ($this->groupImplementorFields) { $this->queryPlan = ['fields' => $queryPlan]; if ($implementors !== []) { $this->queryPlan['implementors'] = $implementors; } } else { $this->queryPlan = $queryPlan; } } /** * @param Type&NamedType $parentType * @param array<string, mixed> $implementors * * @throws \Exception * @throws Error * @throws InvariantViolation * * @return array<mixed> */ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType, array &$implementors): array { $fields = []; $implementors = []; foreach ($selectionSet->selections as $selection) { if ($selection instanceof FieldNode) { $fieldName = $selection->name->value; if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) { continue; } assert($parentType instanceof HasFieldsType, 'ensured by query validation'); $type = $parentType->getField($fieldName); $selectionType = $type->getType(); $subImplementors = []; $nestedSelectionSet = $selection->selectionSet; $subfields = $nestedSelectionSet === null ? [] : $this->analyzeSubFields($selectionType, $nestedSelectionSet, $subImplementors); $fields[$fieldName] = [ 'type' => $selectionType, 'fields' => $subfields, 'args' => Values::getArgumentValues($type, $selection, $this->variableValues), ]; if ($this->groupImplementorFields && $subImplementors !== []) { $fields[$fieldName]['implementors'] = $subImplementors; } } elseif ($selection instanceof FragmentSpreadNode) { $spreadName = $selection->name->value; $fragment = $this->fragments[$spreadName] ?? null; if ($fragment === null) { continue; } $type = $this->schema->getType($fragment->typeCondition->name->value); assert($type instanceof Type, 'ensured by query validation'); $subfields = $this->analyzeSubFields($type, $fragment->selectionSet); $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors); } elseif ($selection instanceof InlineFragmentNode) { $typeCondition = $selection->typeCondition; $type = $typeCondition === null ? $parentType : $this->schema->getType($typeCondition->name->value); assert($type instanceof Type, 'ensured by query validation'); $subfields = $this->analyzeSubFields($type, $selection->selectionSet); $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors); } } $parentTypeName = $parentType->name(); // TODO evaluate if this line is really necessary. // It causes abstract types to appear in getReferencedTypes() even if they do not have any fields directly referencing them. $this->typeToFields[$parentTypeName] ??= []; foreach ($fields as $fieldName => $_) { $this->typeToFields[$parentTypeName][$fieldName] = true; } return $fields; } /** * @param array<string, mixed> $implementors * * @throws \Exception * @throws Error * * @return array<mixed> */ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet, array &$implementors = []): array { $type = Type::getNamedType($type); return $type instanceof ObjectType || $type instanceof AbstractType ? $this->analyzeSelectionSet($selectionSet, $type, $implementors) : []; } /** * @param Type&NamedType $parentType * @param Type&NamedType $type * @param array<mixed> $fields * @param array<mixed> $subfields * @param array<string, mixed> $implementors * * @return array<mixed> */ private function mergeFields(Type $parentType, Type $type, array $fields, array $subfields, array &$implementors): array { if ($this->groupImplementorFields && $parentType instanceof AbstractType && ! $type instanceof AbstractType) { $name = $type->name; assert(is_string($name)); $implementors[$name] = [ 'type' => $type, 'fields' => $this->arrayMergeDeep( $implementors[$name]['fields'] ?? [], array_diff_key($subfields, $fields) ), ]; $fields = $this->arrayMergeDeep( $fields, array_intersect_key($subfields, $fields) ); } else { $fields = $this->arrayMergeDeep($subfields, $fields); } return $fields; } /** * Merges nested arrays, but handles non array values differently from array_merge_recursive. * While array_merge_recursive tries to merge non-array values, in this implementation they will be overwritten. * * @see https://stackoverflow.com/a/25712428 * * @param array<mixed> $array1 * @param array<mixed> $array2 * * @return array<mixed> */ private function arrayMergeDeep(array $array1, array $array2): array { foreach ($array2 as $key => &$value) { if (is_numeric($key)) { if (! in_array($value, $array1, true)) { $array1[] = $value; } } elseif (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) { $array1[$key] = $this->arrayMergeDeep($array1[$key], $value); } else { $array1[$key] = $value; } } return $array1; } } graphql/lib/Type/Definition/NamedTypeImplementation.php 0000644 00000002604 15166657210 0017301 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; /** @see NamedType */ trait NamedTypeImplementation { public string $name; public ?string $description; public function toString(): string { return $this->name; } /** @throws InvariantViolation */ protected function inferName(): string { if (isset($this->name)) { // @phpstan-ignore-line property might be uninitialized return $this->name; } // If class is extended - infer name from className // QueryType -> Type // SomeOtherType -> SomeOther $reflection = new \ReflectionClass($this); $name = $reflection->getShortName(); if ($reflection->getNamespaceName() !== __NAMESPACE__) { $withoutPrefixType = preg_replace('~Type$~', '', $name); assert(is_string($withoutPrefixType), 'regex is statically known to be correct'); return $withoutPrefixType; } throw new InvariantViolation('Must provide name for Type.'); } public function isBuiltInType(): bool { return in_array($this->name, Type::BUILT_IN_TYPE_NAMES, true); } public function name(): string { return $this->name; } public function description(): ?string { return $this->description; } } graphql/lib/Type/Definition/BooleanType.php 0000644 00000002671 15166657210 0014732 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Language\AST\BooleanValueNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Utils\Utils; class BooleanType extends ScalarType { public string $name = Type::BOOLEAN; public ?string $description = 'The `Boolean` scalar type represents `true` or `false`.'; /** * Serialize the given value to a Boolean. * * The GraphQL spec leaves this up to the implementations, so we just do what * PHP does natively to make this intuitive for developers. */ public function serialize($value): bool { return (bool) $value; } /** @throws Error */ public function parseValue($value): bool { if (is_bool($value)) { return $value; } $notBoolean = Utils::printSafeJson($value); throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}"); } /** * @throws \JsonException * @throws Error */ public function parseLiteral(Node $valueNode, ?array $variables = null): bool { if ($valueNode instanceof BooleanValueNode) { return $valueNode->value; } $notBoolean = Printer::doPrint($valueNode); throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}", $valueNode); } } graphql/lib/Type/Definition/ScalarType.php 0000644 00000004024 15166657210 0014552 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Utils\Utils; /** * Scalar Type Definition. * * The leaf values of any request and input values to arguments are * Scalars (or Enums) and are defined with a name and a series of coercion * functions used to ensure validity. * * Example: * * class OddType extends ScalarType * { * public $name = 'Odd', * public function serialize($value) * { * return $value % 2 === 1 ? $value : null; * } * } * * @phpstan-type ScalarConfig array{ * name?: string|null, * description?: string|null, * astNode?: ScalarTypeDefinitionNode|null, * extensionASTNodes?: array<ScalarTypeExtensionNode>|null * } */ abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NullableType, NamedType { use NamedTypeImplementation; public ?ScalarTypeDefinitionNode $astNode; /** @var array<ScalarTypeExtensionNode> */ public array $extensionASTNodes; /** @phpstan-var ScalarConfig */ public array $config; /** * @phpstan-param ScalarConfig $config * * @throws InvariantViolation */ public function __construct(array $config = []) { $this->name = $config['name'] ?? $this->inferName(); $this->description = $config['description'] ?? $this->description ?? null; $this->astNode = $config['astNode'] ?? null; $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; $this->config = $config; } public function assertValid(): void { Utils::assertValidName($this->name); } public function astNode(): ?ScalarTypeDefinitionNode { return $this->astNode; } /** @return array<ScalarTypeExtensionNode> */ public function extensionASTNodes(): array { return $this->extensionASTNodes; } } graphql/lib/Type/Definition/OutputType.php 0000644 00000000356 15166657210 0014651 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /* GraphQLScalarType | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType | GraphQLEnumType | GraphQLList | GraphQLNonNull; */ interface OutputType {} graphql/lib/Type/Definition/InputObjectField.php 0000644 00000006551 15166657210 0015704 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType) * @phpstan-type InputObjectFieldConfig array{ * name: string, * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } * @phpstan-type UnnamedInputObjectFieldConfig array{ * name?: string, * type: ArgumentType, * defaultValue?: mixed, * description?: string|null, * deprecationReason?: string|null, * astNode?: InputValueDefinitionNode|null * } */ class InputObjectField { public string $name; /** @var mixed */ public $defaultValue; public ?string $description; public ?string $deprecationReason; /** @var Type&InputType */ private Type $type; public ?InputValueDefinitionNode $astNode; /** @phpstan-var InputObjectFieldConfig */ public array $config; /** @phpstan-param InputObjectFieldConfig $config */ public function __construct(array $config) { $this->name = $config['name']; $this->defaultValue = $config['defaultValue'] ?? null; $this->description = $config['description'] ?? null; $this->deprecationReason = $config['deprecationReason'] ?? null; // Do nothing for type, it is lazy loaded in getType() $this->astNode = $config['astNode'] ?? null; $this->config = $config; } /** @return Type&InputType */ public function getType(): Type { if (! isset($this->type)) { $this->type = Schema::resolveType($this->config['type']); } return $this->type; } public function defaultValueExists(): bool { return array_key_exists('defaultValue', $this->config); } public function isRequired(): bool { return $this->getType() instanceof NonNull && ! $this->defaultValueExists(); } public function isDeprecated(): bool { return (bool) $this->deprecationReason; } /** * @param Type&NamedType $parentType * * @throws InvariantViolation */ public function assertValid(Type $parentType): void { $error = Utils::isValidNameError($this->name); if ($error !== null) { throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}"); } $type = Type::getNamedType($this->getType()); if (! $type instanceof InputType) { $notInputType = Utils::printSafe($this->type); throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Input Type but got: {$notInputType}"); } // @phpstan-ignore-next-line should not happen if used properly if (array_key_exists('resolve', $this->config)) { throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers."); } if ($this->isRequired() && $this->isDeprecated()) { throw new InvariantViolation("Required input field {$parentType->name}.{$this->name} cannot be deprecated."); } } } graphql/lib/Type/Definition/IntType.php 0000644 00000005641 15166657210 0014105 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Utils\Utils; class IntType extends ScalarType { // As per the GraphQL Spec, Integers are only treated as valid when a valid // 32-bit signed integer, providing the broadest support across platforms. // // n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because // they are internally represented as IEEE 754 doubles. public const MAX_INT = 2147483647; public const MIN_INT = -2147483648; public string $name = Type::INT; public ?string $description = 'The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. '; /** @throws SerializationError */ public function serialize($value): int { // Fast path for 90+% of cases: if (is_int($value) && $value <= self::MAX_INT && $value >= self::MIN_INT) { return $value; } $float = is_numeric($value) || is_bool($value) ? (float) $value : null; if ($float === null || floor($float) !== $float) { $notInt = Utils::printSafe($value); throw new SerializationError("Int cannot represent non-integer value: {$notInt}"); } if ($float > self::MAX_INT || $float < self::MIN_INT) { $outOfRangeInt = Utils::printSafe($value); throw new SerializationError("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}"); } return (int) $float; } /** @throws Error */ public function parseValue($value): int { $isInt = is_int($value) || (is_float($value) && floor($value) === $value); if (! $isInt) { $notInt = Utils::printSafeJson($value); throw new Error("Int cannot represent non-integer value: {$notInt}"); } if ($value > self::MAX_INT || $value < self::MIN_INT) { $outOfRangeInt = Utils::printSafeJson($value); throw new Error("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}"); } return (int) $value; } /** * @throws \JsonException * @throws Error */ public function parseLiteral(Node $valueNode, ?array $variables = null): int { if ($valueNode instanceof IntValueNode) { $val = (int) $valueNode->value; if ($valueNode->value === (string) $val && $val >= self::MIN_INT && $val <= self::MAX_INT) { return $val; } } $notInt = Printer::doPrint($valueNode); throw new Error("Int cannot represent non-integer value: {$notInt}", $valueNode); } } graphql/lib/Type/Definition/Directive.php 0000644 00000012716 15166657210 0014430 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\DirectiveLocation; /** * @phpstan-import-type ArgumentListConfig from Argument * * @phpstan-type DirectiveConfig array{ * name: string, * description?: string|null, * args?: ArgumentListConfig|null, * locations: array<string>, * isRepeatable?: bool|null, * astNode?: DirectiveDefinitionNode|null * } */ class Directive { public const DEFAULT_DEPRECATION_REASON = 'No longer supported'; public const INCLUDE_NAME = 'include'; public const IF_ARGUMENT_NAME = 'if'; public const SKIP_NAME = 'skip'; public const DEPRECATED_NAME = 'deprecated'; public const REASON_ARGUMENT_NAME = 'reason'; public const ONE_OF_NAME = 'oneOf'; /** * Lazily initialized. * * @var array<string, Directive>|null */ protected static ?array $internalDirectives = null; public string $name; public ?string $description; /** @var array<int, Argument> */ public array $args; public bool $isRepeatable; /** @var array<string> */ public array $locations; public ?DirectiveDefinitionNode $astNode; /** * @var array<string, mixed> * * @phpstan-var DirectiveConfig */ public array $config; /** * @param array<string, mixed> $config * * @phpstan-param DirectiveConfig $config */ public function __construct(array $config) { $this->name = $config['name']; $this->description = $config['description'] ?? null; $this->args = isset($config['args']) ? Argument::listFromConfig($config['args']) : []; $this->isRepeatable = $config['isRepeatable'] ?? false; $this->locations = $config['locations']; $this->astNode = $config['astNode'] ?? null; $this->config = $config; } /** @return array<string, Directive> */ public static function getInternalDirectives(): array { return [ self::INCLUDE_NAME => self::includeDirective(), self::SKIP_NAME => self::skipDirective(), self::DEPRECATED_NAME => self::deprecatedDirective(), self::ONE_OF_NAME => self::oneOfDirective(), ]; } public static function includeDirective(): Directive { return self::$internalDirectives[self::INCLUDE_NAME] ??= new self([ 'name' => self::INCLUDE_NAME, 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.', 'locations' => [ DirectiveLocation::FIELD, DirectiveLocation::FRAGMENT_SPREAD, DirectiveLocation::INLINE_FRAGMENT, ], 'args' => [ self::IF_ARGUMENT_NAME => [ 'type' => Type::nonNull(Type::boolean()), 'description' => 'Included when true.', ], ], ]); } public static function skipDirective(): Directive { return self::$internalDirectives[self::SKIP_NAME] ??= new self([ 'name' => self::SKIP_NAME, 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.', 'locations' => [ DirectiveLocation::FIELD, DirectiveLocation::FRAGMENT_SPREAD, DirectiveLocation::INLINE_FRAGMENT, ], 'args' => [ self::IF_ARGUMENT_NAME => [ 'type' => Type::nonNull(Type::boolean()), 'description' => 'Skipped when true.', ], ], ]); } public static function deprecatedDirective(): Directive { return self::$internalDirectives[self::DEPRECATED_NAME] ??= new self([ 'name' => self::DEPRECATED_NAME, 'description' => 'Marks an element of a GraphQL schema as no longer supported.', 'locations' => [ DirectiveLocation::FIELD_DEFINITION, DirectiveLocation::ENUM_VALUE, DirectiveLocation::ARGUMENT_DEFINITION, DirectiveLocation::INPUT_FIELD_DEFINITION, ], 'args' => [ self::REASON_ARGUMENT_NAME => [ 'type' => Type::string(), 'description' => 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).', 'defaultValue' => self::DEFAULT_DEPRECATION_REASON, ], ], ]); } public static function oneOfDirective(): Directive { return self::$internalDirectives[self::ONE_OF_NAME] ??= new self([ 'name' => self::ONE_OF_NAME, 'description' => 'Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its fields be provided).', 'locations' => [ DirectiveLocation::INPUT_OBJECT, ], 'args' => [], ]); } public static function isSpecifiedDirective(Directive $directive): bool { return array_key_exists($directive->name, self::getInternalDirectives()); } public static function resetCachedInstances(): void { self::$internalDirectives = null; } } graphql/lib/Type/Definition/HasFieldsType.php 0000644 00000001540 15166657210 0015207 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; interface HasFieldsType { /** @throws InvariantViolation */ public function getField(string $name): FieldDefinition; public function hasField(string $name): bool; public function findField(string $name): ?FieldDefinition; /** * @throws InvariantViolation * * @return array<string, FieldDefinition> */ public function getFields(): array; /** * @throws InvariantViolation * * @return array<string, FieldDefinition> */ public function getVisibleFields(): array; /** * Get all field names, including only visible fields. * * @throws InvariantViolation * * @return array<int, string> */ public function getFieldNames(): array; } graphql/lib/Type/Definition/EnumValueDefinition.php 0000644 00000002175 15166657210 0016422 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; /** * @phpstan-type EnumValueConfig array{ * name: string, * value?: mixed, * deprecationReason?: string|null, * description?: string|null, * astNode?: EnumValueDefinitionNode|null * } */ class EnumValueDefinition { public string $name; /** @var mixed */ public $value; public ?string $deprecationReason; public ?string $description; public ?EnumValueDefinitionNode $astNode; /** @phpstan-var EnumValueConfig */ public array $config; /** @phpstan-param EnumValueConfig $config */ public function __construct(array $config) { $this->name = $config['name']; $this->value = $config['value'] ?? null; $this->deprecationReason = $config['deprecationReason'] ?? null; $this->description = $config['description'] ?? null; $this->astNode = $config['astNode'] ?? null; $this->config = $config; } public function isDeprecated(): bool { return (bool) $this->deprecationReason; } } graphql/lib/Type/Definition/InterfaceType.php 0000644 00000006426 15166657210 0015255 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-import-type ResolveType from AbstractType * @phpstan-import-type FieldsConfig from FieldDefinition * * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType * @phpstan-type InterfaceConfig array{ * name?: string|null, * description?: string|null, * fields: FieldsConfig, * interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>, * resolveType?: ResolveType|null, * astNode?: InterfaceTypeDefinitionNode|null, * extensionASTNodes?: array<InterfaceTypeExtensionNode>|null * } */ class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType { use HasFieldsTypeImplementation; use NamedTypeImplementation; use ImplementingTypeImplementation; public ?InterfaceTypeDefinitionNode $astNode; /** @var array<InterfaceTypeExtensionNode> */ public array $extensionASTNodes; /** @phpstan-var InterfaceConfig */ public array $config; /** * @phpstan-param InterfaceConfig $config * * @throws InvariantViolation */ public function __construct(array $config) { $this->name = $config['name'] ?? $this->inferName(); $this->description = $config['description'] ?? null; $this->astNode = $config['astNode'] ?? null; $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; $this->config = $config; } /** * @param mixed $type * * @throws InvariantViolation */ public static function assertInterfaceType($type): self { if (! ($type instanceof self)) { $notInterfaceType = Utils::printSafe($type); throw new InvariantViolation("Expected {$notInterfaceType} to be a GraphQL Interface type."); } return $type; } public function resolveType($objectValue, $context, ResolveInfo $info) { if (isset($this->config['resolveType'])) { return ($this->config['resolveType'])($objectValue, $context, $info); } return null; } /** * @throws Error * @throws InvariantViolation */ public function assertValid(): void { Utils::assertValidName($this->name); $resolveType = $this->config['resolveType'] ?? null; // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if ($resolveType !== null && ! is_callable($resolveType)) { $notCallable = Utils::printSafe($resolveType); throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}."); } $this->assertValidInterfaces(); } public function astNode(): ?InterfaceTypeDefinitionNode { return $this->astNode; } /** @return array<InterfaceTypeExtensionNode> */ public function extensionASTNodes(): array { return $this->extensionASTNodes; } } graphql/lib/Type/Definition/InputObjectType.php 0000644 00000017472 15166657210 0015606 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * * @phpstan-type EagerFieldConfig InputObjectField|(Type&InputType)|UnnamedInputObjectFieldConfig * @phpstan-type LazyFieldConfig callable(): EagerFieldConfig * @phpstan-type FieldConfig EagerFieldConfig|LazyFieldConfig * @phpstan-type ParseValueFn callable(array<string, mixed>): mixed * @phpstan-type InputObjectConfig array{ * name?: string|null, * description?: string|null, * isOneOf?: bool|null, * fields: iterable<FieldConfig>|callable(): iterable<FieldConfig>, * parseValue?: ParseValueFn|null, * astNode?: InputObjectTypeDefinitionNode|null, * extensionASTNodes?: array<InputObjectTypeExtensionNode>|null * } */ class InputObjectType extends Type implements InputType, NullableType, NamedType { use NamedTypeImplementation; public bool $isOneOf; /** * Lazily initialized. * * @var array<string, InputObjectField> */ private array $fields; /** @var ParseValueFn|null */ private $parseValue; public ?InputObjectTypeDefinitionNode $astNode; /** @var array<InputObjectTypeExtensionNode> */ public array $extensionASTNodes; /** @phpstan-var InputObjectConfig */ public array $config; /** * @phpstan-param InputObjectConfig $config * * @throws InvariantViolation * @throws InvariantViolation */ public function __construct(array $config) { $this->name = $config['name'] ?? $this->inferName(); $this->description = $config['description'] ?? null; $this->isOneOf = $config['isOneOf'] ?? false; // $this->fields is initialized lazily $this->parseValue = $config['parseValue'] ?? null; $this->astNode = $config['astNode'] ?? null; $this->extensionASTNodes = $config['extensionASTNodes'] ?? []; $this->config = $config; } /** @throws InvariantViolation */ public function getField(string $name): InputObjectField { $field = $this->findField($name); if ($field === null) { throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\""); } return $field; } /** @throws InvariantViolation */ public function findField(string $name): ?InputObjectField { if (! isset($this->fields)) { $this->initializeFields(); } return $this->fields[$name] ?? null; } /** @throws InvariantViolation */ public function hasField(string $name): bool { if (! isset($this->fields)) { $this->initializeFields(); } return isset($this->fields[$name]); } /** Returns true if this is a oneOf input object type. */ public function isOneOf(): bool { return $this->isOneOf; } /** * @throws InvariantViolation * * @return array<string, InputObjectField> */ public function getFields(): array { if (! isset($this->fields)) { $this->initializeFields(); } return $this->fields; } /** @throws InvariantViolation */ protected function initializeFields(): void { $fields = $this->config['fields']; if (is_callable($fields)) { $fields = $fields(); } $this->fields = []; foreach ($fields as $nameOrIndex => $field) { $this->initializeField($nameOrIndex, $field); } } /** * @param string|int $nameOrIndex * * @phpstan-param FieldConfig $field * * @throws InvariantViolation */ protected function initializeField($nameOrIndex, $field): void { if (is_callable($field)) { $field = $field(); } assert($field instanceof Type || is_array($field) || $field instanceof InputObjectField); if ($field instanceof Type) { $field = ['type' => $field]; } assert(is_array($field) || $field instanceof InputObjectField); // @phpstan-ignore-line TODO remove when using actual union types if (is_array($field)) { $field['name'] ??= $nameOrIndex; if (! is_string($field['name'])) { throw new InvariantViolation("{$this->name} fields must be an associative array with field names as keys, an array of arrays with a name attribute, or a callable which returns one of those."); } $field = new InputObjectField($field); // @phpstan-ignore-line array type is wrongly inferred } assert($field instanceof InputObjectField); // @phpstan-ignore-line TODO remove when using actual union types $this->fields[$field->name] = $field; } /** * Parses an externally provided value (query variable) to use as an input. * * Should throw an exception with a client-friendly message on invalid values, @see ClientAware. * * @param array<string, mixed> $value * * @return mixed */ public function parseValue(array $value) { if (isset($this->parseValue)) { return ($this->parseValue)($value); } return $value; } /** * Validates type config and throws if one of the type options is invalid. * Note: this method is shallow, it won't validate object fields and their arguments. * * @throws Error * @throws InvariantViolation */ public function assertValid(): void { Utils::assertValidName($this->name); $fields = $this->config['fields'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime) if (is_callable($fields)) { $fields = $fields(); } if (! is_iterable($fields)) { $invalidFields = Utils::printSafe($fields); throw new InvariantViolation("{$this->name} fields must be an iterable or a callable which returns an iterable, got: {$invalidFields}."); } $resolvedFields = $this->getFields(); foreach ($resolvedFields as $field) { $field->assertValid($this); } // Additional validation for oneOf input objects if ($this->isOneOf()) { $this->validateOneOfConstraints($resolvedFields); } } /** * Validates that oneOf input object constraints are met. * * @param array<string, InputObjectField> $fields * * @throws InvariantViolation */ private function validateOneOfConstraints(array $fields): void { if (count($fields) === 0) { throw new InvariantViolation("OneOf input object type {$this->name} must define one or more fields."); } foreach ($fields as $fieldName => $field) { $fieldType = $field->getType(); // OneOf fields must be nullable (not wrapped in NonNull) if ($fieldType instanceof NonNull) { throw new InvariantViolation("OneOf input object type {$this->name} field {$fieldName} must be nullable."); } // OneOf fields cannot have default values if ($field->defaultValueExists()) { throw new InvariantViolation("OneOf input object type {$this->name} field {$fieldName} cannot have a default value."); } } } public function astNode(): ?InputObjectTypeDefinitionNode { return $this->astNode; } /** @return array<InputObjectTypeExtensionNode> */ public function extensionASTNodes(): array { return $this->extensionASTNodes; } } graphql/lib/Type/Definition/AbstractType.php 0000644 00000001425 15166657210 0015112 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Deferred; /** * @phpstan-type ResolveTypeReturn ObjectType|string|callable(): (ObjectType|string|null)|Deferred|null * @phpstan-type ResolveType callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): ResolveTypeReturn */ interface AbstractType { /** * Resolves the concrete ObjectType for the given value. * * @param mixed $objectValue The resolved value for the object type * @param mixed $context The context that was passed to GraphQL::execute() * * @return ObjectType|string|callable|Deferred|null * * @phpstan-return ResolveTypeReturn */ public function resolveType($objectValue, $context, ResolveInfo $info); } graphql/lib/Type/Definition/FloatType.php 0000644 00000003635 15166657210 0014421 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\FloatValueNode; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Utils\Utils; class FloatType extends ScalarType { public string $name = Type::FLOAT; public ?string $description = 'The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). '; /** @throws SerializationError */ public function serialize($value): float { $float = is_numeric($value) || is_bool($value) ? (float) $value : null; if ($float === null || ! is_finite($float)) { $notFloat = Utils::printSafe($value); throw new SerializationError("Float cannot represent non numeric value: {$notFloat}"); } return $float; } /** @throws Error */ public function parseValue($value): float { $float = is_float($value) || is_int($value) ? (float) $value : null; if ($float === null || ! is_finite($float)) { $notFloat = Utils::printSafeJson($value); throw new Error("Float cannot represent non numeric value: {$notFloat}"); } return $float; } /** * @throws \JsonException * @throws Error */ public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) { return (float) $valueNode->value; } $notFloat = Printer::doPrint($valueNode); throw new Error("Float cannot represent non numeric value: {$notFloat}", $valueNode); } } graphql/lib/Type/Definition/CustomScalarType.php 0000644 00000010472 15166657210 0015751 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ValueNode; use YOOtheme\GraphQL\Utils\AST; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-type InputCustomScalarConfig array{ * name?: string|null, * description?: string|null, * serialize?: callable(mixed): mixed, * parseValue: callable(mixed): mixed, * parseLiteral: callable(ValueNode&Node, array<string, mixed>|null): mixed, * astNode?: ScalarTypeDefinitionNode|null, * extensionASTNodes?: array<ScalarTypeExtensionNode>|null * } * @phpstan-type OutputCustomScalarConfig array{ * name?: string|null, * description?: string|null, * serialize: callable(mixed): mixed, * parseValue?: callable(mixed): mixed, * parseLiteral?: callable(ValueNode&Node, array<string, mixed>|null): mixed, * astNode?: ScalarTypeDefinitionNode|null, * extensionASTNodes?: array<ScalarTypeExtensionNode>|null * } * @phpstan-type CustomScalarConfig InputCustomScalarConfig|OutputCustomScalarConfig */ class CustomScalarType extends ScalarType { /** @phpstan-var CustomScalarConfig */ // @phpstan-ignore-next-line specialize type public array $config; /** * @param array<string, mixed> $config * * @phpstan-param CustomScalarConfig $config */ public function __construct(array $config) { parent::__construct($config); } public function serialize($value) { if (isset($this->config['serialize'])) { return $this->config['serialize']($value); } return $value; } public function parseValue($value) { if (isset($this->config['parseValue'])) { return $this->config['parseValue']($value); } return $value; } /** @throws \Exception */ public function parseLiteral(Node $valueNode, ?array $variables = null) { if (isset($this->config['parseLiteral'])) { return $this->config['parseLiteral']($valueNode, $variables); } return AST::valueFromASTUntyped($valueNode, $variables); } /** * @throws Error * @throws InvariantViolation */ public function assertValid(): void { parent::assertValid(); $serialize = $this->config['serialize'] ?? null; $parseValue = $this->config['parseValue'] ?? null; $parseLiteral = $this->config['parseLiteral'] ?? null; $hasSerialize = $serialize !== null; $hasParseValue = $parseValue !== null; $hasParseLiteral = $parseLiteral !== null; $hasParse = $hasParseValue && $hasParseLiteral; if ($hasParseValue !== $hasParseLiteral) { throw new InvariantViolation("{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions to work as an input type."); } if (! $hasSerialize && ! $hasParse) { throw new InvariantViolation("{$this->name} must provide \"parseValue\" and \"parseLiteral\" functions, \"serialize\" function, or both."); } // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if ($hasSerialize && ! is_callable($serialize)) { $notCallable = Utils::printSafe($serialize); throw new InvariantViolation("{$this->name} must provide \"serialize\" as a callable if given, but got: {$notCallable}."); } // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if ($hasParseValue && ! is_callable($parseValue)) { $notCallable = Utils::printSafe($parseValue); throw new InvariantViolation("{$this->name} must provide \"parseValue\" as a callable if given, but got: {$notCallable}."); } // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime if ($hasParseLiteral && ! is_callable($parseLiteral)) { $notCallable = Utils::printSafe($parseLiteral); throw new InvariantViolation("{$this->name} must provide \"parseLiteral\" as a callable if given, but got: {$notCallable}."); } } } graphql/lib/Type/Definition/UnmodifiedType.php 0000644 00000000420 15166657210 0015424 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /* export type GraphQLUnmodifiedType = GraphQLScalarType | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType | GraphQLEnumType | GraphQLInputObjectType; */ interface UnmodifiedType {} graphql/lib/Type/Definition/ImplementingType.php 0000644 00000000565 15166657210 0016003 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /** * export type GraphQLImplementingType = * GraphQLObjectType | * GraphQLInterfaceType;. */ interface ImplementingType { public function implementsInterface(InterfaceType $interfaceType): bool; /** @return array<int, InterfaceType> */ public function getInterfaces(): array; } graphql/lib/Type/Definition/InputType.php 0000644 00000000471 15166657210 0014446 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; /** * export type InputType = * | ScalarType * | EnumType * | InputObjectType * | ListOfType<InputType> * | NonNull< * | ScalarType * | EnumType * | InputObjectType * | ListOfType<InputType>, * >;. */ interface InputType {} graphql/lib/Type/Definition/ListOfType.php 0000644 00000002205 15166657210 0014544 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Type\Schema; /** * @template-covariant OfType of Type */ class ListOfType extends Type implements WrappingType, OutputType, NullableType, InputType { /** * @var Type|callable * * @phpstan-var OfType|callable(): OfType */ private $wrappedType; /** * @param Type|callable $type * * @phpstan-param OfType|callable(): OfType $type */ public function __construct($type) { $this->wrappedType = $type; } public function toString(): string { return '[' . $this->getWrappedType()->toString() . ']'; } /** @phpstan-return OfType */ public function getWrappedType(): Type { return Schema::resolveType($this->wrappedType); } public function getInnermostType(): NamedType { $type = $this->getWrappedType(); while ($type instanceof WrappingType) { $type = $type->getWrappedType(); } assert($type instanceof NamedType, 'known because we unwrapped all the way down'); return $type; } } graphql/lib/Type/Definition/IDType.php 0000644 00000004017 15166657210 0013643 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Utils\Utils; class IDType extends ScalarType { public string $name = 'ID'; public ?string $description = 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.'; /** @throws SerializationError */ public function serialize($value): string { $canCast = is_string($value) || is_int($value) || (is_object($value) && method_exists($value, '__toString')); if (! $canCast) { $notID = Utils::printSafe($value); throw new SerializationError("ID cannot represent a non-string and non-integer value: {$notID}"); } return (string) $value; } /** @throws Error */ public function parseValue($value): string { if (is_string($value) || is_int($value)) { return (string) $value; } $notID = Utils::printSafeJson($value); throw new Error("ID cannot represent a non-string and non-integer value: {$notID}"); } /** * @throws \JsonException * @throws Error */ public function parseLiteral(Node $valueNode, ?array $variables = null): string { if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) { return $valueNode->value; } $notID = Printer::doPrint($valueNode); throw new Error("ID cannot represent a non-string and non-integer value: {$notID}", $valueNode); } } graphql/lib/Type/Definition/ImplementingTypeImplementation.php 0000644 00000003704 15166657210 0020707 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type\Definition; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Type\Schema; /** * @see ImplementingType */ trait ImplementingTypeImplementation { /** * Lazily initialized. * * @var array<int, InterfaceType> */ private array $interfaces; public function implementsInterface(InterfaceType $interfaceType): bool { if (! isset($this->interfaces)) { $this->initializeInterfaces(); } foreach ($this->interfaces as $interface) { if ($interfaceType->name === $interface->name) { return true; } } return false; } /** @return array<int, InterfaceType> */ public function getInterfaces(): array { if (! isset($this->interfaces)) { $this->initializeInterfaces(); } return $this->interfaces; } private function initializeInterfaces(): void { $this->interfaces = []; if (! isset($this->config['interfaces'])) { return; } $interfaces = $this->config['interfaces']; if (is_callable($interfaces)) { $interfaces = $interfaces(); } foreach ($interfaces as $interface) { $this->interfaces[] = Schema::resolveType($interface); } } /** @throws InvariantViolation */ protected function assertValidInterfaces(): void { if (! isset($this->config['interfaces'])) { return; } $interfaces = $this->config['interfaces']; if (is_callable($interfaces)) { $interfaces = $interfaces(); } // @phpstan-ignore-next-line should not happen if used correctly if (! is_iterable($interfaces)) { throw new InvariantViolation("{$this->name} interfaces must be an iterable or a callable which returns an iterable."); } } } graphql/lib/Type/TypeKind.php 0000644 00000000601 15166657210 0012137 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type; class TypeKind { public const SCALAR = 'SCALAR'; public const OBJECT = 'OBJECT'; public const INTERFACE = 'INTERFACE'; public const UNION = 'UNION'; public const ENUM = 'ENUM'; public const INPUT_OBJECT = 'INPUT_OBJECT'; public const LIST = 'LIST'; public const NON_NULL = 'NON_NULL'; } graphql/lib/Type/SchemaValidationContext.php 0000644 00000077202 15166657210 0015203 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ListTypeNode; use YOOtheme\GraphQL\Language\AST\NamedTypeNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\NonNullTypeNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\TypeNode; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Language\DirectiveLocation; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\EnumValueDefinition; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Validation\InputObjectCircularRefs; use YOOtheme\GraphQL\Utils\TypeComparators; use YOOtheme\GraphQL\Utils\Utils; class SchemaValidationContext { /** @var list<Error> */ private array $errors = []; private Schema $schema; private InputObjectCircularRefs $inputObjectCircularRefs; public function __construct(Schema $schema) { $this->schema = $schema; $this->inputObjectCircularRefs = new InputObjectCircularRefs($this); } /** @return list<Error> */ public function getErrors(): array { return $this->errors; } public function validateRootTypes(): void { if ($this->schema->getQueryType() === null) { $this->reportError('Query root type must be provided.', $this->schema->astNode); } // Triggers a type error if wrong $this->schema->getMutationType(); $this->schema->getSubscriptionType(); } /** @param array<Node|null>|Node|null $nodes */ public function reportError(string $message, $nodes = null): void { $nodes = array_filter(is_array($nodes) ? $nodes : [$nodes]); $this->addError(new Error($message, $nodes)); } private function addError(Error $error): void { $this->errors[] = $error; } /** @throws InvariantViolation */ public function validateDirectives(): void { $this->validateDirectiveDefinitions(); // Validate directives that are used on the schema $this->validateDirectivesAtLocation( $this->getDirectives($this->schema), DirectiveLocation::SCHEMA ); } /** @throws InvariantViolation */ public function validateDirectiveDefinitions(): void { $directiveDefinitions = []; $directives = $this->schema->getDirectives(); foreach ($directives as $directive) { // Ensure all directives are in fact GraphQL directives. // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless if (! $directive instanceof Directive) { $notDirective = Utils::printSafe($directive); // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless $nodes = is_object($directive) && property_exists($directive, 'astNode') ? $directive->astNode : null; $this->reportError( "Expected directive but got: {$notDirective}.", $nodes ); continue; } $existingDefinitions = $directiveDefinitions[$directive->name] ?? []; $existingDefinitions[] = $directive; $directiveDefinitions[$directive->name] = $existingDefinitions; // Ensure they are named correctly. $this->validateName($directive); // TODO: Ensure proper locations. $argNames = []; foreach ($directive->args as $arg) { // Ensure they are named correctly. $this->validateName($arg); $argName = $arg->name; if (isset($argNames[$argName])) { $this->reportError( "Argument @{$directive->name}({$argName}:) can only be defined once.", $this->getAllDirectiveArgNodes($directive, $argName) ); continue; } $argNames[$argName] = true; // Ensure the type is an input type. // @phpstan-ignore-next-line necessary until PHP supports union types if (! Type::isInputType($arg->getType())) { $type = Utils::printSafe($arg->getType()); $this->reportError( "The type of @{$directive->name}({$argName}:) must be Input Type but got: {$type}.", $this->getDirectiveArgTypeNode($directive, $argName) ); } } } foreach ($directiveDefinitions as $directiveName => $directiveList) { if (count($directiveList) > 1) { $nodes = []; foreach ($directiveList as $dir) { if (isset($dir->astNode)) { $nodes[] = $dir->astNode; } } $this->reportError( "Directive @{$directiveName} defined multiple times.", $nodes ); } } } /** @param (Type&NamedType)|Directive|FieldDefinition|EnumValueDefinition|InputObjectField|Argument $object */ private function validateName(object $object): void { // Ensure names are valid, however introspection types opt out. $error = Utils::isValidNameError($object->name, $object->astNode); if ( $error === null || ($object instanceof Type && Introspection::isIntrospectionType($object)) ) { return; } $this->addError($error); } /** @return array<int, InputValueDefinitionNode> */ private function getAllDirectiveArgNodes(Directive $directive, string $argName): array { $astNode = $directive->astNode; if ($astNode === null) { return []; } $matchingSubnodes = []; foreach ($astNode->arguments as $subNode) { if ($subNode->name->value === $argName) { $matchingSubnodes[] = $subNode; } } return $matchingSubnodes; } /** @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */ private function getDirectiveArgTypeNode(Directive $directive, string $argName): ?TypeNode { $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0] ?? null; return $argNode === null ? null : $argNode->type; } /** @throws InvariantViolation */ public function validateTypes(): void { $typeMap = $this->schema->getTypeMap(); foreach ($typeMap as $type) { // Ensure all provided types are in fact GraphQL type. // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless if (! $type instanceof NamedType) { $notNamedType = Utils::printSafe($type); // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless $node = $type instanceof Type ? $type->astNode : null; $this->reportError("Expected GraphQL named type but got: {$notNamedType}.", $node); continue; } $this->validateName($type); if ($type instanceof ObjectType) { $this->validateFields($type); $this->validateInterfaces($type); $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::OBJECT); } elseif ($type instanceof InterfaceType) { $this->validateFields($type); $this->validateInterfaces($type); $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::IFACE); } elseif ($type instanceof UnionType) { $this->validateUnionMembers($type); $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::UNION); } elseif ($type instanceof EnumType) { $this->validateEnumValues($type); $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::ENUM); } elseif ($type instanceof InputObjectType) { $this->validateInputFields($type); $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::INPUT_OBJECT); $this->inputObjectCircularRefs->validate($type); } else { assert($type instanceof ScalarType, 'only remaining option'); $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::SCALAR); } } } /** * @param NodeList<DirectiveNode> $directives * * @throws InvariantViolation */ private function validateDirectivesAtLocation(NodeList $directives, string $location): void { /** @var array<string, array<int, DirectiveNode>> $potentiallyDuplicateDirectives */ $potentiallyDuplicateDirectives = []; $schema = $this->schema; foreach ($directives as $directiveNode) { $directiveName = $directiveNode->name->value; // Ensure directive used is also defined $schemaDirective = $schema->getDirective($directiveName); if ($schemaDirective === null) { $this->reportError("No directive @{$directiveName} defined.", $directiveNode); continue; } if (! in_array($location, $schemaDirective->locations, true)) { $this->reportError( "Directive @{$directiveName} not allowed at {$location} location.", array_filter([$directiveNode, $schemaDirective->astNode]) ); } if (! $schemaDirective->isRepeatable) { $potentiallyDuplicateDirectives[$directiveName][] = $directiveNode; } } foreach ($potentiallyDuplicateDirectives as $directiveName => $directiveList) { if (count($directiveList) > 1) { $this->reportError("Non-repeatable directive @{$directiveName} used more than once at the same location.", $directiveList); } } } /** * @param ObjectType|InterfaceType $type * * @throws InvariantViolation */ private function validateFields(Type $type): void { $fieldMap = $type->getFields(); if ($fieldMap === []) { $this->reportError( "Type {$type->name} must define one or more fields.", $this->getAllNodes($type) ); } foreach ($fieldMap as $fieldName => $field) { $this->validateName($field); $fieldNodes = $this->getAllFieldNodes($type, $fieldName); if (count($fieldNodes) > 1) { $this->reportError("Field {$type->name}.{$fieldName} can only be defined once.", $fieldNodes); continue; } $fieldType = $field->getType(); // @phpstan-ignore-next-line not statically provable until we can use union types if (! Type::isOutputType($fieldType)) { $safeFieldType = Utils::printSafe($fieldType); $this->reportError( "The type of {$type->name}.{$fieldName} must be Output Type but got: {$safeFieldType}.", $this->getFieldTypeNode($type, $fieldName) ); } $this->validateTypeIsSingleton($fieldType, "{$type->name}.{$fieldName}"); $argNames = []; foreach ($field->args as $arg) { $argName = $arg->name; $argPath = "{$type->name}.{$fieldName}({$argName}:)"; $this->validateName($arg); if (isset($argNames[$argName])) { $this->reportError( "Field argument {$argPath} can only be defined once.", $this->getAllFieldArgNodes($type, $fieldName, $argName) ); } $argNames[$argName] = true; $argType = $arg->getType(); // @phpstan-ignore-next-line the type of $arg->getType() says it is an input type, but it might not always be true if (! Type::isInputType($argType)) { $safeType = Utils::printSafe($argType); $this->reportError( "The type of {$argPath} must be Input Type but got: {$safeType}.", $this->getFieldArgTypeNode($type, $fieldName, $argName) ); } $this->validateTypeIsSingleton($argType, $argPath); if (isset($arg->astNode->directives)) { $this->validateDirectivesAtLocation($arg->astNode->directives, DirectiveLocation::ARGUMENT_DEFINITION); } } if (isset($field->astNode->directives)) { $this->validateDirectivesAtLocation($field->astNode->directives, DirectiveLocation::FIELD_DEFINITION); } } } /** * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $obj * * @return list<SchemaDefinitionNode|SchemaExtensionNode>|list<ObjectTypeDefinitionNode|ObjectTypeExtensionNode>|list<InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode>|list<UnionTypeDefinitionNode|UnionTypeExtensionNode>|list< EnumTypeDefinitionNode|EnumTypeExtensionNode>|list<InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode>|list<DirectiveDefinitionNode> */ private function getAllNodes(object $obj): array { $astNode = $obj->astNode; if ($obj instanceof Schema) { $extensionNodes = $obj->extensionASTNodes; } elseif ($obj instanceof Directive) { $extensionNodes = []; } else { $extensionNodes = $obj->extensionASTNodes; } $allNodes = $astNode === null ? [] : [$astNode]; foreach ($extensionNodes as $extensionNode) { $allNodes[] = $extensionNode; } return $allNodes; } /** * @param ObjectType|InterfaceType $type * * @return list<FieldDefinitionNode> */ private function getAllFieldNodes(Type $type, string $fieldName): array { $allNodes = array_filter([$type->astNode, ...$type->extensionASTNodes]); $matchingFieldNodes = []; foreach ($allNodes as $node) { foreach ($node->fields as $field) { if ($field->name->value === $fieldName) { $matchingFieldNodes[] = $field; } } } return $matchingFieldNodes; } /** * @param ObjectType|InterfaceType $type * * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */ private function getFieldTypeNode(Type $type, string $fieldName): ?TypeNode { $fieldNode = $this->getFieldNode($type, $fieldName); return $fieldNode === null ? null : $fieldNode->type; } /** @param ObjectType|InterfaceType $type */ private function getFieldNode(Type $type, string $fieldName): ?FieldDefinitionNode { $nodes = $this->getAllFieldNodes($type, $fieldName); return $nodes[0] ?? null; } /** * @param ObjectType|InterfaceType $type * * @return array<int, InputValueDefinitionNode> */ private function getAllFieldArgNodes(Type $type, string $fieldName, string $argName): array { $argNodes = []; $fieldNode = $this->getFieldNode($type, $fieldName); if ($fieldNode !== null) { foreach ($fieldNode->arguments as $node) { if ($node->name->value === $argName) { $argNodes[] = $node; } } } return $argNodes; } /** * @param ObjectType|InterfaceType $type * * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */ private function getFieldArgTypeNode(Type $type, string $fieldName, string $argName): ?TypeNode { $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName); return $fieldArgNode === null ? null : $fieldArgNode->type; } /** @param ObjectType|InterfaceType $type */ private function getFieldArgNode(Type $type, string $fieldName, string $argName): ?InputValueDefinitionNode { $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName); return $nodes[0] ?? null; } /** * @param ObjectType|InterfaceType $type * * @throws InvariantViolation */ private function validateInterfaces(ImplementingType $type): void { $ifaceTypeNames = []; foreach ($type->getInterfaces() as $interface) { // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless if (! $interface instanceof InterfaceType) { $notInterface = Utils::printSafe($interface); $this->reportError( "Type {$type->name} must only implement Interface types, it cannot implement {$notInterface}.", $this->getImplementsInterfaceNode($type, $interface) ); continue; } if ($type === $interface) { $this->reportError( "Type {$type->name} cannot implement itself because it would create a circular reference.", $this->getImplementsInterfaceNode($type, $interface) ); continue; } if (isset($ifaceTypeNames[$interface->name])) { $this->reportError( "Type {$type->name} can only implement {$interface->name} once.", $this->getAllImplementsInterfaceNodes($type, $interface) ); continue; } $ifaceTypeNames[$interface->name] = true; $this->validateTypeImplementsAncestors($type, $interface); $this->validateTypeImplementsInterface($type, $interface); } } /** * @param Schema|(Type&NamedType) $object * * @return NodeList<DirectiveNode> */ private function getDirectives(object $object): NodeList { $directives = []; /** * Excluding directiveNode, since $object is not Directive. * * @var SchemaDefinitionNode|SchemaExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|EnumTypeDefinitionNode|EnumTypeExtensionNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode $node */ // @phpstan-ignore-next-line union types are not pervasive foreach ($this->getAllNodes($object) as $node) { foreach ($node->directives as $directive) { $directives[] = $directive; } } return new NodeList($directives); } /** * @param ObjectType|InterfaceType $type * @param Type&NamedType $shouldBeInterface */ private function getImplementsInterfaceNode(ImplementingType $type, NamedType $shouldBeInterface): ?NamedTypeNode { $nodes = $this->getAllImplementsInterfaceNodes($type, $shouldBeInterface); return $nodes[0] ?? null; } /** * @param ObjectType|InterfaceType $type * @param Type&NamedType $shouldBeInterface * * @return list<NamedTypeNode> */ private function getAllImplementsInterfaceNodes(ImplementingType $type, NamedType $shouldBeInterface): array { $allNodes = array_filter([$type->astNode, ...$type->extensionASTNodes]); $shouldBeInterfaceName = $shouldBeInterface->name; $matchingInterfaceNodes = []; foreach ($allNodes as $node) { foreach ($node->interfaces as $interface) { if ($interface->name->value === $shouldBeInterfaceName) { $matchingInterfaceNodes[] = $interface; } } } return $matchingInterfaceNodes; } /** * @param ObjectType|InterfaceType $type * * @throws InvariantViolation */ private function validateTypeImplementsInterface(ImplementingType $type, InterfaceType $iface): void { $typeFieldMap = $type->getFields(); $ifaceFieldMap = $iface->getFields(); foreach ($ifaceFieldMap as $fieldName => $ifaceField) { $typeField = $typeFieldMap[$fieldName] ?? null; if ($typeField === null) { $this->reportError( "Interface field {$iface->name}.{$fieldName} expected but {$type->name} does not provide it.", array_merge( [$this->getFieldNode($iface, $fieldName)], $this->getAllNodes($type) ) ); continue; } $typeFieldType = $typeField->getType(); $ifaceFieldType = $ifaceField->getType(); if (! TypeComparators::isTypeSubTypeOf($this->schema, $typeFieldType, $ifaceFieldType)) { $this->reportError( "Interface field {$iface->name}.{$fieldName} expects type {$ifaceFieldType} but {$type->name}.{$fieldName} is type {$typeFieldType}.", [ $this->getFieldTypeNode($iface, $fieldName), $this->getFieldTypeNode($type, $fieldName), ] ); } foreach ($ifaceField->args as $ifaceArg) { $argName = $ifaceArg->name; $typeArg = $typeField->getArg($argName); if ($typeArg === null) { $this->reportError( "Interface field argument {$iface->name}.{$fieldName}({$argName}:) expected but {$type->name}.{$fieldName} does not provide it.", [ $this->getFieldArgNode($iface, $fieldName, $argName), $this->getFieldNode($type, $fieldName), ] ); continue; } $ifaceArgType = $ifaceArg->getType(); $typeArgType = $typeArg->getType(); if (! TypeComparators::isEqualType($ifaceArgType, $typeArgType)) { $this->reportError( "Interface field argument {$iface->name}.{$fieldName}({$argName}:) expects type {$ifaceArgType} but {$type->name}.{$fieldName}({$argName}:) is type {$typeArgType}.", [ $this->getFieldArgTypeNode($iface, $fieldName, $argName), $this->getFieldArgTypeNode($type, $fieldName, $argName), ] ); } // TODO: validate default values? } foreach ($typeField->args as $typeArg) { $argName = $typeArg->name; $ifaceArg = $ifaceField->getArg($argName); if ($typeArg->isRequired() && $ifaceArg === null) { $this->reportError( "Object field {$type->name}.{$fieldName} includes required argument {$argName} that is missing from the Interface field {$iface->name}.{$fieldName}.", [ $this->getFieldArgNode($type, $fieldName, $argName), $this->getFieldNode($iface, $fieldName), ] ); } } } } /** @param ObjectType|InterfaceType $type */ private function validateTypeImplementsAncestors(ImplementingType $type, InterfaceType $iface): void { $typeInterfaces = $type->getInterfaces(); foreach ($iface->getInterfaces() as $transitive) { if (! in_array($transitive, $typeInterfaces, true)) { $this->reportError( $transitive === $type ? "Type {$type->name} cannot implement {$iface->name} because it would create a circular reference." : "Type {$type->name} must implement {$transitive->name} because it is implemented by {$iface->name}.", array_merge( $this->getAllImplementsInterfaceNodes($iface, $transitive), $this->getAllImplementsInterfaceNodes($type, $iface) ) ); } } } /** @throws InvariantViolation */ private function validateUnionMembers(UnionType $union): void { $memberTypes = $union->getTypes(); if ($memberTypes === []) { $this->reportError( "Union type {$union->name} must define one or more member types.", $this->getAllNodes($union) ); } $includedTypeNames = []; foreach ($memberTypes as $memberType) { // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless if (! $memberType instanceof ObjectType) { $notObjectType = Utils::printSafe($memberType); $this->reportError( "Union type {$union->name} can only include Object types, it cannot include {$notObjectType}.", $this->getUnionMemberTypeNodes($union, $notObjectType) ); continue; } if (isset($includedTypeNames[$memberType->name])) { $this->reportError( "Union type {$union->name} can only include type {$memberType->name} once.", $this->getUnionMemberTypeNodes($union, $memberType->name) ); continue; } $includedTypeNames[$memberType->name] = true; } } /** @return list<NamedTypeNode> */ private function getUnionMemberTypeNodes(UnionType $union, string $typeName): array { $allNodes = array_filter([$union->astNode, ...$union->extensionASTNodes]); $types = []; foreach ($allNodes as $node) { foreach ($node->types as $type) { if ($type->name->value === $typeName) { $types[] = $type; } } } return $types; } /** @throws InvariantViolation */ private function validateEnumValues(EnumType $enumType): void { $enumValues = $enumType->getValues(); if ($enumValues === []) { $this->reportError( "Enum type {$enumType->name} must define one or more values.", $this->getAllNodes($enumType) ); } foreach ($enumValues as $enumValue) { $valueName = $enumValue->name; // Ensure valid name. $this->validateName($enumValue); if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') { $this->reportError( "Enum type {$enumType->name} cannot include value: {$valueName}.", $enumValue->astNode ); } // Ensure valid directives if (isset($enumValue->astNode, $enumValue->astNode->directives)) { $this->validateDirectivesAtLocation( $enumValue->astNode->directives, DirectiveLocation::ENUM_VALUE ); } } } /** @throws InvariantViolation */ private function validateInputFields(InputObjectType $inputObj): void { $fieldMap = $inputObj->getFields(); if ($fieldMap === []) { $this->reportError( "Input Object type {$inputObj->name} must define one or more fields.", $this->getAllNodes($inputObj) ); } // Ensure the arguments are valid foreach ($fieldMap as $fieldName => $field) { // Ensure they are named correctly. $this->validateName($field); // TODO: Ensure they are unique per field. // Ensure the type is an input type. $type = $field->getType(); // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless if (! Type::isInputType($type)) { $notInputType = Utils::printSafe($type); $this->reportError( "The type of {$inputObj->name}.{$fieldName} must be Input Type but got: {$notInputType}.", $field->astNode->type ?? null ); } // Ensure valid directives if (isset($field->astNode, $field->astNode->directives)) { $this->validateDirectivesAtLocation( $field->astNode->directives, DirectiveLocation::INPUT_FIELD_DEFINITION ); } } } /** @throws InvariantViolation */ private function validateTypeIsSingleton(Type $type, string $path): void { $schemaConfig = $this->schema->getConfig(); if (! isset($schemaConfig->typeLoader)) { return; } $namedType = Type::getNamedType($type); if ($namedType->isBuiltInType()) { return; } $name = $namedType->name; if ($namedType !== ($schemaConfig->typeLoader)($name)) { throw new InvariantViolation(static::duplicateType($this->schema, $path, $name)); } } public static function duplicateType(Schema $schema, string $path, string $name): string { $hint = isset($schema->getConfig()->typeLoader) ? 'Ensure the type loader returns the same instance. ' : ''; return "Found duplicate type in schema at {$path}: {$name}. {$hint}See https://webonyx.github.io/graphql-php/type-definitions/#type-registry."; } } graphql/lib/Type/Schema.php 0000644 00000041274 15166657210 0011623 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Type; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\GraphQL; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Type\Definition\AbstractType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Utils\InterfaceImplementations; use YOOtheme\GraphQL\Utils\TypeInfo; use YOOtheme\GraphQL\Utils\Utils; /** * Schema Definition (see [schema definition docs](schema-definition.md)). * * A Schema is created by supplying the root types of each type of operation: * query, mutation (optional) and subscription (optional). A schema definition is * then supplied to the validator and executor. Usage Example: * * $schema = new GraphQL\Type\Schema([ * 'query' => $MyAppQueryRootType, * 'mutation' => $MyAppMutationRootType, * ]); * * Or using Schema Config instance: * * $config = GraphQL\Type\SchemaConfig::create() * ->setQuery($MyAppQueryRootType) * ->setMutation($MyAppMutationRootType); * * $schema = new GraphQL\Type\Schema($config); * * @phpstan-import-type SchemaConfigOptions from SchemaConfig * @phpstan-import-type OperationType from OperationDefinitionNode * * @see \GraphQL\Tests\Type\SchemaTest */ class Schema { private SchemaConfig $config; /** * Contains currently resolved schema types. * * @var array<string, Type&NamedType> */ private array $resolvedTypes = []; /** * Lazily initialised. * * @var array<string, InterfaceImplementations> */ private array $implementationsMap; /** True when $resolvedTypes contains all possible schema types. */ private bool $fullyLoaded = false; /** @var array<int, Error> */ private array $validationErrors; public ?SchemaDefinitionNode $astNode; /** @var array<SchemaExtensionNode> */ public array $extensionASTNodes = []; /** * @param SchemaConfig|array<string, mixed> $config * * @phpstan-param SchemaConfig|SchemaConfigOptions $config * * @throws InvariantViolation * * @api */ public function __construct($config) { if (is_array($config)) { $config = SchemaConfig::create($config); } // If this schema was built from a source known to be valid, then it may be // marked with assumeValid to avoid an additional type system validation. if ($config->getAssumeValid()) { $this->validationErrors = []; } $this->astNode = $config->astNode; $this->extensionASTNodes = $config->extensionASTNodes; $this->config = $config; } /** * Returns all types in this schema. * * This operation requires a full schema scan. Do not use in production environment. * * @throws InvariantViolation * * @return array<string, Type&NamedType> Keys represent type names, values are instances of corresponding type definitions * * @api */ public function getTypeMap(): array { if (! $this->fullyLoaded) { $types = $this->config->types; if (is_callable($types)) { $types = $types(); } // Reset order of user provided types, since calls to getType() may have loaded them $this->resolvedTypes = []; foreach ($types as $typeOrLazyType) { /** @var Type|callable(): Type $typeOrLazyType */ $type = self::resolveType($typeOrLazyType); assert($type instanceof NamedType); /** @var string $typeName Necessary assertion for PHPStan + PHP 8.2 */ $typeName = $type->name; assert( ! isset($this->resolvedTypes[$typeName]) || $type === $this->resolvedTypes[$typeName], "Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).", ); $this->resolvedTypes[$typeName] = $type; } // To preserve order of user-provided types, we add first to add them to // the set of "collected" types, so `collectReferencedTypes` ignore them. /** @var array<string, Type&NamedType> $allReferencedTypes */ $allReferencedTypes = []; foreach ($this->resolvedTypes as $type) { // When we ready to process this type, we remove it from "collected" types // and then add it together with all dependent types in the correct position. unset($allReferencedTypes[$type->name]); TypeInfo::extractTypes($type, $allReferencedTypes); } foreach ([$this->getQueryType(), $this->getMutationType(), $this->getSubscriptionType()] as $rootType) { if ($rootType instanceof ObjectType) { TypeInfo::extractTypes($rootType, $allReferencedTypes); } } foreach ($this->getDirectives() as $directive) { // @phpstan-ignore-next-line generics are not strictly enforceable, error will be caught during schema validation if ($directive instanceof Directive) { TypeInfo::extractTypesFromDirectives($directive, $allReferencedTypes); } } TypeInfo::extractTypes(Introspection::_schema(), $allReferencedTypes); $this->resolvedTypes = $allReferencedTypes; $this->fullyLoaded = true; } return $this->resolvedTypes; } /** * Returns a list of directives supported by this schema. * * @throws InvariantViolation * * @return array<Directive> * * @api */ public function getDirectives(): array { return $this->config->directives ?? GraphQL::getStandardDirectives(); } /** @param mixed $typeLoaderReturn could be anything */ public static function typeLoaderNotType($typeLoaderReturn): string { $typeClass = Type::class; $notType = Utils::printSafe($typeLoaderReturn); return "Type loader is expected to return an instanceof {$typeClass}, but it returned {$notType}"; } public static function typeLoaderWrongTypeName(string $expectedTypeName, string $actualTypeName): string { return "Type loader is expected to return type {$expectedTypeName}, but it returned type {$actualTypeName}."; } /** Returns root type by operation name. */ public function getOperationType(string $operation): ?ObjectType { switch ($operation) { case 'query': return $this->getQueryType(); case 'mutation': return $this->getMutationType(); case 'subscription': return $this->getSubscriptionType(); default: return null; } } /** * Returns root query type. * * @api */ public function getQueryType(): ?ObjectType { $query = $this->config->query; if ($query === null) { return null; } if (is_callable($query)) { return $this->config->query = $query(); } return $query; } /** * Returns root mutation type. * * @api */ public function getMutationType(): ?ObjectType { $mutation = $this->config->mutation; if ($mutation === null) { return null; } if (is_callable($mutation)) { return $this->config->mutation = $mutation(); } return $mutation; } /** * Returns schema subscription. * * @api */ public function getSubscriptionType(): ?ObjectType { $subscription = $this->config->subscription; if ($subscription === null) { return null; } if (is_callable($subscription)) { return $this->config->subscription = $subscription(); } return $subscription; } /** @api */ public function getConfig(): SchemaConfig { return $this->config; } /** * Returns a type by name. * * @throws InvariantViolation * * @return (Type&NamedType)|null * * @api */ public function getType(string $name): ?Type { if (isset($this->resolvedTypes[$name])) { return $this->resolvedTypes[$name]; } $introspectionTypes = Introspection::getTypes(); if (isset($introspectionTypes[$name])) { return $introspectionTypes[$name]; } $standardTypes = Type::getStandardTypes(); if (isset($standardTypes[$name])) { return $standardTypes[$name]; } $type = $this->loadType($name); if ($type === null) { return null; } return $this->resolvedTypes[$name] = self::resolveType($type); } /** @throws InvariantViolation */ public function hasType(string $name): bool { return $this->getType($name) !== null; } /** * @throws InvariantViolation * * @return (Type&NamedType)|null */ private function loadType(string $typeName): ?Type { if (! isset($this->config->typeLoader)) { return $this->getTypeMap()[$typeName] ?? null; } $type = ($this->config->typeLoader)($typeName); if ($type === null) { return null; } // @phpstan-ignore-next-line not strictly enforceable unless PHP gets function types if (! $type instanceof Type) { throw new InvariantViolation(self::typeLoaderNotType($type)); } if ($typeName !== $type->name) { throw new InvariantViolation(self::typeLoaderWrongTypeName($typeName, $type->name)); } return $type; } /** * @template T of Type * * @param Type|callable $type * * @phpstan-param T|callable():T $type * * @phpstan-return T */ public static function resolveType($type): Type { if ($type instanceof Type) { return $type; } return $type(); } /** * Returns all possible concrete types for given abstract type * (implementations for interfaces and members of union type for unions). * * This operation requires full schema scan. Do not use in production environment. * * @param AbstractType&Type $abstractType * * @throws InvariantViolation * * @return array<ObjectType> * * @api */ public function getPossibleTypes(AbstractType $abstractType): array { if ($abstractType instanceof UnionType) { return $abstractType->getTypes(); } assert($abstractType instanceof InterfaceType, 'only other option'); return $this->getImplementations($abstractType)->objects(); } /** * Returns all types that implement a given interface type. * * This operation requires full schema scan. Do not use in production environment. * * @api * * @throws InvariantViolation */ public function getImplementations(InterfaceType $abstractType): InterfaceImplementations { return $this->collectImplementations()[$abstractType->name]; } /** * @throws InvariantViolation * * @return array<string, InterfaceImplementations> */ private function collectImplementations(): array { if (! isset($this->implementationsMap)) { $this->implementationsMap = []; /** * @var array< * string, * array{ * objects: array<int, ObjectType>, * interfaces: array<int, InterfaceType>, * } * > $foundImplementations */ $foundImplementations = []; foreach ($this->getTypeMap() as $type) { if ($type instanceof InterfaceType) { if (! isset($foundImplementations[$type->name])) { $foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []]; } foreach ($type->getInterfaces() as $iface) { if (! isset($foundImplementations[$iface->name])) { $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []]; } $foundImplementations[$iface->name]['interfaces'][] = $type; } } elseif ($type instanceof ObjectType) { foreach ($type->getInterfaces() as $iface) { if (! isset($foundImplementations[$iface->name])) { $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []]; } $foundImplementations[$iface->name]['objects'][] = $type; } } } foreach ($foundImplementations as $name => $implementations) { $this->implementationsMap[$name] = new InterfaceImplementations($implementations['objects'], $implementations['interfaces']); } } return $this->implementationsMap; } /** * Returns true if the given type is a sub type of the given abstract type. * * @param AbstractType&Type $abstractType * @param ImplementingType&Type $maybeSubType * * @api * * @throws InvariantViolation */ public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType): bool { if ($abstractType instanceof InterfaceType) { return $maybeSubType->implementsInterface($abstractType); } assert($abstractType instanceof UnionType, 'only other option'); return $abstractType->isPossibleType($maybeSubType); } /** * Returns instance of directive by name. * * @api * * @throws InvariantViolation */ public function getDirective(string $name): ?Directive { foreach ($this->getDirectives() as $directive) { if ($directive->name === $name) { return $directive; } } return null; } /** * Throws if the schema is not valid. * * This operation requires a full schema scan. Do not use in production environment. * * @throws Error * @throws InvariantViolation * * @api */ public function assertValid(): void { $errors = $this->validate(); if ($errors !== []) { throw new InvariantViolation(implode("\n\n", $this->validationErrors)); } $internalTypes = Type::getStandardTypes() + Introspection::getTypes(); foreach ($this->getTypeMap() as $name => $type) { if (isset($internalTypes[$name])) { continue; } $type->assertValid(); // Make sure type loader returns the same instance as registered in other places of schema if (isset($this->config->typeLoader) && $this->loadType($name) !== $type) { throw new InvariantViolation("Type loader returns different instance for {$name} than field/argument definitions. Make sure you always return the same instance for the same type name."); } } } /** * Validate the schema and return any errors. * * This operation requires a full schema scan. Do not use in production environment. * * @throws InvariantViolation * * @return array<int, Error> * * @api */ public function validate(): array { // If this Schema has already been validated, return the previous results. if (isset($this->validationErrors)) { return $this->validationErrors; } // Validate the schema, producing a list of errors. $context = new SchemaValidationContext($this); $context->validateRootTypes(); $context->validateDirectives(); $context->validateTypes(); // Persist the results of validation before returning to ensure validation // does not run multiple times for this schema. $this->validationErrors = $context->getErrors(); return $this->validationErrors; } } graphql/lib/Error/ClientAware.php 0000644 00000001101 15166657210 0012752 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Implementing ClientAware allows graphql-php to decide if this error is safe to be shown to clients. * * Only errors that both implement this interface and return true from `isClientSafe()` * will retain their original error message during formatting. * * All other errors will have their message replaced with "Internal server error". */ interface ClientAware { /** * Is it safe to show the error message to clients? * * @api */ public function isClientSafe(): bool; } graphql/lib/Error/CoercionError.php 0000644 00000002436 15166657210 0013343 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; use YOOtheme\GraphQL\Utils\Utils; /** * @phpstan-type InputPath list<string|int> */ class CoercionError extends Error { /** @var InputPath|null */ public ?array $inputPath; /** @var mixed whatever invalid value was passed */ public $invalidValue; /** * @param InputPath|null $inputPath * @param mixed $invalidValue whatever invalid value was passed * * @return static */ public static function make( string $message, ?array $inputPath, $invalidValue, ?\Throwable $previous = null ): self { $instance = new static($message, null, null, [], null, $previous); $instance->inputPath = $inputPath; $instance->invalidValue = $invalidValue; return $instance; } public function printInputPath(): ?string { if ($this->inputPath === null) { return null; } $path = ''; foreach ($this->inputPath as $segment) { $path .= is_int($segment) ? "[{$segment}]" : ".{$segment}"; } return $path; } public function printInvalidValue(): string { return Utils::printSafeJson($this->invalidValue); } } graphql/lib/Error/UserError.php 0000644 00000000424 15166657210 0012513 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Caused by GraphQL clients and can safely be displayed. */ class UserError extends \RuntimeException implements ClientAware { public function isClientSafe(): bool { return true; } } graphql/lib/Error/ProvidesExtensions.php 0000644 00000000573 15166657210 0014443 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Implementing HasExtensions allows this error to provide additional data to clients. */ interface ProvidesExtensions { /** * Data to include within the "extensions" key of the formatted error. * * @return array<string, mixed>|null */ public function getExtensions(): ?array; } graphql/lib/Error/SyntaxError.php 0000644 00000000601 15166657210 0013060 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; use YOOtheme\GraphQL\Language\Source; class SyntaxError extends Error { public function __construct(Source $source, int $position, string $description) { parent::__construct( "Syntax Error: {$description}", null, $source, [$position] ); } } graphql/lib/Error/DebugFlag.php 0000644 00000000603 15166657210 0012402 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Collection of flags for [error debugging](error-handling.md#debugging-tools). */ final class DebugFlag { public const NONE = 0; public const INCLUDE_DEBUG_MESSAGE = 1; public const INCLUDE_TRACE = 2; public const RETHROW_INTERNAL_EXCEPTIONS = 4; public const RETHROW_UNSAFE_EXCEPTIONS = 8; } graphql/lib/Error/Warning.php 0000644 00000010037 15166657210 0012171 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Encapsulates warnings produced by the library. * * Warnings can be suppressed (individually or all) if required. * Also, it is possible to override warning handler (which is **trigger_error()** by default). * * @phpstan-type WarningHandler callable(string $errorMessage, int $warningId, ?int $messageLevel): void */ final class Warning { public const NONE = 0; public const WARNING_ASSIGN = 2; public const WARNING_CONFIG = 4; public const WARNING_FULL_SCHEMA_SCAN = 8; public const WARNING_CONFIG_DEPRECATION = 16; public const WARNING_NOT_A_TYPE = 32; public const ALL = 63; private static int $enableWarnings = self::ALL; /** @var array<int, true> */ private static array $warned = []; /** * @var callable|null * * @phpstan-var WarningHandler|null */ private static $warningHandler; /** * Sets warning handler which can intercept all system warnings. * When not set, trigger_error() is used to notify about warnings. * * @phpstan-param WarningHandler|null $warningHandler * * @api */ public static function setWarningHandler(?callable $warningHandler = null): void { self::$warningHandler = $warningHandler; } /** * Suppress warning by id (has no effect when custom warning handler is set). * * @param bool|int $suppress * * @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) suppress a specific warning * @example Warning::suppress(true) suppresses all warnings * @example Warning::suppress(false) enables all warnings * * @api */ public static function suppress($suppress = true): void { if ($suppress === true) { self::$enableWarnings = 0; } elseif ($suppress === false) { self::$enableWarnings = self::ALL; // @phpstan-ignore-next-line necessary until we can use proper unions } elseif (is_int($suppress)) { self::$enableWarnings &= ~$suppress; } else { $type = gettype($suppress); throw new \InvalidArgumentException("Expected type bool|int, got {$type}."); } } /** * Re-enable previously suppressed warning by id (has no effect when custom warning handler is set). * * @param bool|int $enable * * @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) re-enables a specific warning * @example Warning::suppress(true) re-enables all warnings * @example Warning::suppress(false) suppresses all warnings * * @api */ public static function enable($enable = true): void { if ($enable === true) { self::$enableWarnings = self::ALL; } elseif ($enable === false) { self::$enableWarnings = 0; // @phpstan-ignore-next-line necessary until we can use proper unions } elseif (is_int($enable)) { self::$enableWarnings |= $enable; } else { $type = gettype($enable); throw new \InvalidArgumentException("Expected type bool|int, got {$type}."); } } public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null): void { $messageLevel ??= \E_USER_WARNING; if (self::$warningHandler !== null) { (self::$warningHandler)($errorMessage, $warningId, $messageLevel); } elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) { self::$warned[$warningId] = true; trigger_error($errorMessage, $messageLevel); } } public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null): void { $messageLevel ??= \E_USER_WARNING; if (self::$warningHandler !== null) { (self::$warningHandler)($errorMessage, $warningId, $messageLevel); } elseif ((self::$enableWarnings & $warningId) > 0) { trigger_error($errorMessage, $messageLevel); } } } graphql/lib/Error/InvariantViolation.php 0000644 00000000411 15166657210 0014377 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Note: * This exception should not inherit base Error exception as it is raised when there is an error somewhere in * user-land code. */ class InvariantViolation extends \LogicException {} graphql/lib/Error/SerializationError.php 0000644 00000000452 15166657210 0014413 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; /** * Thrown when failing to serialize a leaf value. * * Not generally safe for clients, as the wrong given value could * be something not intended to ever be seen by clients. */ class SerializationError extends \Exception {} graphql/lib/Error/FormattedError.php 0000644 00000026023 15166657210 0013525 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; use YOOtheme\GraphQL\Executor\ExecutionResult; use YOOtheme\GraphQL\Language\Source; use YOOtheme\GraphQL\Language\SourceLocation; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Utils\Utils; use PHPUnit\Framework\Test; /** * This class is used for [default error formatting](error-handling.md). * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors) * and provides tools for error debugging. * * @see ExecutionResult * * @phpstan-import-type SerializableError from ExecutionResult * @phpstan-import-type ErrorFormatter from ExecutionResult * * @see \GraphQL\Tests\Error\FormattedErrorTest */ class FormattedError { private static string $internalErrorMessage = 'Internal server error'; /** * Set default error message for internal errors formatted using createFormattedError(). * This value can be overridden by passing 3rd argument to `createFormattedError()`. * * @api */ public static function setInternalErrorMessage(string $msg): void { self::$internalErrorMessage = $msg; } /** * Prints a GraphQLError to a string, representing useful location information * about the error's position in the source. */ public static function printError(Error $error): string { $printedLocations = []; $nodes = $error->nodes; if (isset($nodes) && $nodes !== []) { foreach ($nodes as $node) { $location = $node->loc; if (isset($location)) { $source = $location->source; if (isset($source)) { $printedLocations[] = self::highlightSourceAtLocation( $source, $source->getLocation($location->start) ); } } } } elseif ($error->getSource() !== null && $error->getLocations() !== []) { $source = $error->getSource(); foreach ($error->getLocations() as $location) { $printedLocations[] = self::highlightSourceAtLocation($source, $location); } } return $printedLocations === [] ? $error->getMessage() : implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n"; } /** * Render a helpful description of the location of the error in the GraphQL * Source document. */ private static function highlightSourceAtLocation(Source $source, SourceLocation $location): string { $line = $location->line; $lineOffset = $source->locationOffset->line - 1; $columnOffset = self::getColumnOffset($source, $location); $contextLine = $line + $lineOffset; $contextColumn = $location->column + $columnOffset; $prevLineNum = (string) ($contextLine - 1); $lineNum = (string) $contextLine; $nextLineNum = (string) ($contextLine + 1); $padLen = strlen($nextLineNum); $lines = Utils::splitLines($source->body); $lines[0] = self::spaces($source->locationOffset->column - 1) . $lines[0]; $outputLines = [ "{$source->name} ({$contextLine}:{$contextColumn})", $line >= 2 ? (self::leftPad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null, self::leftPad($padLen, $lineNum) . ': ' . $lines[$line - 1], self::spaces(2 + $padLen + $contextColumn - 1) . '^', $line < count($lines) ? self::leftPad($padLen, $nextLineNum) . ': ' . $lines[$line] : null, ]; return implode("\n", array_filter($outputLines)); } private static function getColumnOffset(Source $source, SourceLocation $location): int { return $location->line === 1 ? $source->locationOffset->column - 1 : 0; } private static function spaces(int $length): string { return str_repeat(' ', $length); } private static function leftPad(int $length, string $str): string { return self::spaces($length - mb_strlen($str)) . $str; } /** * Convert any exception to a GraphQL spec compliant array. * * This method only exposes the exception message when the given exception * implements the ClientAware interface, or when debug flags are passed. * * For a list of available debug flags @see \GraphQL\Error\DebugFlag constants. * * @return SerializableError * * @api */ public static function createFromException(\Throwable $exception, int $debugFlag = DebugFlag::NONE, ?string $internalErrorMessage = null): array { $internalErrorMessage ??= self::$internalErrorMessage; $message = $exception instanceof ClientAware && $exception->isClientSafe() ? $exception->getMessage() : $internalErrorMessage; $formattedError = ['message' => $message]; if ($exception instanceof Error) { $locations = array_map( static fn (SourceLocation $loc): array => $loc->toSerializableArray(), $exception->getLocations() ); if ($locations !== []) { $formattedError['locations'] = $locations; } if ($exception->path !== null && $exception->path !== []) { $formattedError['path'] = $exception->path; } } if ($exception instanceof ProvidesExtensions) { $extensions = $exception->getExtensions(); if (is_array($extensions) && $extensions !== []) { $formattedError['extensions'] = $extensions; } } if ($debugFlag !== DebugFlag::NONE) { $formattedError = self::addDebugEntries($formattedError, $exception, $debugFlag); } return $formattedError; } /** * Decorates spec-compliant $formattedError with debug entries according to $debug flags. * * @param SerializableError $formattedError * @param int $debugFlag For available flags @see \GraphQL\Error\DebugFlag * * @throws \Throwable * * @return SerializableError */ public static function addDebugEntries(array $formattedError, \Throwable $e, int $debugFlag): array { if ($debugFlag === DebugFlag::NONE) { return $formattedError; } if (($debugFlag & DebugFlag::RETHROW_INTERNAL_EXCEPTIONS) !== 0) { if (! $e instanceof Error) { throw $e; } if ($e->getPrevious() !== null) { throw $e->getPrevious(); } } $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe(); if (($debugFlag & DebugFlag::RETHROW_UNSAFE_EXCEPTIONS) !== 0 && $isUnsafe && $e->getPrevious() !== null) { throw $e->getPrevious(); } if (($debugFlag & DebugFlag::INCLUDE_DEBUG_MESSAGE) !== 0 && $isUnsafe) { $formattedError['extensions']['debugMessage'] = $e->getMessage(); } if (($debugFlag & DebugFlag::INCLUDE_TRACE) !== 0) { $actualError = $e->getPrevious() ?? $e; if ($e instanceof \ErrorException || $e instanceof \Error) { $formattedError['extensions']['file'] = $e->getFile(); $formattedError['extensions']['line'] = $e->getLine(); } else { $formattedError['extensions']['file'] = $actualError->getFile(); $formattedError['extensions']['line'] = $actualError->getLine(); } $isTrivial = $e instanceof Error && $e->getPrevious() === null; if (! $isTrivial) { $formattedError['extensions']['trace'] = static::toSafeTrace($actualError); } } return $formattedError; } /** * Prepares final error formatter taking in account $debug flags. * * If initial formatter is not set, FormattedError::createFromException is used. * * @phpstan-param ErrorFormatter|null $formatter */ public static function prepareFormatter(?callable $formatter, int $debug): callable { return $formatter === null ? static fn (\Throwable $e): array => static::createFromException($e, $debug) : static fn (\Throwable $e): array => static::addDebugEntries($formatter($e), $e, $debug); } /** * Returns error trace as serializable array. * * @return array<int, array{ * file?: string, * line?: int, * function?: string, * call?: string, * }> * * @api */ public static function toSafeTrace(\Throwable $error): array { $trace = $error->getTrace(); if ( isset($trace[0]['function']) && isset($trace[0]['class']) // Remove invariant entries as they don't provide much value: && ($trace[0]['class'] . '::' . $trace[0]['function'] === 'GraphQL\Utils\Utils::invariant') ) { array_shift($trace); } elseif (! isset($trace[0]['file'])) { // Remove root call as it's likely error handler trace: array_shift($trace); } $formatted = []; foreach ($trace as $err) { $safeErr = []; if (isset($err['file'])) { $safeErr['file'] = $err['file']; } if (isset($err['line'])) { $safeErr['line'] = $err['line']; } $func = $err['function']; $args = array_map([self::class, 'printVar'], $err['args'] ?? []); $funcStr = $func . '(' . implode(', ', $args) . ')'; if (isset($err['class'])) { $safeErr['call'] = $err['class'] . '::' . $funcStr; } else { $safeErr['function'] = $funcStr; } $formatted[] = $safeErr; } return $formatted; } /** @param mixed $var */ public static function printVar($var): string { if ($var instanceof Type) { return 'GraphQLType: ' . $var->toString(); } if (is_object($var)) { // Calling `count` on instances of `PHPUnit\Framework\Test` triggers an unintended side effect - see https://github.com/sebastianbergmann/phpunit/issues/5866#issuecomment-2172429263 $count = ! $var instanceof Test && $var instanceof \Countable ? '(' . count($var) . ')' : ''; return 'instance of ' . get_class($var) . $count; } if (is_array($var)) { return 'array(' . count($var) . ')'; } if ($var === '') { return '(empty string)'; } if (is_string($var)) { return "'" . addcslashes($var, "'") . "'"; } if (is_bool($var)) { return $var ? 'true' : 'false'; } if (is_scalar($var)) { return (string) $var; } if ($var === null) { return 'null'; } return gettype($var); } } graphql/lib/Error/Error.php 0000644 00000022671 15166657210 0011664 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Error; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\Source; use YOOtheme\GraphQL\Language\SourceLocation; /** * Describes an Error found during the parse, validate, or * execute phases of performing a GraphQL operation. In addition to a message * and stack trace, it also includes information about the locations in a * GraphQL document and/or execution result that correspond to the Error. * * When the error was caused by an exception thrown in resolver, original exception * is available via `getPrevious()`. * * Also read related docs on [error handling](error-handling.md) * * Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class * are available in addition to those listed below. * * @see \GraphQL\Tests\Error\ErrorTest */ class Error extends \Exception implements \JsonSerializable, ClientAware, ProvidesExtensions { /** * Lazily initialized. * * @var array<int, SourceLocation> */ private array $locations; /** * An array describing the JSON-path into the execution response which * corresponds to this error. Only included for errors during execution. * When fields are aliased, the path includes aliases. * * @var list<int|string>|null */ public ?array $path; /** * An array describing the JSON-path into the execution response which * corresponds to this error. Only included for errors during execution. * This will never include aliases. * * @var list<int|string>|null */ public ?array $unaliasedPath; /** * An array of GraphQL AST Nodes corresponding to this error. * * @var array<Node>|null */ public ?array $nodes; /** * The source GraphQL document for the first location of this error. * * Note that if this Error represents more than one node, the source may not * represent nodes after the first node. */ private ?Source $source; /** @var array<int, int>|null */ private ?array $positions; private bool $isClientSafe; /** @var array<string, mixed>|null */ protected ?array $extensions; /** * @param iterable<array-key, Node|null>|Node|null $nodes * @param array<int, int>|null $positions * @param list<int|string>|null $path * @param array<string, mixed>|null $extensions * @param list<int|string>|null $unaliasedPath */ public function __construct( string $message = '', $nodes = null, ?Source $source = null, ?array $positions = null, ?array $path = null, ?\Throwable $previous = null, ?array $extensions = null, ?array $unaliasedPath = null ) { parent::__construct($message, 0, $previous); // Compute list of blame nodes. if ($nodes instanceof \Traversable) { /** @phpstan-ignore arrayFilter.strict */ $this->nodes = array_filter(iterator_to_array($nodes)); } elseif (is_array($nodes)) { $this->nodes = array_filter($nodes); } elseif ($nodes !== null) { $this->nodes = [$nodes]; } else { $this->nodes = null; } $this->source = $source; $this->positions = $positions; $this->path = $path; $this->unaliasedPath = $unaliasedPath; if (is_array($extensions) && $extensions !== []) { $this->extensions = $extensions; } elseif ($previous instanceof ProvidesExtensions) { $this->extensions = $previous->getExtensions(); } else { $this->extensions = null; } $this->isClientSafe = $previous instanceof ClientAware ? $previous->isClientSafe() : $previous === null; } /** * Given an arbitrary Error, presumably thrown while attempting to execute a * GraphQL operation, produce a new GraphQLError aware of the location in the * document responsible for the original Error. * * @param mixed $error * @param iterable<Node>|Node|null $nodes * @param list<int|string>|null $path * @param list<int|string>|null $unaliasedPath */ public static function createLocatedError($error, $nodes = null, ?array $path = null, ?array $unaliasedPath = null): Error { if ($error instanceof self) { if ($error->isLocated()) { return $error; } $nodes ??= $error->getNodes(); $path ??= $error->getPath(); $unaliasedPath ??= $error->getUnaliasedPath(); } $source = null; $originalError = null; $positions = []; $extensions = []; if ($error instanceof self) { $message = $error->getMessage(); $originalError = $error; $source = $error->getSource(); $positions = $error->getPositions(); $extensions = $error->getExtensions(); } elseif ($error instanceof InvariantViolation) { $message = $error->getMessage(); $originalError = $error->getPrevious() ?? $error; } elseif ($error instanceof \Throwable) { $message = $error->getMessage(); $originalError = $error; } else { $message = (string) $error; } $nonEmptyMessage = $message === '' ? 'An unknown error occurred.' : $message; return new static( $nonEmptyMessage, $nodes, $source, $positions, $path, $originalError, $extensions, $unaliasedPath ); } protected function isLocated(): bool { $path = $this->getPath(); $nodes = $this->getNodes(); return $path !== null && $path !== [] && $nodes !== null && $nodes !== []; } public function isClientSafe(): bool { return $this->isClientSafe; } public function getSource(): ?Source { return $this->source ??= $this->nodes[0]->loc->source ?? null; } /** @return array<int, int> */ public function getPositions(): array { if (! isset($this->positions)) { $this->positions = []; if (isset($this->nodes)) { foreach ($this->nodes as $node) { if (isset($node->loc->start)) { $this->positions[] = $node->loc->start; } } } } return $this->positions; } /** * An array of locations within the source GraphQL document which correspond to this error. * * Each entry has information about `line` and `column` within source GraphQL document: * $location->line; * $location->column; * * Errors during validation often contain multiple locations, for example to * point out to field mentioned in multiple fragments. Errors during execution include a * single location, the field which produced the error. * * @return array<int, SourceLocation> * * @api */ public function getLocations(): array { if (! isset($this->locations)) { $positions = $this->getPositions(); $source = $this->getSource(); $nodes = $this->getNodes(); $this->locations = []; if ($source !== null && $positions !== []) { foreach ($positions as $position) { $this->locations[] = $source->getLocation($position); } } elseif ($nodes !== null && $nodes !== []) { foreach ($nodes as $node) { if (isset($node->loc->source)) { $this->locations[] = $node->loc->source->getLocation($node->loc->start); } } } } return $this->locations; } /** @return array<Node>|null */ public function getNodes(): ?array { return $this->nodes; } /** * Returns an array describing the path from the root value to the field which produced this error. * Only included for execution errors. When fields are aliased, the path includes aliases. * * @return list<int|string>|null * * @api */ public function getPath(): ?array { return $this->path; } /** * Returns an array describing the path from the root value to the field which produced this error. * Only included for execution errors. This will never include aliases. * * @return list<int|string>|null * * @api */ public function getUnaliasedPath(): ?array { return $this->unaliasedPath; } /** @return array<string, mixed>|null */ public function getExtensions(): ?array { return $this->extensions; } /** * Specify data which should be serialized to JSON. * * @see http://php.net/manual/en/jsonserializable.jsonserialize.php * * @return array<string, mixed> data which can be serialized by <b>json_encode</b>, * which is a value of any type other than a resource */ #[\ReturnTypeWillChange] public function jsonSerialize(): array { return FormattedError::createFromException($this); } public function __toString(): string { return FormattedError::printError($this); } } graphql/lib/Utils/Utils.php 0000644 00000017570 15166657210 0011704 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\Warning; use YOOtheme\GraphQL\Language\AST\Node; class Utils { public static function undefined(): \stdClass { static $undefined; return $undefined ??= new \stdClass(); } /** @param array<string, mixed> $vars */ public static function assign(object $obj, array $vars): object { foreach ($vars as $key => $value) { if (! property_exists($obj, $key)) { $cls = get_class($obj); Warning::warn( "Trying to set non-existing property '{$key}' on class '{$cls}'", Warning::WARNING_ASSIGN ); } $obj->{$key} = $value; } return $obj; } /** * Print a value that came from JSON for debugging purposes. * * @param mixed $value */ public static function printSafeJson($value): string { if ($value instanceof \stdClass) { return static::jsonEncodeOrSerialize($value); } return static::printSafeInternal($value); } /** * Print a value that came from PHP for debugging purposes. * * @param mixed $value */ public static function printSafe($value): string { if (is_object($value)) { if (method_exists($value, '__toString')) { return $value->__toString(); } return 'instance of ' . get_class($value); } return static::printSafeInternal($value); } /** @param \stdClass|array<mixed> $value */ protected static function jsonEncodeOrSerialize($value): string { try { return json_encode($value, JSON_THROW_ON_ERROR); } catch (\JsonException $jsonException) { return serialize($value); } } /** @param mixed $value */ protected static function printSafeInternal($value): string { if (is_array($value)) { return static::jsonEncodeOrSerialize($value); } if ($value === '') { return '(empty string)'; } if ($value === null) { return 'null'; } if ($value === false) { return 'false'; } if ($value === true) { return 'true'; } if (is_string($value)) { return "\"{$value}\""; } if (is_scalar($value)) { return (string) $value; } return gettype($value); } /** UTF-8 compatible chr(). */ public static function chr(int $ord, string $encoding = 'UTF-8'): string { if ($encoding === 'UCS-4BE') { return pack('N', $ord); } return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE'); } /** UTF-8 compatible ord(). */ public static function ord(string $char, string $encoding = 'UTF-8'): int { if (! isset($char[1])) { return ord($char); } if ($encoding !== 'UCS-4BE') { $char = mb_convert_encoding($char, 'UCS-4BE', $encoding); assert(is_string($char), 'format string is statically known to be correct'); } $unpacked = unpack('N', $char); assert(is_array($unpacked), 'format string is statically known to be correct'); return $unpacked[1]; } /** Returns UTF-8 char code at given $positing of the $string. */ public static function charCodeAt(string $string, int $position): int { $char = mb_substr($string, $position, 1, 'UTF-8'); return self::ord($char); } /** @throws \JsonException */ public static function printCharCode(?int $code): string { if ($code === null) { return '<EOF>'; } return $code < 0x007F // Trust JSON for ASCII ? json_encode(self::chr($code), JSON_THROW_ON_ERROR) // Otherwise, print the escaped form : '"\\u' . dechex($code) . '"'; } /** * Upholds the spec rules about naming. * * @throws Error */ public static function assertValidName(string $name): void { $error = self::isValidNameError($name); if ($error !== null) { throw $error; } } /** Returns an Error if a name is invalid. */ public static function isValidNameError(string $name, ?Node $node = null): ?Error { if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') { return new Error( "Name \"{$name}\" must not begin with \"__\", which is reserved by GraphQL introspection.", $node ); } if (preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name) !== 1) { return new Error( "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.", $node ); } return null; } /** @param array<string> $items */ public static function quotedOrList(array $items): string { $quoted = array_map( static fn (string $item): string => "\"{$item}\"", $items ); return self::orList($quoted); } /** @param array<string> $items */ public static function orList(array $items): string { if ($items === []) { return ''; } $selected = array_slice($items, 0, 5); $selectedLength = count($selected); $firstSelected = $selected[0]; if ($selectedLength === 1) { return $firstSelected; } return array_reduce( range(1, $selectedLength - 1), static fn ($list, $index): string => $list . ($selectedLength > 2 ? ', ' : ' ') . ($index === $selectedLength - 1 ? 'or ' : '') . $selected[$index], $firstSelected ); } /** * Given an invalid input string and a list of valid options, returns a filtered * list of valid options sorted based on their similarity with the input. * * @param array<string> $options * * @return array<int, string> */ public static function suggestionList(string $input, array $options): array { /** @var array<string, int> $optionsByDistance */ $optionsByDistance = []; $lexicalDistance = new LexicalDistance($input); $threshold = mb_strlen($input) * 0.4 + 1; foreach ($options as $option) { $distance = $lexicalDistance->measure($option, $threshold); if ($distance !== null) { $optionsByDistance[$option] = $distance; } } uksort($optionsByDistance, static function (string $a, string $b) use ($optionsByDistance) { $distanceDiff = $optionsByDistance[$a] - $optionsByDistance[$b]; return $distanceDiff !== 0 ? $distanceDiff : strnatcmp($a, $b); }); return array_map('strval', array_keys($optionsByDistance)); } /** * Try to extract the value for a key from an object like value. * * @param mixed $objectLikeValue * * @return mixed */ public static function extractKey($objectLikeValue, string $key) { if (is_array($objectLikeValue) || $objectLikeValue instanceof \ArrayAccess) { return $objectLikeValue[$key] ?? null; } if (is_object($objectLikeValue)) { return $objectLikeValue->{$key} ?? null; } return null; } /** * Split a string that has either Unix, Windows or Mac style newlines into lines. * * @return list<string> */ public static function splitLines(string $value): array { $lines = preg_split("/\r\n|\r|\n/", $value); assert(is_array($lines), 'given the regex is valid'); return $lines; } } graphql/lib/Utils/BuildClientSchema.php 0000644 00000044423 15166657211 0014121 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Error\SyntaxError; use YOOtheme\GraphQL\Language\Parser; use YOOtheme\GraphQL\Type\Definition\CustomScalarType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InputType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\OutputType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Type\SchemaConfig; use YOOtheme\GraphQL\Type\TypeKind; /** * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * * @phpstan-type Options array{ * assumeValid?: bool * } * * - assumeValid: * When building a schema from a GraphQL service's introspection result, it * might be safe to assume the schema is valid. Set to true to assume the * produced schema is valid. * * Default: false * * @see \GraphQL\Tests\Utils\BuildClientSchemaTest */ class BuildClientSchema { /** @var array<string, mixed> */ private array $introspection; /** * @var array<string, bool> * * @phpstan-var Options */ private array $options; /** @var array<string, NamedType&Type> */ private array $typeMap = []; /** * @param array<string, mixed> $introspectionQuery * @param array<string, bool> $options * * @phpstan-param Options $options */ public function __construct(array $introspectionQuery, array $options = []) { $this->introspection = $introspectionQuery; $this->options = $options; } /** * Build a schema for use by client tools. * * Given the result of a client running the introspection query, creates and * returns a \GraphQL\Type\Schema instance which can be then used with all graphql-php * tools, but cannot be used to execute a query, as introspection does not * represent the "resolver", "parse" or "serialize" functions or any other * server-internal mechanisms. * * This function expects a complete introspection result. Don't forget to check * the "errors" field of a server response before calling this function. * * @param array<string, mixed> $introspectionQuery * @param array<string, bool> $options * * @phpstan-param Options $options * * @api * * @throws \Exception * @throws InvariantViolation */ public static function build(array $introspectionQuery, array $options = []): Schema { return (new self($introspectionQuery, $options))->buildSchema(); } /** * @throws \Exception * @throws InvariantViolation */ public function buildSchema(): Schema { if (! array_key_exists('__schema', $this->introspection)) { $missingSchemaIntrospection = Utils::printSafeJson($this->introspection); throw new InvariantViolation("Invalid or incomplete introspection result. Ensure that you are passing \"data\" property of introspection response and no \"errors\" was returned alongside: {$missingSchemaIntrospection}."); } $schemaIntrospection = $this->introspection['__schema']; $builtInTypes = array_merge( Type::getStandardTypes(), Introspection::getTypes() ); foreach ($schemaIntrospection['types'] as $typeIntrospection) { if (! isset($typeIntrospection['name'])) { throw self::invalidOrIncompleteIntrospectionResult($typeIntrospection); } $name = $typeIntrospection['name']; if (! is_string($name)) { throw self::invalidOrIncompleteIntrospectionResult($typeIntrospection); } // Use the built-in singleton types to avoid reconstruction $this->typeMap[$name] = $builtInTypes[$name] ?? $this->buildType($typeIntrospection); } $queryType = isset($schemaIntrospection['queryType']) ? $this->getObjectType($schemaIntrospection['queryType']) : null; $mutationType = isset($schemaIntrospection['mutationType']) ? $this->getObjectType($schemaIntrospection['mutationType']) : null; $subscriptionType = isset($schemaIntrospection['subscriptionType']) ? $this->getObjectType($schemaIntrospection['subscriptionType']) : null; $directives = isset($schemaIntrospection['directives']) ? array_map( [$this, 'buildDirective'], $schemaIntrospection['directives'] ) : []; return new Schema( (new SchemaConfig()) ->setQuery($queryType) ->setMutation($mutationType) ->setSubscription($subscriptionType) ->setTypes($this->typeMap) ->setDirectives($directives) ->setAssumeValid($this->options['assumeValid'] ?? false) ); } /** * @param array<string, mixed> $typeRef * * @throws InvariantViolation */ private function getType(array $typeRef): Type { if (isset($typeRef['kind'])) { if ($typeRef['kind'] === TypeKind::LIST) { if (! isset($typeRef['ofType'])) { throw new InvariantViolation('Decorated type deeper than introspection query.'); } return new ListOfType($this->getType($typeRef['ofType'])); } if ($typeRef['kind'] === TypeKind::NON_NULL) { if (! isset($typeRef['ofType'])) { throw new InvariantViolation('Decorated type deeper than introspection query.'); } // @phpstan-ignore-next-line if the type is not a nullable type, schema validation will catch it return new NonNull($this->getType($typeRef['ofType'])); } } if (! isset($typeRef['name'])) { $unknownTypeRef = Utils::printSafeJson($typeRef); throw new InvariantViolation("Unknown type reference: {$unknownTypeRef}."); } return $this->getNamedType($typeRef['name']); } /** * @throws InvariantViolation * * @return NamedType&Type */ private function getNamedType(string $typeName): NamedType { if (! isset($this->typeMap[$typeName])) { throw new InvariantViolation("Invalid or incomplete schema, unknown type: {$typeName}. Ensure that a full introspection query is used in order to build a client schema."); } return $this->typeMap[$typeName]; } /** @param array<mixed> $type */ public static function invalidOrIncompleteIntrospectionResult(array $type): InvariantViolation { $incompleteType = Utils::printSafeJson($type); return new InvariantViolation("Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: {$incompleteType}."); } /** * @param array<string, mixed> $typeRef * * @throws InvariantViolation * * @return Type&InputType */ private function getInputType(array $typeRef): InputType { $type = $this->getType($typeRef); if ($type instanceof InputType) { return $type; } $notInputType = Utils::printSafe($type); throw new InvariantViolation("Introspection must provide input type for arguments, but received: {$notInputType}."); } /** * @param array<string, mixed> $typeRef * * @throws InvariantViolation */ private function getOutputType(array $typeRef): OutputType { $type = $this->getType($typeRef); if ($type instanceof OutputType) { return $type; } $notInputType = Utils::printSafe($type); throw new InvariantViolation("Introspection must provide output type for fields, but received: {$notInputType}."); } /** * @param array<string, mixed> $typeRef * * @throws InvariantViolation */ private function getObjectType(array $typeRef): ObjectType { $type = $this->getType($typeRef); return ObjectType::assertObjectType($type); } /** * @param array<string, mixed> $typeRef * * @throws InvariantViolation */ public function getInterfaceType(array $typeRef): InterfaceType { $type = $this->getType($typeRef); return InterfaceType::assertInterfaceType($type); } /** * @param array<string, mixed> $type * * @throws InvariantViolation * * @return Type&NamedType */ private function buildType(array $type): NamedType { if (! array_key_exists('kind', $type)) { throw self::invalidOrIncompleteIntrospectionResult($type); } switch ($type['kind']) { case TypeKind::SCALAR: return $this->buildScalarDef($type); case TypeKind::OBJECT: return $this->buildObjectDef($type); case TypeKind::INTERFACE: return $this->buildInterfaceDef($type); case TypeKind::UNION: return $this->buildUnionDef($type); case TypeKind::ENUM: return $this->buildEnumDef($type); case TypeKind::INPUT_OBJECT: return $this->buildInputObjectDef($type); default: $unknownKindType = Utils::printSafeJson($type); throw new InvariantViolation("Invalid or incomplete introspection result. Received type with unknown kind: {$unknownKindType}."); } } /** * @param array<string, string> $scalar * * @throws InvariantViolation */ private function buildScalarDef(array $scalar): ScalarType { return new CustomScalarType([ 'name' => $scalar['name'], 'description' => $scalar['description'], 'serialize' => static fn ($value) => $value, ]); } /** * @param array<string, mixed> $implementingIntrospection * * @throws InvariantViolation * * @return array<int, InterfaceType> */ private function buildImplementationsList(array $implementingIntrospection): array { // TODO: Temporary workaround until GraphQL ecosystem will fully support 'interfaces' on interface types. if ( array_key_exists('interfaces', $implementingIntrospection) && $implementingIntrospection['interfaces'] === null && $implementingIntrospection['kind'] === TypeKind::INTERFACE ) { return []; } if (! array_key_exists('interfaces', $implementingIntrospection)) { $safeIntrospection = Utils::printSafeJson($implementingIntrospection); throw new InvariantViolation("Introspection result missing interfaces: {$safeIntrospection}."); } return array_map( [$this, 'getInterfaceType'], $implementingIntrospection['interfaces'] ); } /** * @param array<string, mixed> $object * * @throws InvariantViolation */ private function buildObjectDef(array $object): ObjectType { return new ObjectType([ 'name' => $object['name'], 'description' => $object['description'], 'interfaces' => fn (): array => $this->buildImplementationsList($object), 'fields' => fn (): array => $this->buildFieldDefMap($object), ]); } /** * @param array<string, mixed> $interface * * @throws InvariantViolation */ private function buildInterfaceDef(array $interface): InterfaceType { return new InterfaceType([ 'name' => $interface['name'], 'description' => $interface['description'], 'fields' => fn (): array => $this->buildFieldDefMap($interface), 'interfaces' => fn (): array => $this->buildImplementationsList($interface), ]); } /** * @param array<string, mixed> $union * * @throws InvariantViolation */ private function buildUnionDef(array $union): UnionType { if (! array_key_exists('possibleTypes', $union)) { $safeUnion = Utils::printSafeJson($union); throw new InvariantViolation("Introspection result missing possibleTypes: {$safeUnion}."); } return new UnionType([ 'name' => $union['name'], 'description' => $union['description'], 'types' => fn (): array => array_map( [$this, 'getObjectType'], $union['possibleTypes'] ), ]); } /** * @param array<string, mixed> $enum * * @throws InvariantViolation */ private function buildEnumDef(array $enum): EnumType { if (! array_key_exists('enumValues', $enum)) { $safeEnum = Utils::printSafeJson($enum); throw new InvariantViolation("Introspection result missing enumValues: {$safeEnum}."); } $values = []; foreach ($enum['enumValues'] as $value) { $values[$value['name']] = [ 'description' => $value['description'], 'deprecationReason' => $value['deprecationReason'], ]; } return new EnumType([ 'name' => $enum['name'], 'description' => $enum['description'], 'values' => $values, ]); } /** * @param array<string, mixed> $inputObject * * @throws InvariantViolation */ private function buildInputObjectDef(array $inputObject): InputObjectType { if (! array_key_exists('inputFields', $inputObject)) { $safeInputObject = Utils::printSafeJson($inputObject); throw new InvariantViolation("Introspection result missing inputFields: {$safeInputObject}."); } return new InputObjectType([ 'name' => $inputObject['name'], 'description' => $inputObject['description'], 'fields' => fn (): array => $this->buildInputValueDefMap($inputObject['inputFields']), ]); } /** * @param array<string, mixed> $typeIntrospection * * @throws \Exception * @throws InvariantViolation * * @return array<string, UnnamedFieldDefinitionConfig> */ private function buildFieldDefMap(array $typeIntrospection): array { if (! array_key_exists('fields', $typeIntrospection)) { $safeType = Utils::printSafeJson($typeIntrospection); throw new InvariantViolation("Introspection result missing fields: {$safeType}."); } /** @var array<string, UnnamedFieldDefinitionConfig> $map */ $map = []; foreach ($typeIntrospection['fields'] as $field) { if (! array_key_exists('args', $field)) { $safeField = Utils::printSafeJson($field); throw new InvariantViolation("Introspection result missing field args: {$safeField}."); } $map[$field['name']] = [ 'description' => $field['description'], 'deprecationReason' => $field['deprecationReason'], 'type' => $this->getOutputType($field['type']), 'args' => $this->buildInputValueDefMap($field['args']), ]; } // @phpstan-ignore-next-line unless the returned name was numeric, this works return $map; } /** * @param array<int, array<string, mixed>> $inputValueIntrospections * * @throws \Exception * * @return array<string, UnnamedInputObjectFieldConfig> */ private function buildInputValueDefMap(array $inputValueIntrospections): array { /** @var array<string, UnnamedInputObjectFieldConfig> $map */ $map = []; foreach ($inputValueIntrospections as $value) { $map[$value['name']] = $this->buildInputValue($value); } return $map; } /** * @param array<string, mixed> $inputValueIntrospection * * @throws \Exception * @throws SyntaxError * * @return UnnamedInputObjectFieldConfig */ public function buildInputValue(array $inputValueIntrospection): array { $type = $this->getInputType($inputValueIntrospection['type']); $inputValue = [ 'description' => $inputValueIntrospection['description'], 'type' => $type, ]; if (isset($inputValueIntrospection['defaultValue'])) { $inputValue['defaultValue'] = AST::valueFromAST( Parser::parseValue($inputValueIntrospection['defaultValue']), $type ); } return $inputValue; } /** * @param array<string, mixed> $directive * * @throws \Exception * @throws InvariantViolation */ public function buildDirective(array $directive): Directive { if (! array_key_exists('args', $directive)) { $safeDirective = Utils::printSafeJson($directive); throw new InvariantViolation("Introspection result missing directive args: {$safeDirective}."); } if (! array_key_exists('locations', $directive)) { $safeDirective = Utils::printSafeJson($directive); throw new InvariantViolation("Introspection result missing directive locations: {$safeDirective}."); } return new Directive([ 'name' => $directive['name'], 'description' => $directive['description'], 'args' => $this->buildInputValueDefMap($directive['args']), 'isRepeatable' => $directive['isRepeatable'] ?? false, 'locations' => $directive['locations'], ]); } } graphql/lib/Utils/TypeInfo.php 0000644 00000035153 15166657211 0012337 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\ArgumentNode; use YOOtheme\GraphQL\Language\AST\DirectiveNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\FieldNode; use YOOtheme\GraphQL\Language\AST\FragmentDefinitionNode; use YOOtheme\GraphQL\Language\AST\InlineFragmentNode; use YOOtheme\GraphQL\Language\AST\ListValueNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\ObjectFieldNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\SelectionSetNode; use YOOtheme\GraphQL\Language\AST\VariableDefinitionNode; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\CompositeType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\HasFieldsType; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InputType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Definition\WrappingType; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; class TypeInfo { private Schema $schema; /** @var array<int, Type|null> */ private array $typeStack = []; /** @var array<int, (CompositeType&Type)|null> */ private array $parentTypeStack = []; /** @var array<int, (InputType&Type)|null> */ private array $inputTypeStack = []; /** @var array<int, FieldDefinition|null> */ private array $fieldDefStack = []; /** @var array<int, mixed> */ private array $defaultValueStack = []; private ?Directive $directive = null; private ?Argument $argument = null; /** @var mixed */ private $enumValue; public function __construct(Schema $schema) { $this->schema = $schema; } /** @return array<int, (CompositeType&Type)|null> */ public function getParentTypeStack(): array { return $this->parentTypeStack; } /** @return array<int, FieldDefinition|null> */ public function getFieldDefStack(): array { return $this->fieldDefStack; } /** * Given root type scans through all fields to find nested types. * * Returns array where keys are for type name * and value contains corresponding type instance. * * Example output: * [ * 'String' => $instanceOfStringType, * 'MyType' => $instanceOfMyType, * ... * ] * * @param (Type&NamedType)|(Type&WrappingType) $type * @param array<string, Type&NamedType> $typeMap * * @throws InvariantViolation */ public static function extractTypes(Type $type, array &$typeMap): void { if ($type instanceof WrappingType) { self::extractTypes($type->getInnermostType(), $typeMap); return; } $name = $type->name; assert(is_string($name)); if (isset($typeMap[$name])) { if ($typeMap[$name] !== $type) { throw new InvariantViolation("Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry)."); } return; } $typeMap[$name] = $type; if ($type instanceof UnionType) { foreach ($type->getTypes() as $member) { self::extractTypes($member, $typeMap); } return; } if ($type instanceof InputObjectType) { foreach ($type->getFields() as $field) { $fieldType = $field->getType(); assert($fieldType instanceof NamedType || $fieldType instanceof WrappingType); self::extractTypes($fieldType, $typeMap); } return; } if ($type instanceof ImplementingType) { foreach ($type->getInterfaces() as $interface) { self::extractTypes($interface, $typeMap); } } if ($type instanceof HasFieldsType) { foreach ($type->getFields() as $field) { foreach ($field->args as $arg) { $argType = $arg->getType(); assert($argType instanceof NamedType || $argType instanceof WrappingType); self::extractTypes($argType, $typeMap); } $fieldType = $field->getType(); assert($fieldType instanceof NamedType || $fieldType instanceof WrappingType); self::extractTypes($fieldType, $typeMap); } } } /** * @param array<string, Type&NamedType> $typeMap * * @throws InvariantViolation */ public static function extractTypesFromDirectives(Directive $directive, array &$typeMap): void { foreach ($directive->args as $arg) { $argType = $arg->getType(); assert($argType instanceof NamedType || $argType instanceof WrappingType); self::extractTypes($argType, $typeMap); } } /** @return (Type&InputType)|null */ public function getParentInputType(): ?InputType { return $this->inputTypeStack[count($this->inputTypeStack) - 2] ?? null; } public function getArgument(): ?Argument { return $this->argument; } /** @return mixed */ public function getEnumValue() { return $this->enumValue; } /** * @throws \Exception * @throws InvariantViolation */ public function enter(Node $node): void { $schema = $this->schema; // Note: many of the types below are explicitly typed as "mixed" to drop // any assumptions of a valid schema to ensure runtime types are properly // checked before continuing since TypeInfo is used as part of validation // which occurs before guarantees of schema and document validity. switch (true) { case $node instanceof SelectionSetNode: $namedType = Type::getNamedType($this->getType()); $this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null; break; case $node instanceof FieldNode: $parentType = $this->getParentType(); $fieldDef = $parentType === null ? null : self::getFieldDefinition($schema, $parentType, $node); $fieldType = $fieldDef === null ? null : $fieldDef->getType(); $this->fieldDefStack[] = $fieldDef; $this->typeStack[] = $fieldType; break; case $node instanceof DirectiveNode: $this->directive = $schema->getDirective($node->name->value); break; case $node instanceof OperationDefinitionNode: if ($node->operation === 'query') { $type = $schema->getQueryType(); } elseif ($node->operation === 'mutation') { $type = $schema->getMutationType(); } else { // Only other option $type = $schema->getSubscriptionType(); } $this->typeStack[] = Type::isOutputType($type) ? $type : null; break; case $node instanceof InlineFragmentNode: case $node instanceof FragmentDefinitionNode: $typeConditionNode = $node->typeCondition; $outputType = $typeConditionNode === null ? Type::getNamedType($this->getType()) : AST::typeFromAST([$schema, 'getType'], $typeConditionNode); $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; break; case $node instanceof VariableDefinitionNode: $inputType = AST::typeFromAST([$schema, 'getType'], $node->type); $this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push break; case $node instanceof ArgumentNode: $fieldOrDirective = $this->getDirective() ?? $this->getFieldDef(); $argDef = null; $argType = null; if ($fieldOrDirective !== null) { foreach ($fieldOrDirective->args as $arg) { if ($arg->name === $node->name->value) { $argDef = $arg; $argType = $arg->getType(); } } } $this->argument = $argDef; $this->defaultValueStack[] = $argDef !== null && $argDef->defaultValueExists() ? $argDef->defaultValue : Utils::undefined(); $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null; break; case $node instanceof ListValueNode: $type = $this->getInputType(); $listType = $type instanceof NonNull ? $type->getWrappedType() : $type; $itemType = $listType instanceof ListOfType ? $listType->getWrappedType() : $listType; // List positions never have a default value. $this->defaultValueStack[] = Utils::undefined(); $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null; break; case $node instanceof ObjectFieldNode: $objectType = Type::getNamedType($this->getInputType()); $inputField = null; $inputFieldType = null; if ($objectType instanceof InputObjectType) { $tmp = $objectType->getFields(); $inputField = $tmp[$node->name->value] ?? null; $inputFieldType = $inputField === null ? null : $inputField->getType(); } $this->defaultValueStack[] = $inputField !== null && $inputField->defaultValueExists() ? $inputField->defaultValue : Utils::undefined(); $this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null; break; case $node instanceof EnumValueNode: $enumType = Type::getNamedType($this->getInputType()); $this->enumValue = $enumType instanceof EnumType ? $enumType->getValue($node->value) : null; break; } } public function getType(): ?Type { return $this->typeStack[count($this->typeStack) - 1] ?? null; } /** @return (CompositeType&Type)|null */ public function getParentType(): ?CompositeType { return $this->parentTypeStack[count($this->parentTypeStack) - 1] ?? null; } /** * Not exactly the same as the executor's definition of getFieldDef, in this * statically evaluated environment we do not always have an Object type, * and need to handle Interface and Union types. * * @throws InvariantViolation */ private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode): ?FieldDefinition { $name = $fieldNode->name->value; $schemaMeta = Introspection::schemaMetaFieldDef(); if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) { return $schemaMeta; } $typeMeta = Introspection::typeMetaFieldDef(); if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) { return $typeMeta; } $typeNameMeta = Introspection::typeNameMetaFieldDef(); if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) { return $typeNameMeta; } if ( $parentType instanceof ObjectType || $parentType instanceof InterfaceType ) { return $parentType->findField($name); } return null; } public function getDirective(): ?Directive { return $this->directive; } public function getFieldDef(): ?FieldDefinition { return $this->fieldDefStack[count($this->fieldDefStack) - 1] ?? null; } /** @return mixed any value is possible */ public function getDefaultValue() { return $this->defaultValueStack[count($this->defaultValueStack) - 1] ?? null; } /** @return (InputType&Type)|null */ public function getInputType(): ?InputType { return $this->inputTypeStack[count($this->inputTypeStack) - 1] ?? null; } public function leave(Node $node): void { switch (true) { case $node instanceof SelectionSetNode: array_pop($this->parentTypeStack); break; case $node instanceof FieldNode: array_pop($this->fieldDefStack); array_pop($this->typeStack); break; case $node instanceof DirectiveNode: $this->directive = null; break; case $node instanceof OperationDefinitionNode: case $node instanceof InlineFragmentNode: case $node instanceof FragmentDefinitionNode: array_pop($this->typeStack); break; case $node instanceof VariableDefinitionNode: array_pop($this->inputTypeStack); break; case $node instanceof ArgumentNode: $this->argument = null; array_pop($this->defaultValueStack); array_pop($this->inputTypeStack); break; case $node instanceof ListValueNode: case $node instanceof ObjectFieldNode: array_pop($this->defaultValueStack); array_pop($this->inputTypeStack); break; case $node instanceof EnumValueNode: $this->enumValue = null; break; } } } graphql/lib/Utils/PhpDoc.php 0000644 00000002723 15166657211 0011754 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; class PhpDoc { /** @param string|false|null $docBlock */ public static function unwrap($docBlock): ?string { if ($docBlock === false || $docBlock === null) { return null; } $content = preg_replace('~([\r\n]) \* (.*)~i', '$1$2', $docBlock); // strip * assert(is_string($content), 'regex is statically known to be valid'); $content = preg_replace('~([\r\n])[\* ]+([\r\n])~i', '$1$2', $content); // strip single-liner * assert(is_string($content), 'regex is statically known to be valid'); $content = substr($content, 3); // strip leading /** $content = substr($content, 0, -2); // strip trailing */ return static::nonEmptyOrNull($content); } /** @param string|false|null $docBlock */ public static function unpad($docBlock): ?string { if ($docBlock === false || $docBlock === null) { return null; } $lines = explode("\n", $docBlock); $lines = array_map( static fn (string $line): string => ' ' . trim($line), $lines ); $content = implode("\n", $lines); return static::nonEmptyOrNull($content); } protected static function nonEmptyOrNull(string $maybeEmptyString): ?string { $trimmed = trim($maybeEmptyString); return $trimmed === '' ? null : $trimmed; } } graphql/lib/Utils/BreakingChangesFinder.php 0000644 00000105246 15166657211 0014746 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Schema; /** * Utility for finding breaking/dangerous changes between two schemas. * * @phpstan-type Change array{type: string, description: string} * @phpstan-type Changes array{ * breakingChanges: array<int, Change>, * dangerousChanges: array<int, Change> * } * * @see \GraphQL\Tests\Utils\BreakingChangesFinderTest */ class BreakingChangesFinder { public const BREAKING_CHANGE_FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND'; public const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED'; public const BREAKING_CHANGE_TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND'; public const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED'; public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION'; public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM'; public const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED'; public const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND'; public const BREAKING_CHANGE_REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED'; public const BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED'; public const BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED'; public const BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED'; public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE'; public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM'; public const DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED'; public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED'; public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED'; /** * Given two schemas, returns an Array containing descriptions of all the types * of breaking changes covered by the other functions down below. * * @throws \TypeError * @throws InvariantViolation * * @return array<int, Change> */ public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema): array { return array_merge( self::findRemovedTypes($oldSchema, $newSchema), self::findTypesThatChangedKind($oldSchema, $newSchema), self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema), self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'], self::findTypesRemovedFromUnions($oldSchema, $newSchema), self::findValuesRemovedFromEnums($oldSchema, $newSchema), self::findArgChanges($oldSchema, $newSchema)['breakingChanges'], self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema), self::findRemovedDirectives($oldSchema, $newSchema), self::findRemovedDirectiveArgs($oldSchema, $newSchema), self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema), self::findRemovedDirectiveLocations($oldSchema, $newSchema) ); } /** * Given two schemas, returns an Array containing descriptions of any breaking * changes in the newSchema related to removing an entire type. * * @throws InvariantViolation * * @return array<int, Change> */ public static function findRemovedTypes( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $breakingChanges = []; foreach (array_keys($oldTypeMap) as $typeName) { if (! isset($newTypeMap[$typeName])) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_TYPE_REMOVED, 'description' => "{$typeName} was removed.", ]; } } return $breakingChanges; } /** * Given two schemas, returns an Array containing descriptions of any breaking * changes in the newSchema related to changing the type of a type. * * @throws \TypeError * @throws InvariantViolation * * @return array<int, Change> */ public static function findTypesThatChangedKind( Schema $schemaA, Schema $schemaB ): array { $schemaATypeMap = $schemaA->getTypeMap(); $schemaBTypeMap = $schemaB->getTypeMap(); $breakingChanges = []; foreach ($schemaATypeMap as $typeName => $schemaAType) { if (! isset($schemaBTypeMap[$typeName])) { continue; } $schemaBType = $schemaBTypeMap[$typeName]; if ($schemaAType instanceof $schemaBType) { continue; } if ($schemaBType instanceof $schemaAType) { continue; } $schemaATypeKindName = self::typeKindName($schemaAType); $schemaBTypeKindName = self::typeKindName($schemaBType); $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND, 'description' => "{$typeName} changed from {$schemaATypeKindName} to {$schemaBTypeKindName}.", ]; } return $breakingChanges; } /** * @param Type&NamedType $type * * @throws \TypeError */ private static function typeKindName(NamedType $type): string { if ($type instanceof ScalarType) { return 'a Scalar type'; } if ($type instanceof ObjectType) { return 'an Object type'; } if ($type instanceof InterfaceType) { return 'an Interface type'; } if ($type instanceof UnionType) { return 'a Union type'; } if ($type instanceof EnumType) { return 'an Enum type'; } if ($type instanceof InputObjectType) { return 'an Input type'; } throw new \TypeError('Unknown type: ' . $type->name); } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $breakingChanges = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if ( ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType || ! ($newType instanceof $oldType) ) { continue; } $oldTypeFieldsDef = $oldType->getFields(); $newTypeFieldsDef = $newType->getFields(); foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { // Check if the field is missing on the type in the new schema. if (! isset($newTypeFieldsDef[$fieldName])) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "{$typeName}.{$fieldName} was removed.", ]; } else { $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); $newFieldType = $newTypeFieldsDef[$fieldName]->getType(); $isSafe = self::isChangeSafeForObjectOrInterfaceField( $oldFieldType, $newFieldType ); if (! $isSafe) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => "{$typeName}.{$fieldName} changed type from {$oldFieldType} to {$newFieldType}.", ]; } } } } return $breakingChanges; } private static function isChangeSafeForObjectOrInterfaceField( Type $oldType, Type $newType ): bool { if ($oldType instanceof NamedType) { return // if they're both named types, see if their names are equivalent ($newType instanceof NamedType && $oldType->name === $newType->name) // moving from nullable to non-null of the same underlying type is safe || ($newType instanceof NonNull && self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())); } if ($oldType instanceof ListOfType) { return // if they're both lists, make sure the underlying types are compatible ($newType instanceof ListOfType && self::isChangeSafeForObjectOrInterfaceField( $oldType->getWrappedType(), $newType->getWrappedType() )) // moving from nullable to non-null of the same underlying type is safe || ($newType instanceof NonNull && self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())); } if ($oldType instanceof NonNull) { // if they're both non-null, make sure the underlying types are compatible return $newType instanceof NonNull && self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType()); } return false; } /** * @throws InvariantViolation * * @return Changes */ public static function findFieldsThatChangedTypeOnInputObjectTypes( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $breakingChanges = []; $dangerousChanges = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) { continue; } $oldTypeFieldsDef = $oldType->getFields(); $newTypeFieldsDef = $newType->getFields(); foreach (array_keys($oldTypeFieldsDef) as $fieldName) { if (! isset($newTypeFieldsDef[$fieldName])) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, 'description' => "{$typeName}.{$fieldName} was removed.", ]; } else { $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); $newFieldType = $newTypeFieldsDef[$fieldName]->getType(); $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( $oldFieldType, $newFieldType ); if (! $isSafe) { $oldFieldTypeString = $oldFieldType instanceof NamedType ? $oldFieldType->name : $oldFieldType; $newFieldTypeString = $newFieldType instanceof NamedType ? $newFieldType->name : $newFieldType; $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => "{$typeName}.{$fieldName} changed type from {$oldFieldTypeString} to {$newFieldTypeString}.", ]; } } } // Check if a field was added to the input object type foreach ($newTypeFieldsDef as $fieldName => $fieldDef) { if (isset($oldTypeFieldsDef[$fieldName])) { continue; } $newTypeName = $newType->name; if ($fieldDef->isRequired()) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED, 'description' => "A required field {$fieldName} on input type {$newTypeName} was added.", ]; } else { $dangerousChanges[] = [ 'type' => self::DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED, 'description' => "An optional field {$fieldName} on input type {$newTypeName} was added.", ]; } } } return [ 'breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges, ]; } /** @throws InvariantViolation */ private static function isChangeSafeForInputObjectFieldOrFieldArg( Type $oldType, Type $newType ): bool { if ($oldType instanceof NamedType) { if (! $newType instanceof NamedType) { return false; } // if they're both named types, see if their names are equivalent return $oldType->name === $newType->name; } if ($oldType instanceof ListOfType) { // if they're both lists, make sure the underlying types are compatible return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg( $oldType->getWrappedType(), $newType->getWrappedType() ); } if ($oldType instanceof NonNull) { return // if they're both non-null, make sure the underlying types are compatible ($newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg( $oldType->getWrappedType(), $newType->getWrappedType() )) // moving from non-null to nullable of the same underlying type is safe || ! ($newType instanceof NonNull) && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType); } return false; } /** * Given two schemas, returns an Array containing descriptions of any breaking * changes in the newSchema related to removing types from a union type. * * @throws InvariantViolation * * @return array<int, Change> */ public static function findTypesRemovedFromUnions( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $typesRemovedFromUnion = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) { continue; } $typeNamesInNewUnion = []; foreach ($newType->getTypes() as $type) { $typeNamesInNewUnion[$type->name] = true; } foreach ($oldType->getTypes() as $type) { if (! isset($typeNamesInNewUnion[$type->name])) { $typesRemovedFromUnion[] = [ 'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => "{$type->name} was removed from union type {$typeName}.", ]; } } } return $typesRemovedFromUnion; } /** * Given two schemas, returns an Array containing descriptions of any breaking * changes in the newSchema related to removing values from an enum type. * * @throws InvariantViolation * * @return array<int, Change> */ public static function findValuesRemovedFromEnums( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $valuesRemovedFromEnums = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) { continue; } $valuesInNewEnum = []; foreach ($newType->getValues() as $value) { $valuesInNewEnum[$value->name] = true; } foreach ($oldType->getValues() as $value) { if (! isset($valuesInNewEnum[$value->name])) { $valuesRemovedFromEnums[] = [ 'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => "{$value->name} was removed from enum type {$typeName}.", ]; } } } return $valuesRemovedFromEnums; } /** * Given two schemas, returns an Array containing descriptions of any * breaking or dangerous changes in the newSchema related to arguments * (such as removal or change of type of an argument, or a change in an * argument's default value). * * @throws InvariantViolation * * @return Changes */ public static function findArgChanges( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $breakingChanges = []; $dangerousChanges = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if ( ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType || ! ($newType instanceof $oldType) ) { continue; } $oldTypeFields = $oldType->getFields(); $newTypeFields = $newType->getFields(); foreach ($oldTypeFields as $fieldName => $oldField) { if (! isset($newTypeFields[$fieldName])) { continue; } foreach ($oldField->args as $oldArgDef) { $newArgDef = null; foreach ($newTypeFields[$fieldName]->args as $newArg) { if ($newArg->name === $oldArgDef->name) { $newArgDef = $newArg; } } if ($newArgDef !== null) { $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( $oldArgDef->getType(), $newArgDef->getType() ); $oldArgType = $oldArgDef->getType(); $oldArgName = $oldArgDef->name; if (! $isSafe) { $newArgType = $newArgDef->getType(); $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => "{$typeName}.{$fieldName} arg {$oldArgName} has changed type from {$oldArgType} to {$newArgType}", ]; } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) { $dangerousChanges[] = [ 'type' => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED, 'description' => "{$typeName}.{$fieldName} arg {$oldArgName} has changed defaultValue", ]; } } else { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_ARG_REMOVED, 'description' => "{$typeName}.{$fieldName} arg {$oldArgDef->name} was removed", ]; } // Check if arg was added to the field foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) { $oldArgDef = null; foreach ($oldTypeFields[$fieldName]->args as $oldArg) { if ($oldArg->name === $newTypeFieldArgDef->name) { $oldArgDef = $oldArg; } } if ($oldArgDef !== null) { continue; } $newTypeName = $newType->name; $newArgName = $newTypeFieldArgDef->name; if ($newTypeFieldArgDef->isRequired()) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_REQUIRED_ARG_ADDED, 'description' => "A required arg {$newArgName} on {$newTypeName}.{$fieldName} was added", ]; } else { $dangerousChanges[] = [ 'type' => self::DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED, 'description' => "An optional arg {$newArgName} on {$newTypeName}.{$fieldName} was added", ]; } } } } } return [ 'breakingChanges' => $breakingChanges, 'dangerousChanges' => $dangerousChanges, ]; } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findInterfacesRemovedFromObjectTypes( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $breakingChanges = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if (! ($oldType instanceof ImplementingType) || ! ($newType instanceof ImplementingType)) { continue; } $oldInterfaces = $oldType->getInterfaces(); $newInterfaces = $newType->getInterfaces(); foreach ($oldInterfaces as $oldInterface) { $interfaceWasRemoved = true; foreach ($newInterfaces as $newInterface) { if ($oldInterface->name === $newInterface->name) { $interfaceWasRemoved = false; } } if ($interfaceWasRemoved) { $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => "{$typeName} no longer implements interface {$oldInterface->name}.", ]; } } } return $breakingChanges; } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema): array { $removedDirectives = []; $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema); foreach ($oldSchema->getDirectives() as $directive) { if (! isset($newSchemaDirectiveMap[$directive->name])) { $removedDirectives[] = [ 'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED, 'description' => "{$directive->name} was removed", ]; } } return $removedDirectives; } /** * @throws InvariantViolation * * @return array<string, Directive> */ private static function getDirectiveMapForSchema(Schema $schema): array { $directives = []; foreach ($schema->getDirectives() as $directive) { $directives[$directive->name] = $directive; } return $directives; } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema): array { $removedDirectiveArgs = []; $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); foreach ($newSchema->getDirectives() as $newDirective) { if (! isset($oldSchemaDirectiveMap[$newDirective->name])) { continue; } foreach ( self::findRemovedArgsForDirectives( $oldSchemaDirectiveMap[$newDirective->name], $newDirective ) as $arg ) { $removedDirectiveArgs[] = [ 'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, 'description' => "{$arg->name} was removed from {$newDirective->name}", ]; } } return $removedDirectiveArgs; } /** @return array<int, Argument> */ public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective): array { $removedArgs = []; $newArgMap = self::getArgumentMapForDirective($newDirective); foreach ($oldDirective->args as $arg) { if (! isset($newArgMap[$arg->name])) { $removedArgs[] = $arg; } } return $removedArgs; } /** @return array<string, Argument> */ private static function getArgumentMapForDirective(Directive $directive): array { $args = []; foreach ($directive->args as $arg) { $args[$arg->name] = $arg; } return $args; } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema): array { $addedNonNullableArgs = []; $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); foreach ($newSchema->getDirectives() as $newDirective) { if (! isset($oldSchemaDirectiveMap[$newDirective->name])) { continue; } foreach ( self::findAddedArgsForDirective( $oldSchemaDirectiveMap[$newDirective->name], $newDirective ) as $arg ) { if ($arg->isRequired()) { $addedNonNullableArgs[] = [ 'type' => self::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED, 'description' => "A required arg {$arg->name} on directive {$newDirective->name} was added", ]; } } } return $addedNonNullableArgs; } /** @return array<int, Argument> */ public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective): array { $addedArgs = []; $oldArgMap = self::getArgumentMapForDirective($oldDirective); foreach ($newDirective->args as $arg) { if (! isset($oldArgMap[$arg->name])) { $addedArgs[] = $arg; } } return $addedArgs; } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema): array { $removedLocations = []; $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); foreach ($newSchema->getDirectives() as $newDirective) { if (! isset($oldSchemaDirectiveMap[$newDirective->name])) { continue; } foreach ( self::findRemovedLocationsForDirective( $oldSchemaDirectiveMap[$newDirective->name], $newDirective ) as $location ) { $removedLocations[] = [ 'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, 'description' => "{$location} was removed from {$newDirective->name}", ]; } } return $removedLocations; } /** @return array<int, string> */ public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective): array { $removedLocations = []; $newLocationSet = array_flip($newDirective->locations); foreach ($oldDirective->locations as $oldLocation) { if (! array_key_exists($oldLocation, $newLocationSet)) { $removedLocations[] = $oldLocation; } } return $removedLocations; } /** * Given two schemas, returns an Array containing descriptions of all the types * of potentially dangerous changes covered by the other functions down below. * * @throws InvariantViolation * * @return array<int, Change> */ public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema): array { return array_merge( self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'], self::findValuesAddedToEnums($oldSchema, $newSchema), self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema), self::findTypesAddedToUnions($oldSchema, $newSchema), self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges'] ); } /** * Given two schemas, returns an Array containing descriptions of any dangerous * changes in the newSchema related to adding values to an enum type. * * @throws InvariantViolation * * @return array<int, Change> */ public static function findValuesAddedToEnums( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $valuesAddedToEnums = []; foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) { continue; } $valuesInOldEnum = []; foreach ($oldType->getValues() as $value) { $valuesInOldEnum[$value->name] = true; } foreach ($newType->getValues() as $value) { if (! isset($valuesInOldEnum[$value->name])) { $valuesAddedToEnums[] = [ 'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, 'description' => "{$value->name} was added to enum type {$typeName}.", ]; } } } return $valuesAddedToEnums; } /** * @throws InvariantViolation * * @return array<int, Change> */ public static function findInterfacesAddedToObjectTypes( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $interfacesAddedToObjectTypes = []; foreach ($newTypeMap as $typeName => $newType) { $oldType = $oldTypeMap[$typeName] ?? null; if ( ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType ) { continue; } $oldInterfaces = $oldType->getInterfaces(); $newInterfaces = $newType->getInterfaces(); foreach ($newInterfaces as $newInterface) { $interfaceWasAdded = true; foreach ($oldInterfaces as $oldInterface) { if ($oldInterface->name === $newInterface->name) { $interfaceWasAdded = false; } } if ($interfaceWasAdded) { $interfacesAddedToObjectTypes[] = [ 'type' => self::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => "{$newInterface->name} added to interfaces implemented by {$typeName}.", ]; } } } return $interfacesAddedToObjectTypes; } /** * Given two schemas, returns an Array containing descriptions of any dangerous * changes in the newSchema related to adding types to a union type. * * @throws InvariantViolation * * @return array<int, Change> */ public static function findTypesAddedToUnions( Schema $oldSchema, Schema $newSchema ): array { $oldTypeMap = $oldSchema->getTypeMap(); $newTypeMap = $newSchema->getTypeMap(); $typesAddedToUnion = []; foreach ($newTypeMap as $typeName => $newType) { $oldType = $oldTypeMap[$typeName] ?? null; if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) { continue; } $typeNamesInOldUnion = []; foreach ($oldType->getTypes() as $type) { $typeNamesInOldUnion[$type->name] = true; } foreach ($newType->getTypes() as $type) { if (! isset($typeNamesInOldUnion[$type->name])) { $typesAddedToUnion[] = [ 'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => "{$type->name} was added to union type {$typeName}.", ]; } } } return $typesAddedToUnion; } } graphql/lib/Utils/PairSet.php 0000644 00000002462 15166657211 0012146 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; /** * A way to keep track of pairs of things when the ordering of the pair does * not matter. We do this by maintaining a sort of double adjacency sets. */ class PairSet { /** @var array<string, array<string, bool>> */ private array $data = []; public function has(string $a, string $b, bool $areMutuallyExclusive): bool { $first = $this->data[$a] ?? null; $result = $first !== null && isset($first[$b]) ? $first[$b] : null; if ($result === null) { return false; } // areMutuallyExclusive being false is a superset of being true, // hence if we want to know if this PairSet "has" these two with no // exclusivity, we have to ensure it was added as such. if ($areMutuallyExclusive === false) { return $result === false; } return true; } public function add(string $a, string $b, bool $areMutuallyExclusive): void { $this->pairSetAdd($a, $b, $areMutuallyExclusive); $this->pairSetAdd($b, $a, $areMutuallyExclusive); } private function pairSetAdd(string $a, string $b, bool $areMutuallyExclusive): void { $this->data[$a] ??= []; $this->data[$a][$b] = $areMutuallyExclusive; } } graphql/lib/Utils/TypeComparators.php 0000644 00000006001 15166657211 0013724 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; class TypeComparators { /** Provided two types, return true if the types are equal (invariant). */ public static function isEqualType(Type $typeA, Type $typeB): bool { // Equivalent types are equal. if ($typeA === $typeB) { return true; } // If either type is non-null, the other must also be non-null. if ($typeA instanceof NonNull && $typeB instanceof NonNull) { return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); } // If either type is a list, the other must also be a list. if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); } // Otherwise the types are not equal. return false; } /** * Provided a type and a super type, return true if the first type is either * equal or a subset of the second super type (covariant). * * @throws InvariantViolation */ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType): bool { // Equivalent type is a valid subtype if ($maybeSubType === $superType) { return true; } // If superType is non-null, maybeSubType must also be nullable. if ($superType instanceof NonNull) { if ($maybeSubType instanceof NonNull) { return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); } return false; } if ($maybeSubType instanceof NonNull) { // If superType is nullable, maybeSubType may be non-null. return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType); } // If superType type is a list, maybeSubType type must also be a list. if ($superType instanceof ListOfType) { if ($maybeSubType instanceof ListOfType) { return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); } return false; } if ($maybeSubType instanceof ListOfType) { // If superType is not a list, maybeSubType must also be not a list. return false; } if (Type::isAbstractType($superType)) { // If superType type is an abstract type, maybeSubType type may be a currently // possible object or interface type. return $maybeSubType instanceof ImplementingType && $schema->isSubType($superType, $maybeSubType); } return false; } } graphql/lib/Utils/BuildSchema.php 0000644 00000022727 15166657211 0012765 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Error\SyntaxError; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeExtensionNode; use YOOtheme\GraphQL\Language\Parser; use YOOtheme\GraphQL\Language\Source; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Type\SchemaConfig; use YOOtheme\GraphQL\Validator\DocumentValidator; /** * Build instance of @see \GraphQL\Type\Schema out of schema language definition (string or parsed AST). * * See [schema definition language docs](schema-definition-language.md) for details. * * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder * * @phpstan-type BuildSchemaOptions array{ * assumeValid?: bool, * assumeValidSDL?: bool * } * * - assumeValid: * When building a schema from a GraphQL service's introspection result, it * might be safe to assume the schema is valid. Set to true to assume the * produced schema is valid. * * Default: false * * - assumeValidSDL: * Set to true to assume the SDL is valid. * * Default: false * * @see \GraphQL\Tests\Utils\BuildSchemaTest */ class BuildSchema { private DocumentNode $ast; /** * @var callable|null * * @phpstan-var TypeConfigDecorator|null */ private $typeConfigDecorator; /** * @var callable|null * * @phpstan-var FieldConfigDecorator|null */ private $fieldConfigDecorator; /** * @var array<string, bool> * * @phpstan-var BuildSchemaOptions */ private array $options; /** * @param array<string, bool> $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param BuildSchemaOptions $options */ public function __construct( DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [], ?callable $fieldConfigDecorator = null ) { $this->ast = $ast; $this->typeConfigDecorator = $typeConfigDecorator; $this->options = $options; $this->fieldConfigDecorator = $fieldConfigDecorator; } /** * A helper function to build a GraphQLSchema directly from a source * document. * * @param DocumentNode|Source|string $source * @param array<string, bool> $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * @phpstan-param BuildSchemaOptions $options * * @api * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation * @throws SyntaxError */ public static function build( $source, ?callable $typeConfigDecorator = null, array $options = [], ?callable $fieldConfigDecorator = null ): Schema { $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); return self::buildAST($doc, $typeConfigDecorator, $options, $fieldConfigDecorator); } /** * This takes the AST of a schema from @see \GraphQL\Language\Parser::parse(). * * If no schema definition is provided, then it will look for types named Query and Mutation. * * Given that AST it constructs a @see \GraphQL\Type\Schema. The resulting schema * has no resolve methods, so execution will use default resolvers. * * @param array<string, bool> $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * @phpstan-param BuildSchemaOptions $options * * @api * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation */ public static function buildAST( DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = [], ?callable $fieldConfigDecorator = null ): Schema { return (new self($ast, $typeConfigDecorator, $options, $fieldConfigDecorator))->buildSchema(); } /** * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation */ public function buildSchema(): Schema { if ( ! ($this->options['assumeValid'] ?? false) && ! ($this->options['assumeValidSDL'] ?? false) ) { DocumentValidator::assertValidSDL($this->ast); } $schemaDef = null; /** @var array<string, Node&TypeDefinitionNode> */ $typeDefinitionsMap = []; /** @var array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap */ $typeExtensionsMap = []; /** @var array<int, DirectiveDefinitionNode> $directiveDefs */ $directiveDefs = []; foreach ($this->ast->definitions as $definition) { switch (true) { case $definition instanceof SchemaDefinitionNode: $schemaDef = $definition; break; case $definition instanceof TypeDefinitionNode: $name = $definition->getName()->value; $typeDefinitionsMap[$name] = $definition; break; case $definition instanceof TypeExtensionNode: $name = $definition->getName()->value; $typeExtensionsMap[$name][] = $definition; break; case $definition instanceof DirectiveDefinitionNode: $directiveDefs[] = $definition; break; } } $operationTypes = $schemaDef !== null ? $this->getOperationTypes($schemaDef) : [ 'query' => 'Query', 'mutation' => 'Mutation', 'subscription' => 'Subscription', ]; $definitionBuilder = new ASTDefinitionBuilder( $typeDefinitionsMap, $typeExtensionsMap, // @phpstan-ignore-next-line TODO add union type when available static function (string $typeName): Type { throw self::unknownType($typeName); }, $this->typeConfigDecorator, $this->fieldConfigDecorator ); $directives = array_map( [$definitionBuilder, 'buildDirective'], $directiveDefs ); $directivesByName = []; foreach ($directives as $directive) { $directivesByName[$directive->name][] = $directive; } // If specified directives were not explicitly declared, add them. if (! isset($directivesByName['include'])) { $directives[] = Directive::includeDirective(); } if (! isset($directivesByName['skip'])) { $directives[] = Directive::skipDirective(); } if (! isset($directivesByName['deprecated'])) { $directives[] = Directive::deprecatedDirective(); } if (! isset($directivesByName['oneOf'])) { $directives[] = Directive::oneOfDirective(); } // Note: While this could make early assertions to get the correctly // typed values below, that would throw immediately while type system // validation with validateSchema() will produce more actionable results. return new Schema( (new SchemaConfig()) // @phpstan-ignore-next-line ->setQuery(isset($operationTypes['query']) ? $definitionBuilder->maybeBuildType($operationTypes['query']) : null) // @phpstan-ignore-next-line ->setMutation(isset($operationTypes['mutation']) ? $definitionBuilder->maybeBuildType($operationTypes['mutation']) : null) // @phpstan-ignore-next-line ->setSubscription(isset($operationTypes['subscription']) ? $definitionBuilder->maybeBuildType($operationTypes['subscription']) : null) ->setTypeLoader(static fn (string $name): ?Type => $definitionBuilder->maybeBuildType($name)) ->setDirectives($directives) ->setAstNode($schemaDef) ->setTypes(fn (): array => array_map( static fn (TypeDefinitionNode $def): Type => $definitionBuilder->buildType($def->getName()->value), $typeDefinitionsMap, )) ); } /** @return array<string, string> */ private function getOperationTypes(SchemaDefinitionNode $schemaDef): array { /** @var array<string, string> $operationTypes */ $operationTypes = []; foreach ($schemaDef->operationTypes as $operationType) { $operationTypes[$operationType->operation] = $operationType->type->name->value; } return $operationTypes; } public static function unknownType(string $typeName): Error { return new Error("Unknown type: \"{$typeName}\"."); } } graphql/lib/Utils/SchemaExtender.php 0000644 00000055566 15166657211 0013513 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\SchemaDefinitionNode; use YOOtheme\GraphQL\Language\AST\SchemaExtensionNode; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeExtensionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\CustomScalarType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; use YOOtheme\GraphQL\Type\SchemaConfig; use YOOtheme\GraphQL\Validator\DocumentValidator; /** * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder * @phpstan-import-type UnnamedArgumentConfig from Argument * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * * @see \GraphQL\Tests\Utils\SchemaExtenderTest */ class SchemaExtender { /** @var array<string, Type> */ protected array $extendTypeCache = []; /** @var array<string, array<TypeExtensionNode>> */ protected array $typeExtensionsMap = []; protected ASTDefinitionBuilder $astBuilder; /** * @param array<string, bool> $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @api * * @throws \Exception * @throws InvariantViolation */ public static function extend( Schema $schema, DocumentNode $documentAST, array $options = [], ?callable $typeConfigDecorator = null, ?callable $fieldConfigDecorator = null ): Schema { return (new static())->doExtend($schema, $documentAST, $options, $typeConfigDecorator, $fieldConfigDecorator); } /** * @param array<string, bool> $options * * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation */ protected function doExtend( Schema $schema, DocumentNode $documentAST, array $options = [], ?callable $typeConfigDecorator = null, ?callable $fieldConfigDecorator = null ): Schema { if ( ! ($options['assumeValid'] ?? false) && ! ($options['assumeValidSDL'] ?? false) ) { DocumentValidator::assertValidSDLExtension($documentAST, $schema); } /** @var array<string, Node&TypeDefinitionNode> $typeDefinitionMap */ $typeDefinitionMap = []; /** @var array<int, DirectiveDefinitionNode> $directiveDefinitions */ $directiveDefinitions = []; /** @var SchemaDefinitionNode|null $schemaDef */ $schemaDef = null; /** @var array<int, SchemaExtensionNode> $schemaExtensions */ $schemaExtensions = []; foreach ($documentAST->definitions as $def) { if ($def instanceof SchemaDefinitionNode) { $schemaDef = $def; } elseif ($def instanceof SchemaExtensionNode) { $schemaExtensions[] = $def; } elseif ($def instanceof TypeDefinitionNode) { $name = $def->getName()->value; $typeDefinitionMap[$name] = $def; } elseif ($def instanceof TypeExtensionNode) { $name = $def->getName()->value; $this->typeExtensionsMap[$name][] = $def; } elseif ($def instanceof DirectiveDefinitionNode) { $directiveDefinitions[] = $def; } } if ( $this->typeExtensionsMap === [] && $typeDefinitionMap === [] && $directiveDefinitions === [] && $schemaExtensions === [] && $schemaDef === null ) { return $schema; } $this->astBuilder = new ASTDefinitionBuilder( $typeDefinitionMap, [], // @phpstan-ignore-next-line no idea what is wrong here function (string $typeName) use ($schema): Type { $existingType = $schema->getType($typeName); if ($existingType === null) { throw new InvariantViolation("Unknown type: \"{$typeName}\"."); } return $this->extendNamedType($existingType); }, $typeConfigDecorator, $fieldConfigDecorator ); $this->extendTypeCache = []; $types = []; // Iterate through all types, getting the type definition for each, ensuring // that any type not directly referenced by a field will get created. foreach ($schema->getTypeMap() as $type) { $types[] = $this->extendNamedType($type); } // Do the same with new types. foreach ($typeDefinitionMap as $type) { $types[] = $this->astBuilder->buildType($type); } $operationTypes = [ 'query' => $this->extendMaybeNamedType($schema->getQueryType()), 'mutation' => $this->extendMaybeNamedType($schema->getMutationType()), 'subscription' => $this->extendMaybeNamedType($schema->getSubscriptionType()), ]; if ($schemaDef !== null) { foreach ($schemaDef->operationTypes as $operationType) { $operationTypes[$operationType->operation] = $this->astBuilder->buildType($operationType->type); } } foreach ($schemaExtensions as $schemaExtension) { foreach ($schemaExtension->operationTypes as $operationType) { $operationTypes[$operationType->operation] = $this->astBuilder->buildType($operationType->type); } } $schemaConfig = (new SchemaConfig()) // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors ->setQuery($operationTypes['query']) // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors ->setMutation($operationTypes['mutation']) // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors ->setSubscription($operationTypes['subscription']) ->setTypes($types) ->setDirectives($this->getMergedDirectives($schema, $directiveDefinitions)) ->setAstNode($schema->astNode ?? $schemaDef) ->setExtensionASTNodes([...$schema->extensionASTNodes, ...$schemaExtensions]); return new Schema($schemaConfig); } /** * @param Type&NamedType $type * * @return array<TypeExtensionNode>|null */ protected function extensionASTNodes(NamedType $type): ?array { return [ ...$type->extensionASTNodes ?? [], ...$this->typeExtensionsMap[$type->name] ?? [], ]; } /** * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation */ protected function extendScalarType(ScalarType $type): CustomScalarType { /** @var array<ScalarTypeExtensionNode> $extensionASTNodes */ $extensionASTNodes = $this->extensionASTNodes($type); return new CustomScalarType([ 'name' => $type->name, 'description' => $type->description, 'serialize' => [$type, 'serialize'], 'parseValue' => [$type, 'parseValue'], 'parseLiteral' => [$type, 'parseLiteral'], 'astNode' => $type->astNode, 'extensionASTNodes' => $extensionASTNodes, ]); } /** @throws InvariantViolation */ protected function extendUnionType(UnionType $type): UnionType { /** @var array<UnionTypeExtensionNode> $extensionASTNodes */ $extensionASTNodes = $this->extensionASTNodes($type); return new UnionType([ 'name' => $type->name, 'description' => $type->description, 'types' => fn (): array => $this->extendUnionPossibleTypes($type), 'resolveType' => [$type, 'resolveType'], 'astNode' => $type->astNode, 'extensionASTNodes' => $extensionASTNodes, ]); } /** * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation */ protected function extendEnumType(EnumType $type): EnumType { /** @var array<EnumTypeExtensionNode> $extensionASTNodes */ $extensionASTNodes = $this->extensionASTNodes($type); return new EnumType([ 'name' => $type->name, 'description' => $type->description, 'values' => $this->extendEnumValueMap($type), 'astNode' => $type->astNode, 'extensionASTNodes' => $extensionASTNodes, ]); } /** @throws InvariantViolation */ protected function extendInputObjectType(InputObjectType $type): InputObjectType { /** @var array<InputObjectTypeExtensionNode> $extensionASTNodes */ $extensionASTNodes = $this->extensionASTNodes($type); return new InputObjectType([ 'name' => $type->name, 'description' => $type->description, 'fields' => fn (): array => $this->extendInputFieldMap($type), 'parseValue' => [$type, 'parseValue'], 'astNode' => $type->astNode, 'extensionASTNodes' => $extensionASTNodes, 'isOneOf' => $type->isOneOf, ]); } /** * @throws \Exception * @throws InvariantViolation * * @return array<string, UnnamedInputObjectFieldConfig> */ protected function extendInputFieldMap(InputObjectType $type): array { /** @var array<string, UnnamedInputObjectFieldConfig> $newFieldMap */ $newFieldMap = []; $oldFieldMap = $type->getFields(); foreach ($oldFieldMap as $fieldName => $field) { $extendedType = $this->extendType($field->getType()); $newFieldConfig = [ 'description' => $field->description, 'type' => $extendedType, 'deprecationReason' => $field->deprecationReason, 'astNode' => $field->astNode, ]; if ($field->defaultValueExists()) { $newFieldConfig['defaultValue'] = $field->defaultValue; } $newFieldMap[$fieldName] = $newFieldConfig; } if (isset($this->typeExtensionsMap[$type->name])) { foreach ($this->typeExtensionsMap[$type->name] as $extension) { assert($extension instanceof InputObjectTypeExtensionNode, 'proven by schema validation'); foreach ($extension->fields as $field) { $newFieldMap[$field->name->value] = $this->astBuilder->buildInputField($field); } } } return $newFieldMap; } /** * @throws \Exception * @throws InvariantViolation * * @return array<string, array<string, mixed>> */ protected function extendEnumValueMap(EnumType $type): array { $newValueMap = []; foreach ($type->getValues() as $value) { $newValueMap[$value->name] = [ 'name' => $value->name, 'description' => $value->description, 'value' => $value->value, 'deprecationReason' => $value->deprecationReason, 'astNode' => $value->astNode, ]; } if (isset($this->typeExtensionsMap[$type->name])) { foreach ($this->typeExtensionsMap[$type->name] as $extension) { assert($extension instanceof EnumTypeExtensionNode, 'proven by schema validation'); foreach ($extension->values as $value) { $newValueMap[$value->name->value] = $this->astBuilder->buildEnumValue($value); } } } return $newValueMap; } /** * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation * * @return array<int, ObjectType> */ protected function extendUnionPossibleTypes(UnionType $type): array { $possibleTypes = array_map( [$this, 'extendNamedType'], $type->getTypes() ); if (isset($this->typeExtensionsMap[$type->name])) { foreach ($this->typeExtensionsMap[$type->name] as $extension) { assert($extension instanceof UnionTypeExtensionNode, 'proven by schema validation'); foreach ($extension->types as $namedType) { $possibleTypes[] = $this->astBuilder->buildType($namedType); } } } // @phpstan-ignore-next-line proven by schema validation return $possibleTypes; } /** * @param ObjectType|InterfaceType $type * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation * * @return array<int, InterfaceType> */ protected function extendImplementedInterfaces(ImplementingType $type): array { $interfaces = array_map( [$this, 'extendNamedType'], $type->getInterfaces() ); if (isset($this->typeExtensionsMap[$type->name])) { foreach ($this->typeExtensionsMap[$type->name] as $extension) { assert( $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode, 'proven by schema validation' ); foreach ($extension->interfaces as $namedType) { $interface = $this->astBuilder->buildType($namedType); assert($interface instanceof InterfaceType, 'we know this, but PHP templates cannot express it'); $interfaces[] = $interface; } } } return $interfaces; } /** * @template T of Type * * @param T $typeDef * * @return T */ protected function extendType(Type $typeDef): Type { if ($typeDef instanceof ListOfType) { // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input return Type::listOf($this->extendType($typeDef->getWrappedType())); } if ($typeDef instanceof NonNull) { // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input return Type::nonNull($this->extendType($typeDef->getWrappedType())); } // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input return $this->extendNamedType($typeDef); } /** * @param array<Argument> $args * * @return array<string, UnnamedArgumentConfig> */ protected function extendArgs(array $args): array { $extended = []; foreach ($args as $arg) { $extendedType = $this->extendType($arg->getType()); $def = [ 'type' => $extendedType, 'description' => $arg->description, 'deprecationReason' => $arg->deprecationReason, 'astNode' => $arg->astNode, ]; if ($arg->defaultValueExists()) { $def['defaultValue'] = $arg->defaultValue; } $extended[$arg->name] = $def; } return $extended; } /** * @param InterfaceType|ObjectType $type * * @throws \Exception * @throws Error * @throws InvariantViolation * * @return array<string, array<string, mixed>> */ protected function extendFieldMap(Type $type): array { $newFieldMap = []; $oldFieldMap = $type->getFields(); foreach (array_keys($oldFieldMap) as $fieldName) { $field = $oldFieldMap[$fieldName]; $newFieldMap[$fieldName] = [ 'name' => $fieldName, 'description' => $field->description, 'deprecationReason' => $field->deprecationReason, 'type' => $this->extendType($field->getType()), 'args' => $this->extendArgs($field->args), 'resolve' => $field->resolveFn, 'argsMapper' => $field->argsMapper, 'astNode' => $field->astNode, ]; } if (isset($this->typeExtensionsMap[$type->name])) { foreach ($this->typeExtensionsMap[$type->name] as $extension) { assert( $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode, 'proven by schema validation' ); foreach ($extension->fields as $field) { $newFieldMap[$field->name->value] = $this->astBuilder->buildField($field, $extension); } } } return $newFieldMap; } /** @throws InvariantViolation */ protected function extendObjectType(ObjectType $type): ObjectType { /** @var array<ObjectTypeExtensionNode> $extensionASTNodes */ $extensionASTNodes = $this->extensionASTNodes($type); return new ObjectType([ 'name' => $type->name, 'description' => $type->description, 'interfaces' => fn (): array => $this->extendImplementedInterfaces($type), 'fields' => fn (): array => $this->extendFieldMap($type), 'isTypeOf' => [$type, 'isTypeOf'], 'resolveField' => $type->resolveFieldFn, 'argsMapper' => $type->argsMapper, 'astNode' => $type->astNode, 'extensionASTNodes' => $extensionASTNodes, ]); } /** @throws InvariantViolation */ protected function extendInterfaceType(InterfaceType $type): InterfaceType { /** @var array<InterfaceTypeExtensionNode> $extensionASTNodes */ $extensionASTNodes = $this->extensionASTNodes($type); return new InterfaceType([ 'name' => $type->name, 'description' => $type->description, 'interfaces' => fn (): array => $this->extendImplementedInterfaces($type), 'fields' => fn (): array => $this->extendFieldMap($type), 'resolveType' => [$type, 'resolveType'], 'astNode' => $type->astNode, 'extensionASTNodes' => $extensionASTNodes, ]); } protected function isSpecifiedScalarType(Type $type): bool { return $type instanceof NamedType && ( $type->name === Type::STRING || $type->name === Type::INT || $type->name === Type::FLOAT || $type->name === Type::BOOLEAN || $type->name === Type::ID ); } /** * @template T of Type * * @param T&NamedType $type * * @throws \ReflectionException * @throws InvariantViolation * * @return T&NamedType */ protected function extendNamedType(Type $type): Type { if (Introspection::isIntrospectionType($type) || $this->isSpecifiedScalarType($type)) { return $type; } // @phpstan-ignore-next-line the subtypes line up return $this->extendTypeCache[$type->name] ??= $this->extendNamedTypeWithoutCache($type); } /** @throws \Exception */ protected function extendNamedTypeWithoutCache(Type $type): Type { switch (true) { case $type instanceof ScalarType: return $this->extendScalarType($type); case $type instanceof ObjectType: return $this->extendObjectType($type); case $type instanceof InterfaceType: return $this->extendInterfaceType($type); case $type instanceof UnionType: return $this->extendUnionType($type); case $type instanceof EnumType: return $this->extendEnumType($type); case $type instanceof InputObjectType: return $this->extendInputObjectType($type); default: $unconsideredType = get_class($type); throw new \Exception("Unconsidered type: {$unconsideredType}."); } } /** * @template T of Type * * @param (T&NamedType)|null $type * * @throws \ReflectionException * @throws InvariantViolation * * @return (T&NamedType)|null */ protected function extendMaybeNamedType(?Type $type = null): ?Type { if ($type !== null) { return $this->extendNamedType($type); } return null; } /** * @param array<DirectiveDefinitionNode> $directiveDefinitions * * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation * * @return array<int, Directive> */ protected function getMergedDirectives(Schema $schema, array $directiveDefinitions): array { $directives = array_map( [$this, 'extendDirective'], $schema->getDirectives() ); if ($directives === []) { throw new InvariantViolation('Schema must have default directives.'); } foreach ($directiveDefinitions as $directive) { $directives[] = $this->astBuilder->buildDirective($directive); } return $directives; } protected function extendDirective(Directive $directive): Directive { return new Directive([ 'name' => $directive->name, 'description' => $directive->description, 'locations' => $directive->locations, 'args' => $this->extendArgs($directive->args), 'isRepeatable' => $directive->isRepeatable, 'astNode' => $directive->astNode, ]); } } graphql/lib/Utils/InterfaceImplementations.php 0000644 00000001736 15166657211 0015573 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\ObjectType; /** * A way to track interface implementations. * * Distinguishes between implementations by ObjectTypes and InterfaceTypes. */ class InterfaceImplementations { /** @var array<int, ObjectType> */ private $objects; /** @var array<int, InterfaceType> */ private $interfaces; /** * @param array<int, ObjectType> $objects * @param array<int, InterfaceType> $interfaces */ public function __construct(array $objects, array $interfaces) { $this->objects = $objects; $this->interfaces = $interfaces; } /** @return array<int, ObjectType> */ public function objects(): array { return $this->objects; } /** @return array<int, InterfaceType> */ public function interfaces(): array { return $this->interfaces; } } graphql/lib/Utils/SchemaPrinter.php 0000644 00000044054 15166657211 0013346 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\BlockString; use YOOtheme\GraphQL\Language\Printer; use YOOtheme\GraphQL\Type\Definition\Argument; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\EnumValueDefinition; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\ImplementingType; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; use YOOtheme\GraphQL\Type\Introspection; use YOOtheme\GraphQL\Type\Schema; /** * Prints the contents of a Schema in schema definition language. * * All sorting options sort alphabetically. If not given or `false`, the original schema definition order will be used. * * @phpstan-type Options array{ * sortArguments?: bool, * sortEnumValues?: bool, * sortFields?: bool, * sortInputFields?: bool, * sortTypes?: bool, * } * * @see \GraphQL\Tests\Utils\SchemaPrinterTest */ class SchemaPrinter { /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @api * * @throws \JsonException * @throws Error * @throws InvariantViolation * @throws SerializationError */ public static function doPrint(Schema $schema, array $options = []): string { return static::printFilteredSchema( $schema, static fn (Directive $directive): bool => ! Directive::isSpecifiedDirective($directive), static fn (NamedType $type): bool => ! $type->isBuiltInType(), $options ); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @api * * @throws \JsonException * @throws Error * @throws InvariantViolation * @throws SerializationError */ public static function printIntrospectionSchema(Schema $schema, array $options = []): string { return static::printFilteredSchema( $schema, [Directive::class, 'isSpecifiedDirective'], [Introspection::class, 'isIntrospectionType'], $options ); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws Error * @throws InvariantViolation * @throws SerializationError */ public static function printType(Type $type, array $options = []): string { if ($type instanceof ScalarType) { return static::printScalar($type, $options); } if ($type instanceof ObjectType) { return static::printObject($type, $options); } if ($type instanceof InterfaceType) { return static::printInterface($type, $options); } if ($type instanceof UnionType) { return static::printUnion($type, $options); } if ($type instanceof EnumType) { return static::printEnum($type, $options); } if ($type instanceof InputObjectType) { return static::printInputObject($type, $options); } $unknownType = Utils::printSafe($type); throw new Error("Unknown type: {$unknownType}."); } /** * @param callable(Directive $directive): bool $directiveFilter * @param callable(Type&NamedType $type): bool $typeFilter * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws Error * @throws InvariantViolation * @throws SerializationError */ protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string { $directives = array_filter($schema->getDirectives(), $directiveFilter); $types = array_filter($schema->getTypeMap(), $typeFilter); if (isset($options['sortTypes']) && $options['sortTypes']) { ksort($types); } $elements = [static::printSchemaDefinition($schema)]; foreach ($directives as $directive) { $elements[] = static::printDirective($directive, $options); } foreach ($types as $type) { $elements[] = static::printType($type, $options); } /** @phpstan-ignore arrayFilter.strict */ return implode("\n\n", array_filter($elements)) . "\n"; } /** @throws InvariantViolation */ protected static function printSchemaDefinition(Schema $schema): ?string { $queryType = $schema->getQueryType(); $mutationType = $schema->getMutationType(); $subscriptionType = $schema->getSubscriptionType(); // Special case: When a schema has no root operation types, no valid schema // definition can be printed. if ($queryType === null && $mutationType === null && $subscriptionType === null) { return null; } // TODO add condition for schema.description // Only print a schema definition if there is a description or if it should // not be omitted because of having default type names. if (! static::hasDefaultRootOperationTypes($schema)) { return "schema {\n" . ($queryType !== null ? " query: {$queryType->name}\n" : '') . ($mutationType !== null ? " mutation: {$mutationType->name}\n" : '') . ($subscriptionType !== null ? " subscription: {$subscriptionType->name}\n" : '') . '}'; } return null; } /** * GraphQL schema define root types for each type of operation. These types are * the same as any other type and can be named in any manner, however there is * a common naming convention:. * * ```graphql * schema { * query: Query * mutation: Mutation * subscription: Subscription * } * ``` * * When using this naming convention, the schema description can be omitted. * When using this naming convention, the schema description can be omitted so * long as these names are only used for operation types. * * Note however that if any of these default names are used elsewhere in the * schema but not as a root operation type, the schema definition must still * be printed to avoid ambiguity. * * @throws InvariantViolation */ protected static function hasDefaultRootOperationTypes(Schema $schema): bool { return $schema->getQueryType() === $schema->getType('Query') && $schema->getMutationType() === $schema->getType('Mutation') && $schema->getSubscriptionType() === $schema->getType('Subscription'); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printDirective(Directive $directive, array $options): string { return static::printDescription($options, $directive) . 'directive @' . $directive->name . static::printArgs($options, $directive->args) . ($directive->isRepeatable ? ' repeatable' : '') . ' on ' . implode(' | ', $directive->locations); } /** * @param array<string, bool> $options * @param (Type&NamedType)|Directive|EnumValueDefinition|Argument|FieldDefinition|InputObjectField $def * * @throws \JsonException */ protected static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string { $description = $def->description; if ($description === null) { return ''; } $prefix = $indentation !== '' && ! $firstInBlock ? "\n{$indentation}" : $indentation; if (count(Utils::splitLines($description)) === 1) { $description = json_encode($description, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE); } else { $description = BlockString::print($description); $description = $indentation !== '' ? str_replace("\n", "\n{$indentation}", $description) : $description; } return "{$prefix}{$description}\n"; } /** * @param array<string, bool> $options * @param array<int, Argument> $args * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printArgs(array $options, array $args, string $indentation = ''): string { if ($args === []) { return ''; } if (isset($options['sortArguments']) && $options['sortArguments']) { usort($args, static fn (Argument $left, Argument $right): int => $left->name <=> $right->name); } $allArgsWithoutDescription = true; foreach ($args as $arg) { $description = $arg->description; if ($description !== null && $description !== '') { $allArgsWithoutDescription = false; break; } } if ($allArgsWithoutDescription) { return '(' . implode( ', ', array_map( [static::class, 'printInputValue'], $args ) ) . ')'; } $argsStrings = []; $firstInBlock = true; $previousHasDescription = false; foreach ($args as $arg) { $hasDescription = $arg->description !== null; if ($previousHasDescription && ! $hasDescription) { $argsStrings[] = ''; } $argsStrings[] = static::printDescription($options, $arg, ' ' . $indentation, $firstInBlock) . ' ' . $indentation . static::printInputValue($arg); $firstInBlock = false; $previousHasDescription = $hasDescription; } return "(\n" . implode("\n", $argsStrings) . "\n" . $indentation . ')'; } /** * @param InputObjectField|Argument $arg * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printInputValue($arg): string { $argDecl = "{$arg->name}: {$arg->getType()->toString()}"; if ($arg->defaultValueExists()) { $defaultValueAST = AST::astFromValue($arg->defaultValue, $arg->getType()); if ($defaultValueAST === null) { $inconvertibleDefaultValue = Utils::printSafe($arg->defaultValue); throw new InvariantViolation("Unable to convert defaultValue of argument {$arg->name} into AST: {$inconvertibleDefaultValue}."); } $printedDefaultValue = Printer::doPrint($defaultValueAST); $argDecl .= " = {$printedDefaultValue}"; } return $argDecl . static::printDeprecated($arg); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException */ protected static function printScalar(ScalarType $type, array $options): string { return static::printDescription($options, $type) . "scalar {$type->name}"; } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printObject(ObjectType $type, array $options): string { return static::printDescription($options, $type) . "type {$type->name}" . static::printImplementedInterfaces($type) . static::printFields($options, $type); } /** * @param array<string, bool> $options * @param ObjectType|InterfaceType $type * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printFields(array $options, $type): string { $fields = []; $firstInBlock = true; $previousHasDescription = false; $fieldDefinitions = $type->getFields(); if (isset($options['sortFields']) && $options['sortFields']) { ksort($fieldDefinitions); } foreach ($fieldDefinitions as $f) { $hasDescription = $f->description !== null; if ($previousHasDescription && ! $hasDescription) { $fields[] = ''; } $fields[] = static::printDescription($options, $f, ' ', $firstInBlock) . ' ' . $f->name . static::printArgs($options, $f->args, ' ') . ': ' . $f->getType()->toString() . static::printDeprecated($f); $firstInBlock = false; $previousHasDescription = $hasDescription; } return static::printBlock($fields); } /** * @param FieldDefinition|EnumValueDefinition|InputObjectField|Argument $deprecation * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printDeprecated($deprecation): string { $reason = $deprecation->deprecationReason; if ($reason === null) { return ''; } if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) { return ' @deprecated'; } $reasonAST = AST::astFromValue($reason, Type::string()); assert($reasonAST instanceof StringValueNode); $reasonASTString = Printer::doPrint($reasonAST); return " @deprecated(reason: {$reasonASTString})"; } protected static function printImplementedInterfaces(ImplementingType $type): string { $interfaces = $type->getInterfaces(); return $interfaces === [] ? '' : ' implements ' . implode( ' & ', array_map( static fn (InterfaceType $interface): string => $interface->name, $interfaces ) ); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printInterface(InterfaceType $type, array $options): string { return static::printDescription($options, $type) . "interface {$type->name}" . static::printImplementedInterfaces($type) . static::printFields($options, $type); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation */ protected static function printUnion(UnionType $type, array $options): string { $types = $type->getTypes(); $types = $types === [] ? '' : ' = ' . implode(' | ', $types); return static::printDescription($options, $type) . 'union ' . $type->name . $types; } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printEnum(EnumType $type, array $options): string { $values = []; $firstInBlock = true; $valueDefinitions = $type->getValues(); if (isset($options['sortEnumValues']) && $options['sortEnumValues']) { usort($valueDefinitions, static fn (EnumValueDefinition $left, EnumValueDefinition $right): int => $left->name <=> $right->name); } foreach ($valueDefinitions as $value) { $values[] = static::printDescription($options, $value, ' ', $firstInBlock) . ' ' . $value->name . static::printDeprecated($value); $firstInBlock = false; } return static::printDescription($options, $type) . "enum {$type->name}" . static::printBlock($values); } /** * @param array<string, bool> $options * * @phpstan-param Options $options * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError */ protected static function printInputObject(InputObjectType $type, array $options): string { $fields = []; $firstInBlock = true; $fieldDefinitions = $type->getFields(); if (isset($options['sortInputFields']) && $options['sortInputFields']) { ksort($fieldDefinitions); } foreach ($fieldDefinitions as $field) { $fields[] = static::printDescription($options, $field, ' ', $firstInBlock) . ' ' . static::printInputValue($field); $firstInBlock = false; } return static::printDescription($options, $type) . "input {$type->name}" . ($type->isOneOf() ? ' @oneOf' : '') . static::printBlock($fields); } /** @param array<string> $items */ protected static function printBlock(array $items): string { return $items === [] ? '' : " {\n" . implode("\n", $items) . "\n}"; } } graphql/lib/Utils/LazyException.php 0000644 00000000562 15166657211 0013374 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; /** * Allows lazy calculation of a complex message when the exception is used in `assert()`. */ class LazyException extends \Exception { /** @param callable(): string $makeMessage */ public function __construct(callable $makeMessage) { parent::__construct($makeMessage()); } } graphql/lib/Utils/ASTDefinitionBuilder.php 0000644 00000055552 15166657211 0014556 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Executor\Values; use YOOtheme\GraphQL\Language\AST\DirectiveDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\EnumTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\EnumValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\FieldDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InputObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\InputValueDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\InterfaceTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ListTypeNode; use YOOtheme\GraphQL\Language\AST\NamedTypeNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\NonNullTypeNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ObjectTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\ScalarTypeExtensionNode; use YOOtheme\GraphQL\Language\AST\TypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\TypeExtensionNode; use YOOtheme\GraphQL\Language\AST\TypeNode; use YOOtheme\GraphQL\Language\AST\UnionTypeDefinitionNode; use YOOtheme\GraphQL\Language\AST\UnionTypeExtensionNode; use YOOtheme\GraphQL\Type\Definition\CustomScalarType; use YOOtheme\GraphQL\Type\Definition\Directive; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\FieldDefinition; use YOOtheme\GraphQL\Type\Definition\InputObjectField; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InputType; use YOOtheme\GraphQL\Type\Definition\InterfaceType; use YOOtheme\GraphQL\Type\Definition\NamedType; use YOOtheme\GraphQL\Type\Definition\ObjectType; use YOOtheme\GraphQL\Type\Definition\OutputType; use YOOtheme\GraphQL\Type\Definition\Type; use YOOtheme\GraphQL\Type\Definition\UnionType; /** * @see FieldDefinition, InputObjectField * * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition * @phpstan-import-type InputObjectFieldConfig from InputObjectField * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField * * @phpstan-type ResolveType callable(string, Node|null): Type&NamedType * @phpstan-type TypeConfigDecorator callable(array<string, mixed>, Node&TypeDefinitionNode, array<string, Node&TypeDefinitionNode>): array<string, mixed> * @phpstan-type FieldConfigDecorator callable(UnnamedFieldDefinitionConfig, FieldDefinitionNode, ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode): UnnamedFieldDefinitionConfig */ class ASTDefinitionBuilder { /** @var array<string, Node&TypeDefinitionNode> */ private array $typeDefinitionsMap; /** * @var callable * * @phpstan-var ResolveType */ private $resolveType; /** * @var callable|null * * @phpstan-var TypeConfigDecorator|null */ private $typeConfigDecorator; /** * @var callable|null * * @phpstan-var FieldConfigDecorator|null */ private $fieldConfigDecorator; /** @var array<string, Type&NamedType> */ private array $cache; /** @var array<string, array<int, Node&TypeExtensionNode>> */ private array $typeExtensionsMap; /** * @param array<string, Node&TypeDefinitionNode> $typeDefinitionsMap * @param array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap * * @phpstan-param ResolveType $resolveType * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator * * @throws InvariantViolation */ public function __construct( array $typeDefinitionsMap, array $typeExtensionsMap, callable $resolveType, ?callable $typeConfigDecorator = null, ?callable $fieldConfigDecorator = null ) { $this->typeDefinitionsMap = $typeDefinitionsMap; $this->typeExtensionsMap = $typeExtensionsMap; $this->resolveType = $resolveType; $this->typeConfigDecorator = $typeConfigDecorator; $this->fieldConfigDecorator = $fieldConfigDecorator; $this->cache = Type::builtInTypes(); } /** @throws \Exception */ public function buildDirective(DirectiveDefinitionNode $directiveNode): Directive { $locations = []; foreach ($directiveNode->locations as $location) { $locations[] = $location->value; } return new Directive([ 'name' => $directiveNode->name->value, 'description' => $directiveNode->description->value ?? null, 'args' => $this->makeInputValues($directiveNode->arguments), 'isRepeatable' => $directiveNode->repeatable, 'locations' => $locations, 'astNode' => $directiveNode, ]); } /** * @param NodeList<InputValueDefinitionNode> $values * * @throws \Exception * * @return array<string, UnnamedInputObjectFieldConfig> */ private function makeInputValues(NodeList $values): array { /** @var array<string, UnnamedInputObjectFieldConfig> $map */ $map = []; foreach ($values as $value) { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. /** @var Type&InputType $type */ $type = $this->buildWrappedType($value->type); $config = [ 'name' => $value->name->value, 'type' => $type, 'description' => $value->description->value ?? null, 'deprecationReason' => $this->getDeprecationReason($value), 'astNode' => $value, ]; if ($value->defaultValue !== null) { $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); } $map[$value->name->value] = $config; } return $map; } /** * @param array<InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode> $nodes * * @throws \Exception * * @return array<string, UnnamedInputObjectFieldConfig> */ private function makeInputFields(array $nodes): array { /** @var array<int, InputValueDefinitionNode> $fields */ $fields = []; foreach ($nodes as $node) { array_push($fields, ...$node->fields); } return $this->makeInputValues(new NodeList($fields)); } /** * @param ListTypeNode|NonNullTypeNode|NamedTypeNode $typeNode * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation */ private function buildWrappedType(TypeNode $typeNode): Type { if ($typeNode instanceof ListTypeNode) { return Type::listOf($this->buildWrappedType($typeNode->type)); } if ($typeNode instanceof NonNullTypeNode) { // @phpstan-ignore-next-line contained type is NullableType return Type::nonNull($this->buildWrappedType($typeNode->type)); } return $this->buildType($typeNode); } /** * @param string|(Node&NamedTypeNode)|(Node&TypeDefinitionNode) $ref * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation * * @return Type&NamedType */ public function buildType($ref): Type { if ($ref instanceof TypeDefinitionNode) { return $this->internalBuildType($ref->getName()->value, $ref); } if ($ref instanceof NamedTypeNode) { return $this->internalBuildType($ref->name->value, $ref); } return $this->internalBuildType($ref); } /** * Calling this method is an equivalent of `typeMap[typeName]` in `graphql-js`. * It is legal to access a type from the map of already-built types that doesn't exist in the map. * Since we build types lazily, and we don't have a such map of built types, * this method provides a way to build a type that may not exist in the SDL definitions and returns null instead. * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation * * @return (Type&NamedType)|null */ public function maybeBuildType(string $name): ?Type { return isset($this->typeDefinitionsMap[$name]) ? $this->buildType($name) : null; } /** * @param (Node&NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode * * @throws \Exception * @throws \ReflectionException * @throws Error * @throws InvariantViolation * * @return Type&NamedType */ private function internalBuildType(string $typeName, ?Node $typeNode = null): Type { if (isset($this->cache[$typeName])) { return $this->cache[$typeName]; } if (isset($this->typeDefinitionsMap[$typeName])) { $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]); if ($this->typeConfigDecorator !== null) { try { $config = ($this->typeConfigDecorator)( $type->config, $this->typeDefinitionsMap[$typeName], $this->typeDefinitionsMap ); } catch (\Throwable $e) { $class = static::class; throw new Error("Type config decorator passed to {$class} threw an error when building {$typeName} type: {$e->getMessage()}", null, null, [], null, $e); } // @phpstan-ignore-next-line should not happen, but function types are not enforced by PHP if (! is_array($config) || isset($config[0])) { $class = static::class; $notArray = Utils::printSafe($config); throw new Error("Type config decorator passed to {$class} is expected to return an array, but got {$notArray}"); } $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config); } return $this->cache[$typeName] = $type; } return $this->cache[$typeName] = ($this->resolveType)($typeName, $typeNode); } /** * @param TypeDefinitionNode&Node $def * * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation * * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType */ private function makeSchemaDef(Node $def): Type { switch (true) { case $def instanceof ObjectTypeDefinitionNode: return $this->makeTypeDef($def); case $def instanceof InterfaceTypeDefinitionNode: return $this->makeInterfaceDef($def); case $def instanceof EnumTypeDefinitionNode: return $this->makeEnumDef($def); case $def instanceof UnionTypeDefinitionNode: return $this->makeUnionDef($def); case $def instanceof ScalarTypeDefinitionNode: return $this->makeScalarDef($def); default: assert($def instanceof InputObjectTypeDefinitionNode, 'all implementations are known'); return $this->makeInputObjectDef($def); } } /** @throws InvariantViolation */ private function makeTypeDef(ObjectTypeDefinitionNode $def): ObjectType { $name = $def->name->value; /** @var array<ObjectTypeExtensionNode> $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; $allNodes = [$def, ...$extensionASTNodes]; return new ObjectType([ 'name' => $name, 'description' => $def->description->value ?? null, 'fields' => fn (): array => $this->makeFieldDefMap($allNodes), 'interfaces' => fn (): array => $this->makeImplementedInterfaces($allNodes), 'astNode' => $def, 'extensionASTNodes' => $extensionASTNodes, ]); } /** * @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes * * @throws \Exception * * @phpstan-return array<string, UnnamedFieldDefinitionConfig> */ private function makeFieldDefMap(array $nodes): array { $map = []; foreach ($nodes as $node) { foreach ($node->fields as $field) { $map[$field->name->value] = $this->buildField($field, $node); } } return $map; } /** * @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode $node * * @throws \Exception * @throws Error * * @return UnnamedFieldDefinitionConfig */ public function buildField(FieldDefinitionNode $field, object $node): array { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. /** @var OutputType&Type $type */ $type = $this->buildWrappedType($field->type); $config = [ 'type' => $type, 'description' => $field->description->value ?? null, 'args' => $this->makeInputValues($field->arguments), 'deprecationReason' => $this->getDeprecationReason($field), 'astNode' => $field, ]; if ($this->fieldConfigDecorator !== null) { $config = ($this->fieldConfigDecorator)($config, $field, $node); } return $config; } /** * Given a collection of directives, returns the string value for the * deprecation reason. * * @param EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node * * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation */ private function getDeprecationReason(Node $node): ?string { $deprecated = Values::getDirectiveValues( Directive::deprecatedDirective(), $node ); return $deprecated['reason'] ?? null; } /** * @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes * * @throws \Exception * @throws Error * @throws InvariantViolation * * @return array<int, InterfaceType> */ private function makeImplementedInterfaces(array $nodes): array { // Note: While this could make early assertions to get the correctly // typed values, that would throw immediately while type system // validation with validateSchema() will produce more actionable results. $interfaces = []; foreach ($nodes as $node) { foreach ($node->interfaces as $interface) { $interfaces[] = $this->buildType($interface); } } // @phpstan-ignore-next-line generic type will be validated during schema validation return $interfaces; } /** @throws InvariantViolation */ private function makeInterfaceDef(InterfaceTypeDefinitionNode $def): InterfaceType { $name = $def->name->value; /** @var array<InterfaceTypeExtensionNode> $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; $allNodes = [$def, ...$extensionASTNodes]; return new InterfaceType([ 'name' => $name, 'description' => $def->description->value ?? null, 'fields' => fn (): array => $this->makeFieldDefMap($allNodes), 'interfaces' => fn (): array => $this->makeImplementedInterfaces($allNodes), 'astNode' => $def, 'extensionASTNodes' => $extensionASTNodes, ]); } /** * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation */ private function makeEnumDef(EnumTypeDefinitionNode $def): EnumType { $name = $def->name->value; /** @var array<EnumTypeExtensionNode> $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; $values = []; foreach ([$def, ...$extensionASTNodes] as $node) { foreach ($node->values as $value) { $values[$value->name->value] = [ 'description' => $value->description->value ?? null, 'deprecationReason' => $this->getDeprecationReason($value), 'astNode' => $value, ]; } } return new EnumType([ 'name' => $name, 'description' => $def->description->value ?? null, 'values' => $values, 'astNode' => $def, 'extensionASTNodes' => $extensionASTNodes, ]); } /** @throws InvariantViolation */ private function makeUnionDef(UnionTypeDefinitionNode $def): UnionType { $name = $def->name->value; /** @var array<UnionTypeExtensionNode> $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; return new UnionType([ 'name' => $name, 'description' => $def->description->value ?? null, // Note: While this could make assertions to get the correctly typed // values below, that would throw immediately while type system // validation with validateSchema() will produce more actionable results. 'types' => function () use ($def, $extensionASTNodes): array { $types = []; foreach ([$def, ...$extensionASTNodes] as $node) { foreach ($node->types as $type) { $types[] = $this->buildType($type); } } /** @var array<int, ObjectType> $types */ return $types; }, 'astNode' => $def, 'extensionASTNodes' => $extensionASTNodes, ]); } /** @throws InvariantViolation */ private function makeScalarDef(ScalarTypeDefinitionNode $def): CustomScalarType { $name = $def->name->value; /** @var array<ScalarTypeExtensionNode> $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; return new CustomScalarType([ 'name' => $name, 'description' => $def->description->value ?? null, 'serialize' => static fn ($value) => $value, 'astNode' => $def, 'extensionASTNodes' => $extensionASTNodes, ]); } /** * @throws \Exception * @throws \ReflectionException * @throws InvariantViolation */ private function makeInputObjectDef(InputObjectTypeDefinitionNode $def): InputObjectType { $name = $def->name->value; /** @var array<InputObjectTypeExtensionNode> $extensionASTNodes (proven by schema validation) */ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? []; $oneOfDirective = Directive::oneOfDirective(); // Check for @oneOf directive in the definition node $isOneOf = Values::getDirectiveValues($oneOfDirective, $def) !== null; // Check for @oneOf directive in extension nodes if (! $isOneOf) { foreach ($extensionASTNodes as $extensionNode) { if (Values::getDirectiveValues($oneOfDirective, $extensionNode) !== null) { $isOneOf = true; break; } } } return new InputObjectType([ 'name' => $name, 'description' => $def->description->value ?? null, 'isOneOf' => $isOneOf, 'fields' => fn (): array => $this->makeInputFields([$def, ...$extensionASTNodes]), 'astNode' => $def, 'extensionASTNodes' => $extensionASTNodes, ]); } /** * @param array<string, mixed> $config * * @throws Error * * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType */ private function makeSchemaDefFromConfig(Node $def, array $config): Type { switch (true) { case $def instanceof ObjectTypeDefinitionNode: // @phpstan-ignore-next-line assume the config matches return new ObjectType($config); case $def instanceof InterfaceTypeDefinitionNode: // @phpstan-ignore-next-line assume the config matches return new InterfaceType($config); case $def instanceof EnumTypeDefinitionNode: // @phpstan-ignore-next-line assume the config matches return new EnumType($config); case $def instanceof UnionTypeDefinitionNode: // @phpstan-ignore-next-line assume the config matches return new UnionType($config); case $def instanceof ScalarTypeDefinitionNode: // @phpstan-ignore-next-line assume the config matches return new CustomScalarType($config); case $def instanceof InputObjectTypeDefinitionNode: // @phpstan-ignore-next-line assume the config matches return new InputObjectType($config); default: throw new Error("Type kind of {$def->kind} not supported."); } } /** * @throws \Exception * * @return InputObjectFieldConfig */ public function buildInputField(InputValueDefinitionNode $value): array { $type = $this->buildWrappedType($value->type); assert($type instanceof InputType, 'proven by schema validation'); $config = [ 'name' => $value->name->value, 'type' => $type, 'description' => $value->description->value ?? null, 'astNode' => $value, ]; if ($value->defaultValue !== null) { $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); } return $config; } /** * @throws \Exception * * @return array<string, mixed> */ public function buildEnumValue(EnumValueDefinitionNode $value): array { return [ 'description' => $value->description->value ?? null, 'deprecationReason' => $this->getDeprecationReason($value), 'astNode' => $value, ]; } } graphql/lib/Utils/Value.php 0000644 00000021147 15166657211 0011654 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\ClientAware; use YOOtheme\GraphQL\Error\CoercionError; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InputType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; /** * @phpstan-type CoercedValue array{errors: null, value: mixed} * @phpstan-type CoercedErrors array{errors: array<int, CoercionError>, value: null} * * @phpstan-import-type InputPath from CoercionError */ class Value { /** * Coerce the given value to match the given GraphQL Input Type. * * Returns either a value which is valid for the provided type, * or a list of encountered coercion errors. * * @param mixed $value * @param InputType&Type $type * * @phpstan-param InputPath|null $path * * @throws InvariantViolation * * @phpstan-return CoercedValue|CoercedErrors */ public static function coerceInputValue($value, InputType $type, ?array $path = null): array { if ($type instanceof NonNull) { if ($value === null) { return self::ofErrors([ CoercionError::make("Expected non-nullable type \"{$type}\" not to be null.", $path, $value), ]); } // @phpstan-ignore-next-line wrapped type is known to be input type after schema validation return self::coerceInputValue($value, $type->getWrappedType(), $path); } if ($value === null) { // Explicitly return the value null. return self::ofValue(null); } if ($type instanceof ScalarType || $type instanceof EnumType) { // Scalars and Enums determine if a input value is valid via parseValue(), which can // throw to indicate failure. If it throws, maintain a reference to // the original error. try { return self::ofValue($type->parseValue($value)); } catch (\Throwable $error) { if ( $error instanceof Error || ($error instanceof ClientAware && $error->isClientSafe()) ) { return self::ofErrors([ CoercionError::make($error->getMessage(), $path, $value, $error), ]); } return self::ofErrors([ CoercionError::make("Expected type \"{$type->name}\".", $path, $value, $error), ]); } } if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); assert($itemType instanceof InputType, 'known through schema validation'); if (is_iterable($value)) { $errors = []; $coercedValue = []; foreach ($value as $index => $itemValue) { $coercedItem = self::coerceInputValue( $itemValue, $itemType, [...$path ?? [], $index] ); if (isset($coercedItem['errors'])) { $errors = self::add($errors, $coercedItem['errors']); } else { $coercedValue[] = $coercedItem['value']; } } return $errors === [] ? self::ofValue($coercedValue) : self::ofErrors($errors); } // Lists accept a non-list value as a list of one. $coercedItem = self::coerceInputValue($value, $itemType); return isset($coercedItem['errors']) ? $coercedItem : self::ofValue([$coercedItem['value']]); } assert($type instanceof InputObjectType, 'we handled all other cases at this point'); if ($value instanceof \stdClass) { // Cast objects to associative array before checking the fields. // Note that the coerced value will be an array. $value = (array) $value; } elseif (! is_array($value)) { return self::ofErrors([ CoercionError::make("Expected type \"{$type->name}\" to be an object.", $path, $value), ]); } $errors = []; $coercedValue = []; $fields = $type->getFields(); foreach ($fields as $fieldName => $field) { if (array_key_exists($fieldName, $value)) { $fieldValue = $value[$fieldName]; $coercedField = self::coerceInputValue( $fieldValue, $field->getType(), [...$path ?? [], $fieldName], ); if (isset($coercedField['errors'])) { $errors = self::add($errors, $coercedField['errors']); } else { $coercedValue[$fieldName] = $coercedField['value']; } } elseif ($field->defaultValueExists()) { $coercedValue[$fieldName] = $field->defaultValue; } elseif ($field->getType() instanceof NonNull) { $errors = self::add( $errors, CoercionError::make("Field \"{$fieldName}\" of required type \"{$field->getType()->toString()}\" was not provided.", $path, $value) ); } } // Ensure every provided field is defined. foreach ($value as $fieldName => $field) { if (array_key_exists($fieldName, $fields)) { continue; } $suggestions = Utils::suggestionList( (string) $fieldName, array_keys($fields) ); $message = "Field \"{$fieldName}\" is not defined by type \"{$type->name}\"." . ($suggestions === [] ? '' : ' Did you mean ' . Utils::quotedOrList($suggestions) . '?'); $errors = self::add( $errors, CoercionError::make($message, $path, $value) ); } // Validate OneOf constraints if this is a OneOf input type if ($type->isOneOf()) { $providedFieldCount = 0; $nullFieldName = null; foreach ($coercedValue as $fieldName => $fieldValue) { if ($fieldValue !== null) { ++$providedFieldCount; } else { $nullFieldName = $fieldName; } } // Check for null field values first (takes precedence) if ($nullFieldName !== null) { $errors = self::add( $errors, CoercionError::make("OneOf input object \"{$type->name}\" field \"{$nullFieldName}\" must be non-null.", $path, $value) ); } elseif ($providedFieldCount === 0) { $errors = self::add( $errors, CoercionError::make("OneOf input object \"{$type->name}\" must specify exactly one field.", $path, $value) ); } elseif ($providedFieldCount > 1) { $errors = self::add( $errors, CoercionError::make("OneOf input object \"{$type->name}\" must specify exactly one field.", $path, $value) ); } } return $errors === [] ? self::ofValue($type->parseValue($coercedValue)) : self::ofErrors($errors); } /** * @param array<int, CoercionError> $errors * * @phpstan-return CoercedErrors */ private static function ofErrors(array $errors): array { return ['errors' => $errors, 'value' => null]; } /** * @param mixed $value any value * * @phpstan-return CoercedValue */ private static function ofValue($value): array { return ['errors' => null, 'value' => $value]; } /** * @param array<int, CoercionError> $errors * @param CoercionError|array<int, CoercionError> $errorOrErrors * * @return array<int, CoercionError> */ private static function add(array $errors, $errorOrErrors): array { $moreErrors = is_array($errorOrErrors) ? $errorOrErrors : [$errorOrErrors]; return array_merge($errors, $moreErrors); } } graphql/lib/Utils/MixedStore.php 0000644 00000013333 15166657211 0012661 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; /** * Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars). * * When storing array as key, access and modification is O(N). Avoid if possible. * * @template TValue of mixed * * @implements \ArrayAccess<mixed, TValue> * * @see \GraphQL\Tests\Utils\MixedStoreTest */ class MixedStore implements \ArrayAccess { /** @var array<TValue> */ private array $standardStore = []; /** @var array<TValue> */ private array $floatStore = []; /** @var \SplObjectStorage<object, TValue> */ private \SplObjectStorage $objectStore; /** @var array<int, array<mixed>> */ private array $arrayKeys = []; /** @var array<int, TValue> */ private array $arrayValues = []; /** @var array<mixed> */ private ?array $lastArrayKey = null; /** @var TValue|null */ private $lastArrayValue; /** @var TValue|null */ private $nullValue; private bool $nullValueIsSet = false; /** @var TValue|null */ private $trueValue; private bool $trueValueIsSet = false; /** @var TValue|null */ private $falseValue; private bool $falseValueIsSet = false; public function __construct() { $this->objectStore = new \SplObjectStorage(); } /** @param mixed $offset */ #[\ReturnTypeWillChange] public function offsetExists($offset): bool { if ($offset === false) { return $this->falseValueIsSet; } if ($offset === true) { return $this->trueValueIsSet; } if (is_int($offset) || is_string($offset)) { return array_key_exists($offset, $this->standardStore); } if (is_float($offset)) { return array_key_exists((string) $offset, $this->floatStore); } if (is_object($offset)) { return $this->objectStore->offsetExists($offset); } if (is_array($offset)) { foreach ($this->arrayKeys as $index => $entry) { if ($entry === $offset) { $this->lastArrayKey = $offset; $this->lastArrayValue = $this->arrayValues[$index]; return true; } } } if ($offset === null) { return $this->nullValueIsSet; } return false; } /** * @param mixed $offset * * @return TValue|null */ #[\ReturnTypeWillChange] public function offsetGet($offset) { if ($offset === true) { return $this->trueValue; } if ($offset === false) { return $this->falseValue; } if (is_int($offset) || is_string($offset)) { return $this->standardStore[$offset]; } if (is_float($offset)) { return $this->floatStore[(string) $offset]; } if (is_object($offset)) { return $this->objectStore->offsetGet($offset); } if (is_array($offset)) { // offsetGet is often called directly after offsetExists, so optimize to avoid second loop: if ($this->lastArrayKey === $offset) { return $this->lastArrayValue; } foreach ($this->arrayKeys as $index => $entry) { if ($entry === $offset) { return $this->arrayValues[$index]; } } } if ($offset === null) { return $this->nullValue; } return null; } /** * @param mixed $offset * @param TValue $value * * @throws \InvalidArgumentException */ #[\ReturnTypeWillChange] public function offsetSet($offset, $value): void { if ($offset === false) { $this->falseValue = $value; $this->falseValueIsSet = true; } elseif ($offset === true) { $this->trueValue = $value; $this->trueValueIsSet = true; } elseif (is_int($offset) || is_string($offset)) { $this->standardStore[$offset] = $value; } elseif (is_float($offset)) { $this->floatStore[(string) $offset] = $value; } elseif (is_object($offset)) { $this->objectStore[$offset] = $value; } elseif (is_array($offset)) { $this->arrayKeys[] = $offset; $this->arrayValues[] = $value; } elseif ($offset === null) { $this->nullValue = $value; $this->nullValueIsSet = true; } else { $unexpectedOffset = Utils::printSafe($offset); throw new \InvalidArgumentException("Unexpected offset type: {$unexpectedOffset}"); } } /** @param mixed $offset */ #[\ReturnTypeWillChange] public function offsetUnset($offset): void { if ($offset === true) { $this->trueValue = null; $this->trueValueIsSet = false; } elseif ($offset === false) { $this->falseValue = null; $this->falseValueIsSet = false; } elseif (is_int($offset) || is_string($offset)) { unset($this->standardStore[$offset]); } elseif (is_float($offset)) { unset($this->floatStore[(string) $offset]); } elseif (is_object($offset)) { $this->objectStore->offsetUnset($offset); } elseif (is_array($offset)) { $index = array_search($offset, $this->arrayKeys, true); if ($index !== false) { array_splice($this->arrayKeys, $index, 1); array_splice($this->arrayValues, $index, 1); } } elseif ($offset === null) { $this->nullValue = null; $this->nullValueIsSet = false; } } } graphql/lib/Utils/AST.php 0000644 00000052014 15166657211 0011224 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; use YOOtheme\GraphQL\Error\Error; use YOOtheme\GraphQL\Error\InvariantViolation; use YOOtheme\GraphQL\Error\SerializationError; use YOOtheme\GraphQL\Language\AST\BooleanValueNode; use YOOtheme\GraphQL\Language\AST\DefinitionNode; use YOOtheme\GraphQL\Language\AST\DocumentNode; use YOOtheme\GraphQL\Language\AST\EnumValueNode; use YOOtheme\GraphQL\Language\AST\FloatValueNode; use YOOtheme\GraphQL\Language\AST\IntValueNode; use YOOtheme\GraphQL\Language\AST\ListTypeNode; use YOOtheme\GraphQL\Language\AST\ListValueNode; use YOOtheme\GraphQL\Language\AST\Location; use YOOtheme\GraphQL\Language\AST\NamedTypeNode; use YOOtheme\GraphQL\Language\AST\NameNode; use YOOtheme\GraphQL\Language\AST\Node; use YOOtheme\GraphQL\Language\AST\NodeKind; use YOOtheme\GraphQL\Language\AST\NodeList; use YOOtheme\GraphQL\Language\AST\NonNullTypeNode; use YOOtheme\GraphQL\Language\AST\NullValueNode; use YOOtheme\GraphQL\Language\AST\ObjectFieldNode; use YOOtheme\GraphQL\Language\AST\ObjectValueNode; use YOOtheme\GraphQL\Language\AST\OperationDefinitionNode; use YOOtheme\GraphQL\Language\AST\StringValueNode; use YOOtheme\GraphQL\Language\AST\ValueNode; use YOOtheme\GraphQL\Language\AST\VariableNode; use YOOtheme\GraphQL\Type\Definition\EnumType; use YOOtheme\GraphQL\Type\Definition\IDType; use YOOtheme\GraphQL\Type\Definition\InputObjectType; use YOOtheme\GraphQL\Type\Definition\InputType; use YOOtheme\GraphQL\Type\Definition\LeafType; use YOOtheme\GraphQL\Type\Definition\ListOfType; use YOOtheme\GraphQL\Type\Definition\NonNull; use YOOtheme\GraphQL\Type\Definition\NullableType; use YOOtheme\GraphQL\Type\Definition\ScalarType; use YOOtheme\GraphQL\Type\Definition\Type; /** * Various utilities dealing with AST. */ class AST { /** * Convert representation of AST as an associative array to instance of GraphQL\Language\AST\Node. * * For example: * * ```php * AST::fromArray([ * 'kind' => 'ListValue', * 'values' => [ * ['kind' => 'StringValue', 'value' => 'my str'], * ['kind' => 'StringValue', 'value' => 'my other str'] * ], * 'loc' => ['start' => 21, 'end' => 25] * ]); * ``` * * Will produce instance of `ListValueNode` where `values` prop is a lazily-evaluated `NodeList` * returning instances of `StringValueNode` on access. * * This is a reverse operation for AST::toArray($node) * * @param array<string, mixed> $node * * @api * * @throws \JsonException * @throws InvariantViolation */ public static function fromArray(array $node): Node { $kind = $node['kind'] ?? null; if ($kind === null) { $safeNode = Utils::printSafeJson($node); throw new InvariantViolation("Node is missing kind: {$safeNode}"); } $class = NodeKind::CLASS_MAP[$kind] ?? null; if ($class === null) { $safeNode = Utils::printSafeJson($node); throw new InvariantViolation("Node has unexpected kind: {$safeNode}"); } $instance = new $class([]); if (isset($node['loc']['start'], $node['loc']['end'])) { $instance->loc = Location::create($node['loc']['start'], $node['loc']['end']); } foreach ($node as $key => $value) { if ($key === 'loc' || $key === 'kind') { continue; } if (is_array($value)) { $value = isset($value[0]) || $value === [] ? new NodeList($value) : self::fromArray($value); } $instance->{$key} = $value; } return $instance; } /** * Convert AST node to serializable array. * * @return array<string, mixed> * * @api */ public static function toArray(Node $node): array { return $node->toArray(); } /** * Produces a GraphQL Value AST given a PHP value. * * Optionally, a GraphQL type may be provided, which will be used to * disambiguate between value primitives. * * | PHP Value | GraphQL Value | * | ------------- | -------------------- | * | Object | Input Object | * | Assoc Array | Input Object | * | Array | List | * | Boolean | Boolean | * | String | String / Enum Value | * | Int | Int | * | Float | Int / Float | * | Mixed | Enum Value | * | null | NullValue | * * @param mixed $value * @param InputType&Type $type * * @throws \JsonException * @throws InvariantViolation * @throws SerializationError * * @return (ValueNode&Node)|null * * @api */ public static function astFromValue($value, InputType $type): ?ValueNode { if ($type instanceof NonNull) { $wrappedType = $type->getWrappedType(); assert($wrappedType instanceof InputType); $astValue = self::astFromValue($value, $wrappedType); return $astValue instanceof NullValueNode ? null : $astValue; } if ($value === null) { return new NullValueNode([]); } // Convert PHP iterables to GraphQL list. If the GraphQLType is a list, but // the value is not an array, convert the value using the list's item type. if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); assert($itemType instanceof InputType, 'proven by schema validation'); if (is_iterable($value)) { $valuesNodes = []; foreach ($value as $item) { $itemNode = self::astFromValue($item, $itemType); if ($itemNode !== null) { $valuesNodes[] = $itemNode; } } return new ListValueNode(['values' => new NodeList($valuesNodes)]); } return self::astFromValue($value, $itemType); } // Populate the fields of the input object by creating ASTs from each value // in the PHP object according to the fields in the input type. if ($type instanceof InputObjectType) { $isArray = is_array($value); $isArrayLike = $isArray || $value instanceof \ArrayAccess; if (! $isArrayLike && ! is_object($value)) { return null; } $fields = $type->getFields(); $fieldNodes = []; foreach ($fields as $fieldName => $field) { $fieldValue = $isArrayLike ? $value[$fieldName] ?? null : $value->{$fieldName} ?? null; // Have to check additionally if key exists, since we differentiate between // "no key" and "value is null": if ($fieldValue !== null) { $fieldExists = true; } elseif ($isArray) { $fieldExists = array_key_exists($fieldName, $value); } elseif ($isArrayLike) { $fieldExists = $value->offsetExists($fieldName); } else { $fieldExists = property_exists($value, $fieldName); } if (! $fieldExists) { continue; } $fieldNode = self::astFromValue($fieldValue, $field->getType()); if ($fieldNode === null) { continue; } $fieldNodes[] = new ObjectFieldNode([ 'name' => new NameNode(['value' => $fieldName]), 'value' => $fieldNode, ]); } return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]); } assert($type instanceof LeafType, 'other options were exhausted'); // Since value is an internally represented value, it must be serialized // to an externally represented value before converting into an AST. $serialized = $type->serialize($value); // Others serialize based on their corresponding PHP scalar types. if (is_bool($serialized)) { return new BooleanValueNode(['value' => $serialized]); } if (is_int($serialized)) { return new IntValueNode(['value' => (string) $serialized]); } if (is_float($serialized)) { /** @phpstan-ignore equal.notAllowed (int cast with == used for performance reasons) */ if ((int) $serialized == $serialized) { return new IntValueNode(['value' => (string) $serialized]); } return new FloatValueNode(['value' => (string) $serialized]); } if (is_string($serialized)) { // Enum types use Enum literals. if ($type instanceof EnumType) { return new EnumValueNode(['value' => $serialized]); } // ID types can use Int literals. $asInt = (int) $serialized; if ($type instanceof IDType && (string) $asInt === $serialized) { return new IntValueNode(['value' => $serialized]); } // Use json_encode, which uses the same string encoding as GraphQL, // then remove the quotes. return new StringValueNode(['value' => $serialized]); } $notConvertible = Utils::printSafe($serialized); throw new InvariantViolation("Cannot convert value to AST: {$notConvertible}"); } /** * Produces a PHP value given a GraphQL Value AST. * * A GraphQL type must be provided, which will be used to interpret different * GraphQL Value literals. * * Returns `null` when the value could not be validly coerced according to * the provided type. * * | GraphQL Value | PHP Value | * | -------------------- | ------------- | * | Input Object | Assoc Array | * | List | Array | * | Boolean | Boolean | * | String | String | * | Int / Float | Int / Float | * | Enum Value | Mixed | * | Null Value | null | * * @param (ValueNode&Node)|null $valueNode * @param array<string, mixed>|null $variables * * @throws \Exception * * @return mixed * * @api */ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null) { $undefined = Utils::undefined(); if ($valueNode === null) { // When there is no AST, then there is also no value. // Importantly, this is different from returning the GraphQL null value. return $undefined; } if ($type instanceof NonNull) { if ($valueNode instanceof NullValueNode) { // Invalid: intentionally return no value. return $undefined; } return self::valueFromAST($valueNode, $type->getWrappedType(), $variables); } if ($valueNode instanceof NullValueNode) { // This is explicitly returning the value null. return null; } if ($valueNode instanceof VariableNode) { $variableName = $valueNode->name->value; if ($variables === null || ! array_key_exists($variableName, $variables)) { // No valid return value. return $undefined; } // Note: This does no further checking that this variable is correct. // This assumes that this query has been validated and the variable // usage here is of the correct type. return $variables[$variableName]; } if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); if ($valueNode instanceof ListValueNode) { $coercedValues = []; $itemNodes = $valueNode->values; foreach ($itemNodes as $itemNode) { if (self::isMissingVariable($itemNode, $variables)) { // If an array contains a missing variable, it is either coerced to // null or if the item type is non-null, it considered invalid. if ($itemType instanceof NonNull) { // Invalid: intentionally return no value. return $undefined; } $coercedValues[] = null; } else { $itemValue = self::valueFromAST($itemNode, $itemType, $variables); if ($undefined === $itemValue) { // Invalid: intentionally return no value. return $undefined; } $coercedValues[] = $itemValue; } } return $coercedValues; } $coercedValue = self::valueFromAST($valueNode, $itemType, $variables); if ($undefined === $coercedValue) { // Invalid: intentionally return no value. return $undefined; } return [$coercedValue]; } if ($type instanceof InputObjectType) { if (! $valueNode instanceof ObjectValueNode) { // Invalid: intentionally return no value. return $undefined; } $coercedObj = []; $fields = $type->getFields(); $fieldNodes = []; foreach ($valueNode->fields as $field) { $fieldNodes[$field->name->value] = $field; } foreach ($fields as $field) { $fieldName = $field->name; $fieldNode = $fieldNodes[$fieldName] ?? null; if ($fieldNode === null || self::isMissingVariable($fieldNode->value, $variables)) { if ($field->defaultValueExists()) { $coercedObj[$fieldName] = $field->defaultValue; } elseif ($field->getType() instanceof NonNull) { // Invalid: intentionally return no value. return $undefined; } continue; } $fieldValue = self::valueFromAST( $fieldNode->value, $field->getType(), $variables ); if ($undefined === $fieldValue) { // Invalid: intentionally return no value. return $undefined; } $coercedObj[$fieldName] = $fieldValue; } return $type->parseValue($coercedObj); } if ($type instanceof EnumType) { try { return $type->parseLiteral($valueNode, $variables); } catch (\Throwable $error) { return $undefined; } } assert($type instanceof ScalarType, 'only remaining option'); // Scalars fulfill parsing a literal value via parseLiteral(). // Invalid values represent a failure to parse correctly, in which case // no value is returned. try { return $type->parseLiteral($valueNode, $variables); } catch (\Throwable $error) { return $undefined; } } /** * Returns true if the provided valueNode is a variable which is not defined * in the set of variables. * * @param ValueNode&Node $valueNode * @param array<string, mixed>|null $variables */ private static function isMissingVariable(ValueNode $valueNode, ?array $variables): bool { return $valueNode instanceof VariableNode && ($variables === null || ! array_key_exists($valueNode->name->value, $variables)); } /** * Produces a PHP value given a GraphQL Value AST. * * Unlike `valueFromAST()`, no type is provided. The resulting PHP value * will reflect the provided GraphQL value AST. * * | GraphQL Value | PHP Value | * | -------------------- | ------------- | * | Input Object | Assoc Array | * | List | Array | * | Boolean | Boolean | * | String | String | * | Int / Float | Int / Float | * | Enum | Mixed | * | Null | null | * * @param array<string, mixed>|null $variables * * @throws \Exception * * @return mixed * * @api */ public static function valueFromASTUntyped(Node $valueNode, ?array $variables = null) { switch (true) { case $valueNode instanceof NullValueNode: return null; case $valueNode instanceof IntValueNode: return (int) $valueNode->value; case $valueNode instanceof FloatValueNode: return (float) $valueNode->value; case $valueNode instanceof StringValueNode: case $valueNode instanceof EnumValueNode: case $valueNode instanceof BooleanValueNode: return $valueNode->value; case $valueNode instanceof ListValueNode: $values = []; foreach ($valueNode->values as $node) { $values[] = self::valueFromASTUntyped($node, $variables); } return $values; case $valueNode instanceof ObjectValueNode: $values = []; foreach ($valueNode->fields as $field) { $values[$field->name->value] = self::valueFromASTUntyped($field->value, $variables); } return $values; case $valueNode instanceof VariableNode: $variableName = $valueNode->name->value; return ($variables ?? []) !== [] && isset($variables[$variableName]) ? $variables[$variableName] : null; } throw new Error("Unexpected value kind: {$valueNode->kind}"); } /** * Returns type definition for given AST Type node. * * @param callable(string): ?Type $typeLoader * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * * @throws \Exception * * @api */ public static function typeFromAST(callable $typeLoader, Node $inputTypeNode): ?Type { if ($inputTypeNode instanceof ListTypeNode) { $innerType = self::typeFromAST($typeLoader, $inputTypeNode->type); return $innerType === null ? null : new ListOfType($innerType); } if ($inputTypeNode instanceof NonNullTypeNode) { $innerType = self::typeFromAST($typeLoader, $inputTypeNode->type); if ($innerType === null) { return null; } assert($innerType instanceof NullableType, 'proven by schema validation'); return new NonNull($innerType); } return $typeLoader($inputTypeNode->name->value); } /** * Returns the operation within a document by name. * * If a name is not provided, an operation is only returned if the document has exactly one. * * @api */ public static function getOperationAST(DocumentNode $document, ?string $operationName = null): ?OperationDefinitionNode { $operation = null; foreach ($document->definitions->getIterator() as $node) { if (! $node instanceof OperationDefinitionNode) { continue; } if ($operationName === null) { // We found a second operation, so we bail instead of returning an ambiguous result. if ($operation !== null) { return null; } $operation = $node; } elseif ($node->name instanceof NameNode && $node->name->value === $operationName) { return $node; } } return $operation; } /** * Provided a collection of ASTs, presumably each from different files, * concatenate the ASTs together into batched AST, useful for validating many * GraphQL source files which together represent one conceptual application. * * @param array<DocumentNode> $documents * * @api */ public static function concatAST(array $documents): DocumentNode { /** @var array<int, Node&DefinitionNode> $definitions */ $definitions = []; foreach ($documents as $document) { foreach ($document->definitions as $definition) { $definitions[] = $definition; } } return new DocumentNode(['definitions' => new NodeList($definitions)]); } } graphql/lib/Utils/LexicalDistance.php 0000644 00000007242 15166657211 0013634 0 ustar 00 <?php declare(strict_types=1); namespace YOOtheme\GraphQL\Utils; /** * Computes the lexical distance between strings A and B. * * The "distance" between two strings is given by counting the minimum number * of edits needed to transform string A into string B. An edit can be an * insertion, deletion, or substitution of a single character, or a swap of two * adjacent characters. * * Includes a custom alteration from Damerau-Levenshtein to treat case changes * as a single edit which helps identify mis-cased values with an edit distance * of 1. * * This distance can be useful for detecting typos in input or sorting * * Unlike the native levenshtein() function that always returns int, LexicalDistance::measure() returns int|null. * It takes into account the threshold and returns null if the measured distance is bigger. */ class LexicalDistance { private string $input; private string $inputLowerCase; /** * List of char codes in the input string. * * @var array<int> */ private array $inputArray; public function __construct(string $input) { $this->input = $input; $this->inputLowerCase = strtolower($input); $this->inputArray = self::stringToArray($this->inputLowerCase); } public function measure(string $option, float $threshold): ?int { if ($this->input === $option) { return 0; } $optionLowerCase = strtolower($option); // Any case change counts as a single edit if ($this->inputLowerCase === $optionLowerCase) { return 1; } $a = self::stringToArray($optionLowerCase); $b = $this->inputArray; if (count($a) < count($b)) { $tmp = $a; $a = $b; $b = $tmp; } $aLength = count($a); $bLength = count($b); if ($aLength - $bLength > $threshold) { return null; } /** @var array<array<int>> $rows */ $rows = []; for ($i = 0; $i <= $bLength; ++$i) { $rows[0][$i] = $i; } for ($i = 1; $i <= $aLength; ++$i) { $upRow = &$rows[($i - 1) % 3]; $currentRow = &$rows[$i % 3]; $smallestCell = ($currentRow[0] = $i); for ($j = 1; $j <= $bLength; ++$j) { $cost = $a[$i - 1] === $b[$j - 1] ? 0 : 1; $currentCell = min( $upRow[$j] + 1, // delete $currentRow[$j - 1] + 1, // insert $upRow[$j - 1] + $cost, // substitute ); if ($i > 1 && $j > 1 && $a[$i - 1] === $b[$j - 2] && $a[$i - 2] === $b[$j - 1]) { // transposition $doubleDiagonalCell = $rows[($i - 2) % 3][$j - 2]; $currentCell = min($currentCell, $doubleDiagonalCell + 1); } if ($currentCell < $smallestCell) { $smallestCell = $currentCell; } $currentRow[$j] = $currentCell; } // Early exit, since distance can't go smaller than smallest element of the previous row. if ($smallestCell > $threshold) { return null; } } $distance = $rows[$aLength % 3][$bLength]; return $distance <= $threshold ? $distance : null; } /** * Returns a list of char codes in the given string. * * @return array<int> */ private static function stringToArray(string $str): array { $array = []; foreach (mb_str_split($str) as $char) { $array[] = mb_ord($char); } return $array; } } graphql/LICENSE 0000644 00000002067 15166657211 0007226 0 ustar 00 MIT License Copyright (c) 2015-present, Webonyx, LLC. 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. builder-source-filesystem/src/Type/FilesQueryType.php 0000644 00000012323 15166657211 0017040 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Filesystem\Type; use function YOOtheme\app; use YOOtheme\Builder\Source\Filesystem\FileHelper; use function YOOtheme\trans; class FilesQueryType { /** * @param string $rootDir * * @return array */ public static function config($rootDir) { return [ 'fields' => [ 'files' => [ 'type' => [ 'listOf' => 'File', ], 'args' => [ 'pattern' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Files'), 'group' => trans('External'), 'fields' => [ 'pattern' => [ 'label' => trans('Path Pattern'), 'description' => "Pick a folder to load file content dynamically. Alternatively, set a path <a href=\"https://www.php.net/manual/en/function.glob.php\" target=\"_blank\">glob pattern</a> to filter files. For example <code>{$rootDir}/*.{jpg,png}</code>. The path is relative to the system folder and has to be a subdirectory of <code>{$rootDir}</code>.", 'type' => 'select-file', ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of files.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'description' => trans( 'The Default order will follow the order set by the brackets or fallback to the default files order set by the system.', ), 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'name', 'options' => [ trans('Default') => 'default', trans('Alphabetical') => 'name', trans('Random') => 'rand', ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'ASC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], 'enable' => 'order != "rand"', ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { return app(FileHelper::class)->query($args); } } builder-source-filesystem/src/Type/FileType.php 0000644 00000014472 15166657211 0015636 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Filesystem\Type; use function YOOtheme\app; use YOOtheme\File; use YOOtheme\Path; use YOOtheme\Str; use function YOOtheme\trans; use YOOtheme\Url; use YOOtheme\View; class FileType { /** * @return array */ public static function config() { return [ 'fields' => [ 'name' => [ 'type' => 'String', 'args' => [ 'title_case' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Name'), 'arguments' => [ 'title_case' => [ 'label' => trans('Convert'), 'type' => 'checkbox', 'text' => trans('Convert to title-case'), ], ], 'filters' => ['limit', 'preserve'], ], 'extensions' => [ 'call' => __CLASS__ . '::name', ], ], 'basename' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Basename'), ], 'extensions' => [ 'call' => __CLASS__ . '::basename', ], ], 'dirname' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Dirname'), ], 'extensions' => [ 'call' => __CLASS__ . '::dirname', ], ], 'url' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Url'), ], 'extensions' => [ 'call' => __CLASS__ . '::url', ], ], 'path' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Path'), ], 'extensions' => [ 'call' => __CLASS__ . '::path', ], ], 'content' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Content'), 'filters' => ['limit', 'preserve'], ], 'extensions' => [ 'call' => __CLASS__ . '::content', ], ], 'size' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Size'), ], 'extensions' => [ 'call' => __CLASS__ . '::size', ], ], 'extension' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Extension'), ], 'extensions' => [ 'call' => __CLASS__ . '::extension', ], ], 'mimetype' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Mimetype'), ], 'extensions' => [ 'call' => __CLASS__ . '::mimetype', ], ], 'accessed' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Accessed Date'), 'filters' => ['date'], ], 'extensions' => [ 'call' => __CLASS__ . '::accessed', ], ], 'changed' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Changed Date'), 'filters' => ['date'], ], 'extensions' => [ 'call' => __CLASS__ . '::changed', ], ], 'modified' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Modified Date'), 'filters' => ['date'], ], 'extensions' => [ 'call' => __CLASS__ . '::modified', ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('File'), ], ]; } public static function name($file, $args) { $name = basename($file, '.' . File::getExtension($file)); if (!empty($args['title_case'])) { $name = Str::titleCase($name); } return $name; } public static function content($file) { return File::getContents($file); } public static function size($file) { return app(View::class)->formatBytes(File::getSize($file) ?: 0); } public static function accessed($file) { return File::getATime($file); } public static function changed($file) { return File::getCTime($file); } public static function modified($file) { return File::getMTime($file); } public static function mimetype($file) { return File::getMimetype($file); } public static function extension($file) { return File::getExtension($file); } public static function basename($file) { return basename($file); } public static function dirname($file) { return dirname(self::path($file)); } public static function path($file) { return Path::relative('~', $file); } public static function url($file) { return Url::relative(Url::to($file)); } } builder-source-filesystem/src/Type/FileQueryType.php 0000644 00000010537 15166657211 0016662 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Filesystem\Type; use function YOOtheme\app; use YOOtheme\Builder\Source\Filesystem\FileHelper; use function YOOtheme\trans; class FileQueryType { /** * @param string $rootDir * * @return array */ public static function config($rootDir) { return [ 'fields' => [ 'file' => [ 'type' => 'File', 'args' => [ 'pattern' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('File'), 'group' => trans('External'), 'fields' => [ 'pattern' => [ 'label' => trans('Path Pattern'), 'description' => "Pick a folder to load file content dynamically. Alternatively, set a path <a href=\"https://www.php.net/manual/en/function.glob.php\" target=\"_blank\">glob pattern</a> to filter files. For example <code>{$rootDir}/*.{jpg,png}</code>. The path is relative to the system folder and has to be a subdirectory of <code>{$rootDir}</code>.", 'type' => 'select-file', ], 'offset' => [ 'label' => trans('Offset'), 'description' => trans( 'Set the offset to specify which file is loaded.', ), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'description' => trans( 'The Default order will follow the order set by the brackets or fallback to the default files order set by the system.', ), 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'name', 'options' => [ trans('Default') => 'default', trans('Alphabetical') => 'name', trans('Random') => 'rand', ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'ASC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], 'enable' => 'order != "rand"', ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $files = app(FileHelper::class)->query(['limit' => 1] + $args); return array_shift($files); } } builder-source-filesystem/src/FileHelper.php 0000644 00000003114 15166657211 0015202 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Filesystem; use YOOtheme\File; use YOOtheme\Path; class FileHelper { /** * @var string[] */ protected $rootDirs; /** * @param string|string[] $rootDirs */ public function __construct($rootDirs) { $this->rootDirs = (array) $rootDirs; } /** * Query files. * * @param array $args * * @return array */ public function query(array $args = []) { $args += ['offset' => 0, 'limit' => 10, 'order' => '', 'order_direction' => 'ASC']; if (empty($args['pattern'])) { return []; } $pattern = $args['pattern']; $pattern = str_starts_with($pattern, '~') ? $pattern : Path::join('~', $pattern); $files = File::glob($pattern, GLOB_NOSORT); // filter out any dir $files = array_filter( $files, fn($file) => array_any($this->rootDirs, fn($dir) => str_starts_with($file, $dir)) && is_file($file), ); // order if ($args['order'] === 'rand') { shuffle($files); } else { if ($args['order'] === 'name') { natcasesort($files); } // direction if ($args['order_direction'] === 'DESC') { $files = array_reverse($files); } } // offset/limit if ($args['offset'] || $args['limit']) { $files = array_slice($files, (int) $args['offset'], (int) $args['limit'] ?: null); } return $files; } } builder-source-filesystem/src/Listener/LoadSourceTypes.php 0000644 00000001407 15166657211 0020040 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Filesystem\Listener; use YOOtheme\Builder\Source\Filesystem\Type; use YOOtheme\Config; use YOOtheme\Path; class LoadSourceTypes { public Config $config; public function __construct(Config $config) { $this->config = $config; } public function handle($source): void { try { $rootDir = Path::relative( $this->config->get('app.rootDir'), $this->config->get('app.uploadDir'), ); $source->queryType(Type\FileQueryType::config($rootDir)); $source->queryType(Type\FilesQueryType::config($rootDir)); $source->objectType('File', Type\FileType::config()); } catch (\Exception $e) { } } } builder-source-filesystem/bootstrap.php 0000644 00000000356 15166657211 0014416 0 ustar 00 <?php namespace YOOtheme\Builder\Source\Filesystem; return [ 'events' => [ // -5 to show the 'External' Group after the 'Custom' Group 'source.init' => [Listener\LoadSourceTypes::class => ['@handle', -5]], ], ]; theme-joomla-menus/bootstrap.php 0000644 00000000652 15166657211 0013015 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; return [ 'routes' => [['get', '/items', [MenuController::class, 'getItems']]], 'events' => [ 'customizer.init' => [Listener\LoadMenuData::class => '@handle'], ], 'actions' => [ 'onAfterCleanModuleList' => [ Listener\LoadMenuModules::class => '@handle', Listener\LoadSplitNavbar::class => ['@handle', -20], ], ], ]; theme-joomla-menus/config/customizer.json 0000644 00000011170 15166657211 0014630 0 ustar 00 { "sections": { "joomla-menus": { "title": "Menus", "priority": 30, "help": { "Menus": [ { "title": "Managing Menus", "src": "https://www.youtube-nocookie.com/watch?v=9WljKJbxzZc&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:03", "documentation": "support/yootheme-pro/joomla/menus", "support": "support/search?tags=125&q=menu" }, { "title": "Setting Menu Items", "src": "https://www.youtube-nocookie.com/watch?v=Kvvp3il_yXY&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "3:07", "documentation": "support/yootheme-pro/joomla/menus#menu-items", "support": "support/search?tags=125&q=menu%20item" }, { "title": "Using the Mega Menu Builder", "src": "https://www.youtube-nocookie.com/watch?v=hE_xXcc10K4&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "2:21", "documentation": "support/yootheme-pro/joomla/menus#mega-menu-builder", "support": "support/search?tags=125&q=mega%20menu" }, { "title": "Using Menu Positions", "src": "https://www.youtube-nocookie.com/watch?v=Yny3ilroGag&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "2:29", "documentation": "support/yootheme-pro/joomla/menus#menu-positions", "support": "support/search?tags=125&q=menu%20position" }, { "title": "Using Menu Position Options", "src": "https://www.youtube-nocookie.com/watch?v=_gnAQE_MlsI&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "2:17", "documentation": "support/yootheme-pro/joomla/menus#menu-positions", "support": "support/search?tags=125&q=menu%20position%20options" }, { "title": "Using the Menu Module", "src": "https://www.youtube-nocookie.com/watch?v=uJyUxVOT30Y&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:27", "documentation": "support/yootheme-pro/joomla/menus#menu-module", "support": "support/search?tags=125&q=menu%20module" } ], "Menu Items": [ { "title": "Creating Menu Dividers", "src": "https://www.youtube-nocookie.com/watch?v=PVKV0B2fOOk&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "0:48", "documentation": "support/yootheme-pro/joomla/menus#menu-divider", "support": "support/search?tags=125&q=menu%20divider" }, { "title": "Creating Menu Heading", "src": "https://www.youtube-nocookie.com/watch?v=mAWbl_Qyckg&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "0:50", "documentation": "support/yootheme-pro/joomla/menus#menu-heading", "support": "support/search?tags=125&q=menu%20heading" }, { "title": "Creating Menu Text Items", "src": "https://www.youtube-nocookie.com/watch?v=RKY8qDgCTnc&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:43", "documentation": "support/yootheme-pro/joomla/menus#menu-text-item", "support": "support/search?tags=125&q=menu%20text%20item" }, { "title": "Creating Accordion Menus", "src": "https://www.youtube-nocookie.com/watch?v=Kq87emJcq68&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:30", "documentation": "support/yootheme-pro/joomla/menus#accordion-menu", "support": "support/search?tags=125&q=accordion%20menu" } ] } } } } theme-joomla-menus/src/Listener/LoadMenuModules.php 0000644 00000003077 15166657211 0016415 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; use YOOtheme\Config; class LoadMenuModules { public Config $config; public function __construct(Config $config) { $this->config = $config; } public function handle($event): void { $modules = $event->getArgument('modules'); if ($this->config->get('app.isAdmin') || !$this->config->get('theme.active')) { return; } // create menu modules when assigned in theme settings foreach ($this->config->get('~theme.menu.positions', []) as $position => $menu) { if (empty($menu['menu'])) { continue; } array_unshift( $modules, (object) [ 'id' => "menu-{$position}", 'name' => 'menu', 'module' => 'mod_menu', 'title' => '', 'showtitle' => 0, 'position' => $position, 'params' => json_encode([ 'menutype' => $menu['menu'], 'showAllChildren' => true, 'yoo_config' => json_encode( array_combine( array_map(fn($key) => "menu_{$key}", array_keys($menu)), $menu, ), ), ]), ], ); } $event->setArgument(0, $modules); $event->setArgument('modules', $modules); } } theme-joomla-menus/src/Listener/LoadSplitNavbar.php 0000644 00000002572 15166657211 0016404 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; use YOOtheme\Config; class LoadSplitNavbar { public Config $config; public function __construct(Config $config) { $this->config = $config; } public function handle($event): void { $modules = $event->getArgument('modules'); if ($this->config->get('app.isAdmin') || !$this->config->get('theme.active')) { return; } if ( in_array($this->config->get('~theme.header.layout'), [ 'stacked-center-split-a', 'stacked-center-split-b', ]) ) { foreach ($modules as $module) { if ( $module->module != 'mod_menu' || $module->position != 'navbar' || !in_array($this->config->get("~theme.modules.{$module->id}.menu_type"), [ '', 'nav', ]) ) { continue; } $clone = clone $module; $clone->id = "{$module->id}-split"; $clone->position = 'navbar-split'; array_splice($modules, array_search($module, $modules) + 1, 0, [$clone]); } } $event->setArgument(0, $modules); $event->setArgument('modules', $modules); } } theme-joomla-menus/src/Listener/LoadMenuData.php 0000644 00000001426 15166657211 0015652 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; use Joomla\CMS\User\User; use YOOtheme\Config; use YOOtheme\Path; use YOOtheme\Theme\Joomla\MenuConfig; class LoadMenuData { public User $user; public Config $config; public MenuConfig $menu; public function __construct(Config $config, MenuConfig $menu, User $user) { $this->menu = $menu; $this->user = $user; $this->config = $config; } public function handle() { $this->config->add('customizer', ['menu' => $this->menu->getArrayCopy()]); if ($this->user->authorise('core.manage', 'com_menus')) { $this->config->addFile( 'customizer', Path::get('../../config/customizer.json', __DIR__), ); } } } theme-joomla-menus/src/MenuController.php 0000644 00000000424 15166657211 0014534 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; use YOOtheme\Http\Request; use YOOtheme\Http\Response; class MenuController { public static function getItems(Request $request, Response $response, MenuConfig $menu) { return $response->withJson($menu->items); } } theme-joomla-menus/src/MenuConfig.php 0000644 00000003406 15166657211 0013621 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; use Joomla\CMS\HTML\Helpers\Menu; use Joomla\CMS\Menu\AbstractMenu; use Joomla\CMS\User\User; use YOOtheme\Config; use YOOtheme\ConfigObject; /** * @property array $menus * @property array $items * @property array $positions * @property bool $canEdit * @property bool $canCreate * @property bool $canDelete */ class MenuConfig extends ConfigObject { /** * Constructor. */ public function __construct(Config $config, User $user) { parent::__construct([ 'menus' => $this->getMenus(), 'items' => $this->getItems(), 'positions' => $config->get('theme.menus'), 'canEdit' => $user->authorise('core.edit', 'com_menus'), 'canCreate' => $user->authorise('core.create', 'com_menus'), 'canDelete' => $user->authorise('core.edit.state', 'com_menus'), ]); } protected function getMenus() { return array_map( fn($menu) => [ 'id' => $menu->value, 'name' => $menu->text, ], Menu::menus(), ); } protected function getItems() { return array_values( array_map( fn($item) => [ 'id' => (string) $item->id, 'title' => $item->title, 'level' => $item->level - 1, 'menu' => $item->menutype, 'link' => $item->link, 'home' => $item->home, 'parent' => (string) $item->parent_id, 'type' => $item->type == 'separator' ? 'heading' : $item->type, ], AbstractMenu::getInstance('site')->getMenu(), ), ); } } view-metadata/src/View/MetadataManager.php 0000644 00000010247 15166657211 0014537 0 ustar 00 <?php namespace YOOtheme\View; use YOOtheme\Event; use YOOtheme\Metadata; /** * Manages HTML elements belonging to the metadata content category. * * @link https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Metadata_content */ class MetadataManager implements Metadata, \IteratorAggregate { /** * @var array */ protected $prefix = ['article', 'fb', 'og', 'twitter']; /** * @var array */ protected $metadata = []; /** * @inheritdoc */ public function all(...$names) { if (!$names) { return $this->metadata; } $result = []; foreach ($names as $name) { $prefix = str_ends_with($name, '*') ? substr($name, 0, -1) : false; foreach ($this->metadata as $metadata) { if ( isset($this->metadata[$name]) || ($prefix && str_starts_with($metadata->name, $prefix)) ) { $result[$metadata->name] = $metadata; } } } return $result; } /** * @inheritdoc */ public function get($name) { return $this->metadata[$name] ?? null; } /** * @inheritdoc */ public function set($name, $value, array $attributes = []) { if (is_array($value) && !is_callable($value)) { [$value, $attributes] = [null, array_merge($value, $attributes)]; } $metadata = new MetadataObject($name, $value, $attributes); $metadata = $this->resolveMetadata($metadata); $metadata = Event::emit('metadata.load|filter', $metadata, $this); return $this->metadata[$metadata->name] = $metadata; } /** * @inheritdoc */ public function del($name) { unset($this->metadata[$name]); } /** * @inheritdoc */ public function merge(array $metadata) { foreach ($metadata as $name => $value) { $this->set($name, $value); } } /** * @inheritdoc */ public function filter(callable $filter) { return array_filter($this->metadata, $filter); } /** * @inheritdoc */ public function render() { return join("\n", $this->metadata); } /** * Returns an iterator for metadata tags. * * @return \ArrayIterator */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->metadata); } /** * Resolves the metadata. * * @param MetadataObject $metadata * * @return MetadataObject */ protected function resolveMetadata(MetadataObject $metadata) { if (is_string($metadata->value)) { $metadata = $this->resolveAttributes($metadata); } if ($metadata->tag === 'style' && !isset($metadata->value)) { return $metadata->withTag('link')->withAttribute('rel', 'stylesheet'); } if (in_array($metadata->tag, $this->prefix)) { return $metadata->withTag('meta'); } return $metadata; } /** * Resolve the metadata attributes. * * @param MetadataObject $metadata * * @return MetadataObject */ protected function resolveAttributes($metadata) { if ($metadata->tag === 'base') { return $metadata->withAttributes([ 'href' => $metadata->value, ]); } if ($metadata->tag === 'link') { return $metadata->withAttributes([ 'href' => $metadata->value, 'rel' => str_replace('link:', '', $metadata->name), ]); } if ($metadata->tag === 'meta') { return $metadata->withAttributes([ 'name' => str_replace('meta:', '', $metadata->name), 'content' => $metadata->value, ]); } if (in_array($metadata->tag, $this->prefix)) { return $metadata->withAttributes([ 'property' => $metadata->name, 'content' => $metadata->value, ]); } return $metadata; } } view-metadata/src/View/MetadataObject.php 0000644 00000007406 15166657211 0014376 0 ustar 00 <?php namespace YOOtheme\View; /** * @property string $href * @property string $src * @property string $defer * @property string $version */ class MetadataObject { /** * @var string */ public $tag; /** * @var string */ public $name; /** * @var mixed */ public $value; /** * @var array */ public $attributes; /** * Constructor. * * @param string $name * @param mixed $value * @param array $attributes */ public function __construct($name, $value, array $attributes = []) { $tag = substr($name, 0, strpos($name, ':')); $this->tag = $tag ?: $name; $this->name = $name; $this->value = $value; $this->attributes = $attributes; } /** * Gets an attribute value. * * @param string $name * * @return mixed */ public function __get($name) { return $this->attributes[$name] ?? null; } /** * Checks if an attribute value exists. * * @param string $name * * @return bool */ public function __isset($name) { return isset($this->attributes[$name]); } /** * Gets the rendered tag as string. * * @return string */ public function __toString() { return $this->render(); } /** * Renders the tag. * * @return string */ public function render() { $metadata = $this; if (is_callable($callback = $this->value)) { $metadata = $callback($this) ?: $this; } return HtmlElement::tag($metadata->tag, $metadata->attributes, $metadata->value); } /** * Gets the tag. * * @return string */ public function getTag() { return $this->tag; } /** * Sets the tag. * * @param string $tag * * @return static */ public function withTag($tag) { $clone = clone $this; $clone->tag = $tag; return $clone; } /** * Gets the name. * * @return string */ public function getName() { return $this->name; } /** * Sets the name. * * @param string $name * * @return static */ public function withName($name) { $clone = clone $this; $clone->name = $name; return $clone; } /** * Gets the value. * * @return string */ public function getValue() { return $this->value; } /** * Sets the value. * * @param string $value * * @return static */ public function withValue($value) { $clone = clone $this; $clone->value = $value; return $clone; } /** * Gets an attribute. * * @param string $name * @param mixed $default * * @return array */ public function getAttribute($name, $default = null) { return $this->$name ?? $default; } /** * Adds an attribute. * * @param string $name * @param mixed $value * * @return static */ public function withAttribute($name, $value) { $clone = clone $this; $clone->attributes[$name] = $value; return $clone; } /** * Gets attributes. * * @return array */ public function getAttributes() { return $this->attributes; } /** * Merges multiple attributes. * * @param array $attributes * * @return static */ public function withAttributes(array $attributes) { $clone = clone $this; $clone->attributes = array_merge($this->attributes, $attributes); return $clone; } } view-metadata/src/Metadata.php 0000644 00000002234 15166657211 0012327 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\View\MetadataObject; interface Metadata { /** * Gets all metadata tags. * * @param string $names * * @return MetadataObject[] */ public function all(...$names); /** * Gets a metadata tag. * * @param string $name * * @return MetadataObject|null */ public function get($name); /** * Sets a metadata tag. * * @param string $name * @param mixed $value * @param array $attributes * * @return MetadataObject */ public function set($name, $value, array $attributes = []); /** * Deletes a metadata tag. * * @param string $name */ public function del($name); /** * Merges multiple metadata tags. * * @param array $metadata */ public function merge(array $metadata); /** * Filters metadata tags using a callback. * * @param callable $filter * * @return MetadataObject[] */ public function filter(callable $filter); /** * Renders metadata tags. * * @return string */ public function render(); } view-metadata/bootstrap.php 0000644 00000000610 15166657211 0012031 0 ustar 00 <?php namespace YOOtheme; use YOOtheme\View\MetadataManager; return [ 'extend' => [ View::class => function (View $view, $app) { $view->addFunction('metadata', $app->wrap(Metadata::class . '@set')); }, ], 'aliases' => [ Metadata::class => 'metadata', ], 'services' => [ Metadata::class => MetadataManager::class, ], ]; platform-joomla/src/Media.php 0000644 00000003324 15166657211 0012202 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Extension\MVCComponent; use Joomla\CMS\Factory; use Joomla\Component\Media\Administrator\Model\ApiModel; use Joomla\Component\Media\Administrator\Provider\ProviderInterface; use YOOtheme\Path; class Media { public static function getRoot($root = null): string { $provider = static::getLocalProvider(); $path = null; if ($provider) { $adapters = $provider->getAdapters(); $adapter = $root ? $adapters[$root] ?? null : current($adapters); if ($adapter) { $path = $adapter->getAdapterName(); } } return Path::join( JPATH_ROOT, $path ?: ComponentHelper::getParams('com_media')->get('file_path', 'images'), ); } public static function getRootPaths(): array { $provider = static::getLocalProvider(); if (!$provider) { return []; } return array_values( array_map(fn($adapter) => $adapter->getAdapterName(), $provider->getAdapters()), ); } protected static function getLocalProvider(): ?ProviderInterface { $joomla = Factory::getApplication(); if (!method_exists($joomla, 'bootComponent')) { return null; } try { /** @var MVCComponent $component */ $component = $joomla->bootComponent('com_media'); /** @var ApiModel $model */ $model = $component->getMVCFactory()->createModel('Api', 'Administrator'); return $model->getProvider('local'); } catch (\Exception $e) { return null; } } } platform-joomla/src/Router.php 0000644 00000000722 15166657211 0012442 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Router\Route; use YOOtheme\Url; class Router { public static function generate($pattern = '', array $parameters = [], $secure = null) { if ($pattern) { $parameters = ['p' => $pattern] + $parameters; } return Url::to( Route::_('index.php?' . http_build_query(['option' => 'com_ajax']), false), $parameters, $secure, ); } } platform-joomla/src/Storage.php 0000644 00000004134 15166657211 0012567 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Factory; use Joomla\Database\DatabaseDriver; use YOOtheme\Storage as AbstractStorage; use function YOOtheme\app; class Storage extends AbstractStorage { /** * Constructor. * * @param string $element * @param string $folder * * @throws \Exception */ public function __construct($element = 'yootheme', $folder = 'system') { /** @var DatabaseDriver $db */ $db = app(DatabaseDriver::class); $query = sprintf( 'SELECT custom_data FROM #__extensions WHERE element = %s AND folder = %s LIMIT 1', $db->quote($element), $db->quote($folder), ); if ($result = $db->setQuery($query)->loadResult()) { $this->addJson($result); } $joomla = Factory::getApplication(); $joomla->registerEvent('onAfterRespond', function () use ($db, $element, $folder) { if ($this->isModified()) { $data = json_encode($this, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($data === false) { return; } $this->alterCustomDataColumn($db); $extension = (object) [ 'element' => $element, 'folder' => $folder, 'custom_data' => $data, ]; $db->updateObject('#__extensions', $extension, ['element', 'folder']); } }); } /** * Alter custom_data type to MEDIUMTEXT only in MySQL database */ protected function alterCustomDataColumn($db) { if (!str_contains($db->getName(), 'mysql')) { return; } if ( $db ->setQuery( "SHOW FIELDS FROM #__extensions WHERE Field = 'custom_data' AND Type = 'text'", ) ->loadRow() ) { $db->setQuery( 'ALTER TABLE #__extensions CHANGE `custom_data` `custom_data` MEDIUMTEXT NOT NULL', )->execute(); } } } platform-joomla/src/ActionLoader.php 0000644 00000001741 15166657211 0013530 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Factory; use Joomla\Event\DispatcherInterface; use YOOtheme\Application\EventLoader; use YOOtheme\Container; use YOOtheme\EventDispatcher; /** * @property EventDispatcher|DispatcherInterface $dispatcher */ class ActionLoader extends EventLoader { /** * Constructor. */ public function __construct() { $joomla = Factory::getApplication(); if (version_compare(JVERSION, '5.0', '<')) { $this->dispatcher = new Dispatcher($joomla); } else { $this->dispatcher = $joomla->getDispatcher(); } } /** * Load action listeners. * * @param Container $container * @param array $configs */ public function __invoke(Container $container, array $configs) { if (!$container->has('dispatcher')) { $container->set('dispatcher', $this->dispatcher); } parent::__invoke($container, $configs); } } platform-joomla/src/HttpClient.php 0000644 00000003424 15166657211 0013242 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Http\HttpFactory; use Joomla\Registry\Registry; use YOOtheme\Http\Response; use YOOtheme\HttpClientInterface; class HttpClient implements HttpClientInterface { /** * Execute a GET HTTP request. * * @param string $url * @param array $options * * @return Response */ public function get($url, $options = []) { $response = HttpFactory::getHttp(new Registry($options))->get($url); return (new Response($response->code, $response->headers))->write($response->body); } /** * Execute a POST HTTP request. * * @param string $url * @param string $data * @param array $options * * @return Response */ public function post($url, $data = null, $options = []) { $response = HttpFactory::getHttp(new Registry($options))->post($url, $data); return (new Response($response->code, $response->headers))->write($response->body); } /** * Execute a PUT HTTP request. * * @param string $url * @param string $data * @param array $options * * @return Response */ public function put($url, $data = null, $options = []) { $response = HttpFactory::getHttp(new Registry($options))->put($url, $data); return (new Response($response->code, $response->headers))->write($response->body); } /** * Execute a DELETE HTTP request. * * @param string $url * @param array $options * * @return Response */ public function delete($url, $options = []) { $response = HttpFactory::getHttp(new Registry($options))->delete($url); return (new Response($response->code, $response->headers))->write($response->body); } } platform-joomla/src/Dispatcher.php 0000644 00000004504 15166657211 0013252 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Application\CMSApplication; use Joomla\Event\Event; use YOOtheme\EventDispatcher; class Dispatcher extends EventDispatcher { /** * @var CMSApplication */ protected $joomla; public static $actions = [ 'onAfterCleanModuleList' => ['modules', 'subject'], 'onBeforeCompileHead' => ['subject', 'document'], 'onContentBeforeSave' => ['context', 'subject'], 'onContentPrepare' => ['context', 'subject', 'params', 'page'], 'onContentPrepareData' => ['context', 'data', 'subject'], 'onContentPrepareForm' => ['subject', 'data'], ]; /** * Constructor. * * @param CMSApplication $joomla */ public function __construct($joomla) { parent::__construct(); $this->joomla = $joomla; } /** * Adds an event listener. * * @param string $event * @param callable $listener * @param int $priority */ public function addListener($event, $listener, $priority = 0) { if (version_compare(JVERSION, '4.0', '>=')) { return $this->joomla ->getDispatcher() ->addListener( $event, fn($event) => $listener($this->prepareArguments($event)), $priority, ); } if (empty($this->listeners[$event])) { if ($event === 'onAfterCleanModuleList') { $handler = fn(&$modules) => $this->dispatch( $event, new Event($event, ['modules' => &$modules]), ); } else { $handler = function (...$arguments) use ($event) { return $this->dispatch( $event, $this->prepareArguments(new Event($event, $arguments)), ); }; } $this->joomla->registerEvent($event, $handler); } parent::addListener($event, $listener, $priority); } protected function prepareArguments($event) { foreach (static::$actions[$event->getName()] ?? [] as $i => $key) { if (!isset($event[$key])) { $event[$key] = $event[$i]; } } return $event; } } platform-joomla/src/Platform.php 0000644 00000016567 15166657211 0012764 0 ustar 00 <?php namespace YOOtheme\Joomla; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Document\Document; use Joomla\CMS\Document\HtmlDocument; use Joomla\Input\Input; use YOOtheme\Application; use YOOtheme\Arr; use YOOtheme\Http\Exception; use YOOtheme\Http\Request; use YOOtheme\Http\Response; use YOOtheme\Metadata; use YOOtheme\Path; use YOOtheme\Url; class Platform { /** * Handle application routes. */ public static function handleRoute(Application $app, CMSApplication $joomla, Input $input) { if ($input->getCmd('option') !== 'com_ajax' || !$input->get('p')) { return; } $response = null; // disable cache $joomla->set('caching', 0); // default format $input->def('format', 'raw'); // get response $joomla->registerEvent('onAfterDispatch', function () use ($app, &$response, $input) { // On administrator routes com_login is rendered for guest users if ($input->getCmd('option') !== 'com_ajax') { return; } $response = $app->run(false); }); // send response $joomla->registerEvent('onAfterRender', function () use ($joomla, &$response) { if (!$response) { return; } $isHtml = strpos($response->getContentType(), 'html'); if (!$isHtml) { // disable gzip for none html responses like binary images $joomla->set('gzip', false); } if (version_compare(JVERSION, '4.0', '>')) { $joomla->allowCache(true); $joomla->setResponse($isHtml ? $response->write($joomla->getBody()) : $response); return; } // send headers if (!headers_sent()) { $response->sendHeaders(); } // set body for none html responses if (!$isHtml) { $joomla->setBody($response->getBody()); } // set cms headers (fix issue when headers_sent() is still false) if (!headers_sent()) { $joomla->allowCache(true); $joomla->setHeader('Cache-Control', $response->getHeaderLine('Cache-Control')); $joomla->setHeader('Content-Type', $response->getContentType()); } }); } /** * Handle application errors. * * @param Request $request * @param Response $response * @param \Exception $exception * * @throws \Exception * * @return Response */ public static function handleError(Request $request, $response, $exception) { if ($exception instanceof Exception) { if (str_starts_with($request->getHeaderLine('Content-Type'), 'application/json')) { return $response->withJson($exception->getMessage()); } return $response ->write($exception->getMessage()) ->withHeader('Content-Type', 'text/plain'); } throw $exception; } /** * Callback to register assets. * * @param Metadata $metadata * @param Document $document */ public static function registerAssets(Metadata $metadata, Document $document) { if (version_compare(JVERSION, '4.0', '<')) { static::registerAssetsLegacy($metadata, $document); return; } $wa = $document->getWebAssetManager(); // Ensure WebAssetManager is not locked // This might happen if a view is rendered after the documents head has been rendered (e.g. HikaShop renders multiple Views) if (\Closure::bind(fn() => $this->locked, $wa, $wa)()) { return; } foreach ($metadata->all('style:*') as $style) { if ($style->href) { $attrs = Arr::omit($style->getAttributes(), ['version', 'href', 'rel', 'defer']); if ($style->defer && $document instanceof HtmlDocument) { $attrs = array_merge($attrs, [ 'rel' => 'preload', 'as' => 'style', 'onload' => "this.onload=null;this.rel='stylesheet'", ]); } $wa->registerAndUseStyle( $style->getName(), static::toRelativeUrl($style->href), ['version' => $style->version], $attrs, ); } elseif ($value = $style->getValue()) { $wa->addInlineStyle($value, [], Arr::omit($style->getAttributes(), ['version'])); } } foreach ($metadata->all('script:*') as $script) { if ($script->src) { $wa->registerAndUseScript( $script->getName(), static::toRelativeUrl($script->src), ['version' => $script->version], Arr::omit($script->getAttributes(), ['version', 'src']), ); } elseif ($value = $script->getValue()) { $wa->addInlineScript($value, [], Arr::omit($script->getAttributes(), ['version'])); } } } protected static function toRelativeUrl($url) { $url = Path::resolveAlias($url); if (Path::isBasePath(JPATH_ROOT, $url)) { return Path::relative(JPATH_ROOT, $url); } return $url; } /** * Callback to register assets (Joomla 3.x). * * @param Metadata $metadata * @param Document $document */ protected static function registerAssetsLegacy(Metadata $metadata, Document $document) { foreach ($metadata->all('style:*') as $style) { if ($style->href) { $attrs = Arr::omit($style->getAttributes(), ['version', 'href', 'rel', 'defer']); if ($style->defer && $document instanceof HtmlDocument) { $document->addHeadLink( htmlentities(Url::to($style->href, ['ver' => $style->version])), 'preload', 'rel', [ 'as' => 'style', 'onload' => "this.onload=null;this.rel='stylesheet'", ] + $attrs, ); } else { $document->addStyleSheet( htmlentities(Url::to($style->href)), ['version' => $style->version], $attrs, ); } } elseif ($value = $style->getValue()) { $document->addStyleDeclaration($value); } } foreach ($metadata->all('script:*') as $script) { if ($script->src) { $document->addScript( htmlentities(Url::to($script->src)), ['version' => $script->version], Arr::omit($script->getAttributes(), ['version', 'src']), ); } elseif ($value = $script->getValue()) { if ($document instanceof HtmlDocument) { $document->addCustomTag((string) $script->withAttribute('version', '')); } else { $document->addScriptDeclaration($value); } } } } } platform-joomla/bootstrap.php 0000644 00000011045 15166657211 0012410 0 ustar 00 <?php namespace YOOtheme; use Joomla\CMS\Application\CMSApplication as CMSApp; use Joomla\CMS\Application\SiteApplication as SiteApp; use Joomla\CMS\Document\Document; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Factory; use Joomla\CMS\Language\Language; use Joomla\CMS\Router\Router as JoomlaRouter; use Joomla\CMS\Router\SiteRouter; use Joomla\CMS\Session\Session; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\DI\Container; use Joomla\Database\DatabaseDriver; use Joomla\Input\Input; use YOOtheme\Joomla\ActionLoader; use YOOtheme\Joomla\Media; use YOOtheme\Joomla\Platform; use YOOtheme\Joomla\Router; Url::setBase(Uri::root(true)); Path::setAlias('~', strtr(JPATH_ROOT, '\\', '/')); return [ 'config' => function () { $joomla = Factory::getApplication(); $normalize = fn($path) => strtr($path, '\\', '/'); return [ 'app' => [ 'platform' => 'joomla', 'version' => JVERSION, 'secret' => (string) $joomla->get('secret'), 'debug' => (bool) $joomla->get('debug'), 'rootDir' => $normalize(JPATH_ROOT), 'tempDir' => $normalize($joomla->get('tmp_path', JPATH_ROOT . '/tmp')), 'adminDir' => $normalize(JPATH_ADMINISTRATOR), 'cacheDir' => $normalize($joomla->get('cache_path', JPATH_ROOT . '/cache')), 'uploadDir' => fn() => Media::getRoot(), 'isSite' => $joomla->isClient('site'), 'isAdmin' => $joomla->isClient('administrator'), ], 'req' => [ 'baseUrl' => Uri::base(true), 'rootUrl' => Uri::root(true), 'siteUrl' => rtrim(Uri::root(), '/'), ], 'locale' => [ 'rtl' => fn() => $joomla->getLanguage()->isRtl(), 'code' => fn() => strtr($joomla->getLanguage()->getTag(), '-', '_'), ], 'session' => [ 'token' => fn() => Session::getFormToken(), ], ]; }, 'events' => [ 'url.route' => [Router::class => 'generate'], 'app.error' => [Platform::class => ['handleError', -50]], ], 'actions' => [ 'onAfterRoute' => [Platform::class => ['handleRoute', -50]], 'onBeforeCompileHead' => [Platform::class => ['registerAssets', -50]], ], 'loaders' => [ 'actions' => ActionLoader::class, ], 'aliases' => [ Document::class => HtmlDocument::class, ], 'services' => array_merge( [ ActionLoader::class => '', CsrfMiddleware::class => fn(Config $config) => new CsrfMiddleware( $config('session.token'), ), HttpClientInterface::class => Joomla\HttpClient::class, Storage::class => Joomla\Storage::class, DatabaseDriver::class => [ 'factory' => fn(Container $container) => $container->get(DatabaseDriver::class), ], SiteApp::class => [ 'factory' => fn(CMSApp $joomla) => $joomla->isClient('site') ? $joomla : null, ], CMSApp::class => ['factory' => fn() => Factory::getApplication()], Container::class => ['factory' => fn() => Factory::getContainer()], Document::class => [ 'shared' => false, 'factory' => fn(CMSApp $joomla) => $joomla->getDocument(), ], Input::class => ['factory' => fn(CMSApp $joomla) => $joomla->input], Language::class => ['factory' => fn(CMSApp $joomla) => $joomla->getLanguage()], Session::class => ['factory' => fn(CMSApp $joomla) => $joomla->getSession()], SiteRouter::class => fn(Container $container) => $container->get(SiteRouter::class), User::class => [ 'shared' => false, 'factory' => fn(CMSApp $joomla) => $joomla->getIdentity(), ], ], version_compare(JVERSION, '4.0', '<') ? [ DatabaseDriver::class => [ 'factory' => function () { // Force autoloading (Type hints do not trigger autoloading) class_exists(DatabaseDriver::class); return Factory::getDbo(); }, ], SiteRouter::class => fn() => JoomlaRouter::getInstance('site'), User::class => ['shared' => false, 'factory' => fn() => Factory::getUser()], ] : [], ), ]; configuration/src/ConfigObject.php 0000644 00000004524 15166657211 0013266 0 ustar 00 <?php namespace YOOtheme; class ConfigObject extends \ArrayObject { /** * Constructor. */ public function __construct(array $values = []) { parent::__construct($values, static::ARRAY_AS_PROPS); } /** * Get all configuration values. */ public function all(): array { return $this->getArrayCopy(); } /** * Get a configuration value. * * @param mixed $default * @return mixed */ public function get(string $key, $default = null) { return Arr::get($this, $key, $default); } /** * Set a configuration value. * * @param mixed $value */ public function set(string $key, $value): void { Arr::set($this, $key, $value); } /** * Remove the last value from an array configuration value. * * @return mixed */ public function pop(string $key) { $array = $this->get($key, []); $value = array_pop($array); $this->set($key, $array); return $value; } /** * Push a value onto an array configuration value. * * @param mixed ...$values */ public function push(string $key, ...$values): void { $array = $this->get($key, []); array_push($array, ...$values); $this->set($key, $array); } /** * Remove the first value from an array configuration value. * * @return mixed */ public function shift(string $key) { $array = $this->get($key, []); $value = array_shift($array); $this->set($key, $array); return $value; } /** * Prepend a value onto an array configuration value. * * @param mixed ...$values */ public function unshift(string $key, ...$values): void { $array = $this->get($key, []); array_unshift($array, ...$values); $this->set($key, $array); } /** * Assign an array of configuration values. */ public function assign(array $values): void { $this->exchangeArray(array_replace($this->getArrayCopy(), $values)); } /** * Merge an array of configuration values recursively. */ public function merge(array $values): void { $this->exchangeArray(array_replace_recursive($this->getArrayCopy(), $values)); } } configuration/src/Config.php 0000644 00000003720 15166657211 0012134 0 ustar 00 <?php namespace YOOtheme; interface Config { /** * Gets a value (shortcut). * * @param string $index * @param mixed $default * * @return mixed */ public function __invoke($index, $default = null); /** * Gets a value. * * @param string $index * @param mixed $default * * @return mixed */ public function get($index, $default = null); /** * Sets a value. * * @param string $index * @param mixed $value * * @return $this */ public function set($index, $value); /** * Deletes a value. * * @param string $index * * @return $this */ public function del($index); /** * Adds a value array. * * @param string $index * @param array $values * @param bool $replace * * @return $this */ public function add($index, array $values = [], $replace = true); /** * Sets a value using a update callback. * * @param string $index * @param callable $callback * * @return $this */ public function update($index, callable $callback); /** * Adds an alias. * * @param string $name * @param string $index * * @return $this */ public function addAlias($name, $index); /** * Adds a file. * * @param string $index * @param string $file * @param bool $replace * * @throws \RuntimeException * * @return $this */ public function addFile($index, $file, $replace = true); /** * Adds a filter callback. * * @param string $name * @param callable $filter * * @return $this */ public function addFilter($name, callable $filter); /** * Loads a config file. * * @param string $file * * @throws \RuntimeException * * @return array */ public function loadFile($file); } configuration/src/Configuration/Node.php 0000644 00000002507 15166657211 0014425 0 ustar 00 <?php namespace YOOtheme\Configuration; abstract class Node { /** * Resolves node to their values. * * @param array $params * * @return mixed */ abstract public function resolve(array $params); /** * Compiles node as parsable string. * * @param array $params * * @return string */ abstract public function compile(array $params); /** * Resolves arguments to their values. * * @param array $arguments * @param array $params * * @return array */ public function resolveArgs(array $arguments, array $params = []) { $args = []; foreach ($arguments as $argument) { $args[] = $argument instanceof Node ? $argument->resolve($params) : $argument; } return $args; } /** * Compiles arguments as parsable string. * * @param array $arguments * @param array $params * * @return string */ public function compileArgs(array $arguments, array $params = []) { $args = []; foreach ($arguments as $argument) { $args[] = $argument instanceof Node ? $argument->compile($params) : var_export($argument, true); } return join(', ', $args); } } configuration/src/Configuration/VariableNode.php 0000644 00000001273 15166657211 0016072 0 ustar 00 <?php namespace YOOtheme\Configuration; class VariableNode extends Node { /** * @var string */ protected $name; /** * Constructor. * * @param string $name */ public function __construct($name) { $this->name = $name; } /** * @inheritdoc */ public function resolve(array $params) { $arguments = $this->resolveArgs([$this->name], $params); return $params['config']->get(...$arguments); } /** * @inheritdoc */ public function compile(array $params) { $arguments = $this->compileArgs([$this->name], $params); return "\$config->get({$arguments})"; } } configuration/src/Configuration/Repository.php 0000644 00000006631 15166657211 0015721 0 ustar 00 <?php namespace YOOtheme\Configuration; use YOOtheme\Arr; class Repository { /** * @var array */ protected $values = []; /** * @var array */ protected $aliases = []; /** * Gets a value (shortcut). * * @param string $index * @param mixed $default * * @return mixed */ public function __invoke($index, $default = null) { return $this->get($index, $default); } /** * Gets a value. * * @param string $index * @param mixed $default * * @return mixed */ public function get($index, $default = null) { $index = strtr($index, $this->aliases); return static::getValue($this->values, $index, $default); } /** * Sets a value. * * @param string $index * @param mixed $value * * @return $this */ public function set($index, $value) { $index = strtr($index, $this->aliases); Arr::set($this->values, $index, $value); return $this; } /** * Deletes a value. * * @param string $index * * @return $this */ public function del($index) { $index = strtr($index, $this->aliases); Arr::del($this->values, $index); return $this; } /** * Adds a value array. * * @param string $index * @param array $values * @param bool $replace * * @return $this */ public function add($index, array $values = [], $replace = true) { $value = $index ? $this->get($index) : $this->values; if (is_array($value)) { $arrays = $replace ? [$value, $values] : [$values, $value]; $values = array_replace_recursive(...$arrays); } if ($index) { $this->set($index, $values); } else { $this->values = $values; } return $this; } /** * Sets a value using a update callback. * * @param string $index * @param callable $callback * * @return $this */ public function update($index, callable $callback) { $index = strtr($index, $this->aliases); Arr::update($this->values, $index, $callback); return $this; } /** * Adds an alias. * * @param string $name * @param string $index * * @return $this */ public function addAlias($name, $index) { $this->aliases[$name] = $index; return $this; } /** * Gets a value from array or object. * * @param mixed $object * @param string|array|int $index * @param mixed $default * * @return mixed */ public static function getValue($object, $index, $default = null) { $index = is_array($index) ? $index : explode('.', $index); while (!is_null($key = array_shift($index))) { if ((is_array($object) || $object instanceof \ArrayAccess) && isset($object[$key])) { $object = $object[$key]; } elseif (is_object($object) && isset($object->$key)) { $object = $object->$key; } elseif (is_callable($callable = [$object, $key])) { $object = $callable(); } else { return $default; } } return $object instanceof \Closure ? $object() : $object; } } configuration/src/Configuration/StringNode.php 0000644 00000001715 15166657211 0015614 0 ustar 00 <?php namespace YOOtheme\Configuration; class StringNode extends Node { /** * @var string */ protected $format; /** * @var array */ protected $arguments; /** * Constructor. * * @param string $format * @param array $arguments */ public function __construct($format, array $arguments = []) { $this->format = $format; $this->arguments = $arguments; } /** * @inheritdoc */ public function resolve(array $params) { $arguments = array_merge([$this->format], $this->arguments); $arguments = $this->resolveArgs($arguments, $params); return sprintf(...$arguments); } /** * @inheritdoc */ public function compile(array $params) { $arguments = array_merge([$this->format], $this->arguments); $arguments = $this->compileArgs($arguments, $params); return "sprintf({$arguments})"; } } configuration/src/Configuration/Resolver.php 0000644 00000015701 15166657211 0015341 0 ustar 00 <?php namespace YOOtheme\Configuration; class Resolver { /** * @var int */ protected $ctime; /** * @var string */ protected $cache; /** * @var string|false */ protected $key = false; /** * @var array */ protected $path = []; /** * @var array */ protected $params = []; /** * @var array */ protected $callbacks = []; /** * Constructor. * * @param string $cache * @param array $params * @param array $callbacks */ public function __construct($cache, array $params = [], array $callbacks = []) { if (is_dir($cache)) { $this->cache = $cache; $this->ctime = filectime(__FILE__); } $this->params = $params; $this->callbacks = $callbacks; } /** * Resets the key and path. */ public function __clone() { $this->key = false; $this->path = []; } /** * Resolves value and evaluates it after applying callbacks. * * @param mixed $value * @param array $params * * @return mixed */ public function resolve($value, array $params = []) { $resolve = fn($value) => $value instanceof Node ? $value->resolve($params + $this->params) : $value; return $this->resolveValue($value, array_merge($this->callbacks, [$resolve])); } /** * Resolves value recursively. * * @param mixed $value * @param array $callbacks * * @return mixed */ public function resolveValue($value, array $callbacks) { // apply callbacks foreach ($callbacks as $callback) { $value = $callback($value, $this->key, $this->path); } if (is_array($value) && !empty($value)) { $array = []; $depth = count($this->path); // update path, if key was changed if ($this->key !== end($this->path)) { array_splice($this->path, -1, 1, $this->key); } foreach ($value as $key => $val) { // update key and path $this->key = $key; $this->path[$depth] = $key; // resolve recursively $val = $this->resolveValue($val, $callbacks); $array[$this->key] = $val; } // set key to last path part array_pop($this->path); $this->key = end($this->path); return $array; } return $value; } /** * Compiles a parsable string of a value after applying callbacks. * * @param mixed $value * @param array $params * * @return string */ public function compile($value, array $params = []) { $compile = fn($value) => $value instanceof Node ? $value->compile($params + $this->params) : var_export($value, true); return $this->compileValue($this->resolveValue($value, $this->callbacks), $compile); } /** * Compiles a parsable string representation of a value. * * @param mixed $value * @param callable $callback * @param int $indent * * @return string */ public function compileValue($value, ?callable $callback = null, $indent = 0) { if (is_array($value)) { $array = []; $assoc = array_values($value) !== $value; $indention = str_repeat(' ', $indent); $indentlast = $assoc ? "\n" . $indention : ''; foreach ($value as $key => $val) { $array[] = ($assoc ? "\n " . $indention . var_export($key, true) . ' => ' : '') . $this->compileValue($val, $callback, $indent + 1); } return '[' . join(', ', $array) . $indentlast . ']'; } return $callback ? $callback($value) : var_export($value, true); } /** * Loads a file. * * @param string $file * @param array $params * * @throws \RuntimeException * * @return array|null */ public function loadFile($file, array $params = []) { $params = array_merge($this->params, $params, compact('file')); $extension = pathinfo($file, PATHINFO_EXTENSION); if ($extension === 'php') { return $this->loadPhpFile($file, $params); } if ($extension === 'json') { return $this->loadJsonFile($file, $params); } throw new \RuntimeException("Unable to load file '{$file}'"); } /** * Loads a PHP file. * * @param string $file * @param array $params * * @throws \RuntimeException * * @return array */ protected function loadPhpFile($file, array $params = []) { extract($params, EXTR_SKIP); if (!is_array($value = @include $file)) { throw new \RuntimeException("Unable to load file '{$file}'"); } return $value; } /** * Loads a JSON config file. * * @param string $file * @param array $params * * @throws \RuntimeException * * @return array */ protected function loadJsonFile($file, array $params = []) { extract($params, EXTR_SKIP); $cache = sprintf( '%s/%s-%s.php', $this->cache, pathinfo($file, PATHINFO_FILENAME), hash('crc32b', $file), ); if ( $this->cache && is_file($cache) && filectime($cache) > max($this->ctime, filectime($file)) ) { return include $cache; } if (!($content = @file_get_contents($file))) { throw new \RuntimeException("Unable to load file '{$file}'"); } if (!is_array($value = @json_decode($content, true))) { throw new \RuntimeException("Invalid JSON format in '{$file}'"); } if ($this->cache && $this->writeCacheFile($cache, $value, $params)) { return include $cache; } return $this->resolve($value, $params); } /** * Writes a cache file. * * @param string $cache * @param array $value * @param array $params * * @return bool */ protected function writeCacheFile($cache, array $value, array $params = []) { $temp = uniqid("{$this->cache}/temp-" . hash('crc32b', $cache)); $data = "<?php // \$file = {$params['file']}\n\nreturn {$this->compile( $value, $params, )};\n"; if (@file_put_contents($temp, $data) && @rename($temp, $cache)) { if (function_exists('opcache_invalidate')) { opcache_invalidate($cache, true); } return true; } // remove temp file if rename failed if (file_exists($temp)) { @unlink($temp); } return false; } } configuration/src/Configuration/FilterNode.php 0000644 00000001601 15166657211 0015565 0 ustar 00 <?php namespace YOOtheme\Configuration; class FilterNode extends Node { /** * @var mixed */ protected $value; /** * @var string */ protected $filters; /** * Constructor. * * @param mixed $value * @param mixed $filters */ public function __construct($value, $filters) { $this->value = $value; $this->filters = $filters; } /** * @inheritdoc */ public function resolve(array $params) { $arguments = $this->resolveArgs([$this->filters, $this->value, $params['file']], $params); return $params['filter']->apply(...$arguments); } /** * @inheritdoc */ public function compile(array $params) { $arguments = $this->compileArgs([$this->filters, $this->value], $params); return "\$filter->apply({$arguments}, \$file)"; } } configuration/src/Configuration/Configuration.php 0000644 00000013213 15166657211 0016343 0 ustar 00 <?php namespace YOOtheme\Configuration; use YOOtheme\Config; use YOOtheme\Path; /** * A configuration with cache and value resolving. * * @example * ```json * { * // config.json * "yoo": "yoo" * } * ``` * * ```php * use YOOtheme\Configuration; * * $config = new Configuration('/cache/folder'); * $config->add('app', ['foo' => 'bar', 'woo' => ['baz' => 'baaz']]); * $config->get('app.foo'); * $config->get('app.woo.baz'); * \\=> baaz * * $config->add('app', '/config.json'); * $config->get('app.yoo'); * \\=> yoo * ``` */ class Configuration extends Repository implements Config { public const REGEX_PATH = '/^(\.\.?)\/.*/S'; public const REGEX_STRING = '/\${((?:\w+:)+)?\s*([^}]+)}/S'; /** * @var Filter */ protected $filter; /** * @var Resolver */ protected $resolver; /** * @var array */ protected $cache = []; /** * Constructor. * * @param string $cache */ public function __construct($cache = null) { $values = [ 'env' => $_ENV, 'server' => $_SERVER, 'globals' => $GLOBALS, ]; $filter = [ 'path' => [$this, 'resolvePath'], 'glob' => [$this, 'resolveGlob'], 'load' => [$this, 'resolveLoad'], ]; $params = [ 'config' => $this, 'filter' => ($this->filter = new Filter($filter)), ]; $this->values = $values; $this->resolver = new Resolver($cache, $params, [ [$this, 'matchPath'], [$this, 'matchString'], ]); } /** * @inheritdoc */ public function addFilter($name, callable $filter) { $this->filter->add($name, $filter); return $this; } /** * @inheritdoc */ public function addFile($index, $file, $replace = true) { return $this->add($index, $this->loadFile($file), $replace); } /** * @inheritdoc */ public function loadFile($file) { // load file config $config = $this->resolver->loadFile($file); $config = $this->resolveExtend($config); $config = $this->resolveImport($config); return $config; } /** * Matches paths ./some/path, ~alias/path. * * @param mixed $value * * @return mixed|Node */ public function matchPath($value) { if (!is_string($value) || !preg_match(static::REGEX_PATH, $value, $matches)) { return $value; } if (isset($this->cache[$value])) { return $this->cache[$value]; } return $this->cache[$value] = new FilterNode($this->matchString($matches[0]), 'path'); } /** * Matches string interpolations ${...}. * * @param mixed $value * * @return mixed|Node */ public function matchString($value) { if ( !is_string($value) || !preg_match_all(static::REGEX_STRING, $value, $matches, PREG_SET_ORDER) ) { return $value; } if (isset($this->cache[$value])) { return $this->cache[$value]; } $replace = $arguments = []; foreach ($matches as $match) { [$search, $filter, $val] = $match; $replace[$search] = '%s'; $arguments[] = $filter ? new FilterNode($val, rtrim($filter, ':')) : new VariableNode($val); } $format = strtr($value, $replace + ['%' => '%%']); return $this->cache[$value] = $format !== '%s' ? new StringNode($format, $arguments) : $arguments[0]; } /** * Resolves and evaluates values. * * @param mixed $value * @param array $params * * @return mixed */ public function resolve($value, array $params = []) { return $this->resolver->resolve($value, $params); } /** * Resolves "path: dir/myfile.php" filter. * * @param string $value * @param string $file * * @return string */ public function resolvePath($value, $file) { return Path::resolve(dirname($file), $value); } /** * Resolves "glob: dir/file*.php" filter. * * @param string $value * @param string $file * * @return string[] */ public function resolveGlob($value, $file) { return glob(Path::resolve(dirname($file), $value)) ?: []; } /** * Resolves "load: dir/file.php" filter. * * @param string $value * @param string $file * * @return array */ public function resolveLoad($value, $file) { return $this->loadFile(Path::resolve(dirname($file), $value)); } /** * Resolves "@extend" in config array. * * @param array $config * * @throws \RuntimeException * * @return array */ protected function resolveExtend(array $config) { $extends = $config['@extend'] ?? []; foreach ((array) $extends as $extend) { $config = array_replace_recursive($this->loadFile($extend), $config); } unset($config['@extend']); return $config; } /** * Resolves "@import" in config array. * * @param array $config * * @throws \RuntimeException * * @return array */ protected function resolveImport(array $config) { $imports = $config['@import'] ?? []; foreach ((array) $imports as $import) { $config = array_replace_recursive($config, $this->loadFile($import)); } unset($config['@import']); return $config; } } configuration/src/Configuration/Filter.php 0000644 00000002403 15166657211 0014760 0 ustar 00 <?php namespace YOOtheme\Configuration; class Filter { /** * @var array */ protected $filters = []; /** * Constructor. * * @param array $filters */ public function __construct(array $filters = []) { foreach ($filters as $name => $filter) { $this->add($name, $filter); } } /** * Adds a filter function. * * @param string $name * @param callable $filter * * @return $this */ public function add($name, callable $filter) { $this->filters[$name] = $filter; return $this; } /** * Applies filters to a value. * * @param mixed $value * @param mixed $filters * @param array $arguments * * @throws \RuntimeException * * @return mixed */ public function apply($filters, $value, ...$arguments) { if (is_string($filters)) { $filters = explode('|', $filters); } foreach ($filters as $name) { if (!isset($this->filters[$name])) { throw new \RuntimeException("Undefined filter '{$name}'"); } $value = $this->filters[$name]($value, ...$arguments); } return $value; } } builder-joomla-source/config/customizer.json 0000644 00000001122 15166657211 0015321 0 ustar 00 { "sources": { "articleOrderOptions": [ { "text": "Published", "value": "publish_up" }, { "text": "Unpublished", "value": "publish_down" }, { "text": "Created", "value": "created" }, { "text": "Modified", "value": "modified" }, { "text": "Alphabetical", "value": "title" }, { "text": "Hits", "value": "hits" }, { "text": "Article Order", "value": "ordering" }, { "text": "Featured Articles Order", "value": "front" }, { "text": "Random", "value": "rand" } ] } } builder-joomla-source/updates.php 0000644 00000013540 15166657211 0013142 0 ustar 00 <?php namespace YOOtheme; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; return [ '3.0.0-beta.7.1' => function ($node) { if ( str_starts_with($node->source->query->name ?? '', 'customArticle') && isset($node->source->query->arguments->featured) ) { $node->source->query->arguments->featured = empty( $node->source->query->arguments->featured ) ? '' : 'only'; } }, '2.6.0-beta.0.1' => function ($node) { if (class_exists(FieldsHelper::class) && isset($node->source->props)) { static $fields; if ($fields === null) { $fields = FieldsHelper::getFields('', null, false, null, true); } // update media fields to new MediaFieldType foreach ($node->source->props as $prop) { if (str_contains($prop->name ?? '', 'field.')) { foreach ($fields as $field) { if ( str_ends_with($prop->name, 'field.' . strtr($field->name, '-', '_')) && $field->type === 'media' ) { $prop->name .= '.imagefile'; } } $prop->name = strtr($prop->name, '-', '_'); } } if (str_contains($node->source->query->field->name ?? '', 'field.')) { foreach ($fields as $field) { if ( str_ends_with( $node->source->query->field->name, 'field.' . strtr($field->name, '-', '_'), ) ) { if ($field->type === 'subform') { foreach ($node->source->props as $prop) { $prop->name = Str::snakeCase($prop->name); } foreach ((array) $field->fieldparams->get('options', []) as $option) { foreach ($fields as $subField) { if ( $subField->id === $option->customfield && $subField->type === 'media' ) { $prefix = "{$field->name}_"; foreach ($node->source->props as $prop) { if ( $prop->name === strtr($subField->name, '-', '_') || $prop->name === strtr( substr($subField->name, strlen($prefix)), '-', '_', ) ) { $prop->name .= '.imagefile'; } } } } } } if ($field->type === 'repeatable') { foreach ((array) $field->fieldparams->get('fields', []) as $subField) { if ($subField->fieldtype === 'media') { foreach ($node->source->props ?? [] as $prop) { if ($prop->name === Str::snakeCase($subField->fieldname)) { $prop->name .= '.imagefile'; } } } } } } } } } }, '2.4.0-beta.5' => function ($node) { // refactor show_category argument into show_taxonomy argument foreach ($node->source->props ?? [] as $prop) { if ( isset($prop->name) && $prop->name === 'metaString' && isset($prop->arguments->show_category) ) { /** @var object $arguments */ $arguments = $prop->arguments; $arguments->show_taxonomy = $arguments->show_category ? 'category' : ''; unset($arguments->show_category); } } }, '2.2.0-beta.0.1' => function ($node) { static $fields; if (class_exists(FieldsHelper::class) && is_null($fields)) { $fields = array_column( FieldsHelper::getFields('', null, false, null, true), 'type', 'name', ); } if ( isset($node->source->query->field->name) && in_array('field', $field = explode('.', $node->source->query->field->name)) ) { $node->source->query->field->name = strtr($node->source->query->field->name, '-', '_'); // snake case repeatable field names if (isset($fields[end($field)]) && $fields[end($field)] === 'repeatable') { foreach ($node->source->props ?? [] as $prop) { $prop->name = Str::snakeCase($prop->name); } } } // snake case custom field names foreach ($node->source->props ?? [] as $prop) { if (isset($prop->name) && in_array('field', explode('.', $prop->name))) { $prop->name = strtr($prop->name, '-', '_'); } } }, ]; builder-joomla-source/templates/meta.php 0000644 00000005303 15166657211 0014417 0 ustar 00 <?php use Joomla\CMS\Categories\Categories; use Joomla\CMS\Factory; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Router\Route; use Joomla\Component\Content\Site\Helper\RouteHelper; use YOOtheme\Builder\Joomla\Source\UserHelper; use YOOtheme\Path; $author = $published = $category = $tag = ''; // Author if ($args['show_author']) { $author = $article->created_by_alias ?: $article->author; if (!isset($article->contact_link)) { $article->contact_link = UserHelper::getContactLink($article->created_by); } if (!empty($article->contact_link)) { $author = HTMLHelper::_('link', $article->contact_link, $author); } } // Publish date if ($args['show_publish_date'] && !empty($article->publish_up) && $article->publish_up !== Factory::getDbo()->getNullDate()) { $published = HTMLHelper::_('date', $article->publish_up, $args['date_format'] ?: Text::_('DATE_FORMAT_LC3')); $published = '<time datetime="' . HTMLHelper::_('date', $article->publish_up, 'c') . "\">{$published}</time>"; } // Category if ($args['show_taxonomy'] === 'category') { $category = $article->category_title; if ($article->catid) { if (!$category) { $category = Categories::getInstance('content')->get($article->catid); if ($category) { $category = $category->title; } } $category = HTMLHelper::_('link', Route::_(RouteHelper::getCategoryRoute($article->catid)), $category); } } // Tag if ($tags && $args['show_taxonomy'] === 'tag') { $tag = $view->render(Path::get('./tags', __DIR__), [ 'tags' => $tags, 'args' => [ 'separator' => ', ', 'show_link' => true, 'link_style' => $args['link_style'], ], ]); } if (!$published && !$author && !$category && !$tag) { return; } if ($args['link_style']) { echo "<span class=\"uk-{$args['link_style']}\">"; } switch ($args['format']) { case 'list': echo implode(" {$args['separator']} ", array_filter([$published, $author, $category, $tag])); break; default: // sentence if ($author && $published) { Text::printf('TPL_YOOTHEME_META_AUTHOR_DATE', $author, $published); } elseif ($author) { Text::printf('TPL_YOOTHEME_META_AUTHOR', $author); } elseif ($published) { Text::printf('TPL_YOOTHEME_META_DATE', $published); } if ($category) { echo ' '; Text::printf('TPL_YOOTHEME_META_CATEGORY', $category); } elseif ($tag) { echo ' '; Text::printf('TPL_YOOTHEME_META_TAG', $tag); } } if ($args['link_style']) { echo '</span>'; } builder-joomla-source/templates/tags.php 0000644 00000001023 15166657211 0014422 0 ustar 00 <?php use Joomla\Component\Tags\Site\Helper\RouteHelper; if ($args['show_link'] && $args['link_style']) { echo '<span class="uk-' . $args['link_style'] . '">'; } echo implode($args['separator'], array_map(function ($tag) use ($args) { if (empty($args['show_link'])) { return $tag->title; } $route = RouteHelper::getTagRoute("{$tag->tag_id}:{$tag->alias}"); return "<a href=\"{$route}\">{$tag->title}</a>"; }, $tags ?: [])); if ($args['show_link'] && $args['link_style']) { echo '</span>'; } builder-joomla-source/src/UserHelper.php 0000644 00000012374 15166657211 0014346 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\CMS\Access\Access; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Helper\UserGroupsHelper; use Joomla\CMS\Language\Multilanguage; use Joomla\CMS\MVC\Model\BaseDatabaseModel; use Joomla\CMS\Router\Route; use Joomla\Component\Contact\Site\Helper\RouteHelper; use Joomla\Component\Users\Administrator\Extension\UsersComponent; use Joomla\Component\Users\Administrator\Model\UsersModel; use Joomla\Database\DatabaseDriver; use function YOOtheme\app; class UserHelper { /** * Gets the user's contact. * * @param int $id * * @return object|null */ public static function getContact($id) { static $contacts = []; if (!isset($contacts[$id])) { /** @var DatabaseDriver $db */ $db = app(DatabaseDriver::class); $query = sprintf( 'SELECT id AS contactid, alias, catid FROM #__contact_details WHERE published = 1 AND user_id = %d', $id, ); if (Multilanguage::isEnabled() === true) { $lang = Factory::getApplication()->getLanguage(); $query .= sprintf( ' AND (language IN (%s, %s) OR language IS NULL)', $db->quote($lang->getTag()), $db->quote('*'), ); } $query .= ' ORDER BY id DESC LIMIT 1'; $contacts[$id] = $db->setQuery($query)->loadObject() ?: false; } return $contacts[$id] ?: null; } /** * Query users. * * @param array $args * * @return array */ public static function queryContacts(array $args = []) { $model = new ContactsModel(['ignore_request' => true]); $model->setState('params', ComponentHelper::getParams('com_contact')); $model->setState('filter.published', 1); $props = [ 'offset' => 'list.start', 'limit' => 'list.limit', 'order' => 'list.ordering', 'order_direction' => 'list.direction', 'catid' => 'filter.category_id', 'tag' => 'filter.tags', 'include_child_categories' => 'filter.include_child_categories', 'include_child_tags' => 'filter.include_child_tags', ]; foreach (array_intersect_key($props, $args) as $key => $prop) { $model->setState($prop, $args[$key]); } return $model->getItems(); } /** * Gets the user's contact link. * * @param int $id * * @return string|null */ public static function getContactLink($id) { if (!($contact = self::getContact($id))) { return null; } return Route::_(RouteHelper::getContactRoute($contact->contactid, (int) $contact->catid)); } /** * Query users. * * @param array $args * * @return array */ public static function query(array $args = []) { /** @var UsersModel $model */ $model = static::getModel(); $model->setState('params', ComponentHelper::getParams('com_users')); $model->setState('filter.active', true); $model->setState('filter.state', 0); $props = [ 'offset' => 'list.start', 'limit' => 'list.limit', 'order' => 'list.ordering', 'order_direction' => 'list.direction', 'groups' => 'filter.groups', ]; if (empty($args['groups'])) { unset($args['groups']); } foreach (array_intersect_key($props, $args) as $key => $prop) { $model->setState($prop, $args[$key]); } return $model->getItems(); } public static function getAuthorList() { /** @var DatabaseDriver $db */ $db = app(DatabaseDriver::class); $query = sprintf( 'SELECT DISTINCT(m.user_id) AS value, u.name AS text FROM #__usergroups AS ug1 JOIN #__usergroups AS ug2 ON ug2.lft >= ug1.lft AND ug1.rgt >= ug2.rgt JOIN #__user_usergroup_map AS m ON ug2.id=m.group_id JOIN #__users AS u ON u.id=m.user_id WHERE ug1.id IN (%s)', join( ',', array_filter( array_map(fn($group) => $group->id, UserGroupsHelper::getInstance()->getAll()), fn($id) => Access::checkGroup($id, 'core.create', 'com_content') || Access::checkGroup($id, 'core.admin'), ), ), ); return $db->setQuery($query)->loadObjectList(); } protected static function getModel() { if (version_compare(JVERSION, '4.0', '<')) { BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_users/models'); return BaseDatabaseModel::getInstance('users', 'UsersModel', [ 'ignore_request' => true, ]); } /** @var UsersComponent $component */ $component = Factory::getApplication()->bootComponent('com_users'); return $component ->getMVCFactory() ->createModel('users', 'administrator', ['ignore_request' => true]); } } builder-joomla-source/src/ContactsModel.php 0000644 00000004347 15166657211 0015030 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\Component\Contact\Administrator\Model\ContactsModel as BaseModel; class ContactsModel extends BaseModel { protected function getListQuery() { $categoryId = $this->getState('filter.category_id'); $includeChildCategories = $this->getState('filter.include_child_categories'); if ($categoryId && $includeChildCategories) { $this->setState('filter.category_id'); } $tags = (array) $this->getState('filter.tags', []); $includeChildTags = $this->getState('filter.include_child_tags'); if ($tags && !$includeChildTags && !version_compare(JVERSION, '4.0', '<')) { $this->setState('filter.tag', $tags); } $query = parent::getListQuery(); if ($categoryId && $includeChildCategories) { $categories = implode(',', array_map('intval', (array) $categoryId)); $where = []; if ($includeChildCategories === 'include') { $where[] = "a.catid IN ({$categories})"; } $subQuery = "SELECT sub.id FROM #__categories AS sub JOIN #__categories AS this ON sub.lft > this.lft AND sub.rgt < this.rgt WHERE this.id IN ({$categories})"; $where[] = "a.catid IN ({$subQuery})"; $query->andWhere($where); } if ($tags && ($includeChildTags || version_compare(JVERSION, '4.0', '<'))) { $tags = implode(',', array_map('intval', $tags)); $where = []; if (!$includeChildTags || $includeChildTags === 'include') { $subQuery = "SELECT content_item_id FROM #__contentitem_tag_map WHERE tag_id IN ({$tags}) AND type_alias = 'com_contact.contact'"; $where[] = "a.id IN ({$subQuery})"; } if ($includeChildTags) { $subQuery = "SELECT map.content_item_id FROM #__tags AS sub JOIN #__tags AS this ON sub.lft > this.lft AND sub.rgt < this.rgt JOIN #__contentitem_tag_map as map ON sub.id = map.tag_id WHERE this.id IN ({$tags}) and map.type_alias = 'com_contact.contact'"; $where[] = "a.id IN ({$subQuery})"; } $query->andWhere($where); } return $query; } } builder-joomla-source/src/TagModel.php 0000644 00000001640 15166657211 0013756 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\Component\Tags\Site\Model\TagModel as BaseModel; use Joomla\Database\DatabaseDriver; use function YOOtheme\app; class TagModel extends BaseModel { protected function getListQuery() { $query = parent::getListQuery(); $ordering = $this->getState('list.ordering', ''); if ($ordering === 'c.rand') { $query->clear('order'); $query->order(app(DatabaseDriver::class)->getQuery(true)->Rand()); } elseif ($this->getState('list.alphanum')) { $ordering = $this->getState('list.ordering', 'c.core_ordering'); $order = $this->getState('list.direction', 'ASC'); $query->clear('order'); $query->order( "(substr({$ordering}, 1, 1) > '9') {$order}, {$ordering}+0 {$order}, {$ordering} {$order}", ); } return $query; } } builder-joomla-source/src/Listener/LoadTemplate.php 0000644 00000004555 15166657211 0016432 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use Joomla\CMS\Language\Text; use YOOtheme\Builder; use YOOtheme\Builder\Templates\TemplateHelper; use YOOtheme\Config; use YOOtheme\Event; use function YOOtheme\app; class LoadTemplate { public Config $config; public Builder $builder; public function __construct(Config $config, Builder $builder) { $this->config = $config; $this->builder = $builder; } public function handle($event): void { [$htmlView, $tpl] = $event->getArguments(); $view = Event::emit('builder.template', $htmlView, $tpl); if (empty($view['type'])) { return; } // get template from customizer request? $template = $this->config->get('req.customizer.template'); if ($this->config->get('app.isCustomizer')) { $this->config->set('customizer.view', $view['type']); } if ($this->config->get('app.isBuilder') && empty($template)) { return; } // get visible template $visible = app(TemplateHelper::class)->match($view); // set template identifier if ($this->config->get('app.isCustomizer')) { $this->config->add('customizer.template', [ 'id' => $template['id'] ?? null, 'visible' => $visible['id'] ?? null, ]); } if ($template ??= $visible) { // get output from builder $output = $this->builder->render( json_encode($template['layout'] ?? []), ($view['params'] ?? []) + [ 'prefix' => "template-{$template['id']}", 'template' => $template['type'], ], ); // append frontend edit button? if ($output && isset($view['editUrl']) && !$this->config->get('app.isCustomizer')) { $output .= "<a style=\"position: fixed!important\" class=\"uk-position-medium uk-position-bottom-right uk-position-z-index uk-button uk-button-primary\" href=\"{$view['editUrl']}\">" . Text::_('JACTION_EDIT') . '</a>'; } $htmlView->set('_output', $output ?? ''); $this->config->set('app.isBuilder', true); $this->config->set('app.template', $template); } } } builder-joomla-source/src/Listener/LoadBuilderConfig.php 0000644 00000033214 15166657211 0017365 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Multilanguage; use Joomla\Database\DatabaseDriver; use YOOtheme\Builder\BuilderConfig; use YOOtheme\Builder\Joomla\Source\UserHelper; use function YOOtheme\trans; class LoadBuilderConfig { public DatabaseDriver $db; public function __construct(DatabaseDriver $db) { $this->db = $db; } /** * @param BuilderConfig $config */ public function handle($config): void { $config->merge([ 'languages' => array_map( fn($lang) => [ 'value' => $lang->value == '*' ? '' : strtolower($lang->value), 'text' => $lang->text, ], Multilanguage::isEnabled() ? HTMLHelper::_('contentlanguage.existing', true, true) : [], ), 'templates' => static::getTemplates(), 'categories' => array_map( fn($category) => ['value' => (string) $category->value, 'text' => $category->text], HTMLHelper::_('category.options', 'com_content'), ), 'root_categories' => array_map( fn($category) => ['value' => (string) $category->id, 'text' => $category->title], Categories::getInstance('content')->get()->getChildren(), ), 'com_contact.categories' => array_map( fn($category) => ['value' => (string) $category->value, 'text' => $category->text], HTMLHelper::_('category.options', 'com_contact'), ), 'com_finder.filters' => array_map( fn($filter) => ['value' => $filter->value, 'text' => $filter->text], $this->getSearchFilters(), ), 'tags' => array_map( fn($tag) => ['value' => (string) $tag->value, 'text' => $tag->text], HTMLHelper::_('tag.options'), ), 'authors' => array_map( fn($user) => ['value' => (string) $user->value, 'text' => $user->text], UserHelper::getAuthorList(), ), 'usergroups' => array_map( fn($group) => ['value' => (string) $group->value, 'text' => $group->text], HTMLHelper::_('user.groups'), ), ]); } protected static function getTemplates(): array { return array_merge( [ 'com_content.article' => [ 'label' => trans('Single Article'), 'fieldset' => [ 'default' => [ 'fields' => [ 'catid' => static::getCategoryField(), 'include_child_categories' => static::getIncludeChildCategoriesField( trans( 'The template is only assigned to articles from the selected categories. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple categories.', ), ), 'tag' => static::getTagField(), 'include_child_tags' => static::getIncludeChildTagsField( trans( 'The template is only assigned to articles with the selected tags. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple tags.', ), ), 'lang' => static::getLanguageField(), ], ], ], ], 'com_content.category' => [ 'label' => trans('Category Blog'), 'fieldset' => [ 'default' => [ 'fields' => [ 'catid' => static::getCategoryField(), 'include_child_categories' => static::getIncludeChildCategoriesField( trans( 'The template is only assigned to the selected categories. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple categories.', ), ), 'tag' => static::getTagField(), 'include_child_tags' => static::getIncludeChildTagsField( trans( 'The template is only assigned to categories if the selected tags are set in the menu item. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple tags.', ), ), 'pages' => static::getPagesField(), 'lang' => static::getLanguageField(), ], ], ], ], 'com_content.featured' => [ 'label' => trans('Featured Articles'), 'fieldset' => [ 'default' => [ 'fields' => [ 'pages' => static::getPagesField(), 'lang' => static::getLanguageField(), ], ], ], ], 'com_tags.tag' => [ 'label' => trans('Tagged Items'), 'fieldset' => [ 'default' => [ 'fields' => [ 'tag' => static::getTagField(), 'include_child_tags' => static::getIncludeChildTagsField( trans( 'The template is only assigned to the view if the selected tags are set in the menu item. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple tags.', ), ), 'pages' => static::getPagesField(), 'lang' => static::getLanguageField(), ], ], ], ], 'com_tags.tags' => [ 'label' => trans('List All Tags'), 'fieldset' => [ 'default' => [ 'fields' => [ 'pages' => static::getPagesField(), 'lang' => static::getLanguageField(), ], ], ], ], 'com_contact.contact' => [ 'label' => trans('Single Contact'), 'fieldset' => [ 'default' => [ 'fields' => [ 'catid' => static::getCategoryField('com_contact.categories'), 'include_child_categories' => static::getIncludeChildCategoriesField( trans( 'The template is only assigned to contacts from the selected categories. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple categories.', ), ), 'tag' => static::getTagField(), 'include_child_tags' => static::getIncludeChildTagsField( trans( 'The template is only assigned to contacts with the selected tags. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple tags.', ), ), 'lang' => static::getLanguageField(), ], ], ], ], ], ComponentHelper::isEnabled('com_search') ? [ 'com_search.search' => [ 'label' => trans('Search'), 'fieldset' => [ 'default' => [ 'fields' => [ 'lang' => static::getLanguageField(), ], ], ], ], ] : [], [ 'com_finder.search' => [ 'label' => trans('Smart Search'), 'fieldset' => [ 'default' => [ 'fields' => [ 'pages' => static::getPagesField(), 'lang' => static::getLanguageField(), ], ], ], ], '_search' => [ 'label' => trans('Live Search'), 'fieldset' => [ 'default' => [ 'fields' => [ 'lang' => static::getLanguageField(), ], ], 'params' => [ 'fields' => [ 'live_search_results' => [ 'label' => trans('Items per Page'), 'type' => 'number', 'description' => trans('Set the number of items per page.'), 'attrs' => [ 'placeholder' => trans('Default'), 'min' => '1', 'max' => LoadSearchTemplate::MAX_ITEMS_PER_PAGE, ], ], ], ], ], ], 'error-404' => [ 'label' => trans('Error 404'), 'fieldset' => [ 'default' => [ 'fields' => [ 'lang' => static::getLanguageField(), ], ], ], ], ], ); } protected function getSearchFilters(): array { $query = $this->db ->getQuery(true) ->select('f.title AS text, f.filter_id AS value') ->from($this->db->quoteName('#__finder_filters') . ' AS f') ->where('f.state = 1') ->order('f.title ASC'); return $this->db->setQuery($query)->loadObjectList(); } protected static function getCategoryField($categories = 'categories'): array { return [ 'label' => trans('Limit by Categories'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => "yootheme.builder['{$categories}']"]], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ]; } protected static function getTagField(): array { return [ 'label' => trans('Limit by Tags'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.tags']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ]; } protected static function getIncludeChildCategoriesField($description): array { return [ 'type' => 'select', 'description' => $description, 'options' => [ trans('Exclude child categories') => '', trans('Include child categories') => 'include', trans('Only include child categories') => 'only', ], ]; } protected static function getIncludeChildTagsField($description): array { return [ 'type' => 'select', 'description' => $description, 'options' => [ trans('Exclude child tags') => '', trans('Include child tags') => 'include', trans('Only include child tags') => 'only', ], ]; } protected static function getLanguageField(): array { return [ 'label' => trans('Limit by Language'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [['evaluate' => 'yootheme.builder.languages']], 'show' => 'yootheme.builder.languages.length > 1 || lang', ]; } protected static function getPagesField(): array { return [ 'label' => trans('Limit by Page Number'), 'description' => trans('The template is only assigned to the selected pages.'), 'type' => 'select', 'options' => [ trans('All pages') => '', trans('First page') => 'first', trans('All except first page') => 'except_first', ], ]; } } builder-joomla-source/src/Listener/LoadSourceTypes.php 0000644 00000004504 15166657211 0017136 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use YOOtheme\Builder\Joomla\Source\Type; use YOOtheme\Builder\Source\Type\RequestType; use YOOtheme\Builder\Source\Type\SiteType; class LoadSourceTypes { public static function handle($source): void { $query = [ Type\ArticleQueryType::config(), Type\CategoryQueryType::config(), Type\ContactQueryType::config(), Type\ArticlesQueryType::config(), Type\SmartSearchQueryType::config(), Type\SmartSearchItemsQueryType::config(), Type\TagsQueryType::config(), Type\TagItemsQueryType::config(), Type\CustomArticleQueryType::config(), Type\CustomArticlesQueryType::config(), Type\CustomCategoryQueryType::config(), Type\CustomCategoriesQueryType::config(), Type\CustomTagQueryType::config(), Type\CustomTagsQueryType::config(), Type\CustomMenuItemQueryType::config(), Type\CustomMenuItemsQueryType::config(), Type\CustomUserQueryType::config(), Type\CustomUsersQueryType::config(), Type\SiteQueryType::config(), ]; $types = [ ['Article', Type\ArticleType::config()], ['ArticleEvent', Type\ArticleEventType::config()], ['ArticleImages', Type\ArticleImagesType::config()], ['ArticleUrls', Type\ArticleUrlsType::config()], ['Category', Type\CategoryType::config()], ['CategoryParams', Type\CategoryParamsType::config()], ['Contact', Type\ContactType::config()], ['Event', Type\EventType::config()], ['Images', Type\ImagesType::config()], ['MenuItem', Type\MenuItemType::config()], ['Request', RequestType::config()], ['Site', SiteType::config()], ['SmartSearch', Type\SmartSearchType::config()], ['SmartSearchItem', Type\SmartSearchItemType::config()], ['Tag', Type\TagType::config()], ['TagItem', Type\TagItemType::config()], ['User', Type\UserType::config()], ]; foreach ($query as $args) { $source->queryType($args); } foreach ($types as $args) { $source->objectType(...$args); } } } builder-joomla-source/src/Listener/MatchTemplate.php 0000644 00000020222 15166657211 0016574 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Document\Document; use Joomla\CMS\Factory; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; use Joomla\Component\Content\Site\Helper\RouteHelper; use Joomla\Database\DatabaseDriver; use YOOtheme\Builder\Joomla\Source\TagHelper; class MatchTemplate { public string $language; protected DatabaseDriver $db; public function __construct(?Document $document, DatabaseDriver $db) { $this->language = $document->language ?? 'en-gb'; $this->db = $db; } public function handle($view, $tpl): ?array { if ($tpl) { return null; } $layout = $view->getLayout(); $context = $view->get('context'); if ($context === 'com_content.article' && $layout === 'default') { $item = $view->get('item'); return [ 'type' => $context, 'query' => [ 'catid' => fn($ids, $query) => $this->matchCategory( $item->catid, $ids, $query['include_child_categories'] ?? false, 'content', ), 'tag' => fn($ids, $query) => $this->matchTag( $item->tags->itemTags, $ids, $query['include_child_tags'] ?? false, ), 'lang' => $this->language, ], 'params' => ['item' => $item], 'editUrl' => $item->params->get('access-edit') ? Route::_( RouteHelper::getFormRoute($item->id) . '&return=' . base64_encode(Uri::getInstance()), ) : null, ]; } if ($context === 'com_content.category' && $layout === 'blog') { $category = $view->get('category'); $pagination = $view->get('pagination'); return [ 'type' => $context, 'query' => [ 'catid' => fn($ids, $query) => $this->matchCategory( $category, $ids, $query['include_child_categories'] ?? false, 'content', ), 'tag' => fn($ids, $query) => $this->matchTag( TagHelper::get($view->get('State')->get('filter.tag', [])), $ids, $query['include_child_tags'] ?? false, ), 'pages' => $pagination->pagesCurrent === 1 ? 'first' : 'except_first', 'lang' => $this->language, ], 'params' => [ 'category' => $category, 'items' => array_merge($view->get('lead_items'), $view->get('intro_items')), 'pagination' => $pagination, ], ]; } if ($context === 'com_content.featured') { $pagination = $view->get('pagination'); return [ 'type' => $context, 'query' => [ 'pages' => $pagination->pagesCurrent === 1 ? 'first' : 'except_first', 'lang' => $this->language, ], 'params' => ['items' => $view->get('items'), 'pagination' => $pagination], ]; } if ($context === 'com_tags.tag') { $pagination = $view->get('pagination'); $tags = $view->get('item'); return [ 'type' => $context, 'query' => [ 'tag' => fn($ids, $query) => $this->matchTag( $tags, $ids, $query['include_child_tags'] ?? false, ), 'pages' => $pagination->pagesCurrent === 1 ? 'first' : 'except_first', 'lang' => $this->language, ], 'params' => [ 'tags' => $tags, 'items' => $view->get('items'), 'pagination' => $pagination, ], ]; } if ($context === 'com_tags.tags') { $pagination = $view->get('pagination'); return [ 'type' => $context, 'query' => [ 'lang' => $this->language, 'pages' => $pagination->pagesCurrent === 1 ? 'first' : 'except_first', ], 'params' => [ 'tags' => $view->get('items'), 'pagination' => $pagination, ], ]; } if ($context === 'com_contact.contact') { $item = $view->get('item'); return [ 'type' => $context, 'query' => [ 'catid' => fn($ids, $query) => $this->matchCategory( $item->catid, $ids, $query['include_child_categories'] ?? false, 'contact', ), 'tag' => fn($ids, $query) => $this->matchTag( $item->tags->itemTags, $ids, $query['include_child_tags'] ?? false, ), 'lang' => $this->language, ], 'params' => ['item' => $item], ]; } if ($context === 'com_finder.search') { $pagination = $view->get('pagination'); $input = Factory::getApplication()->input; return [ 'type' => $input->getBool('live-search') ? '_search' : $context, 'query' => [ 'pages' => $pagination->pagesCurrent === 1 ? 'first' : 'except_first', 'lang' => $this->language, ], 'params' => [ 'search' => [ 'searchword' => $view->get('query')->input ?: '', 'total' => $pagination->total, ], 'items' => $view->get('results') ?? [], 'pagination' => $pagination, ], ]; } if ($view->getName() === '404') { return [ 'type' => 'error-404', 'query' => ['lang' => $this->language], ]; } return null; } protected function matchCategory($category, $categoryIds, $includeChildren, $extension): bool { $match = in_array(is_object($category) ? $category->id : $category, $categoryIds); if (!$includeChildren || ($match && $includeChildren === 'include')) { return $match; } if ($match && $includeChildren === 'only') { return false; } if (!is_object($category)) { $category = Categories::getInstance($extension)->get($category); } return $category && array_intersect(array_keys($category->getPath()), $categoryIds); } protected function matchTag($tags, $tagIds, $includeChildren): bool { $match = (bool) array_intersect(array_column($tags, 'id'), $tagIds); if (!$includeChildren || ($match && $includeChildren === 'include')) { return $match; } if ($match && $includeChildren === 'only') { return false; } if (array_intersect(array_column($tags, 'parent_id'), $tagIds)) { return true; } $tags = array_filter($tags, fn($tag) => substr_count($tag->path, '/') >= 2); if (!$tags) { return false; } $query = sprintf( 'SELECT 1 FROM #__tags WHERE id IN (%s) AND (%s) LIMIT 1', join(',', $tagIds), join(' OR ', array_map(fn($tag) => "(lft < {$tag->lft} AND rgt > {$tag->rgt})", $tags)), ); return (bool) $this->db->setQuery($query)->loadResult(); } } builder-joomla-source/src/Listener/LoadNotFound.php 0000644 00000002446 15166657211 0016410 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\MVC\View\HtmlView; use YOOtheme\Config; use YOOtheme\Theme\Joomla\ThemeLoader; use function YOOtheme\app; class LoadNotFound { public Config $config; public CMSApplication $joomla; public function __construct(Config $config, CMSApplication $joomla) { $this->config = $config; $this->joomla = $joomla; } public function handle($event): void { [$result] = $event->getArguments(); if (!$this->config->get('theme.template')) { app()->call([ThemeLoader::class, 'initTheme']); } $view = new HtmlView(['name' => '404', 'base_path' => '', 'template_path' => '']); $this->joomla->triggerEvent('onLoadTemplate', [$view, null]); if ($this->config->get('app.isCustomizer')) { $result['customizer'] = sprintf( '<script id="customizer-data">window.yootheme = window.yootheme || {}; var $customizer = yootheme.customizer = JSON.parse(atob("%s"));</script>', base64_encode(json_encode($this->config->get('customizer'))), ); } if (!empty($view->get('_output'))) { $result['404'] = $view->get('_output'); } } } builder-joomla-source/src/Listener/LoadSearchTemplate.php 0000644 00000003374 15166657211 0017556 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Application\SiteApplication; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Document\RawDocument; use YOOtheme\Builder\Templates\TemplateHelper; use YOOtheme\Config; use function YOOtheme\app; class LoadSearchTemplate { public const MAX_ITEMS_PER_PAGE = 100; public Config $config; public CMSApplication $joomla; public function __construct(Config $config, CMSApplication $joomla) { $this->config = $config; $this->joomla = $joomla; } public function afterInitialiseDocument(): void { if ( $this->joomla instanceof SiteApplication && $this->joomla->input->getCmd('option') === 'com_finder' && $this->joomla->input->getCmd('view') === 'search' && $this->joomla->input->getBool('live-search') && ($template = app(TemplateHelper::class)->match([ 'type' => '_search', 'query' => ['lang' => $this->joomla->getDocument()->language], ])) && ($results = (int) ($template['params']['live_search_results'] ?? 0)) ) { $this->joomla ->getParams() ->set('list_limit', (string) min($results, static::MAX_ITEMS_PER_PAGE)); } } public function afterDispatch(): void { $document = $this->joomla->getDocument(); if ( $document instanceof HtmlDocument && $this->config->get('app.template.type') === '_search' ) { $doc = new RawDocument(); $doc->setBuffer($document->getBuffer('component') ?? ''); $this->joomla->loadDocument($doc); } } } builder-joomla-source/src/Listener/LoadTemplateUrl.php 0000644 00000013704 15166657211 0017111 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Listener; use Joomla\CMS\Categories\Categories; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Router\SiteRouter; use Joomla\Component\Contact\Site\Helper\RouteHelper as ContactRouteHelper; use Joomla\Component\Content\Site\Helper\RouteHelper; use Joomla\Component\Tags\Site\Helper\RouteHelper as TagRouteHelper; use YOOtheme\Arr; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use YOOtheme\Builder\Joomla\Source\TagHelper; use YOOtheme\Builder\Joomla\Source\UserHelper; use YOOtheme\Config; class LoadTemplateUrl { public Config $config; public SiteRouter $router; public function __construct(Config $config, SiteRouter $router) { $this->config = $config; $this->router = $router; } public function handle(array $template): array { $url = ''; try { switch ($template['type'] ?? '') { case 'com_content.article': $lang = $this->getLanguage($template); $args = ['lang' => $lang] + $template['query'] + ['limit' => 1]; Arr::updateKeys($args, ['tag' => 'tags']); if ($articles = ArticleHelper::query($args)) { $url = RouteHelper::getArticleRoute( $articles[0]->id, $articles[0]->catid, $lang, ); } break; case 'com_content.category': $lang = $this->getLanguage($template); $catid = $template['query']['catid'] ?? null ?: [$this->getDefaultCategory($lang)]; if ( isset($catid[0]) && ($template['query']['include_child_categories'] ?? false) === 'only' ) { $catid = [$this->getFirstChildCategory($catid)]; } if (isset($catid[0])) { $url = RouteHelper::getCategoryRoute($catid[0], $lang, 'blog'); } break; case 'com_content.featured': $url = 'index.php?option=com_content&view=featured'; break; case 'com_tags.tag': $tag = $template['query']['tag'] ?? null ?: array_column(TagHelper::query($template['query'] + ['limit' => 1]), 'id'); if ( isset($tag[0]) && ($template['query']['include_child_tags'] ?? false) === 'only' ) { $tag = [$this->getFirstChildTag($tag)]; } if (isset($tag[0])) { $url = version_compare(JVERSION, '4.2', '<') ? TagRouteHelper::getTagRoute($tag[0]) : TagRouteHelper::getComponentTagRoute($tag[0]); } break; case 'com_tags.tags': $url = 'index.php?option=com_tags&view=tags'; break; case 'com_contact.contact': $lang = $this->getLanguage($template); $args = $template['query'] + ['limit' => 1]; if ($contacts = UserHelper::queryContacts($args)) { $url = ContactRouteHelper::getContactRoute( $contacts[0]->id, $contacts[0]->catid, $lang, ); } break; case 'com_finder.search': $url = 'index.php?option=com_finder&view=search'; break; case '_search': $template['url'] = '#live-search'; return $template; case 'error-404': $url = RouteHelper::getArticleRoute(-1, 0, $this->getLanguage($template)); break; } if ($url) { $template['url'] = (string) $this->router->build($url); } } catch (\Exception $e) { // ArticleHelper::query() throws exception if article "attribs" are invalid JSON } return $template; } /** * Fixes lowercase language code from "en-gb" to "en-GB". */ protected function getLanguage(array $template): string { return preg_replace_callback( '/-\w{2}$/', fn($matches) => strtoupper($matches[0]), $template['query']['lang'] ?? '', ); } protected function getDefaultCategory(string $lang): ?string { foreach ( HTMLHelper::_('category.options', 'com_content', [ 'filter.published' => [0, 1], 'filter.language' => [$lang, '*'], ]) as $category ) { if ($this->config->get('~theme.page_category') !== (string) $category->value) { return (string) $category->value; } } return null; } protected function getFirstChildCategory($categoryIds): ?string { $model = Categories::getInstance('content'); foreach ($categoryIds as $id) { $category = $model->get($id); if ($category) { $children = $category->getChildren(true); if (isset($children[0])) { return (string) $children[0]->id; } } } return null; } protected function getFirstChildTag($tagIds): ?string { foreach ($tagIds as $id) { $tags = TagHelper::query(['parent_id' => $id]); if (isset($tags[0])) { return (string) $tags[0]->id; } } return null; } } builder-joomla-source/src/Type/CategoryType.php 0000644 00000033026 15166657211 0015625 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\CategoryNode; use Joomla\CMS\Helper\TagsHelper; use Joomla\CMS\Tree\NodeInterface; use Joomla\CMS\User\User; use Joomla\Component\Content\Site\Helper\RouteHelper; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use YOOtheme\Builder\Joomla\Source\TagHelper; use YOOtheme\Path; use YOOtheme\View; use function YOOtheme\app; use function YOOtheme\trans; class CategoryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Title'), 'filters' => ['limit', 'preserve'], ], ], 'description' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Description'), 'filters' => ['limit', 'preserve'], ], ], 'params' => [ 'type' => 'CategoryParams', 'metadata' => [ 'label' => '', ], 'extensions' => [ 'call' => __CLASS__ . '::params', ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], 'parent' => [ 'type' => 'Category', 'metadata' => [ 'label' => trans('Parent Category'), ], 'extensions' => [ 'call' => __CLASS__ . '::parent', ], ], 'tagString' => [ 'type' => 'String', 'args' => [ 'parent_id' => [ 'type' => 'String', ], 'separator' => [ 'type' => 'String', ], 'show_link' => [ 'type' => 'Boolean', ], 'link_style' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Tags'), 'arguments' => [ 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between tags.'), 'default' => ', ', ], 'show_link' => [ 'label' => trans('Link'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show link'), ], 'link_style' => [ 'label' => trans('Link Style'), 'description' => trans('Set the link style.'), 'type' => 'select', 'default' => '', 'options' => [ 'Default' => '', 'Muted' => 'link-muted', 'Text' => 'link-text', 'Heading' => 'link-heading', 'Reset' => 'link-reset', ], 'enable' => 'arguments.show_link', ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::tagString', ], ], 'categories' => [ 'type' => [ 'listOf' => 'Category', ], 'metadata' => [ 'label' => trans('Child Categories'), ], 'extensions' => [ 'call' => __CLASS__ . '::categories', ], ], 'articles' => [ 'type' => [ 'listOf' => 'Article', ], 'args' => [ 'subcategories' => [ 'type' => 'Boolean', ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], 'order_alphanum' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Articles'), 'arguments' => [ 'subcategories' => [ 'label' => trans('Filter'), 'text' => trans('Include articles from child categories'), 'type' => 'checkbox', ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of articles.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'publish_up', 'options' => [ [ 'evaluate' => 'yootheme.builder.sources.articleOrderOptions', ], ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'DESC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], 'order_alphanum' => [ 'text' => trans('Alphanumeric Ordering'), 'type' => 'checkbox', ], ], 'directives' => [], ], 'extensions' => [ 'call' => __CLASS__ . '::articles', ], ], 'tags' => [ 'type' => [ 'listOf' => 'Tag', ], 'args' => [ 'parent_id' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Tags'), 'fields' => [ 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::tags', ], ], 'numitems' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Article Count'), ], ], 'alias' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Alias'), ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Category'), ], ]; } public static function params($category) { return is_string($category->params) ? json_decode($category->params) : $category->params; } public static function link($category) { return RouteHelper::getCategoryRoute($category->id, $category->language); } /** * @param CategoryNode $category * * @return ?CategoryNode */ public static function parent($category) { /** @var CategoryNode $parent */ $parent = $category->getParent(); return $parent && $parent->id !== 'root' ? $parent : null; } /** * @param CategoryNode $category * * @return CategoryNode[] */ public static function categories($category) { $groups = app(User::class)->getAuthorisedViewLevels(); return array_filter( $category->getChildren(), fn($child) => in_array($child->access, $groups), ); } public static function articles($category, $args) { return ArticleHelper::query(['catid' => $category->id] + $args); } public static function tags($category, $args) { $tags = $category->tags->itemTags ?? (new TagsHelper())->getItemTags('com_content.category', $category->id); if (!empty($args['parent_id'])) { return TagHelper::filterTags($tags, $args['parent_id']); } return $tags; } public static function tagString($category, array $args) { $tags = static::tags($category, $args); $args += [ 'separator' => ', ', 'show_link' => true, 'link_style' => '', ]; return app(View::class)->render( Path::get('../../templates/tags', __DIR__), compact('category', 'tags', 'args'), ); } } builder-joomla-source/src/Type/CategoryQueryType.php 0000644 00000001623 15166657211 0016651 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class CategoryQueryType { protected static $view = ['com_content.category']; /** * @return array */ public static function config() { return [ 'fields' => [ 'category' => [ 'type' => 'Category', 'metadata' => [ 'label' => trans('Category'), 'view' => static::$view, 'group' => trans('Page'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root) { if (in_array($root['template'] ?? '', static::$view)) { return $root['category']; } } } builder-joomla-source/src/Type/CustomArticlesQueryType.php 0000644 00000026217 15166657211 0020043 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use function YOOtheme\trans; class CustomArticlesQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customArticles' => [ 'type' => [ 'listOf' => 'Article', ], 'args' => [ 'catid' => [ 'type' => [ 'listOf' => 'String', ], ], 'cat_operator' => [ 'type' => 'String', ], 'include_child_categories' => [ 'type' => 'String', ], 'tags' => [ 'type' => [ 'listOf' => 'String', ], ], 'tag_operator' => [ 'type' => 'String', ], 'include_child_tags' => [ 'type' => 'String', ], 'users' => [ 'type' => [ 'listOf' => 'String', ], ], 'users_operator' => [ 'type' => 'String', ], 'featured' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], 'order_alphanum' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Custom Articles'), 'group' => trans('Custom'), 'fields' => [ 'catid' => [ 'label' => trans('Filter by Categories'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.categories']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ], 'cat_operator' => [ 'type' => 'select', 'default' => 'IN', 'options' => [ trans('Match (OR)') => 'IN', trans('Don\'t match (NOR)') => 'NOT IN', ], ], 'include_child_categories' => [ 'type' => 'select', 'description' => trans( 'Filter articles by categories. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple categories. Set the logical operator to match or not match the selected categories.', ), 'options' => [ trans('Exclude child categories') => '', trans('Include child categories') => 'include', trans('Only include child categories') => 'only', ], ], 'tags' => [ 'label' => trans('Filter by Tags'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.tags']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ], 'tag_operator' => [ 'type' => 'select', 'default' => 'IN', 'options' => [ trans('Match one (OR)') => 'IN', trans('Match all (AND)') => 'AND', trans('Don\'t match (NOR)') => 'NOT IN', ], ], 'include_child_tags' => [ 'type' => 'select', 'description' => trans( 'Filter articles by tags. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple tags. Set the logical operator to match at least one of the tags, none of the tags or all tags.', ), 'options' => [ trans('Exclude child tags') => '', trans('Include child tags') => 'include', trans('Only include child tags') => 'only', ], ], 'users' => [ 'label' => trans('Filter by Authors'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.authors']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ], 'users_operator' => [ 'description' => trans( 'Filter articles by authors. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple authors. Set the logical operator to match or not match the selected authors.', ), 'type' => 'select', 'default' => 'IN', 'options' => [ trans('Match (OR)') => 'IN', trans('Don\'t match (NOR)') => 'NOT IN', ], ], 'featured' => [ 'label' => trans('Filter by Featured Articles'), 'description' => trans( 'Filter articles by featured status. Load all articles, featured articles only, or articles which are not featured.', ), 'type' => 'select', 'options' => [ 'None' => '', 'Featured only' => 'only', 'Not featured' => 'hide', ], ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of articles.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'publish_up', 'options' => [ [ 'evaluate' => 'yootheme.builder.sources.articleOrderOptions', ], ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'DESC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], 'order_alphanum' => [ 'text' => trans('Alphanumeric Ordering'), 'type' => 'checkbox', ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { return ArticleHelper::query($args); } } builder-joomla-source/src/Type/CustomMenuItemQueryType.php 0000644 00000004666 15166657211 0020024 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Factory; use Joomla\CMS\Language\Multilanguage; use Joomla\CMS\User\User; use function YOOtheme\app; use function YOOtheme\trans; class CustomMenuItemQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customMenuItem' => [ 'type' => 'MenuItem', 'args' => [ 'menu' => [ 'type' => 'String', ], 'id' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom Menu Item'), 'group' => trans('Custom'), 'fields' => [ 'menu' => [ 'label' => trans('Menu'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [ ['evaluate' => 'yootheme.customizer.menu.menusSelect()'], ], ], 'id' => [ 'label' => trans('Menu Item'), 'description' => trans('Select menu item.'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [ ['evaluate' => 'yootheme.customizer.menu.itemsSelect(menu)'], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $item = Factory::getApplication() ->getMenu('site') ->getItem($args['id'] ?? 0); return $item && in_array($item->access, app(User::class)->getAuthorisedViewLevels()) && (!Multilanguage::isEnabled() || in_array($item->language, [Factory::getLanguage()->getTag(), '*'])) ? $item : null; } } builder-joomla-source/src/Type/MenuItemType.php 0000644 00000014671 15166657211 0015600 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Factory; use Joomla\CMS\Language\Multilanguage; use Joomla\CMS\User\User; use YOOtheme\Config; use function YOOtheme\app; use function YOOtheme\trans; class MenuItemType { /** * @return array */ public static function config() { return [ 'fields' => [ 'title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Title'), 'filters' => ['limit', 'preserve'], ], ], 'image' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Image'), ], 'extensions' => [ 'call' => __CLASS__ . '::data', ], ], 'icon' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Icon'), ], 'extensions' => [ 'call' => __CLASS__ . '::data', ], ], 'subtitle' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Subtitle'), ], 'extensions' => [ 'call' => __CLASS__ . '::data', ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], 'active' => [ 'type' => 'Boolean', 'metadata' => [ 'label' => trans('Active'), ], 'extensions' => [ 'call' => __CLASS__ . '::active', ], ], 'type' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Type'), ], 'extensions' => [ 'call' => __CLASS__ . '::type', ], ], ] + (version_compare(JVERSION, '4.0', '>') ? [ 'parent' => [ 'type' => 'MenuItem', 'metadata' => [ 'label' => trans('Parent Menu Item'), ], 'extensions' => [ 'call' => __CLASS__ . '::parent', ], ], 'children' => [ 'type' => [ 'listOf' => 'MenuItem', ], 'metadata' => [ 'label' => trans('Child Menu Items'), ], 'extensions' => [ 'call' => __CLASS__ . '::children', ], ], ] : []) + [ 'alias' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Alias'), ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Menu Item'), ], ]; } public static function link($item) { $link = $item->link; if ($item->type === 'alias' && str_ends_with($link, 'Itemid=')) { $link .= "&Itemid={$item->getParams()->get('aliasoptions')}"; } if (str_starts_with($link, 'index.php?') && !str_contains($link, 'Itemid=')) { $link .= "&Itemid={$item->id}"; } return $link; } public static function active($item): bool { $active = Factory::getApplication()->getMenu()->getActive(); if (!$active) { return false; } $alias_id = $item->getParams()->get('aliasoptions'); // set active state if ($item->id == $active->id || ($item->type == 'alias' && $alias_id == $active->id)) { return true; } if (in_array($item->id, $active->tree)) { return true; } elseif ($item->type == 'alias') { if (count($active->tree) > 0 && $alias_id == $active->tree[count($active->tree) - 1]) { return true; } elseif (in_array($alias_id, $active->tree) && !in_array($alias_id, $item->tree)) { return true; } } return false; } public static function data($item, $args, $context, $info) { $value = app(Config::class)->get("~theme.menu.items.{$item->id}.{$info->fieldName}"); if ($info->fieldName === 'image' && empty($value)) { return $item->getParams()['menu_image']; } return $value; } public static function type($item): string { if ($item->type === 'separator') { return 'divider'; } if ($item->type === 'heading') { return 'heading'; } return ''; } public static function parent($item, $args, $context, $info) { return $item->getParent(); } public static function children($item, $args, $context, $info) { $groups = app(User::class)->getAuthorisedViewLevels(); $language = Multilanguage::isEnabled() ? [Factory::getLanguage()->getTag(), '*'] : false; return array_filter( $item->getChildren(), fn($child) => $child->getParams()->get('menu_show', true) && in_array($child->access, $groups) && (!$language || in_array($child->language, $language)), ); } } builder-joomla-source/src/Type/CustomArticleQueryType.php 0000644 00000027143 15166657211 0017657 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use function YOOtheme\trans; class CustomArticleQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customArticle' => [ 'type' => 'Article', 'args' => [ 'id' => [ 'type' => 'String', ], 'catid' => [ 'type' => [ 'listOf' => 'String', ], ], 'include_child_categories' => [ 'type' => 'String', ], 'cat_operator' => [ 'type' => 'String', ], 'tags' => [ 'type' => [ 'listOf' => 'String', ], ], 'include_child_tags' => [ 'type' => 'String', ], 'tag_operator' => [ 'type' => 'String', ], 'users' => [ 'type' => [ 'listOf' => 'String', ], ], 'users_operator' => [ 'type' => 'String', ], 'featured' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], 'order_alphanum' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Custom Article'), 'group' => trans('Custom'), 'fields' => [ 'id' => [ 'label' => trans('Select Manually'), 'description' => trans( 'Pick an article manually or use filter options to specify which article should be loaded dynamically.', ), 'type' => 'select-item', 'labels' => ['type' => trans('Article')], ], 'catid' => [ 'label' => trans('Filter by Categories'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.categories']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], 'enable' => '!id', ], 'include_child_categories' => [ 'type' => 'select', 'options' => [ trans('Exclude child categories') => '', trans('Include child categories') => 'include', trans('Only include child categories') => 'only', ], ], 'cat_operator' => [ 'type' => 'select', 'description' => trans( 'Filter articles by categories. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple categories. Set the logical operator to match or not match the selected categories.', ), 'default' => 'IN', 'options' => [ trans('Match (OR)') => 'IN', trans('Don\'t match (NOR)') => 'NOT IN', ], 'enable' => '!id', ], 'tags' => [ 'label' => trans('Filter by Tags'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.tags']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], 'enable' => '!id', ], 'include_child_tags' => [ 'type' => 'select', 'options' => [ trans('Exclude child tags') => '', trans('Include child tags') => 'include', trans('Only include child tags') => 'only', ], ], 'tag_operator' => [ 'type' => 'select', 'description' => trans( 'Filter articles by tags. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple tags. Set the logical operator to match at least one of the tags, none of the tags or all tags.', ), 'default' => 'IN', 'options' => [ trans('Match one (OR)') => 'IN', trans('Match all (AND)') => 'AND', trans('Don\'t match (NOR)') => 'NOT IN', ], 'enable' => '!id', ], 'users' => [ 'label' => trans('Filter by Authors'), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.authors']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], 'enable' => '!id', ], 'users_operator' => [ 'description' => trans( 'Filter articles by authors. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple authors. Set the logical operator to match or not match the selected authors.', ), 'type' => 'select', 'default' => 'IN', 'options' => [ trans('Match (OR)') => 'IN', trans('Don\'t match (NOR)') => 'NOT IN', ], 'enable' => '!id', ], 'featured' => [ 'label' => trans('Filter by Featured Articles'), 'description' => trans( 'Filter articles by featured status. Load all articles, featured articles only, or articles which are not featured.', ), 'type' => 'select', 'options' => [ 'None' => '', 'Featured only' => 'only', 'Not featured' => 'hide', ], 'enable' => '!id', ], 'offset' => [ 'label' => trans('Start'), 'description' => trans( 'Set the starting point to specify which article is loaded.', ), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], 'enable' => '!id', ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'publish_up', 'options' => [ [ 'evaluate' => 'yootheme.builder.sources.articleOrderOptions', ], ], 'enable' => '!id', ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'DESC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], 'enable' => '!id', ], ], ], 'order_alphanum' => [ 'text' => trans('Alphanumeric Ordering'), 'type' => 'checkbox', 'enable' => '!id', ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $args += ['id' => 0, 'limit' => 1]; if (!empty($args['id'])) { $articles = ArticleHelper::get($args['id']); } else { $articles = ArticleHelper::query($args); } return array_shift($articles); } } builder-joomla-source/src/Type/ArticleType.php 0000644 00000067675 15166657211 0015454 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Factory; use Joomla\CMS\Helper\TagsHelper; use Joomla\Component\Content\Site\Helper\RouteHelper; use YOOtheme\Builder\Joomla\Fields\Type\FieldsType; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use YOOtheme\Builder\Joomla\Source\TagHelper; use YOOtheme\Path; use YOOtheme\View; use function YOOtheme\app; use function YOOtheme\trans; class ArticleType { /** * @return array */ public static function config() { return [ 'fields' => [ 'title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Title'), 'filters' => ['limit', 'preserve'], ], ], 'content' => [ 'type' => 'String', 'args' => [ 'show_intro_text' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Content'), 'arguments' => [ 'show_intro_text' => [ 'label' => trans('Intro Text'), 'description' => trans('Show or hide the intro text.'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show intro text'), ], ], 'filters' => ['limit', 'preserve'], ], 'extensions' => [ 'call' => __CLASS__ . '::content', ], ], 'teaser' => [ 'type' => 'String', 'args' => [ 'show_excerpt' => [ 'type' => 'Boolean', ], 'show_content' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Teaser'), 'arguments' => [ 'show_excerpt' => [ 'label' => trans('Intro Text'), 'description' => trans( 'Render the intro text if the read more tag is present. Otherwise, fall back to the full content. Optionally, prefer the excerpt field over the intro text. To use an excerpt field, create a custom field with the name excerpt.', ), 'type' => 'checkbox', 'default' => true, 'text' => trans('Prefer excerpt over intro text'), ], 'show_content' => [ 'type' => 'checkbox', 'default' => true, 'text' => trans('Fall back to content'), ], ], 'filters' => ['limit', 'preserve'], ], 'extensions' => [ 'call' => __CLASS__ . '::teaser', ], ], 'publish_up' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Published Date'), 'filters' => ['date'], ], ], 'created' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Created Date'), 'filters' => ['date'], ], ], 'modified' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Modified Date'), 'filters' => ['date'], ], ], 'featured' => [ 'type' => 'Boolean', 'metadata' => [ 'label' => trans('Featured'), 'condition' => true, ], ], 'metaString' => [ 'type' => 'String', 'args' => [ 'format' => [ 'type' => 'String', ], 'separator' => [ 'type' => 'String', ], 'link_style' => [ 'type' => 'String', ], 'show_publish_date' => [ 'type' => 'Boolean', ], 'show_author' => [ 'type' => 'Boolean', ], 'show_taxonomy' => [ 'type' => 'String', ], 'parent_id' => [ 'type' => 'String', ], 'date_format' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Meta'), 'arguments' => [ 'format' => [ 'label' => trans('Format'), 'description' => trans( 'Display the meta text in a sentence or a horizontal list.', ), 'type' => 'select', 'default' => 'list', 'options' => [ trans('List') => 'list', trans('Sentence') => 'sentence', ], ], 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between fields.'), 'default' => '|', 'enable' => 'arguments.format === "list"', ], 'link_style' => [ 'label' => trans('Link Style'), 'description' => trans('Set the link style.'), 'type' => 'select', 'default' => '', 'options' => [ 'Default' => '', 'Muted' => 'link-muted', 'Text' => 'link-text', 'Heading' => 'link-heading', 'Reset' => 'link-reset', ], ], 'show_publish_date' => [ 'label' => trans('Display'), 'description' => trans('Show or hide fields in the meta text.'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show date'), ], 'show_author' => [ 'type' => 'checkbox', 'default' => true, 'text' => trans('Show author'), ], 'show_taxonomy' => [ 'type' => 'select', 'default' => 'category', 'options' => [ trans('Hide Term List') => '', trans('Show Category') => 'category', trans('Show Tags') => 'tag', ], ], 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'show' => 'arguments.show_taxonomy === "tag"', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], 'date_format' => [ 'label' => trans('Date Format'), 'description' => trans( 'Select a predefined date format or enter a custom format.', ), 'type' => 'data-list', 'default' => '', 'options' => [ 'Aug 6, 1999 (M j, Y)' => 'M j, Y', 'August 06, 1999 (F d, Y)' => 'F d, Y', '08/06/1999 (m/d/Y)' => 'm/d/Y', '08.06.1999 (m.d.Y)' => 'm.d.Y', '6 Aug, 1999 (j M, Y)' => 'j M, Y', 'Tuesday, Aug 06 (l, M d)' => 'l, M d', ], 'enable' => 'arguments.show_publish_date', 'attrs' => [ 'placeholder' => 'Default', ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::metaString', ], ], 'category' => [ 'type' => 'Category', 'metadata' => [ 'label' => trans('Category'), ], 'extensions' => [ 'call' => __CLASS__ . '::category', ], ], 'tagString' => [ 'type' => 'String', 'args' => [ 'parent_id' => [ 'type' => 'String', ], 'separator' => [ 'type' => 'String', ], 'show_link' => [ 'type' => 'Boolean', ], 'link_style' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Tags'), 'arguments' => [ 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between tags.'), 'default' => ', ', ], 'show_link' => [ 'label' => trans('Link'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show link'), ], 'link_style' => [ 'label' => trans('Link Style'), 'description' => trans('Set the link style.'), 'type' => 'select', 'default' => '', 'options' => [ 'Default' => '', 'Muted' => 'link-muted', 'Text' => 'link-text', 'Heading' => 'link-heading', 'Reset' => 'link-reset', ], 'enable' => 'arguments.show_link', ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::tagString', ], ], 'images' => [ 'type' => 'ArticleImages', 'metadata' => [ 'label' => '', ], 'extensions' => [ 'call' => __CLASS__ . '::images', ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], 'author' => [ 'type' => 'User', 'metadata' => [ 'label' => trans('Author'), ], 'extensions' => [ 'call' => __CLASS__ . '::author', ], ], 'hits' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Hits'), ], ], 'rating' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Rating'), ], ], 'rating_count' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Votes'), ], ], 'urls' => [ 'type' => 'ArticleUrls', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::urls', ], ], 'event' => [ 'type' => 'ArticleEvent', 'metadata' => [ 'label' => trans('Events'), ], 'extensions' => [ 'call' => __CLASS__ . '::event', ], ], 'tags' => [ 'type' => [ 'listOf' => 'Tag', ], 'args' => [ 'parent_id' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Tags'), 'arguments' => [ 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::tags', ], ], 'alias' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Alias'), ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], 'relatedArticles' => [ 'type' => ['listOf' => 'Article'], 'args' => [ 'category' => [ 'type' => 'String', ], 'tags' => [ 'type' => 'String', ], 'author' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], 'order_alphanum' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Related Articles'), 'arguments' => [ 'category' => [ 'label' => trans('Relationship'), 'type' => 'select', 'default' => 'IN', 'options' => [ trans('Ignore category') => '', trans('Match category (OR)') => 'IN', trans('Don\'t match category (NOR)') => 'NOT IN', ], ], 'tags' => [ 'type' => 'select', 'options' => [ trans('Ignore tags') => '', trans('Match one tag (OR)') => 'IN', trans('Match all tags (AND)') => 'AND', trans('Don\'t match tags (NOR)') => 'NOT IN', ], ], 'author' => [ 'description' => trans( 'Set the logical operators for how the articles relate to category, tags and author. Choose between matching at least one term, all terms or none of the terms.', ), 'type' => 'select', 'options' => [ trans('Ignore author') => '', trans('Match author (OR)') => 'IN', trans('Don\'t match author (NOR)') => 'NOT IN', ], ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of articles.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'publish_up', 'options' => [ [ 'evaluate' => 'yootheme.builder.sources.articleOrderOptions', ], ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'DESC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], 'order_alphanum' => [ 'text' => trans('Alphanumeric Ordering'), 'type' => 'checkbox', ], ], 'directives' => [], ], 'extensions' => [ 'call' => __CLASS__ . '::relatedArticles', ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Article'), ], ]; } public static function content($article, $args) { if ( !$article->params->get('access-view') && $article->params->get('show_noauth') && Factory::getUser()->get('guest') ) { return $article->introtext; } $args += ['show_intro_text' => true]; if (isset($article->text) && $args['show_intro_text']) { return ($article->toc ?? '') . $article->text; } if ($article->params->get('show_intro', '1') === '1' && $args['show_intro_text']) { return "{$article->introtext} {$article->fulltext}"; } if ($article->fulltext) { return $article->fulltext; } return $article->introtext; } public static function teaser($article, $args) { $args += ['show_excerpt' => true, 'show_content' => true]; if ( $args['show_excerpt'] && ($field = FieldsType::getField('excerpt', $article, 'com_content.article')) && $field->rawvalue != '' ) { return $field->rawvalue; } if (!$args['show_content'] && $article->fulltext == '') { return ''; } return $article->introtext; } public static function link($article) { return RouteHelper::getArticleRoute( "{$article->id}:{$article->alias}", $article->catid, $article->language, ); } public static function images($article) { return json_decode($article->images); } public static function urls($article) { return json_decode($article->urls); } public static function author($article) { $user = Factory::getUser($article->created_by); if ($article->created_by_alias) { $user = clone $user; $user->name = $article->created_by_alias; } return $user; } public static function category($article) { return $article->catid && $article->catid !== 'root' ? Categories::getInstance('content', ['countItems' => true])->get($article->catid) : null; } public static function tags($article, $args) { $tags = $article->tags->itemTags ?? (new TagsHelper())->getItemTags('com_content.article', $article->id); if (!empty($args['parent_id'])) { return TagHelper::filterTags($tags, $args['parent_id']); } return $tags; } public static function event($article) { return $article; } public static function tagString($article, array $args) { $tags = static::tags($article, $args); $args += ['separator' => ', ', 'show_link' => true, 'link_style' => '']; return app(View::class)->render( Path::get('../../templates/tags', __DIR__), compact('tags', 'args'), ); } public static function metaString($article, array $args) { $args += [ 'format' => 'list', 'separator' => '|', 'link_style' => '', 'show_publish_date' => true, 'show_author' => true, 'show_taxonomy' => 'category', 'date_format' => '', ]; $tags = $args['show_taxonomy'] === 'tag' ? static::tags($article, $args) : null; return app(View::class)->render( Path::get('../../templates/meta', __DIR__), compact('article', 'tags', 'args'), ); } public static function relatedArticles($article, array $args) { $args['article'] = $article->id; $args['article_operator'] = 'NOT IN'; if (!empty($args['category'])) { $args['cat_operator'] = $args['category']; $args['catid'] = (array) $article->catid; } if (!empty($args['tags'])) { $args['tag_operator'] = $args['tags']; $args['tags'] = array_column(static::tags($article, []), 'id'); if (empty($args['tags'])) { return; } } if (!empty($args['author'])) { $args['users'] = $article->created_by; $args['users_operator'] = $args['author']; } return ArticleHelper::query($args); } } builder-joomla-source/src/Type/CustomTagQueryType.php 0000644 00000002642 15166657211 0017004 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use YOOtheme\Builder\Joomla\Source\TagHelper; use function YOOtheme\trans; class CustomTagQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customTag' => [ 'type' => 'Tag', 'args' => [ 'id' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom Tag'), 'group' => trans('Custom'), 'fields' => [ 'id' => [ 'label' => trans('Tag'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [['evaluate' => 'yootheme.builder.tags']], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { if (!empty($args['id'])) { $tags = TagHelper::get($args['id']); return array_shift($tags); } } } builder-joomla-source/src/Type/ArticleEventType.php 0000644 00000004352 15166657211 0016435 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Factory; use Joomla\CMS\Plugin\PluginHelper; class ArticleEventType extends EventType { public static function resolve($article, $args, $context, $info) { $key = $info->fieldName; if (isset($article->event->$key)) { return $article->event->$key; } $marker = "{# article_{$article->id}_{$key} #}"; Factory::getApplication()->registerEvent('onBeforeRender', function () use ( $article, $key, $marker ) { if (!isset($article->event->$key)) { static::applyContentPlugins($article); } /** @var HtmlDocument $document */ $document = Factory::getApplication()->getDocument(); $document->setBuffer( str_replace($marker, $article->event->$key, $document->getBuffer('component')), [ 'type' => 'component', 'name' => null, 'title' => null, ], ); }); return $marker; } protected static function applyContentPlugins($article) { $joomla = Factory::getApplication(); // Process the content plugins. PluginHelper::importPlugin('content'); $article->event = new \stdClass(); // Joomla content plugins expect $article and $article->params to be passed as reference $results = $joomla->triggerEvent('onContentAfterTitle', [ 'com_content.article', &$article, &$article->params, ]); $article->event->afterDisplayTitle = trim(implode("\n", $results)); $results = $joomla->triggerEvent('onContentBeforeDisplay', [ 'com_content.article', &$article, &$article->params, ]); $article->event->beforeDisplayContent = trim(implode("\n", $results)); $results = $joomla->triggerEvent('onContentAfterDisplay', [ 'com_content.article', &$article, &$article->params, ]); $article->event->afterDisplayContent = trim(implode("\n", $results)); } } builder-joomla-source/src/Type/CustomTagsQueryType.php 0000644 00000012004 15166657211 0017160 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use YOOtheme\Builder\Joomla\Source\TagHelper; use function YOOtheme\trans; class CustomTagsQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customTags' => [ 'type' => [ 'listOf' => 'Tag', ], 'args' => [ 'parent_id' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom Tags'), 'group' => trans('Custom'), 'fields' => [ 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of tags.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'a.title', 'options' => [ trans('Alphabetical') => 'a.title', trans('Tag Order') => 'a.lft', trans('Hits') => 'a.hits', trans('Random') => 'rand', ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'ASC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $args += ['parent_id' => 0]; return TagHelper::query($args); } } builder-joomla-source/src/Type/SmartSearchItemsQueryType.php 0000644 00000016533 15166657211 0020320 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use function YOOtheme\trans; class SmartSearchItemsQueryType { protected static $view = ['com_finder.search', '_search']; /** * @return array */ public static function config() { return [ 'fields' => [ 'smartSearchItem' => [ 'type' => 'SmartSearchItem', 'args' => [ 'catid' => [ 'type' => [ 'listOf' => 'String', ], ], 'offset' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Item'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ 'catid' => [ 'label' => trans('Filter by Root Categories'), 'description' => trans( 'Filter items visually by the selected root categories.', ), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.root_categories']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ], 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolveSingle', ], ], 'smartSearchItems' => [ 'type' => [ 'listOf' => 'SmartSearchItem', ], 'args' => [ 'catid' => [ 'type' => [ 'listOf' => 'String', ], ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Items'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ 'catid' => [ 'label' => trans('Filter by Root Categories'), 'description' => trans( 'Filter items visually by the selected root categories.', ), 'type' => 'select', 'default' => [], 'options' => [['evaluate' => 'yootheme.builder.root_categories']], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of items.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'attrs' => [ 'placeholder' => trans('No limit'), 'min' => 0, ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $args += [ 'catid' => null, 'offset' => 0, 'limit' => null, ]; if (in_array($root['template'] ?? '', static::$view)) { $items = $root['items'] ?? []; if ($args['catid']) { $rootCategories = array_map( fn($catId) => Categories::getInstance('content', ['countItems' => true])->get( $catId, ), $args['catid'], ); if (!$rootCategories) { return []; } $items = array_filter($items, function ($item) use ($rootCategories) { $id = $item->getElement('catid'); if (!$id || $item->getElement('context') !== 'com_content.article') { return false; } $category = Categories::getInstance('content', ['countItems' => true])->get( $id, ); if (!$category) { return false; } foreach ($rootCategories as $rootCategory) { if (array_key_exists($rootCategory->id, $category->getPath())) { return true; } } return false; }); } if ($args['offset'] || $args['limit']) { $items = array_slice($items, (int) $args['offset'], (int) $args['limit'] ?: null); } return $items; } } public static function resolveSingle($root, array $args) { return static::resolve($root, $args + ['limit' => 1])[0] ?? null; } } builder-joomla-source/src/Type/TagsQueryType.php 0000644 00000010406 15166657211 0015771 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class TagsQueryType { protected static $view = ['com_tags.tag', 'com_tags.tags']; /** * @return array */ public static function config() { return [ 'fields' => [ 'tagsSingle' => [ 'type' => 'Tag', 'args' => [ 'offset' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Tag'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolveSingle', ], ], 'tags' => [ 'type' => [ 'listOf' => 'Tag', ], 'args' => [ 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Tags'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of tags.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'attrs' => [ 'placeholder' => trans('No limit'), 'min' => 0, ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $args += [ 'offset' => 0, 'limit' => null, ]; if (in_array($root['template'] ?? '', static::$view)) { $tags = $root['tags']; if ($args['offset'] || $args['limit']) { $tags = array_slice($tags, (int) $args['offset'], (int) $args['limit'] ?: null); } return $tags; } } public static function resolveSingle($root, array $args) { if (in_array($root['template'] ?? '', static::$view)) { return $root['tags'][$args['offset'] ?? 0] ?? null; } } } builder-joomla-source/src/Type/SmartSearchType.php 0000644 00000002347 15166657211 0016266 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\Component\Finder\Site\Helper\RouteHelper; use function YOOtheme\trans; class SmartSearchType { /** * @return array */ public static function config() { return [ 'fields' => [ 'searchword' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Search Word'), ], ], 'total' => [ 'type' => 'Int', 'metadata' => [ 'label' => trans('Item Count'), ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Search'), ], ]; } public static function link() { return RouteHelper::getSearchRoute(); } } builder-joomla-source/src/Type/CustomUserQueryType.php 0000644 00000002563 15166657211 0017211 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Factory; use function YOOtheme\trans; class CustomUserQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customUser' => [ 'type' => 'User', 'args' => [ 'id' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom User'), 'group' => trans('Custom'), 'fields' => [ 'id' => [ 'label' => trans('User'), 'type' => 'select-item', 'module' => 'com_users', 'labels' => ['type' => trans('User')], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { if (empty($args['id'])) { return; } return Factory::getUser($args['id']); } } builder-joomla-source/src/Type/EventType.php 0000644 00000003053 15166657211 0015126 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class EventType { /** * @return array */ public static function config() { return [ 'fields' => [ 'afterDisplayTitle' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('After Display Title'), ], 'extensions' => [ 'call' => get_called_class() . '::resolve', ], ], 'beforeDisplayContent' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Before Display Content'), ], 'extensions' => [ 'call' => get_called_class() . '::resolve', ], ], 'afterDisplayContent' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('After Display Content'), ], 'extensions' => [ 'call' => get_called_class() . '::resolve', ], ], ], 'metadata' => [ 'label' => trans('Events'), ], ]; } public static function resolve($article, $args, $context, $info) { $key = $info->fieldName; return $article->event->$key ?? null; } } builder-joomla-source/src/Type/UserType.php 0000644 00000007145 15166657211 0014771 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Access\Access; use Joomla\Component\Users\Administrator\Helper\UsersHelper; use YOOtheme\Builder\Joomla\Source\UserHelper; use function YOOtheme\trans; class UserType { /** * @return array */ public static function config() { return [ 'fields' => [ 'name' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Name'), 'filters' => ['limit', 'preserve'], ], ], 'username' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Username'), 'filters' => ['limit', 'preserve'], ], ], 'email' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Email'), 'filters' => ['limit', 'preserve'], ], ], 'registerDate' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Registered Date'), 'filters' => ['date'], ], ], 'lastvisitDate' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Last Visit Date'), 'filters' => ['date'], ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], 'userGroupString' => [ 'type' => 'String', 'args' => [ 'separator' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('User Groups'), 'arguments' => [ 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between user groups.'), 'default' => ', ', ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::userGroupString', ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('User'), ], ]; } public static function link($user) { return UserHelper::getContactLink($user->id); } public static function userGroupString($user, $args) { $result = []; $groups = Access::getGroupsByUser($user->id); foreach (UsersHelper::getGroups() as $group) { if (in_array($group->value, $groups)) { $result[] = $group->title; } } return implode($args['separator'] ?? ', ', $result); } } builder-joomla-source/src/Type/SmartSearchQueryType.php 0000644 00000001650 15166657211 0017310 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class SmartSearchQueryType { protected static $view = ['com_finder.search', '_search']; /** * @return array */ public static function config() { return [ 'fields' => [ 'smartSearch' => [ 'type' => 'SmartSearch', 'metadata' => [ 'label' => trans('Search'), 'view' => static::$view, 'group' => trans('Page'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root) { if (in_array($root['template'] ?? '', static::$view)) { return $root['search'] ?? null; } } } builder-joomla-source/src/Type/ContactQueryType.php 0000644 00000001614 15166657211 0016467 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class ContactQueryType { protected static $view = ['com_contact.contact']; /** * @return array */ public static function config() { return [ 'fields' => [ 'contact' => [ 'type' => 'Contact', 'metadata' => [ 'group' => trans('Page'), 'label' => trans('Contact'), 'view' => static::$view, ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root) { if (in_array($root['template'] ?? '', static::$view)) { return $root['item']; } } } builder-joomla-source/src/Type/ArticleUrlsType.php 0000644 00000001711 15166657211 0016275 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; class ArticleUrlsType { /** * @return array */ public static function config() { $fields = []; foreach (['a', 'b', 'c'] as $letter) { $fields["url{$letter}"] = [ 'type' => 'String', 'metadata' => [ 'label' => ucfirst($letter), ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ]; $fields["url{$letter}text"] = [ 'type' => 'String', 'metadata' => [ 'label' => ucfirst($letter) . ' Text', 'filters' => ['limit', 'preserve'], ], ]; } return compact('fields'); } public static function resolve($item, $args, $context, $info) { return $item->{$info->fieldName} ?: ''; } } builder-joomla-source/src/Type/SiteQueryType.php 0000644 00000002356 15166657211 0016004 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Application\SiteApplication; use Joomla\CMS\User\User; use function YOOtheme\app; use function YOOtheme\trans; class SiteQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'site' => [ 'type' => 'Site', 'metadata' => [ 'label' => trans('Site'), 'group' => trans('Global'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve() { /** @var User $user */ $user = app(User::class); /** @var CMSApplication $joomla */ $joomla = app(CMSApplication::class); $params = $joomla instanceof SiteApplication ? $joomla->getParams() : []; return [ 'title' => $joomla->get('sitename'), 'page_title' => $params['page_heading'] ?? '', 'user' => $user, 'is_guest' => $user->guest, ]; } } builder-joomla-source/src/Type/ArticlesQueryType.php 0000644 00000010776 15166657211 0016653 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class ArticlesQueryType { protected static $view = ['com_content.category', 'com_content.featured']; /** * @return array */ public static function config() { return [ 'fields' => [ 'articlesSingle' => [ 'type' => 'Article', 'args' => [ 'offset' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Article'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'description' => trans( 'Set the starting point to specify which article is loaded.', ), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolveSingle', ], ], 'articles' => [ 'type' => [ 'listOf' => 'Article', ], 'args' => [ 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Articles'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of articles.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'attrs' => [ 'placeholder' => trans('No limit'), 'min' => 0, ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $args += [ 'offset' => 0, 'limit' => null, ]; if (in_array($root['template'] ?? '', static::$view)) { $items = $root['items'] ?? []; if ($args['offset'] || $args['limit']) { $items = array_slice($items, (int) $args['offset'], (int) $args['limit'] ?: null); } return $items; } } public static function resolveSingle($root, array $args) { if (in_array($root['template'] ?? '', static::$view)) { return $root['items'][$args['offset'] ?? 0] ?? null; } } } builder-joomla-source/src/Type/TagItemsQueryType.php 0000644 00000010422 15166657211 0016606 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class TagItemsQueryType { protected static $view = ['com_tags.tag']; /** * @return array */ public static function config() { return [ 'fields' => [ 'tagItemsSingle' => [ 'type' => 'TagItem', 'args' => [ 'offset' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Item'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolveSingle', ], ], 'tagItems' => [ 'type' => [ 'listOf' => 'TagItem', ], 'args' => [ 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], ], 'metadata' => [ 'label' => trans('Items'), 'view' => static::$view, 'group' => trans('Page'), 'fields' => [ '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of items.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'attrs' => [ 'placeholder' => trans('No limit'), 'min' => 0, ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $args += [ 'offset' => 0, 'limit' => null, ]; if (in_array($root['template'] ?? '', static::$view)) { $items = $root['items']; if ($args['offset'] || $args['limit']) { $items = array_slice($items, (int) $args['offset'], (int) $args['limit'] ?: null); } return $items; } } public static function resolveSingle($root, array $args) { if (in_array($root['template'] ?? '', static::$view)) { return $root['items'][$args['offset'] ?? 0] ?? null; } } } builder-joomla-source/src/Type/CustomCategoryQueryType.php 0000644 00000002621 15166657211 0020043 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use function YOOtheme\trans; class CustomCategoryQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customCategory' => [ 'type' => 'Category', 'args' => [ 'id' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom Category'), 'group' => trans('Custom'), 'fields' => [ 'id' => [ 'label' => trans('Category'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [['evaluate' => 'yootheme.builder.categories']], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { return Categories::getInstance('content', ['countItems' => true])->get($args['id']); } } builder-joomla-source/src/Type/TagType.php 0000644 00000022357 15166657211 0014570 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Helper\TagsHelper; use Joomla\Component\Tags\Site\Helper\RouteHelper; use YOOtheme\Builder\Joomla\Source\TagHelper; use function YOOtheme\trans; class TagType { /** * @return array */ public static function config() { return [ 'fields' => [ 'title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Title'), 'filters' => ['limit', 'preserve'], ], ], 'description' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Description'), 'filters' => ['limit', 'preserve'], ], ], 'images' => [ 'type' => 'Images', 'metadata' => [ 'label' => '', ], 'extensions' => [ 'call' => __CLASS__ . '::images', ], ], 'hits' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Hits'), ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], 'tags' => [ 'type' => [ 'listOf' => 'Tag', ], 'metadata' => [ 'label' => trans('Child Tags'), ], 'extensions' => [ 'call' => __CLASS__ . '::tags', ], ], 'items' => [ 'type' => [ 'listOf' => 'TagItem', ], 'args' => [ 'include_children' => [ 'type' => 'Boolean', ], 'typesr' => [ 'type' => [ 'listOf' => 'String', ], ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], 'order_alphanum' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Tagged Items'), 'arguments' => [ 'typesr' => [ 'label' => trans('Type'), 'type' => 'select', 'description' => trans('Set the type of tagged items.'), 'options' => array_merge( [trans('All types') => ''], ...array_map( fn($type) => [$type->type_title => (string) $type->type_id], TagsHelper::getTypes('array'), ), ), ], 'include_children' => [ 'label' => trans('Filter'), 'text' => trans('Include items from child tags'), 'type' => 'checkbox', ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of tagged items.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'core_publish_up', 'options' => [ trans('Published') => 'core_publish_up', trans('Unpublished') => 'core_publish_down', trans('Created') => 'core_created_time', trans('Modified') => 'core_modified_time', trans('Alphabetical') => 'core_title', trans('Hits') => 'core_hits', trans('Ordering') => 'core_ordering', trans('Random') => 'rand', ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'DESC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], 'order_alphanum' => [ 'text' => trans('Alphanumeric Ordering'), 'type' => 'checkbox', ], ], 'directives' => [], ], 'extensions' => [ 'call' => __CLASS__ . '::items', ], ], 'alias' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Alias'), ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Tag'), ], ]; } public static function images($tag) { return json_decode($tag->images); } public static function link($tag) { return RouteHelper::getTagRoute("{$tag->id}:{$tag->alias}"); } public static function tags($tag) { return TagHelper::query([ 'parent_id' => $tag->id, 'language' => $tag->language === '*' ? null : $tag->language, ]); } public static function items($tag, $args) { if ($tag->language !== '*') { $args['language'] = $tag->language; } $items = TagHelper::getItems($tag->id, $args); foreach ($items as $item) { if (($item->content_type_title ?? '') === 'Article') { $item->id = $item->content_item_id ?? '0'; $item->catid = $item->core_catid ?? '0'; $item->language = $item->core_language ?? '*'; } } return $items; } } builder-joomla-source/src/Type/CustomMenuItemsQueryType.php 0000644 00000013554 15166657211 0020203 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Factory; use Joomla\CMS\Menu\MenuItem; use function YOOtheme\trans; class CustomMenuItemsQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customMenuItems' => [ 'type' => [ 'listOf' => 'MenuItem', ], 'args' => [ 'id' => [ 'type' => 'String', ], 'parent' => [ 'type' => 'String', ], 'heading' => [ 'type' => 'String', ], 'include_heading' => [ 'type' => 'Boolean', 'defaultValue' => true, ], 'ids' => [ 'type' => [ 'listOf' => 'String', ], ], ], 'metadata' => [ 'label' => trans('Custom Menu Items'), 'group' => trans('Custom'), 'fields' => [ 'id' => [ 'label' => trans('Menu'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [ ['evaluate' => 'yootheme.customizer.menu.menusSelect()'], ], ], 'parent' => [ 'label' => trans('Parent Menu Item'), 'description' => trans( 'Menu items are only loaded from the selected parent item.', ), 'type' => 'select', 'defaultIndex' => 0, 'options' => [ ['value' => '', 'text' => trans('Root')], ['evaluate' => 'yootheme.customizer.menu.itemsSelect(id)'], ], ], 'heading' => [ 'label' => trans('Limit by Menu Heading'), 'type' => 'select', 'defaultIndex' => 0, 'options' => [ ['value' => '', 'text' => trans('None')], [ 'evaluate' => 'yootheme.customizer.menu.headingItemsSelect(id, parent)', ], ], ], 'include_heading' => [ 'description' => trans( 'Only load menu items from the selected menu heading.', ), 'type' => 'checkbox', 'default' => true, 'text' => trans('Include heading itself'), ], 'ids' => [ 'label' => trans('Select Manually'), 'description' => trans( 'Select menu items manually. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple menu items.', ), 'type' => 'select', 'options' => [ ['evaluate' => 'yootheme.customizer.menu.itemsSelect(id)'], ], 'attrs' => [ 'multiple' => true, 'class' => 'uk-height-small', ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { $found = false; return array_filter( Factory::getApplication()->getMenu('site')->getItems('menutype', $args['id']), function (MenuItem $item) use ($args, &$found) { if (!$item->getParams()->get('menu_show', true)) { return false; } if (!empty($args['ids'])) { return in_array($item->id, $args['ids']); } if (!empty($args['heading'])) { if (!$found) { if ($item->id == $args['heading']) { $found = $item; return !empty($args['include_heading']); } return false; } if ($item->parent_id !== $found->parent_id) { return false; } if (!in_array($item->type, ['heading', 'separator'])) { return true; } return $found = false; } if (!empty($args['parent'])) { return $item->parent_id == $args['parent']; } return $item->level == '1'; }, ); } } builder-joomla-source/src/Type/SmartSearchItemType.php 0000644 00000026120 15166657211 0017100 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Categories\CategoryNode; use Joomla\CMS\Factory; use Joomla\CMS\User\User; use Joomla\Component\Finder\Administrator\Indexer\Result; use YOOtheme\Path; use YOOtheme\View; use function YOOtheme\app; use function YOOtheme\trans; class SmartSearchItemType { /** * @return array */ public static function config() { return [ 'fields' => [ 'title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Title'), 'filters' => ['limit', 'preserve'], ], ], 'description' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Content'), 'filters' => ['limit', 'preserve'], ], ], 'publish_start_date' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Published Date'), 'filters' => ['date'], ], ], 'metaString' => [ 'type' => 'String', 'args' => [ 'format' => [ 'type' => 'String', ], 'separator' => [ 'type' => 'String', ], 'link_style' => [ 'type' => 'String', ], 'show_publish_date' => [ 'type' => 'Boolean', ], 'show_author' => [ 'type' => 'Boolean', ], 'show_taxonomy' => [ 'type' => 'String', ], 'parent_id' => [ 'type' => 'String', ], 'date_format' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Meta'), 'arguments' => [ 'format' => [ 'label' => trans('Format'), 'description' => trans( 'Display the meta text in a sentence or a horizontal list.', ), 'type' => 'select', 'default' => 'list', 'options' => [ trans('List') => 'list', trans('Sentence') => 'sentence', ], ], 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between fields.'), 'default' => '|', 'enable' => 'arguments.format === "list"', ], 'link_style' => [ 'label' => trans('Link Style'), 'description' => trans('Set the link style.'), 'type' => 'select', 'default' => '', 'options' => [ 'Default' => '', 'Muted' => 'link-muted', 'Text' => 'link-text', 'Heading' => 'link-heading', 'Reset' => 'link-reset', ], ], 'show_publish_date' => [ 'label' => trans('Display'), 'description' => trans('Show or hide fields in the meta text.'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show date'), ], 'show_author' => [ 'type' => 'checkbox', 'default' => true, 'text' => trans('Show author'), ], 'show_taxonomy' => [ 'type' => 'select', 'default' => 'category', 'options' => [ trans('Hide Term List') => '', trans('Show Category') => 'category', trans('Show Tags') => 'tag', ], ], 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'show' => 'arguments.show_taxonomy === "tag"', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], 'date_format' => [ 'label' => trans('Date Format'), 'description' => trans( 'Select a predefined date format or enter a custom format.', ), 'type' => 'data-list', 'default' => '', 'options' => [ 'Aug 6, 1999 (M j, Y)' => 'M j, Y', 'August 06, 1999 (F d, Y)' => 'F d, Y', '08/06/1999 (m/d/Y)' => 'm/d/Y', '08.06.1999 (m.d.Y)' => 'm.d.Y', '6 Aug, 1999 (j M, Y)' => 'j M, Y', 'Tuesday, Aug 06 (l, M d)' => 'l, M d', ], 'enable' => 'arguments.show_publish_date', 'attrs' => [ 'placeholder' => 'Default', ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::metaString', ], ], 'category' => [ 'type' => 'Category', 'metadata' => [ 'label' => trans('Category'), ], 'extensions' => [ 'call' => __CLASS__ . '::category', ], ], 'images' => [ 'type' => 'ArticleImages', 'metadata' => [ 'label' => '', ], 'extensions' => [ 'call' => __CLASS__ . '::images', ], ], 'route' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], ], 'author' => [ 'type' => 'User', 'metadata' => [ 'label' => trans('Author'), ], 'extensions' => [ 'call' => __CLASS__ . '::author', ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Smart Search Item'), ], ]; } /** * @param Result $item * * @return string */ public static function metaString($item, array $args) { $args += [ 'format' => 'list', 'separator' => '|', 'link_style' => '', 'show_publish_date' => true, 'show_author' => true, 'show_taxonomy' => 'category', 'date_format' => '', ]; $props = [ 'id', 'author', 'created_by', 'created_by_alias', 'contact_link', 'catid', 'category' => 'category_title', ]; $article = new \stdClass(); foreach ($props as $field => $prop) { if (is_numeric($field)) { $article->$prop = $item->getElement($prop); } else { $article->$prop = $item->getElement($field); } } $article->publish_up = $item->publish_start_date; $tags = $args['show_taxonomy'] === 'tag' ? ArticleType::tags($article, $args) : null; return app(View::class)->render( Path::get('../../templates/meta', __DIR__), compact('article', 'tags', 'args'), ); } /** * @param Result $item * * @return array */ public static function images($item) { $images = json_decode($item->getElement('images') ?? ''); // Fallback for plugins not storing images as json representation (like HikaShop) if (!$images && $item->getElement('imageUrl')) { $images = (object) [ 'image_intro' => $item->getElement('imageUrl'), 'image_intro_alt' => $item->getElement('imageAlt'), ]; } return $images; } /** * @param Result $item * * @return CategoryNode|null */ public static function category($item) { if ($item->getElement('context') === 'com_content.article') { $id = $item->getElement('catid'); return $id && $id !== 'root' ? Categories::getInstance('content', ['countItems' => true])->get($id) : null; } return null; } /** * @param Result $item * * @return User */ public static function author($item) { $user = Factory::getUser($item->getElement('created_by')); if ($item->getElement('created_by_alias')) { $user = clone $user; $user->name = $item->getElement('created_by_alias'); } return $user; } } builder-joomla-source/src/Type/CustomCategoriesQueryType.php 0000644 00000013625 15166657211 0020361 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use function YOOtheme\trans; class CustomCategoriesQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customCategories' => [ 'type' => [ 'listOf' => 'Category', ], 'args' => [ 'catid' => [ 'type' => 'String', ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom Categories'), 'group' => trans('Custom'), 'fields' => [ 'catid' => [ 'label' => trans('Parent Category'), 'description' => trans( 'Categories are only loaded from the selected parent category.', ), 'type' => 'select', 'default' => '0', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.categories'], ], ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of categories.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'ordering', 'options' => [ trans('Alphabetical') => 'title', trans('Category Order') => 'ordering', trans('Random') => 'rand', ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'ASC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { if ( $category = Categories::getInstance('content', ['countItems' => true])->get( $args['catid'], ) ) { $categories = $category->getChildren(); if ($args['order'] === 'rand') { shuffle($categories); } elseif ($args['order']) { $prop = $args['order'] === 'ordering' ? 'lft' : $args['order']; usort( $categories, fn($article, $other) => strnatcmp($article->$prop, $other->$prop), ); } if ($args['offset'] || $args['limit']) { $categories = array_slice( $categories, (int) $args['offset'], (int) $args['limit'] ?: null, ); } if ($args['order_direction'] === 'DESC') { $categories = array_reverse($categories); } return $categories; } } } builder-joomla-source/src/Type/ArticleQueryType.php 0000644 00000006031 15166657211 0016455 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Router; use Joomla\Uri\Uri; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use function YOOtheme\trans; class ArticleQueryType { protected static $view = ['com_content.article']; /** * @return array */ public static function config() { return [ 'fields' => [ 'article' => [ 'type' => 'Article', 'metadata' => [ 'label' => trans('Article'), 'view' => static::$view, 'group' => trans('Page'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], 'prevArticle' => [ 'type' => 'Article', 'metadata' => [ 'label' => trans('Previous Article'), 'view' => static::$view, 'group' => trans('Page'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolvePreviousArticle', ], ], 'nextArticle' => [ 'type' => 'Article', 'metadata' => [ 'label' => trans('Next Article'), 'view' => static::$view, 'group' => trans('Page'), ], 'extensions' => [ 'call' => __CLASS__ . '::resolveNextArticle', ], ], ], ]; } public static function resolve($root) { if (in_array($root['template'] ?? '', static::$view)) { return $root['article'] ?? $root['item']; } } public static function resolvePreviousArticle($root) { $article = static::resolve($root); if (!$article) { return; } ArticleHelper::applyPageNavigation($article); if (!empty($article->prev)) { return static::getArticleFromUrl($article->prev); } } public static function resolveNextArticle($root) { $article = static::resolve($root); if (!$article) { return; } ArticleHelper::applyPageNavigation($article); if (!empty($article->next)) { return static::getArticleFromUrl($article->next); } } protected static function getArticleFromUrl($url) { if (version_compare(JVERSION, '4.0', '<')) { $uri = new Uri(Route::_($url)); $vars = Router::getInstance('site')->parse($uri); $id = $vars['id'] ?? 0; } else { $id = (new Uri($url))->getVar('id', '0'); } if (!$id) { return null; } $articles = ArticleHelper::get($id); return array_shift($articles); } } builder-joomla-source/src/Type/ContactType.php 0000644 00000024570 15166657211 0015447 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Factory; use Joomla\CMS\Helper\TagsHelper; use Joomla\Component\Contact\Site\Helper\RouteHelper; use YOOtheme\Builder\Joomla\Source\ArticleHelper; use YOOtheme\Path; use YOOtheme\View; use function YOOtheme\app; use function YOOtheme\trans; class ContactType { /** * @return array */ public static function config() { return [ 'fields' => [ 'name' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Name'), 'filters' => ['limit', 'preserve'], ], ], 'image' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Image'), 'filters' => ['limit', 'preserve'], ], ], 'email_to' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Email'), 'filters' => ['limit', 'preserve'], ], ], 'con_position' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Contacts Position'), 'filters' => ['limit', 'preserve'], ], ], 'address' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Address'), 'filters' => ['limit', 'preserve'], ], ], 'suburb' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('City or Suburb'), 'filters' => ['limit', 'preserve'], ], ], 'state' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('State or County'), 'filters' => ['limit', 'preserve'], ], ], 'postcode' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Postal/ZIP Code'), 'filters' => ['limit', 'preserve'], ], ], 'country' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Country'), 'filters' => ['limit', 'preserve'], ], ], 'telephone' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Telephone'), 'filters' => ['limit', 'preserve'], ], ], 'mobile' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Mobile'), 'filters' => ['limit', 'preserve'], ], ], 'fax' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Fax'), 'filters' => ['limit', 'preserve'], ], ], 'webpage' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Website'), 'filters' => ['limit', 'preserve'], ], ], 'text' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Miscellaneous Information'), 'filters' => ['limit', 'preserve'], ], ], 'created' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Created Date'), 'filters' => ['date'], ], ], 'modified' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Modified Date'), 'filters' => ['date'], ], ], 'category' => [ 'type' => 'Category', 'metadata' => [ 'label' => trans('Category'), ], 'extensions' => [ 'call' => __CLASS__ . '::category', ], ], 'user' => [ 'type' => 'User', 'metadata' => [ 'label' => trans('User'), ], 'extensions' => [ 'call' => __CLASS__ . '::user', ], ], 'tagString' => [ 'type' => 'String', 'args' => [ 'separator' => [ 'type' => 'String', ], 'show_link' => [ 'type' => 'Boolean', ], 'link_style' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Tags'), 'arguments' => [ 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between tags.'), 'default' => ', ', ], 'show_link' => [ 'label' => trans('Link'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show link'), ], 'link_style' => [ 'label' => trans('Link Style'), 'description' => trans('Set the link style.'), 'type' => 'select', 'default' => '', 'options' => [ 'Default' => '', 'Muted' => 'link-muted', 'Text' => 'link-text', 'Heading' => 'link-heading', 'Reset' => 'link-reset', ], 'enable' => 'arguments.show_link', ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::tagString', ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], 'extensions' => [ 'call' => __CLASS__ . '::link', ], ], 'articles' => [ 'type' => [ 'listOf' => 'Article', ], 'metadata' => [ 'label' => trans('Articles'), ], 'extensions' => [ 'call' => __CLASS__ . '::articles', ], ], 'tags' => [ 'type' => [ 'listOf' => 'Tag', ], 'metadata' => [ 'label' => trans('Tags'), ], 'extensions' => [ 'call' => __CLASS__ . '::tags', ], ], 'hits' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Hits'), ], ], 'alias' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Alias'), ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Contact'), ], ]; } public static function category($contact) { return Categories::getInstance('contact', ['countItems' => true])->get($contact->catid); } public static function user($contact) { return Factory::getUser($contact->user_id); } public static function tags($contact) { return $contact->tags->itemTags ?? (new TagsHelper())->getItemTags('com_contact.contact', $contact->id); } public static function tagString($contact, array $args) { $tags = static::tags($contact); $args += ['separator' => ', ', 'show_link' => true, 'link_style' => '']; return app(View::class)->render( Path::get('../../templates/tags', __DIR__), compact('tags', 'args'), ); } public static function link($contact) { return RouteHelper::getContactRoute($contact->id, $contact->catid, $contact->language); } public static function articles($contact) { if (empty($contact->articles)) { return; } $ids = array_column($contact->articles, 'id'); $articles = ArticleHelper::get($ids); usort($articles, fn($a, $b) => array_search($a->id, $ids) - array_search($b->id, $ids)); return $articles; } } builder-joomla-source/src/Type/TagItemType.php 0000644 00000033034 15166657211 0015401 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use Joomla\CMS\Categories\Categories; use Joomla\CMS\Factory; use YOOtheme\Path; use YOOtheme\View; use function YOOtheme\app; use function YOOtheme\trans; class TagItemType { /** * @return array */ public static function config() { return [ 'fields' => [ 'core_title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Title'), 'filters' => ['limit', 'preserve'], ], ], 'content' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Content'), 'filters' => ['limit', 'preserve'], ], 'extensions' => [ 'call' => __CLASS__ . '::content', ], ], 'teaser' => [ 'type' => 'String', 'args' => [ 'show_excerpt' => [ 'type' => 'Boolean', ], ], 'metadata' => [ 'label' => trans('Teaser'), 'arguments' => [ 'show_excerpt' => [ 'label' => trans('Excerpt'), 'description' => trans( 'Display the excerpt field if it has content, otherwise the content. To use an excerpt field, create a custom field with the name excerpt.', ), 'type' => 'checkbox', 'default' => true, 'text' => trans('Prefer excerpt over regular text'), ], ], 'filters' => ['limit', 'preserve'], ], 'extensions' => [ 'call' => __CLASS__ . '::teaser', ], ], 'core_publish_up' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Published Date'), 'filters' => ['date'], ], ], 'core_created_time' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Created Date'), 'filters' => ['date'], ], ], 'core_modified_time' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Modified Date'), 'filters' => ['date'], ], ], 'metaString' => [ 'type' => 'String', 'args' => [ 'format' => [ 'type' => 'String', ], 'separator' => [ 'type' => 'String', ], 'link_style' => [ 'type' => 'String', ], 'show_publish_date' => [ 'type' => 'Boolean', ], 'show_author' => [ 'type' => 'Boolean', ], 'show_taxonomy' => [ 'type' => 'String', ], 'parent_id' => [ 'type' => 'String', ], 'date_format' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Meta'), 'arguments' => [ 'format' => [ 'label' => trans('Format'), 'description' => trans( 'Display the meta text in a sentence or a horizontal list.', ), 'type' => 'select', 'default' => 'list', 'options' => [ trans('List') => 'list', trans('Sentence') => 'sentence', ], ], 'separator' => [ 'label' => trans('Separator'), 'description' => trans('Set the separator between fields.'), 'default' => '|', 'enable' => 'arguments.format === "list"', ], 'link_style' => [ 'label' => trans('Link Style'), 'description' => trans('Set the link style.'), 'type' => 'select', 'default' => '', 'options' => [ 'Default' => '', 'Muted' => 'link-muted', 'Text' => 'link-text', 'Heading' => 'link-heading', 'Reset' => 'link-reset', ], ], 'show_publish_date' => [ 'label' => trans('Display'), 'description' => trans('Show or hide fields in the meta text.'), 'type' => 'checkbox', 'default' => true, 'text' => trans('Show date'), ], 'show_author' => [ 'type' => 'checkbox', 'default' => true, 'text' => trans('Show author'), ], 'show_taxonomy' => [ 'type' => 'select', 'default' => 'category', 'options' => [ trans('Hide Term List') => '', trans('Show Category') => 'category', trans('Show Tags') => 'tag', ], ], 'parent_id' => [ 'label' => trans('Parent Tag'), 'description' => trans( 'Tags are only loaded from the selected parent tag.', ), 'type' => 'select', 'default' => '0', 'show' => 'arguments.show_taxonomy === "tag"', 'options' => [ ['value' => '0', 'text' => trans('Root')], ['evaluate' => 'yootheme.builder.tags'], ], ], 'date_format' => [ 'label' => trans('Date Format'), 'description' => trans( 'Select a predefined date format or enter a custom format.', ), 'type' => 'data-list', 'default' => '', 'options' => [ 'Aug 6, 1999 (M j, Y)' => 'M j, Y', 'August 06, 1999 (F d, Y)' => 'F d, Y', '08/06/1999 (m/d/Y)' => 'm/d/Y', '08.06.1999 (m.d.Y)' => 'm.d.Y', '6 Aug, 1999 (j M, Y)' => 'j M, Y', 'Tuesday, Aug 06 (l, M d)' => 'l, M d', ], 'enable' => 'arguments.show_publish_date', 'attrs' => [ 'placeholder' => 'Default', ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::metaString', ], ], 'category' => [ 'type' => 'Category', 'metadata' => [ 'label' => trans('Category'), ], 'extensions' => [ 'call' => __CLASS__ . '::category', ], ], 'images' => [ 'type' => 'Images', 'metadata' => [ 'label' => '', ], 'extensions' => [ 'call' => __CLASS__ . '::images', ], ], 'link' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Link'), ], ], 'author' => [ 'type' => 'User', 'metadata' => [ 'label' => trans('Author'), ], 'extensions' => [ 'call' => __CLASS__ . '::author', ], ], 'event' => [ 'type' => 'ArticleEvent', 'metadata' => [ 'label' => trans('Events'), ], 'extensions' => [ 'call' => __CLASS__ . '::event', ], ], 'content_type_title' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Content Type Title'), ], ], 'core_alias' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Alias'), ], ], 'id' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('ID'), ], ], ], 'metadata' => [ 'type' => true, 'label' => trans('Tag Item'), ], ]; } public static function content($item) { return $item->core_body ?? ''; } public static function teaser($item, $args) { $args += ['show_excerpt' => true]; if ($args['show_excerpt'] && !empty($item->jcfields['excerpt']->rawvalue)) { return $item->jcfields['excerpt']->rawvalue; } return $item->core_body ?? ''; } public static function metaString($item, array $args) { if ($item->type_alias !== 'com_content.article') { return; } $args += [ 'format' => 'list', 'separator' => '|', 'link_style' => '', 'show_publish_date' => true, 'show_author' => true, 'show_taxonomy' => 'category', 'date_format' => '', ]; $props = [ 'id', 'author', 'contact_link', 'core_catid' => 'catid', 'category_title', 'core_created_user_id' => 'created_by', 'core_created_by_alias' => 'created_by_alias', 'core_publish_up' => 'publish_up', ]; $article = new \stdClass(); foreach ($props as $field => $prop) { if (isset($item->$prop)) { $article->$prop = $item->$prop; } elseif (isset($item->$field)) { $article->$prop = $item->$field; } else { $article->$prop = null; } } $tags = $args['show_taxonomy'] === 'tag' ? ArticleType::tags($article, $args) : null; return app(View::class)->render( Path::get('../../templates/meta', __DIR__), compact('article', 'tags', 'args'), ); } public static function images($item) { return json_decode($item->core_images); } public static function author($item) { $user = Factory::getUser($item->core_created_user_id); if ($item->core_created_by_alias) { $user = clone $user; $user->name = $item->core_created_by_alias; } return $user; } public static function category($item) { return isset($item->catid) ? Categories::getInstance('content', ['countItems' => true])->get($item->catid) : null; } public static function event($item) { return isset($item->event) ? $item : null; } } builder-joomla-source/src/Type/CategoryParamsType.php 0000644 00000001335 15166657211 0016767 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class CategoryParamsType { /** * @return array */ public static function config() { return [ 'fields' => [ 'image' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Image'), ], ], 'image_alt' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Image Alt'), 'filters' => ['limit', 'preserve'], ], ], ], ]; } } builder-joomla-source/src/Type/ImagesType.php 0000644 00000004236 15166657211 0015256 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use function YOOtheme\trans; class ImagesType { /** * @return array */ public static function config() { return [ 'fields' => [ 'image_intro' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Intro Image'), ], 'extensions' => [ 'call' => __CLASS__ . '::image', ], ], 'image_intro_alt' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Intro Image Alt'), 'filters' => ['limit', 'preserve'], ], ], 'image_intro_caption' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Intro Image Caption'), 'filters' => ['limit', 'preserve'], ], ], 'image_fulltext' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Full Article Image'), ], 'extensions' => [ 'call' => __CLASS__ . '::image', ], ], 'image_fulltext_alt' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Full Article Image Alt'), 'filters' => ['limit', 'preserve'], ], ], 'image_fulltext_caption' => [ 'type' => 'String', 'metadata' => [ 'label' => trans('Full Article Image Caption'), 'filters' => ['limit', 'preserve'], ], ], ], ]; } public static function image($data, $args, $context, $info) { return $data->{$info->fieldName} ?? null; } } builder-joomla-source/src/Type/ArticleImagesType.php 0000644 00000000145 15166657211 0016555 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; class ArticleImagesType extends ImagesType {} builder-joomla-source/src/Type/CustomUsersQueryType.php 0000644 00000012041 15166657211 0017364 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source\Type; use YOOtheme\Builder\Joomla\Source\UserHelper; use function YOOtheme\trans; class CustomUsersQueryType { /** * @return array */ public static function config() { return [ 'fields' => [ 'customUsers' => [ 'type' => [ 'listOf' => 'User', ], 'args' => [ 'groups' => [ 'type' => [ 'listOf' => 'String', ], ], 'offset' => [ 'type' => 'Int', ], 'limit' => [ 'type' => 'Int', ], 'order' => [ 'type' => 'String', ], 'order_direction' => [ 'type' => 'String', ], ], 'metadata' => [ 'label' => trans('Custom Users'), 'group' => trans('Custom'), 'fields' => [ 'groups' => [ 'label' => trans('User Group'), 'description' => trans( 'Users are only loaded from the selected user groups. Use the <kbd>shift</kbd> or <kbd>ctrl/cmd</kbd> key to select multiple user groups.', ), 'type' => 'select', 'attrs' => [ 'multiple' => true, ], 'options' => [['evaluate' => 'yootheme.builder.usergroups']], ], '_offset' => [ 'description' => trans( 'Set the starting point and limit the number of users.', ), 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'offset' => [ 'label' => trans('Start'), 'type' => 'number', 'default' => 0, 'modifier' => 1, 'attrs' => [ 'min' => 1, 'required' => true, ], ], 'limit' => [ 'label' => trans('Quantity'), 'type' => 'limit', 'default' => 10, 'attrs' => [ 'min' => 1, ], ], ], ], '_order' => [ 'type' => 'grid', 'width' => '1-2', 'fields' => [ 'order' => [ 'label' => trans('Order'), 'type' => 'select', 'default' => 'a.name', 'options' => [ trans('Alphabetical') => 'a.name', trans('Register date') => 'a.registerDate', trans('Last visit date') => 'a.lastvisitDate', ], ], 'order_direction' => [ 'label' => trans('Direction'), 'type' => 'select', 'default' => 'ASC', 'options' => [ trans('Ascending') => 'ASC', trans('Descending') => 'DESC', ], ], ], ], ], ], 'extensions' => [ 'call' => __CLASS__ . '::resolve', ], ], ], ]; } public static function resolve($root, array $args) { return UserHelper::query($args); } } builder-joomla-source/src/TagHelper.php 0000644 00000006740 15166657211 0014143 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Multilanguage; use Joomla\Component\Tags\Administrator\Table\TagTable; use Joomla\Database\DatabaseDriver; use Joomla\Registry\Registry; use function YOOtheme\app; class TagHelper { /** * Gets the tags. * * @param int[] $ids * * @return object[] */ public static function get($ids) { $tags = []; // Get a level row instance. $table = new TagTable(app(DatabaseDriver::class)); foreach ((array) $ids as $id) { $table->load($id); if ($table->get('published') != 1) { continue; } if (!in_array($table->get('access'), Factory::getUser()->getAuthorisedViewLevels())) { continue; } $tags[] = (object) $table->getProperties(true); } return $tags; } public static function query($args = []) { $model = new TagsModel(['ignore_request' => true]); $params = ComponentHelper::getParams('com_tags'); $params->set('show_pagination_limit', false); $params->set('published', 1); $model->setState('tag.parent_id', !empty($args['parent_id']) ? $args['parent_id'] : 0); $model->setState( 'tag.language', Multilanguage::isEnabled() ? $args['language'] ?? Factory::getApplication()->getLanguage()->getTag() : 'all', ); $props = [ 'limit' => 'maximum', 'order' => 'all_tags_orderby', 'order_direction' => 'all_tags_orderby_direction', 'offset' => 'list.start', ]; foreach (array_intersect_key($props, $args) as $key => $prop) { $params->set($prop, $args[$key]); } $model->setState('params', $params); return $model->getItems(); } public static function filterTags($tags, $parentId) { $parent = current(static::get($parentId)); return $parent ? array_filter($tags, fn($tag) => $tag->lft > $parent->lft && $tag->rgt < $parent->rgt) : []; } public static function getItems($tagId, $args) { $model = new TagModel(['ignore_request' => true]); $model->setState('tag.id', $tagId); $model->setState('tag.state', 1); $model->setState( 'tag.language', $args['language'] ?? Factory::getApplication()->getLanguage()->getTag(), ); $model->setState( 'params', new Registry([ 'include_children' => $args['include_children'] ?? false, ]), ); if (!empty($args['order'])) { $args['order'] = "c.{$args['order']}"; } $args['typesr'] = array_filter($args['typesr'] ?? []); if (empty($args['typesr'])) { unset($args['typesr']); } $props = [ 'typesr' => 'tag.typesr', 'offset' => 'list.start', 'limit' => 'list.limit', 'order' => 'list.ordering', 'order_direction' => 'list.direction', 'include_children' => 'filter.include_children', 'order_alphanum' => 'list.alphanum', ]; foreach (array_intersect_key($props, $args) as $key => $prop) { $model->setState($prop, $args[$key]); } return $model->getItems(); } } builder-joomla-source/src/SourceController.php 0000644 00000002523 15166657211 0015567 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\CMS\Factory; use Joomla\CMS\User\User; use Joomla\Database\DatabaseDriver; use YOOtheme\Http\Request; use YOOtheme\Http\Response; class SourceController { /** * @throws \Exception */ public static function articles( Request $request, Response $response, DatabaseDriver $db, User $user ): Response { $ids = implode(',', array_map('intval', (array) $request->getQueryParam('ids'))); $groups = implode(',', $user->getAuthorisedViewLevels()); $titles = []; if (!empty($ids)) { $query = "SELECT id, title FROM #__content WHERE id IN ({$ids}) AND access IN ({$groups})"; $titles = $db->setQuery($query)->loadAssocList('id', 'title'); } return $response->withJson((object) $titles); } /** * @throws \Exception */ public static function users(Request $request, Response $response, User $user): Response { $titles = []; if ($user->authorise('core.manage', 'com_users')) { foreach ((array) $request->getQueryParam('ids') as $id) { $titles[$id] = Factory::getUser($id)->name; } } return $response->withJson((object) $titles); } } builder-joomla-source/src/ArticleHelper.php 0000644 00000011352 15166657211 0015006 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Event\Content\BeforeDisplayEvent; use Joomla\CMS\Factory; use Joomla\CMS\Language\Multilanguage; use Joomla\Database\DatabaseDriver; use Joomla\Registry\Registry; use function YOOtheme\app; class ArticleHelper { /** * Gets the articles. * * @param int[] $ids * @param array $args * * @return object[] */ public static function get($ids, array $args = []) { return $ids ? static::query(['article' => (array) $ids] + $args) : []; } /** * Query articles. * * @param array $args * * @return object[] */ public static function query(array $args = []) { $model = new ArticlesModel(['ignore_request' => true]); $model->setState('params', ComponentHelper::getParams('com_content')); $model->setState('filter.access', true); $model->setState('filter.published', 1); $model->setState('filter.language', empty($args['lang']) && Multilanguage::isEnabled()); $model->setState('filter.subcategories', false); $model->setState('filter.max_category_levels', -1); $args += [ 'article_operator' => 'IN', 'cat_operator' => 'IN', 'tag_operator' => 'IN', 'users_operator' => 'IN', ]; if (!empty($args['order'])) { if ($args['order'] === 'rand') { $args['order'] = app(DatabaseDriver::class)->getQuery(true)->Rand(); } elseif ($args['order'] === 'front') { $args['order'] = 'fp.ordering'; } else { $args['order'] = "a.{$args['order']}"; } } $props = [ 'offset' => 'list.start', 'limit' => 'list.limit', 'order' => 'list.ordering', 'order_direction' => 'list.direction', 'order_alphanum' => 'list.alphanum', 'featured' => 'filter.featured', 'subcategories' => 'filter.subcategories', 'max_category_levels' => 'filter.max_category_levels', 'tags' => 'filter.tags', 'tag_operator' => 'filter.tag_operator', 'include_child_categories' => 'filter.include_child_categories', 'include_child_tags' => 'filter.include_child_tags', 'lang' => 'filter.lang', ]; foreach (array_intersect_key($props, $args) as $key => $prop) { $model->setState($prop, $args[$key]); } if (!empty($args['article'])) { $model->setState('filter.article_id', (array) $args['article']); $model->setState('filter.article_id.include', $args['article_operator'] === 'IN'); } if (!empty($args['catid'])) { $model->setState('filter.category_id', $args['catid']); $model->setState('filter.category_id.include', $args['cat_operator'] === 'IN'); } if (!empty($args['users'])) { $model->setState('filter.author_id', (array) $args['users']); $model->setState('filter.author_id.include', $args['users_operator'] === 'IN'); } return $model->getItems(); } public static function applyPageNavigation($article) { if (empty($article->pagination)) { $joomla = Factory::getApplication(); if (method_exists($joomla, 'bootPlugin')) { $plugin = $joomla->bootPlugin('pagenavigation', 'content'); } elseif (!($plugin = static::importPlugin('pagenavigation', 'content'))) { return null; } $plugin->params = new Registry(['display' => 0]); $params = clone $article->params; $params->set('show_item_navigation', true); $args = ['com_content.article', $article, $params, 0]; if (version_compare(JVERSION, '5.3', '>=')) { $args = [new BeforeDisplayEvent('onContentBeforeDisplay', $args)]; } $plugin->onContentBeforeDisplay(...$args); } return !empty($article->prev) || !empty($article->next); } /** * Only needed for Joomla 3.x because it has no Application::bootPlugin() method. * * @param string $plugin * @param string $type * * @return ?object */ protected static function importPlugin($plugin, $type) { $path = JPATH_PLUGINS . "{$type}/{$plugin}/{$plugin}.php"; $class = 'Plg' . str_replace('-', '', $type) . $plugin; if (is_file($path)) { require_once $path; } if (!class_exists($class)) { return null; } return (new \ReflectionClass($class))->newInstanceWithoutConstructor(); } } builder-joomla-source/src/ArticlesModel.php 0000644 00000011161 15166657211 0015010 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\Component\Content\Site\Model\ArticlesModel as BaseModel; use Joomla\Database\DatabaseDriver; use function YOOtheme\app; class ArticlesModel extends BaseModel { protected function getListQuery() { $fieldId = false; $ordering = $this->getState('list.ordering', ''); if (str_starts_with($ordering, 'a.field:')) { $fieldId = (int) substr($ordering, 8); $this->setState('list.ordering', 'fields.value'); } $categoryId = $this->getState('filter.category_id'); $includeChildCategories = $this->getState('filter.include_child_categories'); if ($categoryId && $includeChildCategories) { $this->setState('filter.category_id'); } $query = parent::getListQuery(); if ($categoryId && $includeChildCategories) { $categories = implode(',', array_map('intval', (array) $categoryId)); $include = $this->getState('filter.category_id.include', true); $type = $include ? 'IN' : 'NOT IN'; $where = []; if ($includeChildCategories === 'include') { $where[] = "a.catid {$type} ({$categories})"; } $subQuery = "SELECT sub.id FROM #__categories AS sub JOIN #__categories AS this ON sub.lft > this.lft AND sub.rgt < this.rgt WHERE this.id IN ({$categories})"; $where[] = "a.catid {$type} ({$subQuery})"; $query->andWhere($where, $include ? 'OR' : 'AND'); } $tags = array_filter((array) $this->getState('filter.tags', [])); if ($tags) { $tagOperator = $this->getState('filter.tag_operator', 'IN'); $tagCount = count($tags); $tags = implode(',', array_map('intval', $tags)); $includeChildTags = $this->getState('filter.include_child_tags'); if (in_array($tagOperator, ['IN', 'NOT IN'])) { $where = []; if (!$includeChildTags || $includeChildTags === 'include') { $subQuery = "SELECT content_item_id FROM #__contentitem_tag_map WHERE tag_id IN ({$tags}) AND type_alias = 'com_content.article'"; $where[] = "a.id {$tagOperator} ({$subQuery})"; } if ($includeChildTags) { $subQuery = "SELECT map.content_item_id FROM #__tags AS sub JOIN #__tags AS this ON sub.lft > this.lft AND sub.rgt < this.rgt JOIN #__contentitem_tag_map as map ON sub.id = map.tag_id WHERE this.id IN ({$tags}) and map.type_alias = 'com_content.article'"; $where[] = "a.id {$tagOperator} ({$subQuery})"; } $query->andWhere($where, $tagOperator === 'IN' ? 'OR' : 'AND'); } if ($tagOperator === 'AND') { $greaterThan = $includeChildTags === 'include' ? '>=' : '>'; $lesserThan = $includeChildTags === 'include' ? '<=' : '<'; $tagQuery = $includeChildTags ? "SELECT sub.id FROM #__tags AS sub JOIN #__tags AS this ON sub.lft {$greaterThan} this.lft AND sub.rgt {$lesserThan} this.rgt WHERE this.id IN ({$tags})" : $tags; $tagCountQuery = $includeChildTags ? "(SELECT COUNT(sub.id) FROM #__tags AS sub JOIN #__tags AS this ON sub.lft {$greaterThan} this.lft AND sub.rgt {$lesserThan} this.rgt WHERE this.id IN ({$tags}))" : $tagCount; $subQuery = "SELECT COUNT(1) FROM #__contentitem_tag_map WHERE tag_id IN ({$tagQuery}) AND content_item_id = a.id AND type_alias = 'com_content.article'"; $query->where("({$subQuery}) = {$tagCountQuery}"); } } if ($fieldId) { $query->leftJoin( "#__fields_values AS fields ON a.id = fields.item_id AND fields.field_id = {$fieldId}", ); } if ( $this->getState('list.alphanum') && $ordering != app(DatabaseDriver::class)->getQuery(true)->Rand() ) { $ordering = $this->getState('list.ordering', 'a.ordering'); $order = $this->getState('list.direction', 'ASC'); $query->clear('order'); $query->order( "(substr({$ordering}, 1, 1) > '9') {$order}, {$ordering}+0 {$order}, {$ordering} {$order}", ); } // Filter by language if ($lang = $this->getState('filter.lang')) { $db = app(DatabaseDriver::class); $query->where('a.language IN (' . $db->quote($lang) . ',' . $db->quote('*') . ')'); } return $query; } } builder-joomla-source/src/TagsModel.php 0000644 00000001174 15166657211 0014143 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\Component\Tags\Site\Model\TagsModel as BaseModel; use Joomla\Database\DatabaseDriver; use function YOOtheme\app; class TagsModel extends BaseModel { protected function getListQuery() { $query = parent::getListQuery(); $params = $this->state->get('params'); $this->setState('list.start', $params->get('list.start')); if ($params->get('all_tags_orderby', 'title') == 'rand') { $query->clear('order'); $query->order(app(DatabaseDriver::class)->getQuery(true)->rand()); } return $query; } } builder-joomla-source/elements/pagination/templates/template.php 0000644 00000004570 15166657211 0021256 0 ustar 00 <?php use Joomla\CMS\Language\Text; $pagination = $props['pagination']; $el = $this->el('nav', [ 'aria-label' => $props['pagination_type'] == 'numeric' ? Text::_('TPL_YOOTHEME_PAGINATION') : false, ]); $list = $this->el('ul', [ 'class' => [ 'uk-pagination uk-margin-remove-bottom', 'uk-flex-{text_align}[@{text_align_breakpoint} [uk-flex-{text_align_fallback}]]', ], 'uk-margin' => $props['pagination_type'] == 'numeric' ? count($pagination) > 1 : isset($pagination['previous'], $pagination['next']), ]); ?> <?= $el($props, $attrs) ?> <?= $list($props) ?> <?php if ($props['pagination_type'] == 'numeric') : ?> <?php foreach ($pagination as $key => $page) : ?> <?php if ($page->active) : ?> <li class="uk-active"><span aria-current="page"><?= $page->text ?></span></li> <?php elseif ($page->link) : ?> <li> <?php if (in_array($key, ['previous', 'next'], true)) : ?> <a href="<?= $page->link ?>" aria-label="<?= $page->text ?>"> <span uk-pagination-<?= $key ?>></span> </a> <?php else : ?> <a href="<?= $page->link ?>"><?= $page->text ?></a> <?php endif ?> </li> <?php else : ?> <li class="uk-disabled"><span><?= $page->text ?></span></li> <?php endif ?> <?php endforeach ?> <?php else : ?> <?php if (isset($pagination['previous'])) : ?> <?php if ($props['pagination_space_between']) : ?> <li class="uk-margin-auto-right"> <?php else : ?> <li> <?php endif ?> <a href="<?= $pagination['previous']->link ?>"><span uk-pagination-previous></span> <?= $pagination['previous']->text ?></a> </li> <?php endif ?> <?php if (isset($pagination['next'])) : ?> <?php if ($props['pagination_space_between']) : ?> <li class="uk-margin-auto-left"> <?php else : ?> <li> <?php endif ?> <a href="<?= $pagination['next']->link ?>"><?= $pagination['next']->text ?> <span uk-pagination-next></span></a> </li> <?php endif ?> <?php endif ?> <?= $list->end() ?> <?= $el->end() ?> builder-joomla-source/elements/pagination/element.json 0000644 00000010713 15166657211 0017254 0 ustar 00 { "@import": "./element.php", "name": "pagination", "title": "Pagination", "group": "system", "icon": "${url:images/icon.svg}", "iconSmall": "${url:images/iconSmall.svg}", "element": true, "width": 500, "defaults": { "pagination_type": "previous/next", "text_align": "center" }, "updates": "./updates.php", "templates": { "render": "./templates/template.php" }, "fields": { "pagination_type": { "label": "Pagination", "description": "Choose between the previous/next or numeric pagination. The numeric pagination is not available for single articles.", "type": "select", "options": { "Previous/Next": "previous/next", "Numeric": "numeric" } }, "pagination_space_between": { "type": "checkbox", "text": "Show space between links", "enable": "pagination_type == 'previous/next'" }, "position": "${builder.position}", "position_left": "${builder.position_left}", "position_right": "${builder.position_right}", "position_top": "${builder.position_top}", "position_bottom": "${builder.position_bottom}", "position_z_index": "${builder.position_z_index}", "blend": "${builder.blend}", "margin": "${builder.margin}", "margin_remove_top": "${builder.margin_remove_top}", "margin_remove_bottom": "${builder.margin_remove_bottom}", "maxwidth": "${builder.maxwidth}", "maxwidth_breakpoint": "${builder.maxwidth_breakpoint}", "block_align": "${builder.block_align}", "block_align_breakpoint": "${builder.block_align_breakpoint}", "block_align_fallback": "${builder.block_align_fallback}", "text_align": "${builder.text_align}", "text_align_breakpoint": "${builder.text_align_breakpoint}", "text_align_fallback": "${builder.text_align_fallback}", "animation": "${builder.animation}", "_parallax_button": "${builder._parallax_button}", "visibility": "${builder.visibility}", "name": "${builder.name}", "status": "${builder.status}", "id": "${builder.id}", "class": "${builder.cls}", "attributes": "${builder.attrs}", "css": { "label": "CSS", "description": "Enter your own custom CSS. The following selectors will be prefixed automatically for this element: <code>.el-element</code>", "type": "editor", "editor": "code", "mode": "css", "attrs": { "debounce": 500, "hints": [".el-element"] } } }, "fieldset": { "default": { "type": "tabs", "fields": [ { "title": "Settings", "fields": [ { "label": "Pagination", "type": "group", "fields": ["pagination_type", "pagination_space_between"] }, { "label": "General", "type": "group", "fields": [ "position", "position_left", "position_right", "position_top", "position_bottom", "position_z_index", "blend", "margin", "margin_remove_top", "margin_remove_bottom", "maxwidth", "maxwidth_breakpoint", "block_align", "block_align_breakpoint", "block_align_fallback", "text_align", "text_align_breakpoint", "text_align_fallback", "animation", "_parallax_button", "visibility" ] } ] }, "${builder.advanced}" ] } } } builder-joomla-source/elements/pagination/images/iconSmall.svg 0000644 00000000623 15166657211 0020636 0 ustar 00 <svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <polyline fill="none" stroke="#444" points="14.5 8.4 16.5 10.5 14.5 12.6" /> <polyline fill="none" stroke="#444" points="5.5 8.4 3.5 10.5 5.5 12.6" /> <rect width="8" height="8" fill="none" stroke="#444" x="0.5" y="6.5" /> <rect width="8" height="8" fill="none" stroke="#444" x="11.5" y="6.5" /> </svg> builder-joomla-source/elements/pagination/images/icon.svg 0000644 00000000711 15166657211 0017643 0 ustar 00 <svg width="30" height="30" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"> <polyline fill="none" stroke="#444" stroke-width="2" points="21.5 12 24.5 15 21.5 18" /> <polyline fill="none" stroke="#444" stroke-width="2" points="8.5 12 5.5 15 8.5 18" /> <rect width="12" height="12" fill="none" stroke="#444" stroke-width="2" x="1" y="9" /> <rect width="12" height="12" fill="none" stroke="#444" stroke-width="2" x="17" y="9" /> </svg> builder-joomla-source/elements/pagination/element.php 0000644 00000006033 15166657211 0017072 0 ustar 00 <?php namespace YOOtheme; use Joomla\CMS\Language\Text; use Joomla\CMS\Pagination\PaginationObject; use YOOtheme\Builder\Joomla\Source\ArticleHelper; return [ 'transforms' => [ 'render' => function ($node, $params) { // Single Article if (!isset($params['pagination'])) { $article = $params['item'] ?? ($params['article'] ?? false); if (!$article || !ArticleHelper::applyPageNavigation($article)) { return false; } $params['pagination'] = [ 'previous' => $article->prev ? new PaginationObject($article->prev_label, '', null, $article->prev) : null, 'next' => $article->next ? new PaginationObject($article->next_label, '', null, $article->next) : null, ]; } if (is_callable($params['pagination'])) { $params['pagination'] = $params['pagination'](); } if (is_array($params['pagination'])) { $node->props['pagination_type'] = 'previous/next'; $node->props['pagination'] = $params['pagination']; return; } // Article Index if (empty($params['pagination']) || $params['pagination']->pagesTotal < 2) { return false; } $list = $params['pagination']->getPaginationPages(); $total = $params['pagination']->pagesTotal; $current = (int) $params['pagination']->pagesCurrent; $endSize = 1; $midSize = 3; $dots = false; $pagination = []; if ($list['previous']['active']) { $pagination['previous'] = $list['previous']['data']; } $list['start']['data']->text = 1; $list['end']['data']->text = $total; for ($n = 1; $n <= $total; $n++) { $active = $n <= $endSize || ($current && $n >= $current - $midSize && $n <= $current + $midSize) || $n > $total - $endSize; if ($active || $dots) { if ($active) { $pagination[$n] = $n === 1 ? $list['start']['data'] : ($n === $total ? $list['end']['data'] : $list['pages'][$n]['data']); $pagination[$n]->active = $n === $current; } else { $pagination[$n] = new PaginationObject(Text::_('…')); } $dots = $active; } } if ($list['next']['active']) { $pagination['next'] = $list['next']['data']; } $node->props['pagination'] = $pagination; }, ], ]; builder-joomla-source/elements/pagination/updates.php 0000644 00000000352 15166657211 0017104 0 ustar 00 <?php namespace YOOtheme; return [ // Remove obsolete props '4.5.0-beta.0.4' => function ($node) { unset( $node->props['navigation'], $node->props['pagination_start_end'] ); }, ]; builder-joomla-source/bootstrap.php 0000644 00000004750 15166657211 0013515 0 ustar 00 <?php namespace YOOtheme\Builder\Joomla\Source; use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\Database\DatabaseDriver; use YOOtheme\Builder; use YOOtheme\Builder\BuilderConfig; use YOOtheme\Builder\Source\Filesystem\FileHelper; use YOOtheme\Builder\Source\SourceTransform; use YOOtheme\Builder\UpdateTransform; use YOOtheme\Joomla\Media; use YOOtheme\Path; return [ 'config' => [ 'source' => [ 'id' => 1, ], BuilderConfig::class => __DIR__ . '/config/customizer.json', ], 'routes' => [ ['get', '/joomla/articles', [SourceController::class, 'articles']], ['get', '/joomla/users', [SourceController::class, 'users']], ], 'events' => [ 'source.init' => [Listener\LoadSourceTypes::class => 'handle'], 'builder.template' => [Listener\MatchTemplate::class => '@handle'], 'builder.template.load' => [Listener\LoadTemplateUrl::class => '@handle'], BuilderConfig::class => [Listener\LoadBuilderConfig::class => '@handle'], ], 'actions' => [ 'onLoad404' => [Listener\LoadNotFound::class => '@handle'], 'onAfterInitialiseDocument' => [ Listener\LoadSearchTemplate::class => '@afterInitialiseDocument', ], 'onLoadTemplate' => [Listener\LoadTemplate::class => '@handle'], 'onAfterDispatch' => [Listener\LoadSearchTemplate::class => ['@afterDispatch', -10]], ], 'extend' => [ Builder::class => function (Builder $builder) { $builder->addTypePath(__DIR__ . '/elements/*/element.json'); }, UpdateTransform::class => function (UpdateTransform $update) { $update->addGlobals(require __DIR__ . '/updates.php'); }, SourceTransform::class => function (SourceTransform $transform, $app) { $transform->addFilter('date', function ($value, $format) use ($app) { if (!$value) { return $value; } if ($value === $app(DatabaseDriver::class)->getNullDate()) { return; } return HTMLHelper::_('date', $value, $format ?: Text::_('DATE_FORMAT_LC3')); }); }, ], 'services' => [ Listener\LoadSearchTemplate::class => '', FileHelper::class => function () { return new FileHelper( array_map(fn($dir) => Path::join(JPATH_ROOT, $dir), Media::getRootPaths()), ); }, ], ]; theme-joomla-modules/bootstrap.php 0000644 00000001712 15166657211 0013334 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; use Joomla\CMS\Document\Document; use YOOtheme\View; return [ 'routes' => [ ['get', '/module', ModuleController::class . '@getModule'], ['post', '/module', ModuleController::class . '@saveModule'], ['get', '/modules', ModuleController::class . '@getModules'], ['get', '/positions', ModuleController::class . '@getPositions'], ], 'events' => [ 'theme.init' => [Listener\LoadModuleRenderer::class => '@handle'], 'customizer.init' => [Listener\LoadModuleData::class => '@handle'], ], 'actions' => [ 'onContentPrepareForm' => [Listener\LoadModuleForm::class => 'handle'], 'onAfterCleanModuleList' => [Listener\LoadModules::class => ['@handle', -10]], ], 'extend' => [ View::class => function (View $view, $app) { $view->addFunction('countModules', $app->wrap(Document::class . '@countModules')); }, ], ]; theme-joomla-modules/src/ModuleConfig.php 0000644 00000006143 15166657211 0014464 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; use Joomla\CMS\Language\Language; use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\User\User; use Joomla\Database\DatabaseDriver; use YOOtheme\Config; use YOOtheme\ConfigObject; /** * @property array $types * @property array $modules * @property array $positions * @property bool $canCreate */ class ModuleConfig extends ConfigObject { public User $user; public Config $config; public Language $language; public DatabaseDriver $db; /** * Constructor. */ public function __construct(User $user, Config $config, Language $language, DatabaseDriver $db) { $this->db = $db; $this->user = $user; $this->config = $config; $this->language = $language; $component = PluginHelper::isEnabled('system', 'advancedmodules') ? 'com_advancedmodules' : 'com_modules'; parent::__construct([ 'types' => $this->getTypes(), 'modules' => $this->getModules(), 'positions' => $this->getPositions(), 'canCreate' => $this->user->authorise('core.create', 'com_modules'), 'url' => "administrator/index.php?option={$component}", ]); } protected function getTypes() { $query = 'SELECT name, element FROM #__extensions WHERE client_id = 0 AND type = ' . $this->db->quote('module'); $types = array_map(function (object $type): string { $this->language->load("{$type->element}.sys", JPATH_SITE, null, false, true); return Text::_($type->name); }, $this->db->setQuery($query)->loadObjectList('element')); natsort($types); return $types; } protected function getModules() { $query = 'SELECT id, title, module, position, ordering FROM #__modules WHERE client_id = 0 AND published != -2 ORDER BY position, ordering'; return array_map( fn(object $module): array => [ 'id' => (string) $module->id, // In Joomla 4 `id` is int 'type' => $module->module, 'title' => $module->title, 'builder' => $module->module === 'mod_yootheme_builder', 'position' => $module->position, 'canEdit' => $this->user->authorise( 'core.edit', "com_modules.module.{$module->id}", ), 'canDelete' => $this->user->authorise( 'core.edit.state', "com_modules.module.{$module->id}", ), ], $this->db->setQuery($query)->loadObjectList(), ); } protected function getPositions() { $query = 'SELECT DISTINCT(position) FROM #__modules WHERE client_id = 0 ORDER BY position'; return array_values( array_unique( array_merge( array_keys($this->config->get('theme.positions', [])), $this->db->setQuery($query)->loadColumn(), ), ), ); } } theme-joomla-modules/src/ModulesRenderer.php 0000644 00000005323 15166657211 0015207 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Document\DocumentRenderer; use Joomla\CMS\Helper\ModuleHelper; use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\User\User; use YOOtheme\Config; use YOOtheme\View; use function YOOtheme\app; class ModulesRenderer extends DocumentRenderer { public function render($position, $params = [], $content = null) { [$config, $view, $user, $joomla] = app( Config::class, View::class, User::class, CMSApplication::class, ); $modules = ModuleHelper::getModules($position); $renderer = $this->_doc->loadRenderer('module'); $frontEdit = $joomla->isClient('site') && $joomla->get('frontediting', 1) && !$user->guest; $menusEdit = $joomla->get('frontediting', 1) == 2 && $user->authorise('core.edit', 'com_menus'); // Reset section transparent header if ($position === 'top') { $prevSectionTransparency = $config->get('header.section.transparent'); $config->del('header.section.transparent'); } foreach ($modules as $module) { $moduleHtml = $renderer->render($module, $params, $content); if (!isset($module->attrs)) { $module->attrs = []; } if (trim($moduleHtml) != '') { if ( $position === 'top' && ($module->type ?? '') !== 'yootheme_builder' && null === $config->get('header.section.transparent') ) { $config->set( 'header.section.transparent', (bool) $config('~theme.top.header_transparent'), ); } if ( $frontEdit && $user->authorise('module.edit.frontend', "com_modules.module.{$module->id}") ) { $displayData = [ 'moduleHtml' => &$moduleHtml, 'module' => $module, 'position' => $position, 'menusediting' => $menusEdit, ]; LayoutHelper::render('joomla.edit.frontediting_modules', $displayData); } } $module->content = $moduleHtml; } if ($position === 'top' && null === $config->get('header.section.transparent')) { $config->set('header.section.transparent', $prevSectionTransparency); } return $view( '~theme/templates/position', ['name' => $position, 'items' => $modules] + $params, ); } } theme-joomla-modules/src/Listener/LoadModuleRenderer.php 0000644 00000001115 15166657211 0017404 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; use YOOtheme\Config; class LoadModuleRenderer { public Config $config; public function __construct(Config $config) { $this->config = $config; } public function handle(): void { if ($this->config->get('app.isSite')) { $renderer = version_compare(JVERSION, '3.8', '>=') ? 'Joomla\CMS\Document\Renderer\Html\ModulesRenderer' : 'JDocumentRendererHtmlModules'; class_alias('YOOtheme\Theme\Joomla\ModulesRenderer', $renderer); } } } theme-joomla-modules/src/Listener/LoadModules.php 0000644 00000022523 15166657211 0016106 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Document\Document; use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Helper\ModuleHelper; use YOOtheme\Config; use YOOtheme\Path; use YOOtheme\View; class LoadModules { public View $view; public Config $config; public ?Document $document; public function __construct(Config $config, View $view, ?Document $document) { $this->view = $view; $this->config = $config; $this->document = $document; } public function handle($event): void { $modules = $event->getArgument('modules'); if ( $this->config->get('app.isAdmin') || !$this->config->get('theme.active') || !$this->document instanceof HtmlDocument ) { return; } $this->view['sections']->add( 'breadcrumbs', fn() => ModuleHelper::renderModule( $this->createModule([ 'module' => 'mod_breadcrumbs', 'params' => [ 'showLast' => $this->config->get('~theme.site.breadcrumbs_show_current'), 'showHome' => $this->config->get('~theme.site.breadcrumbs_show_home'), 'homeText' => $this->config->get('~theme.site.breadcrumbs_home_text'), ], ]), ), ); // Logo Module foreach (['logo', 'logo-mobile', 'dialog', 'dialog-mobile'] as $position) { if ( $content = trim( $this->view->render('~theme/templates/header-logo', ['position' => $position]), ) ) { $module = $this->createModule([ 'module' => 'mod_custom', 'position' => $position, 'content' => $content, 'type' => 'logo', 'params' => ['layout' => 'blank'], ]); array_unshift($modules, $module); } } // Search Module foreach (['~theme.header.search', '~theme.mobile.header.search'] as $key) { if ($position = $this->config->get($key)) { $position = explode(':', $position, 2); $params = []; if ($this->config->get('~theme.search_module') === 'mod_finder') { $params['show_autosuggest'] = ComponentHelper::getParams('com_finder')->get( 'show_autosuggest', 1, ); } $module = $this->createModule([ 'module' => $this->config->get('~theme.search_module'), 'position' => $position[0], 'params' => $params, ]); $position[1] == 'start' ? array_unshift($modules, $module) : array_push($modules, $module); } } // Social Module foreach (['~theme.header.social', '~theme.mobile.header.social'] as $key) { if ( $this->config->get($key) && ($content = trim( $this->view->render('~theme/templates/socials', [ 'position' => ($position = explode(':', $this->config->get($key), 2))[0], ]), )) ) { $module = $this->createModule([ 'module' => 'mod_custom', 'position' => $position[0], 'content' => $content, 'params' => ['layout' => 'blank'], ]); $position[1] == 'start' ? array_unshift($modules, $module) : array_push($modules, $module); } } // Dialog Toggle Module foreach (['~theme.dialog.toggle', '~theme.mobile.dialog.toggle'] as $key) { if ( $this->config->get($key) && ($content = trim( $this->view->render('~theme/templates/header-dialog', [ 'position' => ($position = explode(':', $this->config->get($key), 2))[0], ]), )) ) { $module = $this->createModule([ 'module' => 'mod_custom', 'position' => $position[0], 'content' => $content, 'type' => 'dialog-toggle', 'params' => ['layout' => 'blank'], ]); $position[1] == 'start' ? array_unshift($modules, $module) : array_push($modules, $module); } } // Split Header Position if ($this->config->get('~theme.header.layout') === 'stacked-center-c') { $headerModules = $this->filterModules($modules, 'header'); // Split Auto $index = $this->config->get('~theme.header.split_index') ?: ceil(count($headerModules) / 2); foreach (array_slice($headerModules, $index) as $module) { $module->position .= '-split'; } } // Push Navbar Position if ( $this->config->get('~theme.header.layout') === 'stacked-left' && ($index = $this->config->get('~theme.header.push_index')) ) { $navbarModules = $this->filterModules($modules, 'navbar'); foreach (array_slice($navbarModules, $index) as $module) { $module->position .= '-push'; } } // Push Dialog Positions foreach ( [ 'dialog' => '~theme.dialog.push_index', 'dialog-mobile' => '~theme.mobile.dialog.push_index', ] as $key => $value ) { if ($index = $this->config->get($value)) { $dialogModules = $this->filterModules($modules, $key); foreach (array_slice($dialogModules, $index) as $module) { $module->position .= '-push'; } } } $temp = $this->config->get('req.customizer.module'); // Module field defaults (Template Tab in Module edit view) $defaults = array_map( fn($field) => $field['default'] ?? '', $this->config->loadFile(Path::get('../../config/modules.json', __DIR__))['fields'], ); foreach ($modules as $module) { if (empty($module->type)) { $module->type = str_replace('mod_', '', $module->module); } $module->attrs = ['id' => "module-{$module->id}", 'class' => []]; // Replace module content with temporary customizer module content if ($temp && $temp['id'] == $module->id && $module->type === 'yootheme_builder') { $module->content = $temp['content']; } $this->config->update("~theme.modules.{$module->id}", function ($values) use ( $temp, $module, $defaults ) { $params = json_decode($module->params); // Replace module config with temporary customizer module config if (isset($temp['yoo_config']) && $temp['id'] == $module->id) { $params->yoo_config = $temp['yoo_config']; } if (isset($params->yoo_config)) { $config = $params->yoo_config; } elseif (isset($params->config)) { $config = $params->config; } else { $config = '{}'; } return [ 'showtitle' => $module->showtitle, 'class' => [$params->moduleclass_sfx ?? ''], 'title_tag' => $params->header_tag ?? 'h3', 'title_class' => $params->header_class ?? '', 'is_list' => in_array($module->type, [ 'articles_archive', 'articles_categories', 'articles_category', 'articles_latest', 'articles_popular', 'tags_popular', 'tags_similar', ]), ] + json_decode($config, true) + $defaults + ($values ?: []); }); } $event->setArgument(0, $modules); $event->setArgument('modules', $modules); } protected function createModule($module) { static $id = 0; $module = (object) array_merge( [ 'id' => 'tm-' . ++$id, 'name' => "tm-{$id}", // Joomla\CMS\Helper\ModuleHelper::getModule() requires 'name' 'title' => '', 'showtitle' => 0, 'position' => '', 'params' => '{}', ], (array) $module, ); if (is_array($module->params)) { $module->params = json_encode($module->params); } return $module; } protected function filterModules($modules, $position) { return array_filter($modules, fn($module) => $module->position === $position); } } theme-joomla-modules/src/Listener/LoadModuleData.php 0000644 00000001626 15166657211 0016516 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; use Joomla\CMS\User\User; use YOOtheme\Config; use YOOtheme\Path; use YOOtheme\Theme\Joomla\ModuleConfig; class LoadModuleData { public User $user; public Config $config; public ModuleConfig $module; public function __construct(User $user, Config $config, ModuleConfig $module) { $this->user = $user; $this->config = $config; $this->module = $module; } public function handle(): void { $this->config->add('customizer', ['module' => $this->module->getArrayCopy()]); $this->config->addFile('customizer.panels.module', Path::get('../../config/modules.json')); if ($this->user->authorise('core.manage', 'com_modules')) { $this->config->addFile( 'customizer', Path::get('../../config/customizer.json', __DIR__), ); } } } theme-joomla-modules/src/Listener/LoadModuleForm.php 0000644 00000001603 15166657211 0016543 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla\Listener; class LoadModuleForm { public static function handle($event): void { $form = $event->getArgument('subject'); $data = $event->getArgument('data'); if ( !in_array($form->getName(), [ 'com_config.modules', 'com_modules.module', 'com_advancedmodules.module', ]) ) { return; } // copy params config to yoo_config if (!isset($data->params['yoo_config']) && isset($data->params['config'])) { $data->params['yoo_config'] = $data->params['config']; } // add yoo_config hidden input field $form->load( '<form><fields name="params"><fieldset name="advanced"><field name="yoo_config" type="hidden" default="{}" /></fieldset></fields></form>', ); } } theme-joomla-modules/src/ModuleController.php 0000644 00000005431 15166657211 0015401 0 ustar 00 <?php namespace YOOtheme\Theme\Joomla; use Joomla\CMS\User\User; use Joomla\Database\DatabaseDriver; use YOOtheme\Builder; use YOOtheme\Http\Request; use YOOtheme\Http\Response; class ModuleController { protected DatabaseDriver $db; public function __construct(DatabaseDriver $db) { $this->db = $db; } public function getModule(Request $request, Response $response, Builder $builder) { $module = $this->getInstance($request->getQueryParam('id')); return $response->withJson([ 'title' => $module->title, 'params' => $module->params, 'content' => $module->module === 'mod_yootheme_builder' ? $builder->load($module->content ?? '') : $module->content, ]); } public function saveModule(Request $request, Response $response, Builder $builder, User $user) { $id = $request->getParam('id'); $data = $request->getParam('data', []); $request->abortIf(!$id, 400); $request->abortIf( !$user->authorise('core.edit', "com_modules.module.{$id}"), 403, 'Insufficient User Rights.', ); // save builder content if (array_key_exists('content', $data)) { $data = [ 'content' => json_encode( $builder ->withParams(['context' => 'save']) ->load(json_encode($data['content'])), ), ]; } return $response->withJson([ 'message' => $this->saveInstance($id, $data) ? 'success' : 'fail', ]); } public function getModules(Request $request, Response $response, ModuleConfig $module) { return $response->withJson($module->modules); } public function getPositions(Request $request, Response $response, ModuleConfig $module) { return $response->withJson($module->positions); } protected function getInstance(string $id): ?object { $query = sprintf('SELECT * FROM #__modules WHERE id = %d', $id); // decode module params $module = $this->db->setQuery($query)->loadObject(); $module->params = json_decode($module->params, true); return $module; } protected function saveInstance(string $id, array $data): bool { $data += ['id' => $id]; $object = (object) $data; // update module params if (is_array($object->params ?? null)) { $module = $this->getInstance($id); $object->params = json_encode( $object->params + $module->params, JSON_UNESCAPED_SLASHES, ); } return $this->db->updateObject('#__modules', $object, 'id'); } } theme-joomla-modules/config/customizer.json 0000644 00000012725 15166657211 0015160 0 ustar 00 { "sections": { "joomla-modules": { "title": "Modules", "priority": 40, "help": { "Modules and Positions": [ { "title": "Managing Modules", "src": "https://www.youtube-nocookie.com/watch?v=KPdzvC1BmbU&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "0:54", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#+modules", "support": "support/search?tags=125&q=modules" }, { "title": "Creating a New Module", "src": "https://www.youtube-nocookie.com/watch?v=rBd5TVnk0pM&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:06", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#add-a-new-module", "support": "support/search?tags=125&q=modules" }, { "title": "Using Module Positions", "src": "https://www.youtube-nocookie.com/watch?v=0qIrLoh1jP4&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:55", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#module-positions", "support": "support/search?tags=125&q=module%20positions" }, { "title": "Assigning Modules to Specific Pages", "src": "https://www.youtube-nocookie.com/watch?v=48GVyeC2OB4&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:09", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#module-visibility", "support": "support/search?tags=125&q=module%20visibility" } ], "Module Theme Options": [ { "title": "Setting the Module Default Options", "src": "https://www.youtube-nocookie.com/watch?v=b1aqNdyLkpA&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "0:33", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#default-options", "support": "support/search?tags=125&q=module%20theme%20settings" }, { "title": "Setting the Module Appearance Options", "src": "https://www.youtube-nocookie.com/watch?v=oNxjulZebfc&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:08", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#appearance-options", "support": "support/search?tags=125&q=module%20theme%20settings" }, { "title": "Setting the Module Grid Options", "src": "https://www.youtube-nocookie.com/watch?v=cYY0hIVJlr0&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:41", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#grid-options", "support": "support/search?tags=125&q=module%20theme%20settings%20grid" }, { "title": "Setting the Module List Options", "src": "https://www.youtube-nocookie.com/watch?v=R3g2cgnC3SM&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "0:38", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#list-options", "support": "support/search?tags=125&q=module%20theme%20settings%20list" }, { "title": "Setting the Module Menu Options", "src": "https://www.youtube-nocookie.com/watch?v=5oXRtSuVTtk&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:24", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#menu-options", "support": "support/search?tags=125&q=module%20theme%20settings%20menu" } ], "Builder Module": [ { "title": "Using the Builder Module", "src": "https://www.youtube-nocookie.com/watch?v=msRBkqxnZ18&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "1:58", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#builder-module", "support": "support/search?tags=125&q=builder%20module" }, { "title": "Creating Advanced Module Layouts", "src": "https://www.youtube-nocookie.com/watch?v=jr09mnXDbIA&list=PLrqT0WH0HPdPfykSwhMt6Jl2_RgJ6ixU-", "duration": "4:16", "documentation": "support/yootheme-pro/joomla/widgets-and-modules#advanced-layouts", "support": "support/search?tags=125&q=builder%20module" } ] } } } } theme-joomla-modules/config/modules.json 0000644 00000023234 15166657211 0014421 0 ustar 00 { "fields": { "visibility": { "label": "Visibility", "description": "Display the module only from this device width and larger.", "type": "select", "default": "", "options": { "Always": "", "Small (Phone Landscape)": "s", "Medium (Tablet Landscape)": "m", "Large (Desktop)": "l", "X-Large (Large Screens)": "xl" } }, "style": { "label": "Style", "description": "Select a panel style.", "type": "select", "default": "", "options": { "None": "", "Card Default": "card-default", "Card Primary": "card-primary", "Card Secondary": "card-secondary", "Card Hover": "card-hover" }, "show": "!$match(this.position, '^(toolbar-(left|right)|(logo|navbar|header|dialog)(-mobile)?|debug)$')" }, "title_style": { "label": "Title Style", "description": "Title styles differ in font-size but may also come with a predefined color, size and font.", "type": "select", "default": "", "options": { "None": "", "3X-Large": "heading-3xlarge", "2X-Large": "heading-2xlarge", "X-Large": "heading-xlarge", "Large": "heading-large", "Medium": "heading-medium", "Small": "heading-small", "H1": "h1", "H2": "h2", "H3": "h3", "H4": "h4", "H5": "h5", "H6": "h6" }, "show": "!$match(this.position, '^(toolbar-(left|right)|(logo|navbar|header)(-mobile)?|debug)$')" }, "title_decoration": { "label": "Title Decoration", "description": "Decorate the title with a divider, bullet or a line that is vertically centered to the heading.", "type": "select", "default": "", "options": { "None": "", "Divider": "divider", "Bullet": "bullet", "Line": "line" }, "show": "!$match(this.position, '^(toolbar-(left|right)|(logo|navbar|header)(-mobile)?|debug)$')" }, "text_align": { "label": "Alignment", "description": "Center, left and right alignment may depend on a breakpoint and require a fallback.", "type": "select", "default": "", "options": { "None": "", "Left": "left", "Center": "center", "Right": "right", "Justify": "justify" }, "show": "!$match(this.position, '^(toolbar-(left|right)|(logo|navbar|header)(-mobile)?|debug)$')" }, "text_align_breakpoint": { "label": "Alignment Breakpoint", "description": "Define the device width from which the alignment will apply.", "type": "select", "default": "", "options": { "Always": "", "Small (Phone Landscape)": "s", "Medium (Tablet Landscape)": "m", "Large (Desktop)": "l", "X-Large (Large Screens)": "xl" }, "show": "!$match(this.position, '^(toolbar-(left|right)|(logo|navbar|header)(-mobile)?|debug)$') && text_align && text_align != 'justify'" }, "text_align_fallback": { "label": "Alignment Fallback", "description": "Define an alignment fallback for device widths below the breakpoint.", "type": "select", "default": "", "options": { "None": "", "Left": "left", "Center": "center", "Right": "right", "Justify": "justify" }, "show": "!$match(this.position, '^(toolbar-(left|right)|(logo|navbar|header)(-mobile)?|debug)$') && text_align && text_align != 'justify' && text_align_breakpoint" }, "width": { "label": "Width", "description": "The width of the grid column that contains the module.", "type": "select", "default": "", "options": { "Expand": "", "20%": "1-5", "25%": "1-4", "33%": "1-3", "40%": "2-5", "50%": "1-2", "100%": "1-1" }, "show": "$match(this.position, '^(top|bottom|builder-\\d+)$')" }, "maxwidth": { "label": "Max Width", "description": "The module maximum width.", "type": "select", "default": "", "options": { "None": "", "Small": "small", "Medium": "medium", "Large": "large", "X-Large": "xlarge", "2X-Large": "2xlarge" }, "show": "$match(this.position, '^(top|bottom|builder-\\d+)$')" }, "maxwidth_align": { "label": "Max Width (Alignment)", "description": "Set how the module should align when the container is larger than its max-width.", "type": "checkbox", "text": "Center the module", "show": "maxwidth && $match(this.position, '^(top|bottom|builder-\\d+)$')" }, "list_style": { "label": "List Style", "description": "Select the list style.", "type": "select", "default": "", "options": { "None": "", "Divider": "divider" }, "show": "$match(this.type, 'articles_(archive|categories|latest|popular)|tags_(popular|similar)')" }, "link_style": { "label": "Link Style", "description": "Select the link style.", "type": "select", "default": "", "options": { "None": "", "Muted": "muted", "Text": "text" }, "show": "$match(this.type, 'articles_(archive|categories|latest|popular)|tags_(popular|similar)')" }, "menu_type": { "label": "Menu Type", "description": "Select the menu type.", "type": "select", "default": "", "options": { "Default": "", "Nav": "nav", "Subnav": "subnav", "Iconnav": "iconnav" }, "show": "$match(this.type, 'menu')" }, "menu_divider": { "label": "Menu Divider", "description": "Show optional dividers between nav or subnav items.", "type": "checkbox", "text": "Show dividers", "show": "$match(this.type, 'menu')" }, "menu_style": { "label": "Menu Style", "description": "Select the nav style.", "type": "select", "default": "default", "options": { "Default": "default", "Primary": "primary", "Secondary": "secondary" }, "show": "$match(this.type, 'menu')" }, "menu_size": { "label": "Menu Primary Size", "description": "Select the primary nav size.", "type": "select", "default": "", "options": { "Default": "", "Medium": "medium", "Large": "large", "X-Large": "xlarge" }, "show": "$match(this.type, 'menu')", "enable": "menu_style == 'primary'" }, "menu_image_width": { "label": "Menu Image Width", "description": "Setting just one value preserves the original proportions. The image will be resized and cropped automatically, and where possible, high resolution images will be auto-generated.", "attrs": { "placeholder": "auto" }, "show": "$match(this.type, 'menu')" }, "menu_image_height": { "label": "Menu Image Height", "description": "Setting just one value preserves the original proportions. The image will be resized and cropped automatically, and where possible, high resolution images will be auto-generated.", "attrs": { "placeholder": "auto" }, "show": "$match(this.type, 'menu')" }, "menu_image_svg_inline": { "label": "Menu Inline SVG", "description": "Inject SVG images into the markup so they adopt the text color automatically.", "type": "checkbox", "text": "Make SVG stylable with CSS", "show": "$match(this.type, 'menu')" }, "menu_icon_width": { "label": "Menu Icon Width", "description": "Set the icon width.", "show": "$match(this.type, 'menu')" }, "menu_image_margin": { "label": "Menu Image and Title", "type": "checkbox", "text": "Add margin between", "default": true, "show": "$match(this.type, 'menu')" }, "menu_image_align": { "label": "Menu Image Align", "type": "select", "default": "center", "options": { "Top": "top", "Center": "center" }, "show": "$match(this.type, 'menu')" } } } theme-analytics/bootstrap.php 0000644 00000000210 15166657211 0012364 0 ustar 00 <?php namespace YOOtheme\Theme\Analytics; return [ 'events' => ['theme.head' => [Listener\LoadThemeHead::class => '@handle']], ]; theme-analytics/app/analytics.min.js 0000644 00000000760 15166657211 0013537 0 ustar 00 /*! YOOtheme Pro v4.5.33 | https://yootheme.com */ (function(){"use strict";function e(t,n={}){a("js",new Date),a("config",t,n),o(`https://www.googletagmanager.com/gtag/js?id=${t}`)}function a(){let{dataLayer:t}=window;t||(t=window.dataLayer=[]),t.push(arguments)}function o(t){const n=document.createElement("script");n.src=t,n.async=!0,document.head.appendChild(n)}(window.$load||(window.$load=[])).push(({google_analytics:t,google_analytics_anonymize:n},c)=>{e(t,{anonymize_ip:n}),c()})})(); theme-analytics/src/Listener/LoadThemeHead.php 0000644 00000001637 15166657211 0015365 0 ustar 00 <?php namespace YOOtheme\Theme\Analytics\Listener; use YOOtheme\Config; use YOOtheme\Metadata; use YOOtheme\Path; class LoadThemeHead { public Config $config; public Metadata $metadata; public function __construct(Config $config, Metadata $metadata) { $this->config = $config; $this->metadata = $metadata; } public function handle(): void { $keys = ['google_analytics', 'google_analytics_anonymize']; if ($this->config->get("~theme.{$keys[0]}")) { foreach ($keys as $key) { $this->config->set( "theme.data.{$key}", trim($this->config->get("~theme.{$key}", '')), ); } $this->metadata->set('script:analytics', [ 'src' => Path::get('../../app/analytics.min.js', __DIR__), 'defer' => true, ]); } } } styler/config/customizer.json 0000644 00000000215 15166657211 0012442 0 ustar 00 { "sections": { "styler": { "title": "Style", "width": 350, "priority": 11 } } } styler/config/styler.json 0000644 00000133067 15166657211 0011574 0 ustar 00 { "ignore": [ "@deprecated", "@breakpoint-*-max", "@internal*", "@heading-primary-*", "@heading-hero-*", "@woocommerce-*" ], "components": { "global": { "name": "Global", "general": true, "groups": { "typography": ["@global(-*)?-font-*", "@global-line-height"], "primary": "@global-primary-*", "secondary": "@global-secondary-*", "tertiary": "@global-tertiary-*", "colors": "@global(-*)?-color", "backgrounds": "@global(-*)?-background", "borders": "@global-border*", "box shadows": "@global-*-box-shadow", "spacings": ["@global(-*)?-margin", "@global(-*)?-gutter"], "controls": "@global-control-*", "z index": "@global-z-index", "breakpoints": "@breakpoint-*" } }, "theme": { "name": "Theme", "general": true, "groups": { "page": "@theme-page-*", "page container": "@theme-page-container-*", "toolbar": "@theme-toolbar-*", "headerbar": "@theme-headerbar-*", "headerbar top": "@theme-headerbar-top-*", "headerbar bottom": "@theme-headerbar-bottom-*", "headerbar stacked": "@theme-headerbar-stacked-*", "sidebar": "@theme-sidebar-*", "section title": "@section-title-*", "mask default": "@theme-mask-default-*", "box decoration": "@theme-box-decoration-*", "box decoration default": "@theme-box-decoration-default-*", "box decoration primary": "@theme-box-decoration-primary-*", "box decoration secondary": "@theme-box-decoration-secondary-*", "transition border": "@theme-transition-border-*" }, "hover": ".tm-page, .tm-toolbar, .tm-sidebar, .tm-headerbar-top, .tm-headerbar-bottom, .tm-headerbar-stacked, .tm-section-title, .tm-box-decoration-default, .tm-box-decoration-primary, .tm-box-decoration-secondary, .tm-transition-border", "inspect": ".tm-page-container, .tm-page, tm-page > *, .tm-toolbar, .tm-toolbar > *, .tm-sidebar, .tm-sidebar > *, .tm-headerbar-top, .tm-headerbar-top > *, .tm-headerbar-bottom, .tm-headerbar-bottom > *, .tm-headerbar-stacked, .tm-headerbar-stacked > *, .tm-section-title, .tm-section-title > *, .tm-box-decoration-default, .tm-box-decoration-default > *, .tm-box-decoration-primary, .tm-box-decoration-primary > *, .tm-box-decoration-secondary, .tm-box-decoration-secondary > *, .tm-transition-border, .tm-transition-border > *" }, "inverse": { "name": "Inverse", "general": true, "groups": { "global": ["@inverse-global-color-mode", "@inverse-global-*"], "theme": [ "@inverse-section-title-*", "@inverse-theme-box-decoration-default-*", "@inverse-theme-box-decoration-primary-*", "@inverse-theme-box-decoration-secondary-*" ], "woocommerce": "@inverse-woocommerce-*", "accordion": "@inverse-accordion-*", "article": "@inverse-article-*", "badge": "@inverse-badge-*", "base": "@inverse-base-*", "breadcrumb": "@inverse-breadcrumb-*", "button default": "@inverse-button-default-*", "button primary": "@inverse-button-primary-*", "button secondary": "@inverse-button-secondary-*", "button danger": "@inverse-button-danger-*", "button disabled": "@inverse-button-disabled-*", "button text": "@inverse-button-text-(?!transform)*", "button link": "@inverse-button-link-*", "card": "@inverse-card-*", "close": "@inverse-close-*", "column": "@inverse-column-*", "countdown": "@inverse-countdown-*", "divider": "@inverse-divider-*", "dotnav": "@inverse-dotnav-*", "form": "@inverse-form-*", "form danger": "@inverse-form-danger-*", "form success": "@inverse-form-success-*", "form blank": "@inverse-form-blank-*", "form select": "@inverse-form-select-*", "form radio": "@inverse-form-radio-*", "form legend": "@inverse-form-legend-*", "form label": "@inverse-form-label-*", "form icon": "@inverse-form-icon-*", "grid divider": "@inverse-grid-divider-*", "heading": "@inverse-heading-*", "icon link": "@inverse-icon-link-*", "icon button": "@inverse-icon-button-*", "iconnav": "@inverse-iconnav-*", "label": "@inverse-label-*", "leader": "@inverse-leader-*", "link": "@inverse-link-*", "list": "@inverse-list-*", "marker": "@inverse-marker-*", "nav": "@inverse-nav-*", "nav default": "@inverse-nav-default-*", "nav primary": "@inverse-nav-primary-*", "nav secondary": "@inverse-nav-secondary-*", "navbar": "@inverse-navbar-*", "navbar nav item": "@inverse-navbar-nav-item-*", "navbar nav item line": "@inverse-navbar-nav-item-line-*", "pagination": "@inverse-pagination-*", "search": "@inverse-search-*", "search default": "@inverse-search-default-*", "search navbar": "@inverse-search-navbar-*", "search medium": "@inverse-search-medium-*", "search large": "@inverse-search-large-*", "search toggle": "@inverse-search-toggle-*", "slidenav": "@inverse-slidenav-*", "subnav": "@inverse-subnav-*", "subnav divider": "@inverse-subnav-divider-*", "subnav pill": "@inverse-subnav-pill-*", "tab": "@inverse-tab-*", "table": "@inverse-table-*", "text": "@inverse-text-*", "thumbnav": "@inverse-thumbnav-*", "totop": "@inverse-totop-*", "utility": ["@inverse-dropcap-*", "@inverse-logo-*"] } }, "accordion": { "name": "Accordion", "groups": { "item": "@accordion-item-*", "title": "@accordion-title-*", "icon": "@accordion-icon-*", "content": "@accordion-content-*" }, "hover": ".uk-accordion", "inspect": ".uk-accordion, .uk-accordion-title, .uk-accordion-title > *, .uk-accordion-content, .uk-accordion-content > *" }, "alert": { "name": "Alert", "groups": { "primary": "@alert-primary-*", "success": "@alert-success-*", "warning": "@alert-warning-*", "danger": "@alert-danger-*", "close": "@alert-close-*" }, "hover": ".uk-alert", "inspect": ".uk-alert, .uk-alert > *, .uk-alert-close" }, "align": { "name": "Align", "hover": "[class*='uk-align']", "inspect": "[class*='uk-align'], [class*='uk-align'] > *" }, "animation": { "name": "Animation", "groups": { "slide": "@animation-slide-*" }, "hover": "[class*='uk-animation-']", "inspect": "[class*='uk-animation-'], [class*='uk-animation-'] > *" }, "article": { "name": "Article", "groups": { "title": "@article-title-*", "meta": "@article-meta-*", "meta link": "@article-meta-link-*" }, "hover": ".uk-article", "inspect": ".uk-article, .uk-article > *, .uk-article-title, .uk-article-title > *, .uk-article-meta, .uk-article-meta > *" }, "background": { "name": "Background", "groups": { "default": "@background-default-*", "muted": "@background-muted-*", "primary": "@background-primary-*", "secondary": "@background-secondary-*" }, "hover": ".uk-background-default, .uk-background-muted, .uk-background-primary, .uk-background-secondary", "inspect": ".uk-background-default, .uk-background-muted, .uk-background-primary, .uk-background-secondary" }, "badge": { "name": "Badge", "groups": { "badge": "@badge-*" }, "hover": ".uk-badge", "inspect": ".uk-badge, .uk-badge > *" }, "base": { "name": "Base", "groups": { "body": "@base-body-*", "link": "@base-link-*", "strong": "@base-strong-*", "code": "@base-code-*", "em": "@base-em-*", "ins": "@base-ins-*", "mark": "@base-mark-*", "quote": "@base-quote-*", "small": "@base-small-*", "margin": "@base-margin-*", "heading": "@base-heading-*", "h1": "@base-h1-*", "h2": "@base-h2-*", "h3": "@base-h3-*", "h4": "@base-h4-*", "h5": "@base-h5-*", "h6": "@base-h6-*", "list": "@base-list-*", "hr": "@base-hr-*", "blockquote": "@base-blockquote-*", "blockquote footer": "@base-blockquote-footer-*", "pre": "@base-pre-*", "focus": "@base-focus-*", "selection": "@base-selection-*" }, "hover": "a, strong, code, kbd, samp, em, ins, mark, q, small, p, ul, ol, dl, pre, address, fieldset, figure, h1, h2, h3, h4, h5, h6, hr, blockquote, .uk-h1, .uk-h2, .uk-h3, .uk-h4, .uk-h5, .uk-h6, .uk-link", "inspect": ":not(.uk-nav):not(.uk-nav-sub):not(.uk-navbar-nav):not(.uk-subnav):not(.uk-breadcrumb):not(.uk-pagination):not(.uk-tab) > * > a:not([class]), * > a:not([class]), * > * > a:not([class]), strong, :not(pre) > code, :not(pre) > kbd, :not(pre) > samp, em, ins, mark, q, small, h1, h2, h3, h4, h5, h6, h1 > *, h2 > *, h3 > *, h4 > *, h5 > *, h6 > *, hr:not(.uk-divider-icon), blockquote, blockquote > *, pre, pre > *, .uk-h1, .uk-h2, .uk-h3, .uk-h4, .uk-h5, .uk-h6, .uk-h1 > *, .uk-h2 > *, .uk-h3 > *, .uk-h4 > *, .uk-h5 > *, .uk-h6 > *, .uk-link" }, "breadcrumb": { "name": "Breadcrumb", "groups": { "item": "@breadcrumb-item-*", "divider": "@breadcrumb-divider*" }, "hover": ".uk-breadcrumb", "inspect": ".uk-breadcrumb, .uk-breadcrumb > *, .uk-breadcrumb > * > *" }, "button": { "name": "Button", "groups": { "default": "@button-default-*", "primary": "@button-primary-*", "secondary": "@button-secondary-*", "danger": "@button-danger-*", "disabled": "@button-disabled-*", "text": ["@button-text-mode", "@button-text-(?!transform)*"], "link": "@button-link-*", "small": "@button-small-*", "large": "@button-large-*" }, "hover": ".uk-button", "inspect": ".uk-button, .uk-button-group" }, "card": { "name": "Card", "groups": { "body": "@card-body-*", "header": "@card-header-*", "footer": "@card-footer-*", "title": "@card-title-*", "badge": "@card-badge-*", "hover": "@card-hover*", "default": "@card-default-*", "default title": "@card-default-title-*", "default header": "@card-default-header-*", "default footer": "@card-default-footer-*", "primary": "@card-primary-*", "primary title": "@card-primary-title-*", "secondary": "@card-secondary-*", "secondary title": "@card-secondary-title-*", "small": "@card-small-*", "large": "@card-large-*" }, "hover": ".uk-card", "inspect": ".uk-card, .uk-card-body, .uk-card-body > *, .uk-card-header, .uk-card-header > *, .uk-card-footer, .uk-card-footer > *, [class*='uk-card-media'], .uk-card-title, .uk-card-title > *, .uk-card-badge" }, "close": { "name": "Close", "hover": ".uk-close", "inspect": ".uk-close, .uk-close svg" }, "column": { "name": "Column", "groups": { "divider": "@column-divider-*" }, "hover": "[class*='uk-column-']", "inspect": "[class*='uk-column-'], [class*='uk-column-'] > *" }, "comment": { "name": "Comment", "groups": { "header": "@comment-header-*", "title": "@comment-title-*", "meta": "@comment-meta-*", "list": "@comment-list-*", "primary": "@comment-primary-*" }, "hover": ".uk-comment", "inspect": ".uk-comment, .uk-comment-body, .uk-comment-body > *, .uk-comment-header, .uk-comment-header > *, .uk-comment-title, .uk-comment-title > *, .uk-comment-meta, .uk-comment-meta > *, .uk-comment-avatar, .uk-comment-avatar > *, .uk-comment-list, .uk-comment-list ul, .uk-comment-list li, .uk-comment-primary, uk-comment-primary > *" }, "container": { "name": "Container", "groups": { "xsmall": "@container-xsmall-*", "small": "@container-small-*", "large": "@container-large-*", "xlarge": "@container-xlarge-*" }, "hover": ".uk-container", "inspect": ".uk-container, .uk-container > *" }, "countdown": { "name": "Countdown", "groups": { "item": "@countdown-item-*", "number": "@countdown-number-*", "separator": "@countdown-separator-*", "label": "@countdown-label-*" }, "hover": ".uk-countdown", "inspect": ".uk-countdown, .uk-countdown > *" }, "description-list": { "name": "Description List", "groups": { "term": "@description-list-term-*", "description": "@description-list-description-*", "divider": "@description-list-divider-*" }, "hover": ".uk-description-list", "inspect": ".uk-description-list, .uk-description-list > dt, .uk-description-list > dd" }, "divider": { "name": "Divider", "groups": { "divider icon": "@divider-icon-*", "divider small": "@divider-small-*", "divider vertical": "@divider-vertical-*" }, "hover": ".uk-divider-icon, .uk-divider-small, .uk-divider-vertical", "inspect": ".uk-divider-icon, .uk-divider-small, .uk-divider-vertical" }, "dotnav": { "name": "Dotnav", "groups": { "item": "@dotnav-item-*" }, "hover": ".uk-dotnav", "inspect": ".uk-dotnav, .uk-dotnav > *, .uk-dotnav > * > *" }, "drop": { "name": "Drop", "groups": { "parent icon": "@drop-parent-icon-*" }, "hover": ".uk-drop", "inspect": ".uk-drop, .uk-drop > *, .uk-drop-grid, .uk-drop-grid > *" }, "dropdown": { "name": "Dropdown", "groups": { "large": "@dropdown-large-*", "nav": "@dropdown-nav-*", "nav item": "@dropdown-nav-item-*", "nav subtitle": "@dropdown-nav-subtitle-*", "nav header": "@dropdown-nav-header-*", "nav divider": "@dropdown-nav-divider-*", "nav sublist": "@dropdown-nav-sublist-*" }, "hover": ".uk-dropdown", "inspect": ".uk-dropdown, .uk-dropdown > *, .uk-dropdown-nav, .uk-dropdown-nav > *, .uk-dropdown-nav > * > *" }, "dropbar": { "name": "Dropbar", "groups": { "large": "@dropbar-large-*", "top": "@dropbar-top-*", "bottom": "@dropbar-bottom-*", "left": "@dropbar-left-*", "right": "@dropbar-right-*" }, "hover": ".uk-dropbar", "inspect": ".uk-dropbar, .uk-dropbar > *" }, "form": { "name": "Form", "groups": { "multi line": ["@form-multi-line-*", "@form-padding-vertical"], "danger": "@form-danger-*", "success": "@form-success-*", "blank": "@form-blank-*", "select": "@form-select-*", "range": "@form-range-*", "small": "@form-small-*", "large": "@form-large-*", "width": "@form-width-*", "radio": "@form-radio-*", "legend": "@form-legend-*", "label": "@form-label-*", "stacked": "@form-stacked-*", "horizontal": "@form-horizontal-*", "icon": "@form-icon-*" }, "hover": ".uk-input, .uk-select, .uk-textarea, .uk-range, .uk-radio, .uk-checkbox, .uk-legend, .uk-fieldset, [class*='uk-form']", "inspect": ".uk-input, .uk-select, .uk-textarea, .uk-range, .uk-radio, .uk-checkbox, .uk-legend, .uk-fieldset, [class*='uk-form'], [class*='uk-form'] > *" }, "grid": { "name": "Grid", "groups": { "small": "@grid-small-*", "medium": "@grid-medium-*", "large": "@grid-large-*", "divider": "@grid-divider-*" }, "hover": ".uk-grid", "inspect": ".uk-grid, .uk-grid > *, .uk-grid > * > *" }, "heading": { "name": "Heading", "groups": { "small": "@heading-small-*", "medium": "@heading-medium-*", "large": "@heading-large-*", "xlarge": "@heading-xlarge-*", "2xlarge": "@heading-2xlarge-*", "3xlarge": "@heading-3xlarge-*", "divider": "@heading-divider-*", "bullet": "@heading-bullet-*", "line": "@heading-line-*" }, "hover": ".uk-heading-small, .uk-heading-medium, .uk-heading-large, .uk-heading-xlarge, .uk-heading-2xlarge, .uk-heading-3xlarge, .uk-heading-divider, .uk-heading-bullet, .uk-heading-line", "inspect": ".uk-heading-small, .uk-heading-small > *, .uk-heading-medium, .uk-heading-medium > *, .uk-heading-large, .uk-heading-large > *, .uk-heading-xlarge, .uk-heading-xlarge > *, .uk-heading-2xlarge, .uk-heading-2xlarge > *, .uk-heading-3xlarge, .uk-heading-3xlarge > *, .uk-heading-divider, .uk-heading-divider > *, .uk-heading-bullet, .uk-heading-bullet > *, .uk-heading-line, .uk-heading-line > *" }, "height": { "name": "Height", "hover": ".uk-height-small, .uk-height-medium, .uk-height-large", "inspect": ".uk-height-small, .uk-height-small > *, .uk-height-medium, .uk-height-medium > *, .uk-height-large, .uk-height-large > *" }, "icon": { "name": "Icon", "groups": { "link": "@icon-link-*", "button": "@icon-button-*", "image": "@icon-image-*" }, "hover": ".uk-icon", "inspect": ".uk-icon, .uk-icon svg" }, "iconnav": { "name": "Iconnav", "groups": { "item": "@iconnav-item-*", "siblings": "@iconnav-siblings-*" }, "hover": ".uk-iconnav", "inspect": ".uk-iconnav, .uk-iconnav > *, .uk-iconnav > * > *, .uk-iconnav > * > * > *" }, "label": { "name": "Label", "groups": { "success": "@label-success-*", "warning": "@label-warning-*", "danger": "@label-danger-*" }, "hover": ".uk-label", "inspect": ".uk-label, .uk-label > *" }, "leader": { "name": "Leader", "hover": ".uk-leader", "inspect": ".uk-leader, .uk-leader > *" }, "lightbox": { "name": "Lightbox", "groups": { "item": "@lightbox-item-*", "thumbnav": "@lightbox-thumbnav-*", "caption": "@lightbox-caption-*" }, "hover": ".uk-lightbox", "inspect": ".uk-lightbox, .uk-lightbox > *, .uk-lightbox-item, .uk-lightbox-item > *" }, "link": { "name": "Link", "groups": { "muted": "@link-muted-*", "text": "@link-text-*", "heading": "@link-heading-*" }, "hover": ".uk-link-muted, .uk-link-text, .uk-link-heading, .uk-link-reset", "inspect": ".uk-link-muted, .uk-link-text, .uk-link-heading, .uk-link-reset" }, "list": { "name": "List", "groups": { "color": [ "@list-muted-color", "@list-emphasis-color", "@list-primary-color", "@list-secondary-color" ], "bullet": "@list-bullet-*", "divider": "@list-divider-*", "striped": "@list-striped-*", "large": "@list-large-*", "large divider": "@list-large-divider-*", "large striped": "@list-large-striped-*" }, "hover": ".uk-list", "inspect": ".uk-list, .uk-list > *, .uk-list > * > *" }, "margin": { "name": "Margin", "groups": { "small": "@margin-small-*", "medium": "@margin-medium-*", "large": "@margin-large-*", "xlarge": "@margin-xlarge-*" }, "hover": "[class*='uk-margin']", "inspect": "[class*='uk-margin'], [class*='uk-margin'] > *" }, "marker": { "name": "Marker", "hover": ".uk-marker", "inspect": ".uk-marker, .uk-marker svg" }, "modal": { "name": "Modal", "groups": { "dialog": "@modal-dialog-*", "container": "@modal-container-*", "body": "@modal-body-*", "header": "@modal-header-*", "footer": "@modal-footer-*", "title": "@modal-title-*", "close": "@modal-close-*", "close outside": "@modal-close-outside-*" }, "hover": ".uk-modal", "inspect": ".uk-modal, .uk-modal-dialog, .uk-modal-container, .uk-modal-body, .uk-modal-body > *, .uk-modal-header, .uk-modal-header > *, .uk-modal-footer, .uk-modal-footer > *, .uk-modal-title, .uk-modal-title > *, [class*='uk-modal-close-']" }, "nav": { "name": "Nav", "groups": { "item": "@nav-item-*", "sublist": "@nav-sublist-*", "parent icon": "@nav-parent-icon-*", "header": "@nav-header-*", "divider": "@nav-divider-*", "default": "@nav-default-*", "default item": "@nav-default-item-*", "default subtitle": "@nav-default-subtitle-*", "default header": "@nav-default-header-*", "default divider": "@nav-default-divider-*", "default sublist": "@nav-default-sublist-*", "default item line": "@nav-default-item-line-*", "default siblings": "@nav-default-siblings-*", "primary": "@nav-primary-*", "primary item": "@nav-primary-item-*", "primary subtitle": "@nav-primary-subtitle-*", "primary header": "@nav-primary-header-*", "primary divider": "@nav-primary-divider-*", "primary sublist": "@nav-primary-sublist-*", "primary item line": "@nav-primary-item-line-*", "primary siblings": "@nav-primary-siblings-*", "secondary": "@nav-secondary-*", "secondary item": "@nav-secondary-item-*", "secondary subtitle": "@nav-secondary-subtitle-*", "secondary header": "@nav-secondary-header-*", "secondary divider": "@nav-secondary-divider-*", "secondary sublist": "@nav-secondary-sublist-*", "dividers": "@nav-dividers-*" }, "hover": ".uk-nav", "inspect": ".uk-nav, .uk-nav > *, .uk-nav > * > *, .uk-nav-sub, .uk-nav-sub > *, .uk-nav-sub > * > *" }, "navbar": { "name": "Navbar", "groups": { "group": "@navbar-group-*", "nav": "@navbar-nav-*", "nav item": "@navbar-nav-item-*", "nav item line": [ "@navbar-nav-item-line-mode", "@navbar-nav-item-line-position-mode", "@navbar-nav-item-line-slide-mode", "@navbar-nav-item-line-*" ], "parent icon": "@navbar-parent-icon-*", "item": "@navbar-item-*", "toggle": "@navbar-toggle-*", "subtitle": "@navbar-subtitle-*", "primary": "@navbar-primary-*", "primary nav": "@navbar-primary-nav-*", "primary nav item": "@navbar-primary-nav-item-*", "primary item": "@navbar-primary-item-*", "primary toggle": "@navbar-primary-toggle-*", "sticky": "@navbar-sticky-*", "dropdown": "@navbar-dropdown-*", "dropdown large": "@navbar-dropdown-large-*", "dropdown dropbar": "@navbar-dropdown-dropbar-*", "dropdown dropbar large": "@navbar-dropdown-dropbar-large-*", "dropdown grid": "@navbar-dropdown-grid-*", "dropdown nav": "@navbar-dropdown-nav-*", "dropdown nav item": "@navbar-dropdown-nav-item-*", "dropdown nav subtitle": "@navbar-dropdown-nav-subtitle-*", "dropdown nav header": "@navbar-dropdown-nav-header-*", "dropdown nav divider": "@navbar-dropdown-nav-divider-*", "dropdown nav sublist": "@navbar-dropdown-nav-sublist-*", "dropbar": "@navbar-dropbar-*" }, "hover": ".uk-navbar-container", "inspect": ".uk-navbar-container, .uk-navbar, .uk-navbar-left, .uk-navbar-right, .uk-navbar-center, .uk-navbar-nav, .uk-navbar-nav > *, .uk-navbar-nav > * > *, .uk-navbar-item, .uk-navbar-item > *, .uk-navbar-toggle, .uk-navbar-toggle > *, .uk-navbar-subtitle, .uk-navbar-dropbar, .uk-navbar-dropdown, .uk-navbar-dropdown-nav, .uk-navbar-dropdown-nav > *, .uk-navbar-dropdown-nav > * > *, .uk-navbar-toggle-icon" }, "notification": { "name": "Notification", "groups": { "message": "@notification-message-*", "close": "@notification-close-*", "message primary": "@notification-message-primary-*", "message success": "@notification-message-success-*", "message warning": "@notification-message-warning-*", "message danger": "@notification-message-danger-*" }, "hover": ".uk-notification", "inspect": ".uk-notification, .uk-notification-message, .uk-notification-message > *, .uk-notification-close" }, "offcanvas": { "name": "Offcanvas", "groups": { "bar": "@offcanvas-bar-*", "close": "@offcanvas-close-*", "overlay": "@offcanvas-overlay-*" }, "hover": ".uk-offcanvas", "inspect": ".uk-offcanvas, .uk-offcanvas-overlay, .uk-offcanvas-bar, .uk-offcanvas-bar > *" }, "overlay": { "name": "Overlay", "groups": { "default": "@overlay-default-*", "primary": "@overlay-primary-*" }, "hover": ".uk-overlay", "inspect": ".uk-overlay, .uk-overlay > *, .uk-overlay-icon, .uk-overlay-icon svg" }, "padding": { "name": "Padding", "groups": { "small": "@padding-small-*", "large": "@padding-large-*" }, "hover": "[class*='uk-padding']", "inspect": "[class*='uk-padding'], [class*='uk-padding'] > *" }, "pagination": { "name": "Pagination", "groups": { "item": "@pagination-item-*", "item next previous": "@pagination-item-next-previous-*" }, "hover": ".uk-pagination", "inspect": ".uk-pagination, .uk-pagination > *, .uk-pagination > * > *, .uk-pagination-next, .uk-pagination-next svg, .uk-pagination-previous, .uk-pagination-previous svg" }, "placeholder": { "name": "Placeholder", "hover": ".uk-placeholder", "inspect": ".uk-placeholder, .uk-placeholder > *" }, "position": { "name": "Position", "hover": "[class*='uk-position']", "inspect": "[class*='uk-position'], [class*='uk-position'] > *" }, "progress": { "name": "Progress", "groups": { "bar": "@progress-bar-*" }, "hover": ".uk-progress", "inspect": ".uk-progress" }, "search": { "name": "Search", "groups": { "default": "@search-default-*", "navbar": "@search-navbar-*", "medium": "@search-medium-*", "large": "@search-large-*", "toggle": "@search-toggle-*" }, "hover": ".uk-search", "inspect": ".uk-search, .uk-search-input, .uk-search-toggle, .uk-search-icon, .uk-search-icon svg" }, "section": { "name": "Section", "groups": { "default": "@section-default-*", "muted": "@section-muted-*", "primary": "@section-primary-*", "secondary": "@section-secondary-*", "xsmall": "@section-xsmall-*", "small": "@section-small-*", "large": "@section-large-*", "xlarge": "@section-xlarge-*" }, "hover": ".uk-section", "inspect": ".uk-section, .uk-section > *" }, "slidenav": { "name": "Slidenav", "groups": { "large": "@slidenav-large-*" }, "hover": ".uk-slidenav", "inspect": ".uk-slidenav, .uk-slidenav svg" }, "sortable": { "name": "Sortable", "hover": ".uk-sortable", "inspect": ".uk-sortable, .uk-sortable > *, .uk-sortable > * > *, .uk-sortable-drag, .uk-sortable-drag > *, .uk-sortable-placeholder, .uk-sortable-placeholder > *, .uk-sortable-empty, .uk-sortable-empty > *" }, "spinner": { "name": "Spinner", "hover": ".uk-spinner", "inspect": ".uk-spinner, .uk-spinner svg" }, "sticky": { "name": "Sticky", "hover": "[uk-sticky]", "inspect": "[uk-sticky], [uk-sticky] > *" }, "subnav": { "name": "Subnav", "groups": { "item": "@subnav-item-*", "divider": "@subnav-divider-*", "siblings": "@subnav-siblings-*", "pill": "@subnav-pill-*", "pill item": "@subnav-pill-item-*" }, "hover": ".uk-subnav", "inspect": ".uk-subnav, .uk-subnav > *, .uk-subnav > * > *" }, "tab": { "name": "Tab", "groups": { "item": "@tab-item-*", "item line": "@tab-item-line-(?!height)*", "vertical": "@tab-vertical-*", "vertical item": "@tab-vertical-item-*" }, "hover": ".uk-tab", "inspect": ".uk-tab, .uk-tab > *, .uk-tab > * > *" }, "table": { "name": "Table", "groups": { "cell": "@table-cell-*", "header cell": "@table-header-cell-*", "footer": "@table-footer-*", "caption": "@table-caption-*", "row": "@table-row-*", "divider": "@table-divider-*", "striped": "@table-striped-*", "hover": "@table-hover-*", "small": "@table-small-*", "large": "@table-large-*", "expand": "@table-expand-*" }, "hover": ".uk-table", "inspect": ".uk-table, .uk-table th, .uk-table th > *, .uk-table td, .uk-table td > *, .uk-table tr, .uk-table tbody, .uk-table thead, .uk-table tfoot, .uk-table caption" }, "text": { "name": "Text", "groups": { "lead": "@text-lead-*", "meta": "@text-meta-*", "small": "@text-small-*", "large": "@text-large-*" }, "hover": "[class*='uk-text-']", "inspect": "[class*='uk-text-'], [class*='uk-text-'] > *" }, "thumbnav": { "name": "Thumbnav", "groups": { "item": "@thumbnav-item-*", "siblings": "@thumbnav-siblings-*" }, "hover": ".uk-thumbnav", "inspect": ".uk-thumbnav, .uk-thumbnav > *, .uk-thumbnav > * > *, .uk-thumbnav > * > * > *" }, "tile": { "name": "Tile", "groups": { "default": "@tile-default-*", "muted": "@tile-muted-*", "primary": "@tile-primary-*", "secondary": "@tile-secondary-*", "xsmall": "@tile-xsmall-*", "small": "@tile-small-*", "large": "@tile-large-*", "xlarge": "@tile-xlarge-*" }, "hover": ".uk-tile", "inspect": ".uk-tile, .uk-tile > *" }, "tooltip": { "name": "Tooltip", "hover": ".uk-tooltip", "inspect": ".uk-tooltip" }, "totop": { "name": "Totop", "hover": ".uk-totop", "inspect": ".uk-totop, .uk-totop svg" }, "transition": { "name": "Transition", "groups": { "slide": "@transition-slide-*" }, "hover": "[class*='uk-transition-']", "inspect": "[class*='uk-transition-'], [class*='uk-transition-'] > *" }, "utility": { "name": "Utility", "groups": { "panel scrollable": "@panel-scrollable-*", "border rounded": "@border-rounded-*", "box shadow": "@box-shadow-*", "box shadow bottom": "@box-shadow-bottom-*", "dropcap": "@dropcap-*", "logo": "@logo-*", "dragover": "@dragover-*" }, "hover": ".uk-panel-scrollable, .uk-box-shadow-bottom, .uk-dropcap, .uk-logo", "inspect": ".uk-panel-scrollable, .uk-panel-scrollable > *, .uk-box-shadow-bottom, .uk-dropcap, .uk-logo, .uk-logo > *" }, "width": { "name": "Width", "hover": ".uk-width-small, .uk-width-medium, .uk-width-large, .uk-width-xlarge, .uk-width-2xlarge", "inspect": ".uk-width-small, .uk-width-small > *, .uk-width-medium, .uk-width-medium > *, .uk-width-large, .uk-width-large > *, .uk-width-xlarge, .uk-width-xlarge > *, .uk-width-2xlarge, .uk-width-2xlarge > *" } }, "types": [ { "type": "color", "vars": [ "*-color", "*-border", "*-border-top", "*-border-bottom", "*-border-left", "*-border-right", "*-background", "*-outline" ], "allowEmpty": false }, { "type": "boxshadow", "vars": "*-box-shadow", "allowEmpty": false }, { "type": "textshadow", "vars": "*-text-shadow", "allowEmpty": false }, { "type": "select-custom", "vars": "*-border-style", "options": { "Solid": "solid", "Dashed": "dashed", "Dotted": "dotted", "None": "none" }, "attrs": { "class": "yo-style-form" } }, { "type": "font", "vars": "*-font-family", "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-font-weight", "options": { "Normal": "normal", "Bold": "bold", "Lighter": "lighter", "Bolder": "bolder", "100 (Thin)": "100", "200 (Extra Light)": "200", "300 (Light)": "300", "400 (Normal)": "400", "500 (Medium)": "500", "600 (Semi Bold)": "600", "700 (Bold)": "700", "800 (Extra Bold)": "800", "900 (Black)": "900", "Inherit": "inherit" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-font-style", "options": { "Normal": "normal", "Italic": "italic", "Inherit": "inherit" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-text-align", "options": { "Left": "left", "Right": "right", "Center": "center", "Justify": "justify", "Inherit": "inherit" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-vertical-align", "options": { "Baseline": "baseline", "Sub": "sub", "Super": "super", "Text-top": "text-top", "Text-bottom": "text-bottom", "Middle": "middle", "Top": "top", "Bottom": "bottom", "Inherit": "inherit" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-text-transform", "options": { "None": "none", "Lowercase": "lowercase", "Uppercase": "uppercase", "Capitalize": "capitalize", "Inherit": "inherit" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-text-decoration", "options": { "None": "none", "Underline": "underline", "Inherit": "inherit" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-blend-mode", "options": { "None": "", "Normal": "normal", "Multiply": "multiply", "Screen": "screen", "Overlay": "overlay", "Darken": "darken", "Lighten": "lighten", "Color Dodge": "color-dodge", "Color Burn": "color-burn", "Hard Light": "hard-light", "Soft Light": "soft-light", "Difference": "difference", "Exclusion": "exclusion", "Hue": "hue", "Saturation": "saturation", "Color": "color", "Luminosity": "luminosity", "Plus Darker": "plus-darker", "Plus Lighter": "plus-lighter" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-color-mode", "options": { "Light": "light", "Dark": "dark" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-border-mode", "options": { "Full": "", "Top": "-top", "Bottom": "-bottom", "Left": "-left", "Right": "-right" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-position-mode", "options": { "Top": "top", "Bottom": "bottom" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-direction-mode", "options": { "Inside": "inside", "Outside": "outside" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*button-text-mode", "options": { "None": "", "Line": "line", "Border Bottom": "border-bottom" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*button-text-icon-mode", "options": { "None": "", "Arrow": "arrow", "Em Dash": "em-dash" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": ["*nav-default-item-mode", "*nav-primary-item-mode", "*tab-item-mode"], "options": { "None": "", "Line": "line" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*navbar-mode", "options": { "None": "", "Border": "border", "Border (Including transparent navbar)": "border-always", "Rail": "rail", "Frame": "frame" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*navbar-mode-border-vertical", "options": { "None": "", "Partial (Navs only)": "partial", "All (Navs and Items)": "all" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": ["*navbar-nav-item-line-mode", "*navbar-nav-item-line-active-mode"], "options": { "Enable": "true", "Disable": "false" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*navbar-nav-item-line-slide-mode", "options": { "None": "", "Left": "left", "Center": "center" }, "attrs": { "class": "yo-style-form" } }, { "type": "select-custom", "vars": "*-inverse-mode", "options": { "None": "", "Inverse": "inverse" }, "attrs": { "class": "yo-style-form" } }, { "type": "checkbox", "vars": ["*-em-dash"], "attrs": { "false-value": "false", "true-value": "true" } }, { "type": "text", "vars": "*", "attrs": { "class": "yo-style-form" } } ] } styler/config/theme.json 0000644 00000000143 15166657211 0011340 0 ustar 00 { "defaults": { "less": [], "custom_less": "", "style": "fuse" } } styler/bootstrap.php 0000644 00000002147 15166657211 0010632 0 ustar 00 <?php namespace YOOtheme\Theme\Styler; use YOOtheme\Config; use YOOtheme\Path; return [ 'theme' => fn(Config $config) => $config->loadFile(__DIR__ . '/config/theme.json'), 'config' => [ StylerConfig::class => __DIR__ . '/config/styler.json', ], 'routes' => [ ['get', '/theme/styles', [StyleController::class, 'index']], ['get', '/theme/style', [StyleController::class, 'get']], ['post', '/theme/style', [StyleController::class, 'save']], ['get', '/styler/library', [LibraryController::class, 'index']], ['post', '/styler/library', [LibraryController::class, 'save']], ['delete', '/styler/library', [LibraryController::class, 'delete']], ], 'events' => [ 'customizer.init' => [Listener\LoadStylerData::class => '@handle'], 'styler.imports' => [Listener\LoadStylerImports::class => ['@handle', 10]], ], 'services' => [ StylerConfig::class => '', StyleFontLoader::class => [ 'arguments' => [ '$cache' => fn() => Path::get('~theme/fonts'), ], ], ], ]; styler/src/Styler.php 0000644 00000010165 15166657212 0010666 0 ustar 00 <?php namespace YOOtheme\Theme\Styler; use YOOtheme\Config; use YOOtheme\File; use YOOtheme\Path; use YOOtheme\Url; class Styler { /** * @var Config */ protected $config; /** * Constructor. * * @param Config $config */ public function __construct(Config $config) { $this->config = $config; } /** * Gets theme styles. * * @return array */ public function getThemes() { $themes = []; $directories = join( ',', array_filter([ $this->config->get('theme.rootDir'), $this->config->get('theme.childDir'), ]), ); foreach (File::glob("{{$directories}}/less/theme.*.less") as $file) { $themes[] = $this->getTheme(substr(basename($file, '.less'), 6)); } return $themes; } public function getTheme($id) { $file = File::get("~theme/less/theme.{$id}.less"); if (!$file) { return; } return array_merge( [ 'id' => $id, 'file' => $file, 'name' => static::namify($id), ], static::getMeta($file), ); } public function resolveImports($file, $vars = []) { $importFile = $file; $extension = Path::extname($file); if (!file_exists($file)) { if ($extension || !file_exists($file = "{$file}.less")) { return []; } } $contents = @file_get_contents($file) ?: ''; if (!$extension || $extension === '.less') { $contents = preg_replace('/^\s*\/\/.*?$/m', '', $contents); $contents = preg_replace('/\/\*.*?\*\//s', '', $contents); $contents = preg_replace('/(^@[a-z0-9-]+:)\s+/m', '$1 ', $contents); $contents = preg_replace('/\n{2,}/', "\n", $contents); $contents = preg_replace('/^\n|\n$/', '', $contents); } $imports = [Path::normalize(Url::to($importFile)) => $contents]; if (preg_match_all('/^@import.*?([\'"])(.+?)\1;/m', $contents, $matches)) { $replacePairs = []; foreach ($vars as $name => $value) { $name = substr($name, 1); $replacePairs["@{{$name}}"] = $value; } foreach ($matches[2] as $path) { $imports += $this->resolveImports( Path::resolve(dirname($file), strtr($path, $replacePairs)), $vars, ); } } return $imports; } protected static function getMeta($file) { $meta = []; $style = false; $handle = fopen($file, 'r'); $content = str_replace("\r", "\n", fread($handle, 8192)); fclose($handle); // parse first comment if (!preg_match('/^\s*\/\*(?:(?!\*\/).|\n)+\*\//', $content, $matches)) { return $meta; } // parse all metadata if ( !preg_match_all( '/^[ \t\/*#@]*(name|style|background|color|type|preview):(.*)$/mi', $matches[0], $matches, ) ) { return $meta; } foreach ($matches[1] as $i => $key) { $key = strtolower(trim($key)); $value = trim($matches[2][$i]); if (!in_array($key, ['name', 'style', 'preview'])) { $value = array_map('ucwords', array_map('trim', explode(',', $value))); } if ($key === 'preview') { $value = Url::to(Path::resolve(dirname($file), $value)); } if (!$style && $key != 'style') { $meta[$key] = $value; } elseif ($key == 'style') { $style = $value; $meta['styles'][$style] = ['name' => static::namify($style)]; } else { $meta['styles'][$style][$key] = $value; } } return $meta; } protected static function namify($id) { return ucwords(str_replace('-', ' ', $id)); } } styler/src/StylerConfig.php 0000644 00000001565 15166657212 0012020 0 ustar 00 <?php namespace YOOtheme\Theme\Styler; use YOOtheme\Config; use YOOtheme\ConfigObject; use YOOtheme\File; use YOOtheme\Path; use YOOtheme\Url; class StylerConfig extends ConfigObject { /** * Constructor. */ public function __construct(Config $config) { // check version in css file, if it needs to be updated $style = File::get("~theme/css/theme.{$config('theme.id')}.css"); $header = $style ? file_get_contents($style, false, null, 0, 34) : ''; $version = preg_match('/\sv([\w\d.\-]+)\s/', $header, $match) ? $match[1] : '1.0.0'; parent::__construct([ 'update' => $version !== $config('theme.version'), 'route' => 'theme/style', 'worker' => Url::to(Path::get('../app/worker.min.js', __DIR__), [ 'ver' => $config('theme.version'), ]), ]); } } styler/src/Listener/LoadStylerImports.php 0000644 00000003773 15166657212 0014640 0 ustar 00 <?php namespace YOOtheme\Theme\Styler\Listener; use YOOtheme\Config; use YOOtheme\File; use YOOtheme\Path; use YOOtheme\Theme\Styler\Styler; use YOOtheme\Url; class LoadStylerImports { public Config $config; public Styler $styler; public function __construct(Config $config, Styler $styler) { $this->config = $config; $this->styler = $styler; } public function handle($imports, $themeId): array { // add general imports foreach ($this->config->get('theme.styles.imports', []) as $path) { foreach (File::glob($path) as $file) { $imports += $this->styler->resolveImports($file); } } // add theme imports if (!($theme = $this->styler->getTheme($themeId))) { return $imports; } // add svg images foreach ( File::glob("~theme/vendor/assets/uikit-themes/master-{$themeId}/images/*.svg") as $file ) { $imports += $this->styler->resolveImports($file); } $file = $theme['file']; $imports += $this->styler->resolveImports($file); // add theme style imports if (isset($theme['styles'])) { foreach (array_keys($theme['styles']) as $style) { $imports += $this->styler->resolveImports( $file, [ '@internal-style' => $style, ] + $this->config->get('theme.styles.vars', []), ); } } // add custom components foreach ($this->config->get('theme.styles.components', []) as $path) { foreach (File::glob($path) as $component) { $imports += $this->styler->resolveImports($component); $imports[Url::to($file)] .= sprintf( "\n@import \"%s\";", Path::relative(dirname($file), $component), ); } } return $imports; } } styler/src/Listener/LoadStylerData.php 0000644 00000001202 15166657212 0014035 0 ustar 00 <?php namespace YOOtheme\Theme\Styler\Listener; use YOOtheme\Metadata; use YOOtheme\Theme\Styler\StylerConfig; class LoadStylerData { public Metadata $metadata; public StylerConfig $config; public function __construct(Metadata $metadata, StylerConfig $config) { $this->config = $config; $this->metadata = $metadata; } public function handle(): void { $this->metadata->set( 'script:styler-data', sprintf( 'window.yootheme ||= {}; var $styler = yootheme.styler = %s;', json_encode($this->config), ), ); } } styler/src/LibraryController.php 0000644 00000001702 15166657212 0013051 0 ustar 00 <?php namespace YOOtheme\Theme\Styler; use YOOtheme\Http\Request; use YOOtheme\Http\Response; use YOOtheme\Storage; class LibraryController { public static function index(Request $request, Response $response, Storage $storage) { return $response->withJson((object) $storage('styler.library')); } public static function save(Request $request, Response $response, Storage $storage) { $id = $request->getParam('id'); $style = $request->getParam('style'); if ($id && $style) { $storage->set("styler.library.{$id}", $style); } return $response->withJson(['message' => 'success']); } public static function delete(Request $request, Response $response, Storage $storage) { $id = $request->getQueryParam('id'); if ($id) { $storage->del("styler.library.{$id}"); } return $response->withJson(['message' => 'success']); } } styler/src/StyleFontLoader.php 0000644 00000015564 15166657212 0012472 0 ustar 00 <?php namespace YOOtheme\Theme\Styler; use YOOtheme\File; use YOOtheme\Http\Uri; use YOOtheme\HttpClientInterface; use YOOtheme\Path; class StyleFontLoader { public const VERSION = '2'; public const PREFIX = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 '; /** * @var string */ public $cache; /** * @var array */ public $formats; /** * @var HttpClientInterface */ public $client; /** * Constructor. * * @param HttpClientInterface $client * @param string $cache */ public function __construct(HttpClientInterface $client, $cache) { File::makeDir($cache, 0777, true); $formats = ['woff2' => self::PREFIX . 'Edge/17.17134']; $this->cache = $cache; $this->client = $client; $this->formats = $formats; } /** * Creates CSS markup. * * @param string $url * @param string $basePath * * @return string|void */ public function css($url, $basePath) { $date = date(DATE_W3C); $hash = hash('crc32b', join(',', [$url, $basePath, static::VERSION])); $file = "{$this->cache}/font-{$hash}.css"; // is already cached? if ( is_file($file) && filemtime($file) >= strtotime('-1 week') && is_string($data = file_get_contents($file)) ) { return preg_replace('/^\/\*.+?\*\/\s*/', '', $data); } // load font url $fonts = $this->load($url); $relative = fn($path) => Path::relative($basePath, $path); // relative font path foreach ($fonts as &$font) { $font['src'] = array_map($relative, $font['src']); } // generate fonts css $info = "/* {$url} generated on {$date} */\n"; $data = join(array_map([$this, 'cssFontFace'], $fonts)); // save file in cache if ($fonts && @file_put_contents($file, $info . $data)) { return $data; } } /** * Creates a single @font-face CSS markup. * * @param array $font * * @return string */ public function cssFontFace(array $font) { $output = '@font-face {'; foreach ($font as $name => $value) { if ($name == 'src') { $urls = []; foreach ($value as $format => $url) { $urls[] = "url({$url}) format('{$format}')"; } $value = join(', ', $urls); } $output .= "{$name}: {$value}; "; } return "$output}\n"; } /** * Loads fonts from url. * * @param string $url * * @return array[] */ public function load($url) { $fonts = []; foreach ($this->parseFontFamilies($url) as $url) { // load font formats based on user agents foreach ($this->formats as $userAgent) { $options = compact('userAgent'); $response = $this->client->get($url, $options); if ($result = $this->parseFonts($response->getBody())) { $fonts = array_replace_recursive($fonts, $result); } } } // load fonts and save them foreach ($fonts as &$font) { $font['src'] = array_map( fn($url) => $this->loadFont( $url, preg_replace('/[^a-z]/', '', strtolower($font['font-family'])), ), $font['src'], ); } return $fonts; } /** * Load font file from url. * * @param string $url * @param string $name * @param array $options * * @return string|void */ public function loadFont($url, $name, array $options = []) { $hash = hash('crc32b', $url); $type = pathinfo($url, PATHINFO_EXTENSION); $file = "{$this->cache}/{$name}-{$hash}.{$type}"; // is already cached? if (file_exists($file)) { return $file; } // load font from url $data = $this->client->get($url, $options); // save file in cache if (@file_put_contents($file, $data->getBody())) { return $file; } } /** * Parses font @import from source. * * @param string $source * * @return array */ public function parse($source) { return preg_match( '/@import\s+url\((https?:\/\/fonts\.googleapis\.com[^)]+)\)\s*;?/i', $source, $matches, ) ? $matches : []; } /** * Parses fonts url from source. * * @param string $source * * @return array */ public function parseFonts($source) { $fonts = []; foreach ($this->parseFontFaces($source) as $fontface) { $font = $src = []; foreach ($this->parseFontProperties($fontface) as $name => $value) { if ($name == 'src') { $src = $this->parseFontSrc($value); } else { $font[$name] = $value; } } if ($src) { $hash = hash('crc32b', json_encode($font)); $fonts[$hash] = array_merge($font, compact('src')); } } return $fonts; } /** * Parses font url from source. * * @param string $source * * @return array */ public function parseFontSrc($source) { return preg_match('/url\((https?:\/\/.+?)\)\s*format\(\'?(\w+)/', $source, $matches) ? [$matches[2] => $matches[1]] : []; } /** * Parses @font-face from source. * * @param string $source * * @return array */ public function parseFontFaces($source) { return preg_match_all('/@font-face\s*{\s*([^}]+)/', $source, $matches) ? $matches[1] : []; } /** * Parses properties from source. * * @param string $source * * @return array */ public function parseFontProperties($source) { return preg_match_all('/([\w-]+)\s*:\s*([^;]+)/', $source, $matches) ? array_combine($matches[1], $matches[2]) : []; } /** * Split url into separate urls, one per font family. * * @param string $url * * @return array */ protected function parseFontFamilies($url) { $uri = new Uri($url); $query = $uri->getQueryParams(); if (isset($query['family'])) { return array_map( fn($family) => (string) $uri->withQueryParams(compact('family') + $query), explode('|', $query['family']), ); } return [$url]; } } styler/src/StyleController.php 0000644 00000006413 15166657212 0012551 0 ustar 00 <?php namespace YOOtheme\Theme\Styler; use YOOtheme\Config; use YOOtheme\Event; use YOOtheme\Http\Request; use YOOtheme\Http\Response; use YOOtheme\Path; use YOOtheme\Url; class StyleController { public static function index(Request $request, Response $response, Styler $styler) { return $response->withJson( array_map(function ($theme) { unset($theme['file']); return $theme; }, $styler->getThemes()), ); } public static function get(Request $request, Response $response, Config $config, Styler $styler) { $themeId = explode('::', $request->getQueryParam('id', ''))[0]; $theme = $styler->getTheme($themeId); if (!$theme) { $request->abort(404, "Theme {$themeId} not found"); } return $response->withJson([ 'id' => $themeId, 'filename' => Url::to($theme['file']), 'imports' => Event::emit('styler.imports|filter', [], $themeId), 'vars' => $config('theme.styles.vars'), 'desturl' => Url::to('~theme/css'), ]); } public static function save( Request $request, Response $response, Config $config, StyleFontLoader $font ) { $upload = $request->getUploadedFile('files'); // validate uploads $request ->abortIf(!$upload || $upload->getError(), 400, 'Invalid file upload.') ->abortIf( !($contents = (string) $upload->getStream()), 400, 'Unable to read contents file.', ) ->abortIf(!($contents = @base64_decode($contents)), 400, 'Base64 Decode failed.') ->abortIf( !($files = @json_decode($contents, true)), 400, 'Unable to decode JSON from temporary file.', ); foreach ($files as $file => $data) { $dir = Path::get('~theme/css'); $rtl = strpos($file, '.rtl') ? '.rtl' : ''; try { // save fonts for theme style if ($matches = $font->parse($data)) { [$import, $url] = $matches; if ($fonts = $font->css($url, $dir)) { $data = str_replace($import, $fonts, $data); } } } catch (\RuntimeException $e) { } $head = "/* YOOtheme Pro v{$config('theme.version')} compiled on " . date(DATE_W3C) . " */\n"; // save css for theme style if ( !file_put_contents( $file = "{$dir}/theme.{$config('theme.id')}{$rtl}.css", $head . $data, ) ) { $request->abort(500, sprintf('Unable to write file (%s).', $file)); } // save css for theme as default/fallback if ( $config('theme.default') && !file_put_contents($file = "{$dir}/theme{$rtl}.css", $head . $data) ) { $request->abort(500, sprintf('Unable to write file (%s).', $file)); } } return $response->withJson(['message' => 'success']); } } styler/tests/tab.html 0000644 00000005106 15166657212 0010701 0 ustar 00 <div class="uk-section uk-section-default uk-flex uk-flex-middle" uk-height-viewport> <div class="uk-width-1-1"> <div class="uk-container"> <div class="uk-panel uk-width-xlarge uk-margin-auto"> <ul uk-tab="animation: uk-animation-fade"> <li class="uk-active"><a href="#">Active</a></li> <li><a href="#">Item</a></li> <li><a href="#">Item</a></li> <li><a href="#">Item</a></li> <li class="uk-disabled"><a>Disabled</a></li> </ul> <div class="uk-switcher uk-margin"> <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div>Ut enim ad minim veniam, quis nostrud exercitation divlamco laboris nisi ut aliquip ex ea commodo consequat.</div> <div>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</div> <div>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div> </div> <div class="uk-margin-large" uk-grid> <div class="uk-width-auto@m"> <ul class="uk-tab-left" uk-tab="connect: #component-tab-left; animation: uk-animation-fade"> <li><a href="#">Active</a></li> <li><a href="#">Item</a></li> <li><a href="#">Item</a></li> <li class="uk-disabled"><a>Disabled</a></li> </ul> </div> <div class="uk-width-expand@m"> <div id="component-tab-left" class="uk-switcher"> <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div>Ut enim ad minim veniam, quis nostrud exercitation divlamco laboris nisi ut aliquip ex ea commodo consequat.</div> <div>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</div> <div>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</div> </div> </div> </div> </div> </div> </div> </div> styler/tests/margin.html 0000644 00000016123 15166657212 0011411 0 ustar 00 <div class="uk-section uk-section-default"> <div class="uk-container"> <h2>Margin</h2> <div class="uk-grid-large uk-child-width-1-2@s" uk-grid> <div> <div class="uk-margin">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> <div class="uk-margin">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> </div> <div> <div class="uk-margin uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div class="uk-margin uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> </div> </div> <h2>Small</h2> <div class="uk-child-width-1-2@s" uk-grid> <div> <div class="uk-margin-small">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> <div class="uk-margin-small">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> </div> <div> <div class="uk-margin-small uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div class="uk-margin-small uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> </div> </div> <h2>Medium</h2> <div class="uk-child-width-1-2@s" uk-grid> <div> <div class="uk-margin-medium">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> <div class="uk-margin-medium">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> </div> <div> <div class="uk-margin-medium uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div class="uk-margin-medium uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> </div> </div> <h2>Large</h2> <div class="uk-child-width-1-2@s" uk-grid> <div> <div class="uk-margin-large">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> <div class="uk-margin-large">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> </div> <div> <div class="uk-margin-large uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div class="uk-margin-large uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> </div> </div> <h2>X-Large</h2> <div class="uk-child-width-1-2@s" uk-grid> <div> <div class="uk-margin-xlarge">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> <div class="uk-margin-xlarge">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat.</div> </div> <div> <div class="uk-margin-xlarge uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> <div class="uk-margin-xlarge uk-card uk-card-body uk-card-default">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</div> </div> </div> </div> </div> styler/tests/tooltip.html 0000644 00000000706 15166657212 0011626 0 ustar 00 <div class="uk-section uk-section-default uk-flex uk-flex-middle" uk-height-viewport> <div class="uk-width-1-1"> <div class="uk-container uk-text-center"> <div class="uk-tooltip uk-tooltip-top-center uk-display-inline-block uk-margin-remove uk-position-relative">Tooltip</div> <button class="uk-button uk-button-default uk-margin-left" uk-tooltip="title: Hello World">Hover</button> </div> </div> </div>