Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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

app/code/Mironsoft/Event/etc/schema.graphqls
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

app/code/Mironsoft/Event/Model/Resolver/UploadEventImage.php
<?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.