roundly-consulting/approvals-for-laravel
Approvals for Laravel adds flexible approval workflows to Eloquent models. Route records through approval chains, assign approvers, and track decisions with a simple API, making it easy to build review and sign-off processes into your Laravel applications.
The package is installed with Composer and, like most Laravel packages, registers its service provider through package discovery — there is usually no manual config/app.php edit required. Publish the package config before doing anything else:
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config
Where to look first, in order:
First use case. The canonical first task is swapping a model in a host application: the package ships a default model (say Vendor\Package\Models\Post), you point the config at App\Models\Post, extend the package model, and confirm the package now reads and writes through your class. A minimal smoke test after the swap:
it('resolves the swapped model', function () {
expect(config('package.models.post'))->toBe(\App\Models\Post::class)
->and(get_class(app(\Vendor\Package\Contracts\PostContract::class)))
->toBe(\App\Models\Post::class);
});
As of 1.2.5, the same test on the factory side is equally important — see below.
Config-driven resolution, not hard-coded classes. The healthy pattern is to always ask the container or the config for the model class rather than referencing a concrete class in your own code:
$postClass = config('package.models.post');
$post = $postClass::create($data);
This keeps your host code compatible if you later swap again (multi-tenant setups, test doubles, or a future package default).
Subclass-and-extend for swaps. When swapping, extend the package model rather than rewriting it, and only override what actually differs:
namespace App\Models;
class Post extends \Vendor\Package\Models\Post
{
protected $table = 'posts';
protected $fillable = [/* widen as needed */];
}
Keep $table, $fillable, $casts, and any $appends in sync — the package's own queries and factories assume the parent's schema.
Factories follow the swap (1.2.5). This release fixes a real correctness bug: previously, calling a factory in a host app that had swapped the model could build the package's model instead of yours. The pattern to rely on now:
// In tests, after swapping config to App\Models\Post
$post = Post::factory()->create();
expect($post)->toBeInstanceOf(\App\Models\Post::class);
expect($post->getTable())->toBe('posts');
Prefer calling factory() on the swapped class (App\Models\Post::factory()) when your model defines newFactory(), and call the package factory directly only when you deliberately want the package's default shape. Add one assertion like the above for each swapped model — this is exactly the regression 1.2.5 addressed, so pin the behaviour in your suite rather than trusting it implicitly.
Isolate swaps in tests. Swap in setUp() and restore in tearDown() (or use a dedicated trait) so config leakage between tests does not silently change which model a later test exercises:
protected function setUp(): void
{
parent::setUp();
config(['package.models.post' => \App\Models\Post::class]);
}
Integration tip — seed and migrate together. Because factories now build the configured model, host migrations must exist before any package factory runs. In a fresh test database, ensure your host migration that creates/alters the table is ordered ahead of package seeders.
Cross-model relationships. When swapping a model that other package models relate to, verify the relation methods resolve through the container too. If a relation is hard-coded to the package class, override the relation method on your subclass and delegate:
public function author()
{
return $this->belongsTo(config('package.models.user'));
}
php artisan config:cache in a deploy can leave stale model mappings. Always run php artisan config:clear after changing the model map, and make sure any runtime swap happens before the config is read — not after the provider has booted.register()), not inside a controller or job.new-ing your model in tests — those assertions may now fail. That is the fix working. Grep your tests for Vendor\Package\Models\ and update expectations to your swapped class.*_type values for polymorphic relations. If you swap after data exists, either add a morph map (Relation::enforceMorphMap([...])) or plan a data migration — the factory fix does not migrate existing rows.get_class($model), $model->getTable(), and dd(config('package.models')) in php artisan tinker will answer "did the swap take?" faster than reading the provider. For factories specifically, dd(Post::factory()->make()::class) confirms 1.2.5 is doing its job.app/ via PSR-4 shadowing works but breaks on package updates; avoid it unless the package documents support for it.composer.json and re-run your model-swap tests in CI, since factory/model resolution is now a behavioural contract you depend on.How can I help you explore Laravel packages today?