Helper class в PHP: эффективные приёмы для организации утилит
Вспомогательный класс (helper class) в PHP предоставляет набор методов, объединённых общей тематикой, для выполнения типовых операций. Такие классы обычно не хранят состояние и служат для инкапсуляции повторяющейся логики. Основная цель - повышение читаемости и переиспользования кода.
Основные подходы к созданию вспомогательных классов
Статический класс с методами
Как организовать универсальные утилиты без необходимости создавать экземпляр класса?
Наиболее распространённый способ - объявить класс со статическими методами. Пример простого хелпера для работы со строками:
class StringHelper {
public static function truncate(string $text, int $limit = 100): string {
if (mb_strlen($text) <= $limit) {
return $text;
}
return mb_substr($text, 0, $limit) . '...';
}
public static function stripTags(string $text): string {
return strip_tags($text);
}
public static function sanitize(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
}Php helper class (вспомогательный класс в php)
Использование:
echo StringHelper::sanitize('<b>Привет</b>'); // <b>Привет</b>
Типичные проблемы
- Статические методы усложняют тестирование: их сложно подменить или замокать.
- При использовании глобальных зависимостей (например,
Config::get()) класс становится жёстко связанным. - Невозможность наследования поведения без переопределения.
Решение: избегать статических зависимостей от внешних ресурсов; передавать нужные сервисы через параметры методов или использовать контейнер внедрения зависимостей.
Трейты для добавления методов в классы
Как разделить общую функциональность между разными классами без наследования?
Трейт позволяет включить методы в любой класс. Пример:
trait ArrayHelper {
public function pluck(array $array, string $key): array {
return array_map(function($item) use ($key) {
return $item[$key] ?? null;
}, $array);
}
public function arrayToCsv(array $array, string $delimiter = ','): string {
$output = fopen('php://temp', 'r+');
foreach ($array as $row) {
fputcsv($output, $row, $delimiter);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
}
class UsersRepository {
use ArrayHelper;
public function getUsersCsv(): string {
$users = [['id' => 1, 'name' => 'Anna'], ['id' => 2, 'name' => 'Boris']];
return $this->arrayToCsv($users);
}
}
Типичные проблемы
- Конфликт имён методов при использовании нескольких трейтов.
- Трейт не может иметь собственные зависимости (они наследуются из класса).
- Сложнее отследить происхождение метода при отладке.
Решение: использовать трейты только для небольших наборов методов, не зависящих от внешнего состояния. Для сложной логики предпочтительны отдельные классы.
Инстанциируемый класс с внедрением зависимостей
Как сделать хелперы тестируемыми и настраиваемыми?
Создание объекта-хелпера с передачей зависимостей через конструктор. Пример:
class DateHelper {
private string $locale;
private \DateTimeZone $timezone;
public function __construct(string $locale = 'ru_RU', string $timezone = 'Europe/Moscow') {
$this->locale = $locale;
$this->timezone = new \DateTimeZone($timezone);
setlocale(LC_TIME, $this->locale);
}
public function formatDate(\DateTimeInterface $date, string $format = '%d %B %Y'): string {
return strftime($format, $date->getTimestamp());
}
public function daysUntil(\DateTimeInterface $target): int {
$now = new \DateTimeImmutable('now', $this->timezone);
return (int) $now->diff($target)->days;
}
}
// Использование
$dateHelper = new DateHelper('en_US', 'UTC');
echo $dateHelper->formatDate(new \DateTime('2025-01-01'));
Типичные проблемы
- Необходимость явного создания экземпляра.
- Излишняя сложность для простых утилит.
- Рост числа зависимостей при обилии хелперов.
Решение: регистрировать такие классы в контейнере внедрения зависимостей (например,
$container->set(DateHelper::class)) и получать их через автоматическое разрешение.
Функции-хелперы (не классы)
Когда написание отдельного класса избыточно?
Если функционал прост и не требует поддержки состояния, можно обойтись обычной функцией:
function generateRandomString(int $length = 16): string {
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return substr(str_shuffle(str_repeat($chars, $length)), 0, $length);
}
echo generateRandomString(8); // 'aB3kL9x2'
Типичные проблемы
- Отсутствие автозагрузки (автозагрузка классов, как правило, настроена, а функции нужно подключать явно).
- Конфликты имён в глобальном пространстве.
- Невозможность использования в наследовании или композиции.
Решение: использовать функции только для очень простых операций, размещая их в отдельном файле с префиксом пространства имён для избежания конфликтов.
Фасады (прокси-классы)
Как предоставить удобный статический интерфейс к нестатическому сервису?
Фасад - это класс, который делегирует вызовы статических методов реальному объекту. Пример:
class FileFacade {
protected static function getInstance(): FileService {
return new FileService(); // в реальном приложении – из контейнера
}
public static function exist(string $path): bool {
return static::getInstance()->exist($path);
}
public static function read(string $path): string {
return static::getInstance()->read($path);
}
}
echo FileFacade::read('/etc/hosts');
Типичные проблемы
- Скрытая зависимость от реального класса (сложно подменить).
- Усложнение архитектуры ради синтаксического сахара.
- Невозможность правильно протестировать без дополнительных инструментов.
Решение: использовать фасады только если они являются частью устойчивого фреймворка (например, Laravel). В собственных проектах лучше отдавать предпочтение внедрению зависимостей.
Расширенные примеры вспомогательных классов
Класс-хелпер для валидации данных
class ValidatorHelper {
public static function isEmail(string $email): bool {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
public static function isPhone(string $phone, string $country = 'RU'): bool {
$pattern = ($country === 'RU') ? '/^7[0-9]{10}$/' : '/^[0-9]{7,15}$/';
return preg_match($pattern, preg_replace('/[\s\-()]/', '', $phone)) === 1;
}
public static function isUrl(string $url): bool {
return filter_var($url, FILTER_VALIDATE_URL) !== false;
}
public static function passwordStrength(string $password): int {
$score = 0;
if (strlen($password) >= 8) $score++;
if (preg_match('/[A-Z]/', $password)) $score++;
if (preg_match('/[a-z]/', $password)) $score++;
if (preg_match('/[0-9]/', $password)) $score++;
if (preg_match('/[^a-zA-Z0-9]/', $password)) $score++;
return $score;
}
}
// Пример использования и вывод
echo 'Email test: ' . (ValidatorHelper::isEmail('user@example.com') ? 'ok' : 'fail') . "\n";
echo 'Phone test: ' . (ValidatorHelper::isPhone('+7 912 345-67-89') ? 'ok' : 'fail') . "\n";
echo 'Password strength: ' . ValidatorHelper::passwordStrength('Pass123#!');
Email test: ok Phone test: ok Password strength: 5
Хелпер для работы с массивами
class ArrayHelper {
public static function sortByKey(array &$array, string $key, int $order = SORT_ASC): void {
usort($array, function($a, $b) use ($key, $order) {
$cmp = strcmp($a[$key] ?? '', $b[$key] ?? '');
return ($order === SORT_ASC) ? $cmp : -$cmp;
});
}
public static function groupBy(array $array, callable $callback): array {
$result = [];
foreach ($array as $item) {
$groupKey = $callback($item);
$result[$groupKey][] = $item;
}
return $result;
}
public static function flatten(array $array, int $depth = INF): array {
$result = [];
array_walk_recursive($array, function($value) use (&$result) {
$result[] = $value;
});
return $result;
}
}
$users = [['name' => 'Anna', 'age' => 30], ['name' => 'Boris', 'age' => 25], ['name' => 'Anna', 'age' => 28]];
ArrayHelper::sortByKey($users, 'age', SORT_DESC);
print_r($users);
$grouped = ArrayHelper::groupBy($users, function($u) { return $u['name']; });
print_r($grouped);
Array
(
[0] => Array ( [name] => Anna [age] => 30 )
[1] => Array ( [name] => Anna [age] => 28 )
[2] => Array ( [name] => Boris [age] => 25 )
)
Array
(
[Anna] => Array (
[0] => Array ( [name] => Anna [age] => 30 )
[1] => Array ( [name] => Anna [age] => 28 )
)
[Boris] => Array (
[0] => Array ( [name] => Boris [age] => 25 )
)
)
Хелпер для форматирования чисел и дат
class FormatHelper {
public static function money(float $amount, string $currency = 'RUB', string $locale = 'ru_RU'): string {
$formatter = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
return $formatter->formatCurrency($amount, $currency);
}
public static function filesize(int $bytes, int $precision = 2): string {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
public static function dateDiffForHumans(\DateTimeInterface $date, \DateTimeInterface $now = null): string {
$now = $now ?? new \DateTimeImmutable();
$interval = $now->diff($date);
if ($interval->y > 0) return $interval->y . ' год(а) назад';
if ($interval->m > 0) return $interval->m . ' месяц(ев) назад';
if ($interval->d > 0) return $interval->d . ' дн(я) назад';
if ($interval->h > 0) return $interval->h . ' час(ов) назад';
if ($interval->i > 0) return $interval->i . ' минут(ы) назад';
return 'только что';
}
}
echo FormatHelper::money(1234.56) . "\n";
echo FormatHelper::filesize(1234567) . "\n";
echo FormatHelper::dateDiffForHumans(new \DateTime('-2 days'));
1 234,56 ₽ 1.18 MB 2 дн(я) назад
Комбинирование трейтов с классами-хелперами
trait JsonHelper {
public function toJson($data, int $options = 0): string {
return json_encode($data, $options | JSON_UNESCAPED_UNICODE);
}
public function fromJson(string $json, bool $assoc = true): mixed {
return json_decode($json, $assoc, 512, JSON_THROW_ON_ERROR);
}
}
class ApiResponse {
use JsonHelper;
public function success(array $data): string {
return $this->toJson(['status' => 'ok', 'data' => $data]);
}
public function error(string $message): string {
return $this->toJson(['status' => 'error', 'message' => $message]);
}
}
$response = new ApiResponse();
echo $response->success(['user' => 'Anna']);
{"status":"ok","data":{"user":"Anna"}}
Хелпер с кешированием (использование локального состояния)
class CacheHelper {
private static array $cache = [];
public static function remember(string $key, callable $callback, int $ttl = 60): mixed {
if (isset(self::$cache[$key]) && (time() - self::$cache[$key]['time']) < $ttl) {
return self::$cache[$key]['value'];
}
$value = $callback();
self::$cache[$key] = ['value' => $value, 'time' => time()];
return $value;
}
public static function forget(string $key): void {
unset(self::$cache[$key]);
}
}
$result = CacheHelper::remember('expensive_calc', function() {
sleep(2); // имитация тяжёлого вычисления
return 42;
});
echo $result; // 42 (первый вызов – ожидание 2 сек)
$result = CacheHelper::remember('expensive_calc', function() {
return 100; // не выполнится
});
echo $result; // 42 (из кеша)
42 42
Хелпер для HTTP-запросов (обёртка над cURL)
class HttpClient {
public static function get(string $url, array $headers = []): string {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
throw new \RuntimeException('cURL error: ' . curl_error($ch));
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
throw new \RuntimeException("HTTP error $httpCode");
}
return $response;
}
public static function post(string $url, array $data, array $headers = []): string {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
throw new \RuntimeException('cURL error: ' . curl_error($ch));
}
curl_close($ch);
return $response;
}
}
// Пример (требуется рабочий endpoint)
// echo HttpClient::get('https://api.example.com/status');
(вывод зависит от реального ответа)