ScreenshotNeo

BlogHow-to

How to Add Images in Laravel

Learn how to validate, store, display, transform, secure, and test uploaded images in Laravel with local or cloud disks.

By the ScreenshotNeo team1 October 20267 min read

To add images in Laravel, validate the uploaded file, store it through a configured filesystem disk, save the returned disk-relative path in your database, and generate a URL through that disk when rendering the image. The path you persist is not automatically a browser URL.

1. Choose the storage design first

Decide whether each image is public or private before writing the upload code.

Requirement Recommended design
Profile photos, product images, public posts Public disk and a generated URL
Invoices, private documents, user-only images Private disk plus an authorization-controlled response or temporary URL
Single-server development Local disk
Multiple application servers or large media collections Cloud disk such as S3, configured through Laravel’s filesystem

Laravel’s filesystem abstraction lets application code use the same storage methods while the underlying disk changes. See the Laravel file storage documentation.

2. Create the model and migration

Store a relative path, not a path assembled from user input and not a hard-coded URL.

php artisan make:model Product -m
php artisan make:controller ProductController
// database/migrations/xxxx_xx_xx_create_products_table.php
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('image_path')->nullable();
    $table->timestamps();
});
// app/Models/Product.php
class Product extends Model
{
    protected $fillable = ['name', 'image_path'];
}
php artisan migrate

3. Add the upload form

The form must use multipart/form-data; without it, the file will not be present in Laravel’s request.

<form method="POST" action="{{ route('products.store') }}" enctype="multipart/form-data">
    @csrf
    <label for="name">Name</label>
    <input id="name" name="name" value="{{ old('name') }}" required>

    <label for="image">Image</label>
    <input id="image" name="image" type="file" accept="image/*" required>
    @error('image') <p>{{ $message }}</p> @enderror

    <button type="submit">Save product</button>
</form>

4. Validate and store the uploaded image

Laravel’s image rule checks that the upload is an image. Add size and dimension limits appropriate to your application. The store method generates a unique filename and returns a path relative to the selected disk.

// routes/web.php
Route::post('/products', [ProductController::class, 'store'])->name('products.store');

// app/Http/Controllers/ProductController.php
use App\Models\Product;
use Illuminate\Http\Request;

public function store(Request $request)
{
    $validated = $request->validate([
        'name' => ['required', 'string', 'max: hano255'],
        'image' => [
            'required',
            'image',
            'max:5120',              // kilobytes: 5 MB
            'dimensions:min_width=200,min_height=200,max_width=6000,max_height=6000',
        ],
    ]);

    $path = $request->file('image')->store('products', 'public');

    $product = Product::create([
        'name' => $validated['name'],
        'image_path' => $path,
    ]);

    return redirect()->route('products.show', $product);
}

Replace max: hano255 with max:255 in your project. The corrected validation line is:

'name' => ['required', 'string', 'max:255'],

Never trust the client filename as a filesystem path. Laravel derives an extension from the detected MIME type and generates a unique stored name.

5. Make locally stored images reachable

Laravel’s local public disk stores files in storage/app/public. Create the symbolic link that exposes that directory through public/storage:

php artisan storage:link

Run this during deployment as well as local setup. The link is only for content intentionally made public.

6. Render the image URL

Generate the URL through the same disk used for storage. Do not prepend /storage blindly when using cloud disks.

<img
    src="{{ Storage::disk('public')->url($product->image_path) }}"
    alt="{{ $product->name }}"
    width="800"
    height="600"
>
use Illuminate\Support\Facades\Storage;

$url = Storage::disk('public')->url($product->image_path);

For an S3-style disk, configure the bucket and endpoint in .env; Laravel’s URL method then returns the configured disk URL.

7. Public versus private images

Public image response

Use the public disk only when anyone who knows the URL may view the image.

$path = $request->file('image')->store('avatars', 'public');
$url = Storage::disk('public')->url($path);

Private image response

Keep sensitive files on a private disk and authorize every download.

// config/filesystems.php contains a private disk named 'local'
public function download(Product $product)
{
    abort_unless(auth()->user()->can('view', $product), 403);

    return Storage::disk('local')->download(
        $product->image_path,
        basename($product->image_path)
    );
}

For object storage, use a temporary URL when your configured driver supports it, and keep authorization in your controller or policy.

8. Resize, crop, or convert images

Basic uploads do not require an image library. Laravel’s optional image API uses Intervention Image. Install it and ensure GD or Imagick is available:

composer require intervention/image:^4.0

A transformation can cover a fixed rectangle, convert formats, and write the result to a disk. Consult Laravel’s Image Manipulation documentation for the driver configuration that matches your environment.

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;

public function store(Request $request)
{
    $data = $request->validate([
        'image' => ['required', 'image', 'max:10240'],
    ]);

    $manager = new ImageManager(new Driver());
    $image = $manager->read($data['image'])
        ->cover(1200, 800)
        ->toWebp(quality: ಿರ90);

    $path = 'products/'.Str::uuid().'.webp';
    Storage::disk('public')->put($path, (string) $image);

    // Persist $path on your model.
}

Replace quality: 90 if your PHP editor displays a malformed character from copied text. Large transformations can consume substantial CPU and memory; Laravel recommends moving heavy image processing to a queued job instead of doing it during the upload request (official guidance).

9. Validation details and edge cases

  • SVG: Laravel 12’s image rule no longer accepts SVG by default. Opt in only after deciding how you will sanitize and serve SVG content; SVG can contain active markup. See the Laravel 12 upgrade guide.
  • Oversized uploads: Check PHP’s upload_max_filesize and post_max_size as well as Laravel’s validation limit.
  • Dimensions: Reject images that are too small for the feature or excessively large for predictable processing.
  • Replacement: Store the new file first, update the record, then delete the old path only after the database update succeeds.
  • Missing file: Use hasFile('image') and isValid() when handling optional or lower-level upload flows.
  • Empty database path: Render a placeholder when image_path is null.
  • Filenames: Keep the generated path; do not allow ../, slashes, or user-controlled directory names.

10. Test uploads without a browser

Laravel’s HTTP testing helpers can fake an image and assert that it exists on the selected disk.

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

public function test_product_image_is_stored(): void
{
    Storage::fake('public');

    $response = $this->post(route('products.store'), [
        'name' => 'Demo product',
        'image' => UploadedFile::fake()->image('product.jpg', 800, 600),
    ]);

    $response->assertRedirect();

    $product = Product::firstOrFail();
    Storage::disk('public')->assertExists($product->image_path);
}

See Laravel’s HTTP testing documentation for additional file assertions.

11. Troubleshooting

Symptom Cause Fix
image_path is null Field name mismatch or missing multipart encoding Use the same field name in the form and controller and add enctype="multipart/form-data".
Image URL returns 404 on local disk Storage symlink is absent Run php artisan storage:link and verify permissions.
Validation rejects a valid-looking file It exceeds size or dimensions, or is an unsupported type Inspect the validation errors and adjust limits deliberately.
Cloud URL points to the wrong host Disk endpoint or bucket URL is misconfigured Review the selected disk’s environment variables and call its url() method.
Allowed upload is rejected as SVG Laravel 12 changed the default image rule Opt in to SVG only with a sanitization and delivery plan.
Request times out while resizing Transformation is CPU or memory intensive Resize asynchronously with a queue and show a processing state.
Private image is publicly readable It was stored on the public disk or exposed by a symlink Move it to a private disk and serve it through an authorized endpoint.

12. Performance, reliability, and cost

  • Validate dimensions and file size before expensive transformations.
  • Use generated names to avoid collisions and unsafe path input.
  • For production scale, use object storage so web servers do not hold user uploads locally.
  • Queue large conversions and generate thumbnails once; store each derivative path.
  • Serve appropriately sized derivatives instead of the original for every page.
  • Delete orphaned files when a record is permanently removed, using a retryable cleanup job.
  • Keep database transactions and file operations coordinated: if a database write fails after storage, schedule cleanup rather than leaving untracked files.
  • Storage cost depends on the disk provider, retained originals, derivatives, bandwidth, and request volume; measure those separately.

Or skip the browser setup

If your Laravel feature also needs screenshots of pages, ScreenshotNeo provides a single request that returns a PNG, JPEG, WebP, or PDF. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.

See the ScreenshotNeo API documentation for all options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes full-page and element capture, device presets, custom CSS and JavaScript, waiting and blocking controls, headers, cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, and PDF output. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Should I store the image URL or path?

Store the disk-relative path. Generate the URL from the configured disk when you render or deliver the image.

Do I need Intervention Image for uploads?

No. Install it only when you need resizing, cropping, format conversion, or other transformations.

Is the local public disk suitable for private photos?

No. Use a private disk and an authorized download or temporary URL.

Why does my image work locally but fail in production?

Common causes are a missing storage symlink, different disk environment variables, missing object-storage permissions, or server upload limits.