JonBlog
Thoughts on website ideas, PHP and other tech topics, plus going car-free
SimpleXML XPath selector: attribute not having suffix
Categories: PHP, XML

Today I had a requirement to match an XML path where a part of the path must have a specified attribute that does not have a specified suffix. Additionally, the solution must be in SimpleXML, and so cannot use the XPath function fn:ends-with, since that is part of XPath 2.0 and hence isn’t supported. Phew!

Here is some XML that illustrates my use case. I want to select any primary key columns from tables that are not versionable; in this example, just one column would be returned.

<database>
    <table name="event">
        <column name="id" type="integer" required="true" primaryKey="true" />
        <column name="name" type="varchar" size="50" required="true" />
        <column name="description" type="varchar" size="250" />
        <column name="location" type="varchar" size="250" />
    </table>
    <table name="event_versionable">
        <column name="id" type="integer" required="true" primaryKey="true" />
        <column name="name" type="varchar" size="50" required="true" />
        <column name="description" type="varchar" size="250" />
        <column name="location" type="varchar" size="250" />
    </table>
</database>

One simple solution is thus:

$suffix = '_versionable';
$match = "contains(@name, '$suffix')";
$search = "
    /database/table[not($match)]/column[@primaryKey=\"true\"]
";

$keys = $this->xml->xpath($search);

However that will also match columns in tables that have ‘_versionable’ somewhere in the name attribute other than at the end, which isn’t quite what is needed. Using this StackOverflow answer, I have come up with the following:

$suffix = '_versionable';
$match = "
    substring(
        @name,
        string-length(@name) - string-length('$suffix') + 1
    )
    = '$suffix'
";
$search = "
    /database/table[not($match)]/column[@primaryKey=\"true\"]
";

$keys = $this->xml->xpath($search);

Note that the answer on StackOverflow uses name(), which I think returns the tag name; I’ve used @name instead, as I want the value of the attribute called ‘name’.

Leave a Reply