Skip to content

Commit

Permalink
add support for phpcr odm
Browse files Browse the repository at this point in the history
  • Loading branch information
alekitto authored and massimilianobraglia committed Oct 29, 2019
1 parent 1486d68 commit 2ea7c73
Show file tree
Hide file tree
Showing 15 changed files with 1,301 additions and 8 deletions.
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@
"doctrine/doctrine-bundle": "^1.7",
"doctrine/mongodb-odm-bundle": "^3.4",
"doctrine/orm": "^2.5",
"doctrine/phpcr-bundle": "^2.0",
"doctrine/phpcr-odm": "^1.4",
"fazland/doctrine-extra": "dev-master",
"fazland/elastica-odm": "^1.0",
"giggsey/libphonenumber-for-php": "^8.10",
"jackalope/jackalope-doctrine-dbal": "^1.3",
"moneyphp/money": "^3.2",
"myclabs/php-enum": "^1.0",
"phpunit/phpunit": "^6.5|^7.0",
Expand Down
111 changes: 111 additions & 0 deletions src/Pagination/Doctrine/PhpCr/PagerIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare(strict_types=1);

namespace Fazland\ApiPlatformBundle\Pagination\Doctrine\PhpCr;

use Doctrine\ODM\PHPCR\DocumentManagerInterface;
use Doctrine\ODM\PHPCR\Query\Builder\AbstractNode;
use Doctrine\ODM\PHPCR\Query\Builder\ConverterPhpcr;
use Doctrine\ODM\PHPCR\Query\Builder\From;
use Doctrine\ODM\PHPCR\Query\Builder\Ordering;
use Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder;
use Doctrine\ODM\PHPCR\Query\Builder\SourceDocument;
use Fazland\ApiPlatformBundle\Pagination\Orderings;
use Fazland\ApiPlatformBundle\Pagination\PagerIterator as BaseIterator;
use Fazland\DoctrineExtra\ObjectIteratorInterface;
use Fazland\DoctrineExtra\ORM\IteratorTrait;

final class PagerIterator extends BaseIterator implements ObjectIteratorInterface
{
use IteratorTrait;

public function __construct(QueryBuilder $searchable, $orderBy)
{
$this->queryBuilder = clone $searchable;
$this->apply(null);

parent::__construct([], $orderBy);
}

/**
* {@inheritdoc}
*/
public function next(): void
{
parent::next();

$this->_current = null;
$this->_currentElement = parent::current();
}

/**
* {@inheritdoc}
*/
public function rewind(): void
{
parent::rewind();

$this->_current = null;
$this->_currentElement = parent::current();
}

/**
* {@inheritdoc}
*/
protected function getObjects(): array
{
$queryBuilder = clone $this->queryBuilder;

/** @var From $fromNode */
$fromNode = $queryBuilder->getChildOfType(AbstractNode::NT_FROM);
/** @var SourceDocument $source */
$source = $fromNode->getChildOfType(AbstractNode::NT_SOURCE);
$alias = $source->getAlias();

$method = new \ReflectionMethod(QueryBuilder::class, 'getConverter');
$method->setAccessible(true);
$converter = $method->invoke($queryBuilder);

/** @var DocumentManagerInterface $documentManager */
$documentManager = (function (): DocumentManagerInterface {
return $this->dm;
})->bindTo($converter, ConverterPhpcr::class)();

$classMetadata = $documentManager->getClassMetadata($source->getDocumentFqn());

foreach ($this->orderBy as $key => [$field, $direction]) {
$method = 0 === $key ? 'orderBy' : 'addOrderBy';

if ('nodename' === $classMetadata->getTypeOfField($field)) {
$queryBuilder->{$method}()->{$direction}()->localName($alias);
} else {
$queryBuilder->{$method}()->{$direction}()->field($alias.'.'.$field);
}
}

$limit = $this->pageSize;
if (null !== $this->token) {
$timestamp = $this->token->getOrderValue();
$limit += $this->token->getOffset();
$mainOrder = $this->orderBy[0];

$type = $documentManager->getClassMetadata($source->getDocumentFqn())->getTypeOfField($mainOrder[0]);
if ('date' === $type) {
$timestamp = \DateTimeImmutable::createFromFormat('U', (string) $timestamp);
}

$direction = Orderings::SORT_ASC === $mainOrder[1] ? 'gte' : 'lte';
/** @var Ordering $ordering */
$ordering = $queryBuilder->andWhere()->{$direction}();

if ('nodename' === $classMetadata->getTypeOfField($mainOrder[0])) {
$ordering->localName($alias)->literal($timestamp);
} else {
$ordering->field($alias.'.'.$mainOrder[0])->literal($timestamp);
}
}

$queryBuilder->setMaxResults($limit);

return $queryBuilder->getQuery()->getResult()->toArray();
}
}
2 changes: 1 addition & 1 deletion src/QueryLanguage/Processor/Doctrine/ORM/Column.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ private function searchForDiscriminator(ClassMetadata $rootEntity, string $field
}

$this->discriminator = true;
$this->validationWalker = function () use ($rootEntity): EnumWalker {
$this->validationWalker = static function () use ($rootEntity): EnumWalker {
return new EnumWalker(\array_keys($rootEntity->discriminatorMap));
};
$this->customWalker = DiscriminatorWalker::class;
Expand Down
250 changes: 250 additions & 0 deletions src/QueryLanguage/Processor/Doctrine/PhpCr/Column.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<?php declare(strict_types=1);

namespace Fazland\ApiPlatformBundle\QueryLanguage\Processor\Doctrine\PhpCr;

use Doctrine\ODM\PHPCR\DocumentManagerInterface;
use Doctrine\ODM\PHPCR\Mapping\ClassMetadata;
use Doctrine\ODM\PHPCR\Query\Builder\AbstractNode;
use Doctrine\ODM\PHPCR\Query\Builder\From;
use Doctrine\ODM\PHPCR\Query\Builder\QueryBuilder;
use Doctrine\ODM\PHPCR\Query\Builder\WhereAnd;
use Fazland\ApiPlatformBundle\QueryLanguage\Exception\Doctrine\FieldNotFoundException;
use Fazland\ApiPlatformBundle\QueryLanguage\Expression\ExpressionInterface;
use Fazland\ApiPlatformBundle\QueryLanguage\Processor\ColumnInterface;
use Fazland\ApiPlatformBundle\QueryLanguage\Walker\PhpCr\NodeWalker;

/**
* @internal
*/
class Column implements ColumnInterface
{
/**
* @var string
*/
private $rootAlias;

/**
* @var string[]
*/
private $mapping;

/**
* @var string
*/
public $fieldName;

/**
* @var string
*/
private $fieldType;

/**
* @var string|callable|null
*/
public $validationWalker;

/**
* @var string|callable|null
*/
public $customWalker;

/**
* @var array
*/
private $associations;

/**
* @var DocumentManagerInterface
*/
private $documentManager;

public function __construct(
string $fieldName,
string $rootAlias,
ClassMetadata $rootEntity,
DocumentManagerInterface $documentManager
) {
$this->fieldName = $fieldName;
$this->rootAlias = $rootAlias;

[$rootField, $rest] = MappingHelper::processFieldName($rootEntity, $fieldName);
$this->mapping = $rootField;

$this->fieldType = 'string';
if (isset($this->mapping['type']) && ! isset($this->mapping['targetDocument'])) {
$this->fieldType = $this->mapping['type'];
}

$this->associations = [];
if (null !== $rest) {
$this->processAssociations($documentManager, $rest);
}

$this->documentManager = $documentManager;
}

/**
* {@inheritdoc}
*/
public function addCondition($queryBuilder, ExpressionInterface $expression): void
{
if ($this->isAssociation()) {
$this->addAssociationCondition($queryBuilder, $expression);
} else {
$this->addWhereCondition($queryBuilder, $expression);
}
}

/**
* {@inheritdoc}
*/
public function getValidationWalker()
{
return $this->validationWalker;
}

/**
* Gets the mapping field name.
*
* @return string
*/
public function getMappingFieldName(): string
{
return $this->mapping['fieldName'];
}

/**
* Whether this column navigates into associations.
*
* @return bool
*/
public function isAssociation(): bool
{
return isset($this->mapping['targetDocument']) || 0 < \count($this->associations);
}

/**
* Processes an association column and attaches the conditions to the query builder.
*
* @param QueryBuilder $queryBuilder
* @param ExpressionInterface $expression
*/
private function addAssociationCondition(QueryBuilder $queryBuilder, ExpressionInterface $expression): void
{
$alias = $this->getMappingFieldName();
$walker = $this->customWalker;

$targetDocument = $this->documentManager->getClassMetadata($this->getTargetDocument());
if (null === $targetDocument->uuidFieldName) {
throw new \RuntimeException('Uuid field must be declared to build association conditions');
}

$queryBuilder->addJoinInner()
->right()->document($this->getTargetDocument(), $alias)->end()
->condition()->equi($this->rootAlias.'.'.$alias, $alias.'.'.$targetDocument->uuidFieldName)->end()
->end();

$currentFieldName = $alias;
$currentAlias = $alias;
foreach ($this->associations as $association) {
if (isset($association['targetDocument'])) {
/** @var From $from */
$from = $queryBuilder->getChildOfType(AbstractNode::NT_FROM);
$from->joinInner()
->left()->document($association['sourceDocument'], $currentAlias)->end()
->right()->document($association['targetDocument'], $currentFieldName = $association['fieldName'])->end()
->end();

$currentAlias = $association['fieldName'];
} else {
$currentFieldName = $currentAlias.'.'.$association['fieldName'];
}
}

if (null !== $walker) {
$walker = \is_string($walker) ? new $walker($queryBuilder, $currentFieldName) : $walker($queryBuilder, $currentFieldName, $this->fieldType);
} else {
$walker = new NodeWalker($currentFieldName, $this->fieldType);
}

$where = new WhereAnd();
$where->addChild($expression->dispatch($walker));

$queryBuilder->addChild($where);
}

/**
* Adds a simple condition to the query builder.
*
* @param QueryBuilder $queryBuilder
* @param ExpressionInterface $expression
*/
private function addWhereCondition(QueryBuilder $queryBuilder, ExpressionInterface $expression): void
{
$alias = $this->getMappingFieldName();
$walker = $this->customWalker;

$fieldName = $this->rootAlias.'.'.$alias;
if (null !== $walker) {
$walker = \is_string($walker) ? new $walker($fieldName) : $walker($fieldName, $this->fieldType);
} else {
$walker = new NodeWalker($fieldName, $this->fieldType);
}

/** @var AbstractNode $node */
$node = $expression->dispatch($walker);
if (AbstractNode::NT_CONSTRAINT === $node->getNodeType()) {
$where = new WhereAnd();
$where->addChild($node);

$queryBuilder->addChild($where);
} else {
$queryBuilder->addChild($node);
}
}

/**
* Process associations chain.
*
* @param DocumentManagerInterface $documentManager
* @param string $rest
*/
private function processAssociations(DocumentManagerInterface $documentManager, string $rest): void
{
$associations = [];
$associationField = $this->mapping;

while (null !== $rest) {
$targetDocument = $documentManager->getClassMetadata($associationField['targetDocument']);
[$associationField, $rest] = MappingHelper::processFieldName($targetDocument, $rest);

if (null === $associationField) {
throw new FieldNotFoundException($rest, $targetDocument->name);
}

$associations[] = $associationField;
}

$this->associations = $associations;
}

/**
* Gets the target document class.
*
* @return string
*/
private function getSourceDocument(): string
{
return $this->mapping['sourceDocument'];
}

/**
* Gets the target document class.
*
* @return string
*/
private function getTargetDocument(): string
{
return $this->mapping['targetDocument'];
}
}
Loading

0 comments on commit 2ea7c73

Please sign in to comment.