Issues (20)

src/xml/XmlParser.php (1 issue)

1
<?php declare(strict_types=1);
2
/**
3
 * This file is part of the Phootwork package.
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 *
7
 * @license MIT License
8
 * @copyright Thomas Gossmann
9
 */
10
namespace phootwork\xml;
11
12
use phootwork\collection\Set;
13
use phootwork\file\exception\FileException;
14
use phootwork\file\File;
15
use phootwork\xml\exception\XmlException;
16
use Stringable;
17
use XMLParser as BaseParser;
18
19
class XmlParser {
20
	/**
21
	 * Controls whether case-folding is enabled for this XML parser. Enabled by default. 
22
	 * 
23
	 * Data Type: integer
24
	 * 
25
	 * @var int
26
	 */
27
	public const OPTION_CASE_FOLDING = XML_OPTION_CASE_FOLDING;
28
29
	/**
30
	 * Specify how many characters should be skipped in the beginning of a tag name.
31
	 * 
32
	 * Data Type: integer
33
	 * 
34
	 * @var int
35
	 */
36
	public const OPTION_SKIP_TAGSTART = XML_OPTION_SKIP_TAGSTART;
37
38
	/**
39
	 * Whether to skip values consisting of whitespace characters. 
40
	 * 
41
	 * Data Type: integer
42
	 * 
43
	 * @var string
44
	 */
45
	public const OPTION_SKIP_WHITE = XML_OPTION_SKIP_WHITE;
46
47
	/**
48
	 * Sets which target encoding to use in this XML parser. By default, it is set to the same as the 
49
	 * source encoding used by XmlParser::construct(). Supported target encodings are ISO-8859-1, US-ASCII and UTF-8.
50
	 * 
51
	 * Data Type: string
52
	 *
53
	 * @var string
54
	 */
55
	public const OPTION_TARGET_ENCODING = XML_OPTION_TARGET_ENCODING;
56
57
	/** @var BaseParser */
58
	private BaseParser $parser;
59
60
	/** @var Set */
61
	private Set $visitors;
62
63
	/**
64
	 * Creates a new XML parser
65
	 * 
66
	 * @param string $encoding Force a specific encoding
67
	 *
68
	 * @psalm-suppress InvalidPropertyAssignmentValue Psalm issue: in PHP8 the function `xml_parser_create`
69
	 *                 returns an `XmlParser` class. Remove it when fixed.
70
	 */
71 3
	public function __construct(string $encoding = 'UTF-8') {
72 3
		$this->visitors = new Set();
73 3
		$this->parser = xml_parser_create($encoding);
0 ignored issues
show
Documentation Bug introduced by
It seems like xml_parser_create($encoding) can also be of type resource. However, the property $parser is declared as type XMLParser. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
74
75 3
		xml_set_element_handler($this->parser, [$this, 'handleElementStart'], [$this, 'handleElementEnd']);
76 3
		xml_set_character_data_handler($this->parser, [$this, 'handleCharacterData']);
77 3
		xml_set_processing_instruction_handler($this->parser, [$this, 'handleProcessingInstruction']);
78 3
		xml_set_notation_decl_handler($this->parser, [$this, 'handleNotationDeclaration']);
79 3
		xml_set_unparsed_entity_decl_handler($this->parser, [$this, 'handleUnparsedEntitiyDeclaration']);
80
	}
81
82
	/**
83
	 * Set an option for the parser
84
	 * 
85
	 * @param int $option Any of the XmlParser::OPTION_* constants
86
	 * @param mixed $value The desired value
87
	 */
88 1
	public function setOption(int $option, mixed $value): bool {
89 1
		return xml_parser_set_option($this->parser, $option, $value);
90
	}
91
92
	/**
93
	 * Gets the value for an option
94
	 * 
95
	 * @param int $option Any of the XmlParser::OPTION_* constants
96
	 *
97
	 * @return mixed
98
	 *
99
	 * @psalm-suppress InvalidArgument Psalm issue: in PHP8 the function `xml_parser_get_option`
100
	 *                  expects a `XmlParser` and not more `resource`. Remove it when fixed.
101
	 */
102 1
	public function getOption(int $option): mixed {
103 1
		return xml_parser_get_option($this->parser, $option);
104
	}
105
106
	/**
107
	 * Adds a visitor
108
	 * 
109
	 * @param XmlParserVisitorInterface $visitor
110
	 */
111 2
	public function addVisitor(XmlParserVisitorInterface $visitor): void {
112 2
		$this->visitors->add($visitor);
113
	}
114
115
	/**
116
	 * Removes a visitor
117
	 * 
118
	 * @param XmlParserVisitorInterface $visitor
119
	 */
120 1
	public function removeVisitor(XmlParserVisitorInterface $visitor): void {
121 1
		$this->visitors->remove($visitor);
122
	}
123
124
	/**
125
	 * Parses a string
126
	 *
127
	 * @param string|Stringable $data
128
	 *
129
	 * @throws XmlException
130
	 *
131
	 */
132 3
	public function parse(string|Stringable $data): void {
133 3
		if (!xml_parse($this->parser, (string) $data)) {
134 1
			$code = xml_get_error_code($this->parser);
135
136 1
			throw new XmlException(xml_error_string($code) ?? 'Undefined error', $code);
137
		}
138
	}
139
140
	/**
141
	 * Parses a file
142
	 *
143
	 * @param string|Stringable $file
144
	 *
145
	 * @throws XmlException
146
	 * @throws FileException If something went wrong in reading file
147
	 */
148 2
	public function parseFile(string|Stringable $file): void {
149 2
		$file = new File($file);
150 2
		$this->parse($file->read());
151
	}
152
153
	/**
154
	 * @return int
155
	 *
156
	 * @psalm-suppress InvalidArgument Psalm issue: in PHP8 the function `xml_get_current_line_number`
157
	 *                  expects a `XmlParser` and not more `resource`. Remove it when fixed.
158
	 */
159 1
	private function getCurrentLineNumber(): int {
160 1
		return xml_get_current_line_number($this->parser);
161
	}
162
163
	/**
164
	 * @return int
165
	 *
166
	 * @psalm-suppress InvalidArgument Psalm issue: in PHP8 the function `xml_get_current_column_number`
167
	 *                  expects a `XmlParser` and not more `resource`. Remove it when fixed.
168
	 */
169 1
	private function getCurrentColumnNumber(): int {
170 1
		return xml_get_current_column_number($this->parser);
171
	}
172
173
	/**
174
	 * handle element start
175
	 * 
176
	 * @param BaseParser $parser
177
	 * @param string $name
178
	 * @param array $attribs
179
	 *
180
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
181
	 *                 call this method passing $parser as the first parameter
182
	 */
183 2
	private function handleElementStart(BaseParser $parser, string $name, array $attribs): void {
184
		/** @var XmlParserVisitorInterface $visitor */
185 2
		foreach ($this->visitors as $visitor) {
186 1
			$visitor->visitElementStart(strtolower($name), $attribs, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
187
		}
188
	}
189
190
	/**
191
	 * handle element end 
192
	 *
193
	 * @param BaseParser $parser
194
	 * @param string $name
195
	 *
196
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
197
	 *                 call this method passing $parser as the first parameter
198
	 */
199 2
	private function handleElementEnd(BaseParser $parser, string $name): void {
200
		/** @var XmlParserVisitorInterface $visitor */
201 2
		foreach ($this->visitors as $visitor) {
202 1
			$visitor->visitElementEnd(strtolower($name), $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
203
		}
204
	}
205
206
	/**
207
	 * handle cdata
208
	 *
209
	 * @param BaseParser $parser
210
	 * @param string $data
211
	 *
212
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
213
	 *                 call this method passing $parser as the first parameter
214
	 */
215 2
	private function handleCharacterData(BaseParser $parser, string $data): void {
216
		/** @var XmlParserVisitorInterface $visitor */
217 2
		foreach ($this->visitors as $visitor) {
218 1
			$visitor->visitCharacterData($data, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
219
		}
220
	}
221
222
	/**
223
	 * handle processing instruction
224
	 *
225
	 * @param BaseParser $parser
226
	 * @param string     $target
227
	 * @param string     $data
228
	 *
229
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
230
	 *                 call this method passing $parser as the first parameter
231
	 */
232
	private function handleProcessingInstruction(BaseParser $parser, string $target, string $data): void {
233
		/** @var XmlParserVisitorInterface $visitor */
234
		foreach ($this->visitors as $visitor) {
235
			$visitor->visitProcessingInstruction($target, $data, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
236
		}
237
	}
238
239
	/**
240
	 * handle notation declaration
241
	 *
242
	 * @param BaseParser $parser
243
	 * @param string     $notationName
244
	 * @param string     $base
245
	 * @param string     $systemId
246
	 * @param string     $publicId
247
	 *
248
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
249
	 *                 call this method passing $parser as the first parameter
250
	 */
251
	private function handleNotationDeclaration(
252
		BaseParser $parser,
253
		string $notationName,
254
		string $base,
255
		string $systemId,
256
		string $publicId
257
	): void {
258
		/** @var XmlParserVisitorInterface $visitor */
259
		foreach ($this->visitors as $visitor) {
260
			$visitor->visitNotationDeclaration($notationName, $base, $systemId, $publicId, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
261
		}
262
	}
263
264
	/**
265
	 * handle unparsed entity declaration
266
	 *
267
	 * @param BaseParser $parser
268
	 * @param string     $entityName
269
	 * @param string     $base
270
	 * @param string     $systemId
271
	 * @param string     $publicId
272
	 * @param string     $notationName
273
	 *
274
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
275
	 *                 call this method passing $parser as the first parameter
276
	 */
277
	private function handleUnparsedEntitiyDeclaration(
278
		BaseParser $parser,
279
		string $entityName,
280
		string $base,
281
		string $systemId,
282
		string $publicId,
283
		string $notationName
284
	): void {
285
		/** @var XmlParserVisitorInterface $visitor */
286
		foreach ($this->visitors as $visitor) {
287
			$visitor->visitUnparsedEntityDeclaration($entityName, $base, $systemId, $publicId, $notationName, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
288
		}
289
	}
290
}
291