Image Uploads, Laravel, Angular and Flow.js

Posted: 2015-09-18 16:59:54

Here is one combination out of many to make this happen.

The libraries are

https://github.com/flowjs/ng-flow

and

https://github.com/flowjs/flow-php-server

Model and Imageable Resource

What I like about this is we can make 1 controller to manage uploads. That controller will upload the file, place it into the correct folder, and setup the relationship to the resource.

In this example the Resource will be a Contact.

So I then follow Laravel docs to create the Polymorphic Imageable/Photo example http://laravel.com/docs/master/eloquent-relationships#polymorphic-relations

Ending up with an image model like this

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    public $timestamps = false;

    /**
     * Get all of the owning imageable models.
     */
    public function imageable()
    {
        return $this->morphTo();
    }
}

And a Contact.php file like this

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Rhumsaa\Uuid\Uuid;

class Contact extends Model
{

    protected $fillable = [
        'first_name',
        'last_name',
        'active'
    ];

    public function images()
    {
        return $this->morphMany(\App\Image::class, 'imageable');
    }

}

Nothing special really

Even the migration is right from the docs

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateTableImageable extends Migration
{
    public function up()
    {
        Schema::create('images', function (Blueprint $table) {
            $table->increments('id');
            $table->string('path');
            $table->integer('imageable_id');
            $table->string('imageable_type');
        });
    }

    public function down()
    {
        Schema::drop('images');
    }
}

Controller

Now for the Controller.

<?php

namespace App\Http\Controllers;

use App\Image;
use Flow\Config;
use Illuminate\Support\Facades\Input;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Response;

class ImageController extends Controller
{

    public $model_id;
    public $model_class_path;
    public $destination_path;
    public $filename;


    public $config;

    public function uploadFile(Request $request, $model_id = false)
    {

        try
        {

            $this->model_id         = $model_id;

            $model_class_path       = $this->getClassName($request);

            $path                   = $this->getImagePublicDestinationPath($request);

            $this->model_class_path = $model_class_path;

            $this->destination_path = $path;

            $this->config = new Config(array(
                'tempDir' => storage_path('chunks_temp_folder')
            ));

            $this->filename = Input::get('flowFilename');

            $this->saveImagable();

            $flowRequest = new \Flow\Request();

            if(\Flow\Basic::save(
                public_path($this->getDestinationPath()). '/' . $this->filename,
                $this->config,
                $flowRequest)) {

                return Response::json(['data' => $model_id, 'message' => "File Uploaded $this->filename"], 200);

            } else {

                return Response::json([], 204);

            }
        }
        catch(\Exception $e)
        {
            throw new \Exception(sprintf("Error saving image %s", $e->getMessage()));
        }
    }

    public function saveImagable()
    {
        $imageable = new Image();
        $imageable->path = $this->destination_path . '/' . $this->filename;
        $imageable->imageable_id = $this->model_id;
        $imageable->imageable_type = $this->model_class_path;
        $imageable->save();
    }

    public function getDestinationPath()
    {
        return $this->destination_path;
    }

    public function setDestinationPath($destination_path)
    {
        $this->destination_path = $destination_path;
    }

    private function getClassName($request)
    {
        return ($request->input('model_class_path')) ? $request->input('model_class_path') : 'App\Contact';
    }

    public function getImagePublicDestinationPath(Request $request)
    {
        return ($request->input('path')) ? $request->input('path') : 'images/contacts';
    }

}

Nothing special there just per the docs of the Flow php library https://github.com/flowjs/flow-php-server

And the route.php file

Route::get('images/upload/{model_id}', 'ImageController@uploadFile');
Route::post('images/upload/{model_id}', 'ImageController@uploadFile');

By the time this project is done though all of this is behind auth middleware. While being built it is behind htaccess.

One catch overall is the resource needs to exist before you can upload a file related to it so on Contact New type pages you may need to wait till the contact is created before you do the upload. There are ways around this but for now we are keeping it simple.

Angular

This is a simple example.

The route I made a simple example path

Route::get('/upload_example', function () {
    return view('upload');
});

That view extends the example layout view

default.blade.php

<!DOCTYPE html>
<html lang="en">
<head>

    <style>
        /* This helps the ng-show/ng-hide animations start at the right place. */
        /* Since Angular has this but needs to load, this gives us the class early. */
        .ng-hide { display: none!important; }
    </style>
    <title ng-bind="title">Ratsoc v2.0</title>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
    <base href="/">

    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">


</head>



<body flow-prevent-drop>
    <div>
        <div ng-include="'layout/shell.html'"></div>
        <div id="splash-page" ng-show="showSplash">
            <div class="page-splash">
                <div class="page-splash-message">
                    Ratsoc v2.0
                </div>
                <div class="progress progress-striped active page-progress-bar">
                    <div class="bar"></div>
                </div>
            </div>
        </div>
    </div>

@yield('content')

    <script src="/temp/jquery.js"></script>
    <script src="/temp/angular.js"></script>
    <script src="/temp/flow.js"></script>
    <script src="/temp/ng-flow.js"></script>
    <script src="/temp/app.upload.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>

</body>

</html>

Of course this would be setup better with Gulp but for now keeping it simple. The the view that is the content.

upload.blade.php

@extends('layouts.default')

@section('content')

    <div ng-app="app.upload" flow-init flow-prevent-drop>


        <div ng-controller="MainController as vm">

            <div class="col-lg-8 col-lg-offset-2">

                    <div class=""
                         flow-init="{
                                            target: '/images/upload/' + vm.contact.id, singleFile: true, testChunks: true,
                                            query: { '_token':  vm.token, 'model_class_path': 'App\\Contact', 'path': 'images/contacts'}
                                        }"

                         flow-files-submitted="vm.upload( $files, $event, $flow )"
                         flow-file-success="vm.setFileName($flow.files)">
                        <div class="alert alert-danger"
                             flow-drop flow-drag-enter="style={opacity: .5}"
                             flow-drag-leave="style={}" ng-style="style"
                             flow-drop-enabled=true>
                            <strong><i class="fa fa-arrow-right"></i> Upload image here by dragging here</strong>
                        </div>
                        <a class="btn btn-xs btn-default" ng-click="vm.removeFile($flow)"
                           ng-if="($flow.files.length > 0)" name="remove-file"> Remove File </a>
                    </div>


                <hr>
                <img ng-src="@{{ vm.image }}" ng-show="vm.image">


            </div>
        </div>


    </div>

@endsection

Not much of a looker here. This makes it super easy to work on things though before plugging it into your more complex applications.

Here I am adding a payload so when I use this on a page I can decide then is it at Contact, a Project, a Product etc. and setup the path eg 'images/projects' or Model name 'App\Contact' as needed.

Then for the app.js file to do all this, again you might break it up into more files just keeping this simple.

(function () {
    'use strict';

    angular.module('app.upload', [
        'flow'
    ]);

    function FlowConfig(flowFactoryProvider)
    {
        flowFactoryProvider.defaults = {
            speedSmoothingFactor: 0.2,
            maxChunkRetries: 10,
            simultaneousUploads: 10
        };
    }

    function MainController($http, $scope)
    {

        var vm = this;
        vm.contact = {};
        vm.contact.id = 'new';
        vm.token = false;
        vm.upload = upload;
        vm.setFileName = setFileName;
        vm.image_root  = '/images/contacts/';

        activate();

        ////
        function activate()
        {
            getToken();
        }

        function setFileName(flow_files)
        {
            vm.image = vm.image_root + flow_files[0].name;
        }

        function upload(files, event, flow)
        {
            angular.forEach(files, function(v,i) {
                files[i].flowObj.opts.query._token = vm.token;
            });


            flow.upload();
        }

        function getToken()
        {
             $http.get('/auth/token').then(
                 successGettingToken,
                 errorGettingToken
             );
        }

        function successGettingToken(response)
        {
            vm.token = response.data;
        }

        function errorGettingToken(response)
        {
            console.log("Error");
            console.log(response);
        }
    }

    angular.module('app.upload')
        .controller("MainController", MainController)
        .config(FlowConfig);

})();

So we use the flow upload event to run our upload method. This then adds the token to the POST request for the CSRF middleware.

Keep in mind my Angular is just a widget in blade. There is no separate session situation that you may have in SPA (Single Page Applications)

Thats it, drag file, file uploads and you have Flow, Angular and Laravel.