Api 处理文件上传的平台

Api platform handling fille uploads

我正在尝试使用 Api Platform 和 Vich Uploader Bundle 上传文件。 当我发送带有 multipart/form-data 和实体 ID 的 POST 请求以附加图像文件时,我的实体收到 200 响应。但是上传的文件不会上传到目标目录,并且生成的文件名不会保留。没有错误,没有任何线索,不知道。

这是我的代码:

//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;
    }
}

我正在发送请求:

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 "logoFile=@test.png;type=image/png"

...并得到响应:

{
  "@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": "lhintz@corwin.com",
  "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"
}

如您所见,一切正常。找到了合适的组织。甚至 logoFile 字段也充满了上传的图片。但是上传的文件没有移动到目的地。 logoPath 包含旧徽标文件名。

正如我所说,没有错误。 请帮我弄清楚在哪里挖掘。

我目前正在做一个允许用户上传媒体文件的项目。

我已经丢弃了 Vich 包。 Api-平台面向 application/ld+json。

相反,我让用户提供一个 base64 编码的内容文件(即仅包含可读字符的字符串表示形式)。

我得到的唯一对应是在 http 传输期间文件大小增加了 ~30%。老实说,没关系。

我建议你像下面的代码那样做。

OrganizationController --use--> Organization 1 <>---> 0..1 ImageObject

徽标(注意 $encodingFormat 属性 上的声明):

<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * An image file.
 *
 * @see http://schema.org/ImageObject Documentation on Schema.org
 *
 * @ORM\Entity
 * @ApiResource(
 *     iri="http://schema.org/ImageObject",
 *     normalizationContext={"groups" = {"imageobject:get"}}
 *     collectionOperations={"get"},
 *     itemOperations={"get"}
 * )
 */
class ImageObject
{
    /**
     * @var int|null
     *
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer")
     * @Groups({"imageobject:get"})
     */
    private $id;

    /**
     * @var string|null the name of the item
     *
     * @ORM\Column(type="text", nullable=true)
     * @ApiProperty(iri="http://schema.org/name")
     * @Groups({"imageobject:get"})
     */
    private $name;

    /**
     * @var string|null actual bytes of the media object, for example the image file or video file
     *
     * @ORM\Column(type="text", nullable=true)
     * @ApiProperty(iri="http://schema.org/contentUrl")
     * @Groups({"imageobject:get"})
     */
    private $contentUrl;

    /**
     * @var string|null mp3, mpeg4, etc
     *
     * @Assert\Regex("#^image/.*$#", message="This is not an image, this is a {{ value }} file.")
     * @ORM\Column(type="text", nullable=true)
     * @ApiProperty(iri="http://schema.org/encodingFormat")
     * @Groups({"imageobject:get"})
     */
    private $encodingFormat;
    
    // getters and setters, nothing specific here

你剥离的 Organization class,声明了 OrganizationController:

<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use App\Repository\OrganizationRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use App\Controller\OrganizationController;

/**
 * @ApiResource(
 *     normalizationContext={
            "groups" = {"organization:get"}
 *     },
 *     denormalizationContext={
            "groups" = {"organization:post"}
 *     },
 *     collectionOperations={
            "get",
 *          "post" = {
 *              "controller" = OrganizationController::class
 *          }
 *     }
 * )
 * @ORM\Entity(repositoryClass=OrganizationRepository::class)
 */
class Organization
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     * @Groups({"organization:get"})
     */
    private $id;

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

    /**
     * @var null|ImageObject
     * @Assert\Valid()
     * @ORM\OneToOne(targetEntity=ImageObject::class, cascade={"persist", "remove"})
     * @Groups({"organization:get"})
     */
    private $logo;

    /**
     * @var string the logo BLOB, base64-encoded, without line separators.
     * @Groups({"organization:post"})
     */
    private $b64LogoContent;

    // getters and setters, nothing specific here...

}

注意 $logo$b64LogoContent 属性的序列化组。

然后是controller(actionclass),为了解码,赋值,写入logo内容

<?php


namespace App\Controller;

use App\Entity\ImageObject;
use App\Entity\Organization;
use finfo;

/**
 * Handle the base64-encoded logo content.
 */
class OrganizationController
{
    public function __invoke(Organization $data)
    {
        $b64LogoContent = $data->getB64LogoContent();
        if (! empty($b64LogoContent)) {
            $logo = $this->buildAndWriteLogo($b64LogoContent);
            $data->setLogo($logo);
        }
        return $data;
    }

    private function buildAndWriteLogo(string $b64LogoContent): ImageObject
    {
        $logo = new ImageObject();
        $content = str_replace("\n", "", base64_decode($b64LogoContent));
        $mimeType = (new finfo())->buffer($content, FILEINFO_MIME_TYPE);
        $autoGeneratedId = $this->createFileName($content, $mimeType); // Or anything to generate an ID, like md5sum
        $logo->setName($autoGeneratedId);
        $logo->setContentUrl("/public/images/logo/$autoGeneratedId");
        $logo->setEncodingFormat($mimeType);
        // check the directory permissions!
        // writing the file should be done after data validation
        file_put_contents("images/logo/$autoGeneratedId", $content);
        return $logo;
    }

    private function createFileName(string $content, string $mimeType): string
    {
        if (strpos($mimeType, "image/") === 0) {
            $extension = explode('/', $mimeType)[1];
        } else {
            $extension = "txt";
        }
        return time() . ".$extension";
    }
}

它检查提供的标志是否是带有 @Assert ImageObject class 注释(编码格式、宽度、高度等)的“小图像”,它们由 @Assert\Valid 注释触发 Organization::$logo 属性.

有了它,您可以通过发送单个 HTTP POST /organizations 请求来创建带有徽标的组织。

VichUploaderBundle 使用 prePersist 和 preUpdate 挂钩在学说事件侦听器中进行上传处理。您的问题是,从学说的角度来看,没有持久性 属性 发生变化。由于没有变化,上传监听器不会被调用。

一个简单的解决方法是在上传文件时始终更改持久性 属性。我将 updatedAt 添加到您的实体和方法 updateLogo 以将 logoFileupdatedAt 的所需更改保持在一起。

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;
    }
}