Merge pull request #17273 from filamentphp/upgrade-directory-structure-command

v4 stable docs & new upgrade directory structure command
This commit is contained in:
Dan Harrin
2025-08-12 10:26:54 +01:00
committed by GitHub
6 changed files with 433 additions and 48 deletions
-28
View File
@@ -38,20 +38,6 @@ Installation comes in two flavors, depending on whether you want to build an app
## Installing the panel builder
Since Filament v4 is in beta, you will need to set the `minimum-stability` in your `composer.json` file to be `beta` before installing any packages. Either adjust it manually or via CLI:
```bash
composer config minimum-stability beta
```
Your `composer.json` should look like this:
```json
{
"minimum-stability": "beta"
}
```
Install the Filament Panel Builder by running the following commands in your Laravel project directory:
```bash
@@ -94,20 +80,6 @@ Open `/admin` in your web browser, sign in, and [start building your app](../get
## Installing the individual components
Since Filament v4 is in beta, you will need to set the `minimum-stability` in your `composer.json` file to be `beta` before installing any packages. Either adjust it manually or via CLI:
```bash
composer config minimum-stability beta
```
Your `composer.json` should look like this:
```json
{
"minimum-stability": "beta"
}
```
Install the Filament components you want to use with Composer:
```bash
+47 -19
View File
@@ -23,26 +23,20 @@ import Disclosure from "@components/Disclosure.astro"
The upgrade script is not a replacement for the upgrade guide. It handles many small changes that are not mentioned in the upgrade guide, but it does not handle all breaking changes. You should still read the [manual upgrade steps](#breaking-changes-that-must-be-handled-manually) to see what changes you need to make to your code.
</Aside>
The first step to upgrade your Filament app is to run the automated upgrade script. Since Filament v4 is in beta, you will need to set the `minimum-stability` in your `composer.json` file to be `beta` before installing any packages. Either adjust it manually or via CLI:
<Aside variant="info">
Some plugins you're using may not be available in v4 just yet. You could temporarily remove them from your `composer.json` file until they've been upgraded, replace them with a similar plugins that are v4-compatible, wait for the plugins to be upgraded before upgrading your app, or even write PRs to help the authors upgrade them.
</Aside>
```bash
composer config minimum-stability beta
```
Your `composer.json` should look like this:
```json
{
"minimum-stability": "beta"
}
```
This script will automatically upgrade your application to the latest version of Filament and make changes to your code, which handles most breaking changes:
The first step to upgrade your Filament app is to run the automated upgrade script. This script will automatically upgrade your application to the latest version of Filament and make changes to your code, which handles most breaking changes:
```bash
composer require filament/upgrade:"^4.0" -W --dev
vendor/bin/filament-v4
# Run the commands output by the upgrade script, they are unique to your app
composer require filament/filament:"^4.0" -W --no-update
composer update
```
<Aside variant="warning">
@@ -52,6 +46,10 @@ vendor/bin/filament-v4
composer require filament/upgrade:"~4.0" -W --dev
vendor/bin/filament-v4
# Run the commands output by the upgrade script, they are unique to your app
composer require filament/filament:"^4.0" -W --no-update
composer update
```
</Aside>
@@ -61,12 +59,24 @@ vendor/bin/filament-v4
Make sure to carefully follow the instructions, and review the changes made by the script. You may need to make some manual changes to your code afterwards, but the script should handle most of the repetitive work for you.
You can now `composer remove filament/upgrade` as you don't need it anymore.
Filament v4 introduces a new default directory structure for your Filament resources and clusters. If you are using Filament panels with resources and clusters, you can choose to keep the old directory structure, or migrate to the new one. If you want to migrate to the new directory structure, you can run the following command:
<Aside variant="info">
Some plugins you're using may not be available in v4 just yet. You could temporarily remove them from your `composer.json` file until they've been upgraded, replace them with a similar plugins that are v4-compatible, wait for the plugins to be upgraded before upgrading your app, or even write PRs to help the authors upgrade them.
```bash
php artisan filament:upgrade-directory-structure-to-v4 --dry-run
```
The `--dry-run` option will show you what the command would do without actually making any changes. If you are happy with the changes, you can run the command without the `--dry-run` option to apply the changes:
```bash
php artisan filament:upgrade-directory-structure-to-v4
```
<Aside variant="warning">
This directory upgrade script is not able to perfectly update any references to classes in the same namespace that were present in resource and cluster files, and those references will need to be updated manually after the script has run. You should use tools like [PHPStan](https://phpstan.org) to identify references to classes that are broken after the upgrade.
</Aside>
You can now `composer remove filament/upgrade` as you don't need it anymore.
## Publishing the configuration file
Some changes in Filament v4 can be reverted using the configuration file. If you haven't published the configuration file yet, you can do so by running the following command:
@@ -102,8 +112,8 @@ return [
'flags' => [
FileGenerationFlag::EMBEDDED_PANEL_RESOURCE_SCHEMAS, // Define new forms and infolists inside the resource class instead of a separate schema class.
FileGenerationFlag::EMBEDDED_PANEL_RESOURCE_TABLES, // Define new tables inside the resource class instead of a separate table class.
FileGenerationFlag::PANEL_CLUSTER_CLASSES_OUTSIDE_DIRECTORIES, // Create new cluster classes outside of their directories.
FileGenerationFlag::PANEL_RESOURCE_CLASSES_OUTSIDE_DIRECTORIES, // Create new resource classes outside of their directories.
FileGenerationFlag::PANEL_CLUSTER_CLASSES_OUTSIDE_DIRECTORIES, // Create new cluster classes outside of their directories. Not required if you run `php artisan filament:upgrade-directory-structure-to-v4`.
FileGenerationFlag::PANEL_RESOURCE_CLASSES_OUTSIDE_DIRECTORIES, // Create new resource classes outside of their directories. Not required if you run `php artisan filament:upgrade-directory-structure-to-v4`.
FileGenerationFlag::PARTIAL_IMPORTS, // Partially import components such as form fields and table columns instead of importing each component explicitly.
],
],
@@ -113,6 +123,24 @@ return [
]
```
<Aside variant="tip">
The `filament/upgrade` package includes a command to help you move panel resources and clusters to the new directory structure, which is the default in v4:
```bash
php artisan filament:upgrade-directory-structure-to-v4 --dry-run
```
The `--dry-run` option will show you what the command would do without actually making any changes. If you are happy with the changes, you can run the command without the `--dry-run` option to apply the changes:
```bash
php artisan filament:upgrade-directory-structure-to-v4
```
This directory upgrade script is not able to perfectly update any references to classes in the same namespace that were present in resource and cluster files, and those references will need to be updated manually after the script has run. You should use tools like [PHPStan](https://phpstan.org) to identify references to classes that are broken after the upgrade.
Once you have run the command, you do not need to keep the `FileGenerationFlag::PANEL_CLUSTER_CLASSES_OUTSIDE_DIRECTORIES` or `FileGenerationFlag::PANEL_RESOURCE_CLASSES_OUTSIDE_DIRECTORIES` flags in your configuration file, as the new directory structure is now the default. You can remove them from the `file_generation.flags` array.
</Aside>
## Breaking changes that must be handled manually
<div x-data="{ packages: ['panels', 'forms', 'infolists', 'tables', 'actions', 'notifications', 'widgets', 'support'] }">
+7
View File
@@ -17,6 +17,13 @@
"Filament\\Upgrade\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Filament\\Upgrade\\UpgradeServiceProvider"
]
}
},
"bin": [
"bin/filament-v4"
],
@@ -0,0 +1,361 @@
<?php
namespace Filament\Upgrade\Commands;
use Exception;
use Filament\Facades\Filament;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use ReflectionClass;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'filament:upgrade-directory-structure-to-v4')]
class UpgradeDirectoryStructureToV4Command extends Command
{
protected $description = 'Upgrade Filament directory structure from v3 to v4';
protected $name = 'filament:upgrade-directory-structure-to-v4';
protected string $phpactorPath;
/**
* @var array<string, array<string, string>>
*/
protected array $movedFiles = [];
protected ?string $currentResource = null;
/**
* @return array<InputOption>
*/
protected function getOptions(): array
{
return [
new InputOption(
name: 'dry-run',
shortcut: 'D',
mode: InputOption::VALUE_NONE,
description: 'Preview changes without executing them',
),
];
}
protected function formatPath(string $path): string
{
return str_replace(base_path() . DIRECTORY_SEPARATOR, '', $path);
}
public function handle(): int
{
$isDryRun = $this->option('dry-run');
if (! $isDryRun && ! $this->components->confirm('This command will modify your Filament resources and clusters to match the new v4 directory structure. Please commit any changes you have made to your project before continuing. Do you want to continue?', default: true)) {
$this->components->info('Migration cancelled.');
return self::FAILURE;
}
$this->components->info('Starting migration from Filament v3 to v4...');
if ($isDryRun) {
$this->newLine();
$this->components->info('Running in dry-run mode. No changes will be made.');
$this->newLine();
}
if (! $isDryRun) {
$this->downloadPhpactor();
} else {
$this->phpactorPath = base_path('vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'phpactor.phar');
}
$panels = Filament::getPanels();
foreach ($panels as $panel) {
$resources = $panel->getResources();
if (count($resources) > 0) {
$this->components->info('Processing resources in ' . $panel->getId() . ' panel');
foreach ($resources as $resourceClass) {
$this->processResource($resourceClass, $isDryRun);
}
$this->newLine();
}
$clusters = $panel->getClusters();
if (count($clusters) > 0) {
$this->components->info('Processing resources in ' . $panel->getId() . ' panel');
foreach ($clusters as $clusterClass) {
$this->processCluster($clusterClass, $isDryRun);
}
$this->newLine();
}
}
if ($isDryRun) {
$this->components->info('Dry run completed. Run without --dry-run to apply changes.');
} else {
$this->components->info('Migration completed successfully!');
}
return self::SUCCESS;
}
protected function downloadPhpactor(): void
{
$this->phpactorPath = base_path('vendor' . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'phpactor.phar');
if (File::exists($this->phpactorPath)) {
$this->components->info('Phpactor already exists at: ' . $this->formatPath($this->phpactorPath));
return;
}
$this->components->task('Downloading phpactor', function () {
$process = Process::command(
'curl -Lo ' . $this->phpactorPath . ' https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar'
);
$processOutput = $process->run();
if (! $processOutput->successful()) {
$this->error('Failed to download phpactor: ' . $processOutput->errorOutput());
throw new RuntimeException('Failed to download phpactor');
}
chmod($this->phpactorPath, 0755);
return true;
});
}
/**
* @param class-string $resourceClass
*/
protected function processResource(string $resourceClass, bool $isDryRun = false): void
{
$resourceReflection = new ReflectionClass($resourceClass);
$resourcePath = $resourceReflection->getFileName();
if ($resourcePath === false) {
$this->components->warn("Could not get file path for resource: {$resourceClass}");
return;
}
if ($this->isVendorPath($resourcePath)) {
$this->components->warn('Skipping resource in vendor directory');
return;
}
$resourceBaseName = class_basename($resourceClass);
$resourceDirectory = dirname($resourcePath);
$this->currentResource = $resourceBaseName;
$resourceName = Str::replaceEnd('Resource', '', $resourceBaseName);
$pluralizedName = Str::plural($resourceName);
$newResourceDirectory = $resourceDirectory . DIRECTORY_SEPARATOR . $pluralizedName;
if ($this->isVendorPath($newResourceDirectory)) {
$this->components->warn('Skipping resource with destination in vendor directory');
return;
}
$this->findAndMoveRelatedClasses($resourceClass, $resourceDirectory, $newResourceDirectory, $resourceBaseName, $isDryRun);
$newResourcePath = $newResourceDirectory . DIRECTORY_SEPARATOR . $resourceBaseName . '.php';
$this->moveClass($resourcePath, $newResourcePath, $isDryRun);
}
/**
* @param class-string $clusterClass
*/
protected function processCluster(string $clusterClass, bool $isDryRun = false): void
{
$clusterReflection = new ReflectionClass($clusterClass);
$clusterPath = $clusterReflection->getFileName();
if ($clusterPath === false) {
$this->components->warn("Could not get file path for cluster: {$clusterClass}");
return;
}
if ($this->isVendorPath($clusterPath)) {
$this->components->warn('Skipping cluster in vendor directory');
return;
}
$clusterBaseName = class_basename($clusterClass);
$clusterDirectory = dirname($clusterPath);
$this->currentResource = $clusterBaseName;
$endsWithCluster = Str::endsWith($clusterBaseName, 'Cluster');
if ($endsWithCluster) {
$newClusterBaseName = $clusterBaseName;
$newClusterDirectory = $clusterDirectory . DIRECTORY_SEPARATOR . $clusterBaseName;
} else {
$newClusterBaseName = $clusterBaseName . 'Cluster';
$newClusterDirectory = $clusterDirectory . DIRECTORY_SEPARATOR . $clusterBaseName;
}
if ($this->isVendorPath($newClusterDirectory)) {
$this->components->warn('Skipping cluster with destination in vendor directory');
return;
}
$newClusterPath = $newClusterDirectory . DIRECTORY_SEPARATOR . $newClusterBaseName . '.php';
$this->moveClass($clusterPath, $newClusterPath, $isDryRun);
}
protected function findAndMoveRelatedClasses(string $resourceClass, string $resourceDirectory, string $newResourceDirectory, string $resourceBaseName, bool $isDryRun = false): void
{
$files = $this->findPhpFiles($resourceDirectory);
$relatedFiles = [];
foreach ($files as $file) {
if (basename($file) === $resourceBaseName . '.php') {
continue;
}
if (strpos($file, $resourceDirectory . DIRECTORY_SEPARATOR . $resourceBaseName) === false) {
continue;
}
$relatedFiles[] = $file;
}
foreach ($relatedFiles as $file) {
$relativePath = str_replace($resourceDirectory, '', $file);
$resourceDirectoryPattern = DIRECTORY_SEPARATOR . $resourceBaseName . DIRECTORY_SEPARATOR;
if (strpos($relativePath, $resourceDirectoryPattern) === 0) {
$relativePath = substr($relativePath, strlen($resourceDirectoryPattern));
$relativePath = DIRECTORY_SEPARATOR . $relativePath;
}
$newPath = $newResourceDirectory . $relativePath;
$this->moveClass($file, $newPath, $isDryRun);
}
}
protected function isVendorPath(string $path): bool
{
return str_contains($path, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR);
}
/**
* @param string $directory
* @return array<int, string>
*/
protected function findPhpFiles(string $directory): array
{
$files = [];
if (! File::exists($directory)) {
return $files;
}
if ($this->isVendorPath($directory)) {
return $files;
}
$items = File::allFiles($directory);
foreach ($items as $item) {
$pathname = $item->getPathname();
if ($this->isVendorPath($pathname)) {
continue;
}
if ($item->getExtension() === 'php') {
$files[] = $pathname;
}
}
return $files;
}
protected function moveClass(string $sourcePath, string $destinationPath, bool $isDryRun = false): void
{
if ($this->isVendorPath($sourcePath)) {
$this->components->warn('Skipping file in vendor directory');
return;
}
if ($this->isVendorPath($destinationPath)) {
$this->components->warn('Skipping move to vendor directory');
return;
}
if ($isDryRun) {
if ($this->currentResource && (! isset($this->movedFiles[$this->currentResource]) || empty($this->movedFiles[$this->currentResource]))) {
$this->line(' <fg=yellow;options=bold>' . $this->currentResource . '</>');
$this->movedFiles[$this->currentResource] = [];
} elseif (! $this->currentResource && (! isset($this->movedFiles['Other']) || empty($this->movedFiles['Other']))) {
$this->line(' <fg=yellow;options=bold>Other</>');
$this->movedFiles['Other'] = [];
}
$this->line(' • ' . $this->formatPath($sourcePath) . ' → ' . $this->formatPath($destinationPath));
if ($this->currentResource) {
$this->movedFiles[$this->currentResource][$sourcePath] = $destinationPath;
} else {
$this->movedFiles['Other'][$sourcePath] = $destinationPath;
}
return;
}
try {
$process = Process::command(
"php {$this->phpactorPath} class:move {$sourcePath} {$destinationPath}"
);
$process->timeout(60);
$processOutput = $process->run();
if ($processOutput->successful()) {
if ($this->currentResource && (! isset($this->movedFiles[$this->currentResource]) || empty($this->movedFiles[$this->currentResource]))) {
$this->line(' <fg=yellow;options=bold>' . $this->currentResource . '</>');
$this->movedFiles[$this->currentResource] = [];
} elseif (! $this->currentResource && (! isset($this->movedFiles['Other']) || empty($this->movedFiles['Other']))) {
$this->line(' <fg=yellow;options=bold>Other</>');
$this->movedFiles['Other'] = [];
}
$this->line(' • ' . $this->formatPath($sourcePath) . ' → ' . $this->formatPath($destinationPath));
if ($this->currentResource) {
$this->movedFiles[$this->currentResource][$sourcePath] = $destinationPath;
} else {
$this->movedFiles['Other'][$sourcePath] = $destinationPath;
}
} else {
$this->components->error('Failed to move class: ' . $processOutput->errorOutput());
}
} catch (Exception $exception) {
$this->components->error('Exception occurred while moving class: ' . $exception->getMessage());
}
}
}
@@ -0,0 +1,16 @@
<?php
namespace Filament\Upgrade;
use Filament\Upgrade\Commands\UpgradeDirectoryStructureToV4Command;
use Illuminate\Support\ServiceProvider;
class UpgradeServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->commands([
UpgradeDirectoryStructureToV4Command::class,
]);
}
}
+2 -1
View File
@@ -5,7 +5,8 @@ parameters:
- packages
excludePaths:
- packages/upgrade/*
- packages/upgrade/bin
- packages/upgrade/src/Rector
reportUnmatchedIgnoredErrors: false