-
-
Save juliyvchirkov/8f325f9ac534fe736b504b93a1a8b2ce to your computer and use it in GitHub Desktop.
<?php declare(strict_types = 1); | |
/** | |
* Provides polyfills of string functions str_starts_with, str_contains and str_ends_with, | |
* core functions since PHP 8, along with their multibyte implementations mb_str_starts_with, | |
* mb_str_contains and mb_str_ends_with | |
* | |
* Covers PHP 4 - PHP 7, safe to utilize with PHP 8 | |
*/ | |
/** | |
* @see https://www.php.net/manual/en/function.str-starts-with | |
*/ | |
if (!function_exists('str_starts_with')) { | |
function str_starts_with(string $haystack, string $needle): bool | |
{ | |
return strlen($needle) === 0 || strpos($haystack, $needle) === 0; | |
} | |
} | |
/** | |
* @see https://www.php.net/manual/en/function.str-contains | |
*/ | |
if (!function_exists('str_contains')) { | |
function str_contains(string $haystack, string $needle): bool | |
{ | |
return strlen($needle) === 0 || strpos($haystack, $needle) !== false; | |
} | |
} | |
/** | |
* @see https://www.php.net/manual/en/function.str-ends-with | |
*/ | |
if (!function_exists('str_ends_with')) { | |
function str_ends_with(string $haystack, string $needle): bool | |
{ | |
return strlen($needle) === 0 || substr($haystack, -strlen($needle)) === $needle; | |
} | |
} | |
if (!function_exists('mb_str_starts_with')) { | |
function mb_str_starts_with(string $haystack, string $needle): bool | |
{ | |
return mb_strlen($needle) === 0 || mb_strpos($haystack, $needle) === 0; | |
} | |
} | |
if (!function_exists('mb_str_contains')) { | |
function mb_str_contains(string $haystack, string $needle): bool | |
{ | |
return mb_strlen($needle) === 0 || mb_strpos($haystack, $needle) !== false; | |
} | |
} | |
if (!function_exists('mb_str_ends_with')) { | |
function mb_str_ends_with(string $haystack, string $needle): bool | |
{ | |
return mb_strlen($needle) === 0 || mb_substr($haystack, -mb_strlen($needle)) === $needle; | |
} | |
} |
@GottemHams sad but true, your point is totally correct, I've tested and confirmed this quite unexpected glitch locally and accordingly updated the gist
Thanks a lot for your report on this inconvenience!
But I should note, being still quite surprised with that irrational behaviour of PHP, by logic I consider this misdirection to be a bug of the engine
Let me explain my point
Surely, I understand subtleties and nuances of false
and falsy
cases clear and crisp (and the tricks like these are among the valid reasons I wherever possible prefer strict types
, identical comparison operators ===
!==
vs equal ==
!=
et cetera), and I no wai would refuse well-known falsyness
of zero
But I wanna focus your attention on the key thing that zero
is treated to be falsy
when it's about value of Integer
type. Not just numerical
, but Number
zero
And I flatly refuse to accept the conception that String
of character Zero
can be falsy
somehow, 'cause this point is totally irrational. Technically '0'
is symbol with charcode 0x30, which is not falsy
no wai, as well as any other character in a String
is not falsy
. The only String
declared to be falsy
is an empty one (i.e. String
of zero
length)
Let's take a closer look at the topic with, for example, Javascript
console.log(Boolean(0))
/* Expected output: false
Real output: false */
console.log(Boolean('0'))
/* Expected output: true
Real output: true */
console.assert(0, 'Number Zero is falsy')
/* The above assertion fails with error in console as expected,
since Number 0 is falsy */
console.assert('0', 'String of char Zero is falsy')
/* And the last one stays silent, 'cause char 0x30 is truthy, as well
as any other char in a String. The only empty String is falsy one */
There are no complaints with the above, Javascript behaves like a charm and honors the logic just fine. But now let's review exactly the same code in PHP
var_dump((bool) 0);
/* Expected output: bool(false)
Real output: bool(false) */
var_dump((bool) '0');
/* Expected output: bool(true)
Real output: bool(false)
*** WRONG ***/
try {
assert(0, 'Number Zero is falsy');
} catch (Throwable $ex) {
echo get_class($ex) . ': ' . $ex->getMessage() . PHP_EOL;
}
/* The above assertion throws AssertionError as expected,
since Number 0 is falsy */
try {
assert('0', 'String of char Zero is falsy');
} catch (Throwable $ex) {
echo get_class($ex) . ': ' . $ex->getMessage() . PHP_EOL;
}
/* But the last one throws AssertionError too, despite String of
char 0x30 is truthy. That's disgusting */
I guess this bug in PHP engine takes roots and most likely persists from the time when 3rd generation of PHP engine has been released at early 2000, 'cause this bug seems to be a side effect and consequence of one of the initial key features of the engine which greatly distinguished it between another languages from a scratch
I mean the concept of no strict typing and declarations, as well as total free style of typecasting for a coder on one hand and the silent trasparent automated casting the engine has always provided and implemented in a hardcore way by guess on another one
Cause the only way I see to finish with a String
of char Zero
being false
is the false positive failure with automated typecasting of (bool) '0'
, when engine before casting to Boolean
, casts character Zero
0x30 ('0'
) to Number
Zero
(0
) at first, thus turning it into a falsy
value and getting Boolean false
as a final result
But anyway I still cannot figure out why the hell this typecasting from a String
into a Number
is implemented with no preconditions at sign. It could be clear on a flow like '0' + 0
or '0' * 1
, but I see no trigger at all to cast String
into a Number
for this case
Covers PHP 4 - PHP 7
It is actually not true, the code uses string
type hints that are part of PHP 7.0 array
was already part of 5.6.
You should consider using this (made it after writing this) or symfony/polyfill-php80 instead of this gist. I am saying this while I use my own polyfills for the 3 string functions as well at this time, but I think I will change to that even though I do not need most of it. Here is why.
- It's done by people who absolutely know what they are doing.
- It exits early if PHP 8.0 is detected.
- It's quite complex how these functions are called, it does not use strict types. First it uses
?string
nullable types and then$haystack ?? ''
to call the actual polyfill that has string types. Meaning that these functions can be called withnull
as arguments that get tuned into empty strings. I assume this is to correctly actually polyfill the functions how they behave natively. That fact that it does not usedeclare(strict_types = 1);
actually means the functions can be called with all kinds of types that will be transformed to string ornull
.
Just look at the code this for example is more lengthy because it has to be the absolute most efficient and best way to polyfill this. In fact, I have something that is in between the above and what is in the symphony polyfill.
public static function str_ends_with(string $haystack, string $needle): bool
{
if ('' === $needle || $needle === $haystack) {
return true;
}
if ('' === $haystack) {
return false;
}
$needleLength = \strlen($needle);
return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
}
The mb_
versions are actually not part of PHP so they are not polyfills.
@nextgenthemes Thanks for your review!
Covers PHP 4 - PHP 7
It is actually not true, the code uses
string
type hints that are part of PHP 7.0array
was already part of 5.6.
C'mon, it's just a gist. Sure thing PHP 4
provides no support for type hints. Remove the type hints from the code and you'll achieve the goal.
That simple.
https://onlinephp.io/c/9bbc68fa-4065-43ac-800b-072275ce4b74
You should consider using this (made it after writing this) or symfony/polyfill-php80 instead of this gist. I am saying this while I use my own polyfills for the 3 string functions as well at this time, but I think I will change to that even though I do not need most of it. Here is why.
- It's done by people who absolutely know what they are doing.
- It exits early if PHP 8.0 is detected.
- It's quite complex how these functions are called, it does not use strict types. First it uses
?string
nullable types and then$haystack ?? ''
to call the actual polyfill that has string types. Meaning that these functions can be called withnull
as arguments that get tuned into empty strings. I assume this is to correctly actually polyfill the functions how they behave natively. That fact that it does not usedeclare(strict_types = 1);
actually means the functions can be called with all kinds of types that will be transformed to string ornull
.
Thank you, I'm utilizing Symfony Polyfill
myself if available. The code of these guys is implemented in a strict clear Symphony way
and I like and respect it.
Mine simplified alternatives shared above have been developed the day after the release of PHP 8
as temporary solution till Symfony
delivers their polyfills for PHP 8
, and as soon as symfony/polyfill-php80
bundle arrived, I switched to it.
But, long story short, the fact that Symfony
no doubt implements top quality solutions doesn't mean my alternatives don't serve their job.
The
mb_
versions are actually not part of PHP so they are not polyfills.
C'mon, the preamble at the gist header comment notes clear and crisp «polyfills of string functions str_starts_with, str_contains and str_ends_with, core functions since PHP 8, along with their multibyte implementations». Period.
I should say - thanks for replying! I was going to incorporate it into a PR for pi-hole but they are apparently redoing the entire web front-end so it ended up being moot.