├── Build ├── Composer │ └── ScriptHelper.php ├── FunctionalTests.xml └── UnitTests.xml ├── CONTRIBUTING.md ├── Classes ├── AbstractExpressionParser.php ├── AbstractRelationshipResolver.php ├── AbstractResolver.php ├── BufferedResolverInterface.php ├── Database │ ├── AbstractPassiveRelationshipResolver.php │ ├── ActiveRelationshipResolver.php │ ├── EntityResolver.php │ ├── FieldQueryHandler.php │ ├── FilterArgumentProvider.php │ ├── FilterExpressionProcessor.php │ ├── FilterQueryHandler.php │ ├── LocalizationQueryHandler.php │ ├── OrderArgumentProvider.php │ ├── OrderExpressionTraversable.php │ ├── OrderQueryHandler.php │ ├── OrderValueHandler.php │ ├── PassiveManyToManyRelationshipResolver.php │ ├── PassiveOneToManyRelationshipResolver.php │ └── QueryHelper.php ├── EntityReader.php ├── EntitySchemaFactory.php ├── Event │ ├── AfterValueResolvingEvent.php │ ├── BeforeFieldArgumentsInitializationEvent.php │ └── BeforeValueResolvingEvent.php ├── Exception │ └── ExecutionException.php ├── ExpressionNodeVisitor.php ├── FilterExpressionParser.php ├── OrderExpressionParser.php ├── ResolverFactory.php ├── ResolverHelper.php ├── ResolverInterface.php ├── Type │ ├── FilterExpressionType.php │ └── OrderExpressionType.php ├── Utility │ └── TypeUtility.php └── Validator │ ├── FiltersOnCorrectTypeRule.php │ ├── NoUndefinedVariablesRule.php │ ├── NoUnsupportedFeaturesRule.php │ ├── NoUnusedVariablesRule.php │ └── OrdersOnCorrectTypeRule.php ├── Configuration └── Services.yaml ├── LICENSE ├── README.md ├── Resources └── Private │ └── Grammar │ ├── Filter.pp │ └── Order.pp ├── Tests ├── Functional │ └── EntityReader │ │ ├── EntityReaderTestTrait.php │ │ ├── Extensions │ │ └── persistence │ │ │ ├── Configuration │ │ │ └── TCA │ │ │ │ ├── tx_persistence_entity.php │ │ │ │ └── tx_persistence_entity_symmetric.php │ │ │ ├── ext_emconf.php │ │ │ ├── ext_tables.php │ │ │ └── ext_tables.sql │ │ ├── Fixtures │ │ ├── live-default.xml │ │ └── live-localization.xml │ │ ├── LiveDefaultTest.php │ │ └── LiveLocalizationTest.php └── Unit │ ├── FilterExpressionParserTest.php │ └── OrderExpressionParserTest.php ├── codesize.xml ├── composer.json ├── ext_emconf.php ├── ext_localconf.php └── phpcs.xml /Build/Composer/ScriptHelper.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | ../Tests/Functional 18 | 19 | 20 | 21 | 22 | ../Classes/ 23 | 24 | ../Classes/ 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Build/UnitTests.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | ../Tests/Unit 19 | 20 | 21 | 22 | 23 | ../Classes/ 24 | 25 | 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Any contributor is welcome to join. All you need is an account at GitHub. If you already have an account, visit https://github.com/typo3-initiatives/graphql/. 4 | 5 | It is also highly recommended to join the [Slack channel](https://typo3.slack.com/messages/C8QU7GRJL) of the [TYPO3 persistence initiative](https://typo3.org/community/teams/typo3-development/initiatives/persistence/). -------------------------------------------------------------------------------- /Classes/AbstractExpressionParser.php: -------------------------------------------------------------------------------- 1 | grammar); 49 | $cacheManager = GeneralUtility::makeInstance(CacheManager::class); 50 | 51 | $this->parser = Llk::load(new Read($path)); 52 | $this->cache = $cacheManager->hasCache('gql') ? $cacheManager->getCache('gql') : null; 53 | } 54 | 55 | /** 56 | * Parses an expression 57 | * 58 | * @param string $expression 59 | * @return TreeNode 60 | * 61 | * @throws UnexpectedToken 62 | */ 63 | public function parse(string $expression): TreeNode 64 | { 65 | if ($this->cache !== null) { 66 | $key = $this->getCacheIdentifier($expression); 67 | 68 | if (!$this->cache->has($key)) { 69 | $this->cache->set($key, $this->parser->parse($expression)); 70 | } 71 | 72 | return $this->cache->get($key); 73 | } 74 | 75 | return $this->parser->parse($expression); 76 | } 77 | 78 | protected function getCacheIdentifier($expression): string 79 | { 80 | return sha1(\spl_object_hash($this) . $expression); 81 | } 82 | } -------------------------------------------------------------------------------- /Classes/AbstractRelationshipResolver.php: -------------------------------------------------------------------------------- 1 | propertyDefinition = $element; 46 | 47 | foreach ($this->propertyDefinition->getConstraints() as $constraint) { 48 | if ($constraint instanceof MultiplicityConstraint) { 49 | $this->multiplicityConstraint = $constraint; 50 | break; 51 | } 52 | } 53 | } 54 | 55 | protected function assertResolveInfoIsValid(ResolveInfo $info) 56 | { 57 | if ($info->field !== $this->propertyDefinition->getName()) { 58 | throw new Exception( 59 | sprintf( 60 | 'Resolver was initialized for field "%s" but requested was "%s"', 61 | $this->propertyDefinition->getName(), $info->field 62 | ), 63 | 1563841651 64 | ); 65 | } 66 | } 67 | 68 | protected function getPropertyDefinition(): PropertyDefinition 69 | { 70 | return $this->propertyDefinition; 71 | } 72 | 73 | protected function getMultiplicityConstraint(): MultiplicityConstraint 74 | { 75 | return $this->multiplicityConstraint; 76 | } 77 | } -------------------------------------------------------------------------------- /Classes/AbstractResolver.php: -------------------------------------------------------------------------------- 1 | element = $element; 48 | $this->type = $type; 49 | $this->handlers = []; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function getArguments(): array 56 | { 57 | return []; 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function getType(): Type 64 | { 65 | return $this->type; 66 | } 67 | 68 | /** 69 | * @inheritdoc 70 | */ 71 | public function getElement(): ElementInterface 72 | { 73 | return $this->element; 74 | } 75 | 76 | protected function getEventDispatcher(): EventDispatcherInterface 77 | { 78 | return GeneralUtility::makeInstance(EventDispatcher::class); 79 | } 80 | } -------------------------------------------------------------------------------- /Classes/BufferedResolverInterface.php: -------------------------------------------------------------------------------- 1 | getCacheIdentifier('keys'); 44 | $keys = $context['cache']->get($keysIdentifier) ?: []; 45 | 46 | if ($source !== null) { 47 | Assert::keyExists($source, '__uid'); 48 | $keys[] = $source['__uid']; 49 | } 50 | 51 | $context['cache']->set($keysIdentifier, $keys); 52 | } 53 | 54 | protected abstract function getTable(): string; 55 | 56 | protected abstract function getForeignKeyField(): string; 57 | 58 | protected function getValue(?array $value): ?array 59 | { 60 | $minimum = $this->getMultiplicityConstraint()->getMinimum(); 61 | $maximum = $this->getMultiplicityConstraint()->getMaximum(); 62 | 63 | if (empty($value)) { 64 | return $minimum > 0 || $maximum > 1 || $maximum === null ? [] : null; 65 | } 66 | 67 | return $maximum > 1 || $maximum === null ? $value : $value[0]; 68 | } 69 | 70 | protected function getCacheIdentifier($identifier): string 71 | { 72 | return \spl_object_hash($this) . '_' . $identifier; 73 | } 74 | 75 | protected function getBufferIndexes(array $row): array 76 | { 77 | return [ 78 | $row['__' . $this->getForeignKeyField()], 79 | ]; 80 | } 81 | 82 | protected function getBuilder(ResolveInfo $info, string $table, array $keys): QueryBuilder 83 | { 84 | $builder = GeneralUtility::makeInstance(ConnectionPool::class) 85 | ->getQueryBuilderForTable($table); 86 | 87 | $builder->getRestrictions() 88 | ->removeAll(); 89 | 90 | $builder->selectLiteral(...$this->getColumns($info, $builder, $table)) 91 | ->from($table); 92 | 93 | $condition = $this->getCondition($builder, $keys); 94 | 95 | if (!empty($condition)) { 96 | $builder->where(...$condition); 97 | } 98 | 99 | return $builder; 100 | } 101 | 102 | protected function getCondition(QueryBuilder $builder, array $keys) 103 | { 104 | $condition = []; 105 | 106 | $condition[] = $builder->expr()->in( 107 | $this->getTable() . '.' . $this->getForeignKeyField(), 108 | $builder->createNamedParameter($keys, Connection::PARAM_INT_ARRAY) 109 | ); 110 | 111 | return $condition; 112 | } 113 | 114 | protected function getColumns(ResolveInfo $info, QueryBuilder $builder, string $table) 115 | { 116 | $columns = []; 117 | 118 | $columns[] = $builder->quote($table, ParameterType::STRING) 119 | . ' AS ' . $builder->quoteIdentifier(EntitySchemaFactory::ENTITY_TYPE_FIELD); 120 | 121 | return array_values($columns); 122 | } 123 | } -------------------------------------------------------------------------------- /Classes/Database/ActiveRelationshipResolver.php: -------------------------------------------------------------------------------- 1 | isManyToManyRelationProperty()) { 51 | return false; 52 | } 53 | 54 | foreach ($element->getActiveRelations() as $activeRelation) { 55 | if (!($activeRelation instanceof ActiveEntityRelation)) { 56 | return false; 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function collect($source, array $arguments, array $context, ResolveInfo $info) 67 | { 68 | Assert::keyExists($context, 'cache'); 69 | Assert::isInstanceOf($context['cache'], FrontendInterface::class); 70 | 71 | $column = $this->getPropertyDefinition()->getName(); 72 | 73 | $keysIdentifier = $this->getCacheIdentifier('keys'); 74 | $keys = $context['cache']->get($keysIdentifier) ?: []; 75 | 76 | if ($source !== null) { 77 | Assert::keyExists($source, $column); 78 | 79 | foreach ($this->getForeignKeys((string) $source[$column]) as $table => $identifier) { 80 | $keys[$table][] = $identifier; 81 | } 82 | 83 | foreach ($keys as $table => $identifiers) { 84 | $keys[$table] = array_keys(array_flip($identifiers)); 85 | } 86 | } 87 | 88 | $context['cache']->set($keysIdentifier, $keys); 89 | } 90 | 91 | /** 92 | * @inheritdoc 93 | * @todo Prevent reaching maximum length of a generated SQL statement. 94 | * @see https://www.sqlite.org/limits.html#max_sql_length 95 | * @see https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet 96 | * @see https://mariadb.com/kb/en/library/server-system-variables/#max_allowed_packet 97 | * @see https://www.postgresql.org/docs/9.1/runtime-config-resource.html#GUC-MAX-STACK-DEPTH 98 | */ 99 | public function resolve($source, array $arguments, array $context, ResolveInfo $info): ?array 100 | { 101 | Assert::keyExists($source, $this->getPropertyDefinition()->getName()); 102 | Assert::keyExists($context, 'cache'); 103 | Assert::isInstanceOf($context['cache'], FrontendInterface::class); 104 | 105 | $dispatcher = $this->getEventDispatcher(); 106 | 107 | $bufferIdentifier = $this->getCacheIdentifier('buffer'); 108 | $buffer = $context['cache']->get($bufferIdentifier) ?: []; 109 | 110 | $keysIdentifier = $this->getCacheIdentifier('keys'); 111 | $keys = $context['cache']->get($keysIdentifier) ?: []; 112 | 113 | $column = $this->getPropertyDefinition()->getName(); 114 | 115 | $value = []; 116 | $tables = []; 117 | 118 | if (!$context['cache']->has($bufferIdentifier)) { 119 | $foreignKeyField = '__' . $this->getForeignKeyField(); 120 | 121 | foreach ($this->getPropertyDefinition()->getRelationTableNames() as $table) { 122 | $builder = $this->getBuilder($info, $table, $keys); 123 | 124 | $dispatcher->dispatch( 125 | new BeforeValueResolvingEvent( 126 | $source, 127 | $arguments, 128 | ['builder' => $builder] + $context, 129 | $info, 130 | $this 131 | ) 132 | ); 133 | 134 | $statement = $builder->execute(); 135 | 136 | while ($row = $statement->fetch()) { 137 | $buffer[$row[EntitySchemaFactory::ENTITY_TYPE_FIELD]][$row[$foreignKeyField]] = $row; 138 | } 139 | } 140 | 141 | $context['cache']->set($bufferIdentifier, $buffer); 142 | } 143 | 144 | foreach ($this->getForeignKeys((string) $source[$column]) as $table => $identifier) { 145 | $tables[$table] = true; 146 | $value[] = $buffer[$table][$identifier]; 147 | } 148 | 149 | $event = new AfterValueResolvingEvent($value, $source, $arguments, $context, $info, $this); 150 | 151 | $dispatcher->dispatch($event); 152 | 153 | return $this->getValue($event->getValue()); 154 | } 155 | 156 | protected function getValue(?array $value): ?array 157 | { 158 | $minimum = $this->getMultiplicityConstraint()->getMinimum(); 159 | $maximum = $this->getMultiplicityConstraint()->getMaximum(); 160 | 161 | if (empty($value)) { 162 | return $minimum > 0 || $maximum > 1 || $maximum === null ? [] : null; 163 | } 164 | 165 | return $maximum > 1 || $maximum === null ? $value : $value[0]; 166 | } 167 | 168 | protected function getCacheIdentifier($identifier): string 169 | { 170 | return \spl_object_hash($this) . '_' . $identifier; 171 | } 172 | 173 | protected function getBuilder(ResolveInfo $info, string $table, array $keys): QueryBuilder 174 | { 175 | $builder = GeneralUtility::makeInstance(ConnectionPool::class) 176 | ->getQueryBuilderForTable($table); 177 | 178 | $builder->getRestrictions() 179 | ->removeAll(); 180 | 181 | $builder->selectLiteral(...$this->getColumns($info, $builder, $table)) 182 | ->from($table); 183 | 184 | $condition = $this->getCondition($builder, $table, $keys); 185 | 186 | if (!empty($condition)) { 187 | $builder->where(...$condition); 188 | } 189 | 190 | return $builder; 191 | } 192 | 193 | protected function getCondition(QueryBuilder $builder, string $table, array $keys): array 194 | { 195 | $condition = []; 196 | 197 | $propertyConfiguration = $this->getPropertyDefinition()->getConfiguration(); 198 | 199 | $condition[] = $builder->expr()->in( 200 | $table . '.' . $this->getForeignKeyField(), 201 | $builder->createNamedParameter($keys[$table], Connection::PARAM_INT_ARRAY) 202 | ); 203 | 204 | if (isset($propertyConfiguration['config']['foreign_table_field'])) { 205 | $condition[] = $builder->expr()->eq( 206 | $table . '.' . $propertyConfiguration['config']['foreign_table_field'], 207 | $builder->createNamedParameter($this->getPropertyDefinition()->getEntityDefinition()->getName()) 208 | ); 209 | } 210 | 211 | foreach ($propertyConfiguration['config']['foreign_match_fields'] ?? [] as $field => $match) { 212 | $condition[] = $builder->expr()->eq( 213 | $table . '.' . $field, 214 | $builder->createNamedParameter($match) 215 | ); 216 | } 217 | 218 | return $condition; 219 | } 220 | 221 | protected function getColumns(ResolveInfo $info, QueryBuilder $builder, string $table): array 222 | { 223 | $columns = []; 224 | 225 | $foreignKeyField = $this->getForeignKeyField(); 226 | 227 | if ($foreignKeyField) { 228 | $columns[] = $builder->quoteIdentifier($table . '.' . $foreignKeyField) 229 | . 'AS ' . $builder->quoteIdentifier('__' . $foreignKeyField); 230 | } 231 | 232 | $columns[] = $builder->quote($table, ParameterType::STRING) 233 | . ' AS ' . $builder->quoteIdentifier(EntitySchemaFactory::ENTITY_TYPE_FIELD); 234 | 235 | return array_values($columns); 236 | } 237 | 238 | protected function getForeignKeyField(): string 239 | { 240 | return 'uid'; 241 | } 242 | 243 | protected function getForeignKeys(string $commaSeparatedValues) 244 | { 245 | $defaultTable = reset($this->getPropertyDefinition()->getRelationTableNames()); 246 | $commaSeparatedValues = array_unique($commaSeparatedValues ? explode(',', $commaSeparatedValues) : []); 247 | 248 | foreach ($commaSeparatedValues as $commaSeparatedValue) { 249 | $separatorPosition = strrpos($commaSeparatedValue, '_'); 250 | $table = $separatorPosition ? substr($commaSeparatedValue, 0, $separatorPosition) : $defaultTable; 251 | $identifier = substr($commaSeparatedValue, ($separatorPosition ?: -1) + 1); 252 | 253 | yield $table => $identifier; 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /Classes/Database/EntityResolver.php: -------------------------------------------------------------------------------- 1 | entityDefinition = $element; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function resolve($source, array $arguments, array $context, ResolveInfo $info): ?array 66 | { 67 | $builder = $this->getBuilder($info); 68 | $dispatcher = $this->getEventDispatcher(); 69 | 70 | $dispatcher->dispatch( 71 | new BeforeValueResolvingEvent( 72 | $source, 73 | $arguments, 74 | ['builder' => $builder] + $context, 75 | $info, 76 | $this 77 | ) 78 | ); 79 | 80 | $value = $builder->execute()->fetchAll(); 81 | $event = new AfterValueResolvingEvent($value, $source, $arguments, $context, $info, $this); 82 | 83 | $dispatcher->dispatch($event); 84 | 85 | return $event->getValue(); 86 | } 87 | 88 | protected function getBuilder(ResolveInfo $info): QueryBuilder 89 | { 90 | $table = $this->getTable(); 91 | 92 | $builder = GeneralUtility::makeInstance(ConnectionPool::class) 93 | ->getQueryBuilderForTable($table); 94 | 95 | $builder->getRestrictions() 96 | ->removeAll(); 97 | 98 | $builder->selectLiteral(...$this->getColumns($info, $builder, $table)) 99 | ->from($table); 100 | 101 | return $builder; 102 | } 103 | 104 | protected function getTable(): string 105 | { 106 | return $this->entityDefinition->getName(); 107 | } 108 | 109 | protected function getColumns(ResolveInfo $info, QueryBuilder $builder, string $table): array 110 | { 111 | return [ 112 | $builder->quoteIdentifier($table . '.uid') 113 | . ' AS ' . $builder->quoteIdentifier('__uid'), 114 | ]; 115 | } 116 | } -------------------------------------------------------------------------------- /Classes/Database/FieldQueryHandler.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder($event); 31 | 32 | if ($builder === null) { 33 | return; 34 | } 35 | 36 | $table = array_pop(QueryHelper::getQueriedTables($builder, QueryHelper::QUERY_PART_FROM)); 37 | $columns = []; 38 | 39 | foreach (ResolverHelper::getFields($event->getInfo(), $table) as $field) { 40 | $columns[$field->name->value] = $builder->quoteIdentifier($table . '.' . $field->name->value) 41 | . ' AS ' . $builder->quoteIdentifier($field->name->value); 42 | } 43 | 44 | foreach (ResolverHelper::getFields($event->getInfo()) as $field) { 45 | if (isset($columns[$field->name->value])) { 46 | continue; 47 | } 48 | 49 | $columns[] = 'NULL AS ' . $builder->quoteIdentifier($field->name->value); 50 | } 51 | 52 | if (!empty($columns)) { 53 | $builder->addSelectLiteral(...array_values($columns)); 54 | } 55 | } 56 | 57 | protected function getQueryBuilder($event): ?QueryBuilder 58 | { 59 | $context = $event->getContext(); 60 | 61 | if (isset($context['builder']) && $context['builder'] instanceof QueryBuilder) { 62 | return $context['builder']; 63 | } 64 | 65 | return null; 66 | } 67 | } -------------------------------------------------------------------------------- /Classes/Database/FilterArgumentProvider.php: -------------------------------------------------------------------------------- 1 | getElement(); 37 | 38 | if (!$meta instanceof PropertyDefinition && !$meta instanceof EntityDefinition) { 39 | return; 40 | } 41 | 42 | $event->addArgument(self::ARGUMENT_NAME, FilterExpressionType::instance()); 43 | } 44 | } -------------------------------------------------------------------------------- /Classes/Database/FilterExpressionProcessor.php: -------------------------------------------------------------------------------- 1 | ['andX', 'orX'], 39 | '#or' => ['orX', 'andX'], 40 | 41 | ]; 42 | 43 | protected const COMPARISION = [ 44 | '#in' => ['IN', 'NOT IN'], 45 | '#equals' => ['=', '<>'], 46 | '#not_equals' => ['<>', '='], 47 | '#less_than' => ['<', '>='], 48 | '#greater_than' => ['>', '<='], 49 | '#greater_than_equals' => ['>=', '<'], 50 | '#less_than_equals' => ['<=', '>'], 51 | ]; 52 | 53 | /** 54 | * @var ResolveInfo 55 | */ 56 | protected $info; 57 | 58 | /** 59 | * @var QueryBuilder 60 | */ 61 | protected $builder; 62 | 63 | /** 64 | * @var callable 65 | */ 66 | protected $handler; 67 | 68 | public function __construct(ResolveInfo $info, QueryBuilder $builder, callable $handler) 69 | { 70 | $this->info = $info; 71 | $this->builder = $builder; 72 | $this->handler = $handler; 73 | } 74 | 75 | public function process(?TreeNode $expression) 76 | { 77 | return $expression !== null ? $this->processNode($expression->getChild(0)) : null; 78 | } 79 | 80 | protected function processNode(TreeNode $node, int $domain = 0) 81 | { 82 | if ($this->isConnective($node)) { 83 | return $this->processConnective($node, $domain); 84 | } elseif ($this->isNegation($node)) { 85 | return $this->processNegation($node, $domain); 86 | } elseif ($this->isNullComparison($node)) { 87 | return $this->processNullComparison($node, $domain); 88 | } elseif ($this->isComparison($node)) { 89 | return $this->processComparison($node, $domain); 90 | } elseif ($this->isList($node)) { 91 | return $this->processList($node); 92 | } else if ($this->isVariable($node)) { 93 | return $this->processVariable($node); 94 | } 95 | 96 | throw new Exception( 97 | sprintf('Failed to process node type "%s" in filter expression', $node->getId()), 98 | 1563841479 99 | ); 100 | } 101 | 102 | protected function processNullComparison(TreeNode $node, int $domain) 103 | { 104 | $operator = ($node->getId() === '#equals' xor $domain === 0) ? 'IS NOT NULL' : 'IS NULL'; 105 | $left = $node->getChild($node->getChild(0)->getId() === '#field' ? 0 : 1); 106 | 107 | return call_user_func_array( 108 | $this->handler, 109 | [ 110 | $this->builder, 111 | $operator, 112 | $this->processField($left), 113 | ] 114 | ); 115 | } 116 | 117 | protected function processConnective(TreeNode $node, int $domain) 118 | { 119 | $left = $node->getChild(0); 120 | $right = $node->getChild(1); 121 | $operator = self::CONNECTIVE[$node->getId()][$domain]; 122 | 123 | return $this->builder->expr()->{$operator}( 124 | $this->{'process' . $this->getType($left)}($left, $domain), 125 | $this->{'process' . $this->getType($right)}($right, $domain) 126 | ); 127 | } 128 | 129 | protected function processComparison(TreeNode $node, int $domain) 130 | { 131 | $operands = []; 132 | 133 | foreach ($node->getChildren() as $operand) { 134 | $operands[] = $operand->getId() === '#field' ? $this->processField($operand) 135 | : $this->{'process' . $this->getType($operand)}($operand); 136 | } 137 | 138 | return call_user_func_array( 139 | $this->handler, 140 | array_merge( 141 | [ 142 | $this->builder, 143 | self::COMPARISION[$node->getId()][$domain], 144 | ], 145 | $operands 146 | ) 147 | ); 148 | } 149 | 150 | protected function processNegation(TreeNode $node, int $domain) 151 | { 152 | return $this->processNode($node->getChildren()[0], ++$domain%2); 153 | } 154 | 155 | protected function processField(TreeNode $node): array 156 | { 157 | $path = $node->getChild($node->getChild(0)->getId() === '#path' ? 0 : 1); 158 | 159 | return [ 160 | 'identifier' => implode('.', array_map(function (TreeNode $node) { 161 | return $node->getValueValue(); 162 | }, $path->getChildren())), 163 | ]; 164 | } 165 | 166 | protected function processInteger(TreeNode $node): array 167 | { 168 | return [ 169 | 'value' => $node->getValueValue(), 170 | 'type' => PDO::PARAM_INT, 171 | ]; 172 | } 173 | 174 | protected function processString(TreeNode $node): array 175 | { 176 | return [ 177 | 'value' => trim($node->getValueValue(), '`'), 178 | 'type' => PDO::PARAM_STR, 179 | ]; 180 | } 181 | 182 | protected function processBoolean(TreeNode $node): array 183 | { 184 | return [ 185 | 'value' => $node->getValueValue(), 186 | 'type' => PDO::PARAM_BOOL, 187 | ]; 188 | } 189 | 190 | protected function processFloat(TreeNode $node): array 191 | { 192 | return [ 193 | 'value' => $node->getValueValue(), 194 | 'type' => PDO::PARAM_STR, 195 | ]; 196 | } 197 | 198 | protected function processList(TreeNode $node): array 199 | { 200 | return [ 201 | 'value' => array_map(function (TreeNode $node) { 202 | return $node->getValueToken() === 'string' 203 | ? trim($node->getValueValue(), '`') : $node->getValueValue(); 204 | }, $node->getChildren()), 205 | 'type' => $node->getChild(0)->getValueToken() === 'int' 206 | ? Connection::PARAM_INT_ARRAY : Connection::PARAM_STR_ARRAY, 207 | ]; 208 | } 209 | 210 | protected function processVariable(TreeNode $node): array 211 | { 212 | $variableName = $node->getChild(0)->getValueValue(); 213 | $variableValue = $this->info->variableValues[$variableName]; 214 | 215 | foreach ($this->info->operation->variableDefinitions as $variableDefinition) { 216 | if ($variableDefinition->variable->name->value === $variableName) { 217 | break; 218 | } 219 | } 220 | 221 | if ($variableDefinition->type instanceof ListTypeNode) { 222 | if ($variableDefinition->type->type->name->value === Type::INT) { 223 | $variableType = Connection::PARAM_INT_ARRAY; 224 | } else { 225 | $variableType = Connection::PARAM_STR_ARRAY; 226 | } 227 | } elseif ($variableDefinition->type instanceof NamedTypeNode) { 228 | if ($variableValue === null) { 229 | $variableType = PDO::PARAM_NULL; 230 | } elseif ($variableDefinition->type->name->value === Type::INT) { 231 | $variableType = PDO::PARAM_INT; 232 | } elseif ($variableDefinition->type->name->value === Type::BOOLEAN) { 233 | $variableType = PDO::PARAM_BOOL; 234 | } elseif ($variableDefinition->type->name->value === Type::STRING) { 235 | $variableType = PDO::PARAM_STR; 236 | } elseif ($variableDefinition->type->name->value === Type::FLOAT) { 237 | $variableType = PDO::PARAM_STR; 238 | } 239 | } 240 | 241 | return [ 242 | 'value' => $variableValue, 243 | 'type' => $variableType, 244 | ]; 245 | } 246 | 247 | protected function processNone(TreeNode $node): array 248 | { 249 | return [ 250 | 'value' => null, 251 | 'type' => PDO::PARAM_NULL, 252 | ]; 253 | } 254 | 255 | protected function isConnective(TreeNode $node) 256 | { 257 | return !$node->isToken() && ($node->getId() === '#and' || $node->getId() === '#or'); 258 | } 259 | 260 | protected function isComparison(TreeNode $node) 261 | { 262 | return !$node->isToken() && in_array($node->getId(), [ 263 | '#equals', '#not_equals', 264 | '#greater_than', '#less_than', 265 | '#greater_than_equals', '#less_than_equals', 266 | '#in', 267 | ]); 268 | } 269 | 270 | protected function isNullComparison(TreeNode $node) 271 | { 272 | if ($node->isToken() || $node->getId() !== '#equals' && $node->getId() !== '#not_equals') { 273 | return false; 274 | } 275 | 276 | if (count(array_filter($node->getChildren(), function (TreeNode $node) { 277 | return $node->isToken() && $node->getValueToken() === 'null' 278 | || !$node->isToken() && $node->getId() === '#variable' 279 | && $this->info->variableValues[$node->getChild(0)->getValueValue()] === null; 280 | })) !== 1) { 281 | return false; 282 | } 283 | 284 | return true; 285 | } 286 | 287 | protected function isNegation(TreeNode $node): bool 288 | { 289 | return !$node->isToken() && $node->getId() === '#not'; 290 | } 291 | 292 | protected function isList(TreeNode $node): bool 293 | { 294 | return !$node->isToken() && $node->getId() === '#list'; 295 | } 296 | 297 | protected function isVariable(TreeNode $node): bool 298 | { 299 | return !$node->isToken() && $node->getId() === '#variable'; 300 | } 301 | 302 | protected function getType(TreeNode $node): string 303 | { 304 | return $node->isToken() ? ucfirst($node->getValueToken()) : 'Node'; 305 | } 306 | } -------------------------------------------------------------------------------- /Classes/Database/FilterQueryHandler.php: -------------------------------------------------------------------------------- 1 | getContext(); 34 | 35 | if (!isset($context['builder']) || !$context['builder'] instanceof QueryBuilder) { 36 | return; 37 | } 38 | 39 | $meta = $event->getResolver()->getElement(); 40 | 41 | if (!$meta instanceof PropertyDefinition && !$meta instanceof EntityDefinition) { 42 | return; 43 | } 44 | 45 | $arguments = $event->getArguments(); 46 | $builder = $context['builder']; 47 | 48 | $processor = GeneralUtility::makeInstance( 49 | FilterExpressionProcessor::class, 50 | $event->getInfo(), 51 | $builder, 52 | function (QueryBuilder $builder, string $operator, array ...$operands) use ($event) { 53 | $table = array_pop(QueryHelper::getQueriedTables($builder, QueryHelper::QUERY_PART_FROM)); 54 | $operands = array_map(function ($operand) use ($builder, $event, $table) { 55 | return isset($operand['identifier']) 56 | ? $builder->quoteIdentifier($table . '.' . $operand['identifier']) 57 | : $builder->createNamedParameter($operand['value'], $operand['type']); 58 | }, $operands); 59 | 60 | if (count($operands) === 2) { 61 | return $builder->expr()->comparison( 62 | $operands[0], 63 | $operator, 64 | in_array($operator, ['IN', 'NOT IN']) ? '(' . $operands[1] . ')' : $operands[1] 65 | ); 66 | } elseif (count($operands) === 1) { 67 | return $operands[0] . ' ' . $operator; 68 | } 69 | 70 | throw new Exception('Unexpected filter expression leaf', 1564352799); 71 | } 72 | ); 73 | 74 | $condition = $processor->process($arguments[FilterArgumentProvider::ARGUMENT_NAME] ?? null); 75 | 76 | if ($condition !== null) { 77 | $builder->andWhere($condition); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Classes/Database/LocalizationQueryHandler.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder($event); 34 | 35 | if ($builder === null) { 36 | return; 37 | } 38 | 39 | $meta = $event->getResolver()->getElement(); 40 | 41 | if (!$meta instanceof EntityDefinition) { 42 | return; 43 | } 44 | 45 | $tables = QueryHelper::getQueriedTables($builder, QueryHelper::QUERY_PART_FROM); 46 | 47 | if (count($tables) > 1) { 48 | return; 49 | } 50 | 51 | $table = array_pop($tables); 52 | 53 | if (!isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) 54 | || !isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) 55 | ) { 56 | return; 57 | } 58 | 59 | $languageField = $GLOBALS['TCA'][$table]['ctrl']['languageField']; 60 | $translationParent = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; 61 | $languageAspect = $this->getLanguageAspect($event); 62 | 63 | if ($languageAspect !== null && $languageAspect->getContentId() > 0) { 64 | switch ($languageAspect->getOverlayType()) { 65 | case LanguageAspect::OVERLAYS_OFF: 66 | $builder->andWhere( 67 | $builder->expr()->in( 68 | $table . '.' . $languageField, 69 | $builder->createNamedParameter( 70 | [-1, $languageAspect->getContentId()], 71 | Connection::PARAM_INT_ARRAY 72 | ) 73 | ), 74 | $builder->expr()->eq( 75 | $table . '.' . $translationParent, 76 | $builder->createNamedParameter( 77 | 0, 78 | \PDO::PARAM_INT 79 | ) 80 | ) 81 | ); 82 | break; 83 | case LanguageAspect::OVERLAYS_MIXED: 84 | $builder->leftJoin( 85 | $table, 86 | $table, 87 | 'language_overlay', 88 | (string) $builder->expr()->eq( 89 | $table . '.uid', 90 | $builder->quoteIdentifier('language_overlay.' . $translationParent) 91 | ) 92 | )->andWhere( 93 | $builder->expr()->orX( 94 | $builder->expr()->andX( 95 | $builder->expr()->neq( 96 | $table . '.' . $translationParent, 97 | $builder->createNamedParameter( 98 | 0, 99 | \PDO::PARAM_INT 100 | ) 101 | ), 102 | $builder->expr()->eq( 103 | $table . '.' . $languageField, 104 | $builder->createNamedParameter( 105 | $languageAspect->getContentId(), 106 | \PDO::PARAM_INT 107 | ) 108 | ) 109 | ), 110 | $builder->expr()->in( 111 | $table . '.' . $languageField, 112 | $builder->createNamedParameter( 113 | [-1, 0], 114 | Connection::PARAM_INT_ARRAY 115 | ) 116 | ) 117 | ), 118 | $builder->expr()->isNull( 119 | 'language_overlay.uid' 120 | ) 121 | ); 122 | break; 123 | case LanguageAspect::OVERLAYS_ON: 124 | $builder->orWhere( 125 | $builder->expr()->eq( 126 | $table . '.' . $languageField, 127 | $builder->createNamedParameter( 128 | -1, 129 | \PDO::PARAM_INT 130 | ) 131 | ), 132 | $builder->expr()->andX( 133 | $builder->expr()->eq( 134 | $table . '.' . $languageField, 135 | $builder->createNamedParameter( 136 | $languageAspect->getContentId(), 137 | \PDO::PARAM_INT 138 | ) 139 | ), 140 | $builder->expr()->neq( 141 | $table . '.' . $translationParent, 142 | $builder->createNamedParameter( 143 | 0, 144 | \PDO::PARAM_INT 145 | ) 146 | ) 147 | ) 148 | ); 149 | $languages[] = 0; 150 | break; 151 | case LanguageAspect::OVERLAYS_ON_WITH_FLOATING: 152 | $builder->andWhere( 153 | $builder->expr()->in( 154 | $table . '.' . $languageField, 155 | $builder->createNamedParameter( 156 | [-1, $languageAspect->getContentId()], 157 | Connection::PARAM_INT_ARRAY 158 | ) 159 | ) 160 | ); 161 | break; 162 | } 163 | } elseif ($languageAspect !== null && $languageAspect->getContentId() === 0) { 164 | $builder->andWhere( 165 | $builder->expr()->in( 166 | $table . '.' . $languageField, 167 | $builder->createNamedParameter( 168 | [-1, $languageAspect->getContentId()], 169 | Connection::PARAM_INT_ARRAY 170 | ) 171 | ) 172 | ); 173 | } 174 | } 175 | 176 | protected function getLanguageAspect($event): ?LanguageAspect 177 | { 178 | $context = $event->getContext(); 179 | 180 | if (isset($context['context']) && $context['context'] instanceof Context) { 181 | return $context['context']->getAspect('language'); 182 | } 183 | 184 | return null; 185 | } 186 | 187 | protected function getQueryBuilder($event): ?QueryBuilder 188 | { 189 | $context = $event->getContext(); 190 | 191 | if (isset($context['builder']) && $context['builder'] instanceof QueryBuilder) { 192 | return $context['builder']; 193 | } 194 | 195 | return null; 196 | } 197 | } -------------------------------------------------------------------------------- /Classes/Database/OrderArgumentProvider.php: -------------------------------------------------------------------------------- 1 | getElement(); 37 | 38 | if (!$meta instanceof PropertyDefinition && !$meta instanceof EntityDefinition) { 39 | return; 40 | } 41 | 42 | $event->addArgument(self::ARGUMENT_NAME, OrderExpressionType::instance()); 43 | } 44 | } -------------------------------------------------------------------------------- /Classes/Database/OrderExpressionTraversable.php: -------------------------------------------------------------------------------- 1 | self::ORDER_ASCENDING, 56 | 'desc' => self::ORDER_DESCENDING, 57 | 'ascending' => self::ORDER_ASCENDING, 58 | 'descending' => self::ORDER_DESCENDING, 59 | ]; 60 | 61 | /** 62 | * @var ElementInterface 63 | */ 64 | protected $element; 65 | 66 | /** 67 | * @var TreeNode 68 | */ 69 | protected $expression; 70 | 71 | /** 72 | * @var int 73 | */ 74 | protected $mode; 75 | 76 | public function __construct(ElementInterface $element, ?TreeNode $expression, int $mode = self::MODE_SQL) 77 | { 78 | Assert::isInstanceOfAny($element, [EntityDefinition::class, PropertyDefinition::class]); 79 | Assert::oneOf($mode, [self::MODE_SQL, self::MODE_GQL]); 80 | 81 | $this->element = $element; 82 | $this->expression = $expression; 83 | $this->mode = $mode; 84 | } 85 | 86 | public function getIterator() 87 | { 88 | if (!$this->expression) { 89 | return; 90 | } 91 | 92 | foreach ($this->expression->getChildren() as $item) { 93 | $path = $item->getChild(0); 94 | $field = $path->getChild(0)->getValueValue(); 95 | $order = strtolower($item->getChild(count($item->getChildren()) > 2 ? 2 : 1)->getValueValue()); 96 | $constraints = [null]; 97 | 98 | if ($item->getChildrenNumber() > 2) { 99 | $constraints = [$item->getChild(1)->getChild(0)->getValueValue()]; 100 | } elseif ($this->mode == self::MODE_SQL && $this->element instanceof EntityDefinition) { 101 | $constraints = [$this->element->getName()]; 102 | } elseif ($this->mode == self::MODE_SQL && $this->element instanceof PropertyDefinition) { 103 | $constraints = $this->element->getRelationTableNames(); 104 | } 105 | 106 | foreach ($constraints as $constraint) { 107 | yield [ 108 | 'constraint' => $constraint, 109 | 'field' => $field, 110 | 'order' => self::ORDER_MAPPINGS[strtolower($order)], 111 | ]; 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Classes/Database/OrderQueryHandler.php: -------------------------------------------------------------------------------- 1 | getContext(); 37 | 38 | if (!isset($context['builder']) || !$context['builder'] instanceof QueryBuilder) { 39 | return; 40 | } 41 | 42 | $meta = $event->getResolver()->getElement(); 43 | 44 | if (!$meta instanceof PropertyDefinition && !$meta instanceof EntityDefinition) { 45 | return; 46 | } 47 | 48 | $arguments = $event->getArguments(); 49 | $builder = $context['builder']; 50 | $tables = array_flip(QueryHelper::getQueriedTables($builder)); 51 | $expression = $arguments[OrderArgumentProvider::ARGUMENT_NAME] ?? null; 52 | 53 | $items = GeneralUtility::makeInstance(OrderExpressionTraversable::class, $meta, $expression); 54 | 55 | foreach ($items as $item) { 56 | if (!isset($tables[$item['constraint']])) { 57 | continue; 58 | } 59 | 60 | if ($meta instanceof PropertyDefinition && count($meta->getRelationTableNames()) > 1) { 61 | $builder->addSelect($item['constraint'] . '.' . $item['field'] . ' AS __' . $item['field']); 62 | } else { 63 | $builder->addOrderBy( 64 | $item['constraint'] . '.' . $item['field'], 65 | $item['order'] === OrderExpressionTraversable::ORDER_ASCENDING ? 'ASC' : 'DESC' 66 | ); 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Classes/Database/OrderValueHandler.php: -------------------------------------------------------------------------------- 1 | getValue(); 35 | 36 | if (empty($value)) { 37 | return; 38 | } 39 | 40 | $meta = $event->getResolver()->getElement(); 41 | 42 | if (!$meta instanceof PropertyDefinition) { 43 | return; 44 | } 45 | 46 | $arguments = $event->getArguments(); 47 | $expression = $arguments[OrderArgumentProvider::ARGUMENT_NAME] ?? null; 48 | 49 | // sort only when more than one table were fetched 50 | if (count($meta->getRelationTableNames()) === 1) { 51 | return; 52 | } 53 | 54 | // do not sort when no expression is given and its an unormalized relationship 55 | if ($expression === null && $event->getResolver() instanceof ActiveRelationshipResolver) { 56 | return; 57 | } 58 | 59 | $items = GeneralUtility::makeInstance( 60 | OrderExpressionTraversable::class, 61 | $meta, 62 | $expression, 63 | OrderExpressionTraversable::MODE_GQL 64 | ); 65 | $arguments = []; 66 | 67 | foreach ($items as $item) { 68 | array_push($arguments, array_map(function ($row) use ($item) { 69 | return !$item['constraint'] || $row[EntitySchemaFactory::ENTITY_TYPE_FIELD] === $item['constraint'] 70 | ? $row['__' . $item['field']] : null; 71 | }, $value), $item['order'] === OrderExpressionTraversable::ORDER_ASCENDING ? SORT_ASC : SORT_DESC); 72 | } 73 | 74 | array_push($arguments, $value); 75 | array_multisort(...$arguments); 76 | 77 | $event->setValue(array_pop($arguments)); 78 | } 79 | } -------------------------------------------------------------------------------- /Classes/Database/PassiveManyToManyRelationshipResolver.php: -------------------------------------------------------------------------------- 1 | isManyToManyRelationProperty(); 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | * @todo Prevent reaching maximum length of a generated SQL statement. 49 | * @see https://www.sqlite.org/limits.html#max_sql_length 50 | * @see https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet 51 | * @see https://mariadb.com/kb/en/library/server-system-variables/#max_allowed_packet 52 | * @see https://www.postgresql.org/docs/9.1/runtime-config-resource.html#GUC-MAX-STACK-DEPTH 53 | */ 54 | public function resolve($source, array $arguments, array $context, ResolveInfo $info) 55 | { 56 | Assert::keyExists($context, 'cache'); 57 | Assert::isInstanceOf($context['cache'], FrontendInterface::class); 58 | 59 | $dispatcher = $this->getEventDispatcher(); 60 | 61 | $bufferIdentifier = $this->getCacheIdentifier('buffer'); 62 | $buffer = $context['cache']->get($bufferIdentifier) ?: []; 63 | 64 | $keysIdentifier = $this->getCacheIdentifier('keys'); 65 | $keys = $context['cache']->get($keysIdentifier) ?? []; 66 | 67 | if (!$context['cache']->has($bufferIdentifier)) { 68 | foreach ($this->getPropertyDefinition()->getRelationTableNames() as $table) { 69 | $builder = $this->getBuilder($info, $table, $keys); 70 | 71 | $dispatcher->dispatch( 72 | new BeforeValueResolvingEvent( 73 | $source, 74 | $arguments, 75 | ['builder' => $builder] + $context, 76 | $info, 77 | $this 78 | ) 79 | ); 80 | 81 | $statement = $builder->execute(); 82 | 83 | while ($row = $statement->fetch()) { 84 | foreach ($this->getBufferIndexes($row) as $index) { 85 | $buffer[$index][] = $row; 86 | } 87 | } 88 | } 89 | 90 | $context['cache']->set($bufferIdentifier, $buffer); 91 | } 92 | 93 | $event = new AfterValueResolvingEvent($buffer[$source['__uid']], $source, $arguments, $context, $info, $this); 94 | 95 | $dispatcher->dispatch($event); 96 | 97 | return $this->getValue($event->getValue()); 98 | } 99 | 100 | protected function getTable(): string 101 | { 102 | return $this->getPropertyDefinition()->getManyToManyTableName(); 103 | } 104 | 105 | protected function getForeignKeyField(): string 106 | { 107 | return 'uid_local'; 108 | } 109 | 110 | /** 111 | * @todo Make `tablenames` depended from the meta configuration. 112 | * @todo Create another test case were `tablenames` is not used. 113 | */ 114 | protected function getBuilder(ResolveInfo $info, string $table, array $keys): QueryBuilder 115 | { 116 | $builder = parent::getBuilder($info, $table, $keys); 117 | 118 | $builder->innerJoin( 119 | $table, 120 | $this->getTable(), 121 | $this->getTable(), 122 | (string) $builder->expr()->andX( 123 | $builder->expr()->eq( 124 | $this->getTable() . '.uid_foreign', 125 | $builder->quoteIdentifier($table . '.uid') 126 | ), 127 | $builder->expr()->eq( 128 | $this->getTable() . '.tablenames', 129 | $builder->createNamedParameter($table) 130 | ) 131 | ) 132 | ); 133 | 134 | return $builder; 135 | } 136 | 137 | protected function getColumns(ResolveInfo $info, QueryBuilder $builder, string $table) 138 | { 139 | $columns = parent::getColumns($info, $builder, $table); 140 | 141 | $columns[] = $builder->quoteIdentifier($this->getTable() . '.' . $this->getForeignKeyField()) 142 | . ' AS ' . $builder->quoteIdentifier('__' . $this->getForeignKeyField()); 143 | 144 | return $columns; 145 | } 146 | 147 | protected function getCondition(QueryBuilder $builder, array $keys): array 148 | { 149 | $condition = parent::getCondition($builder, $keys); 150 | 151 | $configuration = $this->getPropertyDefinition()->getConfiguration(); 152 | $table = $this->getTable(); 153 | 154 | foreach ($configuration['config']['MM_match_fields'] as $field => $match) { 155 | $condition[] = $builder->expr()->eq( 156 | $table . '.' . $field, 157 | $builder->createNamedParameter($match) 158 | ); 159 | } 160 | 161 | return $condition; 162 | } 163 | } -------------------------------------------------------------------------------- /Classes/Database/PassiveOneToManyRelationshipResolver.php: -------------------------------------------------------------------------------- 1 | isManyToManyRelationProperty()) { 45 | return false; 46 | } 47 | 48 | foreach ($element->getActiveRelations() as $activeRelation) { 49 | if (!($activeRelation instanceof ActivePropertyRelation)) { 50 | return false; 51 | } 52 | } 53 | 54 | return true; 55 | } 56 | 57 | /** 58 | * @inheritdoc 59 | * @todo Prevent reaching maximum length of a generated SQL statement. 60 | * @see https://www.sqlite.org/limits.html#max_sql_length 61 | * @see https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_allowed_packet 62 | * @see https://mariadb.com/kb/en/library/server-system-variables/#max_allowed_packet 63 | * @see https://www.postgresql.org/docs/9.1/runtime-config-resource.html#GUC-MAX-STACK-DEPTH 64 | */ 65 | public function resolve($source, array $arguments, array $context, ResolveInfo $info) 66 | { 67 | Assert::keyExists($context, 'cache'); 68 | Assert::isInstanceOf($context['cache'], FrontendInterface::class); 69 | 70 | $bufferIdentifier = $this->getCacheIdentifier('buffer'); 71 | $buffer = $context['cache']->get($bufferIdentifier) ?: []; 72 | 73 | $keysIdentifier = $this->getCacheIdentifier('keys'); 74 | $keys = $context['cache']->get($keysIdentifier) ?? []; 75 | 76 | if (!$context['cache']->has($bufferIdentifier)) { 77 | $table =$this->getTable(); 78 | $builder = $this->getBuilder($info, $table, $keys); 79 | $dispatcher = $this->getEventDispatcher(); 80 | 81 | $dispatcher->dispatch( 82 | new BeforeValueResolvingEvent( 83 | $source, 84 | $arguments, 85 | ['builder' => $builder] + $context, 86 | $info, 87 | $this 88 | ) 89 | ); 90 | 91 | $statement = $builder->execute(); 92 | 93 | while ($row = $statement->fetch()) { 94 | foreach ($this->getBufferIndexes($row) as $index) { 95 | $event = new AfterValueResolvingEvent($row, $source, $arguments, $context, $info, $this); 96 | 97 | $dispatcher->dispatch($event); 98 | 99 | $buffer[$index][] = $event->getValue(); 100 | } 101 | } 102 | 103 | $context['cache']->set($bufferIdentifier, $buffer); 104 | } 105 | 106 | return $this->getValue($buffer[$source['__uid']]); 107 | } 108 | 109 | protected function getBufferIndexes(array $row): array 110 | { 111 | $indexes = parent::getBufferIndexes($row); 112 | 113 | $configuration = $this->getPropertyDefinition()->getConfiguration(); 114 | 115 | if (isset($configuration['config']['symmetric_field'])) { 116 | $indexes[] = $row['__' . $configuration['config']['symmetric_field']]; 117 | } 118 | 119 | return $indexes; 120 | } 121 | 122 | protected function getTable(): string 123 | { 124 | return reset($this->getPropertyDefinition()->getRelationTableNames()); 125 | } 126 | 127 | protected function getForeignKeyField(): string 128 | { 129 | Assert::count($this->getPropertyDefinition()->getActiveRelations(), 1); 130 | 131 | $activeRelation = reset($this->getPropertyDefinition()->getActiveRelations()); 132 | 133 | Assert::isInstanceOf($activeRelation, ActivePropertyRelation::class); 134 | 135 | return $activeRelation->getTo()->getName(); 136 | } 137 | 138 | protected function getColumns(ResolveInfo $info, QueryBuilder $builder, string $table) 139 | { 140 | $columns = parent::getColumns($info, $builder, $table); 141 | 142 | $columns[] = $builder->quoteIdentifier($table . '.' . $this->getForeignKeyField()) 143 | . ' AS ' . $builder->quoteIdentifier('__' . $this->getForeignKeyField()); 144 | 145 | $configuration = $this->getPropertyDefinition()->getConfiguration(); 146 | 147 | if (isset($configuration['config']['symmetric_field'])) { 148 | $columns[] = $builder->quoteIdentifier($table . '.' . $configuration['config']['symmetric_field']) 149 | . ' AS ' . $builder->quoteIdentifier('__' . $configuration['config']['symmetric_field']); 150 | } 151 | 152 | return $columns; 153 | } 154 | 155 | protected function getCondition(QueryBuilder $builder, array $keys): array 156 | { 157 | $condition = parent::getCondition($builder, $keys); 158 | 159 | $configuration = $this->getPropertyDefinition()->getConfiguration(); 160 | $table = $this->getPropertyDefinition()->getEntityDefinition()->getName(); 161 | 162 | if (isset($configuration['config']['foreign_table_field'])) { 163 | $condition[] = $builder->expr()->eq( 164 | $this->getTable() . '.' . $configuration['config']['foreign_table_field'], 165 | $builder->createNamedParameter($table) 166 | ); 167 | 168 | if (isset($configuration['config']['symmetric_field'])) { 169 | $condition[] = $builder->expr()->andX( 170 | array_pop($condition), 171 | $builder->expr()->eq( 172 | $this->getTable() . '.' . $configuration['config']['symmetric_field'], 173 | $builder->createNamedParameter($table) 174 | ) 175 | ); 176 | } 177 | } 178 | 179 | foreach ($configuration['config']['foreign_match_fields'] ?? [] as $field => $match) { 180 | $condition[] = $builder->expr()->eq( 181 | $this->getTable() . '.' . $field, 182 | $builder->createNamedParameter($match) 183 | ); 184 | } 185 | 186 | return $condition; 187 | } 188 | } -------------------------------------------------------------------------------- /Classes/Database/QueryHelper.php: -------------------------------------------------------------------------------- 1 | getQueryPart('from') as $from) { 54 | $tableName = self::unquoteSingleIdentifier($builder, $from['table']); 55 | $tableAlias = isset($from['alias']) 56 | ? self::unquoteSingleIdentifier($builder, $from['alias']) : $tableName; 57 | $queriedTables[$tableAlias] = $tableName; 58 | } 59 | } 60 | 61 | if ($part & self::QUERY_PART_JOIN) { 62 | foreach ($builder->getQueryPart('join') as $joins) { 63 | foreach ($joins as $join) { 64 | $tableName = self::unquoteSingleIdentifier($builder, $join['joinTable']); 65 | $tableAlias = isset($join['joinAlias']) 66 | ? self::unquoteSingleIdentifier($builder, $join['joinAlias']) : $tableName; 67 | $queriedTables[$tableAlias] = $tableName; 68 | } 69 | } 70 | } 71 | 72 | return $queriedTables; 73 | } 74 | 75 | /** 76 | * Unquotes a single identifier. 77 | * 78 | * @param string $identifier Identifier 79 | * @return string Unquoted identifier 80 | */ 81 | public static function unquoteSingleIdentifier(QueryBuilder $builder, string $identifier): string 82 | { 83 | $identifier = trim($identifier); 84 | $platform = $builder->getConnection()->getDatabasePlatform(); 85 | 86 | if ($platform instanceof SQLServerPlatform) { 87 | $identifier = ltrim($identifier, '['); 88 | $identifier = rtrim($identifier, ']'); 89 | } else { 90 | $quoteChar = $platform->getIdentifierQuoteCharacter(); 91 | $identifier = trim($identifier, $quoteChar); 92 | $identifier = str_replace($quoteChar . $quoteChar, $quoteChar, $identifier); 93 | } 94 | 95 | return $identifier; 96 | } 97 | 98 | public static function filterLanguageOverlayTables(array $tables) 99 | { 100 | $tables = array_filter($tables, function ($tableAlias) { 101 | return StringUtility::beginsWith($tableAlias, 'language_overlay'); 102 | }, ARRAY_FILTER_USE_KEY); 103 | 104 | ksort($tables); 105 | 106 | return $tables; 107 | } 108 | } -------------------------------------------------------------------------------- /Classes/EntityReader.php: -------------------------------------------------------------------------------- 1 | create($entityRelationMapFactory->create()); 51 | } 52 | } 53 | 54 | public function execute(string $query, array $bindings = [], Context $context = null): array 55 | { 56 | $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('gql'); 57 | $cache->flush(); 58 | 59 | return GraphQL::executeQuery( 60 | self::$schema, 61 | $query, 62 | null, 63 | [ 64 | 'cache' => $cache, 65 | 'context' => $context, 66 | ], 67 | $bindings, 68 | null, 69 | null, 70 | array_merge( 71 | DocumentValidator::defaultRules(), 72 | [ 73 | NoUndefinedVariables::class => new NoUndefinedVariablesRule(), 74 | NoUnusedVariables::class => new NoUnusedVariablesRule(), 75 | NoUnsupportedFeaturesRule::class => new NoUnsupportedFeaturesRule(), 76 | FiltersOnCorrectTypeRule::class => new FiltersOnCorrectTypeRule(), 77 | OrdersOnCorrectTypeRule::class => new OrdersOnCorrectTypeRule(), 78 | ] 79 | ) 80 | ) 81 | ->setErrorsHandler(function (array $errors, callable $formatter) { 82 | throw new ExecutionException('Query execution has failed.', 1566148265, null, ...$errors); 83 | }) 84 | ->toArray(); 85 | } 86 | } -------------------------------------------------------------------------------- /Classes/EntitySchemaFactory.php: -------------------------------------------------------------------------------- 1 | resolverFactory = GeneralUtility::makeInstance(ResolverFactory::class); 65 | } 66 | 67 | public function create(EntityRelationMap $entityRelationMap): Schema 68 | { 69 | $query = [ 70 | 'name' => self::ENTITY_QUERY_NAME, 71 | 'fields' => [], 72 | ]; 73 | 74 | $dispatcher = $this->getEventDispatcher(); 75 | 76 | foreach ($entityRelationMap->getEntityDefinitions() as $entityDefinition) { 77 | $type = Type::listOf($this->buildObjectType($entityDefinition)); 78 | 79 | $name = $entityDefinition->getName(); 80 | $event = new BeforeFieldArgumentsInitializationEvent($name, $entityDefinition, $type); 81 | 82 | $dispatcher->dispatch($event); 83 | 84 | $resolver = $this->resolverFactory->create($entityDefinition, $type); 85 | 86 | $query['fields'][$name] = [ 87 | 'type' => $type, 88 | 'args' => array_replace_recursive($resolver->getArguments(), $event->getArguments()), 89 | 'meta' => $entityDefinition, 90 | 'resolve' => function ($source, $arguments, $context, ResolveInfo $info) use ($resolver) { 91 | return $resolver->resolve($source, $arguments, $context, $info); 92 | }, 93 | ]; 94 | } 95 | 96 | return new Schema([ 97 | 'query' => new ObjectType($query), 98 | ]); 99 | } 100 | 101 | protected function buildEntityInterfaceType(): Type 102 | { 103 | if (!array_key_exists(self::ENTITY_INTERFACE_NAME, self::$interfaceTypes)) { 104 | self::$interfaceTypes[self::ENTITY_INTERFACE_NAME] = new InterfaceType([ 105 | 'name' => self::ENTITY_INTERFACE_NAME, 106 | 'fields' => [ 107 | 'uid' => [ 108 | 'type' => Type::id(), 109 | ], 110 | 'pid' => [ 111 | 'type' => Type::int(), 112 | ], 113 | ], 114 | 'resolveType' => function ($value) { 115 | return self::$objectTypes[$value[self::ENTITY_TYPE_FIELD]]; 116 | }, 117 | ]); 118 | } 119 | 120 | return self::$interfaceTypes[self::ENTITY_INTERFACE_NAME]; 121 | } 122 | 123 | protected function buildObjectType(EntityDefinition $entityDefinition): Type 124 | { 125 | if (array_key_exists($entityDefinition->getName(), self::$objectTypes)) { 126 | $objectType = self::$objectTypes[$entityDefinition->getName()]; 127 | } else { 128 | $objectType = new ObjectType([ 129 | 'name' => $entityDefinition->getName(), 130 | 'interfaces' => [ 131 | $this->buildEntityInterfaceType(), 132 | ], 133 | 'fields' => function () use ($entityDefinition) { 134 | $table = GeneralUtility::makeInstance(ConnectionPool::class) 135 | ->getConnectionForTable($entityDefinition->getName()) 136 | ->getSchemaManager() 137 | ->listTableDetails($entityDefinition->getName()); 138 | 139 | $propertyDefinitions = array_filter( 140 | $entityDefinition->getPropertyDefinitions(), 141 | function ($propertyDefinition) use ($table) { 142 | return $table->hasColumn($propertyDefinition->getName()); 143 | } 144 | ); 145 | 146 | return array_map(function ($propertyDefinition) use ($table) { 147 | $type = $this->buildFieldType($propertyDefinition); 148 | 149 | $name = $propertyDefinition->getName(); 150 | $event = new BeforeFieldArgumentsInitializationEvent($name, $propertyDefinition, $type); 151 | 152 | $this->getEventDispatcher()->dispatch($event); 153 | 154 | $field = [ 155 | 'name' => $name, 156 | 'type' => $type, 157 | 'meta' => $propertyDefinition, 158 | 'storage' => $table->getColumn($name)->getType(), 159 | ]; 160 | 161 | if ($propertyDefinition->isRelationProperty() 162 | && !$propertyDefinition->isLanguageRelationProperty() 163 | ) { 164 | $resolver = $this->resolverFactory->create($propertyDefinition, $type); 165 | 166 | $field['args'] = array_replace_recursive( 167 | $resolver->getArguments(), 168 | $event->getArguments() 169 | ); 170 | 171 | $field['resolve'] = function ($source, $arguments, $context, $info) use ($resolver) { 172 | $resolver->collect($source, $arguments, $context, $info); 173 | 174 | return new Deferred(function () use ($resolver, $source, $arguments, $context, $info) { 175 | return $resolver->resolve($source, $arguments, $context, $info); 176 | }); 177 | }; 178 | } 179 | 180 | return $field; 181 | }, $propertyDefinitions) + $this->buildEntityInterfaceType()->getFields(); 182 | }, 183 | 'meta' => $entityDefinition, 184 | ]); 185 | 186 | self::$objectTypes[$entityDefinition->getName()] = $objectType; 187 | } 188 | 189 | return $objectType; 190 | } 191 | 192 | protected function buildFieldType(PropertyDefinition $propertyDefinition): Type 193 | { 194 | $type = $propertyDefinition->isRelationProperty() 195 | ? $this->buildCompositeFieldType($propertyDefinition) : $this->buildScalarFieldType($propertyDefinition); 196 | 197 | return $type; 198 | } 199 | 200 | protected function buildCompositeFieldType(PropertyDefinition $propertyDefinition): Type 201 | { 202 | $activeRelations = $propertyDefinition->getActiveRelations(); 203 | $activeRelation = array_pop($activeRelations); 204 | 205 | $type = !empty($activeRelations) ? $this->buildEntityInterfaceType() : $this->buildObjectType( 206 | $activeRelation->getTo() instanceof PropertyDefinition 207 | ? $activeRelation->getTo()->getEntityDefinition() : $activeRelation->getTo() 208 | ); 209 | 210 | foreach ($propertyDefinition->getConstraints() as $constraint) { 211 | if ($constraint instanceof MultiplicityConstraint) { 212 | if ($constraint->getMinimum() > 0) { 213 | $type = Type::nonNull($type); 214 | } 215 | if ($constraint->getMaximum() === null || $constraint->getMaximum() > 1) { 216 | $type = Type::nonNull(Type::listOf($type)); 217 | } 218 | break; 219 | } 220 | } 221 | 222 | return $type; 223 | } 224 | 225 | protected function buildScalarFieldType(PropertyDefinition $propertyDefinition): Type 226 | { 227 | $type = GeneralUtility::makeInstance(ConnectionPool::class) 228 | ->getConnectionForTable($propertyDefinition->getEntityDefinition()->getName()) 229 | ->getSchemaManager() 230 | ->listTableDetails($propertyDefinition->getEntityDefinition()->getName()) 231 | ->getColumn($propertyDefinition->getName()) 232 | ->getType(); 233 | 234 | return TypeUtility::mapDatabaseType($type); 235 | } 236 | 237 | protected function getEventDispatcher(): EventDispatcherInterface 238 | { 239 | return GeneralUtility::makeInstance(EventDispatcher::class); 240 | } 241 | } -------------------------------------------------------------------------------- /Classes/Event/AfterValueResolvingEvent.php: -------------------------------------------------------------------------------- 1 | value = $value; 60 | $this->source = $source; 61 | $this->arguments = $arguments; 62 | $this->context = $context; 63 | $this->info = $info; 64 | $this->resolver = $resolver; 65 | } 66 | 67 | public function getValue() 68 | { 69 | return $this->value; 70 | } 71 | 72 | public function setValue($value): void 73 | { 74 | $this->value = $value; 75 | } 76 | 77 | public function getSource() 78 | { 79 | return $this->source; 80 | } 81 | 82 | public function getArguments(): array 83 | { 84 | return $this->arguments; 85 | } 86 | 87 | public function getContext(): array 88 | { 89 | return $this->context; 90 | } 91 | 92 | public function getInfo(): ResolveInfo 93 | { 94 | return $this->info; 95 | } 96 | 97 | public function getResolver(): ResolverInterface 98 | { 99 | return $this->resolver; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Classes/Event/BeforeFieldArgumentsInitializationEvent.php: -------------------------------------------------------------------------------- 1 | name = $name; 50 | $this->element = $element; 51 | $this->type = $type; 52 | $this->arguments = []; 53 | } 54 | 55 | public function getName(): string 56 | { 57 | return $this->name; 58 | } 59 | 60 | public function getElement(): ElementInterface 61 | { 62 | return $this->element; 63 | } 64 | 65 | public function getType(): Type 66 | { 67 | return $this->type; 68 | } 69 | 70 | public function getArguments(): array 71 | { 72 | return $this->arguments; 73 | } 74 | 75 | public function addArgument(string $name, Type $type): void 76 | { 77 | $this->arguments[] = [ 78 | 'name' => $name, 79 | 'type' => $type, 80 | ]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Classes/Event/BeforeValueResolvingEvent.php: -------------------------------------------------------------------------------- 1 | source = $source; 55 | $this->arguments = $arguments; 56 | $this->context = $context; 57 | $this->info = $info; 58 | $this->resolver = $resolver; 59 | } 60 | 61 | public function getSource() 62 | { 63 | return $this->source; 64 | } 65 | 66 | public function getArguments(): array 67 | { 68 | return $this->arguments; 69 | } 70 | 71 | public function getContext(): array 72 | { 73 | return $this->context; 74 | } 75 | 76 | public function getInfo(): ResolveInfo 77 | { 78 | return $this->info; 79 | } 80 | 81 | public function getResolver(): ResolverInterface 82 | { 83 | return $this->resolver; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Classes/Exception/ExecutionException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 38 | } 39 | 40 | public function getErrors(): array 41 | { 42 | return $this->errors; 43 | } 44 | } -------------------------------------------------------------------------------- /Classes/ExpressionNodeVisitor.php: -------------------------------------------------------------------------------- 1 | map = $map; 35 | } 36 | 37 | public function visit(Element $element, &$handle = null, $eldnah = null) 38 | { 39 | $result = null; 40 | 41 | if (isset($this->map[$element->getId()])) { 42 | $result = call_user_func($this->map[$element->getId()], $element); 43 | } 44 | 45 | if ($result !== false) { 46 | foreach ($element->getChildren() as $child) { 47 | $child->accept($this); 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Classes/FilterExpressionParser.php: -------------------------------------------------------------------------------- 1 | fieldNodes as $fieldNode) { 40 | foreach ($fieldNode->selectionSet->selections as $selection) { 41 | if ($selection->kind === NodeKind::FIELD) { 42 | $fields[] = $selection; 43 | } elseif ($selection->kind === NodeKind::INLINE_FRAGMENT 44 | && ($type === null || $selection->typeCondition->name->value === $type) 45 | ) { 46 | foreach ($selection->selectionSet->selections as $selection) { 47 | if ($selection->kind === NodeKind::FIELD) { 48 | $fields[] = $selection; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | return $fields; 56 | } 57 | } -------------------------------------------------------------------------------- /Classes/ResolverInterface.php: -------------------------------------------------------------------------------- 1 | parse($value); 50 | } 51 | 52 | public function parseLiteral($valueNode, array $variables = null) 53 | { 54 | return GeneralUtility::makeInstance(FilterExpressionParser::class)->parse($valueNode->value); 55 | } 56 | } -------------------------------------------------------------------------------- /Classes/Type/OrderExpressionType.php: -------------------------------------------------------------------------------- 1 | parse($value); 50 | } 51 | 52 | public function parseLiteral($valueNode, array $variables = null) 53 | { 54 | return GeneralUtility::makeInstance(OrderExpressionParser::class)->parse($valueNode->value); 55 | } 56 | } -------------------------------------------------------------------------------- /Classes/Utility/TypeUtility.php: -------------------------------------------------------------------------------- 1 | getId() === '#list') { 39 | return new ListOfType(self::fromFilterExpressionValue($node->getChild(0), $default)); 40 | } 41 | 42 | switch ($node->getValueToken()) { 43 | case 'string': 44 | return Type::string(); 45 | case 'integer': 46 | return Type::int(); 47 | case 'float': 48 | return Type::float(); 49 | case 'boolean': 50 | return Type::boolean(); 51 | case 'null': 52 | return $default; 53 | } 54 | 55 | throw new Exception('Unexpected value in filter expression.', 1566256204); 56 | } 57 | 58 | public static function mapDatabaseType(DatabaseType $type): Type 59 | { 60 | if ($type instanceof IntegerType || $type instanceof BigIntType || $type instanceof SmallIntType) { 61 | return Type::int(); 62 | } else if ($type instanceof FloatType || $type instanceof DecimalType) { 63 | return Type::float(); 64 | } else if ($type instanceof BooleanType) { 65 | return Type::boolean(); 66 | } 67 | 68 | return Type::string(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Classes/Validator/FiltersOnCorrectTypeRule.php: -------------------------------------------------------------------------------- 1 | [ 55 | 'enter' => function () { 56 | $this->variableDefinitions = []; 57 | $this->filterUsages = []; 58 | }, 59 | 'leave' => function (OperationDefinitionNode $operation) use ($context) { 60 | $operationName = $operation->name ? $operation->name->value : null; 61 | 62 | foreach ($this->filterUsages as [$fieldName, $operationName, $operandNode, $type, $argument]) { 63 | if (!$this->isValidField($context, $type, $argument, $fieldName)) { 64 | continue; 65 | } 66 | 67 | $fieldType = $this->getFieldType($type, $fieldName); 68 | 69 | if ($operandNode->getId() === '#variable') { 70 | $operandName = $operandNode->getChild(0)->getValueValue(); 71 | 72 | $operandType = AST::typeFromAST( 73 | $context->getSchema(), 74 | $this->variableDefinitions[$operandName]->type 75 | ); 76 | } elseif ($operandNode->getId() === '#field') { 77 | $operandName = $node->getChild(0)->getChild(0)->getValueValue(); 78 | 79 | if (!$this->isValidField($context, $type, $fieldName)) { 80 | continue; 81 | } 82 | 83 | $operandType = $this->getFieldType($type, $operandName); 84 | } else { 85 | $operandType = TypeUtility::fromFilterExpressionValue($operandNode, $fieldType); 86 | } 87 | 88 | if ($fieldType->name !== Type::ID && $operationName !== '#in' 89 | && $fieldType->name !== $operandType->name 90 | ) { 91 | $context->reportError(new Error( 92 | self::fieldMismatchMessage($fieldName, $operandType->name), 93 | [$argument] 94 | )); 95 | continue; 96 | } 97 | 98 | if ($fieldType->name === Type::ID && $operationName !== '#in' 99 | && !in_array($operandType->name, [Type::INT, Type::STRING]) 100 | ) { 101 | $context->reportError(new Error( 102 | self::fieldMismatchMessage($fieldName, $operandType->name), 103 | [$argument] 104 | )); 105 | continue; 106 | } 107 | 108 | if ($operationName === '#match' && $operandType->name !== Type::STRING) { 109 | $context->reportError(new Error( 110 | self::operationMismatchMessage($operandType->name, $operationName), 111 | [$argument] 112 | )); 113 | continue; 114 | } 115 | 116 | if ($operationName === '#in' && !$operandType instanceof ListOfType) { 117 | $context->reportError(new Error( 118 | self::operationMismatchMessage($operandType->name, $operationName), 119 | [$argument] 120 | )); 121 | continue; 122 | } 123 | 124 | if ($fieldType->name !== Type::ID && $operationName === '#in' 125 | && $fieldType->name !== $operandType->getWrappedType(true)->name 126 | ) { 127 | $context->reportError(new Error( 128 | self::fieldMismatchMessage($fieldName, $operandType->getWrappedType(true)->name), 129 | [$argument] 130 | )); 131 | continue; 132 | } 133 | 134 | if ($fieldType->name === Type::ID && $operationName === '#in' 135 | && !in_array($operandType->getWrappedType(true)->name, [Type::INT, Type::STRING]) 136 | ) { 137 | $context->reportError(new Error( 138 | self::fieldMismatchMessage($fieldName, $operandType->getWrappedType(true)->name), 139 | [$argument] 140 | )); 141 | continue; 142 | } 143 | } 144 | }, 145 | ], 146 | NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $definition) { 147 | $this->variableDefinitions[$definition->variable->name->value] = $definition; 148 | }, 149 | NodeKind::ARGUMENT => function ($argument) use ($context) { 150 | if ($context->getArgument()->getType() instanceof FilterExpressionType) { 151 | if ($argument->value->kind === NodeKind::STRING) { 152 | $expression = $context->getArgument()->getType()->parseValue($argument->value->value); 153 | 154 | $visitor = new ExpressionNodeVisitor([ 155 | '#field' => function (TreeNode $node) use ($argument, $context) { 156 | $type = $context->getType() instanceof WrappingType 157 | ? $context->getType()->getWrappedType(true) : $context->getType(); 158 | 159 | $this->filterUsages[] = [ 160 | $node->getChild(0)->getChild(0)->getValueValue(), 161 | $node->getParent()->getId(), 162 | $node->getParent()->getChild(0) === $node 163 | ? $node->getParent()->getChild(1) : $node->getParent()->getChild(0), 164 | $type, 165 | $argument, 166 | ]; 167 | return false; 168 | }, 169 | ]); 170 | 171 | $visitor->visit($expression); 172 | } 173 | } 174 | }, 175 | ]; 176 | } 177 | 178 | protected function isValidField(ValidationContext $context, Type $type, Node $argument, string $fieldName): bool 179 | { 180 | if (!$type->hasField($fieldName)) { 181 | $context->reportError(new Error( 182 | self::unknownFieldMessage($fieldName, $type->type->name), 183 | [$argument] 184 | )); 185 | return false; 186 | } 187 | 188 | return true; 189 | } 190 | 191 | protected function getFieldType(Type $type, string $fieldName): Type 192 | { 193 | $fieldType = $type->getField($fieldName)->getType(); 194 | 195 | if ($fieldType instanceof WrappingType) { 196 | $fieldType = $fieldType->getWrappedType(true); 197 | } 198 | 199 | if ($fieldType instanceof CompositeType) { 200 | $fieldType = TypeUtility::mapDatabaseType($type->getField($fieldName)->config['storage']); 201 | } 202 | 203 | return $fieldType; 204 | } 205 | 206 | public static function unknownFieldMessage($fieldName, $typeName) 207 | { 208 | return sprintf('Type "%s" does not have any field named "%s".', $typeName, $fieldName); 209 | } 210 | 211 | public static function fieldMismatchMessage($fieldName, $operandType) 212 | { 213 | return sprintf('Field "%s" does not match with filter operand type "%s".', $fieldName, $operandType); 214 | } 215 | 216 | public static function operationMismatchMessage($operandType, $operationName) 217 | { 218 | return sprintf('Type "%s" can not be used in filter operation "%s".', $operandType, $operationName); 219 | } 220 | } -------------------------------------------------------------------------------- /Classes/Validator/NoUndefinedVariablesRule.php: -------------------------------------------------------------------------------- 1 | [ 43 | 'enter' => function () { 44 | $this->variableNameDefined = []; 45 | }, 46 | 'leave' => function (OperationDefinitionNode $operation) use ($context) { 47 | $usages = $context->getRecursiveVariableUsages($operation); 48 | 49 | foreach ($usages as $usage) { 50 | $node = $usage['node']; 51 | $variableName = $node->name->value; 52 | 53 | if (!empty($this->variableNameDefined[$variableName])) { 54 | continue; 55 | } 56 | 57 | $context->reportError(new Error( 58 | self::undefinedVarMessage( 59 | $variableName, 60 | $operation->name ? $operation->name->value : null 61 | ), 62 | [$node, $operation] 63 | )); 64 | } 65 | }, 66 | ], 67 | NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $definition) { 68 | $this->variableNameDefined[$definition->variable->name->value] = true; 69 | }, 70 | NodeKind::ARGUMENT => function ($argument) use ($context) { 71 | if ($context->getArgument()->getType() instanceof FilterExpressionType) { 72 | if ($argument->value->kind === NodeKind::STRING) { 73 | $expression = $context->getArgument()->getType()->parseValue($argument->value->value); 74 | 75 | $visitor = new ExpressionNodeVisitor([ 76 | '#variable' => function (TreeNode $node) { 77 | $this->variableNameDefined[$node->getChild(0)->getValueValue()] = true; 78 | return false; 79 | }, 80 | ]); 81 | 82 | $visitor->visit($expression); 83 | } 84 | } 85 | }, 86 | ]; 87 | } 88 | 89 | public static function undefinedVarMessage($varName, $opName = null) 90 | { 91 | return $opName 92 | ? sprintf('Variable "$%s" is not defined by operation "%s".', $varName, $opName) 93 | : sprintf('Variable "$%s" is not defined.', $varName); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Classes/Validator/NoUnsupportedFeaturesRule.php: -------------------------------------------------------------------------------- 1 | function ($argument) use ($context) { 40 | if (($context->getArgument()->getType() instanceof OrderExpressionType 41 | || $context->getArgument()->getType() instanceof FilterExpressionType) 42 | && $argument->value->kind === NodeKind::STRING 43 | ) { 44 | $expression = $context->getArgument()->getType()->parseValue($argument->value->value); 45 | 46 | $visitor = new ExpressionNodeVisitor([ 47 | '#field' => function (TreeNode $node) use ($argument, $context) { 48 | if ($node->getChild(0)->getChildrenNumber() > 1) { 49 | $context->reportError(new Error( 50 | self::noNestedFieldsMessage(), 51 | [$argument] 52 | )); 53 | } 54 | return false; 55 | }, 56 | ]); 57 | 58 | $visitor->visit($expression); 59 | } 60 | }, 61 | ]; 62 | } 63 | 64 | public static function noNestedFieldsMessage() 65 | { 66 | return 'Nested field access in expressions is not implemented yet.'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Classes/Validator/NoUnusedVariablesRule.php: -------------------------------------------------------------------------------- 1 | [ 47 | 'enter' => function () { 48 | $this->variableDefinitions = []; 49 | $this->variableNameUsed = []; 50 | }, 51 | 'leave' => function (OperationDefinitionNode $operation) use ($context) { 52 | $usages = $context->getRecursiveVariableUsages($operation); 53 | $operationName = $operation->name ? $operation->name->value : null; 54 | 55 | foreach ($usages as $usage) { 56 | $node = $usage['node']; 57 | $this->variableNameUsed[$node->name->value] = true; 58 | } 59 | 60 | foreach ($this->variableDefinitions as $variableDefinition) { 61 | $variableName = $variableDefinition->variable->name->value; 62 | 63 | if (!empty($this->variableNameUsed[$variableName])) { 64 | continue; 65 | } 66 | 67 | $context->reportError(new Error( 68 | self::unusedVariableMessage($variableName, $operationName), 69 | [$variableDefinition] 70 | )); 71 | } 72 | }, 73 | ], 74 | NodeKind::VARIABLE_DEFINITION => function ($definition) { 75 | $this->variableDefinitions[] = $definition; 76 | }, 77 | NodeKind::ARGUMENT => function ($argument) use ($context) { 78 | if ($context->getArgument()->getType() instanceof FilterExpressionType) { 79 | if ($argument->value->kind === NodeKind::STRING) { 80 | $expression = $context->getArgument()->getType()->parseValue($argument->value->value); 81 | 82 | $visitor = new ExpressionNodeVisitor([ 83 | '#variable' => function (TreeNode $node) { 84 | $this->variableNameUsed[$node->getChild(0)->getValueValue()] = true; 85 | return false; 86 | }, 87 | ]); 88 | 89 | $visitor->visit($expression); 90 | } 91 | } 92 | }, 93 | ]; 94 | } 95 | 96 | public static function unusedVariableMessage($variableName, $operationName = null) 97 | { 98 | return $operationName 99 | ? sprintf('Variable "$%s" is never used in operation "%s".', $variableName, $operationName) 100 | : sprintf('Variable "$%s" is never used.', $variableName); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Classes/Validator/OrdersOnCorrectTypeRule.php: -------------------------------------------------------------------------------- 1 | [ 48 | 'enter' => function () { 49 | $this->orderUsages = []; 50 | }, 51 | 'leave' => function (OperationDefinitionNode $operation) use ($context) { 52 | $operationName = $operation->name ? $operation->name->value : null; 53 | 54 | foreach ($this->orderUsages as [$fieldName, $constraintName, $type, $node]) { 55 | if (!$context->getSchema()->hasType($constraintName)) { 56 | $context->reportError(new Error( 57 | self::unknownConstraintMessage($constraint), 58 | [$node] 59 | )); 60 | continue; 61 | } 62 | 63 | $constraintType = $context->getSchema()->getType($constraintName); 64 | 65 | if ($constraintType instanceof WrappingType) { 66 | $constraintType = $constraintType->getWrappedType(true); 67 | } 68 | 69 | if (Type::isLeafType($constraintType)) { 70 | $context->reportError(new Error( 71 | self::badConstraintMessage($constraintName), 72 | [$node] 73 | )); 74 | continue; 75 | } 76 | 77 | if ($constraintType->name !== $type->name && (!$type instanceof AbstractType 78 | || !$context->getSchema()->isPossibleType($type, $constraintType)) 79 | ) { 80 | $context->reportError(new Error( 81 | self::badConstraintMessage($constraintName), 82 | [$node] 83 | )); 84 | continue; 85 | } 86 | 87 | if (!$constraintType->hasField($fieldName)) { 88 | $context->reportError(new Error( 89 | self::unknownFieldMessage($fieldName, $constraintName), 90 | [$node] 91 | )); 92 | continue; 93 | } 94 | } 95 | }, 96 | ], 97 | NodeKind::ARGUMENT => function ($argument) use ($context) { 98 | if ($context->getArgument()->getType() instanceof OrderExpressionType 99 | && $argument->value->kind === NodeKind::STRING 100 | ) { 101 | $expression = $context->getArgument()->getType()->parseValue($argument->value->value); 102 | 103 | $visitor = new ExpressionNodeVisitor([ 104 | '#field' => function (TreeNode $node) use ($argument, $context) { 105 | $type = $context->getType() instanceof WrappingType 106 | ? $context->getType()->getWrappedType(true) : $context->getType(); 107 | 108 | $this->orderUsages[] = [ 109 | $node->getChild(0)->getChild(0)->getValueValue(), 110 | $node->getChildrenNumber() < 3 111 | ? $type->name : $node->getChild(1)->getChild(0)->getValueValue(), 112 | $type, 113 | $argument, 114 | ]; 115 | return false; 116 | }, 117 | ]); 118 | 119 | $visitor->visit($expression); 120 | } 121 | }, 122 | ]; 123 | } 124 | 125 | public static function badConstraintMessage($typeName) 126 | { 127 | return sprintf('Type "%s" can not be used in order clause constraint.', $typeName); 128 | } 129 | 130 | public static function badFieldMessage($fieldName) 131 | { 132 | return sprintf('Field "%s" can not be used in order clause.', $fieldName); 133 | } 134 | 135 | public static function unknownFieldMessage($fieldName, $typeName) 136 | { 137 | return sprintf('Type "%s" does not have any field named "%s".', $typeName, $fieldName); 138 | } 139 | 140 | public static function unknownConstraintMessage($typeName) 141 | { 142 | return sprintf('Type "%s" does not exist.', $typeName); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | TYPO3\CMS\GraphQL\: 8 | resource: '../Classes/*' 9 | 10 | TYPO3\CMS\GraphQL\Database\FilterArgumentProvider: 11 | tags: 12 | - 13 | name: event.listener 14 | identifier: 'entity-query-filter' 15 | event: TYPO3\CMS\GraphQL\Event\BeforeFieldArgumentsInitializationEvent 16 | 17 | TYPO3\CMS\GraphQL\Database\OrderArgumentProvider: 18 | tags: 19 | - 20 | name: event.listener 21 | identifier: 'entity-query-order' 22 | event: TYPO3\CMS\GraphQL\Event\BeforeFieldArgumentsInitializationEvent 23 | 24 | TYPO3\CMS\GraphQL\Database\FieldQueryHandler: 25 | tags: 26 | - 27 | name: event.listener 28 | identifier: 'entity-query-field' 29 | event: TYPO3\CMS\GraphQL\Event\BeforeValueResolvingEvent 30 | 31 | TYPO3\CMS\GraphQL\Database\FilterQueryHandler: 32 | tags: 33 | - 34 | name: event.listener 35 | identifier: 'entity-query-filter' 36 | event: TYPO3\CMS\GraphQL\Event\BeforeValueResolvingEvent 37 | 38 | TYPO3\CMS\GraphQL\Database\LocalizationQueryHandler: 39 | tags: 40 | - 41 | name: event.listener 42 | identifier: 'entity-query-localization' 43 | event: TYPO3\CMS\GraphQL\Event\BeforeValueResolvingEvent 44 | before: 'entity-query-filter, entity-query-field, entity-query-order' 45 | 46 | TYPO3\CMS\GraphQL\Database\OrderQueryHandler: 47 | tags: 48 | - 49 | name: event.listener 50 | identifier: 'entity-query-order' 51 | event: TYPO3\CMS\GraphQL\Event\BeforeValueResolvingEvent 52 | 53 | TYPO3\CMS\GraphQL\Database\OrderValueHandler: 54 | tags: 55 | - 56 | name: event.listener 57 | identifier: 'entity-query-order' 58 | event: TYPO3\CMS\GraphQL\Event\AfterValueResolvingEvent -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL 2 | 3 | [![Build](https://badgen.net/travis/typo3-initiatives/graphql)](https://travis-ci.com/TYPO3-Initiatives/graphql) 4 | [![Coverage](https://badgen.net/codacy/coverage/052bb2cd84cb461a92b172c1953989b4)](https://app.codacy.com/project/TYPO3-Initiatives/graphql/dashboard) 5 | [![Code Quality](https://badgen.net/codacy/grade/052bb2cd84cb461a92b172c1953989b4)](https://app.codacy.com/project/TYPO3-Initiatives/graphql/dashboard) 6 | 7 | This extension integrates GraphQL into TYPO3 CMS. Currently it provides an read API for managed tables. For more information about the planned features see the [draft](https://docs.google.com/document/d/1M-V9H9W_tmWZI-Be9Zo5xTZUMgwJk2dMUxOFw-waO04/). 8 | 9 | *This implementation is a proof-of-concept prototype and thus experimental development. Since not all planned features are implemented, this extension should not be used for production sites.* 10 | 11 | ## Installation 12 | 13 | Use composer to install this extension in your project: 14 | 15 | ```bash 16 | composer config repositories.cms-configuration git https://github.com/typo3-initiatives/configuration 17 | composer config repositories.cms-security git https://github.com/typo3-initiatives/security 18 | composer config repositories.cms-graphql git https://github.com/typo3-initiatives/graphql 19 | composer require typo3/cms-graphql 20 | ``` 21 | 22 | ## Usage 23 | 24 | The *entity reader* provides an easy access to the managed tables of TYPO3 CMS: 25 | 26 | ```php 27 | use TYPO3\CMS\GraphQL; 28 | 29 | $reader = new EntityReader(); 30 | $result = $reader->execute(' 31 | tt_content { 32 | uid, 33 | header, 34 | bodytext 35 | } 36 | '); 37 | ``` 38 | 39 | For more examples checkout the [functional tests](Tests/Functional/EntityReaderTest.php). 40 | 41 | ## Development 42 | 43 | Development for this extension is happening as part of the [TYPO3 persistence initiative](https://typo3.org/community/teams/typo3-development/initiatives/persistence/). 44 | -------------------------------------------------------------------------------- /Resources/Private/Grammar/Filter.pp: -------------------------------------------------------------------------------- 1 | %skip space \s 2 | 3 | %token string `(?:[^\`\\]|\\.)*\` 4 | %token float \-?(?:[0-9]+\.[0-9]+|\.[0-9]+)\b 5 | %token integer \-?(?:[0-9]|[1-9][0-9]+)\b 6 | %token boolean (?i)(true|false)\b 7 | 8 | %token null (?i)null\b 9 | 10 | %token and (?i)and\b 11 | %token or (?i)or\b 12 | %token not (?i)not\b 13 | %token in (?i)in\b 14 | %token match (?i)match\b 15 | %token on (?i)on\b 16 | 17 | %token parenthesis_ \( 18 | %token _parenthesis \) 19 | %token bracket_ \[ 20 | %token _bracket \] 21 | %token comma , 22 | %token dot \. 23 | %token dollar \$ 24 | 25 | %token greater_than_equals >= 26 | %token less_than_equals <= 27 | %token not_equals != 28 | %token equals = 29 | %token greater_than > 30 | %token less_than < 31 | 32 | %token identifier [_A-Za-z][_0-9A-Za-z]* 33 | 34 | #expression: 35 | primary() 36 | 37 | primary: 38 | secondary() ( ::or:: #or primary() )? 39 | 40 | secondary: 41 | ternary() ( ::and:: #and secondary() )? 42 | 43 | ternary: 44 | ::parenthesis_:: primary() ::_parenthesis:: 45 | | ::not:: #not primary() 46 | | field() ::equals:: #equals ( field() | variable() | scalar() ) 47 | | ( field() | variable() | scalar() ) ::equals:: #equals field() 48 | | field() ::greater_than:: #greater_than ( field() | variable() | scalar() ) 49 | | ( field() | variable() | scalar() ) ::greater_than:: #greater_than field() 50 | | field() ::less_than:: #less_than ( field() | variable() | scalar() ) 51 | | ( field() | variable() | scalar() ) ::less_than:: #less_than field() 52 | | field() ::greater_than_equals:: #greater_than_equals ( field() | variable() | scalar() ) 53 | | ( field() | variable() | scalar() ) ::greater_than_equals:: #greater_than_equals field() 54 | | field() ::less_than_equals:: #less_than_equals ( field() | variable() | scalar() ) 55 | | ( field() | variable() | scalar() ) ::less_than_equals:: #less_than_equals field() 56 | | field() ::not_equals:: #not_equals ( field() | variable() | scalar() ) 57 | | ( field() | variable() | scalar() ) ::not_equals:: #not_equals field() 58 | | field() ::in:: #in ( list() | variable() ) 59 | | field() ::match:: #match ( | variable() ) 60 | 61 | #field: 62 | path() constraint()? 63 | 64 | #path: 65 | ( ::dot:: )* 66 | 67 | #constraint: 68 | ::on:: 69 | 70 | #variable: 71 | ::dollar:: 72 | 73 | #list: 74 | ::bracket_:: ( integers() | strings() | floats() ) ::_bracket:: 75 | 76 | integers: 77 | ( ::comma:: ( | ) )* 78 | 79 | floats: 80 | ( ::comma:: ( | ) )* 81 | 82 | strings: 83 | ( ::comma:: ( | ) )* 84 | 85 | scalar: 86 | | | | | 87 | -------------------------------------------------------------------------------- /Resources/Private/Grammar/Order.pp: -------------------------------------------------------------------------------- 1 | %skip space \s 2 | 3 | %token on (?i)on\b 4 | %token order (?i)(ascending|descending)\b 5 | 6 | %token comma , 7 | %token dot \. 8 | 9 | %token identifier [_A-Za-z][_0-9A-Za-z]* 10 | 11 | #expression: 12 | field() ( ::comma:: field() )* 13 | 14 | #field: 15 | path() constraint()? 16 | 17 | #path: 18 | ( ::dot:: )* 19 | 20 | #constraint: 21 | ::on:: 22 | -------------------------------------------------------------------------------- /Tests/Functional/EntityReader/EntityReaderTestTrait.php: -------------------------------------------------------------------------------- 1 | $value) { 61 | if (is_array($value)) { 62 | $this->sortResult($result[$key]); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Tests/Functional/EntityReader/Extensions/persistence/Configuration/TCA/tx_persistence_entity.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'title' => 'Persistence Test Entity', 5 | 'label' => 'title', 6 | 'tstamp' => 'tstamp', 7 | 'crdate' => 'crdate', 8 | 'cruser_id' => 'cruser_id', 9 | 'languageField' => 'sys_language_uid', 10 | 'transOrigPointerField' => 'l10n_parent', 11 | 'transOrigDiffSourceField' => 'l10n_diffsource', 12 | 'prependAtCopy' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.prependAtCopy', 13 | 'sortby' => 'sorting', 14 | 'delete' => 'deleted', 15 | 'enablecolumns' => [ 16 | 'disabled' => 'hidden', 17 | ], 18 | 'versioningWS' => true, 19 | 'origUid' => 't3_origuid', 20 | ], 21 | 'columns' => [ 22 | 'sys_language_uid' => [ 23 | 'exclude' => true, 24 | 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 25 | 'config' => [ 26 | 'type' => 'select', 27 | 'renderType' => 'selectSingle', 28 | 'foreign_table' => 'sys_language', 29 | 'foreign_table_where' => 'ORDER BY sys_language.title', 30 | 'items' => [ 31 | ['LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages', -1], 32 | ['LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.default_value', 0], 33 | ], 34 | 'default' => 0, 35 | ], 36 | ], 37 | 'l10n_parent' => [ 38 | 'exclude' => true, 39 | 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 40 | 'config' => [ 41 | 'type' => 'select', 42 | 'renderType' => 'selectSingle', 43 | 'items' => [ 44 | ['', 0], 45 | ], 46 | 'foreign_table' => 'tx_persistence_entity', 47 | 'foreign_table_where' => ' 48 | AND tx_persistence_entity.pid=###CURRENT_PID### 49 | AND tx_persistence_entity.sys_language_uid IN (-1,0) 50 | ', 51 | 'default' => 0, 52 | ], 53 | ], 54 | 'l10n_diffsource' => [ 55 | 'config' => [ 56 | 'type' => 'passthrough', 57 | 'default' => '', 58 | ], 59 | ], 60 | 'hidden' => [ 61 | 'exclude' => true, 62 | 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hidden', 63 | 'config' => [ 64 | 'type' => 'check', 65 | 'default' => 0, 66 | ], 67 | ], 68 | 'title' => [ 69 | 'exclude' => true, 70 | 'l10n_mode' => 'prefixLangTitle', 71 | 'label' => 'Title', 72 | 'config' => [ 73 | 'type' => 'input', 74 | 'size' => 30, 75 | 'eval' => 'required', 76 | ], 77 | ], 78 | 'scalar_string' => [ 79 | 'exclude' => true, 80 | 'l10n_mode' => 'prefixLangTitle', 81 | 'label' => 'String', 82 | 'config' => [ 83 | 'type' => 'input', 84 | 'size' => 30, 85 | 'eval' => 'required', 86 | ], 87 | ], 88 | 'scalar_float' => [ 89 | 'exclude' => true, 90 | 'l10n_mode' => 'prefixLangTitle', 91 | 'label' => 'Float', 92 | 'config' => [ 93 | 'type' => 'input', 94 | 'size' => 30, 95 | 'eval' => 'required', 96 | ], 97 | ], 98 | 'scalar_integer' => [ 99 | 'exclude' => true, 100 | 'l10n_mode' => 'prefixLangTitle', 101 | 'label' => 'Integer', 102 | 'config' => [ 103 | 'type' => 'input', 104 | 'size' => 30, 105 | 'eval' => 'required', 106 | ], 107 | ], 108 | 'scalar_text' => [ 109 | 'exclude' => true, 110 | 'l10n_mode' => 'prefixLangTitle', 111 | 'label' => 'Text', 112 | 'config' => [ 113 | 'type' => 'input', 114 | 'size' => 30, 115 | 'eval' => 'required', 116 | ], 117 | ], 118 | 'relation_inline_11_file_reference' => [ 119 | 'exclude' => true, 120 | 'label' => 'File reference (inline 1:1)', 121 | 'config' => [ 122 | 'type' => 'inline', 123 | 'foreign_field' => 'uid_foreign', 124 | 'foreign_label' => 'uid_local', 125 | 'foreign_match_fields' => [ 126 | 'fieldname' => 'relation_inline_11_file_reference', 127 | ], 128 | 'foreign_selector' => 'uid_local', 129 | 'foreign_sortby' => 'sorting_foreign', 130 | 'foreign_table' => 'sys_file_reference', 131 | 'foreign_table_field' => 'tablenames', 132 | 'maxitems' => 1, 133 | ], 134 | ], 135 | 'relation_inline_1n_file_reference' => [ 136 | 'exclude' => true, 137 | 'label' => 'File reference (inline 1:n)', 138 | 'config' => [ 139 | 'type' => 'inline', 140 | 'foreign_field' => 'uid_foreign', 141 | 'foreign_label' => 'uid_local', 142 | 'foreign_match_fields' => [ 143 | 'fieldname' => 'relation_inline_1n_file_reference', 144 | ], 145 | 'foreign_selector' => 'uid_local', 146 | 'foreign_sortby' => 'sorting_foreign', 147 | 'foreign_table' => 'sys_file_reference', 148 | 'foreign_table_field' => 'tablenames', 149 | 'maxitems' => 10, 150 | ], 151 | ], 152 | 'relation_inline_1n_csv_file_reference' => [ 153 | 'exclude' => true, 154 | 'label' => 'File reference (inline 1:n csv)', 155 | 'config' => [ 156 | 'type' => 'inline', 157 | 'foreign_table' => 'sys_file_reference', 158 | 'maxitems' => 10, 159 | 'default' => '', 160 | ], 161 | ], 162 | 'relation_inline_mn_mm_content' => [ 163 | 'exclude' => true, 164 | 'label' => 'Content (inline m:n mm)', 165 | 'config' => [ 166 | 'type' => 'inline', 167 | 'foreign_table' => 'tt_content', 168 | 'foreign_table_where' => ' AND tt_content.sys_language_uid IN (-1, 0) ORDER BY tt_content.sorting ASC', 169 | 'MM' => 'tx_persistence_entity_mm', 170 | 'MM_match_fields' => [ 171 | 'fieldname' => 'relation_inline_mn_mm_content', 172 | ], 173 | 'maxitems' => 10, 174 | 'default' => '', 175 | ], 176 | ], 177 | 'relation_inline_mn_symmetric_entity' => [ 178 | 'exclude' => true, 179 | 'label' => 'Entity (inline m:n symmetric)', 180 | 'config' => [ 181 | 'type' => 'inline', 182 | 'foreign_table' => 'tx_persistence_entity_symmetric', 183 | 'foreign_field' => 'entity', 184 | 'foreign_sortby' => 'sorting_entity', 185 | 'symmetric_field' => 'peer', 186 | 'symmetric_sortby' => 'sorting_peer', 187 | 'maxitems' => 10, 188 | 'default' => '', 189 | ], 190 | ], 191 | 'relation_select_1n_page' => [ 192 | 'exclude' => true, 193 | 'label' => 'Page (select 1:n)', 194 | 'config' => [ 195 | 'type' => 'select', 196 | 'renderType' => 'selectSingle', 197 | 'foreign_table' => 'pages', 198 | 'maxitems' => 1, 199 | 'default' => 0, 200 | ], 201 | ], 202 | 'relation_select_mn_csv_category' => [ 203 | 'exclude' => true, 204 | 'label' => 'Category (select m:n csv)', 205 | 'config' => [ 206 | 'type' => 'select', 207 | 'renderType' => 'selectMultipleSideBySide', 208 | 'foreign_table' => 'sys_category', 209 | 'maxitems' => 10, 210 | 'default' => 0, 211 | ], 212 | ], 213 | 'relation_select_mn_mm_content' => [ 214 | 'exclude' => true, 215 | 'label' => 'Content (select m:n mm)', 216 | 'config' => [ 217 | 'type' => 'select', 218 | 'renderType' => 'selectMultipleSideBySide', 219 | 'foreign_table' => 'tt_content', 220 | 'foreign_table_where' => ' AND tt_content.sys_language_uid IN (-1, 0) ORDER BY tt_content.sorting ASC', 221 | 'MM' => 'tx_persistence_entity_mm', 222 | 'MM_match_fields' => [ 223 | 'fieldname' => 'relation_select_mn_mm_content', 224 | ], 225 | 'MM_table_where' => ' AND further = 1', 226 | 'size' => 6, 227 | 'maxitems' => 10, 228 | 'default' => 0, 229 | ], 230 | ], 231 | 'relation_group_1n_content_page' => [ 232 | 'exclude' => true, 233 | 'label' => 'Content, Page (group 1:n)', 234 | 'config' => [ 235 | 'type' => 'group', 236 | 'internal_type' => 'db', 237 | 'allowed' => 'tt_content,pages', 238 | 'maxitems' => 1, 239 | 'autoSizeMax' => 10, 240 | 'default' => '', 241 | ], 242 | ], 243 | 'relation_group_mn_csv_content_page' => [ 244 | 'exclude' => true, 245 | 'label' => 'Content, Page (group m:n csv)', 246 | 'config' => [ 247 | 'type' => 'group', 248 | 'internal_type' => 'db', 249 | 'allowed' => 'tt_content,pages', 250 | 'maxitems' => 10, 251 | 'autoSizeMax' => 10, 252 | 'default' => '', 253 | ], 254 | ], 255 | 'relation_group_mn_csv_any' => [ 256 | 'exclude' => true, 257 | 'label' => 'Content, Page (group m:n csv)', 258 | 'config' => [ 259 | 'type' => 'group', 260 | 'internal_type' => 'db', 261 | 'allowed' => '*', 262 | 'maxitems' => 10, 263 | 'autoSizeMax' => 10, 264 | 'default' => '', 265 | ], 266 | ], 267 | 'relation_group_mn_mm_content_page' => [ 268 | 'exclude' => true, 269 | 'label' => 'Content, Page (group m:n mm)', 270 | 'config' => [ 271 | 'type' => 'group', 272 | 'internal_type' => 'db', 273 | 'allowed' => 'tt_content,pages', 274 | 'MM' => 'tx_persistence_entity_mm', 275 | 'MM_match_fields' => [ 276 | 'fieldname' => 'relation_group_mn_mm_content_page', 277 | ], 278 | 'maxitems' => 10, 279 | 'autoSizeMax' => 10, 280 | 'default' => '', 281 | ], 282 | ], 283 | 'relation_group_mn_mm_any' => [ 284 | 'exclude' => true, 285 | 'label' => 'Content, Page (group m:n mm)', 286 | 'config' => [ 287 | 'type' => 'group', 288 | 'internal_type' => 'db', 289 | 'allowed' => '*', 290 | 'MM' => 'tx_persistence_entity_mm', 291 | 'MM_match_fields' => [ 292 | 'fieldname' => 'relation_group_mn_mm_any', 293 | ], 294 | 'maxitems' => 10, 295 | 'autoSizeMax' => 10, 296 | 'default' => '', 297 | ], 298 | ], 299 | ], 300 | 'types' => [ 301 | '0' => [ 302 | 'showitem' => ' 303 | --div--;LLL:EXT:irre_tutorial/Resources/Private/Language/locallang_db.xml:tabs.general, 304 | title, 305 | scalar_string, 306 | scalar_float, 307 | scalar_integer, 308 | scalar_text, 309 | relation_inline_11_file, 310 | relation_inline_1n_file, 311 | relation_inline_1n_csv_file, 312 | relation_inline_mn_mm_content, 313 | relation_inline_mn_symmetric_entity, 314 | relation_select_1n_page, 315 | relation_select_mn_csv_category, 316 | relation_select_mn_mm_content, 317 | relation_group_1n_content_page, 318 | relation_group_mn_csv_content_page, 319 | relation_group_mn_mm_content_page 320 | --div--;LLL:EXT:irre_tutorial/Resources/Private/Language/locallang_db.xml:tabs.visibility, 321 | sys_language_uid, 322 | l10n_parent, 323 | l10n_diffsource, 324 | hidden 325 | ', 326 | ], 327 | ], 328 | 'palettes' => [ 329 | '1' => [ 330 | 'showitem' => '', 331 | ], 332 | ], 333 | ]; 334 | -------------------------------------------------------------------------------- /Tests/Functional/EntityReader/Extensions/persistence/Configuration/TCA/tx_persistence_entity_symmetric.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'title' => 'Persistence Symmetric Test Entity', 5 | 'label' => 'uid', 6 | 'tstamp' => 'tstamp', 7 | 'crdate' => 'crdate', 8 | 'cruser_id' => 'cruser_id', 9 | 'languageField' => 'sys_language_uid', 10 | 'transOrigPointerField' => 'l10n_parent', 11 | 'transOrigDiffSourceField' => 'l10n_diffsource', 12 | 'delete' => 'deleted', 13 | 'enablecolumns' => [ 14 | 'disabled' => 'hidden', 15 | ], 16 | 'versioningWS' => true, 17 | 'origUid' => 't3_origuid', 18 | ], 19 | 'interface' => [ 20 | 'showRecordFieldList' => 'sys_language_uid,l10n_parent,l10n_diffsource,hidden,title,entity,peer', 21 | ], 22 | 'columns' => [ 23 | 'sys_language_uid' => [ 24 | 'exclude' => true, 25 | 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.language', 26 | 'config' => [ 27 | 'type' => 'select', 28 | 'renderType' => 'selectSingle', 29 | 'foreign_table' => 'sys_language', 30 | 'foreign_table_where' => 'ORDER BY sys_language.title', 31 | 'items' => [ 32 | ['LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages', -1], 33 | ['LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.default_value', 0], 34 | ], 35 | 'default' => 0, 36 | ], 37 | ], 38 | 'l10n_parent' => [ 39 | 'displayCond' => 'FIELD:sys_language_uid:>:0', 40 | 'exclude' => true, 41 | 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.l18n_parent', 42 | 'config' => [ 43 | 'type' => 'select', 44 | 'renderType' => 'selectSingle', 45 | 'items' => [ 46 | ['', 0], 47 | ], 48 | 'foreign_table' => 'tx_persistence_entity_symmetric', 49 | 'foreign_table_where' => ' 50 | AND tx_persistence_entity_symmetric.pid=###CURRENT_PID### 51 | AND tx_persistence_entity_symmetric.sys_language_uid IN (-1,0) 52 | ', 53 | 'default' => 0, 54 | ], 55 | ], 56 | 'l10n_diffsource' => [ 57 | 'config' => [ 58 | 'type' => 'passthrough', 59 | 'default' => '', 60 | ], 61 | ], 62 | 'hidden' => [ 63 | 'exclude' => true, 64 | 'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hidden', 65 | 'config' => [ 66 | 'type' => 'check', 67 | 'default' => 0, 68 | ], 69 | ], 70 | 'entity' => [ 71 | 'label' => 'Entity', 72 | 'config' => [ 73 | 'type' => 'select', 74 | 'renderType' => 'selectSingle', 75 | 'foreign_table' => 'tx_persistence_entity', 76 | 'maxitems' => 1, 77 | 'default' => 0, 78 | ], 79 | ], 80 | 'peer' => [ 81 | 'label' => 'Peer', 82 | 'config' => [ 83 | 'type' => 'select', 84 | 'renderType' => 'selectSingle', 85 | 'foreign_table' => 'tx_persistence_entity', 86 | 'maxitems' => 1, 87 | 'default' => 0, 88 | ], 89 | ], 90 | 'sorting_entity' => [ 91 | 'config' => [ 92 | 'type' => 'passthrough', 93 | ], 94 | ], 95 | 'sorting_peer' => [ 96 | 'config' => [ 97 | 'type' => 'passthrough', 98 | ], 99 | ], 100 | ], 101 | 'types' => [ 102 | '0' => ['showitem' => ' 103 | --div--;LLL:EXT:irre_tutorial/Resources/Private/Language/locallang_db.xml:tabs.general, 104 | title, 105 | entity, 106 | peer, 107 | --div--;LLL:EXT:irre_tutorial/Resources/Private/Language/locallang_db.xml:tabs.visibility, 108 | sys_language_uid, 109 | l18n_parent, 110 | l18n_diffsource, 111 | hidden 112 | ', 113 | ], 114 | ], 115 | 'palettes' => [ 116 | '1' => ['showitem' => ''], 117 | ], 118 | ]; 119 | -------------------------------------------------------------------------------- /Tests/Functional/EntityReader/Extensions/persistence/ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Persistence', 4 | 'description' => 'Persistence', 5 | 'category' => 'example', 6 | 'version' => '9.5.0', 7 | 'state' => 'alpha', 8 | 'uploadfolder' => 0, 9 | 'createDirs' => '', 10 | 'clearCacheOnLoad' => 0, 11 | 'author' => 'Artus Kolanowski', 12 | 'author_email' => 'kolanowski@piccobello.com', 13 | 'author_company' => '', 14 | 'constraints' => [ 15 | 'depends' => [ 16 | 'typo3' => '9.5.0', 17 | ], 18 | 'conflicts' => [], 19 | 'suggests' => [], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /Tests/Functional/EntityReader/Extensions/persistence/ext_tables.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 128 5 | 0 6 | 256 7 | Page 1 8 | 9 | 10 | 129 11 | 128 12 | 512 13 | Page 1.1 14 | 15 | 16 | 130 17 | 128 18 | 768 19 | Page 1.2 20 | 21 | 22 | 512 23 | 128 24 | 512 25 |
Content 1
26 | Lorem ipsum dolor... 27 |
28 | 29 | 513 30 | 128 31 | 128 32 |
Content 2
33 | 34 |
35 | 36 | 514 37 | 128 38 | 256 39 |
Content 3
40 | Lorem ipsum dolor... 41 |
42 | 43 | 1 44 | 0 45 | Default 46 | Local 47 | 48 | 49 | 4 50 | 0 51 | 2 52 | 1 53 | /image_1.jpg 54 | jpeg 55 | image/jpeg 56 | image_1.jpg 57 | 58 | 59 | 5 60 | 0 61 | 2 62 | 1 63 | /image_2.jpg 64 | jpeg 65 | image/jpeg 66 | image_2.jpg 67 | 68 | 69 | 6 70 | 0 71 | 2 72 | 1 73 | /image_3.jpg 74 | jpeg 75 | image/jpeg 76 | image_3.jpg 77 | 78 | 79 | 256 80 | 88 81 | 4 82 | 1024 83 | tx_persistence_entity 84 | relation_inline_11_file_reference 85 | 256 86 | sys_file 87 | File reference 1 88 | 89 | 90 | 257 91 | 88 92 | 5 93 | 1024 94 | tx_persistence_entity 95 | relation_inline_1n_file_reference 96 | 256 97 | sys_file 98 | File reference 2 99 | 100 | 101 | 258 102 | 88 103 | 6 104 | 1024 105 | tx_persistence_entity 106 | relation_inline_1n_file_reference 107 | 512 108 | sys_file 109 | File reference 3 110 | 111 | 112 | 259 113 | 88 114 | 4 115 | 1024 116 | 256 117 | sys_file 118 | File reference 4 119 | 120 | 121 | 260 122 | 88 123 | 5 124 | 1024 125 | 256 126 | sys_file 127 | File reference 5 128 | 129 | 130 | 32 131 | 0 132 | 256 133 | 0 134 | Category 1 135 | 136 | 137 | 33 138 | 0 139 | 256 140 | 32 141 | Category 1.1 142 | 143 | 144 | 34 145 | 0 146 | 512 147 | 32 148 | Category 1.2 149 | 150 | 151 | 1024 152 | 128 153 | 768 154 | Entity 1 155 | String 156 | 0 157 | 0 158 | 159 | 259,260 160 | 161 | 162 | 1025 163 | 128 164 | 512 165 | Entity 2 166 | 3.1415 167 | 0 168 | 169 | 129 170 | 33,34 171 | 172 | 173 | 1026 174 | 128 175 | 1024 176 | Entity 3 177 | 0 178 | 1 179 | 180 | pages_130 181 | 513,pages_129,514,pages_130,512,pages_128 182 | 183 | 184 | 1027 185 | 128 186 | 256 187 | Entity 4 188 | String 189 | -3.1415 190 | 1 191 | 192 | 193 | 1028 194 | 128 195 | 1280 196 | Entity 5 197 | 0 198 | 0 199 | Text 200 | 201 | 202 | 1024 203 | 512 204 | tt_content 205 | 128 206 | 512 207 | relation_inline_mn_mm_content 208 | 0 209 | 210 | 211 | 1024 212 | 513 213 | tt_content 214 | 256 215 | 256 216 | relation_inline_mn_mm_content 217 | 0 218 | 219 | 220 | 1024 221 | 514 222 | tt_content 223 | 512 224 | 256 225 | relation_inline_mn_mm_content 226 | 0 227 | 228 | 229 | 1025 230 | 512 231 | tt_content 232 | 128 233 | 512 234 | relation_select_mn_mm_content 235 | 0 236 | 237 | 238 | 1025 239 | 513 240 | tt_content 241 | 256 242 | 256 243 | relation_select_mn_mm_content 244 | 0 245 | 246 | 247 | 1025 248 | 514 249 | tt_content 250 | 512 251 | 256 252 | relation_select_mn_mm_content 253 | 0 254 | 255 | 256 | 1026 257 | 130 258 | pages 259 | 128 260 | 512 261 | relation_group_mn_mm_content_page 262 | 0 263 | 264 | 265 | 1026 266 | 514 267 | tt_content 268 | 512 269 | 768 270 | relation_group_mn_mm_content_page 271 | 0 272 | 273 | 274 | 1026 275 | 128 276 | pages 277 | 256 278 | 128 279 | relation_group_mn_mm_content_page 280 | 0 281 | 282 | 283 | 1026 284 | 512 285 | tt_content 286 | 768 287 | 256 288 | relation_group_mn_mm_content_page 289 | 0 290 | 291 | 292 | 1026 293 | 129 294 | pages 295 | 1024 296 | 1024 297 | relation_group_mn_mm_content_page 298 | 0 299 | 300 | 301 | 1024 302 | 1025 303 | 256 304 | 512 305 | 306 | 307 | 1024 308 | 1026 309 | 512 310 | 256 311 | 312 |
-------------------------------------------------------------------------------- /Tests/Functional/EntityReader/Fixtures/live-localization.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 0 6 | Dansk 7 | dk 8 | 9 | 10 | 2 11 | 0 12 | Deutsch 13 | de 14 | 15 | 16 | 131 17 | 0 18 | 1024 19 | 2 20 | 128 21 | Seite 1 22 | 23 | 24 | 132 25 | 128 26 | 1280 27 | 2 28 | 129 29 | Seite 1.1 30 | 31 | 32 | 133 33 | 0 34 | 1536 35 | 2 36 | Seite 2 37 | 38 | 39 | 134 40 | 0 41 | 1792 42 | 2 43 | Seite 3 44 | 45 | 46 | 261 47 | 88 48 | 2 49 | 4 50 | 1029 51 | tx_persistence_entity 52 | relation_inline_11_file_reference 53 | 256 54 | sys_file 55 | Dateiverweis 1 56 | 57 | 58 | 262 59 | 88 60 | 2 61 | 5 62 | 1029 63 | tx_persistence_entity 64 | relation_inline_1n_file_reference 65 | 256 66 | sys_file 67 | Dateiverweis 2 68 | 69 | 70 | 263 71 | 88 72 | 2 73 | 6 74 | 1029 75 | tx_persistence_entity 76 | relation_inline_1n_file_reference 77 | 512 78 | sys_file 79 | Dateiverweis 3 80 | 81 | 82 | 264 83 | 88 84 | 2 85 | 4 86 | 1029 87 | 256 88 | sys_file 89 | Dateiverweis 4 90 | 91 | 92 | 265 93 | 88 94 | 2 95 | 5 96 | 1029 97 | 256 98 | sys_file 99 | Dateiverweis 5 100 | 101 | 102 | 1029 103 | 128 104 | 2 105 | 1024 106 | Entität 1 107 | Zeichenkette 108 | 264,265 109 | 110 | 111 | 1030 112 | 128 113 | 2 114 | 1025 115 | Entität 2 116 | 3.14 117 | 131 118 | 119 | 120 | 1031 121 | 128 122 | 2 123 | 1026 124 | Entität 3 125 | 0 126 | 1 127 | 128 | pages_133,pages_134 129 | 130 | 131 | 1032 132 | 128 133 | 2 134 | 1027 135 | Entität 4 136 | Zeichenkette 137 | -3.14 138 | 139 | 140 | 1033 141 | 128 142 | 2 143 | 1028 144 | Entität 5 145 | 146 | -------------------------------------------------------------------------------- /Tests/Functional/EntityReader/LiveLocalizationTest.php: -------------------------------------------------------------------------------- 1 | importDataSet(__DIR__ . '/Fixtures/live-default.xml'); 43 | $this->importDataSet(__DIR__ . '/Fixtures/live-localization.xml'); 44 | } 45 | 46 | public function scalarPropertyQueryProvider() 47 | { 48 | return [ 49 | [ 50 | '{ 51 | pages { 52 | uid 53 | title 54 | } 55 | }', 56 | [ 57 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_OFF, []), 58 | ], 59 | [ 60 | 'data' => [ 61 | 'pages' => [ 62 | ['uid' => '133', 'title' => 'Seite 2'], 63 | ['uid' => '134', 'title' => 'Seite 3'], 64 | ], 65 | ], 66 | ], 67 | ], 68 | [ 69 | '{ 70 | pages { 71 | uid 72 | title 73 | } 74 | }', 75 | [ 76 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_MIXED, []), 77 | ], 78 | [ 79 | 'data' => [ 80 | 'pages' => [ 81 | ['uid' => '130', 'title' => 'Page 1.2'], 82 | ['uid' => '131', 'title' => 'Seite 1'], 83 | ['uid' => '132', 'title' => 'Seite 1.1'], 84 | ], 85 | ], 86 | ], 87 | ], 88 | [ 89 | '{ 90 | pages { 91 | uid 92 | title 93 | } 94 | }', 95 | [ 96 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_ON, []), 97 | ], 98 | [ 99 | 'data' => [ 100 | 'pages' => [ 101 | ['uid' => '131', 'title' => 'Seite 1'], 102 | ['uid' => '132', 'title' => 'Seite 1.1'], 103 | ], 104 | ], 105 | ], 106 | ], 107 | [ 108 | '{ 109 | pages { 110 | uid 111 | title 112 | } 113 | }', 114 | [ 115 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_ON_WITH_FLOATING, []), 116 | ], 117 | [ 118 | 'data' => [ 119 | 'pages' => [ 120 | ['uid' => '131', 'title' => 'Seite 1'], 121 | ['uid' => '132', 'title' => 'Seite 1.1'], 122 | ['uid' => '133', 'title' => 'Seite 2'], 123 | ['uid' => '134', 'title' => 'Seite 3'], 124 | ], 125 | ], 126 | ], 127 | ], 128 | ]; 129 | } 130 | 131 | /** 132 | * @test 133 | * @dataProvider scalarPropertyQueryProvider 134 | */ 135 | public function readScalarProperty(string $query, array $aspects, array $expected) 136 | { 137 | $reader = new EntityReader(); 138 | $result = $reader->execute($query, [], new Context($aspects)); 139 | 140 | $this->sortResult($expected); 141 | $this->sortResult($result); 142 | 143 | $this->assertEquals($expected, $result); 144 | } 145 | 146 | public function relationPropertyQueryProvider() 147 | { 148 | return [ 149 | [ 150 | '{ 151 | tx_persistence_entity { 152 | uid 153 | title 154 | relation_inline_1n_file_reference { 155 | uid 156 | title 157 | uid_local { 158 | identifier 159 | } 160 | } 161 | } 162 | }', 163 | [ 164 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_ON, []), 165 | ], 166 | [ 167 | 'data' => [ 168 | 'tx_persistence_entity' => [ 169 | [ 170 | 'uid' => '1029', 171 | 'title' => 'Entität 1', 172 | 'relation_inline_1n_file_reference' => [ 173 | [ 174 | 'uid' => '262', 175 | 'title' => 'Dateiverweis 2', 176 | 'uid_local' => [ 177 | 'identifier' => '/image_2.jpg', 178 | ], 179 | ], 180 | [ 181 | 'uid' => '263', 182 | 'title' => 'Dateiverweis 3', 183 | 'uid_local' => [ 184 | 'identifier' => '/image_3.jpg', 185 | ], 186 | ], 187 | ], 188 | ], 189 | [ 190 | 'uid' => '1030', 191 | 'title' => 'Entität 2', 192 | 'relation_inline_1n_file_reference' => [], 193 | ], 194 | [ 195 | 'uid' => '1031', 196 | 'title' => 'Entität 3', 197 | 'relation_inline_1n_file_reference' => [], 198 | ], 199 | [ 200 | 'uid' => '1032', 201 | 'title' => 'Entität 4', 202 | 'relation_inline_1n_file_reference' => [], 203 | ], 204 | [ 205 | 'uid' => '1033', 206 | 'title' => 'Entität 5', 207 | 'relation_inline_1n_file_reference' => [], 208 | ], 209 | ], 210 | ], 211 | ], 212 | ], 213 | ]; 214 | } 215 | 216 | /** 217 | * @test 218 | * @dataProvider relationPropertyQueryProvider 219 | */ 220 | public function readRelationProperty(string $query, array $aspects, array $expected) 221 | { 222 | $reader = new EntityReader(); 223 | $result = $reader->execute($query, [], new Context($aspects)); 224 | 225 | $this->sortResult($expected); 226 | $this->sortResult($result); 227 | 228 | $this->assertEquals($expected, $result); 229 | } 230 | 231 | public function orderResultQueryProvider() 232 | { 233 | return [ 234 | ]; 235 | } 236 | 237 | /** 238 | * @test 239 | * @dataProvider orderResultQueryProvider 240 | */ 241 | public function orderResult(string $query, array $expected) 242 | { 243 | $reader = new EntityReader(); 244 | $result = $reader->execute($query); 245 | 246 | $this->assertEquals($expected, $result); 247 | } 248 | 249 | public function filterRestrictedQueryProvider() 250 | { 251 | return [ 252 | [ 253 | '{ 254 | pages(filter: "title = `Seite 2`") { 255 | title 256 | } 257 | }', 258 | [], 259 | [ 260 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_OFF, []), 261 | ], 262 | [ 263 | 'data' => [ 264 | 'pages' => [ 265 | ['title' => 'Seite 2'], 266 | ], 267 | ], 268 | ], 269 | ], 270 | [ 271 | '{ 272 | pages(filter: "title in [`Seite 1.1`, `Page 1.2`]") { 273 | title 274 | } 275 | }', 276 | [], 277 | [ 278 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_MIXED, []), 279 | ], 280 | [ 281 | 'data' => [ 282 | 'pages' => [ 283 | ['title' => 'Page 1.2'], 284 | ['title' => 'Seite 1.1'], 285 | ], 286 | ], 287 | ], 288 | ], 289 | [ 290 | '{ 291 | pages(filter: "title != `Seite 1.1`") { 292 | title 293 | } 294 | }', 295 | [], 296 | [ 297 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_ON, []), 298 | ], 299 | [ 300 | 'data' => [ 301 | 'pages' => [ 302 | ['title' => 'Seite 1'], 303 | ], 304 | ], 305 | ], 306 | ], 307 | [ 308 | '{ 309 | pages(filter: "`Seite 1` = title or `Seite 3` = title") { 310 | title 311 | } 312 | }', 313 | [], 314 | [ 315 | 'language' => new LanguageAspect(2, null, LanguageAspect::OVERLAYS_ON_WITH_FLOATING, []), 316 | ], 317 | [ 318 | 'data' => [ 319 | 'pages' => [ 320 | ['title' => 'Seite 1'], 321 | ['title' => 'Seite 3'], 322 | ], 323 | ], 324 | ], 325 | ], 326 | ]; 327 | } 328 | 329 | /** 330 | * @test 331 | * @dataProvider filterRestrictedQueryProvider 332 | */ 333 | public function readFilterRestricted(string $query, array $bindings, array $aspects, array $expected) 334 | { 335 | $reader = new EntityReader(); 336 | $result = $reader->execute($query, $bindings, new Context($aspects)); 337 | 338 | $this->sortResult($expected); 339 | $this->sortResult($result); 340 | 341 | $this->assertEquals($expected, $result); 342 | } 343 | 344 | public function unsupportedQueryProvider() 345 | { 346 | return [ 347 | ]; 348 | } 349 | 350 | /** 351 | * @test 352 | * @dataProvider unsupportedQueryProvider 353 | */ 354 | public function throwUnsupported(string $query, string $exceptionClass, int $exceptionCode) 355 | { 356 | $this->expectException($exceptionClass); 357 | $this->expectExceptionCode($exceptionCode); 358 | 359 | $reader = new EntityReader(); 360 | $reader->execute($query); 361 | } 362 | 363 | public function invalidQueryProvider() 364 | { 365 | return [ 366 | ]; 367 | } 368 | 369 | /** 370 | * @test 371 | * @dataProvider invalidQueryProvider 372 | */ 373 | public function throwInvalid(string $query, string $exceptionClass, int $exceptionCode) 374 | { 375 | $this->expectException($exceptionClass); 376 | $this->expectExceptionCode($exceptionCode); 377 | 378 | $reader = new EntityReader(); 379 | $reader->execute($query); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /codesize.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Build/* 25 | Tests/* 26 | ext_emconf.php 27 | ext_localconf.php 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typo3/cms-graphql", 3 | "type": "typo3-cms-framework", 4 | "description": "The GraphQL library of TYPO3.", 5 | "homepage": "https://typo3.org", 6 | "license": ["GPL-2.0-or-later"], 7 | "authors": [{ 8 | "name": "TYPO3 Core Team", 9 | "email": "typo3cms@typo3.org", 10 | "role": "Developer" 11 | }], 12 | "config": { 13 | "vendor-dir": ".build/vendor", 14 | "bin-dir": ".build/bin", 15 | "sort-packages": true 16 | }, 17 | "repositories": [ 18 | { 19 | "type": "git", 20 | "url": "https://github.com/typo3-initiatives/configuration" 21 | } 22 | ], 23 | "require": { 24 | "typo3/cms-core": "10.1.*@dev", 25 | "typo3/cms-configuration": "10.1.*@dev", 26 | "hoa/compiler": "^3.0", 27 | "webmozart/assert": "^1.0", 28 | "webonyx/graphql-php": "^0.13" 29 | }, 30 | "require-dev": { 31 | "codacy/coverage": "^1.4", 32 | "slevomat/coding-standard": "^4.8", 33 | "typo3/testing-framework": "^5.0" 34 | }, 35 | "conflict": { 36 | "typo3/cms": "*" 37 | }, 38 | "replace": { 39 | "graphql": "self.version" 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-master": "10.1.x-dev" 44 | }, 45 | "typo3/cms": { 46 | "extension-key": "graphql", 47 | "app-dir": ".build", 48 | "web-dir": ".build/public", 49 | "Package": { 50 | "partOfFactoryDefault": true 51 | } 52 | } 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "TYPO3\\CMS\\GraphQL\\Build\\": "Build/", 57 | "TYPO3\\CMS\\GraphQL\\": "Classes/" 58 | } 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "TYPO3\\CMS\\GraphQL\\Tests\\Unit\\": "Tests/Unit/", 63 | "TYPO3\\CMS\\GraphQL\\Tests\\Functional\\": "Tests/Functional/" 64 | } 65 | }, 66 | "minimum-stability": "dev", 67 | "prefer-stable": true, 68 | "scripts": { 69 | "build:package:link": [ 70 | "TYPO3\\CMS\\GraphQL\\Build\\Composer\\ScriptHelper::linkPackage" 71 | ], 72 | "test:php:unit": [ 73 | "@php .build/bin/phpunit -c Build/UnitTests.xml" 74 | ], 75 | "test:php:functional": [ 76 | "@php .build/bin/phpunit -c Build/FunctionalTests.xml" 77 | ], 78 | "test": [ 79 | "@test:php:unit", 80 | "@test:php:functional" 81 | ] 82 | } 83 | } -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'TYPO3 GraphQL', 4 | 'description' => 'The GraphQL library of TYPO3.', 5 | 'category' => 'be', 6 | 'author' => 'TYPO3 Core Team', 7 | 'author_email' => 'typo3cms@typo3.org', 8 | 'author_company' => '', 9 | 'state' => 'alpha', 10 | 'createDirs' => '', 11 | 'clearCacheOnLoad' => 0, 12 | 'version' => '10.1.0', 13 | 'constraints' => [ 14 | 'depends' => [ 15 | 'typo3' => '10.1.0' 16 | ], 17 | 'conflicts' => [], 18 | 'suggests' => [], 19 | ], 20 | ]; 21 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class, 6 | 'backend' => \TYPO3\CMS\Core\Cache\Backend\TransientMemoryBackend::class, 7 | 'options' => [], 8 | 'groups' => [], 9 | ]; 10 | 11 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['gql'] = [ 12 | 'resolver' => [ 13 | \TYPO3\CMS\GraphQL\Database\ActiveRelationshipResolver::class, 14 | \TYPO3\CMS\GraphQL\Database\EntityResolver::class, 15 | \TYPO3\CMS\GraphQL\Database\PassiveManyToManyRelationshipResolver::class, 16 | \TYPO3\CMS\GraphQL\Database\PassiveOneToManyRelationshipResolver::class, 17 | ], 18 | ]; -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | The PSR-2 coding standard. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 0 46 | 47 | 48 | 0 49 | 50 | 51 | 0 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 | 98 | 99 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 0 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 141 | 142 | 143 | 144 | 145 | 147 | 148 | 149 | 0 150 | 151 | 152 | 0 153 | 154 | 155 | 156 | 157 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 0 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | Configuration/TCA/*.php 219 | ext_*.php 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | --------------------------------------------------------------------------------