Going Further

Helpers

The shared utility functions in `app/helpers.php` — what each does, when to use it, and when to add a new one.

The file app/helpers.php is the project's single grab-bag of global helper functions. Composer autoloads it, so every function below is available without an import — you call res(), format_price($amount) or isWinch() from anywhere.

Helpers exist for two reasons: to make a common call shorter (e.g. res()->data(...) instead of constructing a response envelope by hand) and to keep a project convention enforceable in one place (e.g. combine_plate_parts(...) so plate strings are built the same way everywhere). Reach for an existing helper before you write the same logic inline a second time.

The list below is grouped by purpose. Every function in app/helpers.php is covered. If you don't see one here, it's not in the file.

Response envelope

One helper, used in every controller. See RESTful API for the full envelope shape.

HelperSignatureWhat it does
res()res(int $status = 200, ?string $message = null, array $headers = [], array $replaceAttributesMessage = []): ResponseBuilderReturns a fresh ResponseBuilder. Always use this instead of response()->json(...) directly — it is what gives every endpoint the same {status, message, data} envelope.
return res()->data($payload);
return res(422, 'validation.failed')->errors($errors);

Project flavor and features

The codebase ships two flavors — Winch and SaaS. These helpers gate code by flavor without sprinkling config('app.flavor') === ... checks. See Project flavor for the bigger picture and Feature flags for the feature-flag system.

HelperSignatureWhat it does
projectFlavor()projectFlavor(): ProjectFlavorReturns the current ProjectFlavor enum. Delegates to ProjectFlavor::current().
isSaas()isSaas(): boolTrue when the current flavor is SaaS.
isWinch()isWinch(): boolTrue when the current flavor is Winch.
winchBranchesFeature()winchBranchesFeature(): class-string<WinchBranchesFeature>Returns the fully-qualified WinchBranchesFeature class string — used wherever a feature-flag check needs a class reference.
if (isWinch()) {
    return $this->winchPayload();
}

Feature::for($user)->active(winchBranchesFeature());

Notifications

Two helpers for outbound messages — one for app notifications, one for the Telegram error/log channel.

HelperSignatureWhat it does
notify(array $data)notify(array $data): voidResolves the NotificationContract and dispatches the payload through SendNotificationData::fromArray($data). The single entry point for app notifications — never call the notification service directly.
telegram(...)telegram(array|string $text, ?string $template = 'local', array $info = []): voidSends a message through TelegramLogger. Pass a string or an array of lines; $template and $info shape the formatted output. Used for ad-hoc operational alerts.

Numbers and currency

Most projects accumulate a dozen near-duplicates here. We've tried to consolidate; the table below is what we have today. Read the description carefully — several of these helpers look similar but behave differently.

HelperSignatureWhat it does
currency_symbol()currency_symbol(): stringReturns the Saudi Riyal glyph (custom icon-font codepoint U+E800). Use this anywhere you need the currency symbol — never hardcode SAR or ر.س.
format_price(...)format_price(string $amount, ?string $lang = null): stringFormats "{symbol} {amount}" (en) or "{amount} {symbol}" (ar/other). Locale is read from $lang first, then app()->getLocale().
num_format($num)num_format($num)Custom 2-decimal truncation (not rounding) with sign preservation; trailing zero decimals stripped. Returns '0' when the result is falsy.
invoice_num_format($num)invoice_num_format($num)number_format($num, 2, '.', '') with a trailing .00 stripped. Used for invoice line totals where group separators must be absent.
comma_num_format($num)comma_num_format($num)Adds thousand-separator commas. Integers are formatted with no decimals; floats are rounded to 2 decimals with trailing zeros stripped. Returns 0 for an empty string.
removeZeroDigitsFromDecimal($number, $decimal_sep = '.')stringStrips a trailing .00 (or .0) from a numeric string. Building block for the formatters above; rarely called directly.
points_round($number)points_round($number)floor($number) — used wherever loyalty/credit points must round down. Renamed from floor() to make the business intent explicit at the call site.
extractDigits(?string $value = '')?stringStrips every non-digit character. Use for phone numbers, plate digits and any free-text field where only the digits matter.
toArabicNumbers(?string $number = '')?stringReplaces 0–9 with ٠–٩. Use for any number that will render inside Arabic-language text where Eastern Arabic numerals are expected.
current_balance($balance)numericReturns $balance * -1. The legacy way to flip the sign on a stored balance for display.
balance($balance, ?string $model = null)numericReturns the balance with the sign flipped only when $model is a customer-side class (i.e. not a service provider). Use when the same number must read positive to one side and negative to the other.
reasign_balance($balance)numericMaps positive → negative, negative → positive (abs), zero stays zero. Used in legacy balance recalculations; prefer format_signed_balance() for display.
balance_sign_position(?string $position = null, ?string $direction = null): string'prefix'|'suffix'Decides whether the minus sign goes before or after the number. Reads config('balance.sign_position'), falls back to the language-direction map, and finally to config('balance.default_position', 'suffix').
balance_state($value): string'zero'|'negative'|'positive'Bucket a numeric value, applying config('balance.zero_threshold', 0.01) so near-zero floats render as zero.
format_signed_balance($value, ?string $position = null, ?string $direction = null): stringstringFormats a balance for display: thousands-grouped absolute value with the minus sign placed per balance_sign_position(). Non-negative values return the absolute formatted number with no sign.
signed_raw_value($value, ?string $direction = null): stringstringLike format_signed_balance() but without thousand separators — used where the number must remain machine-parseable.
display_balance($value, array $options = []): stringHTMLBuilds the full balance widget: optional currency, optional colour class (from config('balance.colors.*')), optional flip, wrapped in <span class="...">. Returns escaped HTML.
price_to_words(float $price, ?string $lang = null): stringstringSpells the price out in Arabic (default) or English, splitting whole and fractional parts and appending ريال/Riyals and هللة/Halalas. Used on printed invoices.
The balance helpers (balance, reasign_balance, current_balance, format_signed_balance, display_balance) exist for legacy reasons — they encode the sign convention that storage uses the inverse of what humans expect. When you touch a balance, prefer display_balance() for output and balance_state() for branching. Avoid inventing new sign-flipping logic.

Dates and time

HelperSignatureWhat it does
str_to_date($date)mixedMaps the strings 'day', 'week', 'month', 'year' to a Carbon/date value (today, 6 days ago, 29 days ago, current year). Used by dashboard filters. The branches return mixed types (Carbon vs int year) — callers must know which they asked for.
human_timing($time)stringTakes a number of seconds and returns an Arabic phrase like "٣ ساعة". Returns 0 (the integer) when the input is 0. Hard-coded Arabic — no English fallback.
filter_month($users)CollectionBuckets a collection of ['month' => 0..11, ...] rows into a flat, month-ordered list, dropping months with no entries. Used by month-by-month charts.
chart_data_monthly($data)arrayGiven a [monthNumber => [['count' => n], ...]] map (months 1–12), returns a 12-element array of summed counts in calendar order, zero-filling missing months.
extract_count_in_days($data)arrayGroups a collection by created_at day-of-month and returns [day => count].

Strings and text

HelperSignatureWhat it does
lreplace($search, $replace, $subject)stringReplaces the last occurrence of $search with $replace. Used internally by convertRouteVerb().
remove_url_from_text(?string $text = null): ?string?stringStrips http(s)://... URLs and collapses extra whitespace. Returns the input unchanged when null/empty.
replacePlaceholders(?string $body, ?array $replacement, ?bool $isSensitive = false)string|nullReplaces {{ name }} / {{ 1 }} placeholders in a template. Numeric placeholders index into $replacement (1-based); named placeholders look up by key; unmatched placeholders fall back through $replacement in order. When $isSensitive, every replacement is rendered as XXXX — used to redact OTPs in logs.
replaceHtmlTemplate(?string $html = null, ?string $lang = null): ?string?stringParses an HTML fragment, then for each element decides whether to add direction: ltr or direction: rtl; text-align: right; based on existing styles and $lang. Used to normalise WYSIWYG output to the active locale.
formatDomTextDirection($text)stringIn Arabic locale only, wraps runs of Latin/digits/punctuation embedded inside Arabic text with <span dir="ltr">…</span>. Preserves URLs and existing HTML tags. Returns the input unchanged in non-Arabic locales.
ar_en_languages()arrayReturns the subset of config('languages') whose keys are ar or en (preserving keys).
sql_ar_query($text)stringEscapes regex metacharacters in $text, then expands Arabic letters to character classes (e.g. أ(أ|ا|آ|إ)) so a REGEXP query matches across common spelling variants.
get_starred($str)stringReplaces the middle of each whitespace-separated token with ***, keeping the first and last character. Used to redact names in shareable logs.

URLs and routes

HelperSignatureWhat it does
localize_route($name, $parameters = [], $absolute = true)stringBuilds a route URL prefixed with the current locale — route("{locale}.{name}", ...).
is_current_localize_route($name)boolTrue when the current route's locale-stripped name equals $name.
route_unlocalize()stringReturns the current route name with the ar./en. prefix removed when present.
route_with_locale()stringReturns the URL of the opposite locale for the current route, falling back to {otherLocale}.home when no equivalent route exists. Powers the "EN ↔ AR" language toggle.
replace_url_parameters($url, $newParams)stringParses a URL, merges $newParams into its query string, returns the rebuilt URL. Assumes the URL has a query string — it will warn on URLs without one.
get_url_parameter($url, $param)mixed|nullReads a single query-string parameter from a URL.
convertRouteVerb($route)?stringConverts a *.create route name to *.store and *.edit to *.update; returns null for routes starting with ajax.. Used by the admin scaffolding to map form actions to their POST endpoints.
getQueryParams($url)arrayParses a URL and returns its query parameters as an associative array. Returns [] when the URL has no query string.

Files and assets

HelperSignatureWhat it does
images($collection)App\Packages\ImagesConstructs the project's Images wrapper around the given Eloquent collection. The wrapper exposes the upload/URL helpers used in views.
files($collection)App\Packages\FilesSame shape as images() but for arbitrary file attachments.
fetch_s3_file_content($url)stringDownloads a URL via curl with SSL verification disabled and returns the body when HTTP 200. On any other status, returns an error string — not an exception. Callers must check for the "Failed to fetch file..." prefix.
file_to_base64($url)stringCalls fetch_s3_file_content() then base64-encodes the result with a data:image/{ext};base64, prefix. Returns '' on exception (silent failure).
assets_files_version()assets_files_version(): stringReturns the asset cache-busting version string (currently '1.5.78'). Bump this when you ship a frontend asset change that must invalidate browser caches.
excel_text_style($value)stringReturns an inline CSS color:#...; for a number — green when positive, red when negative, dark grey for zero. Used in Excel exports.
invoice_pdf_cols_width($statement, $orderNo)arrayReturns the column-width map for an invoice PDF table. Four variants based on whether the invoice has an order_no column and/or a statement column.
can_export_data($count)boolFalse when $count > 10000. The single place that enforces the export row cap.
get_colors_by_count($count)stringReturns 'success' (>= 20), 'warning' (> 0 && < 20), or 'danger' (otherwise). Dashboard badge colour mapping.
chartColorPalettes()chartColorPalettes(): arrayReturns the named colour palettes (set2, viridis, tableau, modern) available to charts.
resolveChartColors(int $count, ?string $scheme = null): arrayarrayReturns $count colours from the chosen palette (set2 by default, or request('color_scheme')). When $count exceeds the palette size, generates evenly-spaced HSL colours instead.

Users and types

These wrap the SysUserContract so callers don't have to resolve it themselves. See Domain boundaries for why cross-domain user lookups go through this contract.

HelperSignatureWhat it does
class_user_type(?string $type): ?string?stringGiven a short user-type string (e.g. 'driver'), returns the FQCN of the matching model. Returns null when $type is null.
get_user_type(Model|string $model): ?string?stringInverse of class_user_type() — returns the short type string for a model instance or class string.
get_user_label(Model $model)stringReturns the human-readable label for a user model (used in admin tables and audit logs).
is_service_provider(string $modelClass)boolTrue when the given class string is a service-provider type.
get_users_search_route($type, array $excludeIds = [], int|array|null $winchBranch = null)stringBuilds the AJAX search route URL for a user type — route('ajax.{type}s.search', [...]). Used by select2-style pickers.
tmp_get_user_api()User|nullReturns the authenticated user from the api-users guard, falling back to api-branches. The tmp_ prefix signals this is a temporary shim — do not extend its use.
tmp_get_user_type($user)stringReturns 'user' when $user->id falls inside the 40000001–49999999 range, otherwise 'company'. Legacy ID-range mapping — do not extend.
The two tmp_* helpers encode a legacy ID-range and a legacy guard order. They exist so older code keeps working — new code should resolve users explicitly through the appropriate guard or the SysUserContract.

Geography and data

HelperSignatureWhat it does
json_KSA_coordinates()mixedReturns the coordinates column for App\Country::find(1) (Saudi Arabia).
combine_plate_parts(string $right, string $middle, string $left): stringstringConcatenates the three plate parts ({left}{middle}{right}) and translates each Arabic letter to its character-code equivalent via config('plate_characters.letters'). Unknown characters pass through untouched.
get_vat_registration_no($createdAt)stringReturns the VAT registration number that was in effect on $createdAt: '310316756600003' for entries before 2022-05-01, '311290634100003' from that date on. Used on invoices issued for historical records.

Pagination and collections

HelperSignatureWhat it does
paginate($items, $perPage = 15, $page = null, $options = [])LengthAwarePaginatorPaginates an arbitrary array or collection — the manual equivalent of Eloquent's ->paginate(). Use this when you have an in-memory collection that needs paginator output.
set_collection_after_map($model, $mapping)LengthAwarePaginatorRe-attaches a mapped collection to a paginator ($model->setCollection(collect($mapping))). The standard way to transform paginator items without losing the paginator wrapper.
add_rowspan($tableKeys, $rows)arrayWalks $rows and, for each key in $tableKeys, sets {key}_rowspan on the first row of each run of equal values (and 0 on the rest). Lets a Blade table render a single tall cell for consecutive equal values.
compareOrderCounts($previousMonthOrders, $currentMonthOrders)arrayReturns ['difference', 'arrow' (font-awesome class), 'color', 'percentage_change' (string)] comparing two counts. Used by KPI cards on the dashboard.

When to add a new helper

A new function in app/helpers.php is a permanent commitment — it is autoloaded globally and visible to every contributor. The bar is high:

  • The function will have three or more call sites within a reasonable horizon (not "could be useful eventually").
  • It encodes a project convention (formatting, sign handling, route building) rather than a personal preference.
  • No existing class, trait or DTO is a better home — class-level helpers are easier to find and easier to test.
  • The behaviour fits in roughly 30 lines and has no constructor-time dependencies.
  • The name reads as English (format_price, is_service_provider) and follows the snake_case convention already used in the file.

If the function depends on state, talks to the database, or returns a domain object, it probably belongs in an Action or a Service — not here. See Actions and Services and contracts.

  • RESTful API — the response envelope built by res().
  • Project flavor — the system behind isWinch() / isSaas().
  • Feature flags — how winchBranchesFeature() is wired into the flag system.
  • Code style — naming and PHPDoc rules apply to helpers too.