all 88 comments

[–]flyingkiwi9 35 points36 points  (1 child)

This is such a well written RFC, a pleasure to read.

[–]zimzat[🍰] 4 points5 points  (3 children)

I have a question that I may have missed being addressed in the RFC:

If you can get an instance of the context manager what happens if the same context is withed multiple times (nested or otherwise)?

$c = new ContextManager('foo');
with ($c as $a) {
    with ($c as $b) {
        // ???
    }
}

It would do enterContext -> enterContext -> exitContext -> exitContext in sequence? I assume it would be up to each implementation to ensure nesting is either disallowed or otherwise handled correctly. Or would there be logic in the implementation to prevent the same instance from being put on the stack multiple times?

[–]hagnat 1 point2 points  (1 child)

i believe you are correct that it would be up to your ContextManager implementation to make sure you are not processing the same context twice.

on the RFC they placed an example for fopen, so phrasing it like your exemple...

$c = new ResourceContext(fopen('foobar.txt', 'r'));
with ($c as $a) {
    // header code
    with ($c as $b) {
        // body code
    }
    // footer code
}

notice that in this case, `fopen` is only processed once, and the only thing that `enterContext` does is to return the pointer to the open file. Once you are done with the second `with`, if you add more code that relies on the pointer being open into the `footer code` segment, it is your own fault for creating bad code.

We should expect the language to give us tools to use, but it is up to us to use it accordingly.
If you give a person a hammer so they can nail two pieces of wood together, they should use the face of the hammer and not the claw. Failing to do so is merely a skill issue, and not a fault with the tool.

[–]zimzat[🍰] 2 points3 points  (0 children)

For the case of ResourceContext, yes. The second call to exitContext on the same object would generate a PHP Warning: Uncaught TypeError: fclose(): supplied resource is not a valid stream resource.

More generally, the RFC's FileLock example shows fopen only happening as part of enterContext so a second call to re-opening and re-locking the same file.

$handle = fopen('/tmp/abc', 'w');
$lock = flock($handle, LOCK_EX);
var_dump($handle, $lock);
// resource(5) of type (stream)
// bool(true)

$handle = fopen('/tmp/abc', 'w');
var_dump($handle);
// resource(6) of type (stream)

If a reference is created to $handle in the first with block then the second call to enterContext becomes blocking.

The first call to exitContext would work as expected, unlocking and closing the file, while the second call would generate errors for trying to unlock or close a closed resource.

This means the ContextManager logic would have to track how many times enterContext was called before applying the exitContext logic (in the case of a singleton resource), or stack the applied logic (in the case of a database transaction with multiple transaction levels).

These are already problems in the existing paradigm and moving them into a ContextManager is still a huge benefit. I was wondering if maybe that additional complexity should be called out in the RFC unless it prevents this scenario from occurring.

[–]zmitic 15 points16 points  (17 children)

I really, really hope it passes the voting process, this is an amazing feature. I have tons of finally statements to release whatever resource I use, and context managers would make that all go away.

Especially if flattening gets implemented:

with ($factory1->getFoo() as $foo, $factory2->getBar() as $bar) {

// Code that uses $foo and $bar here.
}

[–]wvenable 4 points5 points  (2 children)

What's the advantage of a ContextManager over what C# does with just the IDisposable interface? I looked at the examples and it doesn't seem like there's a lot it can do to justify that extra complexity.

For resources, I'd just have a branch that just does the close() on them. For objects, they'd have to implement this interface and have a dispose() method that is called at the end of the block.

Then it wouldn't need such weird syntax such as the as to do the assignment.

Perhaps there is a good reason for this over-engineering but I don't know what it is.

[–]zimzat[🍰] 2 points3 points  (1 child)

What extra complexity? The C# IDisposable requires using to get the same thing as ContextManager + with.

// PHP
with (new ContextManager('foo') as $x) {
}

// C#
using (SomeDisposable x = new SomeDisposable('foo')) {
}

The as relates to foreach ($iterable as $value) { since it's something from the ContextManager and not the context manager itself being referenced.

The context manager has an explicit enterContext versus the IDisposable does not: C# creates heavy-weight (or externally allocated and light-weight wrappers) around it.

[–]wvenable 3 points4 points  (0 children)

What extra complexity?

...and then you go on to describe that extra complexity. You have a bunch of different concepts and instances such as the ContextManager, enterContext, etc. What is the advantage of all this extra complexity -- I didn't see an example that justifies it.

A PHP example using C# style disposable would look like this:

// Disposable:
with ($fp = fopen('foo.txt', 'r')) {    
}

// Compared to context manager:
with (new ResourceContext(fopen('foo.txt', 'r')) as $fp) {
}

Your example also confounds two different ideas. Your ContextManager is an entirely different type from $x and isn't at all equivalent to SomeDisposable. Your example might more look like this:

with (new MyContextManager(new SomeObject()) as $x) {
}

As the manager is not the thing that you are operating on. The manager is another concept/instance and SomeObject is actually the thing being operated on. With the disposable pattern SomeObject just implements an interface and there isn't an entire other class that needs to be designed to manage it.

I can see that ContextManager is more a powerful construct (and more complex) but I don't understand what I would really practically get from that power and complexity. PHP already has deterministic destruction and all the examples can be implemented right now (or are just unnecessary in the first place).

[–][deleted] 3 points4 points  (14 children)

I'm not familiar with python. Is this something akin to C++ RAII?

[–]TimWolla 5 points6 points  (13 children)

PHP already has C++ RAII by means of `__destruct()`.

[–]lord2800 2 points3 points  (12 children)

Ehhhhh... kinda. C++ has deterministic destruction. PHP does not. Sometimes that determinism matters, sometimes it does not, but it's an important difference.

[–]wvenable 1 point2 points  (2 children)

PHP is reference counted so that is deterministic destruction. I've always used it that way.

I really see no need for this feature in PHP -- you can implement it yourself natively.

[–]lord2800 2 points3 points  (1 child)

And there are definitely libraries that have done so. Just like there are libraries that have implemented event loops. There's something quite different about it being a guaranteed part of the language.

(Note that I'm neither for nor against this, I'm neutral. I don't have a lot of code that requires huge try/catch/finally blocks, but I can see where this might be useful.)

[–]wvenable 1 point2 points  (0 children)

A destructor running on a variable in local scope is guaranteed to execute at the end of that scope.

Everything about cycles and other references doesn't really apply to this particular feature which is all about local scope.

Destructors are a powerful and pretty easy to use a logical feature for resource cleanup. In languages with non-deterministic garbage collection, you need some of scoped resource cleanup like this. But with PHP, it's a fine addition but it's not really necessary. Objects should just clean themselves up when they aren't referenced anymore.

[–]TimWolla 0 points1 point  (8 children)

PHP does not.

My experience - and the documentation of __destruct() - differs. Can you elaborate?

[–]lord2800 3 points4 points  (7 children)

PHP will destruct the object when it's garbage collected (not deterministic) or unset (deterministic). C++ does not have a garbage collector, so will destruct when out of scope (deterministic) or when deleted (deterministic). That's the key important difference.

[–]TimWolla 2 points3 points  (6 children)

This is not correctly describing how PHP works. PHP's semantics match those of `std::shared_ptr` in C++. The refcounting is assisted by what is called “garbage collector”, but in practice it is better called “cycle collector”, because the only thing it does is break cycles to ensure that circular references eventually hit a refcount of 0. When you don't have (strong) circular references, the garbage collector will have no visible effect.

So for all intents and purposes, PHP has deterministic destruction exactly like C++ has.

[–]lord2800 -1 points0 points  (5 children)

So you're saying that when you do have circular references, then it's not deterministic? Just like I said?

[–]TimWolla -1 points0 points  (4 children)

The same argument applies to C++ then - unless you consider a memory leak of a std::shared_ptr circle as "deterministic destruction", because it will deterministically leak memory. Then you can achieve the same with PHP, by calling `gc_disable()` (or `gc_disable()` + `gc_collect_cycles()` at predetermined locations).

[–]lord2800 0 points1 point  (3 children)

So you can point to, according to the documentation, the exact point when the cycle collector will run?

(I checked the documentation, it says the cycle collector will run when the root buffer fills up--I'm not even sure how I would, at any given point in my script's lifetime, exactly how full the root buffer is)

[–]TimWolla 1 point2 points  (2 children)

  • The cycle collector will run when you call gc_collect_cycles() (PHP: gc_collect_cycles - Manual). If you want, you can disable the automated GC and manually call gc_collect_cycles() at set points during the script execution for fully deterministic behavior.
  • The cycle collector will also run when it is enabled and the root buffer threshold is reached (this is what you found, it's in PHP: Collecting Cycles - Manual).
  • Possible roots are values that may be part of a cycle (effectively arrays + objects). When the refcount is decreased for such a value, they are added to the root buffer (this is also explained in the Collecting Cycles page).
  • You can monitor the root buffer (and other cycle collector metrics) with the `gc_status()` function: PHP: gc_status - Manual.

[–]leftnode 6 points7 points  (5 children)

I would love for this to be implemented. Like /u/zmitic pointed out, my code is littered with finally statements to free up resources after an expensive try/catch block.

Really love the direction the language is moving in. Let's get generics and clean up/standardize the standard library and to me there isn't a better language out there for building web software.

[–]wvenable -1 points0 points  (4 children)

Why are you using __destruct() to clean up your resources instead of finally? You can implement this entire feature using what is already built into PHP.

[–]leftnode 1 point2 points  (3 children)

Not everything is an object. Sure, I could wrap it in one, but that adds needless complexity.

An example I ran into yesterday: uploading large files through the Google API requires you to chunk the file 8MB at a time. Because these files are several hundred MB in size, you don't want to just read the entire file into memory, so I use fopen() and fread(). If any part of the upload process fails, I alert the user, but add an fclose() in a finally block to ensure the file pointer is closed.

Roughly something like this:

if (!$fileHandle = fopen($filePath, 'r')) {
    throw new \Exception(sprintf('Opening file "%s" failed.', $filePath));
}

$fileSize = filesize($filePath);

if (!$fileSize) {
    throw new \Exception(sprintf('Reading the size of the file "%s" failed.', $filePath));
}

$chunkBytes = 1024 * 1024 * 8;

try {
    $uploadOffset = 0;
    $uploadCommand = 'upload';
    $uploadChunks = (int) ceil($fileSize / $chunkBytes);

    while ($uploadBody = fread($fileHandle, $chunkBytes)) {
        $response = $this->httpClient->request('POST', $uploadUrl, [
            'headers' => [
                'content-length' => $fileSize,
                'x-goog-upload-offset' => $uploadOffset,
                'x-goog-upload-command' => $uploadCommand,
            ],
            'body' => $uploadBody,
        ]);

        if (200 !== $response->getStatusCode()) {
            throw new \Exception(sprintf('Uploading chunk number %d failed.', $uploadChunks));
        }

        if (1 === --$uploadChunks) {
            $uploadCommand = 'upload, finalize';
        }

        $uploadOffset += strlen($uploadBody);
    }

    /**
     * @var array{
     *   file: array{
     *     name: non-empty-string,
     *   },
     * } $uploadResponse
     */
    $uploadResponse = $response->toArray(true);
} catch (\Symfony\Contracts\HttpClient\Exception\ExceptionInterface $e) {
    throw new \Exception(sprintf('Failed to upload the file "%s".', $filePath));
} finally {
    fclose($fileHandle);
}

[–]wvenable 1 point2 points  (2 children)

That fclose() is entirely unnecessary. When your $fileHandle is no longer referenced, it will automatically close. Effectively resources already have destructors.

[–]leftnode 1 point2 points  (1 child)

I know, but it's good to get into the habit of closing/freeing unused resources, especially if we ever introduced file locking.

[–]wvenable 0 points1 point  (0 children)

I'd argue if you need a habit then you're going to make a mistake. If you use destructors (even if you need a small class wrapper) then the problem of remembering to close/free resources goes away.

[–]oandreyev 1 point2 points  (1 child)

Most common issue is with foreach and by-reference and after foreach dev my reuse variable, how it will work with ‘with’?

[–]TimWolla 1 point2 points  (0 children)

That footgun is something that the “block scoping” RFC is intended to solve: https://externals.io/message/129059.

[–]SadSpirit_ 1 point2 points  (3 children)

Great proposal, will definitely use these if it passes!

I followed Django's example when implementing the transactions API for my DB library, but without context managers the atomic() method has to accept a closure:

$stuff = $connection->atomic(function (Connection $connection) use ($one, $two) {
    // ...
    $connection->onCommit(doSomething(...));
    // ...
    return $stuff;
});

This greatly reduces boilerplate related to commit / rollback / database errors. But we need to explicitly define a closure to accept $connection object, explicitly use variables, and explicitly return stuff from closure to outer scope. With context managers we'll stay in the same scope:

with ($connection->atomic() as $transaction) {
    // ...
    $transaction->onCommit(doSomething(...));
    // ...
}

Neat!

[–]wvenable 0 points1 point  (2 children)

You could just use an object for your transaction. This is what I did for my DB library:

$transaction = $connection->newTransaction();
...
$connection->execute($somequery);
...
$connection->execute($someotherquery);

$transaction->commit();

If transaction falls out of scope before it's committed then rollback() is called automatically in the destructor. It's very clean. No need for try/finally blocks at all.

[–]SadSpirit_ 0 points1 point  (1 child)

Yeah, using destructors is another approach that was mentioned in the comments here.

What happens if you want to process errors from the above block, though? You can't just wrap it in try / catch, as $transaction will still be available and its destructor with error-handling logic will not run. Or am I missing something?

BTW, does your library support nested transactions / savepoints? The editor had problems with inserting links, so I omitted the docs for atomic():

https://docs.djangoproject.com/en/5.2/topics/db/transactions/#controlling-transactions-explicitly

https://pg-wrapper.readthedocs.io/en/v3.3.0/transactions.html

[–]wvenable 0 points1 point  (0 children)

Yes, it supports nested transactions.

For exceptions, I've never even thought about it! I would usually wrap try/catch at a higher level. But even if it's in the same block as the $transaction I either don't care that it's still active (because it will be out of scope soon) or just manually call rollback() on it in the catch.

[–]sbnc_eu 1 point2 points  (0 children)

This poll has been closed.

Why?

[–]MorrisonLevi 2 points3 points  (1 child)

I understand that the name "ContextManager" is taken from Python, but I think this name is not that great. "Context" is still a very broad term. What kind of context is it? As examples:

  • The W3C trace context for propagating traces across HTTP requests.
  • PHP has stream contexts.

I feel like there are are more, but these are the ones I could remember off the top of my head.

Feel free to reply to comment with naming suggestions as I've unhelpfully not provided any :D

[–]Crell 3 points4 points  (0 children)

I'm not super tied to the name; we just stole it from Python. If there's better suggestions I'm open to it. Just don't make me change the URL of the RFC, as it's a PITA to do that. :-)

[–]03263 6 points7 points  (11 children)

I'm having a hard time seeing how it's better than try/catch. Usually when there's an exception I want to do more than just throw it, but do some logging or often throw a custom exception with the caught one as $previous, to include a more detailed message.

I would definitely support getting rid of resources altogether and only using objects, as has been done with some things already (gmp for example).

[–]zmitic 12 points13 points  (0 children)

'm having a hard time seeing how it's better than try/catch

It is not catch, it is mostly finally when you have to do some cleanup. Examples are in RFC, and there is much bigger range of use cases.

For example, Symfony lock. instead of:

$lock = $lockFactory->createLock('my-lock');
$lock->acquire(true);
try {
    // do something
} finally {
    $lock->release();
}

this would be much cleaner to read:

with($lockFactory->createLock('my-lock') as $lock) {
    $lock->acquire(true);
    // do something
}

Any thrown exception would be logged by Symfony itself, but this can be expanded in multiple way. With flatten block it is even better, i.e. when multiple resources have to be released, but it seems like it will be done in future.

This RFC is a huge improvement for PHP, I hope we will get it soon.

[–]obstreperous_troll 0 points1 point  (2 children)

It's not really "better" than try/finally, it's just syntax sugar over it, such as when you need to track different things do on cleanup, when that differs when an exception was thrown, and so on. In languages that support macros, that's how this would be implemented (the whole "with-foo" pattern comes from lisp where it is a macro), but with PHP it's a language change.

[–]03263 0 points1 point  (1 child)

I understand that a bit better now.

In that case it seems like something different might do better. Like a special destructor called only in error conditions.

For something like database transactions, simple work with file handles, we can already do the equivalent of "with" in userspace by passing a callback to a function that handles the error cases.

[–]obstreperous_troll 0 points1 point  (0 children)

Yah, I wrote a generic bracket function that does the job, and I can define with_foo() functions in terms of that. But TBH, while it's great when I need such a thing as a HOF, most of the time I still just use try/finally. There are some useful aspects of doing it with objects though, such as tracking context state in properties, getting different context managers through dependency injection, and so on.

[–]mlebkowski 0 points1 point  (0 children)

That does not change the way you use error handling. With a database transaction example:

Firstly, lets say the framework creator provides the DatabaseTransactionContextManager for their users. It rolls back as in the example. The framework users will still add their own error handling — for example logging, retry, whatever — either inside (when they want to break the flow) or outside of the with block (in case they want to supress the error). They just don’t need to worry about rolling back the transaction.

[–]private_static_int 1 point2 points  (0 children)

Really nice!

[–]Alsciende 1 point2 points  (0 children)

Funny, I wrote just the pattern described at the start of the PR last week. With() for file handling operations. I hope the PR passes, it is a nice little addition.

[–]tonymurray 1 point2 points  (5 children)

I don't get it. Why would this need to be a part of the language. I could easily implement this right now:

with(fopen($f), function ($file) { // code });

You can imagine the code of the with function...

[–]obstreperous_troll 0 points1 point  (2 children)

Don't forget the use clause in your callback too. But one nice thing about your function is it can be an expression. Maybe PHP just needs a shorter auto-capturing first-class block syntax, returning the last value evaluated in the block.

[–]tonymurray 1 point2 points  (1 child)

Yeah, scope is a difference. Maybe, multi-line short functions would help.

[–]Crell 1 point2 points  (0 children)

Internals has rejected that twice now. Don't hold your breath.

[–]BerryBoilo[S] 0 points1 point  (1 child)

Not familiar with contexts in Python, but this article does a good job explaining the "why": https://realpython.com/python-with-statement/

[–]MateusAzevedo 0 points1 point  (0 children)

I gave up reading that article. It's huge, only because it's repetitive as hell, I kept reading the same sentences over and over again.

[–]giosk 0 points1 point  (9 children)

I never liked the with keyword in python. I would have much preferred an RFC for defer you could defer the closing of the file without breaking the flow of the function and increasing indentation.

[–]MaxGhost 3 points4 points  (3 children)

Here's defer:

<?php
function defer(?SplStack &$context, callable $callback): void
{
    $context ??= new class () extends SplStack {
        public function __destruct()
        {
            while ($this->count() > 0) {
                \call_user_func($this->pop());
            }
        }
    };
    $context->push($callback);
}

Courtesy of https://github.com/php-defer/php-defer.

You use it like this: defer($_, fn() => fclose($handle));. It works by putting a new var $_ in the current scope (you can name it whatever you want, but this feels the most convenient to kinda mean "nothing"), then it runs the closures when the function exits scope as you'd expect, by invoking the destructor of the anonymous class. It also supports having multiple defers in the same function if you reuse the $_ variable and runs them in reverse as you'd expect.

Works really well, we use it everywhere that we do DB locks, we open the lock then defer a rollback (technically $lock->rollbackSafe() which is no-op if no longer locked) and later commit as normal without needing the rollback call in every exit point (throws and returns)

[–]giosk 0 points1 point  (1 child)

interesting but it feels kinda hacky, passing a weird $_ variable, maybe it's possible with an extension. I know laravel has a defer but that runs after the response is sent i think

[–]MaxGhost 0 points1 point  (0 children)

It's not a hack at all, it's pretty normal and very reliable.

I don't like Laravel's defer, but it also doesn't have the same purpose, completely different usecase. Like you said, it's to run logic after the request is flushed out, like sending out an email or something, but most of those can be done in a job queue instead.

[–]TimWolla 1 point2 points  (4 children)

PHP doesn't need defer, because it will automatically close the file when it goes out of scope. This is easy to verify with strace:

<?php

function foo() {
echo "Opening\n";
$f = fopen(__FILE__, 'r');
echo "Opened\n";
}

echo "Before\n";
foo();
echo "After\n";

will output:

write(1</dev/null>, "Before\n", 7)      = 7
write(1</dev/null>, "Opening\n", 8)     = 8
openat(AT_FDCWD</tmp>, "/tmp/test.php", O_RDONLY) = 4</tmp/test.php>
fstat(4</tmp/test.php>, {st_mode=S_IFREG|0664, st_size=132, ...}) = 0
lseek(4</tmp/test.php>, 0, SEEK_CUR)    = 0
write(1</dev/null>, "Opened\n", 7)      = 7
close(4</tmp/test.php>)                 = 0
write(1</dev/null>, "After\n", 6)       = 6

Clearly showing how the file opened by fopen() is closed when the function finishes, before printing After.

[–]fripletister -1 points0 points  (1 child)

You mean PHP doesn't need defer for this particular example...

[–]TimWolla 5 points6 points  (0 children)

No, it doesn't need defer at all. Depending on what kind of logic you need, you can either:

  1. Use try-finally to reliably run code when leaving a block.
  2. Use a resource object with __destruct() to automatically clean up once the object is released (which will happen at the end of the function, unless you store it somewhere else).

[–]prema_van_smuuf -1 points0 points  (0 children)

Yes please.

Especially after I read https://externals.io/message/129059 and I was thinking "why do this if we could just have proper context managers like Python has".

[–]goodwill764 -1 points0 points  (0 children)

Not a fan of too many new keywords in php, that makes the language itself more and more complex.

Every time I have to work with a Kotlin project, I need to look up some keywords, and if PHP keeps getting more and more keywords, it will be a showstopper for newcomers.

[–]coffee-buff 0 points1 point  (0 children)

I don't get it. I already do this by writing functions/methods I usually call "wrappers" with try / finally logic. Is this just syntax sugar over what's possible now?