limoncello-php / app

Quick start JSON API application
MIT License
83 stars 7 forks source link

File upload handling flow and sample code #40

Closed dreamsbond closed 6 years ago

dreamsbond commented 6 years ago

I am still having difficult on handling file upload in limoncello-app during the general crud. can you provide sample code for it?

neomerx commented 6 years ago

This is really moslty about plain HTML and PSR7, but that's a solution below.

Create a folder for uploads server/storage/uploads.

Add HTML element for file upload, for example, in server/resources/views/pages/en/boards.html.twig

    <main class="content-boards">

        <!-- Add this form (and forget about HTML markup :)) -->
        <form method="post" enctype="multipart/form-data">
            Select file to upload:
            <input type="file" name="fileToUpload">
            <input type="submit" value="Upload File">
        </form>

        {% for board in boards %}

And add the corresponding method create to controller \App\Web\Controllers\BoardsController

    // Register the handler (these lines will be 41-42)
    $routes->post('/', [BoardsController::class, BoardsController::METHOD_CREATE]);
class BoardsController extends BaseController implements
    ControllerIndexInterface, ControllerReadInterface, ControllerCreateInterface
{
    ...

    /**
     * @inheritdoc
     */
    public static function create(
        array $routeParams,
        ContainerInterface $container,
        ServerRequestInterface $request
    ): ResponseInterface {
        if (empty($files = $request->getUploadedFiles()) === false) {
            foreach ($files as $file) {
                /** @var \Zend\Diactoros\UploadedFile $file */
                $originalFileName = $file->getClientFilename();
                $fileContent = $file->getStream();

                $originalFileName && $fileContent ?: null;

                // with original file name and file content you might want such things as
                // image resizing/conversion, then you're likely to save the original name
                // to database and got some file ID.
                $fileId = 123;

                // or just save the file in `uploads` folder
                // for simplicity I hardcode path to `server/storage/uploads` but
                // strongly recommend to have it as a config option in real app.
                $uploadFolder = implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '..', '..', 'storage', 'uploads']);
                $uploadPath = realpath($uploadFolder) . DIRECTORY_SEPARATOR . $fileId;

                $file->moveTo($uploadPath);
            }

            return new TextResponse('Upload OK.');
        }

        return new TextResponse('Nothing to upload.', 400);
    }
}
dreamsbond commented 6 years ago

thanks for your sample code and guideline if in case i am creating a record to the api along with the file uploading?

the same case to capture uploaded files by the ServerRequestInterface $request in api routes?

neomerx commented 6 years ago

if in case i am creating a record to the api along with the file uploading?

Yes, you're likely to use API to associate the file with the current user, keep original name and rename to guarantee file name to be unique in your file storage. However, unlike the sample above file name should not be predictable but long and random otherwise, it's easy to iterate through all of them.

the same case to capture uploaded files by the ServerRequestInterface $request in api routes?

Yes, API and Web routes both use PSR-7, so file uploads work identically. You just replace the default Controller handler like this


    /** @noinspection PhpMissingParentCallCommonInspection
     * @inheritdoc
     */
    public static function create(
        array $routeParams,
        ContainerInterface $container,
        ServerRequestInterface $request
    ): ResponseInterface {
        // check we have files attached
        /** @var UploadedFileInterface[] $files */
        $files      = $request->getUploadedFiles();
        $filesCount = count($files);
        if ($filesCount !== 1) {
            // We can handle only 1 file and it must be given.
            return new EmptyResponse(400);
        }
        $file = reset($files);
        ...
        return $response;
    }

And if you write test (you really should :wink: ) here is hint

        $this->setPreventCommits();
        $authHeaders = ...;

        $headers  = array_merge($authHeaders, [
            'CONTENT_TYPE' => 'multipart/form-data',
            'ACCEPT'       => 'application/vnd.api+json',
        ]);
        $filePath = __DIR__ . '/..................../1.jpg';
        $file     = new UploadedFile(
            $filePath,
            filesize($filePath),
            UPLOAD_ERR_OK,
            'my-photo.jpg'
        );
        $response = $this->postFile(self::API_URI, $file, $headers);
        $this->assertEquals(201, $response->getStatusCode());

where


    /**
     * @param string                $uri
     * @param UploadedFileInterface $file
     * @param array                 $headers
     *
     * @return ResponseInterface
     */
    protected function postFile($uri, UploadedFileInterface $file, array $headers = [])
    {
        return $this->call('POST', $uri, [], [], $headers, [], [$file]);
    }
dreamsbond commented 6 years ago

trying out the your guildeline handling file. seems on the right track. thanks!