├── 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 | [](https://travis-ci.com/TYPO3-Initiatives/graphql)
4 | [](https://app.codacy.com/project/TYPO3-Initiatives/graphql/dashboard)
5 | [](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 |
26 | Lorem ipsum dolor...
27 |
28 |
29 | 513
30 | 128
31 | 128
32 |
33 |
34 |
35 |
36 | 514
37 | 128
38 | 256
39 |
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 |
--------------------------------------------------------------------------------