php - Api platform handling fille uploads
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;
}
}
Answer
Solution:
I am currently working on a project which allow users to upload media files.
I have discarded the Vich bundle. Api-platform is application/ld+json oriented.
Instead, i let the user provide a base64-encoded content file (i.e a string representation with readable characters only).
The only counterpart i got is that the file size is increased by ~30% during http transfer. Honestly, it does not matter.
I suggest you to do something like the code below.
OrganizationController --use--> Organization 1 <>---> 0..1 ImageObject
The logo (note the assertion on the $encodingFormat property):
<?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
Your stripped Organization class, which declare the 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...
}
Note the serialization groups of both $logo and $b64LogoContent properties.
Then the controller (action class), in order to decode, assign and write the logo content.
<?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";
}
}
It checks whether the supplied logo is a "tiny image" with @Assert annotations of the ImageObject class (encodingFormat, width, height etc.), they are triggered by the @Assert\Valid annotation of the Organization::$logo property.
With that, you can create an organization with its logo by sending a single HTTP POST /organizations request.
Source