From 4ca4dbaabe1f411de04cf1d55768842b6c1ed1ad Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 8 Jan 2024 10:48:04 +0000
Subject: [PATCH] Issue #3328456 by xjm, dimitriskr, murilohp, smustgrave:
 Replace substr($a, 0, $i) with str_starts_with()

---
 core/includes/theme.inc                                   | 2 +-
 core/lib/Drupal/Component/FileSystem/FileSystem.php       | 2 +-
 core/lib/Drupal/Component/Utility/UserAgent.php           | 2 +-
 core/lib/Drupal/Component/Utility/Xss.php                 | 8 ++++----
 core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php  | 2 +-
 core/lib/Drupal/Core/File/FileSystem.php                  | 8 ++++----
 core/lib/Drupal/Core/Form/FormState.php                   | 2 +-
 .../lib/Drupal/Core/Password/PhpassHashedPasswordBase.php | 4 ++--
 .../lib/Drupal/Core/Render/MainContent/DialogRenderer.php | 2 +-
 .../Core/Routing/Enhancer/ParamConversionEnhancer.php     | 2 +-
 core/lib/Drupal/Core/Routing/RouteMatch.php               | 2 +-
 core/lib/Drupal/Core/Test/PhpUnitTestRunner.php           | 2 +-
 core/modules/ckeditor5/src/HTMLRestrictions.php           | 2 +-
 .../field/tests/src/Kernel/FieldTypePluginManagerTest.php | 2 +-
 .../file/src/Plugin/migrate/process/d6/FileUri.php        | 2 +-
 core/modules/filter/filter.module                         | 3 +--
 .../src/Normalizer/JsonApiDocumentTopLevelNormalizer.php  | 2 +-
 .../link/src/Plugin/Field/FieldWidget/LinkWidget.php      | 2 +-
 core/modules/locale/locale.module                         | 2 +-
 .../src/Plugin/MigrateDestinationPluginManager.php        | 2 +-
 .../EventSubscriber/EntityResourcePostRouteSubscriber.php | 2 +-
 .../migrate/process/SearchConfigurationRankings.php       | 2 +-
 core/modules/system/system.install                        | 2 +-
 core/modules/user/src/Form/UserLoginForm.php              | 2 +-
 .../Validation/Constraint/UserNameConstraintValidator.php | 2 +-
 core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php | 2 +-
 core/tests/Drupal/KernelTests/Core/File/FileTestBase.php  | 4 ++--
 .../KernelTests/Core/Theme/Stable9LibraryOverrideTest.php | 2 +-
 .../Tests/Listeners/DrupalComponentTestListenerTrait.php  | 2 +-
 29 files changed, 37 insertions(+), 38 deletions(-)

diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index ca0564118d94..ba8f4ee7b7ea 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -449,7 +449,7 @@ function theme_settings_convert_to_config(array $theme_settings, Config $config)
     elseif ($key == 'favicon_mimetype') {
       $config->set('favicon.mimetype', $value);
     }
-    elseif (substr($key, 0, 7) == 'toggle_') {
+    elseif (str_starts_with($key, 'toggle_')) {
       $config->set('features.' . mb_substr($key, 7), $value);
     }
     elseif (!in_array($key, ['theme', 'logo_upload'])) {
diff --git a/core/lib/Drupal/Component/FileSystem/FileSystem.php b/core/lib/Drupal/Component/FileSystem/FileSystem.php
index 69bc993dca19..a0486604316d 100644
--- a/core/lib/Drupal/Component/FileSystem/FileSystem.php
+++ b/core/lib/Drupal/Component/FileSystem/FileSystem.php
@@ -25,7 +25,7 @@ public static function getOsTemporaryDirectory() {
     }
 
     // Operating system specific dirs.
-    if (substr(PHP_OS, 0, 3) == 'WIN') {
+    if (str_starts_with(PHP_OS, 'WIN')) {
       $directories[] = 'c:\\windows\\temp';
       $directories[] = 'c:\\winnt\\temp';
     }
diff --git a/core/lib/Drupal/Component/Utility/UserAgent.php b/core/lib/Drupal/Component/Utility/UserAgent.php
index f7403e2fdd57..360b2d954c40 100644
--- a/core/lib/Drupal/Component/Utility/UserAgent.php
+++ b/core/lib/Drupal/Component/Utility/UserAgent.php
@@ -88,7 +88,7 @@ public static function getBestMatchingLangcode($http_accept_language, $langcodes
       // first occurrence of '-' otherwise we get a non-existing language zh.
       // All other languages use a langcode without a '-', so we can safely
       // split on the first occurrence of it.
-      if (strlen($langcode) > 7 && (substr($langcode, 0, 7) == 'zh-hant' || substr($langcode, 0, 7) == 'zh-hans')) {
+      if (strlen($langcode) > 7 && (str_starts_with($langcode, 'zh-hant') || str_starts_with($langcode, 'zh-hans'))) {
         $generic_tag = substr($langcode, 0, 7);
       }
       else {
diff --git a/core/lib/Drupal/Component/Utility/Xss.php b/core/lib/Drupal/Component/Utility/Xss.php
index 7480c3793eeb..2c143cddf84b 100644
--- a/core/lib/Drupal/Component/Utility/Xss.php
+++ b/core/lib/Drupal/Component/Utility/Xss.php
@@ -141,7 +141,7 @@ public static function filterAdmin($string) {
    *   version of the HTML element.
    */
   protected static function split($string, array $html_tags, $class) {
-    if (substr($string, 0, 1) != '<') {
+    if (!str_starts_with($string, '<')) {
       // We matched a lone ">" character.
       return '&gt;';
     }
@@ -217,8 +217,8 @@ protected static function attributes($attributes) {
             $attribute_name = strtolower($match[1]);
             $skip = (
               $attribute_name == 'style' ||
-              substr($attribute_name, 0, 2) == 'on' ||
-              substr($attribute_name, 0, 1) == '-' ||
+              str_starts_with($attribute_name, 'on') ||
+              str_starts_with($attribute_name, '-') ||
               // Ignore long attributes to avoid unnecessary processing
               // overhead.
               strlen($attribute_name) > 96
@@ -232,7 +232,7 @@ protected static function attributes($attributes) {
             // such attributes.
             // @see \Drupal\Component\Utility\UrlHelper::filterBadProtocol()
             // @see http://www.w3.org/TR/html4/index/attributes.html
-            $skip_protocol_filtering = substr($attribute_name, 0, 5) === 'data-' || in_array($attribute_name, [
+            $skip_protocol_filtering = str_starts_with($attribute_name, 'data-') || in_array($attribute_name, [
               'title',
               'alt',
               'rel',
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php b/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php
index 03e00ef45102..3e3cb1e62dc7 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/QueryAggregate.php
@@ -154,7 +154,7 @@ protected function finish() {
   public function createSqlAlias($field, $sql_field) {
     $alias = str_replace('.', '_', $sql_field);
     // If the alias contains of field_*_value remove the _value at the end.
-    if (substr($alias, 0, 6) === 'field_' && substr($field, -6) !== '_value' && substr($alias, -6) === '_value') {
+    if (str_starts_with($alias, 'field_') && substr($field, -6) !== '_value' && substr($alias, -6) === '_value') {
       $alias = substr($alias, 0, -6);
     }
     return $alias;
diff --git a/core/lib/Drupal/Core/File/FileSystem.php b/core/lib/Drupal/Core/File/FileSystem.php
index c82d61fbe351..9d1e66b4bd4b 100644
--- a/core/lib/Drupal/Core/File/FileSystem.php
+++ b/core/lib/Drupal/Core/File/FileSystem.php
@@ -114,7 +114,7 @@ public function chmod($uri, $mode = NULL) {
    * {@inheritdoc}
    */
   public function unlink($uri, $context = NULL) {
-    if (!$this->streamWrapperManager->isValidUri($uri) && (substr(PHP_OS, 0, 3) == 'WIN')) {
+    if (!$this->streamWrapperManager->isValidUri($uri) && str_starts_with(PHP_OS, 'WIN')) {
       chmod($uri, 0600);
     }
     if ($context) {
@@ -257,7 +257,7 @@ protected function mkdirCall($uri, $mode, $recursive, $context) {
    * {@inheritdoc}
    */
   public function rmdir($uri, $context = NULL) {
-    if (!$this->streamWrapperManager->isValidUri($uri) && (substr(PHP_OS, 0, 3) == 'WIN')) {
+    if (!$this->streamWrapperManager->isValidUri($uri) && str_starts_with(PHP_OS, 'WIN')) {
       chmod($uri, 0700);
     }
     if ($context) {
@@ -383,7 +383,7 @@ public function move($source, $destination, $replace = self::EXISTS_RENAME) {
 
     // Ensure compatibility with Windows.
     // @see \Drupal\Core\File\FileSystemInterface::unlink().
-    if (!$this->streamWrapperManager->isValidUri($source) && (substr(PHP_OS, 0, 3) == 'WIN')) {
+    if (!$this->streamWrapperManager->isValidUri($source) && str_starts_with(PHP_OS, 'WIN')) {
       chmod($source, 0600);
     }
     // Attempt to resolve the URIs. This is necessary in certain
@@ -590,7 +590,7 @@ public function createFilename($basename, $directory) {
     if (preg_last_error() !== PREG_NO_ERROR) {
       throw new FileException(sprintf("Invalid filename '%s'", $original));
     }
-    if (substr(PHP_OS, 0, 3) == 'WIN') {
+    if (str_starts_with(PHP_OS, 'WIN')) {
       // These characters are not allowed in Windows filenames.
       $basename = str_replace([':', '*', '?', '"', '<', '>', '|'], '_', $basename);
     }
diff --git a/core/lib/Drupal/Core/Form/FormState.php b/core/lib/Drupal/Core/Form/FormState.php
index 9c94c79d2c53..7e6d974cb73f 100644
--- a/core/lib/Drupal/Core/Form/FormState.php
+++ b/core/lib/Drupal/Core/Form/FormState.php
@@ -1192,7 +1192,7 @@ public function isRebuilding() {
    * {@inheritdoc}
    */
   public function prepareCallback($callback) {
-    if (is_string($callback) && substr($callback, 0, 2) == '::') {
+    if (is_string($callback) && str_starts_with($callback, '::')) {
       $callback = [$this->getFormObject(), substr($callback, 2)];
     }
     return $callback;
diff --git a/core/lib/Drupal/Core/Password/PhpassHashedPasswordBase.php b/core/lib/Drupal/Core/Password/PhpassHashedPasswordBase.php
index 7be8e393801f..ae9ae35d12fc 100644
--- a/core/lib/Drupal/Core/Password/PhpassHashedPasswordBase.php
+++ b/core/lib/Drupal/Core/Password/PhpassHashedPasswordBase.php
@@ -246,7 +246,7 @@ public function check(#[\SensitiveParameter] $password, #[\SensitiveParameter] $
     if ($hash === NULL || $hash === '') {
       return FALSE;
     }
-    if (substr($hash, 0, 2) == 'U$') {
+    if (str_starts_with($hash, 'U$')) {
       // This may be an updated password from user_update_7000(). Such hashes
       // have 'U' added as the first character and need an extra md5() (see the
       // Drupal 7 documentation).
@@ -293,7 +293,7 @@ public function needsRehash(#[\SensitiveParameter] $hash) {
     }
 
     // Check whether this was an updated password.
-    if ((substr($hash, 0, 3) != '$S$') || (strlen($hash) != static::HASH_LENGTH)) {
+    if (!str_starts_with($hash, '$S$') || (strlen($hash) != static::HASH_LENGTH)) {
       return TRUE;
     }
     // Ensure that $count_log2 is within set bounds.
diff --git a/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php b/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
index 6468e02d17e7..1fec7345f5f1 100644
--- a/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
@@ -86,7 +86,7 @@ protected function determineTargetSelector(array &$options, RouteMatchInterface
       // If the target was nominated in the incoming options, use that.
       $target = $options['target'];
       // Ensure the target includes the #.
-      if (substr($target, 0, 1) != '#') {
+      if (!str_starts_with($target, '#')) {
         $target = '#' . $target;
       }
       // This shouldn't be passed on to jQuery.ui.dialog.
diff --git a/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
index 2ec558a36e05..9d089d890767 100644
--- a/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
+++ b/core/lib/Drupal/Core/Routing/Enhancer/ParamConversionEnhancer.php
@@ -70,7 +70,7 @@ protected function copyRawVariables(array $defaults) {
     // Route defaults that do not start with a leading "_" are also
     // parameters, even if they are not included in path or host patterns.
     foreach ($route->getDefaults() as $name => $value) {
-      if (!isset($raw_variables[$name]) && substr($name, 0, 1) !== '_') {
+      if (!isset($raw_variables[$name]) && !str_starts_with($name, '_')) {
         $raw_variables[$name] = $value;
       }
     }
diff --git a/core/lib/Drupal/Core/Routing/RouteMatch.php b/core/lib/Drupal/Core/Routing/RouteMatch.php
index 63304fa25a01..175865993afb 100644
--- a/core/lib/Drupal/Core/Routing/RouteMatch.php
+++ b/core/lib/Drupal/Core/Routing/RouteMatch.php
@@ -149,7 +149,7 @@ protected function getParameterNames() {
       // Route defaults that do not start with a leading "_" are also
       // parameters, even if they are not included in path or host patterns.
       foreach ($route->getDefaults() as $name => $value) {
-        if (!isset($names[$name]) && substr($name, 0, 1) !== '_') {
+        if (!isset($names[$name]) && !str_starts_with($name, '_')) {
           $names[$name] = $name;
         }
       }
diff --git a/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php b/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php
index 35d28ccf5dfd..425f125aa87c 100644
--- a/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php
+++ b/core/lib/Drupal/Core/Test/PhpUnitTestRunner.php
@@ -82,7 +82,7 @@ public function phpUnitCommand(): string {
     // The file in Composer's bin dir is a *nix link, which does not work when
     // extracted from a tarball and generally not on Windows.
     $command = $vendor_dir . '/phpunit/phpunit/phpunit';
-    if (substr(PHP_OS, 0, 3) == 'WIN') {
+    if (str_starts_with(PHP_OS, 'WIN')) {
       // On Windows it is necessary to run the script using the PHP executable.
       $php_executable_finder = new PhpExecutableFinder();
       $php = $php_executable_finder->find();
diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php
index 09485d93a2b8..4924cf45f98c 100644
--- a/core/modules/ckeditor5/src/HTMLRestrictions.php
+++ b/core/modules/ckeditor5/src/HTMLRestrictions.php
@@ -1116,7 +1116,7 @@ public function extractPlainTagsSubset(): HTMLRestrictions {
    *   TRUE if it is a wildcard, otherwise FALSE.
    */
   private static function isWildcardTag(string $tag_name): bool {
-    return substr($tag_name, 0, 1) === '$' && array_key_exists($tag_name, self::WILDCARD_ELEMENT_METHODS);
+    return str_starts_with($tag_name, '$') && array_key_exists($tag_name, self::WILDCARD_ELEMENT_METHODS);
   }
 
   /**
diff --git a/core/modules/field/tests/src/Kernel/FieldTypePluginManagerTest.php b/core/modules/field/tests/src/Kernel/FieldTypePluginManagerTest.php
index 83aae5e2b4ce..677528acda27 100644
--- a/core/modules/field/tests/src/Kernel/FieldTypePluginManagerTest.php
+++ b/core/modules/field/tests/src/Kernel/FieldTypePluginManagerTest.php
@@ -131,7 +131,7 @@ protected function enableAllCoreModules() {
     /** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
     $module_handler = $this->container->get('module_handler');
     $module_list = array_filter(array_keys($module_list), function ($module) use ($module_handler, $module_list) {
-      return !$module_handler->moduleExists($module) && substr($module_list[$module]->getPath(), 0, 4) === 'core';
+      return !$module_handler->moduleExists($module) && str_starts_with($module_list[$module]->getPath(), 'core');
     });
     $this->enableModules($module_list);
   }
diff --git a/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php b/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php
index 2acf883f97d4..08eb189a3901 100644
--- a/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php
+++ b/core/modules/file/src/Plugin/migrate/process/d6/FileUri.php
@@ -27,7 +27,7 @@ public function transform($value, MigrateExecutableInterface $migrate_executable
     [$filepath, $file_directory_path, $temp_directory_path, $is_public] = $value;
 
     // Specific handling using $temp_directory_path for temporary files.
-    if (substr($filepath, 0, strlen($temp_directory_path)) === $temp_directory_path) {
+    if (str_starts_with($filepath, $temp_directory_path)) {
       $uri = preg_replace('/^' . preg_quote($temp_directory_path, '/') . '/', '', $filepath);
       return 'temporary://' . ltrim($uri, '/');
     }
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 8814e0d8ea02..32fc19fe377b 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -713,8 +713,7 @@ function _filter_autop($text) {
   $output = '';
   foreach ($chunks as $i => $chunk) {
     if ($i % 2) {
-      $comment = (substr($chunk, 0, 4) == '<!--');
-      if ($comment) {
+      if (str_starts_with($chunk, '<!--')) {
         // Nothing to do, this is a comment.
         $output .= $chunk;
         continue;
diff --git a/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
index d12ec670f41e..6954c5b73ab5 100644
--- a/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
+++ b/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -146,7 +146,7 @@ public function denormalize($data, $class, $format = NULL, array $context = []):
             $reference_item += $relationship['data'][$delta]['meta'];
           }
           $canonical_ids[] = array_filter($reference_item, function ($key) {
-            return substr($key, 0, strlen('drupal_internal__')) !== 'drupal_internal__';
+            return !str_starts_with($key, 'drupal_internal__');
           }, ARRAY_FILTER_USE_KEY);
         }
 
diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
index 7452a38eccf3..97015d5bef8f 100644
--- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
+++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
@@ -147,7 +147,7 @@ public static function validateUriElement($element, FormStateInterface $form_sta
     // URI , ensure the raw value begins with '/', '?' or '#'.
     // @todo '<front>' is valid input for BC reasons, may be removed by
     //   https://www.drupal.org/node/2421941
-    if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') {
+    if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && !str_starts_with($element['#value'], '<front>')) {
       $form_state->setError($element, new TranslatableMarkup('Manually entered paths should start with one of the following characters: / ? #'));
       return;
     }
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index 2075c37c0159..acbbaba12ea6 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -526,7 +526,7 @@ function locale_js_translate(array $files = [], $language_interface = NULL) {
   foreach ($files as $filepath) {
     if (!in_array($filepath, $parsed)) {
       // Don't parse our own translations files.
-      if (substr($filepath, 0, strlen($dir)) != $dir) {
+      if (!str_starts_with($filepath, $dir)) {
         _locale_parse_js_file($filepath);
         $parsed[] = $filepath;
         $new_files = TRUE;
diff --git a/core/modules/migrate/src/Plugin/MigrateDestinationPluginManager.php b/core/modules/migrate/src/Plugin/MigrateDestinationPluginManager.php
index 04de74954612..b65515c3afed 100644
--- a/core/modules/migrate/src/Plugin/MigrateDestinationPluginManager.php
+++ b/core/modules/migrate/src/Plugin/MigrateDestinationPluginManager.php
@@ -55,7 +55,7 @@ public function __construct($type, \Traversable $namespaces, CacheBackendInterfa
    * A specific createInstance method is necessary to pass the migration on.
    */
   public function createInstance($plugin_id, array $configuration = [], MigrationInterface $migration = NULL) {
-    if (substr($plugin_id, 0, 7) == 'entity:' && !$this->entityTypeManager->getDefinition(substr($plugin_id, 7), FALSE)) {
+    if (str_starts_with($plugin_id, 'entity:') && !$this->entityTypeManager->getDefinition(substr($plugin_id, 7), FALSE)) {
       $plugin_id = 'null';
     }
     return parent::createInstance($plugin_id, $configuration, $migration);
diff --git a/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php b/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php
index 42830f66b2ed..02a4a14e3d6b 100644
--- a/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php
+++ b/core/modules/rest/src/EventSubscriber/EntityResourcePostRouteSubscriber.php
@@ -44,7 +44,7 @@ public function onDynamicRouteEvent(RouteBuildEvent $event) {
       // We only care about REST resource config entities for the
       // \Drupal\rest\Plugin\rest\resource\EntityResource plugin.
       $plugin_id = $resource_config->toArray()['plugin_id'];
-      if (substr($plugin_id, 0, 6) !== 'entity') {
+      if (!str_starts_with($plugin_id, 'entity')) {
         continue;
       }
 
diff --git a/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php b/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php
index 153161f9a91e..f35bdfe7bcd9 100644
--- a/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php
+++ b/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php
@@ -23,7 +23,7 @@ class SearchConfigurationRankings extends ProcessPluginBase {
   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
     $return = NULL;
     foreach ($row->getSource() as $name => $rank) {
-      if (substr($name, 0, 10) == 'node_rank_' && is_numeric($rank)) {
+      if (str_starts_with($name, 'node_rank_') && is_numeric($rank)) {
         $return[substr($name, 10)] = $rank;
       }
     }
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 8f557e9ac4d2..118125e60f9c 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -1426,7 +1426,7 @@ function system_requirements($phase) {
   // file names. There is no definite root directory depth below which Drupal is
   // guaranteed to function correctly on Windows. Since problems are likely
   // with more than 100 characters in the Drupal root path, show an error.
-  if (substr(PHP_OS, 0, 3) == 'WIN') {
+  if (str_starts_with(PHP_OS, 'WIN')) {
     $depth = strlen(realpath(DRUPAL_ROOT . '/' . PublicStream::basePath()));
     if ($depth > 120) {
       $requirements['max_path_on_windows'] = [
diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php
index 0c86edbee59d..54a3d4280fa0 100644
--- a/core/modules/user/src/Form/UserLoginForm.php
+++ b/core/modules/user/src/Form/UserLoginForm.php
@@ -217,7 +217,7 @@ public function validateAuthentication(array &$form, FormStateInterface $form_st
           // Now check the actual limit for the user. Default is to allow 5
           // failed attempts every 6 hours. This means we check the flood table
           // twice if flood control has already been triggered by a previous
-          // login attempt, bu this should be the less common case.
+          // login attempt, but this should be the less common case.
           if (!$this->userFloodControl->isAllowed('user.failed_login_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
             $form_state->set('flood_control_triggered', 'user');
             return;
diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php
index b775d09fa790..bbe7ea07809c 100644
--- a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php
+++ b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php
@@ -20,7 +20,7 @@ public function validate($items, Constraint $constraint) {
       return;
     }
     $name = $items->first()->value;
-    if (substr($name, 0, 1) == ' ') {
+    if (str_starts_with($name, ' ')) {
       $this->context->addViolation($constraint->spaceBeginMessage);
     }
     if (substr($name, -1) == ' ') {
diff --git a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
index 867dce7d031b..5530bba27b4f 100644
--- a/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/DirectoryTest.php
@@ -77,7 +77,7 @@ public function testFileCheckDirectoryHandling() {
     // Make sure directory actually exists.
     $this->assertDirectoryExists($directory);
     $file_system = \Drupal::service('file_system');
-    if (substr(PHP_OS, 0, 3) != 'WIN') {
+    if (!str_starts_with(PHP_OS, 'WIN')) {
       // PHP on Windows doesn't support any kind of useful read-only mode for
       // directories. When executing a chmod() on a directory, PHP only sets the
       // read-only flag, which doesn't prevent files to actually be written
diff --git a/core/tests/Drupal/KernelTests/Core/File/FileTestBase.php b/core/tests/Drupal/KernelTests/Core/File/FileTestBase.php
index ac0376d69622..b41787f452ad 100644
--- a/core/tests/Drupal/KernelTests/Core/File/FileTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/File/FileTestBase.php
@@ -98,7 +98,7 @@ public function assertFilePermissions($filepath, $expected_mode, $message = NULL
     // read/write/execute bits. On Windows, chmod() ignores the "group" and
     // "other" bits, and fileperms() returns the "user" bits in all three
     // positions. $expected_mode is updated to reflect this.
-    if (substr(PHP_OS, 0, 3) == 'WIN') {
+    if (str_starts_with(PHP_OS, 'WIN')) {
       // Reset the "group" and "other" bits.
       $expected_mode = $expected_mode & 0700;
       // Shift the "user" bits to the "group" and "other" positions also.
@@ -134,7 +134,7 @@ public function assertDirectoryPermissions($directory, $expected_mode, $message
     // read/write/execute bits. On Windows, chmod() ignores the "group" and
     // "other" bits, and fileperms() returns the "user" bits in all three
     // positions. $expected_mode is updated to reflect this.
-    if (substr(PHP_OS, 0, 3) == 'WIN') {
+    if (str_starts_with(PHP_OS, 'WIN')) {
       // Reset the "group" and "other" bits.
       $expected_mode = $expected_mode & 0700;
       // Shift the "user" bits to the "group" and "other" positions also.
diff --git a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php
index 2c2f0e6a0059..39ccd4038d53 100644
--- a/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Theme/Stable9LibraryOverrideTest.php
@@ -71,7 +71,7 @@ public function testStable9LibraryOverrides() {
           continue;
         }
         // Skip internal libraries.
-        if (substr($library_name, 0, 9) === 'internal.') {
+        if (str_starts_with($library_name, 'internal.')) {
           continue;
         }
         $library_after = $libraries_after[$extension][$library_name];
diff --git a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php b/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php
index 6428bae39c0e..9730c37dedd6 100644
--- a/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php
+++ b/core/tests/Drupal/Tests/Listeners/DrupalComponentTestListenerTrait.php
@@ -24,7 +24,7 @@ trait DrupalComponentTestListenerTrait {
    */
   protected function componentEndTest($test, $time) {
     /** @var \PHPUnit\Framework\Test $test */
-    if (substr($test->toString(), 0, 22) == 'Drupal\Tests\Component') {
+    if (str_starts_with($test->toString(), 'Drupal\Tests\Component')) {
       if ($test instanceof BrowserTestBase || $test instanceof KernelTestBase || $test instanceof UnitTestCase) {
         $error = new AssertionFailedError('Component tests should not extend a core test base class.');
         $test->getTestResultObject()->addFailure($test, $error, $time);
-- 
GitLab