Skip to content

Add support for ParentNode::$children #18908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ PHP NEWS
- DOM:
. Added Dom\Element::$outerHTML. (nielsdos)
. Added Dom\Element::insertAdjacentHTML(). (nielsdos)
. Added $children property to ParentNode implementations. (nielsdos)

- Enchant:
. Added enchant_dict_remove_from_session(). (nielsdos)
Expand Down
1 change: 1 addition & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ PHP 8.5 UPGRADE NOTES

- DOM:
. Added Dom\Element::$outerHTML.
. Added $children property to Dom\ParentNode implementations.

- EXIF:
. Add OffsetTime* Exif tags.
Expand Down
1 change: 1 addition & 0 deletions ext/dom/dom_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ zend_result dom_entity_reference_child_nodes_read(dom_object *obj, zval *retval)
zend_result dom_namednodemap_length_read(dom_object *obj, zval *retval);

/* parent node properties */
zend_result dom_parent_node_children_read(dom_object *obj, zval *retval);
zend_result dom_parent_node_first_element_child_read(dom_object *obj, zval *retval);
zend_result dom_parent_node_last_element_child_read(dom_object *obj, zval *retval);
zend_result dom_parent_node_child_element_count(dom_object *obj, zval *retval);
Expand Down
19 changes: 11 additions & 8 deletions ext/dom/element.c
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,21 @@ zend_result dom_element_class_name_write(dom_object *obj, zval *newval)
}
/* }}} */

zval *dom_element_class_list_zval(dom_object *obj)
zval *dom_get_prop_checked_offset(dom_object *obj, uint32_t offset, const char *name)
{
const uint32_t PROP_INDEX = 0;

#if ZEND_DEBUG
zend_string *class_list_str = ZSTR_INIT_LITERAL("classList", false);
const zend_property_info *prop_info = zend_get_property_info(dom_modern_element_class_entry, class_list_str, 0);
zend_string_release_ex(class_list_str, false);
ZEND_ASSERT(OBJ_PROP_TO_NUM(prop_info->offset) == PROP_INDEX);
zend_string *name_zstr = ZSTR_INIT_LITERAL(name, false);
const zend_property_info *prop_info = zend_get_property_info(obj->std.ce, name_zstr, 0);
zend_string_release_ex(name_zstr, false);
ZEND_ASSERT(OBJ_PROP_TO_NUM(prop_info->offset) == offset);
#endif

return OBJ_PROP_NUM(&obj->std, PROP_INDEX);
return OBJ_PROP_NUM(&obj->std, offset);
}

zval *dom_element_class_list_zval(dom_object *obj)
{
return dom_get_prop_checked_offset(obj, 1, "classList");
}

/* {{{ classList TokenList
Expand Down
40 changes: 36 additions & 4 deletions ext/dom/html_collection.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,23 @@ static dom_named_item dom_html_collection_named_item(zend_string *key, zend_obje
zend_long cur = 0;
zend_long next = cur; /* not +1, otherwise we skip the first candidate */
xmlNodePtr candidate = basep->children;
bool iterate_tag_name = objmap->handler == &php_dom_obj_map_by_tag_name;
while (candidate != NULL) {
candidate = dom_get_elements_by_tag_name_ns_raw(basep, candidate, objmap->ns, objmap->local, objmap->local_lower, &cur, next);
if (candidate == NULL) {
break;
if (iterate_tag_name) {
candidate = dom_get_elements_by_tag_name_ns_raw(basep, candidate, objmap->ns, objmap->local, objmap->local_lower, &cur, next);
if (candidate == NULL) {
break;
}
next = cur + 1;
} else {
if (candidate->type != XML_ELEMENT_NODE) {
candidate = candidate->next;
continue;
}
}

ZEND_ASSERT(candidate->type == XML_ELEMENT_NODE);

xmlAttrPtr attr;

/* it has an ID which is key; */
Expand All @@ -73,7 +84,9 @@ static dom_named_item dom_html_collection_named_item(zend_string *key, zend_obje
}
}

next = cur + 1;
if (!iterate_tag_name) {
candidate = candidate->next;
}
}
}

Expand Down Expand Up @@ -141,4 +154,23 @@ int dom_html_collection_has_dimension(zend_object *object, zval *member, int che
}
}

HashTable *dom_html_collection_get_gc(zend_object *object, zval **table, int *n)
{
dom_nnodemap_object *objmap = php_dom_obj_from_obj(object)->ptr;

if (objmap->baseobj) {
zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
zend_get_gc_buffer_add_obj(gc_buffer, &objmap->baseobj->std);
zend_get_gc_buffer_use(gc_buffer, table, n);

if (object->properties == NULL && object->ce->default_properties_count == 0) {
return NULL;
} else {
return zend_std_get_properties(object);
}
} else {
return zend_std_get_gc(object, table, n);
}
}

#endif
1 change: 1 addition & 0 deletions ext/dom/html_collection.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@

zval *dom_html_collection_read_dimension(zend_object *object, zval *offset, int type, zval *rv);
int dom_html_collection_has_dimension(zend_object *object, zval *member, int check_empty);
HashTable *dom_html_collection_get_gc(zend_object *object, zval **table, int *n);

#endif
11 changes: 1 addition & 10 deletions ext/dom/html_document.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,7 @@ typedef struct dom_decoding_encoding_ctx {
/* https://dom.spec.whatwg.org/#dom-document-implementation */
zend_result dom_modern_document_implementation_read(dom_object *obj, zval *retval)
{
const uint32_t PROP_INDEX = 0;

#if ZEND_DEBUG
zend_string *implementation_str = ZSTR_INIT_LITERAL("implementation", false);
const zend_property_info *prop_info = zend_get_property_info(dom_abstract_base_document_class_entry, implementation_str, 0);
zend_string_release_ex(implementation_str, false);
ZEND_ASSERT(OBJ_PROP_TO_NUM(prop_info->offset) == PROP_INDEX);
#endif

zval *cached_implementation = OBJ_PROP_NUM(&obj->std, PROP_INDEX);
zval *cached_implementation = dom_get_prop_checked_offset(obj, 1, "implementation");
if (Z_ISUNDEF_P(cached_implementation)) {
php_dom_create_implementation(cached_implementation, true);
}
Expand Down
60 changes: 56 additions & 4 deletions ext/dom/obj_map.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@
static zend_always_inline void objmap_cache_release_cached_obj(dom_nnodemap_object *objmap)
{
if (objmap->cached_obj) {
/* Since the DOM is a tree there can be no cycles. */
if (GC_DELREF(&objmap->cached_obj->std) == 0) {
zend_objects_store_del(&objmap->cached_obj->std);
}
OBJ_RELEASE(&objmap->cached_obj->std);
objmap->cached_obj = NULL;
objmap->cached_obj_index = 0;
}
Expand Down Expand Up @@ -82,6 +79,20 @@ static zend_long dom_map_get_nodes_length(dom_nnodemap_object *map)
return count;
}

static zend_long dom_map_get_elements_length(dom_nnodemap_object *map)
{
zend_long count = 0;
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
if (nodep) {
for (xmlNodePtr curnode = dom_nodelist_iter_start_first_child(nodep); curnode; curnode = curnode->next) {
if (curnode->type == XML_ELEMENT_NODE) {
count++;
}
}
}
return count;
}

static zend_long dom_map_get_by_tag_name_length(dom_nnodemap_object *map)
{
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
Expand Down Expand Up @@ -223,6 +234,38 @@ static void dom_map_get_nodes_item(dom_nnodemap_object *map, zend_long index, zv
}
}

static void dom_map_get_elements_item(dom_nnodemap_object *map, zend_long index, zval *return_value)
{
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
xmlNodePtr itemnode = NULL;
if (nodep && index >= 0) {
dom_node_idx_pair start_point = dom_obj_map_get_start_point(map, nodep, index);
if (start_point.node) {
/* Guaranteed to be an element */
itemnode = start_point.node;
} else {
/* Fetch first element child */
itemnode = nodep->children;
while (itemnode && itemnode->type != XML_ELEMENT_NODE) {
itemnode = itemnode->next;
}
}

for (; start_point.index > 0 && itemnode; --start_point.index) {
do {
itemnode = itemnode->next;
} while (itemnode && itemnode->type != XML_ELEMENT_NODE);
}
if (itemnode && itemnode->type != XML_ELEMENT_NODE) {
itemnode = NULL;
}
}
dom_ret_node_to_zobj(map, itemnode, return_value);
if (itemnode) {
dom_map_cache_obj(map, itemnode, index, return_value);
}
}

static void dom_map_get_by_tag_name_item(dom_nnodemap_object *map, zend_long index, zval *return_value)
{
xmlNodePtr nodep = dom_object_get_node(map->baseobj);
Expand Down Expand Up @@ -456,6 +499,15 @@ const php_dom_obj_map_handler php_dom_obj_map_notations = {
.nameless = false,
};

const php_dom_obj_map_handler php_dom_obj_map_child_elements = {
.length = dom_map_get_elements_length,
.get_item = dom_map_get_elements_item,
.get_named_item = dom_map_get_named_item_null,
.has_named_item = dom_map_has_named_item_null,
.use_cache = true,
.nameless = true,
};

const php_dom_obj_map_handler php_dom_obj_map_noop = {
.length = dom_map_get_zero_length,
.get_item = dom_map_get_null_item,
Expand Down
1 change: 1 addition & 0 deletions ext/dom/obj_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ zend_long php_dom_get_nodelist_length(dom_object *obj);

extern const php_dom_obj_map_handler php_dom_obj_map_attributes;
extern const php_dom_obj_map_handler php_dom_obj_map_by_tag_name;
extern const php_dom_obj_map_handler php_dom_obj_map_child_elements;
extern const php_dom_obj_map_handler php_dom_obj_map_child_nodes;
extern const php_dom_obj_map_handler php_dom_obj_map_nodeset;
extern const php_dom_obj_map_handler php_dom_obj_map_entities;
Expand Down
25 changes: 25 additions & 0 deletions ext/dom/parentnode/tree.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,34 @@
#include "php.h"
#if defined(HAVE_LIBXML) && defined(HAVE_DOM)
#include "../php_dom.h"
#include "../obj_map.h"
#include "../internal_helpers.h"
#include "../dom_properties.h"

zval *dom_parent_node_children(dom_object *obj)
{
return dom_get_prop_checked_offset(obj, 0, "children");
}

zend_result dom_parent_node_children_read(dom_object *obj, zval *retval)
{
zval *cached_children = dom_parent_node_children(obj);
if (Z_ISUNDEF_P(cached_children)) {
object_init_ex(cached_children, dom_html_collection_class_entry);
php_dom_create_obj_map(obj, Z_DOMOBJ_P(cached_children), NULL, NULL, NULL, &php_dom_obj_map_child_elements);

/* Handle cycles for potential TMPVARs (could also be CV but we can't differentiate).
* RC == 2 because of 1 TMPVAR and 1 in HTMLCollection. */
if (GC_REFCOUNT(&obj->std) == 2) {
gc_possible_root(Z_COUNTED_P(cached_children));
}
}

ZVAL_OBJ_COPY(retval, Z_OBJ_P(cached_children));

return SUCCESS;
}

/* {{{ firstElementChild DomParentNode
readonly=yes
URL: https://www.w3.org/TR/dom/#dom-parentnode-firstelementchild
Expand Down
40 changes: 35 additions & 5 deletions ext/dom/php_dom.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ PHP_DOM_EXPORT zend_class_entry *dom_namespace_info_class_entry;
static zend_object_handlers dom_object_handlers;
static zend_object_handlers dom_nnodemap_object_handlers;
static zend_object_handlers dom_nodelist_object_handlers;
static zend_object_handlers dom_unset_children_property_object_handlers;
static zend_object_handlers dom_modern_nnodemap_object_handlers;
static zend_object_handlers dom_modern_nodelist_object_handlers;
static zend_object_handlers dom_html_collection_object_handlers;
Expand Down Expand Up @@ -668,14 +669,35 @@ static zend_object *dom_objects_store_clone_obj(zend_object *zobject) /* {{{ */
static zend_object *dom_modern_element_clone_obj(zend_object *zobject)
{
zend_object *clone = dom_objects_store_clone_obj(zobject);
dom_object *intern = php_dom_obj_from_obj(clone);

/* The $classList property is unique per element, and cached due to its [[SameObject]] requirement.
* Remove it from the clone so the clone will get a fresh instance upon demand. */
zval *class_list = dom_element_class_list_zval(php_dom_obj_from_obj(clone));
zval *class_list = dom_element_class_list_zval(intern);
if (!Z_ISUNDEF_P(class_list)) {
zval_ptr_dtor(class_list);
ZVAL_UNDEF(class_list);
}
/* Likewise for $children */
zval *children = dom_parent_node_children(intern);
if (!Z_ISUNDEF_P(children)) {
zval_ptr_dtor(children);
ZVAL_UNDEF(children);
}

return clone;
}

static zend_object *dom_clone_obj_unset_children_property(zend_object *zobject)
{
zend_object *clone = dom_objects_store_clone_obj(zobject);
dom_object *intern = php_dom_obj_from_obj(clone);

zval *children = dom_parent_node_children(intern);
if (!Z_ISUNDEF_P(children)) {
zval_ptr_dtor(children);
ZVAL_UNDEF(children);
}

return clone;
}
Expand Down Expand Up @@ -777,6 +799,9 @@ PHP_MINIT_FUNCTION(dom)
memcpy(&dom_modern_element_object_handlers, &dom_object_handlers, sizeof(zend_object_handlers));
dom_modern_element_object_handlers.clone_obj = dom_modern_element_clone_obj;

memcpy(&dom_unset_children_property_object_handlers, &dom_object_handlers, sizeof(zend_object_handlers));
dom_unset_children_property_object_handlers.clone_obj = dom_clone_obj_unset_children_property;

memcpy(&dom_nnodemap_object_handlers, &dom_object_handlers, sizeof(zend_object_handlers));
dom_nnodemap_object_handlers.free_obj = dom_nnodemap_objects_free_storage;
dom_nnodemap_object_handlers.read_dimension = dom_nodemap_read_dimension;
Expand All @@ -797,6 +822,8 @@ PHP_MINIT_FUNCTION(dom)
memcpy(&dom_html_collection_object_handlers, &dom_modern_nodelist_object_handlers, sizeof(zend_object_handlers));
dom_html_collection_object_handlers.read_dimension = dom_html_collection_read_dimension;
dom_html_collection_object_handlers.has_dimension = dom_html_collection_has_dimension;
dom_html_collection_object_handlers.get_gc = dom_html_collection_get_gc;
dom_html_collection_object_handlers.clone_obj = NULL;

memcpy(&dom_object_namespace_node_handlers, &dom_object_handlers, sizeof(zend_object_handlers));
dom_object_namespace_node_handlers.offset = XtOffsetOf(dom_object_namespace_node, dom.std);
Expand Down Expand Up @@ -911,9 +938,10 @@ PHP_MINIT_FUNCTION(dom)

dom_modern_documentfragment_class_entry = register_class_Dom_DocumentFragment(dom_modern_node_class_entry, dom_modern_parentnode_class_entry);
dom_modern_documentfragment_class_entry->create_object = dom_objects_new;
dom_modern_documentfragment_class_entry->default_object_handlers = &dom_object_handlers;
dom_modern_documentfragment_class_entry->default_object_handlers = &dom_unset_children_property_object_handlers;
zend_hash_init(&dom_modern_documentfragment_prop_handlers, 0, NULL, NULL, true);

DOM_REGISTER_PROP_HANDLER(&dom_modern_documentfragment_prop_handlers, "children", dom_parent_node_children_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_documentfragment_prop_handlers, "firstElementChild", dom_parent_node_first_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_documentfragment_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_documentfragment_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL);
Expand All @@ -922,7 +950,7 @@ PHP_MINIT_FUNCTION(dom)
zend_hash_add_new_ptr(&classes, dom_modern_documentfragment_class_entry->name, &dom_modern_documentfragment_prop_handlers);

dom_abstract_base_document_class_entry = register_class_Dom_Document(dom_modern_node_class_entry, dom_modern_parentnode_class_entry);
dom_abstract_base_document_class_entry->default_object_handlers = &dom_object_handlers;
dom_abstract_base_document_class_entry->default_object_handlers = &dom_unset_children_property_object_handlers;
zend_hash_init(&dom_abstract_base_document_prop_handlers, 0, NULL, NULL, true);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "implementation", dom_modern_document_implementation_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "URL", dom_document_document_uri_read, dom_document_document_uri_write);
Expand All @@ -932,6 +960,7 @@ PHP_MINIT_FUNCTION(dom)
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "inputEncoding", dom_document_encoding_read, dom_html_document_encoding_write);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "doctype", dom_document_doctype_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "documentElement", dom_document_document_element_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "children", dom_parent_node_children_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "firstElementChild", dom_parent_node_first_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_abstract_base_document_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL);
Expand Down Expand Up @@ -1118,6 +1147,7 @@ PHP_MINIT_FUNCTION(dom)
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "className", dom_element_class_name_read, dom_element_class_name_write);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "classList", dom_element_class_list_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "attributes", dom_node_attributes_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "children", dom_parent_node_children_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "firstElementChild", dom_parent_node_first_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "lastElementChild", dom_parent_node_last_element_child_read, NULL);
DOM_REGISTER_PROP_HANDLER(&dom_modern_element_prop_handlers, "childElementCount", dom_parent_node_child_element_count, NULL);
Expand Down Expand Up @@ -1534,8 +1564,8 @@ void dom_nnodemap_objects_free_storage(zend_object *object) /* {{{ */
dom_nnodemap_object *objmap = (dom_nnodemap_object *)intern->ptr;

if (objmap) {
if (objmap->cached_obj && GC_DELREF(&objmap->cached_obj->std) == 0) {
zend_objects_store_del(&objmap->cached_obj->std);
if (objmap->cached_obj) {
OBJ_RELEASE(&objmap->cached_obj->std);
}
if (objmap->release_local) {
dom_zend_string_release_from_char_pointer(objmap->local);
Expand Down
4 changes: 4 additions & 0 deletions ext/dom/php_dom.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ bool php_dom_create_nullable_object(xmlNodePtr obj, zval *return_value, dom_obje
xmlNodePtr dom_clone_node(php_dom_libxml_ns_mapper *ns_mapper, xmlNodePtr node, xmlDocPtr doc, bool recursive);
void dom_set_document_ref_pointers(xmlNodePtr node, php_libxml_ref_obj *document);
void dom_set_document_ref_pointers_attr(xmlAttrPtr attr, php_libxml_ref_obj *document);

/* Prop getters by offset */
zval *dom_get_prop_checked_offset(dom_object *obj, uint32_t offset, const char *name);
zval *dom_element_class_list_zval(dom_object *obj);
zval *dom_parent_node_children(dom_object *obj);

typedef enum {
DOM_LOAD_STRING = 0,
Expand Down
Loading