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

Uploadable Laravel Package

youshido/uploadable

View on GitHub
Deep Wiki
Context7

Getting Started

Verify the bundle actually installs before designing around it. This is a 1-star, 0-dependent package targeting Symfony2, so the first "step" is a 10-minute spike:

composer require youshido/uploadable --dev   # or clone the repo and path-repo it

If Composer refuses on modern PHP (likely — Symfony2-era constraints usually cap at php: >=5.5,<7.2), don't fight it. Clone the repo, read the whole thing (it is small), and decide whether to path-require it, vendor it into src/, or port the logic. Reading DependencyInjection/Configuration.php and the Doctrine event subscriber is the real "where to look first" — those two files define every config key and the exact lifecycle hooks. The README on a package this small is frequently stale relative to the code.

Registration is the standard Symfony2 dance:

// app/AppKernel.php
$bundles = [
    // ...
    new Youshido\UploadableBundle\YoushidoUploadableBundle(),
];

Then configure a storage location. The key names below are the shape — confirm against Configuration.php:

# app/config/config.yml
youshido_uploadable:
    storage:
        local:
            upload_path: "%kernel.root_dir%/../web/uploads"

Annotate an entity with a class-level marker and a property-level marker on a non-persisted file property (the bundle moves the file during Doctrine lifecycle events and writes the resulting filename into a separate, persisted column):

/**
 * @Uploadable\Uploadable
 */
class Product
{
    /** @ORM\Column(type="string", nullable=true) */
    private $imageName;

    /** @Uploadable\UploadableFile */
    private $imageFile;   // no @ORM\Column

    public function setImageFile(\Symfony\Component\HttpFoundation\File\UploadedFile $file) { ... }
    public function getImageFile() { ... }
    public function setImageName(string $name) { ... }
}

First use case: a form with an image/PDF field on an entity — handleRequest() → persist() → flush() should move the file to web/uploads and persist the generated name. Build exactly that one flow end-to-end before anything else; if it doesn't work, the package isn't worth further investment.

Implementation Patterns

The unmapped-form-field pattern. The file property must be excluded from form mapping, or Symfony will complain that the property doesn't exist on the entity:

$builder->add('imageFile', FileType::class, [
    'mapped'        => false,
    'required'      => false,
    'constraints'   => [new File(['maxSize' => '2M', 'mimeTypes' => ['image/jpeg', 'image/png']])],
]);

Uploads outside of forms. Because the work happens in a Doctrine prePersist/preUpdate listener, anything that flushes works — console commands, fixtures, API endpoints. Construct an UploadedFile from a temp path (e.g. new UploadedFile($path, $name, null, null, true) with the test flag) and set it on the entity before flush(). Do not try to call the bundle's services directly; the public API is the entity setter plus flush().

Wrap it in your own service. With 1 star and 0 dependents, treat this as unmaintained. Put a thin FileStorage service in your app that owns the store/replace/delete logic and delegates to the bundle. When you inevitably swap to vich/uploader-bundle, you change one class instead of every controller.

Reusable form type.

class ImageUploadType extends AbstractType
{
    public function buildForm(FormBuilderInterface $b, array $o): void
    {
        $b->add('imageFile', FileType::class, ['mapped' => false, 'required' => false]);
    }
}

Functional tests. Override upload_path in config_test.yml to point at %kernel.cache_dir%/uploads and assert on both sides:

$client->request('POST', '/products', ['product' => [...]], ['product' => ['imageFile' => $file]]);
self::assertFileExists($dir.'/'.$product->getImageName());

Use UploadedFile fixtures generated in setUp() (a 1x1 PNG byte string is enough) and clean the temp dir in tearDown() so tests don't leak files.

Bulk import. Loop, set the file, $em->persist() each entity, but flush in batches — the listener runs per-flush, so batch flushing keeps I/O and memory predictable.

Gotchas and Tips

  • preUpdate silently doesn't fire. Doctrine's preUpdate only runs when the entity's change set is non-empty. Since the file property is unmapped, changing only the file means no update event — the file is never moved. Mitigation: always touch a mapped field when replacing a file ($this->updatedAt = new \DateTime();).
  • Orphaned files. Small bundles like this typically handle persist/update but forget preRemove. Every delete or file replacement leaves a stale file on disk. Add your own Doctrine preRemove listener, or a nightly reconciler that diffs the directory against imageName values in the DB.
  • Never trust getClientOriginalName(). It's attacker-controlled. Generate the stored name yourself (UUID/hash + whitelisted extension) and validate MIME in the form constraint, not in the entity.
  • Store outside web/ for sensitive files. Public-served uploads should be non-executable (php_flag engine off / nginx location block). If documents are private, store in var/ and stream via a controller with an access check.
  • Path variables changed in Symfony 3.3. This bundle assumes %kernel.root_dir%; on Symfony 4+ that parameter is gone (use %kernel.project_dir%). If you're grafting it onto a modern kernel, expect to rewrite the config default.
  • Annotation cache. After editing @Uploadable annotations, app/console cache:clear --env=prod — otherwise the compiled annotation metadata is stale and your entity silently isn't registered.
  • Debugging "nothing happens": confirm the subscriber is wired with app/console container:debug --tag=doctrine.event_subscriber (S2) and check that the Uploadable class annotation is actually present on the entity — missing the class-level marker while having the property-level one is the most common misconfiguration.
  • No test suite = no safety net. Pin the exact commit in composer.json ("youshido/uploadable": "dev-master#<sha>") rather than a branch alias, or vendor the source into your repo so a force-push can't break your build.
  • Extension points are thin. The useful seam is the storage/name-generation layer; if it doesn't expose one you can configure, fork and add it — but at that point compare against vich/uploader-bundle, which solves all of the above (namers, preRemove cleanup, directory mapping per entity, Symfony 4–7 support) and is the pragmatic default unless you specifically need this package's exact annotation surface.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr