stinger/ckeditor-uploadimage-bundle
A Symfony bundle that integrates CKEditor's uploadimage plugin, letting editors drag and drop or paste images directly into the editor. It handles upload requests server-side, saves files to a configurable directory, and returns the JSON response CKEditor expects, with simple configuration for uploa...
Read this first: it is not a Laravel package. stinger/ckeditor-uploadimage-bundle is a Symfony2 bundle (note the Bundle suffix and the DependencyInjection/…Extension + Resources/config/routing.yml layout). It will not composer require into a Laravel app and boot: it registers services against the Symfony container, ships a bundle class, and expects the Symfony kernel to load its routes. With 5 stars, a 2015-era last release, and zero dependents, you should treat it as reference source, not a dependency.
What you actually get from it is the server-side contract for CKEditor's Upload Image plugin: an upload endpoint that accepts a multipart image, stores it, and returns the JSON envelope CKEditor expects. Your job in Laravel is to reimplement that endpoint.
Where to look first:
Controller/ — find the upload action. This tells you the exact request field name (usually upload) and the JSON response keys.Resources/config/routing.yml — the route name and URL pattern the frontend points at.DependencyInjection/Configuration.php + the extension class — this is where upload directory, public URL, allowed extensions, and size limits are parameterised. These become your config/ckeditor.php.First use case: a single authenticated admin route that accepts an image from CKEditor and returns a URL, wired to uploadUrl in the editor config. That is the whole feature.
// routes/web.php
Route::post('/admin/ckeditor/upload', [CkeditorUploadController::class, 'store'])
->middleware(['auth', 'can:edit-content'])
->name('ckeditor.upload');
// resources/js/editor.js
ClassicEditor // or CKEDITOR.replace(...) for CKEditor 4
CKEDITOR.replace('body', {
extraPlugins: 'uploadimage',
uploadUrl: document.querySelector('meta[name="ckeditor-upload-url"]').content,
});
Controller replicating the bundle's upload action. The plugin posts a single file field named upload and expects either a success envelope or an error envelope. Keep the response shape byte-for-byte compatible with what the plugin version you load expects; the classic CKEditor 4 uploadimage shape is {uploaded: 1, fileName, url} and on failure {uploaded: 0, error: {message}}.
final class CkeditorUploadController extends Controller
{
public function store(Request $request): JsonResponse
{
$request->validate([
'upload' => ['required', 'file', 'image', 'mimes:jpg,jpeg,png,gif,webp', 'max:5120'],
]);
$file = $request->file('upload');
$path = $file->store('uploads/ckeditor/'.now()->format('Y/m'), 'public');
return response()->json([
'uploaded' => 1,
'fileName' => $file->getClientOriginalName(),
'url' => Storage::disk('public')->url($path),
]);
}
}
Route-model the bundle's parameters. The bundle exposes upload dir / public URL as container parameters. Mirror that as a config file so the editor URL and the storage disk stay in sync:
// config/ckeditor.php
return [
'disk' => env('CKEDITOR_DISK', 'public'),
'directory' => 'uploads/ckeditor',
'max_kb' => 5120,
];
CSRF + auth is the integration friction point. CKEditor's XHR upload does not automatically carry Laravel's X-CSRF-TOKEN. Either exclude the route from CSRF in VerifyCsrfToken::$except (acceptable only because it is behind auth), or inject the token into the editor config and set it as a request header in a fileUploadRequest listener. The Symfony bundle solved this with the session-based CSRF token — you get the same result with the header approach and it is tidier.
Multi-tenant / per-user isolation. Partition the storage path by user or tenant ID (uploads/ckeditor/{tenant}/{user}/…) exactly as you would in the Symfony bundle by injecting a directory resolver service. Do this from day one; retrofitting it means migrating files.
Do not build a file browser. Upload Image only handles drag/paste image insertion. The moment a stakeholder asks for "browse existing images," you have outgrown this pattern — reach for UniSharp/laravel-filemanager or a media library instead of bolting listing endpoints onto this controller.
If you truly must reuse the Symfony code, the realistic path is to extract only the controller logic and the response formatting, not the bundle. Composer will resolve it, but nothing in Laravel will call registerBundles().
ContainerAwareInterface, RequestStack from Symfony\Component\HttpFoundation). Laravel's container is PSR-11 compatible but has no bundle/extension concept, so the wiring is dead weight. Copy, don't depend.getRequest() patterns, and old annotation routes will not survive a modern codebase. Read it as pseudocode.uploadimage and CKEditor 5's UploadAdapter use different JSON contracts (CKEditor 5 expects {url} or {urls: {default}}). The single most common bug report for this class of integration is a perfectly successful upload that the editor silently discards because one key name is wrong. Verify with the network tab, not with the editor's UI.uploaded: 1. If you return a url that 404s (wrong disk, missing storage:link, wrong APP_URL), CKEditor inserts a broken image with no console error. Test the returned URL with curl before blaming the editor.image + mimes rules are not sufficient on their own — also cap dimensions/decompressed size to blunt image-bomb DoS, and never let the client-supplied filename influence the storage path. store() already generates a hashed name; keep it.storage:link is the gotcha that eats an afternoon. Files land in storage/app/public/... and are served from public/storage/.... If you point CKEditor at the filesystem path with asset(), it works locally and breaks on deploy.config/ckeditor.php so ops can tune limits without a deploy of controller code.How can I help you explore Laravel packages today?