|
| 1 | +<?php declare(strict_types=1); |
| 2 | + |
| 3 | +namespace Fazland\ApiPlatformBundle\QueryLanguage\Processor\Doctrine\PhpCr; |
| 4 | + |
| 5 | +use Doctrine\ODM\PHPCR\DocumentManagerInterface; |
| 6 | +use Doctrine\ODM\PHPCR\Mapping\ClassMetadata; |
| 7 | +use Doctrine\ODM\PHPCR\Query\Builder\AbstractNode; |
| 8 | +use Doctrine\ODM\PHPCR\Query\Builder\From; |
| 9 | +use Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder; |
| 10 | +use Doctrine\ODM\PHPCR\Query\Builder\WhereAnd; |
| 11 | +use Fazland\ApiPlatformBundle\QueryLanguage\Exception\Doctrine\FieldNotFoundException; |
| 12 | +use Fazland\ApiPlatformBundle\QueryLanguage\Expression\ExpressionInterface; |
| 13 | +use Fazland\ApiPlatformBundle\QueryLanguage\Processor\ColumnInterface; |
| 14 | +use Fazland\ApiPlatformBundle\QueryLanguage\Walker\PhpCr\NodeWalker; |
| 15 | + |
| 16 | +/** |
| 17 | + * @internal |
| 18 | + */ |
| 19 | +class Column implements ColumnInterface |
| 20 | +{ |
| 21 | + /** |
| 22 | + * @var string |
| 23 | + */ |
| 24 | + private $rootAlias; |
| 25 | + |
| 26 | + /** |
| 27 | + * @var string[] |
| 28 | + */ |
| 29 | + private $mapping; |
| 30 | + |
| 31 | + /** |
| 32 | + * @var string |
| 33 | + */ |
| 34 | + public $fieldName; |
| 35 | + |
| 36 | + /** |
| 37 | + * @var string |
| 38 | + */ |
| 39 | + private $fieldType; |
| 40 | + |
| 41 | + /** |
| 42 | + * @var string|callable|null |
| 43 | + */ |
| 44 | + public $validationWalker; |
| 45 | + |
| 46 | + /** |
| 47 | + * @var string|callable|null |
| 48 | + */ |
| 49 | + public $customWalker; |
| 50 | + |
| 51 | + /** |
| 52 | + * @var array |
| 53 | + */ |
| 54 | + private $associations; |
| 55 | + |
| 56 | + /** |
| 57 | + * @var DocumentManagerInterface |
| 58 | + */ |
| 59 | + private $documentManager; |
| 60 | + |
| 61 | + public function __construct( |
| 62 | + string $fieldName, |
| 63 | + string $rootAlias, |
| 64 | + ClassMetadata $rootEntity, |
| 65 | + DocumentManagerInterface $documentManager |
| 66 | + ) { |
| 67 | + $this->fieldName = $fieldName; |
| 68 | + $this->rootAlias = $rootAlias; |
| 69 | + |
| 70 | + [$rootField, $rest] = MappingHelper::processFieldName($rootEntity, $fieldName); |
| 71 | + $this->mapping = $rootField; |
| 72 | + |
| 73 | + $this->fieldType = 'string'; |
| 74 | + if (isset($this->mapping['type']) && ! isset($this->mapping['targetDocument'])) { |
| 75 | + $this->fieldType = $this->mapping['type']; |
| 76 | + } |
| 77 | + |
| 78 | + $this->associations = []; |
| 79 | + if (null !== $rest) { |
| 80 | + $this->processAssociations($documentManager, $rest); |
| 81 | + } |
| 82 | + |
| 83 | + $this->documentManager = $documentManager; |
| 84 | + } |
| 85 | + |
| 86 | + /** |
| 87 | + * {@inheritdoc} |
| 88 | + */ |
| 89 | + public function addCondition($queryBuilder, ExpressionInterface $expression): void |
| 90 | + { |
| 91 | + if ($this->isAssociation()) { |
| 92 | + $this->addAssociationCondition($queryBuilder, $expression); |
| 93 | + } else { |
| 94 | + $this->addWhereCondition($queryBuilder, $expression); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + /** |
| 99 | + * {@inheritdoc} |
| 100 | + */ |
| 101 | + public function getValidationWalker() |
| 102 | + { |
| 103 | + return $this->validationWalker; |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * Gets the mapping field name. |
| 108 | + * |
| 109 | + * @return string |
| 110 | + */ |
| 111 | + public function getMappingFieldName(): string |
| 112 | + { |
| 113 | + return $this->mapping['fieldName']; |
| 114 | + } |
| 115 | + |
| 116 | + /** |
| 117 | + * Whether this column navigates into associations. |
| 118 | + * |
| 119 | + * @return bool |
| 120 | + */ |
| 121 | + public function isAssociation(): bool |
| 122 | + { |
| 123 | + return isset($this->mapping['targetDocument']) || 0 < \count($this->associations); |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * Processes an association column and attaches the conditions to the query builder. |
| 128 | + * |
| 129 | + * @param QueryBuilder $queryBuilder |
| 130 | + * @param ExpressionInterface $expression |
| 131 | + */ |
| 132 | + private function addAssociationCondition(QueryBuilder $queryBuilder, ExpressionInterface $expression): void |
| 133 | + { |
| 134 | + $alias = $this->getMappingFieldName(); |
| 135 | + $walker = $this->customWalker; |
| 136 | + |
| 137 | + $targetDocument = $this->documentManager->getClassMetadata($this->getTargetDocument()); |
| 138 | + if (null === $targetDocument->uuidFieldName) { |
| 139 | + throw new \RuntimeException('Uuid field must be declared to build association conditions'); |
| 140 | + } |
| 141 | + |
| 142 | + $queryBuilder->addJoinInner() |
| 143 | + ->right()->document($this->getTargetDocument(), $alias)->end() |
| 144 | + ->condition()->equi($this->rootAlias.'.'.$alias, $alias.'.'.$targetDocument->uuidFieldName)->end() |
| 145 | + ->end(); |
| 146 | + |
| 147 | + $currentFieldName = $alias; |
| 148 | + $currentAlias = $alias; |
| 149 | + foreach ($this->associations as $association) { |
| 150 | + if (isset($association['targetDocument'])) { |
| 151 | + /** @var From $from */ |
| 152 | + $from = $queryBuilder->getChildOfType(AbstractNode::NT_FROM); |
| 153 | + $from->joinInner() |
| 154 | + ->left()->document($association['sourceDocument'], $currentAlias)->end() |
| 155 | + ->right()->document($association['targetDocument'], $currentFieldName = $association['fieldName'])->end() |
| 156 | + ->end(); |
| 157 | + |
| 158 | + $currentAlias = $association['fieldName']; |
| 159 | + } else { |
| 160 | + $currentFieldName = $currentAlias.'.'.$association['fieldName']; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + if (null !== $walker) { |
| 165 | + $walker = \is_string($walker) ? new $walker($queryBuilder, $currentFieldName) : $walker($queryBuilder, $currentFieldName, $this->fieldType); |
| 166 | + } else { |
| 167 | + $walker = new NodeWalker($currentFieldName, $this->fieldType); |
| 168 | + } |
| 169 | + |
| 170 | + $where = new WhereAnd(); |
| 171 | + $where->addChild($expression->dispatch($walker)); |
| 172 | + |
| 173 | + $queryBuilder->addChild($where); |
| 174 | + } |
| 175 | + |
| 176 | + /** |
| 177 | + * Adds a simple condition to the query builder. |
| 178 | + * |
| 179 | + * @param QueryBuilder $queryBuilder |
| 180 | + * @param ExpressionInterface $expression |
| 181 | + */ |
| 182 | + private function addWhereCondition(QueryBuilder $queryBuilder, ExpressionInterface $expression): void |
| 183 | + { |
| 184 | + $alias = $this->getMappingFieldName(); |
| 185 | + $walker = $this->customWalker; |
| 186 | + |
| 187 | + $fieldName = $this->rootAlias.'.'.$alias; |
| 188 | + if (null !== $walker) { |
| 189 | + $walker = \is_string($walker) ? new $walker($fieldName) : $walker($fieldName, $this->fieldType); |
| 190 | + } else { |
| 191 | + $walker = new NodeWalker($fieldName, $this->fieldType); |
| 192 | + } |
| 193 | + |
| 194 | + /** @var AbstractNode $node */ |
| 195 | + $node = $expression->dispatch($walker); |
| 196 | + if (AbstractNode::NT_CONSTRAINT === $node->getNodeType()) { |
| 197 | + $where = new WhereAnd(); |
| 198 | + $where->addChild($node); |
| 199 | + |
| 200 | + $queryBuilder->addChild($where); |
| 201 | + } else { |
| 202 | + $queryBuilder->addChild($node); |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + /** |
| 207 | + * Process associations chain. |
| 208 | + * |
| 209 | + * @param DocumentManagerInterface $documentManager |
| 210 | + * @param string $rest |
| 211 | + */ |
| 212 | + private function processAssociations(DocumentManagerInterface $documentManager, string $rest): void |
| 213 | + { |
| 214 | + $associations = []; |
| 215 | + $associationField = $this->mapping; |
| 216 | + |
| 217 | + while (null !== $rest) { |
| 218 | + $targetDocument = $documentManager->getClassMetadata($associationField['targetDocument']); |
| 219 | + [$associationField, $rest] = MappingHelper::processFieldName($targetDocument, $rest); |
| 220 | + |
| 221 | + if (null === $associationField) { |
| 222 | + throw new FieldNotFoundException($rest, $targetDocument->name); |
| 223 | + } |
| 224 | + |
| 225 | + $associations[] = $associationField; |
| 226 | + } |
| 227 | + |
| 228 | + $this->associations = $associations; |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Gets the target document class. |
| 233 | + * |
| 234 | + * @return string |
| 235 | + */ |
| 236 | + private function getSourceDocument(): string |
| 237 | + { |
| 238 | + return $this->mapping['sourceDocument']; |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * Gets the target document class. |
| 243 | + * |
| 244 | + * @return string |
| 245 | + */ |
| 246 | + private function getTargetDocument(): string |
| 247 | + { |
| 248 | + return $this->mapping['targetDocument']; |
| 249 | + } |
| 250 | +} |
0 commit comments