Skip to content

Commit

Permalink
Proposal: Add println(string $data = ''): int
Browse files Browse the repository at this point in the history
What this does
--------------

This function behaves similarly to this userland code

```
function println(string $data = ''): int {
    return printf("%s\n", $data);
}
```

Similarly to `printf("%s\n", $data);`.

- `println` is NOT a keyword. (e.g. functions named println can continue to be
  declared outside of the global namespace)
- It returns the number of bytes that were successfully written to standard
  output. In the unlikely event that there was an error writing,
  this and printf return a smaller number.
- This deliberately always prints the unix newline (`\n`)
  **instead of PHP_EOL**.

  I would find it very unexpected if println were to behave
  differently based on the web server was running it,
  e.g. if you moved a website's backend from/to a linux server
  to/from a windows server, responses generated by `println` would
  suddenly be different.

  Additionally, https://www.php-fig.org/psr/psr-2/ recommends that all php
  source files contain unix line endings.
  If those files contain inline html/text snippets mixed with php+println(),
  it would be inconsistent to have `\r\n` in the lines printed by
  println() and `\n` anywhere else.

  This is same choice of line ending as var_dump, debug_zval_dump,
  and var_export use for dumping output.
  Otherwise, `println("myArray=" . var_export($myArray, true));`
  would be a mix of multiple line ending choices.

  Many new languages have elected to always use only the unix newlines,
  e.g. https://golang.org/pkg/fmt/#Println and
  https://doc.rust-lang.org/std/macro.println.html

  Overall, editors do a much better job of detecting newline choices and
  displaying different newline choices than they did decades ago.

  My opinion is that this anything generating files targeting a specific OS's
  line endings should continue to use PHP_EOL or continue to base
  the newline choice on the OS of the user requesting the output.

  This newline choice differs from the implementation PR for a similar proposal
  made 2 years ago https://externals.io/message/104545 ,
  for which an RFC was never written.

Differently from printf's argument list, echo, and print,
the argument $data is type checked based on the file's `strict_types` setting.
This is consistent with handling of $data in
`fwrite($stream, string $data): int` or the way format strings($format)
of `printf` are checked.

`println((string)$value)` should be used when strict_types=1 but you are uncertain
of the type.

Reasons to add this
-------------------

1. This is useful for self-contained scripts and a useful helper function to
   have overall. E.g. phpt tests of php itself print multiple lines for the
   `--EXPECT--` section, and var_dump can be overused even for known strings
   because `var_dump(some_function())`
   is shorter than `echo some_function() . "\n";`

2. Even if codebases add userland helper equivalents that do exactly this,
   If you are new to a codebase, or contribute to multiple
   codebases, it is inconvenient to use `xyz_println`,
   `ABCUtils::println()`, `echo X, "\n"`, etc., and remember if those different
   functions actually use the line endings you think they do.

   Additionally, the prefixing is much more verbose.
3. In tutorials or language references that teach a developer
   how to use php functionality, it is often preferable to use
   functions that append a newline when multiple snippets would be evaluated
   together to keep examples simple.

   `println("Hello $name");` would be useful to have for introducing PHP
   to a new developer before `echo "Hello $name\n";`
   (requires explaining escaping first)
   or `var_dump("Hello $name");` (that debug representation is rarely useful
   for `string(11) "Hello world"`)

   E.g. `var_dump` is frequently used instead of
   `var_export`, `echo`, or `print` in the manual even for printing strings
   with no control characters such as
   https://www.php.net/manual/en/function.json-encode.php#example-3972

TODO: Write an rfc document, gather existing counterarguments for/against
naming choices and newline choices, gather examples of other languages that put
a println equivalent in the standard library and their choices.
  • Loading branch information
TysonAndre committed Mar 13, 2021
1 parent d5a15d2 commit 42b1064
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 1 deletion.
1 change: 1 addition & 0 deletions ext/standard/basic_functions.stub.php
Expand Up @@ -966,6 +966,7 @@ function fprintf($stream, string $format, mixed ...$values): int {}
/** @param resource $stream */
function vfprintf($stream, string $format, array $values): int {}

function println(string $data = ''): int {}
/* fsock.c */

/**
Expand Down
8 changes: 7 additions & 1 deletion ext/standard/basic_functions_arginfo.h
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 97edf8c87780c892984099e52ad1c6c745b919f8 */
* Stub hash: 0909e41211d2c51032e107c7829979dff46a6944 */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0)
ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0)
Expand Down Expand Up @@ -1489,6 +1489,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_vfprintf, 0, 3, IS_LONG, 0)
ZEND_ARG_TYPE_INFO(0, values, IS_ARRAY, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_println, 0, 0, IS_LONG, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_STRING, 0, "\'\'")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO_EX(arginfo_fsockopen, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, hostname, IS_STRING, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, port, IS_LONG, 0, "-1")
Expand Down Expand Up @@ -2621,6 +2625,7 @@ ZEND_FUNCTION(vprintf);
ZEND_FUNCTION(vsprintf);
ZEND_FUNCTION(fprintf);
ZEND_FUNCTION(vfprintf);
ZEND_FUNCTION(println);
ZEND_FUNCTION(fsockopen);
ZEND_FUNCTION(pfsockopen);
ZEND_FUNCTION(http_build_query);
Expand Down Expand Up @@ -3261,6 +3266,7 @@ static const zend_function_entry ext_functions[] = {
ZEND_FE(vsprintf, arginfo_vsprintf)
ZEND_FE(fprintf, arginfo_fprintf)
ZEND_FE(vfprintf, arginfo_vfprintf)
ZEND_FE(println, arginfo_println)
ZEND_FE(fsockopen, arginfo_fsockopen)
ZEND_FE(pfsockopen, arginfo_pfsockopen)
ZEND_FE(http_build_query, arginfo_http_build_query)
Expand Down
23 changes: 23 additions & 0 deletions ext/standard/formatted_print.c
Expand Up @@ -919,3 +919,26 @@ PHP_FUNCTION(vfprintf)
zend_string_efree(result);
}
/* }}} */

/* {{{ Output a string followed by a unix newline */
PHP_FUNCTION(println)
{
char* input = NULL;
size_t input_len = 0;

ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_STRING(input, input_len)
ZEND_PARSE_PARAMETERS_END();

if (input_len > 0) {
size_t rlen = PHPWRITE(input, input_len);
if (UNEXPECTED(rlen != input_len)) {
RETURN_LONG(rlen);
}
rlen += PHPWRITE("\n", 1);
RETURN_LONG(rlen);
}
RETURN_LONG(PHPWRITE("\n", 1));
}
/* }}} */
42 changes: 42 additions & 0 deletions ext/standard/tests/general_functions/formatted_print1.phpt
@@ -0,0 +1,42 @@
--TEST--
println() function with strict_types=0
--FILE--
<?php

// Normally, you would never bother checking the number of bytes returned,
// but this is a unit test of the function itself.
$v1 = println('');
$v2 = println('println always adds a newline');
println("the above string was $v2 bytes including the newline, the below appends \\n to \\n");
$v3 = println("\n");
$v4 = println('strict_types=0 allows string coercion the exact same way as any other function');
$v5 = println(1);
$v6 = println(true);
$v7 = println(); // the default argument is the empty string
println('println byte lengths');
println(json_encode([$v1, $v2, $v3, $v4, $v5, $v6, $v7]));
try {
println([]);
} catch (TypeError $e) {
echo "Caught TypeError {$e->getMessage()}\n";
}
try {
println('test', 'too many');
} catch (ArgumentCountError $e) {
echo "Caught ArgumentCountError {$e->getMessage()}\n";
}

?>
--EXPECT--
println always adds a newline
the above string was 30 bytes including the newline, the below appends \n to \n


strict_types=0 allows string coercion the exact same way as any other function
1
1

println byte lengths
[1,30,2,79,2,2,1]
Caught TypeError println(): Argument #1 ($data) must be of type string, array given
Caught ArgumentCountError println() expects at most 1 argument, 2 given
28 changes: 28 additions & 0 deletions ext/standard/tests/general_functions/formatted_print2.phpt
@@ -0,0 +1,28 @@
--TEST--
println() function with strict_types=1 throws for non-string
--FILE--
<?php

declare(strict_types=1);

$v1 = println('println always adds a newline');
println("wrote $v1 bytes");
try {
println(1);
} catch (TypeError $e) {
println("Caught: " . $e->getMessage());
}
println(); // valid
try {
println(null);
} catch (TypeError $e) {
println("Caught: " . $e->getMessage());
}

?>
--EXPECT--
println always adds a newline
wrote 30 bytes
Caught: println(): Argument #1 ($data) must be of type string, int given

Caught: println(): Argument #1 ($data) must be of type string, null given

0 comments on commit 42b1064

Please sign in to comment.