Skip to content

feat: replace the deprecated file upload functions with the FormFileUploader and ManagedFileElementHelper services (#433) - #434

Open
bbrala wants to merge 1 commit into
feat/user-login-logout-finalize-2012976from
feat/file-save-upload-3375423
Open

bbrala wants to merge 1 commit into
feat/user-login-logout-finalize-2012976from
feat/file-save-upload-3375423

Conversation

@bbrala

@bbrala bbrala commented Sep 20, 2026

Copy link
Copy Markdown
Collaborator

Closes #433.

Adds ReplaceFileSaveUploadFunctionsRector to the Drupal 11.5 deprecation set. file_save_upload(), file_managed_file_save_upload() and _file_save_upload_from_form() are deprecated in drupal:11.5.0 and removed in drupal:13.0.0 (#3375423, change record, core e8424495f0d).

Stacked on #432 (feat/user-login-logout-finalize-2012976) for the 11.5 set — review that one first; the diff here is only the new rule.

file_save_upload('upload', $validators, FALSE, 0);
// ->
\Drupal::service(\Drupal\file\Upload\FormFileUploader::class)
  ->saveFormUploadedFiles('upload', $validators, 'temporary://', 0);

file_managed_file_save_upload($element, $form_state);
// ->
\Drupal::service(\Drupal\file\Upload\ManagedFileElementHelper::class)
  ->managedFileSaveUpload($element, $form_state);

_file_save_upload_from_form($element, $form_state, 0);
// ->
\Drupal::service(\Drupal\file\Upload\ManagedFileElementHelper::class)
  ->saveFileUploads($element, $form_state, 0);

Why a custom rule and not two FunctionToServiceRector entries

Core moved each function body into the new service verbatim, so the argument order carries over — but the deprecated wrapper also normalised its arguments, which the service methods do not:

  • $destination. file_save_upload() turned a FALSE/NULL $destination into 'temporary://' before delegating, and saveFormUploadedFiles() types that parameter string. A verbatim pass-through hands the service a bool and fatals with a TypeError. This is the dominant contrib shape — a literal FALSE/NULL third argument appears in 47 files across the contrib index — so the rule rewrites the literal.
  • $fileExists. Both file_save_upload() and _file_save_upload_from_form() ran a legacy int through FileExists::fromLegacyInt(), while the service parameters accept only the enum. A FileSystemInterface::EXISTS_* constant is already converted by the drupal-10.3 set, so the only shape left is a bare integer literal; such a call is skipped rather than rewritten into a TypeError. No contrib project passes one today.

Calls with named arguments, argument unpacking, or an argument count the deprecated functions never had are skipped as well.

BC

BC-wrapped. FormFileUploader and ManagedFileElementHelper arrive with the deprecation commit itself — git grep at e8424495f0d~1 finds neither — so the rewritten call fatals on Drupal < 11.5. Neither the deprecated functions nor the service methods take a parameter by reference, so the wrapper emits arrow functions, and all three methods return a value, so there is no phpstan function.void.

Verification

Live-tested against D11-compatible contrib on core 11.x-dev (version forced to 11.5.0, since core still reports 11.4-dev): 8 call sites rewritten across gin_login (27,767 installs), media_entity_file_replace (23,146), tmgmt (11,680), file_replace (10,008), paragraphs_browser (9,901), responsive_favicons (4,017), gutenberg (2,790) and file_history — covering all three functions and the FALSE, NULL, variable-destination and FileExists::Replace argument shapes. plupload produced no change: its only occurrence is an @see docblock, correctly left alone.

Verified idempotent with importNames(true) — a second pass is a no-op.

PHPSTAN_MESSAGES were captured from those modules and normalised with scripts/normalize-phpstan-message.php; docs/coverage-registry.php is regenerated.

Test suite (707 tests, 973 assertions), phpstan and php-cs-fixer are all green.

file_save_upload(), file_managed_file_save_upload() and
_file_save_upload_from_form() are deprecated in drupal:11.5.0 and removed in
drupal:13.0.0 (https://www.drupal.org/i/3375423, change record
https://www.drupal.org/node/3382414). Core moved each function body into a new
service (drupal-core e8424495f0d) and left the function delegating to it:

  file_save_upload($name, $validators, $destination, $delta, $fileExists)
  -> \Drupal::service(\Drupal\file\Upload\FormFileUploader::class)
       ->saveFormUploadedFiles(...)

  file_managed_file_save_upload($element, $form_state)
  -> \Drupal::service(\Drupal\file\Upload\ManagedFileElementHelper::class)
       ->managedFileSaveUpload(...)

  _file_save_upload_from_form($element, $form_state, $delta, $fileExists)
  -> \Drupal::service(\Drupal\file\Upload\ManagedFileElementHelper::class)
       ->saveFileUploads(...)

Not a FunctionToServiceRector config entry, because the wrapper also normalised
its arguments where the service methods do not. file_save_upload() turned a
FALSE/NULL $destination into 'temporary://' before delegating, and
saveFormUploadedFiles() types that parameter string — a verbatim pass-through
would hand the service a bool and fatal. That is the dominant contrib shape: a
literal FALSE or NULL third argument appears in 47 files across the contrib
index (tmgmt, file_replace, paragraphs_browser, bootstrap_italia, dxpr_theme,
site_verify, micon, ...), so the rector rewrites the literal.

Both functions also ran a legacy int $fileExists through
FileExists::fromLegacyInt(), while the service parameters accept only the enum.
A FileSystemInterface::EXISTS_* constant is already converted by the
drupal-10.3 set, so the only shape left is a bare integer literal; such a call
is skipped rather than rewritten into a TypeError. No contrib project passes
one today.

BC-wrapped: FormFileUploader and ManagedFileElementHelper arrive with the
deprecation itself (git grep at e8424495f0d~1 finds neither), so the rewritten
call fatals on Drupal < 11.5. Neither the deprecated functions nor the service
methods take a parameter by reference, so the wrapper emits arrow functions,
and all three methods return a value, so no phpstan function.void.

Live-tested against D11-compatible contrib on core 11.x-dev: gin_login (27,767
installs), media_entity_file_replace (23,146), tmgmt (11,680), file_replace
(10,008), paragraphs_browser (9,901), responsive_favicons (4,017), gutenberg
(2,790), file_history — 8 call sites rewritten across all three functions,
including the FALSE and NULL destination shapes and a call already using
FileExists::Replace. plupload was a no-change: its only occurrence is an @see
docblock, correctly left alone. Verified idempotent with importNames(true) — a
second pass is a no-op.

PHPSTAN_MESSAGES captured from those modules and normalised with
scripts/normalize-phpstan-message.php; docs/coverage-registry.php regenerated.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant