DEV Community

Cover image for Laravel app/Support: Build Your App’s Internal Library
Shakil Alam
Shakil Alam

Posted on Originally published at blog.shakiltech.com

Laravel app/Support: Build Your App’s Internal Library

Series: Every Laravel Project Should Have These Building Blocks

Part: 14 of 35 Level: Intermediate Prerequisites: Folder Structure


What You’ll Learn

  • What app/Support/ is for and why it exists
  • The difference between Support/ and a Helpers/ god file
  • Support/Contracts/ – interfaces your services implement
  • Support/Traits/ – model and console behaviors
  • Support/File.php – the universal file utility
  • What belongs in Support/ vs. a dedicated domain folder

What Is app/Support/?

app/Support/ is the internal library of your application. It contains code that:

  • Is used across multiple domains (not owned by a single domain)
  • Is infrastructure-level, not business-logic-level
  • Has no better home elsewhere in app/

It’s the equivalent of what Illuminate/Support/ is to Laravel itself – the toolkit that everything else uses.

This is different from:

  • app/Services/ – domain-specific business logic
  • app/Models/ – Eloquent persistence
  • app/Actions/ – business executors
  • app/Http/ – HTTP layer

app/Support/ is for things like: “generate a unique filename for any uploaded file”, “make any model track who created it”, “give any Artisan command dual console+log output”.


The Full Structure

app/Support/
├── Contracts/
│   ├── HasRoles.php              # Interface for role-aware models
│   ├── Notifiable.php            # Custom notifiable interface
│   └── Searchable.php            # Interface for searchable models
│
├── Traits/
│   ├── Console/
│   │   └── LogsCommandMessages.php   # Dual output for Artisan commands
│   │
│   ├── Models/
│   │   ├── HasCreator.php           # Auto-sets created_by
│   │   ├── HasCachedAttributes.php  # Per-request in-memory cache
│   │   └── HasNonDevelopmentAccess.php
│   │
│   ├── ModelChangeLogger.php        # Polymorphic audit trail
│   ├── Sluggable.php                # Auto-generated slugs
│   ├── CanBeActivated.php           # is_active toggle + scopes
│   ├── DateScopes.php               # lastDays(), thisMonth(), etc.
│   ├── HasAddress.php               # Address fields and validation
│   ├── HasPermissions.php           # Permission check methods
│   ├── HasRoles.php                 # Role assignment methods
│   ├── InteractsWithMedia.php       # File attachment helpers
│   ├── BarcodeGenerator.php         # Barcode generation mixin
│   └── Encryptable.php              # Transparent attribute encryption
│
├── Enum/
│   └── ChangeLogType.php            # For ModelChangeLogger
│
└── File.php                         # File utility class
Enter fullscreen mode Exit fullscreen mode

Support/Contracts/ – Interfaces for Services

Interfaces belong in Support/Contracts/ when they define behaviors that span multiple models or services:

// app/Support/Contracts/HasRoles.php
interface HasRoles
{
    public function roles(): BelongsToMany;

    public function hasRole(string|array $role): bool;

    public function assignRole(string $role): void;

    public function removeRole(string $role): void;
}
Enter fullscreen mode Exit fullscreen mode
// app/Support/Contracts/Searchable.php
interface Searchable
{
    public function scopeSearch(Builder $query, string $term): Builder;

    public static function searchableFields(): array;
}
Enter fullscreen mode Exit fullscreen mode

Then implement them on any model:

class User extends Authenticatable implements HasRoles, Searchable
{
    use \App\Support\Traits\HasRoles;

    public static function searchableFields(): array
    {
        return ['name', 'email', 'phone'];
    }

    public function scopeSearch(Builder $query, string $term): Builder
    {
        return $query->where(function (Builder $q) use ($term): void {
            foreach (static::searchableFields() as $field) {
                $q->orWhere($field, 'like', "%{$term}%");
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The interface lives in Support/Contracts/, the trait implementation in Support/Traits/, and the model just wires them together.


Support/File.php – The Universal File Utility

Every project that handles uploads needs file utilities: safe naming, MIME detection, size formatting, hashing. Instead of scattering these across models and controllers, centralize them in Support/File.php:

// app/Support/File.php
<?php

declare(strict_types=1);

namespace App\Support;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Str;

final class File
{
    /**
     * Generate a unique filename preserving the extension.
     * "My Photo.JPG" → "a3f92b1c4d8e6f7a.jpg"
     */
    public static function uniqueName(UploadedFile $file): string
    {
        return Str::random(16) . '.' . strtolower($file->getClientOriginalExtension());
    }

    /**
     * Generate a filename from a string + extension.
     * ("Invoice", "pdf") → "invoice-1702394871.pdf"
     */
    public static function nameFromString(string $name, string $extension): string
    {
        return Str::slug($name) . '-' . time() . '.' . $extension;
    }

    /**
     * Hash a file for deduplication (e.g., checking if already uploaded).
     */
    public static function hash(UploadedFile $file): string
    {
        return hash_file('sha256', $file->getRealPath());
    }

    /**
     * Human-readable file size.
     * 1536 → "1.5 KB"
     */
    public static function humanSize(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $i = 0;

        while ($bytes >= 1024 && $i < count($units) - 1) {
            $bytes /= 1024;
            $i++;
        }

        return round($bytes, 1) . ' ' . $units[$i];
    }

    /**
     * Check if a MIME type is an image.
     */
    public static function isImage(string $mimeType): bool
    {
        return str_starts_with($mimeType, 'image/');
    }

    /**
     * Check if a MIME type is a document (PDF, Word, Excel).
     */
    public static function isDocument(string $mimeType): bool
    {
        return in_array($mimeType, [
            'application/pdf',
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel',
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        ], true);
    }

    /**
     * Safe storage path for an entity's files.
     * e.g. ("users", 42, "avatar") → "users/42/avatar"
     */
    public static function storagePath(string $entity, int|string $id, string $collection = 'files'): string
    {
        return "{$entity}/{$id}/{$collection}";
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage anywhere in the app:

// In a model or action
$filename = File::uniqueName($request->file('avatar'));
$path     = File::storagePath('users', $user->id, 'avatars') . '/' . $filename;
$size     = File::humanSize($request->file('document')->getSize());
Enter fullscreen mode Exit fullscreen mode

Support/Traits/Encryptable.php – Transparent Attribute Encryption

For models that store sensitive data (API keys, personal identification numbers), the Encryptable trait encrypts and decrypts transparently:

// app/Support/Traits/Encryptable.php
trait Encryptable
{
    /**
     * Define which attributes to encrypt in the model:
     * protected array $encryptable = ['api_key', 'secret_token'];
     *
     * Signatures match Eloquent's untyped getAttribute($key) / setAttribute($key, $value);
     * adding parameter types here is a fatal "must be compatible" error.
     */
    public function getAttribute($key)
    {
        $value = parent::getAttribute($key);

        if (in_array($key, $this->encryptable ?? [], true) && $value !== null) {
            try {
                return decrypt($value);
            } catch (\Illuminate\Contracts\Encryption\DecryptException $e) {
                // Not encrypted yet (legacy row) or APP_KEY changed: return raw, but say so
                report($e);

                return $value;
            }
        }

        return $value;
    }

    public function setAttribute($key, $value)
    {
        if (in_array($key, $this->encryptable ?? [], true) && $value !== null) {
            $value = encrypt($value);
        }

        return parent::setAttribute($key, $value);
    }
}
Enter fullscreen mode Exit fullscreen mode

On any model:

class ApiIntegration extends Model
{
    use Encryptable;

    protected array $encryptable = ['api_key', 'api_secret', 'webhook_secret'];
}

// Usage — encryption/decryption is automatic:
$integration->api_key = 'sk_live_abc123';  // stored as encrypted ciphertext
echo $integration->api_key;                 // returns 'sk_live_abc123'

// Laravel 8+ alternative with no trait at all:
// protected function casts(): array
// {
//     return ['api_key' => 'encrypted', 'api_secret' => 'encrypted'];
// }
Enter fullscreen mode Exit fullscreen mode

What Belongs in Support/ vs. Elsewhere

If it… Put it in…
Is a reusable trait for multiple models Support/Traits/
Is an interface that multiple services implement Support/Contracts/
Is a utility (file, string, date) with no domain Support/File.php or Support/Helpers/
Has business logic for a specific domain Services/{Domain}/
Is a DTO for a specific Action DTOs/
Is a single-use helper for one model Keep it on the model

The key question: “Could this be useful in another project?” If yes, it belongs in Support/. If it’s specific to your business domain, it belongs in Services/ or Actions/.


What Support/ Is NOT

Support/ is not:

  • A dumping ground for code that doesn’t fit elsewhere (that’s a Helpers.php god file by another name)
  • The place for business logic (that lives in Services/ and Actions/)
  • A replacement for properly namespaced domain code

The test: if you copy app/Support/ into another Laravel project, does it still make sense there? If yes — it belongs in Support/. If it references your specific models or domain concepts – it doesn’t.


FAQs

What goes in app/Support in Laravel?

Cross-cutting utilities that aren’t domain logic: contracts/interfaces, reusable traits, file helpers, custom collections. If it could ship as a tiny library, it’s Support.

Where do Laravel helper functions belong?

Prefer static methods on Support classes over global helpers – they’re namespaced, discoverable, and testable.

Key Takeaways

  • app/Support/ is your app’s internal library: traits, contracts, and utilities used across multiple domains.
  • Support/Contracts/ holds interfaces that models and services implement.
  • Support/File.php centralizes all file-related utilities – naming, hashing, MIME detection, size formatting.
  • Support/Traits/Encryptable.php provides transparent attribute encryption for sensitive model fields.
  • The question for what belongs here: “Could this be useful in another project?” If yes, it’s Support/.

Tips and Gotchas

⚠️ Warning: Don’t put business logic in app/Support/. The support namespace is for reusable infrastructure – traits, utilities, contracts. If a class in app/Support/ is tightly coupled to a specific domain model, it belongs in that domain instead.

💡 Tip: Traits in app/Support/Traits/Models/ should be composable – each one does one thing. A model can use Sluggable, CanBeActivated, and DateScopes together without conflict. Design traits to be combined, not to be comprehensive.

🔥 Expert Note: The app/Support/ pattern is an explicit signal to future developers: “this code is shared infrastructure, handle changes carefully.” It’s the difference between “I changed this helper and three things broke unexpectedly” and “I know exactly what uses this because it’s in Support and I grepped for it.”

Further Reading


← Model Change Logger | Next: Jobs and Queues →

Top comments (0)