Skip to content

Commit 2ea7c73

Browse files
alekittomassimilianobraglia
authored andcommitted
add support for phpcr odm
1 parent 1486d68 commit 2ea7c73

File tree

15 files changed

+1301
-8
lines changed

15 files changed

+1301
-8
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,12 @@
4040
"doctrine/doctrine-bundle": "^1.7",
4141
"doctrine/mongodb-odm-bundle": "^3.4",
4242
"doctrine/orm": "^2.5",
43+
"doctrine/phpcr-bundle": "^2.0",
44+
"doctrine/phpcr-odm": "^1.4",
4345
"fazland/doctrine-extra": "dev-master",
4446
"fazland/elastica-odm": "^1.0",
4547
"giggsey/libphonenumber-for-php": "^8.10",
48+
"jackalope/jackalope-doctrine-dbal": "^1.3",
4649
"moneyphp/money": "^3.2",
4750
"myclabs/php-enum": "^1.0",
4851
"phpunit/phpunit": "^6.5|^7.0",
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Fazland\ApiPlatformBundle\Pagination\Doctrine\PhpCr;
4+
5+
use Doctrine\ODM\PHPCR\DocumentManagerInterface;
6+
use Doctrine\ODM\PHPCR\Query\Builder\AbstractNode;
7+
use Doctrine\ODM\PHPCR\Query\Builder\ConverterPhpcr;
8+
use Doctrine\ODM\PHPCR\Query\Builder\From;
9+
use Doctrine\ODM\PHPCR\Query\Builder\Ordering;
10+
use Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder;
11+
use Doctrine\ODM\PHPCR\Query\Builder\SourceDocument;
12+
use Fazland\ApiPlatformBundle\Pagination\Orderings;
13+
use Fazland\ApiPlatformBundle\Pagination\PagerIterator as BaseIterator;
14+
use Fazland\DoctrineExtra\ObjectIteratorInterface;
15+
use Fazland\DoctrineExtra\ORM\IteratorTrait;
16+
17+
final class PagerIterator extends BaseIterator implements ObjectIteratorInterface
18+
{
19+
use IteratorTrait;
20+
21+
public function __construct(QueryBuilder $searchable, $orderBy)
22+
{
23+
$this->queryBuilder = clone $searchable;
24+
$this->apply(null);
25+
26+
parent::__construct([], $orderBy);
27+
}
28+
29+
/**
30+
* {@inheritdoc}
31+
*/
32+
public function next(): void
33+
{
34+
parent::next();
35+
36+
$this->_current = null;
37+
$this->_currentElement = parent::current();
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
public function rewind(): void
44+
{
45+
parent::rewind();
46+
47+
$this->_current = null;
48+
$this->_currentElement = parent::current();
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
protected function getObjects(): array
55+
{
56+
$queryBuilder = clone $this->queryBuilder;
57+
58+
/** @var From $fromNode */
59+
$fromNode = $queryBuilder->getChildOfType(AbstractNode::NT_FROM);
60+
/** @var SourceDocument $source */
61+
$source = $fromNode->getChildOfType(AbstractNode::NT_SOURCE);
62+
$alias = $source->getAlias();
63+
64+
$method = new \ReflectionMethod(QueryBuilder::class, 'getConverter');
65+
$method->setAccessible(true);
66+
$converter = $method->invoke($queryBuilder);
67+
68+
/** @var DocumentManagerInterface $documentManager */
69+
$documentManager = (function (): DocumentManagerInterface {
70+
return $this->dm;
71+
})->bindTo($converter, ConverterPhpcr::class)();
72+
73+
$classMetadata = $documentManager->getClassMetadata($source->getDocumentFqn());
74+
75+
foreach ($this->orderBy as $key => [$field, $direction]) {
76+
$method = 0 === $key ? 'orderBy' : 'addOrderBy';
77+
78+
if ('nodename' === $classMetadata->getTypeOfField($field)) {
79+
$queryBuilder->{$method}()->{$direction}()->localName($alias);
80+
} else {
81+
$queryBuilder->{$method}()->{$direction}()->field($alias.'.'.$field);
82+
}
83+
}
84+
85+
$limit = $this->pageSize;
86+
if (null !== $this->token) {
87+
$timestamp = $this->token->getOrderValue();
88+
$limit += $this->token->getOffset();
89+
$mainOrder = $this->orderBy[0];
90+
91+
$type = $documentManager->getClassMetadata($source->getDocumentFqn())->getTypeOfField($mainOrder[0]);
92+
if ('date' === $type) {
93+
$timestamp = \DateTimeImmutable::createFromFormat('U', (string) $timestamp);
94+
}
95+
96+
$direction = Orderings::SORT_ASC === $mainOrder[1] ? 'gte' : 'lte';
97+
/** @var Ordering $ordering */
98+
$ordering = $queryBuilder->andWhere()->{$direction}();
99+
100+
if ('nodename' === $classMetadata->getTypeOfField($mainOrder[0])) {
101+
$ordering->localName($alias)->literal($timestamp);
102+
} else {
103+
$ordering->field($alias.'.'.$mainOrder[0])->literal($timestamp);
104+
}
105+
}
106+
107+
$queryBuilder->setMaxResults($limit);
108+
109+
return $queryBuilder->getQuery()->getResult()->toArray();
110+
}
111+
}

src/QueryLanguage/Processor/Doctrine/ORM/Column.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ private function searchForDiscriminator(ClassMetadata $rootEntity, string $field
248248
}
249249

250250
$this->discriminator = true;
251-
$this->validationWalker = function () use ($rootEntity): EnumWalker {
251+
$this->validationWalker = static function () use ($rootEntity): EnumWalker {
252252
return new EnumWalker(\array_keys($rootEntity->discriminatorMap));
253253
};
254254
$this->customWalker = DiscriminatorWalker::class;
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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

Comments
 (0)