Description
Brainstorm
Most of the changes will be simple pattern replacements, to do that we need to take into account some base behavior/abilities.
- Before we read and change code, we need to format it to ensure no missed patterns due to unexpected formatting.
- For every file we visit (JS or PHP) before we get to the petterns, we need to parse the imports and have solid logic (using lexers) to add/remove imports. Then we can read the pattern replacements and apply any import changes that come with based on what the replacement needs.
- Detective steps (:p) where we look into code, gather information, delete code, regenerate new code based on gathered info in hopes that no info was lost (tricky, might drop this idea).
Dependencies ✅
composer.json.require.flarum/*
^2.0 (when not set to*)composer.json.require-dev.flarum/*
^2.0 (when not set to*)
Infra ✅
flarum/framework/.github/workflows/REUSABLE_backend.yml@main
or@1.x
=>@2.x
flarum/framework/.github/workflows/REUSABLE_frontend.yml@main
or@1.x
=>@2.x
- phpstan
Frontend
mithril 2.0 -> 2.2
can't find exact instances of this in the code, but the more common instance is a copy from core of:
before
return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,
...results.map((user) => {
const name = username(user);
const children = [highlight(name.text as string, query)];
return (
<li className="UserSearchResult" data-index={'users' + user.id()}>
<Link href={app.route.user(user)}>
{avatar(user)}
{{ ...name, text: undefined, children }}
</Link>
</li> );
}),
];
after
return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,
...results.map((user) => {
const name = username(user, (name: string) => highlight(name, query));
return (
<li className="UserSearchResult" data-index={'users' + user.id()}>
<Link href={app.route.user(user)}>
{avatar(user)}
{name}
</Link>
</li> );
}),
];
Export Registry
Compat API ✅
compat API no longer works, instead all modules must be imported
before
https://github.com/flarum/framework/blob/1.x/extensions/tags/js/src/forum/compat.js
after
https://github.com/flarum/framework/blob/2.x/extensions/tags/js/src/forum/forum.ts
Importing Modules ✅
// Before
import Tag from 'flarum/tags/common/models/Tag';
// After
import Tag from 'ext:flarum/tags/common/models/Tag';
- warn of existing usage of
@flarum/core
imports (must remove/replace). ✅ - warn of usage of
useExtensions
must use new import format ofext:
instead ✅
Code splitting ✅
- auto replace app.modal.show calls for lazy loaded modules ✅
- auto replace extend/override of flarum lazy loaded modules ✅
// before
extend(LogInModal.prototype, 'oninit', function() {
console.log('LogInModal is loaded');
});
// after
extend('flarum/forum/components/LogInModal', 'oninit', function() {
console.log('LogInModal is loaded');
});
forum
Composer
DiscussionsUserPage
ForgotPasswordModal
NotificationsPage
PostStreamScrubber
SearchModal
SignUpModal
DiscussionComposer
EditPostComposer
LogInModal
PostStream
ReplyComposer
SettingsPage
UserSecurityPage
common
EditUserModal
Misc
IndexPage.prototype.sidebar
->IndexSidebar
✅IndexPage.prototype.navItems
->IndexSidebar.prototype.navItems
✅IndexPage.prototype.sidebarItems
->IndexSidebar.prototype.items
✅IndexPage.prototype.currentTag
->app.currentTag
✅avatar(...)
icon(...)
-><Avatar ... />
<Icon ... />
✅this.currentTag
->app.currentTag
✅UploadImageButton
namespaceadmin
->common
✅UploadImageButton
props ✅FormModal
vsModal
✅- detect
onsubmit
method ✅ - => exists, switch to
FormModal
inheritance ✅
- detect
- if inherits
Page
=> warn about usingPageStructure
✅
- if inherits
NotificationsDropdown
=>HeaderDropdown
✅
// before
... extends NotificationsDropdown
getMenu() {
return (
<div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
{this.showing && <FlagList state={this.attrs.state} />}
</div>
);
}
// after
... extends HeaderDropdown
getContent() {
return <FlagList state={this.attrs.state} />;
}
Form
: ✅
// before
<div className="Form$1">$2</div>
// after
import Form from 'flarum/common/components/Form';
<Form className="$1">$2</Form>
initializers.has('lock')
=>initializers.has('flarum-lock')
✅initializers.has('subscriptions')
=>initializers.has('flarum-subscriptions')
✅initializers.has('flarum/nicknames')
=>initializers.add('flarum-nicknames')
✅
Backend
Misc
(Extend\Notification)->type()
remove second arg. ✅
$dates
✅
// before
$dates = [...]
// after
$casts = [... => 'datetime']
Filesystem
NullAdapter
✅
// before
League\Flysystem\Adapter\NullAdapter
NullAdapter
// after
League\Flysystem\InMemory\InMemoryFilesystemAdapter
InMemoryFilesystemAdapter
// Before
League\Flysystem\Adpter\Local
Local
// after
League\Flysystem\Local\LocalFilesystemAdapter
LocalFilesystemAdapter
✅
// before
new FilesystemAdapter(new Filesystem(new LocalAdapter($path)));
// after
new FilesystemAdapter(new Filesystem($adapter = new LocalAdapter($path)), $adapter);
- warn about potential: https://flysystem.thephpleague.com/docs/upgrade-from-1.x/ ✅
Mailer
- find
Swift_Mailer
& warn about the move from swift mailer to symfony mailer: https://symfony.com/doc/current/mailer.html ✅
JSON:API
Attempt to use advanced steps for this, but likely not possible to do any automated upgrading. warn instead to read:
http://localhost:3000/extend/update-2_0#jsonapi
- add before and after changes as an example (use the tags extension) ✅
- create an api resource class for each type of serialized model ✅
- Pre-fill the API resource with endpoints based on the current controllers ✅
- Pre-fill the API resource with relationship declarations based on the Serializer ✅
- auto insert the extender that registers the new api resource class ✅
- add TODO comments on old classes (Serializers and Controllers) and old extenders, so the author knows to migrate their logic to the new api resource class. ✅
Search
- Gambits (back to front) (produced js gambit based on pattern) ✅
// before
class TagFilterGambit extends AbstractRegexGambit implements FilterInterface
{
use ValidateFilterTrait;
/**
* @var SlugManager
*/
protected $slugger;
public function __construct(SlugManager $slugger)
{
$this->slugger = $slugger;
}
protected function getGambitPattern()
{
return 'tag:(.+)';
}
protected function conditions(SearchState $search, array $matches, $negate)
{
...
}
public function getFilterKey(): string
{
return 'tag';
}
public function filter(FilterState $filterState, $filterValue, bool $negate)
{
$this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor());
}
protected function constrain(Builder $query, $rawSlugs, $negate, User $actor)
{
...
}
}
// after
class TagFilter implements FilterInterface
{
use ValidateFilterTrait;
/**
* @var SlugManager
*/
protected $slugger;
public function __construct(SlugManager $slugger)
{
$this->slugger = $slugger;
}
public function getFilterKey(): string
{
return 'tag';
}
public function filter(FilterState $filterState, $filterValue, bool $negate)
{
$this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor());
}
protected function constrain(Builder $query, $rawSlugs, $negate, User $actor)
{
...
}
}
export default class TagGambit extends KeyValueGambit {
predicates = true;
key(): string {
return app.translator.trans('flarum-tags.lib.gambits.discussions.tag.key', {}, true);
}
hint(): string {
return app.translator.trans('flarum-tags.lib.gambits.discussions.tag.hint', {}, true);
}
filterKey(): string {
return 'tag';
}
gambitValueToFilterValue(value: string): string[] {
return [value];
}
fromFilter(value: any, negate: boolean): string {
let gambits = [];
if (Array.isArray(value)) {
gambits = value.map((value) => this.fromFilter(value.toString(), negate));
} else {
return `${negate ? '-' : ''}${this.key()}:${this.filterValueToGambitValue(value)}`;
}
return gambits.join(' ');
}
filterValueToGambitValue(value: string): string {
return value;
}
}
// before
(new Extend\Filter(PostFilterer::class))
->addFilter(PostTagFilter::class),
(new Extend\Filter(DiscussionFilterer::class))
->addFilter(TagFilterGambit::class)
->addFilterMutator(HideHiddenTagsFromAllDiscussionsPage::class),
(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
->addGambit(TagFilterGambit::class),
(new Extend\SimpleFlarumSearch(TagSearcher::class))
->setFullTextGambit(FullTextGambit::class),
// after
(new Extend\SearchDriver(DatabaseSearchDriver::class))
->addFilter(PostSearcher::class, PostTagFilter::class)
->addFilter(DiscussionSearcher::class, TagFilter::class)
->addMutator(DiscussionSearcher::class, HideHiddenTagsFromAllDiscussionsPage::class)
->addSearcher(Tag::class, TagSearcher::class)
->setFulltext(TagSearcher::class, FulltextFilter::class),
Flarum\Search\AbstractSearcher
=>Flarum\Search\Database\AbstractSearcher
✅Flarum\Filter\FilterState
=>Flarum\Search\SearchState
✅Flarum\Query
=>Flarum\Search
✅Flarum\Filter
=>Flarum\Search\Filter
✅
LESS ✅
// before
& when (@config-dark-mode) {
background: black;
}
// after
[data-theme^=dark] & {
background: black;
}
// before
& when (@config-colored-header) {
background: black;
}
// after
[data-colored-header^=true] & {
background: black;
}