Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Upload Crop Image Bundle Laravel Package

marrouchi/upload-crop-image-bundle

A PHP bundle for handling image uploads with built-in cropping. Add file upload and crop workflows to your app so users can select, preview, and crop images before saving, with configurable server-side processing and storage.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check: this is still a Symfony2 bundle (*Bundle suffix, AppKernel.php registration, Symfony DI service definitions), not a framework-agnostic library. You cannot composer require it into a Laravel app and get anything usable — there is no ServiceProvider, no facade, no config publishing, and its service definitions target the Symfony 2.x container API (%kernel.root_dir%, scope, synchronize). Composer will likely install it (the constraint is only on symfony/symfony if one is declared at all), but nothing will autoload or boot.

What 0.1.1 changed — and why it matters even to you: the bundle no longer ships an entity. Your application now supplies the persistence class via config.yml, and the bundle's JS reads route URLs from data attributes instead of hardcoded paths. That is the first release that treats the bundle as contract + glue rather than a full-stack mini-app, and it makes the repo materially better as a reference implementation — which is the only way a Laravel team should consume it. It is still a 0.1.x Symfony 2 bundle with no Laravel surface area.

So your "minimal steps" depend on which of two goals you have:

Goal A — you're maintaining a legacy Symfony 2.x app that already uses it.

  1. Read DependencyInjection/Configuration.php before the README. It is now load-bearing: the entity node added in 0.1.1 determines the config key you must set, and the README is unlikely to have caught up.
  2. Add the entity config to app/config/config.yml (key name per Configuration.php, typically under marrouchi_upload_crop_image:), pointing at a class in your bundle:
    marrouchi_upload_crop_image:
        entity_class: AppBundle\Entity\CroppedImage
    
  3. Create that entity yourself, matching the field/method contract the bundle expects (grep the bundle's controllers/services for ->get / ->set calls on the entity — that is the de-facto interface, there is no shipped interface to implements).
  4. Look at Resources/config/services.yml for the service ID to inject — that's the actual crop implementation.
  5. Look in Controller/ for the example endpoint accepting the upload + crop coordinates together, and note the data attributes it emits for the JS.
  6. Register in app/AppKernel.php:
    new Marrouchi\UploadCropImageBundle\MarrouchiUploadCropImageBundle(),
    
  7. Enable the bundle's route file in app/config/routing.yml if the bundle ships a demo controller.

Goal B — you're a Laravel dev looking for server-side cropping. Don't install it. Treat the repo as a reference implementation of the client→server crop contract and implement the same contract with intervention/image (or spatie/laravel-medialibrary's crop support). The first use case to copy is: the browser sends x, y, width, height in the original image's pixel space; the server validates the box against real dimensions and crops the stored original.

That's the entire value proposition here, and it's ~40 lines of code you can write yourself. The 0.1.1 "bring your own entity" change is a nice reminder to keep the same split: the package owns the crop contract, your app owns the storage model.

Implementation Patterns

1. The crop-box payload pattern (the part worth stealing). The frontend cropper (Cropper.js, Jcrop, or whatever the bundle's JS asset pairs with) submits a bounding box alongside the file:

[email protected]
crop[x]=120  crop[y]=80  crop[width]=640  crop[height]=480

Server side, the pattern is always: load original → assert box is inside getimagesize() bounds → crop → write derivative. In Laravel:

$box = $request->validate([
    'crop.x' => 'required|integer|min:0',
    'crop.y' => 'required|integer|min:0',
    'crop.width' => 'required|integer|min:1',
    'crop.height' => 'required|integer|min:1',
])['crop'];

$img = Image::make($path);
abort_if($box['x'] + $box['width'] > $img->width(), 422);
abort_if($box['y'] + $box['height'] > $img->height(), 422);

$img->crop($box['width'], $box['height'], $box['x'], $box['y'])
    ->save($derivativePath);

2. Routes as data attributes, not hardcoded JS URLs (new in 0.1.1, and genuinely good). Instead of $.post('/upload/crop', ...) buried in an asset, the server renders the endpoint onto the element:

<div class="cropper"
     data-upload-url="{{ route('media.store') }}"
     data-crop-url="{{ route('media.crop', $media) }}"
     data-csrf="{{ csrf_token() }}">

Your JS reads el.dataset.cropUrl. This survives route renames, subdirectory deploys, and locale prefixes for free, and it removes the classic "works in dev, 404s under /app.php/" bug. Steal this verbatim — it's the single most portable idea in the release. Keep the values Blade-escaped and don't stuff anything sensitive into them.

3. The package owns the contract; your app owns the entity. 0.1.1's removal of the shipped entity is the architectural lesson to internalize. In Laravel terms: the crop service should accept a path and a box and return a path — it should never new a model or assume a table name. Persist in your own controller/action:

$media = Media::create(['path' => $stored, 'width' => $w, 'height' => $h]);
$media->crops()->create($box + ['path' => $derivativePath]);

Anything a package wants to persist for you is a modelling decision it shouldn't be making.

4. Two-phase workflow: upload first, crop second. The bundle assumes the image is already persisted before the crop request arrives, because the cropper UI needs to render the image to let the user draw a box. Mirror that: POST /media stores the original and returns an id + dimensions; POST /media/{id}/crop applies the box to the stored original. This gives you re-cropping for free and makes the crop endpoint idempotent.

5. Never destroy the original. Always keep the untransformed upload. Store derivatives under a predictable path (media/{id}/crop-{w}x{h}-{x}-{y}.jpg) so re-rendering a different crop is a pure function, not a destructive edit. This is the single biggest architectural lesson from small bundles like this — several of them crop in place, which is a dead end.

6. Aspect-ratio enforcement belongs on the server. Don't trust a client-side aspectRatio lock. Re-clamp the box server-side (or snap it) before cropping, because users can post arbitrary integers regardless of what your JS enforced.

7. Integration tip for Symfony legacy: inject the bundle's crop service into your controller rather than extending the bundle's demo controller — the demo is usually coupled to a specific directory layout and hardcoded upload path. With the entity now externalized, you're already forced into the right shape; don't fight it by re-adding persistence concerns to the demo.

8. If you must reuse the code in Laravel: extract the crop service class into app/Support/, delete the DependencyInjection/ and Resources/config/ scaffolding (and the now-pointless entity config node), and bind it in a service provider. The class itself is almost certainly a thin GD/Imagick wrapper and portable; the bundle packaging is not.

Gotchas and Tips

0.1.1 is a breaking change shipped as a patch bump. Removing the bundle entity means anyone on 0.1.0 must, in one step: (a) add the new config node, (b) write their own entity, (c) migrate or re-map the old table (@ORM\Table name changes unless you keep it identical), and (d) fix every use Marrouchi\UploadCropImageBundle\Entity\... reference and any Doctrine relation pointing at it. There is no deprecation shim and no upgrade note beyond the two bullets. Because it's 0.x, semver permits this — pin an exact version ("0.1.1", not "^0.1") and never let a composer update cross a minor.

The new entity contract is undocumented — read the code. The bundle now calls methods on your class. Grep the bundle for ->set and ->get on the injected entity to derive the required property/method set, and check Configuration.php for whether the node is required or optional (if optional with a null default, the container will compile and then fatal at runtime on first use — worse than failing fast).

Data-attribute routes: watch escaping and CSP. Route URLs in data-* are fine under most CSPs, but (a) always render them server-side escaped, not echo-interpolated into a JS string, and (b) if you were relying on the bundle emitting its own <script> with a hardcoded URL, 0.1.1 removes that safety net — your markup now has to carry the attributes or the cropper silently posts to undefined. Add a smoke test asserting the attribute is present.

Coordinate space mismatch is the #1 bug. The browser's cropper reports coordinates relative to the displayed (CSS-scaled) image. If the rendered <img> is 600px wide but the natural file is 2400px, every crop is off by 4×. Always compute scale = naturalWidth / displayedWidth and multiply the box client-side, or send naturalWidth and rescale on the server. Add a test that crops a 4000px image displayed at 400px.

EXIF orientation will silently rotate your crops. Phone photos carry an orientation tag; getimagesize() reports the unrotated dimensions while the browser shows the rotated image. If you don't auto-orient before cropping, portrait iPhone shots get landscape crops. Intervention: ->orientate() before ->crop(). Raw GD: no help — use Imagick or exif_read_data().

Maintenance status: dormant, not dead — and still hostile territory.

  • 0.1.1 is a fresh tag, so the "abandoned since 2015" framing no longer holds. But: version 0.1.x, no CI config visible, no test suite advertised, effectively zero dependents. A single release after a decade is a pulse, not a maintenance commitment.
  • It remains a Symfony 2 bundle. Symfony 3+ breaks it (kernel.root_dir removal, Definition::setScope, synchronize deprecation, getContainerBuilder signature changes). composer update in any modern project will either fail resolution or install and fatal at container compile time.
  • It almost certainly still assumes PHP 5.x idioms. Don't put it on a PHP 8 runtime and expect silence.

Memory and image-bomb limits. Both GD and Imagick will happily try to allocate 1GB for a 9000×9000 PNG. Guard with a max pixel count before decoding (getimagesize() is cheap and doesn't inflate the bitmap), and set memory_limit / Imagick resource-limits policy. This bundle predates the concern entirely, so it has no protection.

Validate the file, not the extension. $file->getClientOriginalExtension() is attacker-controlled. Use getimagesize()/mime_content_type() to confirm the payload is really an image, and reject anything that reports false.

Integer rounding. Crop boxes from JS are often floats. (int) truncates; you can end up with width=0 on a tiny selection, which GD throws on or produces a 0-byte file from. Clamp to max(1, ...) and re-validate bounds after rounding.

Config quirks: DependencyInjection/Configuration.php is the only source of truth for real config keys — and as of 0.1.1 that's doubly true, since the entity node only exists there. READMEs on 0.x bundles drift from code. Grep for getParameter( and %...% placeholders in services.yml before assuming a config node's name or requiredness.

Extension point: the image-processing service is still the only clean one — swap its class via a compiler pass or service override. 0.1.1 added a second, indirect seam: because persistence is now yours, you can point entity_class at a thin adapter and keep your real model out of the bundle's world. Everything else in a bundle this small is hardcoded infrastructure.

Verdict for a Laravel team: don't add it as a dependency. Read Controller/ + Resources/config/services.yml + Configuration.php for 15 minutes to confirm the crop contract and borrow the data-attribute routing trick, then implement it with intervention/image where you control validation, orientation, memory limits, and storage layout. The pattern is worth 15 minutes; the package is still worth zero bytes in composer.json.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager