This has been already answered long time ago but can greatly benefit for preg_replace and a much simplified pattern:
$result = preg_replace('/\s*($|\n)/', '\1', $subject);
Pattern: Remove all white-space before a new-line -or- at the end of the string.
Longest match wins:
As the white-space \s has a greedy quantifier * and contains \n consecutive empty lines are matched.
As \s contains \r as well, \r\n new-line sequences are supported, however single \r (without \n) are not.
And when $ matches the end of the buffer the backreference \1 is empty allowing to handle trailing whitespace at the very end, too.
If leading (empty) lines need to be removed as well, they have to match while not capturing, too (this was not directly asked for but could be appropriate):
$result = preg_replace('/^(?:\s*\n)+|\s*($|\n)/', '\1', $subject);
# '----------'
Pattern: Also remove all leading white-space (first line(s) are empty).
And if the new-line at the end of the buffer should be normalized differently (always a newline at the end instead of never), it needs to be added: . "\n".
This variant is portable to \r\n, \r and \n new-line sequences ((?>\r\n|\r|\n)) or \R:
$result = preg_replace('/^(?> |\t|\r\n|\r|\n)+|(?> |\t|\r\n|\r|\n)*($|(?>\r\n|\r|\n))/', '\1', $subject);
# or:
$result = preg_replace('/^(?:\s*\R)+|\s*($|\R)/', '\1', $subject);
Pattern: Support all new-line sequences.
This is with the downside that the new-lines can not be normalized (e.g. any of the three to \n).
Therefore, it can make sense to normalize new-lines before removing:
$result = preg_replace(['/(?>\r\n|\n|\r)/', '/\s*($|\n)/'], ["\n", '\1'], $subject);
# or:
$result = preg_replace(['/\R/u', '/\s*($|\n)/'], ["\n", '\1'], $subject);
It ships with the opportunity to do some normalization apart from the line handling.
For example removal of the trailing white-space and fixing the missing new-line at the end of file.
Then doing more advanced line normalization, for example zero empty lines at the beginning and end; otherwise not more than two consecutive empty lines:
$result = preg_replace(
['/[ \t]*($|\R)/u', '/^\n*|(\n)\n*$|(\n{3})\n+/'],
["\n" , '\1\2' ],
$subject
);
The secondary pattern benefits from the first patterns replacements already.
The power with preg_replace relies here in choosing the backreference(s) to replace with wisely.
Also using multiple patterns can greatly simplify things and keep the process maintainable.