249 lines
7.6 KiB
PHP
249 lines
7.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Report;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class ReportQueryBuilder
|
|
{
|
|
/**
|
|
* Build a query from a database-driven report configuration.
|
|
*/
|
|
public function build(Report $report, array $filters = []): Builder
|
|
{
|
|
// Load all required relationships
|
|
$report->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();
|
|
}
|
|
}
|