Settings, preferences and columns
Admin 0.14 adds four things that change how a site and its panel are configured from the browser:
- Website settings pages: values such as the site name, logo, contact details or social links, edited at
/admin/settingsand read on the website withsetting('site.title'). - Admin panel settings: the panel's name, logo, brand colour, theme, sidebar, content width and table density, changed without editing
config/admin.php. - My preferences: each user's own theme, panel language and table density.
- The Columns button: each user shows or hides the list columns a resource marks as toggleable.
Setup
php flo admin:install writes two migrations the first time it runs (and on existing installations when run again): one for the settings table and one for admin_user_preferences. Create the tables with:
php flo admin:install
php flo migrate
php flo make:admin-settings # optional: admin/settings.php with a General and a Contact page
php flo admin:doctor checks both tables, the settings pages and the uploads folder.
Declaring settings pages
Pages are declared in admin/settings.php (set settings_path in config/admin.php to use another file). The file returns a list of pages; each page has a name, a label, an icon, an optional description and a schema of fields:
<?php
use FloCMS\Admin\Form\Fields\{ImageUpload, Select, Textarea, TextInput, Toggle};
use FloCMS\Admin\Form\Layout\Section;
use FloCMS\Admin\Settings\SettingsPage;
return [
SettingsPage::make('site')
->label('General')
->icon('settings')
->description('Name, tagline and logo')
->schema([
TextInput::make('title')->translatable()->required()->rules('max:120'),
Textarea::make('tagline')->translatable()->default('Homes by the sea'),
ImageUpload::make('logo')->translatable(),
]),
SettingsPage::make('contact')
->label('Contact & locations')
->icon('map-pin')
->schema([
Section::make('office')->label('Office')->schema([
TextInput::make('email')->email(),
TextInput::make('phone')->tel(),
Textarea::make('address')->translatable()->columnSpan('full'),
]),
]),
SettingsPage::make('social')->label('Social media')->icon('link')->schema([
TextInput::make('facebook')->url(),
TextInput::make('instagram')->url(),
Toggle::make('show_icons')->default(true),
]),
];
Every field type, layout (Section, Grid, Tabs), validation rule and visibleWhen() / requiredWhen() of resource forms works on settings pages. Slug fields are for records and are not accepted. A page named panel is reserved for the panel's own settings.
| Method | Default | |
|---|---|---|
make('site') |
Page name: lower case letters, digits and _; it is the first part of every key |
|
->label('General') |
the name | Shown in the list of pages and as the title; a key of the site's lang file is translated |
->icon('settings') |
settings |
One of the panel's icon names |
->description('…') |
none | Subtitle in the list of pages and under the title |
->columns(2) |
2 | Columns of the form grid (1–4) |
->permission('settings.seo') |
settings.manage |
Permission needed to open and save the page |
->translationBadges() |
off | Use the resource forms' language badge and dialog instead of the page-level language switch |
->beforeSave(fn (array $values): array => …) |
Change the values before they are stored (keys are field names) | |
->afterSave(fn (array $values): void => …) |
Run after the values are stored, e.g. to clear a cache of your own |
Reading settings on the website
Each field is stored under <page>.<field>:
setting('site.title') // in the current language, else the default language
setting('site.title', lang: 'ar')
setting('contact.phone', '—') // the second argument is used when nothing is set
setting('site.logo') // public URL of the uploaded image
settings('site') // ['title' => …, 'tagline' => …, 'logo' => …]
In templates:
<title>{{ setting('site.title') }}</title>
@if(setting('site.logo') !== '')<img src="{{ setting('site.logo') }}" alt="{{ setting('site.title') }}">@endif
<a href="mailto:{{ setting('contact.email') }}">{{ setting('contact.email') }}</a>
The value returned:
- a translatable field gives the text in the requested language, else the default language, else the first filled one (like
localized()); - a file or image gives its public URL; a translatable image falls back to the default language's image;
- a
Togglegives1or0; other fields give the text as saved; - an empty or never-saved field gives its
->default(), else the second argument ofsetting(), else''. Clearing a field in the admin brings its default back.
FloCMS\Admin\Settings\Settings offers the same with get(), page(), translations('site.title') (every language) and stored() (the raw value).
Cache
The first setting() call of a request loads one PHP file, storage/cache/flocms-admin-settings.php, holding the saved values and the declared defaults; later calls cost nothing. Saving a settings page rebuilds it, and so does changing admin/settings.php. php flo cache:clear deletes it; the next request rebuilds it from the table. If you change the table by hand, run php flo cache:clear.
Without a database or before php flo migrate, setting() returns the defaults and never throws.
The settings page
/admin/settings opens the first page the user may manage; each page is /admin/settings/edit/<name>. The list of pages stands on the start side (a select on phones), the form on the other side. A "Settings" entry appears in the sidebar under "Site" for users who may open at least one page.
Languages. On a multilingual site the header has an Editing: language switch. Every translatable field on the page shows the input of the chosen language, with dir and lang set; all languages are part of the form and saved together, so switching never loses typing. Small dots after each translatable label show which languages are filled. required applies to the default language, as on resources. While another language is chosen, fields that are the same in every language are dimmed, marked "All languages" and can only be changed in the default language. If a save fails, the page opens on the language that needs fixing. Without JavaScript every language's input is shown with its language code.
Files. Files uploaded on settings pages are public, because a logo or favicon is shown on the website and the login page. flocms-uploader stores them under public/uploads/settings/<page>/<field>/ with random names (set settings_uploads in config/admin.php if your web root differs). ImageUpload::make('logo')->translatable() keeps one image per language; a language without its own shows the default language's image ("No file for this language: the English one is used"). Replacing or removing a file deletes the old one after the save. Uploads on resource forms stay private, and translatable uploads are only accepted on settings pages.
State. Like record pages, the header shows "Unsaved changes", Ctrl/⌘+S saves, and leaving with unsaved changes asks first.
Storage
Settings pages use FloCMS's own settings table, the one flocms-core reads with Config::getSetting() (for example offline_mode, written by php flo down). A site has one settings table for everything:
CREATE TABLE settings (`param` VARCHAR(190) NOT NULL, `value` TEXT NULL, `lang` VARCHAR(10) NOT NULL DEFAULT '', PRIMARY KEY (`param`, `lang`));
Each admin setting is one row: param is the key (site.title), lang is empty (the row holds every language) and value is JSON: "Acme", {"en":"Acme","ar":"أكمي"}, 1, {"en":"site/logo/1a2b….png"}.
| param | value | lang |
|---|---|---|
offline_mode |
0 |
en (core's own rows, one per language: never touched by the admin) |
site.title |
{"en":"Acme","ku":"ئەکمی"} |
(empty) |
contact.phone |
"+30 210 000" |
(empty) |
Saving a page writes only its own keys, in one transaction; empty values are removed. The migration written by admin:install creates the table only when the site does not have it yet, and rolling it back deletes only the admin's rows, never the table. Read admin settings with setting(): core's Config::getSetting() reads its own per-language rows. admin:doctor checks the columns and lists stored keys that no page declares any more.
Admin panel settings
Settings → Admin panel (/admin/settings/edit/panel, permission settings.panel) changes the panel's look:
| Setting | Key | config/admin.php |
|---|---|---|
| Panel name (per language) | panel.brand |
brand |
| Brand colour | panel.primary_color |
primary_color |
| Logo (per language) | panel.logo |
logo |
| Default theme | panel.theme |
theme |
| Sidebar style | panel.sidebar |
sidebar |
| Content width | panel.content_width |
content_width |
| Table density | panel.density |
density |
A saved value wins; an empty one ("Default (…)") uses config/admin.php, then the built-in default. config/admin.php therefore stays the place for the defaults you keep in Git, and nothing changes on an existing site until someone saves this page. The login page uses the same name, logo, colour and theme. Set 'panel_settings' => false to hide the page; saved values are then ignored.
Table density: "Compact" reduces the padding of list rows so more fit on a screen.
My preferences
/admin/profile has two tabs: Account (name, email, login, password) and My preferences:
| Preference | Values |
|---|---|
| Theme | Panel default, light, dark, follow the system |
| Panel language | Panel default (the language of the address), or one of the site's languages |
| Table density | Panel default, comfortable, compact |
A user's choice beats the panel settings for that user only. With a panel language, a panel page opened in another language (a bookmark, a link in an email) is shown in the preferred one; choosing a language from the top bar's language menu makes that the new preference. "Panel default" switches this off.
Preferences are kept in admin_user_preferences (user_id, key, JSON value). Before that table exists they last until logout, and the page says so. Deleting a user in Users deletes their preferences. The table has no foreign key, so it works on any MySQL/MariaDB storage engine.
The Columns button
Mark the columns a user may hide:
public function table(Table $table): Table
{
return $table->columns([
TextColumn::make('title')->searchable()->sortable(), // the title column: never hidden
TextColumn::make('city')->toggleable(), // shown, can be hidden
TextColumn::make('price')->sortable()->toggleable(),
DateColumn::make('updated_at')->toggleable(hiddenByDefault: true), // hidden until shown
]);
}
A Columns button appears beside Filters when the table has at least one toggleable column. Its menu lists them with a checkbox each, plus Reset; a dot on the button shows that the user changed something. The column that titles a row on phones (the first searchable column) is never listed.
The choice is a personal preference (columns.<slug>), stored as changes from the defaults: a column added to the table later follows its own default for everyone. Sorting by a column the user hid falls back to the table's default sort. In the full-page list (?static=1, or with 'reactive' => false) the menu is a plain form, so it works without JavaScript.