marcin-orlowski/lockpick
Lockpick is a Laravel package that offers a clean, fluent API for working with cache-based locks. It simplifies acquiring, releasing, and inspecting locks, with support for timeouts and custom lock names, making concurrency control easier in Laravel apps.
Architecture fit
Lockpick is a framework-agnostic PHP utility built on the native Reflection API. Its stated purpose — reading/writing protected and private members and constants on foreign objects — places it firmly in the test and diagnostics tier, not the application runtime tier. That is the correct architectural slot for it: Laravel's own container, Eloquent internals, and queue/session payloads are frequently opaque, and a terse accessor reduces the boilerplate of ReflectionClass + setAccessible incantations in tests. It is not a Laravel package in the conventional sense — no service provider, no facade, no config, no publishable assets — which is a point in its favor for footprint and a point against it for discoverability and framework-idiomatic ergonomics.
The core architectural tension is that the package's entire value proposition is deliberate encapsulation violation. That is acceptable and often correct inside a test suite asserting on state that a vendor class does not expose. It becomes an architectural liability the moment it appears in app/ or a service provider, because it converts a compile-time/visibility contract into a runtime string lookup that no static analyser will validate.
Integration feasibility
Low effort. Composer install as a require-dev dependency, no auto-discovery step, no configuration to publish, no migrations. A developer can be productive within an hour. The integration work is almost entirely governance — deciding where the package is allowed to be referenced and enforcing that boundary — rather than engineering.
Technical risk
ReflectionException that says nothing about the actual regression.ReflectionProperty::setAccessible() has been a no-op since 8.1, readonly properties reject writes, and property hooks in 8.4 change what "accessing a member" even means. Verify which PHP versions are actually exercised in CI, not just declared in composer.json.composer.lock. That is a process risk you must close with tooling, not documentation.Key questions
ReflectionException?spatie/invade, which has orders of magnitude more adoption and effectively occupies this niche?Stack fit
Compatible with any Laravel 10/11/12 application on PHP 8.1+. Sits alongside PHPUnit or Pest without conflict, since it does not extend the testing framework. It complements, rather than replaces, Laravel's own testing helpers (withoutExceptionHandling, container swap, assertDatabaseHas) — those should remain the first resort, with Lockpick reserved for the narrow case of asserting on state a vendor object genuinely refuses to expose.
Migration path
ReflectionClass blocks or uses Closure::bind tricks in tests, Lockpick is a readability win and the migration is mechanical.spatie/invade, the honest recommendation is do not migrate. Invade has vastly more usage, more eyes on the reflection edge cases, and a maintainer with a track record. Adopting Lockpick there trades a well-trodden dependency for an unproven one with no functional gain.Tests\Support\ReadsInternals) that wraps whichever library you pick. Call sites depend on the trait; the underlying package becomes a one-file swap.Compatibility
Check the package's own composer.json constraints against your PHP floor and your PHPUnit major. A narrow PHP pin on an unmaintained package is the classic way a routine framework upgrade gets blocked for a week. Also confirm there is no phpunit/phpunit requirement that would fight your test runner version.
Sequencing
spatie/invade.composer require --dev, committed lockfile, recorded in the ADR log with an explicit "test-scope only" rationale.tests/ — a PHPStan rule, a Deptrac layer, or a plain grep gate in the pipeline. This is the highest-value step in the whole sequence.Maintenance
The dependency itself requires almost nothing — no scheduled upgrades, no config drift, no runtime surface. The maintenance burden is indirect: every test that uses it becomes a test you must revisit whenever the reflected vendor package changes. Budget for that as test-suite friction, not as a line item for this package. Bus factor is one; treat the code as effectively yours and keep a fork path in mind. The small size makes that a genuine option rather than an empty reassurance.
Support
Community-only, no SLA, low issue velocity given adoption signals. Acceptable for a dev-only tool where the fallback is "write four lines of native reflection yourself." Not acceptable anywhere on a production request path.
Scaling
No runtime scaling concerns — the package has no presence in the hot path if the boundary is enforced. The scaling concern is organisational: reflection-based tests couple to implementation detail, so as the suite grows, upgrade work grows superlinearly. Ten tests poking at one vendor class is manageable; two hundred is a migration project. Enforce the guard rail early, while the call-site count is small enough to audit by hand.
Failure modes
setAccessible no-op) → package may lag. Mitigate by pinning the PHP floor deliberately and testing upgrades in CI.Ramp-up
Effectively zero for the API — a mid-level developer understands it in under an hour, and native reflection knowledge transfers directly. The real onboarding cost is judgement: teaching the team the difference between "this is the only way to test this" and "this is the easy way to avoid designing a seam." That deserves a paragraph in the testing guidelines and a named reviewer for the first few pull requests that use it. Allocate roughly a day of combined evaluation-and-decision time; treat anything beyond that as a signal to just use native reflection and skip the dependency entirely.
How can I help you explore Laravel packages today?