Unit Tests Stable Version Downloads Laravel License
A lightweight Laravel package that automatically maintains *_count columns
for:
belongsTorelations (ex: Category → Article)belongsToManyrelations via pivot models (ex: Material ↔ Part)
This allows models to keep real-time counters in the database without writing custom observers or manual logic.
Perfect for dashboards, ERPs, statistics, inventory systems, and any domain where counts must remain immediately available and consistent.
- 🔹 Automatic increment/decrement on create/delete
- 🔹 Automatic sync when foreign key changes (update)
- 🔹 Automatic increment on restore (SoftDelete only)
- 🔹 Automatic increment/decrement on attach/detach/sync (pivot)
- 🔹 Zero configuration for Laravel service provider (auto-discovery)
- 🔹 Simple traits you can reuse anywhere
- 🔹 Works on Laravel 12+
Install via Composer:
composer require xetaio/xetaravel-countsThis package provides a trait to use in your Models:
HasCounts — belongsTo & belongsToMany relations
Used when a child belongs to a parent, and the parent stores a *_count.
You have:
-
categories.articles_count
-
articles.category_id
Category model
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
protected $fillable = ['name', 'articles_count'];
public function articles()
{
return $this->hasMany(Article::class);
}
}Article model (child)
use Illuminate\Database\Eloquent\Model;
use Xetaio\Counts\Concerns\HasCounts;
class Article extends Model
{
use HasCounts;
protected $fillable = ['title', 'category_id'];
protected static array $countsConfig = [
'category' => 'articles_count',
];
public function category()
{
return $this->belongsTo(Category::class);
}
}| Action | Effect |
|---|---|
| Article created | category.articles_count++ |
| Article deleted | category.articles_count-- |
| Article restored (SofDeletes only) | category.articles_count++ |
| Article moved to another category | decrements old, increments new |
Used when two models are linked via a pivot table and both have a *_count.
Example:
-
materials.parts_count -
parts.materials_count -
material_partpivot table
Material model
class Material extends Model
{
protected $fillable = ['name', 'parts_count'];
public function parts()
{
return $this->belongsToMany(Part::class, 'material_part')
->using(MaterialPart::class) // We need a Pivot Model
->withTimestamps();
}
}Part model
class Part extends Model
{
protected $fillable = ['name', 'materials_count'];
public function materials()
{
return $this->belongsToMany(Material::class, 'material_part')
->using(MaterialPart::class) // We need a pivot model
->withTimestamps();
}
}Pivot model (the key part)
use Illuminate\Database\Eloquent\Relations\Pivot;
use Xetaio\Counts\Concerns\HasCounts;
class MaterialPart extends Pivot // Extends to Pivot
{
use HasCounts;
/**
* Config the counts
*/
protected static array $countsConfig = [
'material' => 'parts_count',
'part' => 'materials_count',
];
public function material()
{
return $this->belongsTo(Material::class);
}
public function part()
{
return $this->belongsTo(Part::class);
}
}| Action | Effect |
|---|---|
material->parts()->attach(part) |
increments both counts |
material->parts()->detach(part) |
decrements both counts |
sync([...]) |
decrements/increments both counts |
--
This package uses:
increment() / decrement() → atomic SQL updates
No heavy SELECT COUNT(*)
No observers per model
No risk of race conditions beyond DB atomic ops
For large-scale systems, this approach is highly performant.
--
Pull Requests are welcome! Feel free to suggest improvements, new features, or optimizations.