laravel-graphql
laravel-graphql copied to clipboard
Is it possible to handle file uploads?
If so, how would you do it?
There are the following two repos I discovered some days ago:
https://github.com/jaydenseric/apollo-upload-client https://github.com/jaydenseric/apollo-upload-server
The latter could be an inspiration for an implementation for this module, or a middleware. It is simply a middleware that checks for a multi part request. Then it saves the file to a temporary directory and appends the path of the file to the context.
How does an NPM package help with handling file uploads from a php server?
You could analyze how they handle file uploading and implement it with php?
Further to the suggestion by @n1ru4l this package comes with a middleware that interacts with uploaded files on the request so the same technique could be used to hook into the image uploads.
https://github.com/spatie/laravel-image-optimizer
A quick & dirty hack rather than a full-fledged solution, but maybe it will be helpful for somebody. This is intended to work with the batching version of the apollo-upload-client network interface. The controller probably needs to be adapted in order to work with other client-side solutions.
<?php
namespace App\Http\Controllers;
use Folklore\GraphQL\GraphQLController;
use Illuminate\Http\Request;
class UploadAwareGraphQLController extends GraphQLController {
public function query(Request $request, $schema = null) {
if ($request->has('operations')) {
// request contains file uploads
$operations = json_decode($request->get('operations'), true);
$files = array_except($request->all(), ['operations']);
foreach ($files as $path => $file) {
$pathInDotNotation = str_replace('_', '.', $path);
array_set($operations, $pathInDotNotation, $file);
}
$results = [];
foreach ($operations as $operation) {
$results[] = $this->executeQuery($schema, $operation);
}
$headers = config('graphql.headers', []);
$options = config('graphql.json_encoding_options', 0);
return response()->json($results, 200, $headers, $options);
}
return parent::query($request, $schema);
}
}
<?php
namespace App\GraphQL\Type;
use GraphQL\Type\Definition\ScalarType;
class Upload extends ScalarType {
public $name = 'Upload';
public $description = 'An artifical type to model file uploads';
public function serialize($value) {
return $value->path();
}
public function parseValue($value) {
return $value;
}
public function parseLiteral($ast) {
// cannot be expressed as a literal
return null;
}
public static function getInstance() {
static $inst = null;
if ($inst === null) {
$inst = new Upload();
}
return $inst;
}
}
Then, in the config/graphql.php:
'controllers' => \App\Http\Controllers\UploadAwareGraphQLController::class.'@query',
Example mutation:
class Example extends Mutation {
protected $attributes = ['name' => 'example'];
public function args() {
return [
'file' => ['type' => Type::nonNull(Upload::getInstance())],
];
}
// the rest omitted for brevity
@anowak I'm using your example but i'm getting this: Syntax Error GraphQL (1:1) Unexpected <EOF>
Just released graphql-upload 1.0.0 which could be part of a solution to this issue.
Using @PowerKiKi library while overriding the default graphql controller, a possible solution would be this:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Folklore\GraphQL\GraphQLController as BaseGraphQLController;
use GraphQL\Upload\UploadMiddleware;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
class GraphQLController extends BaseGraphQLController
{
public function query(Request $request, $schema = null)
{
if ($request->has('operations')) {
// convert to a psr-7 request
$psr7request = (new DiactorosFactory())->createRequest($request);
// Process uploaded files
$uploadMiddleware = new UploadMiddleware();
$psr7request = $uploadMiddleware->processRequest($psr7request);
// convert back to a Illuminate\Http\Request
$httpFoundationFactory = new HttpFoundationFactory();
$request = $httpFoundationFactory->createRequest($psr7request);
$request = Request::createFromBase($request);
}
return parent::query($request, $schema);
}
}
@KevinAsher Hi! Thanks for the snippet, I am trying to use it but I am finding quite difficult to get something from the file.
Following @PowerKiKi example on https://github.com/Ecodev/graphql-upload when I get the file from args I got an UploadedFile instance that is already moved (to /tmp) and I have no idea how to get this /tmp path or how to get the stream. Because the UploadedFile interface does not have a getPath function and getStream returns that the file is already moved.
Any idea of how to get the stream or any information from the file?
Apparently doing $httpFoundationFactory->createRequest($psr7request); internally it move the file to a temporary location (/tmp/symfony*) but the file that I am getting does not know where this temporary path is and as it is already move I can't do anything with the file. Am I missing something?
@alietors, you're correct, once u convert the psr7 request to a foundation request, you won't have access to the files in the $args argument of the resolve function, but you can access the files via laravel, for example the resolve function that handles the upload can be written like this:
public function resolve($root, $args, $context, ResolveInfo $info)
{
/*
$args use to have the files, but since we transformed the psr7 request to
a foundation request, now they are only accessible via laravel's normal request.
*/
$files = request()->file(); // returns array of UploadedFile instances
}
Awesome, thanks!
I ported @PowerKiKi 's code to work with laravel directly. the repo is https://github.com/ShaneXie/laravel-graphql-upload . and please lmk if there is any bugs.
IMHO porting https://github.com/Ecodev/graphql-upload is counter-productive, because it already is based on a standard (PSR-15) that should be usable with anything, and it also support non-middleware usage. That makes it very versatile.
In this specific case the ported lib does not include any tests anymore, thus drastically reducing the guarantee of quality for users. So IMHO this port really is a downgrade and I would not recommend using it and instead focus our efforts on the original project.
@PowerKiKi thanks for your options. I strongly agree with you that the tests are essential, I will write them during this weekend, for now I just want our production quickly rolling out.
Actually I tried to use your package before I port it which require me to install 2 packages to enable psr request for laravel/lumen, so I can convert laravel request to psr request then call your code then convert the request back. That's not very convenient but still fine to use, however the file instance it returns isn't the laravel uploaded file instance which missing bunch of method I need to use. That's the main reason why I ported your package to easily use with laravel.
Also in the other word, instead of saying I ported the package, I could say my package is just an implement of graphql-multipart-request-spec for laravel, but reused your logic, and big thanks for that.
I missed the usage of Illuminate\Http\UploadedFile, which makes much more sense to have a "port" if you want to use classes that you are familiar with. Thanks for letting me know.
I took the following approach, so far it's working fine:
import axios from 'axios'
export default {
data: () => ({
form: ({
title: '',
description: '',
cover: ''
})
}),
methods: {
async onSubmit () {
let formData = new FormData()
Object.keys(this.form).map(d => {
formData.append(d, this.form[d])
})
const { data } = axios.post((`graphql/?query=
mutation+books {
newBook(
title: "${this.form.title}",
description: "${this.form.description}",
cover: ""
) {
id
}
}`).replace(/\s+/g, ''), formData, { headers: { 'Content-Type': 'multipart/form-data' } })
}
}
}
Generally, you need to pass the formData as POST data alongside the graphql query, on the resolve method: (I'm using media library)
public function resolve($root, $args, $context, ResolveInfo $info)
{
$book = Book::create($args);
if (request()->hasFile('cover')) {
$book->clearMediaCollection('books/cover');
$book->addMediaFromRequest('cover')->toMediaCollection('books/cover');
}
}
I'm fairly new to GraphQL, it doesn't seem to be the best possible way in my opinion.