Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Iterate on output buffer handling
  • Loading branch information
westonruter committed Aug 2, 2024
commit 13e068d2673bad734dadf622a1fc3c1eb1d9736d
2 changes: 1 addition & 1 deletion plugins/optimization-detective/optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
function od_buffer_output( string $passthrough ): string {
ob_start(
static function ( string $output, ?int $phase ): string {
if ( $phase & PHP_OUTPUT_HANDLER_FINAL ) {
if ( ( $phase & PHP_OUTPUT_HANDLER_FINAL ) > 0 ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions so that I understand this:

  • This is only relevant for when someone calls ob_clean()? Or for other things as well?
  • What's the purpose of $phase and what's the purpose of PHP_OUTPUT_HANDLER_FINAL in this? Why are both checks needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • This is only relevant for when someone calls ob_clean()? Or for other things as well?

That's a good point. There's also the consideration of ob_flush(). Currently if this gets called then the resulting buffer passed into the filter is not complete. I've improved this in f218dc8 by making it so that the buffer is cleanable but not flushable.

  • What's the purpose of $phase and what's the purpose of PHP_OUTPUT_HANDLER_FINAL in this? Why are both checks needed?

The $phase provides information about why the handler was invoked. Note that bitwise operations are being used here (& not &&), so it is checking if the $phase contains the final flag.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@westonruter Thanks, I had indeed missed the bitwise &, so this is only one check, makes sense.

Reading your code comments around the custom flag handling, I'm trying to understand: Does this mean that calling ob_flush() anywhere while this output buffer is active will trigger an error? If so, I think we need to review whether/how that function is commonly used by plugins, mostly caching plugins of course. While it seems necessary for our functionality to be reliable, it also seems risky in terms of cross-plugin compatibility, like you're already indicating in your TODO.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't cause an error, no. It just results in ob_flush() returning false, which is included in the unit tests.

I'm addressing the TODO in another PR as it's not really related to output buffering.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but what about breakage by flushing not being possible in a plugin where that might be intended? That's where I think we need to take a look at the ecosystem to see if that's a real or just a theoretical problem.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a situation where a plugin is attempting to flush the output buffer for a buffer it didn't open, which would be poor form. I did look over the instances of ob_flush() in WPDirectory and I didn't see any obvious examples where ob_flush() was being used in frontend contexts without also having called ob_start(): so they'd only be flushing their own buffer. So I think we're fine here. We'll need to do more testing with actual sites to discover if there is a compatibility problem.

/**
* Filters the template output buffer prior to sending to the client.
*
Expand Down
28 changes: 16 additions & 12 deletions plugins/optimization-detective/tests/test-optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,40 +77,44 @@ function ( $buffer ) use ( $original, $expected, &$filter_invoked ) {
}

/**
* Make output is buffered and that it is also filtered.
* Test that calling ob_clean() will discard previous buffer and never send it into the od_template_output_buffer filter.
*
* @covers ::od_buffer_output
*/
public function test_od_buffer_output_not_finalized(): void {
$original = 'Hello My World!';
$override = 'Ciao mondo!';
$original = 'Hello My World!';
$template_override = 'Ciao mondo!';
$filter_override = '¡Hola Mi Mundo!';

// In order to test, a wrapping output buffer is required because ob_get_clean() does not invoke the output
// buffer callback. See <https://stackoverflow.com/a/61439514/93579>.
ob_start();

$filter_invoked = false;
$filter_count = 0;
add_filter(
'od_template_output_buffer',
function ( $buffer ) use ( $original, &$filter_invoked ) {
$this->assertSame( $original, $buffer );
$filter_invoked = true;
return '¡Hola Mi Mundo!';
function ( $buffer ) use ( $template_override, $filter_override, &$filter_count ) {
$this->assertSame( $template_override, $buffer, 'Expected the original template output to never get passed into the buffer callback since ob_clean() was called after the original was printed.' );
$filter_count++;
return $filter_override;
}
);

$original_ob_level = ob_get_level();
od_buffer_output( '' );
$this->assertSame( $original_ob_level + 1, ob_get_level(), 'Expected call to ob_start().' );
echo $original;
echo $original; // This should never be passed into the od_template_output_buffer filter.

// Abort the original content printed above.
ob_clean(); // Note the lack of flush here.
echo $override;
echo $template_override; // This should get passed into the od_template_output_buffer filter.

$buffer = ob_get_clean(); // Get the buffer from our wrapper output buffer.
Comment thread
westonruter marked this conversation as resolved.
Outdated

$this->assertFalse( $filter_invoked );
$this->assertSame( $override, $buffer );
$this->assertSame( 1, $filter_count, 'Expected filter to be called once.' );
$this->assertSame( $filter_override, $buffer, 'Excepted return value of filter to be the resulting value for the buffer.' ); // TODO: The output buffer callback returns $filter_override, but the $buffer for some reason is actually equal to $template_override. Why?
Comment thread
westonruter marked this conversation as resolved.
Outdated

ob_end_clean(); // Close the wrapper buffer.
}

/**
Expand Down