Download the PHP package goldnead/statamic-preference-center without Composer

On this page you can find all versions of the php package goldnead/statamic-preference-center. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package statamic-preference-center

Statamic Preference Center

One page for everything a person receives. The mailing lists they consented to, the product notifications they can switch per kind and per route, how often those arrive, and the blocks that override all three. It owns no table and invents no setting: every value on the page is read from and written back to the package that already owns it.

Before this existed there were three truths in three places and no page that showed a person all of them. The unsubscribe link was the only door, it worked on one list at a time, and it was a dead end — it did not even reveal that the same person was still on four other lists of the same brand.

Why it is its own package

It is a view, and a view over three packages belongs in none of them.

statamic-marketing owns consent to editorial mail. statamic-notifications owns the type × channel matrix and the digest cadence. statamic-suppression owns the answer to whether an address may be mailed at all. Putting the combined page in any one of them would make that one depend on the other two, and the whole point of the arrangement is that each works alone.

All three are suggest, not require. A host with only notifications gets a working page. So does a host with only marketing. That is not politeness — in the Hub this centre is set up for a brand whose mix is not the mix on the site it was first designed for.

It replaces marketing's preference page

goldnead/statamic-marketing used to serve a preference page of its own at /!/marketing/preferences/{token}. It does not any more. This package owns the preference page for the whole family; marketing keeps only a one-click unsubscribe path that works whether or not this package is installed, and routes every preference link it writes through a resolver that prefers the page here.

If you are installing this next to a marketing that predates the split, read UPGRADE.md first. It is one page and it covers what happens to links already sitting in people's inboxes.

Requirements

PHP 8.2 or newer
Laravel 12.40 or newer, or 13
Statamic 6
Database any Laravel-supported driver; this package owns no table of its own

Two hard requires, both from the same family: goldnead/statamic-brand-context and goldnead/statamic-identity-contracts. The three data sources — marketing, notifications, suppression — are all optional and detected at runtime.

Install

Until the sibling packages are on Packagist, the composer require below is not enough on its own. Composer only reads the repositories key of the root project, never of a package it installs as a dependency, so the entries in this package's composer.json do nothing for you. Add them to your own composer.json first:

Some of these repositories are private. Composer needs a GitHub token with read access to them:

Then:

The routes mount themselves under !/preference-center.

Point the digest footer at it:

The three doors

Three ways in, and all three end at one Identity from goldnead/statamic-identity-contracts — which exists precisely because a token holder has no account, a logged-in person has no subscription token, and something has to carry a user id, a contact uuid, an address and an anonymous id side by side.

Door URL Identity Proof
Token from a marketing mail /!/preference-center/t/{pcToken} contact, from the subscription's contact_uuid, else located by address unsubscribe_token
Signed link, on request /!/preference-center/link/{pcLink} → session contact, located by the address sealed in the link magic_link
Authenticated session /!/preference-center whatever IdentityContext::resolve($user) returns session

The brand comes from the door, never from the session: SetBrandFromRouteValue derives it from the token, and a magic link carries its brand sealed inside it. A page that inherited the brand from whatever the browser was last looking at would show one audience's lists to another's.

The identity rule that is easy to get wrong in the friendly direction

notification_preferences is matched on user_id and contact_uuid with =, never OR. So the identity this page writes must be the identity the sender reads. The session door therefore hands over exactly what IdentityContext::resolve() produced, unimproved — helpfully attaching a contact uuid the sender does not know about would write preferences into a row nothing ever reads.

And where no identity can be established at all, nothing is stored. Both keys NULL is not a row the database rejects; it is one row, shared by every unplaceable visitor, because a hash of two NULLs is the same hash every time. Those controls render locked and the write path refuses them twice.

The three limits

Set in decision L15, and none of them is negotiable.

1. required() types stay unswitchable. For anybody, through any door. The lock cannot live in the preference layer: PreferenceResolver::allows() returns true for a required type on every channel before it reads anything stored — correct for a sender, useless for a form, because a cell that is on and must stay on looks exactly like a cell that is on and may be turned off. So it lives in the view and in the write path.

2. A token does not lift a block. Bounce, complaint and manual opt-out survive all three doors. The two sources of a block — the contact's own opt-out and the suppression table — are read the way statamic-marketing 1.8.1 reads them: batched, per row, and fail-closed. An unqueryable gate is not a third opinion between blocked and clear; it is the closed answer, and the page says which of the two it is rather than leaving a visitor hunting for a block that does not exist.

What a block does not stop is somebody withdrawing consent. Less mail is always allowed.

3. Every change is recorded with the proof that authorised it. unsubscribe_token, magic_link or session — a PreferencesChanged event, a structured log line, and a LeadHub timeline entry where LeadHub is installed. Marketing already recorded a proof for its own writes, but only ever the one value it knew about; a record that names the wrong door is worse than no record, because it would be relied on.

The magic link

The one thing this package builds rather than borrows, and therefore the one whose security is its own fault.

What a click counter does to it, and what this package does about that

A signed URL survives the post and does not always survive the courier. Providers that count clicks rewrite every href in the HTML part onto their own redirector and append their own parameters when they forward the reader — Brevo appends _se, the recipient address in base64. Laravel signs the whole query string, so the URL that arrives is not the URL that was signed and the answer is 403. Measured on staging over a real Brevo send, on a link that passed the whole test suite: 302 …sendibt3.com/tr/cl/… → 403 …/link/…?_se=…&expires=…&signature=…. The plain-text link in the same message, which Brevo leaves alone, worked. Nothing local can find this: no mail sink rewrites links, which is exactly what makes a sink a sink.

Two answers, and a host wants both.

What following one does to the session

The link is spent on arrival and leaves a short-lived note in the session, with its own expiry. Two consequences that are not decoration:

What it deliberately does not have

No table, so no single use and no revocation. A link is good for its lifetime, which is minutes, while the marketing token that opens the same page is good forever — that is the exposure that actually governs, and it is the one L15 named and accepted. Shorten the lifetime rather than reaching for a table.

The cadence, over storage that holds two of its four words

notification_preferences.frequency accepts daily and weekly. There is no immediate and no never in that addon, and inventing columns for them would make this package the owner of a data model it is supposed to be a view over. So the other two are expressed as the channel state they actually describe:

Choice mail digest frequency
Immediately on off —
Daily off on daily
Weekly off on weekly
Never off off —

Four distinct stored states, so a choice reads back as the choice that was made. never leaves the in-app channel alone: it is a cadence for mail, and a page inside the product is not a mailbox. Required types are not touched by any of the four.

The matrix can also hold a state that is none of the four — one type mailed as it happens beside one that is collected. Defaults alone produce it. The control then selects nothing and says the state is mixed, rather than rounding it to the nearest word and putting a caption on the page that the page's own data contradicts.

The cadence is a blunt control: it rewrites the mail and digest channel of every optional type. Two things keep it from flattening a matrix somebody just tuned by hand. It runs only when the choice actually changed — resubmitting the same word writes nothing. And when a submission carries a cadence and a cleared checkbox, the checkbox wins: the cadence writes first, then every cell whose posted value differs from the value the page rendered is written over the top of it. That set is exactly the boxes somebody clicked, so an untouched cell keeps what the cadence gave it and a clicked one keeps what the person asked for.

What it stores

Nothing. There are no migrations, and that is the answer to two traps that took a sibling package down twice: an index too wide for InnoDB, and a unique containing a nullable column, which constrains nothing at all for the rows where it is null. Neither is visible on SQLite. A package that owns no table cannot build either.

Routes

Name Method URI
preference-center.show GET /!/preference-center
preference-center.update POST /!/preference-center
preference-center.token GET /!/preference-center/t/{pcToken}
preference-center.token.update POST /!/preference-center/t/{pcToken}
preference-center.request GET /!/preference-center/request
preference-center.request.send POST /!/preference-center/request
preference-center.link GET /!/preference-center/link/{pcLink}

Every parameter is prefixed pc. A Route::bind() is application-wide, not per package: a binding another addon registers for {token} or {link} applies to every route with that name in every installed package and resolves it against a repository that has never heard of these values. That is exactly how goldnead/statamic-leadhub 1.8.0 shipped a delete button that did nothing.

The token routes are registered only where marketing is installed, because their brand middleware names a marketing model. The route table is therefore a function of what was installed at boot: after adding or removing goldnead/statamic-marketing on a host that runs php artisan route:cache, run php artisan route:clear or the cached table and reality disagree in silence.

The public contract

Three things here are public interface, bound by semver from the release that introduced them (see CHANGELOG.md). Everything else in src/ is free to change in a patch.

1. The route names

preference-center.token, preference-center.show and preference-center.request, also available as constants (PreferenceCenter::ROUTE_TOKEN, ::ROUTE_SHOW, ::ROUTE_REQUEST) so a sibling never has to type them.

2. Link discovery — how another package sends someone here

This is the interface goldnead/statamic-marketing uses to route its preference links at the combined page when it is installed, and at its own one-click path when it is not.

Method Returns
urlForToken(string $token): ?string Absolute URL of the combined page for the person a marketing subscription token names. null when this package cannot serve it.
requestUrl(): ?string Absolute URL of the magic-link door, for a sender holding no token. null when the routes are not mounted.

Two rules, both paid for already:

3. The PreferencesChanged event

Fired once per accepted write, after it has been persisted:

The package's own listener (RecordPreferenceChange) writes the audit line and, where LeadHub is installed, the contact-timeline entry. Yours runs alongside it.

The PreferenceCenter facade (aliased as PreferenceCenter) exposes view(), marketingCenter(), urlForToken() and requestUrl().

Reading the page in a test

The rendered page carries data-* attributes for every state it shows, so a check can quote what the page is showing instead of a person squinting at a screenshot:

Configuration

See config/preference-center.php. The values worth knowing:

Key Default What it decides
routes.prefix !/preference-center Not /preferences: a host that owns that URL should not have to fight this addon for it
sources.* auto false turns a block off even where the package is installed. Nothing turns one on where the classes are missing
magic_link.ttl_minutes 30 Life of the signed URL
magic_link.min_response_ms 350 The floor under a link request. Raise it above your mailer's latency
magic_link.throttle.* 3/hour per address, 10/hour per origin
magic_link.allow_unknown_addresses false Leave it off unless nobody is known yet
delivery.mail_headers [] Per-message headers that tell your provider not to rewrite the links. Verified values per provider in the config file
delivery.ignored_query_parameters _se, the five utm_*, mc_cid, mc_eid, _hsenc, _hsmi, mkt_tok Left out of the signature check because a provider appends them. expires and signature cannot be added
audit.log_channel default channel

Personal data

Nothing of its own. The page reads and writes through the packages that already own the values: subscriptions in goldnead/statamic-marketing, the type × channel matrix and the cadence in goldnead/statamic-notifications, the block state in goldnead/statamic-suppression. Deleting a person's data is done in those packages; there is no table here to clean up.

What this package does add is an audit line per accepted change (audit.log_channel, pseudonymised) and — where LeadHub is installed — a contact-timeline entry. Both are switchable in config/preference-center.php. There is no telemetry and no outbound call of any kind other than the magic-link mail your own mailer sends.

Multi-brand

Brand-aware, per brand, always derived from the door the visitor came through — never from the session. SetBrandFromRouteValue takes it from the subscription token, a magic link carries its brand sealed inside it, and the link-request page asks for it. A page that inherited the brand from whatever the browser last looked at would show one audience's lists to another's. Statamic sites (as distinct from brands) are not used by this package.

Tests

Support

Only the latest version is supported, against the Statamic major it targets. Bugs and questions go to GitHub issues; anything that turns out to be a core bug belongs in statamic/cms. Security reports do not go in a public issue — see SECURITY.md.

Upgrading · Changelog · License

LICENSE.


All versions of statamic-preference-center with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
goldnead/statamic-brand-context Version ^1.13
goldnead/statamic-identity-contracts Version ^1.0
laravel/framework Version ^12.40|^13.0
statamic/cms Version ^6.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package goldnead/statamic-preference-center contains the following files

Loading the files please wait ...