The Upload Plugin is an attempt to easily handle file uploads with CakePHP. It uses the excellent Flysystem <http://flysystem.thephpleague.com/> library to handle file uploads, and can be easily integrated with any image library to handle thumbnail extraction to your exact specifications.
webroot/files
directory organised by the model name and upload field name.As this plugin is a Behavior, there are some field names you can not use because they are used by the internal CakePHP system. Please do not use these field names:
This is a list of all the available configuration options which can be passed in under each field in your behavior configuration.
pathProcessor
: Returns a ProcessorInterface class name.
Josegonzalez\Upload\File\Path\DefaultProcessor
writer
: Returns a WriterInterface class name.
Josegonzalez\Upload\File\Writer\DefaultWriter
transformer
: Returns a TransformerInterface class name. Can also be a PHP callable.
Josegonzalez\Upload\File\Transformer\DefaultTransformer
path
: A path relative to the filesystem.root
.
'webroot{DS}files{DS}{model}{DS}{field}{DS}'
DIRECTORY_SEPARATOR
{field-value:unique_id}
and the entity
being saved has a value of 4b3403665fea6
for the field
unique_id
, then {field-value:unique_id}
will be
replaced with 4b3403665fea6
. This replacement can be used
multiple times for one or more fields. If the value is not
a string or zero-length, a LogicException will be thrown.date('Y')
date('m')
date('d')
time()
microtime()
fields
: An array of fields to use when uploading files
fields.dir
: (default dir
) Field to use for storing the directoryfields.type
: (default type
) Field to use for storing the filetypefields.size
: (default size
) Field to use for storing the filesizefields.ext
: (default ext
) Field to use for storing the file extensionfilesystem
: An array of configuration info for configuring the writer
If using the DefaultWriter, the following options are available:
filesystem.root
: (default ROOT . DS
) Directory where files should be written to by defaultfilesystem.adapter
: (default Local Flysystem Adapter) A Flysystem-compatible adapter. Can also be a callable that returns an adapter.filesystem.visibility
: (default 'public'
) Sets the related file permissions. Should either be 'public'
or 'private'
.nameCallback
: A callable that can be used by the default pathProcessor to rename a file. Only handles original file naming.
NULL
Table $table
: The table of the current entityEntity $entity
: The entity you want to add/editarray $data
: The upload datastring $field
: The field for which data will be added/editedarray $settings
: UploadBehavior settings for the current fieldkeepFilesOnDelete
: Keep all files when deleting a record.
true
deleteCallback
: A callable that can be used to delete different versions of the file.
NULL
string $path
: Basepath of the file you want to deleteEntity $entity
: The entity you want to deletestring $field
: The field for which data will be removedarray $settings
: UploadBehavior settings for the current fieldrestoreValueOnFailure
: Restores original value of the current field when uploaded file has error
true
By default, no validation rules are loaded or attached to the table. You must explicitly load the validation provider(s) and attach each rule if needed.
This plugin allows you to only load the validation rules that cover your needs. At this point there are 3 validation providers:
UploadValidation
: validation rules useful for any uploadImageValidation
: validation rules specifically for imagesDefaultValidation
: loads all of the aboveSince by default, no validation rules are loaded, you should start with that:
<?php
$validator->setProvider('upload', \Josegonzalez\Upload\Validation\UploadValidation::class);
// OR
$validator->setProvider('upload', \Josegonzalez\Upload\Validation\ImageValidation::class);
// OR
$validator->setProvider('upload', \Josegonzalez\Upload\Validation\DefaultValidation::class);
?>
Afterwards, you can use its rules like:
<?php
$validator->add('file', 'customName', [
'rule' => 'nameOfTheRule',
'message' => 'yourErrorMessage',
'provider' => 'upload'
]);
?>
It might come in handy to only use a validation rule when there actually is an uploaded file:
<?php
$validator->add('file', 'customName', [
'rule' => 'nameOfTheRule',
'message' => 'yourErrorMessage',
'provider' => 'upload',
'on' => function($context) {
return !empty($context['data']['file']) && $context['data']['file']['error'] == UPLOAD_ERR_OK;
}
]);
?>
More information on conditional validation can be found here.
isUnderPhpSizeLimit
Check that the file does not exceed the max file size specified by PHP
<?php
$validator->add('file', 'fileUnderPhpSizeLimit', [
'rule' => 'isUnderPhpSizeLimit',
'message' => 'This file is too large',
'provider' => 'upload'
]);
?>
isUnderFormSizeLimit
Check that the file does not exceed the max file size specified in the HTML Form
<?php
$validator->add('file', 'fileUnderFormSizeLimit', [
'rule' => 'isUnderFormSizeLimit',
'message' => 'This file is too large',
'provider' => 'upload'
]);
?>
isCompletedUpload
Check that the file was completely uploaded
<?php
$validator->add('file', 'fileCompletedUpload', [
'rule' => 'isCompletedUpload',
'message' => 'This file could not be uploaded completely',
'provider' => 'upload'
]);
?>
isFileUpload
Check that a file was uploaded
<?php
$validator->add('file', 'fileFileUpload', [
'rule' => 'isFileUpload',
'message' => 'There was no file found to upload',
'provider' => 'upload'
]);
?>
isSuccessfulWrite
Check that the file was successfully written to the server
<?php
$validator->add('file', 'fileSuccessfulWrite', [
'rule' => 'isSuccessfulWrite',
'message' => 'This upload failed',
'provider' => 'upload'
]);
?>
isBelowMaxSize
Check that the file is below the maximum file upload size (checked in bytes)
<?php
$validator->add('file', 'fileBelowMaxSize', [
'rule' => ['isBelowMaxSize', 1024],
'message' => 'This file is too large',
'provider' => 'upload'
]);
?>
isAboveMinSize
Check that the file is above the minimum file upload size (checked in bytes)
<?php
$validator->add('file', 'fileAboveMinSize', [
'rule' => ['isAboveMinSize', 1024],
'message' => 'This file is too small',
'provider' => 'upload'
]);
?>
isAboveMinHeight
Check that the file is above the minimum height requirement (checked in pixels)
<?php
$validator->add('file', 'fileAboveMinHeight', [
'rule' => ['isAboveMinHeight', 200],
'message' => 'This image should at least be 200px high',
'provider' => 'upload'
]);
?>
isBelowMaxHeight
Check that the file is below the maximum height requirement (checked in pixels)
<?php
$validator->add('file', 'fileBelowMaxHeight', [
'rule' => ['isBelowMaxHeight', 200],
'message' => 'This image should not be higher than 200px',
'provider' => 'upload'
]);
?>
isAboveMinWidth
Check that the file is above the minimum width requirement (checked in pixels)
<?php
$validator->add('file', 'fileAboveMinWidth', [
'rule' => ['isAboveMinWidth', 200],
'message' => 'This image should at least be 200px wide',
'provider' => 'upload'
]);
?>
isBelowMaxWidth
Check that the file is below the maximum width requirement (checked in pixels)
<?php
$validator->add('file', 'fileBelowMaxWidth', [
'rule' => ['isBelowMaxWidth', 200],
'message' => 'This image should not be wider than 200px',
'provider' => 'upload'
]);
?>
Note: You may want to define the Upload behavior before the core Translate Behavior as they have been known to conflict with each other.
CREATE table users (
id int(10) unsigned NOT NULL auto_increment,
username varchar(20) NOT NULL,
photo varchar(255)
);
<?php
/*
In the present example, these changes would be made in:
src/Model/Table/UsersTable.php
*/
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Table;
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->setTable('users');
$this->setDisplayField('username');
$this->setPrimaryKey('id');
$this->addBehavior('Josegonzalez/Upload.Upload', [
// You can configure as many upload fields as possible,
// where the pattern is `field` => `config`
//
// Keep in mind that while this plugin does not have any limits in terms of
// number of files uploaded per request, you should keep this down in order
// to decrease the ability of your users to block other requests.
'photo' => [],
]);
}
}
?>
<?php
/*
In the present example, these changes would be made in:
templates/Users/add.php
templates/Users/edit.php
*/
?>
<?= $this->Form->create($user, ['type' => 'file']); ?>
<?= $this->Form->control('username'); ?>
<?= $this->Form->control('photo', ['type' => 'file']); ?>
<?= $this->Form->end(); ?>
Note: If you used *bake* to generate MVC structure after creating
the users table, you will need to remove the default scalar validation
for the photos field.
Using the setup from the previous example, uploaded files can only be deleted as long as the path is configured to use
static tokens. As soon as dynamic tokens are incorporated, like for example {day}
, the generated path will change
over time, and files cannot be deleted anymore at a later point.
In order to prevent such situations, a field must be added to store the directory of the file as follows:
CREATE table users (
`id` int(10) unsigned NOT NULL auto_increment,
`username` varchar(20) NOT NULL,
`photo` varchar(255) DEFAULT NULL,
`photo_dir` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
);
<?php
/*
In the present example, these changes would be made in:
src/Model/Table/UsersTable.php
*/
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Table;
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->setTable('users');
$this->setDisplayField('username');
$this->setPrimaryKey('id');
$this->addBehavior('Josegonzalez/Upload.Upload', [
'photo' => [
'fields' => [
// if these fields or their defaults exist
// the values will be set.
'dir' => 'photo_dir', // defaults to `dir`
'size' => 'photo_size', // defaults to `size`
'type' => 'photo_type', // defaults to `type`
],
],
]);
}
}
?>
<?php
/*
In the present example, these changes would be made in:
templates/Users/add.php
templates/Users/edit.php
*/
?>
<?= $this->Form->create($user, ['type' => 'file']); ?>
<?= $this->Form->control('username'); ?>
<?= $this->Form->control('photo', ['type' => 'file']); ?>
<?= $this->Form->end(); ?>
Using such a setup, the behavior will use the stored path value instead of generating the path dynamically when deleting files.
In this example we’ll cover: - custom database fields - a nameCallback which makes the filename lowercase only - a custom transformer where we generate a thumbnail of the uploaded image - delete the related files when the database record gets deleted - a deleteCallback to ensure the generated thumbnail gets removed together with the original
This example uses the Imagine library. It can be installed through composer:
composer require imagine/imagine
CREATE table users (
id int(10) unsigned NOT NULL auto_increment,
username varchar(20) NOT NULL,
photo varchar(255),
photo_dir varchar(255),
photo_size int(11),
photo_type varchar(255)
);
<?php
/*
In the present example, these changes would be made in:
src/Model/Table/UsersTable.php
*/
declare(strict_types=1);
namespace App\Model\Table;
use Cake\ORM\Table;
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->setTable('users');
$this->setDisplayField('username');
$this->setPrimaryKey('id');
$this->addBehavior('Josegonzalez/Upload.Upload', [
'photo' => [
'fields' => [
'dir' => 'photo_dir',
'size' => 'photo_size',
'type' => 'photo_type',
],
'nameCallback' => function ($table, $entity, $data, $field, $settings) {
return strtolower($data->getClientFilename());
},
'transformer' => function ($table, $entity, $data, $field, $settings, $filename) {
$extension = pathinfo($filename, PATHINFO_EXTENSION);
// Store the thumbnail in a temporary file
$tmp = tempnam(sys_get_temp_dir(), 'upload') . '.' . $extension;
// Use the Imagine library to DO THE THING
$size = new \Imagine\Image\Box(40, 40);
$mode = \Imagine\Image\ImageInterface::THUMBNAIL_INSET;
$imagine = new \Imagine\Gd\Imagine();
// Save that modified file to our temp file
$imagine->open($data->getStream()->getMetadata('uri'))
->thumbnail($size, $mode)
->save($tmp);
// Now return the original *and* the thumbnail
return [
$data->getStream()->getMetadata('uri') => $filename,
$tmp => 'thumbnail-' . $filename,
];
},
'deleteCallback' => function ($path, $entity, $field, $settings) {
// When deleting the entity, both the original and the thumbnail will be removed
// when keepFilesOnDelete is set to false
return [
$path . $entity->{$field},
$path . 'thumbnail-' . $entity->{$field},
];
},
'keepFilesOnDelete' => false,
]
]);
}
}
?>
<?php
/*
In the present example, these changes would be made in:
templates/Users/add.php
templates/Users/edit.php
*/
?>
<?= $this->Form->create($user, ['type' => 'file']); ?>
<?= $this->Form->control('username'); ?>
<?= $this->Form->control('photo', ['type' => 'file']); ?>
<?= $this->Form->end(); ?>
Once your files have been uploaded you can link to them using the HtmlHelper
by specifying the path and using the file information from the database.
This example uses the default behaviour configuration using the model Example
.
<?php
/*
In the present example, variations on these changes would be made in:
templates/Users/view.php
templates/Users/index.php
*/
// assuming an entity that has the following
// data that was set from your controller to your view
$entity = new Entity([
'photo' => 'imageFile.jpg',
'photo_dir' => '7',
]);
$this->set('entity', $entity);
// You could use the following to create a link to
// the image (with default settings in place of course)
echo $this->Html->link('../files/example/image/' . $entity->photo_dir . '/' . $entity->photo);
?>
For Windows systems you’ll have to build a workaround as Windows systems use backslashes as directory separator which isn’t useable in URLs.
<?php
/*
In the present example, variations on these changes would be made in:
templates/Users/view.php
templates/Users/index.php
*/
// assuming an entity that has the following
// data that was set from your controller to your view
$entity = new Entity([
'photo' => 'imageFile.jpg',
'photo_dir' => '7',
]);
$this->set('entity', $entity);
// You could use the following to create a link to
// the image (with default settings in place of course)
echo $this->Html->link('../files/example/image/' . str_replace('\', '/', $entity->photo_dir) . '/' . $entity->photo);
?>
You can optionally create a custom helper to handle url generation, or contain that within your entity. As it is impossible to detect what the actual url for a file should be, such functionality will never be made available via this plugin.
For advanced usage of the upload plugin, you will need to implement one or more of the followng interfaces.
Fully-namespaced class name: Josegonzalez\Upload\File\Path\ProcessorInterface
This interface is used to create a class that knows how to build paths for a given file upload. Other than the constructor, it contains two methods:
basepath
: Returns the basepath for the current field/data combinationfilename
: Returns the filename for the current field/data combinationRefer to Josegonzalez\Upload\File\Path\DefaultProcessor
for more details.
Fully-namespaced class name: Josegonzalez\Upload\File\Transformer\TransformerInterface
This interface is used to transform the uploaded file into one or more files that will be written somewhere to disk. This can be useful in cases where you may wish to use an external library to extract thumbnails or create PDF previews. The previous image manipulation functionality should be created at this layer.
Other than the constructor, it contains one method:
transform
: Returns an array of key/value pairs, where the key is a file on disk and the value is the name of the output file. This can be used for properly naming uploaded/created files.Refer to Josegonzalez\Upload\File\Transformer\DefaultTransformer
for more details. You may also wish to look at Josegonzalez\Upload\File\Transformer\SlugTransformer
as an alternative.
Fully-namespaced class name: Josegonzalez\Upload\File\Writer\WriterInterface
This interface is used to actually write files to disk. It writes files to disk using the Flysystem
library, and defaults to local storage by default. Implement this interface if you want to customize the file writing process.
Other than the constructor, it contains one methods:
write
: Writes a set of files to an output.Refer to Josegonzalez\Upload\File\Writer\DefaultWriter
for more details.