load(['entities', 'columns', 'conditions', 'orders']); // 1. Start with base model query $query = $this->buildBaseQuery($report); // 2. Apply joins from report_entities $this->applyJoins($query, $report); // 3. Select columns from report_columns $this->applySelects($query, $report); // 4. Apply GROUP BY if aggregate functions are used $this->applyGroupBy($query, $report); // 5. Apply conditions from report_conditions $this->applyConditions($query, $report, $filters); // 6. Apply ORDER BY from report_orders $this->applyOrders($query, $report); return $query; } /** * Build the base query from the first entity. */ protected function buildBaseQuery(Report $report): Builder { $baseEntity = $report->entities->firstWhere('join_type', 'base'); if (!$baseEntity) { throw new \RuntimeException("Report {$report->slug} has no base entity defined."); } $modelClass = $baseEntity->model_class; if (!class_exists($modelClass)) { throw new \RuntimeException("Model class {$modelClass} does not exist."); } return $modelClass::query(); } /** * Apply joins from report entities. */ protected function applyJoins(Builder $query, Report $report): void { $entities = $report->entities->where('join_type', '!=', 'base'); foreach ($entities as $entity) { $table = $this->getTableFromModel($entity->model_class); // Use alias if provided if ($entity->alias) { $table = "{$table} as {$entity->alias}"; } $joinMethod = $entity->join_type; $query->{$joinMethod}( $table, $entity->join_first, $entity->join_operator ?? '=', $entity->join_second ); } } /** * Apply column selections. */ protected function applySelects(Builder $query, Report $report): void { $columns = $report->columns ->where('visible', true) ->map(fn($col) => DB::raw("{$col->expression} as {$col->key}")) ->toArray(); if (!empty($columns)) { $query->select($columns); } } /** * Apply GROUP BY clause if aggregate functions are detected. */ protected function applyGroupBy(Builder $query, Report $report): void { $visibleColumns = $report->columns->where('visible', true); // Check if any column uses aggregate functions $hasAggregates = $visibleColumns->contains(function ($col) { return preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression); }); if (!$hasAggregates) { return; // No aggregates, no grouping needed } // Find non-aggregate columns that need to be in GROUP BY $groupByColumns = $visibleColumns ->filter(function ($col) { // Check if this column does NOT use an aggregate function return !preg_match('/\b(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i', $col->expression); }) ->map(function ($col) { // Extract the actual column expression (before any COALESCE, CAST, etc.) // For COALESCE(segments.name, 'default'), we need segments.name if (preg_match('/COALESCE\s*\(\s*([^,]+)\s*,/i', $col->expression, $matches)) { return trim($matches[1]); } // For simple columns, use as-is return $col->expression; }) ->filter() // Remove empty values ->values() ->toArray(); if (!empty($groupByColumns)) { foreach ($groupByColumns as $column) { $query->groupBy(DB::raw($column)); } } } /** * Apply conditions (WHERE clauses). */ protected function applyConditions(Builder $query, Report $report, array $filters): void { $conditions = $report->conditions->where('enabled', true); // Group conditions by group_id $groups = $conditions->groupBy('group_id'); foreach ($groups as $groupId => $groupConditions) { $query->where(function ($subQuery) use ($groupConditions, $filters) { foreach ($groupConditions as $condition) { $value = $this->resolveConditionValue($condition, $filters); // Skip if filter-based and no value provided if ($condition->value_type === 'filter' && $value === null) { continue; } $method = $condition->logical_operator === 'OR' ? 'orWhere' : 'where'; $this->applyCondition($subQuery, $condition, $value, $method); } }); } } /** * Apply a single condition to the query. */ protected function applyCondition(Builder $query, $condition, $value, string $method): void { $column = $condition->column; $operator = strtoupper($condition->operator); switch ($operator) { case 'IS NULL': $query->{$method . 'Null'}($column); break; case 'IS NOT NULL': $query->{$method . 'NotNull'}($column); break; case 'IN': $values = is_array($value) ? $value : explode(',', $value); $query->{$method . 'In'}($column, $values); break; case 'NOT IN': $values = is_array($value) ? $value : explode(',', $value); $query->{$method . 'NotIn'}($column, $values); break; case 'BETWEEN': if (is_array($value) && count($value) === 2) { $query->{$method . 'Between'}($column, $value); } break; case 'LIKE': $query->{$method}($column, 'LIKE', $value); break; default: $query->{$method}($column, $operator, $value); break; } } /** * Resolve condition value based on value_type. */ protected function resolveConditionValue($condition, array $filters) { return match ($condition->value_type) { 'static' => $condition->value, 'filter' => $filters[$condition->filter_key] ?? null, 'expression' => DB::raw($condition->value), default => null, }; } /** * Apply ORDER BY clauses. */ protected function applyOrders(Builder $query, Report $report): void { foreach ($report->orders as $order) { $query->orderBy($order->column, $order->direction); } } /** * Get table name from model class. */ protected function getTableFromModel(string $modelClass): string { if (!class_exists($modelClass)) { throw new \RuntimeException("Model class {$modelClass} does not exist."); } return (new $modelClass)->getTable(); } }