跳到主要内容
版本:2.x

升级到 Flarum 2.0

Flarum 2.0: rebuilt from the foundation. PHP 8.3+, Laravel 13, Symfony 7.4, Flysystem 3, PostgreSQL and SQLite.

Flarum 2.0 是一个主版本,包括一些重大变化和新功能。本指南将帮助您更新扩展,使其与 Flarum 2.0 兼容,并利用新增的功能。

提示

如果您在应用这些更改或使用新功能时需要帮助,请在社区论坛上发起讨论。

自动升级​

您可以使用Flarum CLI来尽可能自动化升级:

请记住有关升级命令的以下信息:

  • 该过程将经历多个步骤,每个步骤都指向相关文档,并提供提示。
  • 在每个步骤之后,更改将被提交到Git,以便您始终可以逐个提交更改。
  • 期间可能发生错误,在每一步完成后,您可以查看所做的更改,并进行任何必要的调整。
  • 除以下情况外,大多数步骤很少需要手动调整:
    • 适应访问/扩展延迟加载的Flarum模块。(可能需要一些手动更改)
    • 准备JSON:API更改。(需要100%的手动更改)
    • 搜索和筛选API更改_(可能需要一些手动更改)_

安装​

升级程序只是[@flarum/clinpm包]的另一个命令(https://github.com/flarum/cli#readme)。

npm install -g @flarum/cli@3

CLI的v3仅与Flarum的v2.x兼容,如果希望继续对Flarum v1.x和v2.x使用CLI,则可以将它们一起安装。[阅读有关它的更多信息](cli.md#installation)。

使用​

现在要使用,只需运行以下命令:

fl upgrade 2.0
警告

在使用自动升级命令之前,请确保至少阅读整个升级指南一次,以了解更改

Frontend​

依赖​

破坏性更改​

在package.json中:

  • 设置flarum-webpack-config为^3.0.0。
  • 设置flarum-tsconfig为^2.0.0。

Mithril 2.2​

破坏性更改​
  • Flarum 2.0 升级 Mithril 到版本 2.2。此版本引入了一个可能影响您扩展的破坏性更改。 Mithril不再在 vnode 上设置一个 text 属性,而是使用一个带有#标签的子项。所以如果要从 vnode 中提取文本,请使用 extractText()` 辅助函数。

Export Registry​

2.0 引入了一个叫做导出注册表的新概念。这是核心和扩展自动注册导出的地方。这允许扩展可以轻松地扩展其他扩展的模块,而不需要这些扩展的明确支持。这也允许延迟加载模块,从而提高性能。

兼容 API​

破坏性更改​

Importing Modules​

破坏性更改​
  • Importing from extensions must now be done using a ext: prefix. This means that any imports you are currently making from bundled extensions will need to be updated. For example, flarum/tags/common/models/Tag becomes ext:flarum/tags/common/models/Tag. This also means you can import from any other extension, not just bundled ones, using the same syntax of ext:vendor/extension/common/....
    // Before
    import Tag from 'flarum/tags/common/models/Tag';

    // After
    import Tag from 'ext:flarum/tags/common/models/Tag';
  • Importing from @flarum/core no longer works. It was previously only allowed for the compat API.
  • The useExtensions webpack option has been removed, use the import format explained above to import using the export registry instead.
  • Some flarum modules are now lazy loaded, such as LogInModal. You have to make sure they have been loaded before using them, or you can trigger the loading yourself. See the Code Splitting documentation for more information.
信息

Read more about the export registry and how to use it in the Export Registry documentation.

提示

Familiarize yourself with the new Code Splitting feature to lazy load modules and improve overall performance.

破坏性更改​
  • The forum search UI has been refactored to use a new SearchModal component.
  • The flarum/forum/components/Search component is no longer the global search component, it can still be used for custom search purposes.
  • Custom global search sources should be registered in flarum/forum/components/GlobalSearch sourceItems method.
  • Custom global search sources should now implement the GlobalSearchSource interface instead of SearchSource, see https://github.com/flarum/framework/blob/2.x/framework/core/js/src/common/components/AbstractGlobalSearch.tsx#L26-L69.
  • The flarum/forum/states/SearchState class has been moved to flarum/common/states/SearchState.
Notable​
  • The admin dashboard now has a search feature, as long as you register your settings/permissions using the Admin extender, then they will be automatically picked up.
  • If your settings/permissions are too custom to be added through the Admin.setting and Admin.permission extenders, then you can use Admin.generalIndexItems to add custom search index entries.

后台管理面板​

破坏性更改​
  • The old extension grid widget on the admin dashboard, which displayed all installed extensions grouped by category, has been removed. It duplicated the information already available in the sidebar and provided no additional value.
Notable​
  • A new Extension Health Widget has been added to the admin dashboard. It surfaces actionable information about installed extensions:
    • Abandoned — extensions whose Composer package is marked abandoned on Packagist, with or without a suggested replacement.
    • Suggested — packages listed in the suggest field of any enabled extension's composer.json that are not yet installed.
    • Disabled — a compact icon grid of all installed-but-disabled extensions.
    • Abandoned extensions also display a warning badge on their entry in the admin sidebar.
    • See the Extension Health Widget docs for how to use the abandoned and suggest fields in your own extension's composer.json.

Locale​

破坏性更改​
  • HTML tags used within translations must be simple tags, without attributes. For example, if you have a locale with the value:

    my_locale_key: "You can read about a <a href='{basic_impl_link}'>basic queue</a> implementation or a <a href='{adv_impl_link}'>more advanced</a> one."

    You must change this to:

    my_locale_key: "You can read about a <basic_impl_link>basic queue</basic_impl_link> implementation or a <adv_impl_link>more advanced</adv_impl_link> one."

    With the following code in your JavaScript:

    app.translator.trans('acme.my_locale_key', {
    basic_impl_link: <a href="https://discuss.flarum.org/d/28151-database-queue-the-simplest-queue-even-for-shared-hosting" />,
    adv_impl_link: <a href="https://discuss.flarum.org/d/21873-redis-sessions-cache-queues" />,
    })
  • Tags used within translations such as <strong> were previously auto provided as parameters

Miscellaneous​

There have been many changes to the core frontend codebase, including renamed or moved methods/components, new methods/components, and more. It might help to look directly at the JavaScript diffs to see what has changed. But here are some notable changes.

破坏性更改​
  • app.extensionData has been renamed to app.registry, but you should now use the Admin extender instead.
  • Some extension initializers have been renamed to be more uniform, if you are checking for initializer existence with app.initializers.has('...') you should update the name accordingly.
  • IndexPage.prototype.sidebar has been removed, use the IndexSidebar component instead.
  • IndexPage.prototype.navItems has been moved to IndexSidebar.prototype.navItems.
  • IndexPage.prototype.sidebarItems has been moved to IndexSidebar.prototype.items.
  • IndexPage.prototype.currentTag has been moved to app.currentTag.
  • The UploadImageButton component has been refactored to allow using it in different contexts. An admin setting definition has also been added to allow you to use an upload setting directly. Additionally, the component has been moved to the common namespace.
  • The FieldSet component has been refactored.
  • The avatar and icon helpers have been refactored to new Avatar and Icon components. Which now allows you to extend them to modify their behavior.
  • The Modal component has been split into Modal and FormModal. The Modal component is now a simple modal that can be used for any content, while the FormModal component is a modal that is specifically designed for forms.
  • app.extensionData has been removed. You must now use the Admin extender to register settings, permissions and custom extension pages.
Notable​
  • A Reset Settings button is now available on extension admin pages. For extensions using the standard Admin.setting() extender, the button appears automatically alongside the save button. If your extension has a custom settings page, you should add the reset button yourself by calling this.resetButton() in your content() method. Pass a label as the third argument to this.setting() so the modal can display human-readable names for each key:

    content() {
    const myValue = this.setting('acme.my_key', '', app.translator.trans('acme.admin.my_key_label'));

    return (
    <Form>
    {/* ... your form fields ... */}
    <div className="Form-group Form-controls">
    {this.submitButton()}
    {this.resetButton()}
    </div>
    </Form>
    );
    }

    Confirming the reset deletes the listed setting rows from the database and reloads the page, reverting them to their PHP-side defaults. A Flarum\Settings\Event\Reset event is dispatched on the backend with the $actor, $extensionId, and $keys involved. See the Reset Settings Button documentation for full details.

  • The admin sidebar navigation has been overhauled. Extension categories are now collapsible groups with count badges and category icons. Categories start collapsed by default, and searching auto-expands categories with matching results. The active extension's category is pre-expanded on page load. Extensions should declare their category in composer.json under extra.flarum-extension.category. See the Extension Categories section in the admin docs for the full list of available categories and how to register custom ones.

  • All forum pages now use the same page structure through the new PageStructure component. You should use this component in your extension if you are creating a new forum page.

  • A HeaderDropdown component has been added which is used for the NotificationsDropdown and FlagsDropdown your component should extend that instead of the NotificationsDropdown. Along with it has been also added the following components: HeaderList and HeaderListItem.

  • A DetailedDropdownItem has been added. Checkout the SubsriptionsDropdown component to see how it is used.

  • A Notices component has been added that allows you to easily add global alerts above the hero.

  • A Footer component has been added that allows you to easily add content to the footer.

  • A Form component has been added to ensure consistent styling across forms. You should use this component in your extension if you are creating a form.

  • An API for frontend gambits has been introduced, checkout the full documentation.

  • A FormGroup component has been added that allows you to add any supported type of input similar to the admin panel's settings registration. checkout the documentation for more details.

  • WelcomeHero.prototype.viewItems has been moved to WelcomeHero.prototype.bodyItems.

  • The frontend Routes extender has been modified to allow passing a custom route resolver class as the fourth argument when adding routes.

  • A UserPageResolver has been introduced to allow canonicalizing user slugs if the current URL slug doesn't match. Extensions that add user profile routes should use this resolver to ensure consistent slug canonicalization across the multiple profile routes.

Backend​

PHP 8.3​

破坏性更改​
  • The entire codebase has been updated to use/require PHP 8.3 as a minimum, and with it come more strict types. This is not a breaking change for most extensions. But you should still update your extension's code accordingly and check for any potential issues or deprecated code.
Notable​
  • A new Flarum\Locale\TranslatorInterface has been introduced, it is recommended to use instead of either Illuminate\Contracts\Translation\Translator or Symfony\Contracts\Translation\TranslatorInterface.

依赖​

Main packages​

破坏性更改​

In composer.json:

  • Set flarum/core package requirement to ^2.0.0-beta.
  • Set other flarum/* packages to *.
  • If you have a php requirement, make sure it is not below ^8.3.
  • If you have blomstra/gdpr as a requirement, change it to "flarum/gdpr": "*".

wikimedia/less.php (updated from 4.x to 5.x)​

Flarum 2.0 upgrades the LESS compiler to less.php 5.x. Most extensions will not need to make any changes to their PHP code, as the breaking changes were handled internally by Flarum core. However, there is one LESS language behaviour change that may affect your stylesheets — see LESS Preprocessing below.

Carbon 3​

破坏性更改​
  • Flarum 2.0 upgrades Carbon to version 3. diffIn methods now return floats instead of integers and can return negative values to indicate time direction. This is the most significant breaking change in Carbon 3. If you are using any of these methods, you should update your code to handle floats and negative values.

Other changes can be found in the Carbon 3 change log.

Symfony (updated from 5.x to 7.x)​

Notable​
  • Flarum 2.0 upgrades Symfony components to version 6. Most extensions will not need to make any changes.

Laravel (updated from 8.x to 13.x)​

Flarum 2.0 uses Laravel 13 components, depending on your extension you may need to adapt your code. Here are some notable highlights.

破坏性更改​
  • The $dates property on models has been removed. You should now use the $casts property instead as such:
    protected $casts = [
    'example_at' => 'datetime',
    ];
  • When changing columns in migrations, you must always include the entirety of the column definition. For example, if you are changing a nullable column from string to text, you must include the ->nullable() method in the new column definition. You will have to update your migrations accordingly. (https://laravel.com/docs/13.x/upgrade#modifying-columns)
  • The HasUuids trait now generates UUIDv7 (ordered) instead of UUIDv4. If your extension uses this trait and relies on UUIDv4, use HasVersion4Uuids instead:
    use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids;
  • Schema::getTables(), Schema::getViews(), and Schema::getTypes() now return results across all schemas by default. Pass the schema parameter to limit results to a specific schema.
  • The image validation rule no longer allows SVGs by default. To allow SVGs, use 'image:allow_svg'.

For more details, see the Laravel 9, Laravel 10, Laravel 11, Laravel 12 and Laravel 13 upgrade guides.

Flysystem (updated from 1.x to 3.x)​

Flarum 2.0 upgrades Flysystem to version 3. Most extensions will not need to make any changes, unless you are using/declaring a Flysystem adapter.

破坏性更改​
  • The NullAdapter has been removed. You may instead use the InMemoryFilesystemAdapter.
  • The Local adapter has been removed. You should use the LocalFilesystemAdapter instead.
  • The FilesystemAdapter constructor now takes as a second argument the adapter instance, if you are using the Flarum filesystem driver interface you don't need to make any changes. If you are using Flysystem directly, you will need to pass the adapter instance as a second argument.
    // Before
    new FilesystemAdapter(new Filesystem(new LocalAdapter($path)));

    // After
    $adapter = new LocalFilesystemAdapter($path);
    new FilesystemAdapter(new Filesystem($adapter), $adapter);
  • Some filesystem methods have been renamed and others have been removed.

For more details, read the Flysystem 1.x to V2 & V3 upgrade guide. Additionally, you can see the Flysystem V2 & V3 new features.

Swift Mailer has been replaced with Symfony Mailer​

破坏性更改​
  • If your extension defines a new mail driver, you will need to update your code to use the new Symfony Mailer API.

Checkout the Symfony Mailer documentation for more details.

Intervention Image v3​

破坏性更改​

The Intervention Image library (intervention/image) has been updated to version 3. If your extension makes any image manipulations, you should check the Intervention Image v3 upgrade guide for the breaking changes and adjust your code accordingly.

You may also check out the core pull request that updated the library here.

FontAwesome (updated from 5.x to 7.x)​

Flarum 2.0 upgrades the bundled icon library from FontAwesome Free 5 (Flarum 1.x) to FontAwesome Free 7. For most extensions and themes this requires no changes, but there are a few things to be aware of.

Notable​
  • The legacy shorthand class names (fas, far, fab) remain fully supported in FA7 — existing icon strings like "fas fa-house" or "far fa-envelope" continue to work without modification.
  • Only .woff2 webfonts are shipped. FA5 bundled several font formats; FA7 ships only .woff2. All modern browsers support WOFF2.
  • The underlying Composer package changed from components/font-awesome (a third-party mirror) to fortawesome/font-awesome (the official package). If your extension references the vendor path directly — e.g. for custom asset publishing — update the path from vendor/components/font-awesome to vendor/fortawesome/font-awesome.
  • FA7 introduces fa-solid, fa-regular, and fa-brands as the preferred canonical class names alongside the legacy aliases. You may start using these in new code, but the old names remain valid.
Upgrading existing installs

After upgrading Flarum, old font files in public/assets/fonts/ are not automatically removed. Clear the directory and re-publish assets:

rm -rf public/assets/fonts
php flarum assets:publish

JSON:API​

Flarum 2.0 completely refactors the JSON:API implementation. The way resource CRUD operations, serialization and extending other resources is done has completely changed.

破坏性更改​
  • The AbstractSerializeController, AbstractShowController, AbstractCreateController, AbstractUpdateController, and AbstractDeleteController have been removed.
  • The AbstractSerializer has been removed.
  • The ApiController and ApiSerializer extenders have been removed.
  • The Saving are dispatched after the validation process instead of before.
  • Several validators have been removed. This includes the DiscussionValidator, PostValidator, TagValidator, SuspendValidator, and GroupValidator. (UserValidator is still present.)
  • Many command handlers have been removed. Use the JsonApi class if you wish to execute logic from an existing endpoint internally instead.
  • The flarum.forum.discussions.sortmap singleton has been removed. Instead, you can define an ascendingAlias and descendingAlias on your added SortColumn sorts.
  • The show discussion endpoint no longer includes the posts relationship, so any posts.* relation includes or eager loads added to that endpoint must be removed. You can move those to the list posts endpoint if you are not already doing the same on that endpoint.

Replacing the deleted classes is the new AbstractResource and AbstractDatabaseResource classes. We recommend looking at a comparison between the bundled extensions (like tags) from 1.x to 2.x to have a better understanding of the changes:

Refer to the documentation

Read about the full extent of the new introduced implementation and its usage in the JSON:API section.

提示

Checkout our guide to upgrading the JSON:API layer from 1.x to 2.x, which provides concrete examples for different scenarios.

Notable​
  • We now do not recommend adding default includes to endpoints. Instead it is preferable to add what relations you need included in the payloads of individual requests. This improves the performance of the forum.

Search/Filter system​

The search system has been refactored to allow for more flexibility and extensibility, and to further simplify things, the separate concept of filtering has been removed.

破坏性更改​
  • The old system decided between using the model filterer and the model searcher based on whether a search query filter[q] was provided. The new system does not have filterers anymore, but the distinction is still present, the only difference is that the default database search driver is always used when no search query is provided.
  • If you have a class extending AbstractFilterer (a filterer) you should now extend AbstractSearcher instead (a searcher). If you already have a searcher for the same model, you can remove the filterer. Filters can be assigned to the searcher class.
  • Gambits have been removed from the backend. There are only filters now per model searcher. The concept of gambits has been moved to the frontend instead. See the search documentation gambits section for more details.
  • Some namespaces have been changed or removed. The classes within Flarum\Query have been moved to Flarum\Search. The classes within Flarum\Filter have been moved to Flarum\Search\Filter. The FilterState class has been removed, you should use the SearchState class instead.
  • The database search state now returns an Eloquent query builder instead of a database query builder.
Notable​
  • A new search driver API has been introduced. Checkout the search documentation for more details on how to use it.
  • You can now get the total search result count from SearchResults.
  • You can now replace an existing filter implementation.

SQLite/PostgreSQL​

Notable​
  • Flarum 2.0 introduces support for SQLite and PostgreSQL databases. If your extension uses any database-specific queries, you should ensure they are compatible with these databases.
  • You can use the new whenMysql, whenSqlite, and whenPgsql methods on the query builder to run database-specific queries.
  • You should ensure your extension is explicit about whether it supports SQLite and PostgreSQL. You can use the database-support key in your composer.json to specify this.
提示

Checkout the database documentation for more details.

LESS Preprocessing​

破坏性更改​
  • The LESS code has been refactored to use more vanilla CSS instead of LESS conditionals and variables.

    • The dark mode switch has been refactored to a color scheme setting that takes effect purely through CSS variables. You should away from using LESS variables as much as possible and instead use the equivalent CSS variables. To add more color schemes.
    • The @config-dark-mode and @config-colored-header variables have been removed. Instead, you can use the [data-theme^=dark] and [data-colored-header=true] CSS selectors respectively.
      // before
      & when (@config-dark-mode) {
      background: black;
      }

      // after
      [data-theme^=dark] & {
      background: black;
      }
    • The @config-colored-header variable has been removed. Instead, you can use the [data-colored-header=true] CSS selector.
      // before
      & when (@config-colored-header = true) {
      background: @primary-color;
      }

      // after
      [data-colored-header=true] & {
      background: var(--primary-color);
      }
  • Math expressions inside CSS custom properties are no longer evaluated by the LESS compiler (this aligns with Less.js 3.5+ behaviour). If your extension performs arithmetic inside a custom property declaration, it will now be output as a literal string instead of a computed value:

    // before: outputs --spacing: 32px
    @base: 16px;
    --spacing: @base * 2;

    // after: use a regular LESS variable for the calculation, then assign
    @base: 16px;
    @spacing: @base * 2;
    --spacing: @spacing;

    // or use calc() if the calculation must remain in CSS
    --spacing: calc(@base * 2);
Notable​
  • New high contrast color schemes have been added.
  • You can register more color schemes through the new frontend Theme extender and equivalent CSS code [data-theme=your-scheme].

Email Notifications​

破坏性更改​
  • The getEmailView method of the Flarum\Notification\MailableInterface interface has been renamed to getEmailViews
  • getEmailViews now requires you to return both text and html views for your email notification.

队列​

In 1.x you routed a job onto a named queue by setting the AbstractJob::$sendOnQueue static property on the job class. That property has been removed; route jobs with the new Queue extender instead:

use Flarum\Extend;

return [
// 1.x: SendExportJob::$sendOnQueue = 'exports';
(new Extend\Queue())
->route(\Your\Extension\Jobs\SendExportJob::class, 'exports'),
];

Routing a base or abstract class covers all of its subclasses (the most specific route wins), so if you set $sendOnQueue on a shared base class, route that same base class. The queue is now applied when the job is pushed, so it works for dispatch(), the low-level push($job), and jobs dispatched by other extensions alike. See the queue documentation for details.

The static was briefly renamed $onQueue on early 2.x development builds; either way, replace it with Extend\Queue->route().

OAuth / Forum Auth​

破坏性更改​

ResponseFactory::make() — new $returnTo argument and return type change

Flarum\Forum\Auth\ResponseFactory::make() now requires a fourth argument string $returnTo = '/' and always returns a RedirectResponse instead of an HtmlResponse. The old popup-based HTML response has been removed entirely.

// Before
$this->response->make($provider, $identifier, $configureRegistration);

// After — pass a validated same-origin returnTo URL
$this->response->make($provider, $identifier, $configureRegistration, $returnTo);

The $returnTo value must be a validated same-origin relative path (starting with /). Passing an absolute URL is a security risk — validate it before passing it in.

ForumApplication::authenticationComplete() removed

authenticationComplete() existed solely to close the old popup window. It has been removed. Extensions that called it can drop the call entirely — the new redirect flow handles everything server-side.

makeLoggedInResponse() and makeRegistrationResponse() are now protected

Both helper methods on ResponseFactory are now protected, making them official extension points. Subclass ResponseFactory and override either method to customise login redirect behaviour or the registration handoff mechanism.

class MyResponseFactory extends ResponseFactory
{
protected function makeLoggedInResponse(User $user, string $returnTo): ResponseInterface
{
// Custom behaviour — e.g. append your own query params
return parent::makeLoggedInResponse($user, $returnTo);
}
}

Bind your subclass in a service provider:

$this->container->extend(ResponseFactory::class, fn () => new MyResponseFactory(...));

New POST /api/registration-token endpoint

A new unauthenticated endpoint accepts a short-lived registration token in the POST body and returns only username, email, and provided[]. Sensitive fields (provider name, identifier, payload internals) are never exposed. The token is submitted in the body — not the URL — so it never appears in server access logs, browser history, or Referer headers.

This endpoint is CSRF-exempt (token possession is the credential) and is primarily used by the frontend to pre-populate the sign-up modal after an OAuth redirect.

_flarum_auth and _flarum_linked query parameters

The new redirect flow uses two query parameters appended to the returnTo URL:

  • _flarum_auth=<token> — set when a new user arrives via OAuth and must complete registration. The frontend detects this on boot, strips it, fetches username/email/provided from POST /api/registration-token, and opens the SignUpModal pre-populated.
  • _flarum_linked=<provider> — set when an existing user's account is linked to an OAuth provider for the first time (either via email-match during login, or a manual link from the security page). The frontend detects this on boot and shows a confirmation modal.

Both parameters are stripped from the URL immediately on the client without a page reload.

fof/oauth also updated

If your extension extends or depends on FriendsOfFlarum/oauth, that package has been significantly updated alongside these core changes. The popup flow has been removed, AbstractOAuthController has moved from fof/extend into fof/oauth itself, and several other breaking changes apply. See the fof/oauth upgrade notes for the full migration guide.

Notable​

  • SignUpModal fields (Username, Email, Password) now render a visible <label> element above each input. If your extension renders additional fields in SignUpModal, consider adding a label for consistency.

Miscellaneous​

破坏性更改​
  • The (Extend\Notification)->type() extender no longer accepts a serializer as second argument.
  • Notification Blueprints must now implement the Flarum\Notification\AlertableInterface interface in order them to be sent as alerts. This allows for email-only notifications for example.
  • The staudenmeir/eloquent-eager-limit package has been removed. If you are using the Staudenmeir\EloquentEagerLimit\HasEagerLimit trait in any of your models, you can simply remove it as it is native to Laravel now.
  • The /logout route has changed from GET to POST. The named route logout now refers to the POST action endpoint. A new GET /logout route named logoutPage shows a no-JS confirmation page. If your extension generates a logout URL using $url->to('forum')->route('logout'), update it to route('logoutPage') for the confirmation page, or submit a POST to route('logout') with a csrfToken body field for a direct logout. Extensions calling app.session.logout() on the frontend require no changes.

Notable​

  • The Frontend extender now allows passing extra attributes and classes that will be added to the root html tag, through the extraDocumentAttributes and extraDocumentClasses methods.
  • Console commands can now implement Laravel's Illuminate\Contracts\Console\Isolatable interface, which adds an --isolated option that prevents the command from running when another instance of it is already running. Core's migrate command implements this — useful for multi-server or multi-container deployments where several instances may try to migrate at once. See the console documentation for details.
  • When preparing data in tests, you should use the model class name instead of the table name, this will ensure the factory of the model is used and basic data is autofilled for you.
  • Avatar upload processing has been improved: static images (JPEG, PNG, BMP, WebP) are now converted to WebP (previously PNG), and animated GIFs are now preserved as .gif rather than being flattened. Extensions that read avatar_url and assume a .png extension should be updated to handle .webp as well. A new CLI command php flarum avatars:convert-to-webp is available to batch-convert any existing avatars. WebP is also now accepted as a valid upload format.
  • HiDPI avatar variants are now generated automatically at upload time. When the source image is large enough, Flarum stores @2x (200 px) and @3x (300 px) variants alongside the base 1× (100 px) file and serves them via the HTML srcset attribute. The User API resource exposes a new avatarSrcset field. Custom avatar drivers should now implement avatarSrcset(User $user): ?string (added to DriverInterface) to optionally supply a srcset string for their provider's images. OAuth drivers can provide pre-sized HiDPI URLs via $registration->provideAvatar2x() and ->provideAvatar3x(). See the Avatar Drivers documentation for details.

Infrastructure​

PHPStan (updated from ^1.10 to ^2.1) and Larastan (updated from 2.9 to ^3.8)​

Flarum 2.0 upgrades its static analysis tooling to PHPStan 2.x and Larastan 3.x. Extensions using flarum/phpstan will need to update their configuration and may need to fix newly reported errors.

flarum/phpstan dependency​

Update your composer.json to require the latest flarum/phpstan:

composer require --dev flarum/phpstan:^2.0

Flarum core now runs PHPStan at level 6. We recommend all extensions do the same. Update your phpstan.neon:

parameters:
level: 6

Level 6 checks for missing return types on all methods and properties. You may encounter new errors on your first run after increasing the level — work through them incrementally.

Removed config parameters​

PHPStan 2.x removed several parameters that were commonly used in Flarum extension configs. Remove these from your phpstan.neon if present — they will cause an error if left in:

  • checkMissingIterableValueType — replaced by the missingType.iterableValue error identifier. If you need to suppress these errors, use ignoreErrors with the identifier instead.
  • checkGenericClassInNonGenericObjectType — replaced by missingType.generics.
  • checkAlwaysTrueCheckTypeFunctionCall, checkAlwaysTrueInstanceof, checkAlwaysTrueStrictComparison, checkAlwaysTrueLooseComparison — these checks are now always enabled and can no longer be disabled.

Larastan 3.x: explicit relation return types required​

Larastan 3.x no longer infers generic types for Eloquent relation methods by parsing the method body. You must now annotate return types explicitly:

// Before (Larastan 2.x would infer the generic type automatically)
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}

// After (explicit generic annotation required)
/** @return HasMany<Post, $this> */
public function posts(): HasMany
{
return $this->hasMany(Post::class);
}

A Rector rule is available to automate this migration.

Larastan 3.x: template annotation renames​

If your extension uses Larastan's template type annotations, two names have changed:

  • TModelClass → TModel
  • TChildModel → TDeclaringModel

PHPUnit (updated from ^9.5 to ^12.5)​

Flarum 2.0 upgrades flarum/testing to PHPUnit 12. PHPUnit 12 requires PHP 8.3+ (already the 2.x floor) and removes docblock annotation support, so extension test suites need to migrate to PHP 8 attributes.

Annotations replaced by attributes​

破坏性更改​

Docblock annotations like @test, @dataProvider, @depends, @before, @after, @group, @covers, and @runInSeparateProcess are no longer recognised. Tests that relied on @test to mark methods without a test prefix will silently stop being discovered — PHPUnit will not warn you.

Replace them with their PHP 8 attribute equivalents:

// Before
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
/**
* @test
* @dataProvider provideFoo
*/
public function it_works(int $input): void
{
// ...
}

public static function provideFoo(): array
{
return [[1], [2]];
}
}

// After
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
#[Test]
#[DataProvider('provideFoo')]
public function it_works(int $input): void
{
// ...
}

public static function provideFoo(): array
{
return [[1], [2]];
}
}

See the PHPUnit annotations guide for the full mapping. Rector ships a ruleset (PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES) that automates the conversion.

phpunit.xml schema changes​

破坏性更改​

Several attributes were removed from the PHPUnit config schema between 9.x and 12.x. If your phpunit.unit.xml / phpunit.integration.xml contains any of these, PHPUnit 12 will fail to load the config:

  • convertErrorsToExceptions, convertNoticesToExceptions, convertWarningsToExceptions — removed in PHPUnit 10. Errors, notices, and warnings are always converted to exceptions now.
  • backupStaticAttributes — renamed to backupStaticProperties in PHPUnit 10.
  • <listeners> element — removed in PHPUnit 10. The Mockery TestListener is no longer needed; Mockery integrates automatically via PHPUnit's extension mechanism when mockery/mockery is installed.

See the updated example configs in the testing guide.

Mockery expectations​

Notable​

PHPUnit 12 emits a notice for every mock that is created without any expectations configured (No expectations were configured for the mock object ...). These are advisory and do not fail the suite, but they indicate tests that would be clearer using a stub instead of a mock. To silence the notice for a specific test class or method without changing the mock, add the #[AllowMockObjectsWithoutExpectations] attribute.

Reusable GitHub Workflows​

破坏性更改​
  • The reusable GitHub workflows must be updated to use target the ones from the 2.x branch. For example, you must change flarum/framework/.github/workflows/REUSABLE_backend.yml@main to flarum/framework/.github/workflows/[email protected].