Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
HtmlFactory
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
18 / 18
32
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefinitionFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefinitionFactory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDefinitionsFile
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDefinitionsFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefinitionArray
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 hydrateContainer
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 makeDefinition
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getDefinitionTypes
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getDefinitionType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefinitionIds
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isAmbiguousName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeElement
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 makeAttribute
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 makeEvent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 makeCustomData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * @author: Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7declare(strict_types=1);
8
9namespace pvc\html\htmlBuilder;
10
11use pvc\html\attributeArrayElement\AttributeCustomData;
12use pvc\html\err\DTOInvalidPropertyValueException;
13use pvc\html\err\DuplicateDefinitionIdException;
14use pvc\html\err\InvalidDefinitionsFileException;
15use pvc\html\err\InvalidEventNameException;
16use pvc\html\err\InvalidTagNameException;
17use pvc\html\err\InvalidAttributeIdNameException;
18use pvc\interfaces\html\attributeArrayElement\AttributeCustomDataInterface;
19use pvc\interfaces\html\attributeArrayElement\AttributeInterface;
20use pvc\interfaces\html\attributeArrayElement\EventInterface;
21use pvc\interfaces\html\htmlBuilder\definitions\DefinitionFactoryInterface;
22use pvc\interfaces\html\htmlBuilder\definitions\DefinitionType;
23use pvc\interfaces\html\htmlBuilder\HtmlContainerInterface;
24use pvc\interfaces\html\htmlBuilder\HtmlFactoryInterface;
25use pvc\interfaces\html\element\TagVoidInterface;
26use pvc\interfaces\validator\ValTesterInterface;
27use pvc\validator\val_tester\always_true\AlwaysTrueTester;
28
29/**
30 * Class HtmlFactory
31 *
32 * @phpstan-import-type DefArray from DefinitionFactoryInterface
33 *
34 * @template VendorSpecificDefinition of DefinitionFactoryInterface
35 *
36 * @implements HtmlFactoryInterface<VendorSpecificDefinition>
37 */
38class HtmlFactory implements HtmlFactoryInterface
39{
40    /**
41     * @var HtmlContainerInterface<VendorSpecificDefinition>
42     */
43    protected HtmlContainerInterface $container;
44
45    /**
46     * @var DefinitionFactoryInterface<VendorSpecificDefinition>
47     */
48    protected DefinitionFactoryInterface $definitionFactory;
49
50    /**
51     * @var string
52     */
53    protected string $definitionsFile = __DIR__ . '/definitions/Definitions.json';
54
55    /**
56     * @var array<string, string>
57     */
58    protected array $definitionTypes;
59
60    /**
61     * there are several identifiers in html which are duplicates, i.e. out of context, you would not know whether
62     * you are referring to an attributeArrayElement or an element.  For this reason and because we are using a single container,
63     * there are some cases where the definition id needs to be different from the name of the object.  The
64     * method of disambiguation is to append an _attr or _element to the names of the objects and make those the
65     * definition ids.  For example, cite => cite_attr / cite_element.
66     *
67     * The ambiguous identifiers are:
68     *
69     * cite
70     * data
71     * form
72     * label
73     * span
74     * style
75     * title
76     *
77     * @var array<string>
78     */
79    protected array $ambiguousIdentifiers = [
80        'cite',
81        'data',
82        'form',
83        'label',
84        'span',
85        'style',
86        'title',
87        'type',
88    ];
89
90    /**
91     * @param HtmlContainerInterface<VendorSpecificDefinition> $container
92     * @param DefinitionFactoryInterface<VendorSpecificDefinition> $definitionFactory
93     */
94    public function __construct(
95        HtmlContainerInterface $container,
96        DefinitionFactoryInterface $definitionFactory,
97        string $definitionsFile = null,
98    ) {
99        $this->setContainer($container);
100        $this->setDefinitionFactory($definitionFactory);
101
102        if ($definitionsFile) {
103            $this->setDefinitionsFile($definitionsFile);
104        }
105        $this->hydrateContainer($this->getContainer());
106    }
107
108    /**
109     * getContainer
110     * @return HtmlContainerInterface<VendorSpecificDefinition>
111     */
112    public function getContainer(): HtmlContainerInterface
113    {
114        return $this->container;
115    }
116
117    /**
118     * setContainer
119     * @param HtmlContainerInterface<VendorSpecificDefinition> $container
120     */
121    public function setContainer(HtmlContainerInterface $container): void
122    {
123        $this->container = $container;
124    }
125
126    /**
127     * getDefinitionFactory
128     * @return DefinitionFactoryInterface<VendorSpecificDefinition>
129     */
130    public function getDefinitionFactory(): DefinitionFactoryInterface
131    {
132        return $this->definitionFactory;
133    }
134
135    /**
136     * setDefinitionFactory
137     * @param DefinitionFactoryInterface<VendorSpecificDefinition> $definitionFactory
138     */
139    public function setDefinitionFactory(DefinitionFactoryInterface $definitionFactory): void
140    {
141        $this->definitionFactory = $definitionFactory;
142    }
143
144    protected function setDefinitionsFile(string $filename): void
145    {
146        if (!is_readable($filename)) {
147            throw new InvalidDefinitionsFileException($filename);
148        }
149        $this->definitionsFile = $filename;
150    }
151
152    /**
153     * getDefinitionsFile
154     * @return string
155     */
156    public function getDefinitionsFile(): string
157    {
158        return $this->definitionsFile;
159    }
160
161    /**
162     * getDefinitionArray
163     * @return array<mixed>
164     * @throws InvalidDefinitionsFileException
165     */
166    protected function getDefinitionArray(): array
167    {
168        /** @var string $jsonString */
169        $jsonString = file_get_contents($this->getDefinitionsFile());
170        $defs = json_decode($jsonString, true);
171
172        if (is_null($defs)) {
173            throw new InvalidDefinitionsFileException($this->getDefinitionsFile());
174        }
175
176        assert(is_array($defs));
177        return $defs;
178    }
179
180    /**
181     * hydrateContainer
182     * @param HtmlContainerInterface<VendorSpecificDefinition> $container
183     * @throws DTOInvalidPropertyValueException
184     * @throws DuplicateDefinitionIdException
185     * @throws InvalidDefinitionsFileException
186     */
187    protected function hydrateContainer(HtmlContainerInterface $container): void
188    {
189        /** @var array<DefArray> $jsonDefs */
190        $jsonDefs = $this->getDefinitionArray();
191
192        foreach ($jsonDefs as $jsonDef) {
193            /**
194             * this artifact is really for diagnostics.  Using the getDefinitionIdsTypes method you can return
195             * all of this array or filter it for definition ids of a certain type
196             */
197            $defId = $jsonDef['defId'];
198            $defType = $jsonDef['defType'];
199
200            $def = $this->makeDefinition($jsonDef);
201
202            if (isset($this->definitionTypes[$defId])) {
203                throw new DuplicateDefinitionIdException($defId);
204            } else {
205                $this->definitionTypes[$defId] = $defType;
206            }
207
208            $container->add($defId, $def);
209        }
210    }
211
212    /**
213     * makeDefinition
214     * @param DefArray $defArray
215     * @return VendorSpecificDefinition
216     * @throws DTOInvalidPropertyValueException
217     */
218    protected function makeDefinition(array $defArray): mixed
219    {
220        $defTypeString = $defArray['defType'];
221
222        $result = match(strval($defTypeString)) {
223            'Attribute' => $this->definitionFactory->makeAttributeDefinition($defArray),
224            'AttributeValueTester' => $this->definitionFactory->makeAttributeValueTesterDefinition($defArray),
225            'Element' => $this->definitionFactory->makeElementDefinition($defArray),
226            'Event' => $this->definitionFactory->makeEventDefinition($defArray),
227            'Other' => $this->definitionFactory->makeOtherDefinition($defArray),
228            default => throw new DTOInvalidPropertyValueException('defType', $defTypeString, 'DTOTrait'),
229        };
230        return $result;
231    }
232
233    /**
234     * getDefinitionIdsTypes
235     * @param DefinitionType $type
236     * @return array<string, string>
237     */
238    public function getDefinitionTypes(DefinitionType $type = null): array
239    {
240        $result = [];
241        foreach ($this->definitionTypes as $defId => $defType) {
242            if (is_null($type) || $defType == $type->value) {
243                $result[$defId] = $defType;
244            }
245        }
246        return $result;
247    }
248
249    /**
250     * getDefinitionType
251     * @param string $defId
252     * @return string|null
253     */
254    public function getDefinitionType(string $defId): ?string
255    {
256        return $this->definitionTypes[$defId] ?? null;
257    }
258
259    /**
260     * getDefinitionIds
261     * @param DefinitionType|null $type
262     * @return array<string>
263     */
264    public function getDefinitionIds(DefinitionType $type = null): array
265    {
266        return array_keys($this->getDefinitionTypes($type));
267    }
268
269    protected function isAmbiguousName(string $name): bool
270    {
271        return(in_array($name, $this->ambiguousIdentifiers));
272    }
273
274    /**
275     * makeElement
276     * @param string $elementName
277     * @return TagVoidInterface<VendorSpecificDefinition>
278     * @throws InvalidTagNameException
279     * @throws \Psr\Container\ContainerExceptionInterface
280     * @throws \Psr\Container\NotFoundExceptionInterface
281     */
282    public function makeElement(string $elementName): TagVoidInterface
283    {
284        $elementDefId = ($this->isAmbiguousName($elementName) ? $elementName . '_element' : $elementName);
285
286        if (!$this->container->has($elementDefId)) {
287            throw new InvalidTagNameException($elementName);
288        }
289        /** @var TagVoidInterface<VendorSpecificDefinition> $element */
290        $element = $this->getContainer()->get($elementDefId);
291        $element->setHtmlBuilder($this);
292        return $element;
293    }
294
295    /**
296     * makeAttribute
297     * @param string $attributeName
298     * @return AttributeInterface
299     * @throws InvalidAttributeIdNameException
300     * @throws \Psr\Container\ContainerExceptionInterface
301     * @throws \Psr\Container\NotFoundExceptionInterface
302     */
303    public function makeAttribute(string $attributeName): AttributeInterface
304    {
305        $attributeDefId = ($this->isAmbiguousName($attributeName) ? $attributeName . '_attr' : $attributeName);
306        if (!$this->container->has($attributeDefId)) {
307            throw new InvalidAttributeIdNameException($attributeName);
308        }
309        /** @var AttributeInterface $attributeArrayElement */
310        $attributeArrayElement = $this->getContainer()->get($attributeDefId);
311        return $attributeArrayElement;
312    }
313
314    /**
315     * makeEvent
316     * @param string $eventName
317     * @return EventInterface
318     * @throws InvalidEventNameException
319     * @throws \Psr\Container\ContainerExceptionInterface
320     * @throws \Psr\Container\NotFoundExceptionInterface
321     */
322    public function makeEvent(string $eventName): EventInterface
323    {
324        if (!$this->container->has($eventName)) {
325            throw new InvalidEventNameException($eventName);
326        }
327        /** @var EventInterface $event */
328        $event = $this->getContainer()->get($eventName);
329        return $event;
330    }
331
332
333    /**
334     * makeCustomData
335     * @param string $attributeName
336     * @param ValTesterInterface<string>|null $valTester
337     * @return AttributeCustomDataInterface
338     * @throws \Psr\Container\ContainerExceptionInterface
339     * @throws \Psr\Container\NotFoundExceptionInterface
340     *
341     * although we never like hard dependencies inside a method, this class is a htmlBuilder.  And this method is, by
342     * definition, tightly coupled to the AttributeCustomData class.......
343     */
344    public function makeCustomData(
345        string $attributeName,
346        string $value,
347        ValTesterInterface $valTester = null
348    ): AttributeCustomDataInterface {
349
350        $valTester = $valTester ?: new AlwaysTrueTester();
351
352        $attributeArrayElement = new AttributeCustomData();
353        $attributeArrayElement->setDefId($attributeName);
354        $attributeArrayElement->setName($attributeName);
355        $attributeArrayElement->setTester($valTester);
356        $attributeArrayElement->setValue($value);
357
358        return $attributeArrayElement;
359    }
360}