File uploads
A file upload is a form field through which the visitor sends a picture or a document. Laminas receives the file into a temporary folder, checks it against your rules and moves it wherever you say. Let us add a picture to a post.
Preparation
Create the folder public/uploads — the files will land there. Add a column for the file name to the table:
ALTER TABLE posts ADD image VARCHAR(255) NULL;In the Post class add the property public ?string $image = null; and the line $this->image = $data['image'] ?? null; in exchangeArray(), and in PostTable::save() add the key 'image' => $post->image to the $data array.
The form field
Add an element in the PostForm constructor:
// module/Blog/src/Form/PostForm.php
$this->add([
'name' => 'image',
'type' => Element\File::class,
'options' => ['label' => 'Picture'],
]);In the add.phtml view script render it with <?= $this->formRow($form->get('image')) ?>. The enctype="multipart/form-data" attribute, without which the browser does not send the file, is added by the form itself when prepare() is called.
Rules for the file
In getInputFilterSpecification() a file has a special input type — FileInput:
// module/Blog/src/Form/PostForm.php
use Laminas\Filter\File\RenameUpload;
use Laminas\InputFilter\FileInput;
use Laminas\Validator\File\Extension;
use Laminas\Validator\File\Size;
'image' => [
'type' => FileInput::class,
'required' => false,
'validators' => [
['name' => Size::class, 'options' => ['max' => '2MB']],
['name' => Extension::class, 'options' => ['extension' => ['jpg', 'jpeg', 'png']]],
],
'filters' => [
[
'name' => RenameUpload::class,
'options' => [
'target' => './public/uploads/post',
'randomize' => true,
'use_upload_extension' => true,
],
],
],
],For a FileInput the order is reversed: validators first, then filters. The RenameUpload filter moves the file from the temporary folder to public/uploads. With randomize the name gets a random addition — for example post_64a1f3c2.jpg — so two files with the same name do not overwrite each other.
Handling in the controller
Files arrive separately from the other fields, so they are merged before setData():
// module/Blog/src/Controller/PostController.php
if ($request->isPost()) {
$form->setData(array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray(),
));
if ($form->isValid()) {
$data = $form->getData();
$post = new Post();
$post->exchangeArray($data);
$post->image = empty($data['image']['tmp_name']) ? null : basename($data['image']['tmp_name']);
$this->posts->save($post);
return $this->redirect()->toRoute('blog/view', ['id' => $post->id]);
}
}After validation $data['image'] is an array describing the file, and its tmp_name is already the new path in public/uploads. Only the file name is saved to the database.
Showing the picture
<!-- module/Blog/view/blog/post/view.phtml -->
<h1><?= $this->escapeHtml($this->post->title) ?></h1>
<?php if ($this->post->image !== null): ?>
<img src="<?= $this->basePath('uploads/' . $this->post->image) ?>" alt="">
<?php endif; ?>Add a post with a picture through /blog/add — it appears under the heading on the post page.
Common mistake
Never save a file under the name the visitor sent, and never without checking the extension: that is how a shell.php file gets onto the server, to be opened by a link and executed. Always restrict extensions with the Extension validator and give files your own names through RenameUpload with randomize.