laravelflutterflutter-dependenciesdioapiclient

Flutter tried to send a request of type FormData.fromMap() to Laravel but it was not read by laravel


I have a problem with the API Client using the Dio libarary, where when I try to send a text-only field I will send it using Map or Object, but when I try to send a file I use FormData, but the problem is that when I use FormData my request is not read by Laravel.

    Future<dynamic> _processRequestBody(dynamic body) async {
        if (body == null) return null;

        if (body is! Map<String, dynamic>) return body;

        bool hasFiles = false;
        final formMap = <String, dynamic>{};

        Future<List<MultipartFile>> processMultipleFiles(List<PlatformFile> files) async {
          return Future.wait(files.map((file) => MultipartFile.fromFile(
            file.path!,
            filename: file.name,
          )));
        }

        Future<MultipartFile> processSingleFile(PlatformFile file) async {
          return MultipartFile.fromFile(
            file.path!,
            filename: file.name,
          );
        }

        for (var entry in body.entries) {
          final value = entry.value;

          try {
            if (value is List<PlatformFile>) {
              hasFiles = true;
              if (value.length == 1) {
                formMap[entry.key] = await processSingleFile(value.first);
              } else {
                formMap[entry.key] = await processMultipleFiles(value);
              }
            } else if (value is PlatformFile) {
              hasFiles = true;
              formMap[entry.key] = await processSingleFile(value);
            } else if (value != null) {
              formMap[entry.key] = value.toString();
            }
          } catch (e) {
            print('Error processing field ${entry.key}: $e');
            rethrow;
          }
        }

        print(formMap);
        return hasFiles ? FormData.fromMap(formMap) : body;
    }

Handling PUT request

    Future<Response> put(
          String path, {
            dynamic body,
            Map<String, dynamic>? queryParameters,
            Options? options,
            bool unusedAuth = false,
          }) async {
        try {
          final requestOptions = options ?? Options();
          requestOptions.extra = {
            ...requestOptions.extra ?? {},
            'unusedAuth': unusedAuth,
          };

          final processedBody = await _processRequestBody(body);

          if (processedBody is FormData) {
            requestOptions.headers = {
              ...requestOptions.headers ?? {},
              'Content-Type': 'multipart/form-data',
            };
          }

          final response = await _dio.put(
            path,
            data: processedBody,
            queryParameters: queryParameters,
            options: requestOptions,
          );
          return response;
        } on DioException catch (e) {
          throw _handleError(e);
        }
    }

Using API Client example

    Future<BaseResponse> updateProfile(UserProfileData formData) async {
          final response = await _apiClient.put(
            ApiPath.updateProfile,
            body: formData.toJson(),
          );
    }

In Laravel

    public function editProfileUser(Request $request)
    {
        \Log::info('All request data:', $request->all());
        \Log::info('Files:', $request->allFiles());
        \Log::info('Has file test:', [
            'hasFile' => $request->hasFile('your_file_field_name'),
            'files' => $request->files->all()
        ]);

        return $this->response(500, "Error", null);
    }

Results Log

    \[2024-11-03 00:01:04\] local.INFO: All request data:  
    \[2024-11-03 00:01:04\] local.INFO: Files:
    \[2024-11-03 00:01:04\] local.INFO: Has file test: {"hasFile":false,"files":\[\]}

Everything is empty when using FormData.fromMap. But when my input contains all text then it can

Additional information: Laravel version: 10.48.22 Flutter version: 3.24.0 Dio version: ^5.7.0 My return files are all in the form of List<PlatformFile>

You can see the full API Client code on my drive https://drive.google.com/file/d/1Jhk1gLhCySvfCrRlgqNNc4NDdJu9uNzk/view?usp=drive_link


Solution

  • It appears that this is a well-known issue not with Laravel but with PHP. The solution appears to be to send a POST instead of a PUT or PATCH with a _method: PUT in the form body. You can add this additional field in the body while processing or later while making the request.

    1. Use post instead of put:
       final response = await _dio.post(
                path,
                data: processedBody,
                queryParameters: queryParameters,
                options: requestOptions,
              );
    
    1. In _processRequestBody:
    return hasFiles
          ? FormData.fromMap(formMap..['_method'] = 'PUT')
          : {
              ...body,
              "_method": "PUT",
            };
    

    Note: the ..['method'] syntax is the casacade operator and is equivalent to:

    formMap['method'] = 'PUT'
    FormData.fromMap(formMap)
    

    Suggestion: Refactor _processRequestBody

    You can leverage collection-for and Dart 3's pattern matching to refactor _processRequestBody to a more concise version:

    Future<dynamic> _processRequestBody(dynamic body) async {
      bool hasFiles = false;
    
      Future<List<MultipartFile>> processMultipleFiles(
          List<PlatformFile> files) async {
        hasFiles = true;
        return Future.wait(files.map((file) => MultipartFile.fromFile(
              file.path!,
              filename: file.name,
            )));
      }
    
      Future<MultipartFile> processSingleFile(PlatformFile file) async {
        hasFiles = true;
        return MultipartFile.fromFile(
          file.path!,
          filename: file.name,
        );
      }
    
      switch (body) {
        case null:
          return null;
        case Map<String, dynamic> map:
          try {
            final formMap = {
              for (var entry in map.entries)
                entry.key: switch (entry.value) {
                  PlatformFile file => processSingleFile(file),
                  [PlatformFile file] => processSingleFile(file),
                  [...List<PlatformFile> files] => processMultipleFiles(files),
                  final value => value?.toString(),
                },
              "_method": "PUT",
            };
    
            return hasFiles
                ? FormData.fromMap(formMap)
                : {
                    ...map,
                    "_method": "PUT",
                  };
          } catch (e) {
            debugPrint('Error processing map: $e');
            rethrow;
          }
    
        default:
          return body;
      }
    }