Model resolution
Packages let hosts swap their models through config('<package>.models.*'). ModelResolver::for(string $key, ?string $default = null, ?string $base = null) does the resolve-and-validate dance once: the default applies only when the key is not set (absent, null or blank — '' or whitespace), and the class must exist, be an Eloquent model, and be $base or a subclass of it. $base defaults to $default — the packaged model a host extends — else to Model. Anything else throws InvalidConfigurationException naming the key; a wrong class never silently falls back to the packaged one:
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\PackageToolkit\Support\ModelResolver;
// Resolve + validate a model class from config: absent or blank → the packaged default; anything
// that isn't that class or a subclass of it throws (never a silent fall-back).
$class = ModelResolver::for('comments.models.comment', Comment::class); // class-string<Comment>
$model = ModelResolver::newModel('comments.models.comment', Comment::class); // a fresh instance
// The default is only a suggestion here — a host may name any Eloquent model:
$tenant = ModelResolver::for('comments.models.tenant', Team::class, base: Model::class);
// No default: the key is required, and any Eloquent model is accepted.
$class = ModelResolver::for('comments.models.comment');Pass base: Model::class when the default is only a suggestion a host may replace with an unrelated model. newModel() takes the same arguments and returns a fresh instance of the resolved class. A host model that does not extend the packaged one — or a class name that does not exist — fails like this:
// config('comments.models.comment') === App\Models\Post::class — not a Comment
ModelResolver::for('comments.models.comment', Comment::class);
// throws InvalidConfigurationException:
// Configuration value [comments.models.comment] must be a class-string of [<Comment's class>], [App\Models\Post] given.
// config('comments.models.comment') === 'App\Nope' — no such class
// throws the same message, with [App\Nope] given.The ResolvesModels trait
For classes that resolve several models from config, the ResolvesModels trait wraps ModelResolver in two protected methods — modelClass() and newModel() — each taking the same optional $default and $base, with the same rules:
use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\PackageToolkit\Concerns\ResolvesModels;
final class CommentRepository
{
use ResolvesModels;
public function create(array $attributes): Model
{
$comment = $this->newModel('comments.models.comment', Comment::class);
$comment->fill($attributes)->save();
return $comment;
}
public function find(int|string $id): ?Model
{
return $this->modelClass('comments.models.comment', Comment::class)::query()->find($id);
}
}observesModel() on the base provider resolves through the same validator — see Bindings & observers.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.