sukohi/surpass
Surpass is a lightweight Laravel package by sukohi that adds practical, reusable functionality to your app with minimal setup. Install it via Composer, configure it to fit your project, and use its simple API to speed up everyday development.
1. Verify compatibility before anything else. Surpass is archived, so its composer.json constraint is the first thing to read. Run:
composer show sukohi/surpass --all
and check the require block for laravel/framework / illuminate/*. If your app is Laravel 9+, expect to either pin an old branch, fork the package, or port the ~200 lines of logic into your own controller — the package is small enough that forking is usually cheaper than fighting constraints.
2. Install and publish.
composer require sukohi/surpass
php artisan vendor:publish --provider="Sukohi\Surpass\SurpassServiceProvider"
If the package predates Laravel 5.5 auto-discovery you'll need to add the provider and any facade alias to config/app.php manually. Publishing typically drops a config file (config/surpass.php) plus Blade views and JS/CSS assets under public/vendor/surpass/.
3. Create the storage link and confirm the disk.
php artisan storage:link
Surpass writes uploads through Laravel's filesystem, so the public disk (or whatever the config names) must be writable and linked before you test anything.
4. Where to look first, in this order:
config/surpass.php — upload path, thumbnail dimensions, allowed extensions, disk name.routes.php / service provider boot() — this is where the Ajax endpoint(s) and their middleware are registered.5. First use case. A single "cover image" on an Article form:
<form method="POST" action="{{ route('articles.store') }}" enctype="multipart/form-data">
@csrf
@include('surpass::uploader', ['name' => 'cover', 'value' => old('cover', $article->cover ?? null)])
<button type="submit">Save</button>
</form>
Then on the server side, treat the submitted hidden field as a filename/path, move or copy it out of the temp upload directory into permanent storage, and save it on the model. The whole package is really "temp upload + thumbnail render + hidden input" — once that mental model clicks, the rest is configuration.
The two-phase upload. Surpass almost certainly works like this: the file input posts via XMLHttpRequest to a package route, the server stores it in a temp/upload directory and returns JSON containing the stored filename and thumbnail URL, the JS injects the <img> preview into the DOM and writes the filename into a hidden input. On final form submit, only that filename travels with the request. Design your controller around that: the upload is not the save.
Persisting to the model. Don't blindly trust the hidden value — verify the file exists in the temp directory, then move it:
public function store(Request $request)
{
$request->validate(['cover' => 'required|string']);
$temp = config('surpass.upload_dir') . '/' . basename($request->input('cover'));
abort_unless(File::exists($temp), 422);
$path = 'articles/' . Str::uuid() . '.' . File::extension($temp);
Storage::disk('public')->put($path, File::get($temp));
File::delete($temp);
Article::create(['cover' => $path] + $request->except('cover'));
}
Edit forms. Pass the existing image into the partial and have the JS render it as the initial thumbnail with the hidden input pre-populated, so an untouched form round-trips the current path unchanged. Make the controller tolerate "hidden value === existing value" without re-copying the file.
Multiple images. If the package supports a collection mode, use one hidden input per slot or a single comma/JSON-encoded hidden field, and give each uploader instance a unique DOM id. If it only supports single uploads, chain instances and assemble the array server-side. Keep the ordering explicit — a sort field or array index in the hidden value — because Ajax uploads complete out of order.
Replacing a controller or route. Because the routes are registered by the provider, you can shadow them: define your own route with the same URI before the provider boots (or disable the package route via config if it exposes a flag). This is the cleanest extension point when you need per-user directories, extra validation, or auth-scoped storage instead of one global temp folder.
Wrapping it in a Blade component. Rather than scattering @include('surpass::uploader', ...) everywhere, wrap it once so views stay readable and you have a single place to change when you fork:
<x-image-upload name="cover" :value="$article->cover ?? null" :width="320" />
Cleanup. Temp uploads accumulate. Add a scheduled command that deletes files in the temp directory older than a few hours — Ajax uploads that never get submitted will otherwise pile up indefinitely.
Archived means no patches. There will be no fix for a Laravel upgrade, a PHP 8.2 deprecation, or a Flysystem v3 break. Budget for a fork on day one; the package is small, so a fork is a maintenance win, not a burden.
CSRF will bite you first. The upload is a POST, so the JS must send X-CSRF-TOKEN (or _token). If you get a 419 on upload but the surrounding page works, that's the cause — check that your layout exposes the token in a <meta> tag the package's JS can read.
Middleware and auth. Verify the package's upload routes are behind web and your auth middleware. A publicly reachable unauthenticated upload endpoint in a live app is an abuse vector. Also cap file size and MIME type server-side, not just in the config's extension list — extension allowlists are trivially spoofed.
Size limits are in three places. upload_max_filesize, post_max_size in php.ini, and Laravel's own max: validation rule. A silent failure with no JSON response is almost always post_max_size — PHP drops the whole request and your JS sees an empty body. Check the browser's Network tab before touching code.
Thumbnails need GD or Imagick. Class 'Imagick' not found or blank thumbnails after a server move usually means the PHP extension is missing. Add ext-gd to your composer.json requirements so deploys fail loudly.
storage:link and disk names. Preview images 404 because the public URL points at public/storage while the file landed elsewhere. Confirm config/surpass.php's disk matches the one you actually have defined in config/filesystems.php, and remember php artisan config:cache after any config edit — a stale config cache is a classic "I changed the path and nothing happened."
Republishing overwrites your customizations. If you edited the published Blade or JS, vendor:publish --force after a package update erases it. Either keep your views in your own namespace and never republish, or tag your changes clearly.
Asset cache-busting. After republishing the JS/CSS, browsers hold the old version. Append ?v= to the asset URL or use mix()/@vite in your layout.
Debugging checklist, in order: browser Network tab (status, response body) → storage/logs/laravel.log → confirm the file physically exists in the temp directory → storage:link → config cache. Ninety percent of "Surpass doesn't work" reports resolve in the first two steps.
Sanitize before you trust it. Anything the client sends in the hidden field is user input. basename() it to strip path traversal, confirm it matches an expected filename pattern, and only ever read from the temp directory — never concatenate raw input into a storage path.
How can I help you explore Laravel packages today?