diff --git a/README.md b/README.md index 3a2ca13..5e78868 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ All the documentation can be found in the [docs](docs) folder. * [Customisation](docs/customisation.md) * [Shell tasks](docs/shell.md) * [Examples](docs/examples.md) +* [FAQ](docs/faq.md) ##Contribution Please open a pull request or submit an issue if there is anything you would like to contribute. Please write a test for diff --git a/docs/configuration.md b/docs/configuration.md index 78825af..b05e4a4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,7 +21,7 @@ $this->addBehavior('Proffer.Proffer', [ 'photo' => [ // The name of your upload field 'root' => WWW_DIR . 'files', // Customise the root upload folder here, or omit to use the default 'dir' => 'photo_dir', // The name of the field to store the folder - 'thumbnailSizes' => [ + 'thumbnailSizes' => [ // Declare your thumbnails 'square' => ['w' => 200, 'h' => 200], // Define the size and prefix of your thumbnails 'portrait' => ['w' => 100, 'h' => 300, 'crop' => true], // Crop will crop the image as well as resize it ], @@ -53,14 +53,24 @@ There are a number of configuration options you can pass into the behaviour when The database field which will store the name of the folder in which the files are uploaded. ###thumbnailSizes -**required** `array` +**optional** `array` An array of sizes to create thumbnails of an uploaded image. The format is that the image prefix will be the array key and the sizes are the value as an array. Eg, `'square' => ['w' => 200, 'h' => 200]` would create a thumbnail prefixed with `square_` and would be 100px x 100px. +If you do not specify the `thumbnailSizes` configuration option, no thumbnails will be created. ###root -**default:** `WWW_DIR . 'files'` +**optional:** defaults to, `WWW_DIR . 'files'` Allows you to customise the root folder in which all the file upload folders and files will be created. ###thumbnailMethod -**default:** `gd` -Which Imagine engine to use to convert the images. Defaults to PHP's GD library. Can also be `imagick` and `gmagick`. \ No newline at end of file +**optional:** defaults to, `gd` +Which Imagine engine to use to convert the images. Defaults to PHP's GD library. Can also be `imagick` and `gmagick`. + +## Configuring your templates +You will need to make sure that your forms are using the file type so that the files can be uploaded. + +```php +echo $this->Form->create($entity, ['type' => 'file']); +echo $this->Form->input('photo', ['type' => 'file']); +// etc +``` diff --git a/docs/customisation.md b/docs/customisation.md index a8df8f0..aa2f884 100644 --- a/docs/customisation.md +++ b/docs/customisation.md @@ -45,4 +45,6 @@ creating any thumbnails. ##Customising upload file names and paths Using the `Proffer.afterPath` event you can hook into all the details about the file upload before it is processed. Using this event you can change the name of the file and the upload path to match whatever convention you want. I have created -an example listener which is [available as an example](examples/UploadFilenameListener.md). \ No newline at end of file +an example listener which is [available as an example](examples/UploadFilenameListener.md). + +You would attach this listener in the same way as above, but there is no need to remove the existing listeners first. \ No newline at end of file diff --git a/docs/examples/UploadFilenameListener.md b/docs/examples/UploadFilenameListener.md index eed1c5d..f781122 100644 --- a/docs/examples/UploadFilenameListener.md +++ b/docs/examples/UploadFilenameListener.md @@ -56,6 +56,10 @@ class UploadFilenameListener implements EventListenerInterface // Create a new filename using the id and the name of the entity $newFilename = $event->subject()->get('id') . '_' . Inflector::slug($event->subject()->get('name')) . $ext; + // This would set the containing upload folder to `webroot/files/user_profile_pictures///` + // for every file uploaded through the table this listener was attached to. + $path->setTable('user_profile_pictures'); + // If a seed is set in the data already, we'll use that rather than make a new one each time we upload if (empty($event->subject()->get('image_dir'))) { $path->setSeed(date('Y-m-d-His')); diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..b8feb92 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,62 @@ +#Frequently asked questions +This manual page collects together all the frequent questions about the plugin, it's functionality and some of the more +common errors people might experience. + +## Proffers scope +The scope of the plugin is the limit of the functionality it will provide. + +First and foremost it is an upload plugin. This means it's core responsibility is to copy files from once place to +another. Which, in most cases, will be from a client machine to a server. + +Additional functionality to this is the generation of various sizes of thumbnail and some associated tools. In this +capacity there are some events which process images and create the thumbnails. There are also some related shell tasks +to make thumbnail generation easier. + +Some things which the plugin does not do are provide methods for linking images in the front-end of your website, such +as a helper. It's up to the developer to place the uploaded content in the front-end of the website. Nor will the plugin +interact with your admin to display uploaded images or anything like that. + +Proffer will also not manage your file system for you. It can only upload images, and doesn't version them or anything +similar. This kind of functionality would need to be developed by the developer. + +The provided thumbnail generation is basic. If you want to expand upon this, such as creating new types of thumbnail or +creating watermarked images you are encouraged to hook the events in the plugin and create your own code for generating +your customised thumbnails. + +## Errors +If you are experiencing any of these errors, here are your solutions. + +### Unknown type "proffer.file" +This has two primary causes. + +The first is that in your `config/boostrap.php` you might have forgotten to include the +`'bootstrap' => true` when loading the plugin, which means the datatype isn't loaded. + +```php +// config/bootstrap.php +Plugin::load('Proffer', ['bootstrap' => true]); +``` + +The second thing is that you might have forgotten to include the `_initializeSchema` method in your table class. This +method bind the data type class to the field. + +```php +// src/Model/Table/Examples.php +protected function _initializeSchema(\Cake\Database\Schema\Table $table) { + $table->columnType('file','proffer.file'); + return $table; +} +``` + +### File name is written to the database as "Array" +The thing to check is your form is using the file type, and your input is also a file type. + +```php +echo $this->Form->input($entity, ['type' => 'file']); +echo $this->Form->input('file_upload', ['type' => 'file']); +// etc +``` + +## Still having trouble? +If you're still having trouble, head to `#cakephp` on Freenode.net and ask for help. A web chat client is available +on [the Freenode website](http://webchat.freenode.net/). \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index fe94425..19b17ac 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,7 +10,11 @@ It's always advised to lock your dependencies to a specific version number. You or [read more about versioning on Composer.org](https://getcomposer.org/doc/01-basic-usage.md#package-versions). ## CakePHP -Then you'll need to load the plugin in your `config/bootstrap.php` file. `Plugin::load('Proffer', ['bootstrap' => true]);`. +Then you'll need to load the plugin in your `config/bootstrap.php` file. + +```php +Plugin::load('Proffer', ['bootstrap' => true]); +``` ## Database Next you need to add the fields to your table. You'll want to add your file upload field, this will store the name of the @@ -23,4 +27,4 @@ An example query to add columns might look like this for MySQL. ALTER TABLE `teams` ADD COLUMN `photo` VARCHAR(255), ADD COLUMN `photo_dir` VARCHAR(255) -``` \ No newline at end of file +``` diff --git a/src/Lib/ProfferPath.php b/src/Lib/ProfferPath.php index 73cc416..29cfc81 100644 --- a/src/Lib/ProfferPath.php +++ b/src/Lib/ProfferPath.php @@ -46,7 +46,11 @@ public function __construct(Table $table, Entity $entity, $field, $settings) $this->setTable($table->alias()); $this->setField($field); $this->setSeed($this->generateSeed($entity->get($settings['dir']))); - $this->setPrefixes($settings['thumbnailSizes']); + + if (isset($settings['thumbnailSizes'])) { + $this->setPrefixes($settings['thumbnailSizes']); + } + $this->setFilename($entity->get($field)); } @@ -88,7 +92,7 @@ public function getTable() * @param string $table The name of the table the behaviour is dealing with. * @return void */ - protected function setTable($table) + public function setTable($table) { $this->table = strtolower($table); } diff --git a/src/Model/Behavior/ProfferBehavior.php b/src/Model/Behavior/ProfferBehavior.php index b0199a3..3e57499 100644 --- a/src/Model/Behavior/ProfferBehavior.php +++ b/src/Model/Behavior/ProfferBehavior.php @@ -83,7 +83,7 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options, P $entity->set($settings['dir'], $path->getSeed()); // Don't generate thumbnails for non-images - if (getimagesize($path->fullPath()) !== false) { + if (getimagesize($path->fullPath()) !== false && isset($settings['thumbnailSizes'])) { $this->makeThumbs($field, $path); } } else { @@ -118,13 +118,19 @@ public function afterDelete(Event $event, Entity $entity, ArrayObject $options, foreach ($settings['thumbnailSizes'] as $prefix => $dimensions) { $filename = $path->fullPath($prefix); - unlink($filename); + if (file_exists($filename)) { + unlink($filename); + } } $filename = $path->fullPath(); - unlink($filename); + if (file_exists($filename)) { + unlink($filename); + } - rmdir($path->getFolder()); + if (file_exists($path->getFolder())) { + rmdir($path->getFolder()); + } } } diff --git a/tests/TestCase/Model/Behavior/ProfferBehaviorTest.php b/tests/TestCase/Model/Behavior/ProfferBehaviorTest.php index 6a25c85..281728a 100644 --- a/tests/TestCase/Model/Behavior/ProfferBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/ProfferBehaviorTest.php @@ -418,6 +418,43 @@ public function testAfterDelete() $this->assertFileNotExists($testUploadPath . 'portrait_image_640x480.jpg'); } + public function testAfterDeleteWithMissingFiles() + { + $table = $this->getMock('Cake\ORM\Table', ['alias']); + $table->method('alias') + ->willReturn('ProfferTest'); + + $Proffer = new ProfferBehavior($table, $this->config); + + $entity = new Entity([ + 'photo' => 'image_640x480.jpg', + 'photo_dir' => 'proffer_test' + ]); + + $path = $this->getProfferPathMock($table, $entity, 'photo'); + $testUploadPath = $path->getFolder(); + + if (!file_exists($testUploadPath)) { + mkdir($testUploadPath, 0777, true); + } + + copy( + Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', + $testUploadPath . 'image_640x480.jpg' + ); + + $Proffer->afterDelete( + $this->getMock('Cake\Event\Event', null, ['afterDelete']), + $entity, + new ArrayObject(), + $path + ); + + $this->assertFileNotExists($testUploadPath . 'image_640x480.jpg'); + $this->assertFileNotExists($testUploadPath . 'square_image_640x480.jpg'); + $this->assertFileNotExists($testUploadPath . 'portrait_image_640x480.jpg'); + } + public function testBeforePathEvent() { $entityData = [ @@ -528,4 +565,127 @@ function ($param) use ($entity, $path) { $path ); } + + public function testThumbsNotCreatedWhenNoSizes() + { + $table = $this->getMock('Cake\ORM\Table', ['alias']); + $table->method('alias') + ->willReturn('ProfferTest'); + + $config = $this->config; + unset($config['photo']['thumbnailSizes']); + + $entityData = [ + 'photo' => [ + 'name' => 'image_640x480.jpg', + 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', + 'size' => 33000, + 'error' => UPLOAD_ERR_OK + ], + 'photo_dir' => 'proffer_test' + ]; + $entity = new Entity($entityData); + $path = $this->getProfferPathMock($table, $entity, 'photo'); + + $Proffer = $this->getMockBuilder('Proffer\Model\Behavior\ProfferBehavior') + ->setConstructorArgs([$table, $config]) + ->setMethods(['moveUploadedFile']) + ->getMock(); + + $Proffer->expects($this->once()) + ->method('moveUploadedFile') + ->willReturnCallback(function ($source, $destination) { + if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { + mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); + } + return copy($source, $destination); + }); + + $Proffer->beforeSave( + $this->getMock('Cake\Event\Event', null, ['beforeSave']), + $entity, + new ArrayObject(), + $path + ); + + $this->assertEquals('image_640x480.jpg', $entity->get('photo')); + $this->assertEquals('proffer_test', $entity->get('photo_dir')); + + $testUploadPath = $path->getFolder(); + + $this->assertFileExists($testUploadPath . 'image_640x480.jpg'); + $this->assertFileNotExists($testUploadPath . 'portrait_image_640x480.jpg'); + $this->assertFileNotExists($testUploadPath . 'square_image_640x480.jpg'); + } + + public function testChangingThePathUsingEvents() + { + $table = $this->getMock('Cake\ORM\Table', ['alias']); + $table->method('alias') + ->willReturn('ProfferTest'); + + $listener = $this->getMockBuilder('Cake\Event\EventListenerInterface') + ->setMethods(['implementedEvents', 'filename']) + ->getMock(); + + $listener->expects($this->once()) + ->method('implementedEvents') + ->willReturn(['Proffer.afterPath' => 'filename']); + + $listener->expects($this->once()) + ->method('filename') + ->willReturnCallback(function ($event, $path) { + $path->setTable('proffer_path_event_test'); + $path->setSeed('proffer_event_test'); + $path->setFilename('event_image_640x480.jpg'); + + $event->subject()['photo']['name'] = 'event_image_640x480.jpg'; + + return $path; + }); + + $table->eventManager()->on($listener); + + $entityData = [ + 'photo' => [ + 'name' => 'image_640x480.jpg', + 'tmp_name' => Plugin::path('Proffer') . 'tests' . DS . 'Fixture' . DS . 'image_640x480.jpg', + 'size' => 33000, + 'error' => UPLOAD_ERR_OK + ], + 'photo_dir' => 'proffer_test' + ]; + $entity = new Entity($entityData); + $path = $this->getProfferPathMock($table, $entity, 'photo'); + + $Proffer = $this->getMockBuilder('Proffer\Model\Behavior\ProfferBehavior') + ->setConstructorArgs([$table, $this->config]) + ->setMethods(['moveUploadedFile']) + ->getMock(); + + $Proffer->expects($this->once()) + ->method('moveUploadedFile') + ->willReturnCallback(function ($source, $destination) { + if (!file_exists(pathinfo($destination, PATHINFO_DIRNAME))) { + mkdir(pathinfo($destination, PATHINFO_DIRNAME), 0777, true); + } + return copy($source, $destination); + }); + + $Proffer->beforeSave( + $this->getMock('Cake\Event\Event', null, ['beforeSave']), + $entity, + new ArrayObject(), + $path + ); + + $this->assertEquals('event_image_640x480.jpg', $entity->get('photo')); + $this->assertEquals('proffer_event_test', $entity->get('photo_dir')); + + $testUploadPath = $path->getFolder(); + + $this->assertFileExists($testUploadPath . 'event_image_640x480.jpg'); + $this->assertFileExists($testUploadPath . 'portrait_event_image_640x480.jpg'); + $this->assertFileExists($testUploadPath . 'square_event_image_640x480.jpg'); + } }