diff --git a/.htaccess b/.htaccess index af418c46d96ca8fd5b8e8dafdb516aa5c2cfba0f..73ce26b13a554ee09e78fb7fde13d03eb595805a 100644 --- a/.htaccess +++ b/.htaccess @@ -28,13 +28,14 @@ DirectoryIndex index.php index.html index.htm AddType image/svg+xml svg svgz AddEncoding gzip svgz -# Override PHP settings that cannot be changed at runtime. See +# Most of the following PHP settings cannot be changed at runtime. See # sites/default/default.settings.php and # Drupal\Core\DrupalKernel::bootEnvironment() for settings that can be # changed at runtime. # PHP 5, Apache 1 and 2. + php_value assert.active 0 php_flag session.auto_start off php_value mbstring.http_input pass php_value mbstring.http_output pass diff --git a/core/core.api.php b/core/core.api.php index 63fd607ba7f11abacfbba414723f4ef052809670..c34ca0ec148ff217516b66546e7a85bc2c49e952 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -57,6 +57,7 @@ * - @link queue Queue API @endlink * - @link typed_data Typed Data @endlink * - @link testing Automated tests @endlink + * - @link php_assert PHP Runtime Assert Statements @endlink * - @link third_party Integrating third-party applications @endlink * * @section more_info Further information @@ -982,6 +983,55 @@ * @} */ +/** + * @defgroup php_assert PHP Runtime Assert Statements + * @{ + * Use of the assert() statement in Drupal. + * + * Unit tests also use the term "assertion" to refer to test conditions, so to + * avoid confusion the term "runtime assertion" will be used for the assert() + * statement throughout the documentation. + * + * A runtime assertion is a statement that is expected to always be true at + * the point in the code it appears at. They are tested using PHP's internal + * @link http://www.php.net/assert assert() @endlink statement. If an + * assertion is ever FALSE it indicates an error in the code or in module or + * theme configuration files. User-provided configuration files should be + * verified with standard control structures at all times, not just checked in + * development environments with assert() statements on. + * + * When runtime assertions fail in PHP 7 an \AssertionException is thrown. + * Drupal uses an assertion callback to do the same in PHP 5.x so that unit + * tests involving runtime assertions will work uniformly across both versions. + * + * The Drupal project primarily uses runtime assertions to enforce the + * expectations of the API by failing when incorrect calls are made by code + * under development. While PHP type hinting does this for objects and arrays, + * runtime assertions do this for scalars (strings, integers, floats, etc.) and + * complex data structures such as cache and render arrays. They ensure that + * methods' return values are the documented datatypes. They also verify that + * objects have been properly configured and set up by the service container. + * Runtime assertions are checked throughout development. They supplement unit + * tests by checking scenarios that do not have unit tests written for them, + * and by testing the API calls made by all the code in the system. + * + * When using assert() keep the following in mind: + * - Runtime assertions are disabled by default in production and enabled in + * development, so they can't be used as control structures. Use exceptions + * for errors that can occur in production no matter how unlikely they are. + * - Assert() functions in a buggy manner prior to PHP 7. If you do not use a + * string for the first argument of the statement but instead use a function + * call or expression then that code will be evaluated even when runtime + * assertions are turned off. To avoid this you must use a string as the + * first argument, and assert will pass this string to the eval() statement. + * - Since runtime assertion strings are parsed by eval() use caution when + * using them to work with data that may be unsanitized. + * + * See https://www.drupal.org/node/2492225 for more information on runtime + * assertions. + * @} + */ + /** * @defgroup info_types Information types * @{ diff --git a/core/lib/Drupal/Component/Assertion/Inspector.php b/core/lib/Drupal/Component/Assertion/Inspector.php new file mode 100644 index 0000000000000000000000000000000000000000..cb17632b6c132389069af224a6e512f70d05096e --- /dev/null +++ b/core/lib/Drupal/Component/Assertion/Inspector.php @@ -0,0 +1,409 @@ + 0) { + foreach ($args as $instance) { + if ($member instanceof $instance) { + // We're continuing to the next member on the outer loop. + // @see http://php.net/continue + continue 2; + } + } + return FALSE; + } + elseif (!is_object($member)) { + return FALSE; + } + } + return TRUE; + } + return FALSE; + } + +} diff --git a/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php b/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..55b09d8d57f2d0860d2cb152b6c1fc032856273b --- /dev/null +++ b/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php @@ -0,0 +1,241 @@ +assertTrue(Inspector::assertTraversable([])); + $this->assertTrue(Inspector::assertTraversable(new \ArrayObject())); + $this->assertFalse(Inspector::assertTraversable(new \stdClass())); + $this->assertFalse(Inspector::assertTraversable('foo')); + } + + /** + * Tests asserting all members are strings. + * + * @covers ::assertAllStrings + */ + public function testAssertAllStrings() { + $this->assertTrue(Inspector::assertAllStrings([])); + $this->assertTrue(Inspector::assertAllStrings(['foo', 'bar'])); + $this->assertFalse(Inspector::assertAllStrings('foo')); + $this->assertFalse(Inspector::assertAllStrings(['foo', new StringObject()])); + } + + /** + * Tests asserting all members are strings or objects with __toString(). + * + * @covers ::assertAllStringable + */ + public function testAssertAllStringable() { + $this->assertTrue(Inspector::assertAllStringable([])); + $this->assertTrue(Inspector::assertAllStringable(['foo', 'bar'])); + $this->assertFalse(Inspector::assertAllStringable('foo')); + $this->assertTrue(Inspector::assertAllStringable(['foo', new StringObject()])); + } + + /** + * Tests asserting all members are arrays. + * + * @covers ::assertAllArrays + */ + public function testAssertAllArrays() { + $this->assertTrue(Inspector::assertAllArrays([])); + $this->assertTrue(Inspector::assertAllArrays([[], []])); + $this->assertFalse(Inspector::assertAllArrays([[], 'foo'])); + } + + /** + * Tests asserting array is 0-indexed - the strict definition of array. + * + * @covers ::assertStrictArray + */ + public function testAssertStrictArray() { + $this->assertTrue(Inspector::assertStrictArray([])); + $this->assertTrue(Inspector::assertStrictArray(['bar', 'foo'])); + $this->assertFalse(Inspector::assertStrictArray(['foo' => 'bar', 'bar' => 'foo'])); + } + + /** + * Tests asserting all members are strict arrays. + * + * @covers ::assertAllStrictArrays + */ + public function testAssertAllStrictArrays() { + $this->assertTrue(Inspector::assertAllStrictArrays([])); + $this->assertTrue(Inspector::assertAllStrictArrays([[], []])); + $this->assertFalse(Inspector::assertAllStrictArrays([['foo' => 'bar', 'bar' => 'foo']])); + } + + /** + * Tests asserting all members have specified keys. + * + * @covers ::assertAllHaveKey + */ + public function testAssertAllHaveKey() { + $this->assertTrue(Inspector::assertAllHaveKey([])); + $this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']])); + $this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'foo')); + $this->assertTrue(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo')); + $this->assertFalse(Inspector::assertAllHaveKey([['foo' => 'bar', 'bar' => 'foo']], 'bar', 'foo', 'moo')); + } + + /** + * Tests asserting all members are integers. + * + * @covers ::assertAllIntegers + */ + public function testAssertAllIntegers() { + $this->assertTrue(Inspector::assertAllIntegers([])); + $this->assertTrue(Inspector::assertAllIntegers([1, 2, 3])); + $this->assertFalse(Inspector::assertAllIntegers([1, 2, 3.14])); + $this->assertFalse(Inspector::assertAllIntegers([1, '2', 3])); + } + + /** + * Tests asserting all members are floating point variables. + * + * @covers ::assertAllFloat + */ + public function testAssertAllFloat() { + $this->assertTrue(Inspector::assertAllFloat([])); + $this->assertTrue(Inspector::assertAllFloat([1.0, 2.1, 3.14])); + $this->assertFalse(Inspector::assertAllFloat([1, 2.1, 3.14])); + $this->assertFalse(Inspector::assertAllFloat([1.0, '2', 3])); + $this->assertFalse(Inspector::assertAllFloat(['Titanic'])); + } + + /** + * Tests asserting all members are callable. + * + * @covers ::assertAllCallable + */ + public function testAllCallable() { + $this->assertTrue(Inspector::assertAllCallable([ + 'strchr', + [$this, 'callMe'], + [__CLASS__, 'callMeStatic'], + function() { + return TRUE; + } + ])); + + $this->assertFalse(Inspector::assertAllCallable([ + 'strchr', + [$this, 'callMe'], + [__CLASS__, 'callMeStatic'], + function() { + return TRUE; + }, + "I'm not callable" + ])); + } + + /** + * Tests asserting all members are !empty(). + * + * @covers ::assertAllNotEmpty + */ + public function testAllNotEmpty() { + $this->assertTrue(Inspector::assertAllNotEmpty([1, 'two'])); + $this->assertFalse(Inspector::assertAllNotEmpty([''])); + } + + /** + * Tests asserting all arguments are numbers or strings castable to numbers. + * + * @covers ::assertAllNumeric + */ + public function testAssertAllNumeric() { + $this->assertTrue(Inspector::assertAllNumeric([1, '2', 3.14])); + $this->assertFalse(Inspector::assertAllNumeric([1, 'two', 3.14])); + } + + /** + * Tests asserting strstr() or stristr() match. + * + * @covers ::assertAllMatch + */ + public function testAssertAllMatch() { + $this->assertTrue(Inspector::assertAllMatch('f', ['fee', 'fi', 'fo'])); + $this->assertTrue(Inspector::assertAllMatch('F', ['fee', 'fi', 'fo'])); + $this->assertTrue(Inspector::assertAllMatch('f', ['fee', 'fi', 'fo'], TRUE)); + $this->assertFalse(Inspector::assertAllMatch('F', ['fee', 'fi', 'fo'], TRUE)); + $this->assertFalse(Inspector::assertAllMatch('e', ['fee', 'fi', 'fo'])); + $this->assertFalse(Inspector::assertAllMatch('1', [12])); + } + + /** + * Tests asserting regular expression match. + * + * @covers ::assertAllRegularExpressionMatch + */ + public function testAssertAllRegularExpressionMatch() { + $this->assertTrue(Inspector::assertAllRegularExpressionMatch('/f/i', ['fee', 'fi', 'fo'])); + $this->assertTrue(Inspector::assertAllRegularExpressionMatch('/F/i', ['fee', 'fi', 'fo'])); + $this->assertTrue(Inspector::assertAllRegularExpressionMatch('/f/', ['fee', 'fi', 'fo'])); + $this->assertFalse(Inspector::assertAllRegularExpressionMatch('/F/', ['fee', 'fi', 'fo'])); + $this->assertFalse(Inspector::assertAllRegularExpressionMatch('/e/', ['fee', 'fi', 'fo'])); + $this->assertFalse(Inspector::assertAllRegularExpressionMatch('/1/', [12])); + } + + /** + * Tests asserting all members are objects. + * + * @covers ::assertAllObjects + */ + public function testAssertAllObjects() { + $this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject()])); + $this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject(), 'foo'])); + $this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject()], '\\Traversable')); + $this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new \ArrayObject(), 'foo'], '\\Traversable')); + $this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new StringObject()], '\\Traversable')); + $this->assertTrue(Inspector::assertAllObjects([new \ArrayObject(), new StringObject()], '\\Traversable', '\\Drupal\\Tests\\Component\\Assertion\\StringObject')); + $this->assertFalse(Inspector::assertAllObjects([new \ArrayObject(), new StringObject(), new \stdClass()], '\\ArrayObject', '\\Drupal\\Tests\\Component\\Assertion\\StringObject')); + } + + /** + * Test method referenced by ::testAllCallable(). + */ + public function callMe() { + return TRUE; + } + + /** + * Test method referenced by ::testAllCallable(). + */ + public static function callMeStatic() { + return TRUE; + } + +} + +/** + * Quick class for testing for objects with __toString. + */ +class StringObject { + /** + * {@inheritdoc} + */ + public function __toString() { + return 'foo'; + } + +} diff --git a/sites/example.settings.local.php b/sites/example.settings.local.php index 56fed6f2c50bd7ca1f722ce19452d2eb9fb2ca5a..34b4e191c193c6f614627cc36dfd2b663798496e 100644 --- a/sites/example.settings.local.php +++ b/sites/example.settings.local.php @@ -11,6 +11,24 @@ * mention 'settings.local.php'. */ +/** + * Assertions. + * + * The Drupal project primarily uses runtime assertions to enforce the + * expectations of the API by failing when incorrect calls are made by code + * under development. + * + * @see http://php.net/assert + * @see https://www.drupal.org/node/2492225 + * + * If you are using PHP 7.0 it is strongly recommended that you set + * zend.assertions=1 in the PHP.ini file (It cannot be changed from .htaccess + * or runtime) on development machines and to 0 in production. + * + * @see https://wiki.php.net/rfc/expectations + */ +assert_options(ASSERT_ACTIVE, 1); + /** * Enable local development services. */