Skip to content

Instantly share code, notes, and snippets.

@juliyvchirkov
Last active April 14, 2024 22:24
Show Gist options
  • Save juliyvchirkov/8f325f9ac534fe736b504b93a1a8b2ce to your computer and use it in GitHub Desktop.
Save juliyvchirkov/8f325f9ac534fe736b504b93a1a8b2ce to your computer and use it in GitHub Desktop.
php: polyfills of string functions str_starts_with, str_contains and str_ends_with
<?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
Copy link

Just wanna point out that empty($needle) is a bit unreliable. :> Run php -r 'var_export(empty("0"));' and it will return true, so with these polyfills a call like str_starts_with('foo', '0') will return true as well.

The reason is actually quite simple, but the function's name simply doesn't reflect it properly. From the manual:

Returns true if var does not exist or has a value that is empty or equal to zero, aka falsey, see conversion to boolean. Otherwise returns false.

Better to just check strlen($needle) === 0.

@smithjacobj
Copy link

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.

@juliyvchirkov
Copy link
Author

juliyvchirkov commented Nov 14, 2023

@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

@nextgenthemes
Copy link

nextgenthemes commented Feb 21, 2024

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.

  1. It's done by people who absolutely know what they are doing.
  2. It exits early if PHP 8.0 is detected.
  3. 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 with null 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 use declare(strict_types = 1); actually means the functions can be called with all kinds of types that will be transformed to string or null.

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
Copy link

@juliyvchirkov
Copy link
Author

juliyvchirkov commented Apr 5, 2024

@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.0 array 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

Знімок екрана 2024-04-05 о 19 48 46

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.

  1. It's done by people who absolutely know what they are doing.
  2. It exits early if PHP 8.0 is detected.
  3. 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 with null 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 use declare(strict_types = 1); actually means the functions can be called with all kinds of types that will be transformed to string or null.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment