Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ release-by-release.
- **Drupal 11.5 deprecation set** — added `Drupal11SetList::DRUPAL_115` (`config/drupal-11/drupal-11.5-deprecations.php`), registered in `drupal-11-all-deprecations.php` and in the composer-based `DrupalSetProvider`, so a site on `drupal/core ^11.5` picks it up automatically.
- **`ReplaceUserRolePermissionFunctionsRector`** — rewrites `user_role_grant_permissions()`, `user_role_revoke_permissions()` and `user_role_change_permissions()`, deprecated in drupal:11.5.0 and removed in drupal:13.0.0 ([#2025089](https://www.drupal.org/node/2025089), [change record](https://www.drupal.org/node/3348027)), to the matching `\Drupal\user\RoleInterface` method on a role loaded with `\Drupal\user\Entity\Role::loadOverrideFree()` — the nullsafe chain core itself adopted in `media_install()` and `node_install()`. BC-wrapped, because `loadOverrideFree()` is new in 11.5.0 ([#3620216](https://www.drupal.org/i/3620216)) and the rewritten chain would fatal on older minors. Calls with named arguments, argument unpacking or an argument count the deprecated functions never had are left untouched.
- **`user_login_finalize()` / `user_logout()` → `LoginFinalizer` / `LogoutFinalizer`** — two `FunctionToServiceRector` entries in the Drupal 11.5 set rewrite the procedural login/logout functions, deprecated in drupal:11.5.0 and removed in drupal:13.0.0 ([#2012976](https://www.drupal.org/node/2012976), [change record](https://www.drupal.org/node/3379194)), to `\Drupal::service(\Drupal\user\LoginFinalizer::class)->finalizeLogin($account)` and `\Drupal::service(\Drupal\user\LogoutFinalizer::class)->finalizeLogout()`. Both service classes arrive with the deprecation itself, so the rewrite is BC-wrapped. The mapping is 1-to-1 — core moved both function bodies verbatim — with one caveat: the service parameter is `$user` where the function's was `$account`, so a named argument would not survive the rewrite. No contrib project calls either function that way.
- **`ReplaceFileSaveUploadFunctionsRector`** — rewrites `file_save_upload()`, `file_managed_file_save_upload()` and `_file_save_upload_from_form()`, deprecated in drupal:11.5.0 and removed in drupal:13.0.0 ([#3375423](https://www.drupal.org/node/3375423), [change record](https://www.drupal.org/node/3382414)), to `\Drupal::service(\Drupal\file\Upload\FormFileUploader::class)->saveFormUploadedFiles()` and the matching `\Drupal\file\Upload\ManagedFileElementHelper` methods. BC-wrapped, because both service classes arrive with the deprecation itself. The deprecated functions also normalised their arguments, which the service methods do not: a literal `FALSE`/`NULL` `$destination` is rewritten to `'temporary://'` (the shape 47 contrib files use) because the new parameter is typed `string`, and a call passing an integer literal for `$fileExists` is left untouched because the new parameter only accepts the `\Drupal\Core\File\FileExists` enum — a `FileSystemInterface::EXISTS_*` constant is already converted by the Drupal 10.3 set. Calls with named arguments, argument unpacking or an argument count the deprecated functions never had are skipped.

## [1.1.3] — 2026-09-03

Expand Down
29 changes: 29 additions & 0 deletions config/drupal-11/drupal-11.5-deprecations.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

declare(strict_types=1);

use DrupalRector\Drupal11\Rector\Deprecation\ReplaceFileSaveUploadFunctionsRector;
use DrupalRector\Drupal11\Rector\Deprecation\ReplaceUserRolePermissionFunctionsRector;
use DrupalRector\Rector\Deprecation\FunctionToServiceRector;
use DrupalRector\Rector\ValueObject\DrupalIntroducedVersionConfiguration;
Expand Down Expand Up @@ -53,4 +54,32 @@
new FunctionToServiceConfiguration('11.5.0', 'user_login_finalize', 'Drupal\user\LoginFinalizer', 'finalizeLogin', true),
new FunctionToServiceConfiguration('11.5.0', 'user_logout', 'Drupal\user\LogoutFinalizer', 'finalizeLogout', true),
]);

// https://www.drupal.org/node/3375423
// https://www.drupal.org/node/3382414 (change record)
//
// file_save_upload(), file_managed_file_save_upload() and
// _file_save_upload_from_form() deprecated in drupal:11.5.0, removed in
// drupal:13.0.0. Each function body moved into the new
// \Drupal\file\Upload\FormFileUploader or
// \Drupal\file\Upload\ManagedFileElementHelper service
// (drupal-core e8424495f0d), leaving the function a delegating wrapper, so
// the argument order carries over unchanged.
//
// Not config-only, because the wrapper also normalised its arguments:
// file_save_upload() turned a FALSE/NULL $destination into 'temporary://'
// before delegating, and saveFormUploadedFiles() types that parameter
// string. That is the dominant contrib shape — a literal FALSE or NULL
// third argument appears in 47 files across the contrib index — so a
// verbatim FunctionToServiceRector pass-through would hand the service a
// bool and fatal with a TypeError.
//
// BC-wrapped: both services arrive with the deprecation itself, so the
// rewritten call fatals on Drupal < 11.5.
//
// PHPSTAN_MESSAGES live on the rector class itself
// (ReplaceFileSaveUploadFunctionsRector::PHPSTAN_MESSAGES).
$rectorConfig->ruleWithConfiguration(ReplaceFileSaveUploadFunctionsRector::class, [
new DrupalIntroducedVersionConfiguration('11.5.0'),
]);
};
6 changes: 6 additions & 0 deletions docs/coverage-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
array (
0 => 'Call to deprecated method expectDeprecation() of class Drupal\\KernelTests\\KernelTestBase. Deprecated in drupal:11.4.0 and is removed from drupal:12.0.0. Use $this->expectUserDeprecationMessage() or $this->expectUserDeprecationMessageMatches() instead.',
),
'ReplaceFileSaveUploadFunctionsRector' =>
array (
0 => 'Call to deprecated function _file_save_upload_from_form(). Deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use Drupal::service(ManagedFileElementHelper::class)->saveFileUploads() instead.',
1 => 'Call to deprecated function file_managed_file_save_upload(). Deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use Drupal::service(ManagedFileElementHelper::class)->managedFileSaveUpload() instead.',
2 => 'Call to deprecated function file_save_upload(). Deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use Drupal::service(FormFileUploader::class)->saveFormUploadedFiles() instead.',
),
'ReplaceLocaleBatchProceduralFunctionsRector' =>
array (
0 => 'Call to deprecated function _locale_translation_batch_status_operations(). Deprecated in drupal:11.4.0 and is removed from drupal:13.0.0. Use Drupal::service(LocaleFetch::class)->getStatusOperations() instead.',
Expand Down
18 changes: 18 additions & 0 deletions docs/implemented-digests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,24 @@ digests:
phase: '2'
class: [RemoveConfigSaveTrustedDataArgRector, RemoveTrustDataCallRector]
digest_file: remove-deprecated-trusted-data-concept-from-drupal-config-3347842.php
'3375423':
status: implemented
phase: '1a'
class: ReplaceFileSaveUploadFunctionsRector
digest_file: replace-deprecated-file-upload-functions-with-service-calls-3375423.php
note: >-
BC-wrapped. file_save_upload() -> \Drupal\file\Upload\FormFileUploader
::saveFormUploadedFiles(), file_managed_file_save_upload() and
_file_save_upload_from_form() -> \Drupal\file\Upload\ManagedFileElementHelper
::managedFileSaveUpload() / ::saveFileUploads() (drupal-core e8424495f0d).
Not config-only: the deprecated wrapper normalised a FALSE/NULL $destination
to 'temporary://' (47 contrib files pass that literal) and ran a legacy int
$fileExists through FileExists::fromLegacyInt(), while the service parameters
are typed string and FileExists. The rector rewrites the destination literal
and skips a call passing an integer literal for $fileExists; a
FileSystemInterface::EXISTS_* constant is already converted by the
drupal-10.3 set. Implemented on feat/file-save-upload-3375423, stacked on
#432.
'3396062':
status: implemented
phase: '2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
<?php

declare(strict_types=1);

namespace DrupalRector\Drupal11\Rector\Deprecation;

use DrupalRector\Contract\VersionedConfigurationInterface;
use DrupalRector\Rector\AbstractDrupalCoreRector;
use DrupalRector\Rector\ValueObject\DrupalIntroducedVersionConfiguration;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\Int_;
use PhpParser\Node\Scalar\String_;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* Replaces the deprecated form file upload functions with their services.
*
* Handles file_save_upload(), file_managed_file_save_upload() and
* _file_save_upload_from_form(). Core moved each function body into
* \Drupal\file\Upload\FormFileUploader or
* \Drupal\file\Upload\ManagedFileElementHelper and left the function as a
* delegating wrapper, so the argument order carries over unchanged.
*
* The one thing the wrapper did that the service method does not is normalise
* its arguments: file_save_upload() turned a FALSE/NULL $destination into
* 'temporary://' before delegating, and both it and
* _file_save_upload_from_form() ran a legacy integer $fileExists through
* \Drupal\Core\File\FileExists::fromLegacyInt(). The service parameters are
* typed string and FileExists, so this rector rewrites a literal FALSE/NULL
* destination and skips a call that passes an integer literal for $fileExists.
*
* Deprecated in drupal:11.5.0 and removed in drupal:13.0.0.
*
* @see https://www.drupal.org/node/3375423
* @see https://www.drupal.org/node/3382414
*/
class ReplaceFileSaveUploadFunctionsRector extends AbstractDrupalCoreRector
{
/**
* The PHPStan deprecation messages this rector covers.
*
* Captured against drupal/core 11.x-dev via tmgmt, file_history and
* gin_login, normalised with scripts/normalize-phpstan-message.php.
*/
public const PHPSTAN_MESSAGES = [
'Call to deprecated function file_save_upload(). Deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use Drupal::service(FormFileUploader::class)->saveFormUploadedFiles() instead.',
'Call to deprecated function file_managed_file_save_upload(). Deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use Drupal::service(ManagedFileElementHelper::class)->managedFileSaveUpload() instead.',
'Call to deprecated function _file_save_upload_from_form(). Deprecated in drupal:11.5.0 and is removed from drupal:13.0.0. Use Drupal::service(ManagedFileElementHelper::class)->saveFileUploads() instead.',
];

private const FORM_FILE_UPLOADER = 'Drupal\file\Upload\FormFileUploader';

private const MANAGED_FILE_ELEMENT_HELPER = 'Drupal\file\Upload\ManagedFileElementHelper';

/**
* Maps each deprecated function to its service class, method and arity.
*
* The argument counts are core's own: anything outside that range is not a
* call to the deprecated function as it was ever declared.
*
* @var array<string, array{class: string, method: string, minArgs: int, maxArgs: int, fileExistsPosition: int|null, destinationPosition: int|null}>
*/
private const FUNCTION_MAP = [
'file_save_upload' => [
'class' => self::FORM_FILE_UPLOADER,
'method' => 'saveFormUploadedFiles',
'minArgs' => 1,
'maxArgs' => 5,
'fileExistsPosition' => 4,
'destinationPosition' => 2,
],
'file_managed_file_save_upload' => [
'class' => self::MANAGED_FILE_ELEMENT_HELPER,
'method' => 'managedFileSaveUpload',
'minArgs' => 2,
'maxArgs' => 2,
'fileExistsPosition' => null,
'destinationPosition' => null,
],
'_file_save_upload_from_form' => [
'class' => self::MANAGED_FILE_ELEMENT_HELPER,
'method' => 'saveFileUploads',
'minArgs' => 2,
'maxArgs' => 4,
'fileExistsPosition' => 3,
'destinationPosition' => null,
],
];

/** @var DrupalIntroducedVersionConfiguration[] */
protected array $configuration;

public function configure(array $configuration): void
{
foreach ($configuration as $value) {
if (!$value instanceof DrupalIntroducedVersionConfiguration) {
throw new \InvalidArgumentException(sprintf('Each configuration item must be an instance of "%s"', DrupalIntroducedVersionConfiguration::class));
}
}
parent::configure($configuration);
}

/** @return array<class-string<Node>> */
public function getNodeTypes(): array
{
return [FuncCall::class];
}

protected function refactorWithConfiguration(Node $node, VersionedConfigurationInterface $configuration): ?Node
{
assert($node instanceof FuncCall);

if (!$node->name instanceof Name) {
return null;
}

$replacement = self::FUNCTION_MAP[$node->name->toString()] ?? null;
if ($replacement === null) {
return null;
}

$args = $node->args;
$argCount = count($args);
if ($argCount < $replacement['minArgs'] || $argCount > $replacement['maxArgs']) {
return null;
}

// Named arguments and argument unpacking are too rare here to be worth
// resolving into positional order.
foreach ($args as $arg) {
if (!$arg instanceof Arg || $arg->name !== null || $arg->unpack) {
return null;
}
}

// The deprecated functions accepted FileExists|int and converted a
// legacy integer with FileExists::fromLegacyInt(); the service methods
// only accept the enum. A FileSystemInterface::EXISTS_* constant is
// rewritten to its enum case by the drupal-10.3 set, so the only shape
// left is a bare integer literal, which is left alone for a human.
$fileExistsPosition = $replacement['fileExistsPosition'];
if ($fileExistsPosition !== null && isset($args[$fileExistsPosition]) && $args[$fileExistsPosition]->value instanceof Int_) {
return null;
}

// file_save_upload() normalised a FALSE/NULL $destination to
// 'temporary://'; saveFormUploadedFiles() types the parameter string.
$destinationPosition = $replacement['destinationPosition'];
if ($destinationPosition !== null && isset($args[$destinationPosition]) && $this->isFalseOrNull($args[$destinationPosition]->value)) {
$args[$destinationPosition] = new Arg(new String_('temporary://'));
}

$serviceCall = new StaticCall(
new FullyQualified('Drupal'),
'service',
[new Arg(new ClassConstFetch(new FullyQualified($replacement['class']), 'class'))]
);

return new MethodCall($serviceCall, $replacement['method'], $args);
}

private function isFalseOrNull(Node\Expr $expr): bool
{
if (!$expr instanceof ConstFetch) {
return false;
}

return in_array(strtolower($expr->name->toString()), ['false', 'null'], true);
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Replace file_save_upload(), file_managed_file_save_upload() and _file_save_upload_from_form() with the \Drupal\file\Upload\FormFileUploader and \Drupal\file\Upload\ManagedFileElementHelper services.',
[
new ConfiguredCodeSample(
"\$file = file_save_upload('upload', \$validators, FALSE, 0);",
"\$file = \\Drupal::service(\\Drupal\\file\\Upload\\FormFileUploader::class)->saveFormUploadedFiles('upload', \$validators, 'temporary://', 0);",
[new DrupalIntroducedVersionConfiguration('11.5.0')]
),
new ConfiguredCodeSample(
'$files = file_managed_file_save_upload($element, $form_state);',
'$files = \Drupal::service(\Drupal\file\Upload\ManagedFileElementHelper::class)->managedFileSaveUpload($element, $form_state);',
[new DrupalIntroducedVersionConfiguration('11.5.0')]
),
new ConfiguredCodeSample(
'$result = _file_save_upload_from_form($element, $form_state, 0);',
'$result = \Drupal::service(\Drupal\file\Upload\ManagedFileElementHelper::class)->saveFileUploads($element, $form_state, 0);',
[new DrupalIntroducedVersionConfiguration('11.5.0')]
),
]
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace DrupalRector\Tests\Drupal11\Rector\Deprecation\ReplaceFileSaveUploadFunctionsRector;

use DrupalRector\Services\DrupalRectorSettings;
use DrupalRector\Tests\AbstractDrupalRectorTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class ReplaceFileSaveUploadFunctionsRectorTest extends AbstractDrupalRectorTestCase
{
#[DataProvider('provideData')]
public function testAboveVersion(string $filePath): void
{
static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('99.99.99');
$this->doTestFile($filePath);
}

/**
* @return \Iterator<<string>>
*/
public static function provideData(): \Iterator
{
return self::yieldFilesFromDirectory(__DIR__.'/fixture');
}

#[DataProvider('provideDataBelowVersion')]
public function testBelowVersion(string $filePath): void
{
static::getContainer()->make(DrupalRectorSettings::class)->setDrupalVersion('1.0.0');
$this->doTestFile($filePath);
}

/**
* @return \Iterator<<string>>
*/
public static function provideDataBelowVersion(): \Iterator
{
return self::yieldFilesFromDirectory(__DIR__.'/fixture-below-version');
}

public function provideConfigFilePath(): string
{
return __DIR__.'/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

use DrupalRector\Drupal11\Rector\Deprecation\ReplaceFileSaveUploadFunctionsRector;
use DrupalRector\Rector\ValueObject\DrupalIntroducedVersionConfiguration;
use DrupalRector\Tests\Rector\Deprecation\DeprecationBase;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
DeprecationBase::addClass(ReplaceFileSaveUploadFunctionsRector::class, $rectorConfig, false, [
new DrupalIntroducedVersionConfiguration('11.5.0'),
]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

$file = file_save_upload('upload', $validators, FALSE, 0);
$files = file_managed_file_save_upload($element, $form_state);
$result = _file_save_upload_from_form($element, $form_state, 0);
?>
-----
<?php

$file = file_save_upload('upload', $validators, FALSE, 0);
$files = file_managed_file_save_upload($element, $form_state);
$result = _file_save_upload_from_form($element, $form_state, 0);
?>
Loading
Loading