. */ namespace Xibo\Factory; use Illuminate\Support\Str; use Slim\Views\Twig; use Stash\Interfaces\PoolInterface; use Xibo\Entity\Module; use Xibo\Entity\ModuleTemplate; use Xibo\Entity\Widget; use Xibo\Service\ConfigServiceInterface; use Xibo\Support\Exception\NotFoundException; use Xibo\Widget\DataType\DataTypeInterface; use Xibo\Widget\Definition\Asset; use Xibo\Widget\Definition\DataType; use Xibo\Widget\Provider\DataProvider; use Xibo\Widget\Provider\DataProviderInterface; use Xibo\Widget\Provider\DurationProvider; use Xibo\Widget\Provider\DurationProviderInterface; use Xibo\Widget\Provider\WidgetProviderInterface; use Xibo\Widget\Render\WidgetDataProviderCache; use Xibo\Widget\Render\WidgetHtmlRenderer; /** * Class ModuleFactory * @package Xibo\Factory */ class ModuleFactory extends BaseFactory { use ModuleXmlTrait; public static $systemDataTypes = [ 'Article', 'Event', 'Forecast', 'Product', 'ProductCategory', 'SocialMedia', 'dataset' ]; /** @var Module[] all modules */ private $modules = null; /** @var \Xibo\Widget\Definition\DataType[] */ private $dataTypes = null; /** @var \Stash\Interfaces\PoolInterface */ private $pool; /** @var string */ private $cachePath; /** @var \Slim\Views\Twig */ private $twig; /** @var \Xibo\Service\ConfigServiceInterface */ private $config; /** * Construct a factory * @param string $cachePath * @param PoolInterface $pool * @param \Slim\Views\Twig $twig * @param \Xibo\Service\ConfigServiceInterface $config */ public function __construct(string $cachePath, PoolInterface $pool, Twig $twig, ConfigServiceInterface $config) { $this->cachePath = $cachePath; $this->pool = $pool; $this->twig = $twig; $this->config = $config; } /** * @param \Xibo\Entity\Module $module * @param \Xibo\Entity\Widget $widget * @return \Xibo\Widget\Provider\DataProviderInterface */ public function createDataProvider(Module $module, Widget $widget): DataProviderInterface { return new DataProvider( $module, $widget, $this->config->getGuzzleProxy(), $this->getSanitizerService(), $this->pool, ); } /** * @param Module $module * @param Widget $widget * @return DurationProviderInterface */ public function createDurationProvider(Module $module, Widget $widget): DurationProviderInterface { return new DurationProvider($module, $widget); } /** * Create a widget renderer * @return \Xibo\Widget\Render\WidgetHtmlRenderer */ public function createWidgetHtmlRenderer(): WidgetHtmlRenderer { return (new WidgetHtmlRenderer($this->cachePath, $this->twig, $this->config, $this)) ->useLogger($this->getLog()->getLoggerInterface()); } /** * Create a widget data provider cache */ public function createWidgetDataProviderCache(): WidgetDataProviderCache { return (new WidgetDataProviderCache($this->pool)) ->useLogger($this->getLog()->getLoggerInterface()); } /** * Determine the cache key * @param \Xibo\Entity\Module $module * @param \Xibo\Entity\Widget $widget * @param int $displayId the displayId (0 for preview) * @param \Xibo\Widget\Provider\DataProviderInterface $dataProvider * @param \Xibo\Widget\Provider\WidgetProviderInterface|null $widgetInterface * @return string */ public function determineCacheKey( Module $module, Widget $widget, int $displayId, DataProviderInterface $dataProvider, ?WidgetProviderInterface $widgetInterface ): string { // Determine the cache key $cacheKey = $widgetInterface?->getDataCacheKey($dataProvider); if ($cacheKey === null) { // Determinthe cache key from the setting in XML. if (empty($module->dataCacheKey)) { // Best we can do here is a cache per widget, but we should log this as an error. $this->getLog()->debug('determineCacheKey: module without dataCacheKey: ' . $module->moduleId); $cacheKey = $widget->widgetId; } else { // Start with the one provided $this->getLog()->debug('determineCacheKey: module dataCacheKey: ' . $module->dataCacheKey); $cacheKey = $module->dataCacheKey; // Properties $module->decorateProperties($widget, true); $properties = $module->getPropertyValues(false); // Is display location in use? // We should see if the display location property is set (this is a special property), and if it is // update the lat/lng with the details stored on the display $latitude = $properties['latitude'] ?? ''; $longitude = $properties['longitude'] ?? ''; if ($dataProvider->getProperty('useDisplayLocation') == 1) { $latitude = $dataProvider->getDisplayLatitude() ?: $latitude; $longitude = $dataProvider->getDisplayLongitude() ?: $longitude; } // Parse the cache key for variables. $matches = []; preg_match_all('/%(.*?)%/', $cacheKey, $matches); foreach ($matches[1] as $match) { if ($match === 'displayId') { $cacheKey = str_replace('%displayId%', $displayId, $cacheKey); } else if ($match === 'widgetId') { $cacheKey = str_replace('%widgetId%', $widget->widgetId, $cacheKey); } else if ($match === 'latitude') { $cacheKey = str_replace('%latitude%', $latitude, $cacheKey); } else if ($match === 'longitude') { $cacheKey = str_replace('%longitude%', $longitude, $cacheKey); } else { $this->getLog()->debug($match); $cacheKey = str_replace( '%' . $match . '%', $properties[$match] ?? '', $cacheKey ); } } } // Include a separate cache per fallback data? if ($module->fallbackData == 1) { $cacheKey .= '_fb ' . $widget->getOptionValue('showFallback', 'never'); } } $this->getLog()->debug('determineCacheKey: cache key is : ' . $cacheKey); return $cacheKey; } /** * @param string $dataType * @return void */ public function clearCacheForDataType(string $dataType): void { $this->getLog()->debug('clearCacheForDataType: /widget/' . $dataType); $this->pool->deleteItem('/widget/' . $dataType); } /** * @return \Xibo\Entity\Module[] */ public function getKeyedArrayOfModules(): array { $this->getLog()->debug('ModuleFactory: getKeyedArrayOfModules'); $modules = []; foreach ($this->load() as $module) { $modules[$module->type] = $module; } return $modules; } /** * @return Module[] */ public function getAssignableModules(): array { $this->getLog()->debug('ModuleFactory: getAssignableModules'); $modules = []; foreach ($this->load() as $module) { if ($module->enabled === 1 && $module->assignable === 1) { $modules[] = $module; } } return $modules; } /** * @return Module[] */ public function getLibraryModules(): array { $this->getLog()->debug('ModuleFactory: getLibraryModules'); $modules = []; foreach ($this->load() as $module) { if ($module->enabled == 1 && $module->regionSpecific === 0) { $modules[] = $module; } } return $modules; } /** * Get module by Id * @param string $moduleId * @return Module * @throws NotFoundException */ public function getById($moduleId): Module { $this->getLog()->debug('ModuleFactory: getById'); foreach ($this->load() as $module) { if ($module->moduleId === $moduleId) { return $module; } } throw new NotFoundException(); } /** * Get an array of all modules * @return Module[] */ public function getAll(): array { $this->getLog()->debug('ModuleFactory: getAll'); return $this->load(); } /** * Get an array of all modules except canvas * @param array $filter * @return Module[] */ public function getAllExceptCanvas(array $filter = []): array { $sanitizedFilter = $this->getSanitizer($filter); $this->getLog()->debug('ModuleFactory: getAllButCanvas'); $modules = []; foreach ($this->load() as $module) { // Hide the canvas module from the module list if ($module->moduleId != 'core-canvas') { // do we have a name filter? if (!empty($sanitizedFilter->getString('name'))) { if (str_contains(strtolower($module->name), strtolower($sanitizedFilter->getString('name')))) { $modules[] = $module; } } else { $modules[] = $module; } } } return $modules; } /** * Get an array of all enabled modules * @return Module[] */ public function getEnabled(): array { $this->getLog()->debug('ModuleFactory: getEnabled'); $modules = []; foreach ($this->load() as $module) { if ($module->enabled == 1) { $modules[] = $module; } } return $modules; } /** * Get module by Type * this should return the first module enabled by the type specified. * @param string $type * @param array $conditions Conditions that are created based on the widget's option and value, e.g, templateId==worldclock1 * @return Module * @throws \Xibo\Support\Exception\NotFoundException */ public function getByType(string $type, array $conditions = []): Module { $this->getLog()->debug('ModuleFactory: getByType ' . $type); $modules = $this->load(); usort($modules, function ($a, $b) { /** @var Module $a */ /** @var Module $b */ return $a->enabled - $b->enabled; }); foreach ($modules as $module) { if ($module->type === $type) { return $module; } } // Match on legacy type foreach ($modules as $module) { // get the name of the legacytypes $legacyTypes = []; $legacyConditions = []; if (count($module->legacyTypes) > 0) { $legacyTypes = array_column($module->legacyTypes, 'name'); $legacyConditions = array_column($module->legacyTypes, 'condition'); } if (in_array($type, $legacyTypes)) { foreach ($conditions as $value) { if (in_array($value, $legacyConditions)) { return $module; } } return $module; } } throw new NotFoundException(); } /** * Get module by extension * @param string $extension * @return Module * @throws NotFoundException */ public function getByExtension(string $extension): Module { $this->getLog()->debug('ModuleFactory: getByExtension'); foreach ($this->load() as $module) { $validExtensions = $module->getSetting('validExtensions'); if (!empty($validExtensions) && Str::contains($validExtensions, $extension)) { return $module; } } throw new NotFoundException(sprintf(__('Extension %s does not match any enabled Module'), $extension)); } /** * Get Valid Extensions * @param array $filterBy * @return string[] */ public function getValidExtensions($filterBy = []): array { $this->getLog()->debug('ModuleFactory: getValidExtensions'); $filterBy = $this->getSanitizer($filterBy); // Do we allow media type changes? $isAllowMediaTypeChange = $filterBy->getCheckbox('allowMediaTypeChange'); if ($isAllowMediaTypeChange) { // Restrict to any file based media type (i.e. any valid extension) $typeFilter = null; } else { // Restrict to type $typeFilter = $filterBy->getString('type'); } $extensions = []; foreach ($this->load() as $module) { if ($typeFilter !== null && $module->type !== $typeFilter) { continue; } if (!empty($module->getSetting('validExtensions'))) { foreach (explode(',', $module->getSetting('validExtensions')) as $extension) { $extensions[] = $extension; } } } return $extensions; } /** * @param string $dataTypeId * @return \Xibo\Widget\Definition\DataType * @throws \Xibo\Support\Exception\NotFoundException */ public function getDataTypeById(string $dataTypeId): DataType { // Rely on a class if we have one. $className = ucfirst(str_replace('-', '', ucwords($dataTypeId, '-'))); $className = '\\Xibo\\Widget\\DataType\\' . $className; if (class_exists($className)) { $class = new $className(); if ($class instanceof DataTypeInterface) { return ($class->getDefinition()); } } // Otherwise look in our XML definitions foreach ($this->loadDataTypes() as $dataType) { if ($dataType->id === $dataTypeId) { return $dataType; } } throw new NotFoundException(__('DataType not found')); } /** * @return DataType[] */ public function getAllDataTypes() { $dataTypes = []; // get system data types foreach (self::$systemDataTypes as $dataTypeId) { $className = '\\Xibo\\Widget\\DataType\\' . ucfirst($dataTypeId); if (class_exists($className)) { $class = new $className(); if ($class instanceof DataTypeInterface) { $dataTypes[] = $class->getDefinition(); } } // special handling for dataset if ($dataTypeId === 'dataset') { $dataType = new DataType(); $dataType->id = $dataTypeId; $dataType->name = 'DataSet'; $dataTypes[] = $dataType; } } // get data types from xml $files = array_merge( glob(PROJECT_ROOT . '/modules/datatypes/*.xml'), glob(PROJECT_ROOT . '/custom/modules/datatypes/*.xml') ); foreach ($files as $file) { $xml = new \DOMDocument(); $xml->load($file); $dataType = new DataType(); $dataType->id = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id'); $dataType->name = $this->getFirstValueOrDefaultFromXmlNode($xml, 'name'); $dataTypes[] = $dataType; } sort($dataTypes); return $dataTypes; } /** * @param string $assetId * @return \Xibo\Widget\Definition\Asset * @throws \Xibo\Support\Exception\NotFoundException */ public function getAssetById(string $assetId): Asset { $this->getLog()->debug('getAssetById: ' . $assetId); foreach ($this->getEnabled() as $module) { foreach ($module->getAssets() as $asset) { if ($asset->id === $assetId) { return $asset; } } } throw new NotFoundException(__('Asset not found')); } /** * @param string $alias * @return \Xibo\Widget\Definition\Asset * @throws \Xibo\Support\Exception\NotFoundException */ public function getAssetByAlias(string $alias): Asset { $this->getLog()->debug('getAssetByAlias: ' . $alias); foreach ($this->getEnabled() as $module) { foreach ($module->getAssets() as $asset) { if ($asset->alias === $alias) { return $asset; } } } throw new NotFoundException(__('Asset not found')); } /** * @param ModuleTemplate[] $templates * @return Asset[] */ public function getAssetsFromTemplates(array $templates): array { $assets = []; foreach ($this->getEnabled() as $module) { foreach ($module->getAssets() as $asset) { $assets[$asset->id] = $asset; } foreach ($templates as $template) { foreach ($template->getAssets() as $asset) { $assets[$asset->id] = $asset; } } } return $assets; } /** * Get all assets * @return Asset[] */ public function getAllAssets(): array { $assets = []; foreach ($this->getEnabled() as $module) { foreach ($module->getAssets() as $asset) { $assets[$asset->id] = $asset; } } return $assets; } /** * Get an asset from anywhere by its ID * @param string $assetId * @param ModuleTemplateFactory $moduleTemplateFactory * @param bool $isAlias * @return Asset * @throws NotFoundException */ public function getAssetsFromAnywhereById( string $assetId, ModuleTemplateFactory $moduleTemplateFactory, bool $isAlias = false, ): Asset { $asset = null; try { $asset = $isAlias ? $this->getAssetByAlias($assetId) : $this->getAssetById($assetId); } catch (NotFoundException) { // Not a module asset. } // Try a template instead try { $asset = $isAlias ? $moduleTemplateFactory->getAssetByAlias($assetId) : $moduleTemplateFactory->getAssetById($assetId); } catch (NotFoundException) { // Not a module template asset. } if ($asset !== null) { return $asset; } else { throw new NotFoundException(__('Asset not found')); } } /** * Load all modules into an array for use throughout this request * @return \Xibo\Entity\Module[] */ private function load(): array { if ($this->modules === null) { // TODO: these are the only fields we require in the settings table $sql = ' SELECT `moduleId`, `enabled`, `previewEnabled`, `defaultDuration`, `settings` FROM `module` '; $modulesWithSettings = []; foreach ($this->getStore()->select($sql, []) as $row) { // Make a keyed array of these settings $modulesWithSettings[$row['moduleId']] = $this->getSanitizer($row); } // Load in our file system modules. // we consider modules in the module folder, and also custom modules $files = array_merge( glob(PROJECT_ROOT . '/modules/*.xml'), glob(PROJECT_ROOT . '/custom/modules/*.xml') ); foreach ($files as $file) { // Create our module entity from this file try { $module = $this->createFromXml($file, $modulesWithSettings); // Create a widget provider if necessary // Take our module and see if it has a class associated with it if (!empty($module->class)) { // We create a module specific provider if (!class_exists($module->class)) { $module->errors[] = 'Module class not found: ' . $module->class; } else { $class = $module->class; $module->setWidgetProvider(new $class()); } } // Create a widget compatibility if necessary if (!empty($module->compatibilityClass)) { // We create a module specific provider if (!class_exists($module->compatibilityClass)) { $module->errors[] = 'Module compatibilityClass not found: ' . $module->compatibilityClass; } else { $compatibilityClass = $module->compatibilityClass; $module->setWidgetCompatibility(new $compatibilityClass()); } } // Create a widget validator if necessary foreach ($module->validatorClass as $validatorClass) { // We create a module specific provider if (!class_exists($validatorClass)) { $module->errors[] = 'Module validatorClass not found: ' . $validatorClass; } else { $module->addWidgetValidator( (new $validatorClass()) ->setLog($this->getLog()->getLoggerInterface()) ); } } // Set error state $module->isError = $module->errors !== null && count($module->errors) > 0; // Register $this->modules[] = $module; } catch (\Exception $exception) { $this->getLog()->error('Unable to create module from ' . basename($file) . ', skipping. e = ' . $exception->getMessage()); } } } return $this->modules; } /** * Load all data types into an array for use throughout this request * @return \Xibo\Widget\Definition\DataType[] */ private function loadDataTypes(): array { if ($this->dataTypes === null) { $files = array_merge( glob(PROJECT_ROOT . '/modules/datatypes/*.xml'), glob(PROJECT_ROOT . '/custom/modules/datatypes/*.xml') ); foreach ($files as $file) { $this->dataTypes[] = $this->createDataTypeFromXml($file); } } return $this->dataTypes ?? []; } /** * Create a module from its XML definition * @param string $file the path to the module definition * @param array $modulesWithSettings * @return \Xibo\Entity\Module */ private function createFromXml(string $file, array $modulesWithSettings): Module { // TODO: cache this into Stash $xml = new \DOMDocument(); $xml->load($file); $module = new Module($this->getStore(), $this->getLog(), $this->getDispatcher(), $this); $module->moduleId = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id'); $module->name = __($this->getFirstValueOrDefaultFromXmlNode($xml, 'name')); $module->author = $this->getFirstValueOrDefaultFromXmlNode($xml, 'author'); $module->description = __($this->getFirstValueOrDefaultFromXmlNode($xml, 'description')); $module->icon = $this->getFirstValueOrDefaultFromXmlNode($xml, 'icon'); $module->class = $this->getFirstValueOrDefaultFromXmlNode($xml, 'class'); $module->type = $this->getFirstValueOrDefaultFromXmlNode($xml, 'type'); $module->thumbnail = $this->getFirstValueOrDefaultFromXmlNode($xml, 'thumbnail'); $module->startWidth = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'startWidth')); $module->startHeight = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'startHeight')); $module->dataType = $this->getFirstValueOrDefaultFromXmlNode($xml, 'dataType'); $module->dataCacheKey = $this->getFirstValueOrDefaultFromXmlNode($xml, 'dataCacheKey'); $module->fallbackData = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'fallbackData', 0)); $module->schemaVersion = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'schemaVersion')); $module->compatibilityClass = $this->getFirstValueOrDefaultFromXmlNode($xml, 'compatibilityClass'); $module->showIn = $this->getFirstValueOrDefaultFromXmlNode($xml, 'showIn') ?? 'both'; $module->assignable = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'assignable')); $module->regionSpecific = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'regionSpecific')); $module->renderAs = $this->getFirstValueOrDefaultFromXmlNode($xml, 'renderAs'); $module->defaultDuration = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'defaultDuration')); $module->hasThumbnail = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'hasThumbnail', 0)); $module->allowPreview = intval($this->getFirstValueOrDefaultFromXmlNode($xml, 'allowPreview', 1)); // Validator classes foreach ($xml->getElementsByTagName('validatorClass') as $node) { /** @var \DOMNode $node */ if ($node instanceof \DOMElement) { $module->validatorClass[] = trim($node->textContent); } } // Event listeners $module->onInitialize = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onInitialize'); if (!empty($module->onInitialize)) { $module->onInitialize = trim($module->onInitialize); } $module->onParseData = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onParseData'); if (!empty($module->onParseData)) { $module->onParseData = trim($module->onParseData); } $module->onDataLoad = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onDataLoad'); if (!empty($module->onDataLoad)) { $module->onDataLoad = trim($module->onDataLoad); } $module->onRender = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onRender'); if (!empty($module->onRender)) { $module->onRender = trim($module->onRender); } $module->onVisible = $this->getFirstValueOrDefaultFromXmlNode($xml, 'onVisible'); if (!empty($module->onVisible)) { $module->onVisible = trim($module->onVisible); } // We might have sample data (usually only if there is a dataType) $sampleData = $this->getFirstValueOrDefaultFromXmlNode($xml, 'sampleData'); if (!empty($sampleData)) { $module->sampleData = json_decode(trim($sampleData), true); } // Legacy types. try { $module->legacyTypes = $this->parseLegacyTypes($xml->getElementsByTagName('legacyType')); } catch (\Exception $e) { $module->errors[] = __('Invalid legacyType'); $this->getLog()->error('Module ' . $module->moduleId . ' has invalid legacyType. e: ' . $e->getMessage()); } // Group for non datatype modules $module->group = []; $groupNodes = $xml->getElementsByTagName('group'); foreach ($groupNodes as $groupNode) { if ($groupNode instanceof \DOMElement) { $module->group['id'] = $groupNode->getAttribute('id'); $module->group['icon'] = $groupNode->getAttribute('icon'); $module->group['name'] = $groupNode->textContent; } } // Parse assets try { $module->assets = $this->parseAssets($xml->getElementsByTagName('assets')); } catch (\Exception $e) { $module->errors[] = __('Invalid assets'); $this->getLog()->error('Module ' . $module->moduleId . ' has invalid assets. e: ' . $e->getMessage()); } // Default values for remaining expected properties $module->isInstalled = false; $module->isError = false; $module->errors = []; $module->enabled = 0; $module->previewEnabled = 0; // Parse settings/property definitions. try { $module->settings = $this->parseProperties($xml->getElementsByTagName('settings')); } catch (\Exception $e) { $module->errors[] = __('Invalid settings'); $this->getLog()->error('Module ' . $module->moduleId . ' has invalid settings. e: ' . $e->getMessage()); } // Add in any settings we already have if (array_key_exists($module->moduleId, $modulesWithSettings)) { $moduleSettings = $modulesWithSettings[$module->moduleId]; $module->isInstalled = true; // make sure canvas is always enabled if ($module->moduleId === 'core-canvas') { $module->enabled = 1; // update the table if ($moduleSettings->getInt('enabled', ['default' => 0]) === 0) { $this->getStore()->update( 'UPDATE `module` SET enabled = 1 WHERE `module`.moduleId = \'core-canvas\' ', [] ); } } else { $module->enabled = $moduleSettings->getInt('enabled', ['default' => 0]); } $module->previewEnabled = $moduleSettings->getInt('previewEnabled', ['default' => 0]); $module->defaultDuration = $moduleSettings->getInt('defaultDuration', ['default' => 10]); $settings = $moduleSettings->getString('settings'); if ($settings !== null) { $settings = json_decode($settings, true); foreach ($module->settings as $property) { foreach ($settings as $settingId => $setting) { if ($settingId === $property->id) { $property->value = $setting; break; } } } } } try { $module->properties = $this->parseProperties($xml->getElementsByTagName('properties'), $module); } catch (\Exception $e) { $module->errors[] = __('Invalid properties'); $this->getLog()->error('Module ' . $module->moduleId . ' has invalid properties. e: ' . $e->getMessage()); } // Parse group property definitions. try { $module->propertyGroups = $this->parsePropertyGroups($xml->getElementsByTagName('propertyGroups')); } catch (\Exception $e) { $module->errors[] = __('Invalid property groups'); $this->getLog()->error('Module ' . $module->moduleId . ' has invalid property groups. e: ' . $e->getMessage()); } // Parse required elements. $requiredElements = $this->getFirstValueOrDefaultFromXmlNode($xml, 'requiredElements'); if (!empty($requiredElements)) { $module->requiredElements = explode(',', $requiredElements); } // Parse stencils try { $module->preview = $this->getStencils($xml->getElementsByTagName('preview'))[0] ?? null; $module->stencil = $this->getStencils($xml->getElementsByTagName('stencil'))[0] ?? null; } catch (\Exception $e) { $module->errors[] = __('Invalid stencils'); $this->getLog()->error('Module ' . $module->moduleId . ' has invalid stencils. e: ' . $e->getMessage()); } return $module; } /** * Create DataType from XML * @param string $file * @return \Xibo\Widget\Definition\DataType */ private function createDataTypeFromXml(string $file): DataType { $xml = new \DOMDocument(); $xml->load($file); $dataType = new DataType(); $dataType->id = $this->getFirstValueOrDefaultFromXmlNode($xml, 'id'); $dataType->name = $this->getFirstValueOrDefaultFromXmlNode($xml, 'name'); // Fields. foreach ($xml->getElementsByTagName('field') as $field) { if ($field instanceof \DOMElement) { $dataType->addField( $field->getAttribute('id'), trim($field->textContent), $field->getAttribute('type'), $field->getAttribute('isRequired') === 'true', ); } } return $dataType; } }