====== PHP RFC: Strict operators directive ====== * Version: 1.5 * Date: 2020-07-06 (first version: 2019-05-25) * Author: Arnold Daniels, * Status: Withdrawn * First Published at: http://wiki.php.net/rfc/strict_operators ===== Introduction ===== The rules PHP uses for type juggling with operators are complex, varying by operator as well as the types and values of the operands. This can lead to surprising results, where a logical statement can have an illogical result. See the "Motivating examples" section below for details. This RFC proposes a new directive ''%%strict_operators%%'', which limits the type juggling done by operators to avoid unexpected results. ===== Proposal ===== Add a new ''%%declare()%%'' directive ''%%strict_operators%%'', which accepts either ''%%0%%'' or ''%%1%%'' as the value to indicate that the strict_operators mode should be disabled or enabled. The default value for this directive is 0, i.e. strict_operators not enabled and there will be no change from the current behavior of PHP i.e. by default, PHP files do not use the new strict_operators rules. If strict_operators is enabled, the following stricter rules will be used; * Operators may perform type conversion, but not type juggling: * Type conversion is not based on the type of any of the operands * Type conversion is not based on the value of any of the operands * Operators will throw a ''%%TypeError%%'' for unsupported types In case an operator can work with several (or all) types, the operand types need to match as no type conversion will be done by those operators. The one exception is that [[http://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.1.2|widening primitive conversion]] is allowed for ''%%int%%'' to ''%%float%%''. When doing a operation with an ''%%int%%'' and a ''%%float%%'', the ''%%int%%'' will silently casted to a ''%%float%%''. Using ''strict_operators'' never changes the outcome of an operation. ===== Details for operands ===== This section details which types operands support, and what type juggling will be supported (if any) for them. ==== Arithmetic operators ==== Arithmetic operators ''%%+%%'', ''%%-%%'', ''%%*%%'', ''%%/%%'', ''%%%%%'', and ''%%**%%'' will only support ''%%int%%'' or ''%%float%%'' operands. Attempting to use an unsupported operand will throw a ''%%TypeError%%''. The ''%%+%%'' operator will still be available for arrays as union operator, requiring both values to be arrays. ==== Incrementing/Decrementing operators ==== The incrementing/decrementing operators ''%%++%%'' and ''%%--%%'' will only support ''%%int%%'' or ''%%float%%'' operands. Attempting to use an unsupported operand will throw a ''%%TypeError%%''. ==== Bitwise Operators ==== The bitwise operators ''%%&%%'', ''%%|%%'', ''%%^%%'', and ''%%~%%'' will support ''%%int%%'' and ''%%string%%'' operands . The type of both operands need to match. If the operands are of different types or when using an unsupported operand, a ''%%TypeError%%'' will be thrown. The bitwise shift operators ''%%>>%%'' and ''%%<<%%'' will only support ''%%int%%'' operands. Attempting to use an unsupported operand will throw a ''%%TypeError%%''. ==== Comparison operators ==== Enabling the strict_operators directive will not affect the identical (''%%===%%'') and not identical (''%%!==%%'') operators. All other comparison operators (''%%==%%'', ''%%!=%%'', ''%%<%%'', ''%%>%%'', ''%%<=%%'', ''%%>=%%'', ''%%<=>%%'') will only support ''%%int%%'' or ''%%float%%'' operands. When used with a ''%%bool%%'', ''%%string%%'', ''%%array%%'', ''%%object%%'', or ''%%resource%%'' operand, a ''%%TypeError%%'' will be thrown. Custom compare handlers of objects (like ''DateTime'' and ''gmp'') will be applied regardless of ''strict_operators''. The ''ZEND_COMPARE_OBJECTS_FALLBACK'' macro will throw a ''TypeError'' if two objects are compared with different handlers. ==== String concatenation ==== The concatenation operator ''%%.%%'' will only support concatenating ''%%null%%'', ''%%int%%'', ''%%float%%'', ''%%string%%'', and [[https://wiki.php.net/rfc/stringable|stringable object]] operands. If any of the operands is a ''%%bool%%'', ''%%array%%'', ''%%resource%%'', or non-stringable object, a ''%%TypeError%%''will be thrown. === String interpolation === When a string is specified in double quotes or with heredoc, variables are parsed within it. The string interpolation is performed with the same rules as for string concatenation. i.e. this code echo "He drank some $juice juice."; has the same rules as using the string concatenation operator. echo "He drank some " . $juice . " juice."; ==== Logical Operators ==== There a no changes to the logical operators ''%%&&%%'', ''%%||%%'', ''%%!%%'', ''%%and%%'', ''%%or%%'', and ''%%xor%%''. ==== Ternary / Null Coalescing Operator ==== There a no changes to the ternary (''%%?:%%'') and null coalescing (''%%??%%'') operator. ===== Motivating examples ===== The following section demonstrates code that is written logical, but has illogical results. ==== Mixed type comparison ==== The meaning of comparison operators currently change based on the type of each operand. Strings are compared as byte sequence. If one of the operands is an integer the operator performs a numeric comparison. This allows for statements that defy mathematical logic and may be experienced as unexpected behavior. $a = '42'; $b = 10; $c = '9 eur'; if (($a > $b) && ($b > $c) && ($c > $a)) { // Unexpected } ==== Numeric string comparison ==== Non-strict comparison uses a "smart" comparison method that treats strings as numbers if they are numeric. The meaning of the operator changes based on the value of both operands. This can lead to issues when numeric comparison is not expected, for example between two hexidecimal values. The hexidecimal value is instead interpreted as number with scientific notation. $red = '990000'; $purple = '9900e2'; $red == $purple; // true It may also cause issues with sorting, as the meaning of the comparison operators differers based on the operands (similar to mixed type comparison). function sorted(array $arr) { usort($arr, function($x, $y) { return $x <=> $y; }); return $arr; } sorted(['100', '5 eur', '62']); // ['100', '5 eur', '62'] sorted(['100', '62', '5 eur']); // ['5 eur', '62', '100'] sorted(['62', '100', '5 eur']); // ['62', '100', '5 eur'] ==== Array comparison ==== Using the ''%%>%%'', ''%%>=%%'', ''%%<%%'', ''%%<=%%'' and ''%%<=>%%'' operators on arrays or objects that don't have the same keys in the same order gives unexpected results. In the following example ''%%$a%%'' is both greater than and less than ''%%$b%%'' $a = ['x' => 1, 'y' => 22]; $b = ['y' => 10, 'x' => 15]; $a > $b; // true $a < $b; // true ==== Strict vs non-strict comparison of arrays ==== Strict comparison requires that arrays have keys occurring in the same order, while non-strict comparison allows out-of-order keys. ['a' => 'foo', 'b' => 'bar'] == ['b' => 'bar', 'a' => 0]; // true To compare the values of two arrays in a strict way while not concerned about the order, requires ordering the array by key prior to comparison. ==== Inconsistent behavior ==== Operators can do any of the following for unsupported operands * Cast (silent) * Cast with notice / warning * Cast with catchable error (fatal) * Operator specific notice / warning * Operator specific error (fatal) * No operation Please take a look at this [[https://gist.github.com/jasny/bfd711844a8876f8206ed21357e2e2da|list of all combinations of operators and operands]]. ===== Backward Incompatible Changes ===== None known. As this RFC proposes a new directive, it should only affect code is new or updated to use the strict_operators directive. ===== Proposed PHP Version ===== This is proposed for PHP 8.0. ===== Unaffected PHP Functionality ===== This RFC * Does not affect any functionality concerning explicit typecasting. * Is largely unaffected by other proposals like [[rfc/string_to_number_comparison|PHP RFC: Saner string to number comparisons]] that focus on improving type juggling at the cost of breaking BC. ===== FAQ ===== This RFC has an [[rfc/strict_operators/faq|FAQ]] that answers some questions * [[rfc/strict_operators/faq#what_has_been_changed_since_the_initial_proposal|What has been changed since the initial proposal?]] * [[rfc/strict_operators/faq#why_use_a_directive_instead_of_applying_this_behavior_as_default|Why use a directive instead of applying this behavior as default?]] * [[rfc/strict_operators/faq#why_does_and_only_support_int_and_float_operands|Why does == and != only support int and float operands?]] * [[rfc/strict_operators/faq#why_don_t_comparison_operators_support_strings|Why don't comparison operators support strings?]] * [[rfc/strict_operators/faq#why_does_the_concatenation_operator_cast_but_arithmetic_operators_don_t|Why does the concatenation operator cast, but arithmetic operators don't?]] * [[rfc/strict_operators/faq#will_comparing_a_number_to_a_numeric_string_work_with_strict_operators|Will comparing a number to a numeric string work with strict_operators?]] * [[rfc/strict_operators/faq#how_can_arrays_be_compared_as_unsorted_hashmaps|How can arrays be compared as unsorted hashmaps?]] * [[rfc/strict_operators/faq#how_can_objects_be_compared_by_property|How can objects be compared by property?]] * [[rfc/strict_operators/faq#why_isn_t_is_allowed_to_increment_strings_with_strict_operators|Why isn't is allowed to increment strings with strict_operators?]] * [[rfc/strict_operators/faq#are_built-in_functions_affected_by_strict_operators|Are built-in functions affected by strict_operators?]] * [[rfc/strict_operators/faq#can_relational_operators_be_allowed_for_arrays|Can relational operators be allowed for arrays?]] * [[rfc/strict_operators/faq#why_is_switch_not_affected|Why is switch not affected?]] * [[rfc/strict_operators/faq#are_there_cases_where_a_statement_doesn_t_throw_a_typeerror_but_yields_a_different_result|Are there cases where a statement doesn't throw a TypeError but yields a different result?]] * [[rfc/strict_operators/faq#will_this_directive_disable_type_juggling_altogether|Will this directive disable type juggling altogether?]] ===== Implementation ===== https://github.com/php/php-src/pull/4375 ===== Proposed Voting Choices ===== Primary vote: Accept the RFC and merge the patch? Yes/No. Requires a 2/3 majority.