Laravel Livewire Tutorial: Add ZIP Download Feature to Your File Explorer

Learn how to add a ‘Download All as ZIP’ feature to your Laravel Livewire file explorer. Compress all files in a folder – grouped by type – into a single ZIP using Laravel Storage and PHP ZipArchive.

When I first built a file explorer using Laravel Livewire, browsing and viewing documents worked great — but there was one feature missing: a simple way to download everything at once.

In this article, I’ll show you how I added a “Download” button that compresses every file in a folder (even grouped by document type) into a single ZIP, ready for instant download. Using just a few lines of code with Laravel’s Storage facade and PHP’s ZipArchive, you’ll learn how to turn your file explorer into a fully functional, production-ready document manager.

Step 1. Preparing Your Storage

All documents are stored inside:

storage/app/public/documents/

and referenced in the database via a file_path column.

Make sure your storage is linked:

php artisan storage:link

Step 2. Update the Livewire Component

Open your ApplicationExplorer Livewire component and add the following import statements at the top:

use Illuminate\Support\Facades\Storage;
use ZipArchive;

Then add the new method:

public function downloadAppDocuments($appId)
{
    $app = \App\Models\Application::with('documents')->findOrFail($appId);

    if ($app->documents->isEmpty()) {
        session()->flash('message', 'No documents to download for this application.');
        return;
    }

    $zipFileName = 'application_' . $app->id . '_documents.zip';
    $zipPath = storage_path('app/temp/' . $zipFileName);

    // Make sure temp directory exists
    if (!file_exists(storage_path('app/temp'))) {
        mkdir(storage_path('app/temp'), 0777, true);
    }

    $zip = new ZipArchive;
    if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
        foreach ($app->documents as $doc) {
            // Access the public disk
            if (Storage::disk('public')->exists($doc->file_path)) {
                // Add subfolder by document type (optional)
                $folder = $doc->type ? $doc->type . '/' : '';
                $zip->addFile(
                    Storage::disk('public')->path($doc->file_path),
                    $folder . basename($doc->file_path)
                );
            }
        }
        $zip->close();
    }

    return response()->download($zipPath)->deleteFileAfterSend(true);
}

How It Works

  1. The method loads the selected application with all its documents.
  2. It creates a temporary ZIP file in storage/app/temp.
  3. It loops through each document, checking if it exists on the public disk.
  4. Each document is added to the ZIP file, optionally organized into subfolders based on document type.
  5. Finally, Laravel’s response()->download() streams the file to the user and deletes it after sending.

Step 3. Add the “Download” Button in the UI

In your Livewire Blade view (application-explorer.blade.php), add this button next to the search bar:

<button wire:click="downloadAppDocuments({{ $selectedApp->id }})" 
        class="btn btn-success">
    Download
</button>

Or, if you prefer to show it beside each folder in the sidebar, update your partial (sidebar-app.blade.php):

<button wire:click.stop="downloadAppDocuments({{ $app->id }})"
        class="btn btn-sm btn-outline-primary me-2"
        title="Download all documents">
    ⬇️
</button>

The .stop modifier ensures this button doesn’t also trigger folder selection.

Step 4. Organizing Documents by Type

If you store files based on type (like invoices, reports, letters), your upload logic should look like this:

$file->store("applications/{$applicationId}/" . strtolower($type), 'public');

Then, in the ZIP creation logic, the $doc->type value creates the corresponding subfolder inside the ZIP.

Example folder structure in ZIP:

application_3_documents.zip
├── invoices/
│   ├── invoice1.pdf
│   └── invoice2.pdf
├── reports/
│   └── report1.pdf
└── letters/
    └── letter1.pdf

Step 5. Handling Missing Files Gracefully

If a document record exists but the file is missing from storage, Storage::disk('public')->exists() safely skips it.

You can also log missing files for auditing:

if (!Storage::disk('public')->exists($doc->file_path)) {
    \Log::warning("Missing file: {$doc->file_path}");
    continue;
}

Step 6. Testing the Download

  1. Select a folder in your explorer.
  2. Click “Download All”.
  3. A .zip file should download automatically.
  4. Extract it — files are grouped by type (if configured).

If you see “file not found” messages, verify:

  • The file_path in the database points to documents/...
  • The disk is correctly set in .env:
FILESYSTEM_DISK=public

Conclusion

And just like that, your Laravel Livewire file explorer now has the power to download everything in one click. No extra packages, no complicated setup — just smart use of Laravel’s Storage facade and PHP’s native ZipArchive.

This simple addition dramatically improves user experience, especially when handling large sets of documents. It also showcases how flexible Livewire can be for real-time, interactive Laravel apps.

From here, you can expand even further: add selective downloads, handle nested folders recursively, or generate temporary signed URLs for secure file sharing. With this foundation in place, your file explorer isn’t just a viewer anymore — it’s a complete, dynamic file management solution.

How I Built a Full File Explorer in Laravel Livewire with Search

Learn how to create a complete file explorer in Laravel using Livewire and Bootstrap – with folders, subfolders, and document search. Step-by-step tutorial with code.

Have you ever wanted to give your Laravel app a professional, desktop-like file explorer – with folders, nested subfolders and document search?

In this tutorial, I’ll walk you through building full file explorer using Laravel Livewire and Bootstrap 5. We’ll create a clean two-pane layout: the left sidebar displays your folder structure (applications and subfolders), while the right pane dynamically lists documents with instant search and filtering.

By the end, you’ll have a fully functional, real-time file manager where users can browse folders, search documents by name and group them by type – all powered by Livewire’s reactivity and Laravel’s filesystem magic.

What We’re Building

We’ll create a two-pane interface like a typical file explorer:

  • Left sidebar → list of applications/folders and nested subfolders.
  • Right pane → list of documents for the selected application.
  • Features:
    • Real-time search
    • Subfolder navigation
    • Document type–based organization

Step 1. Setting Up the Database

We’ll use two tables: applications and documents. applications table holds our folders and subfolders, while documents table holds all documents with name and path with application relation.

Schema::create('applications', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->foreignId('parent_id')->nullable()->constrained('applications')->cascadeOnDelete();
    $table->timestamps();
});

Schema::create('documents', function (Blueprint $table) {
    $table->id();
    $table->foreignId('application_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->string('file_path');  // e.g. "documents/report.pdf"
    $table->string('type')->nullable(); // e.g. "invoice", "report", "letter"
    $table->timestamps();
});

Step 2. Eloquent Relationships

Define relationships in models as follows,

// Application.php
class Application extends Model
{
    protected $fillable = ['name', 'parent_id'];

    public function parent()
    {
        return $this->belongsTo(Application::class, 'parent_id');
    }

    public function children()
    {
        return $this->hasMany(Application::class, 'parent_id');
    }

    public function documents()
    {
        return $this->hasMany(Document::class);
    }
}

// Document.php
class Document extends Model
{
    protected $fillable = ['application_id', 'title', 'file_path', 'type'];

    public function application()
    {
        return $this->belongsTo(Application::class);
    }
}

Step 3. The Livewire Component

Run the following command to create the livewire component:

php artisan make:livewire ApplicationExplorer

It will create 2 files as follows,

  • app/Http/Livewire/ApplicationExplorer.php for file explorer processing.
  • resources/views/livewire/application-explorer.blade.php for file explorer view.

Define all Processes

Open app/Http/Livewire/ApplicationExplorer.php and add the following code:

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Application;
use App\Models\Document;
use Livewire\WithPagination;
use Illuminate\Support\Facades\Storage;
use ZipArchive;

class ApplicationExplorer extends Component
{
    use WithPagination;
    protected $paginationTheme = 'bootstrap';

    public $selectedAppId = null;
    public $search = '';
    public $filterType = '';

    public function updatingSearch()
    {
        $this->resetPage();
    }

    public function selectApp($appId)
    {
        $this->selectedAppId = $appId;
        $this->reset(['search', 'filterType']);
        $this->resetPage();
    }

    public function render()
    {
        $applications = Application::with('children')->whereNull('parent_id')->get();

        $selectedApp = $this->selectedAppId ? Application::find($this->selectedAppId) : null;

        $documents = collect();
        if ($selectedApp) {
            $documents = Document::where('application_id', $this->selectedAppId)
                ->when($this->filterType, fn($q) => $q->where('type', $this->filterType))
                ->when($this->search, fn($q) => $q->where('title', 'like', "%{$this->search}%"))
                ->paginate(10);
        }

        return view('livewire.application-explorer', [
            'applications' => $applications,
            'selectedApp'  => $selectedApp,
            'documents'    => $documents,
        ]);
    }
}

Update the View

To update the view, open resources/views/livewire/application-explorer.blade.php and add the below code,

<div class="container-fluid vh-100">
    <div class="row h-100">
        {{-- Sidebar --}}
        <div class="col-3 border-end bg-light p-0 overflow-auto">
            <div class="p-3 border-bottom"><h5 class="mb-0">Applications</h5></div>
            <ul class="list-group list-group-flush">
                @foreach($applications as $app)
                    @include('livewire.partials.sidebar-app', ['app' => $app, 'level' => 0])
                @endforeach
            </ul>
        </div>

        {{-- Right Pane --}}
        <div class="col-9 p-4">
            @if($selectedApp)
                <div class="d-flex justify-content-between align-items-center mb-3">
                    <h4 class="mb-0">Documents in "{{ $selectedApp->name }}"</h4>
                    <div class="d-flex gap-2 align-items-center" style="width:50%;">
                        <select class="form-select w-auto" wire:model="filterType">
                            <option value="">All Types</option>
                            <option value="invoice">Invoice</option>
                            <option value="report">Report</option>
                            <option value="letter">Letter</option>
                        </select>
                        <input type="text" class="form-control" placeholder="Search..." wire:model.debounce.300ms="search">
                    </div>
                </div>

                @if($documents->count())
                    <div class="table-responsive">
                        <table class="table table-striped table-bordered align-middle">
                            <thead class="table-dark">
                                <tr>
                                    <th>#</th>
                                    <th>Document</th>
                                    <th>Type</th>
                                    <th>Created</th>
                                    <th>Actions</th>
                                </tr>
                            </thead>
                            <tbody>
                                @foreach($documents as $i => $doc)
                                    <tr>
                                        <td>{{ $i + 1 }}</td>
                                        <td>📄 {{ $doc->title }}</td>
                                        <td>{{ $doc->type ?? '-' }}</td>
                                        <td>{{ $doc->created_at->format('d M Y') }}</td>
                                        <td><button class="btn btn-sm btn-primary">View</button></td>
                                    </tr>
                                @endforeach
                            </tbody>
                        </table>
                    </div>

                    {{ $documents->links() }}
                @else
                    <div class="text-muted fst-italic">No documents found.</div>
                @endif
            @else
                <div class="text-muted fst-italic">Select an application to view documents.</div>
            @endif
        </div>
    </div>
</div>

For sidebar, create a new file at resources/views/livewire/partials/sidebar-app.blade.php and add the following code,

<li class="list-group-item p-0 d-flex justify-content-between align-items-center">
    <button wire:click="selectApp({{ $app->id }})"
            class="btn flex-grow-1 text-start px-3 py-2 {{ $selectedAppId === $app->id ? 'btn-secondary' : 'btn-light' }}"
            style="padding-left: {{ 12 + ($level * 15) }}px;">
        📁 <span class="ms-2">{{ $app->name }}</span>
    </button>
</li>

@if($app->children->count())
    <ul class="list-group list-group-flush">
        @foreach($app->children as $child)
            @include('livewire.partials.sidebar-app', ['app' => $child, 'level' => $level + 1])
        @endforeach
    </ul>
@endif

Step 4. File Storage Notes

Documents are uploaded to storage/app/public/documents/.... So, make sure that your public link exists:

php artisan storage:link

Always use:

Storage::disk('public')->exists($path);
Storage::disk('public')->path($path);

This ensures correct behavior since your files are under the public disk.

Step 5. Directory Structure by Document Type

When uploading new documents, if you want to make a directory structure based on document types, you can follow this approach:

$file->store("documents/{$applicationId}/" . strtolower($type), 'public');

This automatically creates folders like:

/storage/app/public/documents/3/invoices/file.pdf

Final Result

  • Left Sidebar: Displays all applications + nested subfolders.
  • Right Pane: Shows searchable, paginated, filterable document table.

Conclusion

And there you have it – a complete file explorer built with Laravel Livewire, featuring a responsive sidebar, real-time search and type-based organization.

This approach not only showcases the power of Livewire for dynamic interfaces but also demonstrates how easily Laravel’s Storage, and Eloquent relationships can come together to create a feature-rich, production-ready document management system.

From here, you can take it even further – add drag-and-drop uploads, access control, file previews, download all files in single zip with complete folder structure or even integrate third-party storage like AWS S3. With this foundation, your Laravel app can handle documents as efficiently as any modern file manager.

From Static to Dynamic: Creating a Powerful Form Builder with Livewire

Learn how to build a dynamic form builder in Laravel Livewire with repeaters, file uploads, and component-based fields. Step-by-step guide with code examples.

Building dynamic forms is a common requirement in modern applications. Instead of hardcoding fields, you might want users to add components (like text inputs, dropdowns, file uploads, or repeaters) that automatically render inside a master form. With Laravel Livewire, you can develop dynamic form builder in a clean and interactive way without writing tons of JavaScript.

Step 1: Create a Livewire Component

Run the artisan command to create your base form builder component with livewire:

php artisan make:livewire FormBuilder

This creates two files:

  • app/Http/Livewire/FormBuilder.php (backend logic)
  • resources/views/livewire/form-builder.blade.php (frontend view)

Step 2: Define Available Components

Inside app/Http/Livewire/FormBuilder.php, let’s define a list of available components. Each component has a name and a JSON configuration of its fields.

public $availableComponents = [
    [
        'name' => 'Text Input',
        'fields' => [
            ['type' => 'text', 'label' => 'Enter Text', 'value' => '']
        ]
    ],
    [
        'name' => 'Email',
        'fields' => [
            ['type' => 'email', 'label' => 'Enter Email', 'value' => '']
        ]
    ],
    [
        'name' => 'File Upload',
        'fields' => [
            ['type' => 'file', 'label' => 'Upload File', 'value' => '']
        ]
    ],
    [
        'name' => 'Repeater',
        'fields' => [
            ['type' => 'repeater', 'label' => 'Repeater Field', 'items' => []]
        ]
    ],
];

We’ll store user-selected components in:

public $formComponents = [];

Step 3: Add Components to the Form

When a user clicks a component, push it to the form array:

public function addComponent($index)
{
    $component = $this->availableComponents[$index];
    $this->formComponents[] = $component;
}

Step 4: Render Dynamic Fields

Render all selected components to form-builder.blade.php:

<div>
    <!-- Component Menu -->
    <h3>Available Components</h3>
    @foreach ($availableComponents as $i => $comp)
        <button wire:click="addComponent({{ $i }})">
            {{ $comp['name'] }}
        </button>
    @endforeach

    <hr>

    <!-- Dynamic Form -->
    <form wire:submit.prevent="save">
        @foreach ($formComponents as $cIndex => $component)
            <div class="component">
                <h4>{{ $component['name'] }}</h4>

                @foreach ($component['fields'] as $fIndex => $field)
                    @if ($field['type'] === 'text')
                        <input type="text"
                               wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.value"
                               placeholder="{{ $field['label'] }}">
                    @elseif ($field['type'] === 'email')
                        <input type="email"
                               wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.value"
                               placeholder="{{ $field['label'] }}">
                    @elseif ($field['type'] === 'file')
                        <input type="file"
                               wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.value">
                    @elseif ($field['type'] === 'repeater')
                        <button type="button"
                                wire:click="addRepeaterItem({{ $cIndex }}, {{ $fIndex }})">
                            ➕ Add Row
                        </button>

                        @foreach ($field['items'] as $rIndex => $item)
                            <input type="text"
                                   wire:model="formComponents.{{ $cIndex }}.fields.{{ $fIndex }}.items.{{ $rIndex }}.value"
                                   placeholder="Repeater Item {{ $rIndex + 1 }}">
                        @endforeach
                    @endif
                @endforeach
            </div>
        @endforeach

        <button type="submit">Save Form</button>
    </form>
</div>

Step 5: Handle Repeaters in Form

If clicked component is repeater, push it to the form array as follows:

public function addRepeaterItem($cIndex, $fIndex)
{
    $this->formComponents[$cIndex]['fields'][$fIndex]['items'][] = ['value' => ''];
}

Step 6: Save the Form Data

Finally, handle form saving:

public function save()
{
    // Just dump the data for now
    dd($this->formComponents);

    // In real apps, store in DB as JSON
}

Conclusion

Dynamic form builders don’t have to be complicated. With Laravel Livewire, you can build fully interactive forms that support repeaters, file uploads, and dynamic components without writing heavy JavaScript.

This approach keeps your codebase clean, your forms flexible, and your users happy. Once you have the basics working, it’s easy to extend the builder with drag-and-drop sorting, conditional fields, or rich text editors.

Whether you’re building an admin panel, a survey tool, or a custom CMS, this Livewire-powered dynamic form structure gives you a strong foundation to adapt to almost any use case.

Boost Your Filament Resources with Excel Export in Minutes

Learn how to add an Export to Excel button in Filament using Laravel Excel. Includes filter-aware exports and reusable QueryExport class.

Exporting data is a common requirement in admin panels. If you’re using Filament with Laravel, you can easily integrate Export to Excel functionality using the Maatwebsite/Laravel-Excel package.

This guide will show you step by step how to:

  • Install Laravel Excel
  • Create a reusable export class
  • Add an “Export to Excel” button in your Filament resource
  • Ensure exports respect filters and tabs
  • Match Filament table columns in Excel

Step 1: Install Laravel Excel

Run the following command in your project root folder to install Laravel Excel package:

composer require maatwebsite/excel

Step 2: Create a Generic Export Class

Instead of creating a new export class for every model, you can build one reusable export class:

<?php

namespace App\Exports;

use Illuminate\Database\Eloquent\Builder;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;

class QueryExport implements FromCollection, WithHeadings, WithMapping
{
    protected Builder $query;
    protected array $columns;

    public function __construct(Builder $query, array $columns = [])
    {
        $this->query = $query;
        $this->columns = $columns;
    }

    public function collection()
    {
        return $this->query->get();
    }

    public function headings(): array
    {
        return array_merge(
            ['ID'],
            collect($this->columns)
                ->map(fn($col) => $col->getLabel() ?? $col->getName())
                ->toArray()
        );
    }

    public function map($row): array
    {
        return array_merge(
            [$row->id],
            collect($this->columns)
                ->map(fn($col) => data_get($row, $col->getName()))
                ->toArray()
        );
    }
}

This class is reusable across all models/resources.

Step 3: Add Export Action in Filament

In your ListBrands (or any other List<Resource>) page, add a header action:

use App\Exports\QueryExport;
use Maatwebsite\Excel\Facades\Excel;
use Filament\Tables\Actions\Action;

Action::make('export')
    ->label('Export to Excel')
    ->icon('heroicon-o-arrow-down-tray')
    ->action(function ($livewire) {
        $query   = $livewire->getFilteredTableQuery(); // respects filters & tabs
        $columns = $livewire->getTable()->getColumns();

        return Excel::download(new QueryExport($query, $columns), 'brands.xlsx');
    }),

Step 4: How It Works

  • The export respects all filters, search, and tab constraints thanks to getFilteredTableQuery().
  • The export file includes the same columns as your Filament table.
  • The ID column is automatically prepended.

Conclusion

With just a few steps, you can add a powerful Excel export feature to any Filament resource. The approach we used:

  • Reusable QueryExport class
  • Filter & tab aware exports
  • Matching Filament table columns
  • ID column always included

This setup gives your admin panel professional-grade export functionality while staying flexible across different resources.

Using .env File in CodeIgniter 3

Learn how to use the .env file in CodeIgniter 3 to securely manage your environment variables and improve your application’s configuration structure.

Using environment variables via a .env file is a common best practice to keep sensitive configuration (like database credentials or any other secret or api keys) out of your codebase. .env file support is not provided in CodeIgniter 3 out of the box, but you can easily integrate it using the vlucas/phpdotenv library.

This guide will show you how to add .env file support in a CodeIgniter 3 application using the vlucas/phpdotenv library with Composer autoload enabled.

Prerequisites

Ensure your CodeIgniter project has Composer enabled by checking the following in application/config/config.php:

$config['composer_autoload'] = TRUE;

Step-by-Step Setup

The following are the steps to implement .env file support.

Step 1. Install vlucas/phpdotenv via Composer

In Codeigniter 3, composer.json is not available at the project root, but inside the application directory. So, to install any composer library, you have to first navigate to the application directory.

cd application/
composer require vlucas/phpdotenv

It will install the core files to add support for .env files.

Step 2. Create the .env File

At the root of your project (same level as index.php), create a file named .env with database configuration variables as a content:

DB_HOST=localhost
DB_USERNAME=root
DB_PASSWORD=secret
DB_NAME=my_database

3. Load the .env in index.php

Open your index.php file and add the following code before the line that bootstraps CodeIgniter:

require_once __DIR__ . '/vendor/autoload.php';

$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();

Add the above code in index.php file before the following line:

require_once BASEPATH.'core/CodeIgniter.php';

For older versions (PHP < 7.1 or Dotenv v2):

$dotenv = new Dotenv\Dotenv(__DIR__);
$dotenv->load();

This will load the .env file variables using the phpdotenv library. Now, all the variables used in .env file can be used in any code of the project.

4. Use Environment Variables in database.php

We defined the database configuration variables inside the .env file. To use these variables, open application/config/database.php and update the code as follows:

$db['default'] = array(
    'hostname' => getenv('DB_HOST'),
    'username' => getenv('DB_USERNAME'),
    'password' => getenv('DB_PASSWORD'),
    'database' => getenv('DB_NAME'),
    'dbdriver' => 'mysqli',
    'db_debug' => (ENVIRONMENT !== 'production'),
    // ... other settings
);

Note: In some cases, getenv function may not work. Use $_ENV as an alternative.

Secutiry Tip

Never commit your .env file to version control. Add it to .gitignore

Conclusion

Now your CodeIgniter 3 app can securely use environment variables just like modern frameworks. This keeps your config clean, safe, and easy to manage across environments.