Skip to content

Instantly share code, notes, and snippets.

@meekowhy
Last active July 2, 2021 09:09
Show Gist options
  • Save meekowhy/39feb6121b74edaaa90a43e2c68a5b6d to your computer and use it in GitHub Desktop.
Save meekowhy/39feb6121b74edaaa90a43e2c68a5b6d to your computer and use it in GitHub Desktop.
Api platform Base64 file upload
<?php
declare(strict_types=1);
namespace App\FileUpload;
interface Base64FileUpload extends FileUpload
{
public function getBase64fileContent(): ?string;
public function setBase64fileContent(?string $base64fileContent): self;
}
<?php
declare(strict_types=1);
namespace App\Serializer;
use App\FileUpload\Base64FileUpload;
use App\FileUpload\Base64FileUploader;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
final class Base64FileUploadDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
private const ALREADY_CALLED = 'FILE_UPLOAD_DENORMALIZER_ALREADY_CALLED';
public function __construct(private Base64FileUploader $fileUploader)
{
}
/**
* @phpstan-ignore-next-line
*/
public function supportsDenormalization($data, string $type, string $format = null, array $context = []): bool
{
if (isset($context[self::ALREADY_CALLED])) {
return false;
}
return is_subclass_of($type, Base64FileUpload::class);
}
/**
* @phpstan-ignore-next-line
*/
public function denormalize($data, string $type, string $format = null, array $context = [])
{
$context[self::ALREADY_CALLED] = true;
/** @var Base64FileUpload $object */
$object = $this->denormalizer->denormalize($data, $type, $format, $context);
$this->fileUploader->prepareBase64FileUploadObject($object);
return $object;
}
}
<?php
declare(strict_types=1);
namespace App\FileUpload;
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Mime\MimeTypesInterface;
class Base64FileUploader
{
/** @var string[] */
private static array $base64tmpPaths = [];
public function __construct(
private MimeTypesInterface $mimeTypes
) {
}
public function prepareBase64FileUploadObject(Base64FileUpload $base64FileUpload): void
{
$tmpPath = sys_get_temp_dir().'/tmp_upload'.uniqid();
if (null === $base64FileUpload->getBase64fileContent()) {
throw new \RuntimeException('No base64 file content');
}
$decodedFileContent = base64_decode($base64FileUpload->getBase64fileContent());
file_put_contents($tmpPath, $decodedFileContent);
$this->addBase64tmpPath($tmpPath);
$mimeType = $this->mimeTypes->guessMimeType($tmpPath);
if (!$mimeType) {
throw new \RuntimeException('No mimeType');
}
$uploadedFile = new UploadedFile(
$tmpPath, $this->createFileName($mimeType), $mimeType, 0, true
);
$base64FileUpload->setFile($uploadedFile);
}
public function removeBase64tmpFiles(): void
{
foreach (self::$base64tmpPaths as $base64TmpPath) {
if (is_file($base64TmpPath)) {
unlink($base64TmpPath);
}
}
}
private function addBase64tmpPath(string $base64tmpPath): void
{
self::$base64tmpPaths[] = $base64tmpPath;
}
private function createFileName(string $mimeType): string
{
$extension = $this->mimeTypes->getExtensions($mimeType)[0] ??
throw new \RuntimeException('File has no extension');
return uniqid().".$extension";
}
}
<?php
declare(strict_types=1);
namespace App\FileUpload;
use Symfony\Component\HttpFoundation\File\File;
interface FileUpload
{
public function getFile(): File;
public function setFile(File $file): self;
public function getFilePath(): string;
public function setFilePath(string $filePath): self;
public function getContentUrl(): ?string;
public function setContentUrl(string $contentUrl): self;
}
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\FileUpload\Base64FileUploader;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class FileUploadEventSubscriber implements EventSubscriberInterface
{
public function __construct(private Base64FileUploader $fileUploader)
{
}
/** @return array<string, array> */
public static function getSubscribedEvents(): array
{
return [
KernelEvents::RESPONSE => ['removeBase64tmpFiles'],
];
}
public function removeBase64tmpFiles(ResponseEvent $event): void
{
$this->fileUploader->removeBase64tmpFiles();
}
}
<?php
declare(strict_types=1);
namespace App\Serializer;
use App\FileUpload\FileUpload;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Vich\UploaderBundle\Storage\StorageInterface;
final class FileUploadNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private const ALREADY_CALLED = 'FILE_UPLOAD_NORMALIZER_ALREADY_CALLED';
public function __construct(private StorageInterface $storage)
{
}
/**
* @phpstan-ignore-next-line
*/
public function normalize($object, ?string $format = null, array $context = []): array | string | int | float | bool | \ArrayObject | null
{
$context[self::ALREADY_CALLED] = true;
/* @var FileUpload $object */
$object->setContentUrl($this->storage->resolveUri($object, 'file'));
return $this->normalizer->normalize($object, $format, $context);
}
/**
* @phpstan-ignore-next-line
*/
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{
if (isset($context[self::ALREADY_CALLED])) {
return false;
}
return $data instanceof FileUpload;
}
}
<?php
//this is example Api resource implementing Base64FileUpload interface
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\FileUpload\Base64FileUpload;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\OneToOne;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* @ORM\Entity
* @ApiResource(
* normalizationContext={"groups"={"place_logo:read"}},
* denormalizationContext={"groups"={"place_logo:write"}},
* collectionOperations={
* "get",
* "post"={
* "security"="is_granted('ROLE_ADMIN')"
* },
* },
* itemOperations={
* "get",
* "delete"={
* "security"="is_granted('ROLE_ADMIN')"
* },
* },
* )
* @Vich\Uploadable
*/
class PlaceLogo implements Base64FileUpload
{
/**
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
* @ORM\Id
* @Groups({"place_logo:read","place:read"})
*/
private int $id;
/**
* @ApiProperty(iri="http://schema.org/contentUrl")
* @Groups({"place_logo:read","place:read"})
*/
private string $contentUrl;
/**
* @Assert\File(
* groups={"Default","place:write"},
* maxSize="1M", mimeTypes={"image/jpg","image/jpeg","image/png"},
* )
* @Vich\UploadableField(mapping="image_upload", fileNameProperty="filePath")
*/
private File $file;
/**
* @ORM\Column(nullable=false)
*/
private string $filePath;
/**
* @Assert\NotBlank()
* @Groups({"place_logo:write","place:write"})
*/
private ?string $base64fileContent = null;
/**
* @Assert\NotBlank()
* @Groups({"place_logo:write"})
* @OneToOne(targetEntity=Place::class, mappedBy="logo")
*/
private Place $place;
public function getId(): int
{
return $this->id;
}
public function getContentUrl(): ?string
{
return $this->contentUrl;
}
public function setContentUrl(string $contentUrl): PlaceLogo
{
$this->contentUrl = $contentUrl;
return $this;
}
public function getFile(): File
{
return $this->file;
}
public function setFile(File $file): PlaceLogo
{
$this->file = $file;
return $this;
}
public function getFilePath(): string
{
return $this->filePath;
}
public function setFilePath(string $filePath): PlaceLogo
{
$this->filePath = $filePath;
return $this;
}
public function getPlace(): Place
{
return $this->place;
}
public function setPlace(Place $place, bool $updateRelation = true): PlaceLogo
{
$this->place = $place;
if ($updateRelation) {
$place->setLogo($this, false);
}
return $this;
}
public function getBase64fileContent(): ?string
{
return $this->base64fileContent;
}
public function setBase64fileContent(?string $base64fileContent): PlaceLogo
{
$this->base64fileContent = $base64fileContent;
return $this;
}
}
services:
App\Serializer\Base64FileUploadDenormalizer:
tags:
- { name: serializer.normalizer }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment