rfc:expectations

This is an old revision of the document!


PHP RFC: Expectations

Introduction

The assertion statement has the prototype:

void assert (mixed $expression [, mixed $message]);

At execution time, expression will be evaluated, if the result is false, an AssertionException will be thrown.

In some cases, expression will be an expensive evaluation that you do not wish to execute in a production environment, assertions can therefore be disabled and enabled via the PHP_INI_ALL configuration setting zend.assertions. Disabling assertions will almost entirely eliminate the performance penalty making them equivalent to an empty statement.

In any case, assertions should never be used to perform tasks required for the code to function, nor should they change the internal state of any object except where that state is used only by other assertions, these are not rules that are enforced by Zend, but are nonetheless the best rules to follow.

If an object of a class which extends AssertionException is used for message, it will be thrown if the assertion fails, any other expression will be used as the message for the AssertionException. If no message is provided, the statement will be used as the message in AssertionException.

If expression is a constant string, compatibility with the old API is employed, the string is compiled and used as the expression.

Scope of Assertions

PHP programmers tend to document how their code is supposed to work in comments, this is a fine approach for generating automated documentation, but leaves us a little bewildered, and tired of digging through documentation at runtime when things go wrong:

    if ($i % 3 == 0) {
        ...
    } else if ($i % 3 == 1) {
        ...
    } else { // We know ($i % 3 == 2)
        ...
    }

Becomes:

    if ($i % 3 == 0) {
        ...
    } else if ($i % 3 == 1) {
        ...
    } else {
        assert ($i % 3 == 2);
    }

In a development environment, this forces the executor to make you aware of your mistake.

Another good example for using assertions might be a switch block with no default case:

switch ($suit) {
    case CLUBS:
        /* ... */
    break;
 
    case DIAMONDS:
        /* ... */
    break;
 
    case HEARTS:
        /* ... */
    break;
 
    case SPADES:
        /* ... */
    break;
}

The above switch assumes that suit can only be one of four values, to test this assumption add the default case:

switch ($suit) {
    case CLUBS:
        /* ... */
    break;
 
    case DIAMONDS:
        /* ... */
    break;
 
    case HEARTS:
        /* ... */
    break;
 
    case SPADES:
        /* ... */
    break;
 
    default:
        assert (false, "Unrecognized suit passed through switch: {$suit}");
}

The previous example highlights another general area where you should use assertions: place an assertion at any location you assume will not be reached. The statement to use is:

assert(false);

Suppose you have a method that looks like:

public function method() {
    for (/*...*/) {
 
        if (/* ... */)
           return true;
    }
 
}

The above code assumes that one of the iterations results in a return value being passed back to the caller of ::method(), to test this assumption:

public function method() {
    for (/*...*/) {
 
        if (/* ... */)
           return true;
    }
    assert(false);
}

Assertions allow the possibility to perform precondition and postcondition checks:

public function setResponseCode($code) {
    $this->code = $code;
}

Becomes:

public function setResponseCode($code) {
    assert($code < 550 && $code > 100, "Invalid response code provided: {$code}");
 
    $this->code = $code;
}

The example above performs a precondition check on the code parameter.

The same kind of logic can be applied to internal object state:

public function getResponseCode() {
    assert($this->code,"The response code is not yet set");
 
    return $this->code;
}

postcondition checks might also be carried out with assert:

public function getNext() {
    $data = $this->data[++$this->next];
 
    assert(preg_match("~^([a-zA-Z0-9-]+)$~", $data["key"]),
        "malformed key found at {$this->next} \"{$data["key"]}\"");
 
    return $data;
}

The above method during development would be verbose, not allowing the programmer to make a mistake, while during production where assertions should be disabled, it is fast.

Managing Failed Assertions

When an assertion fails, an AssertionException is thrown, these can be caught in the normal way, and come with a stack trace and a useful message about the assertions. An AssertionException extends ErrorException and has a severity of E_ERROR.

Here is an example of managing execution of an object whose methods use the assertion API:

<?php
$headers = [];
 
try {
   while (($header = $request->getHeader())) {
       /* ... */
       $headers[] = $header;
   }  
} catch (AssertionException $ex) {
   printf("Failed getting headers from Request: %s\n", $ex->getMessage());
   /* ... output stack trace in some more useful way perhaps ... */
   /* ... output some state and scope information ... */
   /* ... cleanup gracefully ... */
   exit(1);
}
?>

You can provide custom exceptions for failed assertions:

<?php
$next = 1;
$data = array(
    "key" => "X-HTTP ",
    "value" => "testing"
);
 
class HeaderMalfunctionException extends AssertionException {}
 
/* ... */
public function getData() {
    /* ... */
    assert(preg_match("~^([a-zA-Z0-9-]+)$~", $data["key"]),
        new HeaderMalfunctionException("malformed key found at {$next} \"{$data["key"]}\""));
    /* ... */
}
/* ... */
?>

This allows the caller of ::getData to manage more appropriately failed assertions.

Performance

In order to eliminate the performance penalty for assertions completely, Dmitry proposes the following:

We may turn zend.assertions into a three way switch (e.g. 1 - generate and execute code, 0 - generate code but jump around it, -1 - don't generate assertion code at all).

This is not yet implemented in the patch, but will be by one of us if we merge this patch.

Production Time

Assertions are a debugging and development feature; the programmer should not take code to production with catch blocks to manage AssertionExceptions; the ability to manage the AssertionExceptions exists during development in order to aid the programmer in debugging the exception, the only place where it can be raised.

Library code should not shy away from deploying Assertions everywhere, use it to literally assert what your code expects, rigorously, such that during development the programmer is made aware of every possible mistake before production arrives.

This means production library code does not have to manage inconsistencies in usage, because there should, theoretically, be none left; improving it's performance in production by not making those unnecessary checks that stem from inconsistent or incorrect usage.

prefix everything here with “when deployed and configured properly”

Backward Incompatible Changes

This API replaces the old assertion API in a compatible manner.

Proposed PHP Version(s)

I don't know why this section is suggested since the process is always the same; we vote on merging into master and RM's decide if they will merge into their release.

Impact to Existing Extensions

None that are obvious (or not taken care of by the patch), this does introduce a new opcode so anything working with opcodes may need adjustment.

Optimizer is impacted, and patched.

php.ini

  • zend.assertions
  • assert.exceptions

Assertions should be enabled (zend.assertions=1) on development machines, and disabled (zend.assertions=0) in production.

Exceptions should be enabled (assert.exceptions=1) on development machines.

These defaults can be set in the development and production ini files we distribute.

The hardcoded values are:

  • zend.assertions=1
  • assert.exceptions=0

zend.assertions is an INI_SYSTEM setting, allowing for the safe removal of assertion opcodes.

assert.exceptions is an INI_ALL setting, allowing for exceptions to be disabled at runtime.

Unaffected PHP Functionality

The current assertion API is unaffected by this addition.

Proposed Voting Choices

Simple

Patches and Tests

https://github.com/krakjoe/php-src/compare/expect

This is a working implementation of Assertions as documented here, with some appropriate tests.

References

Other Languages

  Java: http://docs.oracle.com/javase/1.4.2/docs/guide/lang/assert.html
      assert expression : message; evaluates Expression1 and if it is false throws an AssertionError with no detail message, takes message to constructor of AssertionError if present.

.NET (or this implementation for .NET) does not directly result in an exception, more like an exception in a message box, the important part is; it includes the call stack.

  .NET: http://msdn.microsoft.com/en-us/library/system.diagnostics.debug.assert.aspx
      Debug.Assert(expression, message): Checks for a condition; if the condition is false, outputs a specified message and displays a message box that shows the call stack.

Python's implementation is similar to Assertions also, but limited

  Python: http://docs.python.org/2/reference/simple_stmts.html
      assert expression raise AssertionError
  

Javascript has no standard implementation, yet; various implementations exist all the same:

  Chrome: https://developers.google.com/chrome-developer-tools/docs/console-api#consoleassertexpression_object
      console.assert(expression, object): If the specified expression is false, the message is written to the console along with a stack trace.
  
  Firefox (firebug): http://getfirebug.com/wiki/index.php/Console_API
      console.assert(expression[, object, ...]): Tests that an expression is true. If not, it will write a message to the console and throw an exception.
  
  Node.js: http://nodejs.org/api/stdio.html#stdio_console_assert_expression_message
      console.assert(expression, [message]): Same as assert.ok() where if the expression evaluates as false throw an AssertionError with message.

These implementations at least include a stack trace; a benefit of using exceptions for failed Assertions is that the stack trace is present by default.

Rejected Features

N/A

rfc/expectations.1391345438.txt.gz · Last modified: 2017/09/22 13:28 (external edit)