Connect with us

Technology

Writing a Flarum Extension: Constructing a Customized Discipline – SitePoint


Flarum is extremely quick, extensible, free and open-source discussion board software program. It has been in growth since 2014 and is nearing the tip of its beta part.

On this tutorial, we’ll undergo the method of including a customized subject to a consumer account. This tradition subject shall be settable from a consumer’s profile web page by the consumer solely, but additionally manually editable by an administrator. The total and last supply code of this extension is on GitHub.

🙏 Big due to @askvortsov for assessment and help in doing this The Proper Approach™.

What We’re Including

We’ll enable customers so as to add their Web3 handle into their profile. A Web3 handle is a consumer’s cryptographic id within the Web3 ecosystem – the general public a part of a public-private keypair (like SSH) representing one’s blockchain-compatible account.

Word ℹ: the Web3 ecosystem is a brand new web of decentralized internet hosting, self-owned information, and censorship-resistant communication. For a primer on Web3, please see this 15 minute speak at FOSDEM.

Even should you’re not thinking about Web3, this tutorial shall be helpful. This primary a part of the tutorial will present you easy methods to construct a customized subject for a consumer, and the second half will add the precise Web3 handle in a cryptographically safe means.

Conditions

We assume you could have NodeJS put in and on a current sufficient model (12.16+ is OK), and Composer accessible globally. To your sanity, we additionally suggest utilizing Yarn as an alternative of npm. PHP, MySQL, and different necessities for Flarum are assumed to be current and operating correctly.

Within the examples under, we’re internet hosting the native Flarum copy at ubikforum.check, which some screenshots would possibly mirror.

Please additionally be sure that your discussion board is in debug mode by setting the suitable worth in config.php:

<?php return array(
    'debug' => true,
    'database' => 

New Extension

We begin a brand new extension by operating the Associates of Flarum boilerplate wizard inside a newly created packages folder in our native Flarum set up’s root folder:


mkdir packages & cd packages
npx @friendsofflarum/create-flarum-extension web3address

Essential ⚠: bear in mind to comply with greatest deployment practices and ignore the packages folder should you’re pushing this Flarum folder to a repo from which you’re deploying your stay model.

Fill out the inputs offered by the wizard:

✔ Admin CSS & JS … no
✔ Discussion board CSS & JS … sure
✔ Locale … sure
✔ Javascript … sure
✔ CSS … sure

Word ℹ: you’ll wish to set Admin CSS & JS to sure when you’ve got plans to work with settings and/or permissions, like letting just some folks modify their web3address attribute or comparable. On this case, we don’t want it.

Understand that, attributable to a bug, the generator doesn’t assist numbers within the package deal title or namespace. As such, it’s greatest to rename these values after the era is full. (For instance, you may’t use web3address because the title, however blockchain is ok.)

We additionally have to compile the JavaScript. It’s greatest to depart it operating in watch mode, in order that it’s robotically recompiled on file adjustments and you may shortly test adjustments whereas growing:

cd packages/web3address
cd js
yarn && yarn dev

Word ℹ: you’ll wish to depart this operating in a terminal tab and execute the remainder of the instructions in one other tab. The dev command prompts an always-on activity that can occupy the present terminal session.

We then set up our newly created extension:

composer config repositories.0 path "packages/*"
composer require swader/blockchain @dev

The primary line will inform Composer that it ought to search for packages we set up within the packages subfolder, and, if it doesn’t discover them, to default to Packagist.org.

The second line installs our newly created extension. As soon as it’s in, we are able to load our discussion board’s admin interface, activate the extension, and test the console on the discussion board’s entrance finish for a “Hiya world” message. If it’s there, the brand new extension works.

Hello, forum message in the developer console

Extending

When constructing extensions, you’re all the time extending the uncooked Flarum beneath. These extensions are outlined in your extension’s lengthen.php file with varied extenders being “classes” of doable extension factors you may hook into. We’ll modify this file later.

Understand that the discussion board itself has an lengthen.php file in its root folder as properly. This file is helpful for minor, root-level extensions that your customers can do in your occasion of Flarum with out having to jot down a full extension across the performance. If you wish to share what you’ve constructed with others, or distribute it to different copies of Flarum, an extension is the best way to go.

The lengthen.php file presently seems to be like this:

<?php
namespace SwaderWeb3Address;

use FlarumLengthen;

return [
    (new ExtendFrontend('forum'))
        ->js(__DIR__ . '/js/dist/forum.js')
        ->css(__DIR__ . '/resources/less/forum.less'),

    new ExtendLocales(__DIR__ . '/resources/locale')
];

Should you have been extending the admin UI as properly, there can be one other Frontend block referencing admin as an alternative of discussion board. Because it stands, we’re solely including new JS and kinds to the discussion board’s entrance finish and, optionally, localizing our extension’s UI components, so these are the components that get prolonged.

This file is the place we’ll outline different routes and a few listeners, as you’ll see later.

JavaScript

First, let’s add the UI placeholders. We’ll edit the file js/src/discussion board/index.js.

At first, our index.js file accommodates solely this:

app.initializers.add("swader/web3address", () => {
  console.log("[swader/web3address] Hiya, discussion board!");
});

The initializers.add name makes the applying append the JavaScript specified right here to the remainder of the JavaScript within the app. The execution move is as follows:

  • all PHP code masses
  • essential JS code masses
  • extension JS code masses so as of activation within the admin UI

If a sure extension will depend on one other, Flarum will robotically order their dependencies so long as they’re specified as one another’s dependency of their related composer.json information.

Let’s change the file’s contents to:

import { lengthen } from "flarum/lengthen";
import UserCard from "flarum/parts/UserCard";
import Mannequin from "flarum/Mannequin";
import Consumer from "flarum/fashions/Consumer";

app.initializers.add("swader/web3address", () => {
  Consumer.prototype.web3address = Mannequin.attribute("web3address");
  lengthen(UserCard.prototype, "infoItems", perform (objects) {
    objects.add("web3address", <p>{this.attrs.consumer.web3address()}</p>);
    if (app.session.consumer === this.attrs.consumer) {
      objects.add("web3paragraph", <p>Hiya extension</p>);
    }
  });
});
  • flarum/lengthen is a group of utilities for extending or overriding sure UI components and JS parts in Flarum’s front-end code. We use lengthen right here as an alternative of override as a result of we wish to lengthen the UserCard factor with a brand new merchandise. override would as an alternative fully exchange it with our implementation. Extra info on the variations is out there right here.
  • UserCard is the consumer data card on one’s profile. This element has its infoitems, which is an occasion of itemlist. The strategies of this kind are documented right here.
  • Mannequin is the entity shared with the again finish, representing a database mannequin, and Consumer is a particular occasion of that Mannequin.

Within the code above, we inform the JS to increase the Consumer prototype with a brand new subject: web3address, and we set it to be a mannequin attribute known as web3address by calling the attribute technique of Mannequin. Then we wish to lengthen the UserCard’s merchandise checklist by including the web3address worth as output, and likewise if the profile viewer can be the profile proprietor, by including a web3paragraph that’s only a paragraph with “Hiya extension” inside it.

Essential ⚠: lengthen can solely mutate output if the output is mutable (for instance, an object or array, and never a quantity/string). Use override to fully modify output no matter kind. Extra data right here.

Reloading your consumer’s profile within the discussion board will present the “Hiya extension” paragraph added to the objects within the Consumer Card.

Hello extension shown on the user card

Let’s make this a customized element. Create src/discussion board/parts/Web3Field.js (you’ll have to create the parts folder).

Give it the next code:

import Element from "flarum/Element";

export default class Web3Field extends Element {
  view() {
    return (
      <enter
        className="FormControl"
        onblur={this.saveValue.bind(this)}
        placeholder="Your Web3 handle"
      />
    );
  }

  saveValue(e) {
    console.log("Save");
  }
}

The Element import is a base element of Flarum that we wish to lengthen to construct our personal. It’s a wrapped Mithril element with some jQuery sprinkled in for ease of use. We export it as a result of we wish to use it in our index.js file, so we’ll have to import it there. We then outline a view technique which tells Flarum what to point out because the Element’s content material. In our case, it’s simply an enter subject which calls the perform saveValue when it loses focus (that’s, you navigate away from it). Refreshing the discussion board ought to reveal that this already works.

User card with input alongside the devtools view

Entrance-end fashions come by default with a save technique. We will get the present consumer mannequin, which is an occasion of Consumer, by way of app.session.consumer. We will then change the saveValue technique on our element:

  saveValue(e) {
    const consumer = app.session.consumer;
    consumer
      .save({
        web3address: "Some worth that is completely different",
      })
      .then(() => console.log("Saved"));
  }

Calling save on a consumer object will ship a request to the UpdateUserController on the PHP facet:

The request shown in devtools

Word ℹ: you will discover out which objects can be found on the worldwide app object, just like the session object, by console.loging it when the discussion board is open.

Migration

We wish to retailer every consumer’s web3address within the database, so we’ll want so as to add a column to the customers desk. We will do that by making a migration. Create a brand new folder migrations within the root folder of the extension and inside it 2020_11_30_000000_add_web3address_to_user.php with:

<?php

use IlluminateDatabaseSchemaBlueprint;
use IlluminateDatabaseSchemaBuilder;

return [
    'up' => function (Builder $schema) {
        if (!$schema->hasColumn('users', 'web3address')) {
            $schema->table('users', function (Blueprint $table) use ($schema) {
                $table->string('web3address', 100)->index();
            });
        }
    },
    'down' => function (Builder $schema) {
        $schema->table('users', function (Blueprint $table) use ($schema) {
            $table->dropColumn('web3address');
        });
    }
];

This can be a customary means of including fields by way of migrations. Extra data right here.

Word ℹ: the title of the file is a conference: YYYY_MM_DD_HHMMSS_name_of_what_youre_doing.php which helps with sequential execution of migrations. With this title format, they’re be simply sortable which is necessary for migrations which may rely on each other. In principle, even one thing like 000000001_web3address.php would work, however would go in opposition to conference. In Flarum, a migration file’s title should have an underscore in it.

Then, within the root folder of your discussion board’s set up, run php flarum migrate to run this migration.

Listeners

Flarum works by way of listeners: they pay attention for some occasions, after which react to them by invoking sure PHP lessons.

Serializing

Every time a consumer mannequin is up to date by way of app.session.consumer.save, the mannequin is serialized after being saved on the PHP finish and despatched again to the entrance finish. On this serialized type, it’s simply parsed and became a usable JS object for the UI to point out and work together with. Serialization of a PHP object — specifically after it being saved — is one such occasion we are able to pay attention for.

We’ll write a listener which reacts to serialization and provides the brand new web3address subject to the mannequin in flight, in order that the entrance finish turns into conscious of this subject and may show it within the UI.

Create /src/Listener/AddUserWeb3AddressAttribute.php (create the listing if it doesn’t exist):

<?php

namespace SwaderWeb3AddressListener;

use FlarumApiOccasionSerializing;
use FlarumApiSerializerUserSerializer;

class AddUserWeb3AddressAttribute
{
    public perform deal with(Serializing $occasion)
    {
        if ($occasion->isSerializer(UserSerializer::class)) {
            $occasion->attributes += [
                'web3address'        => $event->model->web3address,
            ];
        }
    }
}

We import the Serializing occasion so we are able to learn info from it, and the UserSerializer to test the kind of the occasion (there are numerous serializations occurring always, so we should be particular). Then, if the serialization that’s occurring is certainly consumer serialization, we add a brand new attribute to our occasion and provides it the worth of the web3address subject within the database connected to the mannequin presently being serialized.

Now, why are we including an attribute to the $occasion and never some occasion of consumer? As a result of the $occasion object’s attributes property is a reference (pointer) to the attributes object of the mannequin being serialized — on this case, a consumer.

Earlier than this kicks in, it must be registered in our extension’s lengthen.php. Add the next line after the final comma within the checklist in that file:

(new LengthenOccasion())->pay attention(Serializing::class, AddUserWeb3AddressAttribute::class),

In the identical file, we additionally have to import the 2 lessons we reference:

use FlarumApiOccasionSerializing;
use SwaderWeb3AddressListenerAddUserWeb3AddressAttribute;

If we now refresh the discussion board and attempt to name our save perform once more by transferring into the Web3 handle subject and out of it (bear in mind, it triggers on blur), the console log will reveal that we do get web3address again.

The web3address in the console

We will show this in our enter subject by modifying the Web3Field.js element:


export default class Web3Field extends Element {
  view() {
    return (
      <enter
        className="FormControl"
        onblur={this.saveValue.bind(this)}
        placeholder="Your Web3 handle"
        worth={app.session.consumer.information.attributes.web3address} 
      />
    );
  }

The web3 input displayed on the user card

Now let’s deal with the saving half.

Saving

When the JavaScript code we wrote calls app.session.consumer.save, the UpdateUserController class is invoked.

Word ℹ: you will discover out how these JS fashions are linked to corresponding controllers by taking a look at Mannequin.js#163, which ends up in Mannequin.js#225 and the sort is returned by the serializer as a part of the JSON:API protocol: every serializer has a sort (reminiscent of BasicDiscussionSerializer.php#20).

This UpdateUserController class saves the core-defined fields of this mannequin (every little thing besides our newly added web3address subject), after which dispatches Saving as an occasion so any extensions which may have to piggyback on it may react to it.

We’ll write a listener to react to this occasion in out extension’s /src/Listener/SaveUserWeb3Address.php:

<?php

namespace SwaderWeb3AddressListener;

use FlarumConsumerOccasionSaving;
use IlluminateHelpArr;

class SaveUserWeb3Address
{
    public perform deal with(Saving $occasion)
    {
        $consumer = $occasion->consumer;
        $information = $occasion->information;
        $actor = $occasion->actor;

        $isSelf = $actor->id === $consumer->id;
        $canEdit = $actor->can('edit', $consumer);
        $attributes = Arr::get($information, 'attributes', []);

        if (isset($attributes['web3address'])) {
            if (!$isSelf) {
                $actor->assertPermission($canEdit);
            }
            $consumer->web3address = $attributes['web3address'];
            $consumer->save();
        }
    }
}

To concentrate on the Occasion, we import it. To trivially use some array performance, we add Illuminate’s Arr helper. The $occasion occasion that this listener reacts to shall be handed into it as an argument and can comprise the goal of the occasion (consumer), the actor who initiated this occasion (the logged-in consumer, represented as a Consumer object), and any information connected to the occasion.

Our save perform on the JavaScript facet accommodates this:

.save({
        web3address: "Some worth that is completely different",
      })

That is what $information goes to comprise.

Let’s change the worth to the precise worth of the enter subject:

  saveValue(e) {
    const consumer = app.session.consumer;
    consumer
      .save({
        web3address: e.goal.worth,
      })
      .then(() => console.log("Saved"));
  }

This listener additionally must be registered in lengthen.php. Our last model of this file is now as follows:

namespace SwaderWeb3Address;

use FlarumLengthen;

use FlarumApiOccasionSerializing;
use FlarumConsumerOccasionSaving;
use SwaderWeb3AddressListenerAddUserWeb3AddressAttribute;
use SwaderWeb3AddressListenerSaveUserWeb3Address;

return [
    (new ExtendFrontend('forum'))
        ->js(__DIR__ . '/js/dist/forum.js')
        ->css(__DIR__ . '/resources/less/forum.less'),

    new ExtendLocales(__DIR__ . '/resources/locale'),
    (new ExtendEvent())
        ->listen(Serializing::class, AddUserWeb3AddressAttribute::class)
        ->listen(Saving::class, SaveUserWeb3Address::class),
];

Altering the sector’s worth will now auto-save it within the database. Refreshing the display could have the sector auto-populated with a price. Visiting another person’s profile will reveal their Web3 handle listed. Lastly, let’s enable admins to edit different folks’s handle values.

Admin management

Each admin has an “Edit Consumer” dialog at their fingertips. This management is within the Controls menu in somebody’s profile. By default, this permits an admin to alter a consumer’s Username and the teams they belong to.

Editing option

Editing the username

It’s comparatively easy to increase this dialog with a further web3address possibility. In index.js underneath our app.initializers perform, let’s add this:

  lengthen(EditUserModal.prototype, "oninit", perform () {
    this.web3address = Stream(this.attrs.consumer.web3address());
  });

  lengthen(EditUserModal.prototype, "fields", perform (objects) {
    objects.add(
      "web3address",
      <div className="Kind-group">
        <label>
          Web3 Handle
        </label>
        <enter
          className="FormControl"
          bidi={this.web3address}
        />
      </div>,
      1
    );
  });

  lengthen(EditUserModal.prototype, "information", perform (information) {
    const consumer = this.attrs.consumer;
    if (this.web3address() !== consumer.web3address()) {
      information.web3address = this.web3address();
    }
  });

We’ll additionally have to import the 2 new parts — Stream (that’s Stream), and EditUserModal:

import Stream from "flarum/utils/Stream";
import EditUserModal from "flarum/parts/EditUserModal";

The primary lengthen registers the web3address propery within the edit popup element occasion. The second lengthen provides a brand new subject into the popup. The final worth in add is the precedence; larger means nearer to start out of checklist, so we put this on the finish of the shape by setting it to 1. The bidi param is a bidirectional bind for Mithril, which makes it in order that any edit of the sector’s worth instantly updates the identical worth within the element, stay. Lastly, the information extension makes positive the info object that’ll get despatched to the again finish accommodates the newly added web3address property.

Web3 address added to user card

Conclusion

Our customized subject works, is settable by customers, and is editable by directors of the discussion board.

Up up to now, the extension will be modified so as to add any customized subject to your customers. Simply change the sector and filenames to match your subject (or fields!) and it’ll work. Don’t overlook to inform the world what you’ve constructed!

In a follow-up put up, we’ll have a look at easy methods to cryptographically confirm possession of somebody’s web3 handle earlier than including it to their profile.

Acquired any suggestions about this put up? Want one thing clarified? Be at liberty to contact me on Twitter — @bitfalls.



Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *