rich-id/terms-module-bundle
A Laravel/PHP module bundle for adding terms and conditions management to your app. It packages common model, migration, and routing pieces so you can publish reusable terms content and integrate it into existing projects with less boilerplate.
Install and register the bundle
composer require rich-id/terms-module-bundle
Flex should wire it automatically; otherwise add it to config/bundles.php:
return [
// ...
RichId\TermsModuleBundle\RichIdTermsModuleBundle::class => ['all' => true],
];
Confirm the actual bundle class name against the README — this is a small, early-stage package (2 stars, 0 dependents), so names in docs may drift from the code.
Look here first, in this order
README.md — the only reliable source for the config tree and voter attribute names.src/DependencyInjection/Configuration.php — the canonical list of options (entity class, table prefix, version resolver).src/Security/ — the voter itself. Read it before wiring anything; it defines the attribute string you'll pass to is_granted().src/Entity/ (or a Models/ dir) — the persistence shape for terms versions and user approvals.Configure the minimum
Publish/define the config, point it at your User entity, and choose an approval storage target. Make sure that User class actually implements Symfony\Component\Security\Core\User\UserInterface — as of v3.0.3 the bundle's type hints target the interface rather than a concrete user class, so a custom provider or a legacy user entity that only satisfies an older contract is the most likely thing to blow up first. Then generate and run migrations for whatever tables the bundle ships (terms documents, versions, and a join table to users/approvals).
First use case: gate a route behind acceptance The package exists to answer one question — has this user approved the current terms version? Start by protecting a single controller action rather than the whole app:
#[IsGranted('TERMS_APPROVED')] // verify exact attribute in the voter
public function checkout(): Response { /* ... */ }
Render the terms page, post an "accept" action (the bundle should expose a form type or a service to record approval), and verify the grant flips. Once that single flow works end-to-end, broaden to a listener.
Route-level gating (simplest)
#[IsGranted] on controllers that legally require acceptance (checkout, publishing, API writes). Keeps the blast radius small and makes the rule visible at the call site.
Global gate via kernel listener
For "everyone must accept before using the app," a kernel.request subscriber that checks the voter and redirects to the terms route. Exclude the terms route itself, login/logout, and health checks with a path regex — otherwise you get a redirect loop.
Template-level branching
In Twig: {% if is_granted('TERMS_APPROVED') %} to show/hide a banner or nav item. Note that is_granted with an anonymous token returns false, so pair it with an app.user check if you want different behaviour for guests vs. logged-in-but-not-accepted users.
Version-bump workflow The version support is the point of the package. Typical loop:
(user, version, acceptedAt) — never overwrite the previous row if you want an audit trail.Wire this into your deploy process: bump the version as part of a release, and expect a wave of re-acceptance traffic the first day.
Post-acceptance hook
If the bundle dispatches an event on approval (check src/Event/), subscribe to it for side effects — sending a confirmation email, stamping user->setTermsAcceptedAt(), or recording an audit log entry. Prefer the event over duplicating logic in the controller.
Testing
Instantiate the voter directly in a unit test with a mocked token and user; assert on the expected attribute name and on the version comparison boundary (same version → granted, older version → denied). Mock the user against UserInterface rather than your concrete entity — that mirrors how the bundle resolves the user post-3.0.3 and keeps the test honest about the contract it depends on. End-to-end, use a functional test that logs in, POSTs the acceptance form, and asserts the previously forbidden route now returns 200.
UserInterface compatibility is version-sensitive. v3.0.3 is explicitly a fix for UserInterface compatibility, which means earlier 3.0.x builds had a mismatch worth knowing about. If you're pinned below 3.0.3, expect type errors or silently-failed voter checks with custom user classes, UUID identifiers, or a user provider that returns a non-Doctrine object. Upgrade before debugging anything else.false silently rather than throwing. Grep the voter source for supports() and copy the literal.is_granted() returns false, not an exception. In dev this looks like a broken login. Check var/log/dev.log and confirm the token is actually authenticated before blaming the bundle.AbstractVoter may memoize decisions per request. If you record acceptance mid-request and immediately check the grant, you may get a stale answer — redirect after acceptance rather than re-checking inline.doctrine:migrations:migrate before warming the cache, or you'll get table-not-found errors from a console command that touches the voter.getId() vs. getUserIdentifier()). The 3.0.3 UserInterface fix narrows this, but still verify the join column type matches your user ID type (integer vs. UUID) before running migrations.bin/console debug:container | grep -i terms, bin/console debug:config rich_id_terms (adjust the alias from Configuration.php), and bin/console debug:event-dispatcher will tell you what's actually registered far faster than reading the README.services.yaml (decorates:), decorate the approval storage with your own class, or replace the version resolver via config. Avoid forking the bundle — at this maturity the API may change under you, and a fork cuts you off from upstream fixes.composer.json, watch for breaking changes in patch releases (3.0.3 is a patch that changes a type contract — read the diff), and re-run your voter unit tests on every bump.How can I help you explore Laravel packages today?