pazitron's avatar

File upload fails using Vue.js Laravel

I keep getting "Call to a member function store() on null" when I use Vue to post a file and other data.

Vue.js component:

<template>
    <section>
        <form @submit.prevent="createJob" enctype="multipart/form-data">

		<input v-model="title" type="text" placeholder="title">
		<input type='file' @change="onFileSelected"/>
	
		<button type="submit" > Post </button>
	</form>
    </section>
</template>

<script>
	export default {
       
        data() {
            return {
                title: '',
                companyLogo: ''
            }
        },
methods: {
onFileSelected(e) {
                let image = e.target.files[0];
                let reader = new FileReader();
                reader.readAsDataURL(image);
                reader.onload = e => {
                    this.companyLogo = e.target.result;
                }
            },

	createJob() {
                axios.post('/manage/jobs', {
                    headers: {
                        'Content-Type': 'multipart/form-data'
                    },
                    'title': this.title,
                    'company_logo': this.companyLogo
                })
                .then(response => {
                    console.log(response);
                })
                .catch(errors => {
                    console.log(errors.data);
                })
            }
}

}

My controller:

public function store(Request $request) { 

	$job = new Job;

        $job->title = request('title');
	$job->save();

	if(request()->has('company_logo')) {
            $job->update([
                'company_logo' => $request->file('company_logo')->store('images/company_logos', 'public')
            ]);
        }

        return 'Saved to DB';

}

I think it has something to do with the front end converting the image into base64 on upload and the backend (laravel) expecting the actual file, but not sure. My store() method returns null.

exception: "Error"
file: ".../app/Http/Controllers/ManageJobsController.php"
line: 91
message: "Call to a member function store() on null"


line 91 is 'company_logo' => $request->file('company_logo')->store('images/company_logos', 'public')
0 likes
14 replies
MarianoMoreyra's avatar

Hi @pazitron

I think you're right about base64 being the problem. I don't think Laravel will recognize it as a file, as you'll probably have to decode it first at your controller.

It seems the field is arriving thou, because it returns true to your if(request()->has('company_logo')) validation.

Anyway, take a look at the following article that explains how to upload a file to Laravel with Vue an Axios, and as a bonus, it compares the Base64 way, against this other way that he considers 'The right way':

https://dev.to/diogoko/file-upload-using-laravel-and-vue-js-the-right-way-1775

Hope this helps!

1 like
pazitron's avatar

I tried the two approaches from the article you posted - both resulted in the same error: "message: "Call to a member function store() on null". This is so frustrating.

Sinnbeck's avatar

Try checking if it is a file, not just random data (hasFile)

if(request()->hasFile('company_logo')) {
            $job->update([
                'company_logo' => $request->file('company_logo')->store('images/company_logos', 'public')
            ]);
        }
pazitron's avatar

It's just ignores the if statement if I use hasFile()

Sinnbeck's avatar

What if you simply pass the image instead?

                let image = e.target.files[0];
                let reader = new FileReader();
                reader.readAsDataURL(image);
                reader.onload = e => {
                    this.companyLogo = image;
                }

You can probably skip the whole reader part to be honest

this.companyLogo = e.target.files[0];;
MaverickChan's avatar

because in your vue component , you never send images to laravel.

you need to add

let formData = new formData ()
formData.append('company_logo',this.companyLogo)
formData.append('anything_you_need',this.anything)

after that , laravel request will have a file

@sinnbeck it is a dom object you pass , not the file itself.

pazitron's avatar

I combined yours and @sinnbeck latest replies and I can store the image successfully - thank you for that. Though I have a different issue now.

My other field's validation fails, I suspect due to the data sitting in formData object, this is my controller:

public function store(Request $request) {

        $validatedInput = $request->validate([
            'title' => 'required | max:255',
            'description' => 'required',
            'location' => 'required',
            'link_to_apply' => 'required',
            'email' => 'required | email',
            'company' => 'required',
            'category_id' => 'required'
        ]);

        $job = new Job;

        $job->title = request('title');
        $job->description = request('description');
        $job->location = request('location');
        $job->link_to_apply = request('link_to_apply');
        $job->type = request('type');
        $job->category_id = request('category_id');
        $job->access_token = Str::orderedUuid()->toString();
        $job->email = request('email');
        $job->company = request('company');
        $job->about_company = request('about_company');
        $job->salary = request('salary');

        $job->save();

        if(request()->hasFile('company_logo')) {
            $job->update([
                'company_logo' => $request->file('company_logo')->store('images/company_logos', 'public')
            ]);

        }
        return 'Saved to DB';
    }

I tried adding adding this:

$request['data'] = json_decode($request['data']);

    // Validate
    $request->validate([
        'data.title' => 'required',
        'data.description' => 'required'
    ]);

But it did not help, I still get errors: {description: ["The description field is required."], location: ["The location field is required."],…} message: "The given data was invalid."

ajithlal's avatar

@pazitron you are not getting your image as a file. As @marianomoreyra mentioned it is base64 encoded. So you have to extract the image so you can save it. Here is a small piece of code that will extract the image from a base64 file and save it.

 $image = str_replace('data:image/jpeg;base64,', '', $file); //$file is your companyLogo. I'm accepting only jpg,jpeg format images.
 $image = str_replace(' ', '+', $image);

$fileName = sprintf('%s.%s', uniqid(), '.jpeg');
\File::put( 'uploads/' . $fileName, base64_decode($image));

Note: updated the code

1 like
pazitron's avatar

@ajithlal yes, I am getting them as a file now. Though due to putting everything into the formData object, my validation for other fields like title, description, etc. fails. I am looking at a solution for it now

pazitron's avatar

I tried and got an error ""file_put_contents(uploads/5f2c093002d14..jpeg): failed to open stream: No such file or directory"

then I replaced File:: with Storage:: and it did work - the file was saved but it's zero kbytes

MaverickChan's avatar
Level 47

@pazitron store method should only handle the new save stuff , don't mix a update in $job , little confusing.

the FileReader part is only for generating previews , it has nothing to do with persisting to the database .

there is always an old way to save files in public directory and be alert to the permission setting.

$file = $request->file('image');

		$filename = $file->getClientOriginalName();//or you can give a name

		$path = 'somepath'; // the path in public directory you want to save file, String, full path,

		$file->move(public_path($path),$filename); // move the file from request to directory, public_path method is required!!

		if (!file_exists($path)) {

    		mkdir($path,0777,true);

    	}
pazitron's avatar

thank you all for your help. I've learned a lot about uploading images using vue and storing them in Laravel. After reading your helpful responses and searching on the web, the following worked for me:

Vue components:

FileUploder.vue

<template>
    <label
        class="w-48 flex flex-col items-center px-2 py-4 bg-indigo-100 text-gray-500 rounded tracking-wide border-1 border-indigo-200 cursor-pointer hover:bg-indigo-600 hover:text-gray-200">
          <svg class="w-5 h-5" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
              <path d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z" />
          </svg>
          <span class="mt-2 leading-tight text-xs">Add company logo</span>
          <input type='file' @change="onChange" class="hidden">
    </label>
</template>

<script>
    export default {
        methods: {
            onChange(e) {
                if (! e.target.files.length) return;
                let file = e.target.files[0];
                let reader = new FileReader();
                reader.readAsDataURL(file);
                reader.onload = e => {
                    let src = e.target.result;
                    this.$emit('loaded', { src, file });
                };
            }
        }
    }
</script>

JobCreate.vue

<template>
    <section>
        <form @submit.prevent="createJob" enctype="multipart/form-data">
            <div class="bg-white shadow overflow-hidden sm:rounded-lg">
              <div class="px-4 py-5 border-b border-indigo-100 sm:px-6">
                <h3 class="text-lg leading-6 font-medium text-gray-900">
                  New job form
                </h3>
                <p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
                  <sup>*</sup>Required fields
                </p>
              </div>
              <div>
                <dl>
                      <div class="bg-gray-50 px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Job title<sup>*</sup>
                        </dt>
                        <dd class="mt-1 sm:mt-0 sm:col-span-2">

                            <input
                                v-model="title"
                                type="text"
                                placeholder="Email Developer"
                                aria-label="Job title"
                                class="transition-colors duration-100 ease-in-out focus:outline-0 text-base border border-indigo-200 focus:bg-white focus:outline-none focus:border-indigo-300 text-gray-800 rounded-lg bg-white md:rounded-lg py-2 pr-4 pl-4 block w-full appearance-none leading-normal">
                        </dd>
                      </div>
                      <div class="bg-white px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Company<sup>*</sup>
                        </dt>
                        <dd class="mt-1 sm:mt-0 sm:col-span-2">

                            <input
                                v-model="company"
                                type="text"
                                placeholder="Acme Ltd."
                                aria-label="Company name"
                                class="transition-colors duration-100 ease-in-out focus:outline-0 text-base border border-indigo-200 focus:bg-white focus:outline-none focus:border-indigo-300 text-gray-800 rounded-lg bg-white md:rounded-lg py-2 pr-4 pl-4 block w-full appearance-none leading-normal">
                        </dd>
                      </div>
                      <div class="bg-white px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Company logo
                        </dt>
                        <dd class="mt-1 sm:mt-0 sm:col-span-2">

                            <file-uploader @loaded="onLoad"></file-uploader>
                        </dd>
                      </div>
                      <div class="bg-white px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Location<sup>*</sup>
                        </dt>
                        <dd class="mt-1 sm:mt-0 sm:col-span-2">

                            <input
                                v-model="location"
                                type="text"
                                placeholder="Solihull, UK"
                                aria-label="job location"
                                class="transition-colors duration-100 ease-in-out focus:outline-0 text-base border border-indigo-200 focus:bg-white focus:outline-none focus:border-indigo-300 text-gray-800 rounded-lg bg-white py-2 pr-4 pl-4 block w-full appearance-none leading-normal">
                        </dd>
                      </div>
                      <div class="bg-gray-50 px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Type<sup>*</sup>
                        </dt>
                        <dd class="mt-1 text-sm sm:mt-0 sm:col-span-2">
                            <select
                                v-model="type"
                                aria-label="Job category"
                                class="border text-base border-indigo-200 bg-transparent text-gray-500 leading-normal max-w-sm focus:bg-white focus:outline-none focus:border-indigo-300 rounded-lg bg-white py-2 pr-4 pl-4 block">
                                <option selected disabled value=''>Select type</option>
                                <option>Full-time</option>
                                <option>Part-time</option>
                                <option>Contract</option>
                                <option>Other</option>
                            </select>
                        </dd>
                      </div>
                      <div class="bg-gray-50 px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Category<sup>*</sup>
                        </dt>
                        <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">

                            <select
                                v-model="categoryId"
                                aria-label="Job category"
                                class="border border-indigo-200 bg-transparent text-gray-500 text-base leading-normal max-w-sm focus:bg-white focus:outline-none focus:border-indigo-300 bg-white rounded-lg py-2 pr-4 pl-4 block">
                                <option selected disabled value=''>Select category</option>
                                <option
                                    v-for="(category, id) in categories"
                                    :value="category.id">
                                {{category.name}}
                                </option>
                            </select>
                        </dd>
                      </div>
                      <div class="bg-white px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Salary (recommended)
                        </dt>
                        <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
                            <input
                                v-model="salary"
                                type="text"
                                placeholder="e.g. £50,000"
                                aria-label="Salary"
                                class="transition-colors duration-100 text-base ease-in-out focus:outline-0 border border-indigo-200 focus:bg-white focus:outline-none focus:border-indigo-300 text-gray-800 rounded-lg bg-white md:rounded-lg py-2 pr-4 pl-4 appearance-none leading-normal">
                        </dd>
                      </div>
                      <div class="bg-gray-50 px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Job description<sup>*</sup>
                        </dt>
                        <dd class="mt-1 sm:mt-0 sm:col-span-2">
                            <wysiwyg
                                v-model="jobDescription"
                                name="job description">
                            </wysiwyg>
                        </dd>
                    </div>
                    <div class="bg-white px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Link to apply<sup>*</sup>
                        </dt>
                        <dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">

                            <input
                                v-model="linkToApply"
                                type="text"
                                aria-label="Link to apply"
                                placeholder="https://www.acme.com/career/apply"
                                class="transition-colors duration-100 text-base ease-in-out focus:outline-0 border border-indigo-200 focus:bg-white focus:outline-none focus:border-indigo-300 text-gray-800 rounded-lg bg-white md:rounded-lg py-2 pr-4 pl-4 block w-full appearance-none leading-normal">
                        </dd>
                    </div>
                    <div class="bg-gray-50 px-4 py-5 items-center sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
                        <dt class="text-sm leading-5 font-medium text-gray-600">
                          Email<sup>*</sup>
                        </dt>
                        <dd class="mt-1 sm:mt-0 sm:col-span-2">

                            <input
                                v-model="email"
                                type="text"
                                placeholder="[email protected]"
                                aria-label="Email address"
                                class="transition-colors duration-100 ease-in-out focus:outline-0 text-base border border-indigo-200 focus:bg-white focus:outline-none focus:border-indigo-300 text-gray-800 rounded-lg bg-white md:rounded-lg py-2 pr-4 pl-4 block w-full appearance-none leading-normal">
                        </dd>
                    </div>
                </dl>
                </div>
                <div class="px-4 py-5 bg-gray-100 border-t border-indigo-100 sm:px-6 flex items-center justify-end">
                    <span class="block">
                        <a
                            href="/manage/jobs"
                            class="btn btn-text">
                            Cancel
                        </a>
                    </span>

                    <span class="ml-6">
                        <button
                            type="submit"
                            class="btn btn-primary btn-small">
                            Create job
                        </button>
                    </span>
                </div>
            </div>
        </form>
    </section>
</template>

<script>
    import Wysiwyg from '../../Wysiwyg';
    import FileUploader from '../../FileUploader';

    export default {
        components: {
            'wysiwyg': Wysiwyg,
            'file-uploader': FileUploader
        },
        data() {
            return {
                categories: [],
                title: '',
                company: '',
                companyLogo: '',
                uploadedFile: '',
                email: '',
                location: '',
                type: '',
                categoryId: '',
                salary: '',
                jobDescription: '',
                linkToApply: ''
            }
        },

        mounted() {
            this.fetchCategories();
        },

        methods: {
            fetchCategories() {
                axios.get('/categories')
                    .then(response => {
                      this.categories = response.data;
                    })
                    .catch(error => {
                        console.log(error);
                    })
            },

            onLoad(avatar) {
                console.log(avatar);
                this.companyLogo = avatar.src;
                this.uploadedFile = avatar.file;
            },

            createJob() {
                let data = new FormData();
                data.append('company_logo', this.uploadedFile);
                data.append('title', this.title);
                data.append('description', this.jobDescription);
                data.append('company', this.company);
                data.append('location', this.location);
                data.append('salary', this.salary);
                data.append('link_to_apply', this.linkToApply);
                data.append('type', this.type);
                data.append('category_id', this.categoryId);
                data.append('email', this.email);

                axios.post('/manage/jobs', data)
                .then(response => {
                    console.log(response);
                })
                .catch(errors => {
                    console.log(errors.data);
                })
            }
        }
    }
</script>

Controller:

public function store(Request $request) {


        $validatedInput = $request->validate([
            'title' => 'required | max:255',
            'description' => 'required',
            'location' => 'required',
            'link_to_apply' => 'required',
            'email' => 'required | email',
            'company' => 'required',
            'category_id' => 'required'
        ]);

        if ($request->has('company_logo')) {
            $job = new Job;

            $job->title = request('title');
            $job->description = request('description');
            $job->location = request('location');
            $job->link_to_apply = request('link_to_apply');
            $job->type = request('type');
            $job->category_id = request('category_id');
            $job->access_token = Str::orderedUuid()->toString();
            $job->email = request('email');
            $job->company = request('company');
            $job->about_company = request('about_company');
            $job->salary = request('salary');
            $job->company_logo = $request->file('company_logo')->store('images/company_logos', 'public');

            $job->save();
            return 'New job and image saved';

        } else {
            $job = new Job;

            $job->title = request('title');
            $job->description = request('description');
            $job->location = request('location');
            $job->link_to_apply = request('link_to_apply');
            $job->type = request('type');
            $job->category_id = request('category_id');
            $job->access_token = Str::orderedUuid()->toString();
            $job->email = request('email');
            $job->company = request('company');
            $job->about_company = request('about_company');
            $job->salary = request('salary');

            $job->save();

            return 'New job saved, no image';
        }

Hope this helps someone who is equally as new to the file upload using Vue/Laravel as I was!

Please or to participate in this conversation.