Passed
Push — master ( f6b4a1...5217eb )
by Cristiano
10:27
created

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), $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
	 * @psalm-suppress InvalidArgument Psalm issue: in PHP8 the function `xml_get_current_line_number`
156
	 *                  expects a `XmlParser` and not more `resource`. Remove it when fixed.
157
	 */
158 1
	private function getCurrentLineNumber(): int {
159 1
		return xml_get_current_line_number($this->parser);
160
	}
161
162
	/**
163
	 * @return int
164
	 * @psalm-suppress InvalidArgument Psalm issue: in PHP8 the function `xml_get_current_column_number`
165
	 *                  expects a `XmlParser` and not more `resource`. Remove it when fixed.
166
	 */
167 1
	private function getCurrentColumnNumber(): int {
168 1
		return xml_get_current_column_number($this->parser);
169
	}
170
171
	/**
172
	 * handle element start
173
	 * 
174
	 * @param BaseParser $parser
175
	 * @param string $name
176
	 * @param array $attribs
177
	 *
178
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
179
	 *                 call this method passing $parser as the first parameter
180
	 */
181 2
	private function handleElementStart(BaseParser $parser, string $name, array $attribs): void {
182
		/** @var XmlParserVisitorInterface $visitor */
183 2
		foreach ($this->visitors as $visitor) {
184 1
			$visitor->visitElementStart(strtolower($name), $attribs, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
185
		}
186
	}
187
188
	/**
189
	 * handle element end 
190
	 *
191
	 * @param BaseParser $parser
192
	 * @param string $name
193
	 *
194
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
195
	 *                 call this method passing $parser as the first parameter
196
	 */
197 2
	private function handleElementEnd(BaseParser $parser, string $name): void {
198
		/** @var XmlParserVisitorInterface $visitor */
199 2
		foreach ($this->visitors as $visitor) {
200 1
			$visitor->visitElementEnd(strtolower($name), $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
201
		}
202
	}
203
204
	/**
205
	 * handle cdata
206
	 *
207
	 * @param BaseParser $parser
208
	 * @param string $data
209
	 *
210
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
211
	 *                 call this method passing $parser as the first parameter
212
	 */
213 2
	private function handleCharacterData(BaseParser $parser, string $data): void {
214
		/** @var XmlParserVisitorInterface $visitor */
215 2
		foreach ($this->visitors as $visitor) {
216 1
			$visitor->visitCharacterData($data, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
217
		}
218
	}
219
220
	/**
221
	 * handle processing instruction
222
	 *
223
	 * @param BaseParser $parser
224
	 * @param string     $target
225
	 * @param string     $data
226
	 *
227
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
228
	 *                 call this method passing $parser as the first parameter
229
	 */
230
	private function handleProcessingInstruction(BaseParser $parser, string $target, string $data): void {
231
		/** @var XmlParserVisitorInterface $visitor */
232
		foreach ($this->visitors as $visitor) {
233
			$visitor->visitProcessingInstruction($target, $data, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
234
		}
235
	}
236
237
	/**
238
	 * handle notation declaration
239
	 *
240
	 * @param BaseParser $parser
241
	 * @param string     $notationName
242
	 * @param string     $base
243
	 * @param string     $systemId
244
	 * @param string     $publicId
245
	 *
246
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
247
	 *                 call this method passing $parser as the first parameter
248
	 */
249
	private function handleNotationDeclaration(
250
		BaseParser $parser,
251
		string $notationName,
252
		string $base,
253
		string $systemId,
254
		string $publicId
255
	): void {
256
		/** @var XmlParserVisitorInterface $visitor */
257
		foreach ($this->visitors as $visitor) {
258
			$visitor->visitNotationDeclaration($notationName, $base, $systemId, $publicId, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
259
		}
260
	}
261
262
	/**
263
	 * handle unparsed entity declaration
264
	 *
265
	 * @param BaseParser $parser
266
	 * @param string     $entityName
267
	 * @param string     $base
268
	 * @param string     $systemId
269
	 * @param string     $publicId
270
	 * @param string     $notationName
271
	 *
272
	 * @psalm-suppress UnusedParam xml_parse() function in self::parse(),
273
	 *                 call this method passing $parser as the first parameter
274
	 */
275
	private function handleUnparsedEntitiyDeclaration(
276
		BaseParser $parser,
277
		string $entityName,
278
		string $base,
279
		string $systemId,
280
		string $publicId,
281
		string $notationName
282
	): void {
283
		/** @var XmlParserVisitorInterface $visitor */
284
		foreach ($this->visitors as $visitor) {
285
			$visitor->visitUnparsedEntityDeclaration($entityName, $base, $systemId, $publicId, $notationName, $this->getCurrentLineNumber(), $this->getCurrentColumnNumber());
286
		}
287
	}
288
}
289