limoncello-php / app

Quick start JSON API application

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

File upload handling flow and sample code

dreamsbond opened this issue · comments

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

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 in \App\Routes\WebRoutes
    // Register the handler (these lines will be 41-42)
    $routes->post('/', [BoardsController::class, BoardsController::METHOD_CREATE]);
  • Add ControllerCreateInterface to implements list
  • Add implementation as below
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);
    }
}

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?

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 😉 ) 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]);
    }

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