DoctrineExtensions icon indicating copy to clipboard operation
DoctrineExtensions copied to clipboard

How do I correctly use the `rootIdentifieMethod`?

Open toby-griffiths opened this issue 5 years ago • 9 comments

I've seen this documented (kind of) in the recent change log, however I can't seem to work out how to use it.

I have Taxonomy and TaxonomyTerm entities, so I thought that I could use the 'getTaxonomy' as the rootIdentifierMethod, which returns the taxonomy entity, however this bit of code, from \Gedmo\Tree\Strategy\ORM\Nested::processScheduledInsertion() is setting the treeRoot property to 0

        if (isset($config['root']) && !$meta->hasAssociation($config['root']) && !isset($config['rootIdentifierMethod'])) {
            $meta->getReflectionProperty($config['root'])->setValue($node, 0);
        } else if (isset($config['rootIdentifierMethod']) && is_null($meta->getReflectionProperty($config['root'])->getValue($node))) {
            $meta->getReflectionProperty($config['root'])->setValue($node, 0);
        }

… which then causes Doctrine to choke, as it is a relationship field, so when it attempts to produce the object hash of 0 if throws an error. Here's the Doctrine code from \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::prepareUpdateData()

            if ($newVal !== null) {
                $oid = spl_object_hash($newVal);

                if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
                    // The associated entity $newVal is not yet persisted, so we must
                    // set $newVal = null, in order to insert a null value and schedule an
                    // extra update on the UnitOfWork.
                    $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);

                    $newVal = null;
                }
            }

What have I missed? If I'm using rootIdentifierMethod, can I not have the @TreeRoot annotation on a relationship field? If so, what should I use for the field?

Here are my entities…

<?php
declare(strict_types=1);

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use AppBundle\Entity\Traits\ImmutableTimestampableEntity;
use AppBundle\Model\TaxonomyInterface;
use AppBundle\Model\TaxonomyTermInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
use LogicException;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * A taxonomy for categorising resources to terms.
 *
 * @ORM\Entity
 * @ORM\Table(name="taxonomy")
 * @UniqueEntity(fields={"name"})
 *
 * @package AppBundle
 */
class Taxonomy implements TaxonomyInterface
{

    //------------------------------------------------------------------------------------------------------------------
    // Traits
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Adds createdAt/updatedAt fields.
     */
    use ImmutableTimestampableEntity;

    /**
     * Support not actually deleting a record, but archiving it.
     */
    use SoftDeleteableEntity;


    //------------------------------------------------------------------------------------------------------------------
    // Properties
    //------------------------------------------------------------------------------------------------------------------

    /**
     * @var string|null
     *
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="UUID")
     * @ORM\Column(type="guid")
     * @Assert\Uuid
     * @ApiProperty(iri="https://schema.org/identifier")
     * @Groups({
     *     "Taxonomy:collection:create:read",
     *     "Taxonomy:collection:read:read",
     * })
     */
    private $id;

    /**
     * @var string the name of the item
     *
     * @ORM\Column(type="string", length=64)
     * @ApiProperty(iri="http://schema.org/name")
     * @Assert\NotNull
     * @Groups({
     *     "Taxonomy:collection:create:read",
     *     "Taxonomy:collection:create:write",
     *     "Taxonomy:collection:read:read",
     * })
     */
    private $name;

    /**
     * @var string
     *
     * @ORM\Column(type="string", length=64)
     * @Gedmo\Slug(fields={"name"}, updatable=false)
     * @Groups({
     *     "Taxonomy:collection:create:read",
     *     "Taxonomy:collection:read:read",
     * })
     */
    private $slug;

    /**
     * @var TaxonomyTermInterface[]|Collection
     *
     * @ORM\OneToMany(
     *     targetEntity="AppBundle\Entity\TaxonomyTerm",
     *     mappedBy="taxonomy",
     *     cascade={"remove"}
     * )
     * @ApiProperty(iri="https://schema.org/hasDefinedTerm")
     */
    private $terms;

    /// ...

}
<?php
declare(strict_types=1);

namespace AppBundle\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use AppBundle\Entity\Traits\ImmutableTimestampableEntity;
use AppBundle\Model\TaxonomyInterface;
use AppBundle\Model\TaxonomyTermInterface;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
use LogicException;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * A term within a taxonomy.
 *
 * @ORM\Entity(repositoryClass="TaxonomyTermRepository")
 * @ORM\Table(name="taxonomy_term")
 * @Gedmo\Tree(type="nested")
 *
 * @package AppBundle
 */
class TaxonomyTerm implements TaxonomyTermInterface
{

    //------------------------------------------------------------------------------------------------------------------
    // Traits
    //------------------------------------------------------------------------------------------------------------------

    /**
     * Adds createdAt/updatedAt fields.
     */
    use ImmutableTimestampableEntity;

    /**
     * Support not actually deleting a record, but archiving it.
     */
    use SoftDeleteableEntity;


    //------------------------------------------------------------------------------------------------------------------
    // Properties
    //------------------------------------------------------------------------------------------------------------------

    /**
     * @var string|null
     *
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="UUID")
     * @ORM\Column(type="guid")
     * @Assert\Uuid
     * @ApiProperty(iri="https://schema.org/identifier")
     * @Groups({"TaxonomyTerm"})
     */
    private $id;

    /**
     * @var TaxonomyInterface
     *
     * @ORM\ManyToOne(
     *     targetEntity="AppBundle\Entity\Taxonomy",
     *     inversedBy="terms"
     * )
     * @Assert\NotNull
     * @ApiProperty(iri="http://schema.org/inDefinedTermSet")
     */
    private $taxonomy;

    /**
     * @var string the name of the item
     *
     * @ORM\Column(type="string", length=64)
     * @ApiProperty(iri="http://schema.org/name")
     * @Assert\NotNull
     * @ApiProperty(iri="https://schema.org/name")
     * @Groups({"TaxonomyTerm"})
     */
    private $name;

    /**
     * @var string
     *
     * @ORM\Column(type="string", length=64)
     * @Gedmo\Slug(fields={"name"}, updatable=false)
     * @Groups({"TaxonomyTerm:read"})
     */
    private $slug;

    /**
     * @var int
     *
     * @Gedmo\TreeLeft
     * @ORM\Column(name="tree_left", type="integer")
     * @Groups({"TaxonomyTerm"})
     */
    private $treeLeft;

    /**
     * @var int
     *
     * @Gedmo\TreeLevel
     * @ORM\Column(name="tree_level", type="integer")
     * @Groups({"TaxonomyTerm"})
     */
    private $treeLevel;

    /**
     * @var int
     *
     * @Gedmo\TreeRight
     * @ORM\Column(name="tree_right", type="integer")
     * @Groups({"TaxonomyTerm"})
     */
    private $treeRight;

    /**
     * @var TaxonomyTermInterface
     *
     * @Gedmo\TreeRoot(identifierMethod="getTaxonomy")
     * @ORM\ManyToOne(targetEntity="AppBundle\Entity\TaxonomyTerm")
     * @ORM\JoinColumn(name="tree_root", referencedColumnName="id")
     * @Groups({"TaxonomyTerm"})
     */
    private $treeRoot;

    /**
     * @var TaxonomyTermInterface
     *
     * @Gedmo\TreeParent
     * @ORM\ManyToOne(
     *     targetEntity="AppBundle\Entity\TaxonomyTerm",
     *      inversedBy="children",
     *     )
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
     * @Groups({"TaxonomyTerm"})
     */
    private $parent;

    /**
     * @var TaxonomyTermInterface[]
     *
     * @ORM\OneToMany(
     *     targetEntity="AppBundle\Entity\TaxonomyTerm",
     *     mappedBy="parent",
     *     cascade={"remove", "refresh"},
     *     orphanRemoval=true
     * )
     * @ORM\OrderBy({"treeLeft" = "ASC"})
     * @ApiProperty(iri="https://schema.org/additionalType")
     */
    private $children;

    /// ...

}

toby-griffiths avatar Nov 08 '18 15:11 toby-griffiths

For reference, I modifying the code to only change the value if there is no association, changing…

} else if (isset($config['rootIdentifierMethod']) && is_null($meta->getReflectionProperty($config['root'])->getValue($node))) {

… to…

} else if (isset($config['rootIdentifierMethod']) && !$meta->hasAssociation($config['root']) && is_null($meta->getReflectionProperty($config['root'])->getValue($node))) {

… which seems to help matters, but of course then there's an integrity error when INSERTing the record, as it's got the ID of the Taxonomy entity, rather than a TaxonomyTerm entity.

toby-griffiths avatar Nov 08 '18 15:11 toby-griffiths

@toby-griffiths Here's an example in the PR although it's still not clear to me either: https://github.com/Atlantic18/DoctrineExtensions/pull/1877/files#diff-79eaccdfde782f203894e2ab77b47dcbR48

@l3pp4rd @AkenRoberts It looks like rootIdentifierMethod is not implemented in yaml or XML drivers?

leevigraham avatar Nov 13 '18 12:11 leevigraham

Thanks for the prompt reply, @leevigraham. From the changelog entry in the README I thought it should point to an external entity for root elements, but from that example it looks like it just points to the root entity regardless… which, in the case of root elements would be the element itself. I believed that, in order to sort the root elements, they would need a common identifier, rather than a unique one, but I guess perhaps this might work. I've not dug into the sorting mechanism, so don't know right now. I'll look at this next, when I have a moment.

toby-griffiths avatar Nov 14 '18 08:11 toby-griffiths

Anybody able to comment on this yey?

toby-griffiths avatar Mar 04 '19 10:03 toby-griffiths

It's still doesn't work. I have the same problem. "Undefined index rootIdentifierMethod"

Notice: Undefined index: rootIdentifierMethod in in \vendor/gedmo/doctrine-extensions/lib/Gedmo/Tree/Entity/Repository/NestedTreeRepository.php (line 430)

    /**
     * @Gedmo\TreeRoot(identifierMethod="getRoot")
     * @ORM\ManyToOne(targetEntity="App\Entity\Permission")
     * @ORM\JoinColumns({
     *   @ORM\JoinColumn(name="root", referencedColumnName="id")
     * })
     */
    private $root;

pietrach avatar May 02 '19 21:05 pietrach

I can confirm that this is not supported in xml, it should be inside the xml definition I think. Also more check should be done in case this configuration is not set (default to 'getRoot' perhaps?)

yellow1912 avatar Jan 29 '20 16:01 yellow1912

@pietrach after adding,

  • @Gedmo\TreeRoot(identifierMethod="getRoot") adding a record does not work for me! https://github.com/doctrine-extensions/DoctrineExtensions/issues/2215

shokhaa avatar Apr 08 '21 13:04 shokhaa

I'm also having a problem inserting. I'm using Ulid's for my identifiers.

    /**
     * @Gedmo\TreeRoot
     * @ORM\ManyToOne(targetEntity="NestedListItem")
     * @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
     * @ORM\Column(name="root", type="ulid", nullable=true)
     */
    private $root;
trait ListItemTrait
{
    /**
     * @ORM\Id
     * @ORM\Column(type="ulid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class=UlidGenerator::class)
     */
    private Ulid $id ;

I'm getting

An exception occurred while executing 'UPDATE nested_list_item SET root = ?, lvl = 0, lft = 1, rgt = 2 WHERE id = ?' with params ["01F41TH05VYK3MHDY5X5FKZ1T3", "01F41TH05VYK3MHDY5X5FKZ1T3"]:

SQLSTATE[22P02]: Invalid text representation: 7 ERROR: invalid input syntax for type uuid: "01F41TH05VYK3MHDY5X5FKZ1T3"

I don't think it's a problem with doctrine or symfony, though, because other entities are inserted the same way, although those are primary keys, so maybe doctrine is handling them differently.

I have also tried replacing the 'ulid' with string, which is what NestedListEntityUuid trait does.

My solution for the moment is to set everything to uuid's, which works as expected. Ulid would be preferable, though.

tacman avatar Apr 24 '21 11:04 tacman

i have same problem

Warning: spl_object_hash() expects parameter 1 to be object, int given

/**
     * @Groups({"page_tree"})
     * @ApiSubresource(maxDepth=1)
     * @Gedmo\TreeRoot(identifierMethod="getRoot")
     * @ORM\ManyToOne(targetEntity="Menu")
     * @ORM\JoinColumn(referencedColumnName="id", onDelete="CASCADE")
     * @MaxDepth(1)
     */
    private $root;

stipic avatar Apr 06 '22 13:04 stipic

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Oct 03 '22 13:10 github-actions[bot]