Download the PHP package wexample/symfony-user without Composer

On this page you can find all versions of the php package wexample/symfony-user. 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 symfony-user

symfony-user

Version: 12.0.1

Routes

Routing\UserRoute names every route the package sends users to. Its endpoints:

Route Path What
user_security_login_link /login/link check route of the magic links
user_security_logout /logout handled by the firewall
user_password_reset /password/reset target of the reset mail
user_password_activate /password/activate target of the activation mail
user_security_two_factor_resend /login/2fa/resend sends the code again (POST)
user_totp_regenerate /account/authenticator/backup-codes/regenerate new backup codes (POST)
user_accounts_action /accounts/{id}/{action} what an administrator does to one account (POST)

The pages — LOGIN, TWO_FACTOR, PASSWORD_FORGOT, PASSWORD_NEW, PASSWORD_ACTIVATION_INVALID, PROFILE, TERMS, TOTP, TOTP_BACKUP_CODES, TOTP_SETUP, ACCOUNTS, ACCOUNT, ACCOUNT_ACTIVITY — are symfony-user-ds's. The texts of the forms and the mails stay here, under assets/forms/ and assets/mails/: a form reads its labels and errors from the domain of its class.

Showing the login form elsewhere

The login form is an ajax form: any page can render it, and a successful login lands on the target path saved beforehand — the way a tunnel step would use it.

An ajax call reaching a protected URL gets a 401 JSON payload whose action redirects to /login, instead of an HTML redirect.

Tunnel steps

Two steps for symfony-tunnels, for a checkout or a sign-up that does not start with a login:

The later steps read the account with getTunnelUser($cursor). The tunnel templates of the application include the bodies symfony-user-ds ships: @WexampleSymfonyUserDsBundle/tunnels/partials/user_mail.html.twig and login.html.twig.

Magic links in application mails

The link signs the user in and lands on the given local path. It dies with the next login or password change of the user. MagicLinkService::createLink() also works outside a request (commands, workers).

The account's language

AbstractUser implements HasLocaleInterface (symfony-translations) and carries a nullable locale column, so the mails of an account can be written in its language even when they leave outside its requests (a worker, a webhook). An application adding this version generates a migration for the column.

The application fills it. With remember_locale: true and the URL prefixes of symfony-translations, an account reading a page whose URL names a language keeps that language. Otherwise the application calls setLocale() where the language is chosen (sign-up, a settings page), or leaves it empty, and mails go out in the default locale.

Passwords

A password its holder did not choose

AbstractUser::isPasswordChangeRequired() marks an account whose password somebody else knows. PasswordChangeGate, the first of the account gates, then holds it on /password/new — the page of the reset walk, with the same form, asking for no current password: the one it would ask for is the one being taken away. The new password clears the flag.

PasswordUpdaterService keeps the flag in step with every password it writes: raised when the signed-in actor is someone else than the account — an administrator setting a password —, cleared in every other case, which are all the holder choosing their own. An application creating an account with a password of its own — a fixture, an import, a migration from another system — raises it itself with setPasswordChangeRequired(true).

Nothing is forced after an invitation or a reset: there, the holder already chose the password, and createAccount() writes no password at all.

AbstractUser carries the flag as a password_change_required column: an application adding this version generates a migration for it.

The account's own page

/account (UserRoute::PROFILE, symfony-user-ds) is what the holder reads and changes about itself: its address — shown, not changed, being the identifier it signs in with —, its initials, its roles, its last sign-in, then two forms and a link.

ProfileForm carries the name and the language, and only what the application's user class actually has: the first and last name when it uses UserWithNameTrait, the language when framework.enabled_locales holds more than one. With neither, ProfileFormProcessor::hasFields() is false and the page leaves the card out. ChangePasswordForm is the second one, and /account/authenticator the link.

The entry leading there is the application's to add: the user menu takes it in its items.

Session lifetime

session.idle_lifetime, in seconds, signs out a session nobody has asked anything of — the screen left open in a workshop, the tab forgotten on a shared machine. 0, the default, expires none: the session lives as long as its cookie. It is a delay apart from Symfony's framework.session.cookie_lifetime and gc_maxlifetime, which say how long a session may live at all, not how long it may live unused.

SessionIdleSubscriber stamps the session at each request and, past the delay, signs the account out — the remember-me cookie included, or it would sign straight back in —, journals session.expired and lands on /login?session=expired, which the login page reads to say why. An ajax call gets the 401 payload instead.

What pushes the delay back is a page being read. The requests under /_, which a script makes and not a reader, are held to the delay without pushing it back: a page left open polling keeps no session alive.

Second factor

Roles

findByRoles() matches whole roles: ROLE_ADMIN does not match ROLE_SUPER_ADMIN. For impersonation, enable switch_user on the firewall and test is_granted('IS_IMPERSONATOR') in templates.

Administering accounts

An administrator creates an account with createAccount(): built by the application — email, roles, its own fields —, it is written enabled with no password, and its holder gets an activation mail to choose one (/password/activate, then /password/new, signed in afterwards; the second factor and the account gates apply as on any sign-in). With no password, nothing signs in with it. A dead activation link — expired, or the account already activated — shows a page telling to ask for a new one. sendPasswordMail() sends the activation mail again to an account waiting for it, a mail to choose a new password to an activated one; an earlier link keeps working until it expires, the password is chosen, or the address changes.

Deactivate, reactivate, lock, unlock and change roles through AccountAdministrationService, not the entity setters — those stay for fixtures and migrations:

Who may administer is the application's access control. The roles an actor administers are those they reach through the hierarchy — or, with administration.manages, those their roles manage: a relation apart from the hierarchy, not transitive, granting no security right, for an administrator who must not open the pages of the accounts they manage. An application narrows it further — the accounts of one organization — with an AccountAdministrationGuardInterface, asked before every change.

The service refuses:

AccountRulesService holds what an account must always be, whoever writes it: a role of administration.role_email_domains held — directly or through the hierarchy — by an address of its domains, compared whole; the roles of administration.exclusive_roles never given together. Only given roles count there, so an administrator can reach the roles of the accounts they administer. A Doctrine listener applies these rules on every flush; a profile form changing the address calls assertValid() first, to show the refusal. A database CHECK constraint, in the application's schema, is the last line.

Each change is journaled — account.created, account.activation_sent, account.activated, account.password_mail_sent, account.deactivated, .reactivated, .locked, .unlocked, account.roles_changed with roles_before / roles_after —, each refusal as account.change_refused with its code; extra.actor_id names the actor.

The screens

symfony-user-ds draws them, and administration.page_role decides whether they exist at all: unset — the default — /accounts and everything under it answers 404, for everyone, the way the impersonation page does without switch_user. Set, that role opens them, and the rules above still say what each administrator may then do. Declare it only where administering accounts from the interface is the intent.

A refusal is shown, never an error page: both form processors call the service inside formIsValid(), so AccountAdministrationRefusal comes back as an error on the form. The roles the account holds outside the ones the actor administers are kept untouched.

An application drawing its own screens instead reads through the same services: AccountDirectoryService lists, searches, pages and describes, AssignableRolesService names the roles a field may offer.

Memberships

An account belongs to groups of the application — a project, a team, an establishment —, with grants in each. The package knows neither the group nor the membership: the application implements the interfaces on its own entities, the way its User extends AbstractUser.

A grant undeclared, included or given, or one including itself, stops the container from building.

Records of a group are read through MembershipScopeService, at each query rather than through a global Doctrine filter, invisible where the query is read and lifted for everyone by one forgotten disable():

No group matches nothing, never everything: an account with no membership, or nobody signed in, reads no row. A record of another group answers 404, which does not tell that it exists.

With administer_grant, an account holding it in groups administers the accounts all of whose groups it holds it in (Security\Guard\MembershipAdministrationGuard): one group it does not, and a deactivation would cut that group off too. An account in no group is out of its reach, but for one it creates. An administrator holding the grant nowhere — a platform role — keeps the rules of administration.

Managing members

MembershipAdministrationService adds a member to a group (add($actor, $group, $member, ['contributor'])), changes its grants (changeGrants()) and removes it (remove()). It refuses, with Enum\MembershipAdministrationRefusal:

Each change is journaled — membership.added, membership.grants_changed, membership.removed, membership.change_refused —, on the account it is about, with the group, the member and the grants before and after in extra.

The entities being the application's, it declares how they are made, one MembershipManagerInterface per kind of group: its name in urls (team), the group and member classes, createMembership() and removeMembership(). symfony-user-ds draws the screens on it.

Impersonation

Who may impersonate is the firewall's: its switch_user key, and the role it names. Without the key, nobody can, the impersonation page and search answer 404 — declare it under when@dev alone for an application impersonating in development only:

Who may be impersonated is impersonation.targets: administered, the accounts whose roles the actor administers — the hierarchy, or administration.manages, read at each request —, or any, every account — a development setting, where the scope of the application's AccountAdministrationGuardInterface is not asked either. Never oneself, a disabled or a locked account. ImpersonationService applies the rule to the list, the search and — through ImpersonationGuardSubscriber, on SwitchUserEvent — the switch itself: an account absent from the list cannot be reached by its URL. A voter could not refuse it: with the default strategy, the role voter's grant wins.

The switch happens on the page the chosen account lands on once signed in (post_login), so it opens where that account starts. From an impersonation, the page offers the next account and the way back: Symfony leaves the current account before taking the next, so the rights and the rule are always the original account's.

A switch must come from ImpersonateForm (require_intent): a bare ?_switch_user= link sent to an administrator switches nobody. A refusal is journaled as access.denied, the rule in extra.reasons (impersonation=not_administered, no_intent…).

GET /account/impersonate/search?q= (UserRoute::IMPERSONATE_SEARCH) answers the accounts matching by email, username or name: 2 characters at least, 20 results, 30 searches a minute per actor. The page is symfony-user-ds's.

Account picker

Signing in by choosing an account, with no password and no second factor: for an application whose users trust each other, or a demonstration. It is turned on by the firewall alone — no option:

Without it, the page (/login/accounts, UserRoute::ACCOUNT_PICKER, symfony-user-ds) answers 404 and a crafted submission signs nobody in. Every active account can be chosen: all listed up to impersonation.list_threshold, searched above (30 searches a minute per IP). The session opens as an AccountPickerToken, a class scheb's security_tokens does not list, hence no second factor; the account gates still apply, and the journal records login.succeeded with the method account_picker. Whoever reaches the page signs in as anyone: list it only where that is the intent, or under when@<env> for one environment.

After signing in

A user lands on the page they were sent away from, when access_control lets them in; otherwise on the route of the first role of post_login.routes they hold, through the hierarchy; otherwise on post_login.default_route, or the home page. Every way in ends there — password, second factor, reset, activation, terms accepted, magic link (LoginLinkSuccessHandler, the success_handler of login_link). A page refused by its controller rather than by access_control is not foreseen: it answers 403.

Security journal

Every security fact is dispatched as a Wexample\SymfonyUser\Event\SecurityEvent: its type (Enum\SecurityEventType — login.failed, second_factor.backup_code_used, password.set_by_admin…), the real cause of a failure (unknown_user, bad_password, disabled, locked, throttled, invalid_csrf, invalid_login_link, invalid, expired, too_many_attempts, rate_limited), the method of a sign-in (password, magic_link, password_reset, second_factor), the user id, IP, user agent, firewall, request id (X-Request-Id, or one generated per request — the same for every package of the suite, see symfony-security) and UTC time. Its shape is symfony-security's AbstractSecurityEvent, shared with the machine token journal of symfony-api.

Every access refused to a fully signed-in user — access_control, #[IsGranted], a voter — is journaled as access.denied, with extra.route, path (without its query), http_method, roles, attributes and reasons: what the voters gave to Vote::addReason(), where an application names the scope it refused (organization=42). A visitor sent to the login page is not denied. The same denial — user, route, attributes — is journaled once a minute, the next entry carrying in suppressed the count held back. The response stays the application's.

Every security mail handed to the sender is journaled as security_message.sent, or security_message.failed with the exception class, with extra.message_type — never its link, code or body.

It never carries a secret. An identifier typed for an unknown account is kept as extra.identifier_fingerprint, an HMAC that groups the attempts without the text.

The package writes them to the user_security Monolog channel, warnings for failures: give that channel its own handler and retention. Facts of the application itself go through SecurityJournalService::record().

The history of an account

Where symfony-activity is installed and enabled, SecurityActivitySubscriber writes every fact naming an account into its history, in the security category: security.<type>, with the method, the cause of a failure, the IP, the user agent, and the actor when an administrator acted. A fact naming no account — an address none holds — has no history to go into. Nothing is kept until the application enables security in wexample_symfony_activity.categories; what to keep, for how long, and whether to anonymize is decided there. The login history of an account is this category of its history.

The signature of magic and reset links is masked in every log record, the exceptions it carries included: the router logs each request URI, and a 404 quotes the referer. The package declares a masker (wexample_symfony_user.log_masker.link_signature, the hash query parameter) to symfony-security's redaction processor; an application adds its own there too. Keep doctrine.dbal.logging off outside development: the SQL log carries query parameters.

Account gates

What a signed-in user must do before reaching anything else, once the second factor is checked — whatever the way they signed in. AccountGateSubscriber holds them on the page of the first gate still blocking (an ajax call gets a 403 naming it); logout stays open. The package ships three, in this order:

An application adds a gate by implementing Interface\AccountGateInterface.

Table of Contents

Architecture

Every form of the package is an ordinary symfony-forms ajax form: it posts to /_forms/submit/{name}, and the front Form class reads a FormResponsePayload back. The security forms keep that transport, but the firewall answers before the forms bundle does. This section follows a login through those layers.

Login

src/Form/LoginForm.php is rendered through src/Service/FormProcessor/LoginFormProcessor.php, which is never asked to process it. On submission, src/Security/Authenticator/LoginFormAuthenticator.php supports() the forms bundle route for that form name and builds a passport: a UserBadge resolved by the firewall provider — src/Repository/AbstractUserRepository.php reads an email or a username —, the password, a CSRF badge using the token id the form was built with, and a remember-me badge.

Symfony then runs its own listeners: login throttling, password check, src/Security/UserChecker.php after the password (so a locked account is only revealed to its owner), password upgrade. src/EventSubscriber/LastLoginSubscriber.php stamps dateLastLogin.

onAuthenticationSuccess() and onAuthenticationFailure() answer JSON with the payload builder of the forms bundle when the caller expects it, a redirect otherwise. Failures become form errors through LoginFormProcessor::addAuthenticationError(), which maps unknown user and wrong password to the same message.

Second factor

scheb/2fa-bundle turns the token of a password login into a TwoFactorToken when src/Security/TwoFactor/EmailCodeTwoFactorProvider.php begins, which it does only for the login form route. prepare_on_login makes src/Service/TwoFactorCodeService.php send the code with the password check: the code is kept hmac-hashed in session, with its expiry and attempts, and failures are also counted per account in cache.app.

LoginFormAuthenticator sees the TwoFactorToken and answers with the code page, UserRoute::TWO_FACTOR (served by symfony-user-ds). The code form posts to the forms bundle route too, which the firewall declares as scheb's check_path; src/Security/Handler/TwoFactorResponseHandler.php answers success, failure and "code required" the same way the login does. Trusted devices are scheb's signed cookie, versioned by AbstractUser::getTrustedTokenVersion().

An authenticator app uses scheb's own totp provider, and the email provider steps aside for a user who has one. src/Service/TotpService.php keeps the secret being set up in session until a first code proves the app holds it, then stores it encrypted by src/Service/TotpSecretCipherService.php; src/EventSubscriber/TotpSecretSubscriber.php decrypts it on load, in memory only. Backup codes are stored as hashes on the user and checked by scheb before the provider.

Links

src/Service/MagicLinkService.php wraps the login_link handler of the firewall — the one of the current request, or of main without a request — and appends a checked local _target_path. src/Service/PasswordResetService.php signs reset links with Symfony's SignatureHasher over the password hash, so nothing is stored and the new password kills the link. Both reset modes end on a proof kept in session, granted by the reset link or, in magic link mode, by src/EventSubscriber/PasswordResetProofSubscriber.php; src/Service/FormProcessor/SetPasswordFormProcessor.php requires it.

Every link and code leaves through src/Interface/SecurityMessageSenderInterface.php, aliased to src/Service/SecurityMessageMailerSenderService.php, one template per src/Enum/SecurityMessageType.php under assets/mails/.

A password its holder did not choose

The forced change reuses the reset walk whole rather than adding a second password page beside it. PasswordResetService::getProofUser() — "who may set a password without typing the current one" — answers the holder of a session proof, and, failing that, the signed-in user whose passwordChangeRequired flag is up; hasProof() tells the two apart where it matters, which is the journal (password.reset against password.changed) and whatever a page wants to say about why it is being read. src/Security/Gate/PasswordChangeGate.php holds the account on that page, and src/Service/PasswordUpdaterService.php raises or clears the flag as it writes the password — raised when the actor is somebody else than the account, cleared in every other case.

An impersonated account is left out of both: getProofUser() refuses a SwitchUserToken and so does the gate, or an administrator visiting an account would be choosing its password.

The administration screens

Two forms whose processors act inside formIsValid(): most of what the rules refuse — a guard of the application, a protected role left without a holder — is only known by trying, and past onValid() the answer is already a success. src/Service/FormProcessor/AccountCreateFormProcessor.php keeps the account it opened, src/Service/FormProcessor/AccountRolesFormProcessor.php the one it is editing, and onValid() is left with the notification and where to go next.

Which account a submission is about travels in its URL — the entity form route of symfony-forms, /_forms/submit/{name}/entity/{id} — and never in a field: handleSubmittedRequest() reads the id, createForm() is handed the account and fills one switch per role from it. A switch per role rather than a multiple select, which the design system's select_input does not draw; the field is the role lowercased, and src/Traits/AccountRolesFormTrait.php is where both forms get it.

src/Controller/AccountActionController.php serves what carries no value but the intent — deactivate, lock, the password mail — as one POST route over Enum\AccountAction, so an action nobody declared is a 404 from the router. Its CSRF token id is the application's own form one, submit, so the buttons ask for no second stateless_token_ids entry.

Session lifetime

src/EventSubscriber/SessionIdleSubscriber.php runs between the firewall (8) and the account gates (4), at priority 6: a session past session.idle_lifetime is signed out before a gate can hold it on a page. It signs out through Security::logout() rather than by emptying the token storage, which is what clears the remember-me cookie — src/EventSubscriber/TwoFactorPendingSubscriber.php can do the shorter thing, since a password waiting for its code has no such cookie yet.

The stamp it keeps is refreshed by pages only. A request whose path opens on /_ is reached by a program — a component rendered on demand, a live subscription — and is held to the delay without pushing it back.

Tests

The fixture kernel (tests/Fixtures/App/AppKernel.php) boots the security, forms, loader and scheb bundles, and symfony-user-ds (a dev dependency) so the walks reach real pages, on SQLite in memory, with the stateless CSRF of the Flex recipe. Integration tests walk the flows over HTTP with disableReboot(); three traps:

A delay is never waited for: a test puts the session stamp back instead, as tests/Integration/IdleSessionTest.php and tests/Integration/SecondFactorPendingTest.php do.

Tests run from the design-system containers: docker exec design_system_local_symfony sh -lc 'cd /var/www/vendor-dev/wexample/symfony-user && php vendor/bin/phpunit'.

Integration in the Suite

This package is part of the Wexample Suite — a collection of high-quality, modular tools designed to work seamlessly together across multiple languages and environments.

Related Packages

The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.

Visit the Wexample Suite documentation for the complete package ecosystem.

Dependencies

Versioning & Compatibility Policy

Wexample packages follow Semantic Versioning (SemVer):

We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Free to use in both personal and commercial projects.

About us

Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.

This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.

Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.

Migration Notes

When upgrading between major versions, refer to the migration guides in the documentation.

Breaking changes are clearly documented with upgrade paths and examples.


All versions of symfony-user with dependencies

PHP Build Version
Package Version
Requires php Version >=8.5
doctrine/orm Version ^3.0
symfony/security-bundle Version ^7.4
wexample/symfony-helpers Version >=19.1.0
wexample/symfony-security Version >=4.0.2
wexample/symfony-translations Version >=17.0.1
symfony/validator Version ^7.4
wexample/symfony-forms Version >=14.0.2
wexample/symfony-loader Version >=27.0.0
wexample/symfony-mail Version >=4.0.0
symfony/form Version ^7.4
symfony/rate-limiter Version ^7.4
symfony/mailer Version ^7.4
symfony/twig-bridge Version ^7.4
scheb/2fa-bundle Version ^8.6
scheb/2fa-trusted-device Version ^8.6
scheb/2fa-totp Version ^8.6
scheb/2fa-backup-code Version ^8.6
endroid/qr-code Version ^6.0
wexample/symfony-tunnels Version >=11.0.5
wexample/symfony-activity Version >=3.0.5
wexample/symfony-api Version >=16.0.0
wexample/php-pseudocode Version >=3.0.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 wexample/symfony-user contains the following files

Loading the files please wait ...