romanzipp/laravel-projectable-aggregates

Projectable aggregates for Laravel

0.0.2 2024-08-11 07:27 UTC

This package is auto-updated.

Last update: 2024-09-11 07:37:33 UTC


README

Latest Stable Version Total Downloads License GitHub Build Status

What

Laravel Projectable Aggregates is a package that allows you to easily store aggregate values like counts, sums, averages, etc. in your models eliminating the need to calculate these values on the fly (with withCount, withSum, withAvg, etc.).

  • Speed up database queries by storing aggregate values in the database.
  • Automatically updates aggregate values with Model Events.
  • Option to calculate the aggregate values periodically in bulk.

Installation

composer require romanzipp/laravel-projectable-aggregates

Terminology

🟢 Consumers

Consumers hold the projectable aggregate database field. This is the model which otherwise would calculate the relationship fields via withCount, withSum, withAvg, etc.

🔵 Providers

Providing models provide (duh) the aggregate values for the consumer. Think of the provider to exist many times for one consumer.

Usage

Let's continue with the example of a Car model with Door models. We want to store the Doors count in the Car's project_doors_count field.

1. Add a Projection Field to DB

new class() extends Migration
{
    public function up()
    {
        Schema::create('cars', function (Blueprint $table) {
            $table->id();
            $table->unsignedInteger('project_doors_count')->default(0);
        });
    }
}

2. Update your Models

🟢 Car (Consumer)

The consumer model will attach the ConsumesProjectableAggregate attribute to the provider relation.

use romanzipp\ProjectableAggregates\Attributes\ConsumesProjectableAggregate;
use romanzipp\ProjectableAggregates\ProjectionAggregateType;

class Car extends Model
{
    #[ConsumesProjectableAggregate(
        projectionAttribute: 'project_doors_count',   // <- Name of the projection field in the database
        projectionType: ProjectionAggregateType::TYPE_COUNT
    )]
    public function doors(): HasMany
    {
        return $this->hasMany(Door::class);
    }
}

🔵 Door (Provider)

The provider model will attach the ProvidesProjectableAggregate attribute to the consumer relation.

use romanzipp\ProjectableAggregates\Attributes\ProvidesProjectableAggregate;
use romanzipp\ProjectableAggregates\ProjectionAggregateType;

class Door extends Model
{
    #[ProvidesProjectableAggregate(
        projectionAttribute: 'project_doors_count',   // <- Name of the FOREIGN projection field in the database
        projectionType: ProjectionAggregateType::TYPE_COUNT
    )]
    public function car(): BelongsTo
    {
        return $this->belongsTo(Car::class);
    }
}

3. Register the Projection Aggregates

In order to listen to model events issued by the provider models, you need to register the consumer models in the boot method of your AppServiceProvider.

use romanzipp\ProjectableAggregates\ProjectableAggregateRegistry;

class AppServiceProvider extends ServiceProvider
{
    public function boot(ProjectableAggregateRegistry $registry)
    {
        $registry->registerConsumers([
            Car::class,
        ]);

        $registry->registerProviders([
            Door::class,
        ]);
    }
}

Documentation

Important

Calculating aggregate values (without bulk) relies on Elouent model events which are only dispatched when working with Eloquent model itself. Using the DB facade will not trigger the library to update the aggregate values.

Aggregate Types

There are three types of aggregates that can be calculated:

  • ProjectionAggregateType::TYPE_COUNT: Counts the number of related models.
  • ProjectionAggregateType::TYPE_SUM: Sums the related models' values.
  • ProjectionAggregateType::TYPE_AVG: Averages the related models' values.

Important

In order to use the aggregate types TYPE_SUM and TYPE_AVG, you need to specify the target attribute of the relationship.

#[ProvidesProjectableAggregate(
    projectionAttribute: 'project_price_average',
    projectionType: ProjectionAggregateType::TYPE_AVG,
    targetAttribute: 'price',                          // <- Attribute of the related model to average/sum up
)]

Triggers

You can decide if you would only like to rely on models events or if you want to calculate the aggregate values periodically in bulk.

Rely on Model Events

This will automatically work if your've attached the ProvidesProjectableAggregate attribute to your provider relations. Once a provider model has been created/deleted the according consumer aggregate attribute will be incremented/decremented.

Calculate periodically in Bulk

If you don't want to or can't rely on model events, you can use the bulk-aggregate command to calculate the aggregate values periodically in bulk.

php artisan aggregates:bulk-aggregate {--queued} {--queue=} {--class=}
  • --queued: Dispatch a job to the worker queue.
  • --queue=: Specify the queue to run the command in.
  • --class=: Limit the bulk calculation to a specific consumer class.

Relationships

The following relationships are supported and tested:

Provider::belongsTo() <-> Consumer::hasMany()

  • ✅ Model Events
  • ✅ Bulk Aggregation

Provider::hasOneThrough() <-> Pivot <-> Consumer::hasManyThrough() ⚠️ WIP

  • ❌ Model Events
  • ✅ Bulk Aggregation

Provider::morphTo() <-> Consumer::morphMany()

  • ✅ Model Events
  • ✅ Bulk Aggregation

Testing

This repository contains a Lando configuration file that can be used to run the tests on your local machine.

lando start
lando phpunit

License

The MIT License (MIT). Please see License File for more information.