Search code examples
phpapi-platform.comsymfony5vichuploaderbundle

Api platform handling fille uploads


I'm trying to upload files with Api Platform and Vich Uploader Bundle. When I send POST request with multipart/form-data and Id of the entity to attach image file to, I get 200 response with my entity. But uploaded file doesn't uploads to destination directory and it's generated filename doesn't persists. No errors, no any clues, no idea.

Here is my code:

//vich uploader mappings
vich_uploader:
    db_driver: orm
    mappings:
        logo:
            uri_prefix: /logo
            upload_destination: '%kernel.project_dir%/public/images/logo/'
            namer: App\Infrastructure\Naming\LogoNamer
//Organization Entity
<?php

namespace App\Infrastructure\Dto;

...use

/**
 * @ORM\Entity()
 * @ApiResource(
 *     iri="https://schema.org/Organization",
 *     shortName="Place",
 *     collectionOperations={
 *          "post" = {
 *              "denormalization_context" = {
 *                  "groups"={
 *                      "organization:collection:post"
 *                  }
 *              }
 *          },
 *          "get" = {
 *              "normalization_context" = {
 *                  "groups"={
 *                      "organization:collection:get"
 *                  }
 *              }
 *          }
 *     },
 *     itemOperations={
 *          "get",
 *          "CreateOrganizationLogoAction::OPERATION_NAME" = {
 *              "groups"={"logo:post"},
 *              "method"="POST",
 *              "path"=CreateOrganizationLogoAction::OPERATION_PATH,
 *              "controller"=CreateOrganizationLogoAction::class,
 *              "deserialize"=false,
 *              "validation_groups"={"Default", "logo_create"},
 *              "openapi_context"={
 *                  "summary"="Uploads logo file to given Organization resource",
 *                  "requestBody"={
 *                      "content"={
 *                          "multipart/form-data"={
 *                              "schema"={
 *                                  "type"="object",
 *                                  "properties"={
 *                                      "logoFile"={
 *                                          "type"="string",
 *                                          "format"="binary"
 *                                      }
 *                                  }
 *                              }
 *                          }
 *                      }
 *                  }
 *              }
 *          }
 *     }
 * )
 * @Vich\Uploadable
 */
final class Organization
{
    /**
     * @Groups({"organization:collection:get"})
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     * @ORM\GeneratedValue(strategy="CUSTOM")
     * @ORM\CustomIdGenerator(class=UuidGenerator::class)
     * @ApiProperty(identifier=true)
     */
    protected Uuid $id;

    /**
     * @Groups({"organization:collection:get", "organization:collection:post"})
     * @ORM\Column(type="string", length=100, unique=true)
     */
    public string $slug;

    /**
     * @ORM\Column(type="smallint")
     */
    public int $status;

    /**
     * @ApiProperty(iri="https://schema.org/name")
     * @Groups({"organization:collection:get"})
     * @ORM\Column(type="string", length=100, nullable=true)
     */
    public ?string $title = null;

    /**
     * @ApiProperty(iri="http://schema.org/logo")
     * @Groups({"organization:collection:get", "logo:post"})
     * @ORM\Column(nullable=true)
     */
    public ?string $logoPath = null;

    /**
     * @ApiProperty(iri="https://schema.org/description")
     * @ORM\Column(type="text", nullable=true)
     */
    public ?string $description = null;

    /**
     * @ApiProperty(iri="https://schema.org/disambiguatingDescription")
     * @Groups({"organization:collection:get"})
     * @ORM\Column(type="string", length=150, nullable=true)
     */
    public ?string $disambiguating_description = null;

    /**
     * @ApiProperty(iri="https://schema.org/addressCountry")
     * @ORM\Column(type="string", length=2, nullable=true)
     */
    public ?string $country = null;

    /**
     * @ApiProperty(iri="https://schema.org/addressRegion")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $region = null;

    /**
     * @ApiProperty(iri="https://schema.org/streetAddress")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $street = null;

    /**
     * @ApiProperty(iri="https://schema.org/telephone")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $telephone = null;

    /**
     * @ApiProperty(iri="https://schema.org/email")
     * @ORM\Column(type="string", nullable=true)
     */
    public ?string $email = null;

    /**
     * @ApiProperty(iri="https://schema.org/contentUrl")
     * @Groups({"logo_read"})
     */
    public ?string $logoContentUrl = null;

    /**
     * @var File|null
     *
     * @Assert\NotNull(groups={"logo_create"})
     * @Vich\UploadableField(mapping="logo", fileNameProperty="logoPath")

     */
    public ?File $logoFile = null;

    public function __construct()
    {
        $this->status = OrganizationStatus::DRAFT()->getValue();
    }

    public function getId(): ?Uuid
    {
        return $this->id ?? null;
    }

    public function setId(Uuid $id)
    {
        $this->id = $id;
    }
}
final class CreateOrganizationLogoAction extends AbstractController
{
    const OPERATION_NAME = 'post_logo';

    const OPERATION_PATH = '/places/{id}/logo';

    private OrganizationPgRepository $repository;

    public function __construct(OrganizationPgRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * @param Request $request
     *
     * @return EntityOrganization
     */
    public function __invoke(Request $request): EntityOrganization
    {
        $uploadedFile = $request->files->get('logoFile');
        if (!$uploadedFile) {
            throw new BadRequestHttpException('"file" is required');
        }

        $organization = $this->repository->find(Uuid::fromString($request->attributes->get('id')));
        $organization->logoFile = $uploadedFile;

        return $organization;
    }
}

I'm sending request:

curl -X POST "http://localhost:8081/api/places/0dc43a86-6402-4a45-8392-19d5e398a7ab/logo" -H "accept: application/ld+json" -H "Content-Type: multipart/form-data" -F "[email protected];type=image/png"

... and getting response:

{
  "@context": "/api/contexts/Place",
  "@id": "/api/places/0dc43a86-6402-4a45-8392-19d5e398a7ab",
  "@type": "https://schema.org/Organization",
  "slug": "consequatur-aut-optio-corrupti-quod-sit-libero-aspernatur",
  "status": 0,
  "title": "Block LLC",
  "logoPath": "a268cde1-d93e-4d48-9f0d-177b4f89f1f8.png",
  "description": "Nisi sint ducimus consequatur dicta sint maxime. Et soluta facere in quisquam quia. Tempore quae non qui dignissimos optio rem cum illum. Eum similique vitae autem aut. Reiciendis nesciunt rerum libero in consequuntur excepturi repellendus unde. Tempore ea perferendis sunt quibusdam autem est. Similique qui illum necessitatibus velit dolores. Voluptas sapiente excepturi ad assumenda exercitationem est. Nesciunt sint sint fugiat quis blanditiis. Rerum vel sint temporibus nobis fugiat nostrum aut. Voluptatibus temporibus magnam cumque asperiores. Adipisci qui perferendis mollitia tempore accusantium aut. Possimus numquam asperiores repellendus non facilis.",
  "disambiguating_description": "Et libero temporibus ut impedit esse ipsum quam.",
  "country": "RU",
  "region": "Idaho",
  "street": "15544 Delbert Underpass",
  "telephone": "+78891211558",
  "email": "[email protected]",
  "pictures": [],
  "social_profiles": [],
  "logoContentUrl": "/logo/a268cde1-d93e-4d48-9f0d-177b4f89f1f8.png",
  "logoFile": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD//gA7Q1JFQVRPUjog
...
...
... TgjNWnJ7YWPrMCWGxWbi57Tj58TfPQL1Hi54DRFD/FkuLcuXBKFB3TFLcuaUvpqKuYUJaLL/yV/R/+kf/Z",
  "id": "0dc43a86-6402-4a45-8392-19d5e398a7ab"
}

As you can see it's all ok. Proper organization was found. And even logoFile field was filled with uploaded picture. But uploaded file wasn't moved to destination. And logoPath contains old logo filename.

As I said no errors. Please help me to figure out where to dig.


Solution

  • The VichUploaderBundle does the upload handling in a doctrine event listener using the prePersist and preUpdate hooks. The problem in your case is, that - from doctrines point of view - no persistent property has changed. Since there is no change, the upload listener won't be called.

    A simple workaround is to always change a persistent property when a file was uploaded. I added updatedAt to your entity and the method updateLogo to keep the required change of logoFile and updatedAt together.

    final class Organization
    {
        (...)
    
        /**
         * @ApiProperty(iri="http://schema.org/logo")
         * @Groups({"organization:collection:get", "logo:post"})
         * @ORM\Column(nullable=true)
         */
        public ?string $logoPath = null;
    
        /**
         * @ORM\Column(type="datetime")
         */
        private ?DateTime $updatedAt = null;
    
        /**
         * @var File|null
         *
         * @Assert\NotNull(groups={"logo_create"})
         * @Vich\UploadableField(mapping="logo", fileNameProperty="logoPath")
         */
        private ?File $logoFile = null;
        
        (...)
    
        public function updateLogo(File $logo): void
        {
           $this->logoFile  = $logo;
           $this->updatedAt = new DateTime();
        }
    }
    
    final class CreateOrganizationLogoAction extends AbstractController
    {
        (...)
    
        /**
         * @param Request $request
         *
         * @return EntityOrganization
         */
        public function __invoke(Request $request): EntityOrganization
        {
            $uploadedFile = $request->files->get('logoFile');
            if (!$uploadedFile) {
                throw new BadRequestHttpException('"file" is required');
            }
    
            $organization = $this->repository->find(Uuid::fromString($request->attributes->get('id')));
            $organization->updateLogo($uploadedFile);
    
            return $organization;
        }
    }