wikimedia / dodo
DOM 文档实现
Requires
- php: >=7.2.9|^8.0
- wikimedia/idle-dom: 0.10.0
- wikimedia/remex-html: ^2.3.2|^3.0.0
- wikimedia/zest-css: ^2.0.1
Requires (Dev)
- consolidation/robo: ^3@alpha
- fgnass/domino: ^2.1
- mediawiki/mediawiki-codesniffer: 38.0.0
- mediawiki/mediawiki-phan-config: 0.11.0
- mediawiki/minus-x: 1.1.1
- nikic/php-parser: ^4.10
- ockcyp/covers-validator: 1.3.3
- php-parallel-lint/php-console-highlighter: ^0.5
- php-parallel-lint/php-parallel-lint: 1.3.1
- phpunit/phpunit: ^8.5|^9.5
- web-platform-tests/wpt: ^2.7
- wikimedia/update-history: 1.0.1
This package is auto-updated.
Last update: 2024-09-12 05:59:46 UTC
README
Dodo
Dodo 是将 Domino.js 移植到 PHP,以提供比基于 libxml2 构建的 DOMDocument PHP 类(xml
扩展)更高效且更符合规范的 DOM 库。
Dodo 使用由 IDLeDOM 定义的 WebIDL 的 PHP 绑定。WebIDL 绑定的详细信息可以在 IDLeDOM 文档中找到。
有关该库的更多文档可以在 MediaWiki.org 上找到。
在 Phabricator 上报告问题。
安装
此包在 Packagist 上可用
$ composer require wikimedia/dodo
使用方法
更好的示例和测试即将推出。对于极其基本的用法,请参阅 tests/DodoTest.php。
测试
$ composer test
状态
从 Parsoid 所用 DOM 功能的角度来看,此软件接近完成。完成最后缺少的功能/修复最后剩余的错误,以允许 Parsoid 使用 Dodo 作为其 DOM 库运行是首要目标。
之后,将进行性能基准测试和调整。
我们运行了 W3C 和 WPT 的许多测试,但并非全部。其中一些依赖于特定的 JavaScript 功能,因此可能会始终跳过。我们使用的“已知失败”框架可能需要一些改进,以提供更细粒度的结果。
背景
(摘自 此页面)
PHP DOM 扩展是围绕 libxml2 的包装,在其顶部有一层薄薄的 DOM 兼容层(“在一定程度上,libxml2 提供了对以下附加规范的支持,但并不声称完全实现 [...] 文档对象模型 (DOM) Level 2 Core [...] 但它本身不实现 API,gdome2 在 libxml2 上实现”)。
这根本不接近现代符合标准 HTML5 DOM 的实现,而且维护得很少,更不用说与 WHATWG 变化的步伐保持同步了。
Dodo 库实现了由 IDLeDOM
从 WHATWG DOM 规范中包含的 WebIDL 源直接生成的 PHP 接口。
开发者注意
为什么你需要接口属性的访问器
大多数 DOM 实现必须就适应规范中接口属性的概念做出决定。在许多语言中,唯一的解决方案是使用访问器函数,例如 getFoo()
和 setFoo(value)
,并防止直接访问属性本身。
这并不违反规范的本意,因为它主要捕获数据表示,并且似乎期望在实现规范的库和调用该库的代码之间有一定程度的间接性。
除了通常的论点和理由,支持访问器而不是直接属性访问,以及相反的情况,在这种情况下,大多数实现都迫不得已采取访问器路径,原因特别在于,当前的DOM规范定义某些接口属性为只读,例如Attr接口
interface Attr : Node {
readonly attribute DOMString? namespaceURI;
readonly attribute DOMString? prefix;
readonly attribute DOMString localName;
readonly attribute DOMString name;
[CEReactions] attribute DOMString value;
readonly attribute Element? ownerElement;
readonly attribute boolean specified; // useless; always returns true
};
这意味着一旦它们的值被设置一次(在构造函数中),就不能再修改,但仍然可以被访问。
PHP目前缺少一种在不造成重大性能损失的情况下实现只读属性的方法。尽管有几个RFC(《只读属性》和《属性访问器语法》),但它们一直被拒绝。
因此,在Dodo使用的PHP绑定WebIDL中,我们为每个WebIDL属性显式定义了访问器。如果一个类属性"foo"没有被标记为readonly
,那么将在类上定义方法getFoo()
和setFoo($value)
。如果"foo"被标记为readonly
,那么类上只定义getFoo()
。
我们通过定义特殊的"魔术方法"(如__get
、__set
等)来弥合规范和常见用法之间的差距,以支持常见的$obj->foo
风格的访问。这些将比直接访问适当的getFoo
或setFoo
方法性能更低,因此为了性能,Dodo内部避免使用这种访问方式。
然而,如果你正在阅读这篇文章,并且PHP已经通过了一个改进JavaScript样式属性访问器函数的RFC,你知道该怎么做:用适当的属性访问器替换__get
和__set
魔术方法。(这可以在IDLeDOM生成的Helper
类中完成,并且实际上可能不需要在Dodo本身中更改任何代码。)
指定为"NULL或非空"的字符串
接口属性的类型通常写为DOMString?
,这不是一个单一的类型,而是表示该字段可以取类型NULL
或类型DOMString
(它们是不同的类型)之一。
例如,来自Attr接口的namespaceURI
属性
interface Attr : Node {
readonly attribute DOMString? namespaceURI;
/* ... */
};
然而,通常还有对这类属性值的额外约束,这从IDL接口定义的检查中是不可见的。
例如,namespaceURI被定义为返回命名空间,这可以是"NULL或非空字符串"。
嗯,这有点烦人,因为向接受类型为DOMString
参数的任何接口提供空字符串是完全可能的。
由于这个常见的规定,你会在代码中找到类似下面的东西
class Attr extends Node
{
protected $namespaceURI = NULL;
public function construct(string? $namespace=NULL /* ... other arguments ... */)
{
if ($namespace !== '') {
$this->$namespaceURI = $namespace;
}
/* ... */
}
/* ... */
}
调用者可以提供一个字符串或NULL,但只有在它不是空字符串的情况下才会进行赋值。在这种情况下,$this->$namespaceURI
将保留其默认值NULL
。
指定为"非空"的字符串
这似乎更简单,但实际上比"NULL或非空"更糟!
必须是非空字符串的属性,如localName,通常对于对象正常工作至关重要。localName
,例如,是属性的名字。
不幸的是,空字符串也是一个字符串,甚至是 DOMString
。因此,当函数的参数类型是 DOMString
(或在 PHP 的类型提示中为 string
)时,提供空字符串是有效的。但在构造函数的情况下,一旦我们确定这个参数是空字符串,整个对象就是未定义的。
但在 PHP 中,无法“终止”构造函数——总是会返回指定类的一个对象。在 PHP 的旧版本中,你实际上可以在构造函数中做类似 unset($this)
的事情。这很酷,但你已经很多年不能这样做了。真痛苦...
所以,我们可能不得不抛出异常,或者创建一个“非空字符串”类。
只读并不意味着不可变
只读/可写和可变/不可变
尽管它们看起来可能相似,但实际上并不相同。
Immutable <=> read-only
Read-write => mutable
但
mutable =/> read-write
例如,在 Attr 对象上,ownerElement 是一个只读属性,但如果我们将 Attr 节点与另一个元素关联起来,它仍然可以改变。
另一个例子是,属性的 name 属性是只读的,但 prefix 属性是可写的。由于我可以修改 prefix 属性,我也可以修改 name(包括这个前缀),从而使 name 属性可变,即使它是只读的。
基本上,有些属性即使你不能直接更新,也可以更新用于计算它们值的东西。
介于抽象和具体之间的方法...
Node
接口的方法 isEqualNode
和 cloneNode
是令人烦恼的事物的两个好例子。它们首先做的是所有 Node
对象的共同点,然后继续进行特定于扩展了 Node
的类的操作,例如 Attr
。
这意味着如果你要将它们实现为 abstract
,你必须将 Node
共同的部分包含在所有抽象方法的子类实现中。真痛苦。
所以,我们有了像 _subclass_isEqualNode
这样的抽象方法,当需要执行子类特定的部分时,由 Node::isEqualNode
调用。
其他可读性约定
- 如果属性访问器或方法是规范的一部分,它将按照规范 IDL 中的写法来写(自然是如此)。
- 如果属性或方法是内部使用的,它将以 '_' 为前缀。
Domino.js 中的潜在错误
看起来,当 Element 的 id
或 name
属性改变时,HTMLCollection 不会重新计算缓存。然而,这些被用来索引两个内部缓存,所以 HTMLCollection 就不再“活跃”了。
解决方案是在这些属性被修改时更新 lastModTime
。
性能提示
- 确保你的 Element id 保持唯一。规范要求你返回具有该 id 的第一个 Element,按照文档顺序,计算文档顺序并不高效。
许可和致谢
此代码的初始版本由 Jason Linehan 编写。进一步的改进由 C. Scott Ananian(IDLeDOM,错误修复,缺失功能)和 Tim Abdullin(测试套件)完成。
此代码受 (c) 版权保护 2019-2021 维基媒体基金会。它根据 MIT 许可证分发;有关更多信息,请参阅 LICENSE。