Strings7 min readExamples run on PHP 8.5.10

substr_count()

Learn how PHP 8.4 substr_count() counts non-overlapping, case-sensitive substring matches, including offset and length behaviour and the errors it can throw.

Signature

substr_count(string $haystack, string $needle, int $offset = 0, ?int $length = null): int

Purpose

substr_count() answers one question: how many times does a given piece of text appear inside a larger string? It returns a plain integer, so it is a good fit for quick checks such as counting line breaks, tallying a keyword in a log, or verifying that a template contains the expected number of placeholders.

The search is case sensitive, and matches are non-overlapping. Once a match is found, the search continues after the end of that match.

Signature

Parameters and defaults

  • $haystack is the string to search in.
  • $needle is the substring to count. It must not be empty.
  • $offset is where counting starts. The default is 0, the start of the string. A negative offset counts from the end of the string.
  • $length is the maximum number of characters, measured from the offset, in which to look. The default is null, which means "search to the end". A negative length stops the search that many characters before the end of the haystack.

All positions are measured in bytes, not in characters of a multibyte encoding. For plain ASCII text the two are the same.

Return value and failure cases

The function returns an int, which is 0 when nothing matches. It does not return false for "not found".

Invalid arguments throw a ValueError:

  • An empty $needle is rejected.
  • An $offset that lies outside the haystack is rejected.
  • A $length that makes the search window extend past the haystack is rejected. The manual's own example uses an offset of 5 and a length of 10 on a 14-byte string.

Example 1: case and overlap

The first example shows the two rules that surprise people most. Uppercase IS does not match lowercase text, and aa occurs twice in aaaa rather than three times, because the matches may not overlap.

Example
<?php
$text = 'This is a test';
echo substr_count($text, 'is'), PHP_EOL;
echo substr_count($text, 'IS'), PHP_EOL;
echo substr_count('aaaa', 'aa'), PHP_EOL;
Output
2
0
2

Example 2: offset and length

With an offset, only the remaining part of the string is searched. With a length, the search window is limited further. Negative values count from the end.

Example
<?php
$text = 'This is a test';
echo substr_count($text, 'is', 3), PHP_EOL;
echo substr_count($text, 'is', 3, 3), PHP_EOL;
echo substr_count($text, 'is', -9), PHP_EOL;
echo substr_count($text, 'is', 0, -6), PHP_EOL;
Output
1
0
1
2

The offset of 3 leaves s is a test, which has one match. Adding a length of 3 shrinks the window to s i, which has none. The offset -9 begins at index 5, leaving is a test, with one match. The last call searches the first eight bytes, This is , which contain two matches.

Example 3: practical counting and errors

Counting newline characters tells you how many line breaks a block of text contains, and counting a keyword gives a quick tally. Invalid arguments can be caught as ValueError.

Example
<?php
$log = "ok\nerror: disk\nok\nerror: cpu\n";
echo substr_count($log, "\n"), PHP_EOL;
echo substr_count($log, 'error'), PHP_EOL;

try {
    echo substr_count($log, '');
} catch (ValueError $e) {
    echo get_class($e), PHP_EOL;
}

try {
    echo substr_count('abc', 'a', 10);
} catch (ValueError $e) {
    echo get_class($e), PHP_EOL;
}
Output
4
2
ValueError
ValueError

Edge cases

  • Overlaps are skipped. Counting gcdgcd in gcdgcdgcd gives 1, because the second possible match would reuse characters from the first.
  • An empty haystack is allowed with a non-empty needle and simply returns 0.
  • A needle longer than the haystack returns 0.
  • A length of 0 is allowed according to the manual's changelog, and the search window is then empty.
  • Multibyte text is handled byte by byte. Counting a single-byte needle inside UTF-8 text is usually safe, but offsets and lengths may fall in the middle of a multibyte character. If you need offsets in characters, use a multibyte-aware approach instead.

Common mistakes

  1. Expecting case-insensitive matching. Normalise both strings first, for example with strtolower() for ASCII text, if you want that behaviour.
  2. Counting overlapping patterns. substr_count('aaa', 'aa') is 1, not 2. A regular expression with a lookahead is the usual tool for overlapping counts.
  3. Passing an empty needle, often from an unchecked variable. Validate the input before calling the function.
  4. Using it to count words. Searching for the also matches inside other and bathe. Use a pattern that respects word boundaries when whole words matter.
  5. Treating the result as a boolean flag and forgetting that 0 is falsy. That is fine for an "is there any match" test, but strpos() or str_contains() states that intent more clearly.
  6. Mixing up offset and length. Length is relative to the offset, not an absolute end position.
  • strpos() finds the position of the first occurrence.
  • substr() returns part of a string, which is useful for inspecting the window being searched.
  • strstr() returns the string from the first occurrence onward.
  • count_chars() reports character usage across a string.

Recap

substr_count() returns how many non-overlapping, case-sensitive occurrences of a non-empty needle appear in a haystack, optionally restricted by an offset and length. A missing needle gives 0, while an empty needle or an out-of-range window throws a ValueError.

Tested examples

Case sensitivity and non-overlapping matches
<?php
$text = 'This is a test';
echo substr_count($text, 'is'), PHP_EOL;
echo substr_count($text, 'IS'), PHP_EOL;
echo substr_count('aaaa', 'aa'), PHP_EOL;
Output
2
0
2
Using offset, length and negative values
<?php
$text = 'This is a test';
echo substr_count($text, 'is', 3), PHP_EOL;
echo substr_count($text, 'is', 3, 3), PHP_EOL;
echo substr_count($text, 'is', -9), PHP_EOL;
echo substr_count($text, 'is', 0, -6), PHP_EOL;
Output
1
0
1
2
Counting lines and keywords, and catching ValueError
<?php
$log = "ok\nerror: disk\nok\nerror: cpu\n";
echo substr_count($log, "\n"), PHP_EOL;
echo substr_count($log, 'error'), PHP_EOL;

try {
    echo substr_count($log, '');
} catch (ValueError $e) {
    echo get_class($e), PHP_EOL;
}

try {
    echo substr_count('abc', 'a', 10);
} catch (ValueError $e) {
    echo get_class($e), PHP_EOL;
}
Output
4
2
ValueError
ValueError