Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
|
||||
use Mautic\ReportBundle\Crate\ReportDataResult;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class CsvExporter
|
||||
{
|
||||
public function __construct(
|
||||
protected FormatterHelper $formatterHelper,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
* @param int $page
|
||||
*/
|
||||
public function export(ReportDataResult $reportDataResult, $handle, $page = 1): void
|
||||
{
|
||||
if (1 === $page) {
|
||||
$this->putHeader($reportDataResult, $handle);
|
||||
}
|
||||
|
||||
foreach ($reportDataResult->getData() as $data) {
|
||||
$row = [];
|
||||
foreach ($data as $k => $v) {
|
||||
$type = $reportDataResult->getType($k);
|
||||
$typeString = 'string' !== $type;
|
||||
$row[] = $typeString ? $this->formatterHelper->_($v, $type, true) : $v;
|
||||
}
|
||||
$this->putRow($handle, $row);
|
||||
}
|
||||
|
||||
if ($reportDataResult->isLastPage()) {
|
||||
$totalsRow = $reportDataResult->getTotalsToExport($this->formatterHelper);
|
||||
|
||||
if (!empty($totalsRow)) {
|
||||
$this->putTotals($totalsRow, $handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
*/
|
||||
public function putHeader(ReportDataResult $reportDataResult, $handle): void
|
||||
{
|
||||
$this->putRow($handle, $reportDataResult->getHeaders());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $totals
|
||||
* @param resource $handle
|
||||
*/
|
||||
public function putTotals(array $totals, $handle): void
|
||||
{
|
||||
// Put label if the first item is empty
|
||||
$key = array_key_first($totals);
|
||||
|
||||
if (empty($totals[$key])) {
|
||||
$totals[$key] = $this->translator->trans('mautic.report.report.groupby.totals');
|
||||
}
|
||||
|
||||
$this->putRow($handle, $totals);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handle
|
||||
*/
|
||||
private function putRow($handle, array $row): void
|
||||
{
|
||||
if ($this->coreParametersHelper->get('csv_always_enclose')) {
|
||||
fputs($handle, '"'.implode('","', $row).'"'."\n");
|
||||
} else {
|
||||
CsvHelper::putCsv($handle, $row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Twig\Helper\FormatterHelper;
|
||||
use Mautic\ReportBundle\Crate\ReportDataResult;
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ExcelExporter
|
||||
{
|
||||
public function __construct(
|
||||
protected FormatterHelper $formatterHelper,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function export(ReportDataResult $reportDataResult, $name, string $output = 'php://output'): void
|
||||
{
|
||||
if (!class_exists(Spreadsheet::class)) {
|
||||
throw new \Exception('PHPSpreadsheet is required to export to Excel spreadsheets');
|
||||
}
|
||||
|
||||
try {
|
||||
$objPHPExcel = new Spreadsheet();
|
||||
$objPHPExcel->getProperties()->setTitle($name);
|
||||
$objPHPExcel->createSheet();
|
||||
$objPHPExcelSheet = $objPHPExcel->getActiveSheet();
|
||||
$reportData = $reportDataResult->getData();
|
||||
$rowCount = 1;
|
||||
|
||||
if (empty($reportData)) {
|
||||
throw new \Exception('No report data to be exported');
|
||||
}
|
||||
|
||||
$headersRow = $reportDataResult->getHeaders();
|
||||
$this->putHeader($headersRow, $objPHPExcelSheet);
|
||||
|
||||
// build the data rows
|
||||
foreach ($reportData as $count=>$data) {
|
||||
$row = [];
|
||||
foreach ($data as $k => $v) {
|
||||
$type = $reportDataResult->getType($k);
|
||||
$formatted = htmlspecialchars_decode($this->formatterHelper->_($v, $type, true), ENT_QUOTES);
|
||||
$row[] = $formatted;
|
||||
}
|
||||
|
||||
// write the row
|
||||
$rowCount = $count + 2;
|
||||
$objPHPExcel->getActiveSheet()->fromArray($row, null, "A{$rowCount}");
|
||||
// free memory
|
||||
unset($row, $reportData['data'][$count]);
|
||||
}
|
||||
|
||||
// Add totals to export
|
||||
$totalsRow = $reportDataResult->getTotalsToExport($this->formatterHelper);
|
||||
if (!empty($totalsRow)) {
|
||||
$this->putTotals($totalsRow, $objPHPExcelSheet, 'A'.++$rowCount);
|
||||
}
|
||||
|
||||
$objWriter = IOFactory::createWriter($objPHPExcel, 'Xlsx');
|
||||
$objWriter->setPreCalculateFormulas(false);
|
||||
|
||||
$objWriter->save($output);
|
||||
} catch (Exception $e) {
|
||||
throw new \Exception('PHPSpreadsheet Error', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $headers
|
||||
*/
|
||||
public function putHeader(array $headers, Worksheet $activeSheet): void
|
||||
{
|
||||
$activeSheet->fromArray($headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $totals
|
||||
*/
|
||||
public function putTotals(array $totals, Worksheet $activeSheet, string $startCell): void
|
||||
{
|
||||
// Put label if the first item is empty
|
||||
$key = array_key_first($totals);
|
||||
|
||||
if (empty($totals[$key])) {
|
||||
$totals[$key] = $this->translator->trans('mautic.report.report.groupby.totals');
|
||||
}
|
||||
|
||||
$activeSheet->fromArray($totals, null, $startCell);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Exception\FilePathException;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\FilePathResolver;
|
||||
use Mautic\ReportBundle\Exception\FileIOException;
|
||||
|
||||
class ExportHandler
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dir;
|
||||
|
||||
public function __construct(
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
private FilePathResolver $filePathResolver,
|
||||
) {
|
||||
$this->dir = $coreParametersHelper->get('report_temp_dir');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|resource
|
||||
*
|
||||
* @throws FileIOException
|
||||
*/
|
||||
public function getHandler($fileName)
|
||||
{
|
||||
$path = $this->getPath($fileName);
|
||||
|
||||
if (false === ($handler = @fopen($path, 'a'))) {
|
||||
throw new FileIOException('Could not open file '.$path);
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $handler
|
||||
*/
|
||||
public function closeHandler($handler): void
|
||||
{
|
||||
fclose($handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fileName
|
||||
*/
|
||||
public function removeFile($fileName): void
|
||||
{
|
||||
try {
|
||||
$path = $this->getPath($fileName);
|
||||
$this->filePathResolver->delete($path);
|
||||
} catch (FileIOException) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileIOException
|
||||
*/
|
||||
public function getPath($fileName): string
|
||||
{
|
||||
try {
|
||||
$this->filePathResolver->createDirectory($this->dir);
|
||||
} catch (FilePathException $e) {
|
||||
throw new FileIOException('Could not create directory '.$this->dir, 0, $e);
|
||||
}
|
||||
|
||||
return $this->dir.'/'.$fileName.'.csv';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ExportResponse
|
||||
{
|
||||
/**
|
||||
* @param string $fileName
|
||||
*/
|
||||
public static function setResponseHeaders(Response $response, $fileName): void
|
||||
{
|
||||
$response->headers->set('Content-Type', 'application/octet-stream');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="'.$fileName.'"');
|
||||
$response->headers->set('Expires', '0');
|
||||
$response->headers->set('Cache-Control', 'must-revalidate');
|
||||
$response->headers->set('Pragma', 'public');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\ReportBundle\Scheduler\Model\FileHandler;
|
||||
|
||||
class ReportCleanup
|
||||
{
|
||||
public const KEEP_FILE_DAYS = 7;
|
||||
|
||||
public function __construct(private FileHandler $fileHandler)
|
||||
{
|
||||
}
|
||||
|
||||
public function cleanup(int $reportId): void
|
||||
{
|
||||
if ($this->shouldBeDeleted($this->fileHandler->getPathToCompressedCsvFileForReportId($reportId))) {
|
||||
$this->fileHandler->deleteCompressedCsvFileForReportId($reportId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes files older than KEEP_FILE_DAYS.
|
||||
*/
|
||||
public function cleanupAll(): void
|
||||
{
|
||||
$reportDirectory = $this->fileHandler->getCompressedCsvFileForReportDir();
|
||||
|
||||
if (!file_exists($reportDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = array_diff(scandir($reportDirectory), ['.', '..']);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$filePath = $reportDirectory.'/'.$file;
|
||||
if (is_dir($filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->shouldBeDeleted($filePath)) {
|
||||
$this->fileHandler->delete($filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldBeDeleted(string $filePath): bool
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$created = new \DateTime(date('Y-m-d', filemtime($filePath)));
|
||||
$now = new \DateTime();
|
||||
$days = $created->diff($now)->days;
|
||||
|
||||
if ($days > self::KEEP_FILE_DAYS) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class ReportExportOptions
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $batchSize;
|
||||
|
||||
private int $page;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateFrom;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateTo;
|
||||
|
||||
public function __construct(CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$this->batchSize = $coreParametersHelper->get('report_export_batch_size');
|
||||
$this->page = 1;
|
||||
}
|
||||
|
||||
public function beginExport(): void
|
||||
{
|
||||
$this->page = 1;
|
||||
}
|
||||
|
||||
public function nextBatch(): void
|
||||
{
|
||||
++$this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getBatchSize()
|
||||
{
|
||||
return $this->batchSize;
|
||||
}
|
||||
|
||||
public function getPage(): int
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getNumberOfProcessedResults()
|
||||
{
|
||||
return $this->page * $this->getBatchSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateFrom()
|
||||
{
|
||||
return $this->dateFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateFrom
|
||||
*/
|
||||
public function setDateFrom($dateFrom): void
|
||||
{
|
||||
$this->dateFrom = $dateFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateTo()
|
||||
{
|
||||
return $this->dateTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateTo
|
||||
*/
|
||||
public function setDateTo($dateTo): void
|
||||
{
|
||||
$this->dateTo = $dateTo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Event\JobExtendTimeEvent;
|
||||
use Mautic\ReportBundle\Adapter\ReportDataAdapter;
|
||||
use Mautic\ReportBundle\Entity\Scheduler;
|
||||
use Mautic\ReportBundle\Event\ReportScheduleSendEvent;
|
||||
use Mautic\ReportBundle\Exception\FileIOException;
|
||||
use Mautic\ReportBundle\ReportEvents;
|
||||
use Mautic\ReportBundle\Scheduler\Enum\SchedulerEnum;
|
||||
use Mautic\ReportBundle\Scheduler\Option\ExportOption;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class ReportExporter
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleModel $schedulerModel,
|
||||
private ReportDataAdapter $reportDataAdapter,
|
||||
private ReportExportOptions $reportExportOptions,
|
||||
private ReportFileWriter $reportFileWriter,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileIOException
|
||||
*/
|
||||
public function processExport(ExportOption $exportOption): void
|
||||
{
|
||||
$schedulers = $this->schedulerModel->getScheduledReportsForExport($exportOption);
|
||||
foreach ($schedulers as $scheduler) {
|
||||
$this->processReport($scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileIOException
|
||||
*/
|
||||
private function processReport(Scheduler $scheduler): void
|
||||
{
|
||||
$report = $scheduler->getReport();
|
||||
|
||||
$dateTo = clone $scheduler->getScheduleDate();
|
||||
$dateTo->setTime(0, 0, 0);
|
||||
|
||||
$dateFrom = clone $dateTo;
|
||||
switch ($report->getScheduleUnit()) {
|
||||
case SchedulerEnum::UNIT_NOW:
|
||||
$dateFrom->sub(new \DateInterval('P10Y'));
|
||||
$this->schedulerModel->turnOffScheduler($report);
|
||||
break;
|
||||
case SchedulerEnum::UNIT_DAILY:
|
||||
$dateFrom->sub(new \DateInterval('P1D'));
|
||||
break;
|
||||
case SchedulerEnum::UNIT_WEEKLY:
|
||||
$dateFrom->sub(new \DateInterval('P7D'));
|
||||
break;
|
||||
case SchedulerEnum::UNIT_MONTHLY:
|
||||
$dateFrom->sub(new \DateInterval('P1M'));
|
||||
break;
|
||||
}
|
||||
|
||||
$this->reportExportOptions->setDateFrom($dateFrom);
|
||||
$this->reportExportOptions->setDateTo($dateTo->sub(new \DateInterval('PT1S')));
|
||||
|
||||
// just published reports, but schedule continue
|
||||
if ($report->isPublished()) {
|
||||
$this->reportExportOptions->beginExport();
|
||||
while (true) {
|
||||
$data = $this->reportDataAdapter->getReportData($report, $this->reportExportOptions);
|
||||
|
||||
$this->reportFileWriter->writeReportData($scheduler, $data, $this->reportExportOptions);
|
||||
|
||||
$totalResults = $data->getTotalResults();
|
||||
unset($data);
|
||||
|
||||
if ($this->reportExportOptions->getNumberOfProcessedResults() >= $totalResults) {
|
||||
break;
|
||||
}
|
||||
$this->eventDispatcher->dispatch(new JobExtendTimeEvent());
|
||||
$this->reportExportOptions->nextBatch();
|
||||
}
|
||||
|
||||
$file = $this->reportFileWriter->getFilePath($scheduler);
|
||||
$event = new ReportScheduleSendEvent($scheduler, $file);
|
||||
$this->eventDispatcher->dispatch($event, ReportEvents::REPORT_SCHEDULE_SEND);
|
||||
}
|
||||
|
||||
$this->schedulerModel->reportWasScheduled($report);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\ReportBundle\Crate\ReportDataResult;
|
||||
use Mautic\ReportBundle\Entity\Scheduler;
|
||||
use Mautic\ReportBundle\Exception\FileIOException;
|
||||
|
||||
class ReportFileWriter
|
||||
{
|
||||
public function __construct(
|
||||
private CsvExporter $csvExporter,
|
||||
private ExportHandler $exportHandler,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileIOException
|
||||
*/
|
||||
public function writeReportData(Scheduler $scheduler, ReportDataResult $reportDataResult, ReportExportOptions $reportExportOptions): void
|
||||
{
|
||||
$fileName = $this->getFileName($scheduler);
|
||||
$handler = $this->exportHandler->getHandler($fileName);
|
||||
$this->csvExporter->export($reportDataResult, $handler, $reportExportOptions->getPage());
|
||||
$this->exportHandler->closeHandler($handler);
|
||||
}
|
||||
|
||||
public function clear(Scheduler $scheduler): void
|
||||
{
|
||||
$fileName = $this->getFileName($scheduler);
|
||||
$this->exportHandler->removeFile($fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FileIOException
|
||||
*/
|
||||
public function getFilePath(Scheduler $scheduler): string
|
||||
{
|
||||
$fileName = $this->getFileName($scheduler);
|
||||
|
||||
return $this->exportHandler->getPath($fileName);
|
||||
}
|
||||
|
||||
private function getFileName(Scheduler $scheduler): string
|
||||
{
|
||||
$date = $scheduler->getScheduleDate();
|
||||
$dateString = $date->format('Y-m-d');
|
||||
$reportName = $scheduler->getReport()->getName();
|
||||
|
||||
return $dateString.'_'.InputHelper::alphanum($reportName, false, '-');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,876 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\ChannelBundle\Helper\ChannelListHelper;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Model\FieldModel;
|
||||
use Mautic\ReportBundle\Builder\MauticReportBuilder;
|
||||
use Mautic\ReportBundle\Crate\ReportDataResult;
|
||||
use Mautic\ReportBundle\Entity\Report;
|
||||
use Mautic\ReportBundle\Event\ReportBuilderEvent;
|
||||
use Mautic\ReportBundle\Event\ReportDataEvent;
|
||||
use Mautic\ReportBundle\Event\ReportEvent;
|
||||
use Mautic\ReportBundle\Event\ReportGraphEvent;
|
||||
use Mautic\ReportBundle\Event\ReportQueryEvent;
|
||||
use Mautic\ReportBundle\Generator\ReportGenerator;
|
||||
use Mautic\ReportBundle\Helper\ReportHelper;
|
||||
use Mautic\ReportBundle\ReportEvents;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Report>
|
||||
*/
|
||||
class ReportModel extends FormModel implements GlobalSearchInterface
|
||||
{
|
||||
public const CHANNEL_FEATURE = 'reporting';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $reportBuilderData;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
protected $defaultPageLimit;
|
||||
|
||||
public function __construct(
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
protected Environment $twig,
|
||||
protected ChannelListHelper $channelListHelper,
|
||||
protected FieldModel $fieldModel,
|
||||
protected ReportHelper $reportHelper,
|
||||
private CsvExporter $csvExporter,
|
||||
private ExcelExporter $excelExporter,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
private RequestStack $requestStack,
|
||||
) {
|
||||
$this->defaultPageLimit = $coreParametersHelper->get('default_pagelimit');
|
||||
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\ReportBundle\Entity\ReportRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Report::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'report:reports';
|
||||
}
|
||||
|
||||
protected function getSession(): SessionInterface
|
||||
{
|
||||
try {
|
||||
return $this->requestStack->getSession();
|
||||
} catch (SessionNotFoundException) {
|
||||
return new Session(); // in case of CLI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
*/
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Report) {
|
||||
throw new MethodNotAllowedHttpException(['Report']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
$options = array_merge($options, [
|
||||
'table_list' => $this->getTableData('all', $entity->getSource()),
|
||||
'attr' => [
|
||||
'readonly' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
// Fire the REPORT_ON_BUILD event off to get the table/column data
|
||||
|
||||
$reportGenerator = new ReportGenerator($this->dispatcher, $this->em->getConnection(), $entity, $this->channelListHelper, $formFactory);
|
||||
|
||||
return $reportGenerator->getForm($entity, $options);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Report
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Report();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Report) {
|
||||
throw new MethodNotAllowedHttpException(['Report']);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = ReportEvents::REPORT_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = ReportEvents::REPORT_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = ReportEvents::REPORT_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = ReportEvents::REPORT_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new ReportEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the table and graph data.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function buildAvailableReports($context, ?string $reportSource = null)
|
||||
{
|
||||
if (empty($this->reportBuilderData[$context])) {
|
||||
// Check to see if all has been obtained
|
||||
if (isset($this->reportBuilderData['all'])) {
|
||||
$this->reportBuilderData[$context]['tables'] = $this->reportBuilderData['all']['tables'][$context] ?? [];
|
||||
$this->reportBuilderData[$context]['graphs'] = $this->reportBuilderData['all']['graphs'][$context] ?? [];
|
||||
} else {
|
||||
// build them
|
||||
$eventContext = ('all' == $context) ? '' : $context;
|
||||
|
||||
$event = new ReportBuilderEvent($this->translator, $this->channelListHelper, $eventContext, $this->fieldModel->getPublishedFieldArrays(), $this->reportHelper, $reportSource);
|
||||
$this->dispatcher->dispatch($event, ReportEvents::REPORT_ON_BUILD);
|
||||
|
||||
$tables = $event->getTables();
|
||||
$graphs = $event->getGraphs();
|
||||
|
||||
if ('all' == $context) {
|
||||
$this->reportBuilderData[$context]['tables'] = $tables;
|
||||
$this->reportBuilderData[$context]['graphs'] = $graphs;
|
||||
} else {
|
||||
if (isset($tables[$context])) {
|
||||
$this->reportBuilderData[$context]['tables'] = $tables[$context];
|
||||
} else {
|
||||
$this->reportBuilderData[$context]['tables'] = $tables;
|
||||
}
|
||||
|
||||
if (isset($graphs[$context])) {
|
||||
$this->reportBuilderData[$context]['graphs'] = $graphs[$context];
|
||||
} else {
|
||||
$this->reportBuilderData[$context]['graphs'] = $graphs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->reportBuilderData[$context];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the table lookup data for the report forms.
|
||||
*
|
||||
* @param string $context
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTableData($context = 'all', ?string $reportSource = null)
|
||||
{
|
||||
$data = $this->buildAvailableReports($context, $reportSource);
|
||||
|
||||
$data = (!isset($data['tables'])) ? [] : $data['tables'];
|
||||
|
||||
if (array_key_exists('columns', $data)) {
|
||||
$data['columns'] = $this->preventSameAliases($data['columns']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent same aliases using numeric suffixes for each alias.
|
||||
*/
|
||||
private function preventSameAliases(array $columns): array
|
||||
{
|
||||
$existingAliases = [];
|
||||
|
||||
foreach ($columns as $key => $column) {
|
||||
$alias = $column['alias'];
|
||||
|
||||
// Count suffixes
|
||||
if (!array_key_exists($alias, $existingAliases)) {
|
||||
$existingAliases[$alias] = 0;
|
||||
} else {
|
||||
++$existingAliases[$alias];
|
||||
}
|
||||
|
||||
// Add numeric suffix
|
||||
if ($existingAliases[$alias] > 0) {
|
||||
$columns[$key]['alias'] = $alias.$existingAliases[$alias];
|
||||
}
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $context
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGraphData($context = 'all')
|
||||
{
|
||||
$data = $this->buildAvailableReports($context);
|
||||
|
||||
return (!isset($data['graphs'])) ? [] : $data['graphs'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $context
|
||||
*
|
||||
* @return \stdClass ['choices' => [], 'choiceHtml' => '', definitions => []]
|
||||
*/
|
||||
public function getColumnList($context, $isGroupBy = false): \stdClass
|
||||
{
|
||||
$tableData = $this->getTableData($context);
|
||||
$columns = $tableData['columns'] ?? [];
|
||||
$return = new \stdClass();
|
||||
$return->choices = [];
|
||||
$return->choiceHtml = '';
|
||||
$return->definitions = [];
|
||||
|
||||
foreach ($columns as $column => $data) {
|
||||
if ($isGroupBy && ('unsubscribed' == $column || 'unsubscribed_ratio' == $column || 'unique_ratio' == $column)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($data['label'])) {
|
||||
$return->choiceHtml .= "<option value=\"$column\">{$data['label']}</option>\n";
|
||||
$return->choices[$column] = $data['label'];
|
||||
$return->definitions[$column] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $context
|
||||
*
|
||||
* return \stdClass{filterList: mixed[], definitions: mixed[], operatorChoices: mixed[], operatorHtml: mixed[], filterListHtml: string}
|
||||
*/
|
||||
public function getFilterList($context = 'all'): \stdClass
|
||||
{
|
||||
$tableData = $this->getTableData($context);
|
||||
|
||||
$return = new \stdClass();
|
||||
$filters = $tableData['filters'] ?? $tableData['columns'] ?? [];
|
||||
$return->choices = [];
|
||||
$return->choiceHtml = '';
|
||||
$return->definitions = [];
|
||||
$return->operatorHtml = [];
|
||||
$return->operatorChoices = [];
|
||||
|
||||
foreach ($filters as $filter => $data) {
|
||||
if (isset($data['label'])) {
|
||||
$return->definitions[$filter] = $data;
|
||||
$return->choices[$filter] = $data['label'];
|
||||
$return->choiceHtml .= "<option value=\"$filter\">{$data['label']}</option>\n";
|
||||
|
||||
$return->operatorChoices[$filter] = $this->getOperatorOptions($data);
|
||||
$return->operatorHtml[$filter] = '';
|
||||
|
||||
foreach ($return->operatorChoices[$filter] as $value => $label) {
|
||||
$return->operatorHtml[$filter] .= "<option value=\"$value\">$label</option>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $context
|
||||
*
|
||||
* @return \stdClass ['choices' => [], choiceHtml = '']
|
||||
*/
|
||||
public function getGraphList($context = 'all'): \stdClass
|
||||
{
|
||||
$graphData = $this->getGraphData($context);
|
||||
$return = new \stdClass();
|
||||
$return->choices = [];
|
||||
$return->choiceHtml = '';
|
||||
|
||||
// First sort
|
||||
foreach ($graphData as $key => $details) {
|
||||
$return->choices[$key] = $this->translator->trans($key).' ('.$this->translator->trans('mautic.report.graph.'.$details['type']).')';
|
||||
}
|
||||
natsort($return->choices);
|
||||
|
||||
foreach ($return->choices as $key => $value) {
|
||||
$return->choiceHtml .= '<option value="'.$key.'">'.$value."</option>\n";
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export report.
|
||||
*
|
||||
* @param string $format
|
||||
* @param int $page
|
||||
*
|
||||
* @return StreamedResponse|Response
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function exportResults($format, Report $report, ReportDataResult $reportDataResult, $handle = null, $page = null)
|
||||
{
|
||||
$date = (new DateTimeHelper())->toLocalString();
|
||||
$name = str_replace(' ', '_', $date).'_'.InputHelper::alphanum($report->getName(), false, '-');
|
||||
|
||||
switch ($format) {
|
||||
case 'csv':
|
||||
if (!is_null($handle)) {
|
||||
$this->csvExporter->export($reportDataResult, $handle, $page);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response = new StreamedResponse(
|
||||
function () use ($reportDataResult): void {
|
||||
$handle = fopen('php://output', 'r+');
|
||||
$this->csvExporter->export($reportDataResult, $handle);
|
||||
fclose($handle);
|
||||
}
|
||||
);
|
||||
|
||||
$fileName = $name.'.csv';
|
||||
ExportResponse::setResponseHeaders($response, $fileName);
|
||||
|
||||
return $response;
|
||||
|
||||
case 'html':
|
||||
$content = $this->twig->render(
|
||||
'@MauticReport/Report/export.html.twig',
|
||||
[
|
||||
'pageTitle' => $name,
|
||||
'report' => $report,
|
||||
'reportDataResult' => $reportDataResult,
|
||||
]
|
||||
);
|
||||
|
||||
return new Response($content);
|
||||
|
||||
case 'xlsx':
|
||||
if (!class_exists(Spreadsheet::class)) {
|
||||
throw new \Exception('PHPSpreadsheet is required to export to Excel spreadsheets');
|
||||
}
|
||||
|
||||
$response = new StreamedResponse(
|
||||
function () use ($reportDataResult, $name): void {
|
||||
$this->excelExporter->export($reportDataResult, $name);
|
||||
}
|
||||
);
|
||||
|
||||
$fileName = $name.'.xlsx';
|
||||
ExportResponse::setResponseHeaders($response, $fileName);
|
||||
|
||||
return $response;
|
||||
|
||||
default:
|
||||
return new Response();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get report data for view rendering.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getReportData(Report $entity, ?FormFactoryInterface $formFactory = null, array $options = []): array
|
||||
{
|
||||
// Clone dateFrom/dateTo because they handled separately in charts
|
||||
$chartDateFrom = isset($options['dateFrom']) ? clone $options['dateFrom'] : (new \DateTime('-30 days'));
|
||||
$chartDateTo = isset($options['dateTo']) ? clone $options['dateTo'] : (new \DateTime());
|
||||
$debugData = [];
|
||||
|
||||
// UI doesn't set time so reset it to midnight. API can set time so do not reset it. Using DateTimeImmutable to distinguish.
|
||||
$resetTime = !(isset($options['dateFrom']) && $options['dateFrom'] instanceof \DateTimeImmutable);
|
||||
|
||||
if ($resetTime && isset($options['dateFrom'])) {
|
||||
$now = new \DateTime();
|
||||
|
||||
if (!isset($options['dateTo'])) {
|
||||
$options['dateTo'] = $now;
|
||||
}
|
||||
|
||||
// Set time to the last second of the "to date" date
|
||||
if ($now->format('Y-m-d') === $options['dateTo']->format('Y-m-d')) {
|
||||
$options['dateTo'] = $now->setTime(23, 59, 59);
|
||||
} else {
|
||||
$options['dateTo']->setTime(23, 59, 59);
|
||||
}
|
||||
|
||||
// Convert date ranges to UTC for fetching tabular data
|
||||
$options['dateFrom']->setTimeZone(new \DateTimeZone('UTC'));
|
||||
$options['dateTo']->setTimeZone(new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
$paginate = !empty($options['paginate']);
|
||||
$reportPage = $options['reportPage'] ?? 1;
|
||||
$data = $graphs = [];
|
||||
$reportGenerator = new ReportGenerator($this->dispatcher, $this->getConnection(), $entity, $this->channelListHelper, $formFactory);
|
||||
|
||||
$selectedColumns = $entity->getColumns();
|
||||
$totalResults = $limit = 0;
|
||||
|
||||
// Prepare the query builder
|
||||
$tableDetails = $this->getTableData($entity->getSource());
|
||||
$dataColumns = $dataAggregatorColumns = [];
|
||||
$aggregatorColumns = ($aggregators = $entity->getAggregators()) ? $aggregators : [];
|
||||
|
||||
foreach ($aggregatorColumns as $aggregatorColumn) {
|
||||
$selectedColumns[] = $aggregatorColumn['column'];
|
||||
// add aggregator columns to dataColumns also
|
||||
$dataColumns[$aggregatorColumn['function'].' '.$aggregatorColumn['column']] = $aggregatorColumn['column'];
|
||||
$dataAggregatorColumns[$aggregatorColumn['function'].' '.$aggregatorColumn['column']] = $aggregatorColumn['column'];
|
||||
}
|
||||
// Build a reference for column to data column (without table prefix)
|
||||
foreach ($tableDetails['columns'] as $dbColumn => &$columnData) {
|
||||
$dataColumns[$columnData['alias']] = $dbColumn;
|
||||
}
|
||||
|
||||
$session = $this->getSession();
|
||||
$orderBy = '';
|
||||
$orderByDir = 'ASC';
|
||||
// make sure to use the session if it's started. Otherwise this is impossible to test:
|
||||
// Failed to start the session because headers have already been sent by "/var/www/html/vendor/phpunit/phpunit/src/Util/Printer.php" at line 104.
|
||||
if ($session->isStarted()) {
|
||||
$orderBy = $session->get('mautic.report.'.$entity->getId().'.orderby', $orderBy);
|
||||
$orderByDir = $session->get('mautic.report.'.$entity->getId().'.orderbydir', $orderByDir);
|
||||
}
|
||||
|
||||
$dataOptions = [
|
||||
'order' => (!empty($orderBy)) ? [$orderBy, $orderByDir] : false,
|
||||
'columns' => $tableDetails['columns'],
|
||||
'filters' => $tableDetails['filters'] ?? $tableDetails['columns'],
|
||||
'dateFrom' => $options['dateFrom'] ?? null,
|
||||
'dateTo' => $options['dateTo'] ?? null,
|
||||
'dynamicFilters' => $options['dynamicFilters'] ?? [],
|
||||
];
|
||||
|
||||
/** @var QueryBuilder $query */
|
||||
$query = $reportGenerator->getQuery($dataOptions);
|
||||
$options['translator'] = $this->translator;
|
||||
|
||||
$contentTemplate = $reportGenerator->getContentTemplate();
|
||||
|
||||
// set what page currently on so that we can return here after form submission/cancellation
|
||||
$session = $this->getSession();
|
||||
if ($session->isStarted()) {
|
||||
$session->set('mautic.report.'.$entity->getId().'.page', $reportPage);
|
||||
}
|
||||
|
||||
// Reset the orderBy as it causes errors in graphs and the count query in table data
|
||||
$parts = $query->getQueryParts();
|
||||
$order = $parts['orderBy'];
|
||||
$query->resetQueryPart('orderBy');
|
||||
|
||||
if (empty($options['ignoreGraphData'])) {
|
||||
$chartQuery = new ChartQuery($this->em->getConnection(), $chartDateFrom, $chartDateTo);
|
||||
$options['chartQuery'] = $chartQuery;
|
||||
|
||||
// Check to see if this is an update from AJAX
|
||||
$selectedGraphs = (!empty($options['graphName'])) ? [$options['graphName']] : $entity->getGraphs();
|
||||
if (!empty($selectedGraphs)) {
|
||||
$availableGraphs = $this->getGraphData($entity->getSource());
|
||||
if (empty($query)) {
|
||||
$query = $reportGenerator->getQuery();
|
||||
}
|
||||
|
||||
$eventGraphs = [];
|
||||
$defaultGraphOptions = $options;
|
||||
$defaultGraphOptions['dateFrom'] = $chartDateFrom;
|
||||
$defaultGraphOptions['dateTo'] = $chartDateTo;
|
||||
|
||||
foreach ($selectedGraphs as $g) {
|
||||
if (isset($availableGraphs[$g])) {
|
||||
$graphOptions = $availableGraphs[$g]['options'] ?? [];
|
||||
$graphOptions = array_merge($defaultGraphOptions, $graphOptions);
|
||||
$eventGraphs[$g] = [
|
||||
'options' => $graphOptions,
|
||||
'type' => $availableGraphs[$g]['type'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$event = new ReportGraphEvent($entity, $eventGraphs, $query);
|
||||
$this->dispatcher->dispatch($event, ReportEvents::REPORT_ON_GRAPH_GENERATE);
|
||||
$graphs = $event->getGraphs();
|
||||
|
||||
unset($defaultGraphOptions);
|
||||
}
|
||||
}
|
||||
|
||||
$columnsAllowed = $this->getColumnList($entity->getSource());
|
||||
$order = $this->getOrderBySanitized($order, $columnsAllowed);
|
||||
if ($order['hasOrderBy']) {
|
||||
$query->add('orderBy', $order['orderBy']);
|
||||
}
|
||||
|
||||
// Allow plugin to manipulate the query
|
||||
$event = new ReportQueryEvent($entity, $query, $totalResults, $dataOptions);
|
||||
$this->dispatcher->dispatch($event, ReportEvents::REPORT_QUERY_PRE_EXECUTE);
|
||||
$query = $event->getQuery();
|
||||
|
||||
if (empty($options['ignoreTableData']) && !empty($selectedColumns)) {
|
||||
if ($paginate) {
|
||||
// Build the options array to pass into the query
|
||||
if ($session->isStarted()) {
|
||||
$limit = $session->get('mautic.report.'.$entity->getId().'.limit', $this->defaultPageLimit);
|
||||
}
|
||||
if (!empty($options['limit'])) {
|
||||
$limit = $options['limit'];
|
||||
$reportPage = $options['page'];
|
||||
}
|
||||
$start = (1 === $reportPage) ? 0 : (($reportPage - 1) * $limit);
|
||||
if ($start < 0) {
|
||||
$start = 0;
|
||||
}
|
||||
|
||||
if (empty($options['totalResults'])) {
|
||||
$options['totalResults'] = $totalResults = $this->getTotalCount($query, $debugData);
|
||||
} else {
|
||||
$totalResults = $options['totalResults'];
|
||||
}
|
||||
|
||||
if ($limit > 0) {
|
||||
$query->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
}
|
||||
|
||||
$queryTime = microtime(true);
|
||||
$data = $query->executeQuery()->fetchAllAssociative();
|
||||
$queryTime = round((microtime(true) - $queryTime) * 1000);
|
||||
|
||||
if ($queryTime >= 1000) {
|
||||
$queryTime *= 1000;
|
||||
|
||||
$queryTime .= 's';
|
||||
} else {
|
||||
$queryTime .= 'ms';
|
||||
}
|
||||
|
||||
if (!$paginate) {
|
||||
$totalResults = count($data);
|
||||
}
|
||||
|
||||
// Allow plugin to manipulate the data
|
||||
$event = new ReportDataEvent($entity, $data, $totalResults, $dataOptions);
|
||||
$this->dispatcher->dispatch($event, ReportEvents::REPORT_ON_DISPLAY);
|
||||
$data = $event->getData();
|
||||
$dataOptions = $event->getOptions();
|
||||
}
|
||||
|
||||
if ($this->isDebugMode()) {
|
||||
$debugData['query'] = $query->getSQL();
|
||||
$params = $query->getParameters();
|
||||
|
||||
foreach ($params as $name => $param) {
|
||||
if (is_array($param)) {
|
||||
$param = implode("','", $param);
|
||||
}
|
||||
$debugData['query'] = str_replace(":$name", "'$param'", $debugData['query']);
|
||||
}
|
||||
|
||||
$debugData['query_time'] = $queryTime ?? 'N/A';
|
||||
}
|
||||
|
||||
foreach ($data as $keys => $lead) {
|
||||
foreach ($lead as $key => $field) {
|
||||
$data[$keys][$key] = html_entity_decode((string) $field, ENT_QUOTES);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'totalResults' => $totalResults,
|
||||
'data' => $data,
|
||||
'dataColumns' => $dataColumns,
|
||||
'graphs' => $graphs,
|
||||
'contentTemplate' => $contentTemplate,
|
||||
'columns' => $dataOptions['columns'],
|
||||
'limit' => ($paginate) ? $limit : 0,
|
||||
'page' => ($paginate) ? $reportPage : 1,
|
||||
'dateFrom' => $dataOptions['dateFrom'],
|
||||
'dateTo' => $dataOptions['dateTo'],
|
||||
'debug' => $debugData,
|
||||
'aggregatorColumns' => $dataAggregatorColumns,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize order by array comparing it to the allowed columns.
|
||||
*
|
||||
* @param iterable<mixed> $orderBys
|
||||
*
|
||||
* @return iterable<mixed>
|
||||
*/
|
||||
private function getOrderBySanitized(iterable $orderBys, \stdClass $allowedColumns): iterable
|
||||
{
|
||||
$hasOrderBy = false;
|
||||
foreach ($orderBys as $key => $orderBy) {
|
||||
if ($this->orderByIsValid($orderBy, $allowedColumns->choices)) {
|
||||
$hasOrderBy = true;
|
||||
continue;
|
||||
}
|
||||
$orderBys[$key] = '';
|
||||
}
|
||||
|
||||
return [
|
||||
'orderBy' => $orderBys,
|
||||
'hasOrderBy' => $hasOrderBy,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if order by is valid.
|
||||
*
|
||||
* @param array<string, string> $allowedColumns
|
||||
*/
|
||||
private function orderByIsValid(string $order, array $allowedColumns): bool
|
||||
{
|
||||
if (empty($order)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$orderBy = $order;
|
||||
$oderByDirection = '';
|
||||
|
||||
if (str_contains($order, ' ')) {
|
||||
$orderTemp = explode(' ', $order);
|
||||
$orderBy = $orderTemp[0];
|
||||
$oderByDirection = $orderTemp[1];
|
||||
}
|
||||
|
||||
if (!array_key_exists($orderBy, $allowedColumns) || !in_array($oderByDirection, ['ASC', 'DESC', ''])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getReportsWithGraphs(): array
|
||||
{
|
||||
$ownedBy = $this->security->isGranted('report:reports:viewother') ? null : $this->userHelper->getUser()->getId();
|
||||
|
||||
return $this->getRepository()->findReportsWithGraphs($ownedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine what operators should be used for the filter type.
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
private function getOperatorOptions(array $data)
|
||||
{
|
||||
if (isset($data['operators'])) {
|
||||
// Custom operators
|
||||
$options = $data['operators'];
|
||||
} else {
|
||||
$operator = $data['operatorGroup'] ?? $data['type'];
|
||||
|
||||
if (!array_key_exists($operator, MauticReportBuilder::OPERATORS)) {
|
||||
$operator = 'default';
|
||||
}
|
||||
|
||||
$options = MauticReportBuilder::OPERATORS[$operator];
|
||||
}
|
||||
|
||||
foreach ($options as &$label) {
|
||||
$label = $this->translator->trans($label);
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getTotalCount(QueryBuilder $qb, array &$debugData): int
|
||||
{
|
||||
$countQb = clone $qb;
|
||||
$countQb->resetQueryParts();
|
||||
|
||||
$countQb->select('count(*)')
|
||||
->from('('.$qb->getSQL().')', 'c');
|
||||
|
||||
if ($this->isDebugMode()) {
|
||||
$debugData['count_query'] = $countQb->getSQL();
|
||||
}
|
||||
|
||||
return (int) $countQb->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $segmentId
|
||||
*/
|
||||
public function getReportsIdsWithDependenciesOnSegment($segmentId): array
|
||||
{
|
||||
$search = 'lll.leadlist_id';
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'r.filters', 'expr' => 'LIKE', 'value'=>'%'.$search.'"%'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
$dependents = [];
|
||||
foreach ($entities as $entity) {
|
||||
$retrFilters = $entity->getFilters();
|
||||
foreach ($retrFilters as $eachFilter) {
|
||||
if ($eachFilter['column'] == $search && $eachFilter['value'] == $segmentId) {
|
||||
$dependents[] = $entity->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dependents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getReportsIdsWithDependenciesOnEmail(int $emailId): array
|
||||
{
|
||||
$search = 'e.id';
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'r.source', 'expr' => 'IN', 'value'=> ['emails', 'email.stats']],
|
||||
['column' => 'r.filters', 'expr' => 'LIKE', 'value'=>'%'.$search.'"%'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
|
||||
$dependents = [];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($entity->getFilters() as $entityFilter) {
|
||||
if ($entityFilter['column'] == $search && $entityFilter['value'] == $emailId) {
|
||||
$dependents[] = $entity->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($dependents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getReportsIdsWithDependenciesOnTag(int $tagId): array
|
||||
{
|
||||
$search = 'tag';
|
||||
$filter = [
|
||||
'force' => [
|
||||
['column' => 'r.filters', 'expr' => 'LIKE', 'value'=>'%'.$search.'"%'],
|
||||
],
|
||||
];
|
||||
$entities = $this->getEntities(
|
||||
[
|
||||
'filter' => $filter,
|
||||
]
|
||||
);
|
||||
|
||||
$dependents = [];
|
||||
foreach ($entities as $entity) {
|
||||
foreach ($entity->getFilters() as $entityFilter) {
|
||||
if ($entityFilter['column'] == $search && (is_array($entityFilter['value']) && in_array($tagId, $entityFilter['value']))) {
|
||||
$dependents[] = $entity->getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique($dependents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Doctrine\DBAL\Connection
|
||||
*/
|
||||
private function getConnection()
|
||||
{
|
||||
$connection = $this->em->getConnection();
|
||||
if ($connection instanceof PrimaryReadReplicaConnection) {
|
||||
$connection->ensureConnectedToReplica();
|
||||
}
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
protected function isDebugMode(): bool
|
||||
{
|
||||
return MAUTIC_ENV == 'dev' || $this->coreParametersHelper->get('debug');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ReportBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\ReportBundle\Entity\Report;
|
||||
use Mautic\ReportBundle\Entity\Scheduler;
|
||||
use Mautic\ReportBundle\Entity\SchedulerRepository;
|
||||
use Mautic\ReportBundle\Scheduler\Model\SchedulerPlanner;
|
||||
use Mautic\ReportBundle\Scheduler\Option\ExportOption;
|
||||
|
||||
class ScheduleModel
|
||||
{
|
||||
/**
|
||||
* @var SchedulerRepository
|
||||
*/
|
||||
private \Doctrine\ORM\EntityRepository $schedulerRepository;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private SchedulerPlanner $schedulerPlanner,
|
||||
) {
|
||||
$this->schedulerRepository = $entityManager->getRepository(Scheduler::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Scheduler[]
|
||||
*/
|
||||
public function getScheduledReportsForExport(ExportOption $exportOption)
|
||||
{
|
||||
return $this->schedulerRepository->getScheduledReportsForExport($exportOption);
|
||||
}
|
||||
|
||||
public function reportWasScheduled(Report $report): void
|
||||
{
|
||||
$this->schedulerPlanner->computeScheduler($report);
|
||||
}
|
||||
|
||||
public function turnOffScheduler(Report $report): void
|
||||
{
|
||||
$report->setIsScheduled(false);
|
||||
$this->entityManager->persist($report);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user