- What does the cjmellor/approval package do?
- cjmellor/approval is a Laravel package that stages new or amended Eloquent model data in a polymorphic approvals table before it is persisted, so a reviewer can approve or reject it first. Adding the MustBeApproved trait to a model intercepts create and update events and stores the dirty attributes as JSON alongside the original data. It gives you an auditable change-request ledger without building your own staging table, but it does not ship a review UI, policies, or notifications.
- What Laravel and PHP versions does cjmellor/approval support?
- It requires PHP 8.3 or higher and Laravel 12.4 or higher, or Laravel 13. The 12.4 floor exists because the package uses the #[Scope] attribute introduced in that release, so verify your exact minor version before estimating the work. Laravel 11 support was dropped in v2.1.0 after its end of life in March 2026, and L11 applications are pinned to ^2.0. If you are not already on 12.4 or newer, treat this as an upgrade project rather than a simple install.
- How do I install and configure cjmellor/approval?
- Run composer require cjmellor/approval, then publish and run the migrations with php artisan vendor:publish --tag=approval-migrations followed by php artisan migrate. Publish the config with php artisan vendor:publish --tag=approval-config if you need to change the table name, foreign key, or custom states. Finally, add the MustBeApproved trait to each model you want to moderate. The config file is small and readable, and the publish tags are namespaced so they will not collide with other packages.
- Does the package actually approve the data and apply it to my model?
- No, and the README is explicit about this: the package stores the new or amended data and does not approve or deny it for you. Authorization, the review interface, notifications, conflict resolution, and the code that writes approved data back onto the model are all application responsibilities. That is a clean separation, but it means the package covers perhaps a fifth to a third of a usable approval feature. Budget your real effort in the admin surface and the apply-approved-data path.
- Do query builder writes and seeders bypass the approval process?
- Yes. Enforcement is event-based, so Post::where(...)->update(...), insert(), upsert(), seeders, and raw DB::table() statements never touch the trait. If you intend to rely on this package as an audit gate, that is the single largest gap and must be mitigated with code review rules, static analysis, and database permissions. Model events are the control point, not the database.
- How do I track who requested an approval?
- Use requestedBy when creating the approval and read it back with wasRequestedBy or the requestor relation. Note that the default user_id foreign key is only populated if you pass it explicitly at create time; if you miss it, foreign_key stays null while creator_type and creator_id still carry the requestor. That leaves two parallel notions of who, which is easy to conflate downstream, so pick one convention and stick to it.
- Can I limit approvals to specific model attributes?
- Yes, set the $approvalAttributes property on the model to control which attributes are captured for sign-off. This keeps noisy or irrelevant fields out of the approvals table and makes the pending payload easier to review. It is a per-model setting, so different models can demand different levels of scrutiny.
- Can approvals expire or be processed automatically?
- Yes. Pending approvals support expiresIn with hours, minutes, days, or a datetime, combined with thenReject, thenPostpone, or thenCustom to define what happens at expiry. You can query state with expired(), notExpired(), and hasExpiration(), and the approval:process-expired command handles the sweep. That time-based behaviour is the main feature separating it from a hand-rolled approvals table.
- How do I bypass approvals in tests, seeders, or trusted admin flows?
- The package provides withoutApproval() as an escape hatch, plus isApprovalBypassed() to check the current state. Wrap seeding, imports, or trusted admin operations in withoutApproval() so they persist immediately. Keep those call sites few and visible, since every bypass weakens the audit trail you installed the package for.
- Can I roll back an approved change?
- Yes, rollback(bypass: true) restores the previous data and sets rolled_back_at, giving you a reversible audit trail. The bypass flag is the default, which means a rollback silently skips re-approval; that is convenient but worth flagging in a compliance review if rollbacks are meant to be governed. Nothing in the package defines multi-record conflicts either, so two pending approvals for the same model and last-write-wins or lost-update scenarios remain your responsibility.