1<?php
2
3
4namespace ComboStrap\Meta\Api;
5
6
7use ComboStrap\DataType;
8use ComboStrap\ExceptionBadArgument;
9use ComboStrap\ExceptionNotFound;
10use ComboStrap\ExceptionRuntime;
11use ComboStrap\ExceptionRuntimeInternal;
12use ComboStrap\LogUtility;
13use ComboStrap\Meta\Field\Aliases;
14use ComboStrap\References;
15
16/**
17 * Class MetadataTabular
18 * @package ComboStrap
19 * A list of row represented as a list of column
20 * ie an entity with a map that has a key
21 *
22 * The value of a tabular is an array of row (where a row is an associative array)
23 */
24abstract class MetadataTabular extends Metadata
25{
26
27    /**
28     * In the array, the identifier may be the persistent one
29     * or the identifier one
30     */
31    const PERSISTENT_NAME = "persistent";
32    const IDENTIFIER_NAME = "identifier";
33
34
35    /**
36     * A array of rows where
37     * - the key is the `identifier value`
38     * - the value is a list of column metadata
39     *     * the key is the {@link Metadata::getPersistentName()}
40     *     * the value is the metadata
41     *
42     * @var Metadata[][]
43     */
44    protected ?array $rows = null;
45
46
47    /**
48     * @return array - the rows in associate array format
49     * where the identifier value is the key
50     * @throws ExceptionNotFound
51     */
52    public function getValue(): array
53    {
54        $this->buildCheck();
55        if ($this->rows === null) {
56            throw new ExceptionNotFound("No tabular data found.");
57        }
58        return $this->rows;
59    }
60
61    public function setValue($value): Metadata
62    {
63        if ($value === null) {
64            return $this;
65        }
66        if (!is_array($value)) {
67            throw new ExceptionRuntime("The data set is not an array (The tabular data is an array of rows)");
68        }
69        $keys = array_keys($value);
70        foreach ($keys as $key) {
71            if (!is_numeric($key)) {
72                throw new ExceptionRuntime("The element of the array are not rows. The index ($key) should be numeric and is not");
73            }
74        }
75        $this->rows = $value;
76        return $this;
77
78    }
79
80
81    public function toStoreValue(): ?array
82    {
83        $this->buildCheck();
84        if ($this->rows === null) {
85            return null;
86        }
87        return $this->rowsToStore($this->rows);
88    }
89
90    /**
91     * /**
92     * A list of rows that contains a list of metadata
93     * with their value
94     * Each row has the key has value
95     * @return Metadata[][] - an array of rows
96     * @throws ExceptionNotFound
97     */
98    public abstract function getDefaultValue(): array;
99
100    public function toStoreDefaultValue(): ?array
101    {
102        try {
103            $defaultRows = $this->getDefaultValue();
104        } catch (ExceptionNotFound $e) {
105            return null;
106        }
107
108        return $this->rowsToStore($defaultRows);
109    }
110
111    /**
112     * The value is:
113     *   * a string for a unique identifier value
114     *   * an array of columns or an array of rows
115     */
116    public function setFromStoreValueWithoutException($value): Metadata
117    {
118
119        if ($value === null) {
120            return $this;
121        }
122
123        /**
124         * Value of the metadata id
125         */
126        $identifierMetadataObject = $this->getUidObject();
127        $identifierPersistentName = $identifierMetadataObject::getPersistentName();
128
129        /**
130         * Single value
131         */
132        if (is_string($value)) {
133            /**
134             * @var Metadata $identifierMetadata
135             */
136            $identifierMetadata = (new $identifierMetadataObject());
137            $identifierMetadata
138                ->setResource($this->getResource())
139                ->setValue($value);
140            $rowId = $this->getRowId($identifierMetadata);
141            $this->rows[$rowId] = [$identifierPersistentName => $identifierMetadata];
142            return $this;
143        }
144
145        /**
146         * Array
147         */
148        if (!is_array($value)) {
149            LogUtility::msg("The tabular value is nor a string nor an array");
150            return $this;
151        }
152
153
154        /**
155         * List of columns ({@link MetadataFormDataStore form html way}
156         *
157         * For example: for {@link Aliases}, a form will have two fields
158         *  alias-path:
159         *    0: path1
160         *    1: path2
161         *  alias-type:
162         *    0: redirect
163         *    1: redirect
164         *
165         * or just a list ({@link References}
166         *   references:
167         *    0: reference-1
168         *    1: $colValue-2
169         */
170        $keys = array_keys($value);
171        $firstElement = array_shift($keys);
172        if (!is_numeric($firstElement)) {
173            /**
174             * Check which kind of key is used
175             * Resistance to the property key !
176             */
177            $identifierName = $identifierMetadataObject::getPersistentName();
178            $identifierNameType = self::PERSISTENT_NAME;
179            if (!isset($value[$identifierName])) {
180                $identifierNameType = self::IDENTIFIER_NAME;
181                $identifierName = $identifierMetadataObject::getName();
182            }
183            $identifierValues = $value[$identifierName];
184            if ($identifierValues === null || $identifierValues === "") {
185                // No data
186                return $this;
187            }
188            $index = 0;
189            if (!is_array($identifierValues)) {
190                // only one value
191                $identifierValues = [$identifierValues];
192            }
193            foreach ($identifierValues as $identifierValue) {
194                $row = [];
195                if ($identifierValue === "") {
196                    // an empty row in the table
197                    continue;
198                }
199                $row[$identifierPersistentName] = MetadataSystem::toMetadataObject($identifierMetadataObject, $this)
200                    ->setFromStoreValue($identifierValue);
201                foreach ($this->getChildrenClass() as $childClass) {
202                    if ($childClass === get_class($identifierMetadataObject)) {
203                        continue;
204                    }
205                    $metadataChildObject = MetadataSystem::toMetadataObject($childClass, $this);
206                    $name = $metadataChildObject::getPersistentName();
207                    if ($identifierNameType === self::IDENTIFIER_NAME) {
208                        $name = $metadataChildObject::getName();
209                    }
210                    $childValue = $value[$name];
211                    if (is_array($childValue)) {
212                        $childValue = $childValue[$index];
213                        $index++;
214                    }
215                    $metadataChildObject->setFromStoreValue($childValue);
216                    $row[$metadataChildObject::getPersistentName()] = $metadataChildObject;
217                }
218                $this->rows[] = $row;
219            }
220
221            return $this;
222        }
223
224        /**
225         * List of row (frontmatter, dokuwiki, ...)
226         */
227        $childClassesByPersistentName = [];
228        foreach ($this->getChildrenObject() as $childObject) {
229            $childClassesByPersistentName[$childObject::getPersistentName()] = $childObject;
230        }
231        foreach ($value as $item) {
232
233            /**
234             * By default, the single value is the identifier
235             *
236             * (Note that from {@link MetadataFormDataStore}, tabular data
237             * should be build before via the {@link self::buildFromReadStore()}
238             * as tabular data is represented as a series of column)
239             *
240             */
241            if (is_string($item)) {
242                try {
243                    $identifierMetadata = MetadataSystem::toMetadataObject($identifierMetadataObject, $this)->setFromStoreValue($item);
244                } catch (ExceptionBadArgument $e) {
245                    throw ExceptionRuntimeInternal::withMessageAndError("The $identifierMetadataObject should be known", $e);
246                }
247                $identifierValue = $this->getRowId($identifierMetadata);
248                $this->rows[$identifierValue] = [$identifierPersistentName => $identifierMetadata];
249                continue;
250            }
251            if (!is_array($item)) {
252                LogUtility::msg("The tabular value is not a string nor an array");
253            }
254            $row = [];
255            $idValue = null;
256            foreach ($item as $colName => $colValue) {
257                $childClass = $childClassesByPersistentName[$colName] ?? null;
258                if ($childClass === null) {
259                    LogUtility::internalError("The column ($colName) does not have a metadata definition");
260                    continue;
261                }
262                $childObject = MetadataSystem::toMetadataObject($childClass, $this);
263                $childObject->setFromStoreValueWithoutException($colValue);
264                $row[$childObject::getPersistentName()] = $childObject;
265                if ($childObject::getPersistentName() === $identifierPersistentName) {
266                    $idValue = $this->getRowId($childObject);
267                }
268            }
269            if ($idValue === null) {
270                LogUtility::msg("The value for the identifier ($identifierPersistentName) was not found");
271                continue;
272            }
273            $this->rows[$idValue] = $row;
274
275        }
276        return $this;
277    }
278
279
280    public function valueIsNotNull(): bool
281    {
282        return $this->rows !== null;
283    }
284
285    public
286    function remove(string $identifierValue): MetadataTabular
287    {
288        $this->buildCheck();
289        if ($this->rows === null) {
290            return $this;
291        }
292        unset($this->rows[$identifierValue]);
293        return $this;
294    }
295
296
297    public
298    function has($identifierValue): bool
299    {
300        $this->buildCheck();
301        if ($this->rows === null) {
302            return false;
303        }
304        return isset($this->rows[$identifierValue]);
305    }
306
307    public
308    function getSize(): int
309    {
310        $this->buildCheck();
311        if ($this->rows === null) {
312            return 0;
313        }
314        return sizeof($this->rows);
315    }
316
317    public function getRow($id): ?array
318    {
319        $this->buildCheck();
320        $normalizedValue = $this->getUidObject()
321            ->setFromStoreValueWithoutException($id)
322            ->getValue();
323        if (is_object($normalizedValue)) {
324            $normalizedValue = $normalizedValue->__toString();
325        }
326        return $this->rows[$normalizedValue] ?? null;
327    }
328
329    static public function getDataType(): string
330    {
331        return DataType::TABULAR_TYPE_VALUE;
332    }
333
334    /**
335     * @throws ExceptionNotFound - if the identifier or it's value was not found
336     * @var Metadata[] $metas
337     */
338    public function addRow(array $metas): MetadataTabular
339    {
340        $row = [];
341        $identifier = null;
342        foreach ($metas as $meta) {
343            $row[$meta::getPersistentName()] = $meta;
344            if (get_class($meta) === $this->getUidClass()) {
345                $identifier = $meta->getValue();
346            }
347        }
348        if ($identifier === null) {
349            throw new ExceptionNotFound("The identifier value was not found in the row");
350        }
351        $this->rows[$identifier] = $row;
352        return $this;
353    }
354
355    private function rowsToStore(array $defaultRows): array
356    {
357        $rowsArray = [];
358        ksort($defaultRows);
359        foreach ($defaultRows as $row) {
360            $rowArray = [];
361            foreach ($row as $metadata) {
362                $toStoreValue = $metadata->toStoreValue();
363                $toDefaultStoreValue = $metadata->toStoreDefaultValue();
364                if (
365                    $toStoreValue !== null
366                    && $toStoreValue !== $toDefaultStoreValue
367                ) {
368                    $rowArray[$metadata::getPersistentName()] = $toStoreValue;
369                }
370            }
371            $rowsArray[] = $rowArray;
372        }
373        return $rowsArray;
374    }
375
376    /**
377     * @param Metadata $identifierMetadata
378     * @return mixed
379     * Due to dokuwiki, the same id may be a markup or media path
380     * We build the object from the same id but the url is not the same
381     * This function takes the metadata, get the value and
382     * return the identifier suitable for an array
383     */
384    private function getRowId(Metadata $identifierMetadata)
385    {
386        try {
387            $identifierValue = $identifierMetadata->getValue(); // normalize if any
388        } catch (ExceptionNotFound $e) {
389            throw ExceptionRuntimeInternal::withMessageAndError("The meta identifier ($identifierMetadata) should have a value", $e);
390        }
391        if (DataType::isObject($identifierValue)) {
392            /**
393             * An object cannot be the key of an array
394             */
395            $identifierValue = $identifierValue->__toString();
396        }
397        return $identifierValue;
398    }
399
400}
401