New report system and views
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user