17 Feb 2020
cat: API, Laravel, PHP
0 Comments

Laravel API and Angular Client Tutorial – Part 5 API File Upload

Introduction

In this tutorial we will create an API route where we can upload a file with title. We will create service classes to reduce the codes in the controller and view-models to reduce the arguments. View-models are optional but is helpful with passing data around from controllers to other classes which I already covered here.

View Model

First we will create the base-model which I just copied from here.

app/ViewModels/ViewModel.php

namespace App\ViewModels;

abstract class ViewModel
{
    protected $mappings = [];

    /**
     * Map an array to the view-model's properties.
     *
     * @param $values
     * @return ViewModel
     */
    public function mapArray($values)
    {
        foreach ($this->mappings as $index => $mapping) {
            if (is_array($mapping)) {
                if (count($mapping) == 3 && $mapping[2] == 'array') {
                    foreach ($values[$mapping[1]] as $index2 => $value) {
                        /* @var $vm ViewModel */
                        $vm = new $mapping[0]();
                        $this->$index[] = $vm->mapArray($value);
                    }
                } else {
                    /* @var $vm ViewModel */
                    $vm = new $mapping[0]();
                    $this->$index = $vm->mapArray(@$values[$mapping[1]]);
                }
            } else {
                $this->$index = @$values[$mapping];
            }
        }
        return $this;
    }

    /**
     * Turn view model into associative array with mapping as keys.
     *
     * @return array
     */
    public function toArray()
    {
        $result = [];
        foreach ($this->mappings as $index => $mapping) {
            if (is_array($mapping)) {
                if (count($mapping) == 3 && $mapping[2] == 'array') {
                    foreach ($this->$index as $index2 => $value) {
                        /* @var ViewModel $value */
                        $result[$mapping[1]] = $value->toArray();
                    }
                } else {
                    $result[$mapping[1]] = $this->$index->toArray();
                }
            } else {
                $result[$mapping] = $this->$index;
            }
        }
        return $result;
    }

    function __get($name)
    {
        if (empty($this->$name)) {
            return null;
        }
        return $this->$name;
    }

    function __set($name, $value)
    {
        $this->$name = $value;
    }
}

Then we can create the view-model for the posts. I will create the file inside app/Services/Post directory since I like grouping files together by feature. You can also create it inside app/ViewModels like Laravel grouping files by components.

app/Services/Post/PostViewModel.php

namespace App\Services\Post;

use App\ViewModels\ViewModel;
use Illuminate\Http\UploadedFile;

/**
 * Class PostViewModel
 * @property string $title
 * @property UploadedFile $file
 * @package App\Services\Post
 */
class PostViewModel extends ViewModel
{
    protected $mappings = [
        'title' => 'title',
    ];
}

As you can see, the mappings is just simple but we do need a “file” field for submitting the photo or video file. The base view-model still do not process file from the payload but of course you can add more features to it you like to.

For now we can keep it simple and just assign the file manually in the controller later. Now we can work with the services classes.

Post Service

First we will need an interface. The interface is optional but I like to create them because they are easier to mock in projects where I have unit-tests.

namespace App\Services\Post;

use App\Post;

interface IPostService
{
    public function post(PostViewModel $viewModel, int $userId): Post;
}

Next we can create the implementation.

app/Services/Post/PostService.php

namespace App\Services\Post;

use App\Post;

class PostService implements IPostService
{
    public function post(PostViewModel $viewModel, int $userId): Post
    {
        $filename = uniqid() . '.' . $viewModel->file->getClientOriginalExtension();

        $post = new Post();
        $post->user_id = $userId;
        $post->title = $viewModel->title;
        $post->file = $viewModel->file->storeAs(date('Y'), $filename, ['disk' => 'posts']);
        $post->save();
        return $post;
    }
}

In the method we stored the file using the “file” attribute in the view-model assuming it is a UploadedFile object. We also used the “posts” disk so we have to add that configuration.

config/filesystems.php

'disks' => [

    // ...

    'posts' => [
        'driver' => 'local',
        'root' => public_path('posts'),
    ],

    // ...
],

Now we can register the class.

app/Providers/AppServiceProvider.php

public function register()
{
    // Repositories
    $this->app->bind(
        \App\Persistence\Repositories\IPostRepository::class,
        \App\Persistence\Eloquent\PostRepository::class
    );

    // Services
    $this->app->bind(
        \App\Services\Post\IPostService::class,
        \App\Services\Post\PostService::class
    );
}

Creating the Route

This is the new route added inside the secured group.

routes/api.php

/* @var \Dingo\Api\Routing\Router $api */
$api = app(\Dingo\Api\Routing\Router::class);

$api->group([
    'namespace' => 'App\Http\Controllers',
    'version' => 'v1',
    'middleware' => 'api.auth',
    'scopes' => ['read_user_data', 'write_user_data'],
], function (\Dingo\Api\Routing\Router $api) {

    $api->get('posts', 'PostController@index');
    $api->post('posts', 'PostController@store');

});

Then we can update update the controller and add the store action.

app/Http/Controllers/PostController.php

public function store(IPostService $service, Request $request, PostViewModel $viewModel)
{
    $viewModel->mapArray($request->all());
    $viewModel->file = $request->file('file');

    $post = $service->post($viewModel, $this->user->id);
    return $this->response->item($post, new PostTransformer(), ['key' => 'post']);
}

As you can see we assign the view-models file attribute manually. Then just pass the view-model as argument to PostService. The return value is of type Post so we can store it in a variable and pass it to a transformer.

Testing it out

To test the route, we will have to use form-data instead of JSON.

Laravel API and Angular Client Tutorial – Part 5 API File Upload

Laravel API and Angular Client Tutorial – Part 5 API File Upload

That is it for now. Thanks for reading!

References

Be the first to write a comment.

Post A Comment