[PHP]str_starts_with関数を完全解説!文字列が特定の文字で始まるか確認する方法

PHP

こんにちは!今回は、PHPの標準関数であるstr_starts_with()について詳しく解説していきます。文字列が特定の文字列で始まっているかを簡単にチェックできる、PHP 8.0で追加された便利な関数です!

str_starts_with関数とは?

str_starts_with()関数は、文字列が指定した文字列で始まっているかを判定する関数です。

PHP 8.0で追加された関数で、プレフィックスのチェックが非常に簡潔に書けるようになりました!

基本的な構文

str_starts_with(string $haystack, string $needle): bool
  • $haystack: 検索対象の文字列
  • $needle: 先頭で検索する文字列
  • 戻り値: 先頭が一致する場合はtrue、一致しない場合はfalse

基本的な使用例

シンプルな判定

// 基本的な使用(PHP 8.0+)
$text = "Hello World";

if (str_starts_with($text, "Hello")) {
    echo "「Hello」で始まっています\n";
}
// 出力: 「Hello」で始まっています

if (str_starts_with($text, "World")) {
    echo "「World」で始まっています\n";
} else {
    echo "「World」で始まっていません\n";
}
// 出力: 「World」で始まっていません

大文字小文字の区別

$text = "Hello World";

// 大文字小文字は区別される
var_dump(str_starts_with($text, "hello"));  // false
var_dump(str_starts_with($text, "Hello"));  // true

// 大文字小文字を区別しない場合
$text_lower = strtolower($text);
var_dump(str_starts_with($text_lower, strtolower("HELLO")));  // true

空文字列の扱い

$text = "Hello World";

// 空文字列は常にtrueを返す
var_dump(str_starts_with($text, ""));  // true

// 空の文字列で判定
var_dump(str_starts_with("", "test"));  // false
var_dump(str_starts_with("", ""));      // true

1文字の判定

$text = "Hello World";

// 1文字でも判定可能
var_dump(str_starts_with($text, "H"));  // true
var_dump(str_starts_with($text, "h"));  // false

PHP 8.0未満での代替実装

// PHP 8.0未満用のポリフィル
if (!function_exists('str_starts_with')) {
    function str_starts_with($haystack, $needle) {
        return $needle === '' || strpos($haystack, $needle) === 0;
    }
}

// 使用例
$text = "Hello World";
var_dump(str_starts_with($text, "Hello"));  // true

実践的な使用例

例1: URLとプロトコルの判定

class UrlChecker {
    /**
     * HTTPSかどうか判定
     */
    public static function isHttps($url) {
        return str_starts_with($url, 'https://');
    }
    
    /**
     * HTTPかどうか判定
     */
    public static function isHttp($url) {
        return str_starts_with($url, 'http://');
    }
    
    /**
     * セキュアなプロトコルか判定
     */
    public static function isSecure($url) {
        $secureProtocols = ['https://', 'ftps://', 'sftp://', 'ssh://'];
        
        foreach ($secureProtocols as $protocol) {
            if (str_starts_with($url, $protocol)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 相対URLか判定
     */
    public static function isRelative($url) {
        return str_starts_with($url, '/') && !str_starts_with($url, '//');
    }
    
    /**
     * 絶対URLか判定
     */
    public static function isAbsolute($url) {
        return str_starts_with($url, 'http://') || 
               str_starts_with($url, 'https://') ||
               str_starts_with($url, '//');
    }
    
    /**
     * データURIか判定
     */
    public static function isDataUri($url) {
        return str_starts_with($url, 'data:');
    }
    
    /**
     * プロトコルを取得
     */
    public static function getProtocol($url) {
        $protocols = ['https://', 'http://', 'ftp://', 'ftps://', 'ssh://'];
        
        foreach ($protocols as $protocol) {
            if (str_starts_with($url, $protocol)) {
                return rtrim($protocol, ':/');
            }
        }
        
        return null;
    }
    
    /**
     * 特定ドメインで始まるか判定
     */
    public static function startsWithDomain($url, $domain) {
        $patterns = [
            "https://{$domain}",
            "http://{$domain}",
            "//{$domain}"
        ];
        
        foreach ($patterns as $pattern) {
            if (str_starts_with($url, $pattern)) {
                return true;
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== URLチェック ===\n";

$urls = [
    'https://example.com/path',
    'http://example.com',
    '/relative/path',
    '//cdn.example.com/file.js',
    'data:image/png;base64,iVBOR...',
    'ftp://files.example.com'
];

foreach ($urls as $url) {
    echo "\nURL: {$url}\n";
    echo "  HTTPS: " . (UrlChecker::isHttps($url) ? 'Yes' : 'No') . "\n";
    echo "  HTTP: " . (UrlChecker::isHttp($url) ? 'Yes' : 'No') . "\n";
    echo "  セキュア: " . (UrlChecker::isSecure($url) ? 'Yes' : 'No') . "\n";
    echo "  相対URL: " . (UrlChecker::isRelative($url) ? 'Yes' : 'No') . "\n";
    echo "  絶対URL: " . (UrlChecker::isAbsolute($url) ? 'Yes' : 'No') . "\n";
    echo "  データURI: " . (UrlChecker::isDataUri($url) ? 'Yes' : 'No') . "\n";
    
    $protocol = UrlChecker::getProtocol($url);
    if ($protocol) {
        echo "  プロトコル: {$protocol}\n";
    }
}

例2: ファイルパスの処理

class PathChecker {
    /**
     * 絶対パスか判定
     */
    public static function isAbsolutePath($path) {
        // Unix系の絶対パス
        if (str_starts_with($path, '/')) {
            return true;
        }
        
        // Windowsの絶対パス(C:\など)
        if (preg_match('/^[a-zA-Z]:[\\\\\/]/', $path)) {
            return true;
        }
        
        return false;
    }
    
    /**
     * 相対パスか判定
     */
    public static function isRelativePath($path) {
        return !self::isAbsolutePath($path);
    }
    
    /**
     * ホームディレクトリパスか判定
     */
    public static function isHomePath($path) {
        return str_starts_with($path, '~/');
    }
    
    /**
     * 現在ディレクトリ相対か判定
     */
    public static function isCurrentDirRelative($path) {
        return str_starts_with($path, './');
    }
    
    /**
     * 親ディレクトリ相対か判定
     */
    public static function isParentDirRelative($path) {
        return str_starts_with($path, '../');
    }
    
    /**
     * 特定ディレクトリ配下か判定
     */
    public static function isUnder($path, $directory) {
        // パス区切り文字を統一
        $path = str_replace('\\', '/', $path);
        $directory = str_replace('\\', '/', $directory);
        
        return str_starts_with($path, rtrim($directory, '/') . '/');
    }
    
    /**
     * 隠しファイル/ディレクトリか判定
     */
    public static function isHidden($path) {
        $basename = basename($path);
        return str_starts_with($basename, '.');
    }
}

// 使用例
echo "=== パスチェック ===\n";

$paths = [
    '/var/www/html/index.php',
    'C:\\Users\\Documents\\file.txt',
    './relative/path',
    '../parent/directory',
    '~/Documents/file.txt',
    '.gitignore',
    'normal.txt'
];

foreach ($paths as $path) {
    echo "\nパス: {$path}\n";
    echo "  絶対パス: " . (PathChecker::isAbsolutePath($path) ? 'Yes' : 'No') . "\n";
    echo "  相対パス: " . (PathChecker::isRelativePath($path) ? 'Yes' : 'No') . "\n";
    echo "  ホームパス: " . (PathChecker::isHomePath($path) ? 'Yes' : 'No') . "\n";
    echo "  カレント相対: " . (PathChecker::isCurrentDirRelative($path) ? 'Yes' : 'No') . "\n";
    echo "  親ディレクトリ相対: " . (PathChecker::isParentDirRelative($path) ? 'Yes' : 'No') . "\n";
    echo "  隠しファイル: " . (PathChecker::isHidden($path) ? 'Yes' : 'No') . "\n";
}

echo "\n=== ディレクトリ配下チェック ===\n";
var_dump(PathChecker::isUnder('/var/www/html/index.php', '/var/www'));  // true
var_dump(PathChecker::isUnder('/home/user/file.txt', '/var/www'));      // false

例3: プレフィックスベースのルーティング

class Router {
    private $routes = [];
    
    /**
     * ルートを追加
     */
    public function add($prefix, $handler) {
        $this->routes[] = [
            'prefix' => $prefix,
            'handler' => $handler
        ];
    }
    
    /**
     * パスにマッチするルートを検索
     */
    public function match($path) {
        foreach ($this->routes as $route) {
            if (str_starts_with($path, $route['prefix'])) {
                return [
                    'handler' => $route['handler'],
                    'prefix' => $route['prefix'],
                    'remaining' => substr($path, strlen($route['prefix']))
                ];
            }
        }
        
        return null;
    }
    
    /**
     * APIルートか判定
     */
    public static function isApiRoute($path) {
        return str_starts_with($path, '/api/');
    }
    
    /**
     * 管理画面ルートか判定
     */
    public static function isAdminRoute($path) {
        return str_starts_with($path, '/admin/');
    }
    
    /**
     * 認証が必要なルートか判定
     */
    public static function requiresAuth($path, $publicPrefixes) {
        foreach ($publicPrefixes as $prefix) {
            if (str_starts_with($path, $prefix)) {
                return false;
            }
        }
        
        return true;
    }
    
    /**
     * ロケールプレフィックスを取得
     */
    public static function getLocale($path) {
        $locales = ['en', 'ja', 'es', 'fr', 'de'];
        
        foreach ($locales as $locale) {
            if (str_starts_with($path, "/{$locale}/")) {
                return $locale;
            }
        }
        
        return null;
    }
}

// 使用例
echo "=== ルーティング ===\n";

$router = new Router();
$router->add('/api/', 'ApiController');
$router->add('/admin/', 'AdminController');
$router->add('/blog/', 'BlogController');
$router->add('/', 'HomeController');

$paths = [
    '/api/users/123',
    '/admin/dashboard',
    '/blog/post/hello-world',
    '/about'
];

foreach ($paths as $path) {
    $match = $router->match($path);
    
    if ($match) {
        echo "\nパス: {$path}\n";
        echo "  ハンドラー: {$match['handler']}\n";
        echo "  プレフィックス: {$match['prefix']}\n";
        echo "  残りのパス: {$match['remaining']}\n";
    }
}

echo "\n=== ルート種別判定 ===\n";
foreach ($paths as $path) {
    echo "\n{$path}:\n";
    echo "  API: " . (Router::isApiRoute($path) ? 'Yes' : 'No') . "\n";
    echo "  管理画面: " . (Router::isAdminRoute($path) ? 'Yes' : 'No') . "\n";
}

echo "\n=== 認証チェック ===\n";
$publicPrefixes = ['/', '/about', '/blog/'];
$testPaths = ['/admin/users', '/about', '/blog/post/1'];

foreach ($testPaths as $path) {
    $requiresAuth = Router::requiresAuth($path, $publicPrefixes) ? '必要' : '不要';
    echo "{$path}: 認証{$requiresAuth}\n";
}

echo "\n=== ロケール検出 ===\n";
$localizedPaths = ['/en/products', '/ja/about', '/es/contact', '/products'];
foreach ($localizedPaths as $path) {
    $locale = Router::getLocale($path) ?? 'デフォルト';
    echo "{$path}: {$locale}\n";
}

例4: コマンド/クエリの処理

class CommandParser {
    /**
     * コマンドプレフィックスをチェック
     */
    public static function hasCommandPrefix($input, $prefix = '/') {
        return str_starts_with($input, $prefix);
    }
    
    /**
     * コマンドを解析
     */
    public static function parseCommand($input, $prefix = '/') {
        if (!self::hasCommandPrefix($input, $prefix)) {
            return null;
        }
        
        $withoutPrefix = substr($input, strlen($prefix));
        $parts = explode(' ', $withoutPrefix, 2);
        
        return [
            'command' => $parts[0],
            'args' => $parts[1] ?? ''
        ];
    }
    
    /**
     * メンションか判定
     */
    public static function isMention($text) {
        return str_starts_with($text, '@');
    }
    
    /**
     * ハッシュタグか判定
     */
    public static function isHashtag($text) {
        return str_starts_with($text, '#');
    }
    
    /**
     * URLリンクか判定
     */
    public static function isLink($text) {
        return str_starts_with($text, 'http://') || 
               str_starts_with($text, 'https://');
    }
    
    /**
     * 特定コマンドか判定
     */
    public static function isCommand($input, $command, $prefix = '/') {
        return str_starts_with($input, $prefix . $command);
    }
    
    /**
     * 管理者コマンドか判定
     */
    public static function isAdminCommand($input) {
        $adminCommands = ['/admin', '/sudo', '/mod'];
        
        foreach ($adminCommands as $cmd) {
            if (str_starts_with($input, $cmd)) {
                return true;
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== コマンド解析 ===\n";

$inputs = [
    '/help',
    '/search php tutorial',
    '@john hello',
    '#programming',
    'https://example.com',
    '/admin ban user123',
    'regular message'
];

foreach ($inputs as $input) {
    echo "\n入力: {$input}\n";
    
    if (CommandParser::hasCommandPrefix($input)) {
        $parsed = CommandParser::parseCommand($input);
        echo "  コマンド: {$parsed['command']}\n";
        echo "  引数: {$parsed['args']}\n";
        echo "  管理者コマンド: " . (CommandParser::isAdminCommand($input) ? 'Yes' : 'No') . "\n";
    }
    
    echo "  メンション: " . (CommandParser::isMention($input) ? 'Yes' : 'No') . "\n";
    echo "  ハッシュタグ: " . (CommandParser::isHashtag($input) ? 'Yes' : 'No') . "\n";
    echo "  リンク: " . (CommandParser::isLink($input) ? 'Yes' : 'No') . "\n";
}

echo "\n=== 特定コマンドチェック ===\n";
var_dump(CommandParser::isCommand('/help', 'help'));        // true
var_dump(CommandParser::isCommand('/search test', 'search')); // true
var_dump(CommandParser::isCommand('/help', 'search'));      // false

例5: データフォーマット判定

class DataFormatChecker {
    /**
     * JSONデータか判定
     */
    public static function isJson($data) {
        $data = trim($data);
        return str_starts_with($data, '{') || str_starts_with($data, '[');
    }
    
    /**
     * XMLデータか判定
     */
    public static function isXml($data) {
        return str_starts_with(trim($data), '<?xml') ||
               str_starts_with(trim($data), '<');
    }
    
    /**
     * HTMLデータか判定
     */
    public static function isHtml($data) {
        $data = strtolower(trim($data));
        return str_starts_with($data, '<!doctype') ||
               str_starts_with($data, '<html');
    }
    
    /**
     * Base64データか判定(簡易版)
     */
    public static function isBase64($data) {
        return str_starts_with($data, 'data:') && 
               str_contains($data, ';base64,');
    }
    
    /**
     * HEX文字列か判定
     */
    public static function isHexString($data) {
        return str_starts_with($data, '0x') || 
               str_starts_with($data, '0X');
    }
    
    /**
     * バイナリ文字列か判定
     */
    public static function isBinaryString($data) {
        return str_starts_with($data, '0b');
    }
    
    /**
     * UUID形式か判定(簡易版)
     */
    public static function looksLikeUuid($data) {
        return preg_match('/^[0-9a-f]{8}-/', strtolower($data)) === 1;
    }
}

// 使用例
echo "=== データフォーマット判定 ===\n";

$samples = [
    '{"name": "John"}',
    '<?xml version="1.0"?><root></root>',
    '<!DOCTYPE html><html></html>',
    'data:image/png;base64,iVBOR...',
    '0x1A2B3C',
    '0b10101010',
    '550e8400-e29b-41d4-a716-446655440000',
    'Plain text'
];

foreach ($samples as $sample) {
    echo "\nデータ: " . substr($sample, 0, 40) . "...\n";
    echo "  JSON: " . (DataFormatChecker::isJson($sample) ? 'Yes' : 'No') . "\n";
    echo "  XML: " . (DataFormatChecker::isXml($sample) ? 'Yes' : 'No') . "\n";
    echo "  HTML: " . (DataFormatChecker::isHtml($sample) ? 'Yes' : 'No') . "\n";
    echo "  Base64: " . (DataFormatChecker::isBase64($sample) ? 'Yes' : 'No') . "\n";
    echo "  HEX: " . (DataFormatChecker::isHexString($sample) ? 'Yes' : 'No') . "\n";
    echo "  Binary: " . (DataFormatChecker::isBinaryString($sample) ? 'Yes' : 'No') . "\n";
    echo "  UUID: " . (DataFormatChecker::looksLikeUuid($sample) ? 'Yes' : 'No') . "\n";
}

例6: テキストパターン検出

class TextPatternDetector {
    /**
     * 質問文か判定
     */
    public static function isQuestion($text) {
        $questionStarters = [
            'what', 'why', 'how', 'when', 'where', 'who',
            'can', 'could', 'would', 'should', 'is', 'are', 'do', 'does'
        ];
        
        $text = strtolower(trim($text));
        
        foreach ($questionStarters as $starter) {
            if (str_starts_with($text, $starter)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 命令文か判定
     */
    public static function isCommand($text) {
        $commandStarters = [
            'please', 'go', 'open', 'close', 'start', 'stop',
            'create', 'delete', 'update', 'show', 'display'
        ];
        
        $text = strtolower(trim($text));
        
        foreach ($commandStarters as $starter) {
            if (str_starts_with($text, $starter)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 日本語で始まるか判定
     */
    public static function startsWithJapanese($text) {
        $japaneseStarters = ['こ', 'あ', 'そ', 'お', 'で', 'が', 'を', 'に', 'は'];
        
        foreach ($japaneseStarters as $starter) {
            if (str_starts_with($text, $starter)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 否定文か判定
     */
    public static function isNegative($text) {
        $negativeStarters = [
            "don't", "doesn't", "didn't", "not", "no", "never", "cannot", "can't"
        ];
        
        $text = strtolower(trim($text));
        
        foreach ($negativeStarters as $starter) {
            if (str_starts_with($text, $starter)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 敬語か判定(日本語簡易版)
     */
    public static function isPolite($text) {
        $politeStarters = ['お', 'ご', 'いただ', 'お願い', 'ありがとう'];
        
        foreach ($politeStarters as $starter) {
            if (str_starts_with($text, $starter)) {
                return true;
            }
        }
        
        return false;
    }
}

// 使用例
echo "=== テキストパターン検出 ===\n";

$texts = [
    'What is PHP?',
    'Please open the file',
    "Don't do that",
    'こんにちは',
    'お願いします',
    'This is a statement'
];

foreach ($texts as $text) {
    echo "\nテキスト: {$text}\n";
    echo "  質問: " . (TextPatternDetector::isQuestion($text) ? 'Yes' : 'No') . "\n";
    echo "  命令: " . (TextPatternDetector::isCommand($text) ? 'Yes' : 'No') . "\n";
    echo "  否定: " . (TextPatternDetector::isNegative($text) ? 'Yes' : 'No') . "\n";
    echo "  日本語開始: " . (TextPatternDetector::startsWithJapanese($text) ? 'Yes' : 'No') . "\n";
    echo "  敬語: " . (TextPatternDetector::isPolite($text) ? 'Yes' : 'No') . "\n";
}

例7: IDとプレフィックス検証

class IdValidator {
    /**
     * 特定プレフィックスのIDか検証
     */
    public static function hasPrefix($id, $prefix) {
        return str_starts_with($id, $prefix);
    }
    
    /**
     * ユーザーIDか判定
     */
    public static function isUserId($id) {
        return str_starts_with($id, 'user_') || 
               str_starts_with($id, 'usr_');
    }
    
    /**
     * 注文IDか判定
     */
    public static function isOrderId($id) {
        return str_starts_with($id, 'order_') || 
               str_starts_with($id, 'ord_');
    }
    
    /**
     * 製品IDか判定
     */
    public static function isProductId($id) {
        return str_starts_with($id, 'prod_') || 
               str_starts_with($id, 'product_');
    }
    
    /**
     * 一時IDか判定
     */
    public static function isTemporaryId($id) {
        return str_starts_with($id, 'tmp_') || 
               str_starts_with($id, 'temp_');
    }
    
    /**
     * テストIDか判定
     */
    public static function isTestId($id) {
        return str_starts_with($id, 'test_') || 
               str_starts_with($id, 'dev_');
    }
    
    /**
     * IDの種類を判定
     */
    public static function getIdType($id) {
        if (self::isUserId($id)) return 'user';
        if (self::isOrderId($id)) return 'order';
        if (self::isProductId($id)) return 'product';
        if (self::isTemporaryId($id)) return 'temporary';
        if (self::isTestId($id)) return 'test';
        return 'unknown';
    }
}

// 使用例
echo "=== ID検証 ===\n";

$ids = [
    'user_12345',
    'order_67890',
    'prod_ABC123',
    'tmp_xyz789',
    'test_demo',
    'random_id'
];

foreach ($ids as $id) {
    $type = IdValidator::getIdType($id);
    echo "{$id}: {$type}\n";
}

echo "\n=== プレフィックスチェック ===\n";
var_dump(IdValidator::hasPrefix('user_123', 'user_'));    // true
var_dump(IdValidator::hasPrefix('order_456', 'user_'));   // false

従来の方法との比較

$text = "Hello World";
$prefix = "Hello";

// 新しい方法(PHP 8.0+)
if (str_starts_with($text, $prefix)) {
    echo "先頭が一致\n";
}

// 従来の方法
if (strpos($text, $prefix) === 0) {
    echo "先頭が一致\n";
}

// または
if (substr($text, 0, strlen($prefix)) === $prefix) {
    echo "先頭が一致\n";
}

// 利点:
// - より直感的
// - 型判定の問題がない
// - コードが読みやすい

まとめ

str_starts_with()関数の特徴をまとめると:

できること:

  • 文字列の先頭一致チェック
  • プレフィックスの検証
  • ブール値を返す

推奨される使用場面:

  • URL/プロトコル判定
  • ファイルパス処理
  • ルーティング
  • コマンド解析
  • データフォーマット判定
  • ID検証

利点:

  • 直感的で読みやすい
  • 型の問題がない
  • PHP 8.0で標準化

注意点:

  • PHP 8.0以降でのみ使用可能
  • 大文字小文字を区別する
  • 空文字列は常にtrue

関連関数:

  • str_ends_with(): 末尾一致
  • str_contains(): 含まれるか
  • strpos(): 位置を返す
  • substr(): 部分文字列を取得

使い分け:

// 先頭一致
str_starts_with($text, $prefix)

// 末尾一致
str_ends_with($text, $suffix)

// 含まれるか
str_contains($text, $needle)

// 位置が必要
strpos($text, $needle)

str_starts_with()は、PHP 8.0で追加された非常に便利な関数です。プレフィックスのチェックが直感的に書けるので、PHP 8.0以降を使用している場合は積極的に活用しましょう!

タイトルとURLをコピーしました