Frontend file upload into $has_many

I have a need for “dynamic” as in an unknown number of file uploads from the front end of the site.

I couldn’t get a “SS field” or “SS way” of doing it so I raw dogged it with cloning a file input with js. I get a bunch of inputs like this:

image

How can I save the files into the DataObject’s $has_many relationship and upload the file? The $has_many is also called $Attachment

I can’t get any debugging values out of doMyForm in the controller (it is saving other data from here). and the docs seem non existent.

Closer…

With this:



$_files = $_FILES;

$num = count($data['Attachment']['name']);

for ($i = 0; $i < $num; $i++)
{
	# Group by field
	$_FILES['Attachment'.$i]['name'] = $_files['Attachment']['name'][$i];
	$_FILES['Attachment'.$i]['type'] = $_files['Attachment']['type'][$i];
	$_FILES['Attachment'.$i]['size'] = $_files['Attachment']['size'][$i];
	$_FILES['Attachment'.$i]['error'] = $_files['Attachment']['error'][$i];
	$_FILES['Attachment'.$i]['tmp_name'] = $_files['Attachment']['tmp_name'][$i];
}

# Remove original and clone
unset($_FILES['Attachment'], $_FILES['Attachment0']);

foreach ($_FILES as $field_name => $attachment)
{
	$file = File::create();
	$upload = Upload::create();
	$upload->loadIntoFile($attachment, $file, '/attachments');

	$test = Attachment::create();
	$test->Title = $attachment['name'];
	$test->FileID = $upload->getFile()->ID;

	$myDataObject->Attachment()->add($test);
}

I am getting related $has_many objects created and the name, size and type of the file are there and in the correct folder.

Checking the server, the files do appear to be uploaded. If I delete the image off the server I get: image

The image isn’t doesn’t render however. Nothing in the form field and just a blue square in the asset viewer:
image

If I click the preview in the top right:

image

I do see the image.

What have I missed?!

Hi @LanceH, I have hit this challenge in the past before too. It sounds like you’re 99% there and just need to generate thumbnails. Add something like this in your foreach loop:

// generate image thumbnail to show in asset-admin
if (class_exists(AssetAdmin::class)) {
    AssetAdmin::singleton()->generateThumbnails($file);
}

There’s a github issue about this limitation somewhere I think but I couldn’t find it.

It looks like we came up with a similar solution - here is my hacky code in case it helps!

foreach ($data['files']['name'] as $key => $filename) {
    // ToDo: validate that this is an image
    $up = new Upload();
    // We need to reverse engineer the files array as if they were individual uploads for SilverStripe
    $tmpFile = [
        'name' => $data['files']['name'][$key],
        'type' => $data['files']['type'][$key],
        'tmp_name' => $data['files']['tmp_name'][$key],
        'error' => $data['files']['error'][$key],
        'size' => $data['files']['size'][$key],
    ];
    // ToDo: catch errors. See https://github.com/creative-commoners/silverstripe-userforms/blob/f3cec8a890f21a53d7212ca7cd0b8ab69bd21f1f/code/Control/UserDefinedFormController.php#L259-L267
    $up->loadIntoFile($tmpFile, null, $uploadPath);

    // generate image thumbnail to show in asset-admin
    if (class_exists(AssetAdmin::class)) {
        AssetAdmin::singleton()->generateThumbnails($file);
    }
}
1 Like

@JonoM ah perfect! Thanks that was it. I get thumbnails for documents now.

Images is still broken but it’s to do with the image optimiser I have in the project.

copy(): Filename cannot be empty and a tmp cache dir not existing. I can chase those down separately.

1 Like

This might come in handy too: silverstripe-helpers/RegenerateThumbnailsTask.php at master · jonom/silverstripe-helpers · GitHub

1 Like

@MaximeRainville This post is the genesis of what doesn’t appear to work for me. Turns out not to be due to the optimiser. Or at least the errors I was seeing have stopped and the images are optimised. There is just never a thumbnail generated for images. PDF’s do.


for ($i = 0; $i < $num; $i++)
{
    $_FILES['Attachment'.$i]['name'] = $_files['Attachment']['name'][$i];
    $_FILES['Attachment'.$i]['type'] = $_files['Attachment']['type'][$i];
    $_FILES['Attachment'.$i]['size'] = $_files['Attachment']['size'][$i];
    $_FILES['Attachment'.$i]['error'] = $_files['Attachment']['error'][$i];
    $_FILES['Attachment'.$i]['tmp_name'] = $_files['Attachment']['tmp_name'][$i];
}

foreach ($_FILES as $uploadedFile)
{
    $file = File::create();
    $upload = Upload::create();
    $upload->loadIntoFile($uploadedFile, $file, '/Submission attachments');

    $file->write();

    $attachment = Attachment::create();
    $attachment->Title = $uploadedFile['name'];
    $attachment->FileID = $upload->getFile()->ID;
    $submission->Attachment()->add($attachment);
    
    AssetAdmin::singleton()->generateThumbnails($file);
}

Everything works with the upload and has_many record being created - just no thumbnails for images in asset admin.
I mentioned it on that RFC as I saw a few existing issues about it and that was the latest.

If I change the last line to AssetAdmin::singleton()->generateThumbnails($uploadedFile); it does error saying it’s not receiving the right type (as I’d expect) the reverse being it doesn’t error when it’s $file so must be getting the right type.

@LanceH Sorry, I just go to this. I’m thinking the problem might be that you are creating a plain File irrespective of what type of file is uploaded. When the user upload an image you probably need to create an Image DataObject.

This is how AssetAdmin does it:

$fileClass = File::get_class_for_file_extension(File::get_file_extension($tmpFile['name']));
/** @var File $file */
$file = Injector::inst()->create($fileClass);

Maybe try replicating that approach in your own code.

@MaximeRainville no worries thanks for posting.

That’s got it closer! I do get a preview on the right when clicking the uploaded image now.

The thumbnail also isn’t a blue square anymore, now it’s white.

Looking at the markup the thumbnail is meant to be a base64 encoded string in a background-image style tag. Mine has no style attribute at all so not white, just not there…

Code now being

for ($i = 0; $i < $num; $i++)
{
    $_FILES['Attachment'.$i]['name'] = $_files['Attachment']['name'][$i];
    $_FILES['Attachment'.$i]['type'] = $_files['Attachment']['type'][$i];
    $_FILES['Attachment'.$i]['size'] = $_files['Attachment']['size'][$i];
    $_FILES['Attachment'.$i]['error'] = $_files['Attachment']['error'][$i];
    $_FILES['Attachment'.$i]['tmp_name'] = $_files['Attachment']['tmp_name'][$i];
}

foreach ($_FILES as $uploadedFile)
{
    $fileClass = File::get_class_for_file_extension(File::get_file_extension($uploadedFile['name']));
    $file = Injector::inst()->create($fileClass);
    
    $upload = Upload::create();
    $upload->loadIntoFile($uploadedFile, $file, '/Submission attachments');

    $file->write();

    $attachment = Attachment::create();
    $attachment->Title = $uploadedFile['name'];
    $attachment->FileID = $upload->getFile()->ID;
    $submission->Attachment()->add($attachment);
    
    AssetAdmin::singleton()->generateThumbnails($file);
}

I think I’ve cracked it.The problem is that when you create a DataObject in a Page Controller, it wants to create that object in the “Live” stage. However, AssetAdmin in the backend is only looking at the “Draft” stage. So you need to change your Versioned reading mode when creating the image. This example worked for me.

<?php

use SilverStripe\AssetAdmin\Controller\AssetAdmin;
use SilverStripe\Assets\Upload;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FileField;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Assets\File;
use SilverStripe\Versioned\Versioned;

class MultiUpload extends ContentController
{
    private static $allowed_actions = ['Form'];

    public function index()
    {
        return $this->renderWith(['Page', 'Page']);
    }


    public function Form() {
        $fields = FieldList::create(
            FileField::create('Attachment[]', 'Attachments')
                ->setAttribute('multiple', 'multiple')
        );
        $actions = FieldList::create(
            FormAction::create('boom', 'Boom')
        );


        return Form::create(
            $this,
            'Form',
            $fields,
            $actions
        );
    }

    public function boom($data, $form) {
        $attachments = $data['Attachment'];
        $num = sizeof($attachments['name']);
        $files = [];

        for ($i = 0; $i < $num; $i++)
        {
            $files[] = [
                'name' => $attachments['name'][$i],
                'type' => $attachments['type'][$i],
                'size' => $attachments['size'][$i],
                'error' => $attachments['error'][$i],
                'tmp_name' => $attachments['tmp_name'][$i],
            ];

        }

        foreach ($files as $uploadedFile)
        {
            Versioned::withVersionedMode(function () use ($uploadedFile) {
                Versioned::set_reading_mode('Stage.Stage');
                $fileClass = File::get_class_for_file_extension(File::get_file_extension($uploadedFile['name']));
                $file = Injector::inst()->create($fileClass);

                $upload = Upload::create();
                $upload->loadIntoFile($uploadedFile, $file, '/Submission attachments');

                $file->write();
                AssetAdmin::singleton()->generateThumbnails($file);
            });

        }
    }
}

Add this snipet to YML config to got this thing working.

SilverStripe\Control\Director:
  rules:
    'MultiUpload':
      Controller: '%$MultiUpload'