Skip to content

Signals Helper

The signals() helper provides server-side access to reactive signals sent from the frontend. It allows you to read, validate, and manipulate signal data in your Laravel controllers, with built-in support for locked signal security and file storage.

Basic Usage

The signals() helper returns a HyperSignal instance that reads signals from the current request.

php
use function Dancycodes\Hyper\signals;

public function increment()
{
    $count = signals('count'); // Get single signal

    return hyper()->signals(['count' => $count + 1]);
}

Reading Signals

signals()

Get a specific signal with an optional default value.

php
signals(string $key = null, mixed $default = null): mixed

Get single signal:

php
$username = signals('username');

With default value:

php
$role = signals('role', 'user');

Get all signals:

php
$allSignals = signals()->all();

get()

Get a specific signal value (same as calling signals($key, $default)).

php
get(string $key, mixed $default = null): mixed
php
$email = signals()->get('email');
$status = signals()->get('status', 'pending');

Supports dot notation:

php
$city = signals()->get('user.address.city');

all()

Get all signals as an associative array.

php
all(): array
php
$signals = signals()->all();

// Example result:
// [
//     'username' => 'John',
//     'email' => 'john@example.com',
//     'age' => 25
// ]

only()

Get only specific signals.

php
only(array $keys): array
php
$credentials = signals()->only(['email', 'password']);

// Result:
// [
//     'email' => 'john@example.com',
//     'password' => 'secret'
// ]

has()

Check if a signal exists.

php
has(string $key): bool
php
if (signals()->has('userId')) {
    $userId = signals('userId');
}

collect()

Get signals as a Laravel Collection for fluent manipulation.

php
collect(): \Illuminate\Support\Collection
php
$filtered = signals()
    ->collect()
    ->filter(fn($value) => !is_null($value))
    ->map(fn($value) => strtoupper($value));

Validation

validate()

Validate signals using Laravel's validation system.

php
validate(array $rules, array $messages = [], array $attributes = []): array

Parameters:

  • $rules - Laravel validation rules
  • $messages - Custom error messages (optional)
  • $attributes - Custom attribute names (optional)

Returns: Validated data array

Throws: HyperValidationException on validation failure

Basic validation:

php
public function store()
{
    $validated = signals()->validate([
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed'
    ]);

    User::create($validated);

    return hyper()->signals(['success' => true]);
}

With custom messages:

php
$validated = signals()->validate(
    [
        'email' => 'required|email',
        'name' => 'required|min:3'
    ],
    [
        'email.required' => 'Please provide your email address.',
        'email.email' => 'Email format is invalid.',
        'name.min' => 'Name must be at least 3 characters.'
    ]
);

With custom attribute names:

php
$validated = signals()->validate(
    ['user_email' => 'required|email'],
    [],
    ['user_email' => 'email address']
);
// Error message: "The email address field is required."

Automatic error handling:

When validation fails, Hyper automatically:

  1. Creates an errors signal containing Laravel's error bag
  2. Clears previous errors for validated fields
  3. Sends the errors signal to the frontend

Display errors in your Blade template using the data-error attribute:

blade
<input data-bind="email" />
<div data-error="email"></div>
<!-- Automatically shows first error for 'email' field -->

File Storage

The signals() helper provides methods for handling base64-encoded files from file inputs.

store()

Store a base64-encoded file to disk.

php
store(string $signalKey, string $directory = '', string $disk = 'public', ?string $filename = null): string

Parameters:

  • $signalKey - Signal containing the file (from <input type="file" data-bind="avatar">)
  • $directory - Storage directory path
  • $disk - Laravel storage disk (default: 'public')
  • $filename - Custom filename (optional, auto-generated if not provided)

Returns: Storage path

Basic usage:

php
public function uploadAvatar()
{
    signals()->validate([
        'avatar' => 'required|b64image|b64max:2048'
    ]);

    $path = signals()->store('avatar', 'avatars', 'public');

    auth()->user()->update(['avatar' => $path]);

    return hyper()->signals(['avatarPath' => $path]);
}

With custom filename:

php
$userId = auth()->id();
$path = signals()->store('avatar', 'avatars', 'public', "user-{$userId}.jpg");

File signal structure:

When a user selects a file, Datastar creates a signal with this structure:

json
{
    "avatar": {
        "name": "profile.jpg",
        "size": 45678,
        "type": "image/jpeg",
        "lastModified": 1672531200000,
        "data": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
    }
}

storeAsUrl()

Store a file and return its public URL.

php
storeAsUrl(string $signalKey, string $directory = '', string $disk = 'public', ?string $filename = null): string

Returns: Public URL to the stored file

php
public function uploadDocument()
{
    signals()->validate([
        'document' => 'required|b64file|b64mimes:pdf,doc,docx'
    ]);

    $url = signals()->storeAsUrl('document', 'documents', 'public');

    return hyper()->signals([
        'documentUrl' => $url,
        'message' => 'Document uploaded successfully!'
    ]);
}

storeMultiple()

Store multiple files at once.

php
storeMultiple(array $mapping, string $disk = 'public'): array

Parameters:

  • $mapping - Array mapping signal keys to directories: ['signalKey' => 'directory']
  • $disk - Laravel storage disk

Returns: Array of storage paths

php
public function uploadFiles()
{
    signals()->validate([
        'avatar' => 'required|b64image',
        'resume' => 'required|b64file|b64mimes:pdf'
    ]);

    $paths = signals()->storeMultiple([
        'avatar' => 'avatars',
        'resume' => 'resumes'
    ], 'public');

    // Result:
    // [
    //     'avatar' => 'avatars/abc123.jpg',
    //     'resume' => 'resumes/def456.pdf'
    // ]

    return hyper()->signals(['uploadedPaths' => $paths]);
}

Locked Signals

Locked signals (ending with _) are server-protected signals that cannot be tampered with on the frontend. They're encrypted in the session and validated on every request.

Creating Locked Signals

Use the @signals Blade directive with the trailing underscore naming convention:

blade
<div @signals(['userId_' => auth()->id(), 'role_' => auth()->user()->role])>
    <!-- userId_ and role_ are now locked signals -->
</div>

Important: Locked signals must be created using @signals directive, not data-signals attribute:

blade
<!-- ❌ Does NOT provide security -->
<div data-signals="{userId_: 123}">

<!-- ✅ Provides encryption and validation -->
<div @signals(['userId_' => auth()->id()])>

Reading Locked Signals

Read locked signals the same way as regular signals:

php
public function updateProfile()
{
    $userId = signals('userId_'); // Automatically validated

    // If tampered with, HyperSignalTamperedException is thrown
    // before your code runs
}

How Locked Signals Work

  1. First Call (view rendered with @signals):

    • Value encrypted and stored in session
    • Signal sent to frontend
  2. Subsequent Calls (Hyper requests):

    • Frontend sends signal back to server
    • Server retrieves encrypted value from session
    • Compares received value with stored value
    • Throws HyperSignalTamperedException if mismatch detected

Deleting Locked Signals

Use hyper()->forget() to delete locked signals:

php
return hyper()->forget('userId_');
// Clears from frontend AND session storage

Use Cases for Locked Signals

  • User IDs and authentication state
  • Permission levels and roles
  • Pricing information
  • Sensitive configuration
  • Any data that shouldn't change client-side

Example:

blade
<div @signals([
    'productId_' => $product->id,
    'price_' => $product->price,
    'canEdit_' => auth()->user()->can('edit', $product)
])>
    <button data-on:click="@postx('/cart/add')"
            data-show="$canEdit_">
        Add to Cart (${{ $price_ }})
    </button>
</div>
php
public function addToCart()
{
    $productId = signals('productId_');
    $price = signals('price_');
    $canEdit = signals('canEdit_');

    // These values are guaranteed to be authentic
    // User cannot manipulate price or permissions

    if (!$canEdit) {
        abort(403);
    }

    Cart::add($productId, $price);
}

Validation Rules for Files

When validating file signals, use Hyper's base64-specific validation rules:

Available Rules

  • b64file - Validates a base64-encoded file
  • b64image - Validates a base64-encoded image
  • b64max:size - Maximum file size in kilobytes
  • b64dimensions:constraints - Image dimension validation
  • b64mimes:extensions - File extension validation
  • b64mimetypes:types - MIME type validation

See Validation Rules for complete reference.

Example:

php
signals()->validate([
    'avatar' => [
        'required',
        'b64image',
        'b64max:2048',  // Max 2MB
        'b64dimensions:min_width=100,min_height=100,max_width=500',
        'b64mimes:jpg,png'
    ]
]);

Common Patterns

Form Submission with Validation

php
public function store()
{
    $validated = signals()->validate([
        'name' => 'required|string|max:255',
        'email' => 'required|email|unique:contacts',
        'phone' => 'nullable|string'
    ]);

    $contact = Contact::create($validated);

    return hyper()
        ->signals(['errors' => [], 'message' => 'Contact created!'])
        ->navigate('/contacts');
}

Conditional Processing Based on Signals

php
public function update()
{
    $data = signals()->all();

    if (signals()->has('deleteAvatar') && signals('deleteAvatar')) {
        Storage::delete(auth()->user()->avatar);
        $data['avatar'] = null;
    }

    if (signals()->has('newAvatar')) {
        $data['avatar'] = signals()->store('newAvatar', 'avatars');
    }

    auth()->user()->update($data);

    return hyper()->signals(['success' => true]);
}

Filtering and Transforming Signals

php
public function search()
{
    $filters = signals()
        ->collect()
        ->only(['category', 'priceMin', 'priceMax', 'search'])
        ->filter(fn($value) => !is_null($value) && $value !== '')
        ->toArray();

    $products = Product::query()
        ->when($filters['category'] ?? null, fn($q, $cat) => $q->where('category', $cat))
        ->when($filters['priceMin'] ?? null, fn($q, $min) => $q->where('price', '>=', $min))
        ->when($filters['priceMax'] ?? null, fn($q, $max) => $q->where('price', '<=', $max))
        ->when($filters['search'] ?? null, fn($q, $term) => $q->where('name', 'like', "%{$term}%"))
        ->get();

    return hyper()->fragment('products.index', 'product-list', ['products' => $products]);
}

File Upload with Validation

php
public function uploadDocuments()
{
    signals()->validate([
        'resume' => 'required|b64file|b64mimes:pdf,doc,docx|b64max:5120',
        'coverLetter' => 'nullable|b64file|b64mimes:pdf,doc,docx|b64max:5120'
    ]);

    $paths = signals()->storeMultiple([
        'resume' => 'resumes',
        'coverLetter' => 'cover-letters'
    ]);

    Application::create([
        'user_id' => auth()->id(),
        'resume_path' => $paths['resume'],
        'cover_letter_path' => $paths['coverLetter'] ?? null
    ]);

    return hyper()->signals([
        'errors' => [],
        'message' => 'Application submitted successfully!'
    ]);
}

Protected Operations with Locked Signals

php
// In Blade view
@php
    $isAdmin_ = auth()->user()->isAdmin();
@endphp

<div @signals(['userId_' => auth()->id(), 'isAdmin_' => $isAdmin_])>
    <button data-on:click="@deletex('/users/{{ $user->id }}')"
            data-show="$isAdmin_">
        Delete User
    </button>
</div>
php
// In Controller
public function destroy(User $user)
{
    $userId = signals('userId_');
    $isAdmin = signals('isAdmin_');

    // These values are guaranteed authentic
    // User cannot fake admin permissions

    if (!$isAdmin) {
        abort(403, 'Unauthorized');
    }

    if ($userId === $user->id) {
        abort(400, 'Cannot delete yourself');
    }

    $user->delete();

    return hyper()
        ->signals(['message' => 'User deleted'])
        ->navigate('/users');
}

Request Object Integration

The signals() helper is also available as a request macro:

php
// These are equivalent:
signals('count');
request()->signals('count');

signals()->all();
request()->signals()->all();

signals()->validate($rules);
request()->signals()->validate($rules);

See Request Macros for more information.