Datei-Uploads über GraphQL
Datei-Uploads über GraphQL
~8 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026
Ein naheliegender nächster Schritt für das Veranstaltungen-Projekt: ein Titelbild pro Veranstaltung. GraphQL selbst kennt aber keinen nativen Mechanismus für klassische multipart/form-data-Datei-Uploads wie ein HTML-Formular - jede GraphQL-Anfrage ist strukturiert als JSON. Dieses Kapitel zeigt den in Magento üblichen Weg: Base64-kodierte Bilddaten als String-Argument.
Warum kein klassischer Multipart-Upload?
Der /graphql-Endpunkt erwartet grundsätzlich einen JSON-Body mit query/variables - es gibt keine dedizierte Content-Type-Behandlung für multipart/form-data wie beim GraphQL-Multipart-Request-Community-Standard mancher anderer GraphQL-Server. Magentos eigene Lösung für binäre Daten (Produktbilder, Kundenavatare) ist durchgängig: Datei-Inhalt als Base64-String im JSON-Payload übertragen, serverseitig dekodieren und regulär über die Filesystem-Abstraktion speichern.
Die Mutation im Schema
type Mutation {
uploadEventImage(
input: UploadEventImageInput!
): UploadEventImageOutput
@resolver(class: "Mironsoft\\Event\\Model\\Resolver\\UploadEventImage")
@doc(description: "Uploads a base64-encoded title image for an event")
}
input UploadEventImageInput @doc(description: "Input for uploadEventImage") {
event_id: Int!
file_name: String!
base64_encoded_data: String!
}
type UploadEventImageOutput @doc(description: "Result of uploadEventImage") {
image_url: String
}Der Resolver: validieren, dekodieren, speichern
<?php
declare(strict_types=1);
namespace Mironsoft\Event\Model\Resolver;
use Magento\Framework\Filesystem;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Exception\GraphQlInputException;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
/**
* Resolves the uploadEventImage mutation field.
*/
class UploadEventImage implements ResolverInterface
{
private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'];
private const MAX_BYTES = 2 * 1024 * 1024;
private const UPLOAD_SUBDIR = 'mironsoft/event';
/**
* @param Filesystem $filesystem Magento filesystem abstraction
*/
public function __construct(
private readonly Filesystem $filesystem,
) {
}
/**
* Decodes and stores a base64-encoded event image.
*
* @param Field $field Resolved GraphQL field configuration
* @param mixed $context Resolver context
* @param ResolveInfo $info GraphQL resolve tree info
* @param array|null $value Parent resolver's value, unused for a top-level field
* @param array|null $args Arguments passed to the uploadEventImage field
* @return array<string, mixed>
* @throws GraphQlInputException
*/
public function resolve(
Field $field,
$context,
ResolveInfo $info,
?array $value = null,
?array $args = null
): array {
$fileName = (string) ($args['input']['file_name'] ?? '');
$base64 = (string) ($args['input']['base64_encoded_data'] ?? '');
$extension = strtolower((string) pathinfo($fileName, PATHINFO_EXTENSION));
if (!in_array($extension, self::ALLOWED_EXTENSIONS, true)) {
throw new GraphQlInputException(
__('Only jpg, jpeg, png, and webp images are allowed.')
);
}
$binaryData = base64_decode($base64, true);
if ($binaryData === false || $binaryData === '') {
throw new GraphQlInputException(__('The uploaded data is not valid base64.'));
}
if (strlen($binaryData) > self::MAX_BYTES) {
throw new GraphQlInputException(__('The image must not exceed 2 MB.'));
}
$safeName = bin2hex(random_bytes(8)) . '.' . $extension;
$mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
$relativePath = self::UPLOAD_SUBDIR . '/' . $safeName;
$mediaDirectory->writeFile($relativePath, $binaryData);
return [
'image_url' => '/media/' . $relativePath,
];
}
}Achtung: $fileName fließt ausschließlich zur Ermittlung der Dateiendung ein - niemals direkt als Zieldateiname. Der tatsächlich gespeicherte Name ($safeName) wird serverseitig zufällig neu generiert. Ohne diesen Schritt könnte ein Client über einen präparierten Dateinamen wie ../../etc/passwd.jpg einen Path-Traversal-Angriff versuchen - die ALLOWED_EXTENSIONS-Prüfung allein schützt davor nicht, erst die komplette Neuvergabe des Dateinamens tut das zuverlässig.
Die Mutation aufrufen
{
"query": "mutation($eventId: Int!, $data: String!) { uploadEventImage(input: { event_id: $eventId, file_name: \"banner.png\", base64_encoded_data: $data }) { image_url } }",
"variables": {
"eventId": 3,
"data": "iVBORw0KGgoAAAANSUhEUgAA..."
}
}Tipp: Base64-Kodierung vergrößert die Nutzlast um rund ein Drittel gegenüber der Originaldatei - eine 1,5-MB-Bilddatei wird so leicht zu 2 MB JSON-Text. Sowohl webapi/graphql/max_request_size in der PHP-Konfiguration (post_max_size, memory_limit) als auch die eigene MAX_BYTES-Prüfung im Resolver sollten diesen Aufschlag berücksichtigen.
Mit Bild-Uploads für Veranstaltungen abgedeckt, widmet sich Kapitel 23 dem letzten Baustein von Block 6: ACL-Prüfungen für eigene GraphQL-Endpunkte, am Beispiel eines admin-only nutzbaren Auswertungs-Felds.