HardТеория9 min

Хелперы и контракты

Ключевые хелперы (Arr, Str, data_get, rescue, retry, throw_if), контракты vs фасады, когда использовать что в Laravel 11

Laravel предоставляет богатый набор вспомогательных функций (helpers) и систему контрактов (interfaces), определяющих основные сервисы фреймворка. Понимание их различий критически важно для сертификации.

Хелперы массивов (Arr)

Класс Illuminate\Support\Arr предоставляет удобные методы для работы с массивами:

use Illuminate\Support\Arr;

// Arr::get() - Get value using dot notation
$data = ['user' => ['name' => 'John', 'address' => ['city' => 'Moscow']]];

Arr::get($data, 'user.name');              // 'John'
Arr::get($data, 'user.address.city');      // 'Moscow'
Arr::get($data, 'user.phone', 'N/A');     // 'N/A' (default)

// Arr::has() - Check if key exists
Arr::has($data, 'user.name');              // true
Arr::has($data, 'user.phone');             // false
Arr::has($data, ['user.name', 'user.address']); // true (all keys exist)

// Arr::set() - Set value using dot notation
Arr::set($data, 'user.phone', '+7999');
// $data['user']['phone'] = '+7999'

// Arr::dot() - Flatten multidimensional array to dot notation
$flat = Arr::dot($data);
// ['user.name' => 'John', 'user.address.city' => 'Moscow']

// Arr::undot() - Expand dot notation back
$expanded = Arr::undot($flat);
// ['user' => ['name' => 'John', 'address' => ['city' => 'Moscow']]]

// Arr::only() - Return only specified keys
$filtered = Arr::only($data['user'], ['name', 'address']);

// Arr::except() - Return all except specified keys
$filtered = Arr::except($data['user'], ['address']);

// Arr::first() / Arr::last()
$first = Arr::first([1, 2, 3, 4], fn ($value) => $value > 2);
// 3

$last = Arr::last([1, 2, 3, 4], fn ($value) => $value < 3);
// 2

// Arr::flatten() - Flatten multidimensional array
$nested = ['name' => 'John', 'languages' => ['PHP', 'JS']];
$flat = Arr::flatten($nested);
// ['John', 'PHP', 'JS']

// Arr::pluck()
$users = [
    ['name' => 'John', 'email' => '[email protected]'],
    ['name' => 'Jane', 'email' => '[email protected]'],
];
Arr::pluck($users, 'email');
// ['[email protected]', '[email protected]']

Arr::pluck($users, 'email', 'name');
// ['John' => '[email protected]', 'Jane' => '[email protected]']

// Arr::where() - Filter array by callback
$filtered = Arr::where([1, 2, 3, 4, 5], fn ($value) => $value > 3);
// [3 => 4, 4 => 5]

// Arr::shuffle() - Randomly shuffle
$shuffled = Arr::shuffle([1, 2, 3, 4, 5]);

// Arr::random() - Get random element(s)
$random = Arr::random([1, 2, 3, 4, 5]);     // One random element
$randoms = Arr::random([1, 2, 3, 4, 5], 2); // Two random elements

// Arr::wrap() - Wrap value in array
Arr::wrap('hello');    // ['hello']
Arr::wrap(['hello']);  // ['hello'] (already array)
Arr::wrap(null);       // []

// Arr::pull() - Get value and remove from array
$array = ['name' => 'John', 'age' => 30];
$name = Arr::pull($array, 'name');
// $name = 'John', $array = ['age' => 30]

// Arr::exists() - Check if key exists (even if value is null)
Arr::exists(['key' => null], 'key'); // true

// Arr::map()
$mapped = Arr::map([1, 2, 3], fn ($value) => $value * 2);
// [2, 4, 6]

Хелперы строк (Str)

use Illuminate\Support\Str;

// Case transformations
Str::camel('hello_world');     // 'helloWorld'
Str::studly('hello_world');    // 'HelloWorld'
Str::snake('helloWorld');      // 'hello_world'
Str::kebab('helloWorld');      // 'hello-world'
Str::title('hello world');     // 'Hello World'
Str::headline('helloWorld');   // 'Hello World'
Str::lower('HELLO');           // 'hello'
Str::upper('hello');           // 'HELLO'
Str::ucfirst('hello world');   // 'Hello world'

// String checks
Str::contains('Hello World', 'World');           // true
Str::contains('Hello World', ['Foo', 'World']);  // true (any)
Str::containsAll('Hello World', ['Hello', 'World']); // true
Str::startsWith('Hello', 'He');                  // true
Str::endsWith('Hello', 'lo');                    // true
Str::is('foo*', 'foobar');                       // true (pattern matching)

// String manipulation
Str::slug('Laravel Framework');        // 'laravel-framework'
Str::slug('Laravel Framework', '_');   // 'laravel_framework'
Str::limit('Long text here', 10);     // 'Long text...'
Str::limit('Long text here', 10, '>>>'); // 'Long text>>>'
Str::words('This is a long sentence', 3); // 'This is a...'
Str::plural('car');                    // 'cars'
Str::singular('cars');                 // 'car'
Str::substr('Hello World', 6);        // 'World'
Str::before('Hello World', ' ');      // 'Hello'
Str::after('Hello World', ' ');       // 'World'
Str::between('[Hello]', '[', ']');    // 'Hello'
Str::replace('World', 'Laravel', 'Hello World'); // 'Hello Laravel'
Str::replaceFirst('foo', 'bar', 'foo foo foo');  // 'bar foo foo'
Str::replaceLast('foo', 'bar', 'foo foo foo');   // 'foo foo bar'
Str::remove('World', 'Hello World');  // 'Hello '
Str::trim('  Hello  ');              // 'Hello'
Str::squish('Hello   World  ');      // 'Hello World'
Str::padLeft('1', 5, '0');           // '00001'
Str::padRight('1', 5, '0');          // '10000'
Str::mask('1234567890', '*', 4);     // '1234******'
Str::mask('1234567890', '*', 4, 4);  // '1234****90'
Str::repeat('ha', 3);                // 'hahaha'
Str::reverse('Hello');                // 'olleH'

// IDs and random
Str::uuid();                   // UUID v4
Str::orderedUuid();            // Time-ordered UUID
Str::ulid();                   // ULID
Str::random(32);               // Random alphanumeric string
Str::password(16);             // Random password

// Length
Str::length('Hello');          // 5
Str::wordCount('Hello World'); // 2

// Wrap and Finish
Str::wrap('value', '"');       // '"value"'
Str::finish('path/', '/');     // 'path/' (adds / if not present)
Str::start('/path', '/');      // '/path' (adds / if not present)

Fluent Strings (Stringable)

use Illuminate\Support\Str;

// Fluent chainable API
$result = Str::of('Hello World')
    ->lower()
    ->replace('world', 'laravel')
    ->slug()
    ->toString();
// 'hello-laravel'

// Complex example
$cleanedTitle = Str::of($request->title)
    ->trim()
    ->squish()
    ->limit(100)
    ->title()
    ->toString();

// With conditional operations
$filename = Str::of($file->getClientOriginalName())
    ->beforeLast('.')
    ->slug()
    ->append('-', now()->format('Y-m-d'))
    ->append('.', $file->extension())
    ->toString();
// 'my-document-2026-02-22.pdf'

// Fluent string checks
$result = Str::of('[email protected]')
    ->contains('@');  // true

// When/Unless
$greeting = Str::of('Hello')
    ->when($user->isPremium(), fn ($s) => $s->append(' Premium'))
    ->append(' User')
    ->toString();
// 'Hello Premium User' or 'Hello User'

data_get(), data_set(), data_fill()

Эти хелперы работают с вложенными структурами данных через точечную нотацию и поддерживают wildcards:

// data_get() - Get nested value with dot notation
$data = [
    'orders' => [
        ['id' => 1, 'items' => [['name' => 'Laptop', 'price' => 999]]],
        ['id' => 2, 'items' => [['name' => 'Mouse', 'price' => 49]]],
    ],
];

data_get($data, 'orders.0.id');          // 1
data_get($data, 'orders.0.items.0.name'); // 'Laptop'
data_get($data, 'orders.missing', 'N/A'); // 'N/A' (default)

// Wildcard - get from all elements
data_get($data, 'orders.*.id');
// [1, 2]

data_get($data, 'orders.*.items.*.name');
// [['Laptop'], ['Mouse']]

// data_set() - Set nested value
data_set($data, 'orders.0.status', 'shipped');
data_set($data, 'orders.*.status', 'processing'); // Set all

// data_fill() - Set only if not already set
data_fill($data, 'orders.0.status', 'pending');
// Won't overwrite if 'status' already exists

// data_forget() - Remove nested key
data_forget($data, 'orders.0.items');

::alert{type="warning"} Для экзамена: data_get() поддерживает wildcards (*). Arr::get() не поддерживает wildcards. Если нужен wildcard-доступ к вложенным данным, используйте data_get(). ::

rescue()

Выполняет callback и возвращает значение по умолчанию при ошибке:

// Basic usage - returns default on exception
$value = rescue(fn () => $this->riskyOperation(), 'fallback');

// With closure as default
$value = rescue(
    fn () => ExternalApi::getData(),
    fn () => cache()->get('cached_data', []),
);

// Report exception (true by default)
$value = rescue(fn () => $this->parse($input), 'error', report: true);

// Don't report exception
$value = rescue(fn () => json_decode($json, true), [], report: false);

retry()

Повторяет callback указанное число раз при ошибке:

// Retry 3 times with 100ms delay
$result = retry(3, function (int $attempt) {
    return ExternalApi::call();
}, sleepMilliseconds: 100);

// Retry with custom delay per attempt
$result = retry(5, function (int $attempt) {
    return Http::get('https://api.example.com/data')->json();
}, sleepMilliseconds: fn (int $attempt) => $attempt * 200);

// Retry only on specific exceptions
$result = retry(3, function () {
    return DB::transaction(function () {
        // ...
    });
}, sleepMilliseconds: 100, when: fn (\Exception $e) => $e instanceof DeadlockException);

throw_if() и throw_unless()

// Throw if condition is true
throw_if(
    $order->isPaid(),
    new OrderAlreadyPaidException('Order is already paid.'),
);

// Throw with simple string (creates RuntimeException)
throw_if(empty($data), 'Data cannot be empty.');

// Throw with class
throw_if(!$user->canAccess(), AuthorizationException::class, 'Access denied.');

// throw_unless - throw if condition is false
throw_unless(auth()->check(), AuthenticationException::class);

throw_unless(
    $user->hasPermission('edit'),
    AuthorizationException::class,
    'You do not have permission to edit.'
);

Другие важные хелперы

blank() и filled()

// blank() - true for "empty" values
blank('');        // true
blank('   ');     // true
blank(null);      // true
blank(collect()); // true
blank([]);        // true
blank(0);         // false (!)
blank('0');       // false (!)
blank(false);     // false (!)

// filled() - inverse of blank()
filled('hello');  // true
filled(0);        // true
filled(false);    // true
filled('');       // false
filled(null);     // false

::alert{type="warning"} Внимание: blank() отличается от PHP empty(). blank(0) возвращает false, empty(0) возвращает true. blank() считает 0, '0', false как "filled" (заполненные) значения. ::

value() и with()

// value() - resolve value (execute if closure)
$value = value('hello');           // 'hello'
$value = value(fn () => 'hello');  // 'hello'

// Useful in config/defaults
$default = value($config['callback'] ?? 'fallback');

// with() - return value after applying callback
$result = with('Hello', fn ($value) => strtoupper($value));
// 'HELLO'

optional()

// Safely access properties/methods on potentially null objects
$name = optional($user)->name;         // null if $user is null
$email = optional($user)->getEmail();  // null if $user is null

// With callback
$result = optional($user, fn ($u) => $u->getFullName());
// null if $user is null, otherwise calls getFullName()

once()

// Execute callback only once, cache result
$value = once(fn () => expensiveCalculation());
$value = once(fn () => expensiveCalculation()); // Returns cached result

// Useful in classes
final class ReportService
{
    public function getStats(): array
    {
        return once(fn () => [
            'users' => User::count(),
            'orders' => Order::count(),
            'revenue' => Order::sum('total'),
        ]);
    }
}

today(), now()

$today = today();       // Carbon instance for today (midnight)
$now = now();           // Carbon instance for now
$now = now('UTC');      // Now in UTC timezone

transform()

// Transform value if not blank
$result = transform('hello', fn ($value) => strtoupper($value));
// 'HELLO'

$result = transform('', fn ($value) => strtoupper($value));
// '' (returned as-is because blank)

$result = transform(null, fn ($value) => strtoupper($value), 'default');
// 'default'

Контракты (Contracts)

Контракты - это набор интерфейсов, определяющих основные сервисы Laravel. Они находятся в пакете illuminate/contracts.

Зачем нужны контракты

// Using Facade (implementation)
use Illuminate\Support\Facades\Cache;

Cache::get('key');

// Using Contract (interface)
use Illuminate\Contracts\Cache\Repository;

public function __construct(
    private readonly Repository $cache,
) {}

$this->cache->get('key');

Ключевые контракты

// Authentication
Illuminate\Contracts\Auth\Guard
Illuminate\Contracts\Auth\Factory           // Auth manager
Illuminate\Contracts\Auth\Authenticatable   // User model interface

// Cache
Illuminate\Contracts\Cache\Repository       // Cache store
Illuminate\Contracts\Cache\Factory          // Cache manager

// Queue
Illuminate\Contracts\Queue\Queue
Illuminate\Contracts\Queue\ShouldQueue      // Queue marker

// Mail
Illuminate\Contracts\Mail\Mailer
Illuminate\Contracts\Mail\Mailable

// Filesystem
Illuminate\Contracts\Filesystem\Filesystem
Illuminate\Contracts\Filesystem\Cloud

// Events
Illuminate\Contracts\Events\Dispatcher

// Database
Illuminate\Contracts\Database\Eloquent\Builder

// Container
Illuminate\Contracts\Container\Container
Illuminate\Contracts\Foundation\Application

Контракты vs Фасады

Аспект Контракты (Interfaces) Фасады (Facades)
Тип PHP интерфейс Статический прокси
DI Через конструктор Не требуется
Тестирование Легко мокать Встроенные fakes
IDE поддержка Полная из коробки Нужен плагин/пакет
Связанность Слабая (зависимость от интерфейса) Сильная (зависимость от фасада)
// Contract approach (recommended for services/libraries)
declare(strict_types=1);

namespace App\Services;

use Illuminate\Contracts\Cache\Repository as CacheContract;
use Illuminate\Contracts\Events\Dispatcher;

final class ProductService
{
    public function __construct(
        private readonly CacheContract $cache,
        private readonly Dispatcher $events,
    ) {}

    public function getProduct(int $id): Product
    {
        return $this->cache->remember(
            "product:{$id}",
            3600,
            fn () => Product::findOrFail($id)
        );
    }
}

// Facade approach (common in controllers/routes)
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;

Route::get('/product/{id}', function (int $id) {
    return Cache::remember("product:{$id}", 3600, fn () => Product::findOrFail($id));
});

Когда использовать что

Используйте контракты (интерфейсы) когда:

  • Разрабатываете пакет/библиотеку
  • Нужна слабая связанность
  • Планируете замену реализации
  • В сервисных классах и репозиториях

Используйте фасады когда:

  • В контроллерах и маршрутах
  • В одноразовых скриптах
  • В тестах (встроенные fake-методы)
  • Когда DI не приносит пользы
// Auto-resolving contracts from container
declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Contracts\Cache\Repository;

final class DashboardController extends Controller
{
    // Laravel auto-resolves the contract to the configured implementation
    public function index(Repository $cache): View
    {
        $stats = $cache->remember('dashboard:stats', 300, fn () => [
            'users' => User::count(),
            'orders' => Order::count(),
        ]);

        return view('dashboard', compact('stats'));
    }
}

Хелперы путей

// Application paths
app_path('Models/User.php');        // /app/Models/User.php
base_path('vendor');                // /vendor
config_path('app.php');             // /config/app.php
database_path('migrations');        // /database/migrations
public_path('css/app.css');         // /public/css/app.css
resource_path('views');             // /resources/views
storage_path('logs/laravel.log');   // /storage/logs/laravel.log
lang_path('en/messages.php');       // /lang/en/messages.php

Хелперы конфигурации и окружения

// config()
$value = config('app.name');                  // Get value
$value = config('app.name', 'Default');       // With default
config(['app.name' => 'New Name']);           // Set at runtime

// env() - ONLY use in config files!
$debug = env('APP_DEBUG', false);

// app()
$app = app();                        // Application instance
$service = app(UserService::class);  // Resolve from container
$env = app()->environment();         // Current environment
$isProduction = app()->isProduction();

Проверь себя

Чем blank() отличается от PHP empty()?

Что делает хелпер rescue() при возникновении исключения в callback?

В чём ключевое преимущество контрактов (interfaces) перед фасадами в Laravel?