Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Connections For Laravel Laravel Package

roundly-consulting/connections-for-laravel

Connections for Laravel provides an Eloquent-friendly API for creating and managing connections between models, including requests, statuses, and queries. Ideal for friendships, follows, invitations, or any bidirectional relationship. Lightweight and easy to integrate into existing Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pin the fixed version. The package is small and the release history is short; pin explicitly rather than floating so no environment resolves to a pre-1.1.2 build where factories ignore a host-swapped model.

composer require roundly-consulting/connections-for-laravel:^1.1.2

2. Discover what actually ships before you publish. The README description talks about a named connection registry for external services (APIs, queues, drivers), while the package name, repo and the assessment-pm/assessment-tpm notes describe many-to-many connections between Eloquent models with permissions and expirations. These are two different packages. Resolve the ambiguity from source, not prose:

ls vendor/roundly-consulting/connections-for-laravel
ls vendor/roundly-consulting/connections-for-laravel/database/migrations
cat vendor/roundly-consulting/connections-for-laravel/composer.json   # Laravel/PHP matrix

If there is a database/migrations directory, this is the Eloquent pivot package and the rest of this assessment applies. If there is a config/connections.php full of driver/base_url/api_key keys, it is the service-registry package and the sections below about pivots and permissions do not apply.

3. Publish and migrate.

php artisan vendor:publish --tag=connections-config
php artisan vendor:publish --tag=connections-migrations
php artisan migrate

Tag names are guesses — run php artisan vendor:publish with no arguments and pick from the interactive list on first install, then write the tags down in your service README.

4. Read these three files in this order. They define the entire contract:

  • The published config — the model map (which class backs the connection record and the pivot), the table names, and any default permission/expiry settings. This is the swap seam.
  • The base connection model / pivot model — look for an interface or abstract class the host is expected to extend or implement. That is the contract a swapped model must satisfy.
  • The factory — confirms 1.1.2 behaviour that a factory call returns an instance of the configured class.

5. Add the trait to the models you want to connect. Typically both sides of the relation, or the "connectable" side. Then do the smallest possible end-to-end thing: connect a User to a Project with a permission and an expiry, and assert the round trip.

$project->connect($user, permissions: ['read', 'write'], expiresAt: now()->addDays(30));

$project->connected($user);                       // bool — active connection
$project->connectionFor($user)->can('write');     // bool — permission check

Treat those method names as placeholders: the point of the first use case is to force you to read the real API and to prove that a permission and an expiry both survive the write/read cycle before you build anything on top.

First real use case: time-boxed access grants — a contractor gets write on a client account for 90 days, then silently loses it without any cleanup job.

Implementation Patterns

Wrap the package, don't sprinkle it. The public surface is thin and the project has five stars; treat it as an implementation detail behind one app-level service or trait of your own. Then a future replacement is a one-file change rather than a grep across the codebase.

final class Access
{
    public function grant(Model $resource, User $user, array $abilities, ?Carbon $until = null): void
    {
        $resource->connect($user, permissions: $abilities, expiresAt: $until);
    }

    public function allows(Model $resource, User $user, string $ability): bool
    {
        return (bool) $resource->connectionFor($user)?->can($ability);
    }
}

Split "does the connection exist" from "is it still valid". These are two questions and the package gives you the row, not the policy. Decide once, in one place, whether an expired row means no access (filter at query time) or audit trail (keep the row, filter in a scope). Do not let both idioms leak into different call sites.

// config/connections.php or your own layer
User::scopeWithActiveConnection($q, Model $resource) => ...
    $q->whereHas('connections', fn ($c) => $c
        ->where('connected_id', $resource->getKey())
        ->where(fn ($w) => $w->whereNull('expires_at')->orWhere('expires_at', '>', now())));

Always eager load with the pivot columns declared. Permissions and expiry live on the pivot. If you don't pull them through, can() will return false and you will spend an hour blaming the package.

$users = $project->connectedUsers()
    ->withPivot(['permissions', 'expires_at'])   // verify exact column names in the migration
    ->get();

If the shipped pivot is a real Eloquent model (likely, given the factory fix), use ->using(Connection::class) and define a Connection scopes file: active(), expired(), forAbility($ability).

Expiry is a read-time concept — make the write side do the cleanup. Expired rows accumulate. A daily php artisan connections:prune (or a tiny custom command) keeps the table honest if you don't need the history; if you do need it, never prune, and make sure every query path is scoped. Pick one and write it in the README.

Authorization: one source of truth. Connection permissions are relationship-scoped abilities (can Bob edit this document), not a replacement for Laravel Gates (can Bob access the admin panel). Bridge them rather than duplicating:

Gate::define('update-project', fn (User $u, Project $p) =>
    app(Access::class)->allows($p, $u, 'write'));

This is the highest-leverage integration point in the whole package: it lets you keep @can('update-project', $project) and Blade/authorize() idioms while the actual grant lives in a time-boxed pivot row.

Model swapping is the intended extension point. Host apps replace the package's connection/pivot model via config (or container) to add columns, casts or global scopes. 1.1.2 exists specifically so package factories build that class. If you need an audit_log_id, a granted_by column, or a SoftDeletes connection, add it to your own model and swap it in — don't fork.

// config/connections.php
'models' => [
    'connection' => App\Models\Connection::class,
],

Your swapped class must extend the package base and satisfy whatever interface it documents; keep the table name and the columns the package reads.

Testing. Package factories now return the configured model, so a host-side ConnectionFactory override is no longer needed on ^1.1.2. Assert the class, not just the row:

$connection = Connection::factory()->create();
$this->assertInstanceOf(App\Models\Connection::class, $connection);

Add one test that travels the expiry path (freeze time, assert allows() flips without touching the DB) and one that travels the permission path. Those two tests are your regression net for a package with no visible CI you control.

Gotchas and Tips

The documentation lies to you first. description.md describes a DI-container registry for external services; the repo name and the release notes describe a polymorphic pivot with permissions. Read composer.json, the migrations and the config from vendor before you plan anything. If you built an architecture against the registry description, everything downstream is wrong.

Config cache is the number-one "my override isn't applied" bug. A swapped model bound through config will not take effect until you re-run php artisan config:cache. Do it in the deploy script, not by hand, and note it in the README — this is the non-obvious knowledge the next engineer will otherwise rediscover.

Below 1.1.2, swapped-model tests are silently wrong. Factories built the package's base model, so assertions passed against a class production never instantiates. That is a correctness-of-confidence bug, not a crash. Enforce the version floor in composer.json and re-review any test that only ever ran pre-patch — some of them were asserting against the wrong object graph entirely.

A swapped model that doesn't satisfy the contract fails at runtime, not at boot. Missing interface, missing method, missing cast — you find out on an edge path in production. Add a test that instantiates the swapped model through the package and hits every method your app calls.

Pivot columns are invisible until declared. withPivot([...]) (or an explicit pivot model) is mandatory for permissions and expires_at. Symptom: can() always false, expiry always null. Debug with $connection->pivot->toArray() and dd($query->toSql(), $query->getBindings()) rather than guessing.

No cascade on the pivot. Deleting a User or Project may leave orphaned connection rows depending on the migration's FK setup — check onDelete('cascade') in the published migration and add it via your own migration if absent. Orphan rows plus a permission check is a quiet data-integrity drift.

Permission storage format is a decision you inherit. Whether permissions is JSON, a comma string, or a bitmask determines whether can('write') is an exact match or a subset check, and whether you can index it. Inspect the migration and the cast before you write any query that filters on permissions — whereJsonContains vs LIKE vs bitwise & are not interchangeable.

Prune vs preserve is irreversible. Once you ship a pruning command, the grant history is gone. Decide early whether the table is a current-state table or an audit log; retrofitting audit columns after pruning has been running for months is a backfill project.

Extension points, in order of usefulness: (1) swapped model via config for extra columns/casts/scopes; (2) a custom pivot model class for behaviour; (3) your own service wrapper for policy. There is no obvious driver system in this package despite what the description claims — don't plan for one.

Five stars, single maintainer, no vendor SLA. There's no public CI, static analysis or release cadence you can rely on, and the assessment notes flag bus factor as unknown. Wrap it, pin it, keep the version floor recorded in your dependency policy, and assign a named owner internally. The API is small enough that replacing it with ~200 lines of your own pivot model is a realistic escape hatch if upstream goes quiet — which is exactly why the wrapper in Implementation Patterns matters more than the package itself.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log