Initial Upload
This commit is contained in:
521
lib/Entity/DataSetColumn.php
Normal file
521
lib/Entity/DataSetColumn.php
Normal file
@@ -0,0 +1,521 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Respect\Validation\Validator as v;
|
||||
use Xibo\Factory\DataSetColumnFactory;
|
||||
use Xibo\Factory\DataSetColumnTypeFactory;
|
||||
use Xibo\Factory\DataTypeFactory;
|
||||
use Xibo\Service\LogServiceInterface;
|
||||
use Xibo\Storage\StorageServiceInterface;
|
||||
use Xibo\Support\Exception\InvalidArgumentException;
|
||||
use Xibo\Support\Exception\NotFoundException;
|
||||
use Xibo\Widget\Definition\Sql;
|
||||
|
||||
/**
|
||||
* Class DataSetColumn
|
||||
* @package Xibo\Entity
|
||||
*
|
||||
* @SWG\Definition()
|
||||
*/
|
||||
class DataSetColumn implements \JsonSerializable
|
||||
{
|
||||
use EntityTrait;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The ID of this DataSetColumn")
|
||||
* @var int
|
||||
*/
|
||||
public $dataSetColumnId;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The ID of the DataSet that this Column belongs to")
|
||||
* @var int
|
||||
*/
|
||||
public $dataSetId;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The Column Heading")
|
||||
* @var string
|
||||
*/
|
||||
public $heading;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The ID of the DataType for this Column")
|
||||
* @var int
|
||||
*/
|
||||
public $dataTypeId;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The ID of the ColumnType for this Column")
|
||||
* @var int
|
||||
*/
|
||||
public $dataSetColumnTypeId;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="Comma separated list of valid content for drop down columns")
|
||||
* @var string
|
||||
*/
|
||||
public $listContent;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The order this column should be displayed")
|
||||
* @var int
|
||||
*/
|
||||
public $columnOrder;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="A MySQL formula for this column")
|
||||
* @var string
|
||||
*/
|
||||
public $formula;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The data type for this Column")
|
||||
* @var string
|
||||
*/
|
||||
public $dataType;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The data field of the remote DataSet as a JSON-String")
|
||||
* @var string
|
||||
*/
|
||||
public $remoteField;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="Does this column show a filter on the data entry page?")
|
||||
* @var string
|
||||
*/
|
||||
public $showFilter = 0;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="Does this column allow a sorting on the data entry page?")
|
||||
* @var string
|
||||
*/
|
||||
public $showSort = 0;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="The column type for this Column")
|
||||
* @var string
|
||||
*/
|
||||
public $dataSetColumnType;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="Help text that should be displayed when entering data for this Column.")
|
||||
* @var string
|
||||
*/
|
||||
public $tooltip;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="Flag indicating whether value must be provided for this Column.")
|
||||
* @var int
|
||||
*/
|
||||
public $isRequired = 0;
|
||||
|
||||
/**
|
||||
* @SWG\Property(description="Date format of dates in the source for remote DataSet.")
|
||||
* @var string
|
||||
*/
|
||||
public $dateFormat;
|
||||
|
||||
/** @var DataSetColumnFactory */
|
||||
private $dataSetColumnFactory;
|
||||
|
||||
/** @var DataTypeFactory */
|
||||
private $dataTypeFactory;
|
||||
|
||||
/** @var DataSetColumnTypeFactory */
|
||||
private $dataSetColumnTypeFactory;
|
||||
|
||||
/**
|
||||
* The prior dataset column id, when cloning
|
||||
* @var int
|
||||
*/
|
||||
public $priorDatasetColumnId;
|
||||
|
||||
/**
|
||||
* Entity constructor.
|
||||
* @param StorageServiceInterface $store
|
||||
* @param LogServiceInterface $log
|
||||
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
|
||||
* @param DataSetColumnFactory $dataSetColumnFactory
|
||||
* @param DataTypeFactory $dataTypeFactory
|
||||
* @param DataSetColumnTypeFactory $dataSetColumnTypeFactory
|
||||
*/
|
||||
public function __construct($store, $log, $dispatcher, $dataSetColumnFactory, $dataTypeFactory, $dataSetColumnTypeFactory)
|
||||
{
|
||||
$this->excludeProperty('priorDatasetColumnId');
|
||||
$this->setCommonDependencies($store, $log, $dispatcher);
|
||||
|
||||
$this->dataSetColumnFactory = $dataSetColumnFactory;
|
||||
$this->dataTypeFactory = $dataTypeFactory;
|
||||
$this->dataSetColumnTypeFactory = $dataSetColumnTypeFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->priorDatasetColumnId = $this->dataSetColumnId;
|
||||
$this->dataSetColumnId = null;
|
||||
$this->dataSetId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List Content Array
|
||||
* @return array
|
||||
*/
|
||||
public function listContentArray()
|
||||
{
|
||||
return explode(',', $this->listContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function validate($options = []): void
|
||||
{
|
||||
$options = array_merge([
|
||||
'testFormulas' => true,
|
||||
'allowSpacesInHeading' => false,
|
||||
], $options);
|
||||
|
||||
if ($this->dataSetId == 0 || $this->dataSetId == '') {
|
||||
throw new InvalidArgumentException(__('Missing dataSetId'), 'dataSetId');
|
||||
}
|
||||
|
||||
if ($this->dataTypeId == 0 || $this->dataTypeId == '') {
|
||||
throw new InvalidArgumentException(__('Missing dataTypeId'), 'dataTypeId');
|
||||
}
|
||||
|
||||
if ($this->dataSetColumnTypeId == 0 || $this->dataSetColumnTypeId == '') {
|
||||
throw new InvalidArgumentException(__('Missing dataSetColumnTypeId'), 'dataSetColumnTypeId');
|
||||
}
|
||||
|
||||
if ($this->heading == '') {
|
||||
throw new InvalidArgumentException(__('Please provide a column heading.'), 'heading');
|
||||
}
|
||||
|
||||
// Column heading should not allow reserved/disallowed SQL words
|
||||
if (Str::contains($this->heading, Sql::DISALLOWED_KEYWORDS, true)) {
|
||||
throw new InvalidArgumentException(
|
||||
sprintf(
|
||||
__('Headings cannot contain reserved words, such as %s'),
|
||||
implode(', ', Sql::DISALLOWED_KEYWORDS),
|
||||
),
|
||||
'heading',
|
||||
);
|
||||
}
|
||||
|
||||
// We allow spaces here for backwards compatibility, but only on import and edit.
|
||||
$additionalCharacters = $options['allowSpacesInHeading'] ? ' ' : '';
|
||||
if (!v::stringType()->alnum($additionalCharacters)->validate($this->heading)
|
||||
|| strtolower($this->heading) == 'id'
|
||||
) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
__('Please provide an alternative column heading %s can not be used.'),
|
||||
$this->heading
|
||||
), 'heading');
|
||||
}
|
||||
|
||||
if ($this->dataSetColumnTypeId == 2 && $this->formula == '') {
|
||||
throw new InvalidArgumentException(__('Please enter a valid formula'), 'formula');
|
||||
}
|
||||
|
||||
// Make sure this column name is unique
|
||||
$columns = $this->dataSetColumnFactory->getByDataSetId($this->dataSetId);
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if ($column->heading == $this->heading
|
||||
&& ($this->dataSetColumnId == null || $column->dataSetColumnId != $this->dataSetColumnId)
|
||||
) {
|
||||
throw new InvalidArgumentException(
|
||||
__('A column already exists with this name, please choose another'),
|
||||
'heading',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check the actual values
|
||||
try {
|
||||
$this->dataTypeFactory->getById($this->dataTypeId);
|
||||
} catch (NotFoundException $e) {
|
||||
throw new InvalidArgumentException(__('Provided Data Type doesn\'t exist'), 'datatype');
|
||||
}
|
||||
|
||||
try {
|
||||
$dataSetColumnType = $this->dataSetColumnTypeFactory->getById($this->dataSetColumnTypeId);
|
||||
|
||||
// If we are a remote column, validate we have a field
|
||||
if (strtolower($dataSetColumnType->dataSetColumnType) === 'remote'
|
||||
&& ($this->remoteField === '' || $this->remoteField === null)) {
|
||||
throw new InvalidArgumentException(
|
||||
__('Remote field is required when the column type is set to Remote'),
|
||||
'remoteField',
|
||||
);
|
||||
}
|
||||
} catch (NotFoundException) {
|
||||
throw new InvalidArgumentException(
|
||||
__('Provided DataSet Column Type doesn\'t exist'),
|
||||
'dataSetColumnTypeId',
|
||||
);
|
||||
}
|
||||
|
||||
// Should we validate the list content?
|
||||
if ($this->dataSetColumnId != 0 && $this->listContent != '') {
|
||||
// Look up all DataSet data in this table to make sure that the existing data is covered by the list content
|
||||
$list = $this->listContentArray();
|
||||
|
||||
// Add an empty field
|
||||
$list[] = '';
|
||||
|
||||
// We can check this is valid by building up a NOT IN sql statement, if we get results we know it's not good
|
||||
$select = '';
|
||||
|
||||
$dbh = $this->getStore()->getConnection('isolated');
|
||||
|
||||
for ($i=0; $i < count($list); $i++) {
|
||||
if (!empty($list[$i])) {
|
||||
$list_val = $dbh->quote($list[$i]);
|
||||
$select .= $list_val . ',';
|
||||
}
|
||||
}
|
||||
|
||||
$select = rtrim($select, ',');
|
||||
|
||||
// $select has been quoted in the for loop
|
||||
// always test the original value of the column (we won't have changed the actualised table yet)
|
||||
$SQL = 'SELECT id FROM `dataset_' . $this->dataSetId . '` WHERE `' . $this->getOriginalValue('heading') . '` NOT IN (' . $select . ')';//phpcs:ignore
|
||||
|
||||
$sth = $dbh->prepare($SQL);
|
||||
$sth->execute();
|
||||
|
||||
if ($sth->fetch()) {
|
||||
throw new InvalidArgumentException(
|
||||
__('New list content value is invalid as it does not include values for existing data'),
|
||||
'listcontent'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if formula dataSetType is set and formula is not empty, try to execute the SQL to validate it - we're
|
||||
// ignoring client side formulas here.
|
||||
if ($options['testFormulas']
|
||||
&& $this->dataSetColumnTypeId == 2
|
||||
&& $this->formula != ''
|
||||
&& !str_starts_with($this->formula, '$')
|
||||
) {
|
||||
try {
|
||||
$count = 0;
|
||||
$formula = str_ireplace(
|
||||
Sql::DISALLOWED_KEYWORDS,
|
||||
'',
|
||||
htmlspecialchars_decode($this->formula, ENT_QUOTES),
|
||||
$count
|
||||
);
|
||||
|
||||
if ($count > 0) {
|
||||
throw new InvalidArgumentException(__('Formula contains disallowed keywords.'));
|
||||
}
|
||||
|
||||
$formula = str_replace('[DisplayId]', 0, $formula);
|
||||
$formula = str_replace('[DisplayGeoLocation]', "ST_GEOMFROMTEXT('POINT(51.504 -0.104)')", $formula);
|
||||
|
||||
$this->getStore()->select('
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT `id`, ' . $formula . ' AS `' . $this->heading . '`
|
||||
FROM `dataset_' . $this->dataSetId . '`
|
||||
) dataset
|
||||
', [], 'isolated');
|
||||
} catch (\Exception $e) {
|
||||
$this->getLog()->debug('Formula validation failed with following message ' . $e->getMessage());
|
||||
throw new InvalidArgumentException(__('Provided formula is invalid'), 'formula');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save
|
||||
* @param array $options
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function save($options = [])
|
||||
{
|
||||
$options = array_merge([
|
||||
'validate' => true,
|
||||
'rebuilding' => false,
|
||||
], $options);
|
||||
|
||||
if ($options['validate'] && !$options['rebuilding']) {
|
||||
$this->validate($options);
|
||||
}
|
||||
|
||||
if ($this->dataSetColumnId == 0) {
|
||||
$this->add();
|
||||
} else {
|
||||
$this->edit($options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*/
|
||||
public function delete(bool $isDeletingDataset = false): void
|
||||
{
|
||||
$this->getStore()->update('DELETE FROM `datasetcolumn` WHERE DataSetColumnID = :dataSetColumnId', ['dataSetColumnId' => $this->dataSetColumnId]);
|
||||
|
||||
// Delete column (unless remote, or dropping the whole dataset)
|
||||
if (!$isDeletingDataset && $this->dataSetColumnTypeId !== 2) {
|
||||
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` DROP `' . $this->heading . '`', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add
|
||||
*/
|
||||
private function add()
|
||||
{
|
||||
$this->dataSetColumnId = $this->getStore()->insert('
|
||||
INSERT INTO `datasetcolumn` (DataSetID, Heading, DataTypeID, ListContent, ColumnOrder, DataSetColumnTypeID, Formula, RemoteField, `showFilter`, `showSort`, `tooltip`, `isRequired`, `dateFormat`)
|
||||
VALUES (:dataSetId, :heading, :dataTypeId, :listContent, :columnOrder, :dataSetColumnTypeId, :formula, :remoteField, :showFilter, :showSort, :tooltip, :isRequired, :dateFormat)
|
||||
', [
|
||||
'dataSetId' => $this->dataSetId,
|
||||
'heading' => $this->heading,
|
||||
'dataTypeId' => $this->dataTypeId,
|
||||
'listContent' => $this->listContent,
|
||||
'columnOrder' => $this->columnOrder,
|
||||
'dataSetColumnTypeId' => $this->dataSetColumnTypeId,
|
||||
'formula' => $this->formula,
|
||||
'remoteField' => $this->remoteField,
|
||||
'showFilter' => $this->showFilter,
|
||||
'showSort' => $this->showSort,
|
||||
'tooltip' => $this->tooltip,
|
||||
'isRequired' => $this->isRequired,
|
||||
'dateFormat' => $this->dateFormat
|
||||
]);
|
||||
|
||||
// Add Column to Underlying Table
|
||||
if (($this->dataSetColumnTypeId == 1) || ($this->dataSetColumnTypeId == 3)) {
|
||||
// Use a separate connection for DDL (it operates outside transactions)
|
||||
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` ADD `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL', [], 'isolated', false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit
|
||||
* @param array $options
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function edit($options)
|
||||
{
|
||||
$params = [
|
||||
'dataSetId' => $this->dataSetId,
|
||||
'heading' => $this->heading,
|
||||
'dataTypeId' => $this->dataTypeId,
|
||||
'listContent' => $this->listContent,
|
||||
'columnOrder' => $this->columnOrder,
|
||||
'dataSetColumnTypeId' => $this->dataSetColumnTypeId,
|
||||
'formula' => $this->formula,
|
||||
'dataSetColumnId' => $this->dataSetColumnId,
|
||||
'remoteField' => $this->remoteField,
|
||||
'showFilter' => $this->showFilter,
|
||||
'showSort' => $this->showSort,
|
||||
'tooltip' => $this->tooltip,
|
||||
'isRequired' => $this->isRequired,
|
||||
'dateFormat' => $this->dateFormat
|
||||
];
|
||||
|
||||
$sql = '
|
||||
UPDATE `datasetcolumn` SET
|
||||
dataSetId = :dataSetId,
|
||||
Heading = :heading,
|
||||
ListContent = :listContent,
|
||||
ColumnOrder = :columnOrder,
|
||||
DataTypeID = :dataTypeId,
|
||||
DataSetColumnTypeID = :dataSetColumnTypeId,
|
||||
Formula = :formula,
|
||||
RemoteField = :remoteField,
|
||||
`showFilter` = :showFilter,
|
||||
`showSort` = :showSort,
|
||||
`tooltip` = :tooltip,
|
||||
`isRequired` = :isRequired,
|
||||
`dateFormat` = :dateFormat
|
||||
WHERE dataSetColumnId = :dataSetColumnId
|
||||
';
|
||||
|
||||
$this->getStore()->update($sql, $params);
|
||||
|
||||
try {
|
||||
if ($options['rebuilding'] && ($this->dataSetColumnTypeId == 1 || $this->dataSetColumnTypeId == 3)) {
|
||||
$this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` ADD `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL', [], 'isolated', false, false);
|
||||
|
||||
} else if (($this->dataSetColumnTypeId == 1 || $this->dataSetColumnTypeId == 3)
|
||||
&& ($this->hasPropertyChanged('heading') || $this->hasPropertyChanged('dataTypeId'))) {
|
||||
$sql = 'ALTER TABLE `dataset_' . $this->dataSetId . '` CHANGE `' . $this->getOriginalValue('heading') . '` `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL DEFAULT NULL';
|
||||
$this->getStore()->update($sql, [], 'isolated', false, false);
|
||||
}
|
||||
} catch (\PDOException $PDOException) {
|
||||
$this->getLog()->error('Unable to change DataSetColumn because ' . $PDOException->getMessage());
|
||||
throw new InvalidArgumentException(__('Existing data is incompatible with your new configuration'), 'dataSetData');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL Data Type for this Column Definition
|
||||
* @return string
|
||||
*/
|
||||
private function sqlDataType()
|
||||
{
|
||||
$dataType = null;
|
||||
|
||||
switch ($this->dataTypeId) {
|
||||
|
||||
case 2:
|
||||
$dataType = 'DOUBLE';
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$dataType = 'DATETIME';
|
||||
break;
|
||||
|
||||
case 5:
|
||||
$dataType = 'INT';
|
||||
break;
|
||||
|
||||
case 1:
|
||||
case 6:
|
||||
$dataType = 'TEXT';
|
||||
break;
|
||||
|
||||
case 4:
|
||||
default:
|
||||
$dataType = 'VARCHAR(1000)';
|
||||
}
|
||||
|
||||
return $dataType;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user