ark4ne/laravel-repository
A Laravel package implementing the repository pattern, providing a structured way to separate data access from business logic. It helps keep model queries organized behind repository classes, making applications easier to test, maintain, and adapt as data sources change.
This is a 0.x package, so treat the public API as provisional and pin the version in composer.json ("vendor/package": "0.1.*"). The single headline change in v0.1.0 — instantiating models through an abstract method rather than a hardcoded class — is the seam everything else hangs off.
Minimal steps:
composer require vendor/package:^0.1vendor/vendor/package/src/ first. Locate the abstract method (conventionally newModel() or newInstance()) — that is your integration point, not the constructor.namespace App\Support;
use App\Models\Post;
use Vendor\Package\AbstractThing;
class PostThing extends AbstractThing
{
protected function newModel(): Post
{
return app(Post::class); // or: new Post
}
}
$this->app->bind(AbstractThing::class, PostThing::class);
app() calls so tests can swap the binding:public function __construct(private AbstractThing $thing) {}
If the package ships a config file, publish it now (php artisan vendor:publish --tag=...) before you have working code to debug against — config merges are easier to reason about on a clean install.
One subclass per model, bound by context. The abstract-method design implies the package is model-agnostic. Mirror that: keep a thin subclass per aggregate (PostThing, CommentThing) and bind whichever one a given service needs. Avoid a single "god" subclass with a match on model type inside newModel() — that reintroduces the coupling the abstraction was meant to remove.
Interfaces over the abstract class at injection sites. Type-hint an interface your subclasses implement, not the package's abstract class, so you can double it in tests without dragging in package internals:
interface CreatesModels { public function newModel(): Model; }
Explicit container resolution inside newModel(). Returning app(Post::class) (rather than new Post) means test bindings, Model::preventLazyLoading(), and global scopes all apply consistently. It also lets you swap in a stub model during tests. Use new Post only if you specifically need to bypass container overrides.
Compose with query building, don't rebuild it. The abstract method gives you a blank instance; scopes, where() clauses, and eager loads still belong in your repositories/queries. Pattern:
$post = $this->thing->newModel()->newQuery()->wherePublished()->firstOrFail();
Testing. Two reliable approaches:
tests/Support/ returning a model with a factory-built state.new class extends AbstractThing { protected function newModel(): Post { return new Post; } }.Assert on the returned instance type, not the internal call, since the abstract method is the only contract.
Jobs and queues. Because newModel() produces a fresh, unsaved instance, never serialize the package object itself into a queued job. Pass the model's key and re-resolve inside handle() — otherwise you may serialize a stale or partially-hydrated instance.
Multi-tenant / multi-connection setups. Override newModel() to call $model->setConnection($tenant->connection) or ->on('tenant') before returning. This is the cleanest place for per-context model configuration, since every instance the package creates funnels through it.
app(AbstractThing::class) throws. Every error of the form "Cannot instantiate abstract class" traces back to a missing concrete binding — check your service provider's bind() line first.: Model, PHP covariance permits : Post, but not the reverse, and not a union that omits Model. Getting this wrong produces fatal errors at class-load time, not at call time — the stack trace points at the subclass, not the call site.bind() instead of bind()-per-resolution in a long-running process (Octane, queue workers), a cached model instance can leak state between requests. Register as a non-shared binding, or return a fresh instance from newModel() every call.newModel(). The method name promises a new instance. Memoizing it (??=) turns a factory into a repository and produces subtle "my changes disappeared" bugs when the package internally calls it more than once per operation.dd(get_class($this->thing->newModel()));
A stale bind() in a package's own provider can silently override yours depending on provider registration order — check bootstrap/providers.php / config/app.php ordering.@return docblock (or template annotation) on your overrides. Tools like PHPStan and Psalm cannot infer the concrete model from the abstract signature alone, so property access on the returned model will report as undefined without it.newModel() body is trivial to update if the abstract signature gains parameters or a return type. Anything more elaborate in there becomes migration debt.newModel() for instance construction, but reach for the surrounding template methods (or a decorator wrapping your subclass) before forking the package. Because construction is now abstracted, decorators compose cleanly — wrap the concrete subclass, delegate, and post-process the returned model.How can I help you explore Laravel packages today?