core: Removals from an ArrayCollection gets normalized wrong

Hi,

I just noticed that if one removes an element from a collection, the result is wrong.

Example

/**
 * @ORM\Entity
 */
class Project
{
    /**
     * @ORM\OneToMany(targetEntity="PartKeepr\ProjectBundle\Entity\ProjectPart",mappedBy="project",cascade={"persist", "remove"})
     * @Groups({"default"})
     * @var ArrayCollection
     */
    private $parts;

    public function __construct () {
        $this->parts = new ArrayCollection();
    }

    /**
     * Returns the parts array
     *
     * @return ProjectPart[] An array of ProjectPart objects
     */
    public function getParts()
    {
        return $this->parts;
    }

     /**
     * Adds a Project Part
     *
     * @param ProjectPart $projectPart An attachment to add
     */
    public function addPart($projectPart)
    {
        $projectPart->setProject($this);
        $this->parts->add($projectPart);
    }

    /**
     * Removes a Project Part
     *
     * @param ProjectPart $projectPart An attachment to remove
     */
    public function removePart($projectPart)
    {
        $projectPart->setProject(null);
        $this->parts->removeElement($projectPart);
    }
}

Note that the example class is barebones and doesn’t include e.g. an identifier, which is actually the case in my project.

When I remove any part except the first, the resulting JSON has array indexes in the result:

{  
   "@context":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/contexts\/Project",
   "@id":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/projects\/25",
   "@type":"Project",
   "parts":{  
      "0":{  # This index is included which is otherwise missing
         "@id":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/project_parts\/758",
         "@type":"ProjectPart",
         "part":{  
            "@id":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/parts\/284",
            "@type":"Part",
       }
}

I would expect the resulting JSON like this:

{  
   "@context":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/contexts\/Project",
   "@id":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/projects\/25",
   "@type":"Project",
   "parts":{  
      {  # No index
         "@id":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/project_parts\/758",
         "@type":"ProjectPart",
         "part":{  
            "@id":"\/~felicitus\/PartKeepr\/web\/app_dev.php\/api\/parts\/284",
            "@type":"Part",
       }
}

If my adder/remover implementation is wrong, please let me know. If so, I must refer to https://github.com/dunglas/DunglasApiBundle/issues/153#issuecomment-119265974 again (Sample Implementation of getters/adders/removers)

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Comments: 44 (32 by maintainers)

Most upvoted comments

Hi all ! In ApiPlatform\Core\Serializer\AbstractItemNormalizer :

 /**
     * Normalizes a collection of relations (to-many).
     *
     * @param iterable $attributeValue
     */
    protected function normalizeCollectionOfRelations(PropertyMetadata $propertyMetadata, $attributeValue, string $resourceClass, string $format = null, array $context): array
    {
        $value = [];
        foreach ($attributeValue as $index => $obj) {
            $value[$index] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);
        }
        return $value;
    }

Returning a non-indexed array solves the problem : $value[] = $this->normalizeRelation($propertyMetadata, $obj, $resourceClass, $format, $context);

Is there a case we want to keep the indexes in a to-many relations collection ?

@felicitus I’ve already encountered the problem and unfortunately there is not much solutions as PHP doesn’t have explicit hash map data structures.

When using ArrayCollection you can use the ::toArray():

$arr = new Doctrine\Common\Collections\ArrayCollection();

$e1 = new stdclass();
$e2 = new stdclass();

$arr->add($e1);
$arr->add($e2);

$arr->remove(0);
json_encode($arr->toArray()) // => "{"1":{}}"
json_encode(array_values($arr->toArray())) // => "[{}]"

I was thinking about patching doctrine (provide a reorder constructor flag in for Doctrine Collection implementations for instance).