Багато класів Laravel використовують трейт Macroable: Str, Stringable, Arr, Collection, Request, Builder, Http, Number, Uri та інші. Макрос додає метод «ззовні» без успадкування.
// AppServiceProvider::boot()
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
Str::macro('phone', function (string $value): string {
return preg_replace('/\D+/', '', $value);
});
Stringable::macro('phone', function (): Stringable {
return new Stringable(Str::phone($this->value));
});
Str::phone('+38 (067) 123-45-67'); // '380671234567'
str('+38 (067) 123-45-67')->phone(); // Stringable
Важливі деталі:
StrіStringable- різні класи: макрос наStrне з'являється у fluent-ланцюжку. Для обох стилів потрібні два макроси;$thisу замиканні прив'язується до екземпляра (дляStringable- до об'єкта з властивістюvalue), а в статичному виклику - ні;mixinреєструє кілька макросів з класу, кожен публічний чи захищений метод якого повертає замикання:
Str::mixin(new StrMixin);
Ризики:
- Конфлікт з майбутніми методами фреймворку. Макроси викликаються через
__call/__callStatic, тобто лише якщо справжнього методу немає. Якщо в наступній версії Laravel з'явитьсяStr::phone()з іншою поведінкою, ваш макрос мовчки перестане викликатися. Захист - префікси (Str::appPhone) і тести на поведінку макросів; - Невидимість для інструментів: IDE й PHPStan не бачать макросів без додаткових анотацій чи ide-helper; автодоповнення зникає, а аналізатор лається на «невідомий метод»;
- Глобальний стан: макрос зареєстрований для всього процесу. У пакеті він може зіткнутися з макросом іншого пакета чи застосунку з тим самим ім'ям - перемагає той, хто зареєструвався останнім;
- Прихована логіка: бізнес-правила в макросах (
Str::orderNumber()) важче знайти й тестувати, ніж у звичайному класі.
Коли макрос доречний: невелика загальна утиліта, що природно продовжує API класу (форматування телефону, нормалізація пробілів) і використовується в багатьох місцях.
Коли краще звичайний клас чи value object: доменна логіка, щось із залежностями, чи метод, потрібний в одному модулі. PhoneNumber::fromString($raw)->normalized() явніший, тестується окремо й підтримується IDE без підказок.