Skip to content

Settings Store 2.12 ​

The settings store keeps a site's public business configuration in the database instead of the .env: tracking IDs and consent, search engine verification codes, the site name, branding, mail addresses and the company identity. Stored values are laid over the Laravel config at boot, so every consumer keeps reading config(...) and does not need to know where a value came from.

Why a store: the .env is a deployment file. The settings panel rewriting it on a running server has lost values across deploys and been shadowed by project config files, and there was no place at all for the company identity. The store gives those values one home that survives deploys and database copies, while secrets stay in the .env.

Since 2.18 the panel writes the store

The Settings screen saves the fields of these groups (all but the email logo) to the store and no longer writes their .env variables. Existing sites take their .env values over once with evolve:settings:import; until then nothing breaks, the .env keeps applying and the panel shows an import hint. In 2.12 to 2.17 the store existed but the panel still wrote the .env.

Settings classes ​

Six classes under Racerfish\Evolve\Settings, built on spatie/laravel-settings with their own repository and their own table evolve_settings (a project's own settings table is not touched). Every property is nullable. Each property is bridged to one config key and, where one exists, the env variable behind it.

TrackingSettings (group tracking) ​

PropertyConfig keyEnv variable
gtm_idservices.google.gtm-idGTM_ID
ga4_measurement_idservices.google.analytics-measurement-idANALYTICS_MEASUREMENT_ID
meta_pixel_idservices.facebook.pixel-idFACEBOOK_PIXEL_ID
pinterest_tag_idservices.pinterest.tag-idPINTEREST_TAG_ID
hotjar_site_idservices.hotjar.site-idHOTJAR_SITE_ID
linkedin_partner_idservices.linkedin.partner-idLINKEDIN_PARTNER_ID
linkedin_lead_conversion_idservices.linkedin.lead-conversion-idLINKEDIN_LEAD_CONVERSION_ID
conversion_dataservices.google.conversion-dataANALYTICS_CONVERSION_DATA
server_side (bool)evolve.tracking.server-sideTRACKING_SERVER_SIDE
consent_policyevolve.tracking.consent.policyTRACKING_CONSENT_POLICY
cookie_banner (bool)evolve.cookie-consentAPP_COOKIE_CONSENT
consent_via_cmp (bool, 2.18)evolve.tracking.consent.via-cmpTRACKING_CONSENT_VIA_CMP
consent_cmp (string, 2.19)evolve.tracking.consent.cmpTRACKING_CMP
cookiebot_id (string, 2.19)evolve.tracking.consent.cookiebot-idCOOKIEBOT_ID

API secrets (ANALYTICS_API_SECRET, FACEBOOK_CONVERSIONS_API_KEY) and TRACKING_EMIT are not part of the store. Secrets stay in the .env; TRACKING_EMIT is posture, see Environment Variables.

SeoSettings (group seo) ​

PropertyConfig keyEnv variable
google_site_verificationservices.google.site-verificationGOOGLE_SITE_VERIFICATION
bing_site_verificationservices.bing.site-verificationBING_SITE_VERIFICATION
pinterest_site_verificationservices.pinterest.site-verificationPINTEREST_SITE_VERIFICATION

SiteSettings (group site) ​

PropertyConfig keyEnv variable
nameapp.nameAPP_NAME
ga_property_idanalytics.property_idANALYTICS_PROPERTY_ID

name overlays app.name only. The mail sender is the mail group below.

BrandingSettings (group branding) ​

PropertyConfig keyEnv variable
panel_colorevolve.theme-colorAPP_BACKEND_THEME_COLOR
login_image_id (int)evolve.login-image-idAPP_LOGIN_IMAGE_ID
logo_media_id (int)evolve.branding.logo-media-idnone
email_logo_media_id (int)evolve.branding.email-logo-media-idnone

The panel edits panel_color and login_image_id on the General tab and logo_media_id on the Company tab. email_logo_media_id has no field.

MailSettings (group mail) 2.18 ​

PropertyConfig keyEnv variable
from_addressmail.from.addressMAIL_FROM_ADDRESS
from_namemail.from.nameMAIL_FROM_NAME
reply_to_addressevolve.mail.reply-toEVOLVE_MAIL_REPLY_TO_ADDRESS
notification_addressevolve.mail.notification-addressEVOLVE_MAIL_NOTIFICATION_ADDRESS
alert_addressevolve.mail.alert-addressHEALTH_WEBMASTER_ADDRESS

The notification address receives new leads of forms without their own administrator address. The alert address receives technical alerts such as health check failures. Mailer and SMTP settings stay in the .env. What each address does is on Mail.

CompanySettings (group company) ​

PropertyConfig key
legal_nameevolve.company.legal-name
brand_nameevolve.company.brand-name
streetevolve.company.street
postal_codeevolve.company.postal-code
localityevolve.company.locality
regionevolve.company.region
countryevolve.company.country
phoneevolve.company.phone
emailevolve.company.email
same_as (array)evolve.company.same-as
founding_dateevolve.company.founding-date

The evolve.branding.* and evolve.company.* keys have null defaults (same-as defaults to []) and no env variable. Since 2.18 the Company tab of the panel edits them, and the theme builds the site-wide schema.org Organization from them once the legal name is set, see SEO and Head.

Precedence ​

For every bridged config key, the first of these that is set wins:

  1. The stored value in evolve_settings, when it is not null.
  2. The .env and the config files, as Laravel resolves them.
  3. The package default from the shipped config file.

null in the store means "not stored, fall back". Setting a property back to null and saving restores the config value on the spot.

Only null falls back

An empty string '' or an empty array [] stored in a property is a value, and it overrides the .env. Store null to clear a setting, never ''.

How the overlay works ​

SettingsOverlay runs in an app->booting() callback, after every provider has registered its config and before any provider boots and reads it. It runs on web requests and in the console alike.

  • Reads are cached. The stored values are read with one query and kept in the application cache under evolve.settings.v1, without expiry. A warm boot costs no query (with a database cache store, one cache query).
  • Saving refreshes. Saving any of the settings classes forgets the cache entry and applies the overlay again in the same request.
  • Missing table. Before php artisan migrate has created evolve_settings, nothing is applied and a "missing" marker is cached. The marker is forgotten when migrations finish.
  • config:cache never contains stored values. The overlay is skipped while config:cache, optimize and package:discover run, so the cached config file holds only .env and config values. At runtime the overlay is applied on top of the cached config, so stored values take effect with or without a config cache, and changing a stored value never requires rebuilding it.

Reading and writing in code ​

Read through the config. That gives the resolved value whatever its source:

php
config('services.google.gtm-id');      // stored value, else GTM_ID, else default
config('evolve.company.legal-name');   // stored value, else null

The settings class gives the raw stored value, null when nothing is stored:

php
use Racerfish\Evolve\Settings\TrackingSettings;

app(TrackingSettings::class)->gtm_id;  // null means "not stored"

Writing is ordinary spatie/laravel-settings. The panel is the normal way; on a server without panel access, tinker works too:

bash
php artisan tinker --execute='
    $settings = app(Racerfish\Evolve\Settings\TrackingSettings::class);
    $settings->gtm_id = "GTM-ABC1234";
    $settings->save();
'

Set the property to null and save to fall back to the .env again.

Importing existing values ​

Existing sites carry these values in the .env. The import copies them into the store; it is explicit and per site, never automatic, because it changes where a site takes its values from. A site installed from the 2.19 starter has nothing to import: its .env.example carries no settings, the panel is the only place they are set.

bash
php artisan evolve:settings:import --dry-run   # review
php artisan evolve:settings:import             # write
OptionEffect
--dry-runPrints the table and writes nothing.
--forceOverwrites stored values that differ from the config. Without it, only properties that are still null are filled.
--include-defaultsAlso imports values equal to the package default (see below).
--group=Limits the run to tracking, seo, site, branding, company or mail. An unknown group fails.

The table lists every bridged property with the columns Setting, Source (the env variable, or the config key where there is none), Config value, Stored value and Action. The actions:

ActionMeaning
importNothing stored yet, the config value is written.
overwriteOnly with --force: the stored value differs and is replaced.
skip (no config value)The config has no value to import.
skip (package default)The value equals the package default, see below.
skip (same)The stored value already equals the config value.
skip (stored, use --force)A different value is stored and --force was not given.

Behaviour worth knowing:

  • Package defaults stay out of the store. A value equal to what the shipped config resolves with no env variable set is skipped, also under --force. TRACKING_CONSENT_POLICY=opt-out, TRACKING_SERVER_SIDE=true, APP_NAME=Laravel, the default panel colour and Laravel's hello@example.com / Example sender are typical. Those keep following the package when a future release changes the default. --include-defaults pins them anyway.
  • It reads the config before the overlay, so a stored value is never imported over itself.
  • Types are cast to the property: APP_LOGIN_IMAGE_ID=12 becomes the integer 12, TRACKING_SERVER_SIDE=false the boolean false.
  • It is idempotent. A second run finds nothing to do.
  • It never edits the .env. The env values remain the fallback for anything set back to null.
  • It is logged. Every run that changes something writes one settings-imported entry to the activity log with the new values, the old values and whether --force was used.
  • It fails with a hint when evolve_settings does not exist yet: run php artisan migrate first.

Health check ​

The tracking and mail blocks of /api/health-check carry a sources field that says where each value comes from: settings (the store), config (the .env or a config file) or null (unset).

json
"tracking": {
  "gtm_id": "GTM-ABC1234",
  "consent_policy": "opt-out",
  ...
  "sources": {
    "gtm_id": "settings",
    "ga_measurement_id": "config",
    "meta_pixel_id": null,
    "pinterest_tag_id": null,
    "linkedin_partner_id": null,
    "hotjar_site_id": null,
    "consent_policy": "config",
    "cookie_banner": "settings",
    "consent_via_cmp": null,
    "server_side": "config"
  }
}

The mail block lists from_address, from_name, reply_to_address, notification_address and alert_address with the same sources map (2.18). The meta of the tracking and mailer health checks names the source of each value too (gtm_id_source, notification_address_source and so on).

The Shadowed Config health check lists config keys a project config file empties. A shadowed key that a stored setting resolves is reported under resolved_by_settings in the check's meta and no longer warns. See Health Checks.

The settings panel ​

Since 2.18 the panel writes the store for every group on this page: each field shows the stored value with the inherited .env value as placeholder, an empty field stores null, and switches are selects with an inherit option. Details on Settings. The former variables (APP_NAME, GTM_ID, MAIL_FROM_ADDRESS and the rest of the bridge) are no longer written by the panel; an outdated copy of the settings view that still posts them has them dropped and logged. The .env lines stay where they are and remain the fallback.

The .env part of the screen keeps two fixes from 2.12.

The config cache survives a save (#222). Deployed sites run on a cached config. Saving settings used to clear it and leave the site uncached until the next deploy. Now a save checks whether a config cache exists: without one it runs config:clear as before, with one it rebuilds it by running php artisan config:cache in a separate process (60 second timeout). If the rebuild fails (non-zero exit, no PHP CLI found, proc_open disabled), the cache is cleared so the new values take effect, the reason is logged at error, and the panel shows an error next to the success message: "Settings saved, but the configuration cache could not be rebuilt. The site works with the new values but runs uncached until the next deploy." A save without changes touches neither the .env nor the cache.

To verify on a server: save a setting, then ls bootstrap/cache/config.php still exists and php artisan about --only=cache reports the config as cached.

The queue health check has three states (#225). The Health tab shows it as a select:

OptionWritesEffect
Auto (on/off, queue driver X)removes HEALTH_CHECK_QUEUE from the .envThe check runs when the queue driver is not sync. The label shows what Auto currently resolves to.
OnHEALTH_CHECK_QUEUE=trueAlways runs.
OffHEALTH_CHECK_QUEUE=falseNever runs.

Before 2.12 the first save pinned the value to on or off and Auto could not be restored. An empty HEALTH_CHECK_QUEUE= written by hand now also means Auto.

Pitfalls ​

  • Editing the .env after an import. Once a value is stored, changing the variable in the .env has no visible effect. Change it in the panel, or empty the field there so the .env applies again. (Before 2.18 the panel itself wrote the .env and ran into this.)
  • Several servers, one database. The cached map lives in the application cache. With a per-server cache store (the file driver), a save on one server forgets the entry only there; the others keep the old values until their cache is cleared. Use a shared cache store (database or redis) for multi-server setups, or clear the cache on every server after a change.
  • Changes outside the settings classes. Editing evolve_settings with SQL does not forget the cached map. Run php artisan cache:forget evolve.settings.v1 afterwards. evolve:db:copy and the Envoy database tasks forget it on the receiving side themselves (since 2.12.1). A deploy without --keep-caches clears the application cache anyway (optimize:clear).
  • Database copies carry the store. Tracking IDs stored on live arrive on staging with a database copy. That is harmless for tracking, which only emits on live unless TRACKING_EMIT says otherwise, but check branding and company values on the copy.

Verifying on a server ​

bash
php artisan evolve:settings:import --dry-run      # what is stored, what would be imported
php artisan tinker --execute='dump(config("services.google.gtm-id"))'
curl -s -H "Authorization: Bearer $TOKEN" https://www.example.ch/api/health-check | jq .tracking.sources

The dry run's Stored value column shows every stored value; sources in the health check shows which tracking and mail values the store currently supplies.