Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

whoisthisstud's avatar

Help with refactoring

The query for a report I'm building is maxing out the memory on the live server. I think I'm overthinking this whole query, tbh, but could use some honest feedback and help simplifying and speeding up my junk code. :/

Three associated tables: quotes, quote_task, and tasks.

The report has five options/filters – the limit of results returned, the type of quote, the timestamp field to filter by, and the start and end dates.

Beat me up!

<?php

namespace App\Http\Livewire\Admin\Reporting\Tasks;

use App\Models\Task;
use App\Models\Quote;
use App\Models\QuoteTask;

use Livewire\Component;
use Livewire\WithPagination;

use Illuminate\Support\Collection;
use Illuminate\Pagination\Paginator;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;

class TopUsedTasksReport extends Component
{
    /*
     * TASKS
     * 
     * Foreach Task
     * - Task Description
     * - Task Category
     * - Number of Quotes that include that Task
     * --- Associated Quotes (for linking)
     * --- From QuoteTasks pivot
     * ------ Average && Sum of Task's Labor Price used on Quote
     * ------ Average && Sum of Task's Material Price used on Quote
     * ------ Number of Quotes that were Approved that include this Task
     * 
     * Sort results by Total Number of Quotes, desc
     * 
     * Report Options
     * - Initial Quotes | Active Quotes | Approved Quotes | Completed Quotes
     * - created_at | updated_at | approved_at | scheduled_at | completed_at
     * - Timeframe of Quotes
     * 
     */


    use WithPagination;

    public $tasks;
    public $quoteType;
    public $calendar_start;
    public $range_start;
    public $range_end;
    public $time_type;
    public $topLimit = 10;

    public $requests = [];
    public $unique_ids;

    public $panelOpen = false;

    const RELATION = [
        'Active' => [ // Active Quote, may not be initial quote
            'request_method' => 'activeQuote',
            'task_method' => 'activeQuotes'
        ],
        'Approved' => [ // Approved Quote, may not be initial quote
            'request_method' => 'approvedQuote',
            'task_method' => 'approvedQuotes'
        ],
        'Completed' => [ // Completed Quote, may not be initial quote
            'request_method' => 'completedQuote',
            'task_method' => 'completedQuotes'
        ],
        'Initial' => [ // First created quote
            'request_method' => 'initialQuote',
            'task_method' => 'initialQuotes'
        ],
    ];

    public function updatedTimeType()
    {
        $this->buildResults();
    }

    public function updatedQuoteType()
    {
        $this->buildResults();
    }

    public function updatedTopLimit()
    {
        $this->buildResults();
    }

    public function updatedRangeStart()
    {
        $this->range_start = \Carbon\Carbon::parse($this->range_start)->startOfDay();
    }

    public function updatedRangeEnd()
    {
        $this->range_end = \Carbon\Carbon::parse($this->range_end)->endOfDay();
    }

    public function boot()
    {
        // date_default_timezone_set( "America/Los_Angeles" );

        $this->quoteType = $this->quoteType ?? 'Initial';
        $this->calendar_start = now()->startOfYear()->addYear(-2);
        $this->range_start = $this->range_start ?? now()->addYear(-1)->startOfDay();
        $this->range_end = $this->range_end ?? now();
        $this->time_type = $this->time_type ?? 'created_at';

        $this->tasks = collect([]);
    }

    public function reloadResults()
    {
        return $this->buildResults();
    }


    protected function buildResults()
    {
        
        // Get associated tasks
        $this->tasks = Task::query()
                    ->with(
                        self::RELATION[$this->quoteType]['task_method'], 
                        function($quote) {
                            $quote->whereNotNull('was_submitted_on');
                            $quote->whereBetween(
                                'quotes.'.$this->time_type,
                                [
                                    \Carbon\Carbon::parse($this->range_start),
                                    \Carbon\Carbon::parse($this->range_end)
                                ]
                            );
                        }
                    )
                    ->whereHas(
                        self::RELATION[$this->quoteType]['task_method'], 
                        function($quote) {
                            $quote->whereNotNull('was_submitted_on');
                            $quote->whereBetween(
                                'quotes.'.$this->time_type,
                                [
                                    \Carbon\Carbon::parse($this->range_start),
                                    \Carbon\Carbon::parse($this->range_end)
                                ]
                            );
                        }
                    )->withCount([
                    'approvedQuotes',
                    'approvedQuotes as approved_quotes_count' => function (Builder $query) {
                        return $query->when(
                                $this->quoteType === 'Initial', 
                                function($quote) {
                                    $quote->where('version',1);
                                }
                            )->whereBetween(
                                'quotes.'.$this->time_type,
                                [
                                    \Carbon\Carbon::parse($this->range_start),
                                    \Carbon\Carbon::parse($this->range_end)
                                ]
                            );
                        },
                    ])->get();


        // Add requested data to collections
        $this->tasks->map( function($task) {
            $task->quotes_count = $task->{self::RELATION[$this->quoteType]['task_method']}->count();
            $task->average_labor = number_format( 
                $task->{self::RELATION[$this->quoteType]['task_method']}->avg('info.labor_price') / 100,
                2);
            $task->average_materials = number_format( 
                $task->{self::RELATION[$this->quoteType]['task_method']}->avg('info.material_price') / 100,
                2);
        });

        // Sort data by usage
        $this->tasks = $this->tasks->sortBy([
            ['quotes_count', 'desc'],
            ['id', 'asc'],
        ]);
    }


    public function render()
    {
        return view('livewire.admin.reporting.tasks.top-used-tasks-report', [
            'results' => $this->tasks->take($this->topLimit),
        ]);
    }
}

0 likes
11 replies
Snapey's avatar

you are loading all tasks and then paginating the in-memory collection?

Sinnbeck's avatar

@whoisthisstud if you load enough models. Remember that laravel needs to create each and every one (also relations), even if you never use them

Sinnbeck's avatar

@whoisthisstud set up a dummy test set (same amount of records as live server) and use debug bar to check hydration of models (amount) as well as memory usage

1 like
whoisthisstud's avatar

There were two mistakes in the code I posted originally – I was missing a whereHas() in the query and had a leftover paginate() vs take() when passing to the view – of which both have been corrected.

Tray2's avatar

@whoisthisstud

These calculations and sorting should be done in the database and not in php.

  $this->tasks->map( function($task) {
            $task->quotes_count = $task->{self::RELATION[$this->quoteType]['task_method']}->count();
            $task->average_labor = number_format( 
                $task->{self::RELATION[$this->quoteType]['task_method']}->avg('info.labor_price') / 100,
                2);
            $task->average_materials = number_format( 
                $task->{self::RELATION[$this->quoteType]['task_method']}->avg('info.material_price') / 100,
                2);
        });

        // Sort data by usage
        $this->tasks = $this->tasks->sortBy([
            ['quotes_count', 'desc'],
            ['id', 'asc'],
        ]);
whoisthisstud's avatar

@Tray2 excuse my ignorance, but how would I do that on a dynamic query? Parameterized stored procedures? ??

Tray2's avatar

@whoisthisstud Not knowing what your database model, your data and what the desired result looks like makes it hard to give advice.

jekinney's avatar

Don't forget for large data sets use ->chunk() as a option too. This will allow many small queries allowing for PHP to release items in the old data in memory and set the new items in its place while keeping the take and limit for you.

This is almost necessary for any reporting especially outputting to a file.

1 like
whoisthisstud's avatar

@jekinney after much playing around, it's apparent I don't understand the chunk() method, nor how to apply it to my query. Can you point me in the right direction? Possibly a focused and relatable laracast?

Please or to participate in this conversation.