mnapoli/dotlock
dotlock is a small PHP library implementing dotlock-style file locking, letting applications create lock files to coordinate access and prevent concurrent processes from clashing. It provides a simple API for acquiring, checking, and releasing locks.
This is the first tagged release (0.1.0), so treat the package as pre-stable: install it on a branch first and confirm it behaves before it reaches composer.json on main.
Install and pin
composer require <vendor>/<package>:^0.1
Commit composer.lock. Under pre-1.0 semver, a 0.2.0 bump is allowed to break you — pin to 0.1.* if you need a quiet week, and let Renovate/Dependabot open a PR when 0.2 lands so you review the diff deliberately rather than absorbing it on a composer update.
Where to look first, in this order
composer.json — check require for the Laravel version constraint, and the extra.laravel.providers / extra.laravel.aliases block. That tells you package auto-discovery is wired up and you don't need to register anything in config/app.php by hand.register() and boot() — this is the package's real table of contents. What it binds into the container, what it merges (mergeConfigFrom), what it publishes (publishes()), what it loads (migrations, routes, translations, views), and what it registers (commands, schedule entries, event listeners). Five minutes here saves an hour of guessing.config/<package>.php in the package source — the comments in that file are usually the most accurate documentation a 0.1.0 has. Publish it and read it before writing any code against the package.src/ entry points — the facade (if any), the primary manager/service class, and any Console/ directory.Publish what's publishable, then diff it
php artisan vendor:publish --provider="Vendor\Package\PackageServiceProvider"
Publish into your own repo and commit the result. A published config file is your record of what the defaults were at install time; when 0.2.0 changes a default, git diff tells you instead of a silent behavior shift in production. Use --tag= to publish selectively (config only, migrations only) so you aren't dumping views you'll never override.
First use case
Pick the smallest real thing the package does and prove it end-to-end in a test, not a tinker session. A first smoke test that resolves the main class from the container and asserts one output is worth more than a manual click-through — it becomes the regression test that catches a breaking 0.2.0.
it('resolves the package service', function () {
expect(app(\Vendor\Package\TheMainClass::class))->toBeInstanceOf(\Vendor\Package\TheMainClass::class);
});
Wrap it, don't sprinkle it
The single highest-value pattern for a 0.1.0 dependency: put it behind one thin application-owned class rather than calling it from controllers, jobs, and commands directly.
namespace App\Support;
class ThingGateway
{
public function __construct(private \Vendor\Package\Client $client) {}
public function push(array $payload): Result
{
return $this->client->send($payload);
}
}
Bind it in a AppServiceProvider, inject that everywhere. When 0.2.0 renames a method or changes a return type, you edit one file. This is the cheapest insurance available against pre-1.0 churn, and it costs almost nothing.
Resolve from the container, never new
If a class is bound in the provider, app(Foo::class) and constructor injection get you the configured singleton — the one with your config applied, the right HTTP client, the right logger. new Foo() gets you a default-constructed instance that ignores all of it and will behave differently in tests than in production. Let the container do its job.
Configuration belongs in config/ and .env
Never pass credentials or endpoints inline. Add the package's keys to your .env.example with placeholder values and to config/<package>.php reading via env(). In tests, override with config(['package.key' => ...]) in the test or phpunit.xml <env> entries — not by mutating $_ENV.
Integrate through Laravel's seams
The package is most useful when it plugs into mechanisms you already have, rather than being called from a controller:
$tries, $backoff, and failed() handling. If the package ships its own jobs, check whether they're ShouldQueue and whether they're idempotent on retry.routes/console.php or the kernel with withoutOverlapping().TestCase::setUp() so the suite never touches the network.Watch the migration story
If publishes() includes migrations, decide immediately whether you own them. Publish them and treat them as yours (safer: you control schema changes), or leave them package-owned and re-run vendor:publish --force on upgrade (simpler: you inherit fixes). Don't half-do it — a published migration plus a package that also registers its own will collide. Check loadMigrationsFrom vs publishes in the provider.
Config cache is the number one source of "it worked locally"
php artisan config:cache in production freezes config() at boot. Calling env() from anywhere outside config/ returns null once cached. If the package reads env() directly in its source, it will work in local dev and silently fail in a cached production environment — grep the package for env( and verify. Always php artisan config:clear after editing config, and test the deploy path with config:cache on, not off.
Auto-discovery can be disabled in your app
If bootstrap/providers.php (Laravel 11+) or config/app.php lists providers explicitly, or if someone previously ran php artisan vendor:publish on a providers stub, discovery may be off. Symptom: class not found, config not merged, commands missing from php artisan list. Check php artisan about and php artisan list before debugging anything else.
Pre-1.0 breakage is normal, not a bug report
0.1.x is explicitly allowed to make breaking changes in patches and minors. Read CHANGELOG.md on every bump — not just the release title. "First release" today means the changelog has nowhere to go but detailed; hold the package to that as it matures.
Debugging: go to the container, not the docs
php artisan tinker then app()->make(Foo::class) tells you if binding works. app()->getBindings() and dd() inside the service provider's boot() (temporarily) tells you what actually ran. Laravel's exception handler will surface a class-not-found from a package as a generic error — read the full stack trace to see whether it originates in the package's register() (wiring problem) or in your call site (usage problem).
Check the boot() side effects
Providers run on every request, including queue workers and Artisan commands. If boot() registers listeners that hit the network, or publishes views, or does heavy work, you'll feel it on every job. Look for anything in boot() that isn't a cheap registration call.
Extension points to note for later
While reading the provider, record: are classes bound via interfaces (swappable, testable), or is everything a concrete singleton? Are there Macroable traits, custom driver managers, or extend() hooks? If the package offers none, your wrapper class from the section above becomes the extension point — which is another reason to have one on day one.
Verify it on your actual Laravel version and PHP version
0.1.0 packages are often tested against a single matrix. Check composer.json constraints and, if you're on the newest Laravel minor or an odd PHP version, run the smoke test before anything else. Composer's --with-all-dependencies may be needed if the package drags a conflicting transitive constraint.
Logging and observability from the start
Whatever the package does, ensure failures surface: check that exceptions aren't swallowed inside a try/catch with no report(), and that queued integrations have failed() handlers logging with context (payload ID, tenant, etc.). On a 0.1.0 you're the QA team — make sure you can see what broke.
How can I help you explore Laravel packages today?