Skip to content

Commit

Permalink
Added support to <sch:let> , document() and updated tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rahal committed Dec 8, 2023
1 parent 36d8e5b commit d590637
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 6 deletions.
71 changes: 66 additions & 5 deletions src/Schematron.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ class Schematron
/** @var array[id => value] {@see self::findPhases()} */
protected $phases = array();

/* var array list of opened external DOMDOCUMENT and Xpath (to support document() in xpath ) */
protected $externals = [];




Expand Down Expand Up @@ -279,9 +282,47 @@ public function validate(DOMDocument $doc, $result = self::RESULT_SIMPLE, $phase
$pattern = $this->patterns[$patternKey];
foreach ($pattern->rules as $ruleKey => $rule) {
foreach ($xpath->queryContext($rule->context, $doc) as $currentNode) {
$lets = [];
if ($rule->lets) {
foreach ($rule->lets as $name => $value) {
$let = $xpath->evaluate("string($value)", $currentNode);
// Adding quotes is necessary to be able search strings
// TODO : maybe escape?
$lets[$name] = is_numeric($let) ? $let : "'$let'";
}
}
foreach ($rule->statements as $statement) {
if ($statement->isAssert ^ $xpath->evaluate("boolean($statement->test)", $currentNode)) {
$message = $this->statementToMessage($statement->node, $xpath, $currentNode);
$testStatement = $statement->test ;
$nodeToEval = $currentNode;
$xpathToEval = $xpath;
if ($lets) {
$testStatement = call_user_func($this->getReplaceCb(), $testStatement, $lets);
}
// Added support to evaluate document()
// Maybe it should move to SchematronXPath, but we would have to deal with paths
// as neither DOMDocument or XPATH holds information about the file, so is it really worth the trouble?
$parts = explode('//', $testStatement);
if (count($parts) == 2) {
if ($parts[0]) {
if (strpos($parts[0], 'document(') == 0) {
$file = substr($parts[0], 10, -2);
if (!isset($this->externals[$file])) {
$external = new DOMDocument();
$external->load($this->directory.DIRECTORY_SEPARATOR.$file);
$this->externals[$file] = [
'node' => $external,
'xpath' => new DOMXPath($external)
];
}
$nodeToEval = $this->externals[$file]['node'];
$xpathToEval = $this->externals[$file]['xpath'];
$testStatement = "//".$parts[1];
}
}

}
if ($statement->isAssert ^ $xpathToEval->evaluate("boolean($testStatement)", $nodeToEval)) {
$message = $this->statementToMessage($statement->node, $xpath, $currentNode, $lets);

switch ($result) {
case self::RESULT_EXCEPTION:
Expand Down Expand Up @@ -739,6 +780,7 @@ protected function findRules(DOMElement $pattern)

$rules[] = (object) array(
'context' => $context,
'lets' => $this->findLets($element),
'statements' => $statements = $this->findStatements($element, $abstracts),
);

Expand Down Expand Up @@ -848,13 +890,28 @@ protected function findActives(DOMElement $phase)
return $actives;
}

/**
* Search for all <sch:let>.
* @return array
*/
protected function findLets(DOMElement $rule)
{
$variables = array();
foreach ($this->xPath->query('sch:let', $rule) as $node) {
$name = Helpers::getAttribute($node, 'name');
$value = Helpers::getAttribute($node, 'value');
$variables[$name] = $value;
}
return $variables;
}



/**
* Expands <sch:name> and <sch:value-of> in assertion/report message.
* @return string
*/
protected function statementToMessage(DOMElement $stmt, SchematronXPath $xPath, DOMNode $current)
protected function statementToMessage(DOMElement $stmt, SchematronXPath $xPath, DOMNode $current, $lets = array())
{
$message = '';
foreach ($stmt->childNodes as $node) {
Expand All @@ -863,7 +920,11 @@ protected function statementToMessage(DOMElement $stmt, SchematronXPath $xPath,
$message .= $xPath->evaluate('name(' . Helpers::getAttribute($node, 'path', '') . ')', $current);

} elseif ($node->localName === 'value-of') {
$message .= $xPath->evaluate('string(' . Helpers::getAttribute($node, 'select') . ')', $current);
$selected = Helpers::getAttribute($node, 'select');
if ($lets){
$selected = call_user_func($this->getReplaceCb(),$selected, $lets) ;
}
$message .= $xPath->evaluate('string(' . $selected . ')', $current);

} else {
/** @todo warning? */
Expand Down Expand Up @@ -1015,7 +1076,7 @@ public function evaluate($expression, DOMNode $context = NULL, $registerNodeNS =



public function queryContext($expression, DOMNode $context = NULL, $registerNodeNS = FALSE)
public function queryContext($expression, DOMNode $context = NULL, $registerNodeNS = FALSE): mixed
{
if (isset($expression[0]) && $expression[0] !== '.' && $expression[0] !== '/') {
$expression = "//$expression";
Expand Down
5 changes: 4 additions & 1 deletion tests/Schematron.validate.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ $simple = array(
'S13 - fail - milo',
'S14 - fail - name',
'S15 - fail',
'S17 - fail',
'S19 - fail',
);
Assert::same($simple, $sch->validate($doc));


# RESULT_COMPLEX
$complex = $sch->validate($doc, $sch::RESULT_COMPLEX);
Assert::same(count($complex), 3);
Assert::same(count($complex), 4);
Assert::true(isset($complex['#p1']->rules[0]->errors[0]->message));
Assert::same($complex['#p1']->rules[0]->errors[0]->message, 'S15 - fail');
Assert::same(reset($complex)->title, 'Pattern 1');
Assert::true(isset($complex['#let']->rules[0]->errors[0]->message));


# RESULT_EXCEPTION
Expand Down
7 changes: 7 additions & 0 deletions tests/resources/external-document.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<root>
<names>
<name value="milo"/>
<name value="rahal"/>
</names>
</root>
11 changes: 11 additions & 0 deletions tests/resources/validate-schema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,15 @@
</rule>
</pattern>

<pattern id="let">
<rule context="root">
<let name="milo" value="//person/name"/>
<assert test="//person/name = $milo">S16 - pass</assert>
<let name="nick" value="//a:nickname"/>
<assert test="//person/name = $nick">S17 - fail</assert>
<assert test="document('external-document.xml')//names/name[@value=$nick]">S18 - pass</assert>
<assert test="document('external-document.xml')//names/name[@value=$milo]">S19 - fail</assert>
</rule>
</pattern>

</schema>

0 comments on commit d590637

Please sign in to comment.