Files
Cloud-CMS/lib/Factory/DataSetFactory.php

918 lines
35 KiB
PHP
Raw Normal View History

2025-12-02 10:32:59 -05:00
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Factory;
use Carbon\Carbon;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\DataSet;
use Xibo\Entity\DataSetColumn;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\DisplayNotifyServiceInterface;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class DataSetFactory
* @package Xibo\Factory
*/
class DataSetFactory extends BaseFactory
{
/** @var ConfigServiceInterface */
private $config;
/** @var PoolInterface */
private $pool;
/** @var DataSetColumnFactory */
private $dataSetColumnFactory;
/** @var PermissionFactory */
private $permissionFactory;
/** @var DisplayNotifyServiceInterface */
private $displayNotifyService;
/**
* Construct a factory
* @param \Xibo\Entity\User $user
* @param UserFactory $userFactory
* @param ConfigServiceInterface $config
* @param PoolInterface $pool
* @param DataSetColumnFactory $dataSetColumnFactory
* @param PermissionFactory $permissionFactory
* @param DisplayNotifyServiceInterface $displayNotifyService
*/
public function __construct($user, $userFactory, $config, $pool, $dataSetColumnFactory, $permissionFactory, $displayNotifyService)
{
$this->setAclDependencies($user, $userFactory);
$this->config = $config;
$this->pool = $pool;
$this->dataSetColumnFactory = $dataSetColumnFactory;
$this->permissionFactory = $permissionFactory;
$this->displayNotifyService = $displayNotifyService;
}
/**
* @return DataSetColumnFactory
*/
public function getDataSetColumnFactory()
{
return $this->dataSetColumnFactory;
}
/**
* @return DataSet
*/
public function createEmpty()
{
return new DataSet(
$this->getStore(),
$this->getLog(),
$this->getDispatcher(),
$this->getSanitizerService(),
$this->config,
$this->pool,
$this,
$this->dataSetColumnFactory,
$this->permissionFactory,
$this->displayNotifyService
);
}
/**
* Get DataSets by ID
* @param $dataSetId
* @return DataSet
* @throws NotFoundException
*/
public function getById($dataSetId)
{
$dataSets = $this->query(null, ['disableUserCheck' => 1, 'dataSetId' => $dataSetId]);
if (count($dataSets) <= 0) {
throw new NotFoundException();
}
return $dataSets[0];
}
/**
* Get DataSets by Code
* @param $code
* @return DataSet
* @throws NotFoundException
*/
public function getByCode($code)
{
$dataSets = $this->query(null, ['disableUserCheck' => 1, 'code' => $code]);
if (count($dataSets) <= 0) {
throw new NotFoundException();
}
return $dataSets[0];
}
/**
* Get DataSets by Name
* @param $dataSet
* @param int|null $userId the userId
* @return DataSet
* @throws NotFoundException
*/
public function getByName($dataSet, $userId = null)
{
$dataSets = $this->query(null, ['dataSetExact' => $dataSet, 'userId' => $userId]);
if (count($dataSets) <= 0) {
throw new NotFoundException();
}
return $dataSets[0];
}
/**
* @param $userId
* @return DataSet[]
* @throws NotFoundException
*/
public function getByOwnerId($userId)
{
$dataSets = $this->query(null, ['disableUserCheck' => 1, 'userId' => $userId]);
return $dataSets;
}
/**
* @param int $folderId
* @return DataSet[]
* @throws NotFoundException
*/
public function getByFolderId(int $folderId)
{
return $this->query(null, ['disableUserCheck' => 1, 'folderId' => $folderId]);
}
/**
* @param array $sortOrder
* @param array $filterBy
* @return array[DataSet]
* @throws NotFoundException
*/
public function query($sortOrder = null, $filterBy = [])
{
$entries = [];
$params = [];
$parsedFilter = $this->getSanitizer($filterBy);
if ($sortOrder === null) {
$sortOrder = ['dataSet'];
}
$select = '
SELECT dataset.dataSetId,
dataset.dataSet,
dataset.description,
dataset.userId,
dataset.lastDataEdit,
dataset.`code`,
dataset.`isLookup`,
dataset.`isRemote`,
dataset.`isRealTime`,
dataset.`dataConnectorSource`,
dataset.`method`,
dataset.`uri`,
dataset.`postData`,
dataset.`authentication`,
dataset.`username`,
dataset.`password`,
dataset.`customHeaders`,
dataset.`userAgent`,
dataset.`refreshRate`,
dataset.`clearRate`,
dataset.`truncateOnEmpty`,
dataset.`runsAfter`,
dataset.`dataRoot`,
dataset.`summarize`,
dataset.`summarizeField`,
dataset.`lastSync`,
dataset.`lastClear`,
dataset.`sourceId`,
dataset.`ignoreFirstRow`,
dataset.`rowLimit`,
dataset.`limitPolicy`,
dataset.`csvSeparator`,
dataset.`folderId`,
dataset.`permissionsFolderId`,
user.userName AS owner,
(
SELECT GROUP_CONCAT(DISTINCT `group`.group)
FROM `permission`
INNER JOIN `permissionentity`
ON `permissionentity`.entityId = permission.entityId
INNER JOIN `group`
ON `group`.groupId = `permission`.groupId
WHERE entity = :groupsWithPermissionsEntity
AND objectId = dataset.dataSetId
) AS groupsWithPermissions
';
$params['groupsWithPermissionsEntity'] = 'Xibo\\Entity\\DataSet';
$body = '
FROM dataset
INNER JOIN `user` ON user.userId = dataset.userId
WHERE 1 = 1
';
if ($parsedFilter->getInt('dataSetId') !== null) {
$body .= ' AND dataset.dataSetId = :dataSetId ';
$params['dataSetId'] = $parsedFilter->getInt('dataSetId');
}
if ($parsedFilter->getInt('userId') !== null) {
$body .= ' AND dataset.userId = :userId ';
$params['userId'] = $parsedFilter->getInt('userId');
}
if ($parsedFilter->getInt('isRemote') !== null) {
$body .= ' AND dataset.isRemote = :isRemote ';
$params['isRemote'] = $parsedFilter->getInt('isRemote');
}
if ($parsedFilter->getInt('isRealTime') !== null) {
$body .= ' AND dataset.isRealTime = :isRealTime ';
$params['isRealTime'] = $parsedFilter->getInt('isRealTime');
}
if ($parsedFilter->getString('dataSet') != null) {
$terms = explode(',', $parsedFilter->getString('dataSet'));
$logicalOperator = $parsedFilter->getString('logicalOperatorName', ['default' => 'OR']);
$this->nameFilter(
'dataset',
'dataSet',
$terms,
$body,
$params,
($parsedFilter->getCheckbox('useRegexForName') == 1),
$logicalOperator
);
}
if ($parsedFilter->getString('dataSetExact') != '') {
$body.= " AND dataset.dataSet = :exact ";
$params['exact'] = $parsedFilter->getString('dataSetExact');
}
if ($parsedFilter->getString('code') != null) {
$body .= ' AND `dataset`.`code` = :code ';
$params['code'] = $parsedFilter->getString('code');
}
if ($parsedFilter->getInt('folderId') !== null) {
$body .= ' AND dataset.folderId = :folderId ';
$params['folderId'] = $parsedFilter->getInt('folderId');
}
// View Permissions
$this->viewPermissionSql('Xibo\Entity\DataSet', $body, $params, '`dataset`.dataSetId', '`dataset`.userId', $filterBy, '`dataset`.permissionsFolderId');
// Sorting?
$order = '';
if (is_array($sortOrder))
$order .= 'ORDER BY ' . implode(',', $sortOrder);
$limit = '';
// Paging
if ($filterBy !== null && $parsedFilter->getInt('start') !== null && $parsedFilter->getInt('length') !== null) {
$limit = ' LIMIT ' . $parsedFilter->getInt('start', ['default' => 0]) . ', ' . $parsedFilter->getInt('length', ['default' => 10]);
}
$sql = $select . $body . $order . $limit;
foreach ($this->getStore()->select($sql, $params) as $row) {
$entries[] = $this->createEmpty()->hydrate($row, [
'intProperties' => [
'isLookup',
'isRemote',
'isRealTime',
'clearRate',
'refreshRate',
'lastDataEdit',
'runsAfter',
'lastSync',
'lastClear',
'ignoreFirstRow',
],
]);
}
// Paging
if ($limit != '' && count($entries) > 0) {
unset($params['groupsWithPermissionsEntity']);
$results = $this->getStore()->select('SELECT COUNT(*) AS total ' . $body, $params);
$this->_countLast = intval($results[0]['total']);
}
return $entries;
}
/**
* Makes a call to a Remote Dataset and returns all received data as a JSON decoded Object.
* In case of an Error, null is returned instead.
* @param DataSet $dataSet The Dataset to get Data for
* @param DataSet|null $dependant The Dataset $dataSet depends on
* @param bool $enableCaching Should we cache check the results and store the resulting cache
* @return \stdClass {entries:[], number:int, isEligibleToTruncate:bool}
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function callRemoteService(DataSet $dataSet, ?DataSet $dependant = null, $enableCaching = true)
{
$this->getLog()->debug('Calling remote service for DataSet: ' . $dataSet->dataSet
. ' and URL ' . $dataSet->uri);
// Record our max memory
$maxMemory = Environment::getMemoryLimitBytes() / 2;
// Guzzle for this and add proxy support.
$client = new Client($this->config->getGuzzleProxy());
$result = new \stdClass();
$result->entries = [];
$result->number = 0;
$result->isEligibleToTruncate = false;
// Getting all dependant values if needed
// just an empty array if we don't have a dependent
$values = [
[]
];
if ($dependant != null && $dataSet->containsDependantFieldsInRequest()) {
$this->getLog()->debug('Dependant provided with fields in the request.');
$values = $dependant->getData();
}
// Fetching data for every field in the dependant dataSet
foreach ($values as $options) {
// Make some request params to provide to the HTTP client
$resolvedUri = $this->replaceParams($dataSet->uri, $options);
$requestParams = [];
// Auth
switch ($dataSet->authentication) {
case 'basic':
$requestParams['auth'] = [$dataSet->username, $dataSet->password];
break;
case 'digest':
$requestParams['auth'] = [$dataSet->username, $dataSet->password, 'digest'];
break;
case 'ntlm':
$requestParams['auth'] = [$dataSet->username, $dataSet->password, 'ntlm'];
break;
case 'bearer':
$requestParams['headers'] = ['Authorization' => 'Bearer ' . $dataSet->password];
break;
case 'none':
default:
$this->getLog()->debug('No authentication required');
}
if (isset($dataSet->customHeaders)) {
$arrayOfCustomHeaders = array_filter(explode(',', $dataSet->customHeaders));
foreach ($arrayOfCustomHeaders as $customHeader) {
$header = array_filter(explode(':', $customHeader));
$requestParams['headers'][$header[0]] = $header[1];
}
}
// Post request?
if ($dataSet->method === 'POST') {
parse_str($this->replaceParams($dataSet->postData, $options), $requestParams['form_params']);
} else {
// Get the query params from the URL.
$queryParamsArray = [];
$dataSetPostData = [];
$parsedUrl = parse_url($resolvedUri, PHP_URL_QUERY);
if ($parsedUrl) {
parse_str($parsedUrl, $queryParamsArray);
}
parse_str($this->replaceParams($dataSet->postData, $options), $dataSetPostData);
$requestParams['query'] = array_merge($queryParamsArray, $dataSetPostData);
}
if (!empty($dataSet->userAgent)) {
$requestParams['headers']['User-Agent'] = trim($dataSet->userAgent);
}
$this->getLog()->debug('Making request to ' . $resolvedUri . ' with params: ' . var_export($requestParams, true));
try {
// Make a HEAD request to the URI and see if we are able to process this.
if ($dataSet->method === 'GET') {
try {
$request = $client->head($resolvedUri, $requestParams);
$contentLength = $request->getHeader('Content-Length');
if ($maxMemory > 0 && count($contentLength) > 0 && $contentLength[0] > $maxMemory)
throw new InvalidArgumentException(__('The request %d is too large to fit inside the configured memory limit. %d', $contentLength[0], $maxMemory), 'contentLength');
} catch (RequestException $requestException) {
$this->getLog()->info('Cannot make head request for remote dataSet ' . $dataSet->dataSetId);
}
}
$request = $client->request($dataSet->method, $resolvedUri, $requestParams);
// Check the cache control situation
if ($enableCaching) {
// recache if necessary
$cacheControlKey = $this->pool->getItem('/dataset/cache/' . $dataSet->dataSetId . '/' . md5($resolvedUri . json_encode($requestParams)));
$cacheControlKeyValue = ($cacheControlKey->isMiss()) ? '' : $cacheControlKey->get();
$this->getLog()->debug('Cache Control Key is ' . $cacheControlKeyValue);
$etags = $request->getHeader('E-Tag');
$lastModifieds = $request->getHeader('Last-Modified');
if (count($etags) > 0) {
// Compare the etag with the cache key and see if they are the same, if they are
// then we stop processing this data set
if ($cacheControlKeyValue === $etags[0]) {
$this->getLog()->debug('Skipping due to eTag');
continue;
}
$cacheControlKeyValue = $etags[0];
} else if (count($lastModifieds) > 0) {
if ($cacheControlKeyValue === $lastModifieds[0]) {
$this->getLog()->debug('Skipping due to Last-Modified');
continue;
}
$cacheControlKeyValue = $lastModifieds[0];
} else {
// Request doesn't have any cache control of its own
// use the md5
$md5 = md5($request->getBody());
// Rewind so we can use it again
$request->getBody()->rewind();
if ($cacheControlKeyValue === $md5) {
$this->getLog()->debug('Skipping due to MD5');
continue;
}
$cacheControlKeyValue = $md5;
}
$this->getLog()->debug('Cache Control Key is now ' . $cacheControlKeyValue);
// Store the cache key
$cacheControlKey->set($cacheControlKeyValue);
$cacheControlKey->expiresAfter(86400 * 365);
$this->pool->saveDeferred($cacheControlKey);
}
// We have passed any caching and therefore expect results
$result->isEligibleToTruncate = true;
if ($dataSet->sourceId === 1) {
// Make sure we have JSON in the response
$body = $request->getBody()->getContents();
try {
$json = \GuzzleHttp\json_decode($body);
} catch (\GuzzleHttp\Exception\InvalidArgumentException $invalidArgumentException) {
$this->getLog()->debug('JSON decode error: ' . $invalidArgumentException->getMessage());
throw new InvalidArgumentException(__('Unable to get Data for %s because the response was not valid JSON.', $dataSet->dataSet), 'url');
}
$result->entries[] = $json;
foreach ($result->entries as $entry) {
$data = $this->getDataRootFromResult($dataSet->dataRoot, $entry);
if (is_array($data)) {
$result->number = count($data);
} elseif (is_object($data)) {
$result->number = count(get_object_vars($data));
}
}
} else {
$csv = $request->getBody()->getContents();
$array = array_map(
function ($v) use ($dataSet) {
return str_getcsv($v, $dataSet->csvSeparator ?? ',');
},
explode("\n", $csv)
);
if ($dataSet->ignoreFirstRow == 1) {
array_shift($array);
}
// Filter out rows that are entirely empty
$array = array_filter($array, function($row) {
// Check if the row is empty (all elements are empty or null)
return array_filter($row, function($value) {
return !empty($value);
});
});
$result->entries = $array;
$result->number = count($array);
}
} catch (RequestException $requestException) {
$this->getLog()->error('Error making request. ' . $requestException->getMessage());
// No point in carrying on through this stack of requests, dependent or original data will be
// missing
throw new InvalidArgumentException(__('Unable to get Data for %s because %s.', $dataSet->dataSet, $requestException->getMessage()), 'dataSetId');
}
}
return $result;
}
/**
* Replaces all URI/PostData parameters
* @param string String to replace {{DATE}}, {{TIME}} and {{COL.xxx}}
* @param array $values ColumnValues to use on {{COL.xxx}} parts
* @return string
*/
private function replaceParams($string = '', array $values = [])
{
if (empty($string)) {
return $string;
}
$string = str_replace('{{DATE}}', date('Y-m-d'), $string);
$string = str_replace('%7B%7BDATE%7D%7D', date('Y-m-d'), $string);
$string = str_replace('{{TIME}}', date('H:m:s'), $string);
$string = str_replace('%7B%7BTIME%7D%7D', date('H:m:s'), $string);
foreach ($values as $k => $v) {
$string = str_replace('{{COL.' . $k . '}}', urlencode($v), $string);
$string = str_replace('%7B%7BCOL.' . $k . '%7D%7D', urlencode($v), $string);
}
return $string;
}
/**
* Tries to process received Data against the configured DataSet with all Columns
*
* @param DataSet $dataSet The RemoteDataset to process
* @param \stdClass $results A simple Object with one Property 'entries' which contains all results
* @param bool $save
* @throws InvalidArgumentException
*/
public function processResults(DataSet $dataSet, \stdClass $results, $save = true)
{
$results->processed = [];
if (property_exists($results, 'entries') && is_array($results->entries)) {
// Load the DataSet fully
$dataSet->load();
$results->messages = [__('Processing %d results into %d potential columns', count($results->entries), count($dataSet->columns))];
foreach ($results->entries as $result) {
$results->messages[] = __('Processing Result with Data Root %s', $dataSet->dataRoot);
// Remote Data has to have the configured DataRoot which has to be an Array
$data = $this->getDataRootFromResult($dataSet->dataRoot, $result);
$columns = $dataSet->columns;
$entries = [];
// Process the data root according to its type
if (is_array($data)) {
// An array of results as the DataRoot
$results->messages[] = 'DataRoot is an array';
// First process each entry form the remote and try to map the values to the configured columns
foreach ($data as $k => $entry) {
$this->getLog()->debug('Processing key ' . $k . ' from the remote results');
$this->getLog()->debug('Entry is: ' . var_export($entry, true));
$results->messages[] = 'Processing ' . $k;
if (is_array($entry) || is_object($entry)) {
$entries[] = $this->processEntry((array)$entry, $columns);
} else {
$this->getLog()->error('DataSet ' . $dataSet->dataSet . ' failed: DataRoot ' . $dataSet->dataRoot . ' contains data which is not arrays or objects.');
break;
}
}
} else if (is_object($data)) {
// An object as the DataRoot.
$results->messages[] = 'DataRoot is an object';
// We should treat this as a single row? Or as multiple rows?
// we could try and guess from the configuration of the dataset columns
$singleRow = false;
foreach ($columns as $column) {
if ($column->dataSetColumnTypeId === 3 && $column->remoteField != null && !is_numeric($column->remoteField)) {
$singleRow = true;
break;
}
}
if ($singleRow) {
// Process as a single row
$results->messages[] = __('Processing as a Single Row');
$entries[] = $this->processEntry((array)$data, $columns);
} else {
// Process as multiple rows
$results->messages[] = __('Processing as Multiple Rows');
foreach (get_object_vars($data) as $property => $value) {
// Treat each property as an index key (flattening the array)
$results->messages[] = 'Processing ' . $property;
$entries[] = $this->processEntry([$property, $value], $columns);
}
}
} else {
throw new InvalidArgumentException(__('No data found at the DataRoot %s', $dataSet->dataRoot), 'dataRoot');
}
$results->messages[] = __('Consolidating entries');
// If there is a Consolidation-Function, use the Data against it
$entries = $this->consolidateEntries($dataSet, $entries, $columns);
$results->messages[] = __('There are %d entries in total', count($entries));
// Finally add each entry as a new Row in the DataSet
if ($save) {
foreach ($entries as $entry) {
$dataSet->addRow($entry);
}
}
$results->processed[] = $entries;
}
}
}
/**
* Process the RemoteResult to get the main DataRoot value which can be stay in a structure as well as the values
*
* @param String Chunks split by a Dot where the main entries are hold
* @param array|\stdClass The Value from the remote request
* @return array|\stdClass The Data hold in the configured dataRoot
*/
private function getDataRootFromResult($dataRoot, $result)
{
$this->getLog()->debug('Getting ' . $dataRoot . 'from result.');
if (empty($dataRoot)) {
return $result;
}
$chunks = explode('.', $dataRoot);
$entries = $this->getFieldValueFromEntry($chunks, $result);
return $entries[1];
}
/**
* Process a single Data-Entry form the remote system and map it to the configured Columns
*
* @param array $entry The Data from the remote system
* @param DataSetColumn[] $dataSetColumns The configured Columns form the current DataSet
* @return array The processed $entry as a List of Fields from $columns
*/
private function processEntry(array $entry, array $dataSetColumns)
{
$result = [];
foreach ($dataSetColumns as $column) {
if ($column->dataSetColumnTypeId === 3 && $column->remoteField != null) {
$this->getLog()->debug('Trying to match dataSetColumn ' . $column->heading . ' with remote field ' . $column->remoteField);
// The Field may be a Date, timestamp or a real field
if ($column->remoteField == '{{DATE}}') {
$value = [0, date('Y-m-d')];
} else if ($column->remoteField == '{{TIMESTAMP}}') {
$value = [0, Carbon::now()->format('U')];
} else {
$chunks = explode('.', $column->remoteField);
$value = $this->getFieldValueFromEntry($chunks, $entry);
}
$this->getLog()->debug('Resolved value: ' . var_export($value, true));
// Only add it to the result if we were able to process the field
if ($value != null && $value[1] !== null) {
// 1,String
// 2,Number
// 3,Date
// 4,External Image
// 5,Library Image
// 6,HTML
$validator = $this->getValidator();
switch ($column->dataTypeId) {
case 2:
// Number
if (empty($value[1]) || !($validator->double($value[1]) || $validator->int($value[1]))) {
$result[$column->heading] = 0;
} else {
$result[$column->heading] = doubleval($value[1]);
}
break;
case 3:
// Date
// This expects an ISO date
// check if we were provided with custom dateFormat
$dateFormat = $column->dateFormat ?: DateFormatHelper::getSystemFormat();
try {
// Parse into a date object from any format, and then save using the system format
$result[$column->heading] = Carbon::createFromFormat($dateFormat, $value[1])
->format(DateFormatHelper::getSystemFormat());
} catch (\Exception $e) {
$this->getLog()->error(
sprintf(
'Incorrect date provided %s, expected date format %s',
$value[1],
$dateFormat
)
);
}
break;
case 5:
// Library Image
if (empty($value[1]) || !$validator->int($value[1])) {
$result[$column->heading] = 0;
} else {
$result[$column->heading] = intval($value[1]);
}
break;
case 6:
// HTML, without any sanitization
$result[$column->heading] = $value[1];
break;
default:
// Default value, assume it will be a string and filter it accordingly.
$result[$column->heading] = strip_tags($value[1]);
}
}
} else {
$this->getLog()->debug('Column not matched');
}
}
return $result;
}
/**
* Returns the Value of the remote DataEntry based on the remoteField definition split into chunks
*
* This function is recursive, so be sure you remove the first value from chunks and pass it in again
*
* @param array List of Chunks which interprets the FieldNames in the actual DataEntry
* @param array|\stdClass $entry Current DataEntry
* @return array of the last FieldName and the corresponding value
*/
private function getFieldValueFromEntry(array $chunks, $entry)
{
$value = null;
$key = array_shift($chunks);
$this->getLog()->debug('Entry: ' . var_export($entry, true));
$this->getLog()->debug('Looking for key: ' . $key . '. Chunks: ' . var_export($chunks, true));
if (($entry instanceof \stdClass) && property_exists($entry, $key)) {
$value = $entry->{$key};
} else if (array_key_exists($key, $entry)) {
$value = $entry[$key];
}
$this->getLog()->debug('Value found is: ' . var_export($value, true));
if (($value != null) && (count($chunks) > 0)) {
return $this->getFieldValueFromEntry($chunks, (array) $value);
}
return [ $key, $value ];
}
/**
* Consolidates all Entries by the defined Function in the DataSet
*
* This Method *sums* or *counts* all same entries and returns them.
* If no consolidation function is configured, nothing is done here.
*
* @param DataSet $dataSet the current DataSet
* @param array $entries All processed entries which may be consolidated
* @param array $columns The columns form this DataSet
* @return array which contains all Entries to be added to the DataSet
*/
private function consolidateEntries(DataSet $dataSet, array $entries, array $columns)
{
// Do we need to consolidate?
if ((count($entries) > 0) && $dataSet->doConsolidate()) {
// Yes
$this->getLog()->debug('Consolidate Required on field ' . $dataSet->getConsolidationField());
$consolidated = [];
$field = $dataSet->getConsolidationField();
// Get the Field-Heading based on the consolidation field
foreach ($columns as $k => $column) {
if ($column->remoteField == $dataSet->summarizeField) {
$field = $column->heading;
break;
}
}
// Check each entry and consolidate the value form the defined field
foreach ($entries as $entry) {
if (array_key_exists($field, $entry)) {
$key = $field . '-' . $entry[$field];
$existing = (isset($consolidated[$key])) ? $consolidated[$key] : null;
// Create a new one if there is no currently consolidated field for this value
if ($existing == null) {
$existing = $entry;
$existing[$field] = 0;
}
// Consolidate: Summarize, Count, Unknown
if ($dataSet->summarize == 'sum') {
$existing[$field] = $existing[$field] + $entry[$field];
} else if ($dataSet->summarize == 'count') {
$existing[$field] = $existing[$field] + 1;
} else {
// Unknown consolidation type :?
$existing[$field] = 0;
}
$consolidated[$key] = $existing;
}
}
return $consolidated;
}
return $entries;
}
public function processCsvEntries(DataSet $dataSet, \stdClass $results, $save = true)
{
$this->getLog()->debug('Processing CSV results');
$dataSet->load();
$entries = [];
foreach ($results->entries as $entry) {
$entries[] = $this->processEntry((array)$entry, $dataSet->columns);
}
$results->processed = $entries;
if ($save) {
foreach ($entries as $row) {
$dataSet->addRow($row);
}
}
}
}