Howdy Internals,
I discovered today (purely on accident) that class_exists($subject)
actually parses the "$subect" class in question.
I was attempting to use class_exists(UsesPropertyPromotion::class) to
determine if an attribute implementation existed in order to generate a PHP
8 appropriate class - else fall back to generating a PHP 7 appropriate
class. For context, this check was being written for Symfony's MakerBundle
which generates classes for Symfony projects.
Consequently the conditional worked as expected in PHP 8, but in 7 I
received a Parse Error. See the bug report
https://bugs.php.net/bug.php?id=80938 for additional details. Nikic pointed
out the problem (not a bug) is that w/ autoload === true, class_exists will
load the class.
The purpose of this email is to spark the conversation on a path forward
for code bases that must support multiple versions of PHP and still be able
to use native PHP functions without having to jump through hoops. It would
appear from a userland standpoint that class_exist() is broken / unreliable
if it must first be determined if the class being checked is compatible for
the PHP version used at runtime. Which brings us to the chicken and the
egg...
At the very least this could be a "mental note" for RFC's that introduce
changes that create BC in unexpected ways in userland.
Cheers!
Jesse Rushlow
I was attempting to use class_exists(UsesPropertyPromotion::class) to
determine if an attribute implementation existed in order to generate a PHP
8 appropriate class - else fall back to generating a PHP 7 appropriate
class. For context, this check was being written for Symfony's MakerBundle
which generates classes for Symfony projects.
Hi Jesse,
Can you explain the use case in a bit more detail, perhaps giving some
example code you were hoping to write with class_exists()
?
I suspect there are other ways to achieve what you were trying to do,
but don't want to confuse things if I've misunderstood the requirements.
Regards,
--
Rowan Tommins
[IMSoP]
Sure thing -
In Symfony's MakeBundle we have a command "php bin/console make:command".
This ultimately generates a "App\Console\XyzUserlandCommand::class" that
allows the developer to customize in order to perform some task within
their app. e.g. run persistence operations, generate a backup, etc..
What we do internally to generate that userland class is determine which
features to include by checking the PHP version, dependencies available,
etc.. As an example, when the user runs "make:command"; we check if they
have PHP 8 && if "class_exists(Symfony.....\Attributes::class)" - if both
of those === true, we import the attribute class and generate the needed
attributes for the command. Otherwise we would fall back to using
annotations if the user is running PHP 7. Where I ran into trouble was the
Attributes::class (defines the attributes to be used in userland) utilized
constructor property promotion (which is a pretty sweet addition to 8 imo).
Up to this point if it's possible, we generally pick a
class/interface/method that introduces new functionality (Symfony
feature/bugfix) and call one of the *_exists(class/method/interface)
functions in a conditional to determine if we can indeed use that feature.
But because in this particular case the Attributes::class also uses
constructor property promotion, when calling
class_exists(Attributes::class) in PHP 7 we get:
ParseError: syntax error, unexpected 'public' (T_PUBLIC), expecting
variable (T_VARIABLE)
The work around was to "$canUseAttributes = 80000 <= \PHP_VERSION_ID &&
class_exists(AsCommand::class)"
All is fine and well, but this in my opinion, this feels like bad mojo from
a developers standpoint. I must now need to be aware of the exact
implementation details of the object I'm checking before I can even check
if it exists or not. I would expect any of the *_exists() to tell me if
the class/method/interface exists regardless of the implementation. My
first thought would be if the implementation is not compatible with the PHP
version some sort of \RuntimeException::class would be thrown by PHP. But
that is where my knowledge of PHP internals is disconnected from developing
software in PHP.
I created a simple reproducer repo
https://github.com/rushlow-development/attribute-test (forgive the repo
name, it really has nothing to do with attributes.) and for an even simpler
example https://3v4l.org/4cMW4
I apologize in advance for the wall of text - but I'm curious if there is a
way to either improve our native functions, improve the docs, and/or create
new functions for use cases like this.
Thanks!
Jesse Rushlow
On Mon, Apr 5, 2021 at 3:30 PM Rowan Tommins rowan.collins@gmail.com
wrote:
I was attempting to use class_exists(UsesPropertyPromotion::class) to
determine if an attribute implementation existed in order to generate a
PHP
8 appropriate class - else fall back to generating a PHP 7 appropriate
class. For context, this check was being written for Symfony's
MakerBundle
which generates classes for Symfony projects.Hi Jesse,
Can you explain the use case in a bit more detail, perhaps giving some
example code you were hoping to write withclass_exists()
?I suspect there are other ways to achieve what you were trying to do,
but don't want to confuse things if I've misunderstood the requirements.Regards,
--
Rowan Tommins
[IMSoP]
Hey Jesse,
Sure thing -
In Symfony's MakeBundle we have a command "php bin/console make:command".
This ultimately generates a "App\Console\XyzUserlandCommand::class" that
allows the developer to customize in order to perform some task within
their app. e.g. run persistence operations, generate a backup, etc..What we do internally to generate that userland class is determine which
features to include by checking the PHP version, dependencies available,
etc.. As an example, when the user runs "make:command"; we check if they
have PHP 8 && if "class_exists(Symfony.....\Attributes::class)" - if both
of those === true, we import the attribute class and generate the needed
attributes for the command. Otherwise we would fall back to using
annotations if the user is running PHP 7. Where I ran into trouble was the
Attributes::class (defines the attributes to be used in userland) utilized
constructor property promotion (which is a pretty sweet addition to 8 imo).Up to this point if it's possible, we generally pick a
class/interface/method that introduces new functionality (Symfony
feature/bugfix) and call one of the *_exists(class/method/interface)
functions in a conditional to determine if we can indeed use that feature.
But because in this particular case the Attributes::class also uses
constructor property promotion, when calling
class_exists(Attributes::class) in PHP 7 we get:ParseError: syntax error, unexpected 'public' (T_PUBLIC), expecting
variable (T_VARIABLE)The work around was to "$canUseAttributes = 80000 <= \PHP_VERSION_ID &&
class_exists(AsCommand::class)"All is fine and well, but this in my opinion, this feels like bad mojo from
a developers standpoint. I must now need to be aware of the exact
implementation details of the object I'm checking before I can even check
if it exists or not. I would expect any of the *_exists() to tell me if
the class/method/interface exists regardless of the implementation. My
first thought would be if the implementation is not compatible with the PHP
version some sort of \RuntimeException::class would be thrown by PHP. But
that is where my knowledge of PHP internals is disconnected from developing
software in PHP.I created a simple reproducer repo
https://github.com/rushlow-development/attribute-test (forgive the repo
name, it really has nothing to do with attributes.) and for an even simpler
example https://3v4l.org/4cMW4I apologize in advance for the wall of text - but I'm curious if there is a
way to either improve our native functions, improve the docs, and/or create
new functions for use cases like this.
*_exists()
functions triggering autoloading is intentional and required.
Perhaps you are trying to reflect in unsafe symbols, which explicitly need
to skip autoloading, as you are not yet sure about what you will encounter.
This is exactly what https://github.com/Roave/BetterReflection was built
for: perhaps you can help in getting up to speed with 8.x featured, and
then it can be used in the bundle internals (rather than relying on runtime
side-effects)?
As an example, when the user runs "make:command"; we check if they
have PHP 8 && if "class_exists(Symfony.....\Attributes::class)" - if both
of those === true, we import the attribute class and generate the needed
attributes for the command. Otherwise we would fall back to using
annotations if the user is running PHP 7. Where I ran into trouble was the
Attributes::class (defines the attributes to be used in userland) utilized
constructor property promotion (which is a pretty sweet addition to 8 imo).
The way you write it here, there is no problem: you first check if the
user is running PHP >= 8.0 (to see if you should use native attributes)
and only if they are do you check if the Attributes class is available.
I would expect any of the *_exists() to tell me if
the class/method/interface exists regardless of the implementation.
As far as the PHP run-time is concerned, there is no definition of
"exists" that doesn't involve an implementation. It might help to step
through what actually happens in this case:
-
class_exists()
first checks if the class is already defined; it's not,
and you've left $autoload as true, so it calls the autoloader - the autoloader is just a callback function which is given the desired
class name; generally, it translates that to a file name, and calls
include/require - the autoloader asked PHP to include a particular file, so PHP tries to
parse that file - as far as PHP 7.4 is concerned, the file contains a syntax error
- since the file didn't compile, the class was never defined, so it
continues to not exist
Note that PHP 7.4 doesn't know that this is a PHP 8.0 class definition;
it just sees it as invalid PHP. In the same way, a file containing the
keyword "enum" has a syntax error under 8.0 but will be valid in 8.1;
while one with typed properties has a syntax error under 7.3 but is
valid from 7.4 onwards - I'm pretty sure I could come up with an example
for every PHP release.
My first thought would be if the implementation is not compatible with the PHP
version some sort of \RuntimeException::class would be thrown by PHP.
This is in fact exactly what happens: a ParseError is thrown, and can be
caught like any other exception. However, I'm not clear what you'd do
when you caught it, which is why I was hoping to see an example of the
code you were actually trying to write.
By far the most common solution to this problem is to mark the required
PHP version at the package level: if you have files using PHP 8.0
syntax, the package containing them shouldn't be installed on a PHP 7.4
system. If they're not installed, the autoloader can't try to load them,
and your class_exists()
check will cleanly return false.
If you do want to mix files for different PHP versions in one package,
you could do it with a custom autoloader - e.g. all the PHP 8 only
classes could be in a "src-php8" and the rest in "src"; the autoloader
would check PHP_ VERSION_ID and simply ignore the "src-php8" directory
for older versions. Again, the class_exists()
check would cleanly return
false.
If you want to be able to generate PHP 8.0 code while running under PHP
7.4, then something like the library Marco linked to would seem to be
the solution.
Regards,
--
Rowan Tommins
[IMSoP]
Sure thing -
In Symfony's MakeBundle we have a command "php bin/console make:command".
This ultimately generates a "App\Console\XyzUserlandCommand::class" that
allows the developer to customize in order to perform some task within
their app. e.g. run persistence operations, generate a backup, etc..What we do internally to generate that userland class is determine which
features to include by checking the PHP version, dependencies available,
etc.. As an example, when the user runs "make:command"; we check if they
have PHP 8 && if "class_exists(Symfony.....\Attributes::class)" - if both
of those === true, we import the attribute class and generate the needed
attributes for the command. Otherwise we would fall back to using
annotations if the user is running PHP 7. Where I ran into trouble was the
Attributes::class (defines the attributes to be used in userland) utilized
constructor property promotion (which is a pretty sweet addition to 8 imo).Up to this point if it's possible, we generally pick a
class/interface/method that introduces new functionality (Symfony
feature/bugfix) and call one of the *_exists(class/method/interface)
functions in a conditional to determine if we can indeed use that feature.
But because in this particular case the Attributes::class also uses
constructor property promotion, when calling
class_exists(Attributes::class) in PHP 7 we get:ParseError: syntax error, unexpected 'public' (T_PUBLIC), expecting
variable (T_VARIABLE)The work around was to "$canUseAttributes = 80000 <= \PHP_VERSION_ID &&
class_exists(AsCommand::class)"All is fine and well, but this in my opinion, this feels like bad mojo from
a developers standpoint. I must now need to be aware of the exact
implementation details of the object I'm checking before I can even check
if it exists or not. I would expect any of the *_exists() to tell me if
the class/method/interface exists regardless of the implementation. My
first thought would be if the implementation is not compatible with the PHP
version some sort of \RuntimeException::class would be thrown by PHP. But
that is where my knowledge of PHP internals is disconnected from developing
software in PHP.I created a simple reproducer repo
https://github.com/rushlow-development/attribute-test (forgive the repo
name, it really has nothing to do with attributes.) and for an even simpler
example https://3v4l.org/4cMW4I apologize in advance for the wall of text - but I'm curious if there is a
way to either improve our native functions, improve the docs, and/or create
new functions for use cases like this.
It is not clear to me exactly what you are asking for. It seems like your use-case exists when loading code in an older version of PHP that does not support newer features, right?
If that is the case, the only "solution" — which is obviously a non-starter — is to update older versions of PHP to be able to gracefully fail when attempting to load a class designed for a newer version of PHP to use.
OTOH if I misunderstand then can you clarify exactly what you are asking for?
-Mike
Thanks!
Jesse RushlowOn Mon, Apr 5, 2021 at 3:30 PM Rowan Tommins rowan.collins@gmail.com
wrote:I was attempting to use class_exists(UsesPropertyPromotion::class) to
determine if an attribute implementation existed in order to generate a
PHP
8 appropriate class - else fall back to generating a PHP 7 appropriate
class. For context, this check was being written for Symfony's
MakerBundle
which generates classes for Symfony projects.Hi Jesse,
Can you explain the use case in a bit more detail, perhaps giving some
example code you were hoping to write withclass_exists()
?I suspect there are other ways to achieve what you were trying to do,
but don't want to confuse things if I've misunderstood the requirements.Regards,
--
Rowan Tommins
[IMSoP]