Skip to content

Commit

Permalink
Add a compat trait to address B/C issues with EntityRepository
Browse files Browse the repository at this point in the history
  • Loading branch information
mbabker authored and franmomu committed Feb 11, 2024
1 parent eebe57b commit 50352f7
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 88 deletions.
3 changes: 3 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ parameters:
- '#^Method Gedmo\\Uploadable\\Mapping\\Validator::validateConfiguration\(\) with return type void returns array<string, mixed> but should not return anything\.$#'
- '#^Result of static method Gedmo\\Uploadable\\Mapping\\Validator::validateConfiguration\(\) \(void\) is used\.$#'
- '#^Result of method Gedmo\\Mapping\\Driver::readExtendedMetadata\(\) \(void\) is used\.$#'
excludePaths:
# Generates non-ignorable errors like " Parameter #1 $method (string) of method Gedmo\Tree\Entity\Repository\NestedTreeRepository::__call() is not contravariant with parameter #1 $method (mixed) of method Doctrine\ORM\EntityRepository::__call()."
- src/Tool/ORM/Repository/EntityRepositoryCompat.php
82 changes: 82 additions & 0 deletions src/Tool/ORM/Repository/EntityRepositoryCompat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Gedmo\Tool\ORM\Repository;

use Doctrine\ORM\EntityRepository;

if ((new \ReflectionClass(EntityRepository::class))->getMethod('__call')->hasReturnType()) {
// ORM 3.x
/**
* Helper trait to address compatibility issues between ORM 2.x and 3.x.
*
* @mixin EntityRepository
*
* @internal
*/
trait EntityRepositoryCompat
{
/**
* @param string $method
* @param array $args
*
* @return mixed
*
* @phpstan-param list<mixed> $args
*/
public function __call(string $method, array $args): mixed
{
return $this->doCallWithCompat($method, $args);
}

/**
* @param string $method
* @param array $args
*
* @return mixed
*
* @phpstan-param list<mixed> $args
*/
abstract protected function doCallWithCompat($method, $args);
}
} else {
// ORM 2.x
/**
* Helper trait to address compatibility issues between ORM 2.x and 3.x.
*
* @mixin EntityRepository
*
* @internal
*/
trait EntityRepositoryCompat
{
/**
* @param string $method
* @param array $args
*
* @return mixed
*
* @phpstan-param list<mixed> $args
*/
public function __call($method, $args)
{
return $this->doCallWithCompat($method, $args);
}

/**
* @param string $method
* @param array $args
*
* @return mixed
*
* @phpstan-param list<mixed> $args
*/
abstract protected function doCallWithCompat($method, $args);
}
}
184 changes: 96 additions & 88 deletions src/Tree/Entity/Repository/NestedTreeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Exception\RuntimeException;
use Gedmo\Exception\UnexpectedValueException;
use Gedmo\Tool\ORM\Repository\EntityRepositoryCompat;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Gedmo\Tree\Node;
use Gedmo\Tree\Strategy;
Expand Down Expand Up @@ -43,94 +44,7 @@
*/
class NestedTreeRepository extends AbstractTreeRepository
{
/**
* Allows the following 'virtual' methods:
* - persistAsFirstChild($node)
* - persistAsFirstChildOf($node, $parent)
* - persistAsLastChild($node)
* - persistAsLastChildOf($node, $parent)
* - persistAsNextSibling($node)
* - persistAsNextSiblingOf($node, $sibling)
* - persistAsPrevSibling($node)
* - persistAsPrevSiblingOf($node, $sibling)
* Inherited virtual methods:
* - find*
*
* @see \Doctrine\ORM\EntityRepository
*
* @throws InvalidArgumentException If arguments are invalid
* @throws \BadMethodCallException If the method called is an invalid find* or persistAs* method
* or no find* either persistAs* method at all and therefore an invalid method call
*
* @return mixed TreeNestedRepository if persistAs* is called
*/
public function __call($method, $args)
{
if ('persistAs' === substr($method, 0, 9)) {
if (!isset($args[0])) {
throw new InvalidArgumentException('Node to persist must be available as first argument.');
}
$node = $args[0];
$wrapped = new EntityWrapper($node, $this->getEntityManager());
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName());
$position = substr($method, 9);
if ('Of' === substr($method, -2)) {
if (!isset($args[1])) {
throw new InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument.');
}
$parentOrSibling = $args[1];
if (strstr($method, 'Sibling')) {
$wrappedParentOrSibling = new EntityWrapper($parentOrSibling, $this->getEntityManager());
$newParent = $wrappedParentOrSibling->getPropertyValue($config['parent']);
if (null === $newParent && isset($config['root'])) {
throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible');
}

if (!$node instanceof Node) {
@trigger_error(\sprintf(
'Not implementing the "%s" interface from node "%s" is deprecated since gedmo/doctrine-extensions'
.' 3.13 and will throw a "%s" error in version 4.0.',
Node::class,
\get_class($node),
\TypeError::class
), \E_USER_DEPRECATED);
}

// @todo: In the next major release, remove the previous condition and uncomment the following one.

// if (!$node instanceof Node) {
// throw new \TypeError(\sprintf(
// 'Node MUST implement "%s" interface.',
// Node::class
// ));
// }

// @todo: In the next major release, remove the `method_exists()` condition and left the `else` branch.
if (!method_exists($node, 'setSibling')) {
$node->sibling = $parentOrSibling;
} else {
$node->setSibling($parentOrSibling);
}
$parentOrSibling = $newParent;
}
$wrapped->setPropertyValue($config['parent'], $parentOrSibling);
$position = substr($position, 0, -2);
}
$wrapped->setPropertyValue($config['left'], 0); // simulate changeset
$oid = spl_object_id($node);
$this->listener
->getStrategy($this->getEntityManager(), $meta->getName())
->setNodePosition($oid, $position)
;

$this->getEntityManager()->persist($node);

return $this;
}

return parent::__call($method, $args);
}
use EntityRepositoryCompat;

public function getRootNodesQueryBuilder($sortByField = null, $direction = 'asc')
{
Expand Down Expand Up @@ -1169,6 +1083,100 @@ public function getNodesHierarchy($node = null, $direct = false, array $options
return $this->getNodesHierarchyQuery($node, $direct, $options, $includeNode)->getArrayResult();
}

/**
* Allows the following 'virtual' methods:
* - persistAsFirstChild($node)
* - persistAsFirstChildOf($node, $parent)
* - persistAsLastChild($node)
* - persistAsLastChildOf($node, $parent)
* - persistAsNextSibling($node)
* - persistAsNextSiblingOf($node, $sibling)
* - persistAsPrevSibling($node)
* - persistAsPrevSiblingOf($node, $sibling)
* Inherited virtual methods:
* - find*
*
* @param string $method
* @param array $args
*
* @phpstan-param list<mixed> $args
*
* @throws \BadMethodCallException If the method called is an invalid find* or persistAs* method
* or no find* either persistAs* method at all and therefore an invalid method call
* @throws InvalidArgumentException If arguments are invalid
*
* @return mixed TreeNestedRepository if persistAs* is called
*
* @see \Doctrine\ORM\EntityRepository
*/
protected function doCallWithCompat($method, $args)
{
if ('persistAs' === substr($method, 0, 9)) {
if (!isset($args[0])) {
throw new InvalidArgumentException('Node to persist must be available as first argument.');
}
$node = $args[0];
$wrapped = new EntityWrapper($node, $this->getEntityManager());
$meta = $this->getClassMetadata();
$config = $this->listener->getConfiguration($this->getEntityManager(), $meta->getName());
$position = substr($method, 9);
if ('Of' === substr($method, -2)) {
if (!isset($args[1])) {
throw new InvalidArgumentException('If "Of" is specified you must provide parent or sibling as the second argument.');
}
$parentOrSibling = $args[1];
if (strstr($method, 'Sibling')) {
$wrappedParentOrSibling = new EntityWrapper($parentOrSibling, $this->getEntityManager());
$newParent = $wrappedParentOrSibling->getPropertyValue($config['parent']);
if (null === $newParent && isset($config['root'])) {
throw new UnexpectedValueException('Cannot persist sibling for a root node, tree operation is not possible');
}

if (!$node instanceof Node) {
@trigger_error(\sprintf(
'Not implementing the "%s" interface from node "%s" is deprecated since gedmo/doctrine-extensions'
.' 3.13 and will throw a "%s" error in version 4.0.',
Node::class,
\get_class($node),
\TypeError::class
), \E_USER_DEPRECATED);
}

// @todo: In the next major release, remove the previous condition and uncomment the following one.

// if (!$node instanceof Node) {
// throw new \TypeError(\sprintf(
// 'Node MUST implement "%s" interface.',
// Node::class
// ));
// }

// @todo: In the next major release, remove the `method_exists()` condition and left the `else` branch.
if (!method_exists($node, 'setSibling')) {
$node->sibling = $parentOrSibling;
} else {
$node->setSibling($parentOrSibling);
}
$parentOrSibling = $newParent;
}
$wrapped->setPropertyValue($config['parent'], $parentOrSibling);
$position = substr($position, 0, -2);
}
$wrapped->setPropertyValue($config['left'], 0); // simulate changeset
$oid = spl_object_id($node);
$this->listener
->getStrategy($this->getEntityManager(), $meta->getName())
->setNodePosition($oid, $position)
;

$this->getEntityManager()->persist($node);

return $this;
}

return parent::__call($method, $args);
}

protected function validate()
{
return Strategy::NESTED === $this->listener->getStrategy($this->getEntityManager(), $this->getClassMetadata()->name)->getName();
Expand Down

0 comments on commit 50352f7

Please sign in to comment.