Resource to object conversion
- Date: 2023-10-31
- Author: Máté Kocsis kocsismate@php.net
- Status: Accepted
Introduction
Resources are a legacy data structure which were superseded by objects a long time ago. They have quite a few disadvantages like the inability of using resource
type declarations, lack of garbage collection (they have to be explicitly freed), bad feedback about memory leaks, etc.
As far as I know, Nikita was the first one who realized these shortcomings and started converting resources to objects, ''GMP'' back in 2013, ''XMLParser'' in 2019. However, this ambitious project only got traction during the release cycle of PHP 8 where dozens of extensions were refactored so that they started to use objects instead of resources (tracker ticket). At the time of writing, only around 7 built-in extensions remain which still expose resources...
Discussion
As eliminating resources altogether should be a strategic goal for PHP, this time, the main question is not whether the remaining resources should be converted to objects or not, but the timeline when we can do so, in order to have acceptable level of backward compatibility breaks.
Therefore the goal of this RFC is to set the rules and BC expectations based on which the project can continue. So far, the vast majority of the individual conversions were “unofficial”, meaning they didn't have to go through the formal RFC process, there was no deprecation period (it is not really feasible anyway), and were performed even in minor versions. I do believe this kind of flexibility has been beneficial for the overall project, however, we occasionally get complaints due to the introduced BC breaks. That's what this RFC should prevent for future conversions. But what are these breaks?
First and foremost, the return value changes of is_resource()
invocations cause the biggest issues. An example of such situation is the following:
$pipes = []; $process = proc_open("sleep 1", [], $pipes); if (is_resource($process)) { // do some stuff proc_close($process); }
The above code won't work anymore after the conversion, since the is_resource($process)
condition will yield a different result than before (false
compared to true
).
The easiest solution to fix the problem is to change the condition to the following:
$pipes = []; $process = proc_open("sleep 1", [], $pipes); if ($process !== false) { // do some stuff proc_close($process); }
By using $process !== false
instead of is_resource($process)
, the if
will yield the correct result, and the code is going to be compatible with any PHP version. One could also use is_resource($process) || $process instanceof ProcessHandle
, but this is a more verbose way to achieve the same result.
Implementation details
Some people suggest that the is_resource()
function should return true
for classes which originally used to be resources (read a long thread about this topic here). Doing so would apparently minimize the BC break by making the above mentioned code change unnecessary.
However, the current RFC rejects the usage of the above workaround for any resources besides streams mostly because of the low perceived BC impact of their migration (see the impact analysis below for more information). Additionally, the issues introduced by the is_resource()
return value changes are mostly easy to spot and fix (but admittedly need some careful manual work).
In case of streams though - due to their ubiquitous nature - a different course of action is applied in order to avoid the possibly huge BC breaks: is_resouce()
will return true
if any object is passed to it which previously used to be stream
or persistent stream
resource. This way the following code will continue to work no matter that streams are represented as objects:
$file = fopen(__DIR__ . "/foo.txt", "r"); if (is_resource($file)) { // do some stuff fclose($file); }
Moreover, get_resource_type()
is also changed so that it gives a consistent and BC compliant result for streams after the migration:
$pipes = []; $process = proc_open("sleep 1", [], $pipes); var_dump(get_resource_type($process)); // TypeError: Argument #1 ($resource) must be of type resource $file = fopen(__DIR__ . "/foo.txt", "r"); var_dump(get_resource_type($file)); // string(6) "stream"
This way, code using streams doesn't have to be changed at the time of the stream resource to object migration. However, as the end goal is to get rid of resources altogether, the is_resource()
function and anything related to resources may occasionally be deprecated and removed in the future.
Resource categories
The resources yet to be migrated can be categorized the following way based on their prevalence (biggest to smallest):
Primary stream-related resources
There are a number of stream-related resources which are to be converted: the most important ones are stream
and persistent stream
themselves. Historically, these have always been the most widely used resources in the whole PHP ecosystem, since the file handling API is organized around them.
The list of functions which return primary stream resources:
- File-handling related functions:
opendir()
popen()
fopen()
tmpfile()
- . Compressed file handling related functions:
bzopen()
gzopen()
ZipArchive::getStreamIndex()
ZipArchive::getStreamName()
ZipArchive::getStream()
- Socket-related functions:
fsockopen()
pfsockopen()
socket_export_stream()
stream_socket_client()
stream_socket_server()
stream_socket_accept()
- Database-related functions:
PDO::pgsqlLOBOpen()
PdoPgsql::lobOpen()
pg_socket()
SQLite::openBlob()
Impact analysis: Primary stream resources won't be affected by the above mentioned BC break due to workaround discussed in the previous section.
Auxilliary stream-related resources
Furthermore, streams have some auxiliary resources:
- stream context: it stores parameters/options for the different stream wrappers
- stream filter: it helps perform operations on streams
- userfilter.bucket, userfilter.bucket brigade: a very detailed explanation, written by Bob
Auxilliary resources are “isolated” from the primary ones, and used less often than them, that's why it may be possible to migrate the two resource categories separately from each other.
Impact analysis:
- stream context: since
stream_context_create()
throws in case of failure, the issue with the is_resource() check doesn't apply to stream context resources - stream filter: 3 out of the 2000 most popular PHP packages rely on checking the result of
stream_filter_append()
andstream_filter_prepend()
withis_resource()
- userfilter.bucket, userfilter.bucket brigade: None of the 2000 most popular PHP packages rely on checking the result of
stream_bucket_new()
withis_resource()
. Sinceuserfilter.bucket brigade
is created by PHP itself when thephp_user_filter::filter()
method is invoked, therefore neither it is affected by the BC break.
Process resource
The Process
resource is returned by proc_open()
. According to my earlier straw poll, its new class name is going to be ProcessHandle
once the migration takes place.
Impact analysis: 31 out of the 2000 most popular PHP packages rely on checking the result of proc_open()
with is_resource()
.
Implementation: https://github.com/php/php-src/pull/12098/
Other resources
The rest of the built-in extensions which currently expose resources are the following:
- COM (implementation: https://github.com/php/php-src/pull/14282)
- com_dotnet_istream_wrapper
- com_dotnet_dispatch_wrapper
- DBA (implementation: https://github.com/php/php-src/pull/14239)
- dba: connection resource returned by
dba_open()
- dba persistent: persistent connection resource returned by
dba_popen()
- ODBC (implementation: https://github.com/php/php-src/pull/12040)
- odbc link: connection resource returned by
odbc_connect()
- odbc link persistent: persistent connection returned by
odbc_pconnect()
- odbc result: result resources returned by various
odbc_*
functions
- SOAP (implementation: https://github.com/php/php-src/pull/14121, https://github.com/php/php-src/pull/14174)
- SOAP SDL:
SoapClient::$sdl
- SOAP table:
SoapClient::$typemap
- SOAP URL:
SoapClient::$httpurl
Impact analysis: None of the 2000 most popular PHP packages rely on the above mentioned resources.
Vote
The RFC consists of a primary vote for accepting the implementation approach and 4 secondary votes for each resource category to decide when it is possible to convert them. Since the majority of the resource migrations in question don't yet have an implementation, most voting choices are intentionally left vague so that they only define whether a resource group can only be migrated in a major version or the conversion can happen in minor versions as well. The primary vote requires 2/3, while the secondary ones require a simple majority in order to be accepted.
Implementation
Primary stream resources (stream, persistent stream)
Auxiliary stream resources (contexts, filters, brigades, buckets)
Process resource
Other resources
Impact on existing extensions
Extensions which use resources must adapt to the conversion and use objects instead.
Future Scope
Support for resources altogether could be removed in the future.