Quotas and meters
Credits are spent and come back. Seats and projects are different: a standing count of what exists, which the plan caps. Those are meters, and they are a separate question with a separate door, $org->quota().
Why a class and not a number
The application this came from had a one seat plan, showed it on the usage screen, and never checked it when inviting. The cap was enforced for cases and forgotten for members, in the same codebase, because enforcing it meant every caller had to remember to count first and the person writing the invite form was not thinking about billing.
A meter moves the counting into one class, so no caller has to know how:
$org->quota()->allows('members'); $org->quota()->allows('members', 4); // inviting four at once
Writing one
php artisan make:meter MemberMeter Organization
namespace App\Meters\Organization; use EduLazaro\Larameter\Meter; class MemberMeter extends Meter { public string $handle = 'members'; public function count(): int { return $this->meterable->members()->count(); } }
count() is the only thing you have to write. It returns how many exist right now, by whatever means makes sense: a relation, a query, a cached figure.
The generated class lands in App\Meters\<Model>, grouped by what it caps rather than flat. An application of any size ends up with several models that have ceilings, and flat you would be reading class names to tell the seats of an organisation from the members of a case.
The handle
The handle matches the key under limits in your plans:
'pro' => ['limits' => ['members' => 25]],
Leave $handle off and it is derived from the class name: MemberMeter counts members, CaseMeter counts cases. The command writes the line anyway so the connection is visible, and deleting it changes nothing.
The label
label() derives from the handle through Str::headline(), so members becomes "Members". Override it to translate:
public function label(): string { return __('meters.members'); }
The package never sees the string and depends on no translation package.
Declaring them
Three ways, interchangeable, and declaring the same meter twice does not double it.
The property, which is the usual one. A plain list rather than a map, because a meter already knows its own handle:
class Organization extends Model { use HasCredits, HasPlans, HasMeters; protected array $meters = [MemberMeter::class, CaseMeter::class]; }
The attribute, the same shape larakeep uses for keepers:
#[MeteredBy(MemberMeter::class)] #[MeteredBy(CaseMeter::class)] class Organization extends Model
From outside, for a model you cannot edit, a module bringing its own relation, or a meter that only applies when something is switched on:
Organization::meter(MemberMeter::class); Organization::flushRegisteredMeters();
The same arrangement as $casts and mergeCasts(): the property declares, the call adds.
Asking
$org->quota()->allows('members'); // room for one more? $org->quota()->allows('members', 4); // room for four? $org->quota()->get('members'); // the Meter, or null if unmetered $org->quota()->get('members')->count(); // 18 $org->quota()->get('members')->limit(); // 25, or -1 for unlimited $org->quota()->all(); // every meter, keyed by handle $org->quota()->summary(); // rows for a usage screen
summary() gives one row per meter, ready to render:
[
['handle' => 'members', 'label' => 'Members', 'count' => 18, 'limit' => 25],
['handle' => 'cases', 'label' => 'Cases', 'count' => 2, 'limit' => -1],
]
Meters are instantiated once per Quota and the quota once per model instance, so a screen that asks about six resources builds six objects, not six per question. count() itself is not cached, since it is your query and you know whether it is expensive.
Two defaults, both permissive
A resource with no meter is unlimited. allows() on an unknown handle returns true rather than false. The other way round, a package you just installed would start refusing to create things it was never told to count, which is not a decision a dependency gets to make.
A limit nobody wrote down is unlimited. Plan::limit() returns -1 for an absent key. A ceiling that was never declared was never meant to apply. Note the asymmetry with features, which default to off: a feature is something a plan grants, a limit is something it takes away. See Plans.
-1 is unlimited, 0 forbids the resource entirely, and the two are not the same.
Without a plan
Meter::plan() asks the metered model for its plan, and falls back to an empty Plan when the model has no plan() method, which means HasMeters without HasPlans. Every limit is then -1 and every meter fits.
That is consistent rather than useless: with no plans there are no ceilings, and the meters are still there to report counts on a usage screen.
Enforcing it
The package never blocks anything by itself. There is no middleware, no exception thrown from a save, and no observer refusing to create a model, because a ceiling reached is a product decision and the right response differs every time: an error, an upsell, a queued invitation, an administrator override.
What it gives you is one honest answer per resource:
if (! $org->quota()->allows('members', count($emails))) { return back()->with('error', __('Your plan has room for :n more.', [ 'n' => $org->quota()->get('members')->limit() - $org->quota()->get('members')->count(), ])); }