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.
That is it for now. Thanks for reading!
References
- Tutorial Source Codes –
node_modules
andvendor
directories are not included so please runnpm install
andcomposer install
. - Part 1 API Authentication
- Part 2 Client OAuth Login
- Part 3 API Get Photos and Videos
- Part 4 Client Get Photos and Videos
- Part 6 Client Form File Upload
- https://laravel.com/docs/6.x/filesystem#file-uploads