['no-border', 'dictionary-item']]; /** @var array Dictionary Item Fields */ private $fields; /** @var ?FormElement Remove button */ private ?FormElement $removeButton = null; public function __construct(string $name, array $items, $attributes = null) { $this->fields = $items; parent::__construct($name, $attributes); } private static function fetchItemType(UuidInterface $uuid): string { $db = Db::fromResourceName(Config::module('director')->get('db', 'resource'))->getDbAdapter(); $query = $db->select() ->from( ['dp' => 'director_property'], ['value_type' => 'dp.value_type'] ) ->where('dp.parent_uuid = ?', Db\DbUtil::quoteBinaryCompat($uuid->getBytes(), $db)); return $db->fetchOne($query); } /** * Fetch datalist entries for a given property uuid. * * @param UuidInterface $uuid * * @return array */ private static function fetchDataListEntries(UuidInterface $uuid): array { $db = Db::fromResourceName(Config::module('director')->get('db', 'resource'))->getDbAdapter(); $query = $db->select() ->from( ['dle' => 'director_datalist_entry'], ['entry_name' => 'dle.entry_name', 'entry_value' => 'dle.entry_value'] ) ->join(['dl' => 'director_datalist'], 'dl.id = dle.list_id', []) ->join(['dpl' => 'director_property_datalist'], 'dl.uuid = dpl.list_uuid', []) ->where('dpl.property_uuid = ?', Db\DbUtil::quoteBinaryCompat($uuid->getBytes(), $db)); return $db->fetchPairs($query); } protected function assemble(): void { if (empty($this->fields)) { return; } $this->addElement('hidden', 'name', ['value' => $this->fields['key_name'] ?? '']); $this->addElement('hidden', 'type', ['value' => $this->fields['value_type'] ?? '']); $this->addElement('hidden', 'label', ['value' => $this->fields['label'] ?? '']); $this->addElement('hidden', 'parent_type', ['value' => $this->fields['parent_type'] ?? '']); $this->addElement('hidden', 'inherited'); $this->addElement('hidden', 'inherited_from'); $valElementName = 'var'; $type = $this->getElement('type')->getValue(); $label = $this->getElement('label')->getValue(); if ($this->removeButton !== null) { $this->addAttributes(['class' => ['removable']]); $this->addHtml(new HtmlElement( 'div', null, $this->removeButton )); } if ($label === null) { $label = $this->getElement('name')->getValue(); } $uuid = Uuid::fromBytes($this->fields['uuid']); $children = static::fetchChildrenItems( $uuid, $this->fields['value_type'] ?? '' ); $inherited = $this->getElement('inherited')->getValue(); $inheritedFrom = $this->getElement('inherited_from')->getValue(); $placeholder = ''; if ($inherited) { $placeholder = $inherited . ' (' . sprintf($this->translate('Inherited from %s'), $inheritedFrom) . ')'; } if ($type === 'number') { $this->addElement( 'number', $valElementName, [ 'label' => $label . ' (Number)', 'placeholder' => $placeholder, 'step' => 'any' ] ); } elseif ($type == 'bool') { $this->addElement( new IplBoolean( $valElementName, ['label' => $label, 'placeholder' => $placeholder] ) ); } elseif ($type === 'dynamic-array') { $this->addElement((new ArrayElement($valElementName)) ->shouldAutoSubmit() ->setVerticalTermDirection() ->setPlaceHolder($placeholder) ->setLabel($label . ' (Array)')); } elseif (str_starts_with($type, 'datalist-')) { $isStrict = substr($type, strlen('datalist-')) === 'strict'; $itemType = self::fetchItemType($uuid); $datalistEntries = self::fetchDataListEntries($uuid); if ($itemType === 'string') { if ($isStrict) { $this->addElement( 'select', $valElementName, [ 'label' => $label . ' (Datalist String [strict])', 'placeholder' => $placeholder, 'value' => '', 'options' => ['' => $this->translate('- Please choose -')] + $datalistEntries ] ); } else { $fieldsetName = $this->getName(); $listEntriesInput = $this->createElement('text', $valElementName, [ 'autocomplete' => 'off', 'ignore' => true, 'label' => $label . ' (Datalist String [non-strict])', 'data-enrichment-type' => 'completion', 'data-auto-submit' => true, 'data-term-suggestions' => "#{$valElementName}-suggestions-{$fieldsetName}", 'data-suggest-url' => Url::fromPath('director/suggestions/datalist-entry', [ 'uuid' => Uuid::fromBytes($this->fields['uuid'])->toString(), 'showCompact' => true, '_disableLayout' => true ]) ]); $fieldset = new HtmlElement('fieldset'); $this->registerElement($listEntriesInput); $searchInput = $this->createElement('hidden', "{$valElementName}-search", ['ignore' => true]); $this->registerElement($searchInput); $fieldset->addHtml($searchInput); $labelInput = $this->createElement('hidden', "{$valElementName}-label", ['ignore' => true]); $this->registerElement($labelInput); $fieldset->addHtml($labelInput); $this->decorate($listEntriesInput); $fieldset->addHtml( $listEntriesInput, new HtmlElement('div', Attributes::create([ 'id' => "{$valElementName}-suggestions-{$fieldsetName}", 'class' => 'search-suggestions' ])) ); $this->addHtml($fieldset); } } elseif ($itemType === 'dynamic-array') { $listEntriesInput = (new ArrayElement($valElementName)) ->shouldAutoSubmit() ->setSuggestedValues($datalistEntries) ->setVerticalTermDirection() ->setSuggestionUrl(Url::fromPath('director/suggestions/datalist-entry', [ 'uuid' => Uuid::fromBytes($this->fields['uuid'])->toString(), 'showCompact' => true, '_disableLayout' => true ])); if ($isStrict) { $termValidator = function (array $terms) use ($datalistEntries) { (new DatalistEntryValidator()) ->setDatalistEntries($datalistEntries) ->isValid($terms); }; $listEntriesInput ->setLabel($label . ' (Datalist Array [strict])') ->on(TermInput::ON_ENRICH, $termValidator) ->on(TermInput::ON_ADD, $termValidator) ->on(TermInput::ON_PASTE, $termValidator) ->on(TermInput::ON_SAVE, $termValidator); } else { $listEntriesInput->setLabel($label . ' (Datalist Array [non-strict])'); } $this->addElement($listEntriesInput); } } elseif ($type === 'fixed-dictionary' || $type === 'fixed-array') { $this->addElement( (new Dictionary($valElementName, $children)) ->setLabel($label . ' (' . ucfirst(substr($type, strlen('fixed-'))) . ')') ); } elseif ($type === 'dynamic-dictionary') { $this->addElement((new NestedDictionary( $valElementName, $children, ['inherited_from' => $inheritedFrom, 'value' => $inherited] ))->setLabel($label . ' (Dictionary)')); } else { $this->addElement( 'text', $valElementName, [ 'label' => $label . ' (' . ucfirst($type) . ')', 'placeholder' => $placeholder ] ); } } public function populate($values): void { if (empty($values)) { return; } if ( $values['type'] === 'datalist-non-strict' && self::fetchItemType(Uuid::fromBytes($this->fields['uuid'])) === 'string' ) { $datalistEntries = array_flip(self::fetchDataListEntries(Uuid::fromBytes($this->fields['uuid']))); if (isset($datalistEntries[$values['var']])) { $values['var-search'] = $datalistEntries[$values['var']]; $values['var-label'] = $values['var']; } else { $values['var-search'] = $values['var']; } } parent::populate($values); } /** * Prepare the dictionary item for display * * @param array $property * * @return array */ public static function prepare(array $property): array { $values = [ 'name' => $property['key_name'] ?? '', 'label' => $property['label'] ?? '', 'type' => $property['value_type'] ?? '', 'parent_type' => $property['parent_type'] ?? '' ]; $property['uuid'] = Dbutil::binaryResult($property['uuid'] ?? ''); if ( $property['value_type'] === 'dynamic-array' || ( in_array($property['value_type'], ['datalist-strict', 'datalist-non-strict'], true) && self::fetchItemType(Uuid::fromBytes($property['uuid'])) === 'dynamic-array' ) ) { $values['var'] = $property['value'] ?? []; $values['inherited'] = implode(', ', $property['inherited'] ?? []); $values['inherited_from'] = $property['inherited_from'] ?? ''; } elseif ($property['value_type'] === 'fixed-dictionary' || $property['value_type'] === 'fixed-array') { $childrenValues = ['value' => $property['value'] ?? []]; if (! isset($property['value'])) { $childrenValues['inherited'] = $property['inherited'] ?? []; $childrenValues['inherited_from'] = $property['inherited_from'] ?? ''; } $dictionaryItems = static::fetchChildrenItems( Uuid::fromBytes($property['uuid']), $property['value_type'], $childrenValues ); $values['var'] = Dictionary::prepare($dictionaryItems); } elseif ($property['value_type'] === 'dynamic-dictionary') { $childrenValues = [ 'value' => $property['value'] ?? [], 'inherited' => $property['inherited'] ?? [], 'inherited_from' => $property['inherited_from'] ?? '' ]; $dictionaryItems = static::fetchChildrenItems( Uuid::fromBytes($property['uuid']), $property['value_type'], $childrenValues ); $values['var'] = NestedDictionary::prepare( $dictionaryItems, $property['value'] ?? [] ); $values['inherited'] = isset($property['inherited']) ? json_encode($property['inherited'], JSON_PRETTY_PRINT) : ''; $values['inherited_from'] = $property['inherited_from'] ?? ''; } elseif ( $property['value_type'] === 'datalist-non-strict' && self::fetchItemType(Uuid::fromBytes($property['uuid'])) === 'string' ) { $dataListEntries = self::fetchDataListEntries(Uuid::fromBytes($property['uuid'])); $value = $property['value'] ?? ''; if (isset($dataListEntries[$value])) { $values['var'] = $dataListEntries[$value]; $values['var-search'] = $value; $values['var-label'] = $dataListEntries[$value]; } else { $values['var'] = $value; $values['var-search'] = $value; } } else { $values['var'] = $property['value'] ?? ''; $values['inherited'] = $property['inherited'] ?? ''; $values['inherited_from'] = $property['inherited_from'] ?? ''; } return $values; } /** * Fetch children items of the given parent item * * @param UuidInterface $parentUuid * @param string $parentType * @param array $values * * @return array */ private static function fetchChildrenItems(UuidInterface $parentUuid, string $parentType, array $values = []): array { $db = Db::fromResourceName(Config::module('director')->get('db', 'resource'))->getDbAdapter(); $query = $db->select() ->from( ['dp' => 'director_property'], [ 'key_name' => 'dp.key_name', 'uuid' => 'dp.uuid', 'value_type' => 'dp.value_type', 'label' => 'dp.label', 'parent_uuid' => 'dp.parent_uuid', 'children' => 'COUNT(cdp.uuid)' ] ) ->where('dp.parent_uuid = ?', Db\DbUtil::quoteBinaryCompat($parentUuid->getBytes(), $db)) ->joinLeft( ['cdp' => 'director_property'], 'cdp.parent_uuid = dp.uuid', [] ) ->group(['dp.uuid', 'dp.key_name', 'dp.value_type', 'dp.label']) ->order('children') ->order('key_name'); $propertyItems = $db->fetchAll($query, fetchMode: PDO::FETCH_ASSOC); foreach ($propertyItems as $key => $propertyItem) { $propertyItem['uuid'] = DbUtil::binaryResult($propertyItem['uuid']); $propertyItem['parent_uuid'] = DbUtil::binaryResult($propertyItem['parent_uuid']); $propertyItems[$key] = $propertyItem; } if (empty($values)) { return $propertyItems; } $result = []; foreach ($propertyItems as $propertyItem) { $propertyItem['parent_type'] = $parentType; if (isset($values['value'][$propertyItem['key_name']])) { $propertyItem['value'] = $values['value'][$propertyItem['key_name']]; } if (isset($values['inherited'][$propertyItem['key_name']])) { $propertyItem['inherited'] = $values['inherited'][$propertyItem['key_name']]; $propertyItem['inherited_from'] = $values['inherited_from']; } $result[$propertyItem['key_name']] = $propertyItem; } return $result; } /** * Set the remove button. * * @param ?FormElement $removeButton * * @return $this */ public function setRemoveButton(?FormElement $removeButton): static { $this->removeButton = $removeButton; return $this; } /** * Get the dictionary item value * * @return DictionaryItemDataType */ public function getItem(): array { $values = ['name' => $this->getElement('name')->getValue()]; $itemValue = $this->getElement('var'); if ($itemValue instanceof NestedDictionary or $itemValue instanceof Dictionary) { $values['value'] = $itemValue->getDictionary(); if ($this->getElement('type')->getValue() === 'fixed-array') { $value = $values['value']; ksort($value); $values['value'] = array_values($value); } } elseif ( $this->getElement('type')->getValue() === 'datalist-non-strict' && self::fetchItemType(Uuid::fromBytes($this->fields['uuid'])) === 'string' ) { $values['value'] = $this->getElement('var-search')->getValue(); } else { if (! empty($this->getElement('inherited')->getValue())) { $values['value'] = $itemValue->getValue(); } else { $defaultValue = null; // Use the default value for fixed-array items only if the fixed array does not have an inherited value if ($this->getElement('parent_type')->getValue() === 'fixed-array') { match ($this->getElement('type')->getValue()) { 'string' => $defaultValue = '', 'number' => $defaultValue = 0, 'bool' => $defaultValue = 'n', 'fixed-array', 'dynamic-array' => $defaultValue = [] }; } $values['value'] = $itemValue->getValue() ?? $defaultValue; } } $markForRemovalElement = 'delete-' . $this->getName(); if ($this->hasElement($markForRemovalElement)) { $values['delete'] = $this->getElement($markForRemovalElement)->getValue(); } return $values; } }