こんにちは!今回は、PHPの標準関数であるsubstr_compare()について詳しく解説していきます。文字列の一部を効率的に比較できる、非常に便利な関数です!
substr_compare関数とは?
substr_compare()関数は、文字列の指定した位置から別の文字列と比較する関数です。
substr() + strcmp()を一度に行える効率的な関数で、大文字小文字の区別やバイナリセーフな比較が可能です!
基本的な構文
substr_compare(
string $haystack,
string $needle,
int $offset,
?int $length = null,
bool $case_insensitive = false
): int
- $haystack: 比較対象の文字列
- $needle: 比較する文字列
- $offset: 比較を開始する位置
- $length: 比較する長さ(省略可能)
- $case_insensitive:
trueで大文字小文字を区別しない - 戻り値:
0: 一致< 0: haystackの部分がneedleより小さい> 0: haystackの部分がneedleより大きい
基本的な使用例
シンプルな比較
$text = "Hello World";
// 位置0から"Hello"と比較
$result = substr_compare($text, "Hello", 0);
echo $result . "\n"; // 0(一致)
// 位置6から"World"と比較
$result = substr_compare($text, "World", 6);
echo $result . "\n"; // 0(一致)
// 位置0から"World"と比較
$result = substr_compare($text, "World", 0);
echo $result . "\n"; // -1(不一致、H < W)
長さを指定した比較
$text = "Hello World";
// 位置0から5文字を"Hello"と比較
$result = substr_compare($text, "Hello", 0, 5);
echo $result . "\n"; // 0(一致)
// 位置0から3文字を"Hel"と比較
$result = substr_compare($text, "Hel", 0, 3);
echo $result . "\n"; // 0(一致)
// 位置6から3文字を"Wor"と比較
$result = substr_compare($text, "Wor", 6, 3);
echo $result . "\n"; // 0(一致)
大文字小文字を区別しない比較
$text = "Hello World";
// 大文字小文字を区別する(デフォルト)
$result = substr_compare($text, "hello", 0, 5);
echo $result . "\n"; // 1(H > h)
// 大文字小文字を区別しない
$result = substr_compare($text, "hello", 0, 5, true);
echo $result . "\n"; // 0(一致)
// 位置6から、大文字小文字無視
$result = substr_compare($text, "WORLD", 6, 5, true);
echo $result . "\n"; // 0(一致)
負のオフセット
$text = "Hello World";
// 末尾から5文字を"World"と比較
$result = substr_compare($text, "World", -5);
echo $result . "\n"; // 0(一致)
// 末尾から5文字を"WORLD"と比較(大文字小文字無視)
$result = substr_compare($text, "WORLD", -5, 5, true);
echo $result . "\n"; // 0(一致)
実践的な使用例
例1: ファイル拡張子のチェック
class FileExtensionChecker {
/**
* 特定の拡張子か確認
*/
public static function hasExtension($filename, $extension) {
$extLength = strlen($extension);
// 拡張子に.がない場合は追加
if ($extension[0] !== '.') {
$extension = '.' . $extension;
$extLength++;
}
// 大文字小文字を区別せず比較
return substr_compare($filename, $extension, -$extLength, $extLength, true) === 0;
}
/**
* 複数の拡張子のいずれかか確認
*/
public static function hasAnyExtension($filename, $extensions) {
foreach ($extensions as $ext) {
if (self::hasExtension($filename, $ext)) {
return true;
}
}
return false;
}
/**
* 画像ファイルか確認
*/
public static function isImage($filename) {
return self::hasAnyExtension($filename, [
'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'
]);
}
/**
* ドキュメントファイルか確認
*/
public static function isDocument($filename) {
return self::hasAnyExtension($filename, [
'pdf', 'doc', 'docx', 'txt', 'rtf', 'odt'
]);
}
/**
* 実行ファイルか確認
*/
public static function isExecutable($filename) {
return self::hasAnyExtension($filename, [
'exe', 'bat', 'sh', 'cmd', 'com'
]);
}
/**
* 拡張子を取得(大文字小文字統一)
*/
public static function getExtension($filename) {
$dotPos = strrpos($filename, '.');
if ($dotPos === false) {
return '';
}
return strtolower(substr($filename, $dotPos + 1));
}
}
// 使用例
$files = [
'photo.JPG',
'document.PDF',
'script.SH',
'image.PNG',
'README'
];
echo "=== 拡張子チェック ===\n";
foreach ($files as $file) {
echo "\n{$file}:\n";
echo " .jpg: " . (FileExtensionChecker::hasExtension($file, '.jpg') ? 'Yes' : 'No') . "\n";
echo " .pdf: " . (FileExtensionChecker::hasExtension($file, '.pdf') ? 'Yes' : 'No') . "\n";
echo " 画像: " . (FileExtensionChecker::isImage($file) ? 'Yes' : 'No') . "\n";
echo " ドキュメント: " . (FileExtensionChecker::isDocument($file) ? 'Yes' : 'No') . "\n";
echo " 拡張子: " . FileExtensionChecker::getExtension($file) . "\n";
}
例2: URLとプロトコルの検証
class UrlValidator {
/**
* 特定のプロトコルで始まるか
*/
public static function hasProtocol($url, $protocol) {
$protocolLength = strlen($protocol);
// :// を追加
if (substr($protocol, -3) !== '://') {
$protocol .= '://';
$protocolLength += 3;
}
// 大文字小文字を区別せず比較
return substr_compare($url, $protocol, 0, $protocolLength, true) === 0;
}
/**
* HTTPSか確認
*/
public static function isHttps($url) {
return self::hasProtocol($url, 'https');
}
/**
* HTTPか確認
*/
public static function isHttp($url) {
return self::hasProtocol($url, 'http');
}
/**
* セキュアなプロトコルか
*/
public static function isSecure($url) {
$secureProtocols = ['https', 'ftps', 'sftp', 'ssh'];
foreach ($secureProtocols as $protocol) {
if (self::hasProtocol($url, $protocol)) {
return true;
}
}
return false;
}
/**
* 特定のドメインで終わるか
*/
public static function endsWith($url, $domain) {
$domainLength = strlen($domain);
// プロトコルを除去
$cleanUrl = preg_replace('#^[a-z]+://#i', '', $url);
// パスを除去
$slashPos = strpos($cleanUrl, '/');
if ($slashPos !== false) {
$cleanUrl = substr($cleanUrl, 0, $slashPos);
}
return substr_compare($cleanUrl, $domain, -$domainLength, $domainLength, true) === 0;
}
}
// 使用例
$urls = [
'https://example.com/path',
'HTTP://test.org',
'ftp://files.example.com',
'https://secure.example.com'
];
echo "=== URLプロトコルチェック ===\n";
foreach ($urls as $url) {
echo "\n{$url}:\n";
echo " HTTPS: " . (UrlValidator::isHttps($url) ? 'Yes' : 'No') . "\n";
echo " HTTP: " . (UrlValidator::isHttp($url) ? 'Yes' : 'No') . "\n";
echo " セキュア: " . (UrlValidator::isSecure($url) ? 'Yes' : 'No') . "\n";
}
echo "\n=== ドメイン終端チェック ===\n";
var_dump(UrlValidator::endsWith('https://example.com', 'example.com')); // true
var_dump(UrlValidator::endsWith('https://test.example.com', 'example.com')); // true
例3: プレフィックス・サフィックスの検証
class StringMatcher {
/**
* プレフィックス(前置詞)が一致するか
*/
public static function startsWith($haystack, $needle, $caseInsensitive = false) {
$needleLength = strlen($needle);
return substr_compare($haystack, $needle, 0, $needleLength, $caseInsensitive) === 0;
}
/**
* サフィックス(後置詞)が一致するか
*/
public static function endsWith($haystack, $needle, $caseInsensitive = false) {
$needleLength = strlen($needle);
return substr_compare($haystack, $needle, -$needleLength, $needleLength, $caseInsensitive) === 0;
}
/**
* 特定の位置に特定の文字列があるか
*/
public static function hasAt($haystack, $needle, $offset, $caseInsensitive = false) {
$needleLength = strlen($needle);
return substr_compare($haystack, $needle, $offset, $needleLength, $caseInsensitive) === 0;
}
/**
* いずれかのプレフィックスで始まるか
*/
public static function startsWithAny($haystack, $needles, $caseInsensitive = false) {
foreach ($needles as $needle) {
if (self::startsWith($haystack, $needle, $caseInsensitive)) {
return true;
}
}
return false;
}
/**
* いずれかのサフィックスで終わるか
*/
public static function endsWithAny($haystack, $needles, $caseInsensitive = false) {
foreach ($needles as $needle) {
if (self::endsWith($haystack, $needle, $caseInsensitive)) {
return true;
}
}
return false;
}
}
// 使用例
echo "=== プレフィックスチェック ===\n";
var_dump(StringMatcher::startsWith("Hello World", "Hello")); // true
var_dump(StringMatcher::startsWith("Hello World", "HELLO", true)); // true
var_dump(StringMatcher::startsWith("Hello World", "World")); // false
echo "\n=== サフィックスチェック ===\n";
var_dump(StringMatcher::endsWith("Hello World", "World")); // true
var_dump(StringMatcher::endsWith("Hello World", "WORLD", true)); // true
var_dump(StringMatcher::endsWith("Hello World", "Hello")); // false
echo "\n=== 複数パターンチェック ===\n";
$text = "test_file.php";
var_dump(StringMatcher::startsWithAny($text, ['test_', 'prod_', 'dev_'])); // true
var_dump(StringMatcher::endsWithAny($text, ['.php', '.js', '.css'])); // true
例4: バージョン番号の比較
class VersionComparator {
/**
* メジャーバージョンが一致するか
*/
public static function sameMajor($version1, $version2) {
$dotPos1 = strpos($version1, '.');
$dotPos2 = strpos($version2, '.');
if ($dotPos1 === false || $dotPos2 === false) {
return $version1 === $version2;
}
$length = min($dotPos1, $dotPos2);
return substr_compare($version1, $version2, 0, $length) === 0;
}
/**
* バージョンプレフィックスが一致するか
*/
public static function hasPrefix($version, $prefix) {
$prefixLength = strlen($prefix);
return substr_compare($version, $prefix, 0, $prefixLength) === 0;
}
/**
* ビルド情報を除いたバージョンを比較
*/
public static function compareWithoutBuild($version1, $version2) {
// ビルド情報(+以降)を除去して比較
$plusPos1 = strpos($version1, '+');
$plusPos2 = strpos($version2, '+');
$v1 = $plusPos1 !== false ? substr($version1, 0, $plusPos1) : $version1;
$v2 = $plusPos2 !== false ? substr($version2, 0, $plusPos2) : $version2;
return strcmp($v1, $v2);
}
/**
* プレリリース版か確認
*/
public static function isPreRelease($version) {
$preReleaseTags = ['-alpha', '-beta', '-rc', '-dev'];
foreach ($preReleaseTags as $tag) {
$tagLength = strlen($tag);
$versionLength = strlen($version);
for ($i = 0; $i <= $versionLength - $tagLength; $i++) {
if (substr_compare($version, $tag, $i, $tagLength, true) === 0) {
return true;
}
}
}
return false;
}
}
// 使用例
echo "=== バージョン比較 ===\n";
var_dump(VersionComparator::sameMajor('1.2.3', '1.5.8')); // true
var_dump(VersionComparator::sameMajor('1.2.3', '2.0.0')); // false
echo "\n=== バージョンプレフィックス ===\n";
var_dump(VersionComparator::hasPrefix('1.2.3', '1.2')); // true
var_dump(VersionComparator::hasPrefix('2.0.0', '1.')); // false
echo "\n=== プレリリース判定 ===\n";
$versions = ['1.0.0', '2.0.0-alpha', '3.0.0-beta.1', '4.0.0-rc.2'];
foreach ($versions as $version) {
$isPreRelease = VersionComparator::isPreRelease($version) ? 'Yes' : 'No';
echo "{$version}: {$isPreRelease}\n";
}
例5: パス操作
class PathMatcher {
/**
* 特定のディレクトリ内か確認
*/
public static function isInDirectory($path, $directory) {
// パス区切り文字を統一
$path = str_replace('\\', '/', $path);
$directory = str_replace('\\', '/', $directory);
// 末尾のスラッシュを追加
if (substr($directory, -1) !== '/') {
$directory .= '/';
}
$dirLength = strlen($directory);
return substr_compare($path, $directory, 0, $dirLength, true) === 0;
}
/**
* ルートディレクトリが一致するか
*/
public static function sameRoot($path1, $path2) {
$path1 = str_replace('\\', '/', $path1);
$path2 = str_replace('\\', '/', $path2);
$slash1 = strpos($path1, '/');
$slash2 = strpos($path2, '/');
if ($slash1 === false || $slash2 === false) {
return false;
}
$length = min($slash1, $slash2);
return substr_compare($path1, $path2, 0, $length, true) === 0;
}
/**
* 隠しファイルか確認
*/
public static function isHidden($filename) {
$basename = basename($filename);
return substr_compare($basename, '.', 0, 1) === 0;
}
/**
* バックアップファイルか確認
*/
public static function isBackup($filename) {
$backupSuffixes = ['.bak', '.backup', '~'];
foreach ($backupSuffixes as $suffix) {
$suffixLength = strlen($suffix);
if (substr_compare($filename, $suffix, -$suffixLength, $suffixLength, true) === 0) {
return true;
}
}
return false;
}
}
// 使用例
echo "=== ディレクトリチェック ===\n";
var_dump(PathMatcher::isInDirectory('/var/www/html/index.php', '/var/www')); // true
var_dump(PathMatcher::isInDirectory('/var/www/html/index.php', '/var/log')); // false
echo "\n=== ルート比較 ===\n";
var_dump(PathMatcher::sameRoot('/var/www/html', '/var/log/app')); // true
var_dump(PathMatcher::sameRoot('/var/www/html', '/usr/local')); // false
echo "\n=== 隠しファイル ===\n";
var_dump(PathMatcher::isHidden('.htaccess')); // true
var_dump(PathMatcher::isHidden('index.php')); // false
echo "\n=== バックアップファイル ===\n";
var_dump(PathMatcher::isBackup('file.bak')); // true
var_dump(PathMatcher::isBackup('file.php~')); // true
var_dump(PathMatcher::isBackup('file.txt')); // false
例6: データ検証
class DataValidator {
/**
* 特定のプレフィックスを持つIDか
*/
public static function hasValidIdPrefix($id, $prefix) {
$prefixLength = strlen($prefix);
return substr_compare($id, $prefix, 0, $prefixLength) === 0;
}
/**
* ユーザーIDが有効か
*/
public static function isValidUserId($id) {
return self::hasValidIdPrefix($id, 'USER-');
}
/**
* 注文IDが有効か
*/
public static function isValidOrderId($id) {
return self::hasValidIdPrefix($id, 'ORD-');
}
/**
* 電話番号が特定の地域コードか
*/
public static function hasAreaCode($phone, $areaCode) {
// 数字のみ抽出
$clean = preg_replace('/[^0-9]/', '', $phone);
$codeLength = strlen($areaCode);
return substr_compare($clean, $areaCode, 0, $codeLength) === 0;
}
/**
* 郵便番号が特定の地域か
*/
public static function hasZipPrefix($zip, $prefix) {
$clean = preg_replace('/[^0-9]/', '', $zip);
$prefixLength = strlen($prefix);
return substr_compare($clean, $prefix, 0, $prefixLength) === 0;
}
}
// 使用例
echo "=== ID検証 ===\n";
var_dump(DataValidator::isValidUserId('USER-12345')); // true
var_dump(DataValidator::isValidUserId('ORD-67890')); // false
var_dump(DataValidator::isValidOrderId('ORD-67890')); // true
echo "\n=== 電話番号検証 ===\n";
var_dump(DataValidator::hasAreaCode('090-1234-5678', '090')); // true
var_dump(DataValidator::hasAreaCode('080-1234-5678', '090')); // false
echo "\n=== 郵便番号検証 ===\n";
var_dump(DataValidator::hasZipPrefix('123-4567', '123')); // true
var_dump(DataValidator::hasZipPrefix('456-7890', '123')); // false
例7: テキスト処理
class TextProcessor {
/**
* 引用符で始まるか
*/
public static function isQuoted($text) {
$quotes = ['"', "'", '`'];
foreach ($quotes as $quote) {
if (substr_compare($text, $quote, 0, 1) === 0 &&
substr_compare($text, $quote, -1, 1) === 0) {
return true;
}
}
return false;
}
/**
* コメント行か確認
*/
public static function isComment($line, $commentSymbol = '//') {
$line = ltrim($line);
$symbolLength = strlen($commentSymbol);
return substr_compare($line, $commentSymbol, 0, $symbolLength) === 0;
}
/**
* HTMLタグか確認
*/
public static function isHtmlTag($text) {
$trimmed = trim($text);
return substr_compare($trimmed, '<', 0, 1) === 0 &&
substr_compare($trimmed, '>', -1, 1) === 0;
}
/**
* マークダウンヘッダーか確認
*/
public static function isMarkdownHeader($line) {
$trimmed = ltrim($line);
$headerLevels = ['# ', '## ', '### ', '#### ', '##### ', '###### '];
foreach ($headerLevels as $header) {
$headerLength = strlen($header);
if (substr_compare($trimmed, $header, 0, $headerLength) === 0) {
return true;
}
}
return false;
}
}
// 使用例
echo "=== 引用符チェック ===\n";
var_dump(TextProcessor::isQuoted('"Hello"')); // true
var_dump(TextProcessor::isQuoted("'World'")); // true
var_dump(TextProcessor::isQuoted('Hello')); // false
echo "\n=== コメント行チェック ===\n";
var_dump(TextProcessor::isComment('// This is a comment')); // true
var_dump(TextProcessor::isComment('/* Comment */', '/*')); // true
var_dump(TextProcessor::isComment('Code here')); // false
echo "\n=== HTMLタグチェック ===\n";
var_dump(TextProcessor::isHtmlTag('<div>')); // true
var_dump(TextProcessor::isHtmlTag('<p>text</p>')); // false(テキスト含む)
echo "\n=== マークダウンヘッダー ===\n";
$lines = ['# Title', '## Subtitle', '### Section', 'Normal text'];
foreach ($lines as $line) {
$isHeader = TextProcessor::isMarkdownHeader($line) ? 'Yes' : 'No';
echo "{$line}: {$isHeader}\n";
}
substr() + strcmp()との比較
$text = "Hello World";
$needle = "Hello";
// substr_compare()を使用
$result1 = substr_compare($text, $needle, 0, strlen($needle));
// substr() + strcmp()を使用
$result2 = strcmp(substr($text, 0, strlen($needle)), $needle);
// 結果は同じ
var_dump($result1 === $result2); // true
// しかしsubstr_compare()の方が効率的
// - 一度の関数呼び出しで済む
// - 内部で最適化されている
// - コードが簡潔
パフォーマンステスト
$text = str_repeat("abcdefghijklmnopqrstuvwxyz", 10000);
$needle = "abcdefg";
// substr_compare()
$start = microtime(true);
for ($i = 0; $i < 100000; $i++) {
substr_compare($text, $needle, 0, 7);
}
$time1 = microtime(true) - $start;
// substr() + strcmp()
$start = microtime(true);
for ($i = 0; $i < 100000; $i++) {
strcmp(substr($text, 0, 7), $needle);
}
$time2 = microtime(true) - $start;
echo "substr_compare(): {$time1}秒\n";
echo "substr() + strcmp(): {$time2}秒\n";
// substr_compare()の方が高速
まとめ
substr_compare()関数の特徴をまとめると:
できること:
- 部分文字列の効率的な比較
- 大文字小文字を区別しない比較
- 負のオフセットに対応
戻り値:
0: 一致< 0: 比較元が小さい> 0: 比較元が大きい
推奨される使用場面:
- ファイル拡張子のチェック
- URLプロトコルの検証
- プレフィックス・サフィックスの確認
- バージョン番号の比較
- パスの検証
- データフォーマットの確認
利点:
substr() + strcmp()より効率的- 一度の関数呼び出しで完結
- 大文字小文字の区別を制御可能
- バイナリセーフ
注意点:
- バイト単位で処理(マルチバイト文字注意)
- オフセットが範囲外の場合は警告
- 長さを省略すると文字列の最後まで比較
関連関数:
strcmp(): 文字列全体を比較strncmp(): 先頭n文字を比較strcasecmp(): 大文字小文字を区別しない比較substr(): 部分文字列を取得
よく使うパターン:
// プレフィックスチェック
substr_compare($str, $prefix, 0, strlen($prefix)) === 0
// サフィックスチェック(大文字小文字無視)
substr_compare($str, $suffix, -strlen($suffix), strlen($suffix), true) === 0
// 特定位置の比較
substr_compare($str, $needle, $pos, strlen($needle)) === 0
substr_compare()は、部分文字列の比較を効率的に行いたい場合に非常に便利です。特にファイル拡張子やURLプロトコルのチェックなど、文字列の先頭や末尾を頻繁に確認する処理で活躍します!
