Introduction
Assume that you can install the MetaFox site with the the MetaFox Developer package on your local machine or server at http://localhost:8081 (opens in a new tab).
In this acticle, we will create a new app note
of company metafox
. The Notes app will support following features:
- Allow user to post/share notes with attachments, privacy.
- Configure settings including title, description, tags, category, etc.
- Configure permissions: admin can assign permissions based on user roles.
- View notes of user's friends on their activity streams.
- Get notifications when others comment, like notes.
Create New App
Generally, developing a new MetaFox app includes 2 parts: Frontend and Backend. We will create app skeleton for both Frontend and Backend first.
Backend
Enable Code Generator
The Code Generator option will display on the top header in AdminCP Dashboad, next to the User icon.
To enable this option, please do following steps:
- Update the environment variable
APP_ENV
tolocal
in thebackend/.env
file. - Clear cache in AdminCP or run the command
php artisan cache:clear
within the docker container of PHP-FPM
Then, you will see the the Code Generator icon on the top header in AdminCP.
Create New App
Press the Code Generator option on the top header. You will see the popup with many supported options. Let's choose the New App option as we intend to generate code for new app at this time.
On the Create App form, you will need to fill the following app details:
- App Name: App name must have to 2 parts in the follow format vendor_name/app_name, such as
company/note
- Vendor Name: This info will be used as your app namespace, such as
MetaFox
- Author Name: Fill your company/vendor name, such as
phpFox
- Name: fill the app name, such as
Note
- Home page: Fill the link to the home page of your website
Then, click Create button to generate code skeleton for your new app.
The Skeleton for the Backend of Notes app will be generated as below:
packages/
metafox/
note/
config/
config.php
routes/
api.php
resources/
lang/: define supported languages
drivers.php: define drivers
en/
phrase.php: define message translation
validation.php: define message phrases
menu/
items.php: define menu items
menus.php: define menus
src/
Database/
Factories/
Migrations/
Seeders/
Http/
Controllers/
Requests/
Resources/
Listeners/
Models/
Obsevers/
Policies/
Providers/
Repositories/
tests/
Tests/
Unit/: Unit test source root
Features/: Feature test source root
composer.json
Frontend
We assume that you have set up development environment to build React on your machine/server. The Frontend source is located under the frontend
folder.
To create app skeleton for the Frontend of Notes app, you can open Terminal, go to the frontend
folder mentioned above and run the following commands:
cd frontend/
yarn && yarn bootstrap
npx metafox-cli
✔ What do you need? · Create new app
✔ What is vendor/company name? · metafox
✔ What is app name? · note
Generating files ...
Updating workspace ...
You will see that the default skeleton for the Frontend of Notes app will be generated as below:
frontend/
packages/
metafox/
note/
src/
reducers/
sagas/
package.json
tsconfig.json
constants.ts
types.ts: define typings
index.tsx: general export
module.d.ts: integrate typing
Now we should add new routing for '/note'. On this page, we will simply show the text "This is note home page" for now.
Let's create a new file at frontend/packages/company/note/src/pages/HomePage.tsx
and add following code:
/**
* @type: route
* path: /note
* name: note.home
*/
import React from "react";
function HomePage() {
return <div>This is note home page</div>;
}
export default HomePage;
Now, we will open terminal, go to the frontend
folder and run following commands to build the frontend
yarn bundle
Once the building process is done, you can copy all build source files to the web
folder.
In terminal, go to the top root folder and run the following command
cp -rf frontend/app/dist/* web/
Now, you can open browser and go the URL http://localhost:8081/note (opens in a new tab) . The message "This is note home page" on the screen.
Open browser then see "This is note home page" on the screen.
055d51f8a (fix)
Add Landing Page
Editing ./src/pages/HomePage.tsx
/**
* @type: route
* name: note.home
* path: /note
*/
import { createLandingPage } from "@metafox/framework";
export default createLandingPage({
appName: "note",
pageName: "note.home",
resourceName: "note",
});
Add new file ./src/assets/pages/note.home.json
and paste:
{
"large": {
"blocks": [
{
"blockId": "ia",
"component": "core.block.sideAppHeader",
"slotName": "side",
"title": "Notes",
"icon": "ico-compose-alt",
"blockLayout": "sidebar app header",
"freeze": true
},
{
"component": "core.block.sidebarAppMenu",
"slotName": "side",
"title": "Note",
"blockId": "i8",
"menuName": "sidebarMenu",
"blockLayout": "sidebar app menu"
}
],
"templateName": "two-column-fixed"
},
"info": {
"bundle": "web",
"name": "note.home"
}
}
Add New Menu
Open backend source ./packages/company/note/resources/menu/menus.php
, There are 02 menus defines:
<?php
/* this is auto generated file */
return [
[
'name'=> 'note.sidebarMenu',
'resolution'=> 'web',
],
[
'name' => 'note.admin',
'resolution'=> 'admin',
],
];
note.admin define admincp menu for note, and note.sidebarMenu define app menu.
Update menu items defines for note.sidebarMenu at ./packages/company/note/resources/menu/web.php
<?php
/* this is auto generated file */
return [
[
'module_id' => 'note',
'menu' => 'core.primaryMenu',
'name' => '',
'parent_name' => '',
'label' => '',
'ordering' => 4,
'is_deleted' => 0,
'version' => 0,
'is_active' => 1,
'to' => '/',
'icon' => 'ico-newspaper-alt',
'testid' => '',
'extra' => [
'subInfo' => 'Browse ',
],
],
[
'tab' => 'all',
'menu' => 'note.sidebarMenu',
'name' => 'all',
'label' => 'note::phrase.all_note',
'ordering' => 1,
'icon' => 'ico-hashtag',
'to' => '/note',
],
];
Open terminal for backend then upgrade menus:
php artisan package:install company/note --fast
'menu' => 'core.primaryMenu',
apply menu item for primary menu to home page, open browser at '/' to see note
menu items. Click on note
will navigate to /note
page.
Checking ./note
to see the new menu item note::phrase.all_note
menu items.
Add Sample Block
Following guide we create a new layout block for frontend and display a sample block content.
Open layout file ./packages/company/note/src/assets/pages/note.home.json
Update content
{
"large": {
"blocks": [
{
"blockId": "ia",
"component": "core.block.sideAppHeader",
"slotName": "side",
"title": "Notes",
"icon": "ico-compose-alt",
"blockLayout": "sidebar app header",
"freeze": true
},
{
"component": "core.block.sidebarAppMenu",
"slotName": "side",
"title": "Note",
"blockId": "i8",
"menuName": "sidebarMenu",
"blockLayout": "sidebar app menu"
},
{
"blockId": "x8",
"component": "note.block.SampleBlock",
"slotName": "main"
}
],
"templateName": "two-column-fixed"
},
"info": {
"bundle": "web",
"name": "note.home"
}
}
We add new layout block to layout:
{
"blockId": "x8",
"component": "note.block.SampleBlock",
"slotName": "main"
}
The second step is create scripts file to handle block, create new file ./packages/company/note/src/blocks/Sample.tsx
/**
* @type: block
* name: note.block.SampleBlock
* title: Sample Block
* description: This is deveveloper sample guide block
*/
import React from "react";
import { createBlock, ListViewBlockProps } from "@metafox/framework";
function SampleBlock() {
return <div>Sample Block</div>;
}
export default createBlock<ListViewBlockProps>({
name: "SampleBlock",
extendBlock: SampleBlock,
defaults: {
title: "Sample",
},
});
Open terminal at ./frontend
directory then run:
yarn reload
open browser at /note
and you'll see new message "Sample Block" on the screen.
The next step we update block scripts to display more complex layout and present data from backend api.
Add New Schema
Press Code Generator on the top header in AdminCP, then choose Migration item and fill schema name with "notes".
A new Migration file is created under backend/package/company/note/src/Database/Migrations
folder.
Its file name will be prefixed by info of datetime, for example:2022_05_23_101328_migrate_notes_table.php
The datetime prefix helps migration scripts execute Migration files in chronological order.
For more info, you can read Laravel Migration.
By following Laravel Migration, let's update the Migration file to create notes
table, including the following columns
id : Primary key
module_id :
user_id :
user_type : user_type and user_id is morph columns to this note creator.
owner_id :
owner_type : owner_id and owner_type is morph columns to this note owner.
privacy : Who can see this note.
total_view :
total_like :
total_comment :
total_reply :
total_share :
total_attachment :
title : Note title
is_approved :
is_draft : Is this note post as draft ?
is_sponsor :
sponsor_in_feed :
is_featured : Is this note mark as featured ?
featured_at :
image_path :
server_id : Disk id to storage image
tags : Contains notes tags
created_at :
updated_at :
example source
<?php
use MetaFox\Platform\Support\DbTableHelper;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/*
* stub: /packages/database/migration.stub
*/
/*
* @ignore
* @codeCoverageIgnore
* @link \$PACKAGE_NAMESPACE$\Models
*/
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (Schema::hasTable('notes')) {
return;
}
Schema::create('notes', function (Blueprint $table) {
$table->bigIncrements('id');
DbTableHelper::setupResourceColumns($table, true, true, true, true);
DbTableHelper::totalColumns($table, ['view', 'like', 'comment', 'reply', 'share', 'attachment']);
$table->string('title', 255);
$table->unsignedTinyInteger('is_draft')->default(0);
DbTableHelper::featuredColumn($table);
DbTableHelper::sponsorColumn($table);
DbTableHelper::approvedColumn($table);
DbTableHelper::imageColumns($table);
DbTableHelper::tagsColumns($table);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('notes');
}
};
Open terminal at ./backend
directory then re-install note package to apply new schema.
php artisan package:install company/note --fast
Add New Model
Press Code Generator on the top header and choose the Model item.
Then, fill your App name with company/note
, schemas with notes
, Model with Note
and Entity Name with note
.
MetaFox generates somes classes based on what features you have chosen on the previous form.
Has Repository?
Generate files for Repository associated with the model. The pattern is based on l5-repository (opens in a new tab)
Has Model Factory?
Generate files for Model Factory (opens in a new tab).
Has Authorization ?
Add permissions for the model based on laravel-permission (opens in a new tab)
Has Text Data?
Separate text to the second schema. It's helpful to reduce size of the main schema.
Has Category Data
Create a pivot model associated with the main schema to store relationships between the main schema and a category schema.
Has Tags Data
Create a pivot schema associated with the main schema to store relationships between the main schema and a tags data.
Has Activity Feed?
Create a pivot schema to publish a Note item to activity stream.
Has Model Observer?
Create Observer to listen events on the main schema
Register Repository to laravel service provider at ./packages/company/note/src/Providers/PackageServiceProvider.php
<?php
namespace Company\Note\Providers;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\ServiceProvider;
class PackageServiceProvider extends ServiceProvider
{
public array $singletons = [
\Company\Note\Repositories\NoteRepositoryInterface::class => \Company\Note\Repositories\Eloquent\NoteRepository::class,
// .. more dependencies
];
}
Code Generator will generate all necessary source files of the model based on the chosen options. It saves you much time.
Generate APIs for Web and Admin
Press Code Generator on the top header in AdminCP and choose the Web API item.
On the form, choose company/note
package and Note
model, then submit.
You can do the same to generate APIs for Admin but choose Admin API.
Next, we will edit backend/packages/company/note/routes/api.php
file to add routes:
<?php
namespace Company\Note\Http\Controllers\Api;
use Illuminate\Support\Facades\Route;
Route::prefix('/note')
->resource('resource', NoteController::class);
Open terminal at ./backend
directory then clear cache
php artisan cache:reset
Checking api is ready at http://localhost:3000/api/v1/note
you'll see content similar to
{
"data": [],
"links": {
"first": "http://127.0.0.1:8080/api/v1/note?page=1",
"last": "http://127.0.0.1:8080/api/v1/note?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": null,
"last_page": 1,
"links": [
{
"url": null,
"label": "« Previous",
"active": false
},
{
"url": "http://127.0.0.1:8080/api/v1/note?page=1",
"label": "1",
"active": true
},
{
"url": null,
"label": "Next »",
"active": false
}
],
"path": "http://127.0.0.1:8080/api/v1/note",
"per_page": 100,
"to": null,
"total": 0
}
}
Now, you can open the URL http://localhost:8081/note
on your browser to view result.
MetaFox framework supports API versioning,
Http requests are forwarded to Company\Note\Http\Controllers\Api\NoteController
, validated for versioning and then forwarded to Company\Note\Http\Controllers\Api\v1\NoteController
.
Add WebSetting
The Frontend loads all site settings, permissions and all resource settings via the Settings API /api/core/web-settings
.
This API collects all data defined in the WebSetting
classes of all app packages.
Follow this guide step by step to register app settings into the Settings API.
Edit backend/packages/company/note/src/Http/Resources/v1/Note/WebSetting.php
file
<?php
namespace Company\Note\Http\Resources\v1\Note;
use MetaFox\Platform\Resource\WebSetting as ResourceSetting;
/**
*--------------------------------------------------------------------------
* Note Web Resource Setting
*--------------------------------------------------------------------------
* stub: /packages/resources/resource_setting.stub
* Add this class name to resources config gateway.
*/
/**
* Class NoteWebSetting
* Inject this class into property $resources.
* @link \Company\Note\Http\Resources\v1\WebSetting::$resources;
* @SuppressWarnings(PHPMD.ExcessiveClassLength)
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
class WebSetting extends ResourceSetting
{
/**
* Defines frontend redux actions
*/
protected function initActions(): void
{
$this->addActions([
'searchItem' => [
'apiUrl'=> '/note',
'pageUrl' => '/note/search',
'placeholder' => 'Search notes',
],
'homePage' => [
'pageUrl' => '/note',
],
'viewAll' => [
'apiUrl' => '/note',
'apiRules' => [],
],
'viewItem' => [ // view item detail action.
'apiUrl' => '/note/:id',
],
'deleteItem' => [
'apiUrl' => '/note/:id',
'confirm' => [
'title' => 'Confirm',
'message' => 'Are you sure you want to delete this item permanently?',
],
],
'editItem' => [
'pageUrl' => '/note/edit/:id',
],
'addItem' => [
'pageUrl' => '/note/add',
'apiUrl' => '/note/form',
],
]);
}
/**
* Define forms json should return in web-settings.
*/
protected function initForms(): void
{
$this->addForms([
'filter' => new SearchNoteForm(),
]);
}
}
Checking backend/packages/metafox/note/resouces/drivers.php
contains
[
'driver' => 'MetaFox\\Note\\Http\\Resources\\v1\\Note\\WebSetting',
'type' => 'resource-web',
'name' => 'note',
'version' => 'v1',
'resolution' => 'web',
'is_active' => true,
'is_preload' => false,
],
Home Page
Let's open the frontend/packages/metafox/note/src/pages/HomePage/Page.tsx
file and look into some annotations at the top of file
/**
* @type: route
* name: note.home
* path: /note
*/
import { createLandingPage } from "@metafox/framework";
export default createLandingPage({
appName: "note",
pageName: "note.home",
resourceName: "note",
});
@type: route
Define this source code is a route, MetaFox bundle tool collects this info and separate to bundle.
name: note.home
Define a global unique key for route, it can be overwritten by another page when you want to customize logic.
path: /note
This is a pattern string to define a route path, based on path-to-regexp (opens in a new tab)
Check new file:
App Menu
Visit AdminCP > Appearance > Menus to browse all site menus.
MetaFox framework contains built-in menus
core.primaryMenu
: Left side menu of home pagecore.adminSidebarMenu
: Left side menu of the AdminCPcore.headerSubMenu
: Header top-right menucore.accountMenu
: Header account menu
Also, each app may contain menus on sidebar, admin. For example, in Notes app can have following menus:
note.sidebarMenu
: sidebar menu of Notes on home page.note.admin
: admin menu of Notes app in AdminCP
When an app is created, its menu is automatically inserted into core.primaryMenu
and core.adminSidebarMenu
.
Admin can manipulate menus in AdminCP, such as: adding new menu item + Add Note with URL /note/add
to the menu note.sidebarMenu
Resource Menu
Each resource has 2 action menus in contexts of listing item and viewing item detail. For example:
note.note.itemActionMenu
note.note.detailActionMenu
The name of action menus MUST be followed the convention: [appName].[resourceName].[context]
Add Forms
Backend
Visit AdminCP > Code Generator > Forms
Put forms with "store, update, search" to create StoreNoteForm
, UpdateNoteForm
, SearchNoteForm
.
Edit backend/packages/company/note/routes/api.php
file to add routes for form requests.
Dive into StoreNoteForm.php
<?php
namespace Company\Note\Http\Controllers\Api;
use Illuminate\Support\Facades\Route;
/**
* --------------------------------------------------------------------------
* API Routes
* --------------------------------------------------------------------------
*
* This file will be loaded by @link \MetaFox\Platform\ModuleManager::getApiRoutes()
*
* stub: app/Console/Commands/stubs/routes/api.stub
*/
Route::group([
'namespace' => __NAMESPACE__,
'middleware' => 'auth:api', // logged in required
], function () {
// routes to form
Route::get('note/form', 'NoteController@getStoreForm');
Route::get('note/form/:id', 'NoteController@getUpdateForm');
Route::get('note/form/search', 'NoteController@getSearchForm');
// routes for note resource
Route::get('note', 'NoteController');
});
Edit the backend/packages/company/note/src/Http/Controllers/Api/v1/NoteController.php
file, add following methods
<?php
namespace Company\Note\Http\Controllers\Api\v1;
// import use method
class NoteController extends ApiController
{
/**
* @var NoteRepositoryInterface
*/
private NoteRepositoryInterface $repository;
// other method and properties
/**
* Get updating form
*
* @param int $id
*
* @return UpdateNoteForm
*/
public function getUpdateForm(int $id): UpdateNoteForm
{
$resource = $this->repository->find($id);
return new UpdateNoteForm($resource);
}
/**
* Get updating form
*
* @param int $id
*
* @return SearchNoteForm
*/
public function getSearchForm(int $id): SearchNoteForm
{
$resource = $this->repository->find($id);
return new SearchNoteForm($resource);
}
/**
* Get updating form
*
* @param int $id
*
* @return DestroyNoteForm
*/
public function getDestroyForm(int $id): DestroyNoteForm
{
$resource = $this->repository->find($id);
return new DestroyNoteForm($resource);
}
}
<?php
namespace Company\Note\Http\Resources\v1\Note;
use MetaFox\Platform\MetaFoxForm;
use MetaFox\Platform\Support\Form\AbstractForm;
use MetaFox\Platform\Support\Form\Field\CancelButton;
use MetaFox\Platform\Support\Form\Field\Submit;
use MetaFox\Platform\Support\Form\Field\Text;
use Company\Note\Models\Note as Model;
class StoreNoteForm extends AbstractForm
{
/**
* Prepare title, method, action, value of form object
*/
protected function prepare(): void
{
$this->config([
'title' => __p('core.phrase.edit'),
'action' => '/note', // target api url
'method' => 'POST', // use "POST" method
'value' => [
// default value.
],
]);
}
/**
* Define form structure.
*/
protected function initialize(): void
{
$basic = $this->addBasic();
// add form fields.
$basic->addFields(
new Text([
'name' => 'title',
'required' => true,
'returnKeyType' => 'next',
'label' => 'Title',
'validation' => [ // add client validation rules
'required' => true,
'nullable' => false,
'errors'=>[
'required'=> __p('validation.this_is_required_field'),
]
]
])
);
// add cancel buttons
$footer = $this->addFooter();
$footer->addFields(
new CancelButton([]),
new Submit([
'label' => ($this->resource && $this->resource->id) ?
__p('core.phrase.save_changes') :
__p('core.phrase.create'),
]),
);
}
}
Field | Note | |
---|---|---|
Attachment | Multiple file picker to attachment | |
Autocomplete | Autocomplete text field | |
Birthday | Date picker | |
ButtonField | Basic Button | |
CancelButton | Button for cancel action | |
CaptchaField | Captcha field | |
CategoryField | Category picker | |
CheckboxField | Multiple Checkbox field | |
Choice | Combobox Field | |
CountryState | Choose country and state | |
CustomGenders | Choose custom gender | |
Datetime | Datetime picker | |
DescriptionField | Textarea for description | |
Text field with email format | ||
File | Single file picker | |
FilterCategoryField | CategoryField for filter form | |
FriendPicker | Friend picker | |
Hidden | Hidden input | |
Language | Language picker field | |
LinkButtonField | Button with href | |
Location | Location picker field | |
Password | Input password field | |
Privacy | Privacy picker field | |
Radio | Radio Field | |
SearchBoxField | Text field support search | |
SinglePhotoField | Single photo picker field | |
SingleVideoField | Single video picker field | |
Submit | Submit button | |
SwitchField | Alternate checkbox | |
TagsField | Multiple tags input field | |
Text | Single text input field | |
TextArea | Textarea input | |
Timezone | Timezone picker field | |
TitleField | Single title field | |
TypeCategoryField | Type-category field for 02 level type category |
You can check all form fields supported at MetaFox built-in fields support (opens in a new tab)
Frontend
Frontend supports built-in dynamic form builder to transform JSON-based responses into ReactJS Form element.
Below is the sample Form response in JSON format
{
"status": "success",
"data": {
"component": "form", // define ReactJs render component
"title": "Add New Note", // form title
"action": "/note", // target api for http request when form submit.
"method": "POST", // http method for http request when form submit.
"value": { // initial values.
"module_id": "note",
"privacy": 0,
"draft": 0,
"tags": [],
"owner_id": 0,
"attachments": []
},
"validation": { // define validation object, based on https://www.npmjs.com/package/yup
"type": "object",
"properties": {
"title": {
"label": "Title",
"type": "string",
"required": true,
"minLength": 3,
"maxLength": 255,
"errors": {
"maxLength": "Title must be at most 255 characters"
},
}
}
},
"elements": { // define form structure
"basic": { // basic form section
"name": "basic",
"component": "container",
"testid": "field basic",
"elements": {
"title": { // form field
"component": "text", // Define react render component to form.element.[component]
"returnKeyType": "next",
"maxLength": 255,
"fullWidth": true,
"margin": "normal",
"size": "medium",
"variant": "outlined",
"name": "title",
"required": true,
"label": "Title",
"placeholder": "Fill in a title for your note",
"description": "Maximum 255 of characters",
"testid": "field title"
},
"text": {
"fullWidth": true,
"variant": "outlined",
"returnKeyType": "default",
"name": "text",
"required": true,
"label": "Post",
"placeholder": "Add some content to your note",
"component": "RichTextEditor",
"testid": "field text"
},
},
}
}
}
Look into frontend/packages/framework/metafox-form/src/elements/TextField.tsx
file
/**
* @type: formElement
* name: form.element.textarea
*/
import MuiTextField from "@mui/material/TextField";
import { useField } from "formik";
import { camelCase } from "lodash";
import { createElement } from "react";
import { FormFieldProps } from "../types";
const TextAreaField = ({
config,
disabled: forceDisabled,
name,
formik,
}: FormFieldProps) => {
const [field, meta] = useField(name ?? "TextField");
const {
label,
disabled,
labelProps,
placeholder,
variant,
margin = "normal",
fullWidth,
type = "text",
rows = 5,
description,
autoFocus,
required,
maxLength,
} = config;
// fix: A component is changing an uncontrolled input
if (!field.value) {
field.value = config.defaultValue ?? "";
}
const haveError = Boolean(meta.error && (meta.touched || formik.submitCount));
return createElement(MuiTextField, {
...field,
required,
multiline: true,
disabled: disabled || forceDisabled || formik.isSubmitting,
variant,
label,
"data-testid": camelCase(`field ${name}`),
autoFocus,
inputProps: { "data-testid": camelCase(`input ${name}`), maxLength },
rows,
InputLabelProps: labelProps,
placeholder,
margin,
error: haveError ? meta.error : false,
fullWidth,
type,
helperText: haveError ? meta.error : description,
});
};
export default TextAreaField;
@type: formElement
This annotation determines the file defines a form field component, build tool collects the info to bundle all files into a chunks.
name: form.element.textarea
When the form is returned by a API, form builder will detect and use this component to render elements having "component": "textarea"
key-value pair.
Validation
Form supports validation both Frontend and Backend
Backend
Dive into backend/packages/company/note/src/Http/Requests/v1/Note/StoreRequest.php
file
<?php
namespace Company\Note\Http\Requests\v1\Note;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
// Getting validation rules.
return [
'title'=> ['string', 'required']
];
}
}
The main method rules
returns an array of validation rules (opens in a new tab)
Frontend
Frontend validation is based on yup (opens in a new tab). MetaFox dynamic form builder transforms JSON object to a yup
validation object.
Translation
Backend
MetaFox translation feature provides a convenient way to retrieve strings in various languages, allowing you to easily support multiple languages within your application.
Within note
package, translations string are stored within the resources/lang
directive. With thin this directory, the subdirectory is langue code, and files in contains groups of translations phrase.
note/
resources/
lang/
en/ : Language code `vi`
phrase.php : Phrase groups: `phrase`
validation.php : Phrase groups: `validation`
...
fr/ : Others language code.
Dive deeper into backend/packages/company/note/resources/lang/en/phrase.php
, it defines phrase group phrase
, contains a list of phrase_name and phrase value.
<?php
return [
'notes' => 'Notes', //: phrase_name = "note", phrase_value = "Notes"
'label_menu_s' => 'Notes',
'note_label_saved' => 'Note',
'specify_how_many_points_the_user_will_receive_when_adding_a_new_note' => 'Specify how many points the user will receive when adding a new note.',
'specify_how_many_points_the_user_will_receive_when_deleting_a_new_note' => 'Specify how many points the user will receive when deleting a note.',
'new_note_post' => 'New Note Post',
'edit_note' => 'Editing Note',
'add_some_content_to_your_note' => 'Add some content to your note',
'fill_in_a_title_for_your_note' => 'Fill in a title for your note',
'control_who_can_see_this_note' => 'Control who can see this note.',
'note_type' => 'Note Type',
'added_a_note' => 'added a note',
'note_notification_type' => 'Note Notification',
'note_featured_successfully' => 'Note featured successfully.',
];
In order to prevent conflict of phrase name, MetaFox translation feature use namespaced translation key convention {namespace}::{group}.{phrase_name}
to identity translation string in the appliation. etc:
mnhnn
<?php
/**
* namespace: "note" is the alias of package `company\note`.
* group: "phrase"
* note: "note_length_title"
*/
echo __('note::phrase.note_length_title'); // output "Note Title"
In order to support laravel compatible packages, MetaFox also support laravel-translation (opens in a new tab). Without namespaced translation key convention is {group}.{phrase_name}
. Most of theme publish language files into resources
directory of project root.
resources/
lang/
en/
auth.php
pagination.php
passwords.php
phrase.php
validation.php
In this case, the namespace is dropped, and the translation key is {group}.{phrase_name}
. etc
<?php
echo __p('auth.failed'); // ouput: These credentials do not match our records.
Add Phrase
The simplest way to add translation phrases is via AdminCP, visit AdminCP → Localization → Phrases → + Add New Phrase
Then, you can follow the Phrase Creation Wizard to create a new phrase.
Frontend
MetaFox frontend provides translations feature in the messages.json
file. It contains key/value translations as below:
{
"toggle_layout_preview": "Toggle Device Preview",
"total_like": "{ value, plural, =0{No likes} =1{# like} other{# likes} }",
"total_photo": "{ value, plural, =0{No photos} =1{# photo} other{# photos} }",
"total_video": "{ value, plural, =0{No videos} =1{# video} other{# videos} }",
"total_post": "{ value, plural, =0{No posts} =1{# post} other{# posts} }",
"total_view": "{ value, plural, =0{No views} =1{# view} other{# views} }",
"total_vote": "{ value, plural, =0{No votes} =1{# vote} other{# votes} }",
"total_play": "{ value, plural, =0{No plays} =1{# play} other{# plays} }",
"total_comment": "{ value, plural, =1{# comment} other{# comments} }",
"total_share": "{ value, plural, =1{# share} other{# shares} }",
"total_track": "{ value, plural, =1{# track} other{# tracks} }",
"event_start_date": "start {date}",
"text_direction": "Text Direction",
"edit_block_name": "Edit Block {name}",
"ok": "OK",
"copy": "Copy",
"close": "Close",
"cancel": "Cancel",
"add_layout_block": "Add New Block"
}
To translate message in the component, use i18n
helper.
import { useGlobal } from "@metafox/framework";
import React from "react";
function MyComponent() {
// use i18n helper
const { i18n } = useGlobal();
return <div>{i18n.formatMessage({ id: "toggle_layout_preview" })}</div>;
// output: <div>Toggle Device Preview</div>
}
To translate message in the saga function
function * saga(){
const { i18n } = yield* getGlobalContext();
console.log({i18n.formatMessage({id: 'toggle_layout_preview'})});
// output: Toggle Device Preview
}
In order to support complex message translation, frontend translation support icu syntax, allows developer formats plurals, number, date, time, select, selectordinal. For more information checkout [icu-syntax](icu syntax (opens in a new tab))
To support multiple language, frontend load custom language translation using api /core/translations/web/{language}
. The API responds all messages in the translation group web
.
{
"status": "success",
"data": {
"accepted": "The :attribute must be accepted.",
"active_url": "The :attribute is not a valid URL.",
"after": "The :attribute must be a date after :date.",
"after_or_equal": "The :attribute must be a date after or equal to :date.",
"alpha": "The :attribute may only contain letters.",
"alpha_dash": "The :attribute may only contain letters, numbers, dashes and underscores.",
"alpha_num": "The :attribute may only contain letters and numbers.",
"array": "The :attribute must be an array.",
"before": "The :attribute must be a date before :date.",
"before_or_equal": "The :attribute must be a date before or equal to :date."
},
"message": null,
"error": null
}
Page Browsing
Dive deeper into frontend/packages/company/note/src/pages/BrowseNotes/Page.tsx
file
/**
* @type: route
* name: note.browse
* path: /note/:tab(friend|all|pending|feature|spam|draft)
*/
import { createBrowseItemPage } from "@metafox/framework";
export default createBrowseItemPage({
appName: "note",
resourceName: "note",
pageName: "note.browse",
categoryName: "note_category",
});
@type: route
: Define this file must export default route component.
name: note.browse
: Define page name
path: /note/:tab(friend|all|pending|feature|spam|draft)
path-to-regexp (opens in a new tab) pattern to match route.
appName
: Define app name
resourceName
: Define browsing resource name
pageName
: Define layout page name
categoryName
: Define link to category resource type
Page Search
Backend
You can define search form and then add to the WebSetting
Global Search
Global search system is centralized search system in MetaFox. In order for your app to integrate with global search system, you must define which content is searchable by implementing MetaFox\Platform\Contracts\HasGlobalSearch
interface in your main modal. In this example, we will update the Note
model to implement HasGlobalSearch
interface
<?php
namespace Company\Note\Models;
// declares php "uses" directive.
class Note extends Model implements HasGlobalSearch // , and other interfaces
{
// others property and method
public function toSearchable(): ?array
{
// A draft blog is not allowed to be searched
return [
'title' => $this->title,
'text' => 'content of your text',
'category' => '',
// others data.
];
}
}
Search system has event listener listening on modification of Note data and update its data in queue worker.
Activity Feed
To support activity feed system, the Note
model will need to implement the MetaFox\Platform\Contracts\ActivityFeedSource
and MetaFox\Platform\Contracts\HasResourceStream
interfaces as below
<?php
namespace Company\Note\Models;
use MetaFox\Platform\Support\FeedAction;
// declares php "uses" directive.
class Note extends Model implements ActivityFeedSource // , and other interfaces
{
/**
* Define content of feed action put to activity streams
*
* @return FeedAction
*/
public function toActivityFeed(): ?FeedAction
{
if ($this->isDraft()) {
return null;
}
return new FeedAction([
'user_id' => $this->userId(),
'user_type' => $this->userType(),
'owner_id' => $this->ownerId(),
'owner_type' => $this->ownerType(),
'item_id' => $this->entityId(),
'item_type' => $this->entityType(),
'type_id' => $this->entityType(),
'privacy' => $this->privacy,
]);
}
/**
* Define morph map to privacy streams.
*/
public function privacyStreams(): HasMany
{
return $this->hasMany(PrivacyStream::class, 'item_id', 'id');
}
}
Event Listeners
To track model modification, you can use Event Listener and build-in event list.
To list full events your site, you can open terminal and run the command php artisan event:list
Model Observer
In order to track model modification, checkout Eloquent Observer
Export App
Following instruction /export-package to export language pack.